From d037172b701afd66d1ac187a3c7e4a76130a8f4f Mon Sep 17 00:00:00 2001 From: ivan-zhu Date: Thu, 8 Nov 2012 17:56:18 +0800 Subject: [PATCH 0001/1705] Add nova client support for nova-manage service command Implements the one workitem of blueprint apis-for-nova-manage This add three CLI (service-list/sevice-enable/service-diabel) in nova-client. So we can use: "nova service-list" like "nova-manage service list" with two optional parameters. Show a list of all running services. Filter by host and service name. "nova service-enable hostname servicename" like "nova-manage service enable hostname servicename". It will enable the service specified by hostname and serviename. "nova service-disable hostname servicename" like "nova-manage service diable hostname servicename". It will disable the service specified by hostname and serviename. This patch depends on https://review.openstack.org/#/c/15206/ Change-Id: I01d4cee4ef95c1783f6181f8b840244e748387e5 --- novaclient/v1_1/client.py | 2 ++ novaclient/v1_1/services.py | 62 +++++++++++++++++++++++++++++++++ novaclient/v1_1/shell.py | 27 +++++++++++++++ tests/v1_1/fakes.py | 30 ++++++++++++++++ tests/v1_1/test_services.py | 68 +++++++++++++++++++++++++++++++++++++ tests/v1_1/test_shell.py | 26 ++++++++++++++ 6 files changed, 215 insertions(+) create mode 100644 novaclient/v1_1/services.py create mode 100644 tests/v1_1/test_services.py diff --git a/novaclient/v1_1/client.py b/novaclient/v1_1/client.py index ff13780e0..2f315e9d2 100644 --- a/novaclient/v1_1/client.py +++ b/novaclient/v1_1/client.py @@ -23,6 +23,7 @@ from novaclient.v1_1 import volumes from novaclient.v1_1 import volume_snapshots from novaclient.v1_1 import volume_types +from novaclient.v1_1 import services class Client(object): @@ -83,6 +84,7 @@ def __init__(self, username, api_key, project_id, auth_url=None, self.aggregates = aggregates.AggregateManager(self) self.hosts = hosts.HostManager(self) self.hypervisors = hypervisors.HypervisorManager(self) + self.services = services.ServiceManager(self) # Add in any extensions... if extensions: diff --git a/novaclient/v1_1/services.py b/novaclient/v1_1/services.py new file mode 100644 index 000000000..8f54a6727 --- /dev/null +++ b/novaclient/v1_1/services.py @@ -0,0 +1,62 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 IBM +# All Rights Reserved. +# +# 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. + +""" +service interface +""" +from novaclient import base + + +class Service(base.Resource): + def __repr__(self): + return "" % self.service + + def _add_details(self, info): + dico = 'resource' in info and info['resource'] or info + for (k, v) in dico.items(): + setattr(self, k, v) + + +class ServiceManager(base.ManagerWithFind): + resource_class = Service + + def list(self, host=None, service=None): + """ + Describes cpu/memory/hdd info for host. + + :param host: destination host name. + """ + url = "/os-services" + if host: + url = "/os-services?host=%s" % host + if service: + url = "/os-services?service=%s" % service + if host and service: + url = "/os-services?host=%s&service=%s" % (host, service) + return self._list(url, "services") + + def enable(self, host, service): + """Enable the service specified by hostname and servicename""" + body = {"host": host, "service": service} + result = self._update("/os-services/enable", body) + return self.resource_class(self, result) + + def disable(self, host, service): + """Enable the service specified by hostname and servicename""" + body = {"host": host, "service": service} + result = self._update("/os-services/disable", body) + return self.resource_class(self, result) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index c296d61d3..8f9b175e7 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1942,6 +1942,33 @@ def do_reset_state(cs, args): _find_server(cs, args.server).reset_state(args.state) +@utils.arg('--host', metavar='', default=None, + help='Name of host.') +@utils.arg('--servicename', metavar='', default=None, + help='Name of service.') +def do_service_list(cs, args): + """Show a list of all running services. Filter by host & service name.""" + result = cs.services.list(args.host, args.servicename) + columns = ["Binary", "Host", "Zone", "Status", "State", "Updated_at"] + utils.print_list(result, columns) + + +@utils.arg('host', metavar='', help='Name of host.') +@utils.arg('service', metavar='', help='Name of service.') +def do_service_enable(cs, args): + """Enable the service""" + result = cs.services.enable(args.host, args.service) + utils.print_list([result], ['Host', 'Service', 'Disabled']) + + +@utils.arg('host', metavar='', help='Name of host.') +@utils.arg('service', metavar='', help='Name of service.') +def do_service_disable(cs, args): + """Enable the service""" + result = cs.services.disable(args.host, args.service) + utils.print_list([result], ['Host', 'Service', 'Disabled']) + + @utils.arg('host', metavar='', help='Name of host.') def do_host_describe(cs, args): """Describe a specific host""" diff --git a/tests/v1_1/fakes.py b/tests/v1_1/fakes.py index b4f125865..599f81a1c 100644 --- a/tests/v1_1/fakes.py +++ b/tests/v1_1/fakes.py @@ -13,6 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from datetime import datetime import httplib2 import urlparse @@ -868,6 +869,35 @@ def post_os_aggregates_2_action(self, body, **kw): def delete_os_aggregates_1(self, **kw): return (202, None) + # + # Services + # + def get_os_services(self, **kw): + host = kw.get('host', 'host1') + service = kw.get('service', 'nova-compute') + return (200, {'services': + [{'binary': service, + 'host': host, + 'zone': 'nova', + 'status': 'enabled', + 'state': 'up', + 'updated_at': datetime(2012, 10, 29, 13, 42, 2)}, + {'binary': service, + 'host': host, + 'zone': 'nova', + 'status': 'disabled', + 'state': 'down', + 'updated_at': datetime(2012, 9, 18, 8, 3, 38)}, + ]}) + + def put_os_services_enable(self, body, **kw): + return (200, {'host': body['host'], 'service': body['service'], + 'disabled': False}) + + def put_os_services_disable(self, body, **kw): + return (200, {'host': body['host'], 'service': body['service'], + 'disabled': True}) + # # Hosts # diff --git a/tests/v1_1/test_services.py b/tests/v1_1/test_services.py new file mode 100644 index 000000000..4f78c7048 --- /dev/null +++ b/tests/v1_1/test_services.py @@ -0,0 +1,68 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 IBM +# All Rights Reserved. +# +# 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 novaclient.v1_1 import services +from tests.v1_1 import fakes +from tests import utils + + +cs = fakes.FakeClient() + + +class ServicesTest(utils.TestCase): + + def test_list_services(self): + svs = cs.services.list() + cs.assert_called('GET', '/os-services') + [self.assertTrue(isinstance(s, services.Service)) for s in svs] + [self.assertEqual(s.binary, 'nova-compute') for s in svs] + [self.assertEqual(s.host, 'host1') for s in svs] + + def test_list_services_with_hostname(self): + svs = cs.services.list(host='host2') + cs.assert_called('GET', '/os-services?host=host2') + [self.assertTrue(isinstance(s, services.Service)) for s in svs] + [self.assertEqual(s.binary, 'nova-compute') for s in svs] + [self.assertEqual(s.host, 'host2') for s in svs] + + def test_list_services_with_service(self): + svs = cs.services.list(service='nova-cert') + cs.assert_called('GET', '/os-services?service=nova-cert') + [self.assertTrue(isinstance(s, services.Service)) for s in svs] + [self.assertEqual(s.binary, 'nova-cert') for s in svs] + [self.assertEqual(s.host, 'host1') for s in svs] + + def test_list_services_with_host_service(self): + svs = cs.services.list('host2', 'nova-cert') + cs.assert_called('GET', '/os-services?host=host2&service=nova-cert') + [self.assertTrue(isinstance(s, services.Service)) for s in svs] + [self.assertEqual(s.binary, 'nova-cert') for s in svs] + [self.assertEqual(s.host, 'host2') for s in svs] + + def test_services_enable(self): + service = cs.services.enable('host1', 'nova-cert') + values = {"host": "host1", 'service': 'nova-cert'} + cs.assert_called('PUT', '/os-services/enable', values) + self.assertTrue(isinstance(service, services.Service)) + self.assertFalse(service.disabled) + + def test_services_disable(self): + service = cs.services.disable('host1', 'nova-cert') + values = {"host": "host1", 'service': 'nova-cert'} + cs.assert_called('PUT', '/os-services/disable', values) + self.assertTrue(isinstance(service, services.Service)) + self.assertTrue(service.disabled) diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index 78a2da1a7..d03983c7e 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -454,6 +454,32 @@ def test_reset_state(self): self.assert_called('POST', '/servers/1234/action', {'os-resetState': {'state': 'active'}}) + def test_services_list(self): + self.run_command('service-list') + self.assert_called('GET', '/os-services') + + def test_services_list_with_host(self): + self.run_command('service-list --host host1') + self.assert_called('GET', '/os-services?host=host1') + + def test_services_list_with_servicename(self): + self.run_command('service-list --servicename nova-cert') + self.assert_called('GET', '/os-services?service=nova-cert') + + def test_services_list_with_host_servicename(self): + self.run_command('service-list --host host1 --servicename nova-cert') + self.assert_called('GET', '/os-services?host=host1&service=nova-cert') + + def test_services_enable(self): + self.run_command('service-enable host1 nova-cert') + body = {'host': 'host1', 'service': 'nova-cert'} + self.assert_called('PUT', '/os-services/enable', body) + + def test_services_disable(self): + self.run_command('service-disable host1 nova-cert') + body = {'host': 'host1', 'service': 'nova-cert'} + self.assert_called('PUT', '/os-services/disable', body) + def test_host_list(self): self.run_command('host-list') self.assert_called('GET', '/os-hosts') From aa5622147faa0de137f67c6be45dbdb3e11320f6 Mon Sep 17 00:00:00 2001 From: melwitt Date: Wed, 7 Nov 2012 22:58:32 +0000 Subject: [PATCH 0002/1705] discover extensions via entry points Currently, nova client can only discover extensions in two ways: 1. Installing the extension in the novaclient/v1_1/contrib/ directory. 2. Installing the extension in the top-level python path or modifying the path to be picked up by pkgutils.iter_modules() This patch allows a third, more flexible option of discovering extensions via entry points. This means the extension can be installed anywhere and entry points can be registered with python to be picked up by pkg_resources.iter_entry_points(). To register an entry point, simply add the extension module to the setup() call in setup.py like this: setuptools.setup( name='mydistribution', packages=setuptools.find_packages(), entry_points={ 'novaclient.extension' : [ 'foo = mydistribution.mynovaclientexts.foo' ] }, ) Change-Id: Ic1e223a9173546131e742506897f585f4ac65767 --- novaclient/shell.py | 11 +++++- tests/test_discover.py | 79 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 tests/test_discover.py diff --git a/novaclient/shell.py b/novaclient/shell.py index dd2a5c7de..c15893f0e 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -24,6 +24,7 @@ import imp import itertools import os +import pkg_resources import pkgutil import sys import logging @@ -257,7 +258,8 @@ def _discover_extensions(self, version): extensions = [] for name, module in itertools.chain( self._discover_via_python_path(), - self._discover_via_contrib_path(version)): + self._discover_via_contrib_path(version), + self._discover_via_entry_points()): extension = novaclient.extension.Extension(name, module) extensions.append(extension) @@ -289,6 +291,13 @@ def _discover_via_contrib_path(self, version): module = imp.load_source(name, ext_path) yield name, module + def _discover_via_entry_points(self): + for ep in pkg_resources.iter_entry_points('novaclient.extension'): + name = ep.name + module = ep.load() + + yield name, module + def _add_bash_completion_subparser(self, subparsers): subparser = subparsers.add_parser('bash_completion', add_help=False, diff --git a/tests/test_discover.py b/tests/test_discover.py new file mode 100644 index 000000000..dbbddc4e3 --- /dev/null +++ b/tests/test_discover.py @@ -0,0 +1,79 @@ +# Copyright 2012 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import mock +import imp +import inspect +import pkg_resources + +import novaclient.shell +from tests import utils + + +class DiscoverTest(utils.TestCase): + + def test_discover_via_entry_points(self): + + def mock_iter_entry_points(group): + if group == 'novaclient.extension': + fake_ep = mock.Mock() + fake_ep.name = 'foo' + fake_ep.module = imp.new_module('foo') + fake_ep.load.return_value = fake_ep.module + return [fake_ep] + + @mock.patch.object(pkg_resources, 'iter_entry_points', + mock_iter_entry_points) + def test(): + shell = novaclient.shell.OpenStackComputeShell() + for name, module in shell._discover_via_entry_points(): + self.assertEqual(name, 'foo') + self.assertTrue(inspect.ismodule(module)) + + test() + + def test_discover_extensions(self): + + def mock_discover_via_python_path(self): + yield 'foo', imp.new_module('foo') + + def mock_discover_via_contrib_path(self, version): + yield 'bar', imp.new_module('bar') + + def mock_discover_via_entry_points(self): + yield 'baz', imp.new_module('baz') + + @mock.patch.object(novaclient.shell.OpenStackComputeShell, + '_discover_via_python_path', + mock_discover_via_python_path) + @mock.patch.object(novaclient.shell.OpenStackComputeShell, + '_discover_via_contrib_path', + mock_discover_via_contrib_path) + @mock.patch.object(novaclient.shell.OpenStackComputeShell, + '_discover_via_entry_points', + mock_discover_via_entry_points) + def test(): + shell = novaclient.shell.OpenStackComputeShell() + extensions = shell._discover_extensions('1.1') + self.assertEqual(len(extensions), 3) + names = sorted(['foo', 'bar', 'baz']) + sorted_extensions = sorted(extensions, key=lambda ext: ext.name) + for i in range(len(names)): + ext = sorted_extensions[i] + name = names[i] + self.assertEqual(ext.name, name) + self.assertTrue(inspect.ismodule(ext.module)) + + test() From 5d5df1763d83296464a3c4a9c5c82e9a5057edee Mon Sep 17 00:00:00 2001 From: Christian Berendt Date: Thu, 8 Nov 2012 17:06:24 +0100 Subject: [PATCH 0003/1705] fix hypervisor-servers for hypervisors without servers At the moment hypervisor-servers throws an AttributeError for hypervisors with no assigned instances. This patch checks first if there are assigned instances before looping through them to avoid the exception. fixes bug 1076435 Change-Id: I504e3f234fd041325b63295fab77f9ed3f704db0 --- novaclient/v1_1/shell.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index c296d61d3..95631a519 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2020,7 +2020,8 @@ def __init__(self, **kwargs): for hyper in hypers: hyper_host = hyper.hypervisor_hostname hyper_id = hyper.id - instances.extend([InstanceOnHyper(id=serv['uuid'], + if hasattr(hyper, 'servers'): + instances.extend([InstanceOnHyper(id=serv['uuid'], name=serv['name'], hypervisor_hostname=hyper_host, hypervisor_id=hyper_id) From c819c2fb50c63554d70f735a6463c911f2f6d8eb Mon Sep 17 00:00:00 2001 From: Christian Berendt Date: Fri, 9 Nov 2012 10:52:23 +0100 Subject: [PATCH 0004/1705] make tenant id optional for quota-defaults and quota-show When using quota-defaults or quota-show without specifing a tenant the currently used tenant should be used. Change-Id: I1ef71b68673dd4a95cbf8b5a8dc901fb6eb06865 --- novaclient/v1_1/client.py | 1 + novaclient/v1_1/shell.py | 20 ++++++++++++++------ tests/v1_1/test_shell.py | 4 ++-- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/novaclient/v1_1/client.py b/novaclient/v1_1/client.py index ff13780e0..f47bc7ae5 100644 --- a/novaclient/v1_1/client.py +++ b/novaclient/v1_1/client.py @@ -54,6 +54,7 @@ def __init__(self, username, api_key, project_id, auth_url=None, # FIXME(comstud): Rename the api_key argument above when we # know it's not being used as keyword argument password = api_key + self.project_id = project_id self.flavors = flavors.FlavorManager(self) self.flavor_access = flavor_access.FlavorAccessManager(self) self.images = images.ImageManager(self) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index c296d61d3..bb5126333 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2165,22 +2165,30 @@ def _quota_update(manager, identifier, args): manager.update(identifier, **updates) -@utils.arg('tenant', +@utils.arg('--tenant', metavar='', - help='UUID of tenant to list the quotas for.') + default=None, + help='UUID or name of tenant to list the quotas for.') def do_quota_show(cs, args): """List the quotas for a tenant.""" - _quota_show(cs.quotas.get(args.tenant)) + if not args.tenant: + _quota_show(cs.quotas.get(cs.project_id)) + else: + _quota_show(cs.quotas.get(args.tenant)) -@utils.arg('tenant', +@utils.arg('--tenant', metavar='', - help='UUID of tenant to list the default quotas for.') + default=None, + help='UUID or name of tenant to list the default quotas for.') def do_quota_defaults(cs, args): """List the default quotas for a tenant.""" - _quota_show(cs.quotas.defaults(args.tenant)) + if not args.tenant: + _quota_show(cs.quotas.defaults(cs.project_id)) + else: + _quota_show(cs.quotas.defaults(args.tenant)) @utils.arg('tenant', diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index 78a2da1a7..b82042edb 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -515,11 +515,11 @@ def test_hypervisor_stats(self): self.assert_called('GET', '/os-hypervisors/statistics') def test_quota_show(self): - self.run_command('quota-show test') + self.run_command('quota-show --tenant test') self.assert_called('GET', '/os-quota-sets/test') def test_quota_defaults(self): - self.run_command('quota-defaults test') + self.run_command('quota-defaults --tenant test') self.assert_called('GET', '/os-quota-sets/test/defaults') def test_quota_update(self): From aac4fff1532cc96302cd1566406c6d3b3fe60e47 Mon Sep 17 00:00:00 2001 From: Stanislaw Pitucha Date: Fri, 9 Nov 2012 10:49:02 +0000 Subject: [PATCH 0005/1705] Make sure create_image returns result manager.servers.create_image() returned uuid of the snapshot, but Server.create_image() didn't. Make them work the same. Change-Id: I763197ac8ae542e7ce13569d8ce7e98ec92ccc63 --- novaclient/v1_1/servers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index 7b394e9f7..bdc509cfe 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -215,7 +215,7 @@ def create_image(self, image_name, metadata=None): :param image_name: The name to assign the newly create image. :param metadata: Metadata to assign to the image. """ - self.manager.create_image(self, image_name, metadata) + return self.manager.create_image(self, image_name, metadata) def backup(self, backup_name, backup_type, rotation): """ From aa8d44c551643bb97e039624d602bd4ad07cbe9c Mon Sep 17 00:00:00 2001 From: Paul Voccio Date: Fri, 16 Nov 2012 11:34:12 -0600 Subject: [PATCH 0006/1705] Adding support to filter instances by tenant from the admin api Change-Id: I37a2c5ad7bbe3e005e96416ea974051a82879adc --- novaclient/v1_1/shell.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index c296d61d3..bcab119f5 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -670,6 +670,12 @@ def do_image_delete(cs, args): type=int, const=1, help=argparse.SUPPRESS) +@utils.arg('--tenant', + #nova db searches by project_id + dest='tenant', + metavar='', + nargs='?', + help='Display information from single tenant (Admin only).') def do_list(cs, args): """List active servers.""" search_opts = { @@ -681,6 +687,7 @@ def do_list(cs, args): 'image': args.image, 'flavor': args.flavor, 'status': args.status, + 'project_id': args.tenant, 'host': args.host, 'instance_name': args.instance_name} From e9b015c7e14843784debc0d68cfe070c9778f80d Mon Sep 17 00:00:00 2001 From: Gabriel Hurley Date: Sat, 17 Nov 2012 18:18:48 -0800 Subject: [PATCH 0007/1705] Cleans up the flavor creation code. Fixes bug 1080891. Change-Id: Idc76cd01d1537ab87723a05ab8dd81015284e3c8 --- novaclient/v1_1/flavors.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/novaclient/v1_1/flavors.py b/novaclient/v1_1/flavors.py index 48e826557..fa9a0de47 100644 --- a/novaclient/v1_1/flavors.py +++ b/novaclient/v1_1/flavors.py @@ -104,7 +104,7 @@ def delete(self, flavor): """ self._delete("/flavors/%s" % base.getid(flavor)) - def create(self, name, ram, vcpus, disk, flavorid, + def create(self, name, ram, vcpus, disk, flavorid=None, ephemeral=0, swap=0, rxtx_factor=1, is_public=True): """ Create (allocate) a floating ip for a tenant @@ -113,7 +113,9 @@ def create(self, name, ram, vcpus, disk, flavorid, :param ram: Memory in MB for the flavor :param vcpu: Number of VCPUs for the flavor :param disk: Size of local disk in GB - :param flavorid: Integer ID for the flavor + :param flavorid: ID for the flavor (optional). You can use the reserved + value ``"auto"`` to have Nova generate a UUID for the + flavor in cases where you cannot simply pass ``None``. :param swap: Swap space in MB :param rxtx_factor: RX/TX factor :rtype: :class:`Flavor` @@ -143,9 +145,9 @@ def create(self, name, ram, vcpus, disk, flavorid, raise exceptions.CommandError("Swap must be an integer.") try: - ephemerel = int(ephemeral) + ephemeral = int(ephemeral) except: - raise exceptions.CommandError("Ephemerel must be an integer.") + raise exceptions.CommandError("Ephemeral must be an integer.") try: rxtx_factor = int(rxtx_factor) From 3ea4ba9fc97b05ad93af8f9aa6f6bc2a638b9a36 Mon Sep 17 00:00:00 2001 From: Nick Shobe Date: Sat, 17 Nov 2012 04:26:08 -0600 Subject: [PATCH 0008/1705] Added --extra-opts to the nova ssh command This will allow for remote command execution and advanced ssh options like portforwarding and connection sharing. The goal is to support ssh commands like nova ssh fe55adc9-cb1e-44a4-bd36-6a537b238172 --extra-opts='-NfnMS ~/.ssh/master-fe55adc9-cb1e-44a4-bd36-6a537b238172' and ssh fe55adc9-cb1e-44a4-bd36-6a537b238172 --extra-opts='-t "sudo puppet agent --test"' This will alow easier scripting of the cli... It is not intended however to replace using the nova python api. Therefore I kept it simple. Change-Id: Icce811caf637fc92c6d6374bfb846ea9525a7e05 --- novaclient/v1_1/shell.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index bcab119f5..b61bdb147 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2120,6 +2120,10 @@ def do_credentials(cs, _args): dest='identity', help='Private key file, same as the -i option to the ssh command.', default='') +@utils.arg('--extra-opts', + dest='extra', + help='Extra options to pass to ssh. see: man ssh', + default='') def do_ssh(cs, args): """SSH into a server.""" addresses = _find_server(cs, args.server).addresses @@ -2140,8 +2144,9 @@ def do_ssh(cs, args): identity = '-i %s' % args.identity if len(args.identity) else '' if ip_address: - os.system("ssh -%d -p%d %s %s@%s" % (version, args.port, identity, - args.login, ip_address)) + os.system("ssh -%d -p%d %s %s@%s %s" % (version, args.port, identity, + args.login, ip_address, + args.extra)) else: pretty_version = "IPv%d" % version print "ERROR: No %s %s address found." % (address_type, From 3fd0c77d07419606a81f257b8876394ec5b374de Mon Sep 17 00:00:00 2001 From: Nikola Dipanov Date: Wed, 7 Nov 2012 18:58:58 +0100 Subject: [PATCH 0009/1705] Boot from volume without image supplied Allow for booting instances from volume without the image parameter supplied. This change is related to the change I530760cfaa5eb0cae590c7383e0840c6b3f896b9 in opnestack/nova. It allows for boot to work with no image supplied to accommodate booting from volumes only. It also makes it possible to interpret show servers that were started without images, so from volumes only Change-Id: I5ba9b0f35a5084aa91eca260f46cac83b8b6591e --- novaclient/v1_1/base.py | 2 +- novaclient/v1_1/shell.py | 34 +++++++++++++++++++++------------ tests/v1_1/fakes.py | 39 ++++++++++++++++++++++++++++++++++++++ tests/v1_1/test_servers.py | 2 +- tests/v1_1/test_shell.py | 32 +++++++++++++++++++++++++++++++ 5 files changed, 95 insertions(+), 14 deletions(-) diff --git a/novaclient/v1_1/base.py b/novaclient/v1_1/base.py index 85d7141fa..d2f384287 100644 --- a/novaclient/v1_1/base.py +++ b/novaclient/v1_1/base.py @@ -66,7 +66,7 @@ def _boot(self, resource_url, response_key, name, image, flavor, """ body = {"server": { "name": name, - "imageRef": str(base.getid(image)), + "imageRef": str(base.getid(image)) if image else '', "flavorRef": str(base.getid(flavor)), }} if userdata: diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index bcab119f5..1bffe2bb6 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -49,7 +49,11 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): raise exceptions.CommandError("you need to specify a Flavor ID ") flavor = _find_flavor(cs, args.flavor) - image = _find_image(cs, args.image) + + if args.image: + image = _find_image(cs, args.image) + else: + image = None meta = dict(v.split('=', 1) for v in args.meta) @@ -237,8 +241,11 @@ def do_boot(cs, args): info['flavor'] = _find_flavor(cs, flavor_id).name image = info.get('image', {}) - image_id = image.get('id', '') - info['image'] = _find_image(cs, image_id).name + if image: + image_id = image.get('id', '') + info['image'] = _find_image(cs, image_id).name + else: # Booting from volume + info['image'] = "Attempt to boot from volume - no image supplied" info.pop('links', None) info.pop('addresses', None) @@ -992,15 +999,18 @@ def _print_server(cs, args): flavor_id) image = info.get('image', {}) - image_id = image.get('id', '') - if args.minimal: - info['image'] = image_id - else: - try: - info['image'] = '%s (%s)' % (_find_image(cs, image_id).name, - image_id) - except Exception: - info['image'] = '%s (%s)' % ("Image not found", image_id) + if image: + image_id = image.get('id', '') + if args.minimal: + info['image'] = image_id + else: + try: + info['image'] = '%s (%s)' % (_find_image(cs, image_id).name, + image_id) + except Exception: + info['image'] = '%s (%s)' % ("Image not found", image_id) + else: # Booted from volume + info['image'] = "Attempt to boot from volume - no image supplied" info.pop('links', None) info.pop('addresses', None) diff --git a/tests/v1_1/fakes.py b/tests/v1_1/fakes.py index b4f125865..4e3345094 100644 --- a/tests/v1_1/fakes.py +++ b/tests/v1_1/fakes.py @@ -248,6 +248,34 @@ def get_servers_detail(self, **kw): "metadata": { "Server Label": "DB 1" } + }, + { + "id": 9012, + "name": "sample-server3", + "image": "", + "flavor": { + "id": 1, + "name": "256 MB Server", + }, + "hostId": "9e107d9d372bb6826bd81d3542a419d6", + "status": "ACTIVE", + "addresses": { + "public": [{ + "version": 4, + "addr": "4.5.6.7", + }, + { + "version": 4, + "addr": "5.6.9.8", + }], + "private": [{ + "version": 4, + "addr": "10.13.12.13", + }], + }, + "metadata": { + "Server Label": "DB 1" + } } ]}) @@ -261,6 +289,13 @@ def post_servers(self, body, **kw): fakes.assert_has_keys(pfile, required=['path', 'contents']) return (202, self.get_servers_1234()[1]) + def post_os_volumes_boot(self, body, **kw): + assert set(body.keys()) <= set(['server', 'os:scheduler_hints']) + fakes.assert_has_keys(body['server'], + required=['name', 'block_device_mapping', 'flavorRef'], + optional=['imageRef']) + return (202, self.get_servers_9012()[1]) + def get_servers_1234(self, **kw): r = {'server': self.get_servers_detail()[1]['servers'][0]} return (200, r) @@ -269,6 +304,10 @@ def get_servers_5678(self, **kw): r = {'server': self.get_servers_detail()[1]['servers'][1]} return (200, r) + def get_servers_9012(self, **kw): + r = {'server': self.get_servers_detail()[1]['servers'][2]} + return (200, r) + def put_servers_1234(self, body, **kw): assert body.keys() == ['server'] fakes.assert_has_keys(body['server'], optional=['name', 'adminPass']) diff --git a/tests/v1_1/test_servers.py b/tests/v1_1/test_servers.py index 4041dad8b..fba066115 100644 --- a/tests/v1_1/test_servers.py +++ b/tests/v1_1/test_servers.py @@ -136,7 +136,7 @@ def test_find(self): flavor={"id": 1, "name": "256 MB Server"}) sl = cs.servers.findall(flavor={"id": 1, "name": "256 MB Server"}) - self.assertEqual([s.id for s in sl], [1234, 5678]) + self.assertEqual([s.id for s in sl], [1234, 5678, 9012]) def test_reboot_server(self): s = cs.servers.get(1234) diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index 78a2da1a7..7ecceab21 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -86,6 +86,33 @@ def test_boot(self): }}, ) + def test_boot_no_image_no_bdms(self): + cmd = 'boot --flavor 1 some-server' + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + def test_boot_no_image_bdms(self): + self.run_command( + 'boot --flavor 1 --block_device_mapping vda=blah:::0 some-server' + ) + self.assert_called_anytime( + 'POST', '/os-volumes_boot', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'block_device_mapping': [ + { + 'volume_size': '', + 'volume_id': 'blah', + 'delete_on_termination': '0', + 'device_name':'vda' + } + ], + 'imageRef': '', + 'min_count': 1, + 'max_count': 1, + }}, + ) + def test_boot_metadata(self): self.run_command('boot --image 1 --flavor 1 --meta foo=bar=pants' ' --meta spam=eggs some-server ') @@ -276,6 +303,11 @@ def test_show(self): self.assert_called('GET', '/flavors/1', pos=-2) self.assert_called('GET', '/images/2') + def test_show_no_image(self): + self.run_command('show 9012') + self.assert_called('GET', '/servers/9012', pos=-2) + self.assert_called('GET', '/flavors/1', pos=-1) + def test_show_bad_id(self): self.assertRaises(exceptions.CommandError, self.run_command, 'show xxx') From 47355b741b50a63227fb38e6be6b485902efe3e5 Mon Sep 17 00:00:00 2001 From: Nikola Dipanov Date: Fri, 16 Nov 2012 10:00:46 +0100 Subject: [PATCH 0010/1705] Improved quota output This patch makes the output of quota show a bit more informative by not setting a simple None to a non-existing quota, but not showing it all together. Kind of fixes bug #1078906 and also bug #1078089 Change-Id: Ic42837d218a80f37e0c6d56625c9804d076f444c --- novaclient/v1_1/shell.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index bcab119f5..6119c9962 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2157,7 +2157,10 @@ def do_ssh(cs, args): def _quota_show(quotas): quota_dict = {} for resource in _quota_resources: - quota_dict[resource] = getattr(quotas, resource, None) + try: + quota_dict[resource] = getattr(quotas, resource) + except AttributeError: + pass utils.print_dict(quota_dict) From dc6285c810b696949597dcde6717bb3f3a6c47c9 Mon Sep 17 00:00:00 2001 From: Julie Pichon Date: Wed, 21 Nov 2012 18:16:17 +0000 Subject: [PATCH 0011/1705] Expand help message for 'migrate' to explain how the new host is selected Fixes bug 1078247 Change-Id: Iedfd6fc957fd1c2e53f7685b02bd33e16d9342e9 --- novaclient/v1_1/shell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 9e9674f6a..608f1d787 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -810,7 +810,7 @@ def do_resize_revert(cs, args): default=False, help='Blocks while instance migrates so progress can be reported.') def do_migrate(cs, args): - """Migrate a server.""" + """Migrate a server. The new host will be selected by the scheduler.""" server = _find_server(cs, args.server) server.migrate() From ff69e4d3830f463afa48ca432600224f29a2c238 Mon Sep 17 00:00:00 2001 From: Alessio Ababilov Date: Mon, 19 Nov 2012 11:33:50 +0200 Subject: [PATCH 0012/1705] Implement fping calls in nova client Implements blueprint novaclient-fping Provide fping API calls in nova client as well as unit tests. API is accessed by `fping` field of nova client. Methods: * list(all_tenants=False|True, include=[VM ids], exclude=[VM ids]) - perform fping for all VMs in current project (or all projects if all_tenants==True) * get(server object or id) - perform fping for single instance Change-Id: Ic3eda142781d1a14f2cfc83626672a579fc93a7d --- novaclient/v1_1/client.py | 2 ++ novaclient/v1_1/fping.py | 66 +++++++++++++++++++++++++++++++++++++++ tests/v1_1/fakes.py | 29 +++++++++++++++++ tests/v1_1/test_fping.py | 41 ++++++++++++++++++++++++ 4 files changed, 138 insertions(+) create mode 100644 novaclient/v1_1/fping.py create mode 100644 tests/v1_1/test_fping.py diff --git a/novaclient/v1_1/client.py b/novaclient/v1_1/client.py index f726e82e1..a56a19f8d 100644 --- a/novaclient/v1_1/client.py +++ b/novaclient/v1_1/client.py @@ -7,6 +7,7 @@ from novaclient.v1_1 import floating_ip_dns from novaclient.v1_1 import floating_ips from novaclient.v1_1 import floating_ip_pools +from novaclient.v1_1 import fping from novaclient.v1_1 import hosts from novaclient.v1_1 import hypervisors from novaclient.v1_1 import images @@ -69,6 +70,7 @@ def __init__(self, username, api_key, project_id, auth_url=None, self.certs = certs.CertificateManager(self) self.floating_ips = floating_ips.FloatingIPManager(self) self.floating_ip_pools = floating_ip_pools.FloatingIPPoolManager(self) + self.fping = fping.FpingManager(self) self.volumes = volumes.VolumeManager(self) self.volume_snapshots = volume_snapshots.SnapshotManager(self) self.volume_types = volume_types.VolumeTypeManager(self) diff --git a/novaclient/v1_1/fping.py b/novaclient/v1_1/fping.py new file mode 100644 index 000000000..2f9eb89b6 --- /dev/null +++ b/novaclient/v1_1/fping.py @@ -0,0 +1,66 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 OpenStack LLC. +# All Rights Reserved. +# +# 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. + +""" +Fping interface. +""" + +from novaclient import base + + +class Fping(base.Resource): + """ + A server to fping. + """ + HUMAN_ID = True + + def __repr__(self): + return "" % self.id + + +class FpingManager(base.ManagerWithFind): + """ + Manage :class:`Fping` resources. + """ + resource_class = Fping + + def list(self, all_tenants=False, include=[], exclude=[]): + """ + Fping all servers. + + :rtype: list of :class:`Fping`. + """ + params = [] + if all_tenants: + params.append("all_tenants=1") + if include: + params.append("include=%s" % ",".join(include)) + elif exclude: + params.append("exclude=%s" % ",".join(exclude)) + uri = "/os-fping" + if params: + uri = "%s?%s" % (uri, "&".join(params)) + return self._list(uri, "servers") + + def get(self, server): + """ + Fping a specific server. + + :param network: ID of the server to fping. + :rtype: :class:`Fping` + """ + return self._get("/os-fping/%s" % base.getid(server), "server") diff --git a/tests/v1_1/fakes.py b/tests/v1_1/fakes.py index 8ee28c88b..65aaf0321 100644 --- a/tests/v1_1/fakes.py +++ b/tests/v1_1/fakes.py @@ -1119,3 +1119,32 @@ def post_os_networks_add(self, **kw): def post_os_networks_networkdisassociate_action(self, **kw): return (202, None) + + def get_os_fping(self, **kw): + return ( + 200, { + 'servers': [ + { + "id": "1", + "project_id": "fake-project", + "alive": True, + }, + { + "id": "2", + "project_id": "fake-project", + "alive": True, + }, + ] + } + ) + + def get_os_fping_1(self, **kw): + return ( + 200, { + 'server': { + "id": "1", + "project_id": "fake-project", + "alive": True, + } + } + ) diff --git a/tests/v1_1/test_fping.py b/tests/v1_1/test_fping.py new file mode 100644 index 000000000..6462fb213 --- /dev/null +++ b/tests/v1_1/test_fping.py @@ -0,0 +1,41 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 OpenStack LLC. +# All Rights Reserved. +# +# 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 novaclient import exceptions +from novaclient.v1_1 import fping +from tests import utils +from tests.v1_1 import fakes + + +cs = fakes.FakeClient() + + +class FpingTest(utils.TestCase): + + def test_list_fpings(self): + fl = cs.fping.list() + cs.assert_called('GET', '/os-fping') + [self.assertTrue(isinstance(f, fping.Fping)) for f in fl] + [self.assertEqual(f.project_id, "fake-project") for f in fl] + [self.assertEqual(f.alive, True) for f in fl] + + def test_get_fping(self): + f = cs.fping.get(1) + cs.assert_called('GET', '/os-fping/1') + self.assertTrue(isinstance(f, fping.Fping)) + self.assertEqual(f.project_id, "fake-project") + self.assertEqual(f.alive, True) From 3839d284da8bb269ca392727f0716ddc2238b880 Mon Sep 17 00:00:00 2001 From: Chris Yeoh Date: Thu, 22 Nov 2012 22:43:59 +1030 Subject: [PATCH 0013/1705] Adds nova client support for nova-manage fixed command Adds the following commands: - "fixed-ip-get " - displays information about the fixed ip ip_addr - "fixed-ip-reserve " - reserves the fixed ip ip_addr - "fixed-ip-unreserve " - unreserves the fixed ip ip_addr Change-Id: I6a5c8b9f7ab359adeb57b86240279649cd421801 Implements: blueprint apis-for-nova-manage --- novaclient/v1_1/client.py | 2 ++ novaclient/v1_1/fixed_ips.py | 58 ++++++++++++++++++++++++++++++++++++ novaclient/v1_1/shell.py | 19 ++++++++++++ tests/v1_1/fakes.py | 13 ++++++++ tests/v1_1/test_fixed_ips.py | 43 ++++++++++++++++++++++++++ tests/v1_1/test_shell.py | 14 +++++++++ 6 files changed, 149 insertions(+) create mode 100644 novaclient/v1_1/fixed_ips.py create mode 100644 tests/v1_1/test_fixed_ips.py diff --git a/novaclient/v1_1/client.py b/novaclient/v1_1/client.py index f726e82e1..78c969020 100644 --- a/novaclient/v1_1/client.py +++ b/novaclient/v1_1/client.py @@ -24,6 +24,7 @@ from novaclient.v1_1 import volume_snapshots from novaclient.v1_1 import volume_types from novaclient.v1_1 import services +from novaclient.v1_1 import fixed_ips class Client(object): @@ -86,6 +87,7 @@ def __init__(self, username, api_key, project_id, auth_url=None, self.hosts = hosts.HostManager(self) self.hypervisors = hypervisors.HypervisorManager(self) self.services = services.ServiceManager(self) + self.fixed_ips = fixed_ips.FixedIPsManager(self) # Add in any extensions... if extensions: diff --git a/novaclient/v1_1/fixed_ips.py b/novaclient/v1_1/fixed_ips.py new file mode 100644 index 000000000..a45db99fc --- /dev/null +++ b/novaclient/v1_1/fixed_ips.py @@ -0,0 +1,58 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 IBM +# All Rights Reserved. +# +# 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. + +""" +Fixed IPs interface. +""" + +from novaclient import base + + +class FixedIP(base.Resource): + def __repr__(self): + return "" % self.address + + +class FixedIPsManager(base.ManagerWithFind): + resource_class = FixedIP + + def get(self, fixed_ip): + """ + Show information for a Fixed IP + + :param fixed_ip: Fixed IP address to get info for + """ + return self._get('/os-fixed-ips/%s' % base.getid(fixed_ip), + "fixed_ip") + + def reserve(self, fixed_ip): + """Reserve a Fixed IP + + :param fixed_ip: Fixed IP address to reserve + """ + body = {"reserve": None} + self.api.client.post('/os-fixed-ips/%s/action' % base.getid(fixed_ip), + body=body) + + def unreserve(self, fixed_ip): + """Unreserve a Fixed IP + + :param fixed_ip: Fixed IP address to unreserve + """ + body = {"unreserve": None} + self.api.client.post('/os-fixed-ips/%s/action' % base.getid(fixed_ip), + body=body) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 608f1d787..7a285218c 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1986,6 +1986,25 @@ def do_service_disable(cs, args): utils.print_list([result], ['Host', 'Service', 'Disabled']) +@utils.arg('fixed_ip', metavar='', help='Fixed IP Address.') +def do_fixed_ip_get(cs, args): + """Get info on a fixed ip""" + result = cs.fixed_ips.get(args.fixed_ip) + utils.print_list([result], ['address', 'cidr', 'hostname', 'host']) + + +@utils.arg('fixed_ip', metavar='', help='Fixed IP Address.') +def do_fixed_ip_reserve(cs, args): + """Reserve a fixed ip""" + cs.fixed_ips.reserve(args.fixed_ip) + + +@utils.arg('fixed_ip', metavar='', help='Fixed IP Address.') +def do_fixed_ip_unreserve(cs, args): + """Unreserve a fixed ip""" + cs.fixed_ips.unreserve(args.fixed_ip) + + @utils.arg('host', metavar='', help='Name of host.') def do_host_describe(cs, args): """Describe a specific host""" diff --git a/tests/v1_1/fakes.py b/tests/v1_1/fakes.py index 8ee28c88b..c0568d044 100644 --- a/tests/v1_1/fakes.py +++ b/tests/v1_1/fakes.py @@ -937,6 +937,19 @@ def put_os_services_disable(self, body, **kw): return (200, {'host': body['host'], 'service': body['service'], 'disabled': True}) + # + # Fixed IPs + # + def get_os_fixed_ips_192_168_1_1(self, *kw): + return (200, {"fixed_ip": + {'cidr': '192.168.1.0/24', + 'address': '192.168.1.1', + 'hostname': 'foo', + 'host': 'bar'}}) + + def post_os_fixed_ips_192_168_1_1_action(self, body, **kw): + return (202, None) + # # Hosts # diff --git a/tests/v1_1/test_fixed_ips.py b/tests/v1_1/test_fixed_ips.py new file mode 100644 index 000000000..6548755c2 --- /dev/null +++ b/tests/v1_1/test_fixed_ips.py @@ -0,0 +1,43 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 IBM +# All Rights Reserved. +# +# 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 novaclient.v1_1 import fixed_ips +from tests.v1_1 import fakes +from tests import utils + +cs = fakes.FakeClient() + + +class FixedIpsTest(utils.TestCase): + + def test_get_fixed_ip(self): + info = cs.fixed_ips.get(fixed_ip='192.168.1.1') + cs.assert_called('GET', '/os-fixed-ips/192.168.1.1') + self.assertEqual(info.cidr, '192.168.1.0/24') + self.assertEqual(info.address, '192.168.1.1') + self.assertEqual(info.hostname, 'foo') + self.assertEqual(info.host, 'bar') + + def test_reserve_fixed_ip(self): + body = {"reserve": None} + res = cs.fixed_ips.reserve(fixed_ip='192.168.1.1') + cs.assert_called('POST', '/os-fixed-ips/192.168.1.1/action', body) + + def test_unreserve_fixed_ip(self): + body = {"unreserve": None} + res = cs.fixed_ips.unreserve(fixed_ip='192.168.1.1') + cs.assert_called('POST', '/os-fixed-ips/192.168.1.1/action', body) diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index bd76120c0..fbcce7717 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -512,6 +512,20 @@ def test_services_disable(self): body = {'host': 'host1', 'service': 'nova-cert'} self.assert_called('PUT', '/os-services/disable', body) + def test_fixed_ips_get(self): + self.run_command('fixed-ip-get 192.168.1.1') + self.assert_called('GET', '/os-fixed-ips/192.168.1.1') + + def test_fixed_ips_reserve(self): + self.run_command('fixed-ip-reserve 192.168.1.1') + body = {'reserve': None} + self.assert_called('POST', '/os-fixed-ips/192.168.1.1/action', body) + + def test_fixed_ips_unreserve(self): + self.run_command('fixed-ip-unreserve 192.168.1.1') + body = {'unreserve': None} + self.assert_called('POST', '/os-fixed-ips/192.168.1.1/action', body) + def test_host_list(self): self.run_command('host-list') self.assert_called('GET', '/os-hosts') From 00aa8cf9ee2b9063b4aff2a70eaf30e5f4203959 Mon Sep 17 00:00:00 2001 From: ivan-zhu Date: Sun, 25 Nov 2012 20:00:55 +0800 Subject: [PATCH 0014/1705] Add nova client support for nova-manage account scrub command This add a CLI in scrub in nova-client. so we can use "nova scrub project_id" like "nova-manage account scrub project_id".It will delete data associated with project. Change-Id: I6e76fe4357fcdc3765f3ece70d9b6b07c23b59bd --- novaclient/v1_1/shell.py | 17 +++++++++++++++++ tests/v1_1/fakes.py | 10 ++++++++-- tests/v1_1/test_shell.py | 9 +++++++++ 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index c296d61d3..d07fc9283 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -482,6 +482,23 @@ def do_flavor_access_remove(cs, args): utils.print_list(access_list, columns) +@utils.arg('project_id', metavar='', + help='The ID of the project.') +def do_scrub(cs, args): + """Deletes data associated with the project""" + networks_list = cs.networks.list() + networks_list = [network for network in networks_list + if getattr(network, 'project_id', '') == args.project_id] + search_opts = {'all_tenants': 1} + groups = cs.security_groups.list(search_opts) + groups = [group for group in groups + if group.tenant_id == args.project_id] + for network in networks_list: + cs.networks.disassociate(network) + for group in groups: + cs.security_groups.delete(group) + + def do_network_list(cs, _args): """Print a list of available networks.""" network_list = cs.networks.list() diff --git a/tests/v1_1/fakes.py b/tests/v1_1/fakes.py index b4f125865..806c098a8 100644 --- a/tests/v1_1/fakes.py +++ b/tests/v1_1/fakes.py @@ -740,7 +740,8 @@ def put_os_quota_class_sets_test(self, body, **kw): # def get_os_security_groups(self, **kw): return (200, {"security_groups": [ - {'id': 1, 'name': 'test', 'description': 'FAKE_SECURITY_GROUP'} + {'id': 1, 'name': 'test', 'description': 'FAKE_SECURITY_GROUP', + 'tenant_id': '4ffc664c198e435e9853f2538fbcd7a7'} ]}) def get_os_security_groups_1(self, **kw): @@ -1034,7 +1035,9 @@ def get_os_hypervisors_1234_uptime(self, **kw): 'uptime': "fake uptime"}}) def get_os_networks(self, **kw): - return (200, {'networks': [{"label": "1", "cidr": "10.0.0.0/24"}]}) + return (200, {'networks': [{"label": "1", "cidr": "10.0.0.0/24", + 'project_id': '4ffc664c198e435e9853f2538fbcd7a7', + 'id': '1'}]}) def get_os_networks_1(self, **kw): return (200, {'network': {"label": "1", "cidr": "10.0.0.0/24"}}) @@ -1042,6 +1045,9 @@ def get_os_networks_1(self, **kw): def post_os_networks(self, **kw): return (202, {'network': kw}) + def post_os_networks_1_action(self, **kw): + return (202, None) + def delete_os_networks_networkdelete(self, **kw): return (202, None) diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index 78a2da1a7..39b486876 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -270,6 +270,15 @@ def test_root_password(self): self.assert_called('POST', '/servers/1234/action', {'changePassword': {'adminPass': 'p'}}) + def test_scrub(self): + self.run_command('scrub 4ffc664c198e435e9853f2538fbcd7a7') + self.assert_called('GET', '/os-networks', pos=-4) + self.assert_called('GET', '/os-security-groups?all_tenants=1', + pos=-3) + self.assert_called('POST', '/os-networks/1/action', + {"disassociate": None}, pos=-2) + self.assert_called('DELETE', '/os-security-groups/1') + def test_show(self): self.run_command('show 1234') self.assert_called('GET', '/servers/1234', pos=-3) From b9d60c1fd2f887992fc1f6c2246b1f0f11efe1a9 Mon Sep 17 00:00:00 2001 From: Joe Gordon Date: Mon, 26 Nov 2012 14:26:53 -0800 Subject: [PATCH 0015/1705] Fix aggregate command help messages. Fixes bug 1083343 Change-Id: I315a0629bb33480952f2280b053b8db8cb83a1ea --- novaclient/v1_1/shell.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 608f1d787..c984f24f6 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1894,7 +1894,7 @@ def do_aggregate_set_metadata(cs, args): _print_aggregate_details(aggregate) -@utils.arg('id', metavar='', help='Host aggregate id to delete.') +@utils.arg('id', metavar='', help='Aggregate id.') @utils.arg('host', metavar='', help='The host to add to the aggregate.') def do_aggregate_add_host(cs, args): """Add the host to the specified aggregate.""" @@ -1903,8 +1903,9 @@ def do_aggregate_add_host(cs, args): _print_aggregate_details(aggregate) -@utils.arg('id', metavar='', help='Host aggregate id to delete.') -@utils.arg('host', metavar='', help='The host to add to the aggregate.') +@utils.arg('id', metavar='', help='Aggregate id.') +@utils.arg('host', metavar='', + help='The host to remove from the aggregate.') def do_aggregate_remove_host(cs, args): """Remove the specified host from the specified aggregate.""" aggregate = cs.aggregates.remove_host(args.id, args.host) @@ -1912,7 +1913,7 @@ def do_aggregate_remove_host(cs, args): _print_aggregate_details(aggregate) -@utils.arg('id', metavar='', help='Host aggregate id to delete.') +@utils.arg('id', metavar='', help='Aggregate id.') def do_aggregate_details(cs, args): """Show details of the specified aggregate.""" _print_aggregate_details(cs.aggregates.get_details(args.id)) @@ -1938,7 +1939,7 @@ def _print_aggregate_details(aggregate): action='store_true', dest='disk_over_commit', default=False, - help='Allow overcommit.(Default=Flase)') + help='Allow overcommit.(Default=False)') @utils.arg('--disk_over_commit', action='store_true', help=argparse.SUPPRESS) From 4f6419b5553e853f5720084ba01fc0b4cc1f6220 Mon Sep 17 00:00:00 2001 From: Chris Yeoh Date: Mon, 26 Nov 2012 14:21:35 +1030 Subject: [PATCH 0016/1705] Adds nova client support for nova-manage vpn command Adds "nova cloudpipe-configure" command Adds shell tests for cloudpipe-list and cloudpipe-create commands which already exist This patch depends on https://review.openstack.org/#/c/15854/ Change-Id: I784f5bf0f25a2d8cae4b7c2c6ccf345842ffe352 Implements: blueprint apis-for-nova-manage --- novaclient/v1_1/cloudpipe.py | 14 ++++++++++++++ novaclient/v1_1/shell.py | 7 +++++++ tests/v1_1/fakes.py | 3 +++ tests/v1_1/test_cloudpipe.py | 6 ++++++ tests/v1_1/test_shell.py | 15 +++++++++++++++ 5 files changed, 45 insertions(+) diff --git a/novaclient/v1_1/cloudpipe.py b/novaclient/v1_1/cloudpipe.py index 4dea3f2a6..44baa2160 100644 --- a/novaclient/v1_1/cloudpipe.py +++ b/novaclient/v1_1/cloudpipe.py @@ -16,6 +16,7 @@ """Cloudpipe interface.""" from novaclient import base +from novaclient.v1_1 import networks class Cloudpipe(base.Resource): @@ -46,3 +47,16 @@ def list(self): Get a list of cloudpipe instances. """ return self._list('/os-cloudpipe', 'cloudpipes') + + def update(self, address, port): + """ + Update VPN address and port for all networks associated + with the project defined by authentication + + :param address: IP address + :param port: Port number + """ + + body = {'configure_project': {'vpn_ip': address, + 'vpn_port': port}} + self._update("/os-cloudpipe/configure-project", body) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index c984f24f6..4d0296181 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -269,6 +269,13 @@ def do_cloudpipe_create(cs, args): cs.cloudpipe.create(args.project) +@utils.arg('address', metavar='', help='New IP Address.') +@utils.arg('port', metavar='', help='New Port.') +def do_cloudpipe_configure(cs, args): + """Create a cloudpipe instance for the given project""" + cs.cloudpipe.update(args.address, args.port) + + def _poll_for_status(poll_fn, obj_id, action, final_ok_states, poll_period=5, show_progress=True, status_field="status", silent=False): diff --git a/tests/v1_1/fakes.py b/tests/v1_1/fakes.py index 8ee28c88b..ed7662b4b 100644 --- a/tests/v1_1/fakes.py +++ b/tests/v1_1/fakes.py @@ -453,6 +453,9 @@ def get_os_cloudpipe(self, **kw): def post_os_cloudpipe(self, **ks): return (202, {'instance_id': '9d5824aa-20e6-4b9f-b967-76a699fc51fd'}) + def put_os_cloudpipe_configure_project(self, **kw): + return (202, None) + # # Flavors # diff --git a/tests/v1_1/test_cloudpipe.py b/tests/v1_1/test_cloudpipe.py index 82a750f0a..85ed2ff7d 100644 --- a/tests/v1_1/test_cloudpipe.py +++ b/tests/v1_1/test_cloudpipe.py @@ -20,3 +20,9 @@ def test_create(self): body = {'cloudpipe': {'project_id': project}} cs.assert_called('POST', '/os-cloudpipe', body) self.assertTrue(isinstance(cp, str)) + + def test_update(self): + cs.cloudpipe.update("192.168.1.1", 2345) + body = {'configure_project': {'vpn_ip': "192.168.1.1", + 'vpn_port': 2345}} + cs.assert_called('PUT', '/os-cloudpipe/configure-project', body) diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index bd76120c0..90fecd3e3 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -600,6 +600,21 @@ def test_network_show(self): self.run_command('network-show 1') self.assert_called('GET', '/os-networks/1') + def test_cloudpipe_list(self): + self.run_command('cloudpipe-list') + self.assert_called('GET', '/os-cloudpipe') + + def test_cloudpipe_create(self): + self.run_command('cloudpipe-create myproject') + body = {'cloudpipe': {'project_id': "myproject"}} + self.assert_called('POST', '/os-cloudpipe', body) + + def test_cloudpipe_configure(self): + self.run_command('cloudpipe-configure 192.168.1.1 1234') + body = {'configure_project': {'vpn_ip': "192.168.1.1", + 'vpn_port': '1234'}} + self.assert_called('PUT', '/os-cloudpipe/configure-project', body) + def test_backup(self): self.run_command('backup sample-server back1 daily 1') self.assert_called('POST', '/servers/1234/action', From 27d7ad9d8632bdc67e25757a42a91109e393fbf9 Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Tue, 27 Nov 2012 10:59:43 -0500 Subject: [PATCH 0017/1705] Validate that rxtx_factor is a float. Nova stores rxtx_factor as a float internally and as such novaclient should validate that a float is specified when creating a flavor. Fixes LP Bug #1083651. Change-Id: I75f9440d3fe2a0e72ea592f2259640623400ae73 --- novaclient/v1_1/flavors.py | 8 ++++---- novaclient/v1_1/shell.py | 2 +- tests/v1_1/test_flavors.py | 16 ++++++++-------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/novaclient/v1_1/flavors.py b/novaclient/v1_1/flavors.py index fa9a0de47..42778c620 100644 --- a/novaclient/v1_1/flavors.py +++ b/novaclient/v1_1/flavors.py @@ -105,7 +105,7 @@ def delete(self, flavor): self._delete("/flavors/%s" % base.getid(flavor)) def create(self, name, ram, vcpus, disk, flavorid=None, - ephemeral=0, swap=0, rxtx_factor=1, is_public=True): + ephemeral=0, swap=0, rxtx_factor=1.0, is_public=True): """ Create (allocate) a floating ip for a tenant @@ -150,9 +150,9 @@ def create(self, name, ram, vcpus, disk, flavorid=None, raise exceptions.CommandError("Ephemeral must be an integer.") try: - rxtx_factor = int(rxtx_factor) + rxtx_factor = float(rxtx_factor) except: - raise exceptions.CommandError("rxtx_factor must be an integer.") + raise exceptions.CommandError("rxtx_factor must be a float.") try: is_public = utils.bool_from_str(is_public) @@ -168,7 +168,7 @@ def create(self, name, ram, vcpus, disk, flavorid=None, "id": flavorid, "swap": int(swap), "OS-FLV-EXT-DATA:ephemeral": int(ephemeral), - "rxtx_factor": int(rxtx_factor), + "rxtx_factor": float(rxtx_factor), "os-flavor-access:is_public": bool(is_public), } } diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index c984f24f6..60d6b6c43 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -394,7 +394,7 @@ def do_flavor_show(cs, args): @utils.arg('--rxtx-factor', metavar='', help="RX/TX factor (default 1)", - default=1) + default=1.0) @utils.arg('--is-public', metavar='', help="Make flavor accessible to the public (default true)", diff --git a/tests/v1_1/test_flavors.py b/tests/v1_1/test_flavors.py index 6a766d97f..ca4b33789 100644 --- a/tests/v1_1/test_flavors.py +++ b/tests/v1_1/test_flavors.py @@ -60,7 +60,7 @@ def test_create(self): "OS-FLV-EXT-DATA:ephemeral": 10, "id": 1234, "swap": 0, - "rxtx_factor": 1, + "rxtx_factor": 1.0, "os-flavor-access:is_public": False, } } @@ -80,7 +80,7 @@ def test_create_ephemeral_ispublic_defaults(self): "OS-FLV-EXT-DATA:ephemeral": 0, "id": 1234, "swap": 0, - "rxtx_factor": 1, + "rxtx_factor": 1.0, "os-flavor-access:is_public": True, } } @@ -91,25 +91,25 @@ def test_create_ephemeral_ispublic_defaults(self): def test_invalid_parameters_create(self): self.assertRaises(exceptions.CommandError, cs.flavors.create, "flavorcreate", "invalid", 1, 10, 1234, swap=0, - ephemeral=0, rxtx_factor=1, is_public=True) + ephemeral=0, rxtx_factor=1.0, is_public=True) self.assertRaises(exceptions.CommandError, cs.flavors.create, "flavorcreate", 512, "invalid", 10, 1234, swap=0, - ephemeral=0, rxtx_factor=1, is_public=True) + ephemeral=0, rxtx_factor=1.0, is_public=True) self.assertRaises(exceptions.CommandError, cs.flavors.create, "flavorcreate", 512, 1, "invalid", 1234, swap=0, - ephemeral=0, rxtx_factor=1, is_public=True) + ephemeral=0, rxtx_factor=1.0, is_public=True) self.assertRaises(exceptions.CommandError, cs.flavors.create, "flavorcreate", 512, 1, 10, 1234, swap="invalid", - ephemeral=0, rxtx_factor=1, is_public=True) + ephemeral=0, rxtx_factor=1.0, is_public=True) self.assertRaises(exceptions.CommandError, cs.flavors.create, "flavorcreate", 512, 1, 10, 1234, swap=0, - ephemeral="invalid", rxtx_factor=1, is_public=True) + ephemeral="invalid", rxtx_factor=1.0, is_public=True) self.assertRaises(exceptions.CommandError, cs.flavors.create, "flavorcreate", 512, 1, 10, 1234, swap=0, ephemeral=0, rxtx_factor="invalid", is_public=True) self.assertRaises(exceptions.CommandError, cs.flavors.create, "flavorcreate", 512, 1, 10, 1234, swap=0, - ephemeral=0, rxtx_factor=1, is_public='invalid') + ephemeral=0, rxtx_factor=1.0, is_public='invalid') def test_delete(self): cs.flavors.delete("flavordelete") From 494040d7fe36ddd00c8813a6e894e53b1abf31be Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Tue, 27 Nov 2012 11:05:53 -0500 Subject: [PATCH 0018/1705] Remove unnecessary casts in flavor create. We already cast and validate these variables above... no need to cast them again. Change-Id: I87b967925ae77c70eb07a42f3ae050703d44a427 --- novaclient/v1_1/flavors.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/novaclient/v1_1/flavors.py b/novaclient/v1_1/flavors.py index 42778c620..28f065425 100644 --- a/novaclient/v1_1/flavors.py +++ b/novaclient/v1_1/flavors.py @@ -162,14 +162,14 @@ def create(self, name, ram, vcpus, disk, flavorid=None, body = { "flavor": { "name": name, - "ram": int(ram), - "vcpus": int(vcpus), - "disk": int(disk), + "ram": ram, + "vcpus": vcpus, + "disk": disk, "id": flavorid, - "swap": int(swap), - "OS-FLV-EXT-DATA:ephemeral": int(ephemeral), - "rxtx_factor": float(rxtx_factor), - "os-flavor-access:is_public": bool(is_public), + "swap": swap, + "OS-FLV-EXT-DATA:ephemeral": ephemeral, + "rxtx_factor": rxtx_factor, + "os-flavor-access:is_public": is_public, } } From 4e3aa56fe77e47f0b2eba057d751e2459fb88913 Mon Sep 17 00:00:00 2001 From: Chris Yeoh Date: Wed, 28 Nov 2012 18:39:15 +1030 Subject: [PATCH 0019/1705] Adds nova client support for nova-manage floating command Adds the following commands: - nova floating-ip-bulk-list - nova floating-ip-bulk-create - nova floating-ip-bulk-delete Change-Id: Ia183a7a478d23fee3552d43a866ba96abb7472b2 Implements: blueprint apis-for-nova-manage --- novaclient/v1_1/client.py | 2 + novaclient/v1_1/floating_ips_bulk.py | 60 ++++++++++++++++++++++++++ novaclient/v1_1/shell.py | 27 ++++++++++++ tests/v1_1/fakes.py | 25 +++++++++++ tests/v1_1/test_floating_ips_bulk.py | 64 ++++++++++++++++++++++++++++ tests/v1_1/test_shell.py | 23 ++++++++++ 6 files changed, 201 insertions(+) create mode 100644 novaclient/v1_1/floating_ips_bulk.py create mode 100644 tests/v1_1/test_floating_ips_bulk.py diff --git a/novaclient/v1_1/client.py b/novaclient/v1_1/client.py index dbff3e370..d9e8720ca 100644 --- a/novaclient/v1_1/client.py +++ b/novaclient/v1_1/client.py @@ -26,6 +26,7 @@ from novaclient.v1_1 import volume_types from novaclient.v1_1 import services from novaclient.v1_1 import fixed_ips +from novaclient.v1_1 import floating_ips_bulk class Client(object): @@ -90,6 +91,7 @@ def __init__(self, username, api_key, project_id, auth_url=None, self.hypervisors = hypervisors.HypervisorManager(self) self.services = services.ServiceManager(self) self.fixed_ips = fixed_ips.FixedIPsManager(self) + self.floating_ips_bulk = floating_ips_bulk.FloatingIPBulkManager(self) # Add in any extensions... if extensions: diff --git a/novaclient/v1_1/floating_ips_bulk.py b/novaclient/v1_1/floating_ips_bulk.py new file mode 100644 index 000000000..b98a5b66a --- /dev/null +++ b/novaclient/v1_1/floating_ips_bulk.py @@ -0,0 +1,60 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 IBM +# All Rights Reserved. +# +# 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. + +""" +Bulk Floating IPs interface +""" +from novaclient import base + + +class FloatingIP(base.Resource): + def __repr__(self): + return "" % self.address + + +class FloatingIPBulkManager(base.ManagerWithFind): + resource_class = FloatingIP + + def list(self, host=None): + """ + List all floating IPs + """ + if host is None: + return self._list('/os-floating-ips-bulk', 'floating_ip_info') + else: + return self._list('/os-floating-ips-bulk/%s' % host, + 'floating_ip_info') + + def create(self, ip_range, pool=None, interface=None): + """ + Create floating IPs by range + """ + body = {"floating_ips_bulk_create": {'ip_range': ip_range}} + if pool is not None: + body['floating_ips_bulk_create']['pool'] = pool + if interface is not None: + body['floating_ips_bulk_create']['interface'] = interface + + return self._create('/os-floating-ips-bulk', body, + 'floating_ips_bulk_create') + + def delete(self, ip_range): + """ + Delete floating IPs by range + """ + body = {"ip_range": ip_range} + return self._update('/os-floating-ips-bulk/delete', body) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 7b5c1fd94..273039382 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1425,6 +1425,33 @@ def do_floating_ip_pool_list(cs, _args): utils.print_list(cs.floating_ip_pools.list(), ['name']) +@utils.arg('--host', dest='host', metavar='', default=None, + help='Filter by host') +def do_floating_ip_bulk_list(cs, args): + """List all floating ips""" + utils.print_list(cs.floating_ips_bulk.list(args.host), ['project_id', + 'address', + 'instance_uuid', + 'pool', + 'interface']) + + +@utils.arg('ip_range', metavar='', help='Address range to create') +@utils.arg('--pool', dest='pool', metavar='', default=None, + help='Pool for new Floating IPs') +@utils.arg('--interface', metavar='', default=None, + help='Interface for new Floating IPs') +def do_floating_ip_bulk_create(cs, args): + """Bulk create floating ips by range""" + cs.floating_ips_bulk.create(args.ip_range, args.pool, args.interface) + + +@utils.arg('ip_range', metavar='', help='Address range to delete') +def do_floating_ip_bulk_delete(cs, args): + """Bulk delete floating ips by range""" + cs.floating_ips_bulk.delete(args.ip_range) + + def _print_dns_list(dns_entries): utils.print_list(dns_entries, ['ip', 'name', 'domain']) diff --git a/tests/v1_1/fakes.py b/tests/v1_1/fakes.py index 91f33e8fe..fe094e07d 100644 --- a/tests/v1_1/fakes.py +++ b/tests/v1_1/fakes.py @@ -615,6 +615,31 @@ def delete_os_floating_ip_dns_testdomain(self, **kw): def delete_os_floating_ip_dns_testdomain_entries_testname(self, **kw): return (200, None) + def get_os_floating_ips_bulk(self, **kw): + return (200, {'floating_ip_info': [ + {'id': 1, 'fixed_ip': '10.0.0.1', 'ip': '11.0.0.1'}, + {'id': 2, 'fixed_ip': '10.0.0.2', 'ip': '11.0.0.2'}, + ]}) + + def get_os_floating_ips_bulk_testHost(self, **kw): + return (200, {'floating_ip_info': [ + {'id': 1, 'fixed_ip': '10.0.0.1', 'ip': '11.0.0.1'}, + {'id': 2, 'fixed_ip': '10.0.0.2', 'ip': '11.0.0.2'}, + ]}) + + def post_os_floating_ips_bulk(self, **kw): + params = kw.get('body').get('floating_ips_bulk_create') + pool = params.get('pool', 'defaultPool') + interface = params.get('interface', 'defaultInterface') + return (200, {'floating_ips_bulk_create': + {'ip_range': '192.168.1.0/30', + 'pool': pool, + 'interface': interface}}) + + def put_os_floating_ips_bulk_delete(self, **kw): + ip_range = kw.get('body').get('ip_range') + return (200, {'floating_ips_bulk_delete': ip_range}) + # # Images # diff --git a/tests/v1_1/test_floating_ips_bulk.py b/tests/v1_1/test_floating_ips_bulk.py new file mode 100644 index 000000000..7d4ac7665 --- /dev/null +++ b/tests/v1_1/test_floating_ips_bulk.py @@ -0,0 +1,64 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 IBM +# All Rights Reserved. +# +# 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 novaclient.v1_1 import floating_ips_bulk +from tests import utils +from tests.v1_1 import fakes + + +cs = fakes.FakeClient() + + +class FloatingIPsBulkTest(utils.TestCase): + + def test_list_floating_ips_bulk(self): + fl = cs.floating_ips_bulk.list() + cs.assert_called('GET', '/os-floating-ips-bulk') + [self.assertTrue(isinstance(f, floating_ips_bulk.FloatingIP)) + for f in fl] + + def test_list_floating_ips_bulk_host_filter(self): + fl = cs.floating_ips_bulk.list('testHost') + cs.assert_called('GET', '/os-floating-ips-bulk/testHost') + [self.assertTrue(isinstance(f, floating_ips_bulk.FloatingIP)) + for f in fl] + + def test_create_floating_ips_bulk(self): + fl = cs.floating_ips_bulk.create('192.168.1.0/30') + body = {'floating_ips_bulk_create': {'ip_range': '192.168.1.0/30'}} + cs.assert_called('POST', '/os-floating-ips-bulk', body) + self.assertEqual(fl.ip_range, + body['floating_ips_bulk_create']['ip_range']) + + def test_create_floating_ips_bulk_with_pool_and_host(self): + fl = cs.floating_ips_bulk.create('192.168.1.0/30', 'poolTest', + 'interfaceTest') + body = {'floating_ips_bulk_create': + {'ip_range': '192.168.1.0/30', 'pool': 'poolTest', + 'interface': 'interfaceTest'}} + cs.assert_called('POST', '/os-floating-ips-bulk', body) + self.assertEqual(fl.ip_range, + body['floating_ips_bulk_create']['ip_range']) + self.assertEqual(fl.pool, + body['floating_ips_bulk_create']['pool']) + self.assertEqual(fl.interface, + body['floating_ips_bulk_create']['interface']) + + def test_delete_floating_ips_bulk(self): + fl = cs.floating_ips_bulk.delete('192.168.1.0/30') + body = {'ip_range': '192.168.1.0/30'} + cs.assert_called('PUT', '/os-floating-ips-bulk/delete', body) + self.assertEqual(fl['floating_ips_bulk_delete'], body['ip_range']) diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index 206fc8c22..c24ca9119 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -386,6 +386,29 @@ def test_dns_domains(self): self.run_command('dns-domains') self.assert_called('GET', '/os-floating-ip-dns') + def test_floating_ip_bulk_list(self): + self.run_command('floating-ip-bulk-list') + self.assert_called('GET', '/os-floating-ips-bulk') + + def test_floating_ip_bulk_create(self): + self.run_command('floating-ip-bulk-create 10.0.0.1/24') + self.assert_called('POST', '/os-floating-ips-bulk', + {'floating_ips_bulk_create': + {'ip_range': '10.0.0.1/24'}}) + + def test_floating_ip_bulk_create_host_and_interface(self): + self.run_command('floating-ip-bulk-create 10.0.0.1/24 --pool testPool \ + --interface ethX') + self.assert_called('POST', '/os-floating-ips-bulk', + {'floating_ips_bulk_create': + {'ip_range': '10.0.0.1/24', + 'pool': 'testPool', 'interface': 'ethX'}}) + + def test_floating_ip_bulk_delete(self): + self.run_command('floating-ip-bulk-delete 10.0.0.1/24') + self.assert_called('PUT', '/os-floating-ips-bulk/delete', + {'ip_range': '10.0.0.1/24'}) + def test_usage_list(self): self.run_command('usage-list --start 2000-01-20 --end 2005-02-01') self.assert_called('GET', From 955b94c1f7b7c8a181312ef9d323878b068c641b Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Thu, 29 Nov 2012 14:14:44 -0500 Subject: [PATCH 0020/1705] Adds support for injected_file_path_bytes quota. Updates novaclient's quota and quota_class handlers to support the injected_file_path_bytes quota that is now in Nova. Fixes LP Bug #1084672. Change-Id: I2156816fd09bdd388e3acb8143b041e927f33511 --- novaclient/v1_1/quota_classes.py | 4 +++- novaclient/v1_1/quotas.py | 4 +++- novaclient/v1_1/shell.py | 13 ++++++++++++- tests/v1_1/fakes.py | 5 +++++ 4 files changed, 23 insertions(+), 3 deletions(-) diff --git a/novaclient/v1_1/quota_classes.py b/novaclient/v1_1/quota_classes.py index ae5b56308..e9fa77ca1 100644 --- a/novaclient/v1_1/quota_classes.py +++ b/novaclient/v1_1/quota_classes.py @@ -36,7 +36,8 @@ def get(self, class_name): "quota_class_set") def update(self, class_name, metadata_items=None, - injected_file_content_bytes=None, volumes=None, gigabytes=None, + injected_file_content_bytes=None, injected_file_path_bytes=None, + volumes=None, gigabytes=None, ram=None, floating_ips=None, instances=None, injected_files=None, cores=None): @@ -44,6 +45,7 @@ def update(self, class_name, metadata_items=None, 'class_name': class_name, 'metadata_items': metadata_items, 'injected_file_content_bytes': injected_file_content_bytes, + 'injected_file_path_bytes': injected_file_path_bytes, 'volumes': volumes, 'gigabytes': gigabytes, 'ram': ram, diff --git a/novaclient/v1_1/quotas.py b/novaclient/v1_1/quotas.py index 0c31611a3..bd36b20e8 100644 --- a/novaclient/v1_1/quotas.py +++ b/novaclient/v1_1/quotas.py @@ -37,7 +37,8 @@ def get(self, tenant_id): return self._get("/os-quota-sets/%s" % (tenant_id), "quota_set") def update(self, tenant_id, metadata_items=None, - injected_file_content_bytes=None, volumes=None, gigabytes=None, + injected_file_content_bytes=None, injected_file_path_bytes=None, + volumes=None, gigabytes=None, ram=None, floating_ips=None, instances=None, injected_files=None, cores=None): @@ -45,6 +46,7 @@ def update(self, tenant_id, metadata_items=None, 'tenant_id': tenant_id, 'metadata_items': metadata_items, 'injected_file_content_bytes': injected_file_content_bytes, + 'injected_file_path_bytes': injected_file_path_bytes, 'volumes': volumes, 'gigabytes': gigabytes, 'ram': ram, diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 7b5c1fd94..1b1270e73 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2221,7 +2221,8 @@ def do_ssh(cs, args): _quota_resources = ['instances', 'cores', 'ram', 'volumes', 'gigabytes', 'floating_ips', 'metadata_items', 'injected_files', - 'injected_file_content_bytes'] + 'injected_file_content_bytes', + 'injected_file_path_bytes'] def _quota_show(quotas): @@ -2326,6 +2327,11 @@ def do_quota_defaults(cs, args): @utils.arg('--injected_file_content_bytes', type=int, help=argparse.SUPPRESS) +@utils.arg('--injected-file-path-bytes', + metavar='', + type=int, + default=None, + help='New value for the "injected-file-path-bytes" quota.') def do_quota_update(cs, args): """Update the quotas for a tenant.""" @@ -2396,6 +2402,11 @@ def do_quota_class_show(cs, args): @utils.arg('--injected_file_content_bytes', type=int, help=argparse.SUPPRESS) +@utils.arg('--injected-file-path-bytes', + metavar='', + type=int, + default=None, + help='New value for the "injected-file-path-bytes" quota.') def do_quota_class_update(cs, args): """Update the quotas for a quota class.""" diff --git a/tests/v1_1/fakes.py b/tests/v1_1/fakes.py index 91f33e8fe..3b0b38f4e 100644 --- a/tests/v1_1/fakes.py +++ b/tests/v1_1/fakes.py @@ -708,6 +708,7 @@ def get_os_quota_sets_test(self, **kw): 'tenant_id': 'test', 'metadata_items': [], 'injected_file_content_bytes': 1, + 'injected_file_path_bytes': 1, 'volumes': 1, 'gigabytes': 1, 'ram': 1, @@ -721,6 +722,7 @@ def get_os_quota_sets_test_defaults(self): 'tenant_id': 'test', 'metadata_items': [], 'injected_file_content_bytes': 1, + 'injected_file_path_bytes': 1, 'volumes': 1, 'gigabytes': 1, 'ram': 1, @@ -737,6 +739,7 @@ def put_os_quota_sets_test(self, body, **kw): 'tenant_id': 'test', 'metadata_items': [], 'injected_file_content_bytes': 1, + 'injected_file_path_bytes': 1, 'volumes': 2, 'gigabytes': 1, 'ram': 1, @@ -754,6 +757,7 @@ def get_os_quota_class_sets_test(self, **kw): 'class_name': 'test', 'metadata_items': [], 'injected_file_content_bytes': 1, + 'injected_file_path_bytes': 1, 'volumes': 1, 'gigabytes': 1, 'ram': 1, @@ -770,6 +774,7 @@ def put_os_quota_class_sets_test(self, body, **kw): 'class_name': 'test', 'metadata_items': [], 'injected_file_content_bytes': 1, + 'injected_file_path_bytes': 1, 'volumes': 2, 'gigabytes': 1, 'ram': 1, From f279433e53fd09ff1574b1c2204da08f36a1c694 Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Thu, 29 Nov 2012 14:29:33 -0500 Subject: [PATCH 0021/1705] Adds support for key_pairs quota. Updates novaclient's quota and quota_class handlers to support the key_pairs quota that is now in Nova. Fixes LP Bug #1084674. Change-Id: I0eb357a5f7d5dba73f002066ea381cec818f1492 --- novaclient/v1_1/quota_classes.py | 3 ++- novaclient/v1_1/quotas.py | 3 ++- novaclient/v1_1/shell.py | 12 +++++++++++- tests/v1_1/fakes.py | 15 ++++++++++----- 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/novaclient/v1_1/quota_classes.py b/novaclient/v1_1/quota_classes.py index e9fa77ca1..7c16ec016 100644 --- a/novaclient/v1_1/quota_classes.py +++ b/novaclient/v1_1/quota_classes.py @@ -39,11 +39,12 @@ def update(self, class_name, metadata_items=None, injected_file_content_bytes=None, injected_file_path_bytes=None, volumes=None, gigabytes=None, ram=None, floating_ips=None, instances=None, - injected_files=None, cores=None): + injected_files=None, cores=None, key_pairs=None): body = {'quota_class_set': { 'class_name': class_name, 'metadata_items': metadata_items, + 'key_pairs': key_pairs, 'injected_file_content_bytes': injected_file_content_bytes, 'injected_file_path_bytes': injected_file_path_bytes, 'volumes': volumes, diff --git a/novaclient/v1_1/quotas.py b/novaclient/v1_1/quotas.py index bd36b20e8..20fc5c36e 100644 --- a/novaclient/v1_1/quotas.py +++ b/novaclient/v1_1/quotas.py @@ -40,11 +40,12 @@ def update(self, tenant_id, metadata_items=None, injected_file_content_bytes=None, injected_file_path_bytes=None, volumes=None, gigabytes=None, ram=None, floating_ips=None, instances=None, - injected_files=None, cores=None): + injected_files=None, cores=None, key_pairs=None): body = {'quota_set': { 'tenant_id': tenant_id, 'metadata_items': metadata_items, + 'key_pairs': key_pairs, 'injected_file_content_bytes': injected_file_content_bytes, 'injected_file_path_bytes': injected_file_path_bytes, 'volumes': volumes, diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 1b1270e73..f5582829c 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2221,7 +2221,7 @@ def do_ssh(cs, args): _quota_resources = ['instances', 'cores', 'ram', 'volumes', 'gigabytes', 'floating_ips', 'metadata_items', 'injected_files', - 'injected_file_content_bytes', + 'key_pairs', 'injected_file_content_bytes', 'injected_file_path_bytes'] @@ -2332,6 +2332,11 @@ def do_quota_defaults(cs, args): type=int, default=None, help='New value for the "injected-file-path-bytes" quota.') +@utils.arg('--key-pairs', + metavar='', + type=int, + default=None, + help='New value for the "key-pairs" quota.') def do_quota_update(cs, args): """Update the quotas for a tenant.""" @@ -2407,6 +2412,11 @@ def do_quota_class_show(cs, args): type=int, default=None, help='New value for the "injected-file-path-bytes" quota.') +@utils.arg('--key-pairs', + metavar='', + type=int, + default=None, + help='New value for the "key-pairs" quota.') def do_quota_class_update(cs, args): """Update the quotas for a quota class.""" diff --git a/tests/v1_1/fakes.py b/tests/v1_1/fakes.py index 3b0b38f4e..ae68f52ae 100644 --- a/tests/v1_1/fakes.py +++ b/tests/v1_1/fakes.py @@ -715,7 +715,8 @@ def get_os_quota_sets_test(self, **kw): 'floating_ips': 1, 'instances': 1, 'injected_files': 1, - 'cores': 1}}) + 'cores': 1, + 'keypairs': 1}}) def get_os_quota_sets_test_defaults(self): return (200, {'quota_set': { @@ -729,7 +730,8 @@ def get_os_quota_sets_test_defaults(self): 'floating_ips': 1, 'instances': 1, 'injected_files': 1, - 'cores': 1}}) + 'cores': 1, + 'keypairs': 1}}) def put_os_quota_sets_test(self, body, **kw): assert body.keys() == ['quota_set'] @@ -746,7 +748,8 @@ def put_os_quota_sets_test(self, body, **kw): 'floating_ips': 1, 'instances': 1, 'injected_files': 1, - 'cores': 1}}) + 'cores': 1, + 'keypairs': 1}}) # # Quota Classes @@ -764,7 +767,8 @@ def get_os_quota_class_sets_test(self, **kw): 'floating_ips': 1, 'instances': 1, 'injected_files': 1, - 'cores': 1}}) + 'cores': 1, + 'keypairs': 1}}) def put_os_quota_class_sets_test(self, body, **kw): assert body.keys() == ['quota_class_set'] @@ -781,7 +785,8 @@ def put_os_quota_class_sets_test(self, body, **kw): 'floating_ips': 1, 'instances': 1, 'injected_files': 1, - 'cores': 1}}) + 'cores': 1, + 'keypairs': 1}}) # # Security Groups From 99647e23a2e393bc7c176dd8b8f0f3f76ebb5103 Mon Sep 17 00:00:00 2001 From: Unmesh Gurjar Date: Thu, 6 Dec 2012 05:34:00 -0800 Subject: [PATCH 0022/1705] Fixed nics param ignored when bdm is specified 1. Fixed the issue of 'nics' parameter getting ignored in boot command when block_device_mapping is specified. 2. Added unit test coverage. Fixes LP: #1004572 Change-Id: I1b720b8527406ee664f084167426f778793a436f --- novaclient/v1_1/servers.py | 1 + tests/v1_1/test_servers.py | 34 ++++++++++++++++++++++++++++++++++ tests/v1_1/test_shell.py | 4 ---- 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index bdc509cfe..5411b87dc 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -502,6 +502,7 @@ def create(self, name, image, flavor, meta=None, files=None, boot_kwargs['block_device_mapping'] = block_device_mapping else: resource_url = "/servers" + if nics: boot_kwargs['nics'] = nics response_key = "server" diff --git a/tests/v1_1/test_servers.py b/tests/v1_1/test_servers.py index fba066115..f1f2f88db 100644 --- a/tests/v1_1/test_servers.py +++ b/tests/v1_1/test_servers.py @@ -2,6 +2,8 @@ import StringIO +import mock + from novaclient import exceptions from novaclient.v1_1 import servers from tests import utils @@ -46,6 +48,38 @@ def test_create_server(self): cs.assert_called('POST', '/servers') self.assertTrue(isinstance(s, servers.Server)) + def test_create_server_boot_from_volume_with_nics(self): + old_boot = cs.servers._boot + + nics = [{'net-id': '11111111-1111-1111-1111-111111111111', + 'v4-fixed-ip': '10.0.0.7'}] + bdm = {"volume_size": "1", + "volume_id": "11111111-1111-1111-1111-111111111111", + "delete_on_termination": "0", + "device_name": "vda"} + + def wrapped_boot(url, key, *boot_args, **boot_kwargs): + self.assertEqual(boot_kwargs['block_device_mapping'], bdm) + self.assertEqual(boot_kwargs['nics'], nics) + return old_boot(url, key, *boot_args, **boot_kwargs) + + @mock.patch.object(cs.servers, '_boot', wrapped_boot) + def test_create_server_from_volume(): + s = cs.servers.create( + name="My server", + image=1, + flavor=1, + meta={'foo': 'bar'}, + userdata="hello moto", + key_name="fakekey", + block_device_mapping=bdm, + nics=nics + ) + cs.assert_called('POST', '/os-volumes_boot') + self.assertTrue(isinstance(s, servers.Server)) + + test_create_server_from_volume() + def test_create_server_userdata_file_object(self): s = cs.servers.create( name="My server", diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index 206fc8c22..bfb341f49 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -82,7 +82,6 @@ def test_boot(self): 'imageRef': '1', 'min_count': 1, 'max_count': 1, - 'networks': [], }}, ) @@ -125,7 +124,6 @@ def test_boot_metadata(self): 'metadata': {'foo': 'bar=pants', 'spam': 'eggs'}, 'min_count': 1, 'max_count': 1, - 'networks': [], }}, ) @@ -140,7 +138,6 @@ def test_boot_hints(self): 'imageRef': '1', 'min_count': 1, 'max_count': 1, - 'networks': [], }, 'os:scheduler_hints': {'a': 'b=c'}, }, @@ -182,7 +179,6 @@ def test_boot_files(self): 'imageRef': '1', 'min_count': 1, 'max_count': 1, - 'networks': [], 'personality': [ {'path': '/tmp/bar', 'contents': expected_file_data}, {'path': '/tmp/foo', 'contents': expected_file_data}, From b6e14300ca5f78633d51b9f3edc297efb12abc58 Mon Sep 17 00:00:00 2001 From: Akihiro MOTOKI Date: Sat, 8 Dec 2012 03:25:21 +0900 Subject: [PATCH 0023/1705] Fix a wrong substition for '-h' in bash completion Fix bug #1087808 Change-Id: I4b90e892997105027076428c5f3dd8c42b682cb2 --- tools/nova.bash_completion | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/nova.bash_completion b/tools/nova.bash_completion index a552de87f..7dee3e6d7 100644 --- a/tools/nova.bash_completion +++ b/tools/nova.bash_completion @@ -9,7 +9,7 @@ _nova() prev="${COMP_WORDS[COMP_CWORD-1]}" if [ "x$_nova_opts" == "x" ] ; then - nbc="`nova bash-completion | sed -e "s/\s-h\s/\s/"`" + nbc="`nova bash-completion | sed -e "s/\s-h\s/ /"`" _nova_opts="`echo "$nbc" | sed -e "s/--[a-z0-9_-]*//g" -e "s/\s\s*/ /g"`" _nova_flags="`echo " $nbc" | sed -e "s/ [^-][^-][a-z0-9_-]*//g" -e "s/\s\s*/ /g"`" _nova_opts_exp="`echo "$_nova_opts" | sed -e "s/\s/|/g"`" From 2dc70a17a45f2e9d2a81f6de1c8c3a9da6f1563f Mon Sep 17 00:00:00 2001 From: Ken'ichi Ohmichi Date: Mon, 10 Dec 2012 09:39:32 +0000 Subject: [PATCH 0024/1705] Fix argument checking method for 'nova list --flavor' command. According to Vish's comment for patchese 1, flavor-id is uuid/int and it is a problem that uuid cannot be specified in 'nova list --flavor'. This patch fixes the problem. Fixes bug 1076818 Change-Id: I7d216efb59c7f567067ca19b695c7f93de009bd6 --- novaclient/v1_1/shell.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 7b5c1fd94..587be3105 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -380,7 +380,7 @@ def do_flavor_show(cs, args): help="Name of the new flavor") @utils.arg('id', metavar='', - help="Unique integer ID for the new flavor") + help="Unique ID (integer or UUID) for the new flavor") @utils.arg('ram', metavar='', help="Memory size in MB") @@ -657,7 +657,6 @@ def do_image_delete(cs, args): @utils.arg('--flavor', dest='flavor', metavar='', - type=int, default=None, help='Search by flavor ID') @utils.arg('--image', From 68e98fbcb6bf859b4934b3419f3679f615cae495 Mon Sep 17 00:00:00 2001 From: Yaguang Tang Date: Mon, 10 Dec 2012 20:35:14 +0800 Subject: [PATCH 0025/1705] add host-update help info param. fix bug 1084445 Change-Id: I042927a4abf73636d592cd5a3379a6d575f761a4 --- novaclient/v1_1/shell.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 7b5c1fd94..d1259ae33 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2032,10 +2032,10 @@ def do_host_list(cs, args): @utils.arg('host', metavar='', help='Name of host.') -@utils.arg('--status', metavar='', default=None, dest='status', +@utils.arg('--status', metavar='', default=None, dest='status', help='Either enable or disable a host.') @utils.arg('--maintenance', - metavar='', + metavar='', default=None, dest='maintenance', help='Either put or resume host to/from maintenance.') From e68b834fcfb0beb278e95f6bf5fdfacc3e7c1e1d Mon Sep 17 00:00:00 2001 From: Chris Yeoh Date: Mon, 26 Nov 2012 13:00:23 +1030 Subject: [PATCH 0026/1705] Adds nova client support for nova-manage network command Adds the following commands: - nova network-disassociate - nova network-associate-host - nova network-associate-project - nova network-create This patch depends on https://review.openstack.org/#/c/15491/ Change-Id: I44c7d0bf6ba545cf1ed6f6f8390bab836fe52f9d Implements: blueprint apis-for-nova-manage --- novaclient/v1_1/networks.py | 41 ++++++++++-- novaclient/v1_1/shell.py | 128 ++++++++++++++++++++++++++++++++++++ tests/v1_1/fakes.py | 12 ++++ tests/v1_1/test_networks.py | 19 ++++++ tests/v1_1/test_shell.py | 44 +++++++++++++ 5 files changed, 240 insertions(+), 4 deletions(-) diff --git a/novaclient/v1_1/networks.py b/novaclient/v1_1/networks.py index 54a2e3995..1d9496d40 100644 --- a/novaclient/v1_1/networks.py +++ b/novaclient/v1_1/networks.py @@ -91,14 +91,47 @@ def create(self, **kwargs): body = {"network": kwargs} return self._create('/os-networks', body, 'network') - def disassociate(self, network): + def disassociate(self, network, disassociate_host=True, + disassociate_project=True): """ - Disassociate a specific network from project. + Disassociate a specific network from project and/or host. - :param network: The ID of the :class:`Network` to get. + :param network: The ID of the :class:`Network`. + :param disassociate_host: Whether to disassociate the host + :param disassociate_project: Whether to disassociate the project + """ + if disassociate_host and disassociate_project: + body = {"disassociate": None} + elif disassociate_project: + body = {"disassociate_project": None} + elif disassociate_host: + body = {"disassociate_host": None} + else: + raise CommandError( + "Must disassociate either host or project or both") + + self.api.client.post("/os-networks/%s/action" % base.getid(network), + body=body) + + def associate_host(self, network, host): + """ + Associate a specific network with a host. + + :param network: The ID of the :class:`Network`. + :param host: The name of the host to associate the network with """ self.api.client.post("/os-networks/%s/action" % base.getid(network), - body={"disassociate": None}) + body={"associate_host": host}) + + def associate_project(self, network): + """ + Associate a specific network with a project. + + The project is defined by the project authenticated against + + :param network: The ID of the :class:`Network`. + """ + self.api.client.post("/os-networks/add", body={"id": network}) def add(self, network=None): """ diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 7b5c1fd94..ad9053469 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -512,6 +512,134 @@ def do_network_show(cs, args): utils.print_dict(network._info) +@utils.arg('--host-only', + dest='host_only', + metavar='<0|1>', + nargs='?', + type=int, + const=1, + default=0) +@utils.arg('--project-only', + dest='project_only', + metavar='<0|1>', + nargs='?', + type=int, + const=1, + default=0) +@utils.arg('network', + metavar='', + help="uuid of network") +def do_network_disassociate(cs, args): + """Disassociate host and/or project from the given network.""" + if args.host_only: + cs.networks.disassociate(args.network, True, False) + elif args.project_only: + cs.networks.disassociate(args.network, False, True) + else: + cs.networks.disassociate(args.network, True, True) + + +@utils.arg('network', + metavar='', + help="uuid of network") +@utils.arg('host', + metavar='', + help="Name of host") +def do_network_associate_host(cs, args): + """Associate host with network.""" + cs.networks.associate_host(args.network, args.host) + + +@utils.arg('network', + metavar='', + help="uuid of network") +def do_network_associate_project(cs, args): + """Associate project with network.""" + cs.networks.associate_project(args.network) + + +def _filter_network_create_options(args): + valid_args = ['label', 'cidr', 'vlan', 'vpn_start', 'cidr_v6', 'gateway', + 'gateway_v6', 'bridge', 'multi_host', 'dns1', 'dns2', 'uuid', + 'fixed_cidr', 'project_id', 'priority'] + kwargs = {} + for k, v in args.__dict__.items(): + if k in valid_args and v is not None: + kwargs[k] = v + + return kwargs + + +@utils.arg('label', + metavar='', + help="Label for network") +@utils.arg('--fixed-range-v4', + dest='cidr', + metavar='', + help="IPv4 subnet (ex: 10.0.0.0/8)") +@utils.arg('--fixed-range-v6', + dest="cidr_v6", + help='IPv6 subnet (ex: fe80::/64') +@utils.arg('--vlan', + dest='vlan', + metavar='', + help="vlan id") +@utils.arg('--vpn', + dest='vpn_start', + metavar='', + help="vpn start") +@utils.arg('--gateway', + dest="gateway", + help='gateway') +@utils.arg('--gateway-v6', + dest="gateway_v6", + help='ipv6 gateway') +@utils.arg('--bridge', + dest="bridge", + metavar='', + help='VIFs on this network are connected to this bridge') +@utils.arg('--bridge-interface', + dest="bridge_interface", + metavar='', + help='the bridge is connected to this interface') +@utils.arg('--multi-host', + dest="multi_host", + metavar="<'T'|'F'>", + help='Multi host') +@utils.arg('--dns1', + dest="dns1", + metavar="", help='First DNS') +@utils.arg('--dns2', + dest="dns2", + metavar="", + help='Second DNS') +@utils.arg('--uuid', + dest="uuid", + metavar="", + help='Network UUID') +@utils.arg('--fixed-cidr', + dest="fixed_cidr", + metavar='', + help='IPv4 subnet for fixed IPS (ex: 10.20.0.0/16)') +@utils.arg('--project-id', + dest="project_id", + metavar="", + help='Project id') +@utils.arg('--priority', + dest="priority", + metavar="", + help='Network interface priority') +def do_network_create(cs, args): + """Create a network.""" + + if not (args.cidr or args.cidr_v6): + raise exceptions.CommandError( + "Must specify eith fixed_range_v4 or fixed_range_v6") + kwargs = _filter_network_create_options(args) + + cs.networks.create(**kwargs) + + def do_image_list(cs, _args): """Print a list of available images to boot from.""" image_list = cs.images.list() diff --git a/tests/v1_1/fakes.py b/tests/v1_1/fakes.py index 91f33e8fe..5d64a04c9 100644 --- a/tests/v1_1/fakes.py +++ b/tests/v1_1/fakes.py @@ -1164,3 +1164,15 @@ def get_os_fping_1(self, **kw): } } ) + + def post_os_networks(self, **kw): + return (202, {'network': kw}) + + def post_os_networks_1_action(self, **kw): + return (202, None) + + def post_os_networks_networktest_action(self, **kw): + return (202, None) + + def post_os_networks_2_action(self, **kw): + return (202, None) diff --git a/tests/v1_1/test_networks.py b/tests/v1_1/test_networks.py index 65d6b6d9f..d0af5471a 100644 --- a/tests/v1_1/test_networks.py +++ b/tests/v1_1/test_networks.py @@ -29,11 +29,30 @@ def test_create(self): {'network': {'label': 'foo'}}) self.assertTrue(isinstance(f, networks.Network)) + def test_associate_project(self): + cs.networks.associate_project('networktest') + cs.assert_called('POST', '/os-networks/add', {'id': 'networktest'}) + + def test_associate_host(self): + cs.networks.associate_host('networktest', 'testHost') + cs.assert_called('POST', '/os-networks/networktest/action', + {'associate_host': 'testHost'}) + def test_disassociate(self): cs.networks.disassociate('networkdisassociate') cs.assert_called('POST', '/os-networks/networkdisassociate/action', {'disassociate': None}) + def test_disassociate_host_only(self): + cs.networks.disassociate('networkdisassociate', True, False) + cs.assert_called('POST', '/os-networks/networkdisassociate/action', + {'disassociate_host': None}) + + def test_disassociate_project(self): + cs.networks.disassociate('networkdisassociate', False, True) + cs.assert_called('POST', '/os-networks/networkdisassociate/action', + {'disassociate_project': None}) + def test_add(self): cs.networks.add('networkadd') cs.assert_called('POST', '/os-networks/add', diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index 206fc8c22..34b509e0b 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -629,6 +629,50 @@ def test_cloudpipe_configure(self): 'vpn_port': '1234'}} self.assert_called('PUT', '/os-cloudpipe/configure-project', body) + def test_network_associate_host(self): + self.run_command('network-associate-host 1 testHost') + body = {'associate_host': 'testHost'} + self.assert_called('POST', '/os-networks/1/action', body) + + def test_network_associate_project(self): + self.run_command('network-associate-project 1') + body = {'id': "1"} + self.assert_called('POST', '/os-networks/add', body) + + def test_network_disassociate(self): + self.run_command('network-disassociate 1') + body = {'disassociate': None} + self.assert_called('POST', '/os-networks/1/action', body) + + def test_network_disassociate_host(self): + self.run_command('network-disassociate --host-only 1 2') + body = {'disassociate_host': None} + self.assert_called('POST', '/os-networks/2/action', body) + + def test_network_disassociate(self): + self.run_command('network-disassociate --project-only 1 2') + body = {'disassociate_project': None} + self.assert_called('POST', '/os-networks/2/action', body) + + def test_network_create_v4(self): + self.run_command('network-create --fixed-range-v4 10.0.1.0/24 \ + new_network') + body = {'cidr': '10.0.1.0/24', 'label': 'new_network'} + self.assert_called('POST', '/os-networks', body) + + def test_network_create_v4(self): + self.run_command('network-create --fixed-range-v4 10.0.1.0/24 \ + --dns1 10.0.1.254 new_network') + body = {'network': {'cidr': '10.0.1.0/24', 'label': 'new_network', + 'dns1': '10.0.1.254'}} + self.assert_called('POST', '/os-networks', body) + + def test_network_create_v6(self): + self.run_command('network-create --fixed-range-v6 2001::/64 \ + new_network') + body = {'network': {'cidr_v6': '2001::/64', 'label': 'new_network'}} + self.assert_called('POST', '/os-networks', body) + def test_backup(self): self.run_command('backup sample-server back1 daily 1') self.assert_called('POST', '/servers/1234/action', From cf70a2d6f6d950b872a7f47ec5350b01121e5c9f Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Thu, 29 Nov 2012 14:40:48 -0500 Subject: [PATCH 0027/1705] Adds support for security group/rules quotas. Updates novaclient's quota and quota_class handlers to support the security_groups and security_group_rules quotas that are now in Nova. Fixes LP Bug #1084682. Change-Id: I04d90681d535124d7d497e06e8c1ea4f2cb8f4f4 --- novaclient/v1_1/quota_classes.py | 7 +++++-- novaclient/v1_1/quotas.py | 7 +++++-- novaclient/v1_1/shell.py | 23 ++++++++++++++++++++++- tests/v1_1/fakes.py | 20 +++++++++++++++----- 4 files changed, 47 insertions(+), 10 deletions(-) diff --git a/novaclient/v1_1/quota_classes.py b/novaclient/v1_1/quota_classes.py index 7c16ec016..3b52d64f4 100644 --- a/novaclient/v1_1/quota_classes.py +++ b/novaclient/v1_1/quota_classes.py @@ -39,7 +39,8 @@ def update(self, class_name, metadata_items=None, injected_file_content_bytes=None, injected_file_path_bytes=None, volumes=None, gigabytes=None, ram=None, floating_ips=None, instances=None, - injected_files=None, cores=None, key_pairs=None): + injected_files=None, cores=None, key_pairs=None, + security_groups=None, security_group_rules=None): body = {'quota_class_set': { 'class_name': class_name, @@ -53,7 +54,9 @@ def update(self, class_name, metadata_items=None, 'floating_ips': floating_ips, 'instances': instances, 'injected_files': injected_files, - 'cores': cores}} + 'cores': cores, + 'security_groups': security_groups, + 'security_group_rules': security_group_rules}} for key in body['quota_class_set'].keys(): if body['quota_class_set'][key] is None: diff --git a/novaclient/v1_1/quotas.py b/novaclient/v1_1/quotas.py index 20fc5c36e..cc2556384 100644 --- a/novaclient/v1_1/quotas.py +++ b/novaclient/v1_1/quotas.py @@ -40,7 +40,8 @@ def update(self, tenant_id, metadata_items=None, injected_file_content_bytes=None, injected_file_path_bytes=None, volumes=None, gigabytes=None, ram=None, floating_ips=None, instances=None, - injected_files=None, cores=None, key_pairs=None): + injected_files=None, cores=None, key_pairs=None, + security_groups=None, security_group_rules=None): body = {'quota_set': { 'tenant_id': tenant_id, @@ -54,7 +55,9 @@ def update(self, tenant_id, metadata_items=None, 'floating_ips': floating_ips, 'instances': instances, 'injected_files': injected_files, - 'cores': cores}} + 'cores': cores, + 'security_groups': security_groups, + 'security_group_rules': security_group_rules}} for key in body['quota_set'].keys(): if body['quota_set'][key] is None: diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index c2cddad4e..632902b7e 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2394,7 +2394,8 @@ def do_ssh(cs, args): _quota_resources = ['instances', 'cores', 'ram', 'volumes', 'gigabytes', 'floating_ips', 'metadata_items', 'injected_files', 'key_pairs', 'injected_file_content_bytes', - 'injected_file_path_bytes'] + 'injected_file_path_bytes', 'security_groups', + 'security_group_rules'] def _quota_show(quotas): @@ -2509,6 +2510,16 @@ def do_quota_defaults(cs, args): type=int, default=None, help='New value for the "key-pairs" quota.') +@utils.arg('--security-groups', + metavar='', + type=int, + default=None, + help='New value for the "security-groups" quota.') +@utils.arg('--security-group-rules', + metavar='', + type=int, + default=None, + help='New value for the "security-group-rules" quota.') def do_quota_update(cs, args): """Update the quotas for a tenant.""" @@ -2589,6 +2600,16 @@ def do_quota_class_show(cs, args): type=int, default=None, help='New value for the "key-pairs" quota.') +@utils.arg('--security-groups', + metavar='', + type=int, + default=None, + help='New value for the "security-groups" quota.') +@utils.arg('--security-group-rules', + metavar='', + type=int, + default=None, + help='New value for the "security-group-rules" quota.') def do_quota_class_update(cs, args): """Update the quotas for a quota class.""" diff --git a/tests/v1_1/fakes.py b/tests/v1_1/fakes.py index cbe0894e3..1f97d3ea2 100644 --- a/tests/v1_1/fakes.py +++ b/tests/v1_1/fakes.py @@ -741,7 +741,9 @@ def get_os_quota_sets_test(self, **kw): 'instances': 1, 'injected_files': 1, 'cores': 1, - 'keypairs': 1}}) + 'keypairs': 1, + 'security_groups': 1, + 'security_group_rules': 1}}) def get_os_quota_sets_test_defaults(self): return (200, {'quota_set': { @@ -756,7 +758,9 @@ def get_os_quota_sets_test_defaults(self): 'instances': 1, 'injected_files': 1, 'cores': 1, - 'keypairs': 1}}) + 'keypairs': 1, + 'security_groups': 1, + 'security_group_rules': 1}}) def put_os_quota_sets_test(self, body, **kw): assert body.keys() == ['quota_set'] @@ -774,7 +778,9 @@ def put_os_quota_sets_test(self, body, **kw): 'instances': 1, 'injected_files': 1, 'cores': 1, - 'keypairs': 1}}) + 'keypairs': 1, + 'security_groups': 1, + 'security_group_rules': 1}}) # # Quota Classes @@ -793,7 +799,9 @@ def get_os_quota_class_sets_test(self, **kw): 'instances': 1, 'injected_files': 1, 'cores': 1, - 'keypairs': 1}}) + 'keypairs': 1, + 'security_groups': 1, + 'security_group_rules': 1}}) def put_os_quota_class_sets_test(self, body, **kw): assert body.keys() == ['quota_class_set'] @@ -811,7 +819,9 @@ def put_os_quota_class_sets_test(self, body, **kw): 'instances': 1, 'injected_files': 1, 'cores': 1, - 'keypairs': 1}}) + 'keypairs': 1, + 'security_groups': 1, + 'security_group_rules': 1}}) # # Security Groups From e483455a1281412a8b38a483a3f167a2fd27f5d9 Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Fri, 7 Dec 2012 11:47:49 -0500 Subject: [PATCH 0028/1705] Adds --os-cache to replace old --no-cache. Deprecates the old --no-cache option in favor of --os-cache. The old CLI args (--no_cache and --no-cache) and ENV option (OS_NO_CACHE) are still supported but no longer show up in help. The new option for --os-cache can also be set via the OS_CACHE ENV variable... which now defaults to False. This should be much more user friendly. Fixes LP Bug #1087776. Change-Id: I3cea089c7e11ce75f22c2d7f3242b02b80441323 --- novaclient/client.py | 12 ++++++------ novaclient/shell.py | 20 ++++++++++++++------ novaclient/v1_1/client.py | 7 ++++--- tests/test_auth_plugins.py | 12 ++++-------- tests/test_client.py | 24 ++++++++++++++++++++++++ tests/v1_1/test_auth.py | 23 ++++++++--------------- 6 files changed, 60 insertions(+), 38 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index f66dcfe5c..9bd838742 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -86,7 +86,8 @@ def __init__(self, user, password, projectid, auth_url=None, proxy_token=None, region_name=None, endpoint_type='publicURL', service_type=None, service_name=None, volume_service_name=None, - timings=False, bypass_url=None, no_cache=False, + timings=False, bypass_url=None, + os_cache=False, no_cache=True, http_log_debug=False, auth_system='keystone'): super(HTTPClient, self).__init__(timeout=timeout, proxy_info=_get_proxy_info()) @@ -106,7 +107,7 @@ def __init__(self, user, password, projectid, auth_url=None, self.volume_service_name = volume_service_name self.timings = timings self.bypass_url = bypass_url - self.no_cache = no_cache + self.os_cache = os_cache or not no_cache self.http_log_debug = http_log_debug self.times = [] # [("item", starttime, endtime), ...] @@ -130,8 +131,7 @@ def __init__(self, user, password, projectid, auth_url=None, self._logger.addHandler(ch) def use_token_cache(self, use_it): - # One day I'll stop using negative naming. - self.no_cache = not use_it + self.os_cache = use_it def unauthenticate(self): """Forget all of our authentication information.""" @@ -316,7 +316,7 @@ def authenticate(self): if key is None: keys[index] = '?' keyring_key = "/".join(keys) - if not self.no_cache and not self.used_keyring: + if self.os_cache and not self.used_keyring: # Lookup the token/mgmt url from the keyring first time # through. # If we come through again, it's because the old token @@ -388,7 +388,7 @@ def authenticate(self): self.set_management_url(self.bypass_url) # Store the token/mgmt url in the keyring for later requests. - if has_keyring and not self.no_cache: + if has_keyring and self.os_cache: try: keyring_value = "%s|%s" % (self.auth_token, self.management_url) diff --git a/novaclient/shell.py b/novaclient/shell.py index c15893f0e..98df2c0ab 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -93,12 +93,20 @@ def get_base_parser(self): help="Print debugging output") parser.add_argument('--no-cache', - default=utils.env('OS_NO_CACHE', default=False), - action='store_true', - help="Don't use the auth token cache.") + default=utils.env('OS_NO_CACHE', default=True), + action='store_false', + dest='os_cache', + help=argparse.SUPPRESS) parser.add_argument('--no_cache', + action='store_false', + dest='os_cache', help=argparse.SUPPRESS) + parser.add_argument('--os-cache', + default=utils.env('OS_CACHE', default=False), + action='store_true', + help="Use the auth token cache.") + parser.add_argument('--timings', default=False, action='store_true', @@ -384,7 +392,7 @@ def main(self, argv): os_region_name, os_auth_system, endpoint_type, insecure, service_type, service_name, volume_service_name, username, apikey, projectid, url, region_name, - bypass_url, no_cache) = ( + bypass_url, os_cache) = ( args.os_username, args.os_password, args.os_tenant_name, args.os_auth_url, args.os_region_name, args.os_auth_system, @@ -392,7 +400,7 @@ def main(self, argv): args.service_name, args.volume_service_name, args.username, args.apikey, args.projectid, args.url, args.region_name, - args.bypass_url, args.no_cache) + args.bypass_url, args.os_cache) if not endpoint_type: endpoint_type = DEFAULT_NOVA_ENDPOINT_TYPE @@ -463,7 +471,7 @@ def main(self, argv): service_name=service_name, auth_system=os_auth_system, volume_service_name=volume_service_name, timings=args.timings, bypass_url=bypass_url, - no_cache=no_cache, http_log_debug=options.debug) + os_cache=os_cache, http_log_debug=options.debug) try: if not utils.isunauthenticated(args.func): diff --git a/novaclient/v1_1/client.py b/novaclient/v1_1/client.py index d9e8720ca..6b2d3d3fa 100644 --- a/novaclient/v1_1/client.py +++ b/novaclient/v1_1/client.py @@ -53,8 +53,8 @@ def __init__(self, username, api_key, project_id, auth_url=None, endpoint_type='publicURL', extensions=None, service_type='compute', service_name=None, volume_service_name=None, timings=False, - bypass_url=None, no_cache=False, http_log_debug=False, - auth_system='keystone'): + bypass_url=None, os_cache=False, no_cache=True, + http_log_debug=False, auth_system='keystone'): # FIXME(comstud): Rename the api_key argument above when we # know it's not being used as keyword argument password = api_key @@ -92,6 +92,7 @@ def __init__(self, username, api_key, project_id, auth_url=None, self.services = services.ServiceManager(self) self.fixed_ips = fixed_ips.FixedIPsManager(self) self.floating_ips_bulk = floating_ips_bulk.FloatingIPBulkManager(self) + self.os_cache = os_cache or not no_cache # Add in any extensions... if extensions: @@ -116,7 +117,7 @@ def __init__(self, username, api_key, project_id, auth_url=None, volume_service_name=volume_service_name, timings=timings, bypass_url=bypass_url, - no_cache=no_cache, + os_cache=self.os_cache, http_log_debug=http_log_debug) def set_management_url(self, url): diff --git a/tests/test_auth_plugins.py b/tests/test_auth_plugins.py index 8a3a53238..c20ea62de 100644 --- a/tests/test_auth_plugins.py +++ b/tests/test_auth_plugins.py @@ -89,8 +89,7 @@ def mock_iter_entry_points(_type): @mock.patch.object(httplib2.Http, "request", mock_request) def test_auth_call(): cs = client.Client("username", "password", "project_id", - "auth_url/v2.0", auth_system="fake", - no_cache=True) + "auth_url/v2.0", auth_system="fake") cs.client.authenticate() headers = requested_headers(cs) @@ -113,8 +112,7 @@ def mock_iter_entry_points(_t): @mock.patch.object(httplib2.Http, "request", mock_request) def test_auth_call(): cs = client.Client("username", "password", "project_id", - "auth_url/v2.0", auth_system="notexists", - no_cache=True) + "auth_url/v2.0", auth_system="notexists") self.assertRaises(exceptions.AuthSystemNotFound, cs.client.authenticate) @@ -151,8 +149,7 @@ def mock_iter_entry_points(_type): @mock.patch.object(httplib2.Http, "request", mock_request) def test_auth_call(): cs = client.Client("username", "password", "project_id", - auth_system="fakewithauthurl", - no_cache=True) + auth_system="fakewithauthurl") cs.client.authenticate() self.assertEquals(cs.client.auth_url, "http://faked/v2.0") @@ -176,7 +173,6 @@ def mock_iter_entry_points(_type): def test_auth_call(): with self.assertRaises(exceptions.EndpointNotFound): cs = client.Client("username", "password", "project_id", - auth_system="fakewithauthurl", - no_cache=True) + auth_system="fakewithauthurl") test_auth_call() diff --git a/tests/test_client.py b/tests/test_client.py index a1a454655..e2fc9d965 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -24,3 +24,27 @@ def test_get_client_class_v1_1(self): def test_get_client_class_unknown(self): self.assertRaises(novaclient.exceptions.UnsupportedVersion, novaclient.client.get_client_class, '0') + + def test_client_with_os_cache_enabled(self): + cs = novaclient.v1_1.client.Client("user", "password", "project_id", + auth_url="foo/v2", os_cache=True) + self.assertEqual(True, cs.os_cache) + self.assertEqual(True, cs.client.os_cache) + + def test_client_with_os_cache_disabled(self): + cs = novaclient.v1_1.client.Client("user", "password", "project_id", + auth_url="foo/v2", os_cache=False) + self.assertEqual(False, cs.os_cache) + self.assertEqual(False, cs.client.os_cache) + + def test_client_with_no_cache_enabled(self): + cs = novaclient.v1_1.client.Client("user", "password", "project_id", + auth_url="foo/v2", no_cache=True) + self.assertEqual(False, cs.os_cache) + self.assertEqual(False, cs.client.os_cache) + + def test_client_with_no_cache_disabled(self): + cs = novaclient.v1_1.client.Client("user", "password", "project_id", + auth_url="foo/v2", no_cache=False) + self.assertEqual(True, cs.os_cache) + self.assertEqual(True, cs.client.os_cache) diff --git a/tests/v1_1/test_auth.py b/tests/v1_1/test_auth.py index f1ae081f8..5f312d637 100644 --- a/tests/v1_1/test_auth.py +++ b/tests/v1_1/test_auth.py @@ -18,8 +18,7 @@ def to_http_response(resp_dict): class AuthenticateAgainstKeystoneTests(utils.TestCase): def test_authenticate_success(self): cs = client.Client("username", "password", "project_id", - "auth_url/v2.0", service_type='compute', - no_cache=True) + "auth_url/v2.0", service_type='compute') resp = { "access": { "token": { @@ -82,7 +81,7 @@ def test_auth_call(): def test_authenticate_failure(self): cs = client.Client("username", "password", "project_id", - "auth_url/v2.0", no_cache=True) + "auth_url/v2.0") resp = {"unauthorized": {"message": "Unauthorized", "code": "401"}} auth_response = httplib2.Response({ "status": 401, @@ -100,8 +99,7 @@ def test_auth_call(): def test_auth_redirect(self): cs = client.Client("username", "password", "project_id", - "auth_url/v1.0", service_type='compute', - no_cache=True) + "auth_url/v1.0", service_type='compute') dict_correct_response = { "access": { "token": { @@ -182,8 +180,7 @@ def test_auth_call(): def test_ambiguous_endpoints(self): cs = client.Client("username", "password", "project_id", - "auth_url/v2.0", service_type='compute', - no_cache=True) + "auth_url/v2.0", service_type='compute') resp = { "access": { "token": { @@ -235,8 +232,7 @@ def test_auth_call(): class AuthenticationTests(utils.TestCase): def test_authenticate_success(self): - cs = client.Client("username", "password", "project_id", "auth_url", - no_cache=True) + cs = client.Client("username", "password", "project_id", "auth_url") management_url = 'https://localhost/v1.1/443470' auth_response = httplib2.Response({ 'status': 204, @@ -265,8 +261,7 @@ def test_auth_call(): test_auth_call() def test_authenticate_failure(self): - cs = client.Client("username", "password", "project_id", "auth_url", - no_cache=True) + cs = client.Client("username", "password", "project_id", "auth_url") auth_response = httplib2.Response({'status': 401}) mock_request = mock.Mock(return_value=(auth_response, None)) @@ -277,8 +272,7 @@ def test_auth_call(): test_auth_call() def test_auth_automatic(self): - cs = client.Client("username", "password", "project_id", "auth_url", - no_cache=True) + cs = client.Client("username", "password", "project_id", "auth_url") http_client = cs.client http_client.management_url = '' mock_request = mock.Mock(return_value=(None, None)) @@ -293,8 +287,7 @@ def test_auth_call(m): test_auth_call() def test_auth_manual(self): - cs = client.Client("username", "password", "project_id", "auth_url", - no_cache=True) + cs = client.Client("username", "password", "project_id", "auth_url") @mock.patch.object(cs.client, 'authenticate') def test_auth_call(m): From 08d0c05013483e89054bf8c0261c24965a4b40b1 Mon Sep 17 00:00:00 2001 From: ivan-zhu Date: Mon, 12 Nov 2012 17:04:49 +0800 Subject: [PATCH 0029/1705] Add nova client support for nova-manage agent command This add four CLI (agent-list/agent-create/agent-delete/agent-modify) in nova-client so we can use: 'nova agent-list' like 'nova-manage agent list' with one optional parameter. Show a list of all agent-builds. Filter by hypervisor. The difference between the two commands is that 'nova agent-list' will display the id of the agent build, 'nova-manage agent list' will not. 'nova agent-create' like 'nova-manage agent create'. It will create a agent build. 'nova agent-delete id' will delete the agent-build with specific id. 'nova agent-modfiy' will update the version, url, md5hash of the agent build with specific id. This patch depends on https://review.openstack.org/#/c/15831/ Implements one workitem of blueprint apis-for-nova-manage Change-Id: Ic7589077d130efa5abc77252bd79addcaea483c8 --- novaclient/v1_1/agents.py | 68 +++++++++++++++++++++++++++++++++++++++ novaclient/v1_1/client.py | 2 ++ novaclient/v1_1/shell.py | 43 +++++++++++++++++++++++++ tests/v1_1/fakes.py | 43 +++++++++++++++++++++++++ tests/v1_1/test_agents.py | 68 +++++++++++++++++++++++++++++++++++++++ tests/v1_1/test_shell.py | 32 ++++++++++++++++++ 6 files changed, 256 insertions(+) create mode 100644 novaclient/v1_1/agents.py create mode 100644 tests/v1_1/test_agents.py diff --git a/novaclient/v1_1/agents.py b/novaclient/v1_1/agents.py new file mode 100644 index 000000000..8552119d4 --- /dev/null +++ b/novaclient/v1_1/agents.py @@ -0,0 +1,68 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 IBM +# All Rights Reserved. +# +# 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. + +""" +agent interface +""" + +from novaclient import base + + +class Agent(base.Resource): + def __repr__(self): + return "" % self.agent + + def _add_details(self, info): + dico = 'resource' in info and info['resource'] or info + for (k, v) in dico.items(): + setattr(self, k, v) + + +class AgentsManager(base.ManagerWithFind): + resource_class = Agent + + def list(self, hypervisor=None): + """Lists all agent builds.""" + url = "/os-agents" + if hypervisor: + url = "/os-agents?hypervisor=%s" % hypervisor + return self._list(url, "agents") + + def update(self, id, version, + url, md5hash): + """Update an existing agent build.""" + body = {'para': { + 'version': version, + 'url': url, + 'md5hash': md5hash}} + return self._update('/os-agents/%s' % id, body) + + def create(self, os, architecture, version, + url, md5hash, hypervisor): + """Creates a new agent build""" + body = {'agent': { + 'hypervisor': hypervisor, + 'os': os, + 'architecture': architecture, + 'version': version, + 'url': url, + 'md5hash': md5hash}} + return self._create('/os-agents', body, 'agent') + + def delete(self, id): + """Deletes an existing agent build.""" + self._delete('/os-agents/%s' % id) diff --git a/novaclient/v1_1/client.py b/novaclient/v1_1/client.py index dbff3e370..76f1deaa0 100644 --- a/novaclient/v1_1/client.py +++ b/novaclient/v1_1/client.py @@ -1,4 +1,5 @@ from novaclient import client +from novaclient.v1_1 import agents from novaclient.v1_1 import certs from novaclient.v1_1 import cloudpipe from novaclient.v1_1 import aggregates @@ -65,6 +66,7 @@ def __init__(self, username, api_key, project_id, auth_url=None, self.servers = servers.ServerManager(self) # extensions + self.agents = agents.AgentsManager(self) self.dns_domains = floating_ip_dns.FloatingIPDNSDomainManager(self) self.dns_entries = floating_ip_dns.FloatingIPDNSEntryManager(self) self.cloudpipe = cloudpipe.CloudpipeManager(self) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 7b5c1fd94..0772bfc80 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1844,6 +1844,49 @@ def do_x509_get_root_cert(cs, args): print "Wrote x509 root cert to %s" % args.filename +@utils.arg('--hypervisor', metavar='', default=None, + help='type of hypervisor.') +def do_agent_list(cs, args): + """List all builds""" + result = cs.agents.list(args.hypervisor) + columns = ["Agent_id", "Hypervisor", "OS", "Architecture", "Version", + 'Md5hash', 'Url'] + utils.print_list(result, columns) + + +@utils.arg('os', metavar='', help='type of os.') +@utils.arg('architecture', metavar='', + help='type of architecture') +@utils.arg('version', metavar='', help='version') +@utils.arg('url', metavar='', help='url') +@utils.arg('md5hash', metavar='', help='md5 hash') +@utils.arg('hypervisor', metavar='', default='xen', + help='type of hypervisor.') +def do_agent_create(cs, args): + """Creates a new agent build.""" + result = cs.agents.create(args.os, args.architecture, + args.version, args.url, + args.md5hash, args.hypervisor) + utils.print_dict(result._info.copy()) + + +@utils.arg('id', metavar='', help='id of the agent-build') +def do_agent_delete(cs, args): + """Deletes an existing agent build.""" + cs.agents.delete(args.id) + + +@utils.arg('id', metavar='', help='id of the agent-build') +@utils.arg('version', metavar='', help='version') +@utils.arg('url', metavar='', help='url') +@utils.arg('md5hash', metavar='', help='md5hash') +def do_agent_modify(cs, args): + """Modify an existing agent build.""" + result = cs.agents.update(args.id, args.version, + args.url, args.md5hash) + utils.print_dict(result['agent']) + + def do_aggregate_list(cs, args): """Print a list of all aggregates.""" aggregates = cs.aggregates.list() diff --git a/tests/v1_1/fakes.py b/tests/v1_1/fakes.py index 91f33e8fe..bcd7ec7b5 100644 --- a/tests/v1_1/fakes.py +++ b/tests/v1_1/fakes.py @@ -69,6 +69,49 @@ def _cs_request(self, url, method, **kwargs): else: return httplib2.Response({"status": status}), body + # + # agents + # + + def get_os_agents(self, **kw): + hypervisor = kw.get('hypervisor', 'kvm') + return (200, {'agents': + [{'hypervisor': hypervisor, + 'os': 'win', + 'architecture': 'x86', + 'version': '7.0', + 'url': 'xxx://xxxx/xxx/xxx', + 'md5hash': 'add6bb58e139be103324d04d82d8f545', + 'id': 1}, + {'hypervisor': hypervisor, + 'os': 'linux', + 'architecture': 'x86', + 'version': '16.0', + 'url': 'xxx://xxxx/xxx/xxx1', + 'md5hash': 'add6bb58e139be103324d04d82d8f546', + 'id': 2}, + ]}) + + def post_os_agents(self, body): + return (200, {'agent': { + 'url': '/xxx/xxx/xxx', + 'hypervisor': body['agent']['hypervisor'], + 'md5hash': 'add6bb58e139be103324d04d82d8f546', + 'version': '7.0', + 'architecture': 'x86', + 'os': 'win', + 'id': 1}}) + + def delete_os_agents_1(self, **kw): + return (202, None) + + def put_os_agents_1(self, body, **kw): + return (200, {"agent": { + "url": "/yyy/yyyy/yyyy", + "version": "8.0", + "md5hash": "add6bb58e139be103324d04d82d8f546", + 'id': 1}}) + # # List all extensions # diff --git a/tests/v1_1/test_agents.py b/tests/v1_1/test_agents.py new file mode 100644 index 000000000..97d537994 --- /dev/null +++ b/tests/v1_1/test_agents.py @@ -0,0 +1,68 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 IBM +# All Rights Reserved. +# +# 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 novaclient.v1_1 import agents +from tests.v1_1 import fakes +from tests import utils + + +cs = fakes.FakeClient() + + +class AgentsTest(utils.TestCase): + + def test_list_agents(self): + ags = cs.agents.list() + cs.assert_called('GET', '/os-agents') + [self.assertTrue(isinstance(a, agents.Agent)) for a in ags] + [self.assertEqual(a.hypervisor, 'kvm') for a in ags] + + def test_list_agents_with_hypervisor(self): + ags = cs.agents.list('xen') + cs.assert_called('GET', '/os-agents?hypervisor=xen') + [self.assertTrue(isinstance(a, agents.Agent)) for a in ags] + [self.assertEqual(a.hypervisor, 'xen') for a in ags] + + def test_agents_create(self): + ag = cs.agents.create('win', 'x86', '7.0', + '/xxx/xxx/xxx', + 'add6bb58e139be103324d04d82d8f546', + 'xen') + body = {'agent': { + 'url': '/xxx/xxx/xxx', + 'hypervisor': 'xen', + 'md5hash': 'add6bb58e139be103324d04d82d8f546', + 'version': '7.0', + 'architecture': 'x86', + 'os': 'win'}} + cs.assert_called('POST', '/os-agents', body) + self.assertEqual(1, ag._info.copy()['id']) + + def test_agents_delete(self): + cs.agents.delete('1') + cs.assert_called('DELETE', '/os-agents/1') + + def test_agents_modify(self): + ag = cs.agents.update('1', '8.0', + '/yyy/yyyy/yyyy', + 'add6bb58e139be103324d04d82d8f546') + body = {"para": { + "url": "/yyy/yyyy/yyyy", + "version": "8.0", + "md5hash": "add6bb58e139be103324d04d82d8f546"}} + cs.assert_called('PUT', '/os-agents/1', body) + self.assertEqual(1, ag['agent']['id']) diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index 206fc8c22..d85f7f4fa 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -72,6 +72,38 @@ def assert_called(self, method, url, body=None, **kwargs): def assert_called_anytime(self, method, url, body=None): return self.shell.cs.assert_called_anytime(method, url, body) + def test_agents_list_with_hypervisor(self): + self.run_command('agent-list --hypervisor xen') + self.assert_called('GET', '/os-agents?hypervisor=xen') + + def test_agents_create(self): + self.run_command('agent-create win x86 7.0 ' + '/xxx/xxx/xxx ' + 'add6bb58e139be103324d04d82d8f546 ' + 'kvm') + self.assert_called( + 'POST', '/os-agents', + {'agent': { + 'hypervisor': 'kvm', + 'os': 'win', + 'architecture': 'x86', + 'version': '7.0', + 'url': '/xxx/xxx/xxx', + 'md5hash': 'add6bb58e139be103324d04d82d8f546'}}) + + def test_agents_delete(self): + self.run_command('agent-delete 1') + self.assert_called('DELETE', '/os-agents/1') + + def test_agents_modify(self): + self.run_command('agent-modify 1 8.0 /yyy/yyyy/yyyy ' + 'add6bb58e139be103324d04d82d8f546') + self.assert_called('PUT', '/os-agents/1', + {"para": { + "url": "/yyy/yyyy/yyyy", + "version": "8.0", + "md5hash": "add6bb58e139be103324d04d82d8f546"}}) + def test_boot(self): self.run_command('boot --flavor 1 --image 1 some-server') self.assert_called_anytime( From 35b18965e496f5c05dde9ba177edc4bafb97efe2 Mon Sep 17 00:00:00 2001 From: Julie Pichon Date: Wed, 12 Dec 2012 16:55:10 +0000 Subject: [PATCH 0030/1705] Add optional argument to include reservations in os-used-limits Fixes bug #1089271 Change-Id: I01a3823a4f4b1045e4f8e35f749fc9233bef4489 --- novaclient/v1_1/limits.py | 6 ++++-- novaclient/v1_1/shell.py | 7 ++++++- tests/v1_1/test_limits.py | 17 +++++++++++++++++ tests/v1_1/test_shell.py | 7 +++++++ 4 files changed, 34 insertions(+), 3 deletions(-) diff --git a/novaclient/v1_1/limits.py b/novaclient/v1_1/limits.py index 75bf54161..6c137f364 100644 --- a/novaclient/v1_1/limits.py +++ b/novaclient/v1_1/limits.py @@ -70,10 +70,12 @@ class LimitsManager(base.Manager): resource_class = Limits - def get(self): + def get(self, reserved=False): """ Get a specific extension. :rtype: :class:`Limits` """ - return self._get("/limits", "limits") + query_string = "?reserved=1" if reserved else "" + + return self._get("/limits%s" % query_string, "limits") diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index f7fee3290..96e692083 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1910,9 +1910,14 @@ def do_keypair_list(cs, args): utils.print_list(keypairs, columns) +@utils.arg('--reserved', + dest='reserved', + action='store_true', + default=False, + help='Include reservations count.') def do_absolute_limits(cs, args): """Print a list of absolute limits for a user""" - limits = cs.limits.get().absolute + limits = cs.limits.get(args.reserved).absolute columns = ['Name', 'Value'] utils.print_list(limits, columns) diff --git a/tests/v1_1/test_limits.py b/tests/v1_1/test_limits.py index f41a2e76b..3a8fa745c 100644 --- a/tests/v1_1/test_limits.py +++ b/tests/v1_1/test_limits.py @@ -31,6 +31,23 @@ def test_absolute_limits(self): for limit in abs_limits: self.assertTrue(limit in expected) + def test_absolute_limits_reserved(self): + obj = cs.limits.get(reserved=True) + + expected = ( + limits.AbsoluteLimit("maxTotalRAMSize", 51200), + limits.AbsoluteLimit("maxServerMeta", 5), + limits.AbsoluteLimit("maxImageMeta", 5), + limits.AbsoluteLimit("maxPersonality", 5), + limits.AbsoluteLimit("maxPersonalitySize", 10240), + ) + + abs_limits = list(obj.absolute) + self.assertEqual(len(abs_limits), len(expected)) + + for limit in abs_limits: + self.assertTrue(limit in expected) + def test_rate_limits(self): obj = cs.limits.get() diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index 068c4fbee..a2e987615 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -712,3 +712,10 @@ def test_backup(self): {'createBackup': {'name': 'back1', 'backup_type': 'daily', 'rotation': '1'}}) + + def test_absolute_limits(self): + self.run_command('absolute-limits') + self.assert_called('GET', '/limits') + + self.run_command('absolute-limits --reserved') + self.assert_called('GET', '/limits?reserved=1') From 80a72e1a92aa103a07286060b9cf34444afd5526 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Wed, 12 Dec 2012 15:41:31 -0800 Subject: [PATCH 0031/1705] Makes the OS_NO_CACHE env variable work again The commit to replace --os-no-cache with --os-no-cache works fine with the cli options, but the env variable is stored in os_cache which has the opposite of the intended effect. This patch converts the variable to a bool and then inverts it before it stores it in os_cache. This makes it work properly again. Fixes bug 1089696 Change-Id: Iea12806603ecdc39c6475ad4d6f867ebb1e01633 --- novaclient/shell.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/novaclient/shell.py b/novaclient/shell.py index 98df2c0ab..07267da9d 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -93,7 +93,8 @@ def get_base_parser(self): help="Print debugging output") parser.add_argument('--no-cache', - default=utils.env('OS_NO_CACHE', default=True), + default=not utils.bool_from_str( + utils.env('OS_NO_CACHE', default='true')), action='store_false', dest='os_cache', help=argparse.SUPPRESS) From eaf3c366c34c64f2e0a6ce0ada2b2db7ff09fc5b Mon Sep 17 00:00:00 2001 From: Ken'ichi Ohmichi Date: Wed, 19 Dec 2012 04:23:36 +0900 Subject: [PATCH 0032/1705] Specify some arguments by name. 'nova boot', 'nova flavor-key', 'nova flavor-show' and the other subcommands can be specified by a flavor name also. But 'nova flavor-delete' and 'nova list --flavor' cannot, also 'nova list --image' cannot be specified by a image name. I feel it is user-friendly that a user can specify arguments by name. By this patch, a user can do it like the following. Before appying this patch: $ nova flavor-delete m1.tiny ERROR: Flavor m1.tiny could not be found. (HTTP 404) (Request-ID: req-af2c049f-1b77-42cf-a9ed-a3914626bc83) $ $ nova list --flavor m1.tiny ERROR: Flavor could not be found (HTTP 422) (Request-ID: req-dbfd5df2-77d7-4a71-a3e6-b1f7fd25b96c) $ $ nova list --image cirros-0.3.0-x86_64-uec $ After appying this patch: $ nova flavor-delete m1.tiny $ $ nova list --flavor m1.tiny +--------------------------------------+--------+--------+------------------+ | ID | Name | Status | Networks | +--------------------------------------+--------+--------+------------------+ | 2f003e0e-bfdf-4f56-bdf9-276732e640a0 | test01 | ACTIVE | private=10.0.0.2 | +--------------------------------------+--------+--------+------------------+ $ $ nova list --image cirros-0.3.0-x86_64-uec +--------------------------------------+--------+--------+------------------+ | ID | Name | Status | Networks | +--------------------------------------+--------+--------+------------------+ | 2f003e0e-bfdf-4f56-bdf9-276732e640a0 | test01 | ACTIVE | private=10.0.0.2 | +--------------------------------------+--------+--------+------------------+ $ Fixes bug 1091814 Change-Id: Ic7f6ce76608a448dea3c151bc349391fbf3fa456 --- novaclient/v1_1/shell.py | 23 +++++++++++++++-------- tests/v1_1/fakes.py | 3 +++ tests/v1_1/test_shell.py | 4 ++-- 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index a0d304971..6c469c1e0 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -358,12 +358,13 @@ def do_flavor_list(cs, _args): _print_flavor_list(cs, flavors) -@utils.arg('id', - metavar='', - help="Unique ID of the flavor to delete") +@utils.arg('flavor', + metavar='', + help="Name or ID of the flavor to delete") def do_flavor_delete(cs, args): """Delete a specific flavor""" - cs.flavors.delete(args.id) + flavorid = _find_flavor(cs, args.flavor) + cs.flavors.delete(flavorid) @utils.arg('flavor', @@ -803,12 +804,12 @@ def do_image_delete(cs, args): dest='flavor', metavar='', default=None, - help='Search by flavor ID') + help='Search by flavor name or ID') @utils.arg('--image', dest='image', metavar='', default=None, - help='Search by image ID') + help='Search by image name or ID') @utils.arg('--host', dest='host', metavar='', @@ -836,14 +837,20 @@ def do_image_delete(cs, args): help='Display information from single tenant (Admin only).') def do_list(cs, args): """List active servers.""" + imageid = None + flavorid = None + if args.image: + imageid = _find_image(cs, args.image).id + if args.flavor: + flavorid = _find_flavor(cs, args.flavor).id search_opts = { 'all_tenants': args.all_tenants, 'reservation_id': args.reservation_id, 'ip': args.ip, 'ip6': args.ip6, 'name': args.name, - 'image': args.image, - 'flavor': args.flavor, + 'image': imageid, + 'flavor': flavorid, 'status': args.status, 'project_id': args.tenant, 'host': args.host, diff --git a/tests/v1_1/fakes.py b/tests/v1_1/fakes.py index c9ff8b618..eb106a790 100644 --- a/tests/v1_1/fakes.py +++ b/tests/v1_1/fakes.py @@ -535,6 +535,9 @@ def get_flavors_3(self, **kw): def delete_flavors_flavordelete(self, **kw): return (202, None) + def delete_flavors_2(self, **kw): + return (202, None) + def post_flavors(self, body, **kw): return (202, {'flavor': self.get_flavors_detail()[1]['flavors'][0]}) diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index 191e910f0..4b03f6eff 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -464,8 +464,8 @@ def test_usage_list_no_args(self): 'detailed=1') def test_flavor_delete(self): - self.run_command("flavor-delete flavordelete") - self.assert_called('DELETE', '/flavors/flavordelete') + self.run_command("flavor-delete 2") + self.assert_called('DELETE', '/flavors/2') def test_flavor_create(self): self.run_command("flavor-create flavorcreate " From c59de35be22abec7351c6b8b3dbb53f79eaafe92 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 12 Dec 2012 16:32:35 -0500 Subject: [PATCH 0033/1705] Add support for the coverage extension. This adds support for the coverage extension and also adds 3 CLI commands to interact with coverage: nova coverage-start nova coverage-stop nova coverage-report 'nova coverage-report' has a required '--filename' argument which gives the filename for the generated report files. Change-Id: I9cb002b0e32cfe5572878d2c8436270663c0ae96 --- novaclient/v1_1/client.py | 2 ++ novaclient/v1_1/coverage_ext.py | 54 +++++++++++++++++++++++++++++++++ novaclient/v1_1/shell.py | 19 ++++++++++++ tests/v1_1/fakes.py | 8 +++++ tests/v1_1/test_coverage_ext.py | 42 +++++++++++++++++++++++++ tests/v1_1/test_shell.py | 12 ++++++++ 6 files changed, 137 insertions(+) create mode 100644 novaclient/v1_1/coverage_ext.py create mode 100644 tests/v1_1/test_coverage_ext.py diff --git a/novaclient/v1_1/client.py b/novaclient/v1_1/client.py index 6b2d3d3fa..230f8d4b3 100644 --- a/novaclient/v1_1/client.py +++ b/novaclient/v1_1/client.py @@ -2,6 +2,7 @@ from novaclient.v1_1 import certs from novaclient.v1_1 import cloudpipe from novaclient.v1_1 import aggregates +from novaclient.v1_1 import coverage_ext from novaclient.v1_1 import flavors from novaclient.v1_1 import flavor_access from novaclient.v1_1 import floating_ip_dns @@ -93,6 +94,7 @@ def __init__(self, username, api_key, project_id, auth_url=None, self.fixed_ips = fixed_ips.FixedIPsManager(self) self.floating_ips_bulk = floating_ips_bulk.FloatingIPBulkManager(self) self.os_cache = os_cache or not no_cache + self.coverage = coverage_ext.CoverageManager(self) # Add in any extensions... if extensions: diff --git a/novaclient/v1_1/coverage_ext.py b/novaclient/v1_1/coverage_ext.py new file mode 100644 index 000000000..1a526a64f --- /dev/null +++ b/novaclient/v1_1/coverage_ext.py @@ -0,0 +1,54 @@ +# Copyright 2012 IBM +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import urllib + +from novaclient import base + + +class Coverage(base.Resource): + def __repr__(self): + return "" % self.name + + +class CoverageManager(base.ManagerWithFind): + + resource_class = Coverage + + def start(self, combine=False, **kwargs): + body = {'start': {}} + if combine: + body['start'] = {'combine': True} + self.run_hooks('modify_body_for_action', body) + url = '/os-coverage/action' + return self.api.client.post(url, body=body) + + def stop(self): + body = {'stop': {}} + self.run_hooks('modify_body_for_action', body) + url = '/os-coverage/action' + return self.api.client.post(url, body=body) + + def report(self, filename, xml=False): + body = { + 'report': { + 'file': filename, + } + } + if xml: + body['report']['xml'] = True + self.run_hooks('modify_body_for_action', body) + url = '/os-coverage/action' + return self.api.client.post(url, body=body) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index f7fee3290..a24c15f7c 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2235,6 +2235,25 @@ def do_host_action(cs, args): utils.print_list([result], ['HOST', 'power_action']) +def do_coverage_start(cs, args): + """Start Nova coverage reporting""" + cs.coverage.start() + print "Coverage collection started" + + +def do_coverage_stop(cs, args): + """Stop Nova coverage reporting""" + cs.coverage.stop() + print "Coverage collection stopped" + + +@utils.arg('filename', metavar='', help='report filename') +def do_coverage_report(cs, args): + """Generate a coverage report""" + cov = cs.coverage.report(args.filename) + print "Report path: %s" % cov[-1]['path'] + + @utils.arg('--matching', metavar='', default=None, help='List hypervisors matching the given .') def do_hypervisor_list(cs, args): diff --git a/tests/v1_1/fakes.py b/tests/v1_1/fakes.py index 1f97d3ea2..ab9ade4ca 100644 --- a/tests/v1_1/fakes.py +++ b/tests/v1_1/fakes.py @@ -1227,3 +1227,11 @@ def post_os_networks_networktest_action(self, **kw): def post_os_networks_2_action(self, **kw): return (202, None) + + def post_os_coverage_action(self, body, **kw): + if 'report' not in body: + return (200, None) + else: + return (200, { + 'path': '/tmp/tmpdir/' + body['report']['file'] + }) diff --git a/tests/v1_1/test_coverage_ext.py b/tests/v1_1/test_coverage_ext.py new file mode 100644 index 000000000..818f33220 --- /dev/null +++ b/tests/v1_1/test_coverage_ext.py @@ -0,0 +1,42 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 IBM +# +# 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 + +# See: http://wiki.openstack.org/Nova/CoverageExtension for more information +# and usage explanation for this API extension + +from novaclient import exceptions +from novaclient.v1_1 import flavors +from tests import utils +from tests.v1_1 import fakes + + +cs = fakes.FakeClient() + + +class CoverageTest(utils.TestCase): + + def test_start_coverage(self): + c = cs.coverage.start() + cs.assert_called('POST', '/os-coverage/action') + + def test_stop_coverage(self): + c = cs.coverage.stop() + cs.assert_called('POST', '/os-coverage/action') + + def test_report_coverage(self): + c = cs.coverage.report('report') + return_dict = {'path': '/tmp/tmpdir/report'} + cs.assert_called_anytime('POST', '/os-coverage/action') diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index 068c4fbee..05a2c763a 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -590,6 +590,18 @@ def test_host_reboot(self): self.run_command('host-action sample-host --action reboot') self.assert_called('GET', '/os-hosts/sample-host/reboot') + def test_coverage_start(self): + self.run_command('coverage-start') + self.assert_called('POST', '/os-coverage/action') + + def test_coverage_stop(self): + self.run_command('coverage-stop') + self.assert_called('POST', '/os-coverage/action') + + def test_coverage_report(self): + self.run_command('coverage-report report') + self.assert_called_anytime('POST', '/os-coverage/action') + def test_hypervisor_list(self): self.run_command('hypervisor-list') self.assert_called('GET', '/os-hypervisors') From e6e22dbe77cbcb9e2078ab8574ebb75575ab2168 Mon Sep 17 00:00:00 2001 From: Eoghan Glynn Date: Wed, 19 Dec 2012 20:07:41 +0000 Subject: [PATCH 0034/1705] Make --tenant a required arg for quota-show Fixes LP 1088519 Previously, novaclient was incorrectly defaulting to the tenant name as the tenant ID when retrieving quota thresholds via the os-quota-sets API extension, in an apparent attempt to default to the current tenant if no specific tenant is explicitly requested. As a result, the default quotas were always returned, regardless of whether there were specific overridden quotas for this tenant. We now require that the --tenant option is always specified for the quota-show verb, as a sensible default isn't always possible (because the tenant ID may be opaque to the client, for example when the auth token is cached in the keyring from a previous call out to keystone - now the tenant ID is of course generally embedded in the nova publicURL retrieved from the service catalog, but that is not guaranteed to be the case, i.e. I don't think we can safely make assumptions about that URL format). This change also makes the quota-show verb more consistent with the quota-update verb, which currently requires that the tenant is always explicitly specified. Change-Id: I1706ad993059e70ca0e2f3bcf7b1d06cbcc39f2d --- novaclient/v1_1/shell.py | 2 +- tests/v1_1/test_shell.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index a0d304971..a67da61f5 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2474,7 +2474,7 @@ def do_quota_show(cs, args): """List the quotas for a tenant.""" if not args.tenant: - _quota_show(cs.quotas.get(cs.project_id)) + raise exceptions.CommandError("you need to specify a Tenant ID ") else: _quota_show(cs.quotas.get(args.tenant)) diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index 191e910f0..c4f61ad93 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -650,6 +650,11 @@ def test_quota_show(self): self.run_command('quota-show --tenant test') self.assert_called('GET', '/os-quota-sets/test') + def test_quota_show_no_tenant(self): + self.assertRaises(exceptions.CommandError, + self.run_command, + 'quota-show') + def test_quota_defaults(self): self.run_command('quota-defaults --tenant test') self.assert_called('GET', '/os-quota-sets/test/defaults') From 993741988804fcba6efbab0cf182300c779c00e5 Mon Sep 17 00:00:00 2001 From: Rohan Rhishikesh Kanade Date: Sun, 16 Dec 2012 23:18:39 -0800 Subject: [PATCH 0035/1705] Fix find for alphanumeic flavor id/name Fixes LP bug #1089299 Change-Id: Ia9cc756b85096532acfc9ecacd1330de8a765fba --- novaclient/utils.py | 7 +++++++ novaclient/v1_1/flavors.py | 1 + tests/v1_1/fakes.py | 15 ++++++++++++++- tests/v1_1/test_flavors.py | 9 +++++++++ tests/v1_1/test_shell.py | 6 +++++- 5 files changed, 36 insertions(+), 2 deletions(-) diff --git a/novaclient/utils.py b/novaclient/utils.py index 02c961188..bf76a7316 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -192,6 +192,13 @@ def find_resource(manager, name_or_id): except (ValueError, exceptions.NotFound): pass + # for str id which is not uuid (for Flavor search currently) + if getattr(manager, 'is_alphanum_id_allowed', False): + try: + return manager.get(name_or_id) + except exceptions.NotFound: + pass + try: try: return manager.find(human_id=name_or_id) diff --git a/novaclient/v1_1/flavors.py b/novaclient/v1_1/flavors.py index 28f065425..b8aef6ea5 100644 --- a/novaclient/v1_1/flavors.py +++ b/novaclient/v1_1/flavors.py @@ -74,6 +74,7 @@ class FlavorManager(base.ManagerWithFind): Manage :class:`Flavor` resources. """ resource_class = Flavor + is_alphanum_id_allowed = True def list(self, detailed=True): """ diff --git a/tests/v1_1/fakes.py b/tests/v1_1/fakes.py index c9ff8b618..a70b60830 100644 --- a/tests/v1_1/fakes.py +++ b/tests/v1_1/fakes.py @@ -506,7 +506,8 @@ def put_os_cloudpipe_configure_project(self, **kw): def get_flavors(self, **kw): return (200, {'flavors': [ {'id': 1, 'name': '256 MB Server'}, - {'id': 2, 'name': '512 MB Server'} + {'id': 2, 'name': '512 MB Server'}, + {'id': 'aa1', 'name': '128 MB Server'} ]}) def get_flavors_detail(self, **kw): @@ -519,6 +520,10 @@ def get_flavors_detail(self, **kw): 'OS-FLV-EXT-DATA:ephemeral': 20, 'os-flavor-access:is_public': False, 'links': {}}, + {'id': 'aa1', 'name': '128 MB Server', 'ram': 128, 'disk': 0, + 'OS-FLV-EXT-DATA:ephemeral': 0, + 'os-flavor-access:is_public': True, + 'links': {}} ]}) def get_flavors_1(self, **kw): @@ -532,6 +537,10 @@ def get_flavors_3(self, **kw): return (200, {'flavor': {'id': 3, 'name': '256 MB Server', 'ram': 256, 'disk': 10}}) + def get_flavors_aa1(self, **kw): + # Aplhanumeric flavor id are allowed. + return (200, {'flavor': self.get_flavors_detail()[1]['flavors'][2]}) + def delete_flavors_flavordelete(self, **kw): return (202, None) @@ -546,6 +555,10 @@ def get_flavors_2_os_extra_specs(self, **kw): return (200, {'extra_specs': {"k2": "v2"}}) + def get_flavors_aa1_os_extra_specs(self, **kw): + return (200, + {'extra_specs': {"k3": "v3"}}) + def post_flavors_1_os_extra_specs(self, body, **kw): assert body.keys() == ['extra_specs'] fakes.assert_has_keys(body['extra_specs'], diff --git a/tests/v1_1/test_flavors.py b/tests/v1_1/test_flavors.py index ca4b33789..084e34afd 100644 --- a/tests/v1_1/test_flavors.py +++ b/tests/v1_1/test_flavors.py @@ -28,6 +28,15 @@ def test_get_flavor_details(self): self.assertEqual(f.ephemeral, 10) self.assertEqual(f.is_public, True) + def test_get_flavor_details_alphanum_id(self): + f = cs.flavors.get('aa1') + cs.assert_called('GET', '/flavors/aa1') + self.assertTrue(isinstance(f, flavors.Flavor)) + self.assertEqual(f.ram, 128) + self.assertEqual(f.disk, 0) + self.assertEqual(f.ephemeral, 0) + self.assertEqual(f.is_public, True) + def test_get_flavor_details_diablo(self): f = cs.flavors.get(3) cs.assert_called('GET', '/flavors/3') diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index 191e910f0..cbd3dddf2 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -226,13 +226,17 @@ def test_boot_invalid_file(self): def test_flavor_list(self): self.run_command('flavor-list') - self.assert_called('GET', '/flavors/2/os-extra_specs') + self.assert_called('GET', '/flavors/aa1/os-extra_specs') self.assert_called_anytime('GET', '/flavors/detail') def test_flavor_show(self): self.run_command('flavor-show 1') self.assert_called_anytime('GET', '/flavors/1') + def test_flavor_show_with_alphanum_id(self): + self.run_command('flavor-show aa1') + self.assert_called_anytime('GET', '/flavors/aa1') + def test_image_show(self): self.run_command('image-show 1') self.assert_called('GET', '/images/1') From aa1df04badd0b1326be9aff0512948b85804f621 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Tue, 18 Dec 2012 14:05:29 -0600 Subject: [PATCH 0036/1705] Use requests module for HTTP/HTTPS * Implement correct certificate verification * Add --os-cacert * Rework tests for requests Pinned requests module to < 1.0 as 1.0.2 is now current in pipi as of 17Dec2012. Blueprint: tls-verify Change-Id: I9a25a94c8dfcaf483c4c8328439809d65cf10b38 --- novaclient/client.py | 129 ++++++------- novaclient/exceptions.py | 19 +- novaclient/shell.py | 18 +- novaclient/v1_1/client.py | 6 +- novaclient/v1_1/servers.py | 3 +- tests/test_auth_plugins.py | 27 +-- tests/test_http.py | 61 +++--- tests/test_shell.py | 6 - tests/utils.py | 31 +++- tests/v1_1/fakes.py | 367 +++++++++++++++++++++---------------- tests/v1_1/test_auth.py | 121 ++++++------ tools/pip-requires | 2 +- 12 files changed, 442 insertions(+), 348 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index 9bd838742..1fbfd9750 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -9,11 +9,12 @@ import logging import os +import sys import time import urlparse -import httplib2 import pkg_resources +import requests try: import json @@ -46,41 +47,14 @@ def get_auth_system_url(auth_system): raise exceptions.AuthSystemNotFound(auth_system) -def _get_proxy_info(): - """Work around httplib2 proxying bug. - - Full details of the bug here: - - http://code.google.com/p/httplib2/issues/detail?id=228 - - Basically, in the case of plain old http with httplib2>=0.7.5 we - want to ensure that PROXY_TYPE_HTTP_NO_TUNNEL is used. - """ - def get_proxy_info(method): - pi = httplib2.ProxyInfo.from_environment(method) - if pi is None or method != 'http': - return pi - - # We can't rely on httplib2.socks being available - # PROXY_TYPE_HTTP_NO_TUNNEL was introduced in 0.7.5 - if not (hasattr(httplib2, 'socks') and - hasattr(httplib2.socks, 'PROXY_TYPE_HTTP_NO_TUNNEL')): - return pi - - pi.proxy_type = httplib2.socks.PROXY_TYPE_HTTP_NO_TUNNEL - return pi - - # 0.7.3 introduced configuring proxy from the environment - if not hasattr(httplib2.ProxyInfo, 'from_environment'): - return None - - return get_proxy_info - - -class HTTPClient(httplib2.Http): +class HTTPClient(object): USER_AGENT = 'python-novaclient' + requests_config = { + 'danger_mode': False, + } + def __init__(self, user, password, projectid, auth_url=None, insecure=False, timeout=None, proxy_tenant_id=None, proxy_token=None, region_name=None, @@ -88,9 +62,8 @@ def __init__(self, user, password, projectid, auth_url=None, service_name=None, volume_service_name=None, timings=False, bypass_url=None, os_cache=False, no_cache=True, - http_log_debug=False, auth_system='keystone'): - super(HTTPClient, self).__init__(timeout=timeout, - proxy_info=_get_proxy_info()) + http_log_debug=False, auth_system='keystone', + cacert=None): self.user = user self.password = password self.projectid = projectid @@ -118,9 +91,13 @@ def __init__(self, user, password, projectid, auth_url=None, self.proxy_tenant_id = proxy_tenant_id self.used_keyring = False - # httplib2 overrides - self.force_exception_to_status_code = True - self.disable_ssl_certificate_validation = insecure + if insecure: + self.verify_cert = False + else: + if cacert: + self.verify_cert = cacert + else: + self.verify_cert = True self.auth_system = auth_system @@ -129,6 +106,7 @@ def __init__(self, user, password, projectid, auth_url=None, ch = logging.StreamHandler() self._logger.setLevel(logging.DEBUG) self._logger.addHandler(ch) + self.requests_config['verbose'] = sys.stderr def use_token_cache(self, use_it): self.os_cache = use_it @@ -167,40 +145,53 @@ def http_log_req(self, args, kwargs): string_parts.append(" -d '%s'" % (kwargs['body'])) self._logger.debug("\nREQ: %s\n" % "".join(string_parts)) - def http_log_resp(self, resp, body): + def http_log_resp(self, resp): if not self.http_log_debug: return - self._logger.debug("RESP:%s %s\n", resp, body) + self._logger.debug( + "RESP: [%s] %s\nRESP BODY: %s\n", + resp.status_code, + resp.headers, + resp.text) - def request(self, *args, **kwargs): + def request(self, url, method, **kwargs): kwargs.setdefault('headers', kwargs.get('headers', {})) kwargs['headers']['User-Agent'] = self.USER_AGENT kwargs['headers']['Accept'] = 'application/json' if 'body' in kwargs: kwargs['headers']['Content-Type'] = 'application/json' - kwargs['body'] = json.dumps(kwargs['body']) - - self.http_log_req(args, kwargs) - resp, body = super(HTTPClient, self).request(*args, **kwargs) - self.http_log_resp(resp, body) - - if body: + kwargs['data'] = json.dumps(kwargs['body']) + del kwargs['body'] + + self.http_log_req((url, method,), kwargs) + resp = requests.request( + method, + url, + verify=self.verify_cert, + config=self.requests_config, + **kwargs) + self.http_log_resp(resp) + + if resp.text: + # TODO(dtroyer): verify the note below in a requests context # NOTE(alaski): Because force_exceptions_to_status_code=True # httplib2 returns a connection refused event as a 400 response. # To determine if it is a bad request or refused connection we need # to check the body. httplib2 tests check for 'Connection refused' # or 'actively refused' in the body, so that's what we'll do. - if resp.status == 400: - if 'Connection refused' in body or 'actively refused' in body: - raise exceptions.ConnectionRefused(body) + if resp.status_code == 400: + if ('Connection refused' in resp.text or + 'actively refused' in resp.text): + raise exceptions.ConnectionRefused(resp.text) try: - body = json.loads(body) + body = json.loads(resp.text) except ValueError: pass + body = None else: body = None - if resp.status >= 400: + if resp.status_code >= 400: raise exceptions.from_response(resp, body) return resp, body @@ -254,7 +245,7 @@ def _extract_service_catalog(self, url, resp, body, extract_token=True): We may get redirected to another site, fail or actually get back a service catalog with a token and our endpoints.""" - if resp.status == 200: # content must always present + if resp.status_code == 200: # content must always present try: self.auth_url = url self.service_catalog = \ @@ -281,8 +272,8 @@ def _extract_service_catalog(self, url, resp, body, extract_token=True): print "Could not find any suitable endpoint. Correct region?" raise - elif resp.status == 305: - return resp['location'] + elif resp.status_code == 305: + return resp.headers['location'] else: raise exceptions.from_response(resp, body) @@ -407,16 +398,16 @@ def _v1_auth(self, url): headers['X-Auth-Project-Id'] = self.projectid resp, body = self._time_request(url, 'GET', headers=headers) - if resp.status in (200, 204): # in some cases we get No Content + if resp.status_code in (200, 204): # in some cases we get No Content try: mgmt_header = 'x-server-management-url' - self.management_url = resp[mgmt_header].rstrip('/') - self.auth_token = resp['x-auth-token'] + self.management_url = resp.headers[mgmt_header].rstrip('/') + self.auth_token = resp.headers['x-auth-token'] self.auth_url = url - except KeyError: + except (KeyError, TypeError): raise exceptions.AuthorizationFailure() - elif resp.status == 305: - return resp['location'] + elif resp.status_code == 305: + return resp.headers['location'] else: raise exceptions.from_response(resp, body) @@ -444,13 +435,11 @@ def _authenticate(self, url, body): token_url = url + "/tokens" # Make sure we follow redirects when trying to reach Keystone - tmp_follow_all_redirects = self.follow_all_redirects - self.follow_all_redirects = True - - try: - resp, body = self._time_request(token_url, "POST", body=body) - finally: - self.follow_all_redirects = tmp_follow_all_redirects + resp, body = self._time_request( + token_url, + "POST", + body=body, + allow_redirects=True) return self._extract_service_catalog(url, resp, body) diff --git a/novaclient/exceptions.py b/novaclient/exceptions.py index da6f8d4c8..e64e12d35 100644 --- a/novaclient/exceptions.py +++ b/novaclient/exceptions.py @@ -143,16 +143,19 @@ class HTTPNotImplemented(ClientException): def from_response(response, body): """ Return an instance of an ClientException or subclass - based on an httplib2 response. + based on an requests response. Usage:: - resp, body = http.request(...) - if resp.status != 200: - raise exception_from_response(resp, body) + resp, body = requests.request(...) + if resp.status_code != 200: + raise exception_from_response(resp, rest.text) """ - cls = _code_map.get(response.status, ClientException) - request_id = response.get('x-compute-request-id') + cls = _code_map.get(response.status_code, ClientException) + if response.headers: + request_id = response.headers.get('x-compute-request-id') + else: + request_id = None if body: message = "n/a" details = "n/a" @@ -160,7 +163,7 @@ def from_response(response, body): error = body[body.keys()[0]] message = error.get('message', None) details = error.get('details', None) - return cls(code=response.status, message=message, details=details, + return cls(code=response.status_code, message=message, details=details, request_id=request_id) else: - return cls(code=response.status, request_id=request_id) + return cls(code=response.status_code, request_id=request_id) diff --git a/novaclient/shell.py b/novaclient/shell.py index 07267da9d..d44fd3303 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -20,7 +20,6 @@ import argparse import glob -import httplib2 import imp import itertools import os @@ -196,6 +195,13 @@ def get_base_parser(self): parser.add_argument('--os_compute_api_version', help=argparse.SUPPRESS) + parser.add_argument('--os-cacert', + metavar='', + default=utils.env('OS_CACERT', default=None), + help='Specify a CA bundle file to use in ' + 'verifying a TLS (https) server certificate. ' + 'Defaults to env[OS_CACERT]') + parser.add_argument('--insecure', default=utils.env('NOVACLIENT_INSECURE', default=False), action='store_true', @@ -349,8 +355,6 @@ def setup_debugging(self, debug): logger.setLevel(logging.DEBUG) logger.addHandler(streamhandler) - httplib2.debuglevel = 1 - def main(self, argv): # Parse args once to find version and debug settings parser = self.get_base_parser() @@ -393,7 +397,7 @@ def main(self, argv): os_region_name, os_auth_system, endpoint_type, insecure, service_type, service_name, volume_service_name, username, apikey, projectid, url, region_name, - bypass_url, os_cache) = ( + bypass_url, os_cache, cacert) = ( args.os_username, args.os_password, args.os_tenant_name, args.os_auth_url, args.os_region_name, args.os_auth_system, @@ -401,7 +405,8 @@ def main(self, argv): args.service_name, args.volume_service_name, args.username, args.apikey, args.projectid, args.url, args.region_name, - args.bypass_url, args.os_cache) + args.bypass_url, args.os_cache, + args.os_cacert) if not endpoint_type: endpoint_type = DEFAULT_NOVA_ENDPOINT_TYPE @@ -472,7 +477,8 @@ def main(self, argv): service_name=service_name, auth_system=os_auth_system, volume_service_name=volume_service_name, timings=args.timings, bypass_url=bypass_url, - os_cache=os_cache, http_log_debug=options.debug) + os_cache=os_cache, http_log_debug=options.debug, + cacert=cacert) try: if not utils.isunauthenticated(args.func): diff --git a/novaclient/v1_1/client.py b/novaclient/v1_1/client.py index 3f35beb36..77a4d08f1 100644 --- a/novaclient/v1_1/client.py +++ b/novaclient/v1_1/client.py @@ -56,7 +56,8 @@ def __init__(self, username, api_key, project_id, auth_url=None, service_type='compute', service_name=None, volume_service_name=None, timings=False, bypass_url=None, os_cache=False, no_cache=True, - http_log_debug=False, auth_system='keystone'): + http_log_debug=False, auth_system='keystone', + cacert=None): # FIXME(comstud): Rename the api_key argument above when we # know it's not being used as keyword argument password = api_key @@ -122,7 +123,8 @@ def __init__(self, username, api_key, project_id, auth_url=None, timings=timings, bypass_url=bypass_url, os_cache=self.os_cache, - http_log_debug=http_log_debug) + http_log_debug=http_log_debug, + cacert=cacert) def set_management_url(self, url): self.client.set_management_url(url) diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index 5411b87dc..9ea2cb008 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -611,7 +611,8 @@ def create_image(self, server, image_name, metadata=None): :param meta: Metadata to give newly-created image entity """ body = {'name': image_name, 'metadata': metadata or {}} - location = self._action('createImage', server, body)[0]['location'] + resp = self._action('createImage', server, body)[0] + location = resp.headers['location'] image_uuid = location.split('/')[-1] return image_uuid diff --git a/tests/test_auth_plugins.py b/tests/test_auth_plugins.py index c20ea62de..00b3a7597 100644 --- a/tests/test_auth_plugins.py +++ b/tests/test_auth_plugins.py @@ -13,9 +13,9 @@ # License for the specific language governing permissions and limitations # under the License. -import httplib2 import mock import pkg_resources +import requests try: import json @@ -52,12 +52,11 @@ def mock_http_request(resp=None): }, } - auth_response = httplib2.Response({ - "status": 200, - "body": json.dumps(resp), + auth_response = utils.TestResponse({ + "status_code": 200, + "text": json.dumps(resp), }) - return mock.Mock(return_value=(auth_response, - json.dumps(resp))) + return mock.Mock(return_value=(auth_response)) def requested_headers(cs): @@ -86,7 +85,7 @@ def mock_iter_entry_points(_type): @mock.patch.object(pkg_resources, "iter_entry_points", mock_iter_entry_points) - @mock.patch.object(httplib2.Http, "request", mock_request) + @mock.patch.object(requests, "request", mock_request) def test_auth_call(): cs = client.Client("username", "password", "project_id", "auth_url/v2.0", auth_system="fake") @@ -95,9 +94,13 @@ def test_auth_call(): headers = requested_headers(cs) token_url = cs.client.auth_url + "/tokens" - mock_request.assert_called_with(token_url, "POST", - headers=headers, - body='{"fake": "me"}') + mock_request.assert_called_with( + "POST", + token_url, + headers=headers, + data='{"fake": "me"}', + allow_redirects=True, + **self.TEST_REQUEST_BASE) test_auth_call() @@ -109,7 +112,7 @@ def mock_iter_entry_points(_t): @mock.patch.object(pkg_resources, "iter_entry_points", mock_iter_entry_points) - @mock.patch.object(httplib2.Http, "request", mock_request) + @mock.patch.object(requests, "request", mock_request) def test_auth_call(): cs = client.Client("username", "password", "project_id", "auth_url/v2.0", auth_system="notexists") @@ -146,7 +149,7 @@ def mock_iter_entry_points(_type): @mock.patch.object(pkg_resources, "iter_entry_points", mock_iter_entry_points) - @mock.patch.object(httplib2.Http, "request", mock_request) + @mock.patch.object(requests, "request", mock_request) def test_auth_call(): cs = client.Client("username", "password", "project_id", auth_system="fakewithauthurl") diff --git a/tests/test_http.py b/tests/test_http.py index aab7104b5..57cc481d8 100644 --- a/tests/test_http.py +++ b/tests/test_http.py @@ -1,32 +1,28 @@ -import httplib2 import mock +import requests from novaclient import client from novaclient import exceptions from tests import utils -fake_response = httplib2.Response({"status": 200}) -fake_body = '{"hi": "there"}' -mock_request = mock.Mock(return_value=(fake_response, fake_body)) +fake_response = utils.TestResponse({ + "status_code": 200, + "text": '{"hi": "there"}', +}) +mock_request = mock.Mock(return_value=(fake_response)) -refused_response = httplib2.Response({"status": 400}) -refused_body = '[Errno 111] Connection refused' -refused_mock_request = mock.Mock( - return_value=( - refused_response, - refused_body, - ) -) +refused_response = utils.TestResponse({ + "status_code": 400, + "text": '[Errno 111] Connection refused', +}) +refused_mock_request = mock.Mock(return_value=(refused_response)) -bad_req_response = httplib2.Response({"status": 400}) -bad_req_body = '' -bad_req_mock_request = mock.Mock( - return_value=( - bad_req_response, - bad_req_body, - ) -) +bad_req_response = utils.TestResponse({ + "status_code": 400, + "text": '', +}) +bad_req_mock_request = mock.Mock(return_value=(bad_req_response)) def get_client(): @@ -47,7 +43,7 @@ class ClientTest(utils.TestCase): def test_get(self): cl = get_authed_client() - @mock.patch.object(httplib2.Http, "request", mock_request) + @mock.patch.object(requests, "request", mock_request) @mock.patch('time.time', mock.Mock(return_value=1234)) def test_get_call(): resp, body = cl.get("/hi") @@ -56,8 +52,11 @@ def test_get_call(): "User-Agent": cl.USER_AGENT, 'Accept': 'application/json', } - mock_request.assert_called_with("http://example.com/hi", - "GET", headers=headers) + mock_request.assert_called_with( + "GET", + "http://example.com/hi", + headers=headers, + **self.TEST_REQUEST_BASE) # Automatic JSON parsing self.assertEqual(body, {"hi": "there"}) @@ -66,7 +65,7 @@ def test_get_call(): def test_post(self): cl = get_authed_client() - @mock.patch.object(httplib2.Http, "request", mock_request) + @mock.patch.object(requests, "request", mock_request) def test_post_call(): cl.post("/hi", body=[1, 2, 3]) headers = { @@ -76,8 +75,12 @@ def test_post_call(): 'Accept': 'application/json', "User-Agent": cl.USER_AGENT } - mock_request.assert_called_with("http://example.com/hi", "POST", - headers=headers, body='[1, 2, 3]') + mock_request.assert_called_with( + "POST", + "http://example.com/hi", + headers=headers, + data='[1, 2, 3]', + **self.TEST_REQUEST_BASE) test_post_call() @@ -85,7 +88,7 @@ def test_auth_failure(self): cl = get_client() # response must not have x-server-management-url header - @mock.patch.object(httplib2.Http, "request", mock_request) + @mock.patch.object(requests, "request", mock_request) def test_auth_call(): self.assertRaises(exceptions.AuthorizationFailure, cl.authenticate) @@ -94,7 +97,7 @@ def test_auth_call(): def test_connection_refused(self): cl = get_client() - @mock.patch.object(httplib2.Http, "request", refused_mock_request) + @mock.patch.object(requests, "request", refused_mock_request) def test_refused_call(): self.assertRaises(exceptions.ConnectionRefused, cl.get, "/hi") @@ -103,7 +106,7 @@ def test_refused_call(): def test_bad_request(self): cl = get_client() - @mock.patch.object(httplib2.Http, "request", bad_req_mock_request) + @mock.patch.object(requests, "request", bad_req_mock_request) def test_refused_call(): self.assertRaises(exceptions.BadRequest, cl.get, "/hi") diff --git a/tests/test_shell.py b/tests/test_shell.py index 87f477201..79c3d2f48 100644 --- a/tests/test_shell.py +++ b/tests/test_shell.py @@ -1,6 +1,5 @@ import cStringIO import os -import httplib2 import sys from novaclient import exceptions @@ -44,11 +43,6 @@ def tearDown(self): def test_help_unknown_command(self): self.assertRaises(exceptions.CommandError, self.shell, 'help foofoo') - def test_debug(self): - httplib2.debuglevel = 0 - self.shell('--debug help') - assert httplib2.debuglevel == 1 - def test_help(self): required = [ '^usage: ', diff --git a/tests/utils.py b/tests/utils.py index 7f1c5dc71..3bbe8bbad 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,5 +1,34 @@ +import requests import unittest2 class TestCase(unittest2.TestCase): - pass + TEST_REQUEST_BASE = { + 'config': {'danger_mode': False}, + 'verify': True, + } + + +class TestResponse(requests.Response): + """ + Class used to wrap requests.Response and provide some + convenience to initialize with a dict + """ + + def __init__(self, data): + self._text = None + super(TestResponse, self) + if isinstance(data, dict): + self.status_code = data.get('status_code', None) + self.headers = data.get('headers', None) + # Fake the text attribute to streamline Response creation + self._text = data.get('text', None) + else: + self.status_code = data + + def __eq__(self, other): + return self.__dict__ == other.__dict__ + + @property + def text(self): + return self._text diff --git a/tests/v1_1/fakes.py b/tests/v1_1/fakes.py index 5d0f72a41..545701ba9 100644 --- a/tests/v1_1/fakes.py +++ b/tests/v1_1/fakes.py @@ -14,12 +14,14 @@ # limitations under the License. from datetime import datetime -import httplib2 import urlparse +import requests + from novaclient import client as base_client from novaclient.v1_1 import client from tests import fakes +from tests import utils class FakeClient(fakes.FakeClient, client.Client): @@ -63,11 +65,17 @@ def _cs_request(self, url, method, **kwargs): # Note the call self.callstack.append((method, url, kwargs.get('body', None))) - status, body = getattr(self, callback)(**kwargs) - if hasattr(status, 'items'): - return httplib2.Response(status), body + if 'body' in kwargs: + b = kwargs['body'] else: - return httplib2.Response({"status": status}), body + b = '' + status, headers, body = getattr(self, callback)(**kwargs) + r = utils.TestResponse({ + "status_code": status, + "text": body, + "headers": headers, + }) + return r, body # # agents @@ -75,7 +83,7 @@ def _cs_request(self, url, method, **kwargs): def get_os_agents(self, **kw): hypervisor = kw.get('hypervisor', 'kvm') - return (200, {'agents': + return (200, {}, {'agents': [{'hypervisor': hypervisor, 'os': 'win', 'architecture': 'x86', @@ -93,7 +101,7 @@ def get_os_agents(self, **kw): ]}) def post_os_agents(self, body): - return (200, {'agent': { + return (200, {}, {'agent': { 'url': '/xxx/xxx/xxx', 'hypervisor': body['agent']['hypervisor'], 'md5hash': 'add6bb58e139be103324d04d82d8f546', @@ -103,10 +111,10 @@ def post_os_agents(self, body): 'id': 1}}) def delete_os_agents_1(self, **kw): - return (202, None) + return (202, {}, None) def put_os_agents_1(self, body, **kw): - return (200, {"agent": { + return (200, {}, {"agent": { "url": "/yyy/yyyy/yyyy", "version": "8.0", "md5hash": "add6bb58e139be103324d04d82d8f546", @@ -155,7 +163,7 @@ def get_extensions(self, **kw): "updated": "2011-11-03T00:00:00+00:00" }, ] - return (200, { + return (200, {}, { "extensions": exts, }) @@ -164,7 +172,7 @@ def get_extensions(self, **kw): # def get_limits(self, **kw): - return (200, {"limits": { + return (200, {}, {"limits": { "rate": [ { "uri": "*", @@ -222,13 +230,13 @@ def get_limits(self, **kw): # def get_servers(self, **kw): - return (200, {"servers": [ + return (200, {}, {"servers": [ {'id': 1234, 'name': 'sample-server'}, {'id': 5678, 'name': 'sample-server2'} ]}) def get_servers_detail(self, **kw): - return (200, {"servers": [ + return (200, {}, {"servers": [ { "id": 1234, "name": "sample-server", @@ -331,52 +339,52 @@ def post_servers(self, body, **kw): if 'personality' in body['server']: for pfile in body['server']['personality']: fakes.assert_has_keys(pfile, required=['path', 'contents']) - return (202, self.get_servers_1234()[1]) + return (202, {}, self.get_servers_1234()[2]) def post_os_volumes_boot(self, body, **kw): assert set(body.keys()) <= set(['server', 'os:scheduler_hints']) fakes.assert_has_keys(body['server'], required=['name', 'block_device_mapping', 'flavorRef'], optional=['imageRef']) - return (202, self.get_servers_9012()[1]) + return (202, {}, self.get_servers_9012()[2]) def get_servers_1234(self, **kw): - r = {'server': self.get_servers_detail()[1]['servers'][0]} - return (200, r) + r = {'server': self.get_servers_detail()[2]['servers'][0]} + return (200, {}, r) def get_servers_5678(self, **kw): - r = {'server': self.get_servers_detail()[1]['servers'][1]} - return (200, r) + r = {'server': self.get_servers_detail()[2]['servers'][1]} + return (200, {}, r) def get_servers_9012(self, **kw): - r = {'server': self.get_servers_detail()[1]['servers'][2]} - return (200, r) + r = {'server': self.get_servers_detail()[2]['servers'][2]} + return (200, {}, r) def put_servers_1234(self, body, **kw): assert body.keys() == ['server'] fakes.assert_has_keys(body['server'], optional=['name', 'adminPass']) - return (204, None) + return (204, {}, None) def delete_servers_1234(self, **kw): - return (202, None) + return (202, {}, None) def delete_servers_1234_metadata_test_key(self, **kw): - return (204, None) + return (204, {}, None) def delete_servers_1234_metadata_key1(self, **kw): - return (204, None) + return (204, {}, None) def delete_servers_1234_metadata_key2(self, **kw): - return (204, None) + return (204, {}, None) def post_servers_1234_metadata(self, **kw): - return (204, {'metadata': {'test_key': 'test_value'}}) + return (204, {}, {'metadata': {'test_key': 'test_value'}}) def get_servers_1234_diagnostics(self, **kw): - return (200, {'data': 'Fake diagnostics'}) + return (200, {}, {'data': 'Fake diagnostics'}) def get_servers_1234_actions(self, **kw): - return (200, {'actions': [ + return (200, {}, {'actions': [ { 'action': 'rebuild', 'error': None, @@ -394,25 +402,26 @@ def get_servers_1234_actions(self, **kw): # def get_servers_1234_ips(self, **kw): - return (200, {'addresses': + return (200, {}, {'addresses': self.get_servers_1234()[1]['server']['addresses']}) def get_servers_1234_ips_public(self, **kw): - return (200, {'public': + return (200, {}, {'public': self.get_servers_1234_ips()[1]['addresses']['public']}) def get_servers_1234_ips_private(self, **kw): - return (200, {'private': + return (200, {}, {'private': self.get_servers_1234_ips()[1]['addresses']['private']}) def delete_servers_1234_ips_public_1_2_3_4(self, **kw): - return (202, None) + return (202, {}, None) # # Server actions # def post_servers_1234_action(self, body, **kw): + _headers = None _body = None resp = 202 assert len(body.keys()) == 1 @@ -425,13 +434,13 @@ def post_servers_1234_action(self, body, **kw): if 'adminPass' in keys: keys.remove('adminPass') assert keys == ['imageRef'] - _body = self.get_servers_1234()[1] + _body = self.get_servers_1234()[2] elif action == 'resize': assert body[action].keys() == ['flavorRef'] elif action == 'confirmResize': assert body[action] is None # This one method returns a different response code - return (204, None) + return (204, {}, None) elif action == 'revertResize': assert body[action] is None elif action == 'migrate': @@ -458,12 +467,12 @@ def post_servers_1234_action(self, body, **kw): assert body[action].keys() == ['address'] elif action == 'createImage': assert set(body[action].keys()) == set(['name', 'metadata']) - resp = dict(status=202, location="http://blah/images/456") + _headers = dict(location="http://blah/images/456") elif action == 'changePassword': assert body[action].keys() == ['adminPass'] elif action == 'os-getConsoleOutput': assert body[action].keys() == ['length'] - return (202, {'output': 'foo'}) + return (202, {}, {'output': 'foo'}) elif action == 'os-getVNCConsole': assert body[action].keys() == ['type'] elif action == 'os-migrateLive': @@ -482,36 +491,42 @@ def post_servers_1234_action(self, body, **kw): 'rotation']) else: raise AssertionError("Unexpected server action: %s" % action) - return (resp, _body) + return (resp, _headers, _body) # # Cloudpipe # def get_os_cloudpipe(self, **kw): - return (200, {'cloudpipes': [ - {'project_id':1} - ]}) + return ( + 200, + {}, + {'cloudpipes': [{'project_id':1}]} + ) def post_os_cloudpipe(self, **ks): - return (202, {'instance_id': '9d5824aa-20e6-4b9f-b967-76a699fc51fd'}) + return ( + 202, + {}, + {'instance_id': '9d5824aa-20e6-4b9f-b967-76a699fc51fd'} + ) def put_os_cloudpipe_configure_project(self, **kw): - return (202, None) + return (202, {}, None) # # Flavors # def get_flavors(self, **kw): - return (200, {'flavors': [ + return (200, {}, {'flavors': [ {'id': 1, 'name': '256 MB Server'}, {'id': 2, 'name': '512 MB Server'}, {'id': 'aa1', 'name': '128 MB Server'} ]}) def get_flavors_detail(self, **kw): - return (200, {'flavors': [ + return (200, {}, {'flavors': [ {'id': 1, 'name': '256 MB Server', 'ram': 256, 'disk': 10, 'OS-FLV-EXT-DATA:ephemeral': 10, 'os-flavor-access:is_public': True, @@ -527,39 +542,65 @@ def get_flavors_detail(self, **kw): ]}) def get_flavors_1(self, **kw): - return (200, {'flavor': self.get_flavors_detail()[1]['flavors'][0]}) + return ( + 200, + {}, + {'flavor': self.get_flavors_detail()[2]['flavors'][0]} + ) def get_flavors_2(self, **kw): - return (200, {'flavor': self.get_flavors_detail()[1]['flavors'][1]}) + return ( + 200, + {}, + {'flavor': self.get_flavors_detail()[2]['flavors'][1]} + ) def get_flavors_3(self, **kw): # Diablo has no ephemeral - return (200, {'flavor': {'id': 3, 'name': '256 MB Server', - 'ram': 256, 'disk': 10}}) + return ( + 200, + {}, + {'flavor': { + 'id': 3, + 'name': '256 MB Server', + 'ram': 256, + 'disk': 10, + }}, + ) def get_flavors_aa1(self, **kw): # Aplhanumeric flavor id are allowed. - return (200, {'flavor': self.get_flavors_detail()[1]['flavors'][2]}) + return ( + 200, + {}, + {'flavor': self.get_flavors_detail()[2]['flavors'][2]} + ) def delete_flavors_flavordelete(self, **kw): - return (202, None) + return (202, {}, None) def delete_flavors_2(self, **kw): - return (202, None) + return (202, {}, None) def post_flavors(self, body, **kw): - return (202, {'flavor': self.get_flavors_detail()[1]['flavors'][0]}) + return ( + 202, + {}, + {'flavor': self.get_flavors_detail()[2]['flavors'][0]} + ) def get_flavors_1_os_extra_specs(self, **kw): return (200, + {}, {'extra_specs': {"k1": "v1"}}) def get_flavors_2_os_extra_specs(self, **kw): return (200, + {}, {'extra_specs': {"k2": "v2"}}) def get_flavors_aa1_os_extra_specs(self, **kw): - return (200, + return (200, {}, {'extra_specs': {"k3": "v3"}}) def post_flavors_1_os_extra_specs(self, body, **kw): @@ -567,69 +608,78 @@ def post_flavors_1_os_extra_specs(self, body, **kw): fakes.assert_has_keys(body['extra_specs'], required=['k1']) return (200, + {}, {'extra_specs': {"k1": "v1"}}) def delete_flavors_1_os_extra_specs_k1(self, **kw): - return (204, None) + return (204, {}, None) # # Flavor access # def get_flavors_1_os_flavor_access(self, **kw): - return (404, None) + return (404, {}, None) def get_flavors_2_os_flavor_access(self, **kw): - return (200, {'flavor_access': [ + return (200, {}, {'flavor_access': [ {'flavor_id': '2', 'tenant_id': 'proj1'}, {'flavor_id': '2', 'tenant_id': 'proj2'} ]}) def post_flavors_2_action(self, body, **kw): - return (202, self.get_flavors_2_os_flavor_access()[1]) + return (202, {}, self.get_flavors_2_os_flavor_access()[2]) # # Floating ips # def get_os_floating_ip_pools(self): - return (200, {'floating_ip_pools': [{'name': 'foo', 'name': 'bar'}]}) + return ( + 200, + {}, + {'floating_ip_pools': [{'name': 'foo', 'name': 'bar'}]} + ) def get_os_floating_ips(self, **kw): - return (200, {'floating_ips': [ - {'id': 1, 'fixed_ip': '10.0.0.1', 'ip': '11.0.0.1'}, - {'id': 2, 'fixed_ip': '10.0.0.2', 'ip': '11.0.0.2'}, - ]}) + return ( + 200, + {}, + {'floating_ips': [ + {'id': 1, 'fixed_ip': '10.0.0.1', 'ip': '11.0.0.1'}, + {'id': 2, 'fixed_ip': '10.0.0.2', 'ip': '11.0.0.2'}, + ]}, + ) def get_os_floating_ips_1(self, **kw): - return (200, {'floating_ip': + return (200, {}, {'floating_ip': {'id': 1, 'fixed_ip': '10.0.0.1', 'ip': '11.0.0.1'} }) def post_os_floating_ips(self, body, **kw): - return (202, self.get_os_floating_ips_1()[1]) + return (202, {}, self.get_os_floating_ips_1()[1]) def post_os_floating_ips(self, body): if body.get('pool'): - return (200, {'floating_ip': + return (200, {}, {'floating_ip': {'id': 1, 'fixed_ip': '10.0.0.1', 'ip': '11.0.0.1', 'pool': 'nova'}}) else: - return (200, {'floating_ip': + return (200, {}, {'floating_ip': {'id': 1, 'fixed_ip': '10.0.0.1', 'ip': '11.0.0.1', 'pool': None}}) def delete_os_floating_ips_1(self, **kw): - return (204, None) + return (204, {}, None) def get_os_floating_ip_dns(self, **kw): - return (205, {'domain_entries': - [{'domain': 'example.org'}, - {'domain': 'example.com'}]}) + return (205, {}, {'domain_entries': + [{'domain': 'example.org'}, + {'domain': 'example.com'}]}) def get_os_floating_ip_dns_testdomain_entries(self, **kw): if kw.get('ip'): - return (205, {'dns_entries': + return (205, {}, {'dns_entries': [{'dns_entry': {'ip': kw.get('ip'), 'name': "host1", @@ -641,10 +691,10 @@ def get_os_floating_ip_dns_testdomain_entries(self, **kw): 'type': "A", 'domain': 'testdomain'}}]}) else: - return (404, None) + return (404, {}, None) def get_os_floating_ip_dns_testdomain_entries_testname(self, **kw): - return (205, {'dns_entry': + return (205, {}, {'dns_entry': {'ip': "10.10.10.10", 'name': 'testname', 'type': "A", @@ -661,27 +711,27 @@ def put_os_floating_ip_dns_testdomain(self, body, **kw): else: fakes.assert_has_keys(body['domain_entry'], required=['project', 'scope']) - return (205, None) + return (205, {}, None) def put_os_floating_ip_dns_testdomain_entries_testname(self, body, **kw): fakes.assert_has_keys(body['dns_entry'], required=['ip', 'dns_type']) - return (205, None) + return (205, {}, None) def delete_os_floating_ip_dns_testdomain(self, **kw): - return (200, None) + return (200, {}, None) def delete_os_floating_ip_dns_testdomain_entries_testname(self, **kw): - return (200, None) + return (200, {}, None) def get_os_floating_ips_bulk(self, **kw): - return (200, {'floating_ip_info': [ + return (200, {}, {'floating_ip_info': [ {'id': 1, 'fixed_ip': '10.0.0.1', 'ip': '11.0.0.1'}, {'id': 2, 'fixed_ip': '10.0.0.2', 'ip': '11.0.0.2'}, ]}) def get_os_floating_ips_bulk_testHost(self, **kw): - return (200, {'floating_ip_info': [ + return (200, {}, {'floating_ip_info': [ {'id': 1, 'fixed_ip': '10.0.0.1', 'ip': '11.0.0.1'}, {'id': 2, 'fixed_ip': '10.0.0.2', 'ip': '11.0.0.2'}, ]}) @@ -690,26 +740,26 @@ def post_os_floating_ips_bulk(self, **kw): params = kw.get('body').get('floating_ips_bulk_create') pool = params.get('pool', 'defaultPool') interface = params.get('interface', 'defaultInterface') - return (200, {'floating_ips_bulk_create': + return (200, {}, {'floating_ips_bulk_create': {'ip_range': '192.168.1.0/30', 'pool': pool, 'interface': interface}}) def put_os_floating_ips_bulk_delete(self, **kw): ip_range = kw.get('body').get('ip_range') - return (200, {'floating_ips_bulk_delete': ip_range}) + return (200, {}, {'floating_ips_bulk_delete': ip_range}) # # Images # def get_images(self, **kw): - return (200, {'images': [ + return (200, {}, {'images': [ {'id': 1, 'name': 'CentOS 5.2'}, {'id': 2, 'name': 'My Server Backup'} ]}) def get_images_detail(self, **kw): - return (200, {'images': [ + return (200, {}, {'images': [ { 'id': 1, 'name': 'CentOS 5.2', @@ -734,52 +784,53 @@ def get_images_detail(self, **kw): ]}) def get_images_1(self, **kw): - return (200, {'image': self.get_images_detail()[1]['images'][0]}) + return (200, {}, {'image': self.get_images_detail()[2]['images'][0]}) def get_images_2(self, **kw): - return (200, {'image': self.get_images_detail()[1]['images'][1]}) + return (200, {}, {'image': self.get_images_detail()[2]['images'][1]}) def post_images(self, body, **kw): assert body.keys() == ['image'] fakes.assert_has_keys(body['image'], required=['serverId', 'name']) - return (202, self.get_images_1()[1]) + return (202, {}, self.get_images_1()[2]) def post_images_1_metadata(self, body, **kw): assert body.keys() == ['metadata'] fakes.assert_has_keys(body['metadata'], required=['test_key']) return (200, - {'metadata': self.get_images_1()[1]['image']['metadata']}) + {}, + {'metadata': self.get_images_1()[2]['image']['metadata']}) def delete_images_1(self, **kw): - return (204, None) + return (204, {}, None) def delete_images_1_metadata_test_key(self, **kw): - return (204, None) + return (204, {}, None) # # Keypairs # def get_os_keypairs(self, *kw): - return (200, {"keypairs": [ + return (200, {}, {"keypairs": [ {'fingerprint': 'FAKE_KEYPAIR', 'name': 'test'} ]}) def delete_os_keypairs_test(self, **kw): - return (202, None) + return (202, {}, None) def post_os_keypairs(self, body, **kw): assert body.keys() == ['keypair'] fakes.assert_has_keys(body['keypair'], required=['name']) - r = {'keypair': self.get_os_keypairs()[1]['keypairs'][0]} - return (202, r) + r = {'keypair': self.get_os_keypairs()[2]['keypairs'][0]} + return (202, {}, r) # # Virtual Interfaces # def get_servers_1234_os_virtual_interfaces(self, **kw): - return (200, {"virtual_interfaces": [ + return (200, {}, {"virtual_interfaces": [ {'id': 'fakeid', 'mac_address': 'fakemac'} ]}) @@ -788,7 +839,7 @@ def get_servers_1234_os_virtual_interfaces(self, **kw): # def get_os_quota_sets_test(self, **kw): - return (200, {'quota_set': { + return (200, {}, {'quota_set': { 'tenant_id': 'test', 'metadata_items': [], 'injected_file_content_bytes': 1, @@ -805,7 +856,7 @@ def get_os_quota_sets_test(self, **kw): 'security_group_rules': 1}}) def get_os_quota_sets_test_defaults(self): - return (200, {'quota_set': { + return (200, {}, {'quota_set': { 'tenant_id': 'test', 'metadata_items': [], 'injected_file_content_bytes': 1, @@ -825,7 +876,7 @@ def put_os_quota_sets_test(self, body, **kw): assert body.keys() == ['quota_set'] fakes.assert_has_keys(body['quota_set'], required=['tenant_id']) - return (200, {'quota_set': { + return (200, {}, {'quota_set': { 'tenant_id': 'test', 'metadata_items': [], 'injected_file_content_bytes': 1, @@ -846,7 +897,7 @@ def put_os_quota_sets_test(self, body, **kw): # def get_os_quota_class_sets_test(self, **kw): - return (200, {'quota_class_set': { + return (200, {}, {'quota_class_set': { 'class_name': 'test', 'metadata_items': [], 'injected_file_content_bytes': 1, @@ -866,7 +917,7 @@ def put_os_quota_class_sets_test(self, body, **kw): assert body.keys() == ['quota_class_set'] fakes.assert_has_keys(body['quota_class_set'], required=['class_name']) - return (200, {'quota_class_set': { + return (200, {}, {'quota_class_set': { 'class_name': 'test', 'metadata_items': [], 'injected_file_content_bytes': 1, @@ -886,39 +937,39 @@ def put_os_quota_class_sets_test(self, body, **kw): # Security Groups # def get_os_security_groups(self, **kw): - return (200, {"security_groups": [ + return (200, {}, {"security_groups": [ {'id': 1, 'name': 'test', 'description': 'FAKE_SECURITY_GROUP', 'tenant_id': '4ffc664c198e435e9853f2538fbcd7a7'} ]}) def get_os_security_groups_1(self, **kw): - return (200, {"security_group": + return (200, {}, {"security_group": {'id': 1, 'name': 'test', 'description': 'FAKE_SECURITY_GROUP'} }) def delete_os_security_groups_1(self, **kw): - return (202, None) + return (202, {}, None) def post_os_security_groups(self, body, **kw): assert body.keys() == ['security_group'] fakes.assert_has_keys(body['security_group'], required=['name', 'description']) r = {'security_group': - self.get_os_security_groups()[1]['security_groups'][0]} - return (202, r) + self.get_os_security_groups()[2]['security_groups'][0]} + return (202, {}, r) # # Security Group Rules # def get_os_security_group_rules(self, **kw): - return (200, {"security_group_rules": [ + return (200, {}, {"security_group_rules": [ {'id': 1, 'parent_group_id': 1, 'group_id': 2, 'ip_protocol': 'TCP', 'from_port': '22', 'to_port': 22, 'cidr': '10.0.0.0/8'} ]}) def delete_os_security_group_rules_1(self, **kw): - return (202, None) + return (202, {}, None) def post_os_security_group_rules(self, body, **kw): assert body.keys() == ['security_group_rule'] @@ -927,14 +978,14 @@ def post_os_security_group_rules(self, body, **kw): optional=['group_id', 'ip_protocol', 'from_port', 'to_port', 'cidr']) r = {'security_group_rule': - self.get_os_security_group_rules()[1]['security_group_rules'][0]} - return (202, r) + self.get_os_security_group_rules()[2]['security_group_rules'][0]} + return (202, {}, r) # # Tenant Usage # def get_os_simple_tenant_usage(self, **kw): - return (200, {u'tenant_usages': [{ + return (200, {}, {u'tenant_usages': [{ u'total_memory_mb_usage': 25451.762807466665, u'total_vcpus_usage': 49.71047423333333, u'total_hours': 49.71047423333333, @@ -952,7 +1003,7 @@ def get_os_simple_tenant_usage(self, **kw): u'total_local_gb_usage': 0.0}]}) def get_os_simple_tenant_usage_tenantfoo(self, **kw): - return (200, {u'tenant_usage': { + return (200, {}, {u'tenant_usage': { u'total_memory_mb_usage': 25451.762807466665, u'total_vcpus_usage': 49.71047423333333, u'total_hours': 49.71047423333333, @@ -973,16 +1024,24 @@ def get_os_simple_tenant_usage_tenantfoo(self, **kw): # Certificates # def get_os_certificates_root(self, **kw): - return (200, {'certificate': {'private_key': None, 'data': 'foo'}}) + return ( + 200, + {}, + {'certificate': {'private_key': None, 'data': 'foo'}} + ) def post_os_certificates(self, **kw): - return (200, {'certificate': {'private_key': 'foo', 'data': 'bar'}}) + return ( + 200, + {}, + {'certificate': {'private_key': 'foo', 'data': 'bar'}} + ) # # Aggregates # def get_os_aggregates(self, *kw): - return (200, {"aggregates": [ + return (200, {}, {"aggregates": [ {'id':'1', 'name': 'test', 'availability_zone': 'nova1'}, @@ -992,8 +1051,8 @@ def get_os_aggregates(self, *kw): ]}) def _return_aggregate(self): - r = {'aggregate': self.get_os_aggregates()[1]['aggregates'][0]} - return (200, r) + r = {'aggregate': self.get_os_aggregates()[2]['aggregates'][0]} + return (200, {}, r) def get_os_aggregates_1(self, **kw): return self._return_aggregate() @@ -1014,7 +1073,7 @@ def post_os_aggregates_2_action(self, body, **kw): return self._return_aggregate() def delete_os_aggregates_1(self, **kw): - return (202, None) + return (202, {}, None) # # Services @@ -1022,7 +1081,7 @@ def delete_os_aggregates_1(self, **kw): def get_os_services(self, **kw): host = kw.get('host', 'host1') service = kw.get('service', 'nova-compute') - return (200, {'services': + return (200, {}, {'services': [{'binary': service, 'host': host, 'zone': 'nova', @@ -1038,31 +1097,31 @@ def get_os_services(self, **kw): ]}) def put_os_services_enable(self, body, **kw): - return (200, {'host': body['host'], 'service': body['service'], + return (200, {}, {'host': body['host'], 'service': body['service'], 'disabled': False}) def put_os_services_disable(self, body, **kw): - return (200, {'host': body['host'], 'service': body['service'], + return (200, {}, {'host': body['host'], 'service': body['service'], 'disabled': True}) # # Fixed IPs # def get_os_fixed_ips_192_168_1_1(self, *kw): - return (200, {"fixed_ip": + return (200, {}, {"fixed_ip": {'cidr': '192.168.1.0/24', 'address': '192.168.1.1', 'hostname': 'foo', 'host': 'bar'}}) def post_os_fixed_ips_192_168_1_1_action(self, body, **kw): - return (202, None) + return (202, {}, None) # # Hosts # def get_os_hosts_host(self, *kw): - return (200, {'host': + return (200, {}, {'host': [{'resource': {'project': '(total)', 'host': 'dummy', 'cpu': 16, 'memory_mb': 32234, 'disk_gb': 128}}, {'resource': {'project': '(used_now)', 'host': 'dummy', @@ -1074,7 +1133,7 @@ def get_os_hosts_host(self, *kw): def get_os_hosts(self, **kw): zone = kw.get('zone', 'nova1') - return (200, {'hosts': + return (200, {}, {'hosts': [{'host': 'host1', 'service': 'nova-compute', 'zone': zone}, @@ -1083,46 +1142,46 @@ def get_os_hosts(self, **kw): 'zone': zone}]}) def get_os_hosts_sample_host(self, *kw): - return (200, {'host': [{'resource': {'host': 'sample_host'}}], }) + return (200, {}, {'host': [{'resource': {'host': 'sample_host'}}], }) def put_os_hosts_sample_host_1(self, body, **kw): - return (200, {'host': 'sample-host_1', + return (200, {}, {'host': 'sample-host_1', 'status': 'enabled'}) def put_os_hosts_sample_host_2(self, body, **kw): - return (200, {'host': 'sample-host_2', + return (200, {}, {'host': 'sample-host_2', 'maintenance_mode': 'on_maintenance'}) def put_os_hosts_sample_host_3(self, body, **kw): - return (200, {'host': 'sample-host_3', + return (200, {}, {'host': 'sample-host_3', 'status': 'enabled', 'maintenance_mode': 'on_maintenance'}) def get_os_hosts_sample_host_startup(self, **kw): - return (200, {'host': 'sample_host', + return (200, {}, {'host': 'sample_host', 'power_action': 'startup'}) def get_os_hosts_sample_host_reboot(self, **kw): - return (200, {'host': 'sample_host', + return (200, {}, {'host': 'sample_host', 'power_action': 'reboot'}) def get_os_hosts_sample_host_shutdown(self, **kw): - return (200, {'host': 'sample_host', + return (200, {}, {'host': 'sample_host', 'power_action': 'shutdown'}) def put_os_hosts_sample_host(self, body, **kw): result = {'host': 'dummy'} result.update(body) - return (200, result) + return (200, {}, result) def get_os_hypervisors(self, **kw): - return (200, {"hypervisors": [ + return (200, {}, {"hypervisors": [ {'id': 1234, 'hypervisor_hostname': 'hyper1'}, {'id': 5678, 'hypervisor_hostname': 'hyper2'}, ]}) def get_os_hypervisors_detail(self, **kw): - return (200, {"hypervisors": [ + return (200, {}, {"hypervisors": [ {'id': 1234, 'service': {'id': 1, 'host': 'compute1'}, 'vcpus': 4, @@ -1160,7 +1219,7 @@ def get_os_hypervisors_detail(self, **kw): ]}) def get_os_hypervisors_statistics(self, **kw): - return (200, {"hypervisor_statistics": { + return (200, {}, {"hypervisor_statistics": { 'count': 2, 'vcpus': 8, 'memory_mb': 20 * 1024, @@ -1176,13 +1235,13 @@ def get_os_hypervisors_statistics(self, **kw): }}) def get_os_hypervisors_hyper_search(self, **kw): - return (200, {'hypervisors': [ + return (200, {}, {'hypervisors': [ {'id': 1234, 'hypervisor_hostname': 'hyper1'}, {'id': 5678, 'hypervisor_hostname': 'hyper2'} ]}) def get_os_hypervisors_hyper_servers(self, **kw): - return (200, {'hypervisors': [ + return (200, {}, {'hypervisors': [ {'id': 1234, 'hypervisor_hostname': 'hyper1', 'servers': [ @@ -1198,7 +1257,7 @@ def get_os_hypervisors_hyper_servers(self, **kw): ]}) def get_os_hypervisors_1234(self, **kw): - return (200, {'hypervisor': + return (200, {}, {'hypervisor': {'id': 1234, 'service': {'id': 1, 'host': 'compute1'}, 'vcpus': 4, @@ -1218,37 +1277,37 @@ def get_os_hypervisors_1234(self, **kw): 'disk_available_least': 100}}) def get_os_hypervisors_1234_uptime(self, **kw): - return (200, {'hypervisor': + return (200, {}, {'hypervisor': {'id': 1234, 'hypervisor_hostname': "hyper1", 'uptime': "fake uptime"}}) def get_os_networks(self, **kw): - return (200, {'networks': [{"label": "1", "cidr": "10.0.0.0/24", + return (200, {}, {'networks': [{"label": "1", "cidr": "10.0.0.0/24", 'project_id': '4ffc664c198e435e9853f2538fbcd7a7', 'id': '1'}]}) def get_os_networks_1(self, **kw): - return (200, {'network': {"label": "1", "cidr": "10.0.0.0/24"}}) + return (200, {}, {'network': {"label": "1", "cidr": "10.0.0.0/24"}}) def post_os_networks(self, **kw): - return (202, {'network': kw}) + return (202, {}, {'network': kw}) def post_os_networks_1_action(self, **kw): - return (202, None) + return (202, {}, None) def delete_os_networks_networkdelete(self, **kw): - return (202, None) + return (202, {}, None) def post_os_networks_add(self, **kw): - return (202, None) + return (202, {}, None) def post_os_networks_networkdisassociate_action(self, **kw): - return (202, None) + return (202, {}, None) def get_os_fping(self, **kw): return ( - 200, { + 200, {}, { 'servers': [ { "id": "1", @@ -1266,7 +1325,7 @@ def get_os_fping(self, **kw): def get_os_fping_1(self, **kw): return ( - 200, { + 200, {}, { 'server': { "id": "1", "project_id": "fake-project", @@ -1276,21 +1335,21 @@ def get_os_fping_1(self, **kw): ) def post_os_networks(self, **kw): - return (202, {'network': kw}) + return (202, {}, {'network': kw}) def post_os_networks_1_action(self, **kw): - return (202, None) + return (202, {}, None) def post_os_networks_networktest_action(self, **kw): - return (202, None) + return (202, {}, None) def post_os_networks_2_action(self, **kw): - return (202, None) + return (202, {}, None) def post_os_coverage_action(self, body, **kw): if 'report' not in body: - return (200, None) + return (200, {}, None) else: - return (200, { + return (200, {}, { 'path': '/tmp/tmpdir/' + body['report']['file'] }) diff --git a/tests/v1_1/test_auth.py b/tests/v1_1/test_auth.py index 5f312d637..d10b49cb2 100644 --- a/tests/v1_1/test_auth.py +++ b/tests/v1_1/test_auth.py @@ -1,20 +1,14 @@ -import httplib2 +import copy import json import mock +import requests + from novaclient.v1_1 import client from novaclient import exceptions from tests import utils -def to_http_response(resp_dict): - """Converts dict of response attributes to httplib response.""" - resp = httplib2.Response(resp_dict) - for k, v in resp_dict['headers'].items(): - resp[k] = v - return resp - - class AuthenticateAgainstKeystoneTests(utils.TestCase): def test_authenticate_success(self): cs = client.Client("username", "password", "project_id", @@ -40,15 +34,14 @@ def test_authenticate_success(self): ], }, } - auth_response = httplib2.Response({ - "status": 200, - "body": json.dumps(resp), - }) + auth_response = utils.TestResponse({ + "status_code": 200, + "text": json.dumps(resp), + }) - mock_request = mock.Mock(return_value=(auth_response, - json.dumps(resp))) + mock_request = mock.Mock(return_value=(auth_response)) - @mock.patch.object(httplib2.Http, "request", mock_request) + @mock.patch.object(requests, "request", mock_request) def test_auth_call(): cs.client.authenticate() headers = { @@ -67,9 +60,13 @@ def test_auth_call(): } token_url = cs.client.auth_url + "/tokens" - mock_request.assert_called_with(token_url, "POST", - headers=headers, - body=json.dumps(body)) + mock_request.assert_called_with( + "POST", + token_url, + headers=headers, + data=json.dumps(body), + allow_redirects=True, + **self.TEST_REQUEST_BASE) endpoints = resp["access"]["serviceCatalog"][0]['endpoints'] public_url = endpoints[0]["publicURL"].rstrip('/') @@ -83,15 +80,14 @@ def test_authenticate_failure(self): cs = client.Client("username", "password", "project_id", "auth_url/v2.0") resp = {"unauthorized": {"message": "Unauthorized", "code": "401"}} - auth_response = httplib2.Response({ - "status": 401, - "body": json.dumps(resp), - }) + auth_response = utils.TestResponse({ + "status_code": 401, + "text": json.dumps(resp), + }) - mock_request = mock.Mock(return_value=(auth_response, - json.dumps(resp))) + mock_request = mock.Mock(return_value=(auth_response)) - @mock.patch.object(httplib2.Http, "request", mock_request) + @mock.patch.object(requests, "request", mock_request) def test_auth_call(): self.assertRaises(exceptions.Unauthorized, cs.client.authenticate) @@ -124,29 +120,28 @@ def test_auth_redirect(self): correct_response = json.dumps(dict_correct_response) dict_responses = [ {"headers": {'location':'http://127.0.0.1:5001'}, - "status": 305, - "body": "Use proxy"}, + "status_code": 305, + "text": "Use proxy"}, # Configured on admin port, nova redirects to v2.0 port. # When trying to connect on it, keystone auth succeed by v1.0 # protocol (through headers) but tokens are being returned in # body (looks like keystone bug). Leaved for compatibility. {"headers": {}, - "status": 200, - "body": correct_response}, + "status_code": 200, + "text": correct_response}, {"headers": {}, - "status": 200, - "body": correct_response} + "status_code": 200, + "text": correct_response} ] - responses = [(to_http_response(resp), resp['body']) \ - for resp in dict_responses] + responses = [(utils.TestResponse(resp)) for resp in dict_responses] def side_effect(*args, **kwargs): return responses.pop(0) mock_request = mock.Mock(side_effect=side_effect) - @mock.patch.object(httplib2.Http, "request", mock_request) + @mock.patch.object(requests, "request", mock_request) def test_auth_call(): cs.client.authenticate() headers = { @@ -165,9 +160,14 @@ def test_auth_call(): } token_url = cs.client.auth_url + "/tokens" - mock_request.assert_called_with(token_url, "POST", - headers=headers, - body=json.dumps(body)) + kwargs = copy.copy(self.TEST_REQUEST_BASE) + kwargs['headers'] = headers + kwargs['data'] = json.dumps(body) + mock_request.assert_called_with( + "POST", + token_url, + allow_redirects=True, + **kwargs) resp = dict_correct_response endpoints = resp["access"]["serviceCatalog"][0]['endpoints'] @@ -214,15 +214,14 @@ def test_ambiguous_endpoints(self): ], }, } - auth_response = httplib2.Response({ - "status": 200, - "body": json.dumps(resp), - }) + auth_response = utils.TestResponse({ + "status_code": 200, + "text": json.dumps(resp), + }) - mock_request = mock.Mock(return_value=(auth_response, - json.dumps(resp))) + mock_request = mock.Mock(return_value=(auth_response)) - @mock.patch.object(httplib2.Http, "request", mock_request) + @mock.patch.object(requests, "request", mock_request) def test_auth_call(): self.assertRaises(exceptions.AmbiguousEndpoints, cs.client.authenticate) @@ -234,14 +233,16 @@ class AuthenticationTests(utils.TestCase): def test_authenticate_success(self): cs = client.Client("username", "password", "project_id", "auth_url") management_url = 'https://localhost/v1.1/443470' - auth_response = httplib2.Response({ - 'status': 204, - 'x-server-management-url': management_url, - 'x-auth-token': '1b751d74-de0c-46ae-84f0-915744b582d1', + auth_response = utils.TestResponse({ + 'status_code': 204, + 'headers': { + 'x-server-management-url': management_url, + 'x-auth-token': '1b751d74-de0c-46ae-84f0-915744b582d1', + }, }) - mock_request = mock.Mock(return_value=(auth_response, None)) + mock_request = mock.Mock(return_value=(auth_response)) - @mock.patch.object(httplib2.Http, "request", mock_request) + @mock.patch.object(requests, "request", mock_request) def test_auth_call(): cs.client.authenticate() headers = { @@ -251,21 +252,25 @@ def test_auth_call(): 'X-Auth-Project-Id': 'project_id', 'User-Agent': cs.client.USER_AGENT } - mock_request.assert_called_with(cs.client.auth_url, 'GET', - headers=headers) + mock_request.assert_called_with( + "GET", + cs.client.auth_url, + headers=headers, + **self.TEST_REQUEST_BASE) + self.assertEqual(cs.client.management_url, - auth_response['x-server-management-url']) + auth_response.headers['x-server-management-url']) self.assertEqual(cs.client.auth_token, - auth_response['x-auth-token']) + auth_response.headers['x-auth-token']) test_auth_call() def test_authenticate_failure(self): cs = client.Client("username", "password", "project_id", "auth_url") - auth_response = httplib2.Response({'status': 401}) - mock_request = mock.Mock(return_value=(auth_response, None)) + auth_response = utils.TestResponse({'status_code': 401}) + mock_request = mock.Mock(return_value=(auth_response)) - @mock.patch.object(httplib2.Http, "request", mock_request) + @mock.patch.object(requests, "request", mock_request) def test_auth_call(): self.assertRaises(exceptions.Unauthorized, cs.client.authenticate) diff --git a/tools/pip-requires b/tools/pip-requires index 2c409b3c2..7fbcbbe7c 100644 --- a/tools/pip-requires +++ b/tools/pip-requires @@ -1,5 +1,5 @@ argparse -httplib2 iso8601>=0.1.4 prettytable>=0.6,<0.7 +requests<1.0 simplejson From fba20df12c376185eecfa21a4f54b66a72fc5365 Mon Sep 17 00:00:00 2001 From: Yaguang Tang Date: Thu, 20 Dec 2012 17:55:33 +0800 Subject: [PATCH 0037/1705] add num_instances option for nova boot. fix bug #1092475. Change-Id: I584b27c1f24c6a5183a2289e77580f204db2e4db --- novaclient/v1_1/shell.py | 10 ++++++++++ tests/v1_1/test_shell.py | 18 ++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 77743deaa..b3fe46b1a 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -48,6 +48,11 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): if not args.flavor: raise exceptions.CommandError("you need to specify a Flavor ID ") + if args.num_instances: + if args.num_instances <= 1: + raise exceptions.CommandError("num_instances should be > 1") + max_count = args.num_instances + flavor = _find_flavor(cs, args.flavor) if args.image: @@ -149,6 +154,11 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): default=None, metavar='', help="Image ID (see 'nova image-list'). ") +@utils.arg('--num-instances', + default=None, + type=int, + metavar='', + help="boot multi instances at a time") @utils.arg('--meta', metavar="", action='append', diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index 12b13c907..de85a3e57 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -224,6 +224,24 @@ def test_boot_invalid_file(self): cmd = 'boot some-server --image 1 --file /foo=%s' % invalid_file self.assertRaises(exceptions.CommandError, self.run_command, cmd) + def test_boot_num_instances(self): + self.run_command('boot --image 1 --flavor 1 --num-instances 3 server') + self.assert_called_anytime( + 'POST', '/servers', + { + 'server': { + 'flavorRef': '1', + 'name': 'server', + 'imageRef': '1', + 'min_count': 1, + 'max_count': 3, + } + }) + + def test_boot_invalid_num_instances(self): + cmd = 'boot --image 1 --flavor 1 --num-instances 1 server' + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + def test_flavor_list(self): self.run_command('flavor-list') self.assert_called('GET', '/flavors/aa1/os-extra_specs') From 4cf314b2ecb1d6138b925d0b3f96cea9646cd022 Mon Sep 17 00:00:00 2001 From: Ken'ichi Ohmichi Date: Sun, 23 Dec 2012 04:51:56 +0900 Subject: [PATCH 0038/1705] Fix some usage messages of 'nova volume-*' 'nova volume-show', 'nova volume-delete', 'nova volume-snapshot-show' and 'nova volume-snapshot-delete' can be specified by ID or name as the argument. But that is not described on the usage messages. This patch fixes the usage messages. Fixes bug 1093172 Change-Id: I3dcdd2aca923e9304efd9ea7b467d5b81cd642a7 --- novaclient/v1_1/shell.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index b3fe46b1a..c9bd3f63c 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1245,12 +1245,12 @@ def do_remove_fixed_ip(cs, args): def _find_volume(cs, volume): - """Get a volume by ID.""" + """Get a volume by name or ID.""" return utils.find_resource(cs.volumes, volume) def _find_volume_snapshot(cs, snapshot): - """Get a volume snapshot by ID.""" + """Get a volume snapshot by name or ID.""" return utils.find_resource(cs.volume_snapshots, snapshot) @@ -1308,7 +1308,7 @@ def do_volume_list(cs, args): 'Size', 'Volume Type', 'Attached to']) -@utils.arg('volume', metavar='', help='ID of the volume.') +@utils.arg('volume', metavar='', help='Name or ID of the volume.') @utils.service_type('volume') def do_volume_show(cs, args): """Show details about a volume.""" @@ -1364,7 +1364,9 @@ def do_volume_create(cs, args): _print_volume(volume) -@utils.arg('volume', metavar='', help='ID of the volume to delete.') +@utils.arg('volume', + metavar='', + help='Name or ID of the volume to delete.') @utils.service_type('volume') def do_volume_delete(cs, args): """Remove a volume.""" @@ -1413,7 +1415,9 @@ def do_volume_snapshot_list(cs, _args): 'Size']) -@utils.arg('snapshot', metavar='', help='ID of the snapshot.') +@utils.arg('snapshot', + metavar='', + help='Name or ID of the snapshot.') @utils.service_type('volume') def do_volume_snapshot_show(cs, args): """Show details about a snapshot.""" @@ -1451,13 +1455,13 @@ def do_volume_snapshot_create(cs, args): _print_volume_snapshot(snapshot) -@utils.arg('snapshot_id', - metavar='', - help='ID of the snapshot to delete.') +@utils.arg('snapshot', + metavar='', + help='Name or ID of the snapshot to delete.') @utils.service_type('volume') def do_volume_snapshot_delete(cs, args): """Remove a snapshot.""" - snapshot = _find_volume_snapshot(cs, args.snapshot_id) + snapshot = _find_volume_snapshot(cs, args.snapshot) snapshot.delete() From 63073104665ee4597cf3b7aa8dc2295a8a7db794 Mon Sep 17 00:00:00 2001 From: Alessio Ababilov Date: Sat, 22 Dec 2012 20:12:20 +0400 Subject: [PATCH 0039/1705] Unify Manager._update behaviour Now _update call usually returns an instance of self.resource_class. This simplifies the code and makes novaclient closer to keystoneclient. Also, update hosts and services API according to changes on nova. (If50a6b6e20f9b3fe66d486bb9b15d3eb4b62daf9). Change-Id: I447e49e5fce0afba8a9c1a5df6dfa7200cc93e18 --- novaclient/base.py | 14 +++---- novaclient/v1_1/agents.py | 2 +- novaclient/v1_1/aggregates.py | 6 +-- novaclient/v1_1/floating_ip_dns.py | 13 ++++--- novaclient/v1_1/hosts.py | 5 +-- novaclient/v1_1/quota_classes.py | 6 ++- novaclient/v1_1/quotas.py | 4 +- novaclient/v1_1/servers.py | 2 +- novaclient/v1_1/services.py | 33 ++++++++-------- novaclient/v1_1/shell.py | 2 +- tests/v1_1/fakes.py | 60 +++++++++++++++--------------- tests/v1_1/test_agents.py | 2 +- tests/v1_1/test_hosts.py | 12 +++--- tests/v1_1/test_services.py | 14 +++---- tests/v1_1/test_shell.py | 20 +++++----- 15 files changed, 100 insertions(+), 95 deletions(-) diff --git a/novaclient/base.py b/novaclient/base.py index c5aa4a769..351399a78 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -136,12 +136,9 @@ def write_to_completion_cache(self, cache_type, val): if cache: cache.write("%s\n" % val) - def _get(self, url, response_key=None): + def _get(self, url, response_key): _resp, body = self.api.client.get(url) - if response_key: - return self.resource_class(self, body[response_key], loaded=True) - else: - return self.resource_class(self, body, loaded=True) + return self.resource_class(self, body[response_key], loaded=True) def _create(self, url, body, response_key, return_raw=False, **kwargs): self.run_hooks('modify_body_for_create', body, **kwargs) @@ -156,10 +153,13 @@ def _create(self, url, body, response_key, return_raw=False, **kwargs): def _delete(self, url): _resp, _body = self.api.client.delete(url) - def _update(self, url, body, **kwargs): + def _update(self, url, body, response_key=None, **kwargs): self.run_hooks('modify_body_for_update', body, **kwargs) _resp, body = self.api.client.put(url, body=body) - return body + if response_key: + return self.resource_class(self, body[response_key]) + else: + return body class ManagerWithFind(Manager): diff --git a/novaclient/v1_1/agents.py b/novaclient/v1_1/agents.py index 8552119d4..17e3cad31 100644 --- a/novaclient/v1_1/agents.py +++ b/novaclient/v1_1/agents.py @@ -49,7 +49,7 @@ def update(self, id, version, 'version': version, 'url': url, 'md5hash': md5hash}} - return self._update('/os-agents/%s' % id, body) + return self._update('/os-agents/%s' % id, body, 'agent') def create(self, os, architecture, version, url, md5hash, hypervisor): diff --git a/novaclient/v1_1/aggregates.py b/novaclient/v1_1/aggregates.py index f55f486bc..2a57703f8 100644 --- a/novaclient/v1_1/aggregates.py +++ b/novaclient/v1_1/aggregates.py @@ -62,9 +62,9 @@ def get_details(self, aggregate): def update(self, aggregate, values): """Update the name and/or availability zone.""" body = {'aggregate': values} - result = self._update("/os-aggregates/%s" % base.getid(aggregate), - body) - return self.resource_class(self, result["aggregate"]) + return self._update("/os-aggregates/%s" % base.getid(aggregate), + body, + "aggregate") def add_host(self, aggregate, host): """Add a host into the Host Aggregate.""" diff --git a/novaclient/v1_1/floating_ip_dns.py b/novaclient/v1_1/floating_ip_dns.py index c8bd563fd..1c00bdeb1 100644 --- a/novaclient/v1_1/floating_ip_dns.py +++ b/novaclient/v1_1/floating_ip_dns.py @@ -59,9 +59,9 @@ def create_private(self, fqdomain, availability_zone): body = {'domain_entry': {'scope': 'private', 'availability_zone': availability_zone}} - return self._update('/os-floating-ip-dns/%s' % _quote_domain(fqdomain), - body) + body, + 'domain_entry') def create_public(self, fqdomain, project): """Add or modify a public DNS domain.""" @@ -70,7 +70,8 @@ def create_public(self, fqdomain, project): 'project': project}} return self._update('/os-floating-ip-dns/%s' % _quote_domain(fqdomain), - body) + body, + 'domain_entry') def delete(self, fqdomain): """Delete the specified domain""" @@ -115,7 +116,8 @@ def create(self, domain, name, ip, dns_type): return self._update("/os-floating-ip-dns/%s/entries/%s" % (_quote_domain(domain), name), - body) + body, + "dns_entry") def modify_ip(self, domain, name, ip): """Add a new DNS entry.""" @@ -125,7 +127,8 @@ def modify_ip(self, domain, name, ip): return self._update("/os-floating-ip-dns/%s/entries/%s" % (_quote_domain(domain), name), - body) + body, + "dns_entry") def delete(self, domain, name): """Delete entry specified by name and domain.""" diff --git a/novaclient/v1_1/hosts.py b/novaclient/v1_1/hosts.py index b2377800d..a6f5a04a0 100644 --- a/novaclient/v1_1/hosts.py +++ b/novaclient/v1_1/hosts.py @@ -54,13 +54,12 @@ def get(self, host): def update(self, host, values): """Update status or maintenance mode for the host.""" - result = self._update("/os-hosts/%s" % host, values) - return self.resource_class(self, result) + return self._update("/os-hosts/%s" % host, {"host": values}, "host") def host_action(self, host, action): """Performs an action on a host.""" url = "/os-hosts/%s/%s" % (host, action) - return self._get(url) + return self._create(url, None, "host") def list_all(self, zone=None): url = '/os-hosts' diff --git a/novaclient/v1_1/quota_classes.py b/novaclient/v1_1/quota_classes.py index 3b52d64f4..066428461 100644 --- a/novaclient/v1_1/quota_classes.py +++ b/novaclient/v1_1/quota_classes.py @@ -25,7 +25,7 @@ def id(self): return self.class_name def update(self, *args, **kwargs): - self.manager.update(self.class_name, *args, **kwargs) + return self.manager.update(self.class_name, *args, **kwargs) class QuotaClassSetManager(base.ManagerWithFind): @@ -62,4 +62,6 @@ def update(self, class_name, metadata_items=None, if body['quota_class_set'][key] is None: body['quota_class_set'].pop(key) - self._update('/os-quota-class-sets/%s' % (class_name), body) + return self._update('/os-quota-class-sets/%s' % (class_name), + body, + 'quota_class_set') diff --git a/novaclient/v1_1/quotas.py b/novaclient/v1_1/quotas.py index cc2556384..c63cb4694 100644 --- a/novaclient/v1_1/quotas.py +++ b/novaclient/v1_1/quotas.py @@ -25,7 +25,7 @@ def id(self): return self.tenant_id def update(self, *args, **kwargs): - self.manager.update(self.tenant_id, *args, **kwargs) + return self.manager.update(self.tenant_id, *args, **kwargs) class QuotaSetManager(base.ManagerWithFind): @@ -63,7 +63,7 @@ def update(self, tenant_id, metadata_items=None, if body['quota_set'][key] is None: body['quota_set'].pop(key) - self._update('/os-quota-sets/%s' % (tenant_id), body) + return self._update('/os-quota-sets/%s' % tenant_id, body, 'quota_set') def defaults(self, tenant_id): return self._get('/os-quota-sets/%s/defaults' % tenant_id, diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index 9ea2cb008..3aa4ec967 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -525,7 +525,7 @@ def update(self, server, name=None): }, } - self._update("/servers/%s" % base.getid(server), body) + return self._update("/servers/%s" % base.getid(server), body, "server") def change_password(self, server, password): """ diff --git a/novaclient/v1_1/services.py b/novaclient/v1_1/services.py index 8f54a6727..7aa26f7bf 100644 --- a/novaclient/v1_1/services.py +++ b/novaclient/v1_1/services.py @@ -34,29 +34,28 @@ def _add_details(self, info): class ServiceManager(base.ManagerWithFind): resource_class = Service - def list(self, host=None, service=None): + def list(self, host=None, binary=None): """ Describes cpu/memory/hdd info for host. :param host: destination host name. """ url = "/os-services" + filters = [] if host: - url = "/os-services?host=%s" % host - if service: - url = "/os-services?service=%s" % service - if host and service: - url = "/os-services?host=%s&service=%s" % (host, service) + filters.append("host=%s" % host) + if binary: + filters.append("binary=%s" % binary) + if filters: + url = "%s?%s" % (url, "&".join(filters)) return self._list(url, "services") - def enable(self, host, service): - """Enable the service specified by hostname and servicename""" - body = {"host": host, "service": service} - result = self._update("/os-services/enable", body) - return self.resource_class(self, result) - - def disable(self, host, service): - """Enable the service specified by hostname and servicename""" - body = {"host": host, "service": service} - result = self._update("/os-services/disable", body) - return self.resource_class(self, result) + def enable(self, host, binary): + """Enable the service specified by hostname and binary""" + body = {"host": host, "binary": binary} + return self._update("/os-services/enable", body, "service") + + def disable(self, host, binary): + """Enable the service specified by hostname and binary""" + body = {"host": host, "binary": binary} + return self._update("/os-services/disable", body, "service") diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index b3fe46b1a..133785918 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2077,7 +2077,7 @@ def do_agent_modify(cs, args): """Modify an existing agent build.""" result = cs.agents.update(args.id, args.version, args.url, args.md5hash) - utils.print_dict(result['agent']) + utils.print_dict(result._info) def do_aggregate_list(cs, args): diff --git a/tests/v1_1/fakes.py b/tests/v1_1/fakes.py index 545701ba9..4dd17f6e7 100644 --- a/tests/v1_1/fakes.py +++ b/tests/v1_1/fakes.py @@ -363,7 +363,7 @@ def get_servers_9012(self, **kw): def put_servers_1234(self, body, **kw): assert body.keys() == ['server'] fakes.assert_has_keys(body['server'], optional=['name', 'adminPass']) - return (204, {}, None) + return (204, {}, body) def delete_servers_1234(self, **kw): return (202, {}, None) @@ -711,12 +711,12 @@ def put_os_floating_ip_dns_testdomain(self, body, **kw): else: fakes.assert_has_keys(body['domain_entry'], required=['project', 'scope']) - return (205, {}, None) + return (205, {}, body) def put_os_floating_ip_dns_testdomain_entries_testname(self, body, **kw): fakes.assert_has_keys(body['dns_entry'], required=['ip', 'dns_type']) - return (205, {}, None) + return (205, {}, body) def delete_os_floating_ip_dns_testdomain(self, **kw): return (200, {}, None) @@ -1080,7 +1080,7 @@ def delete_os_aggregates_1(self, **kw): # def get_os_services(self, **kw): host = kw.get('host', 'host1') - service = kw.get('service', 'nova-compute') + service = kw.get('binary', 'nova-compute') return (200, {}, {'services': [{'binary': service, 'host': host, @@ -1097,12 +1097,14 @@ def get_os_services(self, **kw): ]}) def put_os_services_enable(self, body, **kw): - return (200, {}, {'host': body['host'], 'service': body['service'], - 'disabled': False}) + return (200, {}, {'service': {'host': body['host'], + 'binary': body['binary'], + 'disabled': False}}) def put_os_services_disable(self, body, **kw): - return (200, {}, {'host': body['host'], 'service': body['service'], - 'disabled': True}) + return (200, {}, {'service': {'host': body['host'], + 'binary': body['binary'], + 'disabled': True}}) # # Fixed IPs @@ -1134,10 +1136,10 @@ def get_os_hosts_host(self, *kw): def get_os_hosts(self, **kw): zone = kw.get('zone', 'nova1') return (200, {}, {'hosts': - [{'host': 'host1', + [{'host_name': 'host1', 'service': 'nova-compute', 'zone': zone}, - {'host': 'host1', + {'host_name': 'host1', 'service': 'nova-cert', 'zone': zone}]}) @@ -1145,34 +1147,34 @@ def get_os_hosts_sample_host(self, *kw): return (200, {}, {'host': [{'resource': {'host': 'sample_host'}}], }) def put_os_hosts_sample_host_1(self, body, **kw): - return (200, {}, {'host': 'sample-host_1', - 'status': 'enabled'}) + return (200, {}, {'host': {'host_name': 'sample-host_1', + 'status': 'enabled'}}) def put_os_hosts_sample_host_2(self, body, **kw): - return (200, {}, {'host': 'sample-host_2', - 'maintenance_mode': 'on_maintenance'}) + return (200, {}, {'host': {'host_name': 'sample-host_2', + 'maintenance_mode': 'on_maintenance'}}) def put_os_hosts_sample_host_3(self, body, **kw): - return (200, {}, {'host': 'sample-host_3', - 'status': 'enabled', - 'maintenance_mode': 'on_maintenance'}) + return (200, {}, {'host': {'host_name': 'sample-host_3', + 'status': 'enabled', + 'maintenance_mode': 'on_maintenance'}}) - def get_os_hosts_sample_host_startup(self, **kw): - return (200, {}, {'host': 'sample_host', - 'power_action': 'startup'}) + def post_os_hosts_sample_host_startup(self, **kw): + return (200, {}, {'host': {'host_name': 'sample_host', + 'power_action': 'startup'}}) - def get_os_hosts_sample_host_reboot(self, **kw): - return (200, {}, {'host': 'sample_host', - 'power_action': 'reboot'}) + def post_os_hosts_sample_host_reboot(self, **kw): + return (200, {}, {'host': {'host_name': 'sample_host', + 'power_action': 'reboot'}}) - def get_os_hosts_sample_host_shutdown(self, **kw): - return (200, {}, {'host': 'sample_host', - 'power_action': 'shutdown'}) + def post_os_hosts_sample_host_shutdown(self, **kw): + return (200, {}, {'host': {'host_name': 'sample_host', + 'power_action': 'shutdown'}}) def put_os_hosts_sample_host(self, body, **kw): - result = {'host': 'dummy'} - result.update(body) - return (200, {}, result) + result = {'host_name': 'dummy'} + result.update(body['host']) + return (200, {}, {'host': result}) def get_os_hypervisors(self, **kw): return (200, {}, {"hypervisors": [ diff --git a/tests/v1_1/test_agents.py b/tests/v1_1/test_agents.py index 97d537994..14ddcf7fc 100644 --- a/tests/v1_1/test_agents.py +++ b/tests/v1_1/test_agents.py @@ -65,4 +65,4 @@ def test_agents_modify(self): "version": "8.0", "md5hash": "add6bb58e139be103324d04d82d8f546"}} cs.assert_called('PUT', '/os-agents/1', body) - self.assertEqual(1, ag['agent']['id']) + self.assertEqual(1, ag.id) diff --git a/tests/v1_1/test_hosts.py b/tests/v1_1/test_hosts.py index 7daa135cb..57c2891a8 100644 --- a/tests/v1_1/test_hosts.py +++ b/tests/v1_1/test_hosts.py @@ -29,14 +29,14 @@ def test_update_enable(self): host = cs.hosts.get('sample_host')[0] values = {"status": "enabled"} result = host.update(values) - cs.assert_called('PUT', '/os-hosts/sample_host', values) + cs.assert_called('PUT', '/os-hosts/sample_host', {'host': values}) self.assertTrue(isinstance(result, hosts.Host)) def test_update_maintenance(self): host = cs.hosts.get('sample_host')[0] values = {"maintenance_mode": "enable"} result = host.update(values) - cs.assert_called('PUT', '/os-hosts/sample_host', values) + cs.assert_called('PUT', '/os-hosts/sample_host', {'host': values}) self.assertTrue(isinstance(result, hosts.Host)) def test_update_both(self): @@ -44,23 +44,23 @@ def test_update_both(self): values = {"status": "enabled", "maintenance_mode": "enable"} result = host.update(values) - cs.assert_called('PUT', '/os-hosts/sample_host', values) + cs.assert_called('PUT', '/os-hosts/sample_host', {'host': values}) self.assertTrue(isinstance(result, hosts.Host)) def test_host_startup(self): host = cs.hosts.get('sample_host')[0] result = host.startup() - cs.assert_called('GET', '/os-hosts/sample_host/startup') + cs.assert_called('POST', '/os-hosts/sample_host/startup') self.assertTrue(isinstance(result, hosts.Host)) def test_host_reboot(self): host = cs.hosts.get('sample_host')[0] result = host.reboot() - cs.assert_called('GET', '/os-hosts/sample_host/reboot') + cs.assert_called('POST', '/os-hosts/sample_host/reboot') self.assertTrue(isinstance(result, hosts.Host)) def test_host_shutdown(self): host = cs.hosts.get('sample_host')[0] result = host.shutdown() - cs.assert_called('GET', '/os-hosts/sample_host/shutdown') + cs.assert_called('POST', '/os-hosts/sample_host/shutdown') self.assertTrue(isinstance(result, hosts.Host)) diff --git a/tests/v1_1/test_services.py b/tests/v1_1/test_services.py index 4f78c7048..d6d58837e 100644 --- a/tests/v1_1/test_services.py +++ b/tests/v1_1/test_services.py @@ -39,30 +39,30 @@ def test_list_services_with_hostname(self): [self.assertEqual(s.binary, 'nova-compute') for s in svs] [self.assertEqual(s.host, 'host2') for s in svs] - def test_list_services_with_service(self): - svs = cs.services.list(service='nova-cert') - cs.assert_called('GET', '/os-services?service=nova-cert') + def test_list_services_with_binary(self): + svs = cs.services.list(binary='nova-cert') + cs.assert_called('GET', '/os-services?binary=nova-cert') [self.assertTrue(isinstance(s, services.Service)) for s in svs] [self.assertEqual(s.binary, 'nova-cert') for s in svs] [self.assertEqual(s.host, 'host1') for s in svs] - def test_list_services_with_host_service(self): + def test_list_services_with_host_binary(self): svs = cs.services.list('host2', 'nova-cert') - cs.assert_called('GET', '/os-services?host=host2&service=nova-cert') + cs.assert_called('GET', '/os-services?host=host2&binary=nova-cert') [self.assertTrue(isinstance(s, services.Service)) for s in svs] [self.assertEqual(s.binary, 'nova-cert') for s in svs] [self.assertEqual(s.host, 'host2') for s in svs] def test_services_enable(self): service = cs.services.enable('host1', 'nova-cert') - values = {"host": "host1", 'service': 'nova-cert'} + values = {"host": "host1", 'binary': 'nova-cert'} cs.assert_called('PUT', '/os-services/enable', values) self.assertTrue(isinstance(service, services.Service)) self.assertFalse(service.disabled) def test_services_disable(self): service = cs.services.disable('host1', 'nova-cert') - values = {"host": "host1", 'service': 'nova-cert'} + values = {"host": "host1", 'binary': 'nova-cert'} cs.assert_called('PUT', '/os-services/disable', values) self.assertTrue(isinstance(service, services.Service)) self.assertTrue(service.disabled) diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index de85a3e57..6565f1317 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -578,20 +578,20 @@ def test_services_list_with_host(self): def test_services_list_with_servicename(self): self.run_command('service-list --servicename nova-cert') - self.assert_called('GET', '/os-services?service=nova-cert') + self.assert_called('GET', '/os-services?binary=nova-cert') def test_services_list_with_host_servicename(self): self.run_command('service-list --host host1 --servicename nova-cert') - self.assert_called('GET', '/os-services?host=host1&service=nova-cert') + self.assert_called('GET', '/os-services?host=host1&binary=nova-cert') def test_services_enable(self): self.run_command('service-enable host1 nova-cert') - body = {'host': 'host1', 'service': 'nova-cert'} + body = {'host': 'host1', 'binary': 'nova-cert'} self.assert_called('PUT', '/os-services/enable', body) def test_services_disable(self): self.run_command('service-disable host1 nova-cert') - body = {'host': 'host1', 'service': 'nova-cert'} + body = {'host': 'host1', 'binary': 'nova-cert'} self.assert_called('PUT', '/os-services/disable', body) def test_fixed_ips_get(self): @@ -618,31 +618,31 @@ def test_host_list_with_zone(self): def test_host_update_status(self): self.run_command('host-update sample-host_1 --status enabled') - body = {'status': 'enabled'} + body = {'host': {'status': 'enabled'}} self.assert_called('PUT', '/os-hosts/sample-host_1', body) def test_host_update_maintenance(self): self.run_command('host-update sample-host_2 --maintenance enable') - body = {'maintenance_mode': 'enable'} + body = {'host': {'maintenance_mode': 'enable'}} self.assert_called('PUT', '/os-hosts/sample-host_2', body) def test_host_update_multiple_settings(self): self.run_command('host-update sample-host_3 ' '--status disabled --maintenance enable') - body = {'status': 'disabled', 'maintenance_mode': 'enable'} + body = {'host': {'status': 'disabled', 'maintenance_mode': 'enable'}} self.assert_called('PUT', '/os-hosts/sample-host_3', body) def test_host_startup(self): self.run_command('host-action sample-host --action startup') - self.assert_called('GET', '/os-hosts/sample-host/startup') + self.assert_called('POST', '/os-hosts/sample-host/startup') def test_host_shutdown(self): self.run_command('host-action sample-host --action shutdown') - self.assert_called('GET', '/os-hosts/sample-host/shutdown') + self.assert_called('POST', '/os-hosts/sample-host/shutdown') def test_host_reboot(self): self.run_command('host-action sample-host --action reboot') - self.assert_called('GET', '/os-hosts/sample-host/reboot') + self.assert_called('POST', '/os-hosts/sample-host/reboot') def test_coverage_start(self): self.run_command('coverage-start') From eb7c3907d18a639a095b39b5d44b5c15a21adea2 Mon Sep 17 00:00:00 2001 From: Lorin Hochstein Date: Mon, 31 Dec 2012 16:29:53 -0500 Subject: [PATCH 0040/1705] Update README.rst Update the README to remove PENDING links to non-existent docs, and to remove Rackspace-specific references. Change-Id: I08311e0da3d181c3817573be93f6eed6e15e3812 --- README.rst | 30 +++++++++--------------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/README.rst b/README.rst index 7886df2e6..0dff06636 100644 --- a/README.rst +++ b/README.rst @@ -5,16 +5,12 @@ This is a client for the OpenStack Nova API. There's a Python API (the ``novaclient`` module), and a command-line script (``nova``). Each implements 100% of the OpenStack Nova API. -[PENDING] `Full documentation is available`__. +See the `OpenStack CLI guide`_ for information on how to use the ``nova`` +command-line tool. You may also want to look at the +`OpenStack API documentation`_. -__ http://packages.python.org/python-novaclient/ - -You'll also probably want to read `OpenStack Compute Developer Guide API`__ -- -the first bit, at least -- to get an idea of the concepts. Rackspace is doing -the cloud hosting thing a bit differently from Amazon, and if you get the -concepts this library should make more sense. - -__ http://docs.openstack.org/api/ +.. _OpenStack CLI Guide: http://docs.openstack.org/cli/quick-start/content/ +.. _OpenStack API documentation: http://docs.openstack.org/api/ The project is hosted on `Launchpad`_, where bugs can be filed. The code is hosted on `Github`_. Patches must be submitted using `Gerrit`_, *not* Github @@ -24,11 +20,9 @@ pull requests. .. _Launchpad: https://launchpad.net/python-novaclient .. _Gerrit: http://wiki.openstack.org/GerritWorkflow -This code a fork of `Jacobian's python-cloudservers`__ If you need API support -for the Rackspace API solely or the BSD license, you should use that repository. -python-client is licensed under the Apache License like the rest of OpenStack. +python-novaclient is licensed under the Apache License like the rest of +OpenStack. -__ http://github.com/jacobian/python-cloudservers .. contents:: Contents: :local: @@ -37,7 +31,7 @@ Command-line API ---------------- Installing this package gets you a shell command, ``nova``, that you -can use to interact with any Rackspace compatible API (including OpenStack). +can use to interact with any OpenStack cloud. You'll need to provide your OpenStack username and password. You can do this with the ``--os-username``, ``--os-password`` and ``--os-tenant-name`` @@ -237,9 +231,8 @@ You'll find complete documentation on the shell by running Python API ---------- -[PENDING] There's also a `complete Python API`__. +There's also a complete Python API, but it has not yet been documented. -__ http://packages.python.org/python-novaclient/ Quick-start using keystone:: @@ -252,8 +245,3 @@ Quick-start using keystone:: [...] >>> nt.keypairs.list() [...] - -What's new? ------------ - -[PENDING] See `the release notes `_. From 53aee5cf4b66c98c1142a57244d7466249e44f1f Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 24 Dec 2012 19:11:38 -0600 Subject: [PATCH 0041/1705] Move from untitest2 to testtools. Use testtools as the base testclass. Use fixtures library for managing fixtures. Part of blueprint grizzly-testtools Change-Id: Iac5af286b988787acf7049344641aadf140b9398 --- tests/test_auth_plugins.py | 7 +++--- tests/test_client.py | 3 --- tests/test_shell.py | 47 ++++++++++++++++++++----------------- tests/test_utils.py | 1 + tests/utils.py | 4 ++-- tests/v1_1/test_shell.py | 48 ++++++++++++++++++++++---------------- tools/test-requires | 3 ++- 7 files changed, 62 insertions(+), 51 deletions(-) diff --git a/tests/test_auth_plugins.py b/tests/test_auth_plugins.py index 00b3a7597..c4950af87 100644 --- a/tests/test_auth_plugins.py +++ b/tests/test_auth_plugins.py @@ -174,8 +174,9 @@ def mock_iter_entry_points(_type): @mock.patch.object(pkg_resources, "iter_entry_points", mock_iter_entry_points) def test_auth_call(): - with self.assertRaises(exceptions.EndpointNotFound): - cs = client.Client("username", "password", "project_id", - auth_system="fakewithauthurl") + self.assertRaises( + exceptions.EndpointNotFound, + client.Client, "username", "password", "project_id", + auth_system="fakewithauthurl") test_auth_call() diff --git a/tests/test_client.py b/tests/test_client.py index e2fc9d965..48d511a81 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -6,9 +6,6 @@ class ClientTest(utils.TestCase): - def setUp(self): - pass - def test_get_client_class_v2(self): output = novaclient.client.get_client_class('2') self.assertEqual(output, novaclient.v1_1.client.Client) diff --git a/tests/test_shell.py b/tests/test_shell.py index 79c3d2f48..63c7244a2 100644 --- a/tests/test_shell.py +++ b/tests/test_shell.py @@ -1,7 +1,10 @@ import cStringIO -import os +import re import sys +import fixtures +from testtools import matchers + from novaclient import exceptions import novaclient.shell from tests import utils @@ -9,16 +12,18 @@ class ShellTest(utils.TestCase): - # Patch os.environ to avoid required auth info. + FAKE_ENV = { + 'OS_USERNAME': 'username', + 'OS_PASSWORD': 'password', + 'OS_TENANT_NAME': 'tenant_name', + 'OS_AUTH_URL': 'http://no.where', + } + def setUp(self): - global _old_env - fake_env = { - 'OS_USERNAME': 'username', - 'OS_PASSWORD': 'password', - 'OS_TENANT_NAME': 'tenant_name', - 'OS_AUTH_URL': 'http://no.where', - } - _old_env, os.environ = os.environ, fake_env.copy() + super(ShellTest, self).setUp() + for var in self.FAKE_ENV: + self.useFixture(fixtures.EnvironmentVariable(var, + self.FAKE_ENV[var])) def shell(self, argstr): orig = sys.stdout @@ -36,29 +41,27 @@ def shell(self, argstr): return out - def tearDown(self): - global _old_env - os.environ = _old_env - def test_help_unknown_command(self): self.assertRaises(exceptions.CommandError, self.shell, 'help foofoo') def test_help(self): required = [ - '^usage: ', - '(?m)^\s+root-password\s+Change the root password', - '(?m)^See "nova help COMMAND" for help on a specific command', + '.*?^usage: ', + '.*?^\s+root-password\s+Change the root password', + '.*?^See "nova help COMMAND" for help on a specific command', ] help_text = self.shell('help') for r in required: - self.assertRegexpMatches(help_text, r) + self.assertThat(help_text, + matchers.MatchesRegex(r, re.DOTALL | re.MULTILINE)) def test_help_on_subcommand(self): required = [ - '^usage: nova root-password', - '(?m)^Change the root password', - '(?m)^Positional arguments:', + '.*?^usage: nova root-password', + '.*?^Change the root password', + '.*?^Positional arguments:', ] help_text = self.shell('help root-password') for r in required: - self.assertRegexpMatches(help_text, r) + self.assertThat(help_text, + matchers.MatchesRegex(r, re.DOTALL | re.MULTILINE)) diff --git a/tests/test_utils.py b/tests/test_utils.py index 0eb64cff2..c619fe14d 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -61,6 +61,7 @@ class FakeDisplayManager(FakeManager): class FindResourceTestCase(test_utils.TestCase): def setUp(self): + super(FindResourceTestCase, self).setUp() self.manager = FakeManager(None) def test_find_none(self): diff --git a/tests/utils.py b/tests/utils.py index 3bbe8bbad..ef231a775 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,8 +1,8 @@ import requests -import unittest2 +import testtools -class TestCase(unittest2.TestCase): +class TestCase(testtools.TestCase): TEST_REQUEST_BASE = { 'config': {'danger_mode': False}, 'verify': True, diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index de85a3e57..375fb8118 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -21,6 +21,8 @@ import sys import tempfile +import fixtures + import novaclient.shell import novaclient.client from novaclient import exceptions @@ -29,39 +31,45 @@ from tests import utils -class ShellTest(utils.TestCase): +class ShellFixture(fixtures.Fixture): - # Patch os.environ to avoid required auth info. def setUp(self): - """Run before each test.""" - self.old_environment = os.environ.copy() - os.environ = { - 'NOVA_USERNAME': 'username', - 'NOVA_PASSWORD': 'password', - 'NOVA_PROJECT_ID': 'project_id', - 'OS_COMPUTE_API_VERSION': '1.1', - 'NOVA_URL': 'http://no.where', - } - + super(ShellFixture, self).setUp() self.shell = novaclient.shell.OpenStackComputeShell() - #HACK(bcwaldon): replace this when we start using stubs - self.old_get_client_class = novaclient.client.get_client_class - novaclient.client.get_client_class = lambda *_: fakes.FakeClient - def tearDown(self): - os.environ = self.old_environment # For some method like test_image_meta_bad_action we are # testing a SystemExit to be thrown and object self.shell has # no time to get instantatiated which is OK in this case, so # we make sure the method is there before launching it. if hasattr(self.shell, 'cs'): self.shell.cs.clear_callstack() + super(ShellFixture, self).tearDown() + + +class ShellTest(utils.TestCase): + + FAKE_ENV = { + 'NOVA_USERNAME': 'username', + 'NOVA_PASSWORD': 'password', + 'NOVA_PROJECT_ID': 'project_id', + 'OS_COMPUTE_API_VERSION': '1.1', + 'NOVA_URL': 'http://no.where', + } + + def setUp(self): + """Run before each test.""" + super(ShellTest, self).setUp() - #HACK(bcwaldon): replace this when we start using stubs - novaclient.client.get_client_class = self.old_get_client_class + for var in self.FAKE_ENV: + self.useFixture(fixtures.EnvironmentVariable(var, + self.FAKE_ENV[var])) + self.shell = self.useFixture(ShellFixture()).shell - timeutils.clear_time_override() + self.useFixture(fixtures.MonkeyPatch( + 'novaclient.client.get_client_class', + lambda *_: fakes.FakeClient)) + self.addCleanup(timeutils.clear_time_override) def run_command(self, cmd): self.shell.main(cmd.split()) diff --git a/tools/test-requires b/tools/test-requires index 43a946955..f69ef390d 100644 --- a/tools/test-requires +++ b/tools/test-requires @@ -1,5 +1,6 @@ distribute>=0.6.24 +fixtures mock nose nose-exclude @@ -8,4 +9,4 @@ openstack.nose_plugin nosehtmloutput pep8==1.1 sphinx>=1.1.2 -unittest2 +testtools From 2d3e0d949e5d2b63c13a0f82b0ab2b4227a5a3e1 Mon Sep 17 00:00:00 2001 From: Nikola Dipanov Date: Fri, 4 Jan 2013 17:03:08 +0100 Subject: [PATCH 0042/1705] Fix the help text of add-fixed-ip Makes the help text of the add-fixed-ip less confusing by pointing out that the IP address is taken from the given network and assigned to the server. Change-Id: Iaf4641f65a5872ee70273d5b972ff5da0ddd7b1d --- novaclient/v1_1/shell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index c9bd3f63c..268ce1b3e 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1231,7 +1231,7 @@ def _find_flavor(cs, flavor): metavar='', help='Network ID.') def do_add_fixed_ip(cs, args): - """Add new IP address to network.""" + """Add new IP address on a network to server.""" server = _find_server(cs, args.server) server.add_fixed_ip(args.network_id) From 9e319ece374c7208668f4c2e3a31f1e7be21562f Mon Sep 17 00:00:00 2001 From: Kravchenko Pavel Date: Mon, 7 Jan 2013 18:46:59 +0200 Subject: [PATCH 0043/1705] Add support for instance evacuate. This adds support for server evacuation from failed host. Adds CLI command: nova evacuate [--password pwd] [--on-shared-storage] Depends on the approval of change: https://review.openstack.org/#change,17991 Change-Id: Icd91c0484b2db532861e23163d043737ad04117a --- README.rst | 1 + novaclient/v1_1/servers.py | 31 +++++++++++++++++++++++++++++++ novaclient/v1_1/shell.py | 22 ++++++++++++++++++++++ tests/v1_1/fakes.py | 5 +++++ tests/v1_1/test_servers.py | 7 +++++++ tests/v1_1/test_shell.py | 21 +++++++++++++++++++++ 6 files changed, 87 insertions(+) diff --git a/README.rst b/README.rst index 0dff06636..0fd35fc33 100644 --- a/README.rst +++ b/README.rst @@ -108,6 +108,7 @@ You'll find complete documentation on the shell by running and name. endpoints Discover endpoints that get returned from the authenticate services + evacuate Evacuate a server from failed host flavor-create Create a new flavor flavor-delete Delete a specific flavor flavor-list Print a list of available 'flavors' (sizes of diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index 3aa4ec967..7ba86805c 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -281,6 +281,17 @@ def remove_security_group(self, security_group): """ self.manager.remove_security_group(self, security_group) + def evacuate(self, host, on_shared_storage, password=None): + """ + Evacuate an instance from failed host to specified host. + + :param host: Name of the target host + :param on_shared_storage: Specifies whether instance files located + on shared storage + :param password: string to set as password on the evacuated server. + """ + return self.manager.evacuate(self, host, on_shared_storage, password) + class ServerManager(local_base.BootingManagerWithFind): resource_class = Server @@ -707,6 +718,26 @@ def remove_security_group(self, server, security_group): """ self._action('removeSecurityGroup', server, {'name': security_group}) + def evacuate(self, server, host, on_shared_storage, password=None): + """ + Evacuate a server instance. + + :param server: The :class:`Server` (or its ID) to share onto. + :param host: Name of the target host. + :param on_shared_storage: Specifies whether instance files located + on shared storage + :param password: string to set as password on the evacuated server. + """ + body = { + 'host': host, + 'onSharedStorage': on_shared_storage, + } + + if password is not None: + body['adminPass'] = password + + return self._action('evacuate', server, body) + def _action(self, action, server, info=None, **kwargs): """ Perform a server "action" -- reboot/rebuild/resize/etc. diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 62dc25144..6ae6afccb 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2701,3 +2701,25 @@ def do_quota_class_update(cs, args): """Update the quotas for a quota class.""" _quota_update(cs.quota_classes, args.class_name, args) + + +@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('host', metavar='', help='Name or ID of target host.') +@utils.arg('--password', + dest='password', + metavar='', + default=None, + help="Set the provided password on the evacuated instance. Not applicable " + "with on-shared-storage flag") +@utils.arg('--on-shared-storage', + dest='on_shared_storage', + action="store_true", + default=False, + help='Specifies whether instance files located on shared storage') +def do_evacuate(cs, args): + """Evacuate server from failed host to specified one.""" + server = _find_server(cs, args.server) + + res = server.evacuate(args.host, args.on_shared_storage, args.password)[0] + if type(res) is dict: + utils.print_dict(res) diff --git a/tests/v1_1/fakes.py b/tests/v1_1/fakes.py index 4dd17f6e7..cff665152 100644 --- a/tests/v1_1/fakes.py +++ b/tests/v1_1/fakes.py @@ -489,6 +489,11 @@ def post_servers_1234_action(self, body, **kw): assert set(body[action].keys()) == set(['name', 'backup_type', 'rotation']) + elif action == 'evacuate': + keys = body[action].keys() + if 'adminPass' in keys: + keys.remove('adminPass') + assert set(keys) == set(['host', 'onSharedStorage']) else: raise AssertionError("Unexpected server action: %s" % action) return (resp, _headers, _body) diff --git a/tests/v1_1/test_servers.py b/tests/v1_1/test_servers.py index f1f2f88db..30e5dd742 100644 --- a/tests/v1_1/test_servers.py +++ b/tests/v1_1/test_servers.py @@ -399,3 +399,10 @@ def test_remove_security_group(self): cs.assert_called('POST', '/servers/1234/action') cs.servers.remove_security_group(s, 'oldsg') cs.assert_called('POST', '/servers/1234/action') + + def test_evacuate(self): + s = cs.servers.get(1234) + s.evacuate('fake_target_host', 'True') + cs.assert_called('POST', '/servers/1234/action') + cs.servers.evacuate(s, 'fake_target_host', 'False', 'NewAdminPassword') + cs.assert_called('POST', '/servers/1234/action') diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index 6565f1317..12b89d11e 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -790,3 +790,24 @@ def test_absolute_limits(self): self.run_command('absolute-limits --reserved') self.assert_called('GET', '/limits?reserved=1') + + def test_evacuate(self): + self.run_command('evacuate sample-server new_host') + self.assert_called('POST', '/servers/1234/action', + {'evacuate': {'host': 'new_host', + 'onSharedStorage': False}}) + self.run_command('evacuate sample-server new_host ' + '--password NewAdminPass') + self.assert_called('POST', '/servers/1234/action', + {'evacuate': {'host': 'new_host', + 'onSharedStorage': False, + 'adminPass': 'NewAdminPass'}}) + self.run_command('evacuate sample-server new_host') + self.assert_called('POST', '/servers/1234/action', + {'evacuate': {'host': 'new_host', + 'onSharedStorage': False}}) + self.run_command('evacuate sample-server new_host ' + '--on-shared-storage') + self.assert_called('POST', '/servers/1234/action', + {'evacuate': {'host': 'new_host', + 'onSharedStorage': True}}) From 207969f6cdcc965895238586fc011bdbdd2173bf Mon Sep 17 00:00:00 2001 From: Andrew Laski Date: Thu, 10 Jan 2013 11:20:07 -0500 Subject: [PATCH 0044/1705] When logging request include request data When the --debug flag is used it should print out the body of a POST request. It did that previously but the kwargs dict used for the request stopped using 'body' which is what the logging relied on. This changes it to use 'data' which is a string representation of what was in 'body'. Bug 1098241 Change-Id: I82f36e59b459706d9d5a3a38563a45c80e84c6e2 --- novaclient/client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index 1fbfd9750..9b9797928 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -141,8 +141,8 @@ def http_log_req(self, args, kwargs): header = ' -H "%s: %s"' % (element, kwargs['headers'][element]) string_parts.append(header) - if 'body' in kwargs: - string_parts.append(" -d '%s'" % (kwargs['body'])) + if 'data' in kwargs: + string_parts.append(" -d '%s'" % (kwargs['data'])) self._logger.debug("\nREQ: %s\n" % "".join(string_parts)) def http_log_resp(self, resp): From b5f3307e8013d5817eee5022dd026e06caef792f Mon Sep 17 00:00:00 2001 From: Alessio Ababilov Date: Thu, 10 Jan 2013 20:49:22 +0400 Subject: [PATCH 0045/1705] Update hosts API action calls (startup etc.) These calls are now implemented as normal POST os-hosts/{host_name}/action requests. Change-Id: I8cd401e3b4e552c6787d1f984041ad3c345e6eca --- novaclient/v1_1/hosts.py | 5 +++-- tests/v1_1/fakes.py | 13 ++----------- tests/v1_1/test_hosts.py | 9 +++------ tests/v1_1/test_shell.py | 6 +++--- 4 files changed, 11 insertions(+), 22 deletions(-) diff --git a/novaclient/v1_1/hosts.py b/novaclient/v1_1/hosts.py index a6f5a04a0..3dea7021e 100644 --- a/novaclient/v1_1/hosts.py +++ b/novaclient/v1_1/hosts.py @@ -58,8 +58,9 @@ def update(self, host, values): def host_action(self, host, action): """Performs an action on a host.""" - url = "/os-hosts/%s/%s" % (host, action) - return self._create(url, None, "host") + body = {action: None} + url = '/os-hosts/%s/action' % host + return self.api.client.post(url, body=body) def list_all(self, zone=None): url = '/os-hosts' diff --git a/tests/v1_1/fakes.py b/tests/v1_1/fakes.py index 4dd17f6e7..42fdde7c8 100644 --- a/tests/v1_1/fakes.py +++ b/tests/v1_1/fakes.py @@ -1159,17 +1159,8 @@ def put_os_hosts_sample_host_3(self, body, **kw): 'status': 'enabled', 'maintenance_mode': 'on_maintenance'}}) - def post_os_hosts_sample_host_startup(self, **kw): - return (200, {}, {'host': {'host_name': 'sample_host', - 'power_action': 'startup'}}) - - def post_os_hosts_sample_host_reboot(self, **kw): - return (200, {}, {'host': {'host_name': 'sample_host', - 'power_action': 'reboot'}}) - - def post_os_hosts_sample_host_shutdown(self, **kw): - return (200, {}, {'host': {'host_name': 'sample_host', - 'power_action': 'shutdown'}}) + def post_os_hosts_sample_host_action(self, **kw): + return (202, {}, None) def put_os_hosts_sample_host(self, body, **kw): result = {'host_name': 'dummy'} diff --git a/tests/v1_1/test_hosts.py b/tests/v1_1/test_hosts.py index 57c2891a8..28964279d 100644 --- a/tests/v1_1/test_hosts.py +++ b/tests/v1_1/test_hosts.py @@ -50,17 +50,14 @@ def test_update_both(self): def test_host_startup(self): host = cs.hosts.get('sample_host')[0] result = host.startup() - cs.assert_called('POST', '/os-hosts/sample_host/startup') - self.assertTrue(isinstance(result, hosts.Host)) + cs.assert_called('POST', '/os-hosts/sample_host/action', {'startup': None}) def test_host_reboot(self): host = cs.hosts.get('sample_host')[0] result = host.reboot() - cs.assert_called('POST', '/os-hosts/sample_host/reboot') - self.assertTrue(isinstance(result, hosts.Host)) + cs.assert_called('POST', '/os-hosts/sample_host/action', {'reboot': None}) def test_host_shutdown(self): host = cs.hosts.get('sample_host')[0] result = host.shutdown() - cs.assert_called('POST', '/os-hosts/sample_host/shutdown') - self.assertTrue(isinstance(result, hosts.Host)) + cs.assert_called('POST', '/os-hosts/sample_host/action', {'shutdown': None}) diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index 6565f1317..db2647f11 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -634,15 +634,15 @@ def test_host_update_multiple_settings(self): def test_host_startup(self): self.run_command('host-action sample-host --action startup') - self.assert_called('POST', '/os-hosts/sample-host/startup') + self.assert_called('POST', '/os-hosts/sample-host/action', {'startup': None}) def test_host_shutdown(self): self.run_command('host-action sample-host --action shutdown') - self.assert_called('POST', '/os-hosts/sample-host/shutdown') + self.assert_called('POST', '/os-hosts/sample-host/action', {'shutdown': None}) def test_host_reboot(self): self.run_command('host-action sample-host --action reboot') - self.assert_called('POST', '/os-hosts/sample-host/reboot') + self.assert_called('POST', '/os-hosts/sample-host/action', {'reboot': None}) def test_coverage_start(self): self.run_command('coverage-start') From 020de7cebc774cdb6518ddc9070b5a5309da9a0a Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Fri, 11 Jan 2013 14:56:23 -0800 Subject: [PATCH 0046/1705] Fix a couple of broken shell tests Pyflakes picked up a couple of duplicated names. Rename the one that is correct and delete the incorrect one. Change-Id: Id4af1269a030be5a725bbbbcf3400341b09fddc3 --- tests/v1_1/test_shell.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index de85a3e57..9057748b3 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -748,17 +748,11 @@ def test_network_disassociate_host(self): body = {'disassociate_host': None} self.assert_called('POST', '/os-networks/2/action', body) - def test_network_disassociate(self): + def test_network_disassociate_project(self): self.run_command('network-disassociate --project-only 1 2') body = {'disassociate_project': None} self.assert_called('POST', '/os-networks/2/action', body) - def test_network_create_v4(self): - self.run_command('network-create --fixed-range-v4 10.0.1.0/24 \ - new_network') - body = {'cidr': '10.0.1.0/24', 'label': 'new_network'} - self.assert_called('POST', '/os-networks', body) - def test_network_create_v4(self): self.run_command('network-create --fixed-range-v4 10.0.1.0/24 \ --dns1 10.0.1.254 new_network') From 6f580661f647bf5085edff75689eb28565728acb Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 3 Jan 2013 17:11:06 -0500 Subject: [PATCH 0047/1705] Update functionality of coverage extension. Change If1aa25fc7237e9bb5100d2a4a8e560f0a68eba61 adds additional functionality to the coverage extension. Mainly returning the data file path for the 'stop' action and adding support for generating html reports. This commit adds support for this new functionality. Change-Id: Icd147350d5c038f6b9c8063e77a75370eb8422e9 --- novaclient/v1_1/coverage_ext.py | 6 ++++-- novaclient/v1_1/shell.py | 4 ++-- tests/v1_1/fakes.py | 4 +++- tests/v1_1/test_coverage_ext.py | 3 ++- tests/v1_1/test_shell.py | 2 +- 5 files changed, 12 insertions(+), 7 deletions(-) diff --git a/novaclient/v1_1/coverage_ext.py b/novaclient/v1_1/coverage_ext.py index 1a526a64f..f0b099e1d 100644 --- a/novaclient/v1_1/coverage_ext.py +++ b/novaclient/v1_1/coverage_ext.py @@ -20,7 +20,7 @@ class Coverage(base.Resource): def __repr__(self): - return "" % self.name + return "" % self.name class CoverageManager(base.ManagerWithFind): @@ -41,7 +41,7 @@ def stop(self): url = '/os-coverage/action' return self.api.client.post(url, body=body) - def report(self, filename, xml=False): + def report(self, filename, xml=False, html=True): body = { 'report': { 'file': filename, @@ -49,6 +49,8 @@ def report(self, filename, xml=False): } if xml: body['report']['xml'] = True + elif html: + body['report']['html'] = True self.run_hooks('modify_body_for_action', body) url = '/os-coverage/action' return self.api.client.post(url, body=body) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index b3fe46b1a..5333e0087 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2307,8 +2307,8 @@ def do_coverage_start(cs, args): def do_coverage_stop(cs, args): """Stop Nova coverage reporting""" - cs.coverage.stop() - print "Coverage collection stopped" + out = cs.coverage.stop() + print "Coverage data file path: %s" % out[-1]['path'] @utils.arg('filename', metavar='', help='report filename') diff --git a/tests/v1_1/fakes.py b/tests/v1_1/fakes.py index 545701ba9..760b4e7e6 100644 --- a/tests/v1_1/fakes.py +++ b/tests/v1_1/fakes.py @@ -1347,8 +1347,10 @@ def post_os_networks_2_action(self, **kw): return (202, {}, None) def post_os_coverage_action(self, body, **kw): - if 'report' not in body: + if 'start' in body: return (200, {}, None) + elif 'stop' in body: + return (200, {}, {'path': '/tmp/tmpdir/'}) else: return (200, {}, { 'path': '/tmp/tmpdir/' + body['report']['file'] diff --git a/tests/v1_1/test_coverage_ext.py b/tests/v1_1/test_coverage_ext.py index 818f33220..4596c8789 100644 --- a/tests/v1_1/test_coverage_ext.py +++ b/tests/v1_1/test_coverage_ext.py @@ -34,7 +34,8 @@ def test_start_coverage(self): def test_stop_coverage(self): c = cs.coverage.stop() - cs.assert_called('POST', '/os-coverage/action') + return_dict = {'path': '/tmp/tmpdir/report'} + cs.assert_called_anytime('POST', '/os-coverage/action') def test_report_coverage(self): c = cs.coverage.report('report') diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index de85a3e57..dbee5b4dd 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -650,7 +650,7 @@ def test_coverage_start(self): def test_coverage_stop(self): self.run_command('coverage-stop') - self.assert_called('POST', '/os-coverage/action') + self.assert_called_anytime('POST', '/os-coverage/action') def test_coverage_report(self): self.run_command('coverage-report report') From dffd415fba15c177cc3b809194159cacfd44772f Mon Sep 17 00:00:00 2001 From: Matt Dietz Date: Tue, 18 Dec 2012 15:03:55 +0000 Subject: [PATCH 0048/1705] Adds tenant network support to the client Modifies novaclient, changing the existing networks implementation to a more appropriate os-admin-networks namespace, and supplements with a tenant-base network extenion that lives under the os-networks namespace as a Nova API extension. Also removes from the duplicately named network test methods. Implements: blueprint tenant-networks Change-Id: I54c9f017b86fc413f1646c7bded8cebd94f6a287 --- novaclient/v1_1/contrib/tenant_networks.py | 77 ++++++++++++++++++++++ novaclient/v1_1/networks.py | 13 ++-- tests/v1_1/contrib/fakes.py | 44 +++++++++++++ tests/v1_1/contrib/test_tenant_networks.py | 50 ++++++++++++++ tests/v1_1/fakes.py | 25 +++---- tests/v1_1/test_networks.py | 12 ++-- tests/v1_1/test_shell.py | 11 ---- 7 files changed, 196 insertions(+), 36 deletions(-) create mode 100644 novaclient/v1_1/contrib/tenant_networks.py create mode 100644 tests/v1_1/contrib/fakes.py create mode 100644 tests/v1_1/contrib/test_tenant_networks.py diff --git a/novaclient/v1_1/contrib/tenant_networks.py b/novaclient/v1_1/contrib/tenant_networks.py new file mode 100644 index 000000000..9f70634d9 --- /dev/null +++ b/novaclient/v1_1/contrib/tenant_networks.py @@ -0,0 +1,77 @@ +# Copyright 2013 OpenStack, LLC +# +# 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 novaclient import base +from novaclient import utils + + +class TenantNetwork(base.Resource): + def delete(self): + self.manager.delete(network=self) + + +class TenantNetworkManager(base.ManagerWithFind): + resource_class = base.Resource + + def list(self): + return self._list('/os-tenant-networks', 'networks') + + def get(self, network): + return self._get('/os-tenant-networks/%s' % base.getid(network), + 'network') + + def delete(self, network): + self._delete('/os-tenant-networks/%s' % base.getid(network)) + + def create(self, label, cidr): + body = {'network': {'label': label, 'cidr': cidr}} + return self._create('/os-tenant-networks', body, 'network') + + +@utils.arg('network_id', metavar='', help='ID of network') +def do_net(cs, args): + """ + Show a network + """ + network = cs.tenant_networks.get(args.network_id) + utils.print_dict(network._info) + + +def do_net_list(cs, args): + """ + List networks + """ + networks = cs.tenant_networks.list() + utils.print_list(networks, ['ID', 'Label', 'CIDR']) + + +@utils.arg('label', metavar='', + help='Network label (ex. my_new_network)') +@utils.arg('cidr', metavar='', + help='IP block to allocate from (ex. 172.16.0.0/24 or ' + '2001:DB8::/64)') +def do_net_create(cs, args): + """ + Create a network + """ + network = cs.tenant_networks.create(args.label, args.cidr) + utils.print_dict(network._info) + + +@utils.arg('network_id', metavar='', help='ID of network') +def do_net_delete(cs, args): + """ + Delete a network + """ + cs.tenant_networks.delete(args.network_id) diff --git a/novaclient/v1_1/networks.py b/novaclient/v1_1/networks.py index 1d9496d40..b1fbb8886 100644 --- a/novaclient/v1_1/networks.py +++ b/novaclient/v1_1/networks.py @@ -18,6 +18,7 @@ """ from novaclient import base +from novaclient import exceptions class Network(base.Resource): @@ -55,7 +56,8 @@ def get(self, network): :param network: The ID of the :class:`Network` to get. :rtype: :class:`Network` """ - return self._get("/os-networks/%s" % base.getid(network), "network") + return self._get("/os-networks/%s" % base.getid(network), + "network") def delete(self, network): """ @@ -107,11 +109,11 @@ def disassociate(self, network, disassociate_host=True, elif disassociate_host: body = {"disassociate_host": None} else: - raise CommandError( + raise exceptions.CommandError( "Must disassociate either host or project or both") - self.api.client.post("/os-networks/%s/action" % base.getid(network), - body=body) + self.api.client.post("/os-networks/%s/action" % + base.getid(network), body=body) def associate_host(self, network, host): """ @@ -120,7 +122,8 @@ def associate_host(self, network, host): :param network: The ID of the :class:`Network`. :param host: The name of the host to associate the network with """ - self.api.client.post("/os-networks/%s/action" % base.getid(network), + self.api.client.post("/os-networks/%s/action" % + base.getid(network), body={"associate_host": host}) def associate_project(self, network): diff --git a/tests/v1_1/contrib/fakes.py b/tests/v1_1/contrib/fakes.py new file mode 100644 index 000000000..dd7b889fe --- /dev/null +++ b/tests/v1_1/contrib/fakes.py @@ -0,0 +1,44 @@ +# Copyright 2012 OpenStack, LLC +# +# 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 novaclient.v1_1 import client +from tests.v1_1 import fakes + + +class FakeClient(fakes.FakeClient): + def __init__(self, *args, **kwargs): + client.Client.__init__(self, 'username', 'password', + 'project_id', 'auth_url', + extensions=kwargs.get('extensions')) + self.client = FakeHTTPClient(**kwargs) + + +class FakeHTTPClient(fakes.FakeHTTPClient): + def get_os_tenant_networks(self): + return (200, {}, {'networks': [{"label": "1", "cidr": "10.0.0.0/24", + 'project_id': '4ffc664c198e435e9853f2538fbcd7a7', + 'id': '1'}]}) + + def get_os_tenant_networks_1(self, **kw): + return (200, {}, {'network': {"label": "1", "cidr": "10.0.0.0/24", + 'project_id': '4ffc664c198e435e9853f2538fbcd7a7', + 'id': '1'}}) + + def post_os_tenant_networks(self, **kw): + return (201, {}, {'network': {"label": "1", "cidr": "10.0.0.0/24", + 'project_id': '4ffc664c198e435e9853f2538fbcd7a7', + 'id': '1'}}) + + def delete_os_tenant_networks_1(self, **kw): + return (204, {}, None) diff --git a/tests/v1_1/contrib/test_tenant_networks.py b/tests/v1_1/contrib/test_tenant_networks.py new file mode 100644 index 000000000..0df914edc --- /dev/null +++ b/tests/v1_1/contrib/test_tenant_networks.py @@ -0,0 +1,50 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 OpenStack LLC. +# All Rights Reserved. +# +# 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 novaclient import extension +from novaclient.v1_1.contrib import tenant_networks + +from tests import utils +from tests.v1_1.contrib import fakes + + +extensions = [ + extension.Extension(tenant_networks.__name__.split(".")[-1], + tenant_networks), +] +cs = fakes.FakeClient(extensions=extensions) + + +class TenantNetworkExtensionTests(utils.TestCase): + def test_list_tenant_networks(self): + nets = cs.tenant_networks.list() + cs.assert_called('GET', '/os-tenant-networks') + self.assertTrue(len(nets) > 0) + + def test_get_tenant_network(self): + net = cs.tenant_networks.get(1) + cs.assert_called('GET', '/os-tenant-networks/1') + print net + + def test_create_tenant_networks(self): + cs.tenant_networks.create(label="net", + cidr="10.0.0.0/24") + cs.assert_called('POST', '/os-tenant-networks') + + def test_delete_tenant_networks(self): + cs.tenant_networks.delete(1) + cs.assert_called('DELETE', '/os-tenant-networks/1') diff --git a/tests/v1_1/fakes.py b/tests/v1_1/fakes.py index 5cb6d2b4d..dbf7fc052 100644 --- a/tests/v1_1/fakes.py +++ b/tests/v1_1/fakes.py @@ -16,8 +16,6 @@ from datetime import datetime import urlparse -import requests - from novaclient import client as base_client from novaclient.v1_1 import client from tests import fakes @@ -65,10 +63,6 @@ def _cs_request(self, url, method, **kwargs): # Note the call self.callstack.append((method, url, kwargs.get('body', None))) - if 'body' in kwargs: - b = kwargs['body'] - else: - b = '' status, headers, body = getattr(self, callback)(**kwargs) r = utils.TestResponse({ "status_code": status, @@ -656,9 +650,6 @@ def get_os_floating_ips_1(self, **kw): {'id': 1, 'fixed_ip': '10.0.0.1', 'ip': '11.0.0.1'} }) - def post_os_floating_ips(self, body, **kw): - return (202, {}, self.get_os_floating_ips_1()[1]) - def post_os_floating_ips(self, body): if body.get('pool'): return (200, {}, {'floating_ip': @@ -1289,14 +1280,11 @@ def get_os_networks(self, **kw): 'project_id': '4ffc664c198e435e9853f2538fbcd7a7', 'id': '1'}]}) - def get_os_networks_1(self, **kw): - return (200, {}, {'network': {"label": "1", "cidr": "10.0.0.0/24"}}) - def post_os_networks(self, **kw): return (202, {}, {'network': kw}) - def post_os_networks_1_action(self, **kw): - return (202, {}, None) + def get_os_networks_1(self, **kw): + return (200, {}, {'network': {"label": "1", "cidr": "10.0.0.0/24"}}) def delete_os_networks_networkdelete(self, **kw): return (202, {}, None) @@ -1336,8 +1324,13 @@ def get_os_fping_1(self, **kw): } ) - def post_os_networks(self, **kw): - return (202, {}, {'network': kw}) + def post_os_coverage_action(self, body, **kw): + if 'report' not in body: + return (200, {}, None) + else: + return (200, {}, { + 'path': '/tmp/tmpdir/' + body['report']['file'] + }) def post_os_networks_1_action(self, **kw): return (202, {}, None) diff --git a/tests/v1_1/test_networks.py b/tests/v1_1/test_networks.py index d0af5471a..5481e908c 100644 --- a/tests/v1_1/test_networks.py +++ b/tests/v1_1/test_networks.py @@ -31,7 +31,8 @@ def test_create(self): def test_associate_project(self): cs.networks.associate_project('networktest') - cs.assert_called('POST', '/os-networks/add', {'id': 'networktest'}) + cs.assert_called('POST', '/os-networks/add', + {'id': 'networktest'}) def test_associate_host(self): cs.networks.associate_host('networktest', 'testHost') @@ -40,17 +41,20 @@ def test_associate_host(self): def test_disassociate(self): cs.networks.disassociate('networkdisassociate') - cs.assert_called('POST', '/os-networks/networkdisassociate/action', + cs.assert_called('POST', + '/os-networks/networkdisassociate/action', {'disassociate': None}) def test_disassociate_host_only(self): cs.networks.disassociate('networkdisassociate', True, False) - cs.assert_called('POST', '/os-networks/networkdisassociate/action', + cs.assert_called('POST', + '/os-networks/networkdisassociate/action', {'disassociate_host': None}) def test_disassociate_project(self): cs.networks.disassociate('networkdisassociate', False, True) - cs.assert_called('POST', '/os-networks/networkdisassociate/action', + cs.assert_called('POST', + '/os-networks/networkdisassociate/action', {'disassociate_project': None}) def test_add(self): diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index a6903071c..1056c5bdb 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -738,11 +738,6 @@ def test_network_associate_project(self): body = {'id': "1"} self.assert_called('POST', '/os-networks/add', body) - def test_network_disassociate(self): - self.run_command('network-disassociate 1') - body = {'disassociate': None} - self.assert_called('POST', '/os-networks/1/action', body) - def test_network_disassociate_host(self): self.run_command('network-disassociate --host-only 1 2') body = {'disassociate_host': None} @@ -753,12 +748,6 @@ def test_network_disassociate(self): body = {'disassociate_project': None} self.assert_called('POST', '/os-networks/2/action', body) - def test_network_create_v4(self): - self.run_command('network-create --fixed-range-v4 10.0.1.0/24 \ - new_network') - body = {'cidr': '10.0.1.0/24', 'label': 'new_network'} - self.assert_called('POST', '/os-networks', body) - def test_network_create_v4(self): self.run_command('network-create --fixed-range-v4 10.0.1.0/24 \ --dns1 10.0.1.254 new_network') From 020ff909cc312da0e67f293ea14a81423dac6b15 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Fri, 11 Jan 2013 14:30:41 -0800 Subject: [PATCH 0049/1705] Implement get password for novaclient This will download and decrypt a base64 encoded encrypted password from the os-server-password extension. It depends on the user having openssl installed, but if there is an error of any kind it will print out the encoded and encrypted password instead. It also implements clear_password which will delete the password so it can no longer be retrieved. Change-Id: I2c4e6c3f03b70dc98d6d339381648a6058f46e21 --- novaclient/crypto.py | 37 +++++++++++++++++++++++++++++++ novaclient/v1_1/servers.py | 45 ++++++++++++++++++++++++++++++++++++++ novaclient/v1_1/shell.py | 18 +++++++++++++++ tests/v1_1/fakes.py | 10 +++++++++ tests/v1_1/test_servers.py | 10 +++++++++ tests/v1_1/test_shell.py | 8 +++++++ 6 files changed, 128 insertions(+) create mode 100644 novaclient/crypto.py diff --git a/novaclient/crypto.py b/novaclient/crypto.py new file mode 100644 index 000000000..a264823ad --- /dev/null +++ b/novaclient/crypto.py @@ -0,0 +1,37 @@ +# Copyright 2013 Nebula, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import base64 +import subprocess + + +class DecryptionFailure(Exception): + pass + + +def decrypt_password(private_key, password): + """Base64 decodes password and unecrypts it with private key. + + Requires openssl binary available in the path""" + unencoded = base64.b64decode(password) + cmd = ['openssl', 'rsautl', '-decrypt', '-inkey', private_key] + proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + out, err = proc.communicate(unencoded) + proc.stdin.close() + if proc.returncode: + raise DecryptionFailure(err) + return out diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index 7ba86805c..c93bdf78c 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -22,6 +22,7 @@ import urllib from novaclient import base +from novaclient import crypto from novaclient.v1_1 import base as local_base @@ -65,6 +66,21 @@ def get_vnc_console(self, console_type): """ return self.manager.get_vnc_console(self, console_type) + def get_password(self, private_key): + """ + Get password for a Server. + + :param private_key: Path to private key file for decryption + """ + return self.manager.get_password(self, private_key) + + def clear_password(self): + """ + Get password for a Server. + + """ + return self.manager.clear_password(self) + def add_fixed_ip(self, network_id): """ Add an IP address on a network. @@ -381,6 +397,35 @@ def get_vnc_console(self, server, console_type): return self._action('os-getVNCConsole', server, {'type': console_type})[1] + def get_password(self, server, private_key): + """ + Get password for an instance + + Requires that openssl in installed and in the path + + :param server: The :class:`Server` (or its ID) to add an IP to. + :param private_key: The private key to decrypt password + """ + + _resp, body = self.api.client.get("/servers/%s/os-server-password" + % base.getid(server)) + if body and body.get('password'): + try: + return crypto.decrypt_password(private_key, body['password']) + except Exception as exc: + return '%sFailed to decrypt:\n%s' % (exc, body['password']) + return '' + + def clear_password(self, server): + """ + Clear password for an instance + + :param server: The :class:`Server` (or its ID) to add an IP to. + """ + + return self._delete("/servers/%s/os-server-password" + % base.getid(server)) + def stop(self, server): """ Stop the server. diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index f82ee6d63..8c5d55945 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1512,6 +1512,24 @@ def __init__(self, console_dict): utils.print_list([VNCConsole(data['console'])], ['Type', 'Url']) +@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('private_key', + metavar='', + help='Private key (used locally to decrypt password).') +def do_get_password(cs, args): + """Get password for a server.""" + server = _find_server(cs, args.server) + data = server.get_password(args.private_key) + print data + + +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_clear_password(cs, args): + """Clear password for a server.""" + server = _find_server(cs, args.server) + data = server.clear_password() + + def _print_floating_ip_list(floating_ips): utils.print_list(floating_ips, ['Ip', 'Instance Id', 'Fixed Ip', 'Pool']) diff --git a/tests/v1_1/fakes.py b/tests/v1_1/fakes.py index 478fc9df0..795ceec18 100644 --- a/tests/v1_1/fakes.py +++ b/tests/v1_1/fakes.py @@ -410,6 +410,16 @@ def get_servers_1234_ips_private(self, **kw): def delete_servers_1234_ips_public_1_2_3_4(self, **kw): return (202, {}, None) + # + # Server password + # + + def get_servers_1234_os_server_password(self, **kw): + return (200, {}, {'password': ''}) + + def delete_servers_1234_os_server_password(self, **kw): + return (202, {}, None) + # # Server actions # diff --git a/tests/v1_1/test_servers.py b/tests/v1_1/test_servers.py index 30e5dd742..1caaa422f 100644 --- a/tests/v1_1/test_servers.py +++ b/tests/v1_1/test_servers.py @@ -328,6 +328,16 @@ def test_get_console_output_with_length(self): self.assertEqual(cs.servers.get_console_output(s, length=50), success) cs.assert_called('POST', '/servers/1234/action') + def test_get_password(self): + s = cs.servers.get(1234) + self.assertEqual(s.get_password('/foo/id_rsa'), '') + cs.assert_called('GET', '/servers/1234/os-server-password') + + def test_clear_password(self): + s = cs.servers.get(1234) + s.clear_password() + cs.assert_called('DELETE', '/servers/1234/os-server-password') + def test_get_server_actions(self): s = cs.servers.get(1234) actions = s.actions() diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index 0d32ff331..8fd88757a 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -808,3 +808,11 @@ def test_evacuate(self): self.assert_called('POST', '/servers/1234/action', {'evacuate': {'host': 'new_host', 'onSharedStorage': True}}) + + def test_get_password(self): + self.run_command('get-password sample-server /foo/id_rsa') + self.assert_called('GET', '/servers/1234/os-server-password') + + def test_clear_password(self): + self.run_command('clear-password sample-server') + self.assert_called('DELETE', '/servers/1234/os-server-password') From 9fd2c8a59da7e7e328f4813f6fbc834fc560b25d Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Fri, 11 Jan 2013 21:44:56 -0800 Subject: [PATCH 0050/1705] Allow request timeout to be specified. Add a new cli argument (--timeout) which is by default 600 seconds which will be set in the requests library so that timeouts can occur correctly. Change-Id: I716ac15fe08f42c9464ee43010bc8fd2667bcbde --- novaclient/client.py | 6 ++++++ novaclient/shell.py | 26 +++++++++++++++++++++++--- tests/test_client.py | 40 ++++++++++++++++++++++++++++++++++++++++ tests/test_shell.py | 31 +++++++++++++++++++++++-------- 4 files changed, 92 insertions(+), 11 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index 9b9797928..1778e7a5d 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -82,6 +82,10 @@ def __init__(self, user, password, projectid, auth_url=None, self.bypass_url = bypass_url self.os_cache = os_cache or not no_cache self.http_log_debug = http_log_debug + if timeout is not None: + self.timeout = float(timeout) + else: + self.timeout = None self.times = [] # [("item", starttime, endtime), ...] @@ -162,6 +166,8 @@ def request(self, url, method, **kwargs): kwargs['headers']['Content-Type'] = 'application/json' kwargs['data'] = json.dumps(kwargs['body']) del kwargs['body'] + if self.timeout is not None: + kwargs.setdefault('timeout', self.timeout) self.http_log_req((url, method,), kwargs) resp = requests.request( diff --git a/novaclient/shell.py b/novaclient/shell.py index d44fd3303..1e47f2ea5 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -42,6 +42,20 @@ logger = logging.getLogger(__name__) +def positive_non_zero_float(text): + if text is None: + return None + try: + value = float(text) + except ValueError: + msg = "%s must be a float" % text + raise argparse.ArgumentTypeError(msg) + if value <= 0: + msg = "%s must be greater than 0" % text + raise argparse.ArgumentTypeError(msg) + return value + + class NovaClientArgumentParser(argparse.ArgumentParser): def __init__(self, *args, **kwargs): @@ -112,6 +126,12 @@ def get_base_parser(self): action='store_true', help="Print call timing info") + parser.add_argument('--timeout', + default=600, + metavar='', + type=positive_non_zero_float, + help="Set HTTP call timeout (in seconds)") + parser.add_argument('--os-username', metavar='', default=utils.env('OS_USERNAME', 'NOVA_USERNAME'), @@ -397,7 +417,7 @@ def main(self, argv): os_region_name, os_auth_system, endpoint_type, insecure, service_type, service_name, volume_service_name, username, apikey, projectid, url, region_name, - bypass_url, os_cache, cacert) = ( + bypass_url, os_cache, cacert, timeout) = ( args.os_username, args.os_password, args.os_tenant_name, args.os_auth_url, args.os_region_name, args.os_auth_system, @@ -406,7 +426,7 @@ def main(self, argv): args.username, args.apikey, args.projectid, args.url, args.region_name, args.bypass_url, args.os_cache, - args.os_cacert) + args.os_cacert, args.timeout) if not endpoint_type: endpoint_type = DEFAULT_NOVA_ENDPOINT_TYPE @@ -478,7 +498,7 @@ def main(self, argv): volume_service_name=volume_service_name, timings=args.timings, bypass_url=bypass_url, os_cache=os_cache, http_log_debug=options.debug, - cacert=cacert) + cacert=cacert, timeout=timeout) try: if not utils.isunauthenticated(args.func): diff --git a/tests/test_client.py b/tests/test_client.py index 48d511a81..ca21fb0ce 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1,3 +1,21 @@ +# Copyright 2012 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +import mock +import requests import novaclient.client import novaclient.v1_1.client @@ -6,6 +24,28 @@ class ClientTest(utils.TestCase): + def test_client_with_timeout(self): + instance = novaclient.client.HTTPClient(user='user', + password='password', + projectid='project', + timeout=2, + auth_url="http://www.blah.com") + self.assertEqual(instance.timeout, 2) + mock_request = mock.Mock() + mock_request.return_value = requests.Response() + mock_request.return_value.status_code = 200 + mock_request.return_value.headers = { + 'x-server-management-url': 'blah.com', + 'x-auth-token': 'blah', + } + with mock.patch('requests.request', mock_request): + instance.authenticate() + requests.request.assert_called_with(mock.ANY, mock.ANY, + timeout=2, + headers=mock.ANY, + verify=mock.ANY, + config=mock.ANY) + def test_get_client_class_v2(self): output = novaclient.client.get_client_class('2') self.assertEqual(output, novaclient.v1_1.client.Client) diff --git a/tests/test_shell.py b/tests/test_shell.py index 63c7244a2..d35814dc0 100644 --- a/tests/test_shell.py +++ b/tests/test_shell.py @@ -25,34 +25,49 @@ def setUp(self): self.useFixture(fixtures.EnvironmentVariable(var, self.FAKE_ENV[var])) - def shell(self, argstr): + def shell(self, argstr, exitcodes=(0,)): orig = sys.stdout + orig_stderr = sys.stderr try: sys.stdout = cStringIO.StringIO() + sys.stderr = cStringIO.StringIO() _shell = novaclient.shell.OpenStackComputeShell() _shell.main(argstr.split()) except SystemExit: exc_type, exc_value, exc_traceback = sys.exc_info() - self.assertEqual(exc_value.code, 0) + self.assertIn(exc_value.code, exitcodes) finally: - out = sys.stdout.getvalue() + stdout = sys.stdout.getvalue() sys.stdout.close() sys.stdout = orig + stderr = sys.stderr.getvalue() + sys.stderr.close() + sys.stderr = orig_stderr - return out + return (stdout, stderr) def test_help_unknown_command(self): self.assertRaises(exceptions.CommandError, self.shell, 'help foofoo') + def test_invalid_timeout(self): + for f in [0, -1, -10]: + cmd_text = '--timeout %s' % (f) + stdout, stderr = self.shell(cmd_text, exitcodes=[0, 2]) + required = [ + 'argument --timeout: %s must be greater than 0' % (f), + ] + for r in required: + self.assertIn(r, stderr) + def test_help(self): required = [ '.*?^usage: ', '.*?^\s+root-password\s+Change the root password', '.*?^See "nova help COMMAND" for help on a specific command', ] - help_text = self.shell('help') + stdout, stderr = self.shell('help') for r in required: - self.assertThat(help_text, + self.assertThat((stdout + stderr), matchers.MatchesRegex(r, re.DOTALL | re.MULTILINE)) def test_help_on_subcommand(self): @@ -61,7 +76,7 @@ def test_help_on_subcommand(self): '.*?^Change the root password', '.*?^Positional arguments:', ] - help_text = self.shell('help root-password') + stdout, stderr = self.shell('help root-password') for r in required: - self.assertThat(help_text, + self.assertThat((stdout + stderr), matchers.MatchesRegex(r, re.DOTALL | re.MULTILINE)) From 3e190c5e494f6a5a77fdaec781ea947978e25ff2 Mon Sep 17 00:00:00 2001 From: zhiyanliu Date: Thu, 17 Jan 2013 11:51:46 +0800 Subject: [PATCH 0051/1705] Ensure list output function can support non-sorting printing Ensure list printing function in utils can handle non-sorting calls. Allow result list can be formatted and outputted without any sorting field, keep natural order to the list printing. Adding necessary checking to avoid prettytable (0.6.1 for me) exception: "Invalid field name: None!". Fix bug: #1099732 Change-Id: Ied869d987e608fff8b8b5f5a65d21e02f0cebeaa Signed-off-by: zhiyanliu --- novaclient/utils.py | 5 +++- tests/test_utils.py | 64 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/novaclient/utils.py b/novaclient/utils.py index bf76a7316..d65afd481 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -161,7 +161,10 @@ def print_list(objs, fields, formatters={}, sortby_index=0): row.append(data) pt.add_row(row) - print pt.get_string(sortby=sortby) + if sortby is not None: + print pt.get_string(sortby=sortby) + else: + print pt.get_string() def print_dict(d, dict_property="Property"): diff --git a/tests/test_utils.py b/tests/test_utils.py index c619fe14d..7a0949ed2 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,3 +1,7 @@ +import StringIO +import sys + +import mock from novaclient import exceptions from novaclient import utils @@ -99,3 +103,63 @@ def test_find_by_str_displayname(self): display_manager = FakeDisplayManager(None) output = utils.find_resource(display_manager, 'entity_three') self.assertEqual(output, display_manager.get('4242')) + + +class _FakeResult(object): + def __init__(self, name, value): + self.name = name + self.value = value + + +class PrintResultTestCase(test_utils.TestCase): + @mock.patch('sys.stdout', StringIO.StringIO()) + def test_print_list_sort_by_str(self): + objs = [_FakeResult("k1", 1), + _FakeResult("k3", 2), + _FakeResult("k2", 3)] + + utils.print_list(objs, ["Name", "Value"], sortby_index=0) + + self.assertEqual(sys.stdout.getvalue(), + '+------+-------+\n' + '| Name | Value |\n' + '+------+-------+\n' + '| k1 | 1 |\n' + '| k2 | 3 |\n' + '| k3 | 2 |\n' + '+------+-------+\n') + + @mock.patch('sys.stdout', StringIO.StringIO()) + def test_print_list_sort_by_integer(self): + objs = [_FakeResult("k1", 1), + _FakeResult("k3", 2), + _FakeResult("k2", 3)] + + utils.print_list(objs, ["Name", "Value"], sortby_index=1) + + self.assertEqual(sys.stdout.getvalue(), + '+------+-------+\n' + '| Name | Value |\n' + '+------+-------+\n' + '| k1 | 1 |\n' + '| k3 | 2 |\n' + '| k2 | 3 |\n' + '+------+-------+\n') + + # without sorting + @mock.patch('sys.stdout', StringIO.StringIO()) + def test_print_list_sort_by_none(self): + objs = [_FakeResult("k1", 1), + _FakeResult("k3", 3), + _FakeResult("k2", 2)] + + utils.print_list(objs, ["Name", "Value"], sortby_index=None) + + self.assertEqual(sys.stdout.getvalue(), + '+------+-------+\n' + '| Name | Value |\n' + '+------+-------+\n' + '| k1 | 1 |\n' + '| k3 | 3 |\n' + '| k2 | 2 |\n' + '+------+-------+\n') From 5b8099cd0e86443697192fcdd58633a2550b4d4c Mon Sep 17 00:00:00 2001 From: "Daniel P. Berrange" Date: Wed, 2 Jan 2013 18:37:25 +0000 Subject: [PATCH 0052/1705] Add support for get_spice_console RPC API Add the new get_spice_console API mirroring the impl of the existing get_vnc_console support. Blueprint: libvirt-spice Change-Id: Id549de57ebbed95dc01749838ed539b3b47efc8b Signed-off-by: Daniel P. Berrange --- README.rst | 1 + novaclient/v1_1/servers.py | 19 +++++++++++++++++++ novaclient/v1_1/shell.py | 17 +++++++++++++++++ tests/v1_1/fakes.py | 2 ++ tests/v1_1/test_servers.py | 8 ++++++++ 5 files changed, 47 insertions(+) diff --git a/README.rst b/README.rst index 0fd35fc33..c0416d2a4 100644 --- a/README.rst +++ b/README.rst @@ -119,6 +119,7 @@ You'll find complete documentation on the shell by running floating-ip-pool-list List all floating ip pools. get-vnc-console Get a vnc console to a server. + get-spice-console Get a spice console to a server. host-action Perform a power action on a host. host-update Update host settings. image-create Create a new image by taking a snapshot of a running diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index c93bdf78c..9a9bb1ec1 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -66,6 +66,14 @@ def get_vnc_console(self, console_type): """ return self.manager.get_vnc_console(self, console_type) + def get_spice_console(self, console_type): + """ + Get spice console for a Server. + + :param console_type: Type of console ('spice-html5') + """ + return self.manager.get_spice_console(self, console_type) + def get_password(self, private_key): """ Get password for a Server. @@ -397,6 +405,17 @@ def get_vnc_console(self, server, console_type): return self._action('os-getVNCConsole', server, {'type': console_type})[1] + def get_spice_console(self, server, console_type): + """ + Get a spice console for an instance + + :param server: The :class:`Server` (or its ID) to add an IP to. + :param console_type: Type of spice console to get ('spice-html5') + """ + + return self._action('os-getSPICEConsole', server, + {'type': console_type})[1] + def get_password(self, server, private_key): """ Get password for an instance diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 8c5d55945..92b77f883 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1512,6 +1512,23 @@ def __init__(self, console_dict): utils.print_list([VNCConsole(data['console'])], ['Type', 'Url']) +@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('console_type', + metavar='', + help='Type of spice console ("spice-html5").') +def do_get_spice_console(cs, args): + """Get a spice console to a server.""" + server = _find_server(cs, args.server) + data = server.get_spice_console(args.console_type) + + class SPICEConsole: + def __init__(self, console_dict): + self.type = console_dict['type'] + self.url = console_dict['url'] + + utils.print_list([SPICEConsole(data['console'])], ['Type', 'Url']) + + @utils.arg('server', metavar='', help='Name or ID of server.') @utils.arg('private_key', metavar='', diff --git a/tests/v1_1/fakes.py b/tests/v1_1/fakes.py index 795ceec18..36aafca31 100644 --- a/tests/v1_1/fakes.py +++ b/tests/v1_1/fakes.py @@ -479,6 +479,8 @@ def post_servers_1234_action(self, body, **kw): return (202, {}, {'output': 'foo'}) elif action == 'os-getVNCConsole': assert body[action].keys() == ['type'] + elif action == 'os-getSPICEConsole': + assert body[action].keys() == ['type'] elif action == 'os-migrateLive': assert set(body[action].keys()) == set(['host', 'block_migration', diff --git a/tests/v1_1/test_servers.py b/tests/v1_1/test_servers.py index 1caaa422f..82a09d35f 100644 --- a/tests/v1_1/test_servers.py +++ b/tests/v1_1/test_servers.py @@ -370,6 +370,14 @@ def test_get_vnc_console(self): cs.servers.get_vnc_console(s, 'fake') cs.assert_called('POST', '/servers/1234/action') + def test_get_spice_console(self): + s = cs.servers.get(1234) + s.get_spice_console('fake') + cs.assert_called('POST', '/servers/1234/action') + + cs.servers.get_spice_console(s, 'fake') + cs.assert_called('POST', '/servers/1234/action') + def test_create_image(self): s = cs.servers.get(1234) s.create_image('123') From 3b72eb886f4e7e29a14467469f0873bffdca3033 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Mon, 14 Jan 2013 16:52:47 -0800 Subject: [PATCH 0053/1705] Allow for image selection using the metadata properties. Instead of forcing users to remember a image name and/or uuid instead allow them to specify a image metadata property to 'key' off of. For example: I want to boot a image of ubuntu. This could now be a property set in the image metadata and the following could be used to find said image. $ nova boot my-vm --image-with type=ubuntu Now that user does not need to care what ubuntu version is being selected or what the uuid is or what the image name is, they just know that some image with ubuntu will likely get selected. Change-Id: Ifaf71e282b8411d2e303cb52fd2623f6cf0bf32a --- novaclient/v1_1/shell.py | 51 ++++++++++++++++++++++++++++++++++------ tests/v1_1/test_shell.py | 14 +++++++++++ 2 files changed, 58 insertions(+), 7 deletions(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 62dc25144..36b7a82aa 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -29,6 +29,28 @@ from novaclient.v1_1 import servers +def _key_value_pairing(text): + try: + (k, v) = text.split('=', 1) + return (k, v) + except ValueError: + msg = "%r is not in the format of key=value" % text + raise argparse.ArgumentTypeError(msg) + + +def _match_image(cs, wanted_properties): + image_list = cs.images.list() + images_matched = [] + match = set(wanted_properties) + for img in image_list: + try: + if match == match.intersection(set(img.metadata.items())): + images_matched.append(img) + except AttributeError: + pass + return images_matched + + def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): """Boot a new server.""" if min_count is None: @@ -42,9 +64,23 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): raise exceptions.CommandError("min_instances nor max_instances should" "be 0") - if not args.image and not args.block_device_mapping: + if args.image: + image = _find_image(cs, args.image) + else: + image = None + + if not image and args.image_with: + images = _match_image(cs, args.image_with) + if images: + # TODO(harlowja): log a warning that we + # are selecting the first of many? + image = images[0] + + if not image and not args.block_device_mapping: raise exceptions.CommandError("you need to specify an Image ID " - "or a block device mapping ") + "or a block device mapping " + "or provide a set of properties to match" + " against an image") if not args.flavor: raise exceptions.CommandError("you need to specify a Flavor ID ") @@ -55,11 +91,6 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): flavor = _find_flavor(cs, args.flavor) - if args.image: - image = _find_image(cs, args.image) - else: - image = None - meta = dict(v.split('=', 1) for v in args.meta) files = {} @@ -154,6 +185,12 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): default=None, metavar='', help="Image ID (see 'nova image-list'). ") +@utils.arg('--image-with', + default=[], + type=_key_value_pairing, + action='append', + metavar='', + help="Image metadata property (see 'nova image-show'). ") @utils.arg('--num-instances', default=None, type=int, diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index 6565f1317..e72cc6c44 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -117,6 +117,20 @@ def test_boot(self): }}, ) + def test_boot_image_with(self): + self.run_command("boot --flavor 1" + " --image-with test_key=test_value some-server") + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': '1', + 'min_count': 1, + 'max_count': 1, + }}, + ) + def test_boot_no_image_no_bdms(self): cmd = 'boot --flavor 1 some-server' self.assertRaises(exceptions.CommandError, self.run_command, cmd) From f9aa5ec834410c0e5d37d14ba701344e91941cfc Mon Sep 17 00:00:00 2001 From: Melanie Witt Date: Thu, 10 Jan 2013 01:27:33 +0000 Subject: [PATCH 0054/1705] make print_dict split strings with newlines into multiple rows When printing a dict e.g. "nova show" one of the properties is "fault". When the user is admin, "details" of the fault is shown, which includes a backtrace. This causes the printing of the table to be unreadable as the backtrace overflows the table row. This patch adds a separate row for each substring after splitting on newlines. Change-Id: I4c1bc8725a2bb6970be2c884c5e044d9eade8302 --- novaclient/utils.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/novaclient/utils.py b/novaclient/utils.py index bf76a7316..a3144e190 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -167,8 +167,21 @@ def print_list(objs, fields, formatters={}, sortby_index=0): def print_dict(d, dict_property="Property"): pt = prettytable.PrettyTable([dict_property, 'Value'], caching=False) pt.align = 'l' - [pt.add_row(list(r)) for r in d.iteritems()] - print pt.get_string(sortby=dict_property) + for k, v in d.iteritems(): + # convert dict to str to check length + if isinstance(v, dict): + v = str(v) + # if value has a newline, add in multiple rows + # e.g. fault with stacktrace + if v and isinstance(v, basestring) and r'\n' in v: + lines = v.strip().split(r'\n') + col1 = k + for line in lines: + pt.add_row([col1, line]) + col1 = '' + else: + pt.add_row([k, v]) + print pt.get_string() def find_resource(manager, name_or_id): From ffe13ef94aceb527dfcbb54603cf652d8b60a97f Mon Sep 17 00:00:00 2001 From: Sean McCully Date: Tue, 22 Jan 2013 16:12:02 -0600 Subject: [PATCH 0055/1705] RateLimit does not have method attribute Fixes Bug #1103220. RateLimit class raises Exceptions anytime __repr__ is called because cls does not have method attribute. Change-Id: I7db2e1a3afddaae18d006e0cc884e9910afb8eaf --- novaclient/v1_1/limits.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/v1_1/limits.py b/novaclient/v1_1/limits.py index 6c137f364..bcf8fa2d4 100644 --- a/novaclient/v1_1/limits.py +++ b/novaclient/v1_1/limits.py @@ -48,7 +48,7 @@ def __eq__(self, other): and self.next_available == other.next_available def __repr__(self): - return "" % (self.method, self.uri) + return "" % (self.verb, self.uri) class AbsoluteLimit(object): From aecd98430a53a6dc5258f19c81cb013001b4d338 Mon Sep 17 00:00:00 2001 From: Chris Jones Date: Tue, 22 Jan 2013 13:58:25 +0000 Subject: [PATCH 0056/1705] Adds baremetal nova API support This implements the various REST API calls that BareMetal adds to Nova. (The nova API is implemented in review 19077) Change-Id: I187862e9aa5dea41a7edf716aa75cc3d9982fbc8 --- novaclient/v1_1/contrib/baremetal.py | 283 +++++++++++++++++++++++++++ tests/v1_1/contrib/fakes.py | 87 ++++++++ tests/v1_1/contrib/test_baremetal.py | 61 ++++++ 3 files changed, 431 insertions(+) create mode 100644 novaclient/v1_1/contrib/baremetal.py create mode 100644 tests/v1_1/contrib/test_baremetal.py diff --git a/novaclient/v1_1/contrib/baremetal.py b/novaclient/v1_1/contrib/baremetal.py new file mode 100644 index 000000000..3a753c0b1 --- /dev/null +++ b/novaclient/v1_1/contrib/baremetal.py @@ -0,0 +1,283 @@ +# Copyright 2013 Hewlett-Packard Development Company, L.P. +# All Rights Reserved. +# +# 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. + +""" +Baremetal interface (v2 extension). +""" + +import urllib + +from novaclient import base +from novaclient import utils + + +class BareMetalNode(base.Resource): + """ + A baremetal node (typically a physical server or an empty VM). + """ + + def __repr__(self): + return "" % self.id + + +class BareMetalNodeInterface(base.Resource): + """ + An interface belonging to a baremetal node. + """ + + def __repr__(self): + return "" % self.id + + +class BareMetalNodeManager(base.ManagerWithFind): + """ + Manage :class:`BareMetalNode` resources. + """ + resource_class = BareMetalNode + + def create(self, + service_host, + cpus, + memory_mb, + local_gb, + prov_mac_address, + pm_address=None, + pm_user=None, + pm_password=None, + prov_vlan_id=None, + terminal_port=None): + """ + Create a baremetal node. + + :param service_host: Name of controlling compute host + :param cpus: Number of CPUs in the node + :param memory_mb: Megabytes of RAM in the node + :param local_gb: Gigabytes of local storage in the node + :param pm_address: Power management IP for the node + :param pm_user: Username for the node's power management + :param pm_password: Password for the node's power management + :param prov_mac_address: MAC address to provision the node + :param prov_vlan_id: VLAN ID to use to provision the node + :param terminal_port: ShellInABox port + :rtype: :class:`BareMetalNode` + """ + body = {'node': {'service_host': service_host, + 'cpus': cpus, + 'memory_mb': memory_mb, + 'local_gb': local_gb, + 'pm_address': pm_address, + 'pm_user': pm_user, + 'pm_password': pm_password, + 'prov_mac_address': prov_mac_address, + 'prov_vlan_id': prov_vlan_id, + 'terminal_port': terminal_port}} + + return self._create('/os-baremetal-nodes', body, 'node') + + def delete(self, node): + """ + Delete a baremetal node. + + :param node: The :class:`BareMetalNode` to delete. + """ + self._delete('/os-baremetal-nodes/%s' % base.getid(node)) + + def get(self, node_id): + """ + Get a baremetal node. + + :param node_id: The ID of the node to delete. + :rtype: :class:`BareMetalNode` + """ + return self._get("/os-baremetal-nodes/%s" % node_id, 'node') + + def list(self): + """ + Get a list of all baremetal nodes. + + :rtype: list of :class:`BareMetalNode` + """ + return self._list('/os-baremetal-nodes', 'nodes') + + def add_interface(self, node_id, address, datapath_id=0, port_no=0): + """ + Add an interface to a baremetal node. + + :param node_id: The ID of the node to modify. + :param address: The MAC address to add. + :param datapath_id: Datapath ID of OpenFlow switch for the interface + :param port_no: OpenFlow port number for the interface + :rtype: :class:`BareMetalNodeInterface` + """ + body = {'add_interface': {'address': address, + 'datapath_id': datapath_id, + 'port_no': port_no}} + url = '/os-baremetal-nodes/%s/action' % node_id + _resp, body = self.api.client.post(url, body=body) + return BareMetalNodeInterface(self, body['interface']) + + def remove_interface(self, node_id, address): + """ + Remove an interface from a baremetal node. + + :param node_id: The ID of the node to modify. + :param address: The MAC address to remove. + :rtype: bool + """ + req_body = {'remove_interface': {'address': address}} + url = '/os-baremetal-nodes/%s/action' % node_id + self.api.client.post(url, body=req_body) + + +@utils.arg('service_host', + metavar='', + help='Name of nova compute host which will control this baremetal node') +@utils.arg('cpus', + metavar='', + type=int, + help='Number of CPUs in the node') +@utils.arg('memory_mb', + metavar='', + type=int, + help='Megabytes of RAM in the node') +@utils.arg('local_gb', + metavar='', + type=int, + help='Gigabytes of local storage in the node') +@utils.arg('prov_mac_address', + metavar='', + help='MAC address to provision the node') +@utils.arg('--pm_address', default=None, + metavar='', + help='Power management IP for the node') +@utils.arg('--pm_user', default=None, + metavar='', + help='Username for the node\'s power management') +@utils.arg('--pm_password', default=None, + metavar='', + help='Password for the node\'s power management') +@utils.arg('--prov_vlan_id', default=None, + metavar='', + type=int, + help='VLAN ID to use to provision the node') +@utils.arg('--terminal_port', default=None, + metavar='', + type=int, + help='ShellInABox port?') +def do_baremetal_node_create(cs, args): + """Create a baremetal node""" + return cs.baremetal.create(args.service_host, args.cpus, + args.memory_mb, args.local_gb, args.prov_mac_address, + pm_address=args.pm_address, pm_user=args.pm_user, + pm_password=args.pm_password, + prov_vlan_id=args.prov_vlan_id, + terminal_port=args.terminal_port) + + +@utils.arg('node', + metavar='', + help='ID of the node to delete.') +def do_baremetal_node_delete(cs, args): + """Remove a volume.""" + node = _find_baremetal_node(cs, args.node) + cs.baremetal.delete(node) + + +def _translate_baremetal_node_keys(collection): + convert = [('service_host', 'host'), + ('local_gb', 'disk_gb'), + ('prov_mac_address', 'mac_address'), + ('prov_vlan_id', 'vlan'), + ('pm_address', 'pm_address'), + ('pm_user', 'pm_username'), + ('pm_password', 'pm_password'), + ('terminal_port', 'terminal_port'), + ] + for item in collection: + keys = item.__dict__.keys() + for from_key, to_key in convert: + if from_key in keys and to_key not in keys: + setattr(item, to_key, item._info[from_key]) + + +def _print_baremetal_nodes_list(cs, nodes): + """Print the list of baremetal nodes""" + _translate_baremetal_node_keys(nodes) + utils.print_list(nodes, [ + 'ID', + 'Host', + 'CPUs', + 'Memory_MB', + 'Disk_GB', + 'MAC Address', + 'VLAN', + 'PM Address', + 'PM Username', + 'PM Password', + 'Terminal Port', + ]) + + +def do_baremetal_node_list(cs, _args): + """Print a list of available baremetal nodes.""" + nodes = cs.baremetal.list() + _print_baremetal_nodes_list(cs, nodes) + + +def _find_baremetal_node(cs, node): + """Get a node by ID.""" + return utils.find_resource(cs.baremetal, node) + + +def _print_baremetal_node(cs, node): + """Print the details of a baremetal node""" + info = node._info.copy() + utils.print_dict(info) + + +@utils.arg('node', + metavar='', + help="ID of node") +def do_baremetal_node_show(cs, args): + """Show information about a node""" + node = _find_baremetal_node(cs, args.node) + _print_baremetal_node(cs, node) + + +@utils.arg('node', + metavar='', + help="ID of node") +@utils.arg('address', + metavar='
', + help="MAC address of interface") +@utils.arg('--datapath_id', + default=0, + metavar='', + help="OpenFlow Datapath ID of interface") +@utils.arg('--port_no', + default=0, + metavar='', + help="OpenFlow port number of interface") +def do_baremetal_add_interface(cs, args): + """Add a network interface to a baremetal node""" + return cs.baremetal.add_interface(args.node, args.address, + args.datapath_id, args.port_no) + + +@utils.arg('node', metavar='', help="ID of node") +@utils.arg('address', metavar='
', help="MAC address of interface") +def do_baremetal_remove_interface(cs, args): + """Remove a network interface from a baremetal node""" + cs.baremetal.remove_interface(args.node, args.address) diff --git a/tests/v1_1/contrib/fakes.py b/tests/v1_1/contrib/fakes.py index dd7b889fe..b5c041d89 100644 --- a/tests/v1_1/contrib/fakes.py +++ b/tests/v1_1/contrib/fakes.py @@ -42,3 +42,90 @@ def post_os_tenant_networks(self, **kw): def delete_os_tenant_networks_1(self, **kw): return (204, {}, None) + + def get_os_baremetal_nodes(self, **kw): + return ( + 200, {}, { + 'nodes': [ + { + "id": 1, + "instance_uuid": None, + "pm_address": "1.2.3.4", + "interfaces": [], + "cpus": 2, + "local_gb": 10, + "memory_mb": 5, + "pm_address": "2.3.4.5", + "pm_user": "pmuser", + "pm_password": "pmpass", + "prov_mac_address": "aa:bb:cc:dd:ee:ff", + "prov_vlan_id": 1, + "service_host": "somehost", + "terminal_port": 8080, + } + ] + } + ) + + def get_os_baremetal_nodes_1(self, **kw): + return ( + 200, {}, { + 'node': { + "id": 1, + "instance_uuid": None, + "pm_address": "1.2.3.4", + "interfaces": [], + "cpus": 2, + "local_gb": 10, + "memory_mb": 5, + "pm_user": "pmuser", + "pm_password": "pmpass", + "prov_mac_address": "aa:bb:cc:dd:ee:ff", + "prov_vlan_id": 1, + "service_host": "somehost", + "terminal_port": 8080, + } + } + ) + + def post_os_baremetal_nodes(self, **kw): + return ( + 200, {}, { + 'node': { + "id": 1, + "instance_uuid": None, + "cpus": 2, + "local_gb": 10, + "memory_mb": 5, + "pm_address": "2.3.4.5", + "pm_user": "pmuser", + "pm_password": "pmpass", + "prov_mac_address": "aa:bb:cc:dd:ee:ff", + "prov_vlan_id": 1, + "service_host": "somehost", + "terminal_port": 8080, + } + } + ) + + def delete_os_baremetal_nodes_1(self, **kw): + return (202, {}, {}) + + def post_os_baremetal_nodes_1_action(self, **kw): + body = kw['body'] + action = body.keys()[0] + if action == "add_interface": + return ( + 200, {}, { + 'interface': { + "id": 2, + "address": "bb:cc:dd:ee:ff:aa", + "datapath_id": 1, + "port_no": 2, + } + } + ) + elif action == "remove_interface": + return (202, {}, {}) + else: + return (500, {}, {}) diff --git a/tests/v1_1/contrib/test_baremetal.py b/tests/v1_1/contrib/test_baremetal.py new file mode 100644 index 000000000..b95fc97d5 --- /dev/null +++ b/tests/v1_1/contrib/test_baremetal.py @@ -0,0 +1,61 @@ +# Copyright 2013 Hewlett-Packard Development Company, L.P. +# All Rights Reserved. +# +# 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 novaclient import extension +from novaclient.v1_1.contrib import baremetal + +from tests import utils +from tests.v1_1.contrib import fakes + + +extensions = [ + extension.Extension(baremetal.__name__.split(".")[-1], baremetal), + ] +cs = fakes.FakeClient(extensions=extensions) + + +class BaremetalExtensionTest(utils.TestCase): + + def test_list_nodes(self): + nl = cs.baremetal.list() + cs.assert_called('GET', '/os-baremetal-nodes') + for n in nl: + self.assertTrue(isinstance(n, baremetal.BareMetalNode)) + + def test_get_node(self): + n = cs.baremetal.get(1) + cs.assert_called('GET', '/os-baremetal-nodes/1') + self.assertTrue(isinstance(n, baremetal.BareMetalNode)) + + def test_create_node(self): + n = cs.baremetal.create("service_host", 1, 1024, 2048, + "aa:bb:cc:dd:ee:ff") + cs.assert_called('POST', '/os-baremetal-nodes') + self.assertTrue(isinstance(n, baremetal.BareMetalNode)) + + def test_delete_node(self): + n = cs.baremetal.get(1) + cs.baremetal.delete(n) + cs.assert_called('DELETE', '/os-baremetal-nodes/1') + + def test_node_add_interface(self): + i = cs.baremetal.add_interface(1, "bb:cc:dd:ee:ff:aa", 1, 2) + cs.assert_called('POST', '/os-baremetal-nodes/1/action') + self.assertTrue(isinstance(i, baremetal.BareMetalNodeInterface)) + + def test_node_remove_interface(self): + cs.baremetal.remove_interface(1, "bb:cc:dd:ee:ff:aa") + cs.assert_called('POST', '/os-baremetal-nodes/1/action') From 0614ae75f6f92ba990a55a355bfec97438b9fb15 Mon Sep 17 00:00:00 2001 From: Sean McCully Date: Wed, 23 Jan 2013 11:14:41 -0600 Subject: [PATCH 0057/1705] ClientExceptions should include url and method Fixes Bug 1103557. novaclient abstracts out http request from user/client making it unknown to user what root cause behind nova client exceptions being raised. By including url and method in exception handling, this allows user to handle accordingly. Change-Id: I1a509bb932b3fd029bd0870ab699a39e21da19bb --- novaclient/client.py | 6 +++--- novaclient/exceptions.py | 12 ++++++++---- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index 9b9797928..e843134c1 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -192,7 +192,7 @@ def request(self, url, method, **kwargs): body = None if resp.status_code >= 400: - raise exceptions.from_response(resp, body) + raise exceptions.from_response(resp, body, url, method) return resp, body @@ -275,7 +275,7 @@ def _extract_service_catalog(self, url, resp, body, extract_token=True): elif resp.status_code == 305: return resp.headers['location'] else: - raise exceptions.from_response(resp, body) + raise exceptions.from_response(resp, body, url) def _fetch_endpoints_from_auth(self, url): """We have a token, but don't know the final endpoint for @@ -409,7 +409,7 @@ def _v1_auth(self, url): elif resp.status_code == 305: return resp.headers['location'] else: - raise exceptions.from_response(resp, body) + raise exceptions.from_response(resp, body, url) def _plugin_auth(self, auth_url): """Load plugin-based authentication""" diff --git a/novaclient/exceptions.py b/novaclient/exceptions.py index e64e12d35..142f2f3f6 100644 --- a/novaclient/exceptions.py +++ b/novaclient/exceptions.py @@ -66,11 +66,14 @@ class ClientException(Exception): """ The base exception class for all exceptions this library raises. """ - def __init__(self, code, message=None, details=None, request_id=None): + def __init__(self, code, message=None, details=None, request_id=None, + url=None, method=None): self.code = code self.message = message or self.__class__.message self.details = details self.request_id = request_id + self.url = url + self.method = method def __str__(self): formatted_string = "%s (HTTP %s)" % (self.message, self.code) @@ -140,7 +143,7 @@ class HTTPNotImplemented(ClientException): Forbidden, NotFound, OverLimit, HTTPNotImplemented]) -def from_response(response, body): +def from_response(response, body, url, method=None): """ Return an instance of an ClientException or subclass based on an requests response. @@ -164,6 +167,7 @@ def from_response(response, body): message = error.get('message', None) details = error.get('details', None) return cls(code=response.status_code, message=message, details=details, - request_id=request_id) + request_id=request_id, url=url, method=method) else: - return cls(code=response.status_code, request_id=request_id) + return cls(code=response.status_code, request_id=request_id, url=url, + method=method) From 168636e744012f264310897603eb4e3dde6d1df8 Mon Sep 17 00:00:00 2001 From: Haiwei Xu Date: Wed, 26 Dec 2012 11:24:55 +0000 Subject: [PATCH 0058/1705] Check tenant_id's format in "nova quota-update" Fix bug 1088835 "nova quota-update" command is executed without checking the format of the tenant_id argument. The tenant_id should be in the format of UUID. The tenant_id of quotas should be in accord with the form of keystone's tenant_id. So this patch checks the format of the tenant_id when "nova quota-update" command is executed. Change-Id: I47c4f2ff9adbab5da4697270dcf024ac88e24529 --- novaclient/utils.py | 13 +++++++ novaclient/v1_1/shell.py | 4 +++ tests/v1_1/fakes.py | 76 +++++++++++++++++++++++++++++++++++++-- tests/v1_1/test_quotas.py | 6 ++-- tests/v1_1/test_shell.py | 19 +++++++--- 5 files changed, 110 insertions(+), 8 deletions(-) diff --git a/novaclient/utils.py b/novaclient/utils.py index bf76a7316..90902a2e2 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -289,3 +289,16 @@ def slugify(value): value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore') value = unicode(_slugify_strip_re.sub('', value).strip().lower()) return _slugify_hyphenate_re.sub('-', value) + + +def is_uuid_like(val): + """ + The UUID which doesn't contain hyphens or 'A-F' is allowed. + """ + try: + if uuid.UUID(val) and val.isalnum() and val.islower(): + return True + else: + return False + except (TypeError, ValueError, AttributeError): + return False diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 92b77f883..ff74a3fcc 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2532,6 +2532,10 @@ def _quota_show(quotas): def _quota_update(manager, identifier, args): updates = {} + if not utils.is_uuid_like(identifier): + raise exceptions.CommandError( + "error: Invalid tenant-id %s supplied for update" + % identifier) for resource in _quota_resources: val = getattr(args, resource, None) if val is not None: diff --git a/tests/v1_1/fakes.py b/tests/v1_1/fakes.py index 36aafca31..48ab33d1a 100644 --- a/tests/v1_1/fakes.py +++ b/tests/v1_1/fakes.py @@ -863,6 +863,57 @@ def get_os_quota_sets_test(self, **kw): 'security_groups': 1, 'security_group_rules': 1}}) + def get_os_quota_sets_97f4c221bff44578b0300df4ef119353(self, **kw): + return (200, {}, {'quota_set': { + 'tenant_id': '97f4c221bff44578b0300df4ef119353', + 'metadata_items': [], + 'injected_file_content_bytes': 1, + 'injected_file_path_bytes': 1, + 'volumes': 1, + 'gigabytes': 1, + 'ram': 1, + 'floating_ips': 1, + 'instances': 1, + 'injected_files': 1, + 'cores': 1, + 'keypairs': 1, + 'security_groups': 1, + 'security_group_rules': 1}}) + + def put_os_quota_sets_97f4c221_bff4_4578_b030_0df4ef119353(self, **kw): + return (200, {}, {'quota_set': { + 'tenant_id': '97f4c221-bff4-4578-b030-0df4ef119353', + 'metadata_items': [], + 'injected_file_content_bytes': 1, + 'injected_file_path_bytes': 1, + 'volumes': 1, + 'gigabytes': 1, + 'ram': 1, + 'floating_ips': 1, + 'instances': 1, + 'injected_files': 1, + 'cores': 1, + 'keypairs': 1, + 'security_groups': 1, + 'security_group_rules': 1}}) + + def get_os_quota_sets_97f4c221_bff4_4578_b030_0df4ef119353(self, **kw): + return (200, {}, {'quota_set': { + 'tenant_id': '97f4c221-bff4-4578-b030-0df4ef119353', + 'metadata_items': [], + 'injected_file_content_bytes': 1, + 'injected_file_path_bytes': 1, + 'volumes': 1, + 'gigabytes': 1, + 'ram': 1, + 'floating_ips': 1, + 'instances': 1, + 'injected_files': 1, + 'cores': 1, + 'keypairs': 1, + 'security_groups': 1, + 'security_group_rules': 1}}) + def get_os_quota_sets_test_defaults(self): return (200, {}, {'quota_set': { 'tenant_id': 'test', @@ -880,12 +931,12 @@ def get_os_quota_sets_test_defaults(self): 'security_groups': 1, 'security_group_rules': 1}}) - def put_os_quota_sets_test(self, body, **kw): + def put_os_quota_sets_97f4c221bff44578b0300df4ef119353(self, body, **kw): assert body.keys() == ['quota_set'] fakes.assert_has_keys(body['quota_set'], required=['tenant_id']) return (200, {}, {'quota_set': { - 'tenant_id': 'test', + 'tenant_id': '97f4c221bff44578b0300df4ef119353', 'metadata_items': [], 'injected_file_content_bytes': 1, 'injected_file_path_bytes': 1, @@ -941,6 +992,27 @@ def put_os_quota_class_sets_test(self, body, **kw): 'security_groups': 1, 'security_group_rules': 1}}) + def put_os_quota_class_sets_97f4c221bff44578b0300df4ef119353(self, + body, **kw): + assert body.keys() == ['quota_class_set'] + fakes.assert_has_keys(body['quota_class_set'], + required=['class_name']) + return (200, {}, {'quota_class_set': { + 'class_name': '97f4c221bff44578b0300df4ef119353', + 'metadata_items': [], + 'injected_file_content_bytes': 1, + 'injected_file_path_bytes': 1, + 'volumes': 2, + 'gigabytes': 1, + 'ram': 1, + 'floating_ips': 1, + 'instances': 1, + 'injected_files': 1, + 'cores': 1, + 'keypairs': 1, + 'security_groups': 1, + 'security_group_rules': 1}}) + # # Security Groups # diff --git a/tests/v1_1/test_quotas.py b/tests/v1_1/test_quotas.py index 61e06ef67..27cdb26ee 100644 --- a/tests/v1_1/test_quotas.py +++ b/tests/v1_1/test_quotas.py @@ -16,6 +16,7 @@ from tests import utils from tests.v1_1 import fakes +from novaclient import exceptions cs = fakes.FakeClient() @@ -33,9 +34,10 @@ def test_tenant_quotas_defaults(self): cs.assert_called('GET', '/os-quota-sets/%s/defaults' % tenant_id) def test_update_quota(self): - q = cs.quotas.get('test') + q = cs.quotas.get('97f4c221bff44578b0300df4ef119353') q.update(volumes=2) - cs.assert_called('PUT', '/os-quota-sets/test') + cs.assert_called('PUT', + '/os-quota-sets/97f4c221bff44578b0300df4ef119353') def test_refresh_quota(self): q = cs.quotas.get('test') diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index 8fd88757a..8e206e5ec 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -702,16 +702,27 @@ def test_quota_defaults(self): self.assert_called('GET', '/os-quota-sets/test/defaults') def test_quota_update(self): - self.run_command('quota-update test --instances=5') - self.assert_called('PUT', '/os-quota-sets/test') + self.run_command( + 'quota-update 97f4c221bff44578b0300df4ef119353 \ + --instances=5') + self.assert_called('PUT', + '/os-quota-sets/97f4c221bff44578b0300df4ef119353') + + def test_quota_update_error(self): + self.assertRaises(exceptions.CommandError, + self.run_command, + 'quota-update 7f4c221-bff4-4578-b030-0df4ef119353 \ + --instances=5') def test_quota_class_show(self): self.run_command('quota-class-show test') self.assert_called('GET', '/os-quota-class-sets/test') def test_quota_class_update(self): - self.run_command('quota-class-update test --instances=5') - self.assert_called('PUT', '/os-quota-class-sets/test') + self.run_command('quota-class-update 97f4c221bff44578b0300df4ef119353 \ + --instances=5') + self.assert_called('PUT', + '/os-quota-class-sets/97f4c221bff44578b0300df4ef119353') def test_network_list(self): self.run_command('network-list') From 364ef418b4b71336cec52869718056a1b6ce2273 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Fri, 25 Jan 2013 15:46:01 -0800 Subject: [PATCH 0059/1705] Fix bash completion on osx The osx version of sed does not support \s so replace uses of it with [ ] and clean up the regex a bit. Change-Id: Ic6fd2e4234352ddb8ec70d42b44ad00a3906db0e --- tools/nova.bash_completion | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/nova.bash_completion b/tools/nova.bash_completion index 7dee3e6d7..fefa79831 100644 --- a/tools/nova.bash_completion +++ b/tools/nova.bash_completion @@ -9,10 +9,10 @@ _nova() prev="${COMP_WORDS[COMP_CWORD-1]}" if [ "x$_nova_opts" == "x" ] ; then - nbc="`nova bash-completion | sed -e "s/\s-h\s/ /"`" - _nova_opts="`echo "$nbc" | sed -e "s/--[a-z0-9_-]*//g" -e "s/\s\s*/ /g"`" - _nova_flags="`echo " $nbc" | sed -e "s/ [^-][^-][a-z0-9_-]*//g" -e "s/\s\s*/ /g"`" - _nova_opts_exp="`echo "$_nova_opts" | sed -e "s/\s/|/g"`" + nbc="`nova bash-completion | sed -e "s/[ ]+/ /"`" + _nova_opts="`echo "$nbc" | sed -e "s/--[a-z0-9_-]*//g" -e "s/[ ]+/ /g"`" + _nova_flags="`echo " $nbc" | sed -e "s/ [^-][^-][a-z0-9_-]*//g" -e "s/[ ]+/ /g"`" + _nova_opts_exp="`echo "$_nova_opts" | sed -e "s/[ ]/|/g"`" fi if [[ " ${COMP_WORDS[@]} " =~ " "($_nova_opts_exp)" " && "$prev" != "help" ]] ; then From e2edcef40d8e3faa06126e9b9a01458c3cd14672 Mon Sep 17 00:00:00 2001 From: Aaron Rosen Date: Fri, 25 Jan 2013 15:01:12 -0800 Subject: [PATCH 0060/1705] _get_secgroup returns first group even if multiple groups match Nova's security group api layer ensures that a given tenant is not able to create multiple security groups with the same name. That said, if one is the admin user and does: "nova secgroup-delete " The first security group that is returned with that name will be removed, which might not have been what the user intended to do. In addition, quantum allows multiple security groups to be created with the same name therefore this change is needed to ensure the correct behavior when nova has a quantum security group proxy. Fixes bug 1105542 Change-Id: I99734355026e2ad18056c2f4180b5740fda8775a --- novaclient/v1_1/shell.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 92b77f883..45e2812fa 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1756,14 +1756,21 @@ def _print_secgroups(secgroups): def _get_secgroup(cs, secgroup): + match_found = False for s in cs.security_groups.list(): encoding = (locale.getpreferredencoding() or sys.stdin.encoding or 'UTF-8') s.name = s.name.encode(encoding) if secgroup == s.name: - return s - raise exceptions.CommandError("Secgroup %s not found" % secgroup) + if match_found != False: + msg = ("Multiple security group matches found for name" + " '%s', use an ID to be more specific." % secgroup) + raise exceptions.NoUniqueMatch(msg) + match_found = s + if match_found is False: + raise exceptions.CommandError("Secgroup %s not found" % secgroup) + return match_found @utils.arg('secgroup', metavar='', help='ID of security group.') From 7b2097e706e944ce868e633467b89f728a5d5042 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 24 Dec 2012 19:58:11 -0600 Subject: [PATCH 0061/1705] Migrate from nose to testr. Run tests with testr for parallel execution. Part of blueprint grizzly-testtools. Change-Id: I38e8a2df12678002e19264a53bad26e80265c6e4 --- .gitignore | 2 ++ .testr.conf | 4 +++ HACKING | 8 ++++++ run_tests.sh | 60 +++++++++++++++++++++++++++++++++------------ setup.cfg | 8 ------ setup.py | 1 - tests/utils.py | 14 +++++++++++ tools/test-requires | 12 ++++----- tox.ini | 33 +++++-------------------- 9 files changed, 84 insertions(+), 58 deletions(-) create mode 100644 .testr.conf diff --git a/.gitignore b/.gitignore index 3576b8447..90d201e2a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ .coverage .venv +.testrepository +subunit.log .tox *,cover cover diff --git a/.testr.conf b/.testr.conf new file mode 100644 index 000000000..2109af6ce --- /dev/null +++ b/.testr.conf @@ -0,0 +1,4 @@ +[DEFAULT] +test_command=OS_STDOUT_CAPTURE=1 OS_STDERR_CAPTURE=1 ${PYTHON:-python} -m subunit.run discover -t ./ ./tests $LISTOPT $IDOPTION +test_id_option=--load-list $IDFILE +test_list_option=--list diff --git a/HACKING b/HACKING index f131e04a1..1c1a04a73 100644 --- a/HACKING +++ b/HACKING @@ -113,3 +113,11 @@ Text encoding returntext = do_some_magic_with(mytext) returnstring = returntext.encode('utf-8') outfile.write(returnstring) + +Running Tests +------------- +The testing system is based on a combination of tox and testr. If you just +want to run the whole suite, run `tox` and all will be fine. However, if +you'd like to dig in a bit more, you might want to learn some things about +testr itself. A basic walkthrough for OpenStack can be found at +http://wiki.openstack.org/testr diff --git a/run_tests.sh b/run_tests.sh index aa832b353..e33a95d98 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -33,8 +33,8 @@ function process_option { -p|--pep8) just_pep8=1;; -P|--no-pep8) no_pep8=1;; -c|--coverage) coverage=1;; - -*) noseopts="$noseopts $1";; - *) noseargs="$noseargs $1" + -*) testropts="$testropts $1";; + *) testrargs="$testrargs $1" esac } @@ -45,34 +45,62 @@ never_venv=0 force=0 no_site_packages=0 installvenvopts= -noseargs= -noseopts= +testrargs= +testropts= wrapper="" just_pep8=0 no_pep8=0 coverage=0 +LANG=en_US.UTF-8 +LANGUAGE=en_US:en +LC_ALL=C + for arg in "$@"; do process_option $arg done -# If enabled, tell nose to collect coverage data -if [ $coverage -eq 1 ]; then - noseopts="$noseopts --with-coverage --cover-package=novaclient" -fi - if [ $no_site_packages -eq 1 ]; then installvenvopts="--no-site-packages" fi +function init_testr { + if [ ! -d .testrepository ]; then + ${wrapper} testr init + fi +} + function run_tests { + # Cleanup *pyc + ${wrapper} find . -type f -name "*.pyc" -delete + + if [ $coverage -eq 1 ]; then + # Do not test test_coverage_ext when gathering coverage. + if [ "x$testrargs" = "x" ]; then + testrargs = "^(?!.*test_coverage_ext).*$" + fi + export PYTHON="${wrapper} coverage run --source novaclient --parallel-mode" + fi # Just run the test suites in current environment - ${wrapper} $NOSETESTS - # If we get some short import error right away, print the error log directly + set +e + TESTRTESTS="$TESTRTESTS $testrargs" + echo "Running \`${wrapper} $TESTRTESTS\`" + ${wrapper} $TESTRTESTS RESULT=$? + set -e + + copy_subunit_log + return $RESULT } +function copy_subunit_log { + LOGNAME=`cat .testrepository/next-stream` + LOGNAME=$(($LOGNAME - 1)) + LOGNAME=".testrepository/${LOGNAME}" + cp $LOGNAME subunit.log +} + function run_pep8 { echo "Running pep8 ..." srcfiles="novaclient tests" @@ -96,7 +124,7 @@ function run_pep8 { ${wrapper} pep8 ${pep8_opts} ${srcfiles} } -NOSETESTS="nosetests $noseopts $noseargs" +TESTRTESTS="testr run --parallel $testropts" if [ $never_venv -eq 0 ] then @@ -134,13 +162,14 @@ if [ $just_pep8 -eq 1 ]; then exit fi +init_testr run_tests # NOTE(sirp): we only want to run pep8 when we're running the full-test suite, # not when we're running tests individually. To handle this, we need to # distinguish between options (noseopts), which begin with a '-', and -# arguments (noseargs). -if [ -z "$noseargs" ]; then +# arguments (testrargs). +if [ -z "$testrargs" ]; then if [ $no_pep8 -eq 0 ]; then run_pep8 fi @@ -148,5 +177,6 @@ fi if [ $coverage -eq 1 ]; then echo "Generating coverage report in covhtml/" - ${wrapper} coverage html -d covhtml -i + ${wrapper} cverage combine + ${wrapper} coverage html --include='novaclient/*' --omit='novaclient/openstack/common/*' -d covhtml -i fi diff --git a/setup.cfg b/setup.cfg index 7fa3ddac5..11c72013c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,11 +1,3 @@ -[nosetests] -cover-package = novaclient -cover-html = true -cover-erase = true -cover-inclusive = true -verbosity=2 -detailed-errors=1 - [build_sphinx] source-dir = doc/source build-dir = doc/build diff --git a/setup.py b/setup.py index e0e68fa14..062e49b75 100644 --- a/setup.py +++ b/setup.py @@ -34,7 +34,6 @@ def read_file(file_name): url="https://github.com/openstack/python-novaclient", packages=setuptools.find_packages(exclude=['tests', 'tests.*']), install_requires=setup.parse_requirements(), - test_suite="nose.collector", cmdclass=setup.get_cmdclass(), classifiers=[ "Development Status :: 5 - Production/Stable", diff --git a/tests/utils.py b/tests/utils.py index ef231a775..d3a744219 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,3 +1,6 @@ +import os + +import fixtures import requests import testtools @@ -8,6 +11,17 @@ class TestCase(testtools.TestCase): 'verify': True, } + def setUp(self): + super(TestCase, self).setUp() + if (os.environ.get('OS_STDOUT_NOCAPTURE') == 'True' and + os.environ.get('OS_STDOUT_NOCAPTURE') == '1'): + stdout = self.useFixture(fixtures.StringStream('stdout')).stream + self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout)) + if (os.environ.get('OS_STDERR_NOCAPTURE') == 'True' and + os.environ.get('OS_STDERR_NOCAPTURE') == '1'): + stderr = self.useFixture(fixtures.StringStream('stderr')).stream + self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr)) + class TestResponse(requests.Response): """ diff --git a/tools/test-requires b/tools/test-requires index f69ef390d..0dacea2ac 100644 --- a/tools/test-requires +++ b/tools/test-requires @@ -1,12 +1,10 @@ distribute>=0.6.24 -fixtures +coverage +discover +fixtures>=0.3.12 mock -nose -nose-exclude -nosexcover -openstack.nose_plugin -nosehtmloutput pep8==1.1 sphinx>=1.1.2 -testtools +testrepository>=0.0.13 +testtools>=0.9.26 diff --git a/tox.ini b/tox.ini index 7ddc885b1..88fb5cc40 100644 --- a/tox.ini +++ b/tox.ini @@ -3,14 +3,13 @@ envlist = py26,py27,pep8 [testenv] setenv = VIRTUAL_ENV={envdir} - NOSE_WITH_OPENSTACK=1 - NOSE_OPENSTACK_COLOR=1 - NOSE_OPENSTACK_RED=0.05 - NOSE_OPENSTACK_YELLOW=0.025 - NOSE_OPENSTACK_SHOW_ELAPSED=1 + LANG=en_US.UTF-8 + LANGUAGE=en_US:en + LC_ALL=C + deps = -r{toxinidir}/tools/pip-requires -r{toxinidir}/tools/test-requires -commands = nosetests +commands = python setup.py testr --testr-args='{posargs}' [testenv:pep8] deps = pep8==1.1 @@ -20,27 +19,7 @@ commands = pep8 --repeat --show-source novaclient setup.py commands = {posargs} [testenv:cover] -commands = nosetests --cover-erase --cover-package=novaclient --with-xcoverage +commands = python setup.py testr --coverage --testr-args='{posargs}' [tox:jenkins] downloadcache = ~/cache/pip - -[testenv:jenkins26] -basepython = python2.6 -setenv = NOSE_WITH_XUNIT=1 -deps = file://{toxinidir}/.cache.bundle - -[testenv:jenkins27] -basepython = python2.7 -setenv = NOSE_WITH_XUNIT=1 -deps = file://{toxinidir}/.cache.bundle - -[testenv:jenkinscover] -deps = file://{toxinidir}/.cache.bundle -setenv = NOSE_WITH_XUNIT=1 -commands = nosetests --cover-erase --cover-package=novaclient --with-xcoverage - -[testenv:jenkinsvenv] -deps = file://{toxinidir}/.cache.bundle -setenv = NOSE_WITH_XUNIT=1 -commands = {posargs} From b0e0319c6656a42040c7d5a0670548249634b665 Mon Sep 17 00:00:00 2001 From: Anita Kuno Date: Tue, 29 Jan 2013 16:38:36 +0000 Subject: [PATCH 0062/1705] Added homedir path expansion for keypair-add. Change-Id: I76c5454952824dc9c238d82af74f82ae5fc888b5 Fixes: Bug #1065366 --- novaclient/v1_1/shell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 92b77f883..836a4c2f9 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1938,7 +1938,7 @@ def do_keypair_add(cs, args): if pub_key: try: - with open(pub_key) as f: + with open(os.path.expanduser(pub_key)) as f: pub_key = f.read() except IOError, e: raise exceptions.CommandError("Can't open or read '%s': %s" % \ From 7c6f538cf6bc86ecfd9cb25f1bf7f14a6652b592 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Wed, 30 Jan 2013 12:53:35 -0800 Subject: [PATCH 0063/1705] Fix the usage of password, keyrings, and tokens. Fix how you keep on getting prompted for the password all over the place and at the wrong time (the keyring code should be used before a auth request, and not during). Fix this by trying to use the token first (which comes from the keyring module), then falling back to using the password (which will get a token, and then store said token so that it isn't fetched again). Change-Id: I58e69f3b3fbcc7a467797f25695b7ca59178a1d7 --- novaclient/client.py | 56 ++++----------- novaclient/shell.py | 165 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 164 insertions(+), 57 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index 0f77af57b..c0f81286e 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -21,13 +21,6 @@ except ImportError: import simplejson as json -has_keyring = False -try: - import keyring - has_keyring = True -except ImportError: - pass - # Python 2.5 compat fix if not hasattr(urlparse, 'parse_qsl'): import cgi @@ -93,7 +86,8 @@ def __init__(self, user, password, projectid, auth_url=None, self.auth_token = None self.proxy_token = proxy_token self.proxy_tenant_id = proxy_tenant_id - self.used_keyring = False + self.keyring_saver = None + self.keyring_saved = False if insecure: self.verify_cert = False @@ -119,7 +113,6 @@ def unauthenticate(self): """Forget all of our authentication information.""" self.management_url = None self.auth_token = None - self.used_keyring = False def set_management_url(self, url): self.management_url = url @@ -305,29 +298,6 @@ def _fetch_endpoints_from_auth(self, url): extract_token=False) def authenticate(self): - if has_keyring: - keys = [self.auth_url, self.projectid, self.user, self.region_name, - self.endpoint_type, self.service_type, self.service_name, - self.volume_service_name] - for index, key in enumerate(keys): - if key is None: - keys[index] = '?' - keyring_key = "/".join(keys) - if self.os_cache and not self.used_keyring: - # Lookup the token/mgmt url from the keyring first time - # through. - # If we come through again, it's because the old token - # was rejected. - try: - block = keyring.get_password("novaclient_auth", - keyring_key) - if block: - self.used_keyring = True - self.auth_token, self.management_url = block.split('|') - return - except Exception: - pass - magic_tuple = urlparse.urlsplit(self.auth_url) scheme, netloc, path, query, frag = magic_tuple port = magic_tuple.port @@ -385,14 +355,10 @@ def authenticate(self): self.set_management_url(self.bypass_url) # Store the token/mgmt url in the keyring for later requests. - if has_keyring and self.os_cache: - try: - keyring_value = "%s|%s" % (self.auth_token, - self.management_url) - keyring.set_password("novaclient_auth", - keyring_key, keyring_value) - except Exception: - pass + if self.keyring_saver and self.os_cache and not self.keyring_saved: + self.keyring_saver.save(self.auth_token, self.management_url) + # Don't save it again + self.keyring_saved = True def _v1_auth(self, url): if self.proxy_token: @@ -427,9 +393,13 @@ def _plugin_auth(self, auth_url): def _v2_auth(self, url): """Authenticate against a v2.0 auth service.""" - body = {"auth": { - "passwordCredentials": {"username": self.user, - "password": self.password}}} + if self.auth_token: + body = {"auth": { + "token": {"id": self.auth_token}}} + else: + body = {"auth": { + "passwordCredentials": {"username": self.user, + "password": self.password}}} if self.projectid: body['auth']['tenantName'] = self.projectid diff --git a/novaclient/shell.py b/novaclient/shell.py index 1e47f2ea5..c60e62892 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -19,14 +19,22 @@ """ import argparse +import getpass import glob import imp import itertools +import logging import os import pkg_resources import pkgutil import sys -import logging + +HAS_KEYRING = False +try: + import keyring + HAS_KEYRING = True +except ImportError: + pass import novaclient from novaclient import client @@ -56,6 +64,108 @@ def positive_non_zero_float(text): return value +class SecretsHelper(object): + def __init__(self, args, client): + self.args = args + self.client = client + self.key = None + + def _validate_string(self, text): + if text is None or len(text) == 0: + return False + return True + + def _make_key(self): + if self.key is not None: + return self.key + keys = [ + self.client.auth_url, + self.client.projectid, + self.client.user, + self.client.region_name, + self.client.endpoint_type, + self.client.service_type, + self.client.service_name, + self.client.volume_service_name, + ] + for (index, key) in enumerate(keys): + if key is None: + keys[index] = '?' + else: + keys[index] = str(keys[index]) + self.key = "/".join(keys) + return self.key + + def _prompt_password(self, verify=True): + pw = None + if hasattr(sys.stdin, 'isatty') and sys.stdin.isatty(): + # Check for Ctl-D + try: + while True: + pw1 = getpass.getpass('OS Password: ') + if verify: + pw2 = getpass.getpass('Please verify: ') + else: + pw2 = pw1 + if pw1 == pw2 and self._validate_string(pw1): + pw = pw1 + break + except EOFError: + pass + return pw + + def save(self, auth_token, management_url): + if not HAS_KEYRING or not self.args.os_cache: + return + if (auth_token == self.auth_token and + management_url == self.management_url): + # Nothing changed.... + return + if not all([management_url, auth_token]): + raise ValueError("Unable to save empty management url/auth token") + value = "|".join([str(auth_token), str(management_url)]) + keyring.set_password("novaclient_auth", self._make_key(), value) + + @property + def password(self): + if self._validate_string(self.args.os_password): + return self.args.os_password + if self._validate_string(self.args.apikey): + return self.args.apikey + verify_pass = utils.bool_from_str(utils.getenv("OS_VERIFY_PASSWORD")) + return self._prompt_password(verify_pass) + + @property + def management_url(self): + if not HAS_KEYRING: + return None + management_url = None + try: + block = keyring.get_password('novaclient_auth', self._make_key()) + if block: + _token, management_url = block.split('|', 1) + except ValueError: + pass + return management_url + + @property + def auth_token(self): + # Now is where it gets complicated since we + # want to look into the keyring module, if it + # exists and see if anything was provided in that + # file that we can use. + if not HAS_KEYRING: + return None + token = None + try: + block = keyring.get_password('novaclient_auth', self._make_key()) + if block: + token, _management_url = block.split('|', 1) + except ValueError: + pass + return token + + class NovaClientArgumentParser(argparse.ArgumentParser): def __init__(self, *args, **kwargs): @@ -413,21 +523,24 @@ def main(self, argv): self.do_bash_completion(args) return 0 - (os_username, os_password, os_tenant_name, os_auth_url, + (os_username, os_tenant_name, os_auth_url, os_region_name, os_auth_system, endpoint_type, insecure, service_type, service_name, volume_service_name, - username, apikey, projectid, url, region_name, + username, projectid, url, region_name, bypass_url, os_cache, cacert, timeout) = ( - args.os_username, args.os_password, + args.os_username, args.os_tenant_name, args.os_auth_url, args.os_region_name, args.os_auth_system, args.endpoint_type, args.insecure, args.service_type, args.service_name, args.volume_service_name, - args.username, args.apikey, args.projectid, + args.username, args.projectid, args.url, args.region_name, args.bypass_url, args.os_cache, args.os_cacert, args.timeout) + # Fetched and set later as needed + os_password = None + if not endpoint_type: endpoint_type = DEFAULT_NOVA_ENDPOINT_TYPE @@ -437,7 +550,6 @@ def main(self, argv): #FIXME(usrleon): Here should be restrict for project id same as # for os_username or os_password but for compatibility it is not. - if not utils.isunauthenticated(args.func): if not os_username: if not username: @@ -446,14 +558,6 @@ def main(self, argv): else: os_username = username - if not os_password: - if not apikey: - raise exc.CommandError("You must provide a password " - "via either --os-password or via " - "env[OS_PASSWORD]") - else: - os_password = apikey - if not os_tenant_name: if not projectid: raise exc.CommandError("You must provide a tenant name " @@ -500,6 +604,39 @@ def main(self, argv): os_cache=os_cache, http_log_debug=options.debug, cacert=cacert, timeout=timeout) + # Now check for the password/token of which pieces of the + # identifying keyring key can come from the underlying client + if not utils.isunauthenticated(args.func): + helper = SecretsHelper(args, self.cs.client) + use_pw = True + auth_token, management_url = (helper.auth_token, + helper.management_url) + if auth_token and management_url: + self.cs.client.auth_token = auth_token + self.cs.client.management_url = management_url + # Try to auth with the given info, if it fails + # go into password mode... + try: + self.cs.authenticate() + use_pw = False + except (exc.Unauthorized, exc.AuthorizationFailure): + # Likely it expired or just didn't work... + self.cs.client.auth_token = None + self.cs.client.management_url = None + if use_pw: + # Auth using token must have failed or not happened + # at all, so now switch to password mode and save + # the token when its gotten... using our keyring + # saver + os_password = helper.password + if not os_password: + raise exc.CommandError( + 'Expecting a password provided via either ' + '--os-password, env[OS_PASSWORD], or ' + 'prompted response') + self.cs.client.password = os_password + self.cs.client.keyring_saver = helper + try: if not utils.isunauthenticated(args.func): self.cs.authenticate() From 9d239236b9bf40e45bc4ec73fad4f1cff5e6aa37 Mon Sep 17 00:00:00 2001 From: Arata Notsu Date: Thu, 31 Jan 2013 10:45:31 +0900 Subject: [PATCH 0064/1705] Show the details of the added bare-metal resource On subcommand baremetal-node-create or baremetal-add-interface, the details of the added resource (a node or an interface) are printed. This is useful in a script (e.g. devstack). Some refactoring are also included. Change-Id: I25f019f3dda33de5b49ab2c5442762283be1cf5a --- novaclient/v1_1/contrib/baremetal.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/novaclient/v1_1/contrib/baremetal.py b/novaclient/v1_1/contrib/baremetal.py index 3a753c0b1..9a97d2888 100644 --- a/novaclient/v1_1/contrib/baremetal.py +++ b/novaclient/v1_1/contrib/baremetal.py @@ -178,12 +178,13 @@ def remove_interface(self, node_id, address): help='ShellInABox port?') def do_baremetal_node_create(cs, args): """Create a baremetal node""" - return cs.baremetal.create(args.service_host, args.cpus, + node = cs.baremetal.create(args.service_host, args.cpus, args.memory_mb, args.local_gb, args.prov_mac_address, pm_address=args.pm_address, pm_user=args.pm_user, pm_password=args.pm_password, prov_vlan_id=args.prov_vlan_id, terminal_port=args.terminal_port) + _print_baremetal_resource(node) @utils.arg('node', @@ -212,7 +213,7 @@ def _translate_baremetal_node_keys(collection): setattr(item, to_key, item._info[from_key]) -def _print_baremetal_nodes_list(cs, nodes): +def _print_baremetal_nodes_list(nodes): """Print the list of baremetal nodes""" _translate_baremetal_node_keys(nodes) utils.print_list(nodes, [ @@ -233,7 +234,7 @@ def _print_baremetal_nodes_list(cs, nodes): def do_baremetal_node_list(cs, _args): """Print a list of available baremetal nodes.""" nodes = cs.baremetal.list() - _print_baremetal_nodes_list(cs, nodes) + _print_baremetal_nodes_list(nodes) def _find_baremetal_node(cs, node): @@ -241,9 +242,9 @@ def _find_baremetal_node(cs, node): return utils.find_resource(cs.baremetal, node) -def _print_baremetal_node(cs, node): - """Print the details of a baremetal node""" - info = node._info.copy() +def _print_baremetal_resource(resource): + """Print the details of a baremetal resource""" + info = resource._info.copy() utils.print_dict(info) @@ -253,7 +254,7 @@ def _print_baremetal_node(cs, node): def do_baremetal_node_show(cs, args): """Show information about a node""" node = _find_baremetal_node(cs, args.node) - _print_baremetal_node(cs, node) + _print_baremetal_resource(node) @utils.arg('node', @@ -272,8 +273,9 @@ def do_baremetal_node_show(cs, args): help="OpenFlow port number of interface") def do_baremetal_add_interface(cs, args): """Add a network interface to a baremetal node""" - return cs.baremetal.add_interface(args.node, args.address, + bmif = cs.baremetal.add_interface(args.node, args.address, args.datapath_id, args.port_no) + _print_baremetal_resource(bmif) @utils.arg('node', metavar='', help="ID of node") From 1e4a778bf85c20d81f475f9cb04fe8c94a1753d5 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Thu, 31 Jan 2013 11:51:29 -0800 Subject: [PATCH 0065/1705] Store tenant_id from keystone and use for quotas Some calls in nova require a tenant_id when it could be interpreted from the current authentication data, so save the tenant id and use it in the quotas command if tenant_id is not specified. Change-Id: I89647cfe9da73bc474ef80a61a5678db42a5571c --- novaclient/client.py | 5 ++++- novaclient/service_catalog.py | 3 +++ novaclient/shell.py | 33 +++++++++++++++++++++++++-------- novaclient/v1_1/shell.py | 4 ++-- tests/test_auth_plugins.py | 3 +++ tests/v1_1/fakes.py | 35 +++++++++++++++++++++++++++++++++++ tests/v1_1/test_auth.py | 9 +++++++++ tests/v1_1/test_shell.py | 9 ++++++--- 8 files changed, 87 insertions(+), 14 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index c0f81286e..404af61e5 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -251,6 +251,7 @@ def _extract_service_catalog(self, url, resp, body, extract_token=True): service_catalog.ServiceCatalog(body) if extract_token: self.auth_token = self.service_catalog.get_token() + self.tenant_id = self.service_catalog.get_tenant_id() management_url = self.service_catalog.url_for( attr='region', @@ -356,7 +357,9 @@ def authenticate(self): # Store the token/mgmt url in the keyring for later requests. if self.keyring_saver and self.os_cache and not self.keyring_saved: - self.keyring_saver.save(self.auth_token, self.management_url) + self.keyring_saver.save(self.auth_token, + self.management_url, + self.tenant_id) # Don't save it again self.keyring_saved = True diff --git a/novaclient/service_catalog.py b/novaclient/service_catalog.py index bb856239a..f4a5f1adb 100644 --- a/novaclient/service_catalog.py +++ b/novaclient/service_catalog.py @@ -28,6 +28,9 @@ def __init__(self, resource_dict): def get_token(self): return self.catalog['access']['token']['id'] + def get_tenant_id(self): + return self.catalog['access']['token']['tenant']['id'] + def url_for(self, attr=None, filter_value=None, service_type=None, endpoint_type='publicURL', service_name=None, volume_service_name=None): diff --git a/novaclient/shell.py b/novaclient/shell.py index c60e62892..ed2480aa1 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -114,16 +114,18 @@ def _prompt_password(self, verify=True): pass return pw - def save(self, auth_token, management_url): + def save(self, auth_token, management_url, tenant_id): if not HAS_KEYRING or not self.args.os_cache: return if (auth_token == self.auth_token and management_url == self.management_url): # Nothing changed.... return - if not all([management_url, auth_token]): + if not all([management_url, auth_token, tenant_id]): raise ValueError("Unable to save empty management url/auth token") - value = "|".join([str(auth_token), str(management_url)]) + value = "|".join([str(auth_token), + str(management_url), + str(tenant_id)]) keyring.set_password("novaclient_auth", self._make_key(), value) @property @@ -143,7 +145,7 @@ def management_url(self): try: block = keyring.get_password('novaclient_auth', self._make_key()) if block: - _token, management_url = block.split('|', 1) + _token, management_url, _tenant_id = block.split('|', 2) except ValueError: pass return management_url @@ -160,11 +162,24 @@ def auth_token(self): try: block = keyring.get_password('novaclient_auth', self._make_key()) if block: - token, _management_url = block.split('|', 1) + token, _management_url, _tenant_id = block.split('|', 2) except ValueError: pass return token + @property + def tenant_id(self): + if not HAS_KEYRING: + return None + tenant_id = None + try: + block = keyring.get_password('novaclient_auth', self._make_key()) + if block: + _token, _management_url, tenant_id = block.split('|', 2) + except ValueError: + pass + return tenant_id + class NovaClientArgumentParser(argparse.ArgumentParser): @@ -609,9 +624,11 @@ def main(self, argv): if not utils.isunauthenticated(args.func): helper = SecretsHelper(args, self.cs.client) use_pw = True - auth_token, management_url = (helper.auth_token, - helper.management_url) - if auth_token and management_url: + tenant_id, auth_token, management_url = (helper.tenant_id, + helper.auth_token, + helper.management_url) + if tenant_id and auth_token and management_url: + self.cs.client.tenant_id = tenant_id self.cs.client.auth_token = auth_token self.cs.client.management_url = management_url # Try to auth with the given info, if it fails diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index eab195bb3..2a42bac09 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2597,7 +2597,7 @@ def do_quota_show(cs, args): """List the quotas for a tenant.""" if not args.tenant: - raise exceptions.CommandError("you need to specify a Tenant ID ") + _quota_show(cs.quotas.get(cs.client.tenant_id)) else: _quota_show(cs.quotas.get(args.tenant)) @@ -2610,7 +2610,7 @@ def do_quota_defaults(cs, args): """List the default quotas for a tenant.""" if not args.tenant: - _quota_show(cs.quotas.defaults(cs.project_id)) + _quota_show(cs.quotas.defaults(cs.client.tenant_id)) else: _quota_show(cs.quotas.defaults(args.tenant)) diff --git a/tests/test_auth_plugins.py b/tests/test_auth_plugins.py index c4950af87..e73c3545e 100644 --- a/tests/test_auth_plugins.py +++ b/tests/test_auth_plugins.py @@ -35,6 +35,9 @@ def mock_http_request(resp=None): "token": { "expires": "12345", "id": "FAKE_ID", + "tenant": { + "id": "FAKE_TENANT_ID", + } }, "serviceCatalog": [ { diff --git a/tests/v1_1/fakes.py b/tests/v1_1/fakes.py index 48ab33d1a..2125762f6 100644 --- a/tests/v1_1/fakes.py +++ b/tests/v1_1/fakes.py @@ -37,6 +37,7 @@ def __init__(self, **kwargs): self.username = 'username' self.password = 'password' self.auth_url = 'auth_url' + self.tenant_id = 'tenant_id' self.callstack = [] def _cs_request(self, url, method, **kwargs): @@ -863,6 +864,23 @@ def get_os_quota_sets_test(self, **kw): 'security_groups': 1, 'security_group_rules': 1}}) + def get_os_quota_sets_tenant_id(self, **kw): + return (200, {}, {'quota_set': { + 'tenant_id': 'test', + 'metadata_items': [], + 'injected_file_content_bytes': 1, + 'injected_file_path_bytes': 1, + 'volumes': 1, + 'gigabytes': 1, + 'ram': 1, + 'floating_ips': 1, + 'instances': 1, + 'injected_files': 1, + 'cores': 1, + 'keypairs': 1, + 'security_groups': 1, + 'security_group_rules': 1}}) + def get_os_quota_sets_97f4c221bff44578b0300df4ef119353(self, **kw): return (200, {}, {'quota_set': { 'tenant_id': '97f4c221bff44578b0300df4ef119353', @@ -931,6 +949,23 @@ def get_os_quota_sets_test_defaults(self): 'security_groups': 1, 'security_group_rules': 1}}) + def get_os_quota_sets_tenant_id_defaults(self): + return (200, {}, {'quota_set': { + 'tenant_id': 'test', + 'metadata_items': [], + 'injected_file_content_bytes': 1, + 'injected_file_path_bytes': 1, + 'volumes': 1, + 'gigabytes': 1, + 'ram': 1, + 'floating_ips': 1, + 'instances': 1, + 'injected_files': 1, + 'cores': 1, + 'keypairs': 1, + 'security_groups': 1, + 'security_group_rules': 1}}) + def put_os_quota_sets_97f4c221bff44578b0300df4ef119353(self, body, **kw): assert body.keys() == ['quota_set'] fakes.assert_has_keys(body['quota_set'], diff --git a/tests/v1_1/test_auth.py b/tests/v1_1/test_auth.py index d10b49cb2..b34f77694 100644 --- a/tests/v1_1/test_auth.py +++ b/tests/v1_1/test_auth.py @@ -18,6 +18,9 @@ def test_authenticate_success(self): "token": { "expires": "12345", "id": "FAKE_ID", + "tenant": { + "id": "FAKE_TENANT_ID", + } }, "serviceCatalog": [ { @@ -101,6 +104,9 @@ def test_auth_redirect(self): "token": { "expires": "12345", "id": "FAKE_ID", + "tenant": { + "id": "FAKE_TENANT_ID", + } }, "serviceCatalog": [ { @@ -186,6 +192,9 @@ def test_ambiguous_endpoints(self): "token": { "expires": "12345", "id": "FAKE_ID", + "tenant": { + "id": "FAKE_TENANT_ID", + } }, "serviceCatalog": [ { diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index 0d3395cae..4384a242b 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -707,14 +707,17 @@ def test_quota_show(self): self.assert_called('GET', '/os-quota-sets/test') def test_quota_show_no_tenant(self): - self.assertRaises(exceptions.CommandError, - self.run_command, - 'quota-show') + self.run_command('quota-show') + self.assert_called('GET', '/os-quota-sets/tenant_id') def test_quota_defaults(self): self.run_command('quota-defaults --tenant test') self.assert_called('GET', '/os-quota-sets/test/defaults') + def test_quota_defaults_no_nenant(self): + self.run_command('quota-defaults') + self.assert_called('GET', '/os-quota-sets/tenant_id/defaults') + def test_quota_update(self): self.run_command( 'quota-update 97f4c221bff44578b0300df4ef119353 \ From c8a6c6b10e9b9ebfd9f72dba769ced3fae7b985f Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Thu, 31 Jan 2013 12:30:58 -0800 Subject: [PATCH 0066/1705] Add usage command to show usage for single tenant The usage-list command is admin-only so add a usage command so that users can see their usage like they do in horizon. Only admins can see usage for other tenants, so default the value of tenant to the tenant_id of the current credentials. Change-Id: Icd6fec36f1f4145b92b227031e5df23db11737f0 --- novaclient/v1_1/shell.py | 49 ++++++++++++++++++++++++++++++++++++++++ tests/v1_1/fakes.py | 36 +++++++++++++++++++++++++++++ tests/v1_1/test_shell.py | 14 ++++++++++++ 3 files changed, 99 insertions(+) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 2a42bac09..a31ddbbc5 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2072,6 +2072,55 @@ def simplify_usage(u): utils.print_list(usage_list, rows) +@utils.arg('--start', metavar='', + help='Usage range start date ex 2012-01-20 (default: 4 weeks ago)', + default=None) +@utils.arg('--end', metavar='', + help='Usage range end date, ex 2012-01-20 (default: tomorrow) ', + default=None) +@utils.arg('--tenant', metavar='', + default=None, + help='UUID or name of tenant to get usage for.') +def do_usage(cs, args): + """Show usage data for a single tenant""" + dateformat = "%Y-%m-%d" + rows = ["Instances", "RAM MB-Hours", "CPU Hours", "Disk GB-Hours"] + + now = timeutils.utcnow() + + if args.start: + start = datetime.datetime.strptime(args.start, dateformat) + else: + start = now - datetime.timedelta(weeks=4) + + if args.end: + end = datetime.datetime.strptime(args.end, dateformat) + else: + end = now + datetime.timedelta(days=1) + + def simplify_usage(u): + simplerows = map(lambda x: x.lower().replace(" ", "_"), rows) + + setattr(u, simplerows[0], "%d" % len(u.server_usages)) + setattr(u, simplerows[1], "%.2f" % u.total_memory_mb_usage) + setattr(u, simplerows[2], "%.2f" % u.total_vcpus_usage) + setattr(u, simplerows[3], "%.2f" % u.total_local_gb_usage) + + if args.tenant: + usage = cs.usage.get(args.tenant, start, end) + else: + usage = cs.usage.get(cs.client.tenant_id, start, end) + + print "Usage from %s to %s:" % (start.strftime(dateformat), + end.strftime(dateformat)) + + if getattr(usage, 'total_vcpus_usage', None): + simplify_usage(usage) + utils.print_list([usage], rows) + else: + print 'None' + + @utils.arg('pk_filename', metavar='', nargs='?', diff --git a/tests/v1_1/fakes.py b/tests/v1_1/fakes.py index 2125762f6..77d7fed44 100644 --- a/tests/v1_1/fakes.py +++ b/tests/v1_1/fakes.py @@ -1135,6 +1135,42 @@ def get_os_simple_tenant_usage_tenantfoo(self, **kw): u'start': u'2011-12-25 19:48:41.750687', u'total_local_gb_usage': 0.0}}) + def get_os_simple_tenant_usage_test(self, **kw): + return (200, {}, {u'tenant_usage': { + u'total_memory_mb_usage': 25451.762807466665, + u'total_vcpus_usage': 49.71047423333333, + u'total_hours': 49.71047423333333, + u'tenant_id': u'7b0a1d73f8fb41718f3343c207597869', + u'stop': u'2012-01-22 19:48:41.750722', + u'server_usages': [{ + u'hours': 49.71047423333333, + u'uptime': 27035, u'local_gb': 0, u'ended_at': None, + u'name': u'f15image1', + u'tenant_id': u'7b0a1d73f8fb41718f3343c207597869', + u'vcpus': 1, u'memory_mb': 512, u'state': u'active', + u'flavor': u'm1.tiny', + u'started_at': u'2012-01-20 18:06:06.479998'}], + u'start': u'2011-12-25 19:48:41.750687', + u'total_local_gb_usage': 0.0}}) + + def get_os_simple_tenant_usage_tenant_id(self, **kw): + return (200, {}, {u'tenant_usage': { + u'total_memory_mb_usage': 25451.762807466665, + u'total_vcpus_usage': 49.71047423333333, + u'total_hours': 49.71047423333333, + u'tenant_id': u'7b0a1d73f8fb41718f3343c207597869', + u'stop': u'2012-01-22 19:48:41.750722', + u'server_usages': [{ + u'hours': 49.71047423333333, + u'uptime': 27035, u'local_gb': 0, u'ended_at': None, + u'name': u'f15image1', + u'tenant_id': u'7b0a1d73f8fb41718f3343c207597869', + u'vcpus': 1, u'memory_mb': 512, u'state': u'active', + u'flavor': u'm1.tiny', + u'started_at': u'2012-01-20 18:06:06.479998'}], + u'start': u'2011-12-25 19:48:41.750687', + u'total_local_gb_usage': 0.0}}) + # # Certificates # diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index 4384a242b..d98bcf5c4 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -507,6 +507,20 @@ def test_usage_list_no_args(self): 'end=2005-02-02T00:00:00&' + 'detailed=1') + def test_usage(self): + self.run_command('usage --start 2000-01-20 --end 2005-02-01 --tenant test') + self.assert_called('GET', + '/os-simple-tenant-usage/test?' + + 'start=2000-01-20T00:00:00&' + + 'end=2005-02-01T00:00:00') + + def test_usage_no_tenant(self): + self.run_command('usage --start 2000-01-20 --end 2005-02-01') + self.assert_called('GET', + '/os-simple-tenant-usage/tenant_id?' + + 'start=2000-01-20T00:00:00&' + + 'end=2005-02-01T00:00:00') + def test_flavor_delete(self): self.run_command("flavor-delete 2") self.assert_called('DELETE', '/flavors/2') From cda7f7a5050e88bc99bfdd823cfff625dc5933ea Mon Sep 17 00:00:00 2001 From: Mitsuhiko Yamazaki Date: Thu, 31 Jan 2013 17:44:48 +0900 Subject: [PATCH 0067/1705] Fix default format of 'nova coverage-report' 'nova coverage-report' returns an error because nova server returns error if the format is HTML and combine is False. And 'nova coverage-start' and 'nova coverage-report' do it now. After applying this patch, we can avoid this problem and text format report is created when we use 'nova coverage-report' Fixes bug 1110972 Change-Id: Ib3d05453de638157152a50ea814e44e6cbc19348 --- novaclient/v1_1/coverage_ext.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/v1_1/coverage_ext.py b/novaclient/v1_1/coverage_ext.py index f0b099e1d..6364da419 100644 --- a/novaclient/v1_1/coverage_ext.py +++ b/novaclient/v1_1/coverage_ext.py @@ -41,7 +41,7 @@ def stop(self): url = '/os-coverage/action' return self.api.client.post(url, body=body) - def report(self, filename, xml=False, html=True): + def report(self, filename, xml=False, html=False): body = { 'report': { 'file': filename, From 7f8f3ce0f8be26cda74230c260d863127b147bdb Mon Sep 17 00:00:00 2001 From: Masayuki Igawa Date: Fri, 1 Feb 2013 13:50:56 +0900 Subject: [PATCH 0068/1705] Add help about the id 'auto' for flavor-create The id parameter of 'nova flavor-create' can be specified 'auto' now. But the help document is not mentioned about that. This patch fixes it. Fixes bug 1112154 Change-Id: I6df71746a0036c6b6d05467e28343fad2972c128 --- novaclient/v1_1/shell.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 2a42bac09..55d1478bf 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -428,7 +428,8 @@ def do_flavor_show(cs, args): help="Name of the new flavor") @utils.arg('id', metavar='', - help="Unique ID (integer or UUID) for the new flavor") + help="Unique ID (integer or UUID) for the new flavor." + " If specifying 'auto', a UUID will be generated as id") @utils.arg('ram', metavar='', help="Memory size in MB") From 7a95186047e4945a5543bcb335095c40ab1c1b76 Mon Sep 17 00:00:00 2001 From: Svetlana Shturm Date: Fri, 1 Feb 2013 13:30:26 +0000 Subject: [PATCH 0069/1705] Live migration with an auto selection of dest. Change-Id: I2e0880b0e5b0565e138ad7f72d88ccd7e431e79f Blueprint: live-migration-scheduling DocImpact: --- novaclient/v1_1/servers.py | 2 +- novaclient/v1_1/shell.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index 9a9bb1ec1..d3ec07292 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -277,7 +277,7 @@ def networks(self): except Exception: return {} - def live_migrate(self, host, + def live_migrate(self, host=None, block_migration=False, disk_over_commit=False): """ diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index eab195bb3..e9f240b42 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2251,7 +2251,8 @@ def _print_aggregate_details(aggregate): @utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg('host', metavar='', help='destination host name.') +@utils.arg('host', metavar='', default=None, nargs='?', + help='destination host name.') @utils.arg('--block-migrate', action='store_true', dest='block_migrate', From 3ca976d4a076c13c7d68de859a8821cc281c9271 Mon Sep 17 00:00:00 2001 From: Anita Kuno Date: Fri, 1 Feb 2013 14:44:51 +0000 Subject: [PATCH 0070/1705] Fixed 7 pep8 errors. Change-Id: I2d509ed383fd8d6cef91dd0c58a51eafc6995bda --- tests/v1_1/test_hosts.py | 9 ++++++--- tests/v1_1/test_shell.py | 12 ++++++++---- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/tests/v1_1/test_hosts.py b/tests/v1_1/test_hosts.py index 28964279d..4d181bb69 100644 --- a/tests/v1_1/test_hosts.py +++ b/tests/v1_1/test_hosts.py @@ -50,14 +50,17 @@ def test_update_both(self): def test_host_startup(self): host = cs.hosts.get('sample_host')[0] result = host.startup() - cs.assert_called('POST', '/os-hosts/sample_host/action', {'startup': None}) + cs.assert_called( + 'POST', '/os-hosts/sample_host/action', {'startup': None}) def test_host_reboot(self): host = cs.hosts.get('sample_host')[0] result = host.reboot() - cs.assert_called('POST', '/os-hosts/sample_host/action', {'reboot': None}) + cs.assert_called( + 'POST', '/os-hosts/sample_host/action', {'reboot': None}) def test_host_shutdown(self): host = cs.hosts.get('sample_host')[0] result = host.shutdown() - cs.assert_called('POST', '/os-hosts/sample_host/action', {'shutdown': None}) + cs.assert_called( + 'POST', '/os-hosts/sample_host/action', {'shutdown': None}) diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index d98bcf5c4..a407370e7 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -508,7 +508,8 @@ def test_usage_list_no_args(self): 'detailed=1') def test_usage(self): - self.run_command('usage --start 2000-01-20 --end 2005-02-01 --tenant test') + self.run_command('usage --start 2000-01-20 --end 2005-02-01 ' + '--tenant test') self.assert_called('GET', '/os-simple-tenant-usage/test?' + 'start=2000-01-20T00:00:00&' + @@ -670,15 +671,18 @@ def test_host_update_multiple_settings(self): def test_host_startup(self): self.run_command('host-action sample-host --action startup') - self.assert_called('POST', '/os-hosts/sample-host/action', {'startup': None}) + self.assert_called( + 'POST', '/os-hosts/sample-host/action', {'startup': None}) def test_host_shutdown(self): self.run_command('host-action sample-host --action shutdown') - self.assert_called('POST', '/os-hosts/sample-host/action', {'shutdown': None}) + self.assert_called( + 'POST', '/os-hosts/sample-host/action', {'shutdown': None}) def test_host_reboot(self): self.run_command('host-action sample-host --action reboot') - self.assert_called('POST', '/os-hosts/sample-host/action', {'reboot': None}) + self.assert_called( + 'POST', '/os-hosts/sample-host/action', {'reboot': None}) def test_coverage_start(self): self.run_command('coverage-start') From 96630b8248bc51a0a9dd111dd3cac170a237813e Mon Sep 17 00:00:00 2001 From: Joe Gordon Date: Fri, 1 Feb 2013 12:13:19 -0800 Subject: [PATCH 0071/1705] Upgrade to pep8 1.3.3 Also expand scope of what is covered by pep8 test Change-Id: Ifc8924914b5a0d625bc8df6442ee85eb21459cde --- novaclient/shell.py | 2 +- novaclient/v1_1/shell.py | 10 +++++----- run_tests.sh | 20 +++----------------- tools/test-requires | 2 +- tox.ini | 6 ++++-- 5 files changed, 14 insertions(+), 26 deletions(-) diff --git a/novaclient/shell.py b/novaclient/shell.py index ed2480aa1..b00b28604 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -209,7 +209,7 @@ def get_base_parser(self): parser = NovaClientArgumentParser( prog='nova', description=__doc__.strip(), - epilog='See "nova help COMMAND" '\ + epilog='See "nova help COMMAND" ' 'for help on a specific command.', add_help=False, formatter_class=OpenStackHelpFormatter, diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index a31ddbbc5..46ec6ea51 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -110,7 +110,7 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): try: userdata = open(args.user_data) except IOError, e: - raise exceptions.CommandError("Can't open '%s': %s" % \ + raise exceptions.CommandError("Can't open '%s': %s" % (args.user_data, e)) else: userdata = None @@ -200,14 +200,14 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): metavar="", action='append', default=[], - help="Record arbitrary key/value metadata to /meta.js "\ + help="Record arbitrary key/value metadata to /meta.js " "on the new server. Can be specified multiple times.") @utils.arg('--file', metavar="", action='append', dest='files', default=[], - help="Store arbitrary files from locally to "\ + help="Store arbitrary files from locally to " "on the new server. You may store up to 5 files.") @utils.arg('--key-name', metavar='', @@ -1961,7 +1961,7 @@ def do_secgroup_delete_group_rule(cs, args): if (rule.get('ip_protocol') == params.get('ip_protocol') and rule.get('from_port') == params.get('from_port') and rule.get('to_port') == params.get('to_port') and - rule.get('group', {}).get('name') == \ + rule.get('group', {}).get('name') == params.get('group_name')): return cs.security_group_rules.delete(rule['id']) @@ -1985,7 +1985,7 @@ def do_keypair_add(cs, args): with open(os.path.expanduser(pub_key)) as f: pub_key = f.read() except IOError, e: - raise exceptions.CommandError("Can't open or read '%s': %s" % \ + raise exceptions.CommandError("Can't open or read '%s': %s" % (pub_key, e)) keypair = cs.keypairs.create(name, pub_key) diff --git a/run_tests.sh b/run_tests.sh index e33a95d98..e5474c413 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -103,25 +103,11 @@ function copy_subunit_log { function run_pep8 { echo "Running pep8 ..." - srcfiles="novaclient tests" + srcfiles="--exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg ." # Just run PEP8 in current environment # - # NOTE(sirp): W602 (deprecated 3-arg raise) is being ignored for the - # following reasons: - # - # 1. It's needed to preserve traceback information when re-raising - # exceptions; this is needed b/c Eventlet will clear exceptions when - # switching contexts. - # - # 2. There doesn't appear to be an alternative, "pep8-tool" compatible way of doing this - # in Python 2 (in Python 3 `with_traceback` could be used). - # - # 3. Can find no corroborating evidence that this is deprecated in Python 2 - # other than what the PEP8 tool claims. It is deprecated in Python 3, so, - # perhaps the mistake was thinking that the deprecation applied to Python 2 - # as well. - pep8_opts="--ignore=E202,W602 --repeat" - ${wrapper} pep8 ${pep8_opts} ${srcfiles} + ignore="--ignore=E12,E711,E721,E712" + ${wrapper} pep8 ${ignore} --show-source ${srcfiles} } TESTRTESTS="testr run --parallel $testropts" diff --git a/tools/test-requires b/tools/test-requires index 0dacea2ac..cae195a34 100644 --- a/tools/test-requires +++ b/tools/test-requires @@ -4,7 +4,7 @@ coverage discover fixtures>=0.3.12 mock -pep8==1.1 +pep8==1.3.3 sphinx>=1.1.2 testrepository>=0.0.13 testtools>=0.9.26 diff --git a/tox.ini b/tox.ini index 88fb5cc40..2c4ec427d 100644 --- a/tox.ini +++ b/tox.ini @@ -12,8 +12,10 @@ deps = -r{toxinidir}/tools/pip-requires commands = python setup.py testr --testr-args='{posargs}' [testenv:pep8] -deps = pep8==1.1 -commands = pep8 --repeat --show-source novaclient setup.py +deps = pep8==1.3.3 +commands = + pep8 --ignore=E12,E711,E721,E712 --show-source \ + --exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg . [testenv:venv] commands = {posargs} From 4516efa8db29f9b551f24efe1045d015c5c16cdb Mon Sep 17 00:00:00 2001 From: Zhi Yan Liu Date: Tue, 5 Feb 2013 15:14:18 +0800 Subject: [PATCH 0072/1705] Show the summary or details of all availability zones of a region Adding "availability-zone-list" sub-command allow user list all availability zones and its status in a region. * summary list for normal user: list availability zones summary whitin a flat view. * details list for administrator user: list availability zones details within a tree view, include zones, hosts and components. Implement one workitem for bp:show-availability-zone Change-Id: Id87fe470c7e0f6fbfb9465551f63717724b5fc18 --- novaclient/v1_1/availability_zones.py | 50 ++++++++++++++ novaclient/v1_1/client.py | 19 ++++++ novaclient/v1_1/shell.py | 94 +++++++++++++++++++++++---- tests/v1_1/fakes.py | 39 +++++++++++ tests/v1_1/test_availability_zone.py | 87 +++++++++++++++++++++++++ tests/v1_1/test_shell.py | 5 ++ 6 files changed, 280 insertions(+), 14 deletions(-) create mode 100644 novaclient/v1_1/availability_zones.py create mode 100644 tests/v1_1/test_availability_zone.py diff --git a/novaclient/v1_1/availability_zones.py b/novaclient/v1_1/availability_zones.py new file mode 100644 index 000000000..7b39b300a --- /dev/null +++ b/novaclient/v1_1/availability_zones.py @@ -0,0 +1,50 @@ +# Copyright 2011 OpenStack LLC. +# Copyright 2013 IBM Corp. +# All Rights Reserved. +# +# 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. + +""" +Availability Zone interface (1.1 extension). +""" + +from novaclient import base + + +class AvailabilityZone(base.Resource): + """ + An availability zone object. + """ + NAME_ATTR = 'display_name' + + def __repr__(self): + return "" % self.zoneName + + +class AvailabilityZoneManager(base.ManagerWithFind): + """ + Manage :class:`AvailabilityZone` resources. + """ + resource_class = AvailabilityZone + + def list(self, detailed=True): + """ + Get a list of all availability zones. + + :rtype: list of :class:`AvailabilityZone` + """ + if detailed is True: + return self._list("/os-availability-zone/detail", + "availabilityZoneInfo") + else: + return self._list("/os-availability-zone", "availabilityZoneInfo") diff --git a/novaclient/v1_1/client.py b/novaclient/v1_1/client.py index 77a4d08f1..383ae19fc 100644 --- a/novaclient/v1_1/client.py +++ b/novaclient/v1_1/client.py @@ -1,8 +1,25 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright 2012 OpenStack LLC. +# Copyright 2013 IBM Corp. +# +# 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 novaclient import client from novaclient.v1_1 import agents from novaclient.v1_1 import certs from novaclient.v1_1 import cloudpipe from novaclient.v1_1 import aggregates +from novaclient.v1_1 import availability_zones from novaclient.v1_1 import coverage_ext from novaclient.v1_1 import flavors from novaclient.v1_1 import flavor_access @@ -98,6 +115,8 @@ def __init__(self, username, api_key, project_id, auth_url=None, self.floating_ips_bulk = floating_ips_bulk.FloatingIPBulkManager(self) self.os_cache = os_cache or not no_cache self.coverage = coverage_ext.CoverageManager(self) + self.availability_zones = \ + availability_zones.AvailabilityZoneManager(self) # Add in any extensions... if extensions: diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 497b18712..0c5905aee 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1,6 +1,7 @@ # Copyright 2010 Jacob Kaplan-Moss # Copyright 2011 OpenStack LLC. +# Copyright 2013 IBM Corp. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -16,6 +17,7 @@ # under the License. import argparse +import copy import datetime import getpass import locale @@ -26,6 +28,7 @@ from novaclient import exceptions from novaclient.openstack.common import timeutils from novaclient import utils +from novaclient.v1_1 import availability_zones from novaclient.v1_1 import servers @@ -367,8 +370,7 @@ def print_progress(progress): time.sleep(poll_period) -def _translate_flavor_keys(collection): - convert = [('ram', 'memory_mb')] +def _translate_keys(collection, convert): for item in collection: keys = item.__dict__.keys() for from_key, to_key in convert: @@ -376,6 +378,10 @@ def _translate_flavor_keys(collection): setattr(item, to_key, item._info[from_key]) +def _translate_flavor_keys(collection): + _translate_keys(collection, [('ram', 'memory_mb')]) + + def _print_flavor_extra_specs(flavor): try: return flavor.get_keys() @@ -1301,21 +1307,20 @@ def _print_volume_snapshot(snapshot): def _translate_volume_keys(collection): - convert = [('displayName', 'display_name'), ('volumeType', 'volume_type')] - for item in collection: - keys = item.__dict__.keys() - for from_key, to_key in convert: - if from_key in keys and to_key not in keys: - setattr(item, to_key, item._info[from_key]) + _translate_keys(collection, + [('displayName', 'display_name'), + ('volumeType', 'volume_type')]) def _translate_volume_snapshot_keys(collection): - convert = [('displayName', 'display_name'), ('volumeId', 'volume_id')] - for item in collection: - keys = item.__dict__.keys() - for from_key, to_key in convert: - if from_key in keys and to_key not in keys: - setattr(item, to_key, item._info[from_key]) + _translate_keys(collection, + [('displayName', 'display_name'), + ('volumeId', 'volume_id')]) + + +def _translate_availability_zone_keys(collection): + _translate_keys(collection, + [('zoneName', 'name'), ('zoneState', 'status')]) @utils.arg('--all-tenants', @@ -2857,3 +2862,64 @@ def do_evacuate(cs, args): res = server.evacuate(args.host, args.on_shared_storage, args.password)[0] if type(res) is dict: utils.print_dict(res) + + +def _treeizeAvailabilityZone(zone): + """Build a tree view for availability zones""" + AvailabilityZone = availability_zones.AvailabilityZone + + az = AvailabilityZone(copy.deepcopy(zone.manager), + copy.deepcopy(zone._info), zone._loaded) + result = [] + + # Zone tree view item + az.zoneName = zone.zoneName + az.zoneState = ('available' + if zone.zoneState['available'] else 'not available') + az._info['zoneName'] = az.zoneName + az._info['zoneState'] = az.zoneState + result.append(az) + + if zone.hosts is not None: + for (host, services) in zone.hosts.items(): + # Host tree view item + az = AvailabilityZone(copy.deepcopy(zone.manager), + copy.deepcopy(zone._info), zone._loaded) + az.zoneName = '|- %s' % host + az.zoneState = '' + az._info['zoneName'] = az.zoneName + az._info['zoneState'] = az.zoneState + result.append(az) + + for (svc, state) in services.items(): + # Service tree view item + az = AvailabilityZone(copy.deepcopy(zone.manager), + copy.deepcopy(zone._info), zone._loaded) + az.zoneName = '| |- %s' % svc + az.zoneState = '%s %s %s' % ( + 'enabled' if state['active'] else 'disabled', + ':-)' if state['available'] else 'XXX', + timeutils.strtime(state['updated_at'])) + az._info['zoneName'] = az.zoneName + az._info['zoneState'] = az.zoneState + result.append(az) + return result + + +@utils.service_type('compute') +def do_availability_zone_list(cs, _args): + """List all the availability zones.""" + try: + availability_zones = cs.availability_zones.list() + except exceptions.Forbidden, e: # policy doesn't allow probably + try: + availability_zones = cs.availability_zones.list(detailed=False) + except: + raise e + + result = [] + for zone in availability_zones: + result += _treeizeAvailabilityZone(zone) + _translate_availability_zone_keys(result) + utils.print_list(result, ['Name', 'Status'], + sortby_index=None) diff --git a/tests/v1_1/fakes.py b/tests/v1_1/fakes.py index 77d7fed44..212bac446 100644 --- a/tests/v1_1/fakes.py +++ b/tests/v1_1/fakes.py @@ -1,5 +1,6 @@ # Copyright (c) 2011 X.commerce, a business unit of eBay Inc. # Copyright 2011 OpenStack, LLC +# Copyright 2013 IBM Corp. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -1501,3 +1502,41 @@ def post_os_coverage_action(self, body, **kw): return (200, {}, { 'path': '/tmp/tmpdir/' + body['report']['file'] }) + + def get_os_availability_zone(self, **kw): + return (200, {}, {"availabilityZoneInfo": [ + {"zoneName": "zone-1", + "zoneState": {"available": True}, + "hosts": None}, + {"zoneName": "zone-2", + "zoneState": {"available": False}, + "hosts": None}]}) + + def get_os_availability_zone_detail(self, **kw): + return (200, {}, {"availabilityZoneInfo": [ + {"zoneName": "zone-1", + "zoneState": {"available": True}, + "hosts": { + "fake_host-1": { + "nova-compute": {"active": True, + "available": True, + "updated_at": + datetime(2012, 12, 26, 14, 45, 25, 0)}}}}, + {"zoneName": "internal", + "zoneState": {"available": True}, + "hosts": { + "fake_host-1": { + "nova-sched": { + "active": True, + "available": True, + "updated_at": + datetime(2012, 12, 26, 14, 45, 25, 0)}}, + "fake_host-2": { + "nova-network": { + "active": True, + "available": False, + "updated_at": + datetime(2012, 12, 26, 14, 45, 24, 0)}}}}, + {"zoneName": "zone-2", + "zoneState": {"available": False}, + "hosts": None}]}) diff --git a/tests/v1_1/test_availability_zone.py b/tests/v1_1/test_availability_zone.py new file mode 100644 index 000000000..b06f21def --- /dev/null +++ b/tests/v1_1/test_availability_zone.py @@ -0,0 +1,87 @@ +# Copyright 2011 OpenStack LLC. +# Copyright 2013 IBM Corp. +# All Rights Reserved. +# +# 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 novaclient.v1_1 import availability_zones +from novaclient.v1_1 import shell +from tests.v1_1 import fakes +from tests import utils + + +cs = fakes.FakeClient() + + +class AvailabilityZoneTest(utils.TestCase): + + def _assertZone(self, zone, name, status): + self.assertEqual(zone.zoneName, name) + self.assertEqual(zone.zoneState, status) + + def test_list_availability_zone(self): + zones = cs.availability_zones.list(detailed=False) + cs.assert_called('GET', '/os-availability-zone') + + for zone in zones: + self.assertTrue(isinstance(zone, + availability_zones.AvailabilityZone)) + + self.assertEqual(2, len(zones)) + + l0 = [u'zone-1', u'available'] + l1 = [u'zone-2', u'not available'] + + z0 = shell._treeizeAvailabilityZone(zones[0]) + z1 = shell._treeizeAvailabilityZone(zones[1]) + + self.assertEqual((len(z0), len(z1)), (1, 1)) + + self._assertZone(z0[0], l0[0], l0[1]) + self._assertZone(z1[0], l1[0], l1[1]) + + def test_detail_availability_zone(self): + zones = cs.availability_zones.list(detailed=True) + cs.assert_called('GET', '/os-availability-zone/detail') + + for zone in zones: + self.assertTrue(isinstance(zone, + availability_zones.AvailabilityZone)) + + self.assertEqual(3, len(zones)) + + l0 = [u'zone-1', u'available'] + l1 = [u'|- fake_host-1', u''] + l2 = [u'| |- nova-compute', u'enabled :-) 2012-12-26T14:45:25.000000'] + l3 = [u'internal', u'available'] + l4 = [u'|- fake_host-1', u''] + l5 = [u'| |- nova-sched', u'enabled :-) 2012-12-26T14:45:25.000000'] + l6 = [u'|- fake_host-2', u''] + l7 = [u'| |- nova-network', u'enabled XXX 2012-12-26T14:45:24.000000'] + l8 = [u'zone-2', u'not available'] + + z0 = shell._treeizeAvailabilityZone(zones[0]) + z1 = shell._treeizeAvailabilityZone(zones[1]) + z2 = shell._treeizeAvailabilityZone(zones[2]) + + self.assertEqual((len(z0), len(z1), len(z2)), (3, 5, 1)) + + self._assertZone(z0[0], l0[0], l0[1]) + self._assertZone(z0[1], l1[0], l1[1]) + self._assertZone(z0[2], l2[0], l2[1]) + self._assertZone(z1[0], l3[0], l3[1]) + self._assertZone(z1[1], l4[0], l4[1]) + self._assertZone(z1[2], l5[0], l5[1]) + self._assertZone(z1[3], l6[0], l6[1]) + self._assertZone(z1[4], l7[0], l7[1]) + self._assertZone(z2[0], l8[0], l8[1]) diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index a407370e7..dd4499225 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -1,6 +1,7 @@ # Copyright 2010 Jacob Kaplan-Moss # Copyright 2011 OpenStack LLC. +# Copyright 2012 IBM Corp. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -862,3 +863,7 @@ def test_get_password(self): def test_clear_password(self): self.run_command('clear-password sample-server') self.assert_called('DELETE', '/servers/1234/os-server-password') + + def test_availability_zone_list(self): + self.run_command('availability-zone-list') + self.assert_called('GET', '/os-availability-zone/detail') From 3724528db78e2ff1f8b475b0ff58e4fbf5291bbf Mon Sep 17 00:00:00 2001 From: Alessio Ababilov Date: Wed, 6 Feb 2013 16:47:06 +0200 Subject: [PATCH 0073/1705] Update .coveragerc Set up proper source and omit options. Change-Id: I966256a3a3bd37b438e98e7bcf1402482c42b567 Implements: blueprint update-coveragerc --- .coveragerc | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 000000000..9f280d178 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,7 @@ +[run] +branch = True +source = novaclient +omit = novaclient/openstack/* + +[report] +ignore-errors = True From 17b251d8b064595470accd0769c03de840d7394c Mon Sep 17 00:00:00 2001 From: Sean McCully Date: Fri, 8 Feb 2013 14:54:36 -0600 Subject: [PATCH 0074/1705] management_url not set by authenticate method Fixes Bug 1109243. If management_url not set, raise UnAuthorized exception, otherwise url chaining methods fail and cause NoneType exceptions. Change-Id: I19e87a5dcfcf93b4fa1d423bd99de352679fa16d --- novaclient/client.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/novaclient/client.py b/novaclient/client.py index 9b9797928..a1d8cb198 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -377,6 +377,8 @@ def authenticate(self): if self.bypass_url: self.set_management_url(self.bypass_url) + elif not self.management_url: + raise exceptions.Unauthorized('Nova Client') # Store the token/mgmt url in the keyring for later requests. if has_keyring and self.os_cache: From e8001968607492218cce4c2a408e54836f627212 Mon Sep 17 00:00:00 2001 From: Kieran Spear Date: Thu, 7 Feb 2013 19:24:11 +1100 Subject: [PATCH 0075/1705] Support showing extra fields in server list Adds a --fields argument that sets the fields to display. ID is always displayed. Fixes bug #1076473 Change-Id: If3462e6a490ea16da4834a7f40f96b111c9e8227 --- novaclient/utils.py | 40 ++++++++++++++++++++++++++++++++++++++++ novaclient/v1_1/shell.py | 32 +++++++++++++++++++++++++++++--- tests/v1_1/fakes.py | 12 ++++++++++-- tests/v1_1/test_shell.py | 11 +++++++++++ 4 files changed, 90 insertions(+), 5 deletions(-) diff --git a/novaclient/utils.py b/novaclient/utils.py index 835977eee..9d2c140ac 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -250,6 +250,46 @@ def _format_servers_list_networks(server): return '; '.join(output) +def _format_security_groups(groups): + return ', '.join(group['name'] for group in groups) + + +def _format_field_name(attr): + """Format an object attribute in a human-friendly way.""" + # Split at ':' and leave the extension name as-is. + parts = attr.rsplit(':', 1) + name = parts[-1].replace('_', ' ') + # Don't title() on mixed case + if name.isupper() or name.islower(): + name = name.title() + parts[-1] = name + return ': '.join(parts) + + +def _make_field_formatter(attr, filters=None): + """ + Given an object attribute, return a formatted field name and a + formatter suitable for passing to print_list. + + Optionally pass a dict mapping attribute names to a function. The function + will be passed the value of the attribute and should return the string to + display. + """ + filter_ = None + if filters: + filter_ = filters.get(attr) + + def get_field(obj): + field = getattr(obj, attr, '') + if field and filter_: + field = filter_(field) + return field + + name = _format_field_name(attr) + formatter = get_field + return name, formatter + + class HookableMixin(object): """Mixin so classes can register and run hooks.""" _hooks_map = {} diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 0c5905aee..3008fb50a 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -889,6 +889,11 @@ def do_image_delete(cs, args): metavar='', nargs='?', help='Display information from single tenant (Admin only).') +@utils.arg('--fields', + default=None, + metavar='', + help='Comma-separated list of fields to display. ' + 'Use the show command to see which fields are available.') def do_list(cs, args): """List active servers.""" imageid = None @@ -910,11 +915,32 @@ def do_list(cs, args): 'host': args.host, 'instance_name': args.instance_name} + filters = {'flavor': lambda f: f['id'], + 'security_groups': utils._format_security_groups} + + formatters = {} + field_titles = [] + if args.fields: + for field in args.fields.split(','): + field_title, formatter = utils._make_field_formatter(field, + filters) + field_titles.append(field_title) + formatters[field_title] = formatter + id_col = 'ID' - columns = [id_col, 'Name', 'Status', 'Networks'] - formatters = {'Networks': utils._format_servers_list_networks} - utils.print_list(cs.servers.list(search_opts=search_opts), columns, + servers = cs.servers.list(search_opts=search_opts) + convert = [('OS-EXT-SRV-ATTR:host', 'host'), + ('OS-EXT-STS:task_state', 'task_state'), + ('OS-EXT-SRV-ATTR:instance_name', 'instance_name'), + ('hostId', 'host_id')] + _translate_keys(servers, convert) + if field_titles: + columns = [id_col] + field_titles + else: + columns = [id_col, 'Name', 'Status', 'Networks'] + formatters['Networks'] = utils._format_servers_list_networks + utils.print_list(servers, columns, formatters, sortby_index=1) diff --git a/tests/v1_1/fakes.py b/tests/v1_1/fakes.py index 212bac446..31da12fb4 100644 --- a/tests/v1_1/fakes.py +++ b/tests/v1_1/fakes.py @@ -264,7 +264,14 @@ def get_servers_detail(self, **kw): "metadata": { "Server Label": "Web Head 1", "Image Version": "2.1" - } + }, + "OS-EXT-SRV-ATTR:host": "computenode1", + "security_groups": [{ + 'id': 1, 'name': 'securitygroup1', + 'description': 'FAKE_SECURITY_GROUP', + 'tenant_id': '4ffc664c198e435e9853f2538fbcd7a7' + }], + "OS-EXT-MOD:some_thing": "mod_some_thing_value", }, { "id": 5678, @@ -295,7 +302,8 @@ def get_servers_detail(self, **kw): }, "metadata": { "Server Label": "DB 1" - } + }, + "OS-EXT-SRV-ATTR:host": "computenode2", }, { "id": 9012, diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index dd4499225..e7eca24eb 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -19,6 +19,7 @@ import datetime import os import mock +import StringIO import sys import tempfile @@ -323,6 +324,16 @@ def test_list(self): self.run_command('list') self.assert_called('GET', '/servers/detail') + @mock.patch('sys.stdout', StringIO.StringIO()) + def test_list_fields(self): + self.run_command('list --fields ' + 'host,security_groups,OS-EXT-MOD:some_thing') + self.assert_called('GET', '/servers/detail') + self.assertIn('computenode1', sys.stdout.getvalue()) + self.assertIn('securitygroup1', sys.stdout.getvalue()) + self.assertIn('OS-EXT-MOD: Some Thing', sys.stdout.getvalue()) + self.assertIn('mod_some_thing_value', sys.stdout.getvalue()) + def test_reboot(self): self.run_command('reboot sample-server') self.assert_called('POST', '/servers/1234/action', From 2eeab72aee2a278c591b9dba5b1723d47ddb90b8 Mon Sep 17 00:00:00 2001 From: Kieran Spear Date: Mon, 11 Feb 2013 11:21:26 +1100 Subject: [PATCH 0076/1705] Fix run_tests.sh --coverage Looks like a couple of typos crept in here preventing coverage generation. Change-Id: I08a61a67dadc5dcd334f9f288d8ee6719d469ca1 --- run_tests.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/run_tests.sh b/run_tests.sh index e5474c413..a3a74cdcf 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -77,7 +77,7 @@ function run_tests { if [ $coverage -eq 1 ]; then # Do not test test_coverage_ext when gathering coverage. if [ "x$testrargs" = "x" ]; then - testrargs = "^(?!.*test_coverage_ext).*$" + testrargs="^(?!.*test_coverage_ext).*$" fi export PYTHON="${wrapper} coverage run --source novaclient --parallel-mode" fi @@ -163,6 +163,6 @@ fi if [ $coverage -eq 1 ]; then echo "Generating coverage report in covhtml/" - ${wrapper} cverage combine + ${wrapper} coverage combine ${wrapper} coverage html --include='novaclient/*' --omit='novaclient/openstack/common/*' -d covhtml -i fi From 0b4590cb2438b4ec1fd8842d7ae3f2627059cabc Mon Sep 17 00:00:00 2001 From: Zane Bitter Date: Fri, 1 Feb 2013 09:39:07 +0100 Subject: [PATCH 0077/1705] Mask permissions on private key files When using "nova x509-create-cert", the private key should be written to a file with the permissions 0400, not (world-readable) 0644, in line with how ssh private keys are treated. bug 1112605 Change-Id: I0b20378efba38fa58f4ad9a33cd15b3432ebb8a2 Signed-off-by: Zane Bitter --- novaclient/v1_1/shell.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 0c5905aee..e969d6a3a 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2149,9 +2149,13 @@ def do_x509_create_cert(cs, args): certs = cs.certs.create() - with open(args.pk_filename, 'w') as private_key: - private_key.write(certs.private_key) - print "Wrote private key to %s" % args.pk_filename + try: + old_umask = os.umask(0o377) + with open(args.pk_filename, 'w') as private_key: + private_key.write(certs.private_key) + print "Wrote private key to %s" % args.pk_filename + finally: + os.umask(old_umask) with open(args.cert_filename, 'w') as cert: cert.write(certs.data) From c73afa9fd1df14bff9186c1e73ac8f3593ef81db Mon Sep 17 00:00:00 2001 From: Sascha Peilicke Date: Mon, 21 Jan 2013 12:00:54 +0100 Subject: [PATCH 0078/1705] Update to requests >= 0.8 The requests module dropped all configuration with the 1.0.0 release. There's no danger_mode and no 'verbose'' mode. The former shouldn't be necessary anymore and the latter can be done by setting a different log handler for the request.logging root logger. Change-Id: Iec169ef6e39097814cdbf1b777bc0590236692ba --- novaclient/client.py | 8 ++------ tests/test_client.py | 3 +-- tests/utils.py | 1 - tools/pip-requires | 2 +- 4 files changed, 4 insertions(+), 10 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index 225b68d54..0e0afebe5 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -44,10 +44,6 @@ class HTTPClient(object): USER_AGENT = 'python-novaclient' - requests_config = { - 'danger_mode': False, - } - def __init__(self, user, password, projectid, auth_url=None, insecure=False, timeout=None, proxy_tenant_id=None, proxy_token=None, region_name=None, @@ -104,7 +100,8 @@ def __init__(self, user, password, projectid, auth_url=None, ch = logging.StreamHandler() self._logger.setLevel(logging.DEBUG) self._logger.addHandler(ch) - self.requests_config['verbose'] = sys.stderr + if hasattr(requests, logging): + requests.logging.getLogger(requests.__name__).addHandler(ch) def use_token_cache(self, use_it): self.os_cache = use_it @@ -167,7 +164,6 @@ def request(self, url, method, **kwargs): method, url, verify=self.verify_cert, - config=self.requests_config, **kwargs) self.http_log_resp(resp) diff --git a/tests/test_client.py b/tests/test_client.py index ca21fb0ce..ecc9606f5 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -43,8 +43,7 @@ def test_client_with_timeout(self): requests.request.assert_called_with(mock.ANY, mock.ANY, timeout=2, headers=mock.ANY, - verify=mock.ANY, - config=mock.ANY) + verify=mock.ANY) def test_get_client_class_v2(self): output = novaclient.client.get_client_class('2') diff --git a/tests/utils.py b/tests/utils.py index d3a744219..11bff80e1 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -7,7 +7,6 @@ class TestCase(testtools.TestCase): TEST_REQUEST_BASE = { - 'config': {'danger_mode': False}, 'verify': True, } diff --git a/tools/pip-requires b/tools/pip-requires index 7fbcbbe7c..ee904d4a3 100644 --- a/tools/pip-requires +++ b/tools/pip-requires @@ -1,5 +1,5 @@ argparse iso8601>=0.1.4 prettytable>=0.6,<0.7 -requests<1.0 +requests>=0.8 simplejson From 0933c64251b24e1558676ef7f9d6ab446e386fea Mon Sep 17 00:00:00 2001 From: Mitsuhiko Yamazaki Date: Mon, 4 Feb 2013 14:08:52 +0900 Subject: [PATCH 0079/1705] Add format options to 'nova coverage-report'. This adds --html and --xml options to 'nova coverage-report' to enable selection of coverage report format. If specifying none of these, text-formatted report will be created. This also adds --combine option to 'nova coverage-start' to make sure that 'nova coverage-report --html' works fine. Fixes bug 1114766 Change-Id: I9fee26bd5c45cac35f425ac7abbced4e2f3ff4df --- novaclient/v1_1/shell.py | 22 ++++++++++++++++++++-- tests/v1_1/test_shell.py | 15 +++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index e969d6a3a..8dd4dce7b 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2442,9 +2442,14 @@ def do_host_action(cs, args): utils.print_list([result], ['HOST', 'power_action']) +@utils.arg('--combine', + dest='combine', + action="store_true", + default=False, + help='Generate a single report for all services.') def do_coverage_start(cs, args): """Start Nova coverage reporting""" - cs.coverage.start() + cs.coverage.start(combine=args.combine) print "Coverage collection started" @@ -2455,9 +2460,22 @@ def do_coverage_stop(cs, args): @utils.arg('filename', metavar='', help='report filename') +@utils.arg('--html', + dest='html', + action="store_true", + default=False, + help='Generate HTML reports instead of text ones.') +@utils.arg('--xml', + dest='xml', + action="store_true", + default=False, + help='Generate XML reports instead of text ones.') def do_coverage_report(cs, args): """Generate a coverage report""" - cov = cs.coverage.report(args.filename) + if args.html == True and args.xml == True: + raise exceptions.CommandError("--html and --xml must not be " + "specified together.") + cov = cs.coverage.report(args.filename, xml=args.xml, html=args.html) print "Report path: %s" % cov[-1]['path'] diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index dd4499225..db508226b 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -689,6 +689,11 @@ def test_coverage_start(self): self.run_command('coverage-start') self.assert_called('POST', '/os-coverage/action') + def test_coverage_start_with_combine(self): + self.run_command('coverage-start --combine') + body = {'start': {'combine': True}} + self.assert_called('POST', '/os-coverage/action', body) + def test_coverage_stop(self): self.run_command('coverage-stop') self.assert_called_anytime('POST', '/os-coverage/action') @@ -697,6 +702,16 @@ def test_coverage_report(self): self.run_command('coverage-report report') self.assert_called_anytime('POST', '/os-coverage/action') + def test_coverage_report_with_html(self): + self.run_command('coverage-report report --html') + body = {'report': {'html': True, 'file': 'report'}} + self.assert_called_anytime('POST', '/os-coverage/action', body) + + def test_coverage_report_with_xml(self): + self.run_command('coverage-report report --xml') + body = {'report': {'xml': True, 'file': 'report'}} + self.assert_called_anytime('POST', '/os-coverage/action', body) + def test_hypervisor_list(self): self.run_command('hypervisor-list') self.assert_called('GET', '/os-hypervisors') From 2b81789f1a86c4446aea20465cccbe08e3a820ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victoria=20Mart=C3=ADnez=20de=20la=20Cruz?= Date: Tue, 12 Feb 2013 11:46:02 -0300 Subject: [PATCH 0080/1705] Corrects 2nd argument type for logging Fixes a regression introduced in commit c73afa9fd1df14bff9186c1e73ac8f3593ef81db. This completely breaks Horizon in last DevStack version. Change-Id: Ib754c6ea325534399a34ff76a290475ac652fea9 Fixes: bug #1122958 --- novaclient/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/client.py b/novaclient/client.py index 0e0afebe5..763eec271 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -100,7 +100,7 @@ def __init__(self, user, password, projectid, auth_url=None, ch = logging.StreamHandler() self._logger.setLevel(logging.DEBUG) self._logger.addHandler(ch) - if hasattr(requests, logging): + if hasattr(requests, 'logging'): requests.logging.getLogger(requests.__name__).addHandler(ch) def use_token_cache(self, use_it): From 9f0cf6a54719fff845c7446dbba89f5e9503564c Mon Sep 17 00:00:00 2001 From: Joe Gordon Date: Tue, 12 Feb 2013 22:55:08 +0000 Subject: [PATCH 0081/1705] Make availability_zone in aggregate_create optional Aggregates do not require an availability zone any more, but in order to keep the current API, allow for availability zone to be set to None (the new default value) Fixes bug 1123468 Change-Id: I216c4fc808a91b0a5f602ee02ae1bca46adb73f4 --- novaclient/v1_1/shell.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index e969d6a3a..d724f5df4 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2232,7 +2232,9 @@ def do_aggregate_list(cs, args): @utils.arg('name', metavar='', help='Name of aggregate.') @utils.arg('availability_zone', metavar='', - help='The availability zone of the aggregate.') + default=None, + nargs='?', + help='The availability zone of the aggregate (optional).') def do_aggregate_create(cs, args): """Create a new aggregate with the specified details.""" aggregate = cs.aggregates.create(args.name, args.availability_zone) From c97879a3f24ac51a8b7673590445014eeb046381 Mon Sep 17 00:00:00 2001 From: Joe Gordon Date: Tue, 12 Feb 2013 23:22:34 +0000 Subject: [PATCH 0082/1705] Fix nova availability-zone-list for admin users Don't need to convert returned timestamp Fix bug 1123381 Change-Id: I1dbc82475b44b8d321fffb512083fc7a3b13153d --- novaclient/v1_1/shell.py | 2 +- tests/v1_1/test_availability_zone.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 8dd4dce7b..d5c0469bb 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2921,7 +2921,7 @@ def _treeizeAvailabilityZone(zone): az.zoneState = '%s %s %s' % ( 'enabled' if state['active'] else 'disabled', ':-)' if state['available'] else 'XXX', - timeutils.strtime(state['updated_at'])) + state['updated_at']) az._info['zoneName'] = az.zoneName az._info['zoneState'] = az.zoneState result.append(az) diff --git a/tests/v1_1/test_availability_zone.py b/tests/v1_1/test_availability_zone.py index b06f21def..f270bec27 100644 --- a/tests/v1_1/test_availability_zone.py +++ b/tests/v1_1/test_availability_zone.py @@ -62,12 +62,12 @@ def test_detail_availability_zone(self): l0 = [u'zone-1', u'available'] l1 = [u'|- fake_host-1', u''] - l2 = [u'| |- nova-compute', u'enabled :-) 2012-12-26T14:45:25.000000'] + l2 = [u'| |- nova-compute', u'enabled :-) 2012-12-26 14:45:25'] l3 = [u'internal', u'available'] l4 = [u'|- fake_host-1', u''] - l5 = [u'| |- nova-sched', u'enabled :-) 2012-12-26T14:45:25.000000'] + l5 = [u'| |- nova-sched', u'enabled :-) 2012-12-26 14:45:25'] l6 = [u'|- fake_host-2', u''] - l7 = [u'| |- nova-network', u'enabled XXX 2012-12-26T14:45:24.000000'] + l7 = [u'| |- nova-network', u'enabled XXX 2012-12-26 14:45:24'] l8 = [u'zone-2', u'not available'] z0 = shell._treeizeAvailabilityZone(zones[0]) From a92621680895282a2a24065314e4bcea8fdc688c Mon Sep 17 00:00:00 2001 From: Chris Jones Date: Tue, 12 Feb 2013 21:17:05 +0000 Subject: [PATCH 0083/1705] Expand and improve baremetal API. This adds an additional command to list the interfaces associated with a baremetal node. It also fixes some docstrings and renames the existing interface commands to be more consistent with the node commands. Change-Id: Ia6ae383d76adb1c9d632bf69ec22438f1412c66f --- novaclient/v1_1/contrib/baremetal.py | 41 ++++++++++++++++++++++++---- tests/v1_1/contrib/test_baremetal.py | 4 +++ 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/novaclient/v1_1/contrib/baremetal.py b/novaclient/v1_1/contrib/baremetal.py index 9a97d2888..a22022c7f 100644 --- a/novaclient/v1_1/contrib/baremetal.py +++ b/novaclient/v1_1/contrib/baremetal.py @@ -140,6 +140,20 @@ def remove_interface(self, node_id, address): url = '/os-baremetal-nodes/%s/action' % node_id self.api.client.post(url, body=req_body) + def list_interfaces(self, node_id): + """ + List the interfaces on a baremetal node. + + :param node_id: The ID of the node to list. + :rtype: list + """ + interfaces = [] + node = self._get("/os-baremetal-nodes/%s" % node_id, 'node') + for interface in node.interfaces: + interface_object = BareMetalNodeInterface(self, interface) + interfaces.append(interface_object) + return interfaces + @utils.arg('service_host', metavar='', @@ -191,7 +205,7 @@ def do_baremetal_node_create(cs, args): metavar='', help='ID of the node to delete.') def do_baremetal_node_delete(cs, args): - """Remove a volume.""" + """Remove a baremetal node and any associated interfaces""" node = _find_baremetal_node(cs, args.node) cs.baremetal.delete(node) @@ -232,7 +246,7 @@ def _print_baremetal_nodes_list(nodes): def do_baremetal_node_list(cs, _args): - """Print a list of available baremetal nodes.""" + """Print a list of available baremetal nodes""" nodes = cs.baremetal.list() _print_baremetal_nodes_list(nodes) @@ -248,11 +262,21 @@ def _print_baremetal_resource(resource): utils.print_dict(info) +def _print_baremetal_node_interfaces(interfaces): + """Print the interfaces of a baremetal node""" + utils.print_list(interfaces, [ + 'ID', + 'Datapath_ID', + 'Port_No', + 'Address', + ]) + + @utils.arg('node', metavar='', help="ID of node") def do_baremetal_node_show(cs, args): - """Show information about a node""" + """Show information about a baremetal node""" node = _find_baremetal_node(cs, args.node) _print_baremetal_resource(node) @@ -271,7 +295,7 @@ def do_baremetal_node_show(cs, args): default=0, metavar='', help="OpenFlow port number of interface") -def do_baremetal_add_interface(cs, args): +def do_baremetal_interface_add(cs, args): """Add a network interface to a baremetal node""" bmif = cs.baremetal.add_interface(args.node, args.address, args.datapath_id, args.port_no) @@ -280,6 +304,13 @@ def do_baremetal_add_interface(cs, args): @utils.arg('node', metavar='', help="ID of node") @utils.arg('address', metavar='
', help="MAC address of interface") -def do_baremetal_remove_interface(cs, args): +def do_baremetal_interface_remove(cs, args): """Remove a network interface from a baremetal node""" cs.baremetal.remove_interface(args.node, args.address) + + +@utils.arg('node', metavar='', help="ID of node") +def do_baremetal_interface_list(cs, args): + """List network interfaces associated with a baremetal node""" + interfaces = cs.baremetal.list_interfaces(args.node) + _print_baremetal_node_interfaces(interfaces) diff --git a/tests/v1_1/contrib/test_baremetal.py b/tests/v1_1/contrib/test_baremetal.py index b95fc97d5..3bf121ead 100644 --- a/tests/v1_1/contrib/test_baremetal.py +++ b/tests/v1_1/contrib/test_baremetal.py @@ -59,3 +59,7 @@ def test_node_add_interface(self): def test_node_remove_interface(self): cs.baremetal.remove_interface(1, "bb:cc:dd:ee:ff:aa") cs.assert_called('POST', '/os-baremetal-nodes/1/action') + + def test_node_list_interfaces(self): + il = cs.baremetal.list_interfaces(1) + cs.assert_called('GET', '/os-baremetal-nodes/1') From d1d4f33aca4f9753dd31972ea53ddc831b52fbfc Mon Sep 17 00:00:00 2001 From: Davanum Srinivas Date: Mon, 11 Feb 2013 15:55:33 -0500 Subject: [PATCH 0084/1705] Fix IOError with gnomekeyring.find_network_password_sync find_network_password_sync throws a gnomekeyring.IOError when a non-root user tries to run nova client from a ssh console. If we don't catch this exception nova client throws the traceback (shown in the bug report) and stops. If we catch this exception (just like we catch ValueError), and return None, Nova client executes just fine. Fixes LP# 1116302 Change-Id: If6937b3f8eafb1dc55224b2ca2bd0f93ae07f8c6 --- novaclient/shell.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/novaclient/shell.py b/novaclient/shell.py index b00b28604..537fef3af 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -33,6 +33,11 @@ try: import keyring HAS_KEYRING = True + try: + if isinstance(keyring.get_keyring(), keyring.backend.GnomeKeyring): + _IOError = gnomekeyring.IOError + except Exception: + _IOError = IOError except ImportError: pass @@ -146,7 +151,7 @@ def management_url(self): block = keyring.get_password('novaclient_auth', self._make_key()) if block: _token, management_url, _tenant_id = block.split('|', 2) - except ValueError: + except (_IOError, ValueError): pass return management_url @@ -163,7 +168,7 @@ def auth_token(self): block = keyring.get_password('novaclient_auth', self._make_key()) if block: token, _management_url, _tenant_id = block.split('|', 2) - except ValueError: + except (_IOError, ValueError): pass return token @@ -176,7 +181,7 @@ def tenant_id(self): block = keyring.get_password('novaclient_auth', self._make_key()) if block: _token, _management_url, tenant_id = block.split('|', 2) - except ValueError: + except (_IOError, ValueError): pass return tenant_id From c0e85a84b0ef60047f9435ef5035d0f65e446847 Mon Sep 17 00:00:00 2001 From: David Scannell Date: Wed, 13 Feb 2013 13:47:45 -0500 Subject: [PATCH 0085/1705] Allow extensions to provide a name when discovered on the python path. Using novaclient with some extensions via python code, you might have an invocation like this: extensions = shell.OpenStackComputeShell()._discover_extensions("1.1") novaclient = Client("1.1", user, apikey, project, authurl, extensions=extensions, endpoint_type=shell.DEFAULT_NOVA_ENDPOINT_TYPE, service_type=shell.DEFAULT_NOVA_SERVICE_TYPE) If you have an extension like 'myextension.py' in the v1_1/contrib directory, you'll end up with a very sensible attribute on the resulting novaclient object, i.e. novaclient.myextension If you have a package distributed in the package myextension_python_novaclient_ext, then it'll automatically be picked up as an extension (awesome!) but the name is not as intuitive. novaclient.myextension_python_novaclient_ext This patch simply changes this to allow the Extension to provide a name for itself. The possibility of collisions exists, but is not really any more significant than before (where you might have different versions of the same package installed in the system or heck, even a bizarrely named 'myextension_python_novaclient_ext.py' in the contrib/ directory). Fixes bug 1058366 Change-Id: Ie68463ffd7a939744e035b20fd50a7dc8da605de --- novaclient/shell.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/novaclient/shell.py b/novaclient/shell.py index b00b28604..aaddc2e02 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -428,12 +428,15 @@ def _discover_extensions(self, version): def _discover_via_python_path(self): for (module_loader, name, _ispkg) in pkgutil.iter_modules(): - if name.endswith('python_novaclient_ext'): + if name.endswith('_python_novaclient_ext'): if not hasattr(module_loader, 'load_module'): # Python 2.6 compat: actually get an ImpImporter obj module_loader = module_loader.find_module(name) module = module_loader.load_module(name) + if hasattr(module, 'extension_name'): + name = module.extension_name + yield name, module def _discover_via_contrib_path(self, version): From 430ef7071e8ea86b71fd4d266bb175549980dff1 Mon Sep 17 00:00:00 2001 From: David Scannell Date: Thu, 14 Feb 2013 15:28:52 -0500 Subject: [PATCH 0086/1705] Avoid doing a deep copy on the availability zone manager When novaclient is in debug mode then the HTTPClient will configure its logger with a StreamHandler that holds an instance of thread.lock, which cannot be copied. As a result running novaclient with the --debug flag will cause errors and mask real issues being debugged. Fixes bug 1123561 Change-Id: Idf19d62ff3e5b02b029f9089f403a697164231ac --- novaclient/v1_1/shell.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 8dd4dce7b..e73e9f033 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2890,7 +2890,7 @@ def _treeizeAvailabilityZone(zone): """Build a tree view for availability zones""" AvailabilityZone = availability_zones.AvailabilityZone - az = AvailabilityZone(copy.deepcopy(zone.manager), + az = AvailabilityZone(zone.manager, copy.deepcopy(zone._info), zone._loaded) result = [] @@ -2905,7 +2905,7 @@ def _treeizeAvailabilityZone(zone): if zone.hosts is not None: for (host, services) in zone.hosts.items(): # Host tree view item - az = AvailabilityZone(copy.deepcopy(zone.manager), + az = AvailabilityZone(zone.manager, copy.deepcopy(zone._info), zone._loaded) az.zoneName = '|- %s' % host az.zoneState = '' @@ -2915,7 +2915,7 @@ def _treeizeAvailabilityZone(zone): for (svc, state) in services.items(): # Service tree view item - az = AvailabilityZone(copy.deepcopy(zone.manager), + az = AvailabilityZone(zone.manager, copy.deepcopy(zone._info), zone._loaded) az.zoneName = '| |- %s' % svc az.zoneState = '%s %s %s' % ( From 132465231a1517aa79c5fad0eb4949a7e02bd763 Mon Sep 17 00:00:00 2001 From: Davanum Srinivas Date: Thu, 14 Feb 2013 20:58:01 -0500 Subject: [PATCH 0087/1705] Issue when gnomekeyring is present but not the current backend Issue was identified in the review for the previous patch for bug 1116302. Took this chance to rename _IOError to a better name (KeyringIOError) Change-Id: I321353d519eaebea27617702f92ecafe2052eb8e --- novaclient/shell.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/novaclient/shell.py b/novaclient/shell.py index 537fef3af..fbbdb6f92 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -35,9 +35,11 @@ HAS_KEYRING = True try: if isinstance(keyring.get_keyring(), keyring.backend.GnomeKeyring): - _IOError = gnomekeyring.IOError + KeyringIOError = gnomekeyring.IOError + else: + KeyringIOError = IOError except Exception: - _IOError = IOError + KeyringIOError = IOError except ImportError: pass @@ -151,7 +153,7 @@ def management_url(self): block = keyring.get_password('novaclient_auth', self._make_key()) if block: _token, management_url, _tenant_id = block.split('|', 2) - except (_IOError, ValueError): + except (KeyringIOError, ValueError): pass return management_url @@ -168,7 +170,7 @@ def auth_token(self): block = keyring.get_password('novaclient_auth', self._make_key()) if block: token, _management_url, _tenant_id = block.split('|', 2) - except (_IOError, ValueError): + except (KeyringIOError, ValueError): pass return token @@ -181,7 +183,7 @@ def tenant_id(self): block = keyring.get_password('novaclient_auth', self._make_key()) if block: _token, _management_url, tenant_id = block.split('|', 2) - except (_IOError, ValueError): + except (KeyringIOError, ValueError): pass return tenant_id From f35635be470b29398a267d1bc92fac3af46e851f Mon Sep 17 00:00:00 2001 From: Vasyl Khomenko Date: Mon, 17 Dec 2012 10:58:08 -0200 Subject: [PATCH 0088/1705] Extend test coverage (shell, fping) Added unit tests for shell code, that wasn't covered before. Implements: bp/python-novaclient-unittests Change-Id: I76e3df05d53e6c08e224feb30eb90ae819d8cdce --- tests/v1_1/fakes.py | 109 ++++++++------ tests/v1_1/test_fping.py | 29 +++- tests/v1_1/test_shell.py | 299 +++++++++++++++++++++++++++++++-------- 3 files changed, 331 insertions(+), 106 deletions(-) diff --git a/tests/v1_1/fakes.py b/tests/v1_1/fakes.py index 31da12fb4..787174413 100644 --- a/tests/v1_1/fakes.py +++ b/tests/v1_1/fakes.py @@ -522,7 +522,7 @@ def get_os_cloudpipe(self, **kw): return ( 200, {}, - {'cloudpipes': [{'project_id':1}]} + {'cloudpipes': [{'project_id': 1}]} ) def post_os_cloudpipe(self, **ks): @@ -1061,10 +1061,18 @@ def put_os_quota_class_sets_97f4c221bff44578b0300df4ef119353(self, # Security Groups # def get_os_security_groups(self, **kw): - return (200, {}, {"security_groups": [ - {'id': 1, 'name': 'test', 'description': 'FAKE_SECURITY_GROUP', - 'tenant_id': '4ffc664c198e435e9853f2538fbcd7a7'} - ]}) + return (200, {}, + {"security_groups": [ + dict(description="FAKE_SECURITY_GROUP", id=1, name="test", + rules=[{ + 'id': 1, + 'parent_group_id': 1, + 'group_id': 2, + 'ip_protocol': 'TCP', + 'from_port': 22, + 'to_port': 22, + 'cidr': '10.0.0.0/8'}], + tenant_id='4ffc664c198e435e9853f2538fbcd7a7')]}) def get_os_security_groups_1(self, **kw): return (200, {}, {"security_group": @@ -1109,40 +1117,48 @@ def post_os_security_group_rules(self, body, **kw): # Tenant Usage # def get_os_simple_tenant_usage(self, **kw): - return (200, {}, {u'tenant_usages': [{ - u'total_memory_mb_usage': 25451.762807466665, - u'total_vcpus_usage': 49.71047423333333, - u'total_hours': 49.71047423333333, - u'tenant_id': u'7b0a1d73f8fb41718f3343c207597869', - u'stop': u'2012-01-22 19:48:41.750722', - u'server_usages': [{ - u'hours': 49.71047423333333, - u'uptime': 27035, u'local_gb': 0, u'ended_at': None, - u'name': u'f15image1', - u'tenant_id': u'7b0a1d73f8fb41718f3343c207597869', - u'vcpus': 1, u'memory_mb': 512, u'state': u'active', - u'flavor': u'm1.tiny', - u'started_at': u'2012-01-20 18:06:06.479998'}], - u'start': u'2011-12-25 19:48:41.750687', - u'total_local_gb_usage': 0.0}]}) + return (200, {}, + {u'tenant_usages': [{ + u'total_memory_mb_usage': 25451.762807466665, + u'total_vcpus_usage': 49.71047423333333, + u'total_hours': 49.71047423333333, + u'tenant_id': u'7b0a1d73f8fb41718f3343c207597869', + u'stop': u'2012-01-22 19:48:41.750722', + u'server_usages': [{ + u'hours': 49.71047423333333, + u'uptime': 27035, + u'local_gb': 0, + u'ended_at': None, + u'name': u'f15image1', + u'tenant_id': u'7b0a1d73f8fb41718f3343c207597869', + u'vcpus': 1, + u'memory_mb': 512, + u'state': u'active', + u'flavor': u'm1.tiny', + u'started_at': u'2012-01-20 18:06:06.479998'}], + u'start': u'2011-12-25 19:48:41.750687', + u'total_local_gb_usage': 0.0}]}) def get_os_simple_tenant_usage_tenantfoo(self, **kw): - return (200, {}, {u'tenant_usage': { - u'total_memory_mb_usage': 25451.762807466665, - u'total_vcpus_usage': 49.71047423333333, - u'total_hours': 49.71047423333333, - u'tenant_id': u'7b0a1d73f8fb41718f3343c207597869', - u'stop': u'2012-01-22 19:48:41.750722', - u'server_usages': [{ - u'hours': 49.71047423333333, - u'uptime': 27035, u'local_gb': 0, u'ended_at': None, - u'name': u'f15image1', - u'tenant_id': u'7b0a1d73f8fb41718f3343c207597869', - u'vcpus': 1, u'memory_mb': 512, u'state': u'active', - u'flavor': u'm1.tiny', - u'started_at': u'2012-01-20 18:06:06.479998'}], - u'start': u'2011-12-25 19:48:41.750687', - u'total_local_gb_usage': 0.0}}) + return (200, {}, + {u'tenant_usage': { + u'total_memory_mb_usage': 25451.762807466665, + u'total_vcpus_usage': 49.71047423333333, + u'total_hours': 49.71047423333333, + u'tenant_id': u'7b0a1d73f8fb41718f3343c207597869', + u'stop': u'2012-01-22 19:48:41.750722', + u'server_usages': [{ + u'hours': 49.71047423333333, + u'uptime': 27035, u'local_gb': 0, + u'ended_at': None, + u'name': u'f15image1', + u'tenant_id': u'7b0a1d73f8fb41718f3343c207597869', + u'vcpus': 1, u'memory_mb': 512, + u'state': u'active', + u'flavor': u'm1.tiny', + u'started_at': u'2012-01-20 18:06:06.479998'}], + u'start': u'2011-12-25 19:48:41.750687', + u'total_local_gb_usage': 0.0}}) def get_os_simple_tenant_usage_test(self, **kw): return (200, {}, {u'tenant_usage': { @@ -1153,10 +1169,12 @@ def get_os_simple_tenant_usage_test(self, **kw): u'stop': u'2012-01-22 19:48:41.750722', u'server_usages': [{ u'hours': 49.71047423333333, - u'uptime': 27035, u'local_gb': 0, u'ended_at': None, + u'uptime': 27035, u'local_gb': 0, + u'ended_at': None, u'name': u'f15image1', u'tenant_id': u'7b0a1d73f8fb41718f3343c207597869', - u'vcpus': 1, u'memory_mb': 512, u'state': u'active', + u'vcpus': 1, u'memory_mb': 512, + u'state': u'active', u'flavor': u'm1.tiny', u'started_at': u'2012-01-20 18:06:06.479998'}], u'start': u'2011-12-25 19:48:41.750687', @@ -1171,18 +1189,20 @@ def get_os_simple_tenant_usage_tenant_id(self, **kw): u'stop': u'2012-01-22 19:48:41.750722', u'server_usages': [{ u'hours': 49.71047423333333, - u'uptime': 27035, u'local_gb': 0, u'ended_at': None, + u'uptime': 27035, u'local_gb': 0, + u'ended_at': None, u'name': u'f15image1', u'tenant_id': u'7b0a1d73f8fb41718f3343c207597869', - u'vcpus': 1, u'memory_mb': 512, u'state': u'active', + u'vcpus': 1, u'memory_mb': 512, + u'state': u'active', u'flavor': u'm1.tiny', u'started_at': u'2012-01-20 18:06:06.479998'}], u'start': u'2011-12-25 19:48:41.750687', u'total_local_gb_usage': 0.0}}) - # # Certificates # + def get_os_certificates_root(self, **kw): return ( 200, @@ -1200,12 +1220,13 @@ def post_os_certificates(self, **kw): # # Aggregates # + def get_os_aggregates(self, *kw): return (200, {}, {"aggregates": [ - {'id':'1', + {'id': '1', 'name': 'test', 'availability_zone': 'nova1'}, - {'id':'2', + {'id': '2', 'name': 'test2', 'availability_zone': 'nova1'}, ]}) diff --git a/tests/v1_1/test_fping.py b/tests/v1_1/test_fping.py index 6462fb213..d4581bd27 100644 --- a/tests/v1_1/test_fping.py +++ b/tests/v1_1/test_fping.py @@ -26,12 +26,35 @@ class FpingTest(utils.TestCase): + def test_fping_repr(self): + r = cs.fping.get(1) + self.assertEqual(repr(r), "") + def test_list_fpings(self): fl = cs.fping.list() cs.assert_called('GET', '/os-fping') - [self.assertTrue(isinstance(f, fping.Fping)) for f in fl] - [self.assertEqual(f.project_id, "fake-project") for f in fl] - [self.assertEqual(f.alive, True) for f in fl] + for f in fl: + self.assertTrue(isinstance(f, fping.Fping)) + self.assertEqual(f.project_id, "fake-project") + self.assertEqual(f.alive, True) + + def test_list_fpings_all_tenants(self): + fl = cs.fping.list(all_tenants=True) + for f in fl: + self.assertTrue(isinstance(f, fping.Fping)) + cs.assert_called('GET', '/os-fping?all_tenants=1') + + def test_list_fpings_exclude(self): + fl = cs.fping.list(exclude=['1']) + for f in fl: + self.assertTrue(isinstance(f, fping.Fping)) + cs.assert_called('GET', '/os-fping?exclude=1') + + def test_list_fpings_include(self): + fl = cs.fping.list(include=['1']) + for f in fl: + self.assertTrue(isinstance(f, fping.Fping)) + cs.assert_called('GET', '/os-fping?include=1') def test_get_fping(self): f = cs.fping.get(1) diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index b308b1817..f31700d4a 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -25,16 +25,15 @@ import fixtures -import novaclient.shell import novaclient.client from novaclient import exceptions from novaclient.openstack.common import timeutils +import novaclient.shell from tests.v1_1 import fakes from tests import utils class ShellFixture(fixtures.Fixture): - def setUp(self): super(ShellFixture, self).setUp() self.shell = novaclient.shell.OpenStackComputeShell() @@ -50,7 +49,6 @@ def tearDown(self): class ShellTest(utils.TestCase): - FAKE_ENV = { 'NOVA_USERNAME': 'username', 'NOVA_PASSWORD': 'password', @@ -94,12 +92,12 @@ def test_agents_create(self): self.assert_called( 'POST', '/os-agents', {'agent': { - 'hypervisor': 'kvm', - 'os': 'win', - 'architecture': 'x86', - 'version': '7.0', - 'url': '/xxx/xxx/xxx', - 'md5hash': 'add6bb58e139be103324d04d82d8f546'}}) + 'hypervisor': 'kvm', + 'os': 'win', + 'architecture': 'x86', + 'version': '7.0', + 'url': '/xxx/xxx/xxx', + 'md5hash': 'add6bb58e139be103324d04d82d8f546'}}) def test_agents_delete(self): self.run_command('agent-delete 1') @@ -109,7 +107,7 @@ def test_agents_modify(self): self.run_command('agent-modify 1 8.0 /yyy/yyyy/yyyy ' 'add6bb58e139be103324d04d82d8f546') self.assert_called('PUT', '/os-agents/1', - {"para": { + {"para": { "url": "/yyy/yyyy/yyyy", "version": "8.0", "md5hash": "add6bb58e139be103324d04d82d8f546"}}) @@ -124,7 +122,7 @@ def test_boot(self): 'imageRef': '1', 'min_count': 1, 'max_count': 1, - }}, + }}, ) def test_boot_image_with(self): @@ -138,13 +136,118 @@ def test_boot_image_with(self): 'imageRef': '1', 'min_count': 1, 'max_count': 1, - }}, + }}, + ) + + def test_boot_key(self): + self.run_command('boot --flavor 1 --image 1 --key_name 1 some-server') + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': '1', + 'key_name': '1', + 'min_count': 1, + 'max_count': 1, + }}, + ) + + def test_boot_user_data(self): + testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt') + expected_file_data = open(testfile).read().encode('base64').strip() + self.run_command( + 'boot --flavor 1 --image 1 --user_data %s some-server' % testfile) + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': '1', + 'min_count': 1, + 'max_count': 1, + 'user_data': expected_file_data + }}, + ) + + def test_boot_avzone(self): + self.run_command( + 'boot --flavor 1 --image 1 --availability-zone avzone ' + 'some-server') + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': '1', + 'availability_zone': 'avzone', + 'min_count': 1, + 'max_count': 1 + }}, + ) + + def test_boot_secgroup(self): + self.run_command( + 'boot --flavor 1 --image 1 --security-groups secgroup1,' + 'secgroup2 some-server') + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'security_groups': [{'name': 'secgroup1'}, + {'name': 'secgroup2'}], + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': '1', + 'min_count': 1, + 'max_count': 1, + }}, ) + def test_boot_config_drive(self): + self.run_command( + 'boot --flavor 1 --image 1 --config-drive 1 some-server') + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': '1', + 'min_count': 1, + 'max_count': 1, + 'config_drive': True + }}, + ) + + def test_boot_config_drive_custom(self): + self.run_command( + 'boot --flavor 1 --image 1 --config-drive /dev/hda some-server') + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': '1', + 'min_count': 1, + 'max_count': 1, + 'config_drive': '/dev/hda' + }}, + ) + + def test_boot_invalid_user_data(self): + invalid_file = os.path.join(os.path.dirname(__file__), + 'no_such_file') + cmd = ('boot some-server --flavor 1 --image 1' + ' --user_data %s' % invalid_file) + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + def test_boot_no_image_no_bdms(self): cmd = 'boot --flavor 1 some-server' self.assertRaises(exceptions.CommandError, self.run_command, cmd) + def test_boot_no_flavor(self): + cmd = 'boot --image 1 some-server' + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + def test_boot_no_image_bdms(self): self.run_command( 'boot --flavor 1 --block_device_mapping vda=blah:::0 some-server' @@ -159,13 +262,13 @@ def test_boot_no_image_bdms(self): 'volume_size': '', 'volume_id': 'blah', 'delete_on_termination': '0', - 'device_name':'vda' + 'device_name': 'vda' } ], 'imageRef': '', 'min_count': 1, 'max_count': 1, - }}, + }}, ) def test_boot_metadata(self): @@ -223,8 +326,8 @@ def test_boot_files(self): testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt') expected_file_data = open(testfile).read().encode('base64') - cmd = 'boot some-server --flavor 1 --image 1 ' \ - '--file /tmp/foo=%s --file /tmp/bar=%s' + cmd = ('boot some-server --flavor 1 --image 1' + ' --file /tmp/foo=%s --file /tmp/bar=%s') self.run_command(cmd % (testfile, testfile)) self.assert_called_anytime( @@ -236,16 +339,17 @@ def test_boot_files(self): 'min_count': 1, 'max_count': 1, 'personality': [ - {'path': '/tmp/bar', 'contents': expected_file_data}, - {'path': '/tmp/foo', 'contents': expected_file_data}, - ]}, - }, + {'path': '/tmp/bar', 'contents': expected_file_data}, + {'path': '/tmp/foo', 'contents': expected_file_data}, + ] + }}, ) - def test_boot_invalid_file(self): + def test_boot_invalid_files(self): invalid_file = os.path.join(os.path.dirname(__file__), 'asdfasdfasdfasdf') - cmd = 'boot some-server --image 1 --file /foo=%s' % invalid_file + cmd = ('boot some-server --flavor 1 --image 1' + ' --file /foo=%s' % invalid_file) self.assertRaises(exceptions.CommandError, self.run_command, cmd) def test_boot_num_instances(self): @@ -279,6 +383,42 @@ def test_flavor_show_with_alphanum_id(self): self.run_command('flavor-show aa1') self.assert_called_anytime('GET', '/flavors/aa1') + def test_flavor_key_set(self): + self.run_command('flavor-key 1 set k1=v1') + self.assert_called('POST', '/flavors/1/os-extra_specs', + {'extra_specs': {'k1': 'v1'}}) + + def test_flavor_key_unset(self): + self.run_command('flavor-key 1 unset k1') + self.assert_called('DELETE', '/flavors/1/os-extra_specs/k1') + + def test_flavor_access_list_flavor(self): + self.run_command('flavor-access-list --flavor 2') + self.assert_called('GET', '/flavors/2/os-flavor-access') + + # FIXME: flavor-access-list is not implemented yet + # def test_flavor_access_list_tenant(self): + # self.run_command('flavor-access-list --tenant proj2') + # self.assert_called('GET', '/flavors/2/os-flavor-access') + + def test_flavor_access_list_bad_filter(self): + cmd = 'flavor-access-list --flavor 2 --tenant proj2' + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + def test_flavor_access_list_no_filter(self): + cmd = 'flavor-access-list' + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + def test_flavor_access_add(self): + self.run_command('flavor-access-add 2 proj2') + self.assert_called('POST', '/flavors/2/action', + {'addTenantAccess': {'tenant': 'proj2'}}) + + def test_flavor_access_remove(self): + self.run_command('flavor-access-remove 2 proj2') + self.assert_called('POST', '/flavors/2/action', + {'removeTenantAccess': {'tenant': 'proj2'}}) + def test_image_show(self): self.run_command('image-show 1') self.assert_called('GET', '/images/1') @@ -286,7 +426,7 @@ def test_image_show(self): def test_image_meta_set(self): self.run_command('image-meta 1 set test_key=test_value') self.assert_called('POST', '/images/1/metadata', - {'metadata': {'test_key': 'test_value'}}) + {'metadata': {'test_key': 'test_value'}}) def test_image_meta_del(self): self.run_command('image-meta 1 delete test_key=test_value') @@ -345,13 +485,13 @@ def test_reboot(self): def test_rebuild(self): self.run_command('rebuild sample-server 1') # XXX need a way to test multiple calls - #self.assert_called('POST', '/servers/1234/action', + # self.assert_called('POST', '/servers/1234/action', # {'rebuild': {'imageRef': 1}}) self.assert_called('GET', '/images/2') self.run_command('rebuild sample-server 1 --rebuild-password asdf') # XXX need a way to test multiple calls - #self.assert_called('POST', '/servers/1234/action', + # self.assert_called('POST', '/servers/1234/action', # {'rebuild': {'imageRef': 1, 'adminPass': 'asdf'}}) self.assert_called('GET', '/images/2') @@ -385,7 +525,7 @@ def test_scrub(self): self.run_command('scrub 4ffc664c198e435e9853f2538fbcd7a7') self.assert_called('GET', '/os-networks', pos=-4) self.assert_called('GET', '/os-security-groups?all_tenants=1', - pos=-3) + pos=-3) self.assert_called('POST', '/os-networks/1/action', {"disassociate": None}, pos=-2) self.assert_called('DELETE', '/os-security-groups/1') @@ -469,7 +609,8 @@ def test_dns_delete_domain(self): def test_dns_list(self): self.run_command('dns-list testdomain --ip 192.168.1.1') self.assert_called('GET', - '/os-floating-ip-dns/testdomain/entries?ip=192.168.1.1') + '/os-floating-ip-dns/testdomain/entries?' + 'ip=192.168.1.1') self.run_command('dns-list testdomain --name testname') self.assert_called('GET', @@ -487,20 +628,21 @@ def test_floating_ip_bulk_create(self): self.run_command('floating-ip-bulk-create 10.0.0.1/24') self.assert_called('POST', '/os-floating-ips-bulk', {'floating_ips_bulk_create': - {'ip_range': '10.0.0.1/24'}}) + {'ip_range': '10.0.0.1/24'}}) def test_floating_ip_bulk_create_host_and_interface(self): - self.run_command('floating-ip-bulk-create 10.0.0.1/24 --pool testPool \ - --interface ethX') + self.run_command('floating-ip-bulk-create 10.0.0.1/24 --pool testPool' + ' --interface ethX') self.assert_called('POST', '/os-floating-ips-bulk', {'floating_ips_bulk_create': - {'ip_range': '10.0.0.1/24', - 'pool': 'testPool', 'interface': 'ethX'}}) + {'ip_range': '10.0.0.1/24', + 'pool': 'testPool', + 'interface': 'ethX'}}) def test_floating_ip_bulk_delete(self): self.run_command('floating-ip-bulk-delete 10.0.0.1/24') self.assert_called('PUT', '/os-floating-ips-bulk/delete', - {'ip_range': '10.0.0.1/24'}) + {'ip_range': '10.0.0.1/24'}) def test_usage_list(self): self.run_command('usage-list --start 2000-01-20 --end 2005-02-01') @@ -594,20 +736,20 @@ def test_live_migration(self): self.run_command('live-migration sample-server hostname') self.assert_called('POST', '/servers/1234/action', {'os-migrateLive': {'host': 'hostname', - 'block_migration': False, - 'disk_over_commit': False}}) - self.run_command('live-migration sample-server hostname \ - --block-migrate') + 'block_migration': False, + 'disk_over_commit': False}}) + self.run_command('live-migration sample-server hostname' + ' --block-migrate') self.assert_called('POST', '/servers/1234/action', {'os-migrateLive': {'host': 'hostname', - 'block_migration': True, - 'disk_over_commit': False}}) - self.run_command('live-migration sample-server hostname \ - --block-migrate --disk-over-commit') + 'block_migration': True, + 'disk_over_commit': False}}) + self.run_command('live-migration sample-server hostname' + ' --block-migrate --disk-over-commit') self.assert_called('POST', '/servers/1234/action', {'os-migrateLive': {'host': 'hostname', - 'block_migration': True, - 'disk_over_commit': True}}) + 'block_migration': True, + 'disk_over_commit': True}}) def test_reset_state(self): self.run_command('reset-state sample-server') @@ -765,26 +907,27 @@ def test_quota_defaults_no_nenant(self): def test_quota_update(self): self.run_command( - 'quota-update 97f4c221bff44578b0300df4ef119353 \ - --instances=5') + 'quota-update 97f4c221bff44578b0300df4ef119353' + ' --instances=5') self.assert_called('PUT', - '/os-quota-sets/97f4c221bff44578b0300df4ef119353') + '/os-quota-sets/97f4c221bff44578b0300df4ef119353') def test_quota_update_error(self): self.assertRaises(exceptions.CommandError, self.run_command, - 'quota-update 7f4c221-bff4-4578-b030-0df4ef119353 \ - --instances=5') + 'quota-update 7f4c221-bff4-4578-b030-0df4ef119353' + ' --instances=5') def test_quota_class_show(self): self.run_command('quota-class-show test') self.assert_called('GET', '/os-quota-class-sets/test') def test_quota_class_update(self): - self.run_command('quota-class-update 97f4c221bff44578b0300df4ef119353 \ - --instances=5') + self.run_command('quota-class-update 97f4c221bff44578b0300df4ef119353' + ' --instances=5') self.assert_called('PUT', - '/os-quota-class-sets/97f4c221bff44578b0300df4ef119353') + '/os-quota-class-sets/97f4c221bff44578b0300' + 'df4ef119353') def test_network_list(self): self.run_command('network-list') @@ -830,18 +973,22 @@ def test_network_disassociate_project(self): self.assert_called('POST', '/os-networks/2/action', body) def test_network_create_v4(self): - self.run_command('network-create --fixed-range-v4 10.0.1.0/24 \ - --dns1 10.0.1.254 new_network') + self.run_command('network-create --fixed-range-v4 10.0.1.0/24' + ' --dns1 10.0.1.254 new_network') body = {'network': {'cidr': '10.0.1.0/24', 'label': 'new_network', 'dns1': '10.0.1.254'}} self.assert_called('POST', '/os-networks', body) def test_network_create_v6(self): - self.run_command('network-create --fixed-range-v6 2001::/64 \ - new_network') + self.run_command('network-create --fixed-range-v6 2001::/64' + ' new_network') body = {'network': {'cidr_v6': '2001::/64', 'label': 'new_network'}} self.assert_called('POST', '/os-networks', body) + def test_network_create_invalid(self): + cmd = 'network-create 10.0.1.0' + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + def test_backup(self): self.run_command('backup sample-server back1 daily 1') self.assert_called('POST', '/servers/1234/action', @@ -867,7 +1014,7 @@ def test_evacuate(self): {'evacuate': {'host': 'new_host', 'onSharedStorage': False}}) self.run_command('evacuate sample-server new_host ' - '--password NewAdminPass') + '--password NewAdminPass') self.assert_called('POST', '/servers/1234/action', {'evacuate': {'host': 'new_host', 'onSharedStorage': False, @@ -877,7 +1024,7 @@ def test_evacuate(self): {'evacuate': {'host': 'new_host', 'onSharedStorage': False}}) self.run_command('evacuate sample-server new_host ' - '--on-shared-storage') + '--on-shared-storage') self.assert_called('POST', '/servers/1234/action', {'evacuate': {'host': 'new_host', 'onSharedStorage': True}}) @@ -891,5 +1038,39 @@ def test_clear_password(self): self.assert_called('DELETE', '/servers/1234/os-server-password') def test_availability_zone_list(self): - self.run_command('availability-zone-list') - self.assert_called('GET', '/os-availability-zone/detail') + self.run_command('availability-zone-list') + self.assert_called('GET', '/os-availability-zone/detail') + + def test_security_group_create(self): + self.run_command('secgroup-create test FAKE_SECURITY_GROUP') + self.assert_called('POST', '/os-security-groups', + {'security_group': + {'name': 'test', + 'description': 'FAKE_SECURITY_GROUP'}}) + + def test_security_group_list(self): + self.run_command('secgroup-list') + self.assert_called('GET', '/os-security-groups') + + def test_security_group_add_rule(self): + self.run_command('secgroup-add-rule test tcp 22 22 10.0.0.0/8') + self.assert_called('POST', '/os-security-group-rules', + {'security_group_rule': + {'from_port': '22', + 'ip_protocol': 'tcp', + 'to_port': '22', + 'parent_group_id': 1, + 'cidr': '10.0.0.0/8', + 'group_id': None}}) + + def test_security_group_list_rules(self): + self.run_command('secgroup-list-rules test') + self.assert_called('GET', '/os-security-groups') + + def test_security_group_list_all_tenants(self): + self.run_command('secgroup-list --all-tenants 1') + self.assert_called('GET', '/os-security-groups?all_tenants=1') + + def test_security_group_delete(self): + self.run_command('secgroup-delete test') + self.assert_called('DELETE', '/os-security-groups/1') From 8811ced00738059d4aee9db0d586eb8724bb1055 Mon Sep 17 00:00:00 2001 From: Anita Kuno Date: Sun, 17 Feb 2013 01:53:40 +0000 Subject: [PATCH 0089/1705] Added limit to image-list in a preparatory step toward addressing bug 1001345. Currently novaclient doesn't use the limit or marker params. As a step to addressing bug 1001345 which requires pagination, this patch introduces the use of limit as an option passed to the image-list function. Change-Id: Ia32f9e923b4eb9bcdde3b7bc1722c59d7791d104 --- novaclient/v1_1/images.py | 16 +++++++++++----- novaclient/v1_1/shell.py | 7 ++++++- tests/v1_1/test_images.py | 4 ++++ 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/novaclient/v1_1/images.py b/novaclient/v1_1/images.py index dfde21bd5..fcc8b547b 100644 --- a/novaclient/v1_1/images.py +++ b/novaclient/v1_1/images.py @@ -2,6 +2,7 @@ """ Image interface. """ +import urllib from novaclient import base @@ -37,16 +38,21 @@ def get(self, image): """ return self._get("/images/%s" % base.getid(image), "image") - def list(self, detailed=True): + def list(self, detailed=True, limit=None): """ Get a list of all images. :rtype: list of :class:`Image` + :param limit: maximum number of images to return. """ - if detailed is True: - return self._list("/images/detail", "images") - else: - return self._list("/images", "images") + params = {} + detail = '' + if detailed: + detail = '/detail' + if limit: + params['limit'] = int(limit) + query = '?%s' % urllib.urlencode(params) if params else '' + return self._list('/images%s%s' % (detail, query), 'images') def delete(self, image): """ diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 8dd4dce7b..9d6ce5e71 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -712,9 +712,14 @@ def do_network_create(cs, args): cs.networks.create(**kwargs) +@utils.arg('--limit', + dest="limit", + metavar="", + help='number of images to return per request') def do_image_list(cs, _args): """Print a list of available images to boot from.""" - image_list = cs.images.list() + limit = _args.limit + image_list = cs.images.list(limit=limit) def parse_server_name(image): try: diff --git a/tests/v1_1/test_images.py b/tests/v1_1/test_images.py index 71676e165..a4f8e6416 100644 --- a/tests/v1_1/test_images.py +++ b/tests/v1_1/test_images.py @@ -18,6 +18,10 @@ def test_list_images_undetailed(self): cs.assert_called('GET', '/images') [self.assertTrue(isinstance(i, images.Image)) for i in il] + def test_list_images_with_limit(self): + il = cs.images.list(limit=4) + cs.assert_called('GET', '/images/detail?limit=4') + def test_get_image_details(self): i = cs.images.get(1) cs.assert_called('GET', '/images/1') From 5c8caf06e9003ea8f9dd055da6f576d22be41104 Mon Sep 17 00:00:00 2001 From: Dan Smith Date: Mon, 18 Feb 2013 10:16:36 -0500 Subject: [PATCH 0090/1705] Add support for os-attach-interfaces This is dependent on nova change: I4f8f677af58afcb928379e5cf859388d1da45d51 Related to blueprint network-adapter-hotplug Change-Id: Ieef603e85c6557cbfd2fe4ae7109e6ca235ba51d --- novaclient/v1_1/servers.py | 57 ++++++++++++++++++++++++++++++++++++++ novaclient/v1_1/shell.py | 51 ++++++++++++++++++++++++++++++++++ tests/v1_1/fakes.py | 21 ++++++++++++++ tests/v1_1/test_servers.py | 15 ++++++++++ tests/v1_1/test_shell.py | 12 ++++++++ 5 files changed, 156 insertions(+) diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index d3ec07292..02af563f1 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -316,6 +316,24 @@ def evacuate(self, host, on_shared_storage, password=None): """ return self.manager.evacuate(self, host, on_shared_storage, password) + def interface_list(self): + """ + List interfaces attached to an instance. + """ + return self.manager.interface_list(self) + + def interface_attach(self, port_id, net_id, fixed_ip): + """ + Attach a network interface to an instance. + """ + return self.manager.interface_attach(self, port_id, net_id, fixed_ip) + + def interface_detach(self, port_id): + """ + Detach a network interface from an instance. + """ + return self.manager.interface_detach(self, port_id) + class ServerManager(local_base.BootingManagerWithFind): resource_class = Server @@ -802,6 +820,45 @@ def evacuate(self, server, host, on_shared_storage, password=None): return self._action('evacuate', server, body) + def interface_list(self, server): + """ + List attached network interfaces + + :param server: The :class:`Server` (or its ID) to query. + """ + return self._list('/servers/%s/os-interface' % base.getid(server), + 'interfaceAttachments') + + def interface_attach(self, server, port_id, net_id, fixed_ip): + """ + Attach a network_interface to an instance. + + :param server: The :class:`Server` (or its ID) to attach to. + :param port_id: The port to attach. + """ + + body = {'interfaceAttachment': {}} + if port_id: + body['interfaceAttachment']['port_id'] = port_id + if net_id: + body['interfaceAttachment']['net_id'] = net_id + if fixed_ip: + body['interfaceAttachment']['fixed_ips'] = [ + {'ip_address': fixed_ip}] + + return self._create('/servers/%s/os-interface' % base.getid(server), + body, 'interfaceAttachment') + + def interface_detach(self, server, port_id): + """ + Detach a network_interface from an instance. + + :param server: The :class:`Server` (or its ID) to detach from. + :param port_id: The port to detach. + """ + self._delete('/servers/%s/os-interface/%s' % (base.getid(server), + port_id)) + def _action(self, action, server, info=None, **kwargs): """ Perform a server "action" -- reboot/rebuild/resize/etc. diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 6edb95848..260581115 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2914,6 +2914,57 @@ def do_evacuate(cs, args): utils.print_dict(res) +def _print_interfaces(interfaces): + columns = ['Port State', 'Port ID', 'Net ID', 'IP addresses', + 'MAC Address'] + + class FormattedInterface(object): + def __init__(self, interface): + for col in columns: + key = col.lower().replace(" ", "_") + if hasattr(interface, key): + setattr(self, key, getattr(interface, key)) + self.ip_addresses = ",".join([fip['ip_address'] + for fip in interface.fixed_ips]) + utils.print_list([FormattedInterface(i) for i in interfaces], columns) + + +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_interface_list(cs, args): + """List interfaces attached to an instance.""" + server = _find_server(cs, args.server) + + res = server.interface_list() + if type(res) is list: + _print_interfaces(res) + + +@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('--port-id', metavar='', help='Port ID.', dest="port_id") +@utils.arg('--net-id', metavar='', help='Network ID', + default=None, dest="net_id") +@utils.arg('--fixed-ip', metavar='', help='Requested fixed IP.', + default=None, dest="fixed_ip") +def do_interface_attach(cs, args): + """Attach a network interface to an instance.""" + server = _find_server(cs, args.server) + + res = server.interface_attach(args.port_id, args.net_id, args.fixed_ip) + if type(res) is dict: + utils.print_dict(res) + + +@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('port_id', metavar='', help='Port ID.') +def do_interface_detach(cs, args): + """Detach a network interface from an instance.""" + server = _find_server(cs, args.server) + + res = server.interface_detach(args.port_id) + if type(res) is dict: + utils.print_dict(res) + + def _treeizeAvailabilityZone(zone): """Build a tree view for availability zones""" AvailabilityZone = availability_zones.AvailabilityZone diff --git a/tests/v1_1/fakes.py b/tests/v1_1/fakes.py index 787174413..542c63c2f 100644 --- a/tests/v1_1/fakes.py +++ b/tests/v1_1/fakes.py @@ -1569,3 +1569,24 @@ def get_os_availability_zone_detail(self, **kw): {"zoneName": "zone-2", "zoneState": {"available": False}, "hosts": None}]}) + + def get_servers_1234_os_interface(self, **kw): + return (200, {}, {"interfaceAttachments": [ + {"port_state": "ACTIVE", + "net_id": "net-id-1", + "port_id": "port-id-1", + "mac_address": "aa:bb:cc:dd:ee:ff", + "fixed_ips": [{"ip_address": "1.2.3.4"}], + }, + {"port_state": "ACTIVE", + "net_id": "net-id-1", + "port_id": "port-id-1", + "mac_address": "aa:bb:cc:dd:ee:ff", + "fixed_ips": [{"ip_address": "1.2.3.4"}], + }]}) + + def post_servers_1234_os_interface(self, **kw): + return (200, {}, {'interfaceAttachment': {}}) + + def delete_servers_1234_os_interface_port_id(self, **kw): + return (200, {}, None) diff --git a/tests/v1_1/test_servers.py b/tests/v1_1/test_servers.py index 82a09d35f..0c8760156 100644 --- a/tests/v1_1/test_servers.py +++ b/tests/v1_1/test_servers.py @@ -424,3 +424,18 @@ def test_evacuate(self): cs.assert_called('POST', '/servers/1234/action') cs.servers.evacuate(s, 'fake_target_host', 'False', 'NewAdminPassword') cs.assert_called('POST', '/servers/1234/action') + + def test_interface_list(self): + s = cs.servers.get(1234) + s.interface_list() + cs.assert_called('GET', '/servers/1234/os-interface') + + def test_interface_attach(self): + s = cs.servers.get(1234) + s.interface_attach(None, None, None) + cs.assert_called('POST', '/servers/1234/os-interface') + + def test_interface_detach(self): + s = cs.servers.get(1234) + s.interface_detach('port-id') + cs.assert_called('DELETE', '/servers/1234/os-interface/port-id') diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index f31700d4a..a57359ee8 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -1074,3 +1074,15 @@ def test_security_group_list_all_tenants(self): def test_security_group_delete(self): self.run_command('secgroup-delete test') self.assert_called('DELETE', '/os-security-groups/1') + + def test_interface_list(self): + self.run_command('interface-list 1234') + self.assert_called('GET', '/servers/1234/os-interface') + + def test_interface_attach(self): + self.run_command('interface-attach --port-id port_id 1234') + self.assert_called('POST', '/servers/1234/os-interface') + + def test_interface_detach(self): + self.run_command('interface-detach 1234 port_id') + self.assert_called('DELETE', '/servers/1234/os-interface/port_id') From 92fef8b29169e69d1adfee8fb7378003a408f0c9 Mon Sep 17 00:00:00 2001 From: Arata Notsu Date: Wed, 20 Feb 2013 08:49:03 +0900 Subject: [PATCH 0091/1705] Remove prov_vlan_id from baremetal In nova, this parameter is not used (and will be unacceptable soon) Change-Id: I072a204bef49940fb2ddc8dc44480b570bc3212c --- novaclient/v1_1/contrib/baremetal.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/novaclient/v1_1/contrib/baremetal.py b/novaclient/v1_1/contrib/baremetal.py index a22022c7f..1624b8dcc 100644 --- a/novaclient/v1_1/contrib/baremetal.py +++ b/novaclient/v1_1/contrib/baremetal.py @@ -56,7 +56,6 @@ def create(self, pm_address=None, pm_user=None, pm_password=None, - prov_vlan_id=None, terminal_port=None): """ Create a baremetal node. @@ -69,7 +68,6 @@ def create(self, :param pm_user: Username for the node's power management :param pm_password: Password for the node's power management :param prov_mac_address: MAC address to provision the node - :param prov_vlan_id: VLAN ID to use to provision the node :param terminal_port: ShellInABox port :rtype: :class:`BareMetalNode` """ @@ -81,7 +79,6 @@ def create(self, 'pm_user': pm_user, 'pm_password': pm_password, 'prov_mac_address': prov_mac_address, - 'prov_vlan_id': prov_vlan_id, 'terminal_port': terminal_port}} return self._create('/os-baremetal-nodes', body, 'node') @@ -182,10 +179,6 @@ def list_interfaces(self, node_id): @utils.arg('--pm_password', default=None, metavar='', help='Password for the node\'s power management') -@utils.arg('--prov_vlan_id', default=None, - metavar='', - type=int, - help='VLAN ID to use to provision the node') @utils.arg('--terminal_port', default=None, metavar='', type=int, @@ -196,7 +189,6 @@ def do_baremetal_node_create(cs, args): args.memory_mb, args.local_gb, args.prov_mac_address, pm_address=args.pm_address, pm_user=args.pm_user, pm_password=args.pm_password, - prov_vlan_id=args.prov_vlan_id, terminal_port=args.terminal_port) _print_baremetal_resource(node) @@ -214,7 +206,6 @@ def _translate_baremetal_node_keys(collection): convert = [('service_host', 'host'), ('local_gb', 'disk_gb'), ('prov_mac_address', 'mac_address'), - ('prov_vlan_id', 'vlan'), ('pm_address', 'pm_address'), ('pm_user', 'pm_username'), ('pm_password', 'pm_password'), @@ -237,7 +228,6 @@ def _print_baremetal_nodes_list(nodes): 'Memory_MB', 'Disk_GB', 'MAC Address', - 'VLAN', 'PM Address', 'PM Username', 'PM Password', From 25ae8f077051ffd2b9348e22e991694722b962a3 Mon Sep 17 00:00:00 2001 From: Sulochan Acharya Date: Wed, 20 Feb 2013 08:12:44 -0600 Subject: [PATCH 0092/1705] Allows admins to reset-network of an instance Adding some functionality to allow admins to call on resetNetwork action. Fixes bug #1130766 Change-Id: I3d0945b115d565bd2c54a5526ba51036e941a28f --- novaclient/v1_1/servers.py | 12 ++++++++++++ novaclient/v1_1/shell.py | 6 ++++++ tests/v1_1/fakes.py | 2 ++ tests/v1_1/test_servers.py | 7 +++++++ tests/v1_1/test_shell.py | 5 +++++ 5 files changed, 32 insertions(+) diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index d3ec07292..b1dfb74fe 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -293,6 +293,12 @@ def reset_state(self, state='error'): """ self.manager.reset_state(self, state) + def reset_network(self): + """ + Reset network of an instance. + """ + self.manager.reset_network(self) + def add_security_group(self, security_group): """ Add a security group to an instance. @@ -762,6 +768,12 @@ def reset_state(self, server, state='error'): """ self._action('os-resetState', server, dict(state=state)) + def reset_network(self, server): + """ + Reset network of an instance. + """ + self._action('resetNetwork', server) + def add_security_group(self, server, security_group): """ Add a Security Group to a instance diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 6edb95848..f62567382 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2374,6 +2374,12 @@ def do_reset_state(cs, args): _find_server(cs, args.server).reset_state(args.state) +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_reset_network(cs, args): + """Reset network of an instance.""" + _find_server(cs, args.server).reset_network() + + @utils.arg('--host', metavar='', default=None, help='Name of host.') @utils.arg('--servicename', metavar='', default=None, diff --git a/tests/v1_1/fakes.py b/tests/v1_1/fakes.py index 787174413..ee94fd092 100644 --- a/tests/v1_1/fakes.py +++ b/tests/v1_1/fakes.py @@ -497,6 +497,8 @@ def post_servers_1234_action(self, body, **kw): 'disk_over_commit']) elif action == 'os-resetState': assert body[action].keys() == ['state'] + elif action == 'resetNetwork': + assert body[action] is None elif action == 'addSecurityGroup': assert body[action].keys() == ['name'] elif action == 'removeSecurityGroup': diff --git a/tests/v1_1/test_servers.py b/tests/v1_1/test_servers.py index 82a09d35f..176bad81d 100644 --- a/tests/v1_1/test_servers.py +++ b/tests/v1_1/test_servers.py @@ -404,6 +404,13 @@ def test_reset_state(self): cs.servers.reset_state(s, 'newstate') cs.assert_called('POST', '/servers/1234/action') + def test_reset_network(self): + s = cs.servers.get(1234) + s.reset_network() + cs.assert_called('POST', '/servers/1234/action') + cs.servers.reset_network(s) + cs.assert_called('POST', '/servers/1234/action') + def test_add_security_group(self): s = cs.servers.get(1234) s.add_security_group('newsg') diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index f31700d4a..87a01171a 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -759,6 +759,11 @@ def test_reset_state(self): self.assert_called('POST', '/servers/1234/action', {'os-resetState': {'state': 'active'}}) + def test_reset_network(self): + self.run_command('reset-network sample-server') + self.assert_called('POST', '/servers/1234/action', + {'resetNetwork': None}) + def test_services_list(self): self.run_command('service-list') self.assert_called('GET', '/os-services') From 68e6af73bafde67894780e5d2317084847109edf Mon Sep 17 00:00:00 2001 From: Thomas Schreiber Date: Wed, 20 Feb 2013 09:46:57 +0100 Subject: [PATCH 0093/1705] A minimum of Python3 fixes so that installation works without errors/warnings. Fixes bug: 1130937 Change-Id: I740652fcd5804fc1c120fc409afdf4693c8e5781 --- novaclient/base.py | 2 +- novaclient/client.py | 10 ++-- novaclient/openstack/common/setup.py | 6 +- novaclient/shell.py | 7 ++- novaclient/utils.py | 6 +- novaclient/v1_1/shell.py | 64 +++++++++++----------- tests/fakes.py | 6 +- tests/v1_1/contrib/test_tenant_networks.py | 2 +- tools/install_venv.py | 21 +++---- 9 files changed, 63 insertions(+), 61 deletions(-) diff --git a/novaclient/base.py b/novaclient/base.py index 351399a78..729b4ca92 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -103,7 +103,7 @@ def completion_cache(self, cache_type, obj_class, mode): cache_dir = os.path.expanduser(os.path.join(base_dir, uniqifier)) try: - os.makedirs(cache_dir, 0755) + os.makedirs(cache_dir, 0o755) except OSError: # NOTE(kiall): This is typicaly either permission denied while # attempting to create the directory, or the directory diff --git a/novaclient/client.py b/novaclient/client.py index 763eec271..a04c3c70b 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -213,7 +213,7 @@ def _cs_request(self, url, method, **kwargs): resp, body = self._time_request(self.management_url + url, method, **kwargs) return resp, body - except exceptions.Unauthorized, ex: + except exceptions.Unauthorized as e: try: self.authenticate() kwargs['headers']['X-Auth-Token'] = self.auth_token @@ -221,7 +221,7 @@ def _cs_request(self, url, method, **kwargs): method, **kwargs) return resp, body except exceptions.Unauthorized: - raise ex + raise e def get(self, url, **kwargs): return self._cs_request(url, 'GET', **kwargs) @@ -259,13 +259,13 @@ def _extract_service_catalog(self, url, resp, body, extract_token=True): self.management_url = management_url.rstrip('/') return None except exceptions.AmbiguousEndpoints: - print "Found more than one valid endpoint. Use a more " \ - "restrictive filter" + print("Found more than one valid endpoint. Use a more " + "restrictive filter") raise except KeyError: raise exceptions.AuthorizationFailure() except exceptions.EndpointNotFound: - print "Could not find any suitable endpoint. Correct region?" + print("Could not find any suitable endpoint. Correct region?") raise elif resp.status_code == 305: diff --git a/novaclient/openstack/common/setup.py b/novaclient/openstack/common/setup.py index e6f72f034..889276f9b 100644 --- a/novaclient/openstack/common/setup.py +++ b/novaclient/openstack/common/setup.py @@ -176,7 +176,7 @@ def _get_git_post_version(): revno = len(out.split("\n")) sha = _run_shell_command("git describe --always") else: - tag_infos = tag_info.split("-") + tag_infos = str(tag_info).split("-") base_version = "-".join(tag_infos[:-2]) (revno, sha) = tag_infos[-2:] return "%s.%s.%s" % (base_version, revno, sha) @@ -277,7 +277,7 @@ def run(self): class LocalBuildDoc(BuildDoc): def generate_autoindex(self): - print "**Autodocumenting from %s" % os.path.abspath(os.curdir) + print("**Autodocumenting from %s" % os.path.abspath(os.curdir)) modules = {} option_dict = self.distribution.get_option_dict('build_sphinx') source_dir = os.path.join(option_dict['source_dir'][1], 'api') @@ -302,7 +302,7 @@ def generate_autoindex(self): values = dict(module=module, heading=heading, underline=underline) - print "Generating %s" % output_filename + print("Generating %s" % output_filename) with open(output_filename, 'w') as output_file: output_file.write(_rst_template % values) autoindex.write(" %s.rst\n" % module) diff --git a/novaclient/shell.py b/novaclient/shell.py index a70499390..b518f88c9 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -18,6 +18,7 @@ Command-line interface to the OpenStack Nova API. """ +from __future__ import print_function import argparse import getpass import glob @@ -708,7 +709,7 @@ def do_bash_completion(self, _args): commands.remove('bash-completion') commands.remove('bash_completion') - print ' '.join(commands | options) + print(' '.join(commands | options)) @utils.arg('command', metavar='', nargs='?', help='Display help for ') @@ -738,9 +739,9 @@ def main(): try: OpenStackComputeShell().main(sys.argv[1:]) - except Exception, e: + except Exception as e: logger.debug(e, exc_info=1) - print >> sys.stderr, "ERROR: %s" % unicode(e) + print("ERROR: %s" % unicode(e), file=sys.stderr) sys.exit(1) diff --git a/novaclient/utils.py b/novaclient/utils.py index 9d2c140ac..8c63452c7 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -162,9 +162,9 @@ def print_list(objs, fields, formatters={}, sortby_index=0): pt.add_row(row) if sortby is not None: - print pt.get_string(sortby=sortby) + print(pt.get_string(sortby=sortby)) else: - print pt.get_string() + print(pt.get_string()) def print_dict(d, dict_property="Property"): @@ -184,7 +184,7 @@ def print_dict(d, dict_property="Property"): col1 = '' else: pt.add_row([k, v]) - print pt.get_string() + print(pt.get_string()) def find_resource(manager, name_or_id): diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 6edb95848..d059e795d 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -101,7 +101,7 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): dst, src = f.split('=', 1) try: files[dst] = open(src) - except IOError, e: + except IOError as e: raise exceptions.CommandError("Can't open '%s': %s" % (src, e)) # use the os-keypair extension @@ -112,7 +112,7 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): if args.user_data: try: userdata = open(args.user_data) - except IOError, e: + except IOError as e: raise exceptions.CommandError("Can't open '%s': %s" % (args.user_data, e)) else: @@ -357,11 +357,11 @@ def print_progress(progress): if status in final_ok_states: if not silent: print_progress(100) - print "\nFinished" + print("\nFinished") break elif status == "error": if not silent: - print "\nError %(action)s instance" % locals() + print("\nError %(action)s instance" % locals()) break if not silent: @@ -518,7 +518,7 @@ def do_flavor_access_list(cs, args): try: access_list = cs.flavor_access.list(**kwargs) - except NotImplementedError, e: + except NotImplementedError as e: raise exceptions.CommandError("%s" % str(e)) columns = ['Flavor_ID', 'Tenant_ID'] @@ -1274,8 +1274,8 @@ def do_delete(cs, args): for server in args.server: try: _find_server(cs, server).delete() - except Exception, e: - print e + except Exception as e: + print(e) def _find_server(cs, server): @@ -1606,7 +1606,7 @@ def do_get_password(cs, args): """Get password for a server.""" server = _find_server(cs, args.server) data = server.get_password(args.private_key) - print data + print(data) @utils.arg('server', metavar='', help='Name or ID of server.') @@ -1629,7 +1629,7 @@ def do_console_log(cs, args): """Get console log output of a server.""" server = _find_server(cs, args.server) data = server.get_console_output(length=args.length) - print data + print(data) @utils.arg('server', metavar='', help='Name or ID of server.') @@ -2016,7 +2016,7 @@ def do_keypair_add(cs, args): try: with open(os.path.expanduser(pub_key)) as f: pub_key = f.read() - except IOError, e: + except IOError as e: raise exceptions.CommandError("Can't open or read '%s': %s" % (pub_key, e)) @@ -2024,7 +2024,7 @@ def do_keypair_add(cs, args): if not pub_key: private_key = keypair.private_key - print private_key + print(private_key) @utils.arg('name', metavar='', help='Keypair name to delete.') @@ -2095,8 +2095,8 @@ def simplify_usage(u): usage_list = cs.usage.list(start, end, detailed=True) - print "Usage from %s to %s:" % (start.strftime(dateformat), - end.strftime(dateformat)) + print("Usage from %s to %s:" % (start.strftime(dateformat), + end.strftime(dateformat))) for usage in usage_list: simplify_usage(usage) @@ -2143,14 +2143,14 @@ def simplify_usage(u): else: usage = cs.usage.get(cs.client.tenant_id, start, end) - print "Usage from %s to %s:" % (start.strftime(dateformat), - end.strftime(dateformat)) + print("Usage from %s to %s:" % (start.strftime(dateformat), + end.strftime(dateformat))) if getattr(usage, 'total_vcpus_usage', None): simplify_usage(usage) utils.print_list([usage], rows) else: - print 'None' + print('None') @utils.arg('pk_filename', @@ -2179,13 +2179,13 @@ def do_x509_create_cert(cs, args): old_umask = os.umask(0o377) with open(args.pk_filename, 'w') as private_key: private_key.write(certs.private_key) - print "Wrote private key to %s" % args.pk_filename + print("Wrote private key to %s" % args.pk_filename) finally: os.umask(old_umask) with open(args.cert_filename, 'w') as cert: cert.write(certs.data) - print "Wrote x509 certificate to %s" % args.cert_filename + print("Wrote x509 certificate to %s" % args.cert_filename) @utils.arg('filename', @@ -2202,7 +2202,7 @@ def do_x509_get_root_cert(cs, args): with open(args.filename, 'w') as cert: cacert = cs.certs.get() cert.write(cacert.data) - print "Wrote x509 root cert to %s" % args.filename + print("Wrote x509 root cert to %s" % args.filename) @utils.arg('--hypervisor', metavar='', default=None, @@ -2271,7 +2271,7 @@ def do_aggregate_create(cs, args): def do_aggregate_delete(cs, args): """Delete the aggregate by its id.""" cs.aggregates.delete(args.id) - print "Aggregate %s has been successfully deleted." % args.id + print("Aggregate %s has been successfully deleted." % args.id) @utils.arg('id', metavar='', help='Aggregate id to update.') @@ -2288,7 +2288,7 @@ def do_aggregate_update(cs, args): updates["availability_zone"] = args.availability_zone aggregate = cs.aggregates.update(args.id, updates) - print "Aggregate %s has been successfully updated." % args.id + print("Aggregate %s has been successfully updated." % args.id) _print_aggregate_details(aggregate) @@ -2303,7 +2303,7 @@ def do_aggregate_set_metadata(cs, args): """Update the metadata associated with the aggregate.""" metadata = _extract_metadata(args) aggregate = cs.aggregates.set_metadata(args.id, metadata) - print "Aggregate %s has been successfully updated." % args.id + print("Aggregate %s has been successfully updated." % args.id) _print_aggregate_details(aggregate) @@ -2312,7 +2312,7 @@ def do_aggregate_set_metadata(cs, args): def do_aggregate_add_host(cs, args): """Add the host to the specified aggregate.""" aggregate = cs.aggregates.add_host(args.id, args.host) - print "Aggregate %s has been successfully updated." % args.id + print("Aggregate %s has been successfully updated." % args.id) _print_aggregate_details(aggregate) @@ -2322,7 +2322,7 @@ def do_aggregate_add_host(cs, args): def do_aggregate_remove_host(cs, args): """Remove the specified host from the specified aggregate.""" aggregate = cs.aggregates.remove_host(args.id, args.host) - print "Aggregate %s has been successfully updated." % args.id + print("Aggregate %s has been successfully updated." % args.id) _print_aggregate_details(aggregate) @@ -2478,13 +2478,13 @@ def do_host_action(cs, args): def do_coverage_start(cs, args): """Start Nova coverage reporting""" cs.coverage.start(combine=args.combine) - print "Coverage collection started" + print("Coverage collection started") def do_coverage_stop(cs, args): """Stop Nova coverage reporting""" out = cs.coverage.stop() - print "Coverage data file path: %s" % out[-1]['path'] + print("Coverage data file path: %s" % out[-1]['path']) @utils.arg('filename', metavar='', help='report filename') @@ -2504,7 +2504,7 @@ def do_coverage_report(cs, args): raise exceptions.CommandError("--html and --xml must not be " "specified together.") cov = cs.coverage.report(args.filename, xml=args.xml, html=args.html) - print "Report path: %s" % cov[-1]['path'] + print("Report path: %s" % cov[-1]['path']) @utils.arg('--matching', metavar='', default=None, @@ -2640,8 +2640,8 @@ def do_ssh(cs, args): version = 6 if args.ipv6 else 4 if address_type not in addresses: - print "ERROR: No %s addresses found for '%s'." % (address_type, - args.server) + print("ERROR: No %s addresses found for '%s'." % (address_type, + args.server)) return ip_address = None @@ -2658,8 +2658,8 @@ def do_ssh(cs, args): args.extra)) else: pretty_version = "IPv%d" % version - print "ERROR: No %s %s address found." % (address_type, - pretty_version) + print("ERROR: No %s %s address found." % (address_type, + pretty_version)) return @@ -2961,7 +2961,7 @@ def do_availability_zone_list(cs, _args): """List all the availability zones.""" try: availability_zones = cs.availability_zones.list() - except exceptions.Forbidden, e: # policy doesn't allow probably + except exceptions.Forbidden as e: # policy doesn't allow probably try: availability_zones = cs.availability_zones.list(detailed=False) except: diff --git a/tests/fakes.py b/tests/fakes.py index 248214ff0..6ff4b8db5 100644 --- a/tests/fakes.py +++ b/tests/fakes.py @@ -57,9 +57,9 @@ def assert_called_anytime(self, method, url, body=None): try: assert entry[2] == body except AssertionError: - print entry[2] - print "!=" - print body + print(entry[2]) + print("!=") + print(body) raise self.client.callstack = [] diff --git a/tests/v1_1/contrib/test_tenant_networks.py b/tests/v1_1/contrib/test_tenant_networks.py index 0df914edc..4be6deeef 100644 --- a/tests/v1_1/contrib/test_tenant_networks.py +++ b/tests/v1_1/contrib/test_tenant_networks.py @@ -38,7 +38,7 @@ def test_list_tenant_networks(self): def test_get_tenant_network(self): net = cs.tenant_networks.get(1) cs.assert_called('GET', '/os-tenant-networks/1') - print net + print(net) def test_create_tenant_networks(self): cs.tenant_networks.create(label="net", diff --git a/tools/install_venv.py b/tools/install_venv.py index 6cb3a6938..953e74bff 100644 --- a/tools/install_venv.py +++ b/tools/install_venv.py @@ -22,6 +22,7 @@ Installation script for Nova's development virtualenv """ +from __future__ import print_function import optparse import os import subprocess @@ -37,7 +38,7 @@ def die(message, *args): - print >> sys.stderr, message % args + print(message % args, file=sys.stderr) sys.exit(1) @@ -77,12 +78,12 @@ def install_virtualenv(self): return if self.check_cmd('easy_install'): - print 'Installing virtualenv via easy_install...', + print('Installing virtualenv via easy_install...') if run_command(['easy_install', 'virtualenv']): - print 'Succeeded' + print('Succeeded') return else: - print 'Failed' + print('Failed') die('ERROR: virtualenv not found.\n\nDevelopment' ' requires virtualenv, please install it using your' @@ -162,17 +163,17 @@ def create_virtualenv(venv=VENV, no_site_packages=True): """Creates the virtual environment and installs PIP only into the virtual environment """ - print 'Creating venv...', + print('Creating venv...') if no_site_packages: run_command(['virtualenv', '-q', '--no-site-packages', VENV]) else: run_command(['virtualenv', '-q', VENV]) - print 'done.' - print 'Installing pip in virtualenv...', + print('done.') + print('Installing pip in virtualenv...') if not run_command(['tools/with_venv.sh', 'easy_install', 'pip>1.0']).strip(): die("Failed to install pip.") - print 'done.' + print('done.') def pip_install(*args): @@ -182,7 +183,7 @@ def pip_install(*args): def install_dependencies(venv=VENV): - print 'Installing dependencies with pip (this can take a while)...' + print('Installing dependencies with pip (this can take a while)...') # First things first, make sure our venv has the latest pip and distribute. pip_install('pip') @@ -221,7 +222,7 @@ def print_help(): Also, make test will automatically use the virtualenv. """ - print help + print(help) def parse_args(): From 34c8a6bb7ad605b107a6f89b7018284811b5fadf Mon Sep 17 00:00:00 2001 From: Davanum Srinivas Date: Thu, 21 Feb 2013 11:41:02 -0500 Subject: [PATCH 0094/1705] Missing import for gnomekeyring follow on to previous attempt to fix to bug 1116302, looks like we missed an import Fixes LP#1116302 Change-Id: If56e3cedaa63a594907bb851a2701bd64806ed85 --- novaclient/shell.py | 1 + 1 file changed, 1 insertion(+) diff --git a/novaclient/shell.py b/novaclient/shell.py index a70499390..b0e45f54c 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -35,6 +35,7 @@ HAS_KEYRING = True try: if isinstance(keyring.get_keyring(), keyring.backend.GnomeKeyring): + import gnomekeyring KeyringIOError = gnomekeyring.IOError else: KeyringIOError = IOError From 8be810b59cb5877d5324f706abf9e6941b5603e3 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Fri, 22 Feb 2013 18:15:08 +0000 Subject: [PATCH 0095/1705] Fix how tests were failing due to missing attributes. Fixes: bug #1131845 For unknown reasons all tests continued to pass even after this change was introduced, besides that being a very odd thing to have happen it is better to fix said attributes for those that are running the tests individually in there own CI pipelines. Change-Id: Iee6acc1d4a528afb840459ca47d754ec7bae8f32 --- tests/v1_1/fakes.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/v1_1/fakes.py b/tests/v1_1/fakes.py index 542c63c2f..00b0185ca 100644 --- a/tests/v1_1/fakes.py +++ b/tests/v1_1/fakes.py @@ -40,6 +40,17 @@ def __init__(self, **kwargs): self.auth_url = 'auth_url' self.tenant_id = 'tenant_id' self.callstack = [] + self.projectid = 'projectid' + self.user = 'user' + self.region_name = 'region_name' + self.endpoint_type = 'endpoint_type' + self.service_type = 'service_type' + self.service_name = 'service_name' + self.volume_service_name = 'volume_service_name' + self.timings = 'timings' + self.bypass_url = 'bypass_url' + self.os_cache = 'os_cache' + self.http_log_debug = 'http_log_debug' def _cs_request(self, url, method, **kwargs): # Check that certain things are called correctly From e043bd1560e6f2f071870c2be09748163fe70b97 Mon Sep 17 00:00:00 2001 From: Gordon Chung Date: Fri, 22 Feb 2013 13:33:40 -0500 Subject: [PATCH 0096/1705] Accept 201 status code on POST On POST requests, keystone should return '201 Created' according to api docs. Related to but not dependent on: https://bugs.launchpad.net/keystone/+bug/1131119 Change-Id: I04c398ad7946f5ddcfd2059e4f8d97608e5d0ed3 --- novaclient/client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/novaclient/client.py b/novaclient/client.py index 763eec271..1ebfaeb05 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -240,7 +240,8 @@ def _extract_service_catalog(self, url, resp, body, extract_token=True): We may get redirected to another site, fail or actually get back a service catalog with a token and our endpoints.""" - if resp.status_code == 200: # content must always present + # content must always present + if resp.status_code == 200 or resp.status_code == 201: try: self.auth_url = url self.service_catalog = \ From aac5767894c81b69dc40337a7ac61a5072eeb42c Mon Sep 17 00:00:00 2001 From: Rami Vaknin Date: Sat, 23 Feb 2013 16:23:27 +0200 Subject: [PATCH 0097/1705] Update the docstring of cloudpipe-configure command Change-Id: Iab820200664618769f11b2d21a3731ef6b81a877 --- novaclient/v1_1/shell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 260581115..a6518077b 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -322,7 +322,7 @@ def do_cloudpipe_create(cs, args): @utils.arg('address', metavar='', help='New IP Address.') @utils.arg('port', metavar='', help='New Port.') def do_cloudpipe_configure(cs, args): - """Create a cloudpipe instance for the given project""" + """Update the VPN IP/port of a cloudpipe instance""" cs.cloudpipe.update(args.address, args.port) From feaff79919bc2d99cb190fedf140c3c30db2ba99 Mon Sep 17 00:00:00 2001 From: Alex Glikson Date: Mon, 25 Feb 2013 06:35:27 +0200 Subject: [PATCH 0098/1705] Fixes the output of 'os-evacuate' command. Change-Id: I5f02e66f316acdd10df42bec3f6628a8efe5fd97 Fixes: bug #1132790 --- novaclient/v1_1/shell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 260581115..bb36abaac 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2909,7 +2909,7 @@ def do_evacuate(cs, args): """Evacuate server from failed host to specified one.""" server = _find_server(cs, args.server) - res = server.evacuate(args.host, args.on_shared_storage, args.password)[0] + res = server.evacuate(args.host, args.on_shared_storage, args.password)[1] if type(res) is dict: utils.print_dict(res) From 9ab600fb8030183cd58e40b2f0ac6d9fd74e46cb Mon Sep 17 00:00:00 2001 From: Vasyl Khomenko Date: Fri, 22 Feb 2013 12:59:06 -0500 Subject: [PATCH 0099/1705] Make ip_protocol parameter in security groups rules case insensitive Added convert ip_protocol to uppercase. Added tests Fixes: bug #1052725 Change-Id: I966ddad0800556b1cc848003d07d4a897ce1b9c1 --- novaclient/v1_1/shell.py | 9 +++---- tests/fakes.py | 4 +++- tests/v1_1/fakes.py | 51 ++++++++++++++++++++++++++++++---------- tests/v1_1/test_shell.py | 27 +++++++++++++++++++++ 4 files changed, 74 insertions(+), 17 deletions(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index a6518077b..dd4b89431 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1880,7 +1880,7 @@ def do_secgroup_delete_rule(cs, args): secgroup = _get_secgroup(cs, args.secgroup) for rule in secgroup.rules: - if (rule['ip_protocol'] == args.ip_proto and + if (rule['ip_protocol'].upper() == args.ip_proto.upper() and rule['from_port'] == int(args.from_port) and rule['to_port'] == int(args.to_port) and rule['ip_range']['cidr'] == args.cidr): @@ -1953,7 +1953,7 @@ def do_secgroup_add_group_rule(cs, args): if not (args.ip_proto and args.from_port and args.to_port): raise exceptions.CommandError("ip_proto, from_port, and to_port" " must be specified together") - params['ip_protocol'] = args.ip_proto + params['ip_protocol'] = args.ip_proto.upper() params['from_port'] = args.from_port params['to_port'] = args.to_port @@ -1985,12 +1985,13 @@ def do_secgroup_delete_group_rule(cs, args): if not (args.ip_proto and args.from_port and args.to_port): raise exceptions.CommandError("ip_proto, from_port, and to_port" " must be specified together") - params['ip_protocol'] = args.ip_proto + params['ip_protocol'] = args.ip_proto.upper() params['from_port'] = int(args.from_port) params['to_port'] = int(args.to_port) for rule in secgroup.rules: - if (rule.get('ip_protocol') == params.get('ip_protocol') and + if (rule.get('ip_protocol').upper() == params.get( + 'ip_protocol').upper() and rule.get('from_port') == params.get('from_port') and rule.get('to_port') == params.get('to_port') and rule.get('group', {}).get('name') == diff --git a/tests/fakes.py b/tests/fakes.py index 248214ff0..805f81873 100644 --- a/tests/fakes.py +++ b/tests/fakes.py @@ -34,7 +34,9 @@ def assert_called(self, method, url, body=None, pos=-1): (expected + called) if body is not None: - assert self.client.callstack[pos][2] == body + if self.client.callstack[pos][2] != body: + raise AssertionError('%r != %r' % + (self.client.callstack[pos][2], body)) def assert_called_anytime(self, method, url, body=None): """ diff --git a/tests/v1_1/fakes.py b/tests/v1_1/fakes.py index 542c63c2f..ab1da609e 100644 --- a/tests/v1_1/fakes.py +++ b/tests/v1_1/fakes.py @@ -1061,18 +1061,39 @@ def put_os_quota_class_sets_97f4c221bff44578b0300df4ef119353(self, # Security Groups # def get_os_security_groups(self, **kw): - return (200, {}, - {"security_groups": [ - dict(description="FAKE_SECURITY_GROUP", id=1, name="test", - rules=[{ - 'id': 1, - 'parent_group_id': 1, - 'group_id': 2, - 'ip_protocol': 'TCP', - 'from_port': 22, - 'to_port': 22, - 'cidr': '10.0.0.0/8'}], - tenant_id='4ffc664c198e435e9853f2538fbcd7a7')]}) + return (200, {}, {"security_groups": [ + {"name": "test", + "description": "FAKE_SECURITY_GROUP", + "tenant_id": + "4ffc664c198e435e9853f2538fbcd7a7", + "id": 1, + "rules": [ + {"id": 11, + "group": {}, + "ip_protocol": "TCP", + "from_port": 22, + "to_port": 22, + "parent_group_id": 1, + "ip_range": + {"cidr": "10.0.0.0/8"}}, + {"id": 12, + "group": { + "tenant_id": + "272bee4c1e624cd4a72a6b0ea55b4582", + "name": "test2"}, + + "ip_protocol": "TCP", + "from_port": 222, + "to_port": 222, + "parent_group_id": 1, + "ip_range": {}}]}, + {"name": "test2", + "description": "FAKE_SECURITY_GROUP2", + "tenant_id": "272bee4c1e624cd4a72a6b0ea55b4582", + "id": 2, + "rules": []} + ]} + ) def get_os_security_groups_1(self, **kw): return (200, {}, {"security_group": @@ -1103,6 +1124,12 @@ def get_os_security_group_rules(self, **kw): def delete_os_security_group_rules_1(self, **kw): return (202, {}, None) + def delete_os_security_group_rules_11(self, **kw): + return (202, {}, None) + + def delete_os_security_group_rules_12(self, **kw): + return (202, {}, None) + def post_os_security_group_rules(self, body, **kw): assert body.keys() == ['security_group_rule'] fakes.assert_has_keys(body['security_group_rule'], diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index a57359ee8..a0e9d1612 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -1063,6 +1063,33 @@ def test_security_group_add_rule(self): 'cidr': '10.0.0.0/8', 'group_id': None}}) + def test_security_group_delete_rule(self): + self.run_command('secgroup-delete-rule test TCP 22 22 10.0.0.0/8') + self.assert_called('DELETE', '/os-security-group-rules/11') + + def test_security_group_delete_rule_protocol_case(self): + self.run_command('secgroup-delete-rule test tcp 22 22 10.0.0.0/8') + self.assert_called('DELETE', '/os-security-group-rules/11') + + def test_security_group_add_group_rule(self): + self.run_command('secgroup-add-group-rule test test2 tcp 22 22') + self.assert_called('POST', '/os-security-group-rules', + {'security_group_rule': + {'from_port': '22', + 'ip_protocol': 'TCP', + 'to_port': '22', + 'parent_group_id': 1, + 'cidr': None, + 'group_id': 2}}) + + def test_security_group_delete_group_rule(self): + self.run_command('secgroup-delete-group-rule test test2 TCP 222 222') + self.assert_called('DELETE', '/os-security-group-rules/12') + + def test_security_group_delete_group_rule_protocol_case(self): + self.run_command('secgroup-delete-group-rule test test2 tcp 222 222') + self.assert_called('DELETE', '/os-security-group-rules/12') + def test_security_group_list_rules(self): self.run_command('secgroup-list-rules test') self.assert_called('GET', '/os-security-groups') From 8ac304f7f577ffe7d0264e1248e4ae87bd74fc65 Mon Sep 17 00:00:00 2001 From: Vasyl Khomenko Date: Thu, 21 Feb 2013 11:15:50 -0500 Subject: [PATCH 0100/1705] Fixed bug with password prompt, added tests Fixes: bug #1131237 Change-Id: Ifcd9543ed5ac9526e32025ff3acd51d36b27a224 --- novaclient/shell.py | 2 +- tests/test_shell.py | 102 +++++++++++++++++++++++++++++++++++++++----- 2 files changed, 93 insertions(+), 11 deletions(-) diff --git a/novaclient/shell.py b/novaclient/shell.py index 2a2d09e4f..946a36872 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -143,7 +143,7 @@ def password(self): return self.args.os_password if self._validate_string(self.args.apikey): return self.args.apikey - verify_pass = utils.bool_from_str(utils.getenv("OS_VERIFY_PASSWORD")) + verify_pass = utils.bool_from_str(utils.env("OS_VERIFY_PASSWORD")) return self._prompt_password(verify_pass) @property diff --git a/tests/test_shell.py b/tests/test_shell.py index d35814dc0..d08a475f4 100644 --- a/tests/test_shell.py +++ b/tests/test_shell.py @@ -3,27 +3,35 @@ import sys import fixtures +import mock from testtools import matchers +import novaclient.client from novaclient import exceptions import novaclient.shell from tests import utils +FAKE_ENV = {'OS_USERNAME': 'username', + 'OS_PASSWORD': 'password', + 'OS_TENANT_NAME': 'tenant_name', + 'OS_AUTH_URL': 'http://no.where'} + class ShellTest(utils.TestCase): - FAKE_ENV = { - 'OS_USERNAME': 'username', - 'OS_PASSWORD': 'password', - 'OS_TENANT_NAME': 'tenant_name', - 'OS_AUTH_URL': 'http://no.where', - } + def make_env(self, exclude=None): + for var, val in FAKE_ENV.items(): + if var == exclude: + continue + self.useFixture(fixtures.EnvironmentVariable(var, val)) def setUp(self): super(ShellTest, self).setUp() - for var in self.FAKE_ENV: - self.useFixture(fixtures.EnvironmentVariable(var, - self.FAKE_ENV[var])) + self.useFixture(fixtures.MonkeyPatch( + 'novaclient.client.get_client_class', + mock.MagicMock)) + self.nc_util = mock.patch('novaclient.utils.isunauthenticated').start() + self.nc_util.return_value = False def shell(self, argstr, exitcodes=(0,)): orig = sys.stdout @@ -43,7 +51,6 @@ def shell(self, argstr, exitcodes=(0,)): stderr = sys.stderr.getvalue() sys.stderr.close() sys.stderr = orig_stderr - return (stdout, stderr) def test_help_unknown_command(self): @@ -80,3 +87,78 @@ def test_help_on_subcommand(self): for r in required: self.assertThat((stdout + stderr), matchers.MatchesRegex(r, re.DOTALL | re.MULTILINE)) + + def test_help_no_options(self): + required = [ + '.*?^usage: ', + '.*?^\s+root-password\s+Change the root password', + '.*?^See "nova help COMMAND" for help on a specific command', + ] + stdout, stderr = self.shell('') + for r in required: + self.assertThat((stdout + stderr), + matchers.MatchesRegex(r, re.DOTALL | re.MULTILINE)) + + def test_bash_completion(self): + stdout, stderr = self.shell('bash-completion') + # just check we have some output + required = ['--matching help secgroup-delete-rule --priority'] + for r in required: + self.assertThat((stdout + stderr), + matchers.MatchesRegex(r, re.DOTALL | re.MULTILINE)) + + def test_no_username(self): + required = ('You must provide a username' + ' via either --os-username or env[OS_USERNAME]',) + self.make_env(exclude='OS_USERNAME') + try: + self.shell('list') + except exceptions.CommandError, message: + self.assertEqual(required, message.args) + else: + self.fail('CommandError not raised') + + def test_no_tenant_name(self): + required = ('You must provide a tenant name' + ' via either --os-tenant-name or env[OS_TENANT_NAME]',) + self.make_env(exclude='OS_TENANT_NAME') + try: + self.shell('list') + except exceptions.CommandError, message: + self.assertEqual(required, message.args) + else: + self.fail('CommandError not raised') + + def test_no_auth_url(self): + required = ('You must provide an auth url' + ' via either --os-auth-url or env[OS_AUTH_URL] or' + ' specify an auth_system which defines a default url' + ' with --os-auth-system or env[OS_AUTH_SYSTEM',) + self.make_env(exclude='OS_AUTH_URL') + try: + self.shell('list') + except exceptions.CommandError, message: + self.assertEqual(required, message.args) + else: + self.fail('CommandError not raised') + + @mock.patch('sys.stdin', side_effect=mock.MagicMock) + @mock.patch('getpass.getpass', return_value='password') + def test_password(self, mock_getpass, mock_stdin): + self.make_env(exclude='OS_PASSWORD') + stdout, stderr = self.shell('list') + self.assertEqual((stdout + stderr), '\n') + + @mock.patch('sys.stdin', side_effect=mock.MagicMock) + @mock.patch('getpass.getpass', side_effect=EOFError) + def test_no_password(self, mock_getpass, mock_stdin): + required = ('Expecting a password provided' + ' via either --os-password, env[OS_PASSWORD],' + ' or prompted response',) + self.make_env(exclude='OS_PASSWORD') + try: + self.shell('list') + except exceptions.CommandError, message: + self.assertEqual(required, message.args) + else: + self.fail('CommandError not raised') From 47e6bc25ae647c9b83b394cfc5734490855acbe2 Mon Sep 17 00:00:00 2001 From: Flaper Fesp Date: Mon, 4 Mar 2013 18:11:54 +0100 Subject: [PATCH 0101/1705] Decodes input and encodes output Currently novaclient doesn't handle properly incoming and outgoing encode / decode process. As a solution for this, this patch implements a decoding process for all data incoming from the user side and decodes everything going out of the client, i.e: http requests, prints, etc. This patch introduces a new module (strutils.py) taken from oslo-incubator in order to use 2 of the functions present in it: About safe_(decode|encode): Both functions try to encode / decode the incoming text using the stdin encoding, fallback to python's default encoding if that returns None or to UTF-8 as the last option. In both functions only basestring objects are accepted and they both raise TypeError if an object of another type is passed. About the general novaclient changes: In order to better support non-ASCII characters, it is a good practice to use unicode interanlly and encode everything that has to go out. This patch aims to do that and introduces this behaviour in the client. Testing: A good test (besides using tox) is to use nova client with and without setting any locale (export LANG=). Fixes bug: 1061156 Change-Id: I20b75e42b0c3dac89f1048faa1127253a64f86c7 --- novaclient/openstack/common/strutils.py | 133 ++++++++++++++++++++++++ novaclient/shell.py | 5 +- novaclient/utils.py | 13 +-- novaclient/v1_1/base.py | 5 +- openstack-common.conf | 2 +- 5 files changed, 147 insertions(+), 11 deletions(-) create mode 100644 novaclient/openstack/common/strutils.py diff --git a/novaclient/openstack/common/strutils.py b/novaclient/openstack/common/strutils.py new file mode 100644 index 000000000..7813b6422 --- /dev/null +++ b/novaclient/openstack/common/strutils.py @@ -0,0 +1,133 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# 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. + +""" +System-level utilities and helper functions. +""" + +import logging +import sys + +LOG = logging.getLogger(__name__) + + +def int_from_bool_as_string(subject): + """ + Interpret a string as a boolean and return either 1 or 0. + + Any string value in: + + ('True', 'true', 'On', 'on', '1') + + is interpreted as a boolean True. + + Useful for JSON-decoded stuff and config file parsing + """ + return bool_from_string(subject) and 1 or 0 + + +def bool_from_string(subject): + """ + Interpret a string as a boolean. + + Any string value in: + + ('True', 'true', 'On', 'on', 'Yes', 'yes', '1') + + is interpreted as a boolean True. + + Useful for JSON-decoded stuff and config file parsing + """ + if isinstance(subject, bool): + return subject + if isinstance(subject, basestring): + if subject.strip().lower() in ('true', 'on', 'yes', '1'): + return True + return False + + +def safe_decode(text, incoming=None, errors='strict'): + """ + Decodes incoming str using `incoming` if they're + not already unicode. + + :param incoming: Text's current encoding + :param errors: Errors handling policy. See here for valid + values http://docs.python.org/2/library/codecs.html + :returns: text or a unicode `incoming` encoded + representation of it. + :raises TypeError: If text is not an isntance of basestring + """ + if not isinstance(text, basestring): + raise TypeError("%s can't be decoded" % type(text)) + + if isinstance(text, unicode): + return text + + if not incoming: + incoming = (sys.stdin.encoding or + sys.getdefaultencoding()) + + try: + return text.decode(incoming, errors) + except UnicodeDecodeError: + # Note(flaper87) If we get here, it means that + # sys.stdin.encoding / sys.getdefaultencoding + # didn't return a suitable encoding to decode + # text. This happens mostly when global LANG + # var is not set correctly and there's no + # default encoding. In this case, most likely + # python will use ASCII or ANSI encoders as + # default encodings but they won't be capable + # of decoding non-ASCII characters. + # + # Also, UTF-8 is being used since it's an ASCII + # extension. + return text.decode('utf-8', errors) + + +def safe_encode(text, incoming=None, + encoding='utf-8', errors='strict'): + """ + Encodes incoming str/unicode using `encoding`. If + incoming is not specified, text is expected to + be encoded with current python's default encoding. + (`sys.getdefaultencoding`) + + :param incoming: Text's current encoding + :param encoding: Expected encoding for text (Default UTF-8) + :param errors: Errors handling policy. See here for valid + values http://docs.python.org/2/library/codecs.html + :returns: text or a bytestring `encoding` encoded + representation of it. + :raises TypeError: If text is not an isntance of basestring + """ + if not isinstance(text, basestring): + raise TypeError("%s can't be encoded" % type(text)) + + if not incoming: + incoming = (sys.stdin.encoding or + sys.getdefaultencoding()) + + if isinstance(text, unicode): + return text.encode(encoding, errors) + elif text and encoding != incoming: + # Decode text before encoding it with `encoding` + text = safe_decode(text, incoming, errors) + return text.encode(encoding, errors) + + return text diff --git a/novaclient/shell.py b/novaclient/shell.py index 2a2d09e4f..4ed2db357 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -49,6 +49,7 @@ from novaclient import client from novaclient import exceptions as exc import novaclient.extension +from novaclient.openstack.common import strutils from novaclient import utils from novaclient.v1_1 import shell as shell_v1_1 @@ -738,11 +739,11 @@ def start_section(self, heading): def main(): try: - OpenStackComputeShell().main(sys.argv[1:]) + OpenStackComputeShell().main(map(strutils.safe_decode, sys.argv[1:])) except Exception as e: logger.debug(e, exc_info=1) - print("ERROR: %s" % unicode(e), file=sys.stderr) + print("ERROR: %s" % strutils.safe_encode(unicode(e)), file=sys.stderr) sys.exit(1) diff --git a/novaclient/utils.py b/novaclient/utils.py index 8c63452c7..49e97f80a 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -6,6 +6,7 @@ import prettytable from novaclient import exceptions +from novaclient.openstack.common import strutils def arg(*args, **kwargs): @@ -139,7 +140,7 @@ def pretty_choice_list(l): def print_list(objs, fields, formatters={}, sortby_index=0): - if sortby_index == None: + if sortby_index is None: sortby = None else: sortby = fields[sortby_index] @@ -162,9 +163,9 @@ def print_list(objs, fields, formatters={}, sortby_index=0): pt.add_row(row) if sortby is not None: - print(pt.get_string(sortby=sortby)) + print(strutils.safe_encode(pt.get_string(sortby=sortby))) else: - print(pt.get_string()) + print(strutils.safe_encode(pt.get_string())) def print_dict(d, dict_property="Property"): @@ -184,7 +185,7 @@ def print_dict(d, dict_property="Property"): col1 = '' else: pt.add_row([k, v]) - print(pt.get_string()) + print(strutils.safe_encode(pt.get_string())) def find_resource(manager, name_or_id): @@ -203,9 +204,9 @@ def find_resource(manager, name_or_id): # now try to get entity as uuid try: - uuid.UUID(str(name_or_id)) + uuid.UUID(strutils.safe_encode(name_or_id)) return manager.get(name_or_id) - except (ValueError, exceptions.NotFound): + except (TypeError, ValueError, exceptions.NotFound): pass # for str id which is not uuid (for Flavor search currently) diff --git a/novaclient/v1_1/base.py b/novaclient/v1_1/base.py index d2f384287..fce840b1b 100644 --- a/novaclient/v1_1/base.py +++ b/novaclient/v1_1/base.py @@ -18,6 +18,7 @@ import base64 from novaclient import base +from novaclient.openstack.common import strutils # FIXME(sirp): Now that v1_0 has been removed, this can be merged with @@ -72,8 +73,8 @@ def _boot(self, resource_url, response_key, name, image, flavor, if userdata: if hasattr(userdata, 'read'): userdata = userdata.read() - elif isinstance(userdata, unicode): - userdata = userdata.encode('utf-8') + + userdata = strutils.safe_encode(userdata) body["server"]["user_data"] = base64.b64encode(userdata) if meta: body["server"]["metadata"] = meta diff --git a/openstack-common.conf b/openstack-common.conf index 76f08056e..833c69a16 100644 --- a/openstack-common.conf +++ b/openstack-common.conf @@ -1,7 +1,7 @@ [DEFAULT] # The list of modules to copy from openstack-common -modules=setup,timeutils +modules=setup,timeutils,strutils # The base module to hold the copy of openstack.common base=novaclient From 650f1c3d5343c2997c9789e9c2d924e4507c8f32 Mon Sep 17 00:00:00 2001 From: Vasyl Khomenko Date: Wed, 6 Mar 2013 07:08:22 -0500 Subject: [PATCH 0102/1705] Extend test coverage for v1_1/shell.py Added unit tests for shell code, that wasn't covered before. Implements: blueprint python-novaclient-unittests Change-Id: Id9973b1edb39ab76e7a232c262ae073cf44dfc0a --- tests/v1_1/fakes.py | 63 +++++++++++++++ tests/v1_1/test_shell.py | 162 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 218 insertions(+), 7 deletions(-) diff --git a/tests/v1_1/fakes.py b/tests/v1_1/fakes.py index 709236316..8f31cc5f1 100644 --- a/tests/v1_1/fakes.py +++ b/tests/v1_1/fakes.py @@ -474,10 +474,27 @@ def post_servers_1234_action(self, body, **kw): assert body[action] is None elif action == 'os-start': assert body[action] is None + elif action == 'start': + assert body[action] is None + elif action == 'stop': + assert body[action] is None + elif action == 'pause': + assert body[action] is None + elif action == 'unpause': + assert body[action] is None + elif action == 'lock': + assert body[action] is None + elif action == 'unlock': + assert body[action] is None elif action == 'rescue': assert body[action] is None + _body = {'Password': 'RescuePassword'} elif action == 'unrescue': assert body[action] is None + elif action == 'resume': + assert body[action] is None + elif action == 'suspend': + assert body[action] is None elif action == 'lock': assert body[action] is None elif action == 'unlock': @@ -1630,3 +1647,49 @@ def post_servers_1234_os_interface(self, **kw): def delete_servers_1234_os_interface_port_id(self, **kw): return (200, {}, None) + + # NOTE (vkhomenko): + # Volume responses was taken from: + # https://wiki.openstack.org/wiki/CreateVolumeFromImage + # http://jorgew.github.com/block-storage-api/content/ + # GET_listDetailVolumes_v1__tenantId__volumes_detail_.html + # I suppose they are outdated and should be updated after Cinder released + + def get_volumes_detail(self, **kw): + return (200, {}, {"volumes": [ + {"display_name": "Work", + "display_description": "volume for work", + "status": "ATTACHED", + "id": "15e59938-07d5-11e1-90e3-e3dffe0c5983", + "created_at": "2011-09-09T00:00:00Z", + "attached": "2011-11-11T00:00:00Z", + "size": 1024, + "attachments": [ + {"id": "3333", + "links": ''}], + "metadata": {}}]}) + + def post_volumes(self, **kw): + return (200, {}, {"volume": + {"status": "creating", + "display_name": "vol-007", + "attachments": [(0)], + "availability_zone": "cinder", + "created_at": "2012-08-13T10:57:17.000000", + "display_description": "create volume from image", + "image_id": "f4cf905f-7c58-4d7b-8314-8dd8a2d1d483", + "volume_type": "None", + "metadata": {}, + "id": "5cb239f6-1baf-4fe1-bd78-c852cf00fa39", + "size": 1}}) + + def delete_volumes_15e59938_07d5_11e1_90e3_e3dffe0c5983(self, **kw): + return (200, {}, {}) + + def post_servers_1234_os_volume_attachments(self, **kw): + return (200, {}, {"volumeAttachment": + {"device": "/dev/vdb", + "volumeId": 2}}) + + def delete_servers_1234_os_volume_attachments_Work(self, **kw): + return (200, {}, {}) diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index 5bb954f0f..80ced9e26 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -125,6 +125,20 @@ def test_boot(self): }}, ) + def test_boot_multiple(self): + self.run_command('boot --flavor 1 --image 1' + ' --num-instances 3 some-server') + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': '1', + 'min_count': 1, + 'max_count': 3, + }}, + ) + def test_boot_image_with(self): self.run_command("boot --flavor 1" " --image-with test_key=test_value some-server") @@ -464,6 +478,14 @@ def test_list(self): self.run_command('list') self.assert_called('GET', '/servers/detail') + def test_list_with_images(self): + self.run_command('list --image 1') + self.assert_called('GET', '/servers/detail?image=1') + + def test_list_with_flavors(self): + self.run_command('list --flavor 1') + self.assert_called('GET', '/servers/detail?flavor=1') + @mock.patch('sys.stdout', StringIO.StringIO()) def test_list_fields(self): self.run_command('list --fields ' @@ -484,17 +506,63 @@ def test_reboot(self): def test_rebuild(self): self.run_command('rebuild sample-server 1') - # XXX need a way to test multiple calls - # self.assert_called('POST', '/servers/1234/action', - # {'rebuild': {'imageRef': 1}}) + self.assert_called('POST', '/servers/1234/action', + {'rebuild': {'imageRef': 1}}, pos=-4) + self.assert_called('GET', '/servers/detail', pos=-3) + self.assert_called('GET', '/flavors/1', pos=-2) self.assert_called('GET', '/images/2') self.run_command('rebuild sample-server 1 --rebuild-password asdf') - # XXX need a way to test multiple calls - # self.assert_called('POST', '/servers/1234/action', - # {'rebuild': {'imageRef': 1, 'adminPass': 'asdf'}}) + self.assert_called('POST', '/servers/1234/action', + {'rebuild': {'imageRef': 1, 'adminPass': 'asdf'}}, + pos=-4) + self.assert_called('GET', '/flavors/1', pos=-2) self.assert_called('GET', '/images/2') + def test_start(self): + self.run_command('start sample-server') + self.assert_called('POST', '/servers/1234/action', {'os-start': None}) + + def test_stop(self): + self.run_command('stop sample-server') + self.assert_called('POST', '/servers/1234/action', {'os-stop': None}) + + def test_pause(self): + self.run_command('pause sample-server') + self.assert_called('POST', '/servers/1234/action', {'pause': None}) + + def test_unpause(self): + self.run_command('unpause sample-server') + self.assert_called('POST', '/servers/1234/action', {'unpause': None}) + + def test_lock(self): + self.run_command('lock sample-server') + self.assert_called('POST', '/servers/1234/action', {'lock': None}) + + def test_unlock(self): + self.run_command('unlock sample-server') + self.assert_called('POST', '/servers/1234/action', {'unlock': None}) + + def test_suspend(self): + self.run_command('suspend sample-server') + self.assert_called('POST', '/servers/1234/action', {'suspend': None}) + + def test_resume(self): + self.run_command('resume sample-server') + self.assert_called('POST', '/servers/1234/action', {'resume': None}) + + def test_rescue(self): + self.run_command('rescue sample-server') + self.assert_called('POST', '/servers/1234/action', {'rescue': None}) + + def test_unrescue(self): + self.run_command('unrescue sample-server') + self.assert_called('POST', '/servers/1234/action', {'unrescue': None}) + + def test_migrate(self): + self.run_command('migrate sample-server') + self.assert_called('POST', '/servers/1234/action', {'migrate': None}) + def test_rename(self): self.run_command('rename sample-server newname') self.assert_called('PUT', '/servers/1234', @@ -620,6 +688,18 @@ def test_dns_domains(self): self.run_command('dns-domains') self.assert_called('GET', '/os-floating-ip-dns') + def test_floating_ip_list(self): + self.run_command('floating-ip-list') + self.assert_called('GET', '/os-floating-ips') + + def test_floating_ip_create(self): + self.run_command('floating-ip-create') + self.assert_called('GET', '/os-floating-ips/1') + + def test_floating_ip_delete(self): + self.run_command('floating-ip-delete 11.0.0.1') + self.assert_called('DELETE', '/os-floating-ips/1') + def test_floating_ip_bulk_list(self): self.run_command('floating-ip-bulk-list') self.assert_called('GET', '/os-floating-ips-bulk') @@ -644,6 +724,16 @@ def test_floating_ip_bulk_delete(self): self.assert_called('PUT', '/os-floating-ips-bulk/delete', {'ip_range': '10.0.0.1/24'}) + def test_server_floating_ip_add(self): + self.run_command('add-floating-ip sample-server 11.0.0.1') + self.assert_called('POST', '/servers/1234/action', + {'addFloatingIp': {'address': '11.0.0.1'}}) + + def test_server_floating_ip_remove(self): + self.run_command('remove-floating-ip sample-server 11.0.0.1') + self.assert_called('POST', '/servers/1234/action', + {'removeFloatingIp': {'address': '11.0.0.1'}}) + def test_usage_list(self): self.run_command('usage-list --start 2000-01-20 --end 2005-02-01') self.assert_called('GET', @@ -994,6 +1084,16 @@ def test_network_create_invalid(self): cmd = 'network-create 10.0.1.0' self.assertRaises(exceptions.CommandError, self.run_command, cmd) + def test_add_fixed_ip(self): + self.run_command('add-fixed-ip sample-server 1') + self.assert_called('POST', '/servers/1234/action', + {'addFixedIp': {'networkId': '1'}}) + + def test_remove_fixed_ip(self): + self.run_command('remove-fixed-ip sample-server 10.0.0.10') + self.assert_called('POST', '/servers/1234/action', + {'removeFixedIp': {'address': '10.0.0.10'}}) + def test_backup(self): self.run_command('backup sample-server back1 daily 1') self.assert_called('POST', '/servers/1234/action', @@ -1107,14 +1207,62 @@ def test_security_group_delete(self): self.run_command('secgroup-delete test') self.assert_called('DELETE', '/os-security-groups/1') + def test_server_security_group_add(self): + self.run_command('add-secgroup sample-server testgroup') + self.assert_called('POST', '/servers/1234/action', + {'addSecurityGroup': {'name': 'testgroup'}}) + + def test_server_security_group_remove(self): + self.run_command('remove-secgroup sample-server testgroup') + self.assert_called('POST', '/servers/1234/action', + {'removeSecurityGroup': {'name': 'testgroup'}}) + def test_interface_list(self): self.run_command('interface-list 1234') self.assert_called('GET', '/servers/1234/os-interface') def test_interface_attach(self): self.run_command('interface-attach --port-id port_id 1234') - self.assert_called('POST', '/servers/1234/os-interface') + self.assert_called('POST', '/servers/1234/os-interface', + {'interfaceAttachment': {'port_id': 'port_id'}}) def test_interface_detach(self): self.run_command('interface-detach 1234 port_id') self.assert_called('DELETE', '/servers/1234/os-interface/port_id') + + def test_volume_list(self): + self.run_command('volume-list') + self.assert_called('GET', '/volumes/detail') + + def test_volume_show(self): + self.run_command('volume-show Work') + self.assert_called('GET', '/volumes/detail') + + def test_volume_create(self): + self.run_command('volume-create 2 --display-name Work') + self.assert_called('POST', '/volumes', + {'volume': + {'display_name': 'Work', + 'imageRef': None, + 'availability_zone': None, + 'volume_type': None, + 'display_description': None, + 'snapshot_id': None, + 'size': 2}}) + + def test_volume_delete(self): + self.run_command('volume-delete Work') + self.assert_called('DELETE', + '/volumes/15e59938-07d5-11e1-90e3-e3dffe0c5983') + + def test_volume_attach(self): + self.run_command('volume-attach sample-server Work /dev/vdb') + self.assert_called('POST', '/servers/1234/os-volume_attachments', + {'volumeAttachment': + {'device': '/dev/vdb', + 'volumeId': 'Work'}}) + + def test_volume_detach(self): + self.run_command('volume-detach sample-server Work') + self.assert_called('DELETE', + '/servers/1234/os-volume_attachments/Work') From f1e5a88d2b4ea7cd0e3e44aa42ddbb108641c625 Mon Sep 17 00:00:00 2001 From: Alvaro Lopez Garcia Date: Wed, 6 Mar 2013 15:40:37 +0100 Subject: [PATCH 0103/1705] Fix typo in error message Change-Id: Ia37ec318329dc527b30a1835e4a52ace14b2bda3 --- novaclient/shell.py | 2 +- tests/test_shell.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/novaclient/shell.py b/novaclient/shell.py index 946a36872..88a58b2cd 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -606,7 +606,7 @@ def main(self, argv): "via either --os-auth-url or env[OS_AUTH_URL] " "or specify an auth_system which defines a " "default url with --os-auth-system " - "or env[OS_AUTH_SYSTEM") + "or env[OS_AUTH_SYSTEM]") if not os_region_name and region_name: os_region_name = region_name diff --git a/tests/test_shell.py b/tests/test_shell.py index d08a475f4..0ab76b6ac 100644 --- a/tests/test_shell.py +++ b/tests/test_shell.py @@ -133,7 +133,7 @@ def test_no_auth_url(self): required = ('You must provide an auth url' ' via either --os-auth-url or env[OS_AUTH_URL] or' ' specify an auth_system which defines a default url' - ' with --os-auth-system or env[OS_AUTH_SYSTEM',) + ' with --os-auth-system or env[OS_AUTH_SYSTEM]',) self.make_env(exclude='OS_AUTH_URL') try: self.shell('list') From b606948593990140d2c081f43fd38078eddf28c9 Mon Sep 17 00:00:00 2001 From: Masayuki Igawa Date: Thu, 7 Mar 2013 18:49:35 +0900 Subject: [PATCH 0104/1705] Fix nova boot --num-instances option checking 'args.num_instances' should be checked by None because of '0 == False'. This patch fixes the checking. Fixes bug 1151787 Change-Id: Iede860caa5c2d03d49471f375d687c3e40102372 --- novaclient/v1_1/shell.py | 2 +- tests/v1_1/test_shell.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 79799db38..e473da8ce 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -87,7 +87,7 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): if not args.flavor: raise exceptions.CommandError("you need to specify a Flavor ID ") - if args.num_instances: + if args.num_instances is not None: if args.num_instances <= 1: raise exceptions.CommandError("num_instances should be > 1") max_count = args.num_instances diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index 5bb954f0f..98d41f455 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -369,6 +369,8 @@ def test_boot_num_instances(self): def test_boot_invalid_num_instances(self): cmd = 'boot --image 1 --flavor 1 --num-instances 1 server' self.assertRaises(exceptions.CommandError, self.run_command, cmd) + cmd = 'boot --image 1 --flavor 1 --num-instances 0 server' + self.assertRaises(exceptions.CommandError, self.run_command, cmd) def test_flavor_list(self): self.run_command('flavor-list') From 1882b99734f5b0091d3b8c2662fea26980f05327 Mon Sep 17 00:00:00 2001 From: Joe Gordon Date: Tue, 5 Mar 2013 01:55:30 +0000 Subject: [PATCH 0105/1705] Check if tenant flag is uuid_like for all quota operations Fix bug 1145706 Change-Id: I9089ea4f968797b248f80bf84027a602e59ccd00 --- novaclient/utils.py | 7 +++++++ novaclient/v1_1/shell.py | 11 +++++------ tests/v1_1/fakes.py | 2 +- tests/v1_1/test_quotas.py | 2 +- tests/v1_1/test_shell.py | 18 ++++++++++++++---- 5 files changed, 28 insertions(+), 12 deletions(-) diff --git a/novaclient/utils.py b/novaclient/utils.py index 8c63452c7..d528c99b2 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -358,3 +358,10 @@ def is_uuid_like(val): return False except (TypeError, ValueError, AttributeError): return False + + +def check_uuid_like(val): + if not is_uuid_like(val): + raise exceptions.CommandError( + "error: Invalid tenant-id %s supplied" + % val) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 79799db38..ef426dea7 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2694,10 +2694,7 @@ def _quota_show(quotas): def _quota_update(manager, identifier, args): updates = {} - if not utils.is_uuid_like(identifier): - raise exceptions.CommandError( - "error: Invalid tenant-id %s supplied for update" - % identifier) + utils.check_uuid_like(identifier) for resource in _quota_resources: val = getattr(args, resource, None) if val is not None: @@ -2710,26 +2707,28 @@ def _quota_update(manager, identifier, args): @utils.arg('--tenant', metavar='', default=None, - help='UUID or name of tenant to list the quotas for.') + help='UUID of tenant to list the quotas for.') def do_quota_show(cs, args): """List the quotas for a tenant.""" if not args.tenant: _quota_show(cs.quotas.get(cs.client.tenant_id)) else: + utils.check_uuid_like(args.tenant) _quota_show(cs.quotas.get(args.tenant)) @utils.arg('--tenant', metavar='', default=None, - help='UUID or name of tenant to list the default quotas for.') + help='UUID of tenant to list the default quotas for.') def do_quota_defaults(cs, args): """List the default quotas for a tenant.""" if not args.tenant: _quota_show(cs.quotas.defaults(cs.client.tenant_id)) else: + utils.check_uuid_like(args.tenant) _quota_show(cs.quotas.defaults(args.tenant)) diff --git a/tests/v1_1/fakes.py b/tests/v1_1/fakes.py index 709236316..5ff06c799 100644 --- a/tests/v1_1/fakes.py +++ b/tests/v1_1/fakes.py @@ -954,7 +954,7 @@ def get_os_quota_sets_97f4c221_bff4_4578_b030_0df4ef119353(self, **kw): 'security_groups': 1, 'security_group_rules': 1}}) - def get_os_quota_sets_test_defaults(self): + def get_os_quota_sets_97f4c221bff44578b0300df4ef119353_defaults(self): return (200, {}, {'quota_set': { 'tenant_id': 'test', 'metadata_items': [], diff --git a/tests/v1_1/test_quotas.py b/tests/v1_1/test_quotas.py index 27cdb26ee..80877cc34 100644 --- a/tests/v1_1/test_quotas.py +++ b/tests/v1_1/test_quotas.py @@ -29,7 +29,7 @@ def test_tenant_quotas_get(self): cs.assert_called('GET', '/os-quota-sets/%s' % tenant_id) def test_tenant_quotas_defaults(self): - tenant_id = 'test' + tenant_id = '97f4c221bff44578b0300df4ef119353' cs.quotas.defaults(tenant_id) cs.assert_called('GET', '/os-quota-sets/%s/defaults' % tenant_id) diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index 5bb954f0f..0f8e7f2a6 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -895,16 +895,26 @@ def test_hypervisor_stats(self): self.assert_called('GET', '/os-hypervisors/statistics') def test_quota_show(self): - self.run_command('quota-show --tenant test') - self.assert_called('GET', '/os-quota-sets/test') + self.run_command('quota-show --tenant ' + '97f4c221bff44578b0300df4ef119353') + self.assert_called('GET', + '/os-quota-sets/97f4c221bff44578b0300df4ef119353') + self.assertRaises(exceptions.CommandError, + self.run_command, + 'quota-show --tenant not_uuid') def test_quota_show_no_tenant(self): self.run_command('quota-show') self.assert_called('GET', '/os-quota-sets/tenant_id') def test_quota_defaults(self): - self.run_command('quota-defaults --tenant test') - self.assert_called('GET', '/os-quota-sets/test/defaults') + self.run_command('quota-defaults --tenant ' + '97f4c221bff44578b0300df4ef119353') + self.assert_called('GET', + '/os-quota-sets/97f4c221bff44578b0300df4ef119353/defaults') + self.assertRaises(exceptions.CommandError, + self.run_command, + 'quota-defaults --tenant not_uuid') def test_quota_defaults_no_nenant(self): self.run_command('quota-defaults') From 21dca44202a3f0899a997ee8b1678017052c2731 Mon Sep 17 00:00:00 2001 From: Joe Gordon Date: Fri, 1 Mar 2013 23:07:18 +0000 Subject: [PATCH 0106/1705] Add wrap option to nova credentials for humans When using Keystone PKI, a token ID can be over 3200 chars long. Add optional --wrap option to make 'keystone token-get' human readable By default wrap=64 (to fit in 80 char terminal). And can be turned off by setting wrap=0 Fix bug 1131001 Change-Id: I50be7ebb4323ab1bf07af557403f5136b49080a4 --- novaclient/utils.py | 5 ++++- novaclient/v1_1/shell.py | 7 +++++-- tests/test_shell.py | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/novaclient/utils.py b/novaclient/utils.py index 8c63452c7..019ddd116 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -1,6 +1,7 @@ import os import re import sys +import textwrap import uuid import prettytable @@ -167,13 +168,15 @@ def print_list(objs, fields, formatters={}, sortby_index=0): print(pt.get_string()) -def print_dict(d, dict_property="Property"): +def print_dict(d, dict_property="Property", wrap=0): pt = prettytable.PrettyTable([dict_property, 'Value'], caching=False) pt.align = 'l' for k, v in d.iteritems(): # convert dict to str to check length if isinstance(v, dict): v = str(v) + if wrap > 0: + v = textwrap.fill(str(v), wrap) # if value has a newline, add in multiple rows # e.g. fault with stacktrace if v and isinstance(v, basestring) and r'\n' in v: diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index e473da8ce..4c85e48c6 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2608,12 +2608,15 @@ def do_endpoints(cs, _args): utils.print_dict(e['endpoints'][0], e['name']) +@utils.arg('--wrap', dest='wrap', metavar='', default=64, + help='wrap PKI tokens to a specified length, or 0 to disable') def do_credentials(cs, _args): """Show user credentials returned from auth""" ensure_service_catalog_present(cs) catalog = cs.client.service_catalog.catalog - utils.print_dict(catalog['access']['user'], "User Credentials") - utils.print_dict(catalog['access']['token'], "Token") + utils.print_dict(catalog['access']['user'], "User Credentials", + wrap=int(_args.wrap)) + utils.print_dict(catalog['access']['token'], "Token", wrap=int(_args.wrap)) @utils.arg('server', metavar='', help='Name or ID of server.') diff --git a/tests/test_shell.py b/tests/test_shell.py index 0ab76b6ac..4a8c3391a 100644 --- a/tests/test_shell.py +++ b/tests/test_shell.py @@ -102,7 +102,7 @@ def test_help_no_options(self): def test_bash_completion(self): stdout, stderr = self.shell('bash-completion') # just check we have some output - required = ['--matching help secgroup-delete-rule --priority'] + required = ['--matching --wrap help secgroup-delete-rule --priority'] for r in required: self.assertThat((stdout + stderr), matchers.MatchesRegex(r, re.DOTALL | re.MULTILINE)) From f73df49e4ff79b7b48503310269b4ec908a197c5 Mon Sep 17 00:00:00 2001 From: Hans Lindgren Date: Fri, 8 Mar 2013 13:09:46 +0100 Subject: [PATCH 0107/1705] Additional "Unify Manager._update behaviour" cleanup Make _update always return an instance of self.resource_class. This is in preparation for fixing bug 1145768 by reverting the API changes made in the original "Unify Manager._update behaviour" change. This is to avoid needing to revert some of the cleanup that was made in that change. Change-Id: I842bda40a0dc168689a7dd8b88433c3cebef5839 --- novaclient/base.py | 9 +++++---- tests/v1_1/test_floating_ips_bulk.py | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/novaclient/base.py b/novaclient/base.py index 729b4ca92..0898785cf 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -156,10 +156,11 @@ def _delete(self, url): def _update(self, url, body, response_key=None, **kwargs): self.run_hooks('modify_body_for_update', body, **kwargs) _resp, body = self.api.client.put(url, body=body) - if response_key: - return self.resource_class(self, body[response_key]) - else: - return body + if body: + if response_key: + return self.resource_class(self, body[response_key]) + else: + return self.resource_class(self, body) class ManagerWithFind(Manager): diff --git a/tests/v1_1/test_floating_ips_bulk.py b/tests/v1_1/test_floating_ips_bulk.py index 7d4ac7665..21981081c 100644 --- a/tests/v1_1/test_floating_ips_bulk.py +++ b/tests/v1_1/test_floating_ips_bulk.py @@ -61,4 +61,4 @@ def test_delete_floating_ips_bulk(self): fl = cs.floating_ips_bulk.delete('192.168.1.0/30') body = {'ip_range': '192.168.1.0/30'} cs.assert_called('PUT', '/os-floating-ips-bulk/delete', body) - self.assertEqual(fl['floating_ips_bulk_delete'], body['ip_range']) + self.assertEqual(fl.floating_ips_bulk_delete, body['ip_range']) From f294635c16cbf56ee90647de4fb2a35c171174a1 Mon Sep 17 00:00:00 2001 From: Scott Devoid Date: Thu, 7 Feb 2013 12:15:13 -0600 Subject: [PATCH 0108/1705] Show Tenant_ID for secgroup-list with all-tenant For the secgroup-list command, append the Tenant_ID column if the --all-tenants flag is used. Fixes bug #1118477 Change-Id: If2ff108cb0aec1327ceecc47a8c5ed9a08678e51 --- novaclient/v1_1/shell.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index c4390882b..e0392c566 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1924,7 +1924,11 @@ def do_secgroup_delete(cs, args): def do_secgroup_list(cs, args): """List security groups for the current tenant.""" search_opts = {'all_tenants': args.all_tenants} - _print_secgroups(cs.security_groups.list(search_opts=search_opts)) + columns = ['Name', 'Description'] + if args.all_tenants: + columns.append('Tenant_ID') + groups = cs.security_groups.list(search_opts=search_opts) + utils.print_list(groups, columns) @utils.arg('secgroup', metavar='', help='Name of security group.') From efafde0c4c24609f3efd851cfb6f36d8ccd1c5d5 Mon Sep 17 00:00:00 2001 From: Alessio Ababilov Date: Mon, 11 Mar 2013 16:13:15 +0200 Subject: [PATCH 0109/1705] Use keyring for testing Mention keyring in test-requires as in python-keystoneclient. So, keyring support is enabled in shell.py and tested. Change-Id: Icb712a07b995b47b286ed0528e04ae1d5ec195d2 --- tools/test-requires | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/test-requires b/tools/test-requires index cae195a34..203b0e264 100644 --- a/tools/test-requires +++ b/tools/test-requires @@ -3,6 +3,7 @@ distribute>=0.6.24 coverage discover fixtures>=0.3.12 +keyring mock pep8==1.3.3 sphinx>=1.1.2 From 65b55d8f9b4ff7ccbd137e94087949a8c92c1de2 Mon Sep 17 00:00:00 2001 From: Hans Lindgren Date: Thu, 7 Mar 2013 21:49:30 +0100 Subject: [PATCH 0110/1705] Revert API changes in "Unify Manager._update behaviour" This revert some of commit 63073104665ee4597cf3b7aa8dc2295a8a7db794, specifically the changes made to the API since those changes were never merged in Nova. Resolves bug 1145768. Change-Id: I8f83c5a33cfed0c3a659f5221b8b2e730ca9463f --- novaclient/v1_1/hosts.py | 2 +- tests/v1_1/fakes.py | 24 ++++++++++++------------ tests/v1_1/test_hosts.py | 6 +++--- tests/v1_1/test_shell.py | 6 +++--- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/novaclient/v1_1/hosts.py b/novaclient/v1_1/hosts.py index 3dea7021e..52a4dc0a0 100644 --- a/novaclient/v1_1/hosts.py +++ b/novaclient/v1_1/hosts.py @@ -54,7 +54,7 @@ def get(self, host): def update(self, host, values): """Update status or maintenance mode for the host.""" - return self._update("/os-hosts/%s" % host, {"host": values}, "host") + return self._update("/os-hosts/%s" % host, values) def host_action(self, host, action): """Performs an action on a host.""" diff --git a/tests/v1_1/fakes.py b/tests/v1_1/fakes.py index 709236316..190ff4c38 100644 --- a/tests/v1_1/fakes.py +++ b/tests/v1_1/fakes.py @@ -1357,10 +1357,10 @@ def get_os_hosts_host(self, *kw): def get_os_hosts(self, **kw): zone = kw.get('zone', 'nova1') return (200, {}, {'hosts': - [{'host_name': 'host1', + [{'host': 'host1', 'service': 'nova-compute', 'zone': zone}, - {'host_name': 'host1', + {'host': 'host1', 'service': 'nova-cert', 'zone': zone}]}) @@ -1368,25 +1368,25 @@ def get_os_hosts_sample_host(self, *kw): return (200, {}, {'host': [{'resource': {'host': 'sample_host'}}], }) def put_os_hosts_sample_host_1(self, body, **kw): - return (200, {}, {'host': {'host_name': 'sample-host_1', - 'status': 'enabled'}}) + return (200, {}, {'host': 'sample-host_1', + 'status': 'enabled'}) def put_os_hosts_sample_host_2(self, body, **kw): - return (200, {}, {'host': {'host_name': 'sample-host_2', - 'maintenance_mode': 'on_maintenance'}}) + return (200, {}, {'host': 'sample-host_2', + 'maintenance_mode': 'on_maintenance'}) def put_os_hosts_sample_host_3(self, body, **kw): - return (200, {}, {'host': {'host_name': 'sample-host_3', - 'status': 'enabled', - 'maintenance_mode': 'on_maintenance'}}) + return (200, {}, {'host': 'sample-host_3', + 'status': 'enabled', + 'maintenance_mode': 'on_maintenance'}) def post_os_hosts_sample_host_action(self, **kw): return (202, {}, None) def put_os_hosts_sample_host(self, body, **kw): - result = {'host_name': 'dummy'} - result.update(body['host']) - return (200, {}, {'host': result}) + result = {'host': 'dummy'} + result.update(body) + return (200, {}, result) def get_os_hypervisors(self, **kw): return (200, {}, {"hypervisors": [ diff --git a/tests/v1_1/test_hosts.py b/tests/v1_1/test_hosts.py index 4d181bb69..b41108f8d 100644 --- a/tests/v1_1/test_hosts.py +++ b/tests/v1_1/test_hosts.py @@ -29,14 +29,14 @@ def test_update_enable(self): host = cs.hosts.get('sample_host')[0] values = {"status": "enabled"} result = host.update(values) - cs.assert_called('PUT', '/os-hosts/sample_host', {'host': values}) + cs.assert_called('PUT', '/os-hosts/sample_host', values) self.assertTrue(isinstance(result, hosts.Host)) def test_update_maintenance(self): host = cs.hosts.get('sample_host')[0] values = {"maintenance_mode": "enable"} result = host.update(values) - cs.assert_called('PUT', '/os-hosts/sample_host', {'host': values}) + cs.assert_called('PUT', '/os-hosts/sample_host', values) self.assertTrue(isinstance(result, hosts.Host)) def test_update_both(self): @@ -44,7 +44,7 @@ def test_update_both(self): values = {"status": "enabled", "maintenance_mode": "enable"} result = host.update(values) - cs.assert_called('PUT', '/os-hosts/sample_host', {'host': values}) + cs.assert_called('PUT', '/os-hosts/sample_host', values) self.assertTrue(isinstance(result, hosts.Host)) def test_host_startup(self): diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index 5bb954f0f..df66788b8 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -814,18 +814,18 @@ def test_host_list_with_zone(self): def test_host_update_status(self): self.run_command('host-update sample-host_1 --status enabled') - body = {'host': {'status': 'enabled'}} + body = {'status': 'enabled'} self.assert_called('PUT', '/os-hosts/sample-host_1', body) def test_host_update_maintenance(self): self.run_command('host-update sample-host_2 --maintenance enable') - body = {'host': {'maintenance_mode': 'enable'}} + body = {'maintenance_mode': 'enable'} self.assert_called('PUT', '/os-hosts/sample-host_2', body) def test_host_update_multiple_settings(self): self.run_command('host-update sample-host_3 ' '--status disabled --maintenance enable') - body = {'host': {'status': 'disabled', 'maintenance_mode': 'enable'}} + body = {'status': 'disabled', 'maintenance_mode': 'enable'} self.assert_called('PUT', '/os-hosts/sample-host_3', body) def test_host_startup(self): From b94fbf59a2899cf08116c6f1e4eb3c4cec8666df Mon Sep 17 00:00:00 2001 From: Hans Lindgren Date: Tue, 12 Mar 2013 10:52:27 +0100 Subject: [PATCH 0111/1705] Make os-services API extensions consistent with Nova Updates the os-services API extensions to match the Nova changes proposed in I932160d64fdd3aaeb2ed90a092ecc7a36dcc9665. Resolves bug 1147746. Change-Id: Ib0f24dea8e937a8e1a1604b1cbf19d96bcdbcd8f --- novaclient/v1_1/shell.py | 20 ++++++++++---------- tests/v1_1/fakes.py | 10 +++++----- tests/v1_1/test_services.py | 6 +++--- tests/v1_1/test_shell.py | 8 ++++---- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index e0392c566..e4e4b9419 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2392,29 +2392,29 @@ def do_reset_network(cs, args): @utils.arg('--host', metavar='', default=None, help='Name of host.') -@utils.arg('--servicename', metavar='', default=None, - help='Name of service.') +@utils.arg('--binary', metavar='', default=None, + help='Service binary.') def do_service_list(cs, args): - """Show a list of all running services. Filter by host & service name.""" - result = cs.services.list(args.host, args.servicename) + """Show a list of all running services. Filter by host & binary.""" + result = cs.services.list(host=args.host, binary=args.binary) columns = ["Binary", "Host", "Zone", "Status", "State", "Updated_at"] utils.print_list(result, columns) @utils.arg('host', metavar='', help='Name of host.') -@utils.arg('service', metavar='', help='Name of service.') +@utils.arg('binary', metavar='', help='Service binary.') def do_service_enable(cs, args): """Enable the service""" - result = cs.services.enable(args.host, args.service) - utils.print_list([result], ['Host', 'Service', 'Disabled']) + result = cs.services.enable(args.host, args.binary) + utils.print_list([result], ['Host', 'Binary', 'Status']) @utils.arg('host', metavar='', help='Name of host.') -@utils.arg('service', metavar='', help='Name of service.') +@utils.arg('binary', metavar='', help='Service binary.') def do_service_disable(cs, args): """Enable the service""" - result = cs.services.disable(args.host, args.service) - utils.print_list([result], ['Host', 'Service', 'Disabled']) + result = cs.services.disable(args.host, args.binary) + utils.print_list([result], ['Host', 'Binary', 'Status']) @utils.arg('fixed_ip', metavar='', help='Fixed IP Address.') diff --git a/tests/v1_1/fakes.py b/tests/v1_1/fakes.py index ec1a0ceba..58f707242 100644 --- a/tests/v1_1/fakes.py +++ b/tests/v1_1/fakes.py @@ -1318,15 +1318,15 @@ def delete_os_aggregates_1(self, **kw): # def get_os_services(self, **kw): host = kw.get('host', 'host1') - service = kw.get('binary', 'nova-compute') + binary = kw.get('binary', 'nova-compute') return (200, {}, {'services': - [{'binary': service, + [{'binary': binary, 'host': host, 'zone': 'nova', 'status': 'enabled', 'state': 'up', 'updated_at': datetime(2012, 10, 29, 13, 42, 2)}, - {'binary': service, + {'binary': binary, 'host': host, 'zone': 'nova', 'status': 'disabled', @@ -1337,12 +1337,12 @@ def get_os_services(self, **kw): def put_os_services_enable(self, body, **kw): return (200, {}, {'service': {'host': body['host'], 'binary': body['binary'], - 'disabled': False}}) + 'status': 'enabled'}}) def put_os_services_disable(self, body, **kw): return (200, {}, {'service': {'host': body['host'], 'binary': body['binary'], - 'disabled': True}}) + 'status': 'disabled'}}) # # Fixed IPs diff --git a/tests/v1_1/test_services.py b/tests/v1_1/test_services.py index d6d58837e..25759e270 100644 --- a/tests/v1_1/test_services.py +++ b/tests/v1_1/test_services.py @@ -47,7 +47,7 @@ def test_list_services_with_binary(self): [self.assertEqual(s.host, 'host1') for s in svs] def test_list_services_with_host_binary(self): - svs = cs.services.list('host2', 'nova-cert') + svs = cs.services.list(host='host2', binary='nova-cert') cs.assert_called('GET', '/os-services?host=host2&binary=nova-cert') [self.assertTrue(isinstance(s, services.Service)) for s in svs] [self.assertEqual(s.binary, 'nova-cert') for s in svs] @@ -58,11 +58,11 @@ def test_services_enable(self): values = {"host": "host1", 'binary': 'nova-cert'} cs.assert_called('PUT', '/os-services/enable', values) self.assertTrue(isinstance(service, services.Service)) - self.assertFalse(service.disabled) + self.assertEqual(service.status, 'enabled') def test_services_disable(self): service = cs.services.disable('host1', 'nova-cert') values = {"host": "host1", 'binary': 'nova-cert'} cs.assert_called('PUT', '/os-services/disable', values) self.assertTrue(isinstance(service, services.Service)) - self.assertTrue(service.disabled) + self.assertEqual(service.status, 'disabled') diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index fd028b381..74453cdf4 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -864,12 +864,12 @@ def test_services_list_with_host(self): self.run_command('service-list --host host1') self.assert_called('GET', '/os-services?host=host1') - def test_services_list_with_servicename(self): - self.run_command('service-list --servicename nova-cert') + def test_services_list_with_binary(self): + self.run_command('service-list --binary nova-cert') self.assert_called('GET', '/os-services?binary=nova-cert') - def test_services_list_with_host_servicename(self): - self.run_command('service-list --host host1 --servicename nova-cert') + def test_services_list_with_host_binary(self): + self.run_command('service-list --host host1 --binary nova-cert') self.assert_called('GET', '/os-services?host=host1&binary=nova-cert') def test_services_enable(self): From d4509dd2ad1e740250f46bb281de3b6bcf39de85 Mon Sep 17 00:00:00 2001 From: Andy Hill Date: Wed, 13 Mar 2013 08:10:48 -0500 Subject: [PATCH 0112/1705] Removes tenant IDs checking for nova quota operations. Until there is a way to validate tenant IDs, remove the existing checks. Fixes bug #1154582 Change-Id: If10fac17b919190c1492cfbf1be9950284a82197 --- novaclient/v1_1/shell.py | 9 +++------ tests/v1_1/test_shell.py | 12 ------------ 2 files changed, 3 insertions(+), 18 deletions(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index e0392c566..56bbc3cd2 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2701,7 +2701,6 @@ def _quota_show(quotas): def _quota_update(manager, identifier, args): updates = {} - utils.check_uuid_like(identifier) for resource in _quota_resources: val = getattr(args, resource, None) if val is not None: @@ -2714,34 +2713,32 @@ def _quota_update(manager, identifier, args): @utils.arg('--tenant', metavar='', default=None, - help='UUID of tenant to list the quotas for.') + help='ID of tenant to list the quotas for.') def do_quota_show(cs, args): """List the quotas for a tenant.""" if not args.tenant: _quota_show(cs.quotas.get(cs.client.tenant_id)) else: - utils.check_uuid_like(args.tenant) _quota_show(cs.quotas.get(args.tenant)) @utils.arg('--tenant', metavar='', default=None, - help='UUID of tenant to list the default quotas for.') + help='ID of tenant to list the default quotas for.') def do_quota_defaults(cs, args): """List the default quotas for a tenant.""" if not args.tenant: _quota_show(cs.quotas.defaults(cs.client.tenant_id)) else: - utils.check_uuid_like(args.tenant) _quota_show(cs.quotas.defaults(args.tenant)) @utils.arg('tenant', metavar='', - help='UUID of tenant to set the quotas for.') + help='ID of tenant to set the quotas for.') @utils.arg('--instances', metavar='', type=int, default=None, diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index 8cc5c19f0..b7901c4cd 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -991,9 +991,6 @@ def test_quota_show(self): '97f4c221bff44578b0300df4ef119353') self.assert_called('GET', '/os-quota-sets/97f4c221bff44578b0300df4ef119353') - self.assertRaises(exceptions.CommandError, - self.run_command, - 'quota-show --tenant not_uuid') def test_quota_show_no_tenant(self): self.run_command('quota-show') @@ -1004,9 +1001,6 @@ def test_quota_defaults(self): '97f4c221bff44578b0300df4ef119353') self.assert_called('GET', '/os-quota-sets/97f4c221bff44578b0300df4ef119353/defaults') - self.assertRaises(exceptions.CommandError, - self.run_command, - 'quota-defaults --tenant not_uuid') def test_quota_defaults_no_nenant(self): self.run_command('quota-defaults') @@ -1019,12 +1013,6 @@ def test_quota_update(self): self.assert_called('PUT', '/os-quota-sets/97f4c221bff44578b0300df4ef119353') - def test_quota_update_error(self): - self.assertRaises(exceptions.CommandError, - self.run_command, - 'quota-update 7f4c221-bff4-4578-b030-0df4ef119353' - ' --instances=5') - def test_quota_class_show(self): self.run_command('quota-class-show test') self.assert_called('GET', '/os-quota-class-sets/test') From c5b579926feb208c785f028d48c6eaf9c36768fc Mon Sep 17 00:00:00 2001 From: Davanum Srinivas Date: Wed, 13 Mar 2013 18:09:17 -0400 Subject: [PATCH 0113/1705] Fix Copyright Headers from LLC to Foundation follow the lead from nova and oslo projects Change-Id: I270c5f1e4eefa4b72e292bfb4a4c60de0c3f6e4a --- novaclient/__init__.py | 2 +- novaclient/base.py | 2 +- novaclient/client.py | 2 +- novaclient/extension.py | 2 +- novaclient/openstack/common/setup.py | 2 +- novaclient/openstack/common/strutils.py | 2 +- novaclient/openstack/common/timeutils.py | 2 +- novaclient/service_catalog.py | 2 +- novaclient/shell.py | 2 +- novaclient/v1_1/__init__.py | 2 +- novaclient/v1_1/aggregates.py | 2 +- novaclient/v1_1/availability_zones.py | 2 +- novaclient/v1_1/base.py | 2 +- novaclient/v1_1/certs.py | 2 +- novaclient/v1_1/client.py | 2 +- novaclient/v1_1/cloudpipe.py | 2 +- novaclient/v1_1/contrib/list_extensions.py | 2 +- novaclient/v1_1/contrib/tenant_networks.py | 2 +- novaclient/v1_1/flavor_access.py | 2 +- novaclient/v1_1/floating_ips.py | 2 +- novaclient/v1_1/fping.py | 2 +- novaclient/v1_1/hosts.py | 2 +- novaclient/v1_1/hypervisors.py | 2 +- novaclient/v1_1/keypairs.py | 2 +- novaclient/v1_1/limits.py | 2 +- novaclient/v1_1/networks.py | 2 +- novaclient/v1_1/quota_classes.py | 2 +- novaclient/v1_1/quotas.py | 2 +- novaclient/v1_1/security_group_rules.py | 2 +- novaclient/v1_1/security_groups.py | 2 +- novaclient/v1_1/servers.py | 2 +- novaclient/v1_1/shell.py | 2 +- novaclient/v1_1/virtual_interfaces.py | 2 +- setup.py | 2 +- tests/test_auth_plugins.py | 2 +- tests/test_client.py | 2 +- tests/test_discover.py | 2 +- tests/v1_1/contrib/fakes.py | 2 +- tests/v1_1/contrib/test_tenant_networks.py | 2 +- tests/v1_1/fakes.py | 2 +- tests/v1_1/test_aggregates.py | 2 +- tests/v1_1/test_availability_zone.py | 2 +- tests/v1_1/test_flavor_access.py | 2 +- tests/v1_1/test_fping.py | 2 +- tests/v1_1/test_hypervisors.py | 2 +- tests/v1_1/test_quota_classes.py | 2 +- tests/v1_1/test_quotas.py | 2 +- tests/v1_1/test_shell.py | 2 +- tools/install_venv.py | 2 +- 49 files changed, 49 insertions(+), 49 deletions(-) diff --git a/novaclient/__init__.py b/novaclient/__init__.py index fa6b8e590..2cc5baa6c 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2012 OpenStack LLC. +# Copyright 2012 OpenStack Foundation # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain diff --git a/novaclient/base.py b/novaclient/base.py index 0898785cf..db9a75487 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -1,6 +1,6 @@ # Copyright 2010 Jacob Kaplan-Moss -# Copyright 2011 OpenStack LLC. +# Copyright 2011 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/novaclient/client.py b/novaclient/client.py index b262c6c32..e49aa4c80 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -1,5 +1,5 @@ # Copyright 2010 Jacob Kaplan-Moss -# Copyright 2011 OpenStack LLC. +# Copyright 2011 OpenStack Foundation # Copyright 2011 Piston Cloud Computing, Inc. # All Rights Reserved. diff --git a/novaclient/extension.py b/novaclient/extension.py index 7c91a8e89..ac105070a 100644 --- a/novaclient/extension.py +++ b/novaclient/extension.py @@ -1,4 +1,4 @@ -# Copyright 2011 OpenStack LLC. +# Copyright 2011 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/novaclient/openstack/common/setup.py b/novaclient/openstack/common/setup.py index 889276f9b..61c01dc87 100644 --- a/novaclient/openstack/common/setup.py +++ b/novaclient/openstack/common/setup.py @@ -1,6 +1,6 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2011 OpenStack LLC. +# Copyright 2011 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/novaclient/openstack/common/strutils.py b/novaclient/openstack/common/strutils.py index 7813b6422..841a0f823 100644 --- a/novaclient/openstack/common/strutils.py +++ b/novaclient/openstack/common/strutils.py @@ -1,6 +1,6 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2011 OpenStack LLC. +# Copyright 2011 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/novaclient/openstack/common/timeutils.py b/novaclient/openstack/common/timeutils.py index 86004391d..15582c820 100644 --- a/novaclient/openstack/common/timeutils.py +++ b/novaclient/openstack/common/timeutils.py @@ -1,6 +1,6 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2011 OpenStack LLC. +# Copyright 2011 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/novaclient/service_catalog.py b/novaclient/service_catalog.py index f4a5f1adb..d3fefe184 100644 --- a/novaclient/service_catalog.py +++ b/novaclient/service_catalog.py @@ -1,4 +1,4 @@ -# Copyright 2011 OpenStack LLC. +# Copyright 2011 OpenStack Foundation # Copyright 2011, Piston Cloud Computing, Inc. # # All Rights Reserved. diff --git a/novaclient/shell.py b/novaclient/shell.py index 7d7c5d3fc..bf56b0fdb 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -1,5 +1,5 @@ # Copyright 2010 Jacob Kaplan-Moss -# Copyright 2011 OpenStack LLC. +# Copyright 2011 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/novaclient/v1_1/__init__.py b/novaclient/v1_1/__init__.py index c8538c1e2..8958eb45a 100644 --- a/novaclient/v1_1/__init__.py +++ b/novaclient/v1_1/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2012 OpenStack, LLC. +# Copyright (c) 2012 OpenStack Foundation # # All Rights Reserved. # diff --git a/novaclient/v1_1/aggregates.py b/novaclient/v1_1/aggregates.py index 2a57703f8..576a81a49 100644 --- a/novaclient/v1_1/aggregates.py +++ b/novaclient/v1_1/aggregates.py @@ -1,4 +1,4 @@ -# Copyright 2012 OpenStack LLC. +# Copyright 2012 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/novaclient/v1_1/availability_zones.py b/novaclient/v1_1/availability_zones.py index 7b39b300a..41362aa25 100644 --- a/novaclient/v1_1/availability_zones.py +++ b/novaclient/v1_1/availability_zones.py @@ -1,4 +1,4 @@ -# Copyright 2011 OpenStack LLC. +# Copyright 2011 OpenStack Foundation # Copyright 2013 IBM Corp. # All Rights Reserved. # diff --git a/novaclient/v1_1/base.py b/novaclient/v1_1/base.py index fce840b1b..af6e689ce 100644 --- a/novaclient/v1_1/base.py +++ b/novaclient/v1_1/base.py @@ -1,6 +1,6 @@ # Copyright 2010 Jacob Kaplan-Moss -# Copyright 2011 OpenStack LLC. +# Copyright 2011 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/novaclient/v1_1/certs.py b/novaclient/v1_1/certs.py index 6e3a4c716..28ff34e3f 100644 --- a/novaclient/v1_1/certs.py +++ b/novaclient/v1_1/certs.py @@ -1,6 +1,6 @@ # Copyright 2010 Jacob Kaplan-Moss -# Copyright 2011 OpenStack LLC. +# Copyright 2011 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/novaclient/v1_1/client.py b/novaclient/v1_1/client.py index 383ae19fc..cae702d1f 100644 --- a/novaclient/v1_1/client.py +++ b/novaclient/v1_1/client.py @@ -1,5 +1,5 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2012 OpenStack LLC. +# Copyright 2012 OpenStack Foundation # Copyright 2013 IBM Corp. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/novaclient/v1_1/cloudpipe.py b/novaclient/v1_1/cloudpipe.py index 44baa2160..de07bf68c 100644 --- a/novaclient/v1_1/cloudpipe.py +++ b/novaclient/v1_1/cloudpipe.py @@ -1,4 +1,4 @@ -# Copyright 2012 OpenStack LLC. +# Copyright 2012 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/novaclient/v1_1/contrib/list_extensions.py b/novaclient/v1_1/contrib/list_extensions.py index f184238dc..7eb9f16c8 100644 --- a/novaclient/v1_1/contrib/list_extensions.py +++ b/novaclient/v1_1/contrib/list_extensions.py @@ -1,4 +1,4 @@ -# Copyright 2011 OpenStack LLC. +# Copyright 2011 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/novaclient/v1_1/contrib/tenant_networks.py b/novaclient/v1_1/contrib/tenant_networks.py index 9f70634d9..9ac97f110 100644 --- a/novaclient/v1_1/contrib/tenant_networks.py +++ b/novaclient/v1_1/contrib/tenant_networks.py @@ -1,4 +1,4 @@ -# Copyright 2013 OpenStack, LLC +# Copyright 2013 OpenStack Foundation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/novaclient/v1_1/flavor_access.py b/novaclient/v1_1/flavor_access.py index ef3992afa..851a9db7e 100644 --- a/novaclient/v1_1/flavor_access.py +++ b/novaclient/v1_1/flavor_access.py @@ -1,4 +1,4 @@ -# Copyright 2012 OpenStack LLC. +# Copyright 2012 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/novaclient/v1_1/floating_ips.py b/novaclient/v1_1/floating_ips.py index c76467474..503e6269c 100644 --- a/novaclient/v1_1/floating_ips.py +++ b/novaclient/v1_1/floating_ips.py @@ -1,5 +1,5 @@ # Copyright (c) 2011 X.commerce, a business unit of eBay Inc. -# Copyright 2011 OpenStack LLC. +# Copyright 2011 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/novaclient/v1_1/fping.py b/novaclient/v1_1/fping.py index 2f9eb89b6..36ecac63a 100644 --- a/novaclient/v1_1/fping.py +++ b/novaclient/v1_1/fping.py @@ -1,6 +1,6 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2012 OpenStack LLC. +# Copyright 2012 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/novaclient/v1_1/hosts.py b/novaclient/v1_1/hosts.py index 52a4dc0a0..cc1a48afe 100644 --- a/novaclient/v1_1/hosts.py +++ b/novaclient/v1_1/hosts.py @@ -1,4 +1,4 @@ -# Copyright 2011 OpenStack LLC. +# Copyright 2011 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/novaclient/v1_1/hypervisors.py b/novaclient/v1_1/hypervisors.py index dfb5c885a..4d02576d6 100644 --- a/novaclient/v1_1/hypervisors.py +++ b/novaclient/v1_1/hypervisors.py @@ -1,4 +1,4 @@ -# Copyright 2012 OpenStack LLC. +# Copyright 2012 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/novaclient/v1_1/keypairs.py b/novaclient/v1_1/keypairs.py index bb31e8657..e3423258e 100644 --- a/novaclient/v1_1/keypairs.py +++ b/novaclient/v1_1/keypairs.py @@ -1,4 +1,4 @@ -# Copyright 2011 OpenStack LLC. +# Copyright 2011 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/novaclient/v1_1/limits.py b/novaclient/v1_1/limits.py index bcf8fa2d4..97b786f80 100644 --- a/novaclient/v1_1/limits.py +++ b/novaclient/v1_1/limits.py @@ -1,4 +1,4 @@ -# Copyright 2011 OpenStack LLC. +# Copyright 2011 OpenStack Foundation from novaclient import base diff --git a/novaclient/v1_1/networks.py b/novaclient/v1_1/networks.py index b1fbb8886..18c5bd53e 100644 --- a/novaclient/v1_1/networks.py +++ b/novaclient/v1_1/networks.py @@ -1,4 +1,4 @@ -# Copyright 2012 OpenStack LLC. +# Copyright 2012 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/novaclient/v1_1/quota_classes.py b/novaclient/v1_1/quota_classes.py index 066428461..874b50630 100644 --- a/novaclient/v1_1/quota_classes.py +++ b/novaclient/v1_1/quota_classes.py @@ -1,4 +1,4 @@ -# Copyright 2012 OpenStack LLC. +# Copyright 2012 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/novaclient/v1_1/quotas.py b/novaclient/v1_1/quotas.py index c63cb4694..d21285694 100644 --- a/novaclient/v1_1/quotas.py +++ b/novaclient/v1_1/quotas.py @@ -1,4 +1,4 @@ -# Copyright 2011 OpenStack LLC. +# Copyright 2011 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/novaclient/v1_1/security_group_rules.py b/novaclient/v1_1/security_group_rules.py index 2e3ba566b..3e23a2873 100644 --- a/novaclient/v1_1/security_group_rules.py +++ b/novaclient/v1_1/security_group_rules.py @@ -1,4 +1,4 @@ -# Copyright 2011 OpenStack LLC. +# Copyright 2011 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/novaclient/v1_1/security_groups.py b/novaclient/v1_1/security_groups.py index 0e2037eea..551811526 100644 --- a/novaclient/v1_1/security_groups.py +++ b/novaclient/v1_1/security_groups.py @@ -1,4 +1,4 @@ -# Copyright 2011 OpenStack LLC. +# Copyright 2011 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index 78bb4351c..5d96b1334 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -1,6 +1,6 @@ # Copyright 2010 Jacob Kaplan-Moss -# Copyright 2011 OpenStack LLC. +# Copyright 2011 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 56bbc3cd2..2b65b9765 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1,6 +1,6 @@ # Copyright 2010 Jacob Kaplan-Moss -# Copyright 2011 OpenStack LLC. +# Copyright 2011 OpenStack Foundation # Copyright 2013 IBM Corp. # All Rights Reserved. # diff --git a/novaclient/v1_1/virtual_interfaces.py b/novaclient/v1_1/virtual_interfaces.py index 64593e018..9c04e8d86 100644 --- a/novaclient/v1_1/virtual_interfaces.py +++ b/novaclient/v1_1/virtual_interfaces.py @@ -1,4 +1,4 @@ -# Copyright 2012 OpenStack LLC. +# Copyright 2012 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/setup.py b/setup.py index 062e49b75..f1e2b7b31 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -# Copyright 2011 OpenStack, LLC +# Copyright 2011 OpenStack Foundation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/tests/test_auth_plugins.py b/tests/test_auth_plugins.py index e73c3545e..fca506959 100644 --- a/tests/test_auth_plugins.py +++ b/tests/test_auth_plugins.py @@ -1,4 +1,4 @@ -# Copyright 2012 OpenStack LLC. +# Copyright 2012 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/tests/test_client.py b/tests/test_client.py index ecc9606f5..dde654869 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1,4 +1,4 @@ -# Copyright 2012 OpenStack LLC. +# Copyright 2012 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/tests/test_discover.py b/tests/test_discover.py index dbbddc4e3..3b78f0caf 100644 --- a/tests/test_discover.py +++ b/tests/test_discover.py @@ -1,4 +1,4 @@ -# Copyright 2012 OpenStack LLC. +# Copyright 2012 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/tests/v1_1/contrib/fakes.py b/tests/v1_1/contrib/fakes.py index b5c041d89..6983406e7 100644 --- a/tests/v1_1/contrib/fakes.py +++ b/tests/v1_1/contrib/fakes.py @@ -1,4 +1,4 @@ -# Copyright 2012 OpenStack, LLC +# Copyright 2012 OpenStack Foundation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/tests/v1_1/contrib/test_tenant_networks.py b/tests/v1_1/contrib/test_tenant_networks.py index 4be6deeef..b66cc5b96 100644 --- a/tests/v1_1/contrib/test_tenant_networks.py +++ b/tests/v1_1/contrib/test_tenant_networks.py @@ -1,6 +1,6 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2012 OpenStack LLC. +# Copyright 2012 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/tests/v1_1/fakes.py b/tests/v1_1/fakes.py index 62fc103da..5a936269b 100644 --- a/tests/v1_1/fakes.py +++ b/tests/v1_1/fakes.py @@ -1,5 +1,5 @@ # Copyright (c) 2011 X.commerce, a business unit of eBay Inc. -# Copyright 2011 OpenStack, LLC +# Copyright 2011 OpenStack Foundation # Copyright 2013 IBM Corp. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/v1_1/test_aggregates.py b/tests/v1_1/test_aggregates.py index ebab2cd9e..0b84067c6 100644 --- a/tests/v1_1/test_aggregates.py +++ b/tests/v1_1/test_aggregates.py @@ -1,4 +1,4 @@ -# Copyright 2012 OpenStack LLC. +# Copyright 2012 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/tests/v1_1/test_availability_zone.py b/tests/v1_1/test_availability_zone.py index f270bec27..56a1480fb 100644 --- a/tests/v1_1/test_availability_zone.py +++ b/tests/v1_1/test_availability_zone.py @@ -1,4 +1,4 @@ -# Copyright 2011 OpenStack LLC. +# Copyright 2011 OpenStack Foundation # Copyright 2013 IBM Corp. # All Rights Reserved. # diff --git a/tests/v1_1/test_flavor_access.py b/tests/v1_1/test_flavor_access.py index 2c137291a..929b51516 100644 --- a/tests/v1_1/test_flavor_access.py +++ b/tests/v1_1/test_flavor_access.py @@ -1,4 +1,4 @@ -# Copyright 2012 OpenStack LLC. +# Copyright 2012 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/tests/v1_1/test_fping.py b/tests/v1_1/test_fping.py index d4581bd27..4f77f7905 100644 --- a/tests/v1_1/test_fping.py +++ b/tests/v1_1/test_fping.py @@ -1,6 +1,6 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2012 OpenStack LLC. +# Copyright 2012 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/tests/v1_1/test_hypervisors.py b/tests/v1_1/test_hypervisors.py index 732210941..12dd2de05 100644 --- a/tests/v1_1/test_hypervisors.py +++ b/tests/v1_1/test_hypervisors.py @@ -1,4 +1,4 @@ -# Copyright 2012 OpenStack LLC. +# Copyright 2012 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/tests/v1_1/test_quota_classes.py b/tests/v1_1/test_quota_classes.py index b917665db..a539ebeec 100644 --- a/tests/v1_1/test_quota_classes.py +++ b/tests/v1_1/test_quota_classes.py @@ -1,4 +1,4 @@ -# Copyright 2011 OpenStack LLC. +# Copyright 2011 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/tests/v1_1/test_quotas.py b/tests/v1_1/test_quotas.py index 80877cc34..11198d6b6 100644 --- a/tests/v1_1/test_quotas.py +++ b/tests/v1_1/test_quotas.py @@ -1,4 +1,4 @@ -# Copyright 2011 OpenStack LLC. +# Copyright 2011 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index b7901c4cd..611f5a3ca 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -1,6 +1,6 @@ # Copyright 2010 Jacob Kaplan-Moss -# Copyright 2011 OpenStack LLC. +# Copyright 2011 OpenStack Foundation # Copyright 2012 IBM Corp. # All Rights Reserved. # diff --git a/tools/install_venv.py b/tools/install_venv.py index 953e74bff..5742f7e11 100644 --- a/tools/install_venv.py +++ b/tools/install_venv.py @@ -4,7 +4,7 @@ # Administrator of the National Aeronautics and Space Administration. # All Rights Reserved. # -# Copyright 2010 OpenStack, LLC +# Copyright 2010 OpenStack Foundation # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain From 301500d6c8931b8c884c78dc046e8950a10392ad Mon Sep 17 00:00:00 2001 From: Wu Wenxiang Date: Fri, 15 Mar 2013 12:50:57 +0800 Subject: [PATCH 0114/1705] Remove unused import Remove unused import line in tests/v1_1/test_quotas.py Change-Id: I07b23d945a361015974b546a455d9cd5a48a5b3d --- tests/v1_1/test_quotas.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/v1_1/test_quotas.py b/tests/v1_1/test_quotas.py index 11198d6b6..85a7a576a 100644 --- a/tests/v1_1/test_quotas.py +++ b/tests/v1_1/test_quotas.py @@ -16,8 +16,6 @@ from tests import utils from tests.v1_1 import fakes -from novaclient import exceptions - cs = fakes.FakeClient() From e9e05d7dfe6809adc009f79ca3ee08a332488094 Mon Sep 17 00:00:00 2001 From: Alvaro Lopez Garcia Date: Wed, 13 Mar 2013 16:47:16 +0100 Subject: [PATCH 0115/1705] Set up debug level on root logger. If we set up the debug level on the root logger, this can be used by the submodules that might need to print some debug output. Change-Id: I2a00b40d4748cc62e6081df7d6a44622f5ad4467 --- novaclient/client.py | 10 ++++++++-- novaclient/shell.py | 8 ++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index e49aa4c80..73595701c 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -97,11 +97,17 @@ def __init__(self, user, password, projectid, auth_url=None, self._logger = logging.getLogger(__name__) if self.http_log_debug: + # Logging level is already set on the root logger ch = logging.StreamHandler() - self._logger.setLevel(logging.DEBUG) self._logger.addHandler(ch) + self._logger.propagate = False if hasattr(requests, 'logging'): - requests.logging.getLogger(requests.__name__).addHandler(ch) + rql = requests.logging.getLogger(requests.__name__) + rql.addHandler(ch) + # Since we have already setup the root logger on debug, we + # have to set it up here on WARNING (its original level) + # otherwise we will get all the requests logging messanges + rql.setLevel(logging.WARNING) def use_token_cache(self, use_it): self.os_cache = use_it diff --git a/novaclient/shell.py b/novaclient/shell.py index bf56b0fdb..8b4aea512 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -507,11 +507,11 @@ def setup_debugging(self, debug): if not debug: return - streamhandler = logging.StreamHandler() streamformat = "%(levelname)s (%(module)s:%(lineno)d) %(message)s" - streamhandler.setFormatter(logging.Formatter(streamformat)) - logger.setLevel(logging.DEBUG) - logger.addHandler(streamhandler) + # Set up the root logger to debug so that the submodules can + # print debug messages + logging.basicConfig(level=logging.DEBUG, + format=streamformat) def main(self, argv): # Parse args once to find version and debug settings From 59f8360d7e087ae99a9dcac84c6c81542aa1c94a Mon Sep 17 00:00:00 2001 From: Michael Still Date: Sat, 16 Mar 2013 06:53:37 +1100 Subject: [PATCH 0116/1705] Add support for the new fixed_ip quota. Required for bug 1125468. Change-Id: If9f8f31a9fd35497a308ae13351685e470e20cd0 --- novaclient/v1_1/quotas.py | 3 ++- novaclient/v1_1/shell.py | 13 +++++++++---- tests/v1_1/test_shell.py | 11 ++++++++++- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/novaclient/v1_1/quotas.py b/novaclient/v1_1/quotas.py index d21285694..d0cdf02ba 100644 --- a/novaclient/v1_1/quotas.py +++ b/novaclient/v1_1/quotas.py @@ -39,7 +39,7 @@ def get(self, tenant_id): def update(self, tenant_id, metadata_items=None, injected_file_content_bytes=None, injected_file_path_bytes=None, volumes=None, gigabytes=None, - ram=None, floating_ips=None, instances=None, + ram=None, floating_ips=None, fixed_ips=None, instances=None, injected_files=None, cores=None, key_pairs=None, security_groups=None, security_group_rules=None): @@ -53,6 +53,7 @@ def update(self, tenant_id, metadata_items=None, 'gigabytes': gigabytes, 'ram': ram, 'floating_ips': floating_ips, + 'fixed_ips': fixed_ips, 'instances': instances, 'injected_files': injected_files, 'cores': cores, diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 2b65b9765..3615f81de 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2683,10 +2683,10 @@ def do_ssh(cs, args): _quota_resources = ['instances', 'cores', 'ram', 'volumes', 'gigabytes', - 'floating_ips', 'metadata_items', 'injected_files', - 'key_pairs', 'injected_file_content_bytes', - 'injected_file_path_bytes', 'security_groups', - 'security_group_rules'] + 'floating_ips', 'fixed_ips', 'metadata_items', + 'injected_files', 'key_pairs', + 'injected_file_content_bytes', 'injected_file_path_bytes', + 'security_groups', 'security_group_rules'] def _quota_show(quotas): @@ -2767,6 +2767,11 @@ def do_quota_defaults(cs, args): @utils.arg('--floating_ips', type=int, help=argparse.SUPPRESS) +@utils.arg('--fixed-ips', + metavar='', + type=int, + default=None, + help='New value for the "fixed-ips" quota.') @utils.arg('--metadata-items', metavar='', type=int, diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index 611f5a3ca..7347e14f7 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -1002,7 +1002,7 @@ def test_quota_defaults(self): self.assert_called('GET', '/os-quota-sets/97f4c221bff44578b0300df4ef119353/defaults') - def test_quota_defaults_no_nenant(self): + def test_quota_defaults_no_tenant(self): self.run_command('quota-defaults') self.assert_called('GET', '/os-quota-sets/tenant_id/defaults') @@ -1013,6 +1013,15 @@ def test_quota_update(self): self.assert_called('PUT', '/os-quota-sets/97f4c221bff44578b0300df4ef119353') + def test_quota_update_fixed_ip(self): + self.run_command( + 'quota-update 97f4c221bff44578b0300df4ef119353' + ' --fixed-ips=5') + self.assert_called( + 'PUT', '/os-quota-sets/97f4c221bff44578b0300df4ef119353', + {'quota_set': {'fixed_ips': 5, + 'tenant_id': '97f4c221bff44578b0300df4ef119353'}}) + def test_quota_class_show(self): self.run_command('quota-class-show test') self.assert_called('GET', '/os-quota-class-sets/test') From 609cbcef17cd0051ecb86bc60a29cf9c2be2517f Mon Sep 17 00:00:00 2001 From: Masayuki Igawa Date: Fri, 22 Mar 2013 21:29:55 +0900 Subject: [PATCH 0117/1705] Fix keypair-delete help documents The help documents of keypair-delete parameter should indicate "name". Fixes bug 1158733 Change-Id: Ib7e648d22024828f8a4e5b3b51c5ed8f6b4dc057 --- README.rst | 2 +- novaclient/v1_1/shell.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index c0416d2a4..6a4fcde1b 100644 --- a/README.rst +++ b/README.rst @@ -129,7 +129,7 @@ You'll find complete documentation on the shell by running image-meta Set or Delete metadata on an image. image-show Show details about the given image. keypair-add Create a new key pair for use with instances - keypair-delete Delete keypair by its id + keypair-delete Delete keypair by its name keypair-list Print a list of keypairs for a user list List active servers. live-migration Migrates a running instance to a new machine. diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 6f7ca2816..a0d802c62 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2039,7 +2039,7 @@ def do_keypair_add(cs, args): @utils.arg('name', metavar='', help='Keypair name to delete.') def do_keypair_delete(cs, args): - """Delete keypair by its id""" + """Delete keypair by its name""" name = args.name cs.keypairs.delete(name) From 54d4b1d355bd8be9ac358eadc425e3495d786e45 Mon Sep 17 00:00:00 2001 From: Chris Krelle Date: Fri, 22 Mar 2013 12:48:14 -0700 Subject: [PATCH 0118/1705] Update tools/pip-requires for prettytable changes pip-requires from: prettytable>=0.6,<0.7 to: prettytable>=0.6,<0.8 Change-Id: Ic04d38078ad06e43947b5e98b26576a4c51dbbb0 Authored-by: Chris Krelle --- tools/pip-requires | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/pip-requires b/tools/pip-requires index ee904d4a3..e06e6f8ec 100644 --- a/tools/pip-requires +++ b/tools/pip-requires @@ -1,5 +1,5 @@ argparse iso8601>=0.1.4 -prettytable>=0.6,<0.7 +prettytable>=0.6,<0.8 requests>=0.8 simplejson From f78617803c9b34e52086b519ba5683f015418fda Mon Sep 17 00:00:00 2001 From: Roman Podolyaka Date: Tue, 19 Mar 2013 17:27:28 +0200 Subject: [PATCH 0119/1705] Use setuptools-git to include files from the repo Fixes bug 1029511. Change-Id: I5aef29ad10ac73a9645111f993621d596321ffcf --- MANIFEST.in | 10 +++------- setup.py | 2 ++ tox.ini | 1 + 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index ecc3b8b21..2e8cbc0b4 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,10 +1,6 @@ include AUTHORS -include HACKING -include LICENSE -include README.rst include ChangeLog -include run_tests.sh tox.ini include novaclient/versioninfo -recursive-include doc * -recursive-include tests * -recursive-include tools * + +exclude .gitignore +exclude .gitreview diff --git a/setup.py b/setup.py index f1e2b7b31..1fdbbc8d1 100644 --- a/setup.py +++ b/setup.py @@ -35,6 +35,8 @@ def read_file(file_name): packages=setuptools.find_packages(exclude=['tests', 'tests.*']), install_requires=setup.parse_requirements(), cmdclass=setup.get_cmdclass(), + setup_requires=['setuptools_git>=0.4'], + include_package_data=True, classifiers=[ "Development Status :: 5 - Production/Stable", "Environment :: Console", diff --git a/tox.ini b/tox.ini index 2c4ec427d..4d5dbdcb1 100644 --- a/tox.ini +++ b/tox.ini @@ -9,6 +9,7 @@ setenv = VIRTUAL_ENV={envdir} deps = -r{toxinidir}/tools/pip-requires -r{toxinidir}/tools/test-requires + setuptools_git>=0.4 commands = python setup.py testr --testr-args='{posargs}' [testenv:pep8] From 114bd74fd2c4358ae16c0b86faf9b43c8d686344 Mon Sep 17 00:00:00 2001 From: Andrew Laski Date: Mon, 25 Mar 2013 13:38:28 -0400 Subject: [PATCH 0120/1705] Remove extraneous output during testing During shell tests commands were being passed to novaclient and output was being printed to stdout. This quickly scrolls useful test output offscreen, so lets suppress it. Also removed a print call from a test. Change-Id: I31c8bf2f92a64d781c9e3350213f2e1503b960ad --- tests/v1_1/contrib/test_tenant_networks.py | 1 - tests/v1_1/test_shell.py | 13 +++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/v1_1/contrib/test_tenant_networks.py b/tests/v1_1/contrib/test_tenant_networks.py index b66cc5b96..a96a83149 100644 --- a/tests/v1_1/contrib/test_tenant_networks.py +++ b/tests/v1_1/contrib/test_tenant_networks.py @@ -38,7 +38,6 @@ def test_list_tenant_networks(self): def test_get_tenant_network(self): net = cs.tenant_networks.get(1) cs.assert_called('GET', '/os-tenant-networks/1') - print(net) def test_create_tenant_networks(self): cs.tenant_networks.create(label="net", diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index 13b018a0e..4d67523f2 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -71,8 +71,10 @@ def setUp(self): lambda *_: fakes.FakeClient)) self.addCleanup(timeutils.clear_time_override) + @mock.patch('sys.stdout', StringIO.StringIO()) def run_command(self, cmd): self.shell.main(cmd.split()) + return sys.stdout.getvalue() def assert_called(self, method, url, body=None, **kwargs): return self.shell.cs.assert_called(method, url, body, **kwargs) @@ -488,15 +490,14 @@ def test_list_with_flavors(self): self.run_command('list --flavor 1') self.assert_called('GET', '/servers/detail?flavor=1') - @mock.patch('sys.stdout', StringIO.StringIO()) def test_list_fields(self): - self.run_command('list --fields ' + output = self.run_command('list --fields ' 'host,security_groups,OS-EXT-MOD:some_thing') self.assert_called('GET', '/servers/detail') - self.assertIn('computenode1', sys.stdout.getvalue()) - self.assertIn('securitygroup1', sys.stdout.getvalue()) - self.assertIn('OS-EXT-MOD: Some Thing', sys.stdout.getvalue()) - self.assertIn('mod_some_thing_value', sys.stdout.getvalue()) + self.assertIn('computenode1', output) + self.assertIn('securitygroup1', output) + self.assertIn('OS-EXT-MOD: Some Thing', output) + self.assertIn('mod_some_thing_value', output) def test_reboot(self): self.run_command('reboot sample-server') From 46cd432bc24ededc55560e84ef5ccd30384a69ff Mon Sep 17 00:00:00 2001 From: Andrew Laski Date: Mon, 25 Mar 2013 13:03:38 -0400 Subject: [PATCH 0121/1705] Split commands properly for bash completion test. The bash completion test was trying to loop over some options in order to test that they appeared in the output. This splits the list so that proper looping occurs. This helps protect against test failures when new options are added and the bash completion output changes order. Change-Id: I81517038953fb429e8d98b762c77749bbbc0e8d7 --- tests/test_shell.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/test_shell.py b/tests/test_shell.py index 4a8c3391a..7e71c5c92 100644 --- a/tests/test_shell.py +++ b/tests/test_shell.py @@ -102,7 +102,12 @@ def test_help_no_options(self): def test_bash_completion(self): stdout, stderr = self.shell('bash-completion') # just check we have some output - required = ['--matching --wrap help secgroup-delete-rule --priority'] + required = [ + '.*--matching', + '.*--wrap', + '.*help', + '.*secgroup-delete-rule', + '.*--priority'] for r in required: self.assertThat((stdout + stderr), matchers.MatchesRegex(r, re.DOTALL | re.MULTILINE)) From 0f7404d7a8f5b6948ae050f6bd434483be42522d Mon Sep 17 00:00:00 2001 From: Andrew Laski Date: Mon, 25 Mar 2013 12:56:34 -0400 Subject: [PATCH 0122/1705] Add support for retrieving instance-actions info Adds an extension for querying instance-actions info on an instance. There are two new commands: 'instance-action-list ' which lists actions that have been recorded for that instance, and 'instance-action ' which provides more details for the action with that request_id on that server. Change-Id: I22492d682d50b78f522f10269221fea9483df6dd --- novaclient/v1_1/contrib/instance_action.py | 65 +++++++++++++++++++++ tests/v1_1/contrib/test_instance_actions.py | 41 +++++++++++++ tests/v1_1/fakes.py | 20 +++++++ tests/v1_1/test_shell.py | 9 +++ 4 files changed, 135 insertions(+) create mode 100644 novaclient/v1_1/contrib/instance_action.py create mode 100644 tests/v1_1/contrib/test_instance_actions.py diff --git a/novaclient/v1_1/contrib/instance_action.py b/novaclient/v1_1/contrib/instance_action.py new file mode 100644 index 000000000..5e9c64db7 --- /dev/null +++ b/novaclient/v1_1/contrib/instance_action.py @@ -0,0 +1,65 @@ +# Copyright 2013 Rackspace Hosting +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import pprint + +from novaclient import base +from novaclient import utils + + +class InstanceActionManager(base.ManagerWithFind): + resource_class = base.Resource + + def get(self, server, request_id): + """ + Get details of an action performed on an instance. + + :param request_id: The request_id of the action to get. + """ + return self._get("/servers/%s/os-instance-actions/%s" % + (base.getid(server), request_id), 'instanceAction') + + def list(self, server): + """ + Get a list of actions performed on an server. + """ + return self._list('/servers/%s/os-instance-actions' % + base.getid(server), 'instanceActions') + + +@utils.arg('server', + metavar='', + help='Name or UUID of the server to show an action for.') +@utils.arg('request_id', + metavar='', + help='Request ID of the action to get.') +def do_instance_action(cs, args): + """Show an action.""" + server = utils.find_resource(cs.servers, args.server) + action_resource = cs.instance_action.get(server, args.request_id) + action = action_resource._info + if 'events' in action: + action['events'] = pprint.pformat(action['events']) + utils.print_dict(action) + + +@utils.arg('server', + metavar='', + help='Name or UUID of the server to list actions for.') +def do_instance_action_list(cs, args): + """List actions on a server.""" + server = utils.find_resource(cs.servers, args.server) + actions = cs.instance_action.list(server) + utils.print_list(actions, ['Action', 'Request_ID', 'Message']) diff --git a/tests/v1_1/contrib/test_instance_actions.py b/tests/v1_1/contrib/test_instance_actions.py new file mode 100644 index 000000000..8f8e1cd58 --- /dev/null +++ b/tests/v1_1/contrib/test_instance_actions.py @@ -0,0 +1,41 @@ +# Copyright 2013 Rackspace Hosting +# All Rights Reserved. +# +# 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 novaclient import extension +from novaclient.v1_1.contrib import instance_action + +from tests import utils +from tests.v1_1.contrib import fakes + + +extensions = [ + extension.Extension(instance_action.__name__.split(".")[-1], + instance_action), +] +cs = fakes.FakeClient(extensions=extensions) + + +class InstanceActionExtensionTests(utils.TestCase): + def test_list_instance_actions(self): + server_uuid = '1234' + cs.instance_action.list(server_uuid) + cs.assert_called('GET', '/servers/%s/os-instance-actions' % + server_uuid) + + def test_get_instance_action(self): + server_uuid = '1234' + request_id = 'req-abcde12345' + cs.instance_action.get(server_uuid, request_id) + cs.assert_called('GET', '/servers/%s/os-instance-actions/%s' % + (server_uuid, request_id)) diff --git a/tests/v1_1/fakes.py b/tests/v1_1/fakes.py index 7f082d5fd..27e1cfbfe 100644 --- a/tests/v1_1/fakes.py +++ b/tests/v1_1/fakes.py @@ -1693,3 +1693,23 @@ def post_servers_1234_os_volume_attachments(self, **kw): def delete_servers_1234_os_volume_attachments_Work(self, **kw): return (200, {}, {}) + + def get_servers_1234_os_instance_actions(self, **kw): + return (200, {}, {"instanceActions": + [{"instance_uuid": "1234", + "user_id": "b968c25e04ab405f9fe4e6ca54cce9a5", + "start_time": "2013-03-25T13:45:09.000000", + "request_id": "req-abcde12345", + "action": "create", + "message": None, + "project_id": "04019601fe3648c0abd4f4abfb9e6106"}]}) + + def get_servers_1234_os_instance_actions_req_abcde12345(self, **kw): + return (200, {}, {"instanceAction": + {"instance_uuid": "1234", + "user_id": "b968c25e04ab405f9fe4e6ca54cce9a5", + "start_time": "2013-03-25T13:45:09.000000", + "request_id": "req-abcde12345", + "action": "create", + "message": None, + "project_id": "04019601fe3648c0abd4f4abfb9e6106"}}) diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index 13b018a0e..337a653b5 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -1275,3 +1275,12 @@ def test_volume_detach(self): self.run_command('volume-detach sample-server Work') self.assert_called('DELETE', '/servers/1234/os-volume_attachments/Work') + + def test_instance_action_list(self): + self.run_command('instance-action-list sample-server') + self.assert_called('GET', '/servers/1234/os-instance-actions') + + def test_instance_action_get(self): + self.run_command('instance-action sample-server req-abcde12345') + self.assert_called('GET', + '/servers/1234/os-instance-actions/req-abcde12345') From d7f1a71311dc8da730d281ac8c73618d3c00dc02 Mon Sep 17 00:00:00 2001 From: Andrew Laski Date: Mon, 25 Mar 2013 16:41:46 -0400 Subject: [PATCH 0123/1705] Don't check build/ for pep8 violations There's no need to run pep8 on the build dir, and it contains E502 false positives due to some part of the build process adding line continuations in places that they're not needed. Change-Id: I7ea19aea2b9e46503aa8acc06ce6b9d7ea18113a --- run_tests.sh | 3 ++- tox.ini | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/run_tests.sh b/run_tests.sh index a3a74cdcf..a42d0d3d6 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -103,7 +103,8 @@ function copy_subunit_log { function run_pep8 { echo "Running pep8 ..." - srcfiles="--exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg ." + srcfiles="--exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*" + srcfiles+=",*egg,build ." # Just run PEP8 in current environment # ignore="--ignore=E12,E711,E721,E712" diff --git a/tox.ini b/tox.ini index 4d5dbdcb1..e4458844a 100644 --- a/tox.ini +++ b/tox.ini @@ -16,7 +16,7 @@ commands = python setup.py testr --testr-args='{posargs}' deps = pep8==1.3.3 commands = pep8 --ignore=E12,E711,E721,E712 --show-source \ - --exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg . + --exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build . [testenv:venv] commands = {posargs} From 94173a3f989c87d0dce1ecdc0ad733b4621fea44 Mon Sep 17 00:00:00 2001 From: Roman Podolyaka Date: Wed, 27 Mar 2013 16:48:02 +0200 Subject: [PATCH 0124/1705] Use correct filter name for listing of instances nova list --tenant tenant_id leads to the following query to Nova API: GET /v2/{admin_tenant_id}/servers/detail?project_id={tenant_id} While Nova actually expects: GET /v2/{admin_tenant_id}/servers/detail?tenant_id={tenant_id} Fixes bug 1134382. Change-Id: I222208bcc9aaf547cd0b1c52dc8856123a823b8e --- novaclient/v1_1/shell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index a0d802c62..d4f24501e 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -916,7 +916,7 @@ def do_list(cs, args): 'image': imageid, 'flavor': flavorid, 'status': args.status, - 'project_id': args.tenant, + 'tenant_id': args.tenant, 'host': args.host, 'instance_name': args.instance_name} From 23ee4b818d800d08abf123a65c69213c0eff0900 Mon Sep 17 00:00:00 2001 From: Christoph Gysin Date: Wed, 27 Mar 2013 20:39:09 +0200 Subject: [PATCH 0125/1705] setuptools: remove data_files section versioninfo is already included through include_package_data Change-Id: I53c28bd26a19d86704c918fa185cde9759140dc1 --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index 1fdbbc8d1..f5343cc00 100644 --- a/setup.py +++ b/setup.py @@ -50,5 +50,4 @@ def read_file(file_name): entry_points={ "console_scripts": ["nova = novaclient.shell:main"] }, - data_files=[('novaclient', ['novaclient/versioninfo'])] ) From 0206c2d5dd37c3ce1440b5083574979129f12e77 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 28 Mar 2013 06:34:53 +0100 Subject: [PATCH 0126/1705] Update to latest openstack.common.setup. We actually don't need the silly versioninfo file at all anymore. Change-Id: Ic759c39a29b07d41a96849db84a7f9990ec8a3eb --- MANIFEST.in | 1 - novaclient/openstack/common/setup.py | 282 ++++++++++++++------------- setup.py | 12 +- 3 files changed, 153 insertions(+), 142 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 2e8cbc0b4..86ea96825 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,5 @@ include AUTHORS include ChangeLog -include novaclient/versioninfo exclude .gitignore exclude .gitreview diff --git a/novaclient/openstack/common/setup.py b/novaclient/openstack/common/setup.py index 61c01dc87..d2a1fbd58 100644 --- a/novaclient/openstack/common/setup.py +++ b/novaclient/openstack/common/setup.py @@ -1,6 +1,7 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2011 OpenStack Foundation +# Copyright 2011 OpenStack Foundation. +# Copyright 2012-2013 Hewlett-Packard Development Company, L.P. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -19,7 +20,7 @@ Utilities with minimum-depends for use in setup.py """ -import datetime +import email import os import re import subprocess @@ -33,20 +34,26 @@ def parse_mailmap(mailmap='.mailmap'): if os.path.exists(mailmap): with open(mailmap, 'r') as fp: for l in fp: - l = l.strip() - if not l.startswith('#') and ' ' in l: - canonical_email, alias = [x for x in l.split(' ') - if x.startswith('<')] - mapping[alias] = canonical_email + try: + canonical_email, alias = re.match( + r'[^#]*?(<.+>).*(<.+>).*', l).groups() + except AttributeError: + continue + mapping[alias] = canonical_email return mapping +def _parse_git_mailmap(git_dir, mailmap='.mailmap'): + mailmap = os.path.join(os.path.dirname(git_dir), mailmap) + return parse_mailmap(mailmap) + + def canonicalize_emails(changelog, mapping): """Takes in a string and an email alias mapping and replaces all instances of the aliases in the string with their real email. """ - for alias, email in mapping.iteritems(): - changelog = changelog.replace(alias, email) + for alias, email_address in mapping.iteritems(): + changelog = changelog.replace(alias, email_address) return changelog @@ -106,24 +113,18 @@ def parse_dependency_links(requirements_files=['requirements.txt', return dependency_links -def write_requirements(): - venv = os.environ.get('VIRTUAL_ENV', None) - if venv is not None: - with open("requirements.txt", "w") as req_file: - output = subprocess.Popen(["pip", "-E", venv, "freeze", "-l"], - stdout=subprocess.PIPE) - requirements = output.communicate()[0].strip() - req_file.write(requirements) - - -def _run_shell_command(cmd): +def _run_shell_command(cmd, throw_on_error=False): if os.name == 'nt': output = subprocess.Popen(["cmd.exe", "/C", cmd], - stdout=subprocess.PIPE) + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) else: output = subprocess.Popen(["/bin/sh", "-c", cmd], - stdout=subprocess.PIPE) + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) out = output.communicate() + if output.returncode and throw_on_error: + raise Exception("%s returned %d" % cmd, output.returncode) if len(out) == 0: return None if len(out[0].strip()) == 0: @@ -131,65 +132,26 @@ def _run_shell_command(cmd): return out[0].strip() -def _get_git_next_version_suffix(branch_name): - datestamp = datetime.datetime.now().strftime('%Y%m%d') - if branch_name == 'milestone-proposed': - revno_prefix = "r" - else: - revno_prefix = "" - _run_shell_command("git fetch origin +refs/meta/*:refs/remotes/meta/*") - milestone_cmd = "git show meta/openstack/release:%s" % branch_name - milestonever = _run_shell_command(milestone_cmd) - if milestonever: - first_half = "%s~%s" % (milestonever, datestamp) - else: - first_half = datestamp - - post_version = _get_git_post_version() - # post version should look like: - # 0.1.1.4.gcc9e28a - # where the bit after the last . is the short sha, and the bit between - # the last and second to last is the revno count - (revno, sha) = post_version.split(".")[-2:] - second_half = "%s%s.%s" % (revno_prefix, revno, sha) - return ".".join((first_half, second_half)) - - -def _get_git_current_tag(): - return _run_shell_command("git tag --contains HEAD") - - -def _get_git_tag_info(): - return _run_shell_command("git describe --tags") - - -def _get_git_post_version(): - current_tag = _get_git_current_tag() - if current_tag is not None: - return current_tag - else: - tag_info = _get_git_tag_info() - if tag_info is None: - base_version = "0.0" - cmd = "git --no-pager log --oneline" - out = _run_shell_command(cmd) - revno = len(out.split("\n")) - sha = _run_shell_command("git describe --always") - else: - tag_infos = str(tag_info).split("-") - base_version = "-".join(tag_infos[:-2]) - (revno, sha) = tag_infos[-2:] - return "%s.%s.%s" % (base_version, revno, sha) +def _get_git_directory(): + parent_dir = os.path.dirname(__file__) + while True: + git_dir = os.path.join(parent_dir, '.git') + if os.path.exists(git_dir): + return git_dir + parent_dir, child = os.path.split(parent_dir) + if not child: # reached to root dir + return None def write_git_changelog(): """Write a changelog based on the git changelog.""" new_changelog = 'ChangeLog' + git_dir = _get_git_directory() if not os.getenv('SKIP_WRITE_GIT_CHANGELOG'): - if os.path.isdir('.git'): - git_log_cmd = 'git log --stat' + if git_dir: + git_log_cmd = 'git --git-dir=%s log' % git_dir changelog = _run_shell_command(git_log_cmd) - mailmap = parse_mailmap() + mailmap = _parse_git_mailmap(git_dir) with open(new_changelog, "w") as changelog_file: changelog_file.write(canonicalize_emails(changelog, mailmap)) else: @@ -201,13 +163,23 @@ def generate_authors(): jenkins_email = 'jenkins@review.(openstack|stackforge).org' old_authors = 'AUTHORS.in' new_authors = 'AUTHORS' + git_dir = _get_git_directory() if not os.getenv('SKIP_GENERATE_AUTHORS'): - if os.path.isdir('.git'): + if git_dir: # don't include jenkins email address in AUTHORS file - git_log_cmd = ("git log --format='%aN <%aE>' | sort -u | " + git_log_cmd = ("git --git-dir=" + git_dir + + " log --format='%aN <%aE>' | sort -u | " "egrep -v '" + jenkins_email + "'") changelog = _run_shell_command(git_log_cmd) - mailmap = parse_mailmap() + signed_cmd = ("git log --git-dir=" + git_dir + + " | grep -i Co-authored-by: | sort -u") + signed_entries = _run_shell_command(signed_cmd) + if signed_entries: + new_entries = "\n".join( + [signed.split(":", 1)[1].strip() + for signed in signed_entries.split("\n") if signed]) + changelog = "\n".join((changelog, new_entries)) + mailmap = _parse_git_mailmap(git_dir) with open(new_authors, 'w') as new_authors_fh: new_authors_fh.write(canonicalize_emails(changelog, mailmap)) if os.path.exists(old_authors): @@ -227,26 +199,6 @@ def generate_authors(): """ -def read_versioninfo(project): - """Read the versioninfo file. If it doesn't exist, we're in a github - zipball, and there's really no way to know what version we really - are, but that should be ok, because the utility of that should be - just about nil if this code path is in use in the first place.""" - versioninfo_path = os.path.join(project, 'versioninfo') - if os.path.exists(versioninfo_path): - with open(versioninfo_path, 'r') as vinfo: - version = vinfo.read().strip() - else: - version = "0.0.0" - return version - - -def write_versioninfo(project, version): - """Write a simple file containing the version of the package.""" - with open(os.path.join(project, 'versioninfo'), 'w') as fil: - fil.write("%s\n" % version) - - def get_cmdclass(): """Return dict of commands to run from setup.py.""" @@ -274,12 +226,21 @@ def run(self): # just ignore it try: from sphinx.setup_command import BuildDoc + from sphinx import application + + class LocalSphinx(application.Sphinx): + + def __init__(self, *args, **kwargs): + kwargs['warningiserror'] = True + super(LocalSphinx, self).__init__(*args, **kwargs) class LocalBuildDoc(BuildDoc): - def generate_autoindex(self): - print("**Autodocumenting from %s" % os.path.abspath(os.curdir)) + + builders = ['html', 'man'] + + def generate_autoindex(self, option_dict): + print "**Autodocumenting from %s" % os.path.abspath(os.curdir) modules = {} - option_dict = self.distribution.get_option_dict('build_sphinx') source_dir = os.path.join(option_dict['source_dir'][1], 'api') if not os.path.exists(source_dir): os.makedirs(source_dir) @@ -302,65 +263,116 @@ def generate_autoindex(self): values = dict(module=module, heading=heading, underline=underline) - print("Generating %s" % output_filename) + print "Generating %s" % output_filename with open(output_filename, 'w') as output_file: output_file.write(_rst_template % values) autoindex.write(" %s.rst\n" % module) def run(self): - if not os.getenv('SPHINX_DEBUG'): - self.generate_autoindex() - - for builder in ['html', 'man']: + option_dict = self.distribution.get_option_dict( + 'build_sphinx') + if ('autoindex' in option_dict and + not os.getenv('SPHINX_DEBUG')): + self.generate_autoindex(option_dict) + if 'warnerrors' in option_dict: + application.Sphinx = LocalSphinx + + for builder in self.builders: self.builder = builder self.finalize_options() self.project = self.distribution.get_name() self.version = self.distribution.get_version() self.release = self.distribution.get_version() BuildDoc.run(self) + + class LocalBuildLatex(LocalBuildDoc): + builders = ['latex'] + cmdclass['build_sphinx'] = LocalBuildDoc + cmdclass['build_sphinx_latex'] = LocalBuildLatex except ImportError: pass return cmdclass -def get_git_branchname(): - for branch in _run_shell_command("git branch --color=never").split("\n"): - if branch.startswith('*'): - _branch_name = branch.split()[1].strip() - if _branch_name == "(no": - _branch_name = "no-branch" - return _branch_name +def _get_revno(git_dir): + """Return the number of commits since the most recent tag. + We use git-describe to find this out, but if there are no + tags then we fall back to counting commits since the beginning + of time. + """ + describe = _run_shell_command( + "git --git-dir=%s describe --always" % git_dir) + if "-" in describe: + return describe.rsplit("-", 2)[-2] -def get_pre_version(projectname, base_version): - """Return a version which is leading up to a version that will - be released in the future.""" - if os.path.isdir('.git'): - current_tag = _get_git_current_tag() - if current_tag is not None: - version = current_tag - else: - branch_name = os.getenv('BRANCHNAME', - os.getenv('GERRIT_REFNAME', - get_git_branchname())) - version_suffix = _get_git_next_version_suffix(branch_name) - version = "%s~%s" % (base_version, version_suffix) - write_versioninfo(projectname, version) - return version - else: - version = read_versioninfo(projectname) - return version + # no tags found + revlist = _run_shell_command( + "git --git-dir=%s rev-list --abbrev-commit HEAD" % git_dir) + return len(revlist.splitlines()) -def get_post_version(projectname): +def _get_version_from_git(pre_version): """Return a version which is equal to the tag that's on the current revision if there is one, or tag plus number of additional revisions if the current revision has no tag.""" - if os.path.isdir('.git'): - version = _get_git_post_version() - write_versioninfo(projectname, version) + git_dir = _get_git_directory() + if git_dir: + if pre_version: + try: + return _run_shell_command( + "git --git-dir=" + git_dir + " describe --exact-match", + throw_on_error=True).replace('-', '.') + except Exception: + sha = _run_shell_command( + "git --git-dir=" + git_dir + " log -n1 --pretty=format:%h") + return "%s.a%s.g%s" % (pre_version, _get_revno(git_dir), sha) + else: + return _run_shell_command( + "git --git-dir=" + git_dir + " describe --always").replace( + '-', '.') + return None + + +def _get_version_from_pkg_info(package_name): + """Get the version from PKG-INFO file if we can.""" + try: + pkg_info_file = open('PKG-INFO', 'r') + except (IOError, OSError): + return None + try: + pkg_info = email.message_from_file(pkg_info_file) + except email.MessageError: + return None + # Check to make sure we're in our own dir + if pkg_info.get('Name', None) != package_name: + return None + return pkg_info.get('Version', None) + + +def get_version(package_name, pre_version=None): + """Get the version of the project. First, try getting it from PKG-INFO, if + it exists. If it does, that means we're in a distribution tarball or that + install has happened. Otherwise, if there is no PKG-INFO file, pull the + version from git. + + We do not support setup.py version sanity in git archive tarballs, nor do + we support packagers directly sucking our git repo into theirs. We expect + that a source tarball be made from our git repo - or that if someone wants + to make a source tarball from a fork of our repo with additional tags in it + that they understand and desire the results of doing that. + """ + version = os.environ.get("OSLO_PACKAGE_VERSION", None) + if version: + return version + version = _get_version_from_pkg_info(package_name) + if version: + return version + version = _get_version_from_git(pre_version) + if version: return version - return read_versioninfo(projectname) + raise Exception("Versioning for this project requires either an sdist" + " tarball, or access to an upstream git repository.") diff --git a/setup.py b/setup.py index f5343cc00..1e4c1cfba 100644 --- a/setup.py +++ b/setup.py @@ -21,14 +21,14 @@ def read_file(file_name): return open(os.path.join(os.path.dirname(__file__), file_name)).read() - +project = 'python-novaclient' setuptools.setup( - name="python-novaclient", - version=setup.get_post_version('novaclient'), - author="Rackspace, based on work by Jacob Kaplan-Moss", - author_email="github@racklabs.com", - description="Client library for OpenStack Nova API.", + name=project, + version=setup.get_version(project), + author='OpenStack', + author_email='openstack-dev@lists.openstack.org', + description="Client library for OpenStack Compute API.", long_description=read_file("README.rst"), license="Apache License, Version 2.0", url="https://github.com/openstack/python-novaclient", From cdab77259a5410fe4e89e90e2d83b316216d0e9b Mon Sep 17 00:00:00 2001 From: Roman Podolyaka Date: Thu, 28 Mar 2013 13:28:37 +0200 Subject: [PATCH 0127/1705] Ensure shell tests use isolated env variables set Fixes bug 1161008. Change-Id: Id9d76f39c898d4cff65aefa636e9dd2f092d42bd --- tests/test_shell.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/test_shell.py b/tests/test_shell.py index 7e71c5c92..433b220be 100644 --- a/tests/test_shell.py +++ b/tests/test_shell.py @@ -20,10 +20,8 @@ class ShellTest(utils.TestCase): def make_env(self, exclude=None): - for var, val in FAKE_ENV.items(): - if var == exclude: - continue - self.useFixture(fixtures.EnvironmentVariable(var, val)) + env = dict((k, v) for k, v in FAKE_ENV.items() if k != exclude) + self.useFixture(fixtures.MonkeyPatch('os.environ', env)) def setUp(self): super(ShellTest, self).setUp() From adb5941df991543b4383817a3e6f30daad42879c Mon Sep 17 00:00:00 2001 From: Davanum Srinivas Date: Mon, 18 Mar 2013 12:02:25 -0400 Subject: [PATCH 0128/1705] catch NoKeyringDaemonError from gnomekeyring Looks like we need to add more exceptions, start to maintain a tuple of exceptions Change-Id: I3a027f5d2d8f82fe397e3096ff82358040f3729e --- novaclient/shell.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/novaclient/shell.py b/novaclient/shell.py index 8b4aea512..ccd0a45a6 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -31,17 +31,18 @@ import sys HAS_KEYRING = False +all_errors = ValueError try: import keyring HAS_KEYRING = True try: if isinstance(keyring.get_keyring(), keyring.backend.GnomeKeyring): import gnomekeyring - KeyringIOError = gnomekeyring.IOError - else: - KeyringIOError = IOError + all_errors = (ValueError, + gnomekeyring.IOError, + gnomekeyring.NoKeyringDaemonError) except Exception: - KeyringIOError = IOError + pass except ImportError: pass @@ -156,7 +157,7 @@ def management_url(self): block = keyring.get_password('novaclient_auth', self._make_key()) if block: _token, management_url, _tenant_id = block.split('|', 2) - except (KeyringIOError, ValueError): + except all_errors: pass return management_url @@ -173,7 +174,7 @@ def auth_token(self): block = keyring.get_password('novaclient_auth', self._make_key()) if block: token, _management_url, _tenant_id = block.split('|', 2) - except (KeyringIOError, ValueError): + except all_errors: pass return token @@ -186,7 +187,7 @@ def tenant_id(self): block = keyring.get_password('novaclient_auth', self._make_key()) if block: _token, _management_url, tenant_id = block.split('|', 2) - except (KeyringIOError, ValueError): + except all_errors: pass return tenant_id From 7f0df56ea355bbccd8fb5db4d557dbbf126991af Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Sun, 31 Mar 2013 21:16:26 -0400 Subject: [PATCH 0129/1705] Skip security groups w/ no protocol. When using Nova w/ the new Quantum security groups driver security groups can exist which have no protocol. Theses entries cause ERROR: 'NoneType' object has no attribute 'upper' when you try to delete other (editable) security groups. This patch updates novaclient so that it skips over entries with no protocol when determining which security group to delete. This fixes novaclient so that deleting *any* security group works when the new quantum security group driver is enabled. Fixes LP Bug #1162622. Change-Id: Ida07b6429eae988a9a64535381082a500f31a521 --- novaclient/v1_1/shell.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index a0d802c62..f269dc3bf 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1885,7 +1885,8 @@ def do_secgroup_delete_rule(cs, args): secgroup = _get_secgroup(cs, args.secgroup) for rule in secgroup.rules: - if (rule['ip_protocol'].upper() == args.ip_proto.upper() and + if (rule['ip_protocol'] and + rule['ip_protocol'].upper() == args.ip_proto.upper() and rule['from_port'] == int(args.from_port) and rule['to_port'] == int(args.to_port) and rule['ip_range']['cidr'] == args.cidr): From abd75f24b1ffc0afab51f7f6c09e3759e5465823 Mon Sep 17 00:00:00 2001 From: Alvaro Lopez Garcia Date: Wed, 6 Mar 2013 16:41:46 +0100 Subject: [PATCH 0130/1705] Improve authentication plugins management. The current auth plugin system lacks some functionality to be used with other methods that might require additional configuration options or that do not require a user to pass some options that are now compulsory (for example, X.509 authentication needs to get a certificate file, and does not need either a username or a password). This commit extends the current system to handle these extra features, while remaining compatible with older plugins. DocImpact: We should documment how to implement additional authentication plugins, such as BasicAuth, X509, etc. Implements: blueprint authentication-plugins Change-Id: I7b0ef4981efba8160dea94bf852dba7e2e4068f5 --- novaclient/auth_plugin.py | 141 ++++++++++++++++++++++++++ novaclient/client.py | 29 ++---- novaclient/shell.py | 40 ++++++-- novaclient/utils.py | 10 ++ novaclient/v1_1/client.py | 2 + tests/test_auth_plugins.py | 203 +++++++++++++++++++++++++++++++++---- 6 files changed, 377 insertions(+), 48 deletions(-) create mode 100644 novaclient/auth_plugin.py diff --git a/novaclient/auth_plugin.py b/novaclient/auth_plugin.py new file mode 100644 index 000000000..39da86aa6 --- /dev/null +++ b/novaclient/auth_plugin.py @@ -0,0 +1,141 @@ +# Copyright 2013 OpenStack Foundation +# Copyright 2013 Spanish National Research Council. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging +import pkg_resources + +from novaclient import exceptions +from novaclient import utils + + +logger = logging.getLogger(__name__) + + +_discovered_plugins = {} + + +def discover_auth_systems(): + """Discover the available auth-systems. + + This won't take into account the old style auth-systems. + """ + ep_name = 'openstack.client.auth_plugin' + for ep in pkg_resources.iter_entry_points(ep_name): + try: + auth_plugin = ep.load() + except (ImportError, pkg_resources.UnknownExtra, AttributeError) as e: + logger.debug("ERROR: Cannot load auth plugin %s" % ep.name) + logger.debug(e, exc_info=1) + else: + _discovered_plugins[ep.name] = auth_plugin + + +def load_auth_system_opts(parser): + """Load options needed by the available auth-systems into a parser. + + This function will try to populate the parser with options from the + available plugins. + """ + for name, auth_plugin in _discovered_plugins.iteritems(): + add_opts_fn = getattr(auth_plugin, "add_opts", None) + if add_opts_fn: + group = parser.add_argument_group("Auth-system '%s' options" % + name) + add_opts_fn(group) + + +def load_plugin(auth_system): + if auth_system in _discovered_plugins: + return _discovered_plugins[auth_system]() + + # NOTE(aloga): If we arrive here, the plugin will be an old-style one, + # so we have to create a fake AuthPlugin for it. + return DeprecatedAuthPlugin(auth_system) + + +class BaseAuthPlugin(object): + """Base class for authentication plugins. + + An authentication plugin needs to override at least the authenticate + method to be a valid plugin. + """ + def __init__(self): + self.opts = {} + + def get_auth_url(self): + """Return the auth url for the plugin (if any).""" + return None + + @staticmethod + def add_opts(parser): + """Populate and return the parser with the options for this plugin. + + If the plugin does not need any options, it should return the same + parser untouched. + """ + return parser + + def parse_opts(self, args): + """Parse the actual auth-system options if any. + + This method is expected to populate the attribute self.opts with a + dict containing the options and values needed to make authentication. + If the dict is empty, the client should assume that it needs the same + options as the 'keystone' auth system (i.e. os_username and + os_password). + + Returns the self.opts dict. + """ + return self.opts + + def authenticate(self, cls, auth_url): + """Authenticate using plugin defined method.""" + raise exceptions.AuthSystemNotFound(self.auth_system) + + +class DeprecatedAuthPlugin(object): + """Class to mimic the AuthPlugin class for deprecated auth systems. + + Old auth systems only define two entry points: openstack.client.auth_url + and openstack.client.authenticate. This class will load those entry points + into a class similar to a valid AuthPlugin. + """ + def __init__(self, auth_system): + self.auth_system = auth_system + + def authenticate(cls, auth_url): + raise exceptions.AuthSystemNotFound(self.auth_system) + + self.opts = {} + + self.get_auth_url = lambda: None + self.authenticate = authenticate + + self._load_endpoints() + + def _load_endpoints(self): + ep_name = 'openstack.client.auth_url' + fn = utils._load_entry_point(ep_name, name=self.auth_system) + if fn: + self.get_auth_url = fn + + ep_name = 'openstack.client.authenticate' + fn = utils._load_entry_point(ep_name, name=self.auth_system) + if fn: + self.authenticate = fn + + def parse_opts(self, args): + return self.opts diff --git a/novaclient/client.py b/novaclient/client.py index 73595701c..e73a6dd4d 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -31,15 +31,6 @@ from novaclient import utils -def get_auth_system_url(auth_system): - """Load plugin-based auth_url""" - ep_name = 'openstack.client.auth_url' - for ep in pkg_resources.iter_entry_points(ep_name): - if ep.name == auth_system: - return ep.load()() - raise exceptions.AuthSystemNotFound(auth_system) - - class HTTPClient(object): USER_AGENT = 'python-novaclient' @@ -52,12 +43,17 @@ def __init__(self, user, password, projectid, auth_url=None, timings=False, bypass_url=None, os_cache=False, no_cache=True, http_log_debug=False, auth_system='keystone', + auth_plugin=None, cacert=None): self.user = user self.password = password self.projectid = projectid + + if auth_system and auth_system != 'keystone' and not auth_plugin: + raise exceptions.AuthSystemNotFound(auth_system) + if not auth_url and auth_system and auth_system != 'keystone': - auth_url = get_auth_system_url(auth_system) + auth_url = auth_plugin.get_auth_url() if not auth_url: raise exceptions.EndpointNotFound() self.auth_url = auth_url.rstrip('/') @@ -94,6 +90,7 @@ def __init__(self, user, password, projectid, auth_url=None, self.verify_cert = True self.auth_system = auth_system + self.auth_plugin = auth_plugin self._logger = logging.getLogger(__name__) if self.http_log_debug: @@ -392,12 +389,7 @@ def _v1_auth(self, url): raise exceptions.from_response(resp, body, url) def _plugin_auth(self, auth_url): - """Load plugin-based authentication""" - ep_name = 'openstack.client.authenticate' - for ep in pkg_resources.iter_entry_points(ep_name): - if ep.name == self.auth_system: - return ep.load()(self, auth_url) - raise exceptions.AuthSystemNotFound(self.auth_system) + self.auth_plugin.authenticate(self, auth_url) def _v2_auth(self, url): """Authenticate against a v2.0 auth service.""" @@ -414,7 +406,7 @@ def _v2_auth(self, url): self._authenticate(url, body) - def _authenticate(self, url, body): + def _authenticate(self, url, body, **kwargs): """Authenticate and extract the service catalog.""" token_url = url + "/tokens" @@ -423,7 +415,8 @@ def _authenticate(self, url, body): token_url, "POST", body=body, - allow_redirects=True) + allow_redirects=True, + **kwargs) return self._extract_service_catalog(url, resp, body) diff --git a/novaclient/shell.py b/novaclient/shell.py index 8b4aea512..074c17948 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -46,6 +46,7 @@ pass import novaclient +import novaclient.auth_plugin from novaclient import client from novaclient import exceptions as exc import novaclient.extension @@ -398,6 +399,9 @@ def get_base_parser(self): parser.add_argument('--bypass_url', help=argparse.SUPPRESS) + # The auth-system-plugins might require some extra options + novaclient.auth_plugin.load_auth_system_opts(parser) + return parser def get_subcommand_parser(self, version): @@ -514,11 +518,15 @@ def setup_debugging(self, debug): format=streamformat) def main(self, argv): + # Parse args once to find version and debug settings parser = self.get_base_parser() (options, args) = parser.parse_known_args(argv) self.setup_debugging(options.debug) + # Discover available auth plugins + novaclient.auth_plugin.discover_auth_systems() + # build available subcommands based on version self.extensions = self._discover_extensions( options.os_compute_api_version) @@ -566,6 +574,11 @@ def main(self, argv): args.bypass_url, args.os_cache, args.os_cacert, args.timeout) + if os_auth_system and os_auth_system != "keystone": + auth_plugin = novaclient.auth_plugin.load_plugin(os_auth_system) + else: + auth_plugin = None + # Fetched and set later as needed os_password = None @@ -579,12 +592,16 @@ def main(self, argv): #FIXME(usrleon): Here should be restrict for project id same as # for os_username or os_password but for compatibility it is not. if not utils.isunauthenticated(args.func): - if not os_username: - if not username: - raise exc.CommandError("You must provide a username " - "via either --os-username or env[OS_USERNAME]") - else: - os_username = username + if auth_plugin: + auth_plugin.parse_opts(args) + + if not auth_plugin or not auth_plugin.opts: + if not os_username: + if not username: + raise exc.CommandError("You must provide a username " + "via either --os-username or env[OS_USERNAME]") + else: + os_username = username if not os_tenant_name: if not projectid: @@ -597,8 +614,7 @@ def main(self, argv): if not os_auth_url: if not url: if os_auth_system and os_auth_system != 'keystone': - os_auth_url = \ - client.get_auth_system_url(os_auth_system) + os_auth_url = auth_plugin.get_auth_url() else: os_auth_url = url @@ -627,6 +643,7 @@ def main(self, argv): region_name=os_region_name, endpoint_type=endpoint_type, extensions=self.extensions, service_type=service_type, service_name=service_name, auth_system=os_auth_system, + auth_plugin=auth_plugin, volume_service_name=volume_service_name, timings=args.timings, bypass_url=bypass_url, os_cache=os_cache, http_log_debug=options.debug, @@ -636,7 +653,12 @@ def main(self, argv): # identifying keyring key can come from the underlying client if not utils.isunauthenticated(args.func): helper = SecretsHelper(args, self.cs.client) - use_pw = True + if (auth_plugin and auth_plugin.opts and + "os_password" not in auth_plugin.opts): + use_pw = False + else: + use_pw = True + tenant_id, auth_token, management_url = (helper.tenant_id, helper.auth_token, helper.management_url) diff --git a/novaclient/utils.py b/novaclient/utils.py index 67a5e53b6..280bef0d2 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -1,4 +1,5 @@ import os +import pkg_resources import re import sys import textwrap @@ -369,3 +370,12 @@ def check_uuid_like(val): raise exceptions.CommandError( "error: Invalid tenant-id %s supplied" % val) + + +def _load_entry_point(ep_name, name=None): + """Try to load the entry point ep_name that matches name.""" + for ep in pkg_resources.iter_entry_points(ep_name, name=name): + try: + return ep.load() + except (ImportError, pkg_resources.UnknownExtra, AttributeError): + continue diff --git a/novaclient/v1_1/client.py b/novaclient/v1_1/client.py index cae702d1f..7b93cc369 100644 --- a/novaclient/v1_1/client.py +++ b/novaclient/v1_1/client.py @@ -74,6 +74,7 @@ def __init__(self, username, api_key, project_id, auth_url=None, volume_service_name=None, timings=False, bypass_url=None, os_cache=False, no_cache=True, http_log_debug=False, auth_system='keystone', + auth_plugin=None, cacert=None): # FIXME(comstud): Rename the api_key argument above when we # know it's not being used as keyword argument @@ -132,6 +133,7 @@ def __init__(self, username, api_key, project_id, auth_url=None, insecure=insecure, timeout=timeout, auth_system=auth_system, + auth_plugin=auth_plugin, proxy_token=proxy_token, proxy_tenant_id=proxy_tenant_id, region_name=region_name, diff --git a/tests/test_auth_plugins.py b/tests/test_auth_plugins.py index fca506959..0cd638af3 100644 --- a/tests/test_auth_plugins.py +++ b/tests/test_auth_plugins.py @@ -13,6 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. +import argparse import mock import pkg_resources import requests @@ -22,6 +23,7 @@ except ImportError: import simplejson as json +from novaclient import auth_plugin from novaclient import exceptions from novaclient.v1_1 import client from tests import utils @@ -71,7 +73,7 @@ def requested_headers(cs): } -class AuthPluginTest(utils.TestCase): +class DeprecatedAuthPluginTest(utils.TestCase): def test_auth_system_success(self): class MockEntrypoint(pkg_resources.EntryPoint): def load(self): @@ -80,9 +82,11 @@ def load(self): def authenticate(self, cls, auth_url): cls._authenticate(auth_url, {"fake": "me"}) - def mock_iter_entry_points(_type): + def mock_iter_entry_points(_type, name): if _type == 'openstack.client.authenticate': return [MockEntrypoint("fake", "fake", ["fake"])] + else: + return [] mock_request = mock_http_request() @@ -90,8 +94,10 @@ def mock_iter_entry_points(_type): mock_iter_entry_points) @mock.patch.object(requests, "request", mock_request) def test_auth_call(): + plugin = auth_plugin.DeprecatedAuthPlugin("fake") cs = client.Client("username", "password", "project_id", - "auth_url/v2.0", auth_system="fake") + "auth_url/v2.0", auth_system="fake", + auth_plugin=plugin) cs.client.authenticate() headers = requested_headers(cs) @@ -108,7 +114,7 @@ def test_auth_call(): test_auth_call() def test_auth_system_not_exists(self): - def mock_iter_entry_points(_t): + def mock_iter_entry_points(_t, name=None): return [pkg_resources.EntryPoint("fake", "fake", ["fake"])] mock_request = mock_http_request() @@ -117,8 +123,11 @@ def mock_iter_entry_points(_t): mock_iter_entry_points) @mock.patch.object(requests, "request", mock_request) def test_auth_call(): + auth_plugin.discover_auth_systems() + plugin = auth_plugin.DeprecatedAuthPlugin("notexists") cs = client.Client("username", "password", "project_id", - "auth_url/v2.0", auth_system="notexists") + "auth_url/v2.0", auth_system="notexists", + auth_plugin=plugin) self.assertRaises(exceptions.AuthSystemNotFound, cs.client.authenticate) @@ -139,29 +148,35 @@ def load(self): def authenticate(self, cls, auth_url): cls._authenticate(auth_url, {"fake": "me"}) - def mock_iter_entry_points(_type): + def mock_iter_entry_points(_type, name): if _type == 'openstack.client.auth_url': return [MockAuthUrlEntrypoint("fakewithauthurl", - "fakewithauthurl.plugin", + "fakewithauthurl", ["auth_url"])] elif _type == 'openstack.client.authenticate': return [MockAuthenticateEntrypoint("fakewithauthurl", - "fakewithauthurl.plugin", - ["auth_url"])] + "fakewithauthurl", + ["authenticate"])] + else: + return [] + mock_request = mock_http_request() @mock.patch.object(pkg_resources, "iter_entry_points", mock_iter_entry_points) @mock.patch.object(requests, "request", mock_request) def test_auth_call(): + plugin = auth_plugin.DeprecatedAuthPlugin("fakewithauthurl") cs = client.Client("username", "password", "project_id", - auth_system="fakewithauthurl") + auth_system="fakewithauthurl", + auth_plugin=plugin) cs.client.authenticate() self.assertEquals(cs.client.auth_url, "http://faked/v2.0") test_auth_call() - def test_auth_system_raises_exception_when_missing_auth_url(self): + @mock.patch.object(pkg_resources, "iter_entry_points") + def test_client_raises_exc_without_auth_url(self, mock_iter_entry_points): class MockAuthUrlEntrypoint(pkg_resources.EntryPoint): def load(self): return self.auth_url @@ -169,17 +184,163 @@ def load(self): def auth_url(self): return None - def mock_iter_entry_points(_type): - return [MockAuthUrlEntrypoint("fakewithauthurl", - "fakewithauthurl.plugin", - ["auth_url"])] + mock_iter_entry_points.side_effect = lambda _t, name: [ + MockAuthUrlEntrypoint("fakewithauthurl", + "fakewithauthurl", + ["auth_url"])] - @mock.patch.object(pkg_resources, "iter_entry_points", - mock_iter_entry_points) - def test_auth_call(): - self.assertRaises( + plugin = auth_plugin.DeprecatedAuthPlugin("fakewithauthurl") + self.assertRaises( + exceptions.EndpointNotFound, + client.Client, "username", "password", "project_id", + auth_system="fakewithauthurl", auth_plugin=plugin) + + +class AuthPluginTest(utils.TestCase): + @mock.patch.object(requests, "request") + @mock.patch.object(pkg_resources, "iter_entry_points") + def test_auth_system_success(self, mock_iter_entry_points, mock_request): + """Test that we can authenticate using the auth system.""" + class MockEntrypoint(pkg_resources.EntryPoint): + def load(self): + return FakePlugin + + class FakePlugin(auth_plugin.BaseAuthPlugin): + def authenticate(self, cls, auth_url): + cls._authenticate(auth_url, {"fake": "me"}) + + mock_iter_entry_points.side_effect = lambda _t: [ + MockEntrypoint("fake", "fake", ["FakePlugin"])] + + mock_request.side_effect = mock_http_request() + + auth_plugin.discover_auth_systems() + plugin = auth_plugin.load_plugin("fake") + cs = client.Client("username", "password", "project_id", + "auth_url/v2.0", auth_system="fake", + auth_plugin=plugin) + cs.client.authenticate() + + headers = requested_headers(cs) + token_url = cs.client.auth_url + "/tokens" + + mock_request.assert_called_with( + "POST", + token_url, + headers=headers, + data='{"fake": "me"}', + allow_redirects=True, + **self.TEST_REQUEST_BASE) + + @mock.patch.object(pkg_resources, "iter_entry_points") + def test_discover_auth_system_options(self, mock_iter_entry_points): + """Test that we can load the auth system options.""" + class FakePlugin(auth_plugin.BaseAuthPlugin): + @staticmethod + def add_opts(parser): + parser.add_argument('--auth_system_opt', + default=False, + action='store_true', + help="Fake option") + return parser + + class MockEntrypoint(pkg_resources.EntryPoint): + def load(self): + return FakePlugin + + mock_iter_entry_points.side_effect = lambda _t: [ + MockEntrypoint("fake", "fake", ["FakePlugin"])] + + parser = argparse.ArgumentParser() + auth_plugin.discover_auth_systems() + auth_plugin.load_auth_system_opts(parser) + opts, args = parser.parse_known_args(['--auth_system_opt']) + + self.assertTrue(opts.auth_system_opt) + + @mock.patch.object(pkg_resources, "iter_entry_points") + def test_parse_auth_system_options(self, mock_iter_entry_points): + """Test that we can parse the auth system options.""" + class MockEntrypoint(pkg_resources.EntryPoint): + def load(self): + return FakePlugin + + class FakePlugin(auth_plugin.BaseAuthPlugin): + def __init__(self): + self.opts = {"fake_argument": True} + + def parse_opts(self, args): + return self.opts + + mock_iter_entry_points.side_effect = lambda _t: [ + MockEntrypoint("fake", "fake", ["FakePlugin"])] + + auth_plugin.discover_auth_systems() + plugin = auth_plugin.load_plugin("fake") + + plugin.parse_opts([]) + self.assertIn("fake_argument", plugin.opts) + + @mock.patch.object(pkg_resources, "iter_entry_points") + def test_auth_system_defining_url(self, mock_iter_entry_points): + """Test the auth_system defining an url.""" + class MockEntrypoint(pkg_resources.EntryPoint): + def load(self): + return FakePlugin + + class FakePlugin(auth_plugin.BaseAuthPlugin): + def get_auth_url(self): + return "http://faked/v2.0" + + mock_iter_entry_points.side_effect = lambda _t: [ + MockEntrypoint("fake", "fake", ["FakePlugin"])] + + auth_plugin.discover_auth_systems() + plugin = auth_plugin.load_plugin("fake") + + cs = client.Client("username", "password", "project_id", + auth_system="fakewithauthurl", + auth_plugin=plugin) + self.assertEquals(cs.client.auth_url, "http://faked/v2.0") + + @mock.patch.object(pkg_resources, "iter_entry_points") + def test_exception_if_no_authenticate(self, mock_iter_entry_points): + """Test that no authenticate raises a proper exception.""" + class MockEntrypoint(pkg_resources.EntryPoint): + def load(self): + return FakePlugin + + class FakePlugin(auth_plugin.BaseAuthPlugin): + pass + + mock_iter_entry_points.side_effect = lambda _t: [ + MockEntrypoint("fake", "fake", ["FakePlugin"])] + + auth_plugin.discover_auth_systems() + plugin = auth_plugin.load_plugin("fake") + + self.assertRaises( exceptions.EndpointNotFound, client.Client, "username", "password", "project_id", - auth_system="fakewithauthurl") + auth_system="fake", auth_plugin=plugin) - test_auth_call() + @mock.patch.object(pkg_resources, "iter_entry_points") + def test_exception_if_no_url(self, mock_iter_entry_points): + """Test that no auth_url at all raises exception.""" + class MockEntrypoint(pkg_resources.EntryPoint): + def load(self): + return FakePlugin + + class FakePlugin(auth_plugin.BaseAuthPlugin): + pass + + mock_iter_entry_points.side_effect = lambda _t: [ + MockEntrypoint("fake", "fake", ["FakePlugin"])] + + auth_plugin.discover_auth_systems() + plugin = auth_plugin.load_plugin("fake") + + self.assertRaises( + exceptions.EndpointNotFound, + client.Client, "username", "password", "project_id", + auth_system="fake", auth_plugin=plugin) From 49ab03e6a68fd5b02aaba213e7ab94468f6600ad Mon Sep 17 00:00:00 2001 From: Alan Pevec Date: Tue, 2 Apr 2013 12:16:37 +0200 Subject: [PATCH 0131/1705] do not ignore --os-cache Change-Id: Ib8808da00967163faa9ce05e580605f4e499891d --- novaclient/shell.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/novaclient/shell.py b/novaclient/shell.py index 8b4aea512..dd7ce8a5f 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -149,7 +149,7 @@ def password(self): @property def management_url(self): - if not HAS_KEYRING: + if not HAS_KEYRING or not self.args.os_cache: return None management_url = None try: @@ -166,7 +166,7 @@ def auth_token(self): # want to look into the keyring module, if it # exists and see if anything was provided in that # file that we can use. - if not HAS_KEYRING: + if not HAS_KEYRING or not self.args.os_cache: return None token = None try: @@ -179,7 +179,7 @@ def auth_token(self): @property def tenant_id(self): - if not HAS_KEYRING: + if not HAS_KEYRING or not self.args.os_cache: return None tenant_id = None try: From bc2ee484f56fd105b4a66ce047fd1c422c68772a Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Tue, 2 Apr 2013 08:51:39 -0700 Subject: [PATCH 0132/1705] Remove actions command from servers. It appears that this command was inherited from the original cloudservers client code. It hasn't ever worked with nova. It is confusing now because we have the instance actions command. Fixes bug 1163033 Change-Id: Id0b36c01cdbd5034d0a7886b809269d838c36b45 --- novaclient/v1_1/servers.py | 9 --------- novaclient/v1_1/shell.py | 9 --------- tests/v1_1/fakes.py | 14 -------------- tests/v1_1/test_servers.py | 12 ------------ tests/v1_1/test_shell.py | 6 ------ 5 files changed, 50 deletions(-) diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index 5d96b1334..c65303fad 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -177,10 +177,6 @@ def diagnostics(self): """Diagnostics -- Retrieve server diagnostics.""" return self.manager.diagnostics(self) - def actions(self): - """Actions -- Retrieve server actions.""" - return self.manager.actions(self) - def migrate(self): """ Migrate a server to a new host. @@ -534,11 +530,6 @@ def diagnostics(self, server): return self.api.client.get("/servers/%s/diagnostics" % base.getid(server)) - def actions(self, server): - """Retrieve server actions.""" - return self._list("/servers/%s/actions" % base.getid(server), - "actions") - def create(self, name, image, flavor, meta=None, files=None, reservation_id=None, min_count=None, max_count=None, security_groups=None, userdata=None, diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index a0d802c62..d0c417ba0 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1129,15 +1129,6 @@ def do_diagnostics(cs, args): utils.print_dict(cs.servers.diagnostics(server)[1]) -@utils.arg('server', metavar='', help='Name or ID of server.') -def do_actions(cs, args): - """Retrieve server actions.""" - server = _find_server(cs, args.server) - utils.print_list( - cs.servers.actions(server), - ["Created_At", "Action", "Error"]) - - @utils.arg('server', metavar='', help='Name or ID of server.') def do_root_password(cs, args): """ diff --git a/tests/v1_1/fakes.py b/tests/v1_1/fakes.py index 27e1cfbfe..06510642c 100644 --- a/tests/v1_1/fakes.py +++ b/tests/v1_1/fakes.py @@ -398,20 +398,6 @@ def post_servers_1234_metadata(self, **kw): def get_servers_1234_diagnostics(self, **kw): return (200, {}, {'data': 'Fake diagnostics'}) - def get_servers_1234_actions(self, **kw): - return (200, {}, {'actions': [ - { - 'action': 'rebuild', - 'error': None, - 'created_at': '2011-12-30 11:45:36' - }, - { - 'action': 'reboot', - 'error': 'Failed!', - 'created_at': '2011-12-30 11:40:29' - }, - ]}) - # # Server Addresses # diff --git a/tests/v1_1/test_servers.py b/tests/v1_1/test_servers.py index f281ee663..d5156032c 100644 --- a/tests/v1_1/test_servers.py +++ b/tests/v1_1/test_servers.py @@ -338,18 +338,6 @@ def test_clear_password(self): s.clear_password() cs.assert_called('DELETE', '/servers/1234/os-server-password') - def test_get_server_actions(self): - s = cs.servers.get(1234) - actions = s.actions() - self.assertTrue(actions is not None) - cs.assert_called('GET', '/servers/1234/actions') - - actions_from_manager = cs.servers.actions(1234) - self.assertTrue(actions_from_manager is not None) - cs.assert_called('GET', '/servers/1234/actions') - - self.assertEqual(actions, actions_from_manager) - def test_get_server_diagnostics(self): s = cs.servers.get(1234) diagnostics = s.diagnostics() diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index 2febe7e08..455fe5ea5 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -628,12 +628,6 @@ def test_diagnostics(self): self.run_command('diagnostics sample-server') self.assert_called('GET', '/servers/1234/diagnostics') - def test_actions(self): - self.run_command('actions 1234') - self.assert_called('GET', '/servers/1234/actions') - self.run_command('actions sample-server') - self.assert_called('GET', '/servers/1234/actions') - def test_set_meta_set(self): self.run_command('meta 1234 set key1=val1 key2=val2') self.assert_called('POST', '/servers/1234/metadata', From 5073b8080ad28bc45c22acc748919cfb2695533b Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Tue, 2 Apr 2013 15:43:47 -0400 Subject: [PATCH 0133/1705] Fix mispelt x-auth-token header Fixes bug 1163546 Change-Id: I4b40ee2be950ee2cd13217f954d72fe1e42a1d6c --- novaclient/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/client.py b/novaclient/client.py index e73a6dd4d..4f5c539f6 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -294,7 +294,7 @@ def _fetch_endpoints_from_auth(self, url): % (self.proxy_token, self.proxy_tenant_id)]) self._logger.debug("Using Endpoint URL: %s" % url) resp, body = self._time_request( - url, "GET", headers={'X-Auth_Token': self.auth_token}) + url, "GET", headers={'X-Auth-Token': self.auth_token}) return self._extract_service_catalog(url, resp, body, extract_token=False) From 90b3a1c505570eae29c74c197a9800916d8bffcd Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Tue, 2 Apr 2013 15:48:44 -0400 Subject: [PATCH 0134/1705] Allow for bypass_url when using proxy_token Change-Id: I1cb76f79fbf2fe02ce012aaa278f50987c073831 --- novaclient/client.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/novaclient/client.py b/novaclient/client.py index e73a6dd4d..6695b37f0 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -335,7 +335,10 @@ def authenticate(self): # existing token? If so, our actual endpoints may # be different than that of the admin token. if self.proxy_token: - self._fetch_endpoints_from_auth(admin_url) + if self.bypass_url: + self.set_management_url(self.bypass_url) + else: + self._fetch_endpoints_from_auth(admin_url) # Since keystone no longer returns the user token # with the endpoints any more, we need to replace # our service account token with the user token. From 20ede68edee89340994d0a6d42122c7484062abf Mon Sep 17 00:00:00 2001 From: Kurt Taylor Date: Tue, 2 Apr 2013 18:36:18 -0400 Subject: [PATCH 0135/1705] Fix IBM copyright strings Update IBM copyright strings to one consistant format Change-Id: Iabd059132cc3092b6dbbaa6c1f19fb93acee0a30 --- novaclient/v1_1/agents.py | 2 +- novaclient/v1_1/coverage_ext.py | 2 +- novaclient/v1_1/fixed_ips.py | 2 +- novaclient/v1_1/floating_ips_bulk.py | 2 +- novaclient/v1_1/services.py | 2 +- tests/v1_1/test_agents.py | 2 +- tests/v1_1/test_coverage_ext.py | 2 +- tests/v1_1/test_fixed_ips.py | 2 +- tests/v1_1/test_floating_ips_bulk.py | 2 +- tests/v1_1/test_services.py | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/novaclient/v1_1/agents.py b/novaclient/v1_1/agents.py index 17e3cad31..069662732 100644 --- a/novaclient/v1_1/agents.py +++ b/novaclient/v1_1/agents.py @@ -1,6 +1,6 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2012 IBM +# Copyright 2012 IBM Corp. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/novaclient/v1_1/coverage_ext.py b/novaclient/v1_1/coverage_ext.py index 6364da419..9003cc77f 100644 --- a/novaclient/v1_1/coverage_ext.py +++ b/novaclient/v1_1/coverage_ext.py @@ -1,4 +1,4 @@ -# Copyright 2012 IBM +# Copyright 2012 IBM Corp. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/novaclient/v1_1/fixed_ips.py b/novaclient/v1_1/fixed_ips.py index a45db99fc..8f9d46c62 100644 --- a/novaclient/v1_1/fixed_ips.py +++ b/novaclient/v1_1/fixed_ips.py @@ -1,6 +1,6 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2012 IBM +# Copyright 2012 IBM Corp. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/novaclient/v1_1/floating_ips_bulk.py b/novaclient/v1_1/floating_ips_bulk.py index b98a5b66a..1eeaaaa23 100644 --- a/novaclient/v1_1/floating_ips_bulk.py +++ b/novaclient/v1_1/floating_ips_bulk.py @@ -1,6 +1,6 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2012 IBM +# Copyright 2012 IBM Corp. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/novaclient/v1_1/services.py b/novaclient/v1_1/services.py index 7aa26f7bf..13805db4b 100644 --- a/novaclient/v1_1/services.py +++ b/novaclient/v1_1/services.py @@ -1,6 +1,6 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2012 IBM +# Copyright 2012 IBM Corp. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/tests/v1_1/test_agents.py b/tests/v1_1/test_agents.py index 14ddcf7fc..f599046e4 100644 --- a/tests/v1_1/test_agents.py +++ b/tests/v1_1/test_agents.py @@ -1,6 +1,6 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2012 IBM +# Copyright 2012 IBM Corp. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/tests/v1_1/test_coverage_ext.py b/tests/v1_1/test_coverage_ext.py index 4596c8789..9e6f4fc9e 100644 --- a/tests/v1_1/test_coverage_ext.py +++ b/tests/v1_1/test_coverage_ext.py @@ -1,6 +1,6 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2012 IBM +# Copyright 2012 IBM Corp. # # 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 diff --git a/tests/v1_1/test_fixed_ips.py b/tests/v1_1/test_fixed_ips.py index 6548755c2..fe4eab272 100644 --- a/tests/v1_1/test_fixed_ips.py +++ b/tests/v1_1/test_fixed_ips.py @@ -1,6 +1,6 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2012 IBM +# Copyright 2012 IBM Corp. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/tests/v1_1/test_floating_ips_bulk.py b/tests/v1_1/test_floating_ips_bulk.py index 21981081c..c7706eca0 100644 --- a/tests/v1_1/test_floating_ips_bulk.py +++ b/tests/v1_1/test_floating_ips_bulk.py @@ -1,6 +1,6 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2012 IBM +# Copyright 2012 IBM Corp. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/tests/v1_1/test_services.py b/tests/v1_1/test_services.py index 25759e270..701b4cb0e 100644 --- a/tests/v1_1/test_services.py +++ b/tests/v1_1/test_services.py @@ -1,6 +1,6 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2012 IBM +# Copyright 2012 IBM Corp. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may From ea94b09ad712adcf932a4f217e4061522ad875d1 Mon Sep 17 00:00:00 2001 From: Haiwei Xu Date: Thu, 28 Mar 2013 19:42:00 +0900 Subject: [PATCH 0136/1705] Make "multi_host" True when it is set to 'T' in network_create. Fixes bug 1161297 Even if creating a network with the "--multi_host=T", the "multi_host" property of the new network is still "False". This is because nova server interprets 'T' to "False". This patch fixes the problem. Change-Id: I171c7dc72cb515c47ea106558080eafa10dee873 --- novaclient/v1_1/shell.py | 4 ++++ tests/v1_1/test_shell.py | 25 +++++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 4b0241438..696b40d9a 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -26,6 +26,7 @@ import time from novaclient import exceptions +from novaclient.openstack.common import strutils from novaclient.openstack.common import timeutils from novaclient import utils from novaclient.v1_1 import availability_zones @@ -708,6 +709,9 @@ def do_network_create(cs, args): raise exceptions.CommandError( "Must specify eith fixed_range_v4 or fixed_range_v6") kwargs = _filter_network_create_options(args) + if args.multi_host is not None: + kwargs['multi_host'] = bool(args.multi_host == 'T' or + strutils.bool_from_string(args.multi_host)) cs.networks.create(**kwargs) diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index 455fe5ea5..bf5c812e0 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -1088,6 +1088,31 @@ def test_network_create_invalid(self): cmd = 'network-create 10.0.1.0' self.assertRaises(exceptions.CommandError, self.run_command, cmd) + def test_network_create_multi_host(self): + self.run_command('network-create --fixed-range-v4 192.168.0.0/24' + ' --multi-host=T new_network') + body = {'network': {'cidr': '192.168.0.0/24', 'label': 'new_network', + 'multi_host': True}} + self.assert_called('POST', '/os-networks', body) + + self.run_command('network-create --fixed-range-v4 192.168.0.0/24' + ' --multi-host=True new_network') + body = {'network': {'cidr': '192.168.0.0/24', 'label': 'new_network', + 'multi_host': True}} + self.assert_called('POST', '/os-networks', body) + + self.run_command('network-create --fixed-range-v4 192.168.0.0/24' + ' --multi-host=1 new_network') + body = {'network': {'cidr': '192.168.0.0/24', 'label': 'new_network', + 'multi_host': True}} + self.assert_called('POST', '/os-networks', body) + + self.run_command('network-create --fixed-range-v4 192.168.1.0/24' + ' --multi-host=F new_network') + body = {'network': {'cidr': '192.168.1.0/24', 'label': 'new_network', + 'multi_host': False}} + self.assert_called('POST', '/os-networks', body) + def test_add_fixed_ip(self): self.run_command('add-fixed-ip sample-server 1') self.assert_called('POST', '/servers/1234/action', From 8ce233024700f70f23d26c5f68b0ffa445eccc32 Mon Sep 17 00:00:00 2001 From: Davanum Srinivas Date: Sat, 6 Apr 2013 23:07:48 -0400 Subject: [PATCH 0137/1705] Fix problem with nova --version Update to latest openstack.common.version.py and fix __init__.py to get "nova --version" to work properly again Fixes LP# 1165325 Change-Id: I29e54cd4cf79759407f3967518e9be575abd994a --- novaclient/__init__.py | 26 +++---- novaclient/openstack/common/version.py | 94 ++++++++++++++++++++++++++ openstack-common.conf | 2 +- 3 files changed, 103 insertions(+), 19 deletions(-) create mode 100644 novaclient/openstack/common/version.py diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 2cc5baa6c..dc9ab76da 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -11,23 +11,13 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -import inspect -import os +from novaclient.openstack.common import version -def _get_novaclient_version(): - """Read version from versioninfo file.""" - mod_abspath = inspect.getabsfile(inspect.currentframe()) - novaclient_path = os.path.dirname(mod_abspath) - version_path = os.path.join(novaclient_path, 'versioninfo') - - if os.path.exists(version_path): - version = open(version_path).read().strip() - else: - version = "Unknown, couldn't find versioninfo file at %s"\ - % version_path - - return version - - -__version__ = _get_novaclient_version() +version_info = version.VersionInfo('python-novaclient') +# We have a circular import problem when we first run python setup.py sdist +# It's harmless, so deflect it. +try: + __version__ = version_info.version_string() +except AttributeError: + __version__ = None diff --git a/novaclient/openstack/common/version.py b/novaclient/openstack/common/version.py new file mode 100644 index 000000000..7dc643436 --- /dev/null +++ b/novaclient/openstack/common/version.py @@ -0,0 +1,94 @@ + +# Copyright 2012 OpenStack Foundation +# Copyright 2012-2013 Hewlett-Packard Development Company, L.P. +# +# 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. + +""" +Utilities for consuming the version from pkg_resources. +""" + +import pkg_resources + + +class VersionInfo(object): + + def __init__(self, package): + """Object that understands versioning for a package + :param package: name of the python package, such as glance, or + python-glanceclient + """ + self.package = package + self.release = None + self.version = None + self._cached_version = None + + def __str__(self): + """Make the VersionInfo object behave like a string.""" + return self.version_string() + + def __repr__(self): + """Include the name.""" + return "VersionInfo(%s:%s)" % (self.package, self.version_string()) + + def _get_version_from_pkg_resources(self): + """Get the version of the package from the pkg_resources record + associated with the package.""" + try: + requirement = pkg_resources.Requirement.parse(self.package) + provider = pkg_resources.get_provider(requirement) + return provider.version + except pkg_resources.DistributionNotFound: + # The most likely cause for this is running tests in a tree + # produced from a tarball where the package itself has not been + # installed into anything. Revert to setup-time logic. + from novaclient.openstack.common import setup + return setup.get_version(self.package) + + def release_string(self): + """Return the full version of the package including suffixes indicating + VCS status. + """ + if self.release is None: + self.release = self._get_version_from_pkg_resources() + + return self.release + + def version_string(self): + """Return the short version minus any alpha/beta tags.""" + if self.version is None: + parts = [] + for part in self.release_string().split('.'): + if part[0].isdigit(): + parts.append(part) + else: + break + self.version = ".".join(parts) + + return self.version + + # Compatibility functions + canonical_version_string = version_string + version_string_with_vcs = release_string + + def cached_version_string(self, prefix=""): + """Generate an object which will expand in a string context to + the results of version_string(). We do this so that don't + call into pkg_resources every time we start up a program when + passing version information into the CONF constructor, but + rather only do the calculation when and if a version is requested + """ + if not self._cached_version: + self._cached_version = "%s%s" % (prefix, + self.version_string()) + return self._cached_version diff --git a/openstack-common.conf b/openstack-common.conf index 833c69a16..05e8eb83d 100644 --- a/openstack-common.conf +++ b/openstack-common.conf @@ -1,7 +1,7 @@ [DEFAULT] # The list of modules to copy from openstack-common -modules=setup,timeutils,strutils +modules=setup,timeutils,strutils,version # The base module to hold the copy of openstack.common base=novaclient From 69f9971da54b46a8883148e4cef6346c7933b6ec Mon Sep 17 00:00:00 2001 From: "Kevin L. Mitchell" Date: Mon, 8 Apr 2013 16:38:07 -0500 Subject: [PATCH 0138/1705] Correct a unit test failure that crept into trunk At some point, the "nova list" command started returning an empty table when no results were obtained, while the tests.test_shell.ShellTest.test_password test was expecting a single newline. Whichever commit caused this change in behavior somehow made it past the gate without forcing the test to be updated. This commit fixes the issue by changing the output that test_shell.test_password is expecting. Fixes bug 1166464. Change-Id: I57636b4a1e525c440925caba0bbb51bbcd94b080 --- tests/test_shell.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test_shell.py b/tests/test_shell.py index 433b220be..ee9a5d7cc 100644 --- a/tests/test_shell.py +++ b/tests/test_shell.py @@ -150,7 +150,11 @@ def test_no_auth_url(self): def test_password(self, mock_getpass, mock_stdin): self.make_env(exclude='OS_PASSWORD') stdout, stderr = self.shell('list') - self.assertEqual((stdout + stderr), '\n') + self.assertEqual((stdout + stderr), + '+----+------+--------+----------+\n' + '| ID | Name | Status | Networks |\n' + '+----+------+--------+----------+\n' + '+----+------+--------+----------+\n') @mock.patch('sys.stdin', side_effect=mock.MagicMock) @mock.patch('getpass.getpass', side_effect=EOFError) From 1216a32a287ac3c276c4ad15c70a12ab3f5d9dba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20G=C3=B3rski?= Date: Mon, 8 Apr 2013 07:04:11 -0700 Subject: [PATCH 0139/1705] Fixing shell command 'service-disable' description Fixes: bug #1166217 Change-Id: I0ba609750551540f9cd91492191b222961e99fb1 --- novaclient/v1_1/shell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 696b40d9a..4a0194c6e 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2408,7 +2408,7 @@ def do_service_enable(cs, args): @utils.arg('host', metavar='', help='Name of host.') @utils.arg('binary', metavar='', help='Service binary.') def do_service_disable(cs, args): - """Enable the service""" + """Disable the service""" result = cs.services.disable(args.host, args.binary) utils.print_list([result], ['Host', 'Binary', 'Status']) From 328805f2bb219f53c49084a52e5eba763e536ba8 Mon Sep 17 00:00:00 2001 From: Mitsuhiko Yamazaki Date: Tue, 9 Apr 2013 18:20:03 +0900 Subject: [PATCH 0140/1705] Add coverage-reset command to reset Nova coverage data. Add a new command coverage-reset to enable users to reset coverage data. This also adds unit tests on coverage-reset command. Fixes bug: 1164331 Change-Id: I101e38165206224927d8ce32c8663a8d9403450b --- novaclient/v1_1/coverage_ext.py | 6 ++++++ novaclient/v1_1/shell.py | 6 ++++++ tests/v1_1/fakes.py | 2 +- tests/v1_1/test_coverage_ext.py | 4 ++++ tests/v1_1/test_shell.py | 5 +++++ 5 files changed, 22 insertions(+), 1 deletion(-) diff --git a/novaclient/v1_1/coverage_ext.py b/novaclient/v1_1/coverage_ext.py index 9003cc77f..e6677dd2a 100644 --- a/novaclient/v1_1/coverage_ext.py +++ b/novaclient/v1_1/coverage_ext.py @@ -54,3 +54,9 @@ def report(self, filename, xml=False, html=False): self.run_hooks('modify_body_for_action', body) url = '/os-coverage/action' return self.api.client.post(url, body=body) + + def reset(self): + body = {'reset': {}} + self.run_hooks('modify_body_for_action', body) + url = '/os-coverage/action' + return self.api.client.post(url, body=body) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 4a0194c6e..8fa124841 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2519,6 +2519,12 @@ def do_coverage_report(cs, args): print("Report path: %s" % cov[-1]['path']) +def do_coverage_reset(cs, args): + """Reset coverage data.""" + cs.coverage.reset() + print("Coverage data reset") + + @utils.arg('--matching', metavar='', default=None, help='List hypervisors matching the given .') def do_hypervisor_list(cs, args): diff --git a/tests/v1_1/fakes.py b/tests/v1_1/fakes.py index 06510642c..fdd7543a2 100644 --- a/tests/v1_1/fakes.py +++ b/tests/v1_1/fakes.py @@ -1566,7 +1566,7 @@ def post_os_networks_2_action(self, **kw): return (202, {}, None) def post_os_coverage_action(self, body, **kw): - if 'start' in body: + if 'start' in body or 'reset' in body: return (200, {}, None) elif 'stop' in body: return (200, {}, {'path': '/tmp/tmpdir/'}) diff --git a/tests/v1_1/test_coverage_ext.py b/tests/v1_1/test_coverage_ext.py index 9e6f4fc9e..f2089bcf4 100644 --- a/tests/v1_1/test_coverage_ext.py +++ b/tests/v1_1/test_coverage_ext.py @@ -41,3 +41,7 @@ def test_report_coverage(self): c = cs.coverage.report('report') return_dict = {'path': '/tmp/tmpdir/report'} cs.assert_called_anytime('POST', '/os-coverage/action') + + def test_reset_coverage(self): + c = cs.coverage.reset() + cs.assert_called_anytime('POST', '/os-coverage/action') diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index bf5c812e0..5275b170a 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -957,6 +957,11 @@ def test_coverage_report_with_xml(self): body = {'report': {'xml': True, 'file': 'report'}} self.assert_called_anytime('POST', '/os-coverage/action', body) + def test_coverage_reset(self): + self.run_command('coverage-reset') + body = {'reset': {}} + self.assert_called_anytime('POST', '/os-coverage/action', body) + def test_hypervisor_list(self): self.run_command('hypervisor-list') self.assert_called('GET', '/os-hypervisors') From 2a495c03d5cac4470f65c9f4dd513f3facfa6fdb Mon Sep 17 00:00:00 2001 From: Mike Lundy Date: Wed, 10 Apr 2013 14:09:21 -0700 Subject: [PATCH 0141/1705] make sure .get() also updates _info Having the _info get out of sync with the actual attributes is kind of a trap for the unwary. _info is used in preference to the attributes in many places, and letting it get out of sync means that the method of retrieval (get() vs. list()) influences the result. Change-Id: I9d9bf086fa790b811c520b2fa317f95cb1921805 --- novaclient/base.py | 1 + tests/v1_1/test_servers.py | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/novaclient/base.py b/novaclient/base.py index db9a75487..26ca3f833 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -313,6 +313,7 @@ def _add_details(self, info): for (k, v) in info.iteritems(): try: setattr(self, k, v) + self._info[k] = v except AttributeError: # In this case we already defined the attribute on the class pass diff --git a/tests/v1_1/test_servers.py b/tests/v1_1/test_servers.py index d5156032c..e44a35189 100644 --- a/tests/v1_1/test_servers.py +++ b/tests/v1_1/test_servers.py @@ -32,6 +32,13 @@ def test_get_server_details(self): self.assertEqual(s.id, 1234) self.assertEqual(s.status, 'BUILD') + def test_get_server_promote_details(self): + s1 = cs.servers.list(detailed=False)[0] + s2 = cs.servers.list(detailed=True)[0] + self.assertNotEquals(s1._info, s2._info) + s1.get() + self.assertEquals(s1._info, s2._info) + def test_create_server(self): s = cs.servers.create( name="My server", From e8b665edbfcd86488f75ed0b79dafdedbf8f5950 Mon Sep 17 00:00:00 2001 From: gengjh Date: Mon, 8 Apr 2013 17:30:02 +0800 Subject: [PATCH 0142/1705] Support force update quota Once we have additional check when update quota in https://review.openstack.org/#/c/25887/, we need provide --force option when run 'nova quota-update'. Fix bug 1160749 DocImpact Change-Id: Ib8d94d4eaa846f620abad5fb55017ac3fb0b322a --- novaclient/v1_1/quotas.py | 5 +++-- novaclient/v1_1/shell.py | 13 ++++++++++++- tests/v1_1/test_quotas.py | 9 +++++++++ tests/v1_1/test_shell.py | 21 ++++++++++++++++++--- 4 files changed, 42 insertions(+), 6 deletions(-) diff --git a/novaclient/v1_1/quotas.py b/novaclient/v1_1/quotas.py index d0cdf02ba..c5ef0dfd7 100644 --- a/novaclient/v1_1/quotas.py +++ b/novaclient/v1_1/quotas.py @@ -36,7 +36,7 @@ def get(self, tenant_id): tenant_id = tenant_id.tenant_id return self._get("/os-quota-sets/%s" % (tenant_id), "quota_set") - def update(self, tenant_id, metadata_items=None, + def update(self, tenant_id, force=None, metadata_items=None, injected_file_content_bytes=None, injected_file_path_bytes=None, volumes=None, gigabytes=None, ram=None, floating_ips=None, fixed_ips=None, instances=None, @@ -58,7 +58,8 @@ def update(self, tenant_id, metadata_items=None, 'injected_files': injected_files, 'cores': cores, 'security_groups': security_groups, - 'security_group_rules': security_group_rules}} + 'security_group_rules': security_group_rules, + 'force': force}} for key in body['quota_set'].keys(): if body['quota_set'][key] is None: diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 4a0194c6e..55cbe5101 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -30,6 +30,7 @@ from novaclient.openstack.common import timeutils from novaclient import utils from novaclient.v1_1 import availability_zones +from novaclient.v1_1 import quotas from novaclient.v1_1 import servers @@ -2703,7 +2704,11 @@ def _quota_update(manager, identifier, args): updates[resource] = val if updates: - manager.update(identifier, **updates) + force_update = getattr(args, 'force', False) + if isinstance(manager, quotas.QuotaSetManager): + manager.update(identifier, force_update, **updates) + else: + manager.update(identifier, **updates) @utils.arg('--tenant', @@ -2812,6 +2817,12 @@ def do_quota_defaults(cs, args): type=int, default=None, help='New value for the "security-group-rules" quota.') +@utils.arg('--force', + dest='force', + action="store_true", + default=False, + help='Whether force update the quota even if the already used' + ' and reserved exceeds the new quota') def do_quota_update(cs, args): """Update the quotas for a tenant.""" diff --git a/tests/v1_1/test_quotas.py b/tests/v1_1/test_quotas.py index 85a7a576a..c7aa64cfa 100644 --- a/tests/v1_1/test_quotas.py +++ b/tests/v1_1/test_quotas.py @@ -37,6 +37,15 @@ def test_update_quota(self): cs.assert_called('PUT', '/os-quota-sets/97f4c221bff44578b0300df4ef119353') + def test_force_update_quota(self): + q = cs.quotas.get('97f4c221bff44578b0300df4ef119353') + q.update(cores=2, force=True) + cs.assert_called( + 'PUT', '/os-quota-sets/97f4c221bff44578b0300df4ef119353', + {'quota_set': {'force': True, + 'cores': 2, + 'tenant_id': '97f4c221bff44578b0300df4ef119353'}}) + def test_refresh_quota(self): q = cs.quotas.get('test') q2 = cs.quotas.get('test') diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index bf5c812e0..3de840880 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -1005,8 +1005,22 @@ def test_quota_update(self): self.run_command( 'quota-update 97f4c221bff44578b0300df4ef119353' ' --instances=5') - self.assert_called('PUT', - '/os-quota-sets/97f4c221bff44578b0300df4ef119353') + self.assert_called( + 'PUT', + '/os-quota-sets/97f4c221bff44578b0300df4ef119353', + {'quota_set': {'force': False, + 'instances': 5, + 'tenant_id': '97f4c221bff44578b0300df4ef119353'}}) + + def test_quota_force_update(self): + self.run_command( + 'quota-update 97f4c221bff44578b0300df4ef119353' + ' --instances=5 --force') + self.assert_called( + 'PUT', '/os-quota-sets/97f4c221bff44578b0300df4ef119353', + {'quota_set': {'force': True, + 'instances': 5, + 'tenant_id': '97f4c221bff44578b0300df4ef119353'}}) def test_quota_update_fixed_ip(self): self.run_command( @@ -1014,7 +1028,8 @@ def test_quota_update_fixed_ip(self): ' --fixed-ips=5') self.assert_called( 'PUT', '/os-quota-sets/97f4c221bff44578b0300df4ef119353', - {'quota_set': {'fixed_ips': 5, + {'quota_set': {'force': False, + 'fixed_ips': 5, 'tenant_id': '97f4c221bff44578b0300df4ef119353'}}) def test_quota_class_show(self): From c23081201779365354bc01346100d5422f43c2ea Mon Sep 17 00:00:00 2001 From: Haiwei Xu Date: Thu, 11 Apr 2013 18:00:00 +0900 Subject: [PATCH 0143/1705] Make --vlan option work in network-create in VLAN mode When creating a network with network-create, DuplicateVlan exception happens whatever the --vlan option is set. This is because --vlan option isn't read correctly by novaclient. This patch fixes this bug. Fixes bug 1167779 Change-Id: I3bf0e8d96d995632698f5aa1b1a07ead9e553c70 --- novaclient/v1_1/shell.py | 8 ++++---- tests/v1_1/test_shell.py | 7 +++++++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 4a0194c6e..ed7f8aa6d 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -632,9 +632,9 @@ def do_network_associate_project(cs, args): def _filter_network_create_options(args): - valid_args = ['label', 'cidr', 'vlan', 'vpn_start', 'cidr_v6', 'gateway', - 'gateway_v6', 'bridge', 'multi_host', 'dns1', 'dns2', 'uuid', - 'fixed_cidr', 'project_id', 'priority'] + valid_args = ['label', 'cidr', 'vlan_start', 'vpn_start', 'cidr_v6', + 'gateway', 'gateway_v6', 'bridge', 'multi_host', 'dns1', + 'dns2', 'uuid', 'fixed_cidr', 'project_id', 'priority'] kwargs = {} for k, v in args.__dict__.items(): if k in valid_args and v is not None: @@ -654,7 +654,7 @@ def _filter_network_create_options(args): dest="cidr_v6", help='IPv6 subnet (ex: fe80::/64') @utils.arg('--vlan', - dest='vlan', + dest='vlan_start', metavar='', help="vlan id") @utils.arg('--vpn', diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index bf5c812e0..b157dc44b 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -1113,6 +1113,13 @@ def test_network_create_multi_host(self): 'multi_host': False}} self.assert_called('POST', '/os-networks', body) + def test_network_create_vlan(self): + self.run_command('network-create --fixed-range-v4 192.168.0.0/24' + ' --vlan=200 new_network') + body = {'network': {'cidr': '192.168.0.0/24', 'label': 'new_network', + 'vlan_start': '200'}} + self.assert_called('POST', '/os-networks', body) + def test_add_fixed_ip(self): self.run_command('add-fixed-ip sample-server 1') self.assert_called('POST', '/servers/1234/action', From dccdd02e4840f27d5476d3fe06967bc2718ec386 Mon Sep 17 00:00:00 2001 From: gtt116 Date: Wed, 17 Apr 2013 13:51:37 +0000 Subject: [PATCH 0144/1705] Cleanup unused import Change-Id: Id7d110110f238077630c9b9ee4d643654844dd3e --- doc/source/conf.py | 1 - novaclient/client.py | 2 -- novaclient/v1_1/cloudpipe.py | 1 - novaclient/v1_1/contrib/baremetal.py | 3 --- novaclient/v1_1/coverage_ext.py | 2 -- setup.py | 1 - tests/v1_1/test_certs.py | 1 - tests/v1_1/test_cloudpipe.py | 1 - tests/v1_1/test_coverage_ext.py | 2 -- tests/v1_1/test_fixed_ips.py | 1 - tests/v1_1/test_floating_ip_dns.py | 1 - tests/v1_1/test_fping.py | 1 - tests/v1_1/test_networks.py | 1 - 13 files changed, 18 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index 1461bff54..7a34d4839 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -12,7 +12,6 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import os import sys # If extensions (or modules to document with autodoc) are in another directory, diff --git a/novaclient/client.py b/novaclient/client.py index 93a05ef5a..54805cb6d 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -9,11 +9,9 @@ import logging import os -import sys import time import urlparse -import pkg_resources import requests try: diff --git a/novaclient/v1_1/cloudpipe.py b/novaclient/v1_1/cloudpipe.py index de07bf68c..19a87d172 100644 --- a/novaclient/v1_1/cloudpipe.py +++ b/novaclient/v1_1/cloudpipe.py @@ -16,7 +16,6 @@ """Cloudpipe interface.""" from novaclient import base -from novaclient.v1_1 import networks class Cloudpipe(base.Resource): diff --git a/novaclient/v1_1/contrib/baremetal.py b/novaclient/v1_1/contrib/baremetal.py index 1624b8dcc..36ab047a0 100644 --- a/novaclient/v1_1/contrib/baremetal.py +++ b/novaclient/v1_1/contrib/baremetal.py @@ -16,9 +16,6 @@ """ Baremetal interface (v2 extension). """ - -import urllib - from novaclient import base from novaclient import utils diff --git a/novaclient/v1_1/coverage_ext.py b/novaclient/v1_1/coverage_ext.py index e6677dd2a..0eb91ee9e 100644 --- a/novaclient/v1_1/coverage_ext.py +++ b/novaclient/v1_1/coverage_ext.py @@ -13,8 +13,6 @@ # License for the specific language governing permissions and limitations # under the License. -import urllib - from novaclient import base diff --git a/setup.py b/setup.py index 1e4c1cfba..d896f34e6 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,6 @@ import os import setuptools -import sys from novaclient.openstack.common import setup diff --git a/tests/v1_1/test_certs.py b/tests/v1_1/test_certs.py index dc6e76784..5cfef0826 100644 --- a/tests/v1_1/test_certs.py +++ b/tests/v1_1/test_certs.py @@ -1,4 +1,3 @@ -from novaclient import exceptions from novaclient.v1_1 import certs from tests import utils from tests.v1_1 import fakes diff --git a/tests/v1_1/test_cloudpipe.py b/tests/v1_1/test_cloudpipe.py index 85ed2ff7d..ad1e5bd7a 100644 --- a/tests/v1_1/test_cloudpipe.py +++ b/tests/v1_1/test_cloudpipe.py @@ -1,4 +1,3 @@ -from novaclient import exceptions from novaclient.v1_1 import cloudpipe from tests import utils from tests.v1_1 import fakes diff --git a/tests/v1_1/test_coverage_ext.py b/tests/v1_1/test_coverage_ext.py index f2089bcf4..27b33da47 100644 --- a/tests/v1_1/test_coverage_ext.py +++ b/tests/v1_1/test_coverage_ext.py @@ -17,8 +17,6 @@ # See: http://wiki.openstack.org/Nova/CoverageExtension for more information # and usage explanation for this API extension -from novaclient import exceptions -from novaclient.v1_1 import flavors from tests import utils from tests.v1_1 import fakes diff --git a/tests/v1_1/test_fixed_ips.py b/tests/v1_1/test_fixed_ips.py index fe4eab272..d8726b657 100644 --- a/tests/v1_1/test_fixed_ips.py +++ b/tests/v1_1/test_fixed_ips.py @@ -15,7 +15,6 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.v1_1 import fixed_ips from tests.v1_1 import fakes from tests import utils diff --git a/tests/v1_1/test_floating_ip_dns.py b/tests/v1_1/test_floating_ip_dns.py index 0dd179e2e..868fc054a 100644 --- a/tests/v1_1/test_floating_ip_dns.py +++ b/tests/v1_1/test_floating_ip_dns.py @@ -1,4 +1,3 @@ -from novaclient import exceptions from novaclient.v1_1 import floating_ip_dns from tests.v1_1 import fakes from tests import utils diff --git a/tests/v1_1/test_fping.py b/tests/v1_1/test_fping.py index 4f77f7905..c17da741d 100644 --- a/tests/v1_1/test_fping.py +++ b/tests/v1_1/test_fping.py @@ -15,7 +15,6 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient import exceptions from novaclient.v1_1 import fping from tests import utils from tests.v1_1 import fakes diff --git a/tests/v1_1/test_networks.py b/tests/v1_1/test_networks.py index 5481e908c..e0834c8e2 100644 --- a/tests/v1_1/test_networks.py +++ b/tests/v1_1/test_networks.py @@ -1,4 +1,3 @@ -from novaclient import exceptions from novaclient.v1_1 import networks from tests import utils from tests.v1_1 import fakes From 27e904af69a913d1705b348e90a1dd0da58376af Mon Sep 17 00:00:00 2001 From: Roman Podolyaka Date: Thu, 18 Apr 2013 18:01:54 +0300 Subject: [PATCH 0145/1705] Use HTTP keep-alive feature in HTTPClient class Currently HTTPClient creates a new TCP connection for each API request. We could actually reuse created connections by relying upon HTTP keep-alive feature. That would enable us to do a few queries in a row more efficiently. Fixes bug 1170385. Change-Id: Ie6d8fb9670938e3790017509a242630b67abd794 --- novaclient/client.py | 4 +++- tests/test_auth_plugins.py | 8 ++++---- tests/test_client.py | 10 +++++----- tests/test_http.py | 10 +++++----- tests/v1_1/test_auth.py | 12 ++++++------ 5 files changed, 23 insertions(+), 21 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index 54805cb6d..0cf595b99 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -103,6 +103,8 @@ def __init__(self, user, password, projectid, auth_url=None, # have to set it up here on WARNING (its original level) # otherwise we will get all the requests logging messanges rql.setLevel(logging.WARNING) + # requests within the same session can reuse TCP connections from pool + self.http = requests.Session() def use_token_cache(self, use_it): self.os_cache = use_it @@ -161,7 +163,7 @@ def request(self, url, method, **kwargs): kwargs.setdefault('timeout', self.timeout) self.http_log_req((url, method,), kwargs) - resp = requests.request( + resp = self.http.request( method, url, verify=self.verify_cert, diff --git a/tests/test_auth_plugins.py b/tests/test_auth_plugins.py index 0cd638af3..85d220b25 100644 --- a/tests/test_auth_plugins.py +++ b/tests/test_auth_plugins.py @@ -92,7 +92,7 @@ def mock_iter_entry_points(_type, name): @mock.patch.object(pkg_resources, "iter_entry_points", mock_iter_entry_points) - @mock.patch.object(requests, "request", mock_request) + @mock.patch.object(requests.Session, "request", mock_request) def test_auth_call(): plugin = auth_plugin.DeprecatedAuthPlugin("fake") cs = client.Client("username", "password", "project_id", @@ -121,7 +121,7 @@ def mock_iter_entry_points(_t, name=None): @mock.patch.object(pkg_resources, "iter_entry_points", mock_iter_entry_points) - @mock.patch.object(requests, "request", mock_request) + @mock.patch.object(requests.Session, "request", mock_request) def test_auth_call(): auth_plugin.discover_auth_systems() plugin = auth_plugin.DeprecatedAuthPlugin("notexists") @@ -164,7 +164,7 @@ def mock_iter_entry_points(_type, name): @mock.patch.object(pkg_resources, "iter_entry_points", mock_iter_entry_points) - @mock.patch.object(requests, "request", mock_request) + @mock.patch.object(requests.Session, "request", mock_request) def test_auth_call(): plugin = auth_plugin.DeprecatedAuthPlugin("fakewithauthurl") cs = client.Client("username", "password", "project_id", @@ -197,7 +197,7 @@ def auth_url(self): class AuthPluginTest(utils.TestCase): - @mock.patch.object(requests, "request") + @mock.patch.object(requests.Session, "request") @mock.patch.object(pkg_resources, "iter_entry_points") def test_auth_system_success(self, mock_iter_entry_points, mock_request): """Test that we can authenticate using the auth system.""" diff --git a/tests/test_client.py b/tests/test_client.py index dde654869..1f61b1716 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -38,12 +38,12 @@ def test_client_with_timeout(self): 'x-server-management-url': 'blah.com', 'x-auth-token': 'blah', } - with mock.patch('requests.request', mock_request): + with mock.patch('requests.Session.request', mock_request): instance.authenticate() - requests.request.assert_called_with(mock.ANY, mock.ANY, - timeout=2, - headers=mock.ANY, - verify=mock.ANY) + requests.Session.request.assert_called_with(mock.ANY, mock.ANY, + timeout=2, + headers=mock.ANY, + verify=mock.ANY) def test_get_client_class_v2(self): output = novaclient.client.get_client_class('2') diff --git a/tests/test_http.py b/tests/test_http.py index 57cc481d8..e89ad3fba 100644 --- a/tests/test_http.py +++ b/tests/test_http.py @@ -43,7 +43,7 @@ class ClientTest(utils.TestCase): def test_get(self): cl = get_authed_client() - @mock.patch.object(requests, "request", mock_request) + @mock.patch.object(requests.Session, "request", mock_request) @mock.patch('time.time', mock.Mock(return_value=1234)) def test_get_call(): resp, body = cl.get("/hi") @@ -65,7 +65,7 @@ def test_get_call(): def test_post(self): cl = get_authed_client() - @mock.patch.object(requests, "request", mock_request) + @mock.patch.object(requests.Session, "request", mock_request) def test_post_call(): cl.post("/hi", body=[1, 2, 3]) headers = { @@ -88,7 +88,7 @@ def test_auth_failure(self): cl = get_client() # response must not have x-server-management-url header - @mock.patch.object(requests, "request", mock_request) + @mock.patch.object(requests.Session, "request", mock_request) def test_auth_call(): self.assertRaises(exceptions.AuthorizationFailure, cl.authenticate) @@ -97,7 +97,7 @@ def test_auth_call(): def test_connection_refused(self): cl = get_client() - @mock.patch.object(requests, "request", refused_mock_request) + @mock.patch.object(requests.Session, "request", refused_mock_request) def test_refused_call(): self.assertRaises(exceptions.ConnectionRefused, cl.get, "/hi") @@ -106,7 +106,7 @@ def test_refused_call(): def test_bad_request(self): cl = get_client() - @mock.patch.object(requests, "request", bad_req_mock_request) + @mock.patch.object(requests.Session, "request", bad_req_mock_request) def test_refused_call(): self.assertRaises(exceptions.BadRequest, cl.get, "/hi") diff --git a/tests/v1_1/test_auth.py b/tests/v1_1/test_auth.py index b34f77694..85dd4e4de 100644 --- a/tests/v1_1/test_auth.py +++ b/tests/v1_1/test_auth.py @@ -44,7 +44,7 @@ def test_authenticate_success(self): mock_request = mock.Mock(return_value=(auth_response)) - @mock.patch.object(requests, "request", mock_request) + @mock.patch.object(requests.Session, "request", mock_request) def test_auth_call(): cs.client.authenticate() headers = { @@ -90,7 +90,7 @@ def test_authenticate_failure(self): mock_request = mock.Mock(return_value=(auth_response)) - @mock.patch.object(requests, "request", mock_request) + @mock.patch.object(requests.Session, "request", mock_request) def test_auth_call(): self.assertRaises(exceptions.Unauthorized, cs.client.authenticate) @@ -147,7 +147,7 @@ def side_effect(*args, **kwargs): mock_request = mock.Mock(side_effect=side_effect) - @mock.patch.object(requests, "request", mock_request) + @mock.patch.object(requests.Session, "request", mock_request) def test_auth_call(): cs.client.authenticate() headers = { @@ -230,7 +230,7 @@ def test_ambiguous_endpoints(self): mock_request = mock.Mock(return_value=(auth_response)) - @mock.patch.object(requests, "request", mock_request) + @mock.patch.object(requests.Session, "request", mock_request) def test_auth_call(): self.assertRaises(exceptions.AmbiguousEndpoints, cs.client.authenticate) @@ -251,7 +251,7 @@ def test_authenticate_success(self): }) mock_request = mock.Mock(return_value=(auth_response)) - @mock.patch.object(requests, "request", mock_request) + @mock.patch.object(requests.Session, "request", mock_request) def test_auth_call(): cs.client.authenticate() headers = { @@ -279,7 +279,7 @@ def test_authenticate_failure(self): auth_response = utils.TestResponse({'status_code': 401}) mock_request = mock.Mock(return_value=(auth_response)) - @mock.patch.object(requests, "request", mock_request) + @mock.patch.object(requests.Session, "request", mock_request) def test_auth_call(): self.assertRaises(exceptions.Unauthorized, cs.client.authenticate) From 339689483da3c1591211762ef631d432844ff9ef Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Wed, 17 Apr 2013 13:12:36 +0000 Subject: [PATCH 0146/1705] Make list flavor show extra specs optional. Flavor list will get each flavors extra_specs by making fresh requests to nova. When there are lots of flavors, the flavor list will take a while to run. So let us make show extra-specs optional. Fix bug: #1166455 Change-Id: I86aef1035be6a88b8d9fb49a89f5a608a72589dd --- novaclient/v1_1/shell.py | 25 +++++++++++++++++++------ tests/v1_1/test_shell.py | 9 ++++++--- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index c6f5b63f4..b138c0b1f 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -391,10 +391,10 @@ def _print_flavor_extra_specs(flavor): return "N/A" -def _print_flavor_list(cs, flavors): +def _print_flavor_list(cs, flavors, show_extra_specs=False): _translate_flavor_keys(flavors) - formatters = {'extra_specs': _print_flavor_extra_specs} - utils.print_list(flavors, [ + + headers = [ 'ID', 'Name', 'Memory_MB', @@ -404,13 +404,26 @@ def _print_flavor_list(cs, flavors): 'VCPUs', 'RXTX_Factor', 'Is_Public', - 'extra_specs'], formatters) + ] + + if show_extra_specs: + formatters = {'extra_specs': _print_flavor_extra_specs} + headers.append('extra_specs') + else: + formatters = {} + utils.print_list(flavors, headers, formatters) -def do_flavor_list(cs, _args): + +@utils.arg('--extra-specs', + dest='extra_specs', + action='store_true', + default=False, + help='Get extra-specs of each flavor.') +def do_flavor_list(cs, args): """Print a list of available 'flavors' (sizes of servers).""" flavors = cs.flavors.list() - _print_flavor_list(cs, flavors) + _print_flavor_list(cs, flavors, args.extra_specs) @utils.arg('flavor', diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index e349c630b..f397a30ae 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -390,6 +390,10 @@ def test_boot_invalid_num_instances(self): def test_flavor_list(self): self.run_command('flavor-list') + self.assert_called_anytime('GET', '/flavors/detail') + + def test_flavor_list_with_extra_specs(self): + self.run_command('flavor-list --extra-specs') self.assert_called('GET', '/flavors/aa1/os-extra_specs') self.assert_called_anytime('GET', '/flavors/detail') @@ -771,9 +775,8 @@ def test_flavor_create(self): self.run_command("flavor-create flavorcreate " "1234 512 10 1 --swap 1024 --ephemeral 10 " "--is-public true") - self.assert_called('POST', '/flavors', pos=-3) - self.assert_called('GET', '/flavors/1', pos=-2) - self.assert_called('GET', '/flavors/1/os-extra_specs', pos=-1) + self.assert_called('POST', '/flavors', pos=-2) + self.assert_called('GET', '/flavors/1', pos=-1) def test_aggregate_list(self): self.run_command('aggregate-list') From e5d2e2c2d4c7fa413517080edad907c757737872 Mon Sep 17 00:00:00 2001 From: Leo Toyoda Date: Mon, 22 Apr 2013 14:31:02 +0900 Subject: [PATCH 0147/1705] Fix nova instance-action-list output field and order 'instance-action-list' is not possible to identify the order and time of the specified instance actions. This patch fixes the sort of 'Start_time' and add field. Change-Id: If9e6aaf8eb631819bf8e1c915cb1da64d6fcd2f4 Implements: blueprint instance-action-list-output --- novaclient/v1_1/contrib/instance_action.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/novaclient/v1_1/contrib/instance_action.py b/novaclient/v1_1/contrib/instance_action.py index 5e9c64db7..6e64d0b50 100644 --- a/novaclient/v1_1/contrib/instance_action.py +++ b/novaclient/v1_1/contrib/instance_action.py @@ -62,4 +62,5 @@ def do_instance_action_list(cs, args): """List actions on a server.""" server = utils.find_resource(cs.servers, args.server) actions = cs.instance_action.list(server) - utils.print_list(actions, ['Action', 'Request_ID', 'Message']) + utils.print_list(actions, + ['Action', 'Request_ID', 'Message', 'Start_Time'], sortby_index=3) From 50fe79b47d566bb0d15c9517b7b64d758688d853 Mon Sep 17 00:00:00 2001 From: Kieran Spear Date: Wed, 24 Apr 2013 17:55:48 +1000 Subject: [PATCH 0148/1705] Add 'flavor-list --all' admin switch Nova gives admins public flavors and flavors from their own projects only by default. For flavor management, admins need to see all flavors regardless of access. Changes: - Adds an 'is_public' argument to flavors.list() that mirrors the Nova API. is_public=None can be used to see all flavors. - Adds an --all switch to flavor-list in the CLI for use by admins when all flavors are wanted. Fixes bug 1172179. Change-Id: I915cd2d8266cb6e32c80691a6ff27d8a23488c98 --- novaclient/v1_1/flavors.py | 20 +++++++++++++++----- novaclient/v1_1/shell.py | 10 +++++++++- tests/v1_1/test_flavors.py | 12 ++++++++++-- tests/v1_1/test_shell.py | 4 ++++ 4 files changed, 38 insertions(+), 8 deletions(-) diff --git a/novaclient/v1_1/flavors.py b/novaclient/v1_1/flavors.py index b8aef6ea5..267bd2da3 100644 --- a/novaclient/v1_1/flavors.py +++ b/novaclient/v1_1/flavors.py @@ -2,6 +2,7 @@ """ Flavor interface. """ +import urllib from novaclient import base from novaclient import exceptions @@ -76,16 +77,25 @@ class FlavorManager(base.ManagerWithFind): resource_class = Flavor is_alphanum_id_allowed = True - def list(self, detailed=True): + def list(self, detailed=True, is_public=True): """ Get a list of all flavors. :rtype: list of :class:`Flavor`. """ - if detailed is True: - return self._list("/flavors/detail", "flavors") - else: - return self._list("/flavors", "flavors") + qparams = {} + # is_public is ternary - None means give all flavors. + # By default Nova assumes True and gives admins public flavors + # and flavors from their own projects only. + if not is_public: + qparams['is_public'] = is_public + query_string = "?%s" % urllib.urlencode(qparams) if qparams else "" + + detail = "" + if detailed: + detail = "/detail" + + return self._list("/flavors%s%s" % (detail, query_string), "flavors") def get(self, flavor): """ diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index b138c0b1f..cb638896a 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -420,9 +420,17 @@ def _print_flavor_list(cs, flavors, show_extra_specs=False): action='store_true', default=False, help='Get extra-specs of each flavor.') +@utils.arg('--all', + dest='all', + action='store_true', + default=False, + help='Display all flavors (Admin only).') def do_flavor_list(cs, args): """Print a list of available 'flavors' (sizes of servers).""" - flavors = cs.flavors.list() + if args.all: + flavors = cs.flavors.list(is_public=None) + else: + flavors = cs.flavors.list() _print_flavor_list(cs, flavors, args.extra_specs) diff --git a/tests/v1_1/test_flavors.py b/tests/v1_1/test_flavors.py index 084e34afd..936035330 100644 --- a/tests/v1_1/test_flavors.py +++ b/tests/v1_1/test_flavors.py @@ -12,12 +12,20 @@ class FlavorsTest(utils.TestCase): def test_list_flavors(self): fl = cs.flavors.list() cs.assert_called('GET', '/flavors/detail') - [self.assertTrue(isinstance(f, flavors.Flavor)) for f in fl] + for flavor in fl: + self.assertTrue(isinstance(flavor, flavors.Flavor)) def test_list_flavors_undetailed(self): fl = cs.flavors.list(detailed=False) cs.assert_called('GET', '/flavors') - [self.assertTrue(isinstance(f, flavors.Flavor)) for f in fl] + for flavor in fl: + self.assertTrue(isinstance(flavor, flavors.Flavor)) + + def test_list_flavors_is_public(self): + fl = cs.flavors.list(is_public=None) + cs.assert_called('GET', '/flavors/detail?is_public=None') + for flavor in fl: + self.assertTrue(isinstance(flavor, flavors.Flavor)) def test_get_flavor_details(self): f = cs.flavors.get(1) diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index f397a30ae..27d237ef9 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -397,6 +397,10 @@ def test_flavor_list_with_extra_specs(self): self.assert_called('GET', '/flavors/aa1/os-extra_specs') self.assert_called_anytime('GET', '/flavors/detail') + def test_flavor_list_with_all(self): + self.run_command('flavor-list --all') + self.assert_called('GET', '/flavors/detail?is_public=None') + def test_flavor_show(self): self.run_command('flavor-show 1') self.assert_called_anytime('GET', '/flavors/1') From c15c8f8e97ab9eae9486101bb17021160e40b15a Mon Sep 17 00:00:00 2001 From: Alessio Ababilov Date: Mon, 29 Apr 2013 11:48:09 +0300 Subject: [PATCH 0149/1705] Synchronize code from oslo Use commit 8c964a25a0904f4153eb4fbcfb3cfd4d8a357e0c Do not import openstack.common.log in strutils (Mon Apr 29 11:13:51 2013 +0300) Changes: 8c964a2 Do not import openstack.common.log in strutils e700d92 Replaces standard logging with common logging 47e9e98 Fix the co-authored-by processing. 6e8b9ba Include Co-authored-by entries in AUTHORS. 547ab34 Fix Copyright Headers - Rename LLC to Foundation 2cb8e45 support ISO 8601 micro-second precision Change-Id: Ida31e60ecac5ba89e72a1d8b0a79fd608ad19092 --- novaclient/openstack/common/setup.py | 23 +++----- novaclient/openstack/common/strutils.py | 5 +- novaclient/openstack/common/timeutils.py | 69 ++++++++++++++++++++---- 3 files changed, 66 insertions(+), 31 deletions(-) diff --git a/novaclient/openstack/common/setup.py b/novaclient/openstack/common/setup.py index d2a1fbd58..ba6b54aff 100644 --- a/novaclient/openstack/common/setup.py +++ b/novaclient/openstack/common/setup.py @@ -171,8 +171,8 @@ def generate_authors(): " log --format='%aN <%aE>' | sort -u | " "egrep -v '" + jenkins_email + "'") changelog = _run_shell_command(git_log_cmd) - signed_cmd = ("git log --git-dir=" + git_dir + - " | grep -i Co-authored-by: | sort -u") + signed_cmd = ("git --git-dir=" + git_dir + + " log | grep -i Co-authored-by: | sort -u") signed_entries = _run_shell_command(signed_cmd) if signed_entries: new_entries = "\n".join( @@ -226,21 +226,15 @@ def run(self): # just ignore it try: from sphinx.setup_command import BuildDoc - from sphinx import application - - class LocalSphinx(application.Sphinx): - - def __init__(self, *args, **kwargs): - kwargs['warningiserror'] = True - super(LocalSphinx, self).__init__(*args, **kwargs) class LocalBuildDoc(BuildDoc): builders = ['html', 'man'] - def generate_autoindex(self, option_dict): + def generate_autoindex(self): print "**Autodocumenting from %s" % os.path.abspath(os.curdir) modules = {} + option_dict = self.distribution.get_option_dict('build_sphinx') source_dir = os.path.join(option_dict['source_dir'][1], 'api') if not os.path.exists(source_dir): os.makedirs(source_dir) @@ -269,13 +263,8 @@ def generate_autoindex(self, option_dict): autoindex.write(" %s.rst\n" % module) def run(self): - option_dict = self.distribution.get_option_dict( - 'build_sphinx') - if ('autoindex' in option_dict and - not os.getenv('SPHINX_DEBUG')): - self.generate_autoindex(option_dict) - if 'warnerrors' in option_dict: - application.Sphinx = LocalSphinx + if not os.getenv('SPHINX_DEBUG'): + self.generate_autoindex() for builder in self.builders: self.builder = builder diff --git a/novaclient/openstack/common/strutils.py b/novaclient/openstack/common/strutils.py index 841a0f823..fe8418e09 100644 --- a/novaclient/openstack/common/strutils.py +++ b/novaclient/openstack/common/strutils.py @@ -1,6 +1,6 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2011 OpenStack Foundation +# Copyright 2011 OpenStack Foundation. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -19,11 +19,8 @@ System-level utilities and helper functions. """ -import logging import sys -LOG = logging.getLogger(__name__) - def int_from_bool_as_string(subject): """ diff --git a/novaclient/openstack/common/timeutils.py b/novaclient/openstack/common/timeutils.py index 15582c820..609436590 100644 --- a/novaclient/openstack/common/timeutils.py +++ b/novaclient/openstack/common/timeutils.py @@ -1,6 +1,6 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2011 OpenStack Foundation +# Copyright 2011 OpenStack Foundation. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -25,18 +25,22 @@ import iso8601 -TIME_FORMAT = "%Y-%m-%dT%H:%M:%S" -PERFECT_TIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%f" +# ISO 8601 extended time format with microseconds +_ISO8601_TIME_FORMAT_SUBSECOND = '%Y-%m-%dT%H:%M:%S.%f' +_ISO8601_TIME_FORMAT = '%Y-%m-%dT%H:%M:%S' +PERFECT_TIME_FORMAT = _ISO8601_TIME_FORMAT_SUBSECOND -def isotime(at=None): +def isotime(at=None, subsecond=False): """Stringify time in ISO 8601 format""" if not at: at = utcnow() - str = at.strftime(TIME_FORMAT) + st = at.strftime(_ISO8601_TIME_FORMAT + if not subsecond + else _ISO8601_TIME_FORMAT_SUBSECOND) tz = at.tzinfo.tzname(None) if at.tzinfo else 'UTC' - str += ('Z' if tz == 'UTC' else tz) - return str + st += ('Z' if tz == 'UTC' else tz) + return st def parse_isotime(timestr): @@ -71,11 +75,15 @@ def normalize_time(timestamp): def is_older_than(before, seconds): """Return True if before is older than seconds.""" + if isinstance(before, basestring): + before = parse_strtime(before).replace(tzinfo=None) return utcnow() - before > datetime.timedelta(seconds=seconds) def is_newer_than(after, seconds): """Return True if after is newer than seconds.""" + if isinstance(after, basestring): + after = parse_strtime(after).replace(tzinfo=None) return after - utcnow() > datetime.timedelta(seconds=seconds) @@ -87,22 +95,37 @@ def utcnow_ts(): def utcnow(): """Overridable version of utils.utcnow.""" if utcnow.override_time: - return utcnow.override_time + try: + return utcnow.override_time.pop(0) + except AttributeError: + return utcnow.override_time return datetime.datetime.utcnow() +def iso8601_from_timestamp(timestamp): + """Returns a iso8601 formated date from timestamp""" + return isotime(datetime.datetime.utcfromtimestamp(timestamp)) + + utcnow.override_time = None def set_time_override(override_time=datetime.datetime.utcnow()): - """Override utils.utcnow to return a constant time.""" + """ + Override utils.utcnow to return a constant time or a list thereof, + one at a time. + """ utcnow.override_time = override_time def advance_time_delta(timedelta): """Advance overridden time using a datetime.timedelta.""" assert(not utcnow.override_time is None) - utcnow.override_time += timedelta + try: + for dt in utcnow.override_time: + dt += timedelta + except TypeError: + utcnow.override_time += timedelta def advance_time_seconds(seconds): @@ -135,3 +158,29 @@ def unmarshall_time(tyme): minute=tyme['minute'], second=tyme['second'], microsecond=tyme['microsecond']) + + +def delta_seconds(before, after): + """ + Compute the difference in seconds between two date, time, or + datetime objects (as a float, to microsecond resolution). + """ + delta = after - before + try: + return delta.total_seconds() + except AttributeError: + return ((delta.days * 24 * 3600) + delta.seconds + + float(delta.microseconds) / (10 ** 6)) + + +def is_soon(dt, window): + """ + Determines if time is going to happen in the next window seconds. + + :params dt: the time + :params window: minimum seconds to remain to consider the time not soon + + :return: True if expiration is within the given duration + """ + soon = (utcnow() + datetime.timedelta(seconds=window)) + return normalize_time(dt) <= soon From 789d15608d5863f31ee0240c38998cd3e630cefb Mon Sep 17 00:00:00 2001 From: Sulochan Acharya Date: Sat, 27 Apr 2013 03:54:53 -0500 Subject: [PATCH 0150/1705] Allow deleting multiple images from shell Adds nargs to image-delete to allow deleting multiple images through cli. Adds shell test for multiple image-delete. Also makes a small correction in fakes to correct image id of the second image to 2. Fixes bug #1173511 Change-Id: I167a97cab75a6df544901e965d2cfa3c10a3958e --- novaclient/v1_1/shell.py | 17 ++++++++--------- tests/v1_1/fakes.py | 5 ++++- tests/v1_1/test_shell.py | 5 +++++ 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index b138c0b1f..4e90252db 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -831,16 +831,15 @@ def do_image_show(cs, args): _print_image(image) -@utils.arg('image', metavar='', help='Name or ID of image.') +@utils.arg('image', metavar='', nargs='+', + help='Name or ID of image(s).') def do_image_delete(cs, args): - """ - Delete an image. - - It should go without saying, but you can only delete images you - created. - """ - image = _find_image(cs, args.image) - image.delete() + """Delete specified image(s).""" + for image in args.image: + try: + _find_image(cs, image).delete() + except Exception as e: + print "Delete for image %s failed: %s" % (image, e) @utils.arg('--reservation-id', diff --git a/tests/v1_1/fakes.py b/tests/v1_1/fakes.py index fdd7543a2..c13e16edd 100644 --- a/tests/v1_1/fakes.py +++ b/tests/v1_1/fakes.py @@ -806,7 +806,7 @@ def get_images_detail(self, **kw): "links": {}, }, { - "id": 743, + "id": 2, "name": "My Server Backup", "serverId": 1234, "updated": "2010-10-10T12:00:00Z", @@ -839,6 +839,9 @@ def post_images_1_metadata(self, body, **kw): def delete_images_1(self, **kw): return (204, {}, None) + def delete_images_2(self, **kw): + return (204, {}, None) + def delete_images_1_metadata_test_key(self, **kw): return (204, {}, None) diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index f397a30ae..cb45e8d43 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -482,6 +482,11 @@ def test_image_delete(self): self.run_command('image-delete 1') self.assert_called('DELETE', '/images/1') + def test_image_delete_multiple(self): + self.run_command('image-delete 1 2') + self.assert_called('DELETE', '/images/1', pos=-3) + self.assert_called('DELETE', '/images/2', pos=-1) + def test_list(self): self.run_command('list') self.assert_called('GET', '/servers/detail') From 201b74b6cc4e458d3b19a948352952e53048b79f Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Mon, 29 Apr 2013 11:33:32 -0700 Subject: [PATCH 0151/1705] Clean up exceptions.from_response Change-Id: I359a911c0813697f091517de493be403e8c1d8a3 --- novaclient/exceptions.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/novaclient/exceptions.py b/novaclient/exceptions.py index 142f2f3f6..958f0bc10 100644 --- a/novaclient/exceptions.py +++ b/novaclient/exceptions.py @@ -154,20 +154,27 @@ def from_response(response, body, url, method=None): if resp.status_code != 200: raise exception_from_response(resp, rest.text) """ - cls = _code_map.get(response.status_code, ClientException) + kwargs = { + 'code': response.status_code, + 'method': method, + 'url': url, + 'request_id': None, + } + if response.headers: - request_id = response.headers.get('x-compute-request-id') - else: - request_id = None + kwargs['request_id'] = response.headers.get('x-compute-request-id') + if body: message = "n/a" details = "n/a" + if hasattr(body, 'keys'): error = body[body.keys()[0]] message = error.get('message', None) details = error.get('details', None) - return cls(code=response.status_code, message=message, details=details, - request_id=request_id, url=url, method=method) - else: - return cls(code=response.status_code, request_id=request_id, url=url, - method=method) + + kwargs['message'] = message + kwargs['details'] = details + + cls = _code_map.get(response.status_code, ClientException) + return cls(**kwargs) From e009bec2208d2f38896160a1ea677a3014f851e5 Mon Sep 17 00:00:00 2001 From: Dave Wilde Date: Sun, 28 Apr 2013 07:37:33 -0500 Subject: [PATCH 0152/1705] Adds extended status fields to nova list The nova list command now includes 'Task State' and 'Power State' fields to bring parity with the dashboard. * Add helper function _translate_extended_states() to convert extended states to human Fixes: bug #954750 Change-Id: I564b7f88e9e2524d8e4ffe21a51608c5e3b23d2d --- novaclient/v1_1/shell.py | 36 +++++++++++++++++++++++++++++++++++- tests/test_shell.py | 8 ++++---- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index cb638896a..138b5f7eb 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -380,6 +380,31 @@ def _translate_keys(collection, convert): setattr(item, to_key, item._info[from_key]) +def _translate_extended_states(collection): + power_states = [ + 'NOSTATE', # 0x00 + 'Running', # 0x01 + '', # 0x02 + 'Paused', # 0x03 + 'Shutdown', # 0x04 + '', # 0x05 + 'Crashed', # 0x06 + 'Suspended' # 0x07 + ] + + for item in collection: + try: + setattr(item, 'power_state', + power_states[getattr(item, 'power_state')] + ) + except AttributeError: + setattr(item, 'power_state', "N/A") + try: + getattr(item, 'task_state') + except AttributeError: + setattr(item, 'task_state', "N/A") + + def _translate_flavor_keys(collection): _translate_keys(collection, [('ram', 'memory_mb')]) @@ -964,12 +989,21 @@ def do_list(cs, args): convert = [('OS-EXT-SRV-ATTR:host', 'host'), ('OS-EXT-STS:task_state', 'task_state'), ('OS-EXT-SRV-ATTR:instance_name', 'instance_name'), + ('OS-EXT-STS:power_state', 'power_state'), ('hostId', 'host_id')] _translate_keys(servers, convert) + _translate_extended_states(servers) if field_titles: columns = [id_col] + field_titles else: - columns = [id_col, 'Name', 'Status', 'Networks'] + columns = [ + id_col, + 'Name', + 'Status', + 'Task State', + 'Power State', + 'Networks' + ] formatters['Networks'] = utils._format_servers_list_networks utils.print_list(servers, columns, formatters, sortby_index=1) diff --git a/tests/test_shell.py b/tests/test_shell.py index ee9a5d7cc..ae78815d2 100644 --- a/tests/test_shell.py +++ b/tests/test_shell.py @@ -151,10 +151,10 @@ def test_password(self, mock_getpass, mock_stdin): self.make_env(exclude='OS_PASSWORD') stdout, stderr = self.shell('list') self.assertEqual((stdout + stderr), - '+----+------+--------+----------+\n' - '| ID | Name | Status | Networks |\n' - '+----+------+--------+----------+\n' - '+----+------+--------+----------+\n') + '+----+------+--------+------------+-------------+----------+\n' + '| ID | Name | Status | Task State | Power State | Networks |\n' + '+----+------+--------+------------+-------------+----------+\n' + '+----+------+--------+------------+-------------+----------+\n') @mock.patch('sys.stdin', side_effect=mock.MagicMock) @mock.patch('getpass.getpass', side_effect=EOFError) From eae3d724c08b7bf13ef2ae5588fc0df4cbcc271c Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Mon, 29 Apr 2013 11:34:24 -0700 Subject: [PATCH 0153/1705] Expose retry_after attribute of OverLimit exception Fixes bug 1174469 Change-Id: Ic1e67f6f91d4fe2072ff68dfb36330cd86c1d5b4 --- novaclient/exceptions.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/novaclient/exceptions.py b/novaclient/exceptions.py index 958f0bc10..b331c3c01 100644 --- a/novaclient/exceptions.py +++ b/novaclient/exceptions.py @@ -123,6 +123,14 @@ class OverLimit(ClientException): http_status = 413 message = "Over limit" + def __init__(self, *args, **kwargs): + try: + self.retry_after = int(kwargs.pop('retry_after')) + except (KeyError, ValueError): + self.retry_after = 0 + + super(OverLimit, self).__init__(*args, **kwargs) + # NotImplemented is a python keyword. class HTTPNotImplemented(ClientException): @@ -164,6 +172,9 @@ def from_response(response, body, url, method=None): if response.headers: kwargs['request_id'] = response.headers.get('x-compute-request-id') + if 'retry-after' in response.headers: + kwargs['retry_after'] = response.headers.get('retry-after') + if body: message = "n/a" details = "n/a" From a3a7ebfe863c047f68dd43b5c9a45ee40359750a Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Mon, 29 Apr 2013 14:05:40 -0700 Subject: [PATCH 0154/1705] Add setuptools_git-*.egg to .gitignore Change-Id: I97128363b13414e9037cf54ffc52a3db405b13a1 --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 90d201e2a..6973696eb 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ python_novaclient.egg-info AUTHORS ChangeLog novaclient/versioninfo +setuptools_git-*.egg/ From e745b4626e6066ea498d7b70212602bff8449fb8 Mon Sep 17 00:00:00 2001 From: Andy McCrae Date: Mon, 13 May 2013 11:21:18 +0100 Subject: [PATCH 0155/1705] Fix for --bridge-interface being ignore by nova network-create Change-Id: I84b3264e9cbf78c1af27a3f2fd9690f60f710299 Fixes: bug #1179437 --- novaclient/v1_1/shell.py | 5 +++-- tests/v1_1/test_networks.py | 23 +++++++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index f0ea69198..649747e82 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -680,8 +680,9 @@ def do_network_associate_project(cs, args): def _filter_network_create_options(args): valid_args = ['label', 'cidr', 'vlan_start', 'vpn_start', 'cidr_v6', - 'gateway', 'gateway_v6', 'bridge', 'multi_host', 'dns1', - 'dns2', 'uuid', 'fixed_cidr', 'project_id', 'priority'] + 'gateway', 'gateway_v6', 'bridge', 'bridge_interface', + 'multi_host', 'dns1', 'dns2', 'uuid', 'fixed_cidr', + 'project_id', 'priority'] kwargs = {} for k, v in args.__dict__.items(): if k in valid_args and v is not None: diff --git a/tests/v1_1/test_networks.py b/tests/v1_1/test_networks.py index e0834c8e2..088f35574 100644 --- a/tests/v1_1/test_networks.py +++ b/tests/v1_1/test_networks.py @@ -28,6 +28,29 @@ def test_create(self): {'network': {'label': 'foo'}}) self.assertTrue(isinstance(f, networks.Network)) + def test_create_allparams(self): + params = { + 'label': 'bar', + 'bridge': 'br0', + 'bridge_interface': 'int0', + 'cidr': '192.0.2.0/24', + 'cidr_v6': '2001:DB8::/32', + 'dns1': '1.1.1.1', + 'dns2': '1.1.1.2', + 'fixed_cidr': '198.51.100.0/24', + 'gateway': '192.0.2.1', + 'gateway_v6': '2001:DB8::1', + 'multi_host': 'T', + 'priority': '1', + 'project_id': '1', + 'vlan_start': 1, + 'vpn_start': 1 + } + + f = cs.networks.create(**params) + cs.assert_called('POST', '/os-networks', {'network': params}) + self.assertTrue(isinstance(f, networks.Network)) + def test_associate_project(self): cs.networks.associate_project('networktest') cs.assert_called('POST', '/os-networks/add', From 075e9ca76ec67eb3b95fc068a60be1ee070114ed Mon Sep 17 00:00:00 2001 From: Shane Wang Date: Tue, 14 May 2013 11:21:08 +0800 Subject: [PATCH 0156/1705] Fix the default parameter in print_list Because sort_index = 0 by default, "if sort_index is not None" doesn't make any sense. Here sort_index should be None by default, which means "donot want to sort". Change-Id: I823ab75eb7a92bdc426dd96a3d1e56f187118729 Signed-off-by: Shane Wang --- novaclient/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/utils.py b/novaclient/utils.py index 280bef0d2..47a59d152 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -141,7 +141,7 @@ def pretty_choice_list(l): return ', '.join("'%s'" % i for i in l) -def print_list(objs, fields, formatters={}, sortby_index=0): +def print_list(objs, fields, formatters={}, sortby_index=None): if sortby_index is None: sortby = None else: From 3c97f768e5e175c1f2217be9d976fee8cbdca58b Mon Sep 17 00:00:00 2001 From: Shane Wang Date: Tue, 14 May 2013 21:47:10 +0800 Subject: [PATCH 0157/1705] Cleanup some flavor commands Some cleanups include: - Add flavor sub-commands into README.rst - Check flavor ID when creating a flavor - Remove check_uuid_like() because it isn't used - Remove parameter cs in some _print_XXX functions because cs is not used Change-Id: If47ce557d33db05f53e382f0670f436e05a340b7 Signed-off-by: Shane Wang --- README.rst | 10 ++++++++-- novaclient/utils.py | 19 +++---------------- novaclient/v1_1/flavors.py | 22 ++++++++++++---------- novaclient/v1_1/shell.py | 11 ++++++----- 4 files changed, 29 insertions(+), 33 deletions(-) diff --git a/README.rst b/README.rst index 6a4fcde1b..ed4dc96b5 100644 --- a/README.rst +++ b/README.rst @@ -109,10 +109,16 @@ You'll find complete documentation on the shell by running endpoints Discover endpoints that get returned from the authenticate services evacuate Evacuate a server from failed host - flavor-create Create a new flavor - flavor-delete Delete a specific flavor + flavor-create Create a new flavor. + flavor-delete Delete a specific flavor. flavor-list Print a list of available 'flavors' (sizes of servers). + flavor-show Show details about the given flavor. + flavor-key Set or unset extra_spec for a flavor. + flavor-access-list Print access information about the given flavor. + flavor-access-add Add flavor access for the given tenant. + flavor-access-remove + Remove flavor access for the given tenant. floating-ip-create Allocate a floating IP for the current tenant. floating-ip-delete De-allocate a floating IP. floating-ip-list List floating ips for this tenant. diff --git a/novaclient/utils.py b/novaclient/utils.py index 47a59d152..3a0ad3b6a 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -196,15 +196,9 @@ def find_resource(manager, name_or_id): """Helper for the _find_* methods.""" # first try to get entity as integer id try: - is_intid = isinstance(name_or_id, int) or name_or_id.isdigit() - except AttributeError: - is_intid = False - - if is_intid: - try: - return manager.get(int(name_or_id)) - except exceptions.NotFound: - pass + return manager.get(int(name_or_id)) + except (TypeError, ValueError, exceptions.NotFound): + pass # now try to get entity as uuid try: @@ -365,13 +359,6 @@ def is_uuid_like(val): return False -def check_uuid_like(val): - if not is_uuid_like(val): - raise exceptions.CommandError( - "error: Invalid tenant-id %s supplied" - % val) - - def _load_entry_point(ep_name, name=None): """Try to load the entry point ep_name that matches name.""" for ep in pkg_resources.iter_entry_points(ep_name, name=name): diff --git a/novaclient/v1_1/flavors.py b/novaclient/v1_1/flavors.py index 267bd2da3..b859d3fbb 100644 --- a/novaclient/v1_1/flavors.py +++ b/novaclient/v1_1/flavors.py @@ -134,35 +134,37 @@ def create(self, name, ram, vcpus, disk, flavorid=None, try: ram = int(ram) - except: + except (TypeError, ValueError): raise exceptions.CommandError("Ram must be an integer.") - try: vcpus = int(vcpus) - except: + except (TypeError, ValueError): raise exceptions.CommandError("VCPUs must be an integer.") - try: disk = int(disk) - except: + except (TypeError, ValueError): raise exceptions.CommandError("Disk must be an integer.") if flavorid == "auto": flavorid = None + elif not utils.is_uuid_like(flavorid): + try: + flavorid = int(flavorid) + except (TypeError, ValueError): + raise exceptions.CommandError("Flavor ID must be an integer " + "or a UUID or auto.") try: swap = int(swap) - except: + except (TypeError, ValueError): raise exceptions.CommandError("Swap must be an integer.") - try: ephemeral = int(ephemeral) - except: + except (TypeError, ValueError): raise exceptions.CommandError("Ephemeral must be an integer.") - try: rxtx_factor = float(rxtx_factor) - except: + except (TypeError, ValueError): raise exceptions.CommandError("rxtx_factor must be a float.") try: diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index f0ea69198..f6bb6d959 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -416,7 +416,7 @@ def _print_flavor_extra_specs(flavor): return "N/A" -def _print_flavor_list(cs, flavors, show_extra_specs=False): +def _print_flavor_list(flavors, show_extra_specs=False): _translate_flavor_keys(flavors) headers = [ @@ -456,7 +456,7 @@ def do_flavor_list(cs, args): flavors = cs.flavors.list(is_public=None) else: flavors = cs.flavors.list() - _print_flavor_list(cs, flavors, args.extra_specs) + _print_flavor_list(flavors, args.extra_specs) @utils.arg('flavor', @@ -466,6 +466,7 @@ def do_flavor_delete(cs, args): """Delete a specific flavor""" flavorid = _find_flavor(cs, args.flavor) cs.flavors.delete(flavorid) + _print_flavor_list([flavorid]) @utils.arg('flavor', @@ -474,7 +475,7 @@ def do_flavor_delete(cs, args): def do_flavor_show(cs, args): """Show details about the given flavor.""" flavor = _find_flavor(cs, args.flavor) - _print_flavor(cs, flavor) + _print_flavor(flavor) @utils.arg('name', @@ -515,7 +516,7 @@ def do_flavor_create(cs, args): f = cs.flavors.create(args.name, args.ram, args.vcpus, args.disk, args.id, args.ephemeral, args.swap, args.rxtx_factor, args.is_public) - _print_flavor_list(cs, [f]) + _print_flavor_list([f]) @utils.arg('flavor', @@ -847,7 +848,7 @@ def _print_image(image): utils.print_dict(info) -def _print_flavor(cs, flavor): +def _print_flavor(flavor): info = flavor._info.copy() # ignore links, we don't need to present those info.pop('links') From c9fc9b5b8ff12218e26a71e0660f774bb3546eb3 Mon Sep 17 00:00:00 2001 From: Alessio Ababilov Date: Wed, 15 May 2013 16:45:01 +0300 Subject: [PATCH 0158/1705] Make ManagerWithFind abstract and fix its descendants ManagerWithFind requires list() method in its descendants. Make it abstract and fix its improper descendants that do not implement list() (SecurityGroupRuleManager and many others). Fixes: bug #1180393 Change-Id: Ic8b466a57554018092c31c6d6b3ea62f181d7000 --- novaclient/base.py | 11 ++++++++--- novaclient/v1_1/certs.py | 2 +- novaclient/v1_1/coverage_ext.py | 2 +- novaclient/v1_1/fixed_ips.py | 2 +- novaclient/v1_1/floating_ip_dns.py | 4 ++-- novaclient/v1_1/hosts.py | 4 +++- novaclient/v1_1/quota_classes.py | 2 +- novaclient/v1_1/quotas.py | 2 +- novaclient/v1_1/security_group_rules.py | 2 +- novaclient/v1_1/shell.py | 2 +- tests/v1_1/test_hosts.py | 4 ++-- 11 files changed, 22 insertions(+), 15 deletions(-) diff --git a/novaclient/base.py b/novaclient/base.py index 26ca3f833..c97c104c4 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -19,6 +19,7 @@ Base utilities to build API operation managers and objects on top of. """ +import abc import contextlib import hashlib import os @@ -167,6 +168,13 @@ class ManagerWithFind(Manager): """ Like a `Manager`, but with additional `find()`/`findall()` methods. """ + + __metaclass__ = abc.ABCMeta + + @abc.abstractmethod + def list(self): + pass + def find(self, **kwargs): """ Find a single item with attributes matching ``**kwargs``. @@ -204,9 +212,6 @@ def findall(self, **kwargs): return found - def list(self): - raise NotImplementedError - class BootingManagerWithFind(ManagerWithFind): """Like a `ManagerWithFind`, but has the ability to boot servers.""" diff --git a/novaclient/v1_1/certs.py b/novaclient/v1_1/certs.py index 28ff34e3f..b33d52134 100644 --- a/novaclient/v1_1/certs.py +++ b/novaclient/v1_1/certs.py @@ -29,7 +29,7 @@ def __repr__(self): len(self.data)) -class CertificateManager(base.ManagerWithFind): +class CertificateManager(base.Manager): """ Manage :class:`Certificate` resources. """ diff --git a/novaclient/v1_1/coverage_ext.py b/novaclient/v1_1/coverage_ext.py index 0eb91ee9e..92fc5a880 100644 --- a/novaclient/v1_1/coverage_ext.py +++ b/novaclient/v1_1/coverage_ext.py @@ -21,7 +21,7 @@ def __repr__(self): return "" % self.name -class CoverageManager(base.ManagerWithFind): +class CoverageManager(base.Manager): resource_class = Coverage diff --git a/novaclient/v1_1/fixed_ips.py b/novaclient/v1_1/fixed_ips.py index 8f9d46c62..fd8f3917a 100644 --- a/novaclient/v1_1/fixed_ips.py +++ b/novaclient/v1_1/fixed_ips.py @@ -27,7 +27,7 @@ def __repr__(self): return "" % self.address -class FixedIPsManager(base.ManagerWithFind): +class FixedIPsManager(base.Manager): resource_class = FixedIP def get(self, fixed_ip): diff --git a/novaclient/v1_1/floating_ip_dns.py b/novaclient/v1_1/floating_ip_dns.py index 1c00bdeb1..e45a174e1 100644 --- a/novaclient/v1_1/floating_ip_dns.py +++ b/novaclient/v1_1/floating_ip_dns.py @@ -47,7 +47,7 @@ def get(self): return None -class FloatingIPDNSDomainManager(base.ManagerWithFind): +class FloatingIPDNSDomainManager(base.Manager): resource_class = FloatingIPDNSDomain def domains(self): @@ -90,7 +90,7 @@ def get(self): return self.manager.get(self.domain, self.name) -class FloatingIPDNSEntryManager(base.ManagerWithFind): +class FloatingIPDNSEntryManager(base.Manager): resource_class = FloatingIPDNSEntry def get(self, domain, name): diff --git a/novaclient/v1_1/hosts.py b/novaclient/v1_1/hosts.py index cc1a48afe..8ea563780 100644 --- a/novaclient/v1_1/hosts.py +++ b/novaclient/v1_1/hosts.py @@ -62,8 +62,10 @@ def host_action(self, host, action): url = '/os-hosts/%s/action' % host return self.api.client.post(url, body=body) - def list_all(self, zone=None): + def list(self, zone=None): url = '/os-hosts' if zone: url = '/os-hosts?zone=%s' % zone return self._list(url, "hosts") + + list_all = list diff --git a/novaclient/v1_1/quota_classes.py b/novaclient/v1_1/quota_classes.py index 874b50630..0b669bc2c 100644 --- a/novaclient/v1_1/quota_classes.py +++ b/novaclient/v1_1/quota_classes.py @@ -28,7 +28,7 @@ def update(self, *args, **kwargs): return self.manager.update(self.class_name, *args, **kwargs) -class QuotaClassSetManager(base.ManagerWithFind): +class QuotaClassSetManager(base.Manager): resource_class = QuotaClassSet def get(self, class_name): diff --git a/novaclient/v1_1/quotas.py b/novaclient/v1_1/quotas.py index c5ef0dfd7..c510b11ce 100644 --- a/novaclient/v1_1/quotas.py +++ b/novaclient/v1_1/quotas.py @@ -28,7 +28,7 @@ def update(self, *args, **kwargs): return self.manager.update(self.tenant_id, *args, **kwargs) -class QuotaSetManager(base.ManagerWithFind): +class QuotaSetManager(base.Manager): resource_class = QuotaSet def get(self, tenant_id): diff --git a/novaclient/v1_1/security_group_rules.py b/novaclient/v1_1/security_group_rules.py index 3e23a2873..cdd456a2c 100644 --- a/novaclient/v1_1/security_group_rules.py +++ b/novaclient/v1_1/security_group_rules.py @@ -28,7 +28,7 @@ def delete(self): self.manager.delete(self) -class SecurityGroupRuleManager(base.ManagerWithFind): +class SecurityGroupRuleManager(base.Manager): resource_class = SecurityGroupRule def create(self, parent_group_id, ip_protocol=None, from_port=None, diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 9daea3f90..8936889ce 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2503,7 +2503,7 @@ def do_host_describe(cs, args): def do_host_list(cs, args): """List all hosts by service""" columns = ["host_name", "service", "zone"] - result = cs.hosts.list_all(args.zone) + result = cs.hosts.list(args.zone) utils.print_list(result, columns) diff --git a/tests/v1_1/test_hosts.py b/tests/v1_1/test_hosts.py index b41108f8d..15bb3cb01 100644 --- a/tests/v1_1/test_hosts.py +++ b/tests/v1_1/test_hosts.py @@ -14,13 +14,13 @@ def test_describe_resource(self): [self.assertTrue(isinstance(h, hosts.Host)) for h in hs] def test_list_host(self): - hs = cs.hosts.list_all() + hs = cs.hosts.list() cs.assert_called('GET', '/os-hosts') [self.assertTrue(isinstance(h, hosts.Host)) for h in hs] [self.assertEqual(h.zone, 'nova1') for h in hs] def test_list_host_with_zone(self): - hs = cs.hosts.list_all('nova') + hs = cs.hosts.list('nova') cs.assert_called('GET', '/os-hosts?zone=nova') [self.assertTrue(isinstance(h, hosts.Host)) for h in hs] [self.assertEqual(h.zone, 'nova') for h in hs] From b1802a59bb275b204bf8306048a466dd16aa546c Mon Sep 17 00:00:00 2001 From: Shane Wang Date: Thu, 16 May 2013 14:30:22 +0800 Subject: [PATCH 0159/1705] Cleanup nova subcommands for security groups and rules The Changes include: - Sanity check for from_port, to_port, and IP protocol when adding a security rule - Print one more column 'Id' for security groups - If there are multiple security groups with the same name, the group can't be deleted unless an ID is specified. However, there is no code to search and delete security group by ID. So, _get_secgroup() will get group by ID if the input is like a UUID. If not found, then get group by name. - Some corresponding changes for help messages. - Test case changes after adding sanity check. Change-Id: Ibd82d8404bdd64e4bca2f8b25756bfaff7b28194 Signed-off-by: Shane Wang --- novaclient/v1_1/security_group_rules.py | 16 +++++++- novaclient/v1_1/shell.py | 52 ++++++++++++++++++------- tests/v1_1/test_security_group_rules.py | 48 +++++++++++++++++++++-- tests/v1_1/test_shell.py | 8 ++-- 4 files changed, 102 insertions(+), 22 deletions(-) diff --git a/novaclient/v1_1/security_group_rules.py b/novaclient/v1_1/security_group_rules.py index 3e23a2873..304019386 100644 --- a/novaclient/v1_1/security_group_rules.py +++ b/novaclient/v1_1/security_group_rules.py @@ -18,6 +18,7 @@ """ from novaclient import base +from novaclient import exceptions class SecurityGroupRule(base.Resource): @@ -34,7 +35,7 @@ class SecurityGroupRuleManager(base.ManagerWithFind): def create(self, parent_group_id, ip_protocol=None, from_port=None, to_port=None, cidr=None, group_id=None): """ - Create a security group + Create a security group rule :param ip_protocol: IP protocol, one of 'tcp', 'udp' or 'icmp' :param from_port: Source port @@ -43,6 +44,19 @@ def create(self, parent_group_id, ip_protocol=None, from_port=None, :param group_id: Security group id (int) :param parent_group_id: Parent security group id (int) """ + + try: + from_port = int(from_port) + except (TypeError, ValueError): + raise exceptions.CommandError("From port must be an integer.") + try: + to_port = int(to_port) + except (TypeError, ValueError): + raise exceptions.CommandError("To port must be an integer.") + if ip_protocol.upper() not in ['TCP', 'UDP', 'ICMP']: + raise exceptions.CommandError("Ip protocol must be 'tcp', 'udp', " + "or 'icmp'.") + body = {"security_group_rule": { "ip_protocol": ip_protocol, "from_port": from_port, diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 9daea3f90..6ac2e92e3 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1878,10 +1878,18 @@ def __init__(self, obj): def _print_secgroups(secgroups): - utils.print_list(secgroups, ['Name', 'Description']) + utils.print_list(secgroups, ['Id', 'Name', 'Description']) def _get_secgroup(cs, secgroup): + # Check secgroup is an ID + if utils.is_uuid_like(strutils.safe_encode(secgroup)): + try: + return cs.security_groups.get(secgroup) + except exceptions.NotFound: + pass + + # Check secgroup as a name match_found = False for s in cs.security_groups.list(): encoding = (locale.getpreferredencoding() or @@ -1895,11 +1903,14 @@ def _get_secgroup(cs, secgroup): raise exceptions.NoUniqueMatch(msg) match_found = s if match_found is False: - raise exceptions.CommandError("Secgroup %s not found" % secgroup) + raise exceptions.CommandError("Secgroup ID or name '%s' not found." + % secgroup) return match_found -@utils.arg('secgroup', metavar='', help='ID of security group.') +@utils.arg('secgroup', + metavar='', + help='ID or name of security group.') @utils.arg('ip_proto', metavar='', help='IP protocol (icmp, tcp, udp).') @@ -1921,7 +1932,9 @@ def do_secgroup_add_rule(cs, args): _print_secgroup_rules([rule]) -@utils.arg('secgroup', metavar='', help='ID of security group.') +@utils.arg('secgroup', + metavar='', + help='ID or name of security group.') @utils.arg('ip_proto', metavar='', help='IP protocol (icmp, tcp, udp).') @@ -1934,7 +1947,6 @@ def do_secgroup_add_rule(cs, args): @utils.arg('cidr', metavar='', help='CIDR for address range.') def do_secgroup_delete_rule(cs, args): """Delete a rule from a security group.""" - secgroup = _get_secgroup(cs, args.secgroup) for rule in secgroup.rules: if (rule['ip_protocol'] and @@ -1942,6 +1954,7 @@ def do_secgroup_delete_rule(cs, args): rule['from_port'] == int(args.from_port) and rule['to_port'] == int(args.to_port) and rule['ip_range']['cidr'] == args.cidr): + _print_secgroup_rules([rule]) return cs.security_group_rules.delete(rule['id']) raise exceptions.CommandError("Rule not found") @@ -1952,13 +1965,18 @@ def do_secgroup_delete_rule(cs, args): help='Description of security group.') def do_secgroup_create(cs, args): """Create a security group.""" - _print_secgroups([cs.security_groups.create(args.name, args.description)]) + secgroup = cs.security_groups.create(args.name, args.description) + _print_secgroups([secgroup]) -@utils.arg('secgroup', metavar='', help='Name of security group.') +@utils.arg('secgroup', + metavar='', + help='ID or name of security group.') def do_secgroup_delete(cs, args): """Delete a security group.""" - cs.security_groups.delete(_get_secgroup(cs, args.secgroup)) + secgroup = _get_secgroup(cs, args.secgroup) + cs.security_groups.delete(secgroup) + _print_secgroups([secgroup]) @utils.arg('--all-tenants', @@ -1977,24 +1995,28 @@ def do_secgroup_delete(cs, args): def do_secgroup_list(cs, args): """List security groups for the current tenant.""" search_opts = {'all_tenants': args.all_tenants} - columns = ['Name', 'Description'] + columns = ['Id', 'Name', 'Description'] if args.all_tenants: columns.append('Tenant_ID') groups = cs.security_groups.list(search_opts=search_opts) utils.print_list(groups, columns) -@utils.arg('secgroup', metavar='', help='Name of security group.') +@utils.arg('secgroup', + metavar='', + help='ID or name of security group.') def do_secgroup_list_rules(cs, args): """List rules for a security group.""" secgroup = _get_secgroup(cs, args.secgroup) _print_secgroup_rules(secgroup.rules) -@utils.arg('secgroup', metavar='', help='ID of security group.') +@utils.arg('secgroup', + metavar='', + help='ID or name of security group.') @utils.arg('source_group', metavar='', - help='ID of source group.') + help='ID or name of source group.') @utils.arg('ip_proto', metavar='', help='IP protocol (icmp, tcp, udp).') @@ -2023,10 +2045,12 @@ def do_secgroup_add_group_rule(cs, args): _print_secgroup_rules([rule]) -@utils.arg('secgroup', metavar='', help='ID of security group.') +@utils.arg('secgroup', + metavar='', + help='ID or name of security group.') @utils.arg('source_group', metavar='', - help='ID of source group.') + help='ID or name of source group.') @utils.arg('ip_proto', metavar='', help='IP protocol (icmp, tcp, udp).') diff --git a/tests/v1_1/test_security_group_rules.py b/tests/v1_1/test_security_group_rules.py index 872dab78d..acf1b7897 100644 --- a/tests/v1_1/test_security_group_rules.py +++ b/tests/v1_1/test_security_group_rules.py @@ -1,3 +1,4 @@ +from novaclient import exceptions from novaclient.v1_1 import security_group_rules from tests import utils from tests.v1_1 import fakes @@ -11,7 +12,48 @@ def test_delete_security_group_rule(self): cs.security_group_rules.delete(1) cs.assert_called('DELETE', '/os-security-group-rules/1') - def test_create_security_group(self): - sg = cs.security_group_rules.create(1) - cs.assert_called('POST', '/os-security-group-rules') + def test_create_security_group_rule(self): + sg = cs.security_group_rules.create(1, "tcp", 1, 65535, "10.0.0.0/16") + + body = { + "security_group_rule": { + "ip_protocol": "tcp", + "from_port": 1, + "to_port": 65535, + "cidr": "10.0.0.0/16", + "group_id": None, + "parent_group_id": 1, + } + } + + cs.assert_called('POST', '/os-security-group-rules', body) self.assertTrue(isinstance(sg, security_group_rules.SecurityGroupRule)) + + def test_create_security_group_group_rule(self): + sg = cs.security_group_rules.create(1, "tcp", 1, 65535, "10.0.0.0/16", + 101) + + body = { + "security_group_rule": { + "ip_protocol": "tcp", + "from_port": 1, + "to_port": 65535, + "cidr": "10.0.0.0/16", + "group_id": 101, + "parent_group_id": 1, + } + } + + cs.assert_called('POST', '/os-security-group-rules', body) + self.assertTrue(isinstance(sg, security_group_rules.SecurityGroupRule)) + + def test_invalid_parameters_create(self): + self.assertRaises(exceptions.CommandError, + cs.security_group_rules.create, "secgrouprulecreate", + 1, "invalid", 1, 65535, "10.0.0.0/16") + self.assertRaises(exceptions.CommandError, + cs.security_group_rules.create, "secgrouprulecreate", + 1, "tcp", "invalid", 65535, "10.0.0.0/16") + self.assertRaises(exceptions.CommandError, + cs.security_group_rules.create, "secgrouprulecreate", + 1, "tcp", 1, "invalid", "10.0.0.0/16") diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index e7fec336d..3071ba08e 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -1229,9 +1229,9 @@ def test_security_group_add_rule(self): self.run_command('secgroup-add-rule test tcp 22 22 10.0.0.0/8') self.assert_called('POST', '/os-security-group-rules', {'security_group_rule': - {'from_port': '22', + {'from_port': 22, 'ip_protocol': 'tcp', - 'to_port': '22', + 'to_port': 22, 'parent_group_id': 1, 'cidr': '10.0.0.0/8', 'group_id': None}}) @@ -1248,9 +1248,9 @@ def test_security_group_add_group_rule(self): self.run_command('secgroup-add-group-rule test test2 tcp 22 22') self.assert_called('POST', '/os-security-group-rules', {'security_group_rule': - {'from_port': '22', + {'from_port': 22, 'ip_protocol': 'TCP', - 'to_port': '22', + 'to_port': 22, 'parent_group_id': 1, 'cidr': None, 'group_id': 2}}) From f08ac04a27fe6bc2969252bcc4578d12f242f1f4 Mon Sep 17 00:00:00 2001 From: Roman Podolyaka Date: Sat, 18 May 2013 11:53:18 +0300 Subject: [PATCH 0160/1705] Migrate to pbr. openstack.common.setup and openstack.common.version are now in the standalone library pbr, so all projects using those two should migrate. Fixes bug 1179007. Change-Id: I7ac1c37f0bf148619dffff8f454db05fc192e471 --- .gitignore | 4 +- novaclient/__init__.py | 11 +- novaclient/openstack/common/setup.py | 367 ------------------------- novaclient/openstack/common/version.py | 94 ------- openstack-common.conf | 2 +- setup.cfg | 31 +++ setup.py | 36 +-- tools/pip-requires | 2 + tox.ini | 1 - 9 files changed, 43 insertions(+), 505 deletions(-) delete mode 100644 novaclient/openstack/common/setup.py delete mode 100644 novaclient/openstack/common/version.py diff --git a/.gitignore b/.gitignore index 6973696eb..b237f4133 100644 --- a/.gitignore +++ b/.gitignore @@ -11,8 +11,8 @@ cover *~ build dist -python_novaclient.egg-info AUTHORS ChangeLog novaclient/versioninfo -setuptools_git-*.egg/ +*.egg +*egg-info diff --git a/novaclient/__init__.py b/novaclient/__init__.py index dc9ab76da..bfa75532f 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -12,12 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.openstack.common import version +import pbr.version -version_info = version.VersionInfo('python-novaclient') -# We have a circular import problem when we first run python setup.py sdist -# It's harmless, so deflect it. -try: - __version__ = version_info.version_string() -except AttributeError: - __version__ = None + +__version__ = pbr.version.VersionInfo('python-novaclient').version_string() diff --git a/novaclient/openstack/common/setup.py b/novaclient/openstack/common/setup.py deleted file mode 100644 index ba6b54aff..000000000 --- a/novaclient/openstack/common/setup.py +++ /dev/null @@ -1,367 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack Foundation. -# Copyright 2012-2013 Hewlett-Packard Development Company, L.P. -# All Rights Reserved. -# -# 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. - -""" -Utilities with minimum-depends for use in setup.py -""" - -import email -import os -import re -import subprocess -import sys - -from setuptools.command import sdist - - -def parse_mailmap(mailmap='.mailmap'): - mapping = {} - if os.path.exists(mailmap): - with open(mailmap, 'r') as fp: - for l in fp: - try: - canonical_email, alias = re.match( - r'[^#]*?(<.+>).*(<.+>).*', l).groups() - except AttributeError: - continue - mapping[alias] = canonical_email - return mapping - - -def _parse_git_mailmap(git_dir, mailmap='.mailmap'): - mailmap = os.path.join(os.path.dirname(git_dir), mailmap) - return parse_mailmap(mailmap) - - -def canonicalize_emails(changelog, mapping): - """Takes in a string and an email alias mapping and replaces all - instances of the aliases in the string with their real email. - """ - for alias, email_address in mapping.iteritems(): - changelog = changelog.replace(alias, email_address) - return changelog - - -# Get requirements from the first file that exists -def get_reqs_from_files(requirements_files): - for requirements_file in requirements_files: - if os.path.exists(requirements_file): - with open(requirements_file, 'r') as fil: - return fil.read().split('\n') - return [] - - -def parse_requirements(requirements_files=['requirements.txt', - 'tools/pip-requires']): - requirements = [] - for line in get_reqs_from_files(requirements_files): - # For the requirements list, we need to inject only the portion - # after egg= so that distutils knows the package it's looking for - # such as: - # -e git://github.com/openstack/nova/master#egg=nova - if re.match(r'\s*-e\s+', line): - requirements.append(re.sub(r'\s*-e\s+.*#egg=(.*)$', r'\1', - line)) - # such as: - # http://github.com/openstack/nova/zipball/master#egg=nova - elif re.match(r'\s*https?:', line): - requirements.append(re.sub(r'\s*https?:.*#egg=(.*)$', r'\1', - line)) - # -f lines are for index locations, and don't get used here - elif re.match(r'\s*-f\s+', line): - pass - # argparse is part of the standard library starting with 2.7 - # adding it to the requirements list screws distro installs - elif line == 'argparse' and sys.version_info >= (2, 7): - pass - else: - requirements.append(line) - - return requirements - - -def parse_dependency_links(requirements_files=['requirements.txt', - 'tools/pip-requires']): - dependency_links = [] - # dependency_links inject alternate locations to find packages listed - # in requirements - for line in get_reqs_from_files(requirements_files): - # skip comments and blank lines - if re.match(r'(\s*#)|(\s*$)', line): - continue - # lines with -e or -f need the whole line, minus the flag - if re.match(r'\s*-[ef]\s+', line): - dependency_links.append(re.sub(r'\s*-[ef]\s+', '', line)) - # lines that are only urls can go in unmolested - elif re.match(r'\s*https?:', line): - dependency_links.append(line) - return dependency_links - - -def _run_shell_command(cmd, throw_on_error=False): - if os.name == 'nt': - output = subprocess.Popen(["cmd.exe", "/C", cmd], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - else: - output = subprocess.Popen(["/bin/sh", "-c", cmd], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - out = output.communicate() - if output.returncode and throw_on_error: - raise Exception("%s returned %d" % cmd, output.returncode) - if len(out) == 0: - return None - if len(out[0].strip()) == 0: - return None - return out[0].strip() - - -def _get_git_directory(): - parent_dir = os.path.dirname(__file__) - while True: - git_dir = os.path.join(parent_dir, '.git') - if os.path.exists(git_dir): - return git_dir - parent_dir, child = os.path.split(parent_dir) - if not child: # reached to root dir - return None - - -def write_git_changelog(): - """Write a changelog based on the git changelog.""" - new_changelog = 'ChangeLog' - git_dir = _get_git_directory() - if not os.getenv('SKIP_WRITE_GIT_CHANGELOG'): - if git_dir: - git_log_cmd = 'git --git-dir=%s log' % git_dir - changelog = _run_shell_command(git_log_cmd) - mailmap = _parse_git_mailmap(git_dir) - with open(new_changelog, "w") as changelog_file: - changelog_file.write(canonicalize_emails(changelog, mailmap)) - else: - open(new_changelog, 'w').close() - - -def generate_authors(): - """Create AUTHORS file using git commits.""" - jenkins_email = 'jenkins@review.(openstack|stackforge).org' - old_authors = 'AUTHORS.in' - new_authors = 'AUTHORS' - git_dir = _get_git_directory() - if not os.getenv('SKIP_GENERATE_AUTHORS'): - if git_dir: - # don't include jenkins email address in AUTHORS file - git_log_cmd = ("git --git-dir=" + git_dir + - " log --format='%aN <%aE>' | sort -u | " - "egrep -v '" + jenkins_email + "'") - changelog = _run_shell_command(git_log_cmd) - signed_cmd = ("git --git-dir=" + git_dir + - " log | grep -i Co-authored-by: | sort -u") - signed_entries = _run_shell_command(signed_cmd) - if signed_entries: - new_entries = "\n".join( - [signed.split(":", 1)[1].strip() - for signed in signed_entries.split("\n") if signed]) - changelog = "\n".join((changelog, new_entries)) - mailmap = _parse_git_mailmap(git_dir) - with open(new_authors, 'w') as new_authors_fh: - new_authors_fh.write(canonicalize_emails(changelog, mailmap)) - if os.path.exists(old_authors): - with open(old_authors, "r") as old_authors_fh: - new_authors_fh.write('\n' + old_authors_fh.read()) - else: - open(new_authors, 'w').close() - - -_rst_template = """%(heading)s -%(underline)s - -.. automodule:: %(module)s - :members: - :undoc-members: - :show-inheritance: -""" - - -def get_cmdclass(): - """Return dict of commands to run from setup.py.""" - - cmdclass = dict() - - def _find_modules(arg, dirname, files): - for filename in files: - if filename.endswith('.py') and filename != '__init__.py': - arg["%s.%s" % (dirname.replace('/', '.'), - filename[:-3])] = True - - class LocalSDist(sdist.sdist): - """Builds the ChangeLog and Authors files from VC first.""" - - def run(self): - write_git_changelog() - generate_authors() - # sdist.sdist is an old style class, can't use super() - sdist.sdist.run(self) - - cmdclass['sdist'] = LocalSDist - - # If Sphinx is installed on the box running setup.py, - # enable setup.py to build the documentation, otherwise, - # just ignore it - try: - from sphinx.setup_command import BuildDoc - - class LocalBuildDoc(BuildDoc): - - builders = ['html', 'man'] - - def generate_autoindex(self): - print "**Autodocumenting from %s" % os.path.abspath(os.curdir) - modules = {} - option_dict = self.distribution.get_option_dict('build_sphinx') - source_dir = os.path.join(option_dict['source_dir'][1], 'api') - if not os.path.exists(source_dir): - os.makedirs(source_dir) - for pkg in self.distribution.packages: - if '.' not in pkg: - os.path.walk(pkg, _find_modules, modules) - module_list = modules.keys() - module_list.sort() - autoindex_filename = os.path.join(source_dir, 'autoindex.rst') - with open(autoindex_filename, 'w') as autoindex: - autoindex.write(""".. toctree:: - :maxdepth: 1 - -""") - for module in module_list: - output_filename = os.path.join(source_dir, - "%s.rst" % module) - heading = "The :mod:`%s` Module" % module - underline = "=" * len(heading) - values = dict(module=module, heading=heading, - underline=underline) - - print "Generating %s" % output_filename - with open(output_filename, 'w') as output_file: - output_file.write(_rst_template % values) - autoindex.write(" %s.rst\n" % module) - - def run(self): - if not os.getenv('SPHINX_DEBUG'): - self.generate_autoindex() - - for builder in self.builders: - self.builder = builder - self.finalize_options() - self.project = self.distribution.get_name() - self.version = self.distribution.get_version() - self.release = self.distribution.get_version() - BuildDoc.run(self) - - class LocalBuildLatex(LocalBuildDoc): - builders = ['latex'] - - cmdclass['build_sphinx'] = LocalBuildDoc - cmdclass['build_sphinx_latex'] = LocalBuildLatex - except ImportError: - pass - - return cmdclass - - -def _get_revno(git_dir): - """Return the number of commits since the most recent tag. - - We use git-describe to find this out, but if there are no - tags then we fall back to counting commits since the beginning - of time. - """ - describe = _run_shell_command( - "git --git-dir=%s describe --always" % git_dir) - if "-" in describe: - return describe.rsplit("-", 2)[-2] - - # no tags found - revlist = _run_shell_command( - "git --git-dir=%s rev-list --abbrev-commit HEAD" % git_dir) - return len(revlist.splitlines()) - - -def _get_version_from_git(pre_version): - """Return a version which is equal to the tag that's on the current - revision if there is one, or tag plus number of additional revisions - if the current revision has no tag.""" - - git_dir = _get_git_directory() - if git_dir: - if pre_version: - try: - return _run_shell_command( - "git --git-dir=" + git_dir + " describe --exact-match", - throw_on_error=True).replace('-', '.') - except Exception: - sha = _run_shell_command( - "git --git-dir=" + git_dir + " log -n1 --pretty=format:%h") - return "%s.a%s.g%s" % (pre_version, _get_revno(git_dir), sha) - else: - return _run_shell_command( - "git --git-dir=" + git_dir + " describe --always").replace( - '-', '.') - return None - - -def _get_version_from_pkg_info(package_name): - """Get the version from PKG-INFO file if we can.""" - try: - pkg_info_file = open('PKG-INFO', 'r') - except (IOError, OSError): - return None - try: - pkg_info = email.message_from_file(pkg_info_file) - except email.MessageError: - return None - # Check to make sure we're in our own dir - if pkg_info.get('Name', None) != package_name: - return None - return pkg_info.get('Version', None) - - -def get_version(package_name, pre_version=None): - """Get the version of the project. First, try getting it from PKG-INFO, if - it exists. If it does, that means we're in a distribution tarball or that - install has happened. Otherwise, if there is no PKG-INFO file, pull the - version from git. - - We do not support setup.py version sanity in git archive tarballs, nor do - we support packagers directly sucking our git repo into theirs. We expect - that a source tarball be made from our git repo - or that if someone wants - to make a source tarball from a fork of our repo with additional tags in it - that they understand and desire the results of doing that. - """ - version = os.environ.get("OSLO_PACKAGE_VERSION", None) - if version: - return version - version = _get_version_from_pkg_info(package_name) - if version: - return version - version = _get_version_from_git(pre_version) - if version: - return version - raise Exception("Versioning for this project requires either an sdist" - " tarball, or access to an upstream git repository.") diff --git a/novaclient/openstack/common/version.py b/novaclient/openstack/common/version.py deleted file mode 100644 index 7dc643436..000000000 --- a/novaclient/openstack/common/version.py +++ /dev/null @@ -1,94 +0,0 @@ - -# Copyright 2012 OpenStack Foundation -# Copyright 2012-2013 Hewlett-Packard Development Company, L.P. -# -# 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. - -""" -Utilities for consuming the version from pkg_resources. -""" - -import pkg_resources - - -class VersionInfo(object): - - def __init__(self, package): - """Object that understands versioning for a package - :param package: name of the python package, such as glance, or - python-glanceclient - """ - self.package = package - self.release = None - self.version = None - self._cached_version = None - - def __str__(self): - """Make the VersionInfo object behave like a string.""" - return self.version_string() - - def __repr__(self): - """Include the name.""" - return "VersionInfo(%s:%s)" % (self.package, self.version_string()) - - def _get_version_from_pkg_resources(self): - """Get the version of the package from the pkg_resources record - associated with the package.""" - try: - requirement = pkg_resources.Requirement.parse(self.package) - provider = pkg_resources.get_provider(requirement) - return provider.version - except pkg_resources.DistributionNotFound: - # The most likely cause for this is running tests in a tree - # produced from a tarball where the package itself has not been - # installed into anything. Revert to setup-time logic. - from novaclient.openstack.common import setup - return setup.get_version(self.package) - - def release_string(self): - """Return the full version of the package including suffixes indicating - VCS status. - """ - if self.release is None: - self.release = self._get_version_from_pkg_resources() - - return self.release - - def version_string(self): - """Return the short version minus any alpha/beta tags.""" - if self.version is None: - parts = [] - for part in self.release_string().split('.'): - if part[0].isdigit(): - parts.append(part) - else: - break - self.version = ".".join(parts) - - return self.version - - # Compatibility functions - canonical_version_string = version_string - version_string_with_vcs = release_string - - def cached_version_string(self, prefix=""): - """Generate an object which will expand in a string context to - the results of version_string(). We do this so that don't - call into pkg_resources every time we start up a program when - passing version information into the CONF constructor, but - rather only do the calculation when and if a version is requested - """ - if not self._cached_version: - self._cached_version = "%s%s" % (prefix, - self.version_string()) - return self._cached_version diff --git a/openstack-common.conf b/openstack-common.conf index 05e8eb83d..284402247 100644 --- a/openstack-common.conf +++ b/openstack-common.conf @@ -1,7 +1,7 @@ [DEFAULT] # The list of modules to copy from openstack-common -modules=setup,timeutils,strutils,version +modules=timeutils,strutils # The base module to hold the copy of openstack.common base=novaclient diff --git a/setup.cfg b/setup.cfg index 11c72013c..0f5f94fb4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,34 @@ +[metadata] +name = python-novaclient +summary = Client library for OpenStack Compute API +description-file = + README.rst +license = Apache License, Version 2.0 +author = OpenStack +author-email = openstack-dev@lists.openstack.org +home-page = https://github.com/openstack/python-novaclient +classifier = + Development Status :: 5 - Production/Stable + Environment :: Console + Environment :: OpenStack + Intended Audience :: Developers + Intended Audience :: Information Technology + License :: OSI Approved :: Apache Software License + Operating System :: OS Independent + Programming Language :: Python + +[files] +packages = + novaclient + +[global] +setup-hooks = + pbr.hooks.setup_hook + +[entry_points] +console_scripts = + nova = novaclient.shell:main + [build_sphinx] source-dir = doc/source build-dir = doc/build diff --git a/setup.py b/setup.py index d896f34e6..726cd9c6d 100644 --- a/setup.py +++ b/setup.py @@ -12,41 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os import setuptools -from novaclient.openstack.common import setup - - -def read_file(file_name): - return open(os.path.join(os.path.dirname(__file__), file_name)).read() -project = 'python-novaclient' setuptools.setup( - name=project, - version=setup.get_version(project), - author='OpenStack', - author_email='openstack-dev@lists.openstack.org', - description="Client library for OpenStack Compute API.", - long_description=read_file("README.rst"), - license="Apache License, Version 2.0", - url="https://github.com/openstack/python-novaclient", - packages=setuptools.find_packages(exclude=['tests', 'tests.*']), - install_requires=setup.parse_requirements(), - cmdclass=setup.get_cmdclass(), - setup_requires=['setuptools_git>=0.4'], - include_package_data=True, - classifiers=[ - "Development Status :: 5 - Production/Stable", - "Environment :: Console", - "Environment :: OpenStack", - "Intended Audience :: Developers", - "Intended Audience :: Information Technology", - "License :: OSI Approved :: Apache Software License", - "Operating System :: OS Independent", - "Programming Language :: Python" + setup_requires=[ + 'd2to1>=0.2.10,<0.3', + 'pbr>=0.5,<0.6' ], - entry_points={ - "console_scripts": ["nova = novaclient.shell:main"] - }, + d2to1=True ) diff --git a/tools/pip-requires b/tools/pip-requires index e06e6f8ec..f7deffed1 100644 --- a/tools/pip-requires +++ b/tools/pip-requires @@ -1,3 +1,5 @@ +d2to1>=0.2.10,<0.3 +pbr>=0.5,<0.6 argparse iso8601>=0.1.4 prettytable>=0.6,<0.8 diff --git a/tox.ini b/tox.ini index e4458844a..464e7859c 100644 --- a/tox.ini +++ b/tox.ini @@ -9,7 +9,6 @@ setenv = VIRTUAL_ENV={envdir} deps = -r{toxinidir}/tools/pip-requires -r{toxinidir}/tools/test-requires - setuptools_git>=0.4 commands = python setup.py testr --testr-args='{posargs}' [testenv:pep8] From 20a3595e3e1b14ca600321c9325ed5f5bd46f968 Mon Sep 17 00:00:00 2001 From: Shane Wang Date: Tue, 21 May 2013 13:49:39 +0800 Subject: [PATCH 0161/1705] Synchronize code from oslo Use commit 8d0228837c40c02d93d80ae0a179275ac2d7f277 Merge "Break out common functionality for filters and weights" (Fri May 17 12:12:40 2013 +0000) Change-Id: Ib2ec8ff64e036a07fad2a38319a9a4c40a698898 Signed-off-by: Shane Wang --- novaclient/openstack/common/gettextutils.py | 50 +++++++++++++++++++++ novaclient/openstack/common/strutils.py | 44 +++++++++++++----- 2 files changed, 82 insertions(+), 12 deletions(-) create mode 100644 novaclient/openstack/common/gettextutils.py diff --git a/novaclient/openstack/common/gettextutils.py b/novaclient/openstack/common/gettextutils.py new file mode 100644 index 000000000..8a660b057 --- /dev/null +++ b/novaclient/openstack/common/gettextutils.py @@ -0,0 +1,50 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 Red Hat, Inc. +# All Rights Reserved. +# +# 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. + +""" +gettext for openstack-common modules. + +Usual usage in an openstack.common module: + + from novaclient.openstack.common.gettextutils import _ +""" + +import gettext +import os + +_localedir = os.environ.get('novaclient'.upper() + '_LOCALEDIR') +_t = gettext.translation('novaclient', localedir=_localedir, fallback=True) + + +def _(msg): + return _t.ugettext(msg) + + +def install(domain): + """Install a _() function using the given translation domain. + + Given a translation domain, install a _() function using gettext's + install() function. + + The main difference from gettext.install() is that we allow + overriding the default localedir (e.g. /usr/share/locale) using + a translation-domain-specific environment variable (e.g. + NOVA_LOCALEDIR). + """ + gettext.install(domain, + localedir=os.environ.get(domain.upper() + '_LOCALEDIR'), + unicode=True) diff --git a/novaclient/openstack/common/strutils.py b/novaclient/openstack/common/strutils.py index fe8418e09..a9805373c 100644 --- a/novaclient/openstack/common/strutils.py +++ b/novaclient/openstack/common/strutils.py @@ -21,6 +21,12 @@ import sys +from novaclient.openstack.common.gettextutils import _ + + +TRUE_STRINGS = ('1', 't', 'true', 'on', 'y', 'yes') +FALSE_STRINGS = ('0', 'f', 'false', 'off', 'n', 'no') + def int_from_bool_as_string(subject): """ @@ -37,24 +43,38 @@ def int_from_bool_as_string(subject): return bool_from_string(subject) and 1 or 0 -def bool_from_string(subject): +def bool_from_string(subject, strict=False): """ Interpret a string as a boolean. - Any string value in: + A case-insensitive match is performed such that strings matching 't', + 'true', 'on', 'y', 'yes', or '1' are considered True and, when + `strict=False`, anything else is considered False. - ('True', 'true', 'On', 'on', 'Yes', 'yes', '1') + Useful for JSON-decoded stuff and config file parsing. - is interpreted as a boolean True. - - Useful for JSON-decoded stuff and config file parsing + If `strict=True`, unrecognized values, including None, will raise a + ValueError which is useful when parsing values passed in from an API call. + Strings yielding False are 'f', 'false', 'off', 'n', 'no', or '0'. """ - if isinstance(subject, bool): - return subject - if isinstance(subject, basestring): - if subject.strip().lower() in ('true', 'on', 'yes', '1'): - return True - return False + if not isinstance(subject, basestring): + subject = str(subject) + + lowered = subject.strip().lower() + + if lowered in TRUE_STRINGS: + return True + elif lowered in FALSE_STRINGS: + return False + elif strict: + acceptable = ', '.join( + "'%s'" % s for s in sorted(TRUE_STRINGS + FALSE_STRINGS)) + msg = _("Unrecognized value '%(val)s', acceptable values are:" + " %(acceptable)s") % {'val': subject, + 'acceptable': acceptable} + raise ValueError(msg) + else: + return False def safe_decode(text, incoming=None, errors='strict'): From bc0ad1cb9dcee8c5c3c11a9dd894ad7c8763f147 Mon Sep 17 00:00:00 2001 From: Shane Wang Date: Tue, 21 May 2013 13:55:53 +0800 Subject: [PATCH 0162/1705] Reuse oslo for is_uuid_like() implementation In the original code, isalnum() in is_uuid_like() doesn't allow "-", while for UUID, format like aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa should be allowed. This patch is to reuse uuidutils's API in oslo. Change-Id: I339974c75a32d27f8e4443a1b97bb6e410933aa4 Signed-off-by: Shane Wang --- novaclient/openstack/common/uuidutils.py | 39 ++++++++++++++++++++++++ novaclient/utils.py | 13 -------- novaclient/v1_1/flavors.py | 3 +- novaclient/v1_1/shell.py | 3 +- openstack-common.conf | 2 +- 5 files changed, 44 insertions(+), 16 deletions(-) create mode 100644 novaclient/openstack/common/uuidutils.py diff --git a/novaclient/openstack/common/uuidutils.py b/novaclient/openstack/common/uuidutils.py new file mode 100644 index 000000000..7608acb94 --- /dev/null +++ b/novaclient/openstack/common/uuidutils.py @@ -0,0 +1,39 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2012 Intel Corporation. +# All Rights Reserved. +# +# 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. + +""" +UUID related utilities and helper functions. +""" + +import uuid + + +def generate_uuid(): + return str(uuid.uuid4()) + + +def is_uuid_like(val): + """Returns validation of a value as a UUID. + + For our purposes, a UUID is a canonical form string: + aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa + + """ + try: + return str(uuid.UUID(val)) == val + except (TypeError, ValueError, AttributeError): + return False diff --git a/novaclient/utils.py b/novaclient/utils.py index 3a0ad3b6a..b1df1472e 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -346,19 +346,6 @@ def slugify(value): return _slugify_hyphenate_re.sub('-', value) -def is_uuid_like(val): - """ - The UUID which doesn't contain hyphens or 'A-F' is allowed. - """ - try: - if uuid.UUID(val) and val.isalnum() and val.islower(): - return True - else: - return False - except (TypeError, ValueError, AttributeError): - return False - - def _load_entry_point(ep_name, name=None): """Try to load the entry point ep_name that matches name.""" for ep in pkg_resources.iter_entry_points(ep_name, name=name): diff --git a/novaclient/v1_1/flavors.py b/novaclient/v1_1/flavors.py index b859d3fbb..0cd5fc77b 100644 --- a/novaclient/v1_1/flavors.py +++ b/novaclient/v1_1/flavors.py @@ -7,6 +7,7 @@ from novaclient import base from novaclient import exceptions from novaclient import utils +from novaclient.openstack.common import uuidutils class Flavor(base.Resource): @@ -147,7 +148,7 @@ def create(self, name, ram, vcpus, disk, flavorid=None, if flavorid == "auto": flavorid = None - elif not utils.is_uuid_like(flavorid): + elif not uuidutils.is_uuid_like(flavorid): try: flavorid = int(flavorid) except (TypeError, ValueError): diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 6b8e24854..227d5ef3a 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -28,6 +28,7 @@ from novaclient import exceptions from novaclient.openstack.common import strutils from novaclient.openstack.common import timeutils +from novaclient.openstack.common import uuidutils from novaclient import utils from novaclient.v1_1 import availability_zones from novaclient.v1_1 import quotas @@ -1883,7 +1884,7 @@ def _print_secgroups(secgroups): def _get_secgroup(cs, secgroup): # Check secgroup is an ID - if utils.is_uuid_like(strutils.safe_encode(secgroup)): + if uuidutils.is_uuid_like(strutils.safe_encode(secgroup)): try: return cs.security_groups.get(secgroup) except exceptions.NotFound: diff --git a/openstack-common.conf b/openstack-common.conf index 284402247..a3197a085 100644 --- a/openstack-common.conf +++ b/openstack-common.conf @@ -1,7 +1,7 @@ [DEFAULT] # The list of modules to copy from openstack-common -modules=timeutils,strutils +modules=timeutils,strutils,uuidutils # The base module to hold the copy of openstack.common base=novaclient From ecbf7705d12ea63025bbd10d47b80c30e4caff52 Mon Sep 17 00:00:00 2001 From: Emanuele Rocca Date: Wed, 22 May 2013 18:24:08 +0200 Subject: [PATCH 0163/1705] Cleanup unused local variables This patch cleans up a few unused local variables, found with pyflakes. Change-Id: Id65acc5d47380c7d6bfbbfe075dc23e1de30813c --- novaclient/utils.py | 1 - novaclient/v1_1/shell.py | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/novaclient/utils.py b/novaclient/utils.py index b1df1472e..f8f4b575d 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -82,7 +82,6 @@ def get_resource_manager_extra_kwargs(f, args, allow_conflicts=False): hooks = getattr(f, "resource_manager_kwargs_hooks", []) extra_kwargs = {} for hook in hooks: - hook_name = hook.__name__ hook_kwargs = hook(args) conflicting_keys = set(hook_kwargs.keys()) & set(extra_kwargs.keys()) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 227d5ef3a..460ef926e 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1063,7 +1063,7 @@ def do_rebuild(cs, args): _password = None kwargs = utils.get_resource_manager_extra_kwargs(do_rebuild, args) - s = server.rebuild(image, _password, **kwargs) + server.rebuild(image, _password, **kwargs) _print_server(cs, args) if args.poll: @@ -1671,7 +1671,7 @@ def do_get_password(cs, args): def do_clear_password(cs, args): """Clear password for a server.""" server = _find_server(cs, args.server) - data = server.clear_password() + server.clear_password() def _print_floating_ip_list(floating_ips): From d27407798d8d7e75cfba7c31b577bc872ddb1ce7 Mon Sep 17 00:00:00 2001 From: Michael Still Date: Tue, 21 May 2013 14:42:00 +1000 Subject: [PATCH 0164/1705] Convert to more modern openstack-common.conf format. Change-Id: Ib02edb34b02d63f2dcf6fa08a224e5ec1fa02525 --- openstack-common.conf | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openstack-common.conf b/openstack-common.conf index a3197a085..74f13100a 100644 --- a/openstack-common.conf +++ b/openstack-common.conf @@ -1,7 +1,9 @@ [DEFAULT] # The list of modules to copy from openstack-common -modules=timeutils,strutils,uuidutils +module=strutils +module=timeutils +module=uuidutils # The base module to hold the copy of openstack.common base=novaclient From e8e7a0e04fa6ced2e5a02a533d29a20d3014b4a8 Mon Sep 17 00:00:00 2001 From: Nicolas Simonds Date: Tue, 21 May 2013 16:23:50 -0700 Subject: [PATCH 0165/1705] Only add logging handlers if there currently aren't any This corrects an odd problem where Horizon would stand up multiple client objects, which would cause duplicate/triplicate/dozens of repeated log lines in its log files, due to multiple identical handlers being added to the logging object Fixes Bug 1182678 Change-Id: I1f3bae0a39f28412c99e5d04f4364611f2a5facb --- novaclient/client.py | 2 +- tests/test_http.py | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/novaclient/client.py b/novaclient/client.py index 0cf595b99..bb963ce70 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -91,7 +91,7 @@ def __init__(self, user, password, projectid, auth_url=None, self.auth_plugin = auth_plugin self._logger = logging.getLogger(__name__) - if self.http_log_debug: + if self.http_log_debug and not self._logger.handlers: # Logging level is already set on the root logger ch = logging.StreamHandler() self._logger.addHandler(ch) diff --git a/tests/test_http.py b/tests/test_http.py index e89ad3fba..7925a91f0 100644 --- a/tests/test_http.py +++ b/tests/test_http.py @@ -111,3 +111,12 @@ def test_refused_call(): self.assertRaises(exceptions.BadRequest, cl.get, "/hi") test_refused_call() + + def test_client_logger(self): + cl1 = client.HTTPClient("username", "password", "project_id", + "auth_test", http_log_debug=True) + self.assertEquals(len(cl1._logger.handlers), 1) + + cl2 = client.HTTPClient("username", "password", "project_id", + "auth_test", http_log_debug=True) + self.assertEquals(len(cl2._logger.handlers), 1) From c305a458926f66d717d827dbad4596a7b22a02eb Mon Sep 17 00:00:00 2001 From: Sean Dague Date: Fri, 24 May 2013 09:04:09 -0400 Subject: [PATCH 0166/1705] Revert "Support force update quota" This reverts commit e8b665edbfcd86488f75ed0b79dafdedbf8f5950. The previous commit created an incompatibility in using new nova client with older nova server. Nova client needs to be always releasable, and work with all nova server API versions out there. Fixes bug #1173353 Change-Id: I2c07d109af4a35bc3b98dedaf991d5d3cc6fdd3b --- novaclient/v1_1/quotas.py | 5 ++--- novaclient/v1_1/shell.py | 13 +------------ tests/v1_1/test_quotas.py | 9 --------- tests/v1_1/test_shell.py | 21 +++------------------ 4 files changed, 6 insertions(+), 42 deletions(-) diff --git a/novaclient/v1_1/quotas.py b/novaclient/v1_1/quotas.py index c510b11ce..6aa7fe5ac 100644 --- a/novaclient/v1_1/quotas.py +++ b/novaclient/v1_1/quotas.py @@ -36,7 +36,7 @@ def get(self, tenant_id): tenant_id = tenant_id.tenant_id return self._get("/os-quota-sets/%s" % (tenant_id), "quota_set") - def update(self, tenant_id, force=None, metadata_items=None, + def update(self, tenant_id, metadata_items=None, injected_file_content_bytes=None, injected_file_path_bytes=None, volumes=None, gigabytes=None, ram=None, floating_ips=None, fixed_ips=None, instances=None, @@ -58,8 +58,7 @@ def update(self, tenant_id, force=None, metadata_items=None, 'injected_files': injected_files, 'cores': cores, 'security_groups': security_groups, - 'security_group_rules': security_group_rules, - 'force': force}} + 'security_group_rules': security_group_rules}} for key in body['quota_set'].keys(): if body['quota_set'][key] is None: diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 460ef926e..f56ad91b1 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -31,7 +31,6 @@ from novaclient.openstack.common import uuidutils from novaclient import utils from novaclient.v1_1 import availability_zones -from novaclient.v1_1 import quotas from novaclient.v1_1 import servers @@ -2791,11 +2790,7 @@ def _quota_update(manager, identifier, args): updates[resource] = val if updates: - force_update = getattr(args, 'force', False) - if isinstance(manager, quotas.QuotaSetManager): - manager.update(identifier, force_update, **updates) - else: - manager.update(identifier, **updates) + manager.update(identifier, **updates) @utils.arg('--tenant', @@ -2904,12 +2899,6 @@ def do_quota_defaults(cs, args): type=int, default=None, help='New value for the "security-group-rules" quota.') -@utils.arg('--force', - dest='force', - action="store_true", - default=False, - help='Whether force update the quota even if the already used' - ' and reserved exceeds the new quota') def do_quota_update(cs, args): """Update the quotas for a tenant.""" diff --git a/tests/v1_1/test_quotas.py b/tests/v1_1/test_quotas.py index c7aa64cfa..85a7a576a 100644 --- a/tests/v1_1/test_quotas.py +++ b/tests/v1_1/test_quotas.py @@ -37,15 +37,6 @@ def test_update_quota(self): cs.assert_called('PUT', '/os-quota-sets/97f4c221bff44578b0300df4ef119353') - def test_force_update_quota(self): - q = cs.quotas.get('97f4c221bff44578b0300df4ef119353') - q.update(cores=2, force=True) - cs.assert_called( - 'PUT', '/os-quota-sets/97f4c221bff44578b0300df4ef119353', - {'quota_set': {'force': True, - 'cores': 2, - 'tenant_id': '97f4c221bff44578b0300df4ef119353'}}) - def test_refresh_quota(self): q = cs.quotas.get('test') q2 = cs.quotas.get('test') diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index 3071ba08e..51ccf509d 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -1022,22 +1022,8 @@ def test_quota_update(self): self.run_command( 'quota-update 97f4c221bff44578b0300df4ef119353' ' --instances=5') - self.assert_called( - 'PUT', - '/os-quota-sets/97f4c221bff44578b0300df4ef119353', - {'quota_set': {'force': False, - 'instances': 5, - 'tenant_id': '97f4c221bff44578b0300df4ef119353'}}) - - def test_quota_force_update(self): - self.run_command( - 'quota-update 97f4c221bff44578b0300df4ef119353' - ' --instances=5 --force') - self.assert_called( - 'PUT', '/os-quota-sets/97f4c221bff44578b0300df4ef119353', - {'quota_set': {'force': True, - 'instances': 5, - 'tenant_id': '97f4c221bff44578b0300df4ef119353'}}) + self.assert_called('PUT', + '/os-quota-sets/97f4c221bff44578b0300df4ef119353') def test_quota_update_fixed_ip(self): self.run_command( @@ -1045,8 +1031,7 @@ def test_quota_update_fixed_ip(self): ' --fixed-ips=5') self.assert_called( 'PUT', '/os-quota-sets/97f4c221bff44578b0300df4ef119353', - {'quota_set': {'force': False, - 'fixed_ips': 5, + {'quota_set': {'fixed_ips': 5, 'tenant_id': '97f4c221bff44578b0300df4ef119353'}}) def test_quota_class_show(self): From 9d4db6f740fe00f50c77920b5ef7a4fbd08d181f Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 18 May 2013 07:13:38 -0700 Subject: [PATCH 0167/1705] Migrate to flake8. Fixes bug 1172444. Change-Id: I0d8faa0819738456a28aaf5cc859f510a2b3ed68 --- run_tests.sh | 9 ++------- tools/test-requires | 7 ++++++- tox.ini | 10 ++++++---- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/run_tests.sh b/run_tests.sh index a42d0d3d6..a7d66dd12 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -102,13 +102,8 @@ function copy_subunit_log { } function run_pep8 { - echo "Running pep8 ..." - srcfiles="--exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*" - srcfiles+=",*egg,build ." - # Just run PEP8 in current environment - # - ignore="--ignore=E12,E711,E721,E712" - ${wrapper} pep8 ${ignore} --show-source ${srcfiles} + echo "Running flake8 ..." + ${wrapper} flake8 } TESTRTESTS="testr run --parallel $testropts" diff --git a/tools/test-requires b/tools/test-requires index 203b0e264..8da41c8fd 100644 --- a/tools/test-requires +++ b/tools/test-requires @@ -1,11 +1,16 @@ distribute>=0.6.24 +# Install bounded pep8/pyflakes first, then let flake8 install +pep8==1.4.5 +pyflakes==0.7.2 +flake8==2.0 +hacking>=0.5.3,<0.6 + coverage discover fixtures>=0.3.12 keyring mock -pep8==1.3.3 sphinx>=1.1.2 testrepository>=0.0.13 testtools>=0.9.26 diff --git a/tox.ini b/tox.ini index 464e7859c..3318c0899 100644 --- a/tox.ini +++ b/tox.ini @@ -12,10 +12,7 @@ deps = -r{toxinidir}/tools/pip-requires commands = python setup.py testr --testr-args='{posargs}' [testenv:pep8] -deps = pep8==1.3.3 -commands = - pep8 --ignore=E12,E711,E721,E712 --show-source \ - --exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build . +commands = flake8 [testenv:venv] commands = {posargs} @@ -25,3 +22,8 @@ commands = python setup.py testr --coverage --testr-args='{posargs}' [tox:jenkins] downloadcache = ~/cache/pip + +[flake8] +ignore = E12,E711,E721,E712,F,H +show-source = True +exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build From 1a0b7b0a649657e2662e62f17322f61e8d7e9ed3 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 18 May 2013 07:11:26 -0700 Subject: [PATCH 0168/1705] Code cleanup in advance of flake8. Change-Id: Ia2b463fdf3092aa72147eeda41faad4733fddd46 --- tests/test_auth_plugins.py | 2 +- tests/v1_1/contrib/test_baremetal.py | 2 +- tests/v1_1/contrib/test_tenant_networks.py | 2 +- tests/v1_1/test_auth.py | 2 +- tests/v1_1/utils.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_auth_plugins.py b/tests/test_auth_plugins.py index 85d220b25..fae63247b 100644 --- a/tests/test_auth_plugins.py +++ b/tests/test_auth_plugins.py @@ -48,7 +48,7 @@ def mock_http_request(resp=None): { "region": "RegionOne", "adminURL": "http://localhost:8774/v1.1", - "internalURL":"http://localhost:8774/v1.1", + "internalURL": "http://localhost:8774/v1.1", "publicURL": "http://localhost:8774/v1.1/", }, ], diff --git a/tests/v1_1/contrib/test_baremetal.py b/tests/v1_1/contrib/test_baremetal.py index 3bf121ead..6925f18db 100644 --- a/tests/v1_1/contrib/test_baremetal.py +++ b/tests/v1_1/contrib/test_baremetal.py @@ -61,5 +61,5 @@ def test_node_remove_interface(self): cs.assert_called('POST', '/os-baremetal-nodes/1/action') def test_node_list_interfaces(self): - il = cs.baremetal.list_interfaces(1) + cs.baremetal.list_interfaces(1) cs.assert_called('GET', '/os-baremetal-nodes/1') diff --git a/tests/v1_1/contrib/test_tenant_networks.py b/tests/v1_1/contrib/test_tenant_networks.py index a96a83149..a79f76dd1 100644 --- a/tests/v1_1/contrib/test_tenant_networks.py +++ b/tests/v1_1/contrib/test_tenant_networks.py @@ -36,7 +36,7 @@ def test_list_tenant_networks(self): self.assertTrue(len(nets) > 0) def test_get_tenant_network(self): - net = cs.tenant_networks.get(1) + cs.tenant_networks.get(1) cs.assert_called('GET', '/os-tenant-networks/1') def test_create_tenant_networks(self): diff --git a/tests/v1_1/test_auth.py b/tests/v1_1/test_auth.py index 85dd4e4de..f5ddf35f8 100644 --- a/tests/v1_1/test_auth.py +++ b/tests/v1_1/test_auth.py @@ -125,7 +125,7 @@ def test_auth_redirect(self): } correct_response = json.dumps(dict_correct_response) dict_responses = [ - {"headers": {'location':'http://127.0.0.1:5001'}, + {"headers": {'location': 'http://127.0.0.1:5001'}, "status_code": 305, "text": "Use proxy"}, # Configured on admin port, nova redirects to v2.0 port. diff --git a/tests/v1_1/utils.py b/tests/v1_1/utils.py index f878a5e26..eaf108f55 100644 --- a/tests/v1_1/utils.py +++ b/tests/v1_1/utils.py @@ -20,7 +20,7 @@ def assert_has_keys(dict, required=[], optional=[]): for k in required: assert_in(k, keys, "required key %s missing from %s" % (k, dict)) allowed_keys = set(required) | set(optional) - extra_keys = set(keys).difference(set(required + optional)) + extra_keys = set(keys).difference(allowed_keys) if extra_keys: fail("found unexpected keys: %s" % list(extra_keys)) From 3bbdcda9d23b54079733ff6f06000cf311eff629 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 18 May 2013 07:17:04 -0700 Subject: [PATCH 0169/1705] Rename requires files to standard names. The python community groks requirements.txt and test-requirements.txt as reasonably standard files. We should use those filenames to make our information more discoverable. Fixes bug 1179008 Change-Id: I50a7c46f880e4257fa31d7d322d7bf70b0f5d3a6 --- tools/pip-requires => requirements.txt | 0 tools/test-requires => test-requirements.txt | 0 tools/install_venv.py | 4 ++-- tox.ini | 4 ++-- 4 files changed, 4 insertions(+), 4 deletions(-) rename tools/pip-requires => requirements.txt (100%) rename tools/test-requires => test-requirements.txt (100%) diff --git a/tools/pip-requires b/requirements.txt similarity index 100% rename from tools/pip-requires rename to requirements.txt diff --git a/tools/test-requires b/test-requirements.txt similarity index 100% rename from tools/test-requires rename to test-requirements.txt diff --git a/tools/install_venv.py b/tools/install_venv.py index 5742f7e11..8dbab463b 100644 --- a/tools/install_venv.py +++ b/tools/install_venv.py @@ -32,8 +32,8 @@ ROOT = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) VENV = os.path.join(ROOT, '.venv') -PIP_REQUIRES = os.path.join(ROOT, 'tools', 'pip-requires') -TEST_REQUIRES = os.path.join(ROOT, 'tools', 'test-requires') +PIP_REQUIRES = os.path.join(ROOT, 'requirements.txt') +TEST_REQUIRES = os.path.join(ROOT, 'test-requirements.txt') PY_VERSION = "python%s.%s" % (sys.version_info[0], sys.version_info[1]) diff --git a/tox.ini b/tox.ini index 3318c0899..42cbe2671 100644 --- a/tox.ini +++ b/tox.ini @@ -7,8 +7,8 @@ setenv = VIRTUAL_ENV={envdir} LANGUAGE=en_US:en LC_ALL=C -deps = -r{toxinidir}/tools/pip-requires - -r{toxinidir}/tools/test-requires +deps = -r{toxinidir}/requirements.txt + -r{toxinidir}/test-requirements.txt commands = python setup.py testr --testr-args='{posargs}' [testenv:pep8] From 51f05965e0743dac3792cc3c88dd836b6aaefcc7 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 18 May 2013 07:18:44 -0700 Subject: [PATCH 0170/1705] Add CONTRIBUTING file. This adds a pointer to how to contribute. Also, github will show this on the pull requests page, so having one of these is a friendly way to let people know we don't use those. Change-Id: I36829798adacb0f6b8144502baeff64ca84e32b6 --- CONTRIBUTING.rst | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 CONTRIBUTING.rst diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 000000000..6b38289cb --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,17 @@ +If you would like to contribute to the development of OpenStack, +you must follow the steps in the "If you're a developer, start here" +section of this page: + + http://wiki.openstack.org/HowToContribute + +Once those steps have been completed, changes to OpenStack +should be submitted for review via the Gerrit tool, following +the workflow documented at: + + http://wiki.openstack.org/GerritWorkflow + +Pull requests submitted through GitHub will be ignored. + +Bugs should be filed on Launchpad, not GitHub: + + https://bugs.launchpad.net/python-novaclient From c34c371e963f04f44339c07dfcd40b7814e3217f Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 18 May 2013 07:26:28 -0700 Subject: [PATCH 0171/1705] Move tests into the novaclient package. tests/__init__.py implies a package in the global namespace. These tests are not global python tests, but rather tests for novaclient. Change-Id: Ifeb8082aa010d15dddc9ae02e35589bc78ad48cc --- .testr.conf | 2 +- {tests => novaclient/tests}/__init__.py | 0 {tests => novaclient/tests}/fakes.py | 0 {tests => novaclient/tests}/test_auth_plugins.py | 2 +- {tests => novaclient/tests}/test_base.py | 4 ++-- {tests => novaclient/tests}/test_client.py | 2 +- {tests => novaclient/tests}/test_discover.py | 2 +- {tests => novaclient/tests}/test_http.py | 2 +- {tests => novaclient/tests}/test_service_catalog.py | 2 +- {tests => novaclient/tests}/test_shell.py | 2 +- {tests => novaclient/tests}/test_utils.py | 2 +- {tests => novaclient/tests}/utils.py | 0 {tests => novaclient/tests}/v1_1/__init__.py | 0 {tests => novaclient/tests}/v1_1/contrib/__init__.py | 0 {tests => novaclient/tests}/v1_1/contrib/fakes.py | 2 +- {tests => novaclient/tests}/v1_1/contrib/test_baremetal.py | 4 ++-- .../tests}/v1_1/contrib/test_instance_actions.py | 4 ++-- .../tests}/v1_1/contrib/test_list_extensions.py | 4 ++-- .../tests}/v1_1/contrib/test_tenant_networks.py | 4 ++-- {tests => novaclient/tests}/v1_1/fakes.py | 4 ++-- {tests => novaclient/tests}/v1_1/test_agents.py | 4 ++-- {tests => novaclient/tests}/v1_1/test_aggregates.py | 4 ++-- {tests => novaclient/tests}/v1_1/test_auth.py | 2 +- {tests => novaclient/tests}/v1_1/test_availability_zone.py | 4 ++-- {tests => novaclient/tests}/v1_1/test_certs.py | 4 ++-- {tests => novaclient/tests}/v1_1/test_cloudpipe.py | 4 ++-- {tests => novaclient/tests}/v1_1/test_coverage_ext.py | 4 ++-- {tests => novaclient/tests}/v1_1/test_fixed_ips.py | 4 ++-- {tests => novaclient/tests}/v1_1/test_flavor_access.py | 4 ++-- {tests => novaclient/tests}/v1_1/test_flavors.py | 4 ++-- {tests => novaclient/tests}/v1_1/test_floating_ip_dns.py | 4 ++-- {tests => novaclient/tests}/v1_1/test_floating_ip_pools.py | 4 ++-- {tests => novaclient/tests}/v1_1/test_floating_ips.py | 4 ++-- {tests => novaclient/tests}/v1_1/test_floating_ips_bulk.py | 4 ++-- {tests => novaclient/tests}/v1_1/test_fping.py | 4 ++-- {tests => novaclient/tests}/v1_1/test_hosts.py | 4 ++-- {tests => novaclient/tests}/v1_1/test_hypervisors.py | 4 ++-- {tests => novaclient/tests}/v1_1/test_images.py | 4 ++-- {tests => novaclient/tests}/v1_1/test_keypairs.py | 4 ++-- {tests => novaclient/tests}/v1_1/test_limits.py | 4 ++-- {tests => novaclient/tests}/v1_1/test_networks.py | 4 ++-- {tests => novaclient/tests}/v1_1/test_quota_classes.py | 4 ++-- {tests => novaclient/tests}/v1_1/test_quotas.py | 4 ++-- {tests => novaclient/tests}/v1_1/test_security_group_rules.py | 4 ++-- {tests => novaclient/tests}/v1_1/test_security_groups.py | 4 ++-- {tests => novaclient/tests}/v1_1/test_servers.py | 4 ++-- {tests => novaclient/tests}/v1_1/test_services.py | 4 ++-- {tests => novaclient/tests}/v1_1/test_shell.py | 4 ++-- {tests => novaclient/tests}/v1_1/test_usage.py | 4 ++-- {tests => novaclient/tests}/v1_1/testfile.txt | 0 {tests => novaclient/tests}/v1_1/utils.py | 0 51 files changed, 78 insertions(+), 78 deletions(-) rename {tests => novaclient/tests}/__init__.py (100%) rename {tests => novaclient/tests}/fakes.py (100%) rename {tests => novaclient/tests}/test_auth_plugins.py (99%) rename {tests => novaclient/tests}/test_base.py (96%) rename {tests => novaclient/tests}/test_client.py (99%) rename {tests => novaclient/tests}/test_discover.py (98%) rename {tests => novaclient/tests}/test_http.py (99%) rename {tests => novaclient/tests}/test_service_catalog.py (99%) rename {tests => novaclient/tests}/test_shell.py (99%) rename {tests => novaclient/tests}/test_utils.py (99%) rename {tests => novaclient/tests}/utils.py (100%) rename {tests => novaclient/tests}/v1_1/__init__.py (100%) rename {tests => novaclient/tests}/v1_1/contrib/__init__.py (100%) rename {tests => novaclient/tests}/v1_1/contrib/fakes.py (99%) rename {tests => novaclient/tests}/v1_1/contrib/test_baremetal.py (96%) rename {tests => novaclient/tests}/v1_1/contrib/test_instance_actions.py (94%) rename {tests => novaclient/tests}/v1_1/contrib/test_list_extensions.py (88%) rename {tests => novaclient/tests}/v1_1/contrib/test_tenant_networks.py (95%) rename {tests => novaclient/tests}/v1_1/fakes.py (99%) rename {tests => novaclient/tests}/v1_1/test_agents.py (97%) rename {tests => novaclient/tests}/v1_1/test_aggregates.py (98%) rename {tests => novaclient/tests}/v1_1/test_auth.py (99%) rename {tests => novaclient/tests}/v1_1/test_availability_zone.py (97%) rename {tests => novaclient/tests}/v1_1/test_certs.py (85%) rename {tests => novaclient/tests}/v1_1/test_cloudpipe.py (91%) rename {tests => novaclient/tests}/v1_1/test_coverage_ext.py (95%) rename {tests => novaclient/tests}/v1_1/test_fixed_ips.py (95%) rename {tests => novaclient/tests}/v1_1/test_flavor_access.py (96%) rename {tests => novaclient/tests}/v1_1/test_flavors.py (98%) rename {tests => novaclient/tests}/v1_1/test_floating_ip_dns.py (97%) rename {tests => novaclient/tests}/v1_1/test_floating_ip_pools.py (93%) rename {tests => novaclient/tests}/v1_1/test_floating_ips.py (96%) rename {tests => novaclient/tests}/v1_1/test_floating_ips_bulk.py (97%) rename {tests => novaclient/tests}/v1_1/test_fping.py (96%) rename {tests => novaclient/tests}/v1_1/test_hosts.py (96%) rename {tests => novaclient/tests}/v1_1/test_hypervisors.py (98%) rename {tests => novaclient/tests}/v1_1/test_images.py (95%) rename {tests => novaclient/tests}/v1_1/test_keypairs.py (93%) rename {tests => novaclient/tests}/v1_1/test_limits.py (96%) rename {tests => novaclient/tests}/v1_1/test_networks.py (97%) rename {tests => novaclient/tests}/v1_1/test_quota_classes.py (94%) rename {tests => novaclient/tests}/v1_1/test_quotas.py (95%) rename {tests => novaclient/tests}/v1_1/test_security_group_rules.py (96%) rename {tests => novaclient/tests}/v1_1/test_security_groups.py (96%) rename {tests => novaclient/tests}/v1_1/test_servers.py (99%) rename {tests => novaclient/tests}/v1_1/test_services.py (97%) rename {tests => novaclient/tests}/v1_1/test_shell.py (99%) rename {tests => novaclient/tests}/v1_1/test_usage.py (93%) rename {tests => novaclient/tests}/v1_1/testfile.txt (100%) rename {tests => novaclient/tests}/v1_1/utils.py (100%) diff --git a/.testr.conf b/.testr.conf index 2109af6ce..60477e871 100644 --- a/.testr.conf +++ b/.testr.conf @@ -1,4 +1,4 @@ [DEFAULT] -test_command=OS_STDOUT_CAPTURE=1 OS_STDERR_CAPTURE=1 ${PYTHON:-python} -m subunit.run discover -t ./ ./tests $LISTOPT $IDOPTION +test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} ${PYTHON:-python} -m subunit.run discover -t ./ ./ $LISTOPT $IDOPTION test_id_option=--load-list $IDFILE test_list_option=--list diff --git a/tests/__init__.py b/novaclient/tests/__init__.py similarity index 100% rename from tests/__init__.py rename to novaclient/tests/__init__.py diff --git a/tests/fakes.py b/novaclient/tests/fakes.py similarity index 100% rename from tests/fakes.py rename to novaclient/tests/fakes.py diff --git a/tests/test_auth_plugins.py b/novaclient/tests/test_auth_plugins.py similarity index 99% rename from tests/test_auth_plugins.py rename to novaclient/tests/test_auth_plugins.py index fae63247b..c4b37b57d 100644 --- a/tests/test_auth_plugins.py +++ b/novaclient/tests/test_auth_plugins.py @@ -26,7 +26,7 @@ from novaclient import auth_plugin from novaclient import exceptions from novaclient.v1_1 import client -from tests import utils +from novaclient.tests import utils def mock_http_request(resp=None): diff --git a/tests/test_base.py b/novaclient/tests/test_base.py similarity index 96% rename from tests/test_base.py rename to novaclient/tests/test_base.py index 30bea5708..58aeae962 100644 --- a/tests/test_base.py +++ b/novaclient/tests/test_base.py @@ -1,8 +1,8 @@ from novaclient import base from novaclient import exceptions from novaclient.v1_1 import flavors -from tests import utils -from tests.v1_1 import fakes +from novaclient.tests import utils +from novaclient.tests.v1_1 import fakes cs = fakes.FakeClient() diff --git a/tests/test_client.py b/novaclient/tests/test_client.py similarity index 99% rename from tests/test_client.py rename to novaclient/tests/test_client.py index 1f61b1716..3e74cdba4 100644 --- a/tests/test_client.py +++ b/novaclient/tests/test_client.py @@ -19,7 +19,7 @@ import novaclient.client import novaclient.v1_1.client -from tests import utils +from novaclient.tests import utils class ClientTest(utils.TestCase): diff --git a/tests/test_discover.py b/novaclient/tests/test_discover.py similarity index 98% rename from tests/test_discover.py rename to novaclient/tests/test_discover.py index 3b78f0caf..29c9e4756 100644 --- a/tests/test_discover.py +++ b/novaclient/tests/test_discover.py @@ -19,7 +19,7 @@ import pkg_resources import novaclient.shell -from tests import utils +from novaclient.tests import utils class DiscoverTest(utils.TestCase): diff --git a/tests/test_http.py b/novaclient/tests/test_http.py similarity index 99% rename from tests/test_http.py rename to novaclient/tests/test_http.py index 7925a91f0..015928015 100644 --- a/tests/test_http.py +++ b/novaclient/tests/test_http.py @@ -3,7 +3,7 @@ from novaclient import client from novaclient import exceptions -from tests import utils +from novaclient.tests import utils fake_response = utils.TestResponse({ diff --git a/tests/test_service_catalog.py b/novaclient/tests/test_service_catalog.py similarity index 99% rename from tests/test_service_catalog.py rename to novaclient/tests/test_service_catalog.py index 4ac7703fe..1d73c73dc 100644 --- a/tests/test_service_catalog.py +++ b/novaclient/tests/test_service_catalog.py @@ -1,6 +1,6 @@ from novaclient import exceptions from novaclient import service_catalog -from tests import utils +from novaclient.tests import utils # Taken directly from keystone/content/common/samples/auth.json diff --git a/tests/test_shell.py b/novaclient/tests/test_shell.py similarity index 99% rename from tests/test_shell.py rename to novaclient/tests/test_shell.py index ae78815d2..91c34bccc 100644 --- a/tests/test_shell.py +++ b/novaclient/tests/test_shell.py @@ -9,7 +9,7 @@ import novaclient.client from novaclient import exceptions import novaclient.shell -from tests import utils +from novaclient.tests import utils FAKE_ENV = {'OS_USERNAME': 'username', 'OS_PASSWORD': 'password', diff --git a/tests/test_utils.py b/novaclient/tests/test_utils.py similarity index 99% rename from tests/test_utils.py rename to novaclient/tests/test_utils.py index 7a0949ed2..0bd396a75 100644 --- a/tests/test_utils.py +++ b/novaclient/tests/test_utils.py @@ -6,7 +6,7 @@ from novaclient import exceptions from novaclient import utils from novaclient import base -from tests import utils as test_utils +from novaclient.tests import utils as test_utils UUID = '8e8ec658-c7b0-4243-bdf8-6f7f2952c0d0' diff --git a/tests/utils.py b/novaclient/tests/utils.py similarity index 100% rename from tests/utils.py rename to novaclient/tests/utils.py diff --git a/tests/v1_1/__init__.py b/novaclient/tests/v1_1/__init__.py similarity index 100% rename from tests/v1_1/__init__.py rename to novaclient/tests/v1_1/__init__.py diff --git a/tests/v1_1/contrib/__init__.py b/novaclient/tests/v1_1/contrib/__init__.py similarity index 100% rename from tests/v1_1/contrib/__init__.py rename to novaclient/tests/v1_1/contrib/__init__.py diff --git a/tests/v1_1/contrib/fakes.py b/novaclient/tests/v1_1/contrib/fakes.py similarity index 99% rename from tests/v1_1/contrib/fakes.py rename to novaclient/tests/v1_1/contrib/fakes.py index 6983406e7..a7bee0429 100644 --- a/tests/v1_1/contrib/fakes.py +++ b/novaclient/tests/v1_1/contrib/fakes.py @@ -13,7 +13,7 @@ # limitations under the License. from novaclient.v1_1 import client -from tests.v1_1 import fakes +from novaclient.tests.v1_1 import fakes class FakeClient(fakes.FakeClient): diff --git a/tests/v1_1/contrib/test_baremetal.py b/novaclient/tests/v1_1/contrib/test_baremetal.py similarity index 96% rename from tests/v1_1/contrib/test_baremetal.py rename to novaclient/tests/v1_1/contrib/test_baremetal.py index 6925f18db..88b129f0f 100644 --- a/tests/v1_1/contrib/test_baremetal.py +++ b/novaclient/tests/v1_1/contrib/test_baremetal.py @@ -17,8 +17,8 @@ from novaclient import extension from novaclient.v1_1.contrib import baremetal -from tests import utils -from tests.v1_1.contrib import fakes +from novaclient.tests import utils +from novaclient.tests.v1_1.contrib import fakes extensions = [ diff --git a/tests/v1_1/contrib/test_instance_actions.py b/novaclient/tests/v1_1/contrib/test_instance_actions.py similarity index 94% rename from tests/v1_1/contrib/test_instance_actions.py rename to novaclient/tests/v1_1/contrib/test_instance_actions.py index 8f8e1cd58..cdfa14985 100644 --- a/tests/v1_1/contrib/test_instance_actions.py +++ b/novaclient/tests/v1_1/contrib/test_instance_actions.py @@ -15,8 +15,8 @@ from novaclient import extension from novaclient.v1_1.contrib import instance_action -from tests import utils -from tests.v1_1.contrib import fakes +from novaclient.tests import utils +from novaclient.tests.v1_1.contrib import fakes extensions = [ diff --git a/tests/v1_1/contrib/test_list_extensions.py b/novaclient/tests/v1_1/contrib/test_list_extensions.py similarity index 88% rename from tests/v1_1/contrib/test_list_extensions.py rename to novaclient/tests/v1_1/contrib/test_list_extensions.py index 8b0651158..f9ede2972 100644 --- a/tests/v1_1/contrib/test_list_extensions.py +++ b/novaclient/tests/v1_1/contrib/test_list_extensions.py @@ -1,8 +1,8 @@ from novaclient import extension from novaclient.v1_1.contrib import list_extensions -from tests import utils -from tests.v1_1 import fakes +from novaclient.tests import utils +from novaclient.tests.v1_1 import fakes extensions = [ diff --git a/tests/v1_1/contrib/test_tenant_networks.py b/novaclient/tests/v1_1/contrib/test_tenant_networks.py similarity index 95% rename from tests/v1_1/contrib/test_tenant_networks.py rename to novaclient/tests/v1_1/contrib/test_tenant_networks.py index a79f76dd1..bb2cbaf8c 100644 --- a/tests/v1_1/contrib/test_tenant_networks.py +++ b/novaclient/tests/v1_1/contrib/test_tenant_networks.py @@ -18,8 +18,8 @@ from novaclient import extension from novaclient.v1_1.contrib import tenant_networks -from tests import utils -from tests.v1_1.contrib import fakes +from novaclient.tests import utils +from novaclient.tests.v1_1.contrib import fakes extensions = [ diff --git a/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py similarity index 99% rename from tests/v1_1/fakes.py rename to novaclient/tests/v1_1/fakes.py index c13e16edd..924d39051 100644 --- a/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -19,8 +19,8 @@ from novaclient import client as base_client from novaclient.v1_1 import client -from tests import fakes -from tests import utils +from novaclient.tests import fakes +from novaclient.tests import utils class FakeClient(fakes.FakeClient, client.Client): diff --git a/tests/v1_1/test_agents.py b/novaclient/tests/v1_1/test_agents.py similarity index 97% rename from tests/v1_1/test_agents.py rename to novaclient/tests/v1_1/test_agents.py index f599046e4..c9eaf3787 100644 --- a/tests/v1_1/test_agents.py +++ b/novaclient/tests/v1_1/test_agents.py @@ -16,8 +16,8 @@ # under the License. from novaclient.v1_1 import agents -from tests.v1_1 import fakes -from tests import utils +from novaclient.tests.v1_1 import fakes +from novaclient.tests import utils cs = fakes.FakeClient() diff --git a/tests/v1_1/test_aggregates.py b/novaclient/tests/v1_1/test_aggregates.py similarity index 98% rename from tests/v1_1/test_aggregates.py rename to novaclient/tests/v1_1/test_aggregates.py index 0b84067c6..f46d0871d 100644 --- a/tests/v1_1/test_aggregates.py +++ b/novaclient/tests/v1_1/test_aggregates.py @@ -14,8 +14,8 @@ # under the License. from novaclient.v1_1 import aggregates -from tests import utils -from tests.v1_1 import fakes +from novaclient.tests import utils +from novaclient.tests.v1_1 import fakes cs = fakes.FakeClient() diff --git a/tests/v1_1/test_auth.py b/novaclient/tests/v1_1/test_auth.py similarity index 99% rename from tests/v1_1/test_auth.py rename to novaclient/tests/v1_1/test_auth.py index f5ddf35f8..11a5e420d 100644 --- a/tests/v1_1/test_auth.py +++ b/novaclient/tests/v1_1/test_auth.py @@ -6,7 +6,7 @@ from novaclient.v1_1 import client from novaclient import exceptions -from tests import utils +from novaclient.tests import utils class AuthenticateAgainstKeystoneTests(utils.TestCase): diff --git a/tests/v1_1/test_availability_zone.py b/novaclient/tests/v1_1/test_availability_zone.py similarity index 97% rename from tests/v1_1/test_availability_zone.py rename to novaclient/tests/v1_1/test_availability_zone.py index 56a1480fb..896b78ad7 100644 --- a/tests/v1_1/test_availability_zone.py +++ b/novaclient/tests/v1_1/test_availability_zone.py @@ -16,8 +16,8 @@ from novaclient.v1_1 import availability_zones from novaclient.v1_1 import shell -from tests.v1_1 import fakes -from tests import utils +from novaclient.tests.v1_1 import fakes +from novaclient.tests import utils cs = fakes.FakeClient() diff --git a/tests/v1_1/test_certs.py b/novaclient/tests/v1_1/test_certs.py similarity index 85% rename from tests/v1_1/test_certs.py rename to novaclient/tests/v1_1/test_certs.py index 5cfef0826..ff1f61121 100644 --- a/tests/v1_1/test_certs.py +++ b/novaclient/tests/v1_1/test_certs.py @@ -1,6 +1,6 @@ from novaclient.v1_1 import certs -from tests import utils -from tests.v1_1 import fakes +from novaclient.tests import utils +from novaclient.tests.v1_1 import fakes cs = fakes.FakeClient() diff --git a/tests/v1_1/test_cloudpipe.py b/novaclient/tests/v1_1/test_cloudpipe.py similarity index 91% rename from tests/v1_1/test_cloudpipe.py rename to novaclient/tests/v1_1/test_cloudpipe.py index ad1e5bd7a..e428629f2 100644 --- a/tests/v1_1/test_cloudpipe.py +++ b/novaclient/tests/v1_1/test_cloudpipe.py @@ -1,6 +1,6 @@ from novaclient.v1_1 import cloudpipe -from tests import utils -from tests.v1_1 import fakes +from novaclient.tests import utils +from novaclient.tests.v1_1 import fakes cs = fakes.FakeClient() diff --git a/tests/v1_1/test_coverage_ext.py b/novaclient/tests/v1_1/test_coverage_ext.py similarity index 95% rename from tests/v1_1/test_coverage_ext.py rename to novaclient/tests/v1_1/test_coverage_ext.py index 27b33da47..0c78afaa4 100644 --- a/tests/v1_1/test_coverage_ext.py +++ b/novaclient/tests/v1_1/test_coverage_ext.py @@ -17,8 +17,8 @@ # See: http://wiki.openstack.org/Nova/CoverageExtension for more information # and usage explanation for this API extension -from tests import utils -from tests.v1_1 import fakes +from novaclient.tests import utils +from novaclient.tests.v1_1 import fakes cs = fakes.FakeClient() diff --git a/tests/v1_1/test_fixed_ips.py b/novaclient/tests/v1_1/test_fixed_ips.py similarity index 95% rename from tests/v1_1/test_fixed_ips.py rename to novaclient/tests/v1_1/test_fixed_ips.py index d8726b657..6881d1b40 100644 --- a/tests/v1_1/test_fixed_ips.py +++ b/novaclient/tests/v1_1/test_fixed_ips.py @@ -15,8 +15,8 @@ # License for the specific language governing permissions and limitations # under the License. -from tests.v1_1 import fakes -from tests import utils +from novaclient.tests.v1_1 import fakes +from novaclient.tests import utils cs = fakes.FakeClient() diff --git a/tests/v1_1/test_flavor_access.py b/novaclient/tests/v1_1/test_flavor_access.py similarity index 96% rename from tests/v1_1/test_flavor_access.py rename to novaclient/tests/v1_1/test_flavor_access.py index 929b51516..aea57a511 100644 --- a/tests/v1_1/test_flavor_access.py +++ b/novaclient/tests/v1_1/test_flavor_access.py @@ -14,8 +14,8 @@ # under the License. from novaclient.v1_1 import flavor_access -from tests import utils -from tests.v1_1 import fakes +from novaclient.tests import utils +from novaclient.tests.v1_1 import fakes cs = fakes.FakeClient() diff --git a/tests/v1_1/test_flavors.py b/novaclient/tests/v1_1/test_flavors.py similarity index 98% rename from tests/v1_1/test_flavors.py rename to novaclient/tests/v1_1/test_flavors.py index 936035330..eb7be2a4c 100644 --- a/tests/v1_1/test_flavors.py +++ b/novaclient/tests/v1_1/test_flavors.py @@ -1,7 +1,7 @@ from novaclient import exceptions from novaclient.v1_1 import flavors -from tests import utils -from tests.v1_1 import fakes +from novaclient.tests import utils +from novaclient.tests.v1_1 import fakes cs = fakes.FakeClient() diff --git a/tests/v1_1/test_floating_ip_dns.py b/novaclient/tests/v1_1/test_floating_ip_dns.py similarity index 97% rename from tests/v1_1/test_floating_ip_dns.py rename to novaclient/tests/v1_1/test_floating_ip_dns.py index 868fc054a..96cf90f43 100644 --- a/tests/v1_1/test_floating_ip_dns.py +++ b/novaclient/tests/v1_1/test_floating_ip_dns.py @@ -1,6 +1,6 @@ from novaclient.v1_1 import floating_ip_dns -from tests.v1_1 import fakes -from tests import utils +from novaclient.tests.v1_1 import fakes +from novaclient.tests import utils cs = fakes.FakeClient() diff --git a/tests/v1_1/test_floating_ip_pools.py b/novaclient/tests/v1_1/test_floating_ip_pools.py similarity index 93% rename from tests/v1_1/test_floating_ip_pools.py rename to novaclient/tests/v1_1/test_floating_ip_pools.py index 6091b2888..ba0fcb517 100644 --- a/tests/v1_1/test_floating_ip_pools.py +++ b/novaclient/tests/v1_1/test_floating_ip_pools.py @@ -15,8 +15,8 @@ # under the License. from novaclient.v1_1 import floating_ip_pools -from tests import utils -from tests.v1_1 import fakes +from novaclient.tests import utils +from novaclient.tests.v1_1 import fakes cs = fakes.FakeClient() diff --git a/tests/v1_1/test_floating_ips.py b/novaclient/tests/v1_1/test_floating_ips.py similarity index 96% rename from tests/v1_1/test_floating_ips.py rename to novaclient/tests/v1_1/test_floating_ips.py index f2895c6db..04088e097 100644 --- a/tests/v1_1/test_floating_ips.py +++ b/novaclient/tests/v1_1/test_floating_ips.py @@ -15,8 +15,8 @@ # under the License. from novaclient.v1_1 import floating_ips -from tests import utils -from tests.v1_1 import fakes +from novaclient.tests import utils +from novaclient.tests.v1_1 import fakes cs = fakes.FakeClient() diff --git a/tests/v1_1/test_floating_ips_bulk.py b/novaclient/tests/v1_1/test_floating_ips_bulk.py similarity index 97% rename from tests/v1_1/test_floating_ips_bulk.py rename to novaclient/tests/v1_1/test_floating_ips_bulk.py index c7706eca0..27acd5c7a 100644 --- a/tests/v1_1/test_floating_ips_bulk.py +++ b/novaclient/tests/v1_1/test_floating_ips_bulk.py @@ -15,8 +15,8 @@ # License for the specific language governing permissions and limitations # under the License. from novaclient.v1_1 import floating_ips_bulk -from tests import utils -from tests.v1_1 import fakes +from novaclient.tests import utils +from novaclient.tests.v1_1 import fakes cs = fakes.FakeClient() diff --git a/tests/v1_1/test_fping.py b/novaclient/tests/v1_1/test_fping.py similarity index 96% rename from tests/v1_1/test_fping.py rename to novaclient/tests/v1_1/test_fping.py index c17da741d..7f240e299 100644 --- a/tests/v1_1/test_fping.py +++ b/novaclient/tests/v1_1/test_fping.py @@ -16,8 +16,8 @@ # under the License. from novaclient.v1_1 import fping -from tests import utils -from tests.v1_1 import fakes +from novaclient.tests import utils +from novaclient.tests.v1_1 import fakes cs = fakes.FakeClient() diff --git a/tests/v1_1/test_hosts.py b/novaclient/tests/v1_1/test_hosts.py similarity index 96% rename from tests/v1_1/test_hosts.py rename to novaclient/tests/v1_1/test_hosts.py index 15bb3cb01..348d59cc1 100644 --- a/tests/v1_1/test_hosts.py +++ b/novaclient/tests/v1_1/test_hosts.py @@ -1,6 +1,6 @@ from novaclient.v1_1 import hosts -from tests.v1_1 import fakes -from tests import utils +from novaclient.tests.v1_1 import fakes +from novaclient.tests import utils cs = fakes.FakeClient() diff --git a/tests/v1_1/test_hypervisors.py b/novaclient/tests/v1_1/test_hypervisors.py similarity index 98% rename from tests/v1_1/test_hypervisors.py rename to novaclient/tests/v1_1/test_hypervisors.py index 12dd2de05..8d59d5984 100644 --- a/tests/v1_1/test_hypervisors.py +++ b/novaclient/tests/v1_1/test_hypervisors.py @@ -13,8 +13,8 @@ # License for the specific language governing permissions and limitations # under the License. -from tests import utils -from tests.v1_1 import fakes +from novaclient.tests import utils +from novaclient.tests.v1_1 import fakes cs = fakes.FakeClient() diff --git a/tests/v1_1/test_images.py b/novaclient/tests/v1_1/test_images.py similarity index 95% rename from tests/v1_1/test_images.py rename to novaclient/tests/v1_1/test_images.py index a4f8e6416..b545cedd5 100644 --- a/tests/v1_1/test_images.py +++ b/novaclient/tests/v1_1/test_images.py @@ -1,6 +1,6 @@ from novaclient.v1_1 import images -from tests import utils -from tests.v1_1 import fakes +from novaclient.tests import utils +from novaclient.tests.v1_1 import fakes cs = fakes.FakeClient() diff --git a/tests/v1_1/test_keypairs.py b/novaclient/tests/v1_1/test_keypairs.py similarity index 93% rename from tests/v1_1/test_keypairs.py rename to novaclient/tests/v1_1/test_keypairs.py index 10afd2497..2814c88ef 100644 --- a/tests/v1_1/test_keypairs.py +++ b/novaclient/tests/v1_1/test_keypairs.py @@ -1,6 +1,6 @@ from novaclient.v1_1 import keypairs -from tests import utils -from tests.v1_1 import fakes +from novaclient.tests import utils +from novaclient.tests.v1_1 import fakes cs = fakes.FakeClient() diff --git a/tests/v1_1/test_limits.py b/novaclient/tests/v1_1/test_limits.py similarity index 96% rename from tests/v1_1/test_limits.py rename to novaclient/tests/v1_1/test_limits.py index 3a8fa745c..550c9c321 100644 --- a/tests/v1_1/test_limits.py +++ b/novaclient/tests/v1_1/test_limits.py @@ -1,7 +1,7 @@ from novaclient.v1_1 import limits -from tests import utils -from tests.v1_1 import fakes +from novaclient.tests import utils +from novaclient.tests.v1_1 import fakes cs = fakes.FakeClient() diff --git a/tests/v1_1/test_networks.py b/novaclient/tests/v1_1/test_networks.py similarity index 97% rename from tests/v1_1/test_networks.py rename to novaclient/tests/v1_1/test_networks.py index 088f35574..d40ec53f2 100644 --- a/tests/v1_1/test_networks.py +++ b/novaclient/tests/v1_1/test_networks.py @@ -1,6 +1,6 @@ from novaclient.v1_1 import networks -from tests import utils -from tests.v1_1 import fakes +from novaclient.tests import utils +from novaclient.tests.v1_1 import fakes cs = fakes.FakeClient() diff --git a/tests/v1_1/test_quota_classes.py b/novaclient/tests/v1_1/test_quota_classes.py similarity index 94% rename from tests/v1_1/test_quota_classes.py rename to novaclient/tests/v1_1/test_quota_classes.py index a539ebeec..eceb606da 100644 --- a/tests/v1_1/test_quota_classes.py +++ b/novaclient/tests/v1_1/test_quota_classes.py @@ -13,8 +13,8 @@ # License for the specific language governing permissions and limitations # under the License. -from tests import utils -from tests.v1_1 import fakes +from novaclient.tests import utils +from novaclient.tests.v1_1 import fakes cs = fakes.FakeClient() diff --git a/tests/v1_1/test_quotas.py b/novaclient/tests/v1_1/test_quotas.py similarity index 95% rename from tests/v1_1/test_quotas.py rename to novaclient/tests/v1_1/test_quotas.py index 85a7a576a..69c2952f1 100644 --- a/tests/v1_1/test_quotas.py +++ b/novaclient/tests/v1_1/test_quotas.py @@ -13,8 +13,8 @@ # License for the specific language governing permissions and limitations # under the License. -from tests import utils -from tests.v1_1 import fakes +from novaclient.tests import utils +from novaclient.tests.v1_1 import fakes cs = fakes.FakeClient() diff --git a/tests/v1_1/test_security_group_rules.py b/novaclient/tests/v1_1/test_security_group_rules.py similarity index 96% rename from tests/v1_1/test_security_group_rules.py rename to novaclient/tests/v1_1/test_security_group_rules.py index acf1b7897..6a3a06c56 100644 --- a/tests/v1_1/test_security_group_rules.py +++ b/novaclient/tests/v1_1/test_security_group_rules.py @@ -1,7 +1,7 @@ from novaclient import exceptions from novaclient.v1_1 import security_group_rules -from tests import utils -from tests.v1_1 import fakes +from novaclient.tests import utils +from novaclient.tests.v1_1 import fakes cs = fakes.FakeClient() diff --git a/tests/v1_1/test_security_groups.py b/novaclient/tests/v1_1/test_security_groups.py similarity index 96% rename from tests/v1_1/test_security_groups.py rename to novaclient/tests/v1_1/test_security_groups.py index 122ebe6a5..962d03822 100644 --- a/tests/v1_1/test_security_groups.py +++ b/novaclient/tests/v1_1/test_security_groups.py @@ -1,6 +1,6 @@ from novaclient.v1_1 import security_groups -from tests import utils -from tests.v1_1 import fakes +from novaclient.tests import utils +from novaclient.tests.v1_1 import fakes cs = fakes.FakeClient() diff --git a/tests/v1_1/test_servers.py b/novaclient/tests/v1_1/test_servers.py similarity index 99% rename from tests/v1_1/test_servers.py rename to novaclient/tests/v1_1/test_servers.py index e44a35189..7120426c6 100644 --- a/tests/v1_1/test_servers.py +++ b/novaclient/tests/v1_1/test_servers.py @@ -6,8 +6,8 @@ from novaclient import exceptions from novaclient.v1_1 import servers -from tests import utils -from tests.v1_1 import fakes +from novaclient.tests import utils +from novaclient.tests.v1_1 import fakes cs = fakes.FakeClient() diff --git a/tests/v1_1/test_services.py b/novaclient/tests/v1_1/test_services.py similarity index 97% rename from tests/v1_1/test_services.py rename to novaclient/tests/v1_1/test_services.py index 701b4cb0e..381fb143d 100644 --- a/tests/v1_1/test_services.py +++ b/novaclient/tests/v1_1/test_services.py @@ -16,8 +16,8 @@ # under the License. from novaclient.v1_1 import services -from tests.v1_1 import fakes -from tests import utils +from novaclient.tests.v1_1 import fakes +from novaclient.tests import utils cs = fakes.FakeClient() diff --git a/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py similarity index 99% rename from tests/v1_1/test_shell.py rename to novaclient/tests/v1_1/test_shell.py index 51ccf509d..5b17b2b9e 100644 --- a/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -29,8 +29,8 @@ from novaclient import exceptions from novaclient.openstack.common import timeutils import novaclient.shell -from tests.v1_1 import fakes -from tests import utils +from novaclient.tests.v1_1 import fakes +from novaclient.tests import utils class ShellFixture(fixtures.Fixture): diff --git a/tests/v1_1/test_usage.py b/novaclient/tests/v1_1/test_usage.py similarity index 93% rename from tests/v1_1/test_usage.py rename to novaclient/tests/v1_1/test_usage.py index 10ca37db6..49b0baedf 100644 --- a/tests/v1_1/test_usage.py +++ b/novaclient/tests/v1_1/test_usage.py @@ -1,8 +1,8 @@ import datetime from novaclient.v1_1 import usage -from tests import utils -from tests.v1_1 import fakes +from novaclient.tests import utils +from novaclient.tests.v1_1 import fakes cs = fakes.FakeClient() diff --git a/tests/v1_1/testfile.txt b/novaclient/tests/v1_1/testfile.txt similarity index 100% rename from tests/v1_1/testfile.txt rename to novaclient/tests/v1_1/testfile.txt diff --git a/tests/v1_1/utils.py b/novaclient/tests/v1_1/utils.py similarity index 100% rename from tests/v1_1/utils.py rename to novaclient/tests/v1_1/utils.py From f2559c4c39af60d8704b813ae0e8effa427a73a0 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Sun, 26 May 2013 08:14:30 -0700 Subject: [PATCH 0172/1705] Add MethodNotAllowed and Conflict exception classes It is expected to recieve a 405 or a 409 from Nova, but novaclient presents them as a generic ClientException. This patch adds MethodNotAllowed and Conflict classes to represent these HTTP responses. Change-Id: If89cee04ebd74daaa28ea30b5c57cdc63edc1551 --- novaclient/exceptions.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/novaclient/exceptions.py b/novaclient/exceptions.py index b331c3c01..afea93326 100644 --- a/novaclient/exceptions.py +++ b/novaclient/exceptions.py @@ -116,6 +116,22 @@ class NotFound(ClientException): message = "Not found" +class MethodNotAllowed(ClientException): + """ + HTTP 405 - Method Not Allowed + """ + http_status = 405 + message = "Method Not Allowed" + + +class Conflict(ClientException): + """ + HTTP 409 - Conflict + """ + http_status = 409 + message = "Conflict" + + class OverLimit(ClientException): """ HTTP 413 - Over limit: you're over the API limits for this time period. @@ -147,8 +163,9 @@ class HTTPNotImplemented(ClientException): # for c in ClientException.__subclasses__()) # # Instead, we have to hardcode it: -_code_map = dict((c.http_status, c) for c in [BadRequest, Unauthorized, - Forbidden, NotFound, OverLimit, HTTPNotImplemented]) +_error_classes = [BadRequest, Unauthorized, Forbidden, NotFound, + MethodNotAllowed, Conflict, OverLimit, HTTPNotImplemented] +_code_map = dict((c.http_status, c) for c in _error_classes) def from_response(response, body, url, method=None): From ff85bd4025db92d318e19bd4d933848a344d5ab4 Mon Sep 17 00:00:00 2001 From: Emanuele Rocca Date: Fri, 24 May 2013 19:04:37 +0200 Subject: [PATCH 0173/1705] 100% test coverage for security groups and rules This patch achieves full test coverage for security_group and security_group_rules by: - Fixing the arguments used in test_invalid_parameters_create - Testing the __str__ and delete methods of SecurityGroupRule - Adding a test for the ___str__ method of SecurityGroup Change-Id: I9cfbc68761f158754aa4b339238d29cc587c91e1 --- .../tests/v1_1/test_security_group_rules.py | 21 +++++++++++++------ novaclient/tests/v1_1/test_security_groups.py | 1 + 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/novaclient/tests/v1_1/test_security_group_rules.py b/novaclient/tests/v1_1/test_security_group_rules.py index 6a3a06c56..e42592761 100644 --- a/novaclient/tests/v1_1/test_security_group_rules.py +++ b/novaclient/tests/v1_1/test_security_group_rules.py @@ -49,11 +49,20 @@ def test_create_security_group_group_rule(self): def test_invalid_parameters_create(self): self.assertRaises(exceptions.CommandError, - cs.security_group_rules.create, "secgrouprulecreate", - 1, "invalid", 1, 65535, "10.0.0.0/16") + cs.security_group_rules.create, + 1, "invalid_ip_protocol", 1, 65535, "10.0.0.0/16", 101) self.assertRaises(exceptions.CommandError, - cs.security_group_rules.create, "secgrouprulecreate", - 1, "tcp", "invalid", 65535, "10.0.0.0/16") + cs.security_group_rules.create, + 1, "tcp", "invalid_from_port", 65535, "10.0.0.0/16", 101) self.assertRaises(exceptions.CommandError, - cs.security_group_rules.create, "secgrouprulecreate", - 1, "tcp", 1, "invalid", "10.0.0.0/16") + cs.security_group_rules.create, + 1, "tcp", 1, "invalid_to_port", "10.0.0.0/16", 101) + + def test_security_group_rule_str(self): + sg = cs.security_group_rules.create(1, "tcp", 1, 65535, "10.0.0.0/16") + self.assertEquals('1', str(sg)) + + def test_security_group_rule_del(self): + sg = cs.security_group_rules.create(1, "tcp", 1, 65535, "10.0.0.0/16") + sg.delete() + cs.assert_called('DELETE', '/os-security-group-rules/1') diff --git a/novaclient/tests/v1_1/test_security_groups.py b/novaclient/tests/v1_1/test_security_groups.py index 962d03822..ce8e66337 100644 --- a/novaclient/tests/v1_1/test_security_groups.py +++ b/novaclient/tests/v1_1/test_security_groups.py @@ -29,6 +29,7 @@ def test_get_security_groups(self): sg = cs.security_groups.get(1) cs.assert_called('GET', '/os-security-groups/1') self.assertTrue(isinstance(sg, security_groups.SecurityGroup)) + self.assertEquals('1', str(sg)) def test_delete_security_group(self): sg = cs.security_groups.list()[0] From a8ed2f273adb39327e0bc1bbd33c45fefc272814 Mon Sep 17 00:00:00 2001 From: Hugh Saunders Date: Tue, 28 May 2013 15:27:20 +0100 Subject: [PATCH 0174/1705] Improve error messages for invalid --nic / --file. Implemented by catching the valueerror thrown when unpacking a split to two variables but the split string is not present. Change-Id: I9930fe828c932e103fdad1b88c3434f097664f31 --- novaclient/v1_1/shell.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index f56ad91b1..33457c59a 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -100,11 +100,14 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): files = {} for f in args.files: - dst, src = f.split('=', 1) try: + dst, src = f.split('=', 1) files[dst] = open(src) except IOError as e: raise exceptions.CommandError("Can't open '%s': %s" % (src, e)) + except ValueError as e: + raise exceptions.CommandError("Invalid file argument '%s'. File " + "arguments must be of the form '--file '" % f) # use the os-keypair extension key_name = None @@ -139,8 +142,14 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): for nic_str in args.nics: nic_info = {"net-id": "", "v4-fixed-ip": "", "port-id": ""} for kv_str in nic_str.split(","): - k, v = kv_str.split("=", 1) - nic_info[k] = v + try: + k, v = kv_str.split("=", 1) + nic_info[k] = v + except ValueError as e: + raise exceptions.CommandError( + "Invalid nic argument '%s'. Nic arguments must be of the " + "form --nic " % nic_str) nics.append(nic_info) hints = {} From 37da28cff4be133f19d5b48ca0257e62c1c49992 Mon Sep 17 00:00:00 2001 From: Jakub Ruzicka Date: Fri, 24 May 2013 17:37:51 +0200 Subject: [PATCH 0175/1705] Provide nova CLI man page. Resolves: bug 1130815 Blueprint: clients-man-pages Provide basic but hopefully useful man page. Change-Id: I6c520fa3acdb82dd564b758b9cdc448eecc6d6ce --- doc/source/conf.py | 13 +++++-- doc/source/man/nova.rst | 85 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 3 deletions(-) create mode 100644 doc/source/man/nova.rst diff --git a/doc/source/conf.py b/doc/source/conf.py index 7a34d4839..8aab078bd 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -40,16 +40,16 @@ # General information about the project. project = u'python-novaclient' -copyright = u'Rackspace, based on work by Jacob Kaplan-Moss' +copyright = u'OpenStack Contributors' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = '2.6' +version = '2.13' # The full version, including alpha/beta/rc tags. -release = '2.6.10' +release = '2.13.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -89,6 +89,13 @@ # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] +# Grouping the document tree for man pages. +# List of tuples 'sourcefile', 'target', u'title', u'Authors name', 'manual' + +man_pages = [ + ('man/nova', 'nova', u'OpenStack Nova command line client', + [u'OpenStack Contributors'], 1), +] # -- Options for HTML output -------------------------------------------------- diff --git a/doc/source/man/nova.rst b/doc/source/man/nova.rst new file mode 100644 index 000000000..a502b4393 --- /dev/null +++ b/doc/source/man/nova.rst @@ -0,0 +1,85 @@ +==== +nova +==== + + +SYNOPSIS +======== + + `nova` [options] [command-options] + + `nova help` + + `nova help` + + +DESCRIPTION +=========== + +`nova` is a command line client for controlling OpenStack Nova, the cloud +computing fabric controller. It implements 100% of the Nova API, allowing +management of instances, images, quotas and much more. + +Before you can issue commands with `nova`, you must ensure that your +environment contains the necessary variables so that you can prove to the CLI +who you are and what credentials you have to issue the commands. See +`Getting Credentials for a CLI` section of `OpenStack CLI Guide` for more +info. + +See `OpenStack Nova CLI Guide` for a full-fledged guide. + + +OPTIONS +======= + +To get a list of available commands and options run:: + + nova help + +To get usage and options of a command run:: + + nova help + + +EXAMPLES +======== + +Get information about boot command:: + + nova help boot + +List available images:: + + nova image-list + +List available flavors:: + + nova flavor-list + +Launch an instance:: + + nova boot myserver --image some-image --flavor 2 + +View instance information:: + + nova show myserver + +List instances:: + + nova list + +Terminate an instance:: + + nova delete myserver + + +SEE ALSO +======== + +OpenStack Nova CLI Guide: http://docs.openstack.org/cli/quick-start/content/nova-cli-reference.html + + +BUGS +==== + +Nova client is hosted in Launchpad so you can view current bugs at https://bugs.launchpad.net/python-novaclient/. From def5df2760b38b1493548722ddd49082f3773c31 Mon Sep 17 00:00:00 2001 From: Adam Gandelman Date: Wed, 29 May 2013 14:20:43 -0700 Subject: [PATCH 0176/1705] Fix shell tests for older prettytable versions. Fix the shell tests so they pass for all supported prettytable versions, as per requirements.txt (>=0.6,<0.8). Fixes bug: #1185580. Change-Id: I8dca23faa3c178494656ebc8088b6d1994e9869f --- novaclient/tests/test_shell.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/novaclient/tests/test_shell.py b/novaclient/tests/test_shell.py index 91c34bccc..3530a6820 100644 --- a/novaclient/tests/test_shell.py +++ b/novaclient/tests/test_shell.py @@ -1,7 +1,10 @@ import cStringIO +import prettytable import re import sys +from distutils.version import StrictVersion + import fixtures import mock from testtools import matchers @@ -148,13 +151,21 @@ def test_no_auth_url(self): @mock.patch('sys.stdin', side_effect=mock.MagicMock) @mock.patch('getpass.getpass', return_value='password') def test_password(self, mock_getpass, mock_stdin): + # default output of empty tables differs depending between prettytable + # versions + if (hasattr(prettytable, '__version__') and + StrictVersion(prettytable.__version__) < StrictVersion('0.7.2')): + ex = '\n' + else: + ex = ( + '+----+------+--------+------------+-------------+----------+\n' + '| ID | Name | Status | Task State | Power State | Networks |\n' + '+----+------+--------+------------+-------------+----------+\n' + '+----+------+--------+------------+-------------+----------+\n' + ) self.make_env(exclude='OS_PASSWORD') stdout, stderr = self.shell('list') - self.assertEqual((stdout + stderr), - '+----+------+--------+------------+-------------+----------+\n' - '| ID | Name | Status | Task State | Power State | Networks |\n' - '+----+------+--------+------------+-------------+----------+\n' - '+----+------+--------+------------+-------------+----------+\n') + self.assertEqual((stdout + stderr), ex) @mock.patch('sys.stdin', side_effect=mock.MagicMock) @mock.patch('getpass.getpass', side_effect=EOFError) From 66a98966bf8924b01c983ac9ac31d4e45476339c Mon Sep 17 00:00:00 2001 From: Zhenguo Niu Date: Wed, 22 May 2013 16:28:34 +0800 Subject: [PATCH 0177/1705] Add update method of security group name and description make it possible to edit the name and description of common security groups, we can not rename the default. nova patch : https://review.openstack.org/#/c/29490/ Fixes: bug #918393 Change-Id: I559f2fa09c1f205d3bbe7352fc169152e6b38586 --- README.rst | 1 + novaclient/tests/v1_1/fakes.py | 6 ++++++ novaclient/tests/v1_1/test_security_groups.py | 6 ++++++ novaclient/tests/v1_1/test_shell.py | 7 +++++++ novaclient/v1_1/security_groups.py | 16 ++++++++++++++++ novaclient/v1_1/shell.py | 13 +++++++++++++ 6 files changed, 49 insertions(+) diff --git a/README.rst b/README.rst index ed4dc96b5..6207d89f0 100644 --- a/README.rst +++ b/README.rst @@ -160,6 +160,7 @@ You'll find complete documentation on the shell by running Add a source group rule to a security group. secgroup-add-rule Add a rule to a security group. secgroup-create Create a security group. + secgroup-update Update a security group. secgroup-delete Delete a security group. secgroup-delete-group-rule Delete a source group rule from a security group. diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index 924d39051..3d6eb7221 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -1130,6 +1130,12 @@ def post_os_security_groups(self, body, **kw): self.get_os_security_groups()[2]['security_groups'][0]} return (202, {}, r) + def put_os_security_groups_1(self, body, **kw): + assert body.keys() == ['security_group'] + fakes.assert_has_keys(body['security_group'], + required=['name', 'description']) + return (205, {}, body) + # # Security Group Rules # diff --git a/novaclient/tests/v1_1/test_security_groups.py b/novaclient/tests/v1_1/test_security_groups.py index ce8e66337..9f3ae46bb 100644 --- a/novaclient/tests/v1_1/test_security_groups.py +++ b/novaclient/tests/v1_1/test_security_groups.py @@ -45,6 +45,12 @@ def test_create_security_group(self): cs.assert_called('POST', '/os-security-groups') self.assertTrue(isinstance(sg, security_groups.SecurityGroup)) + def test_update_security_group(self): + sg = cs.security_groups.list()[0] + secgroup = cs.security_groups.update(sg, "update", "update") + cs.assert_called('PUT', '/os-security-groups/1') + self.assertTrue(isinstance(secgroup, security_groups.SecurityGroup)) + def test_refresh_security_group(self): sg = cs.security_groups.get(1) sg2 = cs.security_groups.get(1) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index 5b17b2b9e..5d4336490 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -1206,6 +1206,13 @@ def test_security_group_create(self): {'name': 'test', 'description': 'FAKE_SECURITY_GROUP'}}) + def test_security_group_update(self): + self.run_command('secgroup-update test te FAKE_SECURITY_GROUP') + self.assert_called('PUT', '/os-security-groups/1', + {'security_group': + {'name': 'te', + 'description': 'FAKE_SECURITY_GROUP'}}) + def test_security_group_list(self): self.run_command('secgroup-list') self.assert_called('GET', '/os-security-groups') diff --git a/novaclient/v1_1/security_groups.py b/novaclient/v1_1/security_groups.py index 551811526..d0778634a 100644 --- a/novaclient/v1_1/security_groups.py +++ b/novaclient/v1_1/security_groups.py @@ -29,6 +29,9 @@ def __str__(self): def delete(self): self.manager.delete(self) + def update(self): + self.manager.update(self) + class SecurityGroupManager(base.ManagerWithFind): resource_class = SecurityGroup @@ -44,6 +47,19 @@ def create(self, name, description): body = {"security_group": {"name": name, 'description': description}} return self._create('/os-security-groups', body, 'security_group') + def update(self, group, name, description): + """ + Update a security group + + :param group: The security group to delete (group or ID) + :param name: name for the security group to update + :param description: description for the security group to update + :rtype: the security group object + """ + body = {"security_group": {"name": name, 'description': description}} + return self._update('/os-security-groups/%s' % base.getid(group), + body, 'security_group') + def delete(self, group): """ Delete a security group diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 33457c59a..d1ee17536 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1978,6 +1978,19 @@ def do_secgroup_create(cs, args): _print_secgroups([secgroup]) +@utils.arg('secgroup', + metavar='', + help='ID or name of security group.') +@utils.arg('name', metavar='', help='Name of security group.') +@utils.arg('description', metavar='', + help='Description of security group.') +def do_secgroup_update(cs, args): + """Update a security group.""" + sg = _get_secgroup(cs, args.secgroup) + secgroup = cs.security_groups.update(sg, args.name, args.description) + _print_secgroups([secgroup]) + + @utils.arg('secgroup', metavar='', help='ID or name of security group.') From 67c80558b1419faa1fd648410e5b9c332a0b367b Mon Sep 17 00:00:00 2001 From: Dirk Mueller Date: Sat, 1 Jun 2013 15:49:51 +0200 Subject: [PATCH 0178/1705] Start using Hacking and PyFlakes Only blacklisting those warnings that are numerous, so that in principle Hacking and PyFlakes warnings are tested. Fix the easy ones alongway. Change-Id: I571f51ebf570ac114509f2dcd71cdce281e7c70a --- novaclient/service_catalog.py | 2 +- novaclient/tests/v1_1/test_servers.py | 4 ++-- novaclient/v1_1/__init__.py | 2 +- novaclient/v1_1/flavors.py | 2 +- novaclient/v1_1/servers.py | 2 +- novaclient/v1_1/shell.py | 4 ++-- tools/install_venv.py | 1 - tox.ini | 2 +- 8 files changed, 9 insertions(+), 10 deletions(-) diff --git a/novaclient/service_catalog.py b/novaclient/service_catalog.py index d3fefe184..2c4c99716 100644 --- a/novaclient/service_catalog.py +++ b/novaclient/service_catalog.py @@ -50,7 +50,7 @@ def url_for(self, attr=None, filter_value=None, raise novaclient.exceptions.EndpointNotFound() # We don't always get a service catalog back ... - if not 'serviceCatalog' in self.catalog['access']: + if 'serviceCatalog' not in self.catalog['access']: return None # Full catalog ... diff --git a/novaclient/tests/v1_1/test_servers.py b/novaclient/tests/v1_1/test_servers.py index 7120426c6..f9b48a278 100644 --- a/novaclient/tests/v1_1/test_servers.py +++ b/novaclient/tests/v1_1/test_servers.py @@ -169,9 +169,9 @@ def test_set_server_meta(self): {'metadata': {'test_key': 'test_value'}}) def test_find(self): - s = cs.servers.find(name='sample-server') + server = cs.servers.find(name='sample-server') cs.assert_called('GET', '/servers/detail') - self.assertEqual(s.name, 'sample-server') + self.assertEqual(server.name, 'sample-server') self.assertRaises(exceptions.NoUniqueMatch, cs.servers.find, flavor={"id": 1, "name": "256 MB Server"}) diff --git a/novaclient/v1_1/__init__.py b/novaclient/v1_1/__init__.py index 8958eb45a..19712285f 100644 --- a/novaclient/v1_1/__init__.py +++ b/novaclient/v1_1/__init__.py @@ -14,4 +14,4 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.v1_1.client import Client +from novaclient.v1_1.client import Client # noqa diff --git a/novaclient/v1_1/flavors.py b/novaclient/v1_1/flavors.py index 0cd5fc77b..1f480f4b1 100644 --- a/novaclient/v1_1/flavors.py +++ b/novaclient/v1_1/flavors.py @@ -170,7 +170,7 @@ def create(self, name, ram, vcpus, disk, flavorid=None, try: is_public = utils.bool_from_str(is_public) - except: + except Exception: raise exceptions.CommandError("is_public must be a boolean.") body = { diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index c65303fad..440c63192 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -536,7 +536,7 @@ def create(self, name, image, flavor, meta=None, files=None, key_name=None, availability_zone=None, block_device_mapping=None, nics=None, scheduler_hints=None, config_drive=None, **kwargs): - # TODO: (anthony) indicate in doc string if param is an extension + # TODO(anthony): indicate in doc string if param is an extension # and/or optional """ Create (boot) a new server. diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 33457c59a..4c1627ce8 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1251,7 +1251,7 @@ def do_image_create(cs, args): @utils.arg('rotation', metavar='', help='Int parameter representing how many backups to keep around.') def do_backup(cs, args): - """ Backup a instance by create a 'backup' type snapshot """ + """Backup a instance by create a 'backup' type snapshot """ _find_server(cs, args.server).backup(args.name, args.backup_type, args.rotation) @@ -3127,7 +3127,7 @@ def do_availability_zone_list(cs, _args): except exceptions.Forbidden as e: # policy doesn't allow probably try: availability_zones = cs.availability_zones.list(detailed=False) - except: + except Exception: raise e result = [] diff --git a/tools/install_venv.py b/tools/install_venv.py index 8dbab463b..59d0b30a3 100644 --- a/tools/install_venv.py +++ b/tools/install_venv.py @@ -27,7 +27,6 @@ import os import subprocess import sys -import platform ROOT = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) diff --git a/tox.ini b/tox.ini index 42cbe2671..067e2b5cf 100644 --- a/tox.ini +++ b/tox.ini @@ -24,6 +24,6 @@ commands = python setup.py testr --coverage --testr-args='{posargs}' downloadcache = ~/cache/pip [flake8] -ignore = E12,E711,E721,E712,F,H +ignore = E12,E711,E721,E712,F841,F811,F821,H302,H306,H402,H403,H404 show-source = True exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build From 8820623d2a32c0a314c811ad58e974c8417df225 Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Sat, 1 Jun 2013 19:40:09 -0500 Subject: [PATCH 0179/1705] python3: Introduce py33 to tox.ini Introduce py33 to tox.ini to make testing with python3 easier. Change-Id: I2f339a2ccf113ff14db64c2bbc67a93bd0270962 Signed-off-by: Chuck Short --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 42cbe2671..6eade2fda 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py26,py27,pep8 +envlist = py26,py27,py33,pep8 [testenv] setenv = VIRTUAL_ENV={envdir} From 0d678ed4bb355a80211144e698da22b287b23027 Mon Sep 17 00:00:00 2001 From: Aarti Kriplani Date: Fri, 10 May 2013 12:04:42 -0500 Subject: [PATCH 0180/1705] Evacuate each instance from one host to another Added a new extension that adds the ability for admins to evacuate an entire host to another host. This internally uses the existing server.evacuate api. The target host is optional so that a free host will be chosen by the scheduler in the api. Implements: blueprint evacuate-host Change-Id: I2352836d01952e281e15edb9bdd1b912106516d6 --- novaclient/tests/v1_1/fakes.py | 12 +++++ novaclient/tests/v1_1/test_shell.py | 49 ++++++++++++++++++++ novaclient/v1_1/contrib/host_evacuate.py | 59 ++++++++++++++++++++++++ 3 files changed, 120 insertions(+) create mode 100644 novaclient/v1_1/contrib/host_evacuate.py diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index 924d39051..9cbcd518e 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -1702,3 +1702,15 @@ def get_servers_1234_os_instance_actions_req_abcde12345(self, **kw): "action": "create", "message": None, "project_id": "04019601fe3648c0abd4f4abfb9e6106"}}) + + def post_servers_uuid1_action(self, **kw): + return 202, {}, {} + + def post_servers_uuid2_action(self, **kw): + return 202, {}, {} + + def post_servers_uuid3_action(self, **kw): + return 202, {}, {} + + def post_servers_uuid4_action(self, **kw): + return 202, {}, {} diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index 5b17b2b9e..0ac3ecc3f 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -942,6 +942,55 @@ def test_host_reboot(self): self.assert_called( 'POST', '/os-hosts/sample-host/action', {'reboot': None}) + def test_host_evacuate(self): + self.run_command('host-evacuate hyper --target target_hyper') + self.assert_called('GET', '/os-hypervisors/hyper/servers', pos=0) + self.assert_called('POST', '/servers/uuid1/action', + {'evacuate': {'host': 'target_hyper', + 'onSharedStorage': False}}, pos=1) + self.assert_called('POST', '/servers/uuid2/action', + {'evacuate': {'host': 'target_hyper', + 'onSharedStorage': False}}, pos=2) + self.assert_called('POST', '/servers/uuid3/action', + {'evacuate': {'host': 'target_hyper', + 'onSharedStorage': False}}, pos=3) + self.assert_called('POST', '/servers/uuid4/action', + {'evacuate': {'host': 'target_hyper', + 'onSharedStorage': False}}, pos=4) + + def test_host_evacuate_with_shared_storage(self): + self.run_command( + 'host-evacuate --on-shared-storage hyper --target target_hyper') + self.assert_called('GET', '/os-hypervisors/hyper/servers', pos=0) + self.assert_called('POST', '/servers/uuid1/action', + {'evacuate': {'host': 'target_hyper', + 'onSharedStorage': True}}, pos=1) + self.assert_called('POST', '/servers/uuid2/action', + {'evacuate': {'host': 'target_hyper', + 'onSharedStorage': True}}, pos=2) + self.assert_called('POST', '/servers/uuid3/action', + {'evacuate': {'host': 'target_hyper', + 'onSharedStorage': True}}, pos=3) + self.assert_called('POST', '/servers/uuid4/action', + {'evacuate': {'host': 'target_hyper', + 'onSharedStorage': True}}, pos=4) + + def test_host_evacuate_with_no_target_host(self): + self.run_command('host-evacuate --on-shared-storage hyper') + self.assert_called('GET', '/os-hypervisors/hyper/servers', pos=0) + self.assert_called('POST', '/servers/uuid1/action', + {'evacuate': {'host': None, + 'onSharedStorage': True}}, pos=1) + self.assert_called('POST', '/servers/uuid2/action', + {'evacuate': {'host': None, + 'onSharedStorage': True}}, pos=2) + self.assert_called('POST', '/servers/uuid3/action', + {'evacuate': {'host': None, + 'onSharedStorage': True}}, pos=3) + self.assert_called('POST', '/servers/uuid4/action', + {'evacuate': {'host': None, + 'onSharedStorage': True}}, pos=4) + def test_coverage_start(self): self.run_command('coverage-start') self.assert_called('POST', '/os-coverage/action') diff --git a/novaclient/v1_1/contrib/host_evacuate.py b/novaclient/v1_1/contrib/host_evacuate.py new file mode 100644 index 000000000..c8acef8b5 --- /dev/null +++ b/novaclient/v1_1/contrib/host_evacuate.py @@ -0,0 +1,59 @@ +# Copyright 2013 Rackspace Hosting +# All Rights Reserved. +# +# 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 novaclient import base +from novaclient import utils + + +class EvacuateHostResponse(base.Resource): + pass + + +def _server_evacuate(cs, server, args): + success = True + error_message = "" + try: + cs.servers.evacuate(server['uuid'], args.target_host, + args.on_shared_storage) + except Exception as e: + success = False + error_message = "Error while evacuating instance: %s" % e + return EvacuateHostResponse(base.Manager, + {"server_uuid": server['uuid'], + "evacuate_accepted": success, + "error_message": error_message}) + + +@utils.arg('host', metavar='', help='Name of host.') +@utils.arg('--target_host', + metavar='', + default=None, + help='Name of target host.') +@utils.arg('--on-shared-storage', + dest='on_shared_storage', + action="store_true", + default=False, + help='Specifies whether all instances files are on shared storage') +def do_host_evacuate(cs, args): + """Evacuate all instances from failed host to specified one.""" + hypervisors = cs.hypervisors.search(args.host, servers=True) + response = [] + for hyper in hypervisors: + if hasattr(hyper, 'servers'): + for server in hyper.servers: + response.append(_server_evacuate(cs, server, args)) + + utils.print_list(response, + ["Server UUID", "Evacuate Accepted", "Error Message"]) From fa0d6e85a2117c61e1f4b1728d49a2c67d509cdf Mon Sep 17 00:00:00 2001 From: Matt Thompson Date: Tue, 4 Jun 2013 10:39:10 +0100 Subject: [PATCH 0181/1705] Exit w/ valid code when no servers are deleted. This change updates do_delete in v1_1/shell.py to keep track of deletion failures and raises an exception when all of the specified servers cannot be deleted. This in turn causes nova client to exit with a correct exit code when no successful deletes occur. Change-Id: I16ee7a4c754cf2e8add09a41becbcc37edc767ff Fixes: bug #1185009 --- novaclient/tests/v1_1/fakes.py | 3 +++ novaclient/tests/v1_1/test_shell.py | 26 ++++++++++++++++++++++++++ novaclient/v1_1/shell.py | 7 +++++++ 3 files changed, 36 insertions(+) diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index 924d39051..f60593675 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -383,6 +383,9 @@ def put_servers_1234(self, body, **kw): def delete_servers_1234(self, **kw): return (202, {}, None) + def delete_servers_5678(self, **kw): + return (202, {}, None) + def delete_servers_1234_metadata_test_key(self, **kw): return (204, {}, None) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index 5b17b2b9e..c44800c17 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -635,6 +635,32 @@ def test_delete(self): self.run_command('delete sample-server') self.assert_called('DELETE', '/servers/1234') + def test_delete_two_with_two_existent(self): + self.run_command('delete 1234 5678') + self.assert_called('DELETE', '/servers/1234', pos=-3) + self.assert_called('DELETE', '/servers/5678', pos=-1) + self.run_command('delete sample-server sample-server2') + self.assert_called('DELETE', '/servers/1234', pos=-3) + self.assert_called('DELETE', '/servers/5678', pos=-1) + + def test_delete_two_with_one_nonexistent(self): + self.run_command('delete 1234 123456789') + self.assert_called_anytime('DELETE', '/servers/1234') + self.run_command('delete sample-server nonexistentserver') + self.assert_called_anytime('DELETE', '/servers/1234') + + def test_delete_one_with_one_nonexistent(self): + cmd = 'delete 123456789' + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + cmd = 'delete nonexistent-server1' + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + def test_delete_two_with_two_nonexistent(self): + cmd = 'delete 123456789 987654321' + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + cmd = 'delete nonexistent-server1 nonexistent-server2' + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + def test_diagnostics(self): self.run_command('diagnostics 1234') self.assert_called('GET', '/servers/1234/diagnostics') diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 4c1627ce8..6dae3622e 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1337,12 +1337,19 @@ def do_show(cs, args): help='Name or ID of server(s).') def do_delete(cs, args): """Immediately shut down and delete specified server(s).""" + failure_count = 0 + for server in args.server: try: _find_server(cs, server).delete() except Exception as e: + failure_count += 1 print(e) + if failure_count == len(args.server): + raise exceptions.CommandError("Unable to delete any of the specified " + "servers.") + def _find_server(cs, server): """Get a server by name or ID.""" From d43b923d8a0509b39c5521153def078a422a491a Mon Sep 17 00:00:00 2001 From: Aarti Kriplani Date: Mon, 3 Jun 2013 17:33:17 +0530 Subject: [PATCH 0182/1705] Delete a quota through admin api. Exposes the quota-delete api implemented as part of blueprint admin-api-for-delete-quota Change-Id: Iab358f0fcf2dfb41bcd9a3a5b73d590d3f55cd6c --- novaclient/tests/v1_1/fakes.py | 6 ++++++ novaclient/tests/v1_1/test_quotas.py | 5 +++++ novaclient/tests/v1_1/test_shell.py | 6 ++++++ novaclient/v1_1/quotas.py | 3 +++ novaclient/v1_1/shell.py | 9 +++++++++ 5 files changed, 29 insertions(+) diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index 924d39051..601a06096 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -1014,6 +1014,12 @@ def put_os_quota_sets_97f4c221bff44578b0300df4ef119353(self, body, **kw): 'security_groups': 1, 'security_group_rules': 1}}) + def delete_os_quota_sets_test(self, **kw): + return (202, {}, {}) + + def delete_os_quota_sets_97f4c221bff44578b0300df4ef119353(self, **kw): + return (202, {}, {}) + # # Quota Classes # diff --git a/novaclient/tests/v1_1/test_quotas.py b/novaclient/tests/v1_1/test_quotas.py index 69c2952f1..a4d68f863 100644 --- a/novaclient/tests/v1_1/test_quotas.py +++ b/novaclient/tests/v1_1/test_quotas.py @@ -45,3 +45,8 @@ def test_refresh_quota(self): self.assertNotEqual(q.volumes, q2.volumes) q2.get() self.assertEqual(q.volumes, q2.volumes) + + def test_quotas_delete(self): + tenant_id = 'test' + cs.quotas.delete(tenant_id) + cs.assert_called('DELETE', '/os-quota-sets/%s' % tenant_id) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index 5b17b2b9e..96c1156a2 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -1034,6 +1034,12 @@ def test_quota_update_fixed_ip(self): {'quota_set': {'fixed_ips': 5, 'tenant_id': '97f4c221bff44578b0300df4ef119353'}}) + def test_quota_delete(self): + self.run_command('quota-delete --tenant ' + '97f4c221bff44578b0300df4ef119353') + self.assert_called('DELETE', + '/os-quota-sets/97f4c221bff44578b0300df4ef119353') + def test_quota_class_show(self): self.run_command('quota-class-show test') self.assert_called('GET', '/os-quota-class-sets/test') diff --git a/novaclient/v1_1/quotas.py b/novaclient/v1_1/quotas.py index 6aa7fe5ac..19f7d7190 100644 --- a/novaclient/v1_1/quotas.py +++ b/novaclient/v1_1/quotas.py @@ -69,3 +69,6 @@ def update(self, tenant_id, metadata_items=None, def defaults(self, tenant_id): return self._get('/os-quota-sets/%s/defaults' % tenant_id, 'quota_set') + + def delete(self, tenant_id): + self._delete("/os-quota-sets/%s" % tenant_id) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 4c1627ce8..21e658959 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2914,6 +2914,15 @@ def do_quota_update(cs, args): _quota_update(cs.quotas, args.tenant, args) +@utils.arg('--tenant', + metavar='', + help='ID of tenant to delete quota for.') +def do_quota_delete(cs, args): + """Delete quota for a tenant so their quota will revert back to default.""" + + cs.quotas.delete(args.tenant) + + @utils.arg('class_name', metavar='', help='Name of quota class to list the quotas for.') From e476179c65617f61df0e804c865f5e728d83ec59 Mon Sep 17 00:00:00 2001 From: Dirk Mueller Date: Fri, 7 Jun 2013 15:58:47 +0200 Subject: [PATCH 0183/1705] Use Python 3.x compatible except: construct Per (proposed) hacking H203 check, use the non-deprecated except x as y: construct, which works with any Python version >= 2.6 Change-Id: Ib7cab00cb8f219154663a4d51a855a1a2718e8cb --- novaclient/tests/test_shell.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/novaclient/tests/test_shell.py b/novaclient/tests/test_shell.py index 3530a6820..4da55d9d7 100644 --- a/novaclient/tests/test_shell.py +++ b/novaclient/tests/test_shell.py @@ -119,7 +119,7 @@ def test_no_username(self): self.make_env(exclude='OS_USERNAME') try: self.shell('list') - except exceptions.CommandError, message: + except exceptions.CommandError as message: self.assertEqual(required, message.args) else: self.fail('CommandError not raised') @@ -130,7 +130,7 @@ def test_no_tenant_name(self): self.make_env(exclude='OS_TENANT_NAME') try: self.shell('list') - except exceptions.CommandError, message: + except exceptions.CommandError as message: self.assertEqual(required, message.args) else: self.fail('CommandError not raised') @@ -143,7 +143,7 @@ def test_no_auth_url(self): self.make_env(exclude='OS_AUTH_URL') try: self.shell('list') - except exceptions.CommandError, message: + except exceptions.CommandError as message: self.assertEqual(required, message.args) else: self.fail('CommandError not raised') @@ -176,7 +176,7 @@ def test_no_password(self, mock_getpass, mock_stdin): self.make_env(exclude='OS_PASSWORD') try: self.shell('list') - except exceptions.CommandError, message: + except exceptions.CommandError as message: self.assertEqual(required, message.args) else: self.fail('CommandError not raised') From 87bd54ff135baeddc80b9fa232fe9be1fc17db55 Mon Sep 17 00:00:00 2001 From: Sergey Lukjanov Date: Sun, 2 Jun 2013 19:37:01 +0400 Subject: [PATCH 0184/1705] The 'nova keypair-show key_name' command added. * 'os-keypairs' api extension used. Change-Id: Idbb529135b6629f02306c49d8095b5fcf94770cc --- novaclient/tests/v1_1/fakes.py | 3 +++ novaclient/tests/v1_1/test_keypairs.py | 6 ++++++ novaclient/v1_1/keypairs.py | 9 +++++++++ novaclient/v1_1/shell.py | 16 ++++++++++++++++ 4 files changed, 34 insertions(+) diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index 924d39051..c545ef719 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -848,6 +848,9 @@ def delete_images_1_metadata_test_key(self, **kw): # # Keypairs # + def get_os_keypairs_test(self, *kw): + return (200, {}, {'keypair': self.get_os_keypairs()[2]['keypairs'][0]}) + def get_os_keypairs(self, *kw): return (200, {}, {"keypairs": [ {'fingerprint': 'FAKE_KEYPAIR', 'name': 'test'} diff --git a/novaclient/tests/v1_1/test_keypairs.py b/novaclient/tests/v1_1/test_keypairs.py index 2814c88ef..64fbc0ead 100644 --- a/novaclient/tests/v1_1/test_keypairs.py +++ b/novaclient/tests/v1_1/test_keypairs.py @@ -8,6 +8,12 @@ class KeypairsTest(utils.TestCase): + def test_get_keypair(self): + kp = cs.keypairs.get('test') + cs.assert_called('GET', '/os-keypairs/test') + self.assertTrue(isinstance(kp, keypairs.Keypair)) + self.assertEqual(kp.name, 'test') + def test_list_keypairs(self): kps = cs.keypairs.list() cs.assert_called('GET', '/os-keypairs') diff --git a/novaclient/v1_1/keypairs.py b/novaclient/v1_1/keypairs.py index e3423258e..28bd760c1 100644 --- a/novaclient/v1_1/keypairs.py +++ b/novaclient/v1_1/keypairs.py @@ -45,6 +45,15 @@ def delete(self): class KeypairManager(base.ManagerWithFind): resource_class = Keypair + def get(self, keypair): + """ + Get a keypair. + + :param keypair: The ID of the keypair to get. + :rtype: :class:`Keypair` + """ + return self._get("/os-keypairs/%s" % base.getid(keypair), "keypair") + def create(self, name, public_key=None): """ Create a keypair diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 4c1627ce8..d28739ace 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2137,6 +2137,22 @@ def do_keypair_list(cs, args): utils.print_list(keypairs, columns) +def _print_keypair(keypair): + kp = keypair._info.copy() + pk = kp.pop('public_key') + utils.print_dict(kp) + print "Public key: %s" % pk + + +@utils.arg('keypair', + metavar='', + help="Name or ID of keypair") +def do_keypair_show(cs, args): + """Show details about the given keypair.""" + keypair = cs.keypairs.get(args.keypair) + _print_keypair(keypair) + + @utils.arg('--reserved', dest='reserved', action='store_true', From 96706d502fd6f2b5c85dfea422740269f28b89e2 Mon Sep 17 00:00:00 2001 From: Aarti Kriplani Date: Tue, 4 Jun 2013 16:15:20 +0530 Subject: [PATCH 0185/1705] Set/Delete metadata on all instances of a host. Adding a wrapper for all instances of a host, to be able to set/delete metadata for all instances at once. Implements blueprint tag-instances-of-host Change-Id: I3d1a9ab54aad60bfccd0ece2285d145031fb5e15 --- novaclient/tests/v1_1/fakes.py | 28 ++++++++++++ novaclient/tests/v1_1/test_shell.py | 26 +++++++++++ .../v1_1/contrib/metadata_extensions.py | 43 +++++++++++++++++++ 3 files changed, 97 insertions(+) create mode 100644 novaclient/v1_1/contrib/metadata_extensions.py diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index 924d39051..85272f2bb 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -398,6 +398,30 @@ def post_servers_1234_metadata(self, **kw): def get_servers_1234_diagnostics(self, **kw): return (200, {}, {'data': 'Fake diagnostics'}) + def post_servers_uuid1_metadata(self, **kw): + return (204, {}, {'metadata': {'key1': 'val1'}}) + + def post_servers_uuid2_metadata(self, **kw): + return (204, {}, {'metadata': {'key1': 'val1'}}) + + def post_servers_uuid3_metadata(self, **kw): + return (204, {}, {'metadata': {'key1': 'val1'}}) + + def post_servers_uuid4_metadata(self, **kw): + return (204, {}, {'metadata': {'key1': 'val1'}}) + + def delete_servers_uuid1_metadata_key1(self, **kw): + return (200, {}, {'data': 'Fake diagnostics'}) + + def delete_servers_uuid2_metadata_key1(self, **kw): + return (200, {}, {'data': 'Fake diagnostics'}) + + def delete_servers_uuid3_metadata_key1(self, **kw): + return (200, {}, {'data': 'Fake diagnostics'}) + + def delete_servers_uuid4_metadata_key1(self, **kw): + return (200, {}, {'data': 'Fake diagnostics'}) + # # Server Addresses # @@ -1476,6 +1500,10 @@ def get_os_hypervisors_hyper_servers(self, **kw): ]} ]}) + def get_os_hypervisors_hyper_no_servers_servers(self, **kw): + return (200, {}, {'hypervisors': + [{'id': 1234, 'hypervisor_hostname': 'hyper1'}]}) + def get_os_hypervisors_1234(self, **kw): return (200, {}, {'hypervisor': {'id': 1234, diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index 5b17b2b9e..14099e19e 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -656,6 +656,32 @@ def test_set_meta_delete_keys(self): self.assert_called('DELETE', '/servers/1234/metadata/key1') self.assert_called('DELETE', '/servers/1234/metadata/key2', pos=-2) + def test_set_host_meta(self): + self.run_command('host-meta hyper set key1=val1 key2=val2') + self.assert_called('GET', '/os-hypervisors/hyper/servers', pos=0) + self.assert_called('POST', '/servers/uuid1/metadata', + {'metadata': {'key1': 'val1', 'key2': 'val2'}}, + pos=1) + self.assert_called('POST', '/servers/uuid2/metadata', + {'metadata': {'key1': 'val1', 'key2': 'val2'}}, + pos=2) + self.assert_called('POST', '/servers/uuid3/metadata', + {'metadata': {'key1': 'val1', 'key2': 'val2'}}, + pos=3) + self.assert_called('POST', '/servers/uuid4/metadata', + {'metadata': {'key1': 'val1', 'key2': 'val2'}}, + pos=4) + + def test_set_host_meta_with_no_servers(self): + self.run_command('host-meta hyper_no_servers set key1=val1 key2=val2') + self.assert_called('GET', '/os-hypervisors/hyper_no_servers/servers') + + def test_delete_host_meta(self): + self.run_command('host-meta hyper delete key1') + self.assert_called('GET', '/os-hypervisors/hyper/servers', pos=0) + self.assert_called('DELETE', '/servers/uuid1/metadata/key1', pos=1) + self.assert_called('DELETE', '/servers/uuid2/metadata/key1', pos=2) + def test_dns_create(self): self.run_command('dns-create 192.168.1.1 testname testdomain') self.assert_called('PUT', diff --git a/novaclient/v1_1/contrib/metadata_extensions.py b/novaclient/v1_1/contrib/metadata_extensions.py new file mode 100644 index 000000000..16e4ad13d --- /dev/null +++ b/novaclient/v1_1/contrib/metadata_extensions.py @@ -0,0 +1,43 @@ +# Copyright 2013 Rackspace Hosting +# All Rights Reserved. +# +# 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 novaclient import utils +from novaclient.v1_1 import shell + + +@utils.arg('host', + metavar='', + help='Name of host.') +@utils.arg('action', + metavar='', + choices=['set', 'delete'], + help="Actions: 'set' or 'delete'") +@utils.arg('metadata', + metavar='', + nargs='+', + action='append', + default=[], + help='Metadata to set or delete (only key is necessary on delete)') +def do_host_meta(cs, args): + """Set or Delete metadata on all instances of a host.""" + hypervisors = cs.hypervisors.search(args.host, servers=True) + for hyper in hypervisors: + metadata = shell._extract_metadata(args) + if hasattr(hyper, 'servers'): + for server in hyper.servers: + if args.action == 'set': + cs.servers.set_meta(server['uuid'], metadata) + elif args.action == 'delete': + cs.servers.delete_meta(server['uuid'], metadata.keys()) From 7f03b092a4b55536af37e6b8f1ffd3a3cde1ed02 Mon Sep 17 00:00:00 2001 From: Aarti Kriplani Date: Thu, 6 Jun 2013 15:49:58 +0530 Subject: [PATCH 0186/1705] Migrate each instances of a host to another. Added a new extension that adds the ability for admins to migrate all servers of a host to another available hosts. Implements blueprint host-servers-migrate Change-Id: I4e9c4be7ceb098d7a3bf553fd44addd46e8bce72 --- novaclient/tests/v1_1/test_shell.py | 12 +++++ .../v1_1/contrib/host_servers_migrate.py | 49 +++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 novaclient/v1_1/contrib/host_servers_migrate.py diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index 8defd288d..4b3aeedbc 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -1017,6 +1017,18 @@ def test_host_evacuate_with_no_target_host(self): {'evacuate': {'host': None, 'onSharedStorage': True}}, pos=4) + def test_host_servers_migrate(self): + self.run_command('host-servers-migrate hyper') + self.assert_called('GET', '/os-hypervisors/hyper/servers', pos=0) + self.assert_called('POST', + '/servers/uuid1/action', {'migrate': None}, pos=1) + self.assert_called('POST', + '/servers/uuid2/action', {'migrate': None}, pos=2) + self.assert_called('POST', + '/servers/uuid3/action', {'migrate': None}, pos=3) + self.assert_called('POST', + '/servers/uuid4/action', {'migrate': None}, pos=4) + def test_coverage_start(self): self.run_command('coverage-start') self.assert_called('POST', '/os-coverage/action') diff --git a/novaclient/v1_1/contrib/host_servers_migrate.py b/novaclient/v1_1/contrib/host_servers_migrate.py new file mode 100644 index 000000000..3076dc51d --- /dev/null +++ b/novaclient/v1_1/contrib/host_servers_migrate.py @@ -0,0 +1,49 @@ +# Copyright 2013 Rackspace Hosting +# All Rights Reserved. +# +# 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 novaclient import base +from novaclient import utils + + +class HostServersMigrateResponse(base.Resource): + pass + + +def _server_migrate(cs, server): + success = True + error_message = "" + try: + cs.servers.migrate(server['uuid']) + except Exception as e: + success = False + error_message = "Error while migrating instance: %s" % e + return HostServersMigrateResponse(base.Manager, + {"server_uuid": server['uuid'], + "migration_accepted": success, + "error_message": error_message}) + + +@utils.arg('host', metavar='', help='Name of host.') +def do_host_servers_migrate(cs, args): + """Migrate all instances of the specified host to other available hosts.""" + hypervisors = cs.hypervisors.search(args.host, servers=True) + response = [] + for hyper in hypervisors: + if hasattr(hyper, 'servers'): + for server in hyper.servers: + response.append(_server_migrate(cs, server)) + + utils.print_list(response, + ["Server UUID", "Migration Accepted", "Error Message"]) From cdee7e12c0fbee38285d54c5143bc7274dc9f383 Mon Sep 17 00:00:00 2001 From: Jianing YANG Date: Tue, 11 Jun 2013 17:07:30 +0800 Subject: [PATCH 0187/1705] Set default value of flavorid to "auto" Fixes: Bug #1189877 Change-Id: I80a0834c5f1d264b64f14d78fa2f71b1e4d9d89a --- novaclient/v1_1/flavors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/v1_1/flavors.py b/novaclient/v1_1/flavors.py index 1f480f4b1..0f244bc18 100644 --- a/novaclient/v1_1/flavors.py +++ b/novaclient/v1_1/flavors.py @@ -116,7 +116,7 @@ def delete(self, flavor): """ self._delete("/flavors/%s" % base.getid(flavor)) - def create(self, name, ram, vcpus, disk, flavorid=None, + def create(self, name, ram, vcpus, disk, flavorid="auto", ephemeral=0, swap=0, rxtx_factor=1.0, is_public=True): """ Create (allocate) a floating ip for a tenant From ee411a6a2ecbd8063e9088ac3e60075d6812c246 Mon Sep 17 00:00:00 2001 From: Kaushik Chandrashekar Date: Mon, 10 Jun 2013 14:15:06 +0530 Subject: [PATCH 0188/1705] Cells Support Adding support for Cell show and Cell capacities calls Implements: blueprint cell-capacity Change-Id: I83243cf224a4487d720d55d8942d28c52924b734 --- novaclient/tests/v1_1/contrib/test_cells.py | 42 +++++++++++++ novaclient/tests/v1_1/fakes.py | 29 +++++++++ novaclient/tests/v1_1/test_shell.py | 12 ++++ novaclient/utils.py | 4 +- novaclient/v1_1/contrib/cells.py | 69 +++++++++++++++++++++ 5 files changed, 154 insertions(+), 2 deletions(-) create mode 100644 novaclient/tests/v1_1/contrib/test_cells.py create mode 100644 novaclient/v1_1/contrib/cells.py diff --git a/novaclient/tests/v1_1/contrib/test_cells.py b/novaclient/tests/v1_1/contrib/test_cells.py new file mode 100644 index 000000000..187fab847 --- /dev/null +++ b/novaclient/tests/v1_1/contrib/test_cells.py @@ -0,0 +1,42 @@ +# Copyright 2013 Rackspace Hosting +# All Rights Reserved. +# +# 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 novaclient import extension +from novaclient.tests import utils +from novaclient.tests.v1_1.contrib import fakes +from novaclient.v1_1.contrib import cells + + +extensions = [ + extension.Extension(cells.__name__.split(".")[-1], + cells), +] +cs = fakes.FakeClient(extensions=extensions) + + +class CellsExtensionTests(utils.TestCase): + def test_get_cells(self): + cell_name = 'child_cell' + cs.cells.get(cell_name) + cs.assert_called('GET', '/os-cells/%s' % cell_name) + + def test_get_capacities_for_a_given_cell(self): + cell_name = 'child_cell' + cs.cells.capacities(cell_name) + cs.assert_called('GET', '/os-cells/%s/capacities' % cell_name) + + def test_get_capacities_for_all_cells(self): + cs.cells.capacities() + cs.assert_called('GET', '/os-cells/capacities') diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index 9cbcd518e..fb103ae24 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -1714,3 +1714,32 @@ def post_servers_uuid3_action(self, **kw): def post_servers_uuid4_action(self, **kw): return 202, {}, {} + + def get_os_cells_child_cell(self, **kw): + cell = {'cell': { + 'username': 'cell1_user', + 'name': 'cell1', + 'rpc_host': '10.0.1.10', + '_info': { + 'username': 'cell1_user', + 'rpc_host': '10.0.1.10', + 'type': 'child', + 'name': 'cell1', + 'rpc_port': 5673 + }, + 'type': 'child', + 'rpc_port': 5673, + '_loaded': True + }} + return (200, {}, cell) + + def get_os_cells_capacities(self, **kw): + cell_capacities_response = {"cell": {"capacities": {"ram_free": { + "units_by_mb": {"8192": 0, "512": 13, "4096": 1, "2048": 3, + "16384": 0}, "total_mb": 7680}, "disk_free": { + "units_by_mb": {"81920": 11, "20480": 46, "40960": 23, "163840": 5, + "0": 0}, "total_mb": 1052672}}}} + return (200, {}, cell_capacities_response) + + def get_os_cells_child_cell_capacities(self, **kw): + return self.get_os_cells_capacities() diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index 0ac3ecc3f..c374bdf87 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -1377,3 +1377,15 @@ def test_instance_action_get(self): self.run_command('instance-action sample-server req-abcde12345') self.assert_called('GET', '/servers/1234/os-instance-actions/req-abcde12345') + + def test_cell_show(self): + self.run_command('cell-show child_cell') + self.assert_called('GET', '/os-cells/child_cell') + + def test_cell_capacities_with_cell_name(self): + self.run_command('cell-capacities --cell child_cell') + self.assert_called('GET', '/os-cells/child_cell/capacities') + + def test_cell_capacities_without_cell_name(self): + self.run_command('cell-capacities') + self.assert_called('GET', '/os-cells/capacities') diff --git a/novaclient/utils.py b/novaclient/utils.py index f8f4b575d..d1e08e961 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -169,8 +169,8 @@ def print_list(objs, fields, formatters={}, sortby_index=None): print(strutils.safe_encode(pt.get_string())) -def print_dict(d, dict_property="Property", wrap=0): - pt = prettytable.PrettyTable([dict_property, 'Value'], caching=False) +def print_dict(d, dict_property="Property", dict_value="Value", wrap=0): + pt = prettytable.PrettyTable([dict_property, dict_value], caching=False) pt.align = 'l' for k, v in d.iteritems(): # convert dict to str to check length diff --git a/novaclient/v1_1/contrib/cells.py b/novaclient/v1_1/contrib/cells.py new file mode 100644 index 000000000..a6cd78a4e --- /dev/null +++ b/novaclient/v1_1/contrib/cells.py @@ -0,0 +1,69 @@ +# Copyright 2013 Rackspace Hosting +# All Rights Reserved. +# +# 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 novaclient import base +from novaclient import utils + + +class Cell(base.Resource): + def __repr__(self): + return "" % self.name + + +class CellsManager(base.Manager): + resource_class = Cell + + def get(self, cell_name): + """ + Get a cell. + + :param cell: Name of the :class:`Cell` to get. + :rtype: :class:`Cell` + """ + return self._get("/os-cells/%s" % cell_name, "cell") + + def capacities(self, cell_name=None): + """ + Get capacities for a cell. + + :param cell: Name of the :class:`Cell` to get capacities for. + :rtype: :class:`Cell` + """ + path = ["%s/capacities" % cell_name, "capacities"][cell_name is None] + return self._get("/os-cells/%s" % path, "cell") + + +@utils.arg('cell', + metavar='', + help='Name of the cell.') +def do_cell_show(cs, args): + """Show details of a given cell.""" + cell = cs.cells.get(args.cell) + utils.print_dict(cell._info) + + +@utils.arg('--cell', + metavar='', + help="Name of the cell to get the capacities.", + default=None) +def do_cell_capacities(cs, args): + """Get cell capacities for all cells or a given cell.""" + cell = cs.cells.capacities(args.cell) + print("Ram Available: %s MB" % cell.capacities['ram_free']['total_mb']) + utils.print_dict(cell.capacities['ram_free']['units_by_mb'], + dict_property='Ram(MB)', dict_value="Units") + print("\nDisk Available: %s MB" % cell.capacities['disk_free']['total_mb']) + utils.print_dict(cell.capacities['disk_free']['units_by_mb'], + dict_property='Disk(MB)', dict_value="Units") From 34573942bf1452823b29dd4e89d58a23de186f17 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 11 Jun 2013 11:40:34 -0700 Subject: [PATCH 0189/1705] Remove explicit distribute depend. Causes issues with the recent re-merge with setuptools. Advice from upstream is to stop doing explicit depends. Change-Id: I729ed2f646aa514fbb7b7dfc4a070df2f7b27ff4 --- test-requirements.txt | 2 -- 1 file changed, 2 deletions(-) diff --git a/test-requirements.txt b/test-requirements.txt index 8da41c8fd..91ffda1ba 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,5 +1,3 @@ -distribute>=0.6.24 - # Install bounded pep8/pyflakes first, then let flake8 install pep8==1.4.5 pyflakes==0.7.2 From be299d8424074f411118b74044a5e040bda4956d Mon Sep 17 00:00:00 2001 From: Phil Day Date: Fri, 17 May 2013 12:39:46 +0100 Subject: [PATCH 0190/1705] Adds support for ExtendedFloatingIps APi extension Allow a floating IP to be associated to a specific fixed IP This is the client side of: https://review.openstack.org/#/c/26113/15 Change-Id: I05f8a0dc60268535231b95a6664719015f67a318 --- novaclient/tests/v1_1/fakes.py | 4 +++- novaclient/tests/v1_1/test_servers.py | 13 +++++++++++++ novaclient/v1_1/servers.py | 18 ++++++++++++++---- novaclient/v1_1/shell.py | 6 +++++- 4 files changed, 35 insertions(+), 6 deletions(-) diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index 552a0847a..528c26d22 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -517,7 +517,9 @@ def post_servers_1234_action(self, body, **kw): elif action == 'removeFixedIp': assert body[action].keys() == ['address'] elif action == 'addFloatingIp': - assert body[action].keys() == ['address'] + assert (body[action].keys() == ['address'] or + body[action].keys() == ['fixed_address', + 'address']) elif action == 'removeFloatingIp': assert body[action].keys() == ['address'] elif action == 'createImage': diff --git a/novaclient/tests/v1_1/test_servers.py b/novaclient/tests/v1_1/test_servers.py index f9b48a278..5874a006c 100644 --- a/novaclient/tests/v1_1/test_servers.py +++ b/novaclient/tests/v1_1/test_servers.py @@ -251,6 +251,19 @@ def test_add_floating_ip(self): s.add_floating_ip(f) cs.assert_called('POST', '/servers/1234/action') + def test_add_floating_ip_to_fixed(self): + s = cs.servers.get(1234) + s.add_floating_ip('11.0.0.1', fixed_address='12.0.0.1') + cs.assert_called('POST', '/servers/1234/action') + cs.servers.add_floating_ip(s, '11.0.0.1', + fixed_address='12.0.0.1') + cs.assert_called('POST', '/servers/1234/action') + f = cs.floating_ips.list()[0] + cs.servers.add_floating_ip(s, f) + cs.assert_called('POST', '/servers/1234/action') + s.add_floating_ip(f) + cs.assert_called('POST', '/servers/1234/action') + def test_remove_floating_ip(self): s = cs.servers.get(1234) s.remove_floating_ip('11.0.0.1') diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index 440c63192..260d81806 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -97,13 +97,15 @@ def add_fixed_ip(self, network_id): """ self.manager.add_fixed_ip(self, network_id) - def add_floating_ip(self, address): + def add_floating_ip(self, address, fixed_address=None): """ Add floating IP to an instance :param address: The ip address or FloatingIP to add to the instance + :param fixed_address: The fixedIP address the FloatingIP is to be + associated with (optional) """ - self.manager.add_floating_ip(self, address) + self.manager.add_floating_ip(self, address, fixed_address) def remove_floating_ip(self, address): """ @@ -392,16 +394,24 @@ def remove_fixed_ip(self, server, address): """ self._action('removeFixedIp', server, {'address': address}) - def add_floating_ip(self, server, address): + def add_floating_ip(self, server, address, fixed_address=None): """ Add a floating ip to an instance :param server: The :class:`Server` (or its ID) to add an IP to. :param address: The FloatingIP or string floating address to add. + :param fixed_address: The FixedIP the floatingIP should be + associated with (optional) """ address = address.ip if hasattr(address, 'ip') else address - self._action('addFloatingIp', server, {'address': address}) + if fixed_address: + if hasattr(fixed_address, 'ip'): + fixed_address = fixed_address.ip + self._action('addFloatingIp', server, + {'address': address, 'fixed_address': fixed_address}) + else: + self._action('addFloatingIp', server, {'address': address}) def remove_floating_ip(self, server, address): """ diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 0bdabe4f1..f1d5d6779 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1707,10 +1707,14 @@ def do_console_log(cs, args): @utils.arg('server', metavar='', help='Name or ID of server.') @utils.arg('address', metavar='
', help='IP Address.') +@utils.arg('--fixed-address', + metavar='', + default=None, + help='Fixed IP Address to associate with.') def do_add_floating_ip(cs, args): """Add a floating IP address to a server.""" server = _find_server(cs, args.server) - server.add_floating_ip(args.address) + server.add_floating_ip(args.address, args.fixed_address) @utils.arg('server', metavar='', help='Name or ID of server.') From 85a4f6cb51001ffec1ea1cea79482c82278d346b Mon Sep 17 00:00:00 2001 From: Matt Thompson Date: Fri, 14 Jun 2013 16:04:13 +0100 Subject: [PATCH 0191/1705] Update help for --nic opt and make net-id or port-id required Commit updates help string for --nic option to reflect that specifying net-id and port-id keys are optional but that at least one is required. Additionally, we change _boot in novaclient/v1_1/shell.py to raise an exception if random keys are added to --nic string and if --nic string does not contain net-id or port-id keys. Change-Id: Icf94c395bd09160aa6a1b849eb464d72e410e1ae Fixes: bug #1052356 Fixes: bug #1191139 --- novaclient/tests/v1_1/test_shell.py | 15 ++++++++++++++ novaclient/v1_1/shell.py | 32 ++++++++++++++++++++--------- 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index e9adfda1e..82e3e60b9 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -338,6 +338,21 @@ def test_boot_nics(self): }, ) + def tets_boot_nics_no_value(self): + cmd = ('boot --image 1 --flavor 1 ' + '--nic net-id some-server') + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + def test_boot_nics_random_key(self): + cmd = ('boot --image 1 --flavor 1 ' + '--nic net-id=a=c,v4-fixed-ip=10.0.0.1,foo=bar some-server') + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + def test_boot_nics_no_netid_or_portid(self): + cmd = ('boot --image 1 --flavor 1 ' + '--nic v4-fixed-ip=10.0.0.1 some-server') + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + def test_boot_files(self): testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt') expected_file_data = open(testfile).read().encode('base64') diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 0bdabe4f1..8dff33c08 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -140,16 +140,26 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): nics = [] for nic_str in args.nics: + err_msg = ("Invalid nic argument '%s'. Nic arguments must be of the " + "form --nic , with at minimum net-id or port-id " + "specified." % nic_str) nic_info = {"net-id": "", "v4-fixed-ip": "", "port-id": ""} + for kv_str in nic_str.split(","): try: k, v = kv_str.split("=", 1) - nic_info[k] = v except ValueError as e: - raise exceptions.CommandError( - "Invalid nic argument '%s'. Nic arguments must be of the " - "form --nic " % nic_str) + raise exceptions.CommandError(err_msg) + + if k in nic_info: + nic_info[k] = v + else: + raise exceptions.CommandError(err_msg) + + if not nic_info['net-id'] and not nic_info['port-id']: + raise exceptions.CommandError(err_msg) + nics.append(nic_info) hints = {} @@ -268,11 +278,13 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): action='append', dest='nics', default=[], - help="Create a NIC on the server.\n" - "Specify option multiple times to create multiple NICs.\n" - "net-id: attach NIC to network with this UUID (optional)\n" - "v4-fixed-ip: IPv4 fixed address for NIC (optional).\n" - "port-id: attach NIC to port with this UUID (optional)") + help="Create a NIC on the server. " + "Specify option multiple times to create multiple NICs. " + "net-id: attach NIC to network with this UUID " + "(required if no port-id), " + "v4-fixed-ip: IPv4 fixed address for NIC (optional), " + "port-id: attach NIC to port with this UUID " + "(required if no net-id)") @utils.arg('--config-drive', metavar="", dest='config_drive', From 267dbd292c5681daaa5659f8f2ce644a80a54a08 Mon Sep 17 00:00:00 2001 From: gengjh Date: Sat, 15 Jun 2013 10:07:49 +0800 Subject: [PATCH 0192/1705] Support force update quota Once we have additional check when update quota in https://review.openstack.org/#/c/25887/, we need provide --force option when run 'nova quota-update'. Since the change in nova server has been merged, we need re-enable the changes in nova client side. Fix bug 1160749 Change-Id: Iceb67c5816312fafed8a68e48a8a136c03d0bb5b --- novaclient/tests/v1_1/test_quotas.py | 9 +++++++++ novaclient/tests/v1_1/test_shell.py | 17 +++++++++++++++-- novaclient/v1_1/quotas.py | 5 +++-- novaclient/v1_1/shell.py | 15 ++++++++++++++- 4 files changed, 41 insertions(+), 5 deletions(-) diff --git a/novaclient/tests/v1_1/test_quotas.py b/novaclient/tests/v1_1/test_quotas.py index a4d68f863..ce1c0388c 100644 --- a/novaclient/tests/v1_1/test_quotas.py +++ b/novaclient/tests/v1_1/test_quotas.py @@ -37,6 +37,15 @@ def test_update_quota(self): cs.assert_called('PUT', '/os-quota-sets/97f4c221bff44578b0300df4ef119353') + def test_force_update_quota(self): + q = cs.quotas.get('97f4c221bff44578b0300df4ef119353') + q.update(cores=2, force=True) + cs.assert_called( + 'PUT', '/os-quota-sets/97f4c221bff44578b0300df4ef119353', + {'quota_set': {'force': True, + 'cores': 2, + 'tenant_id': '97f4c221bff44578b0300df4ef119353'}}) + def test_refresh_quota(self): q = cs.quotas.get('test') q2 = cs.quotas.get('test') diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index e9adfda1e..01fde3aa4 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -1123,8 +1123,21 @@ def test_quota_update(self): self.run_command( 'quota-update 97f4c221bff44578b0300df4ef119353' ' --instances=5') - self.assert_called('PUT', - '/os-quota-sets/97f4c221bff44578b0300df4ef119353') + self.assert_called( + 'PUT', + '/os-quota-sets/97f4c221bff44578b0300df4ef119353', + {'quota_set': {'instances': 5, + 'tenant_id': '97f4c221bff44578b0300df4ef119353'}}) + + def test_quota_force_update(self): + self.run_command( + 'quota-update 97f4c221bff44578b0300df4ef119353' + ' --instances=5 --force') + self.assert_called( + 'PUT', '/os-quota-sets/97f4c221bff44578b0300df4ef119353', + {'quota_set': {'force': True, + 'instances': 5, + 'tenant_id': '97f4c221bff44578b0300df4ef119353'}}) def test_quota_update_fixed_ip(self): self.run_command( diff --git a/novaclient/v1_1/quotas.py b/novaclient/v1_1/quotas.py index 19f7d7190..e174c6f18 100644 --- a/novaclient/v1_1/quotas.py +++ b/novaclient/v1_1/quotas.py @@ -41,7 +41,7 @@ def update(self, tenant_id, metadata_items=None, volumes=None, gigabytes=None, ram=None, floating_ips=None, fixed_ips=None, instances=None, injected_files=None, cores=None, key_pairs=None, - security_groups=None, security_group_rules=None): + security_groups=None, security_group_rules=None, force=None): body = {'quota_set': { 'tenant_id': tenant_id, @@ -58,7 +58,8 @@ def update(self, tenant_id, metadata_items=None, 'injected_files': injected_files, 'cores': cores, 'security_groups': security_groups, - 'security_group_rules': security_group_rules}} + 'security_group_rules': security_group_rules, + 'force': force}} for key in body['quota_set'].keys(): if body['quota_set'][key] is None: diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 0bdabe4f1..fa854e3a4 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -31,6 +31,7 @@ from novaclient.openstack.common import uuidutils from novaclient import utils from novaclient.v1_1 import availability_zones +from novaclient.v1_1 import quotas from novaclient.v1_1 import servers @@ -2835,7 +2836,13 @@ def _quota_update(manager, identifier, args): updates[resource] = val if updates: - manager.update(identifier, **updates) + # default value of force is None to make sure this client + # will be compatibile with old nova server + force_update = getattr(args, 'force', None) + if isinstance(manager, quotas.QuotaSetManager): + manager.update(identifier, force=force_update, **updates) + else: + manager.update(identifier, **updates) @utils.arg('--tenant', @@ -2944,6 +2951,12 @@ def do_quota_defaults(cs, args): type=int, default=None, help='New value for the "security-group-rules" quota.') +@utils.arg('--force', + dest='force', + action="store_true", + default=None, + help='Whether force update the quota even if the already used' + ' and reserved exceeds the new quota') def do_quota_update(cs, args): """Update the quotas for a tenant.""" From 909a53b161b8936dddb40dd25e346c2cbb8db416 Mon Sep 17 00:00:00 2001 From: Eoghan Glynn Date: Wed, 19 Jun 2013 18:27:24 +0000 Subject: [PATCH 0193/1705] Discard possibly expired token before re-authenticating Fixes bug 1192656 Previously, the attempt to re-authenticate on possible token expiry actually re-used the expired token, which was clearly bound to fail in the expired case. Now the old authentication state is discarded before attempting re-authentication. Change-Id: I3fd125702061f7ac84eb501d2a488aab5b2385b9 --- novaclient/client.py | 3 +++ novaclient/tests/test_client.py | 38 +++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/novaclient/client.py b/novaclient/client.py index bb963ce70..1c93a33d0 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -218,6 +218,9 @@ def _cs_request(self, url, method, **kwargs): return resp, body except exceptions.Unauthorized as e: try: + # frist discard auth token, to avoid the possibly expired + # token being re-used in the re-authentication attempt + self.unauthenticate() self.authenticate() kwargs['headers']['X-Auth-Token'] = self.auth_token resp, body = self._time_request(self.management_url + url, diff --git a/novaclient/tests/test_client.py b/novaclient/tests/test_client.py index 3e74cdba4..9f5d90f39 100644 --- a/novaclient/tests/test_client.py +++ b/novaclient/tests/test_client.py @@ -45,6 +45,44 @@ def test_client_with_timeout(self): headers=mock.ANY, verify=mock.ANY) + def test_client_reauth(self): + instance = novaclient.client.HTTPClient(user='user', + password='password', + projectid='project', + timeout=2, + auth_url="http://www.blah.com") + instance.auth_token = 'foobar' + instance.management_url = 'http://example.com' + instance.version = 'v2.0' + mock_request = mock.Mock() + mock_request.side_effect = novaclient.exceptions.Unauthorized(401) + with mock.patch('requests.Session.request', mock_request): + try: + instance.get('/servers/detail') + except Exception: + pass + get_headers = {'X-Auth-Project-Id': 'project', + 'X-Auth-Token': 'foobar', + 'User-Agent': 'python-novaclient', + 'Accept': 'application/json'} + reauth_headers = {'Content-Type': 'application/json', + 'Accept': 'application/json', + 'User-Agent': 'python-novaclient'} + data = ('{"auth": {"tenantName": "project", "passwordCredentials":' + ' {"username": "user", "password": "password"}}}') + expected = [mock.call('GET', + 'http://example.com/servers/detail', + timeout=mock.ANY, + headers=get_headers, + verify=mock.ANY), + mock.call('POST', 'http://www.blah.com/tokens', + timeout=mock.ANY, + headers=reauth_headers, + allow_redirects=mock.ANY, + data=data, + verify=mock.ANY)] + self.assertEqual(mock_request.call_args_list, expected) + def test_get_client_class_v2(self): output = novaclient.client.get_client_class('2') self.assertEqual(output, novaclient.v1_1.client.Client) From ea4712369b55b28b804526326e42ff751f8b605b Mon Sep 17 00:00:00 2001 From: Mahesh Panchaksharaiah Date: Thu, 25 Apr 2013 13:39:41 +0530 Subject: [PATCH 0194/1705] Return Customer's Quota Usage through Admin API Modified the used limits API for Admin to retrieve the used limits for a customer. Changes done to the nova client to fetch limits for a given tenant. This is related to changes made in nova, https://review.openstack.org/#/c/27468/ Change-Id: Id53576eb35d6dab7cb655f8427091e95a6f75a6d Implements: blueprint customer-quota-through-admin-api --- novaclient/tests/v1_1/test_limits.py | 6 ++++++ novaclient/tests/v1_1/test_shell.py | 3 +++ novaclient/v1_1/limits.py | 11 +++++++++-- novaclient/v1_1/shell.py | 8 +++++++- 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/novaclient/tests/v1_1/test_limits.py b/novaclient/tests/v1_1/test_limits.py index 550c9c321..517a6f1fb 100644 --- a/novaclient/tests/v1_1/test_limits.py +++ b/novaclient/tests/v1_1/test_limits.py @@ -14,6 +14,11 @@ def test_get_limits(self): cs.assert_called('GET', '/limits') self.assertTrue(isinstance(obj, limits.Limits)) + def test_get_limits_for_a_tenant(self): + obj = cs.limits.get(tenant_id=1234) + cs.assert_called('GET', '/limits?tenant_id=1234') + self.assertTrue(isinstance(obj, limits.Limits)) + def test_absolute_limits(self): obj = cs.limits.get() @@ -42,6 +47,7 @@ def test_absolute_limits_reserved(self): limits.AbsoluteLimit("maxPersonalitySize", 10240), ) + cs.assert_called('GET', '/limits?reserved=1') abs_limits = list(obj.absolute) self.assertEqual(len(abs_limits), len(expected)) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index 32218ee54..119e72702 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -1298,6 +1298,9 @@ def test_absolute_limits(self): self.run_command('absolute-limits --reserved') self.assert_called('GET', '/limits?reserved=1') + self.run_command('absolute-limits --tenant 1234') + self.assert_called('GET', '/limits?tenant_id=1234') + def test_evacuate(self): self.run_command('evacuate sample-server new_host') self.assert_called('POST', '/servers/1234/action', diff --git a/novaclient/v1_1/limits.py b/novaclient/v1_1/limits.py index 97b786f80..3eba65234 100644 --- a/novaclient/v1_1/limits.py +++ b/novaclient/v1_1/limits.py @@ -1,5 +1,7 @@ # Copyright 2011 OpenStack Foundation +import urllib + from novaclient import base @@ -70,12 +72,17 @@ class LimitsManager(base.Manager): resource_class = Limits - def get(self, reserved=False): + def get(self, reserved=False, tenant_id=None): """ Get a specific extension. :rtype: :class:`Limits` """ - query_string = "?reserved=1" if reserved else "" + opts = {} + if reserved: + opts['reserved'] = 1 + if tenant_id: + opts['tenant_id'] = tenant_id + query_string = "?%s" % urllib.urlencode(opts) if opts else "" return self._get("/limits%s" % query_string, "limits") diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 13f25abc4..68def40a8 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2178,6 +2178,12 @@ def do_keypair_show(cs, args): _print_keypair(keypair) +@utils.arg('--tenant', + #nova db searches by project_id + dest='tenant', + metavar='', + nargs='?', + help='Display information from single tenant (Admin only).') @utils.arg('--reserved', dest='reserved', action='store_true', @@ -2185,7 +2191,7 @@ def do_keypair_show(cs, args): help='Include reservations count.') def do_absolute_limits(cs, args): """Print a list of absolute limits for a user""" - limits = cs.limits.get(args.reserved).absolute + limits = cs.limits.get(args.reserved, args.tenant).absolute columns = ['Name', 'Value'] utils.print_list(limits, columns) From 8c4e145b926e9d29a14dbc0e3886239b312475f4 Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Mon, 24 Jun 2013 10:03:19 -0500 Subject: [PATCH 0195/1705] python3: Fix unicode compatibility python2/python3 Python3 enforces the distinction between byte strings and text strings more rigorously than python2. So use six.text_type/six.u() where appropriate Change-Id: I890e19cb857e10f0292aabdaebaa8e7a7bd8db23 Signed-off-by: Chuck Short --- doc/source/conf.py | 12 +- novaclient/shell.py | 7 +- novaclient/tests/v1_1/fakes.py | 156 +++++++++--------- .../tests/v1_1/test_availability_zone.py | 27 +-- novaclient/tests/v1_1/test_servers.py | 3 +- novaclient/utils.py | 5 +- requirements.txt | 1 + 7 files changed, 115 insertions(+), 96 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index 8aab078bd..4583d15e5 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -39,8 +39,8 @@ master_doc = 'index' # General information about the project. -project = u'python-novaclient' -copyright = u'OpenStack Contributors' +project = 'python-novaclient' +copyright = 'OpenStack Contributors' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -93,8 +93,8 @@ # List of tuples 'sourcefile', 'target', u'title', u'Authors name', 'manual' man_pages = [ - ('man/nova', 'nova', u'OpenStack Nova command line client', - [u'OpenStack Contributors'], 1), + ('man/nova', 'nova', 'OpenStack Nova command line client', + ['OpenStack Contributors'], 1), ] # -- Options for HTML output -------------------------------------------------- @@ -183,8 +183,8 @@ # (source start file, target name, title, author, documentclass [howto/manual]) # . latex_documents = [ - ('index', 'python-novaclient.tex', u'python-novaclient Documentation', - u'Rackspace - based on work by Jacob Kaplan-Moss', 'manual'), + ('index', 'python-novaclient.tex', 'python-novaclient Documentation', + 'Rackspace - based on work by Jacob Kaplan-Moss', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of diff --git a/novaclient/shell.py b/novaclient/shell.py index 289df3750..b10d26acf 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -26,10 +26,12 @@ import itertools import logging import os -import pkg_resources import pkgutil import sys +import pkg_resources +import six + HAS_KEYRING = False all_errors = ValueError try: @@ -766,7 +768,8 @@ def main(): except Exception as e: logger.debug(e, exc_info=1) - print("ERROR: %s" % strutils.safe_encode(unicode(e)), file=sys.stderr) + print("ERROR: %s" % strutils.safe_encode(six.text_type(e)), + file=sys.stderr) sys.exit(1) diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index cab34e6c3..625f4e512 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -17,6 +17,8 @@ from datetime import datetime import urlparse +import six + from novaclient import client as base_client from novaclient.v1_1 import client from novaclient.tests import fakes @@ -1208,87 +1210,93 @@ def post_os_security_group_rules(self, body, **kw): # def get_os_simple_tenant_usage(self, **kw): return (200, {}, - {u'tenant_usages': [{ - u'total_memory_mb_usage': 25451.762807466665, - u'total_vcpus_usage': 49.71047423333333, - u'total_hours': 49.71047423333333, - u'tenant_id': u'7b0a1d73f8fb41718f3343c207597869', - u'stop': u'2012-01-22 19:48:41.750722', - u'server_usages': [{ - u'hours': 49.71047423333333, - u'uptime': 27035, - u'local_gb': 0, - u'ended_at': None, - u'name': u'f15image1', - u'tenant_id': u'7b0a1d73f8fb41718f3343c207597869', - u'vcpus': 1, - u'memory_mb': 512, - u'state': u'active', - u'flavor': u'm1.tiny', - u'started_at': u'2012-01-20 18:06:06.479998'}], - u'start': u'2011-12-25 19:48:41.750687', - u'total_local_gb_usage': 0.0}]}) + {six.u('tenant_usages'): [{ + six.u('total_memory_mb_usage'): 25451.762807466665, + six.u('total_vcpus_usage'): 49.71047423333333, + six.u('total_hours'): 49.71047423333333, + six.u('tenant_id'): + six.u('7b0a1d73f8fb41718f3343c207597869'), + six.u('stop'): six.u('2012-01-22 19:48:41.750722'), + six.u('server_usages'): [{ + six.u('hours'): 49.71047423333333, + six.u('uptime'): 27035, + six.u('local_gb'): 0, + six.u('ended_at'): None, + six.u('name'): six.u('f15image1'), + six.u('tenant_id'): + six.u('7b0a1d73f8fb41718f3343c207597869'), + six.u('vcpus'): 1, + six.u('memory_mb'): 512, + six.u('state'): six.u('active'), + six.u('flavor'): six.u('m1.tiny'), + six.u('started_at'): + six.u('2012-01-20 18:06:06.479998')}], + six.u('start'): six.u('2011-12-25 19:48:41.750687'), + six.u('total_local_gb_usage'): 0.0}]}) def get_os_simple_tenant_usage_tenantfoo(self, **kw): return (200, {}, - {u'tenant_usage': { - u'total_memory_mb_usage': 25451.762807466665, - u'total_vcpus_usage': 49.71047423333333, - u'total_hours': 49.71047423333333, - u'tenant_id': u'7b0a1d73f8fb41718f3343c207597869', - u'stop': u'2012-01-22 19:48:41.750722', - u'server_usages': [{ - u'hours': 49.71047423333333, - u'uptime': 27035, u'local_gb': 0, - u'ended_at': None, - u'name': u'f15image1', - u'tenant_id': u'7b0a1d73f8fb41718f3343c207597869', - u'vcpus': 1, u'memory_mb': 512, - u'state': u'active', - u'flavor': u'm1.tiny', - u'started_at': u'2012-01-20 18:06:06.479998'}], - u'start': u'2011-12-25 19:48:41.750687', - u'total_local_gb_usage': 0.0}}) + {six.u('tenant_usage'): { + six.u('total_memory_mb_usage'): 25451.762807466665, + six.u('total_vcpus_usage'): 49.71047423333333, + six.u('total_hours'): 49.71047423333333, + six.u('tenant_id'): + six.u('7b0a1d73f8fb41718f3343c207597869'), + six.u('stop'): six.u('2012-01-22 19:48:41.750722'), + six.u('server_usages'): [{ + six.u('hours'): 49.71047423333333, + six.u('uptime'): 27035, six.u('local_gb'): 0, + six.u('ended_at'): None, + six.u('name'): six.u('f15image1'), + six.u('tenant_id'): + six.u('7b0a1d73f8fb41718f3343c207597869'), + six.u('vcpus'): 1, six.u('memory_mb'): 512, + six.u('state'): six.u('active'), + six.u('flavor'): six.u('m1.tiny'), + six.u('started_at'): + six.u('2012-01-20 18:06:06.479998')}], + six.u('start'): six.u('2011-12-25 19:48:41.750687'), + six.u('total_local_gb_usage'): 0.0}}) def get_os_simple_tenant_usage_test(self, **kw): - return (200, {}, {u'tenant_usage': { - u'total_memory_mb_usage': 25451.762807466665, - u'total_vcpus_usage': 49.71047423333333, - u'total_hours': 49.71047423333333, - u'tenant_id': u'7b0a1d73f8fb41718f3343c207597869', - u'stop': u'2012-01-22 19:48:41.750722', - u'server_usages': [{ - u'hours': 49.71047423333333, - u'uptime': 27035, u'local_gb': 0, - u'ended_at': None, - u'name': u'f15image1', - u'tenant_id': u'7b0a1d73f8fb41718f3343c207597869', - u'vcpus': 1, u'memory_mb': 512, - u'state': u'active', - u'flavor': u'm1.tiny', - u'started_at': u'2012-01-20 18:06:06.479998'}], - u'start': u'2011-12-25 19:48:41.750687', - u'total_local_gb_usage': 0.0}}) + return (200, {}, {six.u('tenant_usage'): { + six.u('total_memory_mb_usage'): 25451.762807466665, + six.u('total_vcpus_usage'): 49.71047423333333, + six.u('total_hours'): 49.71047423333333, + six.u('tenant_id'): six.u('7b0a1d73f8fb41718f3343c207597869'), + six.u('stop'): six.u('2012-01-22 19:48:41.750722'), + six.u('server_usages'): [{ + six.u('hours'): 49.71047423333333, + six.u('uptime'): 27035, six.u('local_gb'): 0, + six.u('ended_at'): None, + six.u('name'): six.u('f15image1'), + six.u('tenant_id'): six.u('7b0a1d73f8fb41718f3343c207597869'), + six.u('vcpus'): 1, six.u('memory_mb'): 512, + six.u('state'): six.u('active'), + six.u('flavor'): six.u('m1.tiny'), + six.u('started_at'): six.u('2012-01-20 18:06:06.479998')}], + six.u('start'): six.u('2011-12-25 19:48:41.750687'), + six.u('total_local_gb_usage'): 0.0}}) def get_os_simple_tenant_usage_tenant_id(self, **kw): - return (200, {}, {u'tenant_usage': { - u'total_memory_mb_usage': 25451.762807466665, - u'total_vcpus_usage': 49.71047423333333, - u'total_hours': 49.71047423333333, - u'tenant_id': u'7b0a1d73f8fb41718f3343c207597869', - u'stop': u'2012-01-22 19:48:41.750722', - u'server_usages': [{ - u'hours': 49.71047423333333, - u'uptime': 27035, u'local_gb': 0, - u'ended_at': None, - u'name': u'f15image1', - u'tenant_id': u'7b0a1d73f8fb41718f3343c207597869', - u'vcpus': 1, u'memory_mb': 512, - u'state': u'active', - u'flavor': u'm1.tiny', - u'started_at': u'2012-01-20 18:06:06.479998'}], - u'start': u'2011-12-25 19:48:41.750687', - u'total_local_gb_usage': 0.0}}) + return (200, {}, {six.u('tenant_usage'): { + six.u('total_memory_mb_usage'): 25451.762807466665, + six.u('total_vcpus_usage'): 49.71047423333333, + six.u('total_hours'): 49.71047423333333, + six.u('tenant_id'): six.u('7b0a1d73f8fb41718f3343c207597869'), + six.u('stop'): six.u('2012-01-22 19:48:41.750722'), + six.u('server_usages'): [{ + six.u('hours'): 49.71047423333333, + six.u('uptime'): 27035, six.u('local_gb'): 0, + six.u('ended_at'): None, + six.u('name'): six.u('f15image1'), + six.u('tenant_id'): six.u('7b0a1d73f8fb41718f3343c207597869'), + six.u('vcpus'): 1, six.u('memory_mb'): 512, + six.u('state'): six.u('active'), + six.u('flavor'): six.u('m1.tiny'), + six.u('started_at'): six.u('2012-01-20 18:06:06.479998')}], + six.u('start'): six.u('2011-12-25 19:48:41.750687'), + six.u('total_local_gb_usage'): 0.0}}) # # Certificates # diff --git a/novaclient/tests/v1_1/test_availability_zone.py b/novaclient/tests/v1_1/test_availability_zone.py index 896b78ad7..93cb6bebf 100644 --- a/novaclient/tests/v1_1/test_availability_zone.py +++ b/novaclient/tests/v1_1/test_availability_zone.py @@ -14,6 +14,8 @@ # License for the specific language governing permissions and limitations # under the License. +import six + from novaclient.v1_1 import availability_zones from novaclient.v1_1 import shell from novaclient.tests.v1_1 import fakes @@ -39,8 +41,8 @@ def test_list_availability_zone(self): self.assertEqual(2, len(zones)) - l0 = [u'zone-1', u'available'] - l1 = [u'zone-2', u'not available'] + l0 = [six.u('zone-1'), six.u('available')] + l1 = [six.u('zone-2'), six.u('not available')] z0 = shell._treeizeAvailabilityZone(zones[0]) z1 = shell._treeizeAvailabilityZone(zones[1]) @@ -60,15 +62,18 @@ def test_detail_availability_zone(self): self.assertEqual(3, len(zones)) - l0 = [u'zone-1', u'available'] - l1 = [u'|- fake_host-1', u''] - l2 = [u'| |- nova-compute', u'enabled :-) 2012-12-26 14:45:25'] - l3 = [u'internal', u'available'] - l4 = [u'|- fake_host-1', u''] - l5 = [u'| |- nova-sched', u'enabled :-) 2012-12-26 14:45:25'] - l6 = [u'|- fake_host-2', u''] - l7 = [u'| |- nova-network', u'enabled XXX 2012-12-26 14:45:24'] - l8 = [u'zone-2', u'not available'] + l0 = [six.u('zone-1'), six.u('available')] + l1 = [six.u('|- fake_host-1'), six.u('')] + l2 = [six.u('| |- nova-compute'), + six.u('enabled :-) 2012-12-26 14:45:25')] + l3 = [six.u('internal'), six.u('available')] + l4 = [six.u('|- fake_host-1'), six.u('')] + l5 = [six.u('| |- nova-sched'), + six.u('enabled :-) 2012-12-26 14:45:25')] + l6 = [six.u('|- fake_host-2'), six.u('')] + l7 = [six.u('| |- nova-network'), + six.u('enabled XXX 2012-12-26 14:45:24')] + l8 = [six.u('zone-2'), six.u('not available')] z0 = shell._treeizeAvailabilityZone(zones[0]) z1 = shell._treeizeAvailabilityZone(zones[1]) diff --git a/novaclient/tests/v1_1/test_servers.py b/novaclient/tests/v1_1/test_servers.py index 5874a006c..04db70529 100644 --- a/novaclient/tests/v1_1/test_servers.py +++ b/novaclient/tests/v1_1/test_servers.py @@ -3,6 +3,7 @@ import StringIO import mock +import six from novaclient import exceptions from novaclient.v1_1 import servers @@ -108,7 +109,7 @@ def test_create_server_userdata_unicode(self): image=1, flavor=1, meta={'foo': 'bar'}, - userdata=u'こんにちは', + userdata=six.u('こんにちは'), key_name="fakekey", files={ '/etc/passwd': 'some data', # a file diff --git a/novaclient/utils.py b/novaclient/utils.py index d1e08e961..729dcf2ab 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -6,6 +6,7 @@ import uuid import prettytable +import six from novaclient import exceptions from novaclient.openstack.common import strutils @@ -339,9 +340,9 @@ def slugify(value): """ import unicodedata if not isinstance(value, unicode): - value = unicode(value) + value = six.text_type(value) value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore') - value = unicode(_slugify_strip_re.sub('', value).strip().lower()) + value = six.text_type(_slugify_strip_re.sub('', value).strip().lower()) return _slugify_hyphenate_re.sub('-', value) diff --git a/requirements.txt b/requirements.txt index f7deffed1..3390adde9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ iso8601>=0.1.4 prettytable>=0.6,<0.8 requests>=0.8 simplejson +six From a25d4fe59aac92eece3f080f16fdca7ffc453064 Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Mon, 24 Jun 2013 08:32:05 -0500 Subject: [PATCH 0196/1705] python3: Compatibility for iteritems differences In python3 dict.iteritems(), dict.iterkeys(), and dict.itervalues() are no longer supported. So use six.iteritems() where it is appropriate. Change-Id: I8b07dc2a89d790ec275d45f859e1644e9b00c837 Signed-off-by: Chuck Short --- novaclient/auth_plugin.py | 4 +++- novaclient/base.py | 5 ++++- novaclient/utils.py | 3 ++- novaclient/v1_1/security_groups.py | 4 +++- novaclient/v1_1/servers.py | 4 +++- novaclient/v1_1/volumes.py | 4 +++- requirements.txt | 1 + 7 files changed, 19 insertions(+), 6 deletions(-) diff --git a/novaclient/auth_plugin.py b/novaclient/auth_plugin.py index 39da86aa6..843489788 100644 --- a/novaclient/auth_plugin.py +++ b/novaclient/auth_plugin.py @@ -17,6 +17,8 @@ import logging import pkg_resources +import six + from novaclient import exceptions from novaclient import utils @@ -49,7 +51,7 @@ def load_auth_system_opts(parser): This function will try to populate the parser with options from the available plugins. """ - for name, auth_plugin in _discovered_plugins.iteritems(): + for name, auth_plugin in six.iteritems(_discovered_plugins): add_opts_fn = getattr(auth_plugin, "add_opts", None) if add_opts_fn: group = parser.add_argument_group("Auth-system '%s' options" % diff --git a/novaclient/base.py b/novaclient/base.py index c97c104c4..3cbc65ed5 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -23,6 +23,9 @@ import contextlib import hashlib import os + +import six + from novaclient import exceptions from novaclient import utils @@ -315,7 +318,7 @@ def human_id(self): return None def _add_details(self, info): - for (k, v) in info.iteritems(): + for (k, v) in six.iteritems(info): try: setattr(self, k, v) self._info[k] = v diff --git a/novaclient/utils.py b/novaclient/utils.py index d1e08e961..9badbe84a 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -6,6 +6,7 @@ import uuid import prettytable +import six from novaclient import exceptions from novaclient.openstack.common import strutils @@ -172,7 +173,7 @@ def print_list(objs, fields, formatters={}, sortby_index=None): def print_dict(d, dict_property="Property", dict_value="Value", wrap=0): pt = prettytable.PrettyTable([dict_property, dict_value], caching=False) pt.align = 'l' - for k, v in d.iteritems(): + for k, v in six.iteritems(d): # convert dict to str to check length if isinstance(v, dict): v = str(v) diff --git a/novaclient/v1_1/security_groups.py b/novaclient/v1_1/security_groups.py index d0778634a..85e67de7b 100644 --- a/novaclient/v1_1/security_groups.py +++ b/novaclient/v1_1/security_groups.py @@ -19,6 +19,8 @@ import urllib +import six + from novaclient import base @@ -87,7 +89,7 @@ def list(self, search_opts=None): """ search_opts = search_opts or {} - qparams = dict((k, v) for (k, v) in search_opts.iteritems() if v) + qparams = dict((k, v) for (k, v) in six.iteritems(search_opts) if v) query_string = '?%s' % urllib.urlencode(qparams) if qparams else '' diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index 260d81806..02a7aa338 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -21,6 +21,8 @@ import urllib +import six + from novaclient import base from novaclient import crypto from novaclient.v1_1 import base as local_base @@ -365,7 +367,7 @@ def list(self, detailed=True, search_opts=None): qparams = {} - for opt, val in search_opts.iteritems(): + for opt, val in six.iteritems(search_opts): if val: qparams[opt] = val diff --git a/novaclient/v1_1/volumes.py b/novaclient/v1_1/volumes.py index 7e371ac7b..b17457068 100644 --- a/novaclient/v1_1/volumes.py +++ b/novaclient/v1_1/volumes.py @@ -19,6 +19,8 @@ import urllib +import six + from novaclient import base @@ -86,7 +88,7 @@ def list(self, detailed=True, search_opts=None): """ search_opts = search_opts or {} - qparams = dict((k, v) for (k, v) in search_opts.iteritems() if v) + qparams = dict((k, v) for (k, v) in six.iteritems(search_opts) if v) query_string = '?%s' % urllib.urlencode(qparams) if qparams else '' diff --git a/requirements.txt b/requirements.txt index f7deffed1..3390adde9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ iso8601>=0.1.4 prettytable>=0.6,<0.8 requests>=0.8 simplejson +six From 07fd520d161b8612e8e3e33155cc633c73ee7088 Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Wed, 26 Jun 2013 14:35:49 -0400 Subject: [PATCH 0197/1705] python3: Fix print statements Fix print statements while running with python3. This is due to the fact that the print() has changed between python2 and python3. Change-Id: I3af57cf8925e0fcfb34981f5b72ed989ba9f6cd4 Signed-off-by: Chuck Short --- novaclient/v1_1/shell.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 0156cbb55..5d882a646 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -16,6 +16,8 @@ # License for the specific language governing permissions and limitations # under the License. +from __future__ import print_function + import argparse import copy import datetime @@ -896,7 +898,7 @@ def do_image_delete(cs, args): try: _find_image(cs, image).delete() except Exception as e: - print "Delete for image %s failed: %s" % (image, e) + print("Delete for image %s failed: %s" % (image, e)) @utils.arg('--reservation-id', @@ -2178,7 +2180,7 @@ def _print_keypair(keypair): kp = keypair._info.copy() pk = kp.pop('public_key') utils.print_dict(kp) - print "Public key: %s" % pk + print("Public key: %s" % pk) @utils.arg('keypair', From b526c9beff3f88d792d4c5a17d68f357eaf139ac Mon Sep 17 00:00:00 2001 From: Ben Nemec Date: Wed, 26 Jun 2013 13:56:57 -0500 Subject: [PATCH 0198/1705] Remove Diablo compatibility options According to the fixme in the code, these should have been removed in Folsom. Change-Id: If11c576e45931b72c227f51a0b8f63bc5f7dd4cb --- novaclient/shell.py | 57 ++++++--------------------------------------- 1 file changed, 7 insertions(+), 50 deletions(-) diff --git a/novaclient/shell.py b/novaclient/shell.py index 289df3750..5c4697d98 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -144,8 +144,6 @@ def save(self, auth_token, management_url, tenant_id): def password(self): if self._validate_string(self.args.os_password): return self.args.os_password - if self._validate_string(self.args.apikey): - return self.args.apikey verify_pass = utils.bool_from_str(utils.env("OS_VERIFY_PASSWORD")) return self._prompt_password(verify_pass) @@ -367,32 +365,6 @@ def get_base_parser(self): "not be verified against any certificate authorities. " "This option should be used with caution.") - # FIXME(dtroyer): The args below are here for diablo compatibility, - # remove them in folsum cycle - - # alias for --os-username, left in for backwards compatibility - parser.add_argument('--username', - help=argparse.SUPPRESS) - - # alias for --os-region-name, left in for backwards compatibility - parser.add_argument('--region_name', - help=argparse.SUPPRESS) - - # alias for --os-password, left in for backwards compatibility - parser.add_argument('--apikey', '--password', dest='apikey', - default=utils.env('NOVA_API_KEY'), - help=argparse.SUPPRESS) - - # alias for --os-tenant-name, left in for backward compatibility - parser.add_argument('--projectid', '--tenant_name', dest='projectid', - default=utils.env('NOVA_PROJECT_ID'), - help=argparse.SUPPRESS) - - # alias for --os-auth-url, left in for backward compatibility - parser.add_argument('--url', '--auth_url', dest='url', - default=utils.env('NOVA_URL'), - help=argparse.SUPPRESS) - parser.add_argument('--bypass-url', metavar='', dest='bypass_url', @@ -563,15 +535,12 @@ def main(self, argv): (os_username, os_tenant_name, os_auth_url, os_region_name, os_auth_system, endpoint_type, insecure, service_type, service_name, volume_service_name, - username, projectid, url, region_name, bypass_url, os_cache, cacert, timeout) = ( args.os_username, args.os_tenant_name, args.os_auth_url, args.os_region_name, args.os_auth_system, args.endpoint_type, args.insecure, args.service_type, args.service_name, args.volume_service_name, - args.username, args.projectid, - args.url, args.region_name, args.bypass_url, args.os_cache, args.os_cacert, args.timeout) @@ -598,26 +567,17 @@ def main(self, argv): if not auth_plugin or not auth_plugin.opts: if not os_username: - if not username: - raise exc.CommandError("You must provide a username " - "via either --os-username or env[OS_USERNAME]") - else: - os_username = username + raise exc.CommandError("You must provide a username " + "via either --os-username or env[OS_USERNAME]") if not os_tenant_name: - if not projectid: - raise exc.CommandError("You must provide a tenant name " - "via either --os-tenant-name or " - "env[OS_TENANT_NAME]") - else: - os_tenant_name = projectid + raise exc.CommandError("You must provide a tenant name " + "via either --os-tenant-name or " + "env[OS_TENANT_NAME]") if not os_auth_url: - if not url: - if os_auth_system and os_auth_system != 'keystone': - os_auth_url = auth_plugin.get_auth_url() - else: - os_auth_url = url + if os_auth_system and os_auth_system != 'keystone': + os_auth_url = auth_plugin.get_auth_url() if not os_auth_url: raise exc.CommandError("You must provide an auth url " @@ -626,9 +586,6 @@ def main(self, argv): "default url with --os-auth-system " "or env[OS_AUTH_SYSTEM]") - if not os_region_name and region_name: - os_region_name = region_name - if (options.os_compute_api_version and options.os_compute_api_version != '1.0'): if not os_tenant_name: From 0134008f9b305958e65eb02972b6237789189467 Mon Sep 17 00:00:00 2001 From: Dirk Mueller Date: Thu, 27 Jun 2013 21:42:11 +0200 Subject: [PATCH 0199/1705] Fixup trivial License Header mismatch. The currently proposed Hacking check H103 compares the license boilerplate header in source files with a known good version. Fix up the syntactical-only mismatches with that check. Change-Id: Ie8861b9ded858aabb4cebbe9db656e8cccc9efed --- novaclient/tests/v1_1/test_coverage_ext.py | 2 +- novaclient/v1_1/client.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/novaclient/tests/v1_1/test_coverage_ext.py b/novaclient/tests/v1_1/test_coverage_ext.py index 0c78afaa4..de23ee7b4 100644 --- a/novaclient/tests/v1_1/test_coverage_ext.py +++ b/novaclient/tests/v1_1/test_coverage_ext.py @@ -12,7 +12,7 @@ # 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 +# under the License. # See: http://wiki.openstack.org/Nova/CoverageExtension for more information # and usage explanation for this API extension diff --git a/novaclient/v1_1/client.py b/novaclient/v1_1/client.py index 7b93cc369..3be8992f4 100644 --- a/novaclient/v1_1/client.py +++ b/novaclient/v1_1/client.py @@ -12,7 +12,7 @@ # 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 +# under the License. from novaclient import client from novaclient.v1_1 import agents From c7e9b1b8dc23894f3a74e0d63591705c59f81f98 Mon Sep 17 00:00:00 2001 From: Matt Thompson Date: Thu, 20 Jun 2013 15:52:04 +0100 Subject: [PATCH 0200/1705] Bring stdout/stderr capturing in line w/ nova In .testr.conf, we reference OS_STDOUT_CAPTURE / OS_STDERR_CAPTURE while in novaclient/tests/utils.py we reference OS_STDOUT_NOCAPTURE and OS_STDERR_NOCAPTURE. This change brings things more in line with nova project by referencing OS_STDOUT_CAPTURE / OS_STDERR_CAPTURE in both locations. Change-Id: I22efdec84bef78e99d1d95303cadade6011d76a2 Fixes: bug #1192997 --- novaclient/tests/utils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/novaclient/tests/utils.py b/novaclient/tests/utils.py index 11bff80e1..4fccff9b7 100644 --- a/novaclient/tests/utils.py +++ b/novaclient/tests/utils.py @@ -12,12 +12,12 @@ class TestCase(testtools.TestCase): def setUp(self): super(TestCase, self).setUp() - if (os.environ.get('OS_STDOUT_NOCAPTURE') == 'True' and - os.environ.get('OS_STDOUT_NOCAPTURE') == '1'): + if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or + os.environ.get('OS_STDOUT_CAPTURE') == '1'): stdout = self.useFixture(fixtures.StringStream('stdout')).stream self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout)) - if (os.environ.get('OS_STDERR_NOCAPTURE') == 'True' and - os.environ.get('OS_STDERR_NOCAPTURE') == '1'): + if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or + os.environ.get('OS_STDERR_CAPTURE') == '1'): stderr = self.useFixture(fixtures.StringStream('stderr')).stream self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr)) From 2770e059e9b5a3b36298b701e12827aea183acfd Mon Sep 17 00:00:00 2001 From: Daniel Wallace Date: Fri, 28 Jun 2013 20:19:55 -0500 Subject: [PATCH 0201/1705] Adds zsh completion Use nova bash-completion to add native zsh completion using built in parameter expansion. Nothing spectacular or new, this is mostly so that zsh users do not need to autoload bashcompinit just to use nova. Change-Id: I56f62f036e0c85e79197f4c7dfd25abf7eb4110a Implements: zsh completion --- tools/nova.zsh_completion | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 tools/nova.zsh_completion diff --git a/tools/nova.zsh_completion b/tools/nova.zsh_completion new file mode 100644 index 000000000..43c4d649a --- /dev/null +++ b/tools/nova.zsh_completion @@ -0,0 +1,29 @@ +#compdef nova + +local -a nbc _nova_opts _nova_flags _nova_opts_exp cur prev + +nbc=(${(ps: :)$(_call_program options "$service bash-completion" 2>/dev/null)}) +_nova_opts=(${nbc:#-*}) +_nova_flags=(${(M)nbc:#-*}) +_nova_opt_exp=${${nbc:#-*}// /|} +cur=$words[CURRENT] +prev=$words[(( CURRENT - 1 ))] + +_checkcomp(){ + for word in $words[@]; do + if [[ -n ${_nova_opts[(r)$word]} ]]; then + return 0 + fi + done + return 1 +} + +echo $_nova_opts[@] |grep --color nova +if [[ "$prev" != "help" ]] && _checkcomp; then + COMPLETION_CACHE=(~/.novaclient/*/*-cache) + cflags=($_nova_flags[@] ${(ps: :)$(cat $COMPLETION_CACHE 2>/dev/null)}) + compadd "$@" -d $cflags[@] +else + compadd "$@" -d $_nova_opts[@] +fi + From 02f906bcd6866b24fa0b48d47f573197b17f0753 Mon Sep 17 00:00:00 2001 From: Phil Day Date: Thu, 27 Jun 2013 22:57:10 +0100 Subject: [PATCH 0202/1705] Allow tenant ID for authentication Tenant names are not necessarily unique for a User, so the client should also allow authentication by tenant_id If both ID and Name are specificed then use the ID Fixes bug 1195454 Change-Id: Ib62aabc3702db88f02259cd721f9efb31404bcb7 --- novaclient/client.py | 9 ++++++--- novaclient/shell.py | 26 ++++++++++++++++++-------- novaclient/tests/test_shell.py | 26 ++++++++++++++++++++++---- novaclient/v1_1/client.py | 14 ++++++++------ 4 files changed, 54 insertions(+), 21 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index 1c93a33d0..50aedcdd5 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -33,7 +33,7 @@ class HTTPClient(object): USER_AGENT = 'python-novaclient' - def __init__(self, user, password, projectid, auth_url=None, + def __init__(self, user, password, projectid=None, auth_url=None, insecure=False, timeout=None, proxy_tenant_id=None, proxy_token=None, region_name=None, endpoint_type='publicURL', service_type=None, @@ -42,10 +42,11 @@ def __init__(self, user, password, projectid, auth_url=None, os_cache=False, no_cache=True, http_log_debug=False, auth_system='keystone', auth_plugin=None, - cacert=None): + cacert=None, tenant_id=None): self.user = user self.password = password self.projectid = projectid + self.tenant_id = tenant_id if auth_system and auth_system != 'keystone' and not auth_plugin: raise exceptions.AuthSystemNotFound(auth_system) @@ -407,7 +408,9 @@ def _v2_auth(self, url): "passwordCredentials": {"username": self.user, "password": self.password}}} - if self.projectid: + if self.tenant_id: + body['auth']['tenantId'] = self.tenant_id + elif self.projectid: body['auth']['tenantName'] = self.projectid self._authenticate(url, body) diff --git a/novaclient/shell.py b/novaclient/shell.py index 5c4697d98..3c8468dd0 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -288,6 +288,11 @@ def get_base_parser(self): parser.add_argument('--os_tenant_name', help=argparse.SUPPRESS) + parser.add_argument('--os-tenant-id', + metavar='', + default=utils.env('OS_TENANT_ID'), + help='Defaults to env[OS_TENANT_ID].') + parser.add_argument('--os-auth-url', metavar='', default=utils.env('OS_AUTH_URL', 'NOVA_URL'), @@ -532,12 +537,13 @@ def main(self, argv): self.do_bash_completion(args) return 0 - (os_username, os_tenant_name, os_auth_url, + (os_username, os_tenant_name, os_tenant_id, os_auth_url, os_region_name, os_auth_system, endpoint_type, insecure, service_type, service_name, volume_service_name, bypass_url, os_cache, cacert, timeout) = ( args.os_username, - args.os_tenant_name, args.os_auth_url, + args.os_tenant_name, args.os_tenant_id, + args.os_auth_url, args.os_region_name, args.os_auth_system, args.endpoint_type, args.insecure, args.service_type, args.service_name, args.volume_service_name, @@ -570,10 +576,11 @@ def main(self, argv): raise exc.CommandError("You must provide a username " "via either --os-username or env[OS_USERNAME]") - if not os_tenant_name: + if not os_tenant_name and not os_tenant_id: raise exc.CommandError("You must provide a tenant name " - "via either --os-tenant-name or " - "env[OS_TENANT_NAME]") + "or tenant id via --os-tenant-name, " + "--os-tenant-id, env[OS_TENANT_NAME] " + "or env[OS_TENANT_ID]") if not os_auth_url: if os_auth_system and os_auth_system != 'keystone': @@ -588,16 +595,19 @@ def main(self, argv): if (options.os_compute_api_version and options.os_compute_api_version != '1.0'): - if not os_tenant_name: + if not os_tenant_name and not os_tenant_id: raise exc.CommandError("You must provide a tenant name " - "via either --os-tenant-name or env[OS_TENANT_NAME]") + "or tenant id via --os-tenant-name, " + "--os-tenant-id, env[OS_TENANT_NAME] " + "or env[OS_TENANT_ID]") if not os_auth_url: raise exc.CommandError("You must provide an auth url " "via either --os-auth-url or env[OS_AUTH_URL]") self.cs = client.Client(options.os_compute_api_version, os_username, - os_password, os_tenant_name, os_auth_url, insecure, + os_password, os_tenant_name, tenant_id=os_tenant_id, + auth_url=os_auth_url, insecure=insecure, region_name=os_region_name, endpoint_type=endpoint_type, extensions=self.extensions, service_type=service_type, service_name=service_name, auth_system=os_auth_system, diff --git a/novaclient/tests/test_shell.py b/novaclient/tests/test_shell.py index 4da55d9d7..a4d8c5bed 100644 --- a/novaclient/tests/test_shell.py +++ b/novaclient/tests/test_shell.py @@ -19,11 +19,16 @@ 'OS_TENANT_NAME': 'tenant_name', 'OS_AUTH_URL': 'http://no.where'} +FAKE_ENV2 = {'OS_USERNAME': 'username', + 'OS_PASSWORD': 'password', + 'OS_TENANT_ID': 'tenant_id', + 'OS_AUTH_URL': 'http://no.where'} + class ShellTest(utils.TestCase): - def make_env(self, exclude=None): - env = dict((k, v) for k, v in FAKE_ENV.items() if k != exclude) + def make_env(self, exclude=None, fake_env=FAKE_ENV): + env = dict((k, v) for k, v in fake_env.items() if k != exclude) self.useFixture(fixtures.MonkeyPatch('os.environ', env)) def setUp(self): @@ -125,8 +130,9 @@ def test_no_username(self): self.fail('CommandError not raised') def test_no_tenant_name(self): - required = ('You must provide a tenant name' - ' via either --os-tenant-name or env[OS_TENANT_NAME]',) + required = ('You must provide a tenant name or tenant id' + ' via --os-tenant-name, --os-tenant-id,' + ' env[OS_TENANT_NAME] or env[OS_TENANT_ID]',) self.make_env(exclude='OS_TENANT_NAME') try: self.shell('list') @@ -135,6 +141,18 @@ def test_no_tenant_name(self): else: self.fail('CommandError not raised') + def test_no_tenant_id(self): + required = ('You must provide a tenant name or tenant id' + ' via --os-tenant-name, --os-tenant-id,' + ' env[OS_TENANT_NAME] or env[OS_TENANT_ID]',) + self.make_env(exclude='OS_TENANT_ID', fake_env=FAKE_ENV2) + try: + self.shell('list') + except exceptions.CommandError as message: + self.assertEqual(required, message.args) + else: + self.fail('CommandError not raised') + def test_no_auth_url(self): required = ('You must provide an auth url' ' via either --os-auth-url or env[OS_AUTH_URL] or' diff --git a/novaclient/v1_1/client.py b/novaclient/v1_1/client.py index 7b93cc369..df97b7316 100644 --- a/novaclient/v1_1/client.py +++ b/novaclient/v1_1/client.py @@ -65,8 +65,8 @@ class Client(object): """ - # FIXME(jesse): project_id isn't required to authenticate - def __init__(self, username, api_key, project_id, auth_url=None, + # FIXME(jesse): projectid isn't required to authenticate + def __init__(self, username, api_key, projectid, auth_url=None, insecure=False, timeout=None, proxy_tenant_id=None, proxy_token=None, region_name=None, endpoint_type='publicURL', extensions=None, @@ -75,11 +75,12 @@ def __init__(self, username, api_key, project_id, auth_url=None, bypass_url=None, os_cache=False, no_cache=True, http_log_debug=False, auth_system='keystone', auth_plugin=None, - cacert=None): + cacert=None, tenant_id=None): # FIXME(comstud): Rename the api_key argument above when we # know it's not being used as keyword argument password = api_key - self.project_id = project_id + self.projectid = projectid + self.tenant_id = tenant_id self.flavors = flavors.FlavorManager(self) self.flavor_access = flavor_access.FlavorAccessManager(self) self.images = images.ImageManager(self) @@ -128,8 +129,9 @@ def __init__(self, username, api_key, project_id, auth_url=None, self.client = client.HTTPClient(username, password, - project_id, - auth_url, + projectid=projectid, + tenant_id=tenant_id, + auth_url=auth_url, insecure=insecure, timeout=timeout, auth_system=auth_system, From d095b8a335ec7cdc61b4f2db99ae06346c0879be Mon Sep 17 00:00:00 2001 From: Sulochan Acharya Date: Fri, 28 Jun 2013 17:24:54 -0500 Subject: [PATCH 0203/1705] CLI for disable service reason Adds cli option to allow users to give reason for service-disable. Also adds disabled reason as a column in service list, so any disabled service can be seen with reason. A recent nova change that allows disable-log-reason allows users to provide reason for disabling service. This just adds the cli option for the method. Blueprint record-reason-for-disabling-service Change-Id: If263788c34279d6b4c568d5e0320448d2ff67a12 --- novaclient/tests/v1_1/fakes.py | 6 ++++++ novaclient/tests/v1_1/test_services.py | 9 +++++++++ novaclient/tests/v1_1/test_shell.py | 6 ++++++ novaclient/v1_1/services.py | 5 +++++ novaclient/v1_1/shell.py | 16 ++++++++++++++-- 5 files changed, 40 insertions(+), 2 deletions(-) diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index cab34e6c3..aa2ceae42 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -1377,6 +1377,12 @@ def put_os_services_disable(self, body, **kw): 'binary': body['binary'], 'status': 'disabled'}}) + def put_os_services_disable_log_reason(self, body, **kw): + return (200, {}, {'service': {'host': body['host'], + 'binary': body['binary'], + 'status': 'disabled', + 'disabled_reason': body['disabled_reason']}}) + # # Fixed IPs # diff --git a/novaclient/tests/v1_1/test_services.py b/novaclient/tests/v1_1/test_services.py index 381fb143d..ddd43bc8a 100644 --- a/novaclient/tests/v1_1/test_services.py +++ b/novaclient/tests/v1_1/test_services.py @@ -66,3 +66,12 @@ def test_services_disable(self): cs.assert_called('PUT', '/os-services/disable', values) self.assertTrue(isinstance(service, services.Service)) self.assertEqual(service.status, 'disabled') + + def test_services_disable_log_reason(self): + service = cs.services.disable_log_reason('compute1', 'nova-compute', + 'disable bad host') + values = {'host': 'compute1', 'binary': 'nova-compute', + 'disabled_reason': 'disable bad host'} + cs.assert_called('PUT', '/os-services/disable-log-reason', values) + self.assertTrue(isinstance(service, services.Service)) + self.assertEqual(service.status, 'disabled') diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index 0107b0284..5152abcac 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -956,6 +956,12 @@ def test_services_disable(self): body = {'host': 'host1', 'binary': 'nova-cert'} self.assert_called('PUT', '/os-services/disable', body) + def test_services_disable_with_reason(self): + self.run_command('service-disable host1 nova-cert --reason no_reason') + body = {'host': 'host1', 'binary': 'nova-cert', + 'disabled_reason': 'no_reason'} + self.assert_called('PUT', '/os-services/disable-log-reason', body) + def test_fixed_ips_get(self): self.run_command('fixed-ip-get 192.168.1.1') self.assert_called('GET', '/os-fixed-ips/192.168.1.1') diff --git a/novaclient/v1_1/services.py b/novaclient/v1_1/services.py index 13805db4b..ec580caf6 100644 --- a/novaclient/v1_1/services.py +++ b/novaclient/v1_1/services.py @@ -59,3 +59,8 @@ def disable(self, host, binary): """Enable the service specified by hostname and binary""" body = {"host": host, "binary": binary} return self._update("/os-services/disable", body, "service") + + def disable_log_reason(self, host, binary, reason): + """Disable the service with reason""" + body = {"host": host, "binary": binary, "disabled_reason": reason} + return self._update("/os-services/disable-log-reason", body, "service") diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 0156cbb55..c82b32279 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2543,6 +2543,10 @@ def do_service_list(cs, args): """Show a list of all running services. Filter by host & binary.""" result = cs.services.list(host=args.host, binary=args.binary) columns = ["Binary", "Host", "Zone", "Status", "State", "Updated_at"] + # NOTE(sulo): we check if the response has disabled_reason + # so as not to add the column when the extended ext is not enabled. + if hasattr(result[0], 'disabled_reason'): + columns.append("Disabled Reason") utils.print_list(result, columns) @@ -2556,10 +2560,18 @@ def do_service_enable(cs, args): @utils.arg('host', metavar='', help='Name of host.') @utils.arg('binary', metavar='', help='Service binary.') +@utils.arg('--reason', metavar='', + help='Reason for disabling service.') def do_service_disable(cs, args): """Disable the service""" - result = cs.services.disable(args.host, args.binary) - utils.print_list([result], ['Host', 'Binary', 'Status']) + if args.reason: + result = cs.services.disable_log_reason(args.host, args.binary, + args.reason) + utils.print_list([result], ['Host', 'Binary', 'Status', + 'Disabled Reason']) + else: + result = cs.services.disable(args.host, args.binary) + utils.print_list([result], ['Host', 'Binary', 'Status']) @utils.arg('fixed_ip', metavar='', help='Fixed IP Address.') From 1b3cd6ff9e9c77dbb267d293877c0c55c3a0382e Mon Sep 17 00:00:00 2001 From: Joe Gordon Date: Mon, 8 Jul 2013 11:38:33 +0200 Subject: [PATCH 0204/1705] Clean up and make HACKING.rst point to openstack-dev/hacking Instead of having a full local copy of HACKING Reference the OpenStack hacking guide (openstack-dev/hacking) and remove duplicate sections. Change-Id: Iaabc27c42d74b7441c17e63db15724f64114620b --- HACKING | 123 ---------------------------------------------------- HACKING.rst | 59 +++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 123 deletions(-) delete mode 100644 HACKING create mode 100644 HACKING.rst diff --git a/HACKING b/HACKING deleted file mode 100644 index 1c1a04a73..000000000 --- a/HACKING +++ /dev/null @@ -1,123 +0,0 @@ -Nova Style Commandments -======================= - -Step 1: Read http://www.python.org/dev/peps/pep-0008/ -Step 2: Read http://www.python.org/dev/peps/pep-0008/ again -Step 3: Read on - -Imports -------- -- thou shalt not import objects, only modules -- thou shalt not import more than one module per line -- thou shalt not make relative imports -- thou shalt organize your imports according to the following template - -:: - # vim: tabstop=4 shiftwidth=4 softtabstop=4 - {{stdlib imports in human alphabetical order}} - \n - {{nova imports in human alphabetical order}} - \n - \n - {{begin your code}} - - -General -------- -- thou shalt put two newlines twixt toplevel code (funcs, classes, etc) -- thou shalt put one newline twixt methods in classes and anywhere else -- thou shalt not write "except:", use "except Exception:" at the very least -- thou shalt include your name with TODOs as in "TODO(termie)" -- thou shalt not name anything the same name as a builtin or reserved word -- thou shalt not violate causality in our time cone, or else - - -Human Alphabetical Order Examples ---------------------------------- -:: - import httplib - import logging - import random - import StringIO - import time - import unittest - - from nova import flags - from nova import test - from nova.auth import users - from nova.endpoint import api - from nova.endpoint import cloud - -Docstrings ----------- - """A one line docstring looks like this and ends in a period.""" - - - """A multiline docstring has a one-line summary, less than 80 characters. - - Then a new paragraph after a newline that explains in more detail any - general information about the function, class or method. Example usages - are also great to have here if it is a complex class for function. After - you have finished your descriptions add an extra newline and close the - quotations. - - When writing the docstring for a class, an extra line should be placed - after the closing quotations. For more in-depth explanations for these - decisions see http://www.python.org/dev/peps/pep-0257/ - - If you are going to describe parameters and return values, use Sphinx, the - appropriate syntax is as follows. - - :param foo: the foo parameter - :param bar: the bar parameter - :returns: description of the return value - - """ - -Text encoding ----------- -- All text within python code should be of type 'unicode'. - - WRONG: - - >>> s = 'foo' - >>> s - 'foo' - >>> type(s) - - - RIGHT: - - >>> u = u'foo' - >>> u - u'foo' - >>> type(u) - - -- Transitions between internal unicode and external strings should always - be immediately and explicitly encoded or decoded. - -- All external text that is not explicitly encoded (database storage, - commandline arguments, etc.) should be presumed to be encoded as utf-8. - - WRONG: - - mystring = infile.readline() - myreturnstring = do_some_magic_with(mystring) - outfile.write(myreturnstring) - - RIGHT: - - mystring = infile.readline() - mytext = s.decode('utf-8') - returntext = do_some_magic_with(mytext) - returnstring = returntext.encode('utf-8') - outfile.write(returnstring) - -Running Tests -------------- -The testing system is based on a combination of tox and testr. If you just -want to run the whole suite, run `tox` and all will be fine. However, if -you'd like to dig in a bit more, you might want to learn some things about -testr itself. A basic walkthrough for OpenStack can be found at -http://wiki.openstack.org/testr diff --git a/HACKING.rst b/HACKING.rst new file mode 100644 index 000000000..ac363673e --- /dev/null +++ b/HACKING.rst @@ -0,0 +1,59 @@ +Nova Client Style Commandments +============================== + +- Step 1: Read the OpenStack Style Commandments + https://github.com/openstack-dev/hacking/blob/master/HACKING.rst +- Step 2: Read on + + +Nova Client Specific Commandments +--------------------------------- +None so far + +Text encoding +------------- +- All text within python code should be of type 'unicode'. + + WRONG: + + >>> s = 'foo' + >>> s + 'foo' + >>> type(s) + + + RIGHT: + + >>> u = u'foo' + >>> u + u'foo' + >>> type(u) + + +- Transitions between internal unicode and external strings should always + be immediately and explicitly encoded or decoded. + +- All external text that is not explicitly encoded (database storage, + commandline arguments, etc.) should be presumed to be encoded as utf-8. + + WRONG: + + mystring = infile.readline() + myreturnstring = do_some_magic_with(mystring) + outfile.write(myreturnstring) + + RIGHT: + + mystring = infile.readline() + mytext = s.decode('utf-8') + returntext = do_some_magic_with(mytext) + returnstring = returntext.encode('utf-8') + outfile.write(returnstring) + +Running Tests +------------- +The testing system is based on a combination of tox and testr. If you just +want to run the whole suite, run `tox` and all will be fine. However, if +you'd like to dig in a bit more, you might want to learn some things about +testr itself. A basic walkthrough for OpenStack can be found at +http://wiki.openstack.org/testr From a5558f8780c78a9f57d58b0c4ee706cd66bb1c06 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 5 Jul 2013 23:11:26 -0400 Subject: [PATCH 0205/1705] Sync install_venv_common from oslo The current version of install_venv_common uses the --distribute flag in its creation of the virtualenv. This causes some upgrade problems with the new versions of distribute and setuptools. The solution to those problems is to get off of the distribute bandwagon. Change-Id: I5efe196c46b12d88c853f8362ebcbf0cc6f1573d --- openstack-common.conf | 1 + tools/install_venv.py | 263 +++++++---------------------------- tools/install_venv_common.py | 212 ++++++++++++++++++++++++++++ 3 files changed, 260 insertions(+), 216 deletions(-) create mode 100644 tools/install_venv_common.py diff --git a/openstack-common.conf b/openstack-common.conf index 74f13100a..33f18d3a8 100644 --- a/openstack-common.conf +++ b/openstack-common.conf @@ -1,6 +1,7 @@ [DEFAULT] # The list of modules to copy from openstack-common +module=install_venv_common module=strutils module=timeutils module=uuidutils diff --git a/tools/install_venv.py b/tools/install_venv.py index 59d0b30a3..0011a8be1 100644 --- a/tools/install_venv.py +++ b/tools/install_venv.py @@ -5,242 +5,73 @@ # All Rights Reserved. # # Copyright 2010 OpenStack Foundation +# Copyright 2013 IBM Corp. +# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. # -# 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 +# 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 +# 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. - -""" -Installation script for Nova's development virtualenv -""" - -from __future__ import print_function -import optparse +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import ConfigParser import os -import subprocess import sys - -ROOT = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) -VENV = os.path.join(ROOT, '.venv') -PIP_REQUIRES = os.path.join(ROOT, 'requirements.txt') -TEST_REQUIRES = os.path.join(ROOT, 'test-requirements.txt') -PY_VERSION = "python%s.%s" % (sys.version_info[0], sys.version_info[1]) - - -def die(message, *args): - print(message % args, file=sys.stderr) - sys.exit(1) - - -def check_python_version(): - if sys.version_info < (2, 6): - die("Need Python Version >= 2.6") - - -def run_command_with_code(cmd, redirect_output=True, check_exit_code=True): - """ - Runs a command in an out-of-process shell, returning the - output of that command. Working directory is ROOT. - """ - if redirect_output: - stdout = subprocess.PIPE - else: - stdout = None - - proc = subprocess.Popen(cmd, cwd=ROOT, stdout=stdout) - output = proc.communicate()[0] - if check_exit_code and proc.returncode != 0: - die('Command "%s" failed.\n%s', ' '.join(cmd), output) - return (output, proc.returncode) - - -def run_command(cmd, redirect_output=True, check_exit_code=True): - return run_command_with_code(cmd, redirect_output, check_exit_code)[0] - - -class Distro(object): - - def check_cmd(self, cmd): - return bool(run_command(['which', cmd], check_exit_code=False).strip()) - - def install_virtualenv(self): - if self.check_cmd('virtualenv'): - return - - if self.check_cmd('easy_install'): - print('Installing virtualenv via easy_install...') - if run_command(['easy_install', 'virtualenv']): - print('Succeeded') - return - else: - print('Failed') - - die('ERROR: virtualenv not found.\n\nDevelopment' - ' requires virtualenv, please install it using your' - ' favorite package management tool') - - def post_process(self): - """Any distribution-specific post-processing gets done here. - - In particular, this is useful for applying patches to code inside - the venv.""" - pass - - -class Debian(Distro): - """This covers all Debian-based distributions.""" - - def check_pkg(self, pkg): - return run_command_with_code(['dpkg', '-l', pkg], - check_exit_code=False)[1] == 0 - - def apt_install(self, pkg, **kwargs): - run_command(['sudo', 'apt-get', 'install', '-y', pkg], **kwargs) - - def apply_patch(self, originalfile, patchfile): - run_command(['patch', originalfile, patchfile]) - - def install_virtualenv(self): - if self.check_cmd('virtualenv'): - return - - if not self.check_pkg('python-virtualenv'): - self.apt_install('python-virtualenv', check_exit_code=False) - - super(Debian, self).install_virtualenv() +import install_venv_common as install_venv # flake8: noqa -class Fedora(Distro): - """This covers all Fedora-based distributions. - - Includes: Fedora, RHEL, CentOS, Scientific Linux""" - - def check_pkg(self, pkg): - return run_command_with_code(['rpm', '-q', pkg], - check_exit_code=False)[1] == 0 - - def yum_install(self, pkg, **kwargs): - run_command(['sudo', 'yum', 'install', '-y', pkg], **kwargs) - - def apply_patch(self, originalfile, patchfile): - run_command(['patch', originalfile, patchfile]) - - def install_virtualenv(self): - if self.check_cmd('virtualenv'): - return - - if not self.check_pkg('python-virtualenv'): - self.yum_install('python-virtualenv', check_exit_code=False) - - super(Fedora, self).install_virtualenv() - - -def get_distro(): - if os.path.exists('/etc/fedora-release') or \ - os.path.exists('/etc/redhat-release'): - return Fedora() - elif os.path.exists('/etc/debian_version'): - return Debian() - else: - return Distro() - - -def check_dependencies(): - get_distro().install_virtualenv() - - -def create_virtualenv(venv=VENV, no_site_packages=True): - """Creates the virtual environment and installs PIP only into the - virtual environment - """ - print('Creating venv...') - if no_site_packages: - run_command(['virtualenv', '-q', '--no-site-packages', VENV]) - else: - run_command(['virtualenv', '-q', VENV]) - print('done.') - print('Installing pip in virtualenv...') - if not run_command(['tools/with_venv.sh', 'easy_install', - 'pip>1.0']).strip(): - die("Failed to install pip.") - print('done.') - - -def pip_install(*args): - run_command(['tools/with_venv.sh', - 'pip', 'install', '--upgrade'] + list(args), - redirect_output=False) - - -def install_dependencies(venv=VENV): - print('Installing dependencies with pip (this can take a while)...') - - # First things first, make sure our venv has the latest pip and distribute. - pip_install('pip') - pip_install('distribute') - - pip_install('-r', PIP_REQUIRES) - pip_install('-r', TEST_REQUIRES) - - # Tell the virtual env how to "import nova" - pthfile = os.path.join(venv, "lib", PY_VERSION, "site-packages", - "novaclient.pth") - f = open(pthfile, 'w') - f.write("%s\n" % ROOT) - - -def post_process(): - get_distro().post_process() - - -def print_help(): +def print_help(project, venv, root): help = """ - python-novaclient development environment setup is complete. + %(project)s development environment setup is complete. - python-novaclient development uses virtualenv to track and manage Python + %(project)s development uses virtualenv to track and manage Python dependencies while in development and testing. - To activate the python-novaclient virtualenv for the extent of your current + To activate the %(project)s virtualenv for the extent of your current shell session you can run: - $ source .venv/bin/activate + $ source %(venv)s/bin/activate - Or, if you prefer, you can run commands in the virtualenv on a case by case - basis by running: + Or, if you prefer, you can run commands in the virtualenv on a case by + case basis by running: - $ tools/with_venv.sh - - Also, make test will automatically use the virtualenv. + $ %(root)s/tools/with_venv.sh """ - print(help) - - -def parse_args(): - """Parse command-line arguments""" - parser = optparse.OptionParser() - parser.add_option("-n", "--no-site-packages", dest="no_site_packages", - default=False, action="store_true", - help="Do not inherit packages from global Python install") - return parser.parse_args() + print help % dict(project=project, venv=venv, root=root) def main(argv): - (options, args) = parse_args() - check_python_version() - check_dependencies() - create_virtualenv(no_site_packages=options.no_site_packages) - install_dependencies() - post_process() - print_help() + root = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) + + if os.environ.get('tools_path'): + root = os.environ['tools_path'] + venv = os.path.join(root, '.venv') + if os.environ.get('venv'): + venv = os.environ['venv'] + + pip_requires = os.path.join(root, 'requirements.txt') + test_requires = os.path.join(root, 'test-requirements.txt') + py_version = "python%s.%s" % (sys.version_info[0], sys.version_info[1]) + setup_cfg = ConfigParser.ConfigParser() + setup_cfg.read('setup.cfg') + project = setup_cfg.get('metadata', 'name') + + install = install_venv.InstallVenv( + root, venv, pip_requires, test_requires, py_version, project) + options = install.parse_args(argv) + install.check_python_version() + install.check_dependencies() + install.create_virtualenv(no_site_packages=options.no_site_packages) + install.install_dependencies() + install.post_process() + print_help(project, venv, root) if __name__ == '__main__': main(sys.argv) diff --git a/tools/install_venv_common.py b/tools/install_venv_common.py new file mode 100644 index 000000000..f428c1e02 --- /dev/null +++ b/tools/install_venv_common.py @@ -0,0 +1,212 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 OpenStack Foundation +# Copyright 2013 IBM Corp. +# +# 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. + +"""Provides methods needed by installation script for OpenStack development +virtual environments. + +Since this script is used to bootstrap a virtualenv from the system's Python +environment, it should be kept strictly compatible with Python 2.6. + +Synced in from openstack-common +""" + +from __future__ import print_function + +import optparse +import os +import subprocess +import sys + + +class InstallVenv(object): + + def __init__(self, root, venv, requirements, + test_requirements, py_version, + project): + self.root = root + self.venv = venv + self.requirements = requirements + self.test_requirements = test_requirements + self.py_version = py_version + self.project = project + + def die(self, message, *args): + print(message % args, file=sys.stderr) + sys.exit(1) + + def check_python_version(self): + if sys.version_info < (2, 6): + self.die("Need Python Version >= 2.6") + + def run_command_with_code(self, cmd, redirect_output=True, + check_exit_code=True): + """Runs a command in an out-of-process shell. + + Returns the output of that command. Working directory is self.root. + """ + if redirect_output: + stdout = subprocess.PIPE + else: + stdout = None + + proc = subprocess.Popen(cmd, cwd=self.root, stdout=stdout) + output = proc.communicate()[0] + if check_exit_code and proc.returncode != 0: + self.die('Command "%s" failed.\n%s', ' '.join(cmd), output) + return (output, proc.returncode) + + def run_command(self, cmd, redirect_output=True, check_exit_code=True): + return self.run_command_with_code(cmd, redirect_output, + check_exit_code)[0] + + def get_distro(self): + if (os.path.exists('/etc/fedora-release') or + os.path.exists('/etc/redhat-release')): + return Fedora( + self.root, self.venv, self.requirements, + self.test_requirements, self.py_version, self.project) + else: + return Distro( + self.root, self.venv, self.requirements, + self.test_requirements, self.py_version, self.project) + + def check_dependencies(self): + self.get_distro().install_virtualenv() + + def create_virtualenv(self, no_site_packages=True): + """Creates the virtual environment and installs PIP. + + Creates the virtual environment and installs PIP only into the + virtual environment. + """ + if not os.path.isdir(self.venv): + print('Creating venv...', end=' ') + if no_site_packages: + self.run_command(['virtualenv', '-q', '--no-site-packages', + self.venv]) + else: + self.run_command(['virtualenv', '-q', self.venv]) + print('done.') + else: + print("venv already exists...") + pass + + def pip_install(self, *args): + self.run_command(['tools/with_venv.sh', + 'pip', 'install', '--upgrade'] + list(args), + redirect_output=False) + + def install_dependencies(self): + print('Installing dependencies with pip (this can take a while)...') + + # First things first, make sure our venv has the latest pip and + # setuptools. + self.pip_install('pip>=1.3') + self.pip_install('setuptools') + + self.pip_install('-r', self.requirements) + self.pip_install('-r', self.test_requirements) + + def post_process(self): + self.get_distro().post_process() + + def parse_args(self, argv): + """Parses command-line arguments.""" + parser = optparse.OptionParser() + parser.add_option('-n', '--no-site-packages', + action='store_true', + help="Do not inherit packages from global Python " + "install") + return parser.parse_args(argv[1:])[0] + + +class Distro(InstallVenv): + + def check_cmd(self, cmd): + return bool(self.run_command(['which', cmd], + check_exit_code=False).strip()) + + def install_virtualenv(self): + if self.check_cmd('virtualenv'): + return + + if self.check_cmd('easy_install'): + print('Installing virtualenv via easy_install...', end=' ') + if self.run_command(['easy_install', 'virtualenv']): + print('Succeeded') + return + else: + print('Failed') + + self.die('ERROR: virtualenv not found.\n\n%s development' + ' requires virtualenv, please install it using your' + ' favorite package management tool' % self.project) + + def post_process(self): + """Any distribution-specific post-processing gets done here. + + In particular, this is useful for applying patches to code inside + the venv. + """ + pass + + +class Fedora(Distro): + """This covers all Fedora-based distributions. + + Includes: Fedora, RHEL, CentOS, Scientific Linux + """ + + def check_pkg(self, pkg): + return self.run_command_with_code(['rpm', '-q', pkg], + check_exit_code=False)[1] == 0 + + def apply_patch(self, originalfile, patchfile): + self.run_command(['patch', '-N', originalfile, patchfile], + check_exit_code=False) + + def install_virtualenv(self): + if self.check_cmd('virtualenv'): + return + + if not self.check_pkg('python-virtualenv'): + self.die("Please install 'python-virtualenv'.") + + super(Fedora, self).install_virtualenv() + + def post_process(self): + """Workaround for a bug in eventlet. + + This currently affects RHEL6.1, but the fix can safely be + applied to all RHEL and Fedora distributions. + + This can be removed when the fix is applied upstream. + + Nova: https://bugs.launchpad.net/nova/+bug/884915 + Upstream: https://bitbucket.org/eventlet/eventlet/issue/89 + RHEL: https://bugzilla.redhat.com/958868 + """ + + # Install "patch" program if it's not there + if not self.check_pkg('patch'): + self.die("Please install 'patch'.") + + # Apply the eventlet patch + self.apply_patch(os.path.join(self.venv, 'lib', self.py_version, + 'site-packages', + 'eventlet/green/subprocess.py'), + 'contrib/redhat-eventlet.patch') From 912288c4862e4023cb1952ce3e37453dc5cff883 Mon Sep 17 00:00:00 2001 From: Nicolas Simonds Date: Tue, 2 Jul 2013 16:49:03 -0700 Subject: [PATCH 0206/1705] make v2_auth and plugin_auth explictly return their results In cases where the respective authentication methods return non-NoneTypes, (e.g., HTTP 305 redirects) they would get dropped on the floor previously. This patch set splits the test_auth_redirect unit test into two nearly-identical unit tests to exercise the different code paths. Without the patch, the test_v2_auth_redirect unit test fails with an HTTP 401 error Change-Id: I2018bc5b73ce86d6d5383958375d5dbbde2e763c Fixes: Bug 1197191 --- novaclient/client.py | 4 +- novaclient/tests/v1_1/test_auth.py | 90 +++++++++++++++++++++++++++++- 2 files changed, 91 insertions(+), 3 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index 1c93a33d0..df8810241 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -395,7 +395,7 @@ def _v1_auth(self, url): raise exceptions.from_response(resp, body, url) def _plugin_auth(self, auth_url): - self.auth_plugin.authenticate(self, auth_url) + return self.auth_plugin.authenticate(self, auth_url) def _v2_auth(self, url): """Authenticate against a v2.0 auth service.""" @@ -410,7 +410,7 @@ def _v2_auth(self, url): if self.projectid: body['auth']['tenantName'] = self.projectid - self._authenticate(url, body) + return self._authenticate(url, body) def _authenticate(self, url, body, **kwargs): """Authenticate and extract the service catalog.""" diff --git a/novaclient/tests/v1_1/test_auth.py b/novaclient/tests/v1_1/test_auth.py index 11a5e420d..537f59b5f 100644 --- a/novaclient/tests/v1_1/test_auth.py +++ b/novaclient/tests/v1_1/test_auth.py @@ -96,7 +96,7 @@ def test_auth_call(): test_auth_call() - def test_auth_redirect(self): + def test_v1_auth_redirect(self): cs = client.Client("username", "password", "project_id", "auth_url/v1.0", service_type='compute') dict_correct_response = { @@ -184,6 +184,94 @@ def test_auth_call(): test_auth_call() + def test_v2_auth_redirect(self): + cs = client.Client("username", "password", "project_id", + "auth_url/v2.0", service_type='compute') + dict_correct_response = { + "access": { + "token": { + "expires": "12345", + "id": "FAKE_ID", + "tenant": { + "id": "FAKE_TENANT_ID", + } + }, + "serviceCatalog": [ + { + "type": "compute", + "endpoints": [ + { + "adminURL": "http://localhost:8774/v1.1", + "region": "RegionOne", + "internalURL": "http://localhost:8774/v1.1", + "publicURL": "http://localhost:8774/v1.1/", + }, + ], + }, + ], + }, + } + correct_response = json.dumps(dict_correct_response) + dict_responses = [ + {"headers": {'location': 'http://127.0.0.1:5001'}, + "status_code": 305, + "text": "Use proxy"}, + # Configured on admin port, nova redirects to v2.0 port. + # When trying to connect on it, keystone auth succeed by v1.0 + # protocol (through headers) but tokens are being returned in + # body (looks like keystone bug). Leaved for compatibility. + {"headers": {}, + "status_code": 200, + "text": correct_response}, + {"headers": {}, + "status_code": 200, + "text": correct_response} + ] + + responses = [(utils.TestResponse(resp)) for resp in dict_responses] + + def side_effect(*args, **kwargs): + return responses.pop(0) + + mock_request = mock.Mock(side_effect=side_effect) + + @mock.patch.object(requests.Session, "request", mock_request) + def test_auth_call(): + cs.client.authenticate() + headers = { + 'User-Agent': cs.client.USER_AGENT, + 'Content-Type': 'application/json', + 'Accept': 'application/json', + } + body = { + 'auth': { + 'passwordCredentials': { + 'username': cs.client.user, + 'password': cs.client.password, + }, + 'tenantName': cs.client.projectid, + }, + } + + token_url = cs.client.auth_url + "/tokens" + kwargs = copy.copy(self.TEST_REQUEST_BASE) + kwargs['headers'] = headers + kwargs['data'] = json.dumps(body) + mock_request.assert_called_with( + "POST", + token_url, + allow_redirects=True, + **kwargs) + + resp = dict_correct_response + endpoints = resp["access"]["serviceCatalog"][0]['endpoints'] + public_url = endpoints[0]["publicURL"].rstrip('/') + self.assertEqual(cs.client.management_url, public_url) + token_id = resp["access"]["token"]["id"] + self.assertEqual(cs.client.auth_token, token_id) + + test_auth_call() + def test_ambiguous_endpoints(self): cs = client.Client("username", "password", "project_id", "auth_url/v2.0", service_type='compute') From b5c91018e0067a29453c6aa3963e8ce6ecf89b2f Mon Sep 17 00:00:00 2001 From: Xiao Hanyu Date: Tue, 9 Jul 2013 14:31:16 +0800 Subject: [PATCH 0207/1705] Remove uncessary code related to nova start/stop 'nova start' and 'nova stop' actually send request with 'os-start' and 'os-stop', instead of 'start' and 'stop'. Change-Id: I1472e1b648dae8f3b281a113adb60421a00e5a48 --- novaclient/tests/v1_1/fakes.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index cab34e6c3..a8ccb555f 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -487,10 +487,6 @@ def post_servers_1234_action(self, body, **kw): assert body[action] is None elif action == 'os-start': assert body[action] is None - elif action == 'start': - assert body[action] is None - elif action == 'stop': - assert body[action] is None elif action == 'pause': assert body[action] is None elif action == 'unpause': From 70e6cd97906fde9a888bae6eadc435560b64ec09 Mon Sep 17 00:00:00 2001 From: gtt116 Date: Wed, 10 Jul 2013 17:45:22 +0800 Subject: [PATCH 0208/1705] Fix interface-list got none mac address. novaclient expect the response json body has a column 'mac_address', but actually is 'mac_addr'. This patch is a quick fix. Just print out "mac_addr" is readable enough. Fix bug: #1199706 Change-Id: I68823a3d719ee2f5d9d8b6227ca8eb858fc270c3 --- novaclient/v1_1/shell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 5d882a646..e4375ac06 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -3110,7 +3110,7 @@ def do_evacuate(cs, args): def _print_interfaces(interfaces): columns = ['Port State', 'Port ID', 'Net ID', 'IP addresses', - 'MAC Address'] + 'MAC Addr'] class FormattedInterface(object): def __init__(self, interface): From f50ff361e27a8ca688c0f1ba448bbc8bfb284905 Mon Sep 17 00:00:00 2001 From: Dirk Mueller Date: Thu, 27 Jun 2013 19:13:01 +0200 Subject: [PATCH 0209/1705] Skip setting volume_size if not given When the block-device parameters skip volume_size, don't set it. Setting to an empty volume_size would be invalid as it has to be an integer, and Nova API will reject the request if api validation is implemented. (proposed e.g. at https://review.openstack.org/#/c/34749/) Fixes bug LP #1199539 Change-Id: I7ab518886abf8d449caf1c70563a79a990d7765e --- novaclient/tests/v1_1/test_shell.py | 1 - novaclient/v1_1/base.py | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index 0107b0284..66ffca8a0 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -275,7 +275,6 @@ def test_boot_no_image_bdms(self): 'name': 'some-server', 'block_device_mapping': [ { - 'volume_size': '', 'volume_id': 'blah', 'delete_on_termination': '0', 'device_name': 'vda' diff --git a/novaclient/v1_1/base.py b/novaclient/v1_1/base.py index af6e689ce..e8d611aee 100644 --- a/novaclient/v1_1/base.py +++ b/novaclient/v1_1/base.py @@ -139,7 +139,8 @@ def _boot(self, resource_url, response_key, name, image, flavor, else: bdm_dict['volume_id'] = id if len(mapping_parts) > 2: - bdm_dict['volume_size'] = mapping_parts[2] + if mapping_parts[2]: + bdm_dict['volume_size'] = str(int(mapping_parts[2])) if len(mapping_parts) > 3: bdm_dict['delete_on_termination'] = mapping_parts[3] bdm.append(bdm_dict) From c360c3e8dad8bdf9f86b8f8596a956e518d0f526 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Thu, 11 Jul 2013 14:23:32 -0500 Subject: [PATCH 0210/1705] Add AgregatesManager.get() utils.find_resource() uses manager.get() as part of converting Resource names to IDs. AggregatesManager had get_details() instead of get(). Add AggregatesManager.get(), leaving .get_details() in place for backward API compatibility. Bug: 1200341 Change-Id: I7d238bbe43e1760e31f1a9ba783c668246f20844 --- novaclient/tests/v1_1/test_aggregates.py | 19 ++++++++++++++----- novaclient/tests/v1_1/test_shell.py | 18 ++++++++++++------ novaclient/v1_1/aggregates.py | 8 +++++++- novaclient/v1_1/shell.py | 2 +- 4 files changed, 34 insertions(+), 13 deletions(-) diff --git a/novaclient/tests/v1_1/test_aggregates.py b/novaclient/tests/v1_1/test_aggregates.py index f46d0871d..bbd7f2c10 100644 --- a/novaclient/tests/v1_1/test_aggregates.py +++ b/novaclient/tests/v1_1/test_aggregates.py @@ -35,6 +35,15 @@ def test_create_aggregate(self): cs.assert_called('POST', '/os-aggregates', body) self.assertTrue(isinstance(aggregate, aggregates.Aggregate)) + def test_get(self): + aggregate = cs.aggregates.get("1") + cs.assert_called('GET', '/os-aggregates/1') + self.assertTrue(isinstance(aggregate, aggregates.Aggregate)) + + aggregate2 = cs.aggregates.get(aggregate) + cs.assert_called('GET', '/os-aggregates/1') + self.assertTrue(isinstance(aggregate2, aggregates.Aggregate)) + def test_get_details(self): aggregate = cs.aggregates.get_details("1") cs.assert_called('GET', '/os-aggregates/1') @@ -45,7 +54,7 @@ def test_get_details(self): self.assertTrue(isinstance(aggregate2, aggregates.Aggregate)) def test_update(self): - aggregate = cs.aggregates.get_details("1") + aggregate = cs.aggregates.get("1") values = {"name": "foo"} body = {"aggregate": values} @@ -58,7 +67,7 @@ def test_update(self): self.assertTrue(isinstance(result2, aggregates.Aggregate)) def test_update_with_availability_zone(self): - aggregate = cs.aggregates.get_details("1") + aggregate = cs.aggregates.get("1") values = {"name": "foo", "availability_zone": "new_zone"} body = {"aggregate": values} @@ -67,7 +76,7 @@ def test_update_with_availability_zone(self): self.assertTrue(isinstance(result3, aggregates.Aggregate)) def test_add_host(self): - aggregate = cs.aggregates.get_details("1") + aggregate = cs.aggregates.get("1") host = "host1" body = {"add_host": {"host": "host1"}} @@ -84,7 +93,7 @@ def test_add_host(self): self.assertTrue(isinstance(result3, aggregates.Aggregate)) def test_remove_host(self): - aggregate = cs.aggregates.get_details("1") + aggregate = cs.aggregates.get("1") host = "host1" body = {"remove_host": {"host": "host1"}} @@ -101,7 +110,7 @@ def test_remove_host(self): self.assertTrue(isinstance(result3, aggregates.Aggregate)) def test_set_metadata(self): - aggregate = cs.aggregates.get_details("1") + aggregate = cs.aggregates.get("1") metadata = {"foo": "bar"} body = {"set_metadata": {"metadata": metadata}} diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index 0107b0284..4d9786011 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -862,7 +862,8 @@ def test_aggregate_create(self): self.run_command('aggregate-create test_name nova1') body = {"aggregate": {"name": "test_name", "availability_zone": "nova1"}} - self.assert_called('POST', '/os-aggregates', body) + self.assert_called('POST', '/os-aggregates', body, pos=-2) + self.assert_called('GET', '/os-aggregates/1', pos=-1) def test_aggregate_delete(self): self.run_command('aggregate-delete 1') @@ -871,28 +872,33 @@ def test_aggregate_delete(self): def test_aggregate_update(self): self.run_command('aggregate-update 1 new_name') body = {"aggregate": {"name": "new_name"}} - self.assert_called('PUT', '/os-aggregates/1', body) + self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) + self.assert_called('GET', '/os-aggregates/1', pos=-1) def test_aggregate_update_with_availability_zone(self): self.run_command('aggregate-update 1 foo new_zone') body = {"aggregate": {"name": "foo", "availability_zone": "new_zone"}} - self.assert_called('PUT', '/os-aggregates/1', body) + self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) + self.assert_called('GET', '/os-aggregates/1', pos=-1) def test_aggregate_set_metadata(self): self.run_command('aggregate-set-metadata 1 foo=bar delete_key') body = {"set_metadata": {"metadata": {"foo": "bar", "delete_key": None}}} - self.assert_called('POST', '/os-aggregates/1/action', body) + self.assert_called('POST', '/os-aggregates/1/action', body, pos=-2) + self.assert_called('GET', '/os-aggregates/1', pos=-1) def test_aggregate_add_host(self): self.run_command('aggregate-add-host 1 host1') body = {"add_host": {"host": "host1"}} - self.assert_called('POST', '/os-aggregates/1/action', body) + self.assert_called('POST', '/os-aggregates/1/action', body, pos=-2) + self.assert_called('GET', '/os-aggregates/1', pos=-1) def test_aggregate_remove_host(self): self.run_command('aggregate-remove-host 1 host1') body = {"remove_host": {"host": "host1"}} - self.assert_called('POST', '/os-aggregates/1/action', body) + self.assert_called('POST', '/os-aggregates/1/action', body, pos=-2) + self.assert_called('GET', '/os-aggregates/1', pos=-1) def test_aggregate_details(self): self.run_command('aggregate-details 1') diff --git a/novaclient/v1_1/aggregates.py b/novaclient/v1_1/aggregates.py index 576a81a49..c2df31526 100644 --- a/novaclient/v1_1/aggregates.py +++ b/novaclient/v1_1/aggregates.py @@ -54,11 +54,17 @@ def create(self, name, availability_zone): 'availability_zone': availability_zone}} return self._create('/os-aggregates', body, 'aggregate') - def get_details(self, aggregate): + def get(self, aggregate): """Get details of the specified aggregate.""" return self._get('/os-aggregates/%s' % (base.getid(aggregate)), "aggregate") + # NOTE:(dtroyer): utils.find_resource() uses manager.get() but we need to + # keep the API backward compatible + def get_details(self, aggregate): + """Get details of the specified aggregate.""" + return self.get(aggregate) + def update(self, aggregate, values): """Update the name and/or availability zone.""" body = {'aggregate': values} diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 5d882a646..c5d9f85b4 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2486,7 +2486,7 @@ def do_aggregate_remove_host(cs, args): @utils.arg('id', metavar='', help='Aggregate id.') def do_aggregate_details(cs, args): """Show details of the specified aggregate.""" - _print_aggregate_details(cs.aggregates.get_details(args.id)) + _print_aggregate_details(cs.aggregates.get(args.id)) def _print_aggregate_details(aggregate): From af7ca70e3e676bbd8cd20c3847233e6e25a71fde Mon Sep 17 00:00:00 2001 From: Dirk Mueller Date: Thu, 27 Jun 2013 17:49:55 +0200 Subject: [PATCH 0211/1705] Fix and enable gating on H402 End one-line docstrings with punctuation. Change them to command style where necessary. Change-Id: I8ff689c3a2f20d489286f80112c6dc95c97f2f31 --- novaclient/tests/test_utils.py | 2 +- novaclient/utils.py | 4 +- novaclient/v1_1/agents.py | 4 +- novaclient/v1_1/contrib/baremetal.py | 20 ++++----- novaclient/v1_1/flavor_access.py | 2 +- novaclient/v1_1/floating_ip_dns.py | 2 +- novaclient/v1_1/hosts.py | 2 +- novaclient/v1_1/limits.py | 8 ++-- novaclient/v1_1/services.py | 6 +-- novaclient/v1_1/shell.py | 64 ++++++++++++++-------------- tox.ini | 2 +- 11 files changed, 58 insertions(+), 58 deletions(-) diff --git a/novaclient/tests/test_utils.py b/novaclient/tests/test_utils.py index 0bd396a75..9d03c0390 100644 --- a/novaclient/tests/test_utils.py +++ b/novaclient/tests/test_utils.py @@ -69,7 +69,7 @@ def setUp(self): self.manager = FakeManager(None) def test_find_none(self): - """Test a few non-valid inputs""" + """Test a few non-valid inputs.""" self.assertRaises(exceptions.CommandError, utils.find_resource, self.manager, diff --git a/novaclient/utils.py b/novaclient/utils.py index 9d1c1916c..532ea976c 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -47,7 +47,7 @@ def add_arg(f, *args, **kwargs): def bool_from_str(val): - """Convert a string representation of a bool into a bool value""" + """Convert a string representation of a bool into a bool value.""" if not val: return False @@ -62,7 +62,7 @@ def bool_from_str(val): def add_resource_manager_extra_kwargs_hook(f, hook): - """Adds hook to bind CLI arguments to ResourceManager calls. + """Add hook to bind CLI arguments to ResourceManager calls. The `do_foo` calls in shell.py will receive CLI args and then in turn pass them through to the ResourceManager. Before passing through the args, the diff --git a/novaclient/v1_1/agents.py b/novaclient/v1_1/agents.py index 069662732..550d47ab6 100644 --- a/novaclient/v1_1/agents.py +++ b/novaclient/v1_1/agents.py @@ -36,7 +36,7 @@ class AgentsManager(base.ManagerWithFind): resource_class = Agent def list(self, hypervisor=None): - """Lists all agent builds.""" + """List all agent builds.""" url = "/os-agents" if hypervisor: url = "/os-agents?hypervisor=%s" % hypervisor @@ -53,7 +53,7 @@ def update(self, id, version, def create(self, os, architecture, version, url, md5hash, hypervisor): - """Creates a new agent build""" + """Create a new agent build.""" body = {'agent': { 'hypervisor': hypervisor, 'os': os, diff --git a/novaclient/v1_1/contrib/baremetal.py b/novaclient/v1_1/contrib/baremetal.py index 36ab047a0..db1f54977 100644 --- a/novaclient/v1_1/contrib/baremetal.py +++ b/novaclient/v1_1/contrib/baremetal.py @@ -181,7 +181,7 @@ def list_interfaces(self, node_id): type=int, help='ShellInABox port?') def do_baremetal_node_create(cs, args): - """Create a baremetal node""" + """Create a baremetal node.""" node = cs.baremetal.create(args.service_host, args.cpus, args.memory_mb, args.local_gb, args.prov_mac_address, pm_address=args.pm_address, pm_user=args.pm_user, @@ -194,7 +194,7 @@ def do_baremetal_node_create(cs, args): metavar='', help='ID of the node to delete.') def do_baremetal_node_delete(cs, args): - """Remove a baremetal node and any associated interfaces""" + """Remove a baremetal node and any associated interfaces.""" node = _find_baremetal_node(cs, args.node) cs.baremetal.delete(node) @@ -216,7 +216,7 @@ def _translate_baremetal_node_keys(collection): def _print_baremetal_nodes_list(nodes): - """Print the list of baremetal nodes""" + """Print the list of baremetal nodes.""" _translate_baremetal_node_keys(nodes) utils.print_list(nodes, [ 'ID', @@ -233,7 +233,7 @@ def _print_baremetal_nodes_list(nodes): def do_baremetal_node_list(cs, _args): - """Print a list of available baremetal nodes""" + """Print list of available baremetal nodes.""" nodes = cs.baremetal.list() _print_baremetal_nodes_list(nodes) @@ -244,13 +244,13 @@ def _find_baremetal_node(cs, node): def _print_baremetal_resource(resource): - """Print the details of a baremetal resource""" + """Print details of a baremetal resource.""" info = resource._info.copy() utils.print_dict(info) def _print_baremetal_node_interfaces(interfaces): - """Print the interfaces of a baremetal node""" + """Print interfaces of a baremetal node.""" utils.print_list(interfaces, [ 'ID', 'Datapath_ID', @@ -263,7 +263,7 @@ def _print_baremetal_node_interfaces(interfaces): metavar='', help="ID of node") def do_baremetal_node_show(cs, args): - """Show information about a baremetal node""" + """Show information about a baremetal node.""" node = _find_baremetal_node(cs, args.node) _print_baremetal_resource(node) @@ -283,7 +283,7 @@ def do_baremetal_node_show(cs, args): metavar='', help="OpenFlow port number of interface") def do_baremetal_interface_add(cs, args): - """Add a network interface to a baremetal node""" + """Add a network interface to a baremetal node.""" bmif = cs.baremetal.add_interface(args.node, args.address, args.datapath_id, args.port_no) _print_baremetal_resource(bmif) @@ -292,12 +292,12 @@ def do_baremetal_interface_add(cs, args): @utils.arg('node', metavar='', help="ID of node") @utils.arg('address', metavar='
', help="MAC address of interface") def do_baremetal_interface_remove(cs, args): - """Remove a network interface from a baremetal node""" + """Remove a network interface from a baremetal node.""" cs.baremetal.remove_interface(args.node, args.address) @utils.arg('node', metavar='', help="ID of node") def do_baremetal_interface_list(cs, args): - """List network interfaces associated with a baremetal node""" + """List network interfaces associated with a baremetal node.""" interfaces = cs.baremetal.list_interfaces(args.node) _print_baremetal_node_interfaces(interfaces) diff --git a/novaclient/v1_1/flavor_access.py b/novaclient/v1_1/flavor_access.py index 851a9db7e..b314040fe 100644 --- a/novaclient/v1_1/flavor_access.py +++ b/novaclient/v1_1/flavor_access.py @@ -42,7 +42,7 @@ def _list_by_flavor(self, flavor): 'flavor_access') def _list_by_tenant(self, tenant): - """Print flavor list shared with the given tenant""" + """Print flavor list shared with the given tenant.""" # TODO(uni): need to figure out a proper URI for list_by_tenant # since current API already provided current tenant_id information raise NotImplementedError('Sorry, query by tenant not supported.') diff --git a/novaclient/v1_1/floating_ip_dns.py b/novaclient/v1_1/floating_ip_dns.py index e45a174e1..f9c9ea4e8 100644 --- a/novaclient/v1_1/floating_ip_dns.py +++ b/novaclient/v1_1/floating_ip_dns.py @@ -74,7 +74,7 @@ def create_public(self, fqdomain, project): 'domain_entry') def delete(self, fqdomain): - """Delete the specified domain""" + """Delete the specified domain.""" self._delete("/os-floating-ip-dns/%s" % _quote_domain(fqdomain)) diff --git a/novaclient/v1_1/hosts.py b/novaclient/v1_1/hosts.py index 8ea563780..83b4c93db 100644 --- a/novaclient/v1_1/hosts.py +++ b/novaclient/v1_1/hosts.py @@ -57,7 +57,7 @@ def update(self, host, values): return self._update("/os-hosts/%s" % host, values) def host_action(self, host, action): - """Performs an action on a host.""" + """Perform an action on a host.""" body = {action: None} url = '/os-hosts/%s/action' % host return self.api.client.post(url, body=body) diff --git a/novaclient/v1_1/limits.py b/novaclient/v1_1/limits.py index 3eba65234..4b4eabc95 100644 --- a/novaclient/v1_1/limits.py +++ b/novaclient/v1_1/limits.py @@ -6,7 +6,7 @@ class Limits(base.Resource): - """A collection of RateLimit and AbsoluteLimit objects""" + """A collection of RateLimit and AbsoluteLimit objects.""" def __repr__(self): return "" @@ -28,7 +28,7 @@ def rate(self): class RateLimit(object): - """Data model that represents a flattened view of a single rate limit""" + """Data model that represents a flattened view of a single rate limit.""" def __init__(self, verb, uri, regex, value, remain, unit, next_available): @@ -54,7 +54,7 @@ def __repr__(self): class AbsoluteLimit(object): - """Data model that represents a single absolute limit""" + """Data model that represents a single absolute limit.""" def __init__(self, name, value): self.name = name @@ -68,7 +68,7 @@ def __repr__(self): class LimitsManager(base.Manager): - """Manager object used to interact with limits resource""" + """Manager object used to interact with limits resource.""" resource_class = Limits diff --git a/novaclient/v1_1/services.py b/novaclient/v1_1/services.py index ec580caf6..03424026e 100644 --- a/novaclient/v1_1/services.py +++ b/novaclient/v1_1/services.py @@ -51,16 +51,16 @@ def list(self, host=None, binary=None): return self._list(url, "services") def enable(self, host, binary): - """Enable the service specified by hostname and binary""" + """Enable the service specified by hostname and binary.""" body = {"host": host, "binary": binary} return self._update("/os-services/enable", body, "service") def disable(self, host, binary): - """Enable the service specified by hostname and binary""" + """Disable the service specified by hostname and binary.""" body = {"host": host, "binary": binary} return self._update("/os-services/disable", body, "service") def disable_log_reason(self, host, binary, reason): - """Disable the service with reason""" + """Disable the service with reason.""" body = {"host": host, "binary": binary, "disabled_reason": reason} return self._update("/os-services/disable-log-reason", body, "service") diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index bf48db324..2c3b00c03 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -341,14 +341,14 @@ def do_cloudpipe_list(cs, _args): @utils.arg('project', metavar='', help='Name of the project.') def do_cloudpipe_create(cs, args): - """Create a cloudpipe instance for the given project""" + """Create a cloudpipe instance for the given project.""" cs.cloudpipe.create(args.project) @utils.arg('address', metavar='', help='New IP Address.') @utils.arg('port', metavar='', help='New Port.') def do_cloudpipe_configure(cs, args): - """Update the VPN IP/port of a cloudpipe instance""" + """Update the VPN IP/port of a cloudpipe instance.""" cs.cloudpipe.update(args.address, args.port) @@ -627,7 +627,7 @@ def do_flavor_access_remove(cs, args): @utils.arg('project_id', metavar='', help='The ID of the project.') def do_scrub(cs, args): - """Deletes data associated with the project""" + """Delete data associated with the project.""" networks_list = cs.networks.list() networks_list = [network for network in networks_list if getattr(network, 'project_id', '') == args.project_id] @@ -1266,7 +1266,7 @@ def do_image_create(cs, args): @utils.arg('rotation', metavar='', help='Int parameter representing how many backups to keep around.') def do_backup(cs, args): - """Backup a instance by create a 'backup' type snapshot """ + """Backup a instance by create a 'backup' type snapshot.""" _find_server(cs, args.server).backup(args.name, args.backup_type, args.rotation) @@ -1789,7 +1789,7 @@ def do_floating_ip_pool_list(cs, _args): @utils.arg('--host', dest='host', metavar='', default=None, help='Filter by host') def do_floating_ip_bulk_list(cs, args): - """List all floating ips""" + """List all floating ips.""" utils.print_list(cs.floating_ips_bulk.list(args.host), ['project_id', 'address', 'instance_uuid', @@ -1803,13 +1803,13 @@ def do_floating_ip_bulk_list(cs, args): @utils.arg('--interface', metavar='', default=None, help='Interface for new Floating IPs') def do_floating_ip_bulk_create(cs, args): - """Bulk create floating ips by range""" + """Bulk create floating ips by range.""" cs.floating_ips_bulk.create(args.ip_range, args.pool, args.interface) @utils.arg('ip_range', metavar='', help='Address range to delete') def do_floating_ip_bulk_delete(cs, args): - """Bulk delete floating ips by range""" + """Bulk delete floating ips by range.""" cs.floating_ips_bulk.delete(args.ip_range) @@ -2143,7 +2143,7 @@ def do_secgroup_delete_group_rule(cs, args): @utils.arg('--pub_key', help=argparse.SUPPRESS) def do_keypair_add(cs, args): - """Create a new key pair for use with instances""" + """Create a new key pair for use with instances.""" name = args.name pub_key = args.pub_key @@ -2164,7 +2164,7 @@ def do_keypair_add(cs, args): @utils.arg('name', metavar='', help='Keypair name to delete.') def do_keypair_delete(cs, args): - """Delete keypair by its name""" + """Delete keypair given by its name.""" name = args.name cs.keypairs.delete(name) @@ -2224,7 +2224,7 @@ def do_rate_limits(cs, args): help='Usage range end date, ex 2012-01-20 (default: tomorrow) ', default=None) def do_usage_list(cs, args): - """List usage data for all tenants""" + """List usage data for all tenants.""" dateformat = "%Y-%m-%d" rows = ["Tenant ID", "Instances", "RAM MB-Hours", "CPU Hours", "Disk GB-Hours"] @@ -2271,7 +2271,7 @@ def simplify_usage(u): default=None, help='UUID or name of tenant to get usage for.') def do_usage(cs, args): - """Show usage data for a single tenant""" + """Show usage data for a single tenant.""" dateformat = "%Y-%m-%d" rows = ["Instances", "RAM MB-Hours", "CPU Hours", "Disk GB-Hours"] @@ -2321,7 +2321,7 @@ def simplify_usage(u): default='cert.pem', help='Filename for the X.509 certificate [Default: cert.pem]') def do_x509_create_cert(cs, args): - """Create x509 cert for a user in tenant""" + """Create x509 cert for a user in tenant.""" if os.path.exists(args.pk_filename): raise exceptions.CommandError("Unable to write privatekey - %s exists." @@ -2351,7 +2351,7 @@ def do_x509_create_cert(cs, args): default='cacert.pem', help='Filename to write the x509 root cert.') def do_x509_get_root_cert(cs, args): - """Fetches the x509 root cert.""" + """Fetch the x509 root cert.""" if os.path.exists(args.filename): raise exceptions.CommandError("Unable to write x509 root cert - \ %s exists." % args.filename) @@ -2365,7 +2365,7 @@ def do_x509_get_root_cert(cs, args): @utils.arg('--hypervisor', metavar='', default=None, help='type of hypervisor.') def do_agent_list(cs, args): - """List all builds""" + """List all builds.""" result = cs.agents.list(args.hypervisor) columns = ["Agent_id", "Hypervisor", "OS", "Architecture", "Version", 'Md5hash', 'Url'] @@ -2381,7 +2381,7 @@ def do_agent_list(cs, args): @utils.arg('hypervisor', metavar='', default='xen', help='type of hypervisor.') def do_agent_create(cs, args): - """Creates a new agent build.""" + """Create new agent build.""" result = cs.agents.create(args.os, args.architecture, args.version, args.url, args.md5hash, args.hypervisor) @@ -2390,7 +2390,7 @@ def do_agent_create(cs, args): @utils.arg('id', metavar='', help='id of the agent-build') def do_agent_delete(cs, args): - """Deletes an existing agent build.""" + """Delete existing agent build.""" cs.agents.delete(args.id) @@ -2399,7 +2399,7 @@ def do_agent_delete(cs, args): @utils.arg('url', metavar='', help='url') @utils.arg('md5hash', metavar='', help='md5hash') def do_agent_modify(cs, args): - """Modify an existing agent build.""" + """Modify existing agent build.""" result = cs.agents.update(args.id, args.version, args.url, args.md5hash) utils.print_dict(result._info) @@ -2515,7 +2515,7 @@ def _print_aggregate_details(aggregate): action='store_true', help=argparse.SUPPRESS) def do_live_migration(cs, args): - """Migrates a running instance to a new machine.""" + """Migrate running instance to a new machine.""" _find_server(cs, args.server).live_migrate(args.host, args.block_migrate, args.disk_over_commit) @@ -2527,7 +2527,7 @@ def do_live_migration(cs, args): help='Request the instance be reset to "active" state instead ' 'of "error" state (the default).') def do_reset_state(cs, args): - """Reset the state of an instance""" + """Reset the state of an instance.""" _find_server(cs, args.server).reset_state(args.state) @@ -2555,7 +2555,7 @@ def do_service_list(cs, args): @utils.arg('host', metavar='', help='Name of host.') @utils.arg('binary', metavar='', help='Service binary.') def do_service_enable(cs, args): - """Enable the service""" + """Enable the service.""" result = cs.services.enable(args.host, args.binary) utils.print_list([result], ['Host', 'Binary', 'Status']) @@ -2565,7 +2565,7 @@ def do_service_enable(cs, args): @utils.arg('--reason', metavar='', help='Reason for disabling service.') def do_service_disable(cs, args): - """Disable the service""" + """Disable the service.""" if args.reason: result = cs.services.disable_log_reason(args.host, args.binary, args.reason) @@ -2578,26 +2578,26 @@ def do_service_disable(cs, args): @utils.arg('fixed_ip', metavar='', help='Fixed IP Address.') def do_fixed_ip_get(cs, args): - """Get info on a fixed ip""" + """Retrieve info on a fixed ip.""" result = cs.fixed_ips.get(args.fixed_ip) utils.print_list([result], ['address', 'cidr', 'hostname', 'host']) @utils.arg('fixed_ip', metavar='', help='Fixed IP Address.') def do_fixed_ip_reserve(cs, args): - """Reserve a fixed ip""" + """Reserve a fixed IP.""" cs.fixed_ips.reserve(args.fixed_ip) @utils.arg('fixed_ip', metavar='', help='Fixed IP Address.') def do_fixed_ip_unreserve(cs, args): - """Unreserve a fixed ip""" + """Unreserve a fixed IP.""" cs.fixed_ips.unreserve(args.fixed_ip) @utils.arg('host', metavar='', help='Name of host.') def do_host_describe(cs, args): - """Describe a specific host""" + """Describe a specific host.""" result = cs.hosts.get(args.host) columns = ["HOST", "PROJECT", "cpu", "memory_mb", "disk_gb"] utils.print_list(result, columns) @@ -2607,7 +2607,7 @@ def do_host_describe(cs, args): help='Filters the list, returning only those ' 'hosts in the availability zone .') def do_host_list(cs, args): - """List all hosts by service""" + """List all hosts by service.""" columns = ["host_name", "service", "zone"] result = cs.hosts.list(args.zone) utils.print_list(result, columns) @@ -2651,13 +2651,13 @@ def do_host_action(cs, args): default=False, help='Generate a single report for all services.') def do_coverage_start(cs, args): - """Start Nova coverage reporting""" + """Start Nova coverage reporting.""" cs.coverage.start(combine=args.combine) print("Coverage collection started") def do_coverage_stop(cs, args): - """Stop Nova coverage reporting""" + """Stop Nova coverage reporting.""" out = cs.coverage.stop() print("Coverage data file path: %s" % out[-1]['path']) @@ -2674,7 +2674,7 @@ def do_coverage_stop(cs, args): default=False, help='Generate XML reports instead of text ones.') def do_coverage_report(cs, args): - """Generate a coverage report""" + """Generate coverage report.""" if args.html == True and args.xml == True: raise exceptions.CommandError("--html and --xml must not be " "specified together.") @@ -2770,7 +2770,7 @@ def ensure_service_catalog_present(cs): def do_endpoints(cs, _args): - """Discover endpoints that get returned from the authenticate services""" + """Discover endpoints that get returned from the authenticate services.""" ensure_service_catalog_present(cs) catalog = cs.client.service_catalog.catalog for e in catalog['access']['serviceCatalog']: @@ -2780,7 +2780,7 @@ def do_endpoints(cs, _args): @utils.arg('--wrap', dest='wrap', metavar='', default=64, help='wrap PKI tokens to a specified length, or 0 to disable') def do_credentials(cs, _args): - """Show user credentials returned from auth""" + """Show user credentials returned from auth.""" ensure_service_catalog_present(cs) catalog = cs.client.service_catalog.catalog utils.print_dict(catalog['access']['user'], "User Credentials", @@ -3172,7 +3172,7 @@ def do_interface_detach(cs, args): def _treeizeAvailabilityZone(zone): - """Build a tree view for availability zones""" + """Build a tree view for availability zones.""" AvailabilityZone = availability_zones.AvailabilityZone az = AvailabilityZone(zone.manager, diff --git a/tox.ini b/tox.ini index b035bb751..b10c1603f 100644 --- a/tox.ini +++ b/tox.ini @@ -24,6 +24,6 @@ commands = python setup.py testr --coverage --testr-args='{posargs}' downloadcache = ~/cache/pip [flake8] -ignore = E12,E711,E721,E712,F841,F811,F821,H302,H306,H402,H403,H404 +ignore = E12,E711,E721,E712,F841,F811,F821,H302,H306,H403,H404 show-source = True exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build From f702c25d4364583d6532bf11b2cce1107994bcce Mon Sep 17 00:00:00 2001 From: Julie Pichon Date: Fri, 12 Jul 2013 08:27:28 +0100 Subject: [PATCH 0212/1705] Fix backwards-incompatible API change (method signature) project_id was changed to projectid in a previous changeset. Fixes bug 1203001 Change-Id: If9c21ae4f3cfbeb89569f6e4bd415c2041dc6294 --- novaclient/v1_1/client.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/novaclient/v1_1/client.py b/novaclient/v1_1/client.py index 0c8f8fccb..50ab46cff 100644 --- a/novaclient/v1_1/client.py +++ b/novaclient/v1_1/client.py @@ -65,8 +65,8 @@ class Client(object): """ - # FIXME(jesse): projectid isn't required to authenticate - def __init__(self, username, api_key, projectid, auth_url=None, + # FIXME(jesse): project_id isn't required to authenticate + def __init__(self, username, api_key, project_id, auth_url=None, insecure=False, timeout=None, proxy_tenant_id=None, proxy_token=None, region_name=None, endpoint_type='publicURL', extensions=None, @@ -79,7 +79,7 @@ def __init__(self, username, api_key, projectid, auth_url=None, # FIXME(comstud): Rename the api_key argument above when we # know it's not being used as keyword argument password = api_key - self.projectid = projectid + self.projectid = project_id self.tenant_id = tenant_id self.flavors = flavors.FlavorManager(self) self.flavor_access = flavor_access.FlavorAccessManager(self) @@ -129,7 +129,7 @@ def __init__(self, username, api_key, projectid, auth_url=None, self.client = client.HTTPClient(username, password, - projectid=projectid, + projectid=project_id, tenant_id=tenant_id, auth_url=auth_url, insecure=insecure, From 1b5ed91650c7797fea0701112f95413183fef321 Mon Sep 17 00:00:00 2001 From: Jiajun Liu Date: Tue, 16 Jul 2013 09:35:42 +0000 Subject: [PATCH 0213/1705] recognize 429 as an rate limiting status currently novaclient recognize 413 as rate limiting status while it shoule be 429 according to HTTP protocol. fixes bug 1191874 Change-Id: Ib1ae54f7d13d0ca579dd264e8d0d7630770e92d6 --- novaclient/exceptions.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/novaclient/exceptions.py b/novaclient/exceptions.py index afea93326..727638bdc 100644 --- a/novaclient/exceptions.py +++ b/novaclient/exceptions.py @@ -148,6 +148,14 @@ def __init__(self, *args, **kwargs): super(OverLimit, self).__init__(*args, **kwargs) +class RateLimit(OverLimit): + """ + HTTP 429 - Rate limit: you've sent too many requests for this time period. + """ + http_status = 429 + message = "Rate limit" + + # NotImplemented is a python keyword. class HTTPNotImplemented(ClientException): """ @@ -164,7 +172,8 @@ class HTTPNotImplemented(ClientException): # # Instead, we have to hardcode it: _error_classes = [BadRequest, Unauthorized, Forbidden, NotFound, - MethodNotAllowed, Conflict, OverLimit, HTTPNotImplemented] + MethodNotAllowed, Conflict, OverLimit, RateLimit, + HTTPNotImplemented] _code_map = dict((c.http_status, c) for c in _error_classes) From de250bf170517ae187b14c4c19912964631b4a0e Mon Sep 17 00:00:00 2001 From: Ken'ichi Ohmichi Date: Fri, 19 Jul 2013 12:29:49 +0900 Subject: [PATCH 0214/1705] Add name argument to hypervisor commands Fixes #Bug 1202920 This patch adds name arguments to the following subcommands: * hypervisor-show * hypervisor-uptime Change-Id: I9adfb699775d2d8f8ca45a7a28621e634bc03055 --- novaclient/tests/v1_1/test_shell.py | 12 ++++++++++-- novaclient/v1_1/shell.py | 22 ++++++++++++++-------- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index d0e064865..faa635f3a 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -1126,14 +1126,22 @@ def test_hypervisor_servers(self): self.run_command('hypervisor-servers hyper') self.assert_called('GET', '/os-hypervisors/hyper/servers') - def test_hypervisor_show(self): + def test_hypervisor_show_by_id(self): self.run_command('hypervisor-show 1234') self.assert_called('GET', '/os-hypervisors/1234') - def test_hypervisor_uptime(self): + def test_hypervisor_show_by_name(self): + self.run_command('hypervisor-show hyper1') + self.assert_called('GET', '/os-hypervisors/detail') + + def test_hypervisor_uptime_by_id(self): self.run_command('hypervisor-uptime 1234') self.assert_called('GET', '/os-hypervisors/1234/uptime') + def test_hypervisor_uptime_by_name(self): + self.run_command('hypervisor-uptime hyper1') + self.assert_called('GET', '/os-hypervisors/1234/uptime') + def test_hypervisor_stats(self): self.run_command('hypervisor-stats') self.assert_called('GET', '/os-hypervisors/statistics') diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 2c86bc9a9..a9f2380d5 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2688,6 +2688,11 @@ def do_coverage_reset(cs, args): print("Coverage data reset") +def _find_hypervisor(cs, hypervisor): + """Get a hypervisor by name or ID.""" + return utils.find_resource(cs.hypervisors, hypervisor) + + @utils.arg('--matching', metavar='', default=None, help='List hypervisors matching the given .') def do_hypervisor_list(cs, args): @@ -2728,12 +2733,12 @@ def __init__(self, **kwargs): 'Hypervisor Hostname']) -@utils.arg('hypervisor_id', - metavar='', - help='The ID of the hypervisor to show the details of.') +@utils.arg('hypervisor', + metavar='', + help='Name or ID of the hypervisor to show the details of.') def do_hypervisor_show(cs, args): """Display the details of the specified hypervisor.""" - hyper = utils.find_resource(cs.hypervisors, args.hypervisor_id) + hyper = _find_hypervisor(cs, args.hypervisor) # Build up the dict info = hyper._info.copy() @@ -2744,12 +2749,13 @@ def do_hypervisor_show(cs, args): utils.print_dict(info) -@utils.arg('hypervisor_id', - metavar='', - help='The ID of the hypervisor to show the uptime of.') +@utils.arg('hypervisor', + metavar='', + help='Name or ID of the hypervisor to show the uptime of.') def do_hypervisor_uptime(cs, args): """Display the uptime of the specified hypervisor.""" - hyper = cs.hypervisors.uptime(args.hypervisor_id) + hyper = _find_hypervisor(cs, args.hypervisor) + hyper = cs.hypervisors.uptime(hyper) # Output the uptime information utils.print_dict(hyper._info.copy()) From 4025b7e5e3642792d55128bddc96491dc34e0434 Mon Sep 17 00:00:00 2001 From: Ken'ichi Ohmichi Date: Fri, 19 Jul 2013 09:58:52 +0900 Subject: [PATCH 0215/1705] Add name argument to aggregate commands Fixes #Bug 1202901 This patch adds name arguments to the following subcommands: * aggregate-update * aggregate-delete * aggregate-set-metadata * aggregate-add-host * aggregate-remove-host * aggregate-details Change-Id: I93f44a12b6d5a91b448f6f8d238311d58bf40c01 --- novaclient/tests/v1_1/test_shell.py | 53 +++++++++++++++++++++++++---- novaclient/v1_1/shell.py | 50 +++++++++++++++++---------- 2 files changed, 78 insertions(+), 25 deletions(-) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index d0e064865..7dbe9d5f4 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -865,45 +865,84 @@ def test_aggregate_create(self): self.assert_called('POST', '/os-aggregates', body, pos=-2) self.assert_called('GET', '/os-aggregates/1', pos=-1) - def test_aggregate_delete(self): + def test_aggregate_delete_by_id(self): self.run_command('aggregate-delete 1') self.assert_called('DELETE', '/os-aggregates/1') - def test_aggregate_update(self): + def test_aggregate_delete_by_name(self): + self.run_command('aggregate-delete test') + self.assert_called('DELETE', '/os-aggregates/1') + + def test_aggregate_update_by_id(self): self.run_command('aggregate-update 1 new_name') body = {"aggregate": {"name": "new_name"}} self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) self.assert_called('GET', '/os-aggregates/1', pos=-1) - def test_aggregate_update_with_availability_zone(self): + def test_aggregate_update_by_name(self): + self.run_command('aggregate-update test new_name') + body = {"aggregate": {"name": "new_name"}} + self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) + self.assert_called('GET', '/os-aggregates/1', pos=-1) + + def test_aggregate_update_with_availability_zone_by_id(self): self.run_command('aggregate-update 1 foo new_zone') body = {"aggregate": {"name": "foo", "availability_zone": "new_zone"}} self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) self.assert_called('GET', '/os-aggregates/1', pos=-1) - def test_aggregate_set_metadata(self): + def test_aggregate_update_with_availability_zone_by_name(self): + self.run_command('aggregate-update test foo new_zone') + body = {"aggregate": {"name": "foo", "availability_zone": "new_zone"}} + self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) + self.assert_called('GET', '/os-aggregates/1', pos=-1) + + def test_aggregate_set_metadata_by_id(self): self.run_command('aggregate-set-metadata 1 foo=bar delete_key') body = {"set_metadata": {"metadata": {"foo": "bar", "delete_key": None}}} self.assert_called('POST', '/os-aggregates/1/action', body, pos=-2) self.assert_called('GET', '/os-aggregates/1', pos=-1) - def test_aggregate_add_host(self): + def test_aggregate_set_metadata_by_name(self): + self.run_command('aggregate-set-metadata test foo=bar delete_key') + body = {"set_metadata": {"metadata": {"foo": "bar", + "delete_key": None}}} + self.assert_called('POST', '/os-aggregates/1/action', body, pos=-2) + self.assert_called('GET', '/os-aggregates/1', pos=-1) + + def test_aggregate_add_host_by_id(self): self.run_command('aggregate-add-host 1 host1') body = {"add_host": {"host": "host1"}} self.assert_called('POST', '/os-aggregates/1/action', body, pos=-2) self.assert_called('GET', '/os-aggregates/1', pos=-1) - def test_aggregate_remove_host(self): + def test_aggregate_add_host_by_name(self): + self.run_command('aggregate-add-host test host1') + body = {"add_host": {"host": "host1"}} + self.assert_called('POST', '/os-aggregates/1/action', body, pos=-2) + self.assert_called('GET', '/os-aggregates/1', pos=-1) + + def test_aggregate_remove_host_by_id(self): self.run_command('aggregate-remove-host 1 host1') body = {"remove_host": {"host": "host1"}} self.assert_called('POST', '/os-aggregates/1/action', body, pos=-2) self.assert_called('GET', '/os-aggregates/1', pos=-1) - def test_aggregate_details(self): + def test_aggregate_remove_host_by_name(self): + self.run_command('aggregate-remove-host test host1') + body = {"remove_host": {"host": "host1"}} + self.assert_called('POST', '/os-aggregates/1/action', body, pos=-2) + self.assert_called('GET', '/os-aggregates/1', pos=-1) + + def test_aggregate_details_by_id(self): self.run_command('aggregate-details 1') self.assert_called('GET', '/os-aggregates/1') + def test_aggregate_details_by_name(self): + self.run_command('aggregate-details test') + self.assert_called('GET', '/os-aggregates') + def test_live_migration(self): self.run_command('live-migration sample-server hostname') self.assert_called('POST', '/servers/1234/action', diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 2c86bc9a9..daeae30ea 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2405,6 +2405,11 @@ def do_agent_modify(cs, args): utils.print_dict(result._info) +def _find_aggregate(cs, aggregate): + """Get a aggregate by name or ID.""" + return utils.find_resource(cs.aggregates, aggregate) + + def do_aggregate_list(cs, args): """Print a list of all aggregates.""" aggregates = cs.aggregates.list() @@ -2424,14 +2429,17 @@ def do_aggregate_create(cs, args): _print_aggregate_details(aggregate) -@utils.arg('id', metavar='', help='Aggregate id to delete.') +@utils.arg('aggregate', metavar='', + help='Name or ID of aggregate to delete.') def do_aggregate_delete(cs, args): - """Delete the aggregate by its id.""" - cs.aggregates.delete(args.id) - print("Aggregate %s has been successfully deleted." % args.id) + """Delete the aggregate.""" + aggregate = _find_aggregate(cs, args.aggregate) + cs.aggregates.delete(aggregate) + print("Aggregate %s has been successfully deleted." % aggregate.id) -@utils.arg('id', metavar='', help='Aggregate id to update.') +@utils.arg('aggregate', metavar='', + help='Name or ID of aggregate to update.') @utils.arg('name', metavar='', help='Name of aggregate.') @utils.arg('availability_zone', metavar='', @@ -2440,16 +2448,18 @@ def do_aggregate_delete(cs, args): help='The availability zone of the aggregate.') def do_aggregate_update(cs, args): """Update the aggregate's name and optionally availability zone.""" + aggregate = _find_aggregate(cs, args.aggregate) updates = {"name": args.name} if args.availability_zone: updates["availability_zone"] = args.availability_zone - aggregate = cs.aggregates.update(args.id, updates) - print("Aggregate %s has been successfully updated." % args.id) + aggregate = cs.aggregates.update(aggregate.id, updates) + print("Aggregate %s has been successfully updated." % aggregate.id) _print_aggregate_details(aggregate) -@utils.arg('id', metavar='', help='Aggregate id to update.') +@utils.arg('aggregate', metavar='', + help='Name or ID of aggregate to update.') @utils.arg('metadata', metavar='', nargs='+', @@ -2458,35 +2468,39 @@ def do_aggregate_update(cs, args): help='Metadata to add/update to aggregate') def do_aggregate_set_metadata(cs, args): """Update the metadata associated with the aggregate.""" + aggregate = _find_aggregate(cs, args.aggregate) metadata = _extract_metadata(args) - aggregate = cs.aggregates.set_metadata(args.id, metadata) - print("Aggregate %s has been successfully updated." % args.id) + aggregate = cs.aggregates.set_metadata(aggregate.id, metadata) + print("Aggregate %s has been successfully updated." % aggregate.id) _print_aggregate_details(aggregate) -@utils.arg('id', metavar='', help='Aggregate id.') +@utils.arg('aggregate', metavar='', help='Name or ID of aggregate.') @utils.arg('host', metavar='', help='The host to add to the aggregate.') def do_aggregate_add_host(cs, args): """Add the host to the specified aggregate.""" - aggregate = cs.aggregates.add_host(args.id, args.host) - print("Aggregate %s has been successfully updated." % args.id) + aggregate = _find_aggregate(cs, args.aggregate) + aggregate = cs.aggregates.add_host(aggregate.id, args.host) + print("Aggregate %s has been successfully updated." % aggregate.id) _print_aggregate_details(aggregate) -@utils.arg('id', metavar='', help='Aggregate id.') +@utils.arg('aggregate', metavar='', help='Name or ID of aggregate.') @utils.arg('host', metavar='', help='The host to remove from the aggregate.') def do_aggregate_remove_host(cs, args): """Remove the specified host from the specified aggregate.""" - aggregate = cs.aggregates.remove_host(args.id, args.host) - print("Aggregate %s has been successfully updated." % args.id) + aggregate = _find_aggregate(cs, args.aggregate) + aggregate = cs.aggregates.remove_host(aggregate.id, args.host) + print("Aggregate %s has been successfully updated." % aggregate.id) _print_aggregate_details(aggregate) -@utils.arg('id', metavar='', help='Aggregate id.') +@utils.arg('aggregate', metavar='', help='Name or ID of aggregate.') def do_aggregate_details(cs, args): """Show details of the specified aggregate.""" - _print_aggregate_details(cs.aggregates.get(args.id)) + aggregate = _find_aggregate(cs, args.aggregate) + _print_aggregate_details(aggregate) def _print_aggregate_details(aggregate): From cc2c0a8e6f853f16688fb7e4ec0217b34adbec92 Mon Sep 17 00:00:00 2001 From: Yufang Zhang Date: Fri, 28 Jun 2013 19:30:10 +0800 Subject: [PATCH 0216/1705] Enable force_delete and restore instance via novaclient. Bug 1195670 Nova supports force_delete or restore instance in the API level, thus it makes sense to support this feature in novaclient. Change-Id: I7cc3d2d2a7ab8dfe043176a3ea97c10deae683c9 --- novaclient/tests/v1_1/fakes.py | 4 ++++ novaclient/tests/v1_1/test_servers.py | 14 +++++++++++ novaclient/tests/v1_1/test_shell.py | 14 +++++++++++ novaclient/v1_1/contrib/deferred_delete.py | 27 ++++++++++++++++++++++ novaclient/v1_1/servers.py | 24 +++++++++++++++++++ 5 files changed, 83 insertions(+) create mode 100644 novaclient/v1_1/contrib/deferred_delete.py diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index 52e83ced2..cc24c7d42 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -489,6 +489,10 @@ def post_servers_1234_action(self, body, **kw): assert body[action] is None elif action == 'os-start': assert body[action] is None + elif action == 'forceDelete': + assert body[action] is None + elif action == 'restore': + assert body[action] is None elif action == 'pause': assert body[action] is None elif action == 'unpause': diff --git a/novaclient/tests/v1_1/test_servers.py b/novaclient/tests/v1_1/test_servers.py index 04db70529..6fec98bac 100644 --- a/novaclient/tests/v1_1/test_servers.py +++ b/novaclient/tests/v1_1/test_servers.py @@ -284,6 +284,20 @@ def test_stop(self): cs.servers.stop(s) cs.assert_called('POST', '/servers/1234/action') + def test_force_delete(self): + s = cs.servers.get(1234) + s.force_delete() + cs.assert_called('POST', '/servers/1234/action') + cs.servers.force_delete(s) + cs.assert_called('POST', '/servers/1234/action') + + def test_restore(self): + s = cs.servers.get(1234) + s.restore() + cs.assert_called('POST', '/servers/1234/action') + cs.servers.restore(s) + cs.assert_called('POST', '/servers/1234/action') + def test_start(self): s = cs.servers.get(1234) s.start() diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index d0e064865..a1f87a983 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -650,6 +650,20 @@ def test_delete(self): self.run_command('delete sample-server') self.assert_called('DELETE', '/servers/1234') + def test_force_delete(self): + self.run_command('force-delete 1234') + self.assert_called('POST', '/servers/1234/action', + {'forceDelete': None}) + self.run_command('force-delete sample-server') + self.assert_called('POST', '/servers/1234/action', + {'forceDelete': None}) + + def test_restore(self): + self.run_command('restore 1234') + self.assert_called('POST', '/servers/1234/action', {'restore': None}) + self.run_command('restore sample-server') + self.assert_called('POST', '/servers/1234/action', {'restore': None}) + def test_delete_two_with_two_existent(self): self.run_command('delete 1234 5678') self.assert_called('DELETE', '/servers/1234', pos=-3) diff --git a/novaclient/v1_1/contrib/deferred_delete.py b/novaclient/v1_1/contrib/deferred_delete.py new file mode 100644 index 000000000..1412702d5 --- /dev/null +++ b/novaclient/v1_1/contrib/deferred_delete.py @@ -0,0 +1,27 @@ +# Copyright 2013 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from novaclient import utils + + +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_force_delete(cs, args): + """Force delete a server.""" + utils.find_resource(cs.servers, args.server).force_delete() + + +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_restore(cs, args): + """Restore a soft-deleted server.""" + utils.find_resource(cs.servers, args.server).restore() diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index 02a7aa338..b63fa09da 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -123,6 +123,18 @@ def stop(self): """ self.manager.stop(self) + def force_delete(self): + """ + Force delete -- Force delete a server. + """ + self.manager.force_delete(self) + + def restore(self): + """ + Restore -- Restore a server in 'soft-deleted' state. + """ + self.manager.restore(self) + def start(self): """ Start -- Start the paused server. @@ -483,6 +495,18 @@ def stop(self, server): """ return self._action('os-stop', server, None) + def force_delete(self, server): + """ + Force delete the server. + """ + return self._action('forceDelete', server, None) + + def restore(self, server): + """ + Restore soft-deleted server. + """ + return self._action('restore', server, None) + def start(self, server): """ Start the server. From 3f0312eabcfcf0c54a7ea0b6075b7292515f15c5 Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Wed, 24 Jul 2013 13:47:41 -0400 Subject: [PATCH 0217/1705] Remove python 2.4 and python 2.5 support. As a project we do not support python 2.4 or python 2.5 and these versions of python are not tested, so remove the compatibility code. Change-Id: I267d9e7e83d85322c45d56d9c5256b514224ad8c Signed-off-by: Chuck Short --- novaclient/base.py | 8 -------- novaclient/client.py | 5 ----- 2 files changed, 13 deletions(-) diff --git a/novaclient/base.py b/novaclient/base.py index 3cbc65ed5..c37fa3486 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -30,14 +30,6 @@ from novaclient import utils -# Python 2.4 compat -try: - all -except NameError: - def all(iterable): - return True not in (not x for x in iterable) - - def getid(obj): """ Abstracts the common pattern of allowing both an object or an object's ID diff --git a/novaclient/client.py b/novaclient/client.py index 94428315b..ce0e8495e 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -19,11 +19,6 @@ except ImportError: import simplejson as json -# Python 2.5 compat fix -if not hasattr(urlparse, 'parse_qsl'): - import cgi - urlparse.parse_qsl = cgi.parse_qsl - from novaclient import exceptions from novaclient import service_catalog from novaclient import utils From 0d061f0aeb1edaca66fd4e4b633d84b6a018d4b2 Mon Sep 17 00:00:00 2001 From: Xavier Queralt Date: Fri, 26 Jul 2013 08:32:34 +0200 Subject: [PATCH 0218/1705] Merge v1_1's base module into main base module This class was forked from novaclient.base when adding v1.1 support and and because v1.0 is no longer supported it can be merged again. blueprint: improve-block-device-handling Change-Id: I3113eff522a9dc280f48053001afa9e1a0cad3e3 --- novaclient/base.py | 95 +++++++++++++++++++-- novaclient/v1_1/base.py | 164 ------------------------------------- novaclient/v1_1/servers.py | 3 +- 3 files changed, 88 insertions(+), 174 deletions(-) delete mode 100644 novaclient/v1_1/base.py diff --git a/novaclient/base.py b/novaclient/base.py index c37fa3486..ba44361b3 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -20,6 +20,7 @@ """ import abc +import base64 import contextlib import hashlib import os @@ -27,6 +28,7 @@ import six from novaclient import exceptions +from novaclient.openstack.common import strutils from novaclient import utils @@ -211,16 +213,18 @@ def findall(self, **kwargs): class BootingManagerWithFind(ManagerWithFind): """Like a `ManagerWithFind`, but has the ability to boot servers.""" def _boot(self, resource_url, response_key, name, image, flavor, - ipgroup=None, meta=None, files=None, + meta=None, files=None, userdata=None, reservation_id=None, return_raw=False, min_count=None, - max_count=None, **kwargs): + max_count=None, security_groups=None, key_name=None, + availability_zone=None, block_device_mapping=None, nics=None, + scheduler_hints=None, config_drive=None, admin_pass=None, + **kwargs): """ Create (boot) a new server. :param name: Something to name the server. :param image: The :class:`Image` to boot with. :param flavor: The :class:`Flavor` to boot onto. - :param ipgroup: An initial :class:`IPGroup` for this server. :param meta: A dict of arbitrary key/value metadata to store for this server. A maximum of five entries is allowed, and both keys and values must be 255 characters or less. @@ -232,19 +236,45 @@ def _boot(self, resource_url, response_key, name, image, flavor, :param reservation_id: a UUID for the set of servers being requested. :param return_raw: If True, don't try to coearse the result into a Resource object. + :param security_groups: list of security group names + :param key_name: (optional extension) name of keypair to inject into + the instance + :param availability_zone: Name of the availability zone for instance + placement. + :param block_device_mapping: A dict of block device mappings for this + server. + :param nics: (optional extension) an ordered list of nics to be + added to this server, with information about + connected networks, fixed ips, etc. + :param scheduler_hints: (optional extension) arbitrary key-value pairs + specified by the client to help boot an instance. + :param config_drive: (optional extension) value for config drive + either boolean, or volume-id + :param admin_pass: admin password for the server. """ body = {"server": { "name": name, - "imageId": getid(image), - "flavorId": getid(flavor), + "imageRef": str(getid(image)) if image else '', + "flavorRef": str(getid(flavor)), }} - if ipgroup: - body["server"]["sharedIpGroupId"] = getid(ipgroup) + if userdata: + if hasattr(userdata, 'read'): + userdata = userdata.read() + + userdata = strutils.safe_encode(userdata) + body["server"]["user_data"] = base64.b64encode(userdata) if meta: body["server"]["metadata"] = meta if reservation_id: body["server"]["reservation_id"] = reservation_id - + if key_name: + body["server"]["key_name"] = key_name + if scheduler_hints: + body['os:scheduler_hints'] = scheduler_hints + if config_drive: + body["server"]["config_drive"] = config_drive + if admin_pass: + body["server"]["adminPass"] = admin_pass if not min_count: min_count = 1 if not max_count: @@ -252,6 +282,10 @@ def _boot(self, resource_url, response_key, name, image, flavor, body["server"]["min_count"] = min_count body["server"]["max_count"] = max_count + if security_groups: + body["server"]["security_groups"] =\ + [{'name': sg} for sg in security_groups] + # Files are a slight bit tricky. They're passed in a "personality" # list to the POST. Each item is a dict giving a file name and the # base64-encoded contents of the file. We want to allow passing @@ -268,6 +302,51 @@ def _boot(self, resource_url, response_key, name, image, flavor, 'contents': data.encode('base64'), }) + if availability_zone: + body["server"]["availability_zone"] = availability_zone + + # Block device mappings are passed as a list of dictionaries + if block_device_mapping: + bdm = body['server']['block_device_mapping'] = [] + for device_name, mapping in block_device_mapping.items(): + # + # The mapping is in the format: + # :[]:[]:[] + # + bdm_dict = {'device_name': device_name} + + mapping_parts = mapping.split(':') + id = mapping_parts[0] + if len(mapping_parts) == 1: + bdm_dict['volume_id'] = id + if len(mapping_parts) > 1: + type = mapping_parts[1] + if type.startswith('snap'): + bdm_dict['snapshot_id'] = id + else: + bdm_dict['volume_id'] = id + if len(mapping_parts) > 2: + if mapping_parts[2]: + bdm_dict['volume_size'] = str(int(mapping_parts[2])) + if len(mapping_parts) > 3: + bdm_dict['delete_on_termination'] = mapping_parts[3] + bdm.append(bdm_dict) + + if nics is not None: + # NOTE(tr3buchet): nics can be an empty list + all_net_data = [] + for nic_info in nics: + net_data = {} + # if value is empty string, do not send value in body + if nic_info.get('net-id'): + net_data['uuid'] = nic_info['net-id'] + if nic_info.get('v4-fixed-ip'): + net_data['fixed_ip'] = nic_info['v4-fixed-ip'] + if nic_info.get('port-id'): + net_data['port'] = nic_info['port-id'] + all_net_data.append(net_data) + body['server']['networks'] = all_net_data + return self._create(resource_url, body, response_key, return_raw=return_raw, **kwargs) diff --git a/novaclient/v1_1/base.py b/novaclient/v1_1/base.py deleted file mode 100644 index e8d611aee..000000000 --- a/novaclient/v1_1/base.py +++ /dev/null @@ -1,164 +0,0 @@ -# Copyright 2010 Jacob Kaplan-Moss - -# Copyright 2011 OpenStack Foundation -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import base64 - -from novaclient import base -from novaclient.openstack.common import strutils - - -# FIXME(sirp): Now that v1_0 has been removed, this can be merged with -# base.ManagerWithFind -class BootingManagerWithFind(base.ManagerWithFind): - """Like a `ManagerWithFind`, but has the ability to boot servers.""" - def _boot(self, resource_url, response_key, name, image, flavor, - meta=None, files=None, userdata=None, - reservation_id=None, return_raw=False, min_count=None, - max_count=None, security_groups=None, key_name=None, - availability_zone=None, block_device_mapping=None, nics=None, - scheduler_hints=None, config_drive=None, admin_pass=None, - **kwargs): - """ - Create (boot) a new server. - - :param name: Something to name the server. - :param image: The :class:`Image` to boot with. - :param flavor: The :class:`Flavor` to boot onto. - :param meta: A dict of arbitrary key/value metadata to store for this - server. A maximum of five entries is allowed, and both - keys and values must be 255 characters or less. - :param files: A dict of files to overrwrite on the server upon boot. - Keys are file names (i.e. ``/etc/passwd``) and values - are the file contents (either as a string or as a - file-like object). A maximum of five entries is allowed, - and each file must be 10k or less. - :param reservation_id: a UUID for the set of servers being requested. - :param return_raw: If True, don't try to coearse the result into - a Resource object. - :param security_groups: list of security group names - :param key_name: (optional extension) name of keypair to inject into - the instance - :param availability_zone: Name of the availability zone for instance - placement. - :param block_device_mapping: A dict of block device mappings for this - server. - :param nics: (optional extension) an ordered list of nics to be - added to this server, with information about - connected networks, fixed ips, etc. - :param scheduler_hints: (optional extension) arbitrary key-value pairs - specified by the client to help boot an instance. - :param config_drive: (optional extension) value for config drive - either boolean, or volume-id - :param admin_pass: admin password for the server. - """ - body = {"server": { - "name": name, - "imageRef": str(base.getid(image)) if image else '', - "flavorRef": str(base.getid(flavor)), - }} - if userdata: - if hasattr(userdata, 'read'): - userdata = userdata.read() - - userdata = strutils.safe_encode(userdata) - body["server"]["user_data"] = base64.b64encode(userdata) - if meta: - body["server"]["metadata"] = meta - if reservation_id: - body["server"]["reservation_id"] = reservation_id - if key_name: - body["server"]["key_name"] = key_name - if scheduler_hints: - body['os:scheduler_hints'] = scheduler_hints - if config_drive: - body["server"]["config_drive"] = config_drive - if admin_pass: - body["server"]["adminPass"] = admin_pass - if not min_count: - min_count = 1 - if not max_count: - max_count = min_count - body["server"]["min_count"] = min_count - body["server"]["max_count"] = max_count - - if security_groups: - body["server"]["security_groups"] =\ - [{'name': sg} for sg in security_groups] - - # Files are a slight bit tricky. They're passed in a "personality" - # list to the POST. Each item is a dict giving a file name and the - # base64-encoded contents of the file. We want to allow passing - # either an open file *or* some contents as files here. - if files: - personality = body['server']['personality'] = [] - for filepath, file_or_string in files.items(): - if hasattr(file_or_string, 'read'): - data = file_or_string.read() - else: - data = file_or_string - personality.append({ - 'path': filepath, - 'contents': data.encode('base64'), - }) - - if availability_zone: - body["server"]["availability_zone"] = availability_zone - - # Block device mappings are passed as a list of dictionaries - if block_device_mapping: - bdm = body['server']['block_device_mapping'] = [] - for device_name, mapping in block_device_mapping.items(): - # - # The mapping is in the format: - # :[]:[]:[] - # - bdm_dict = {'device_name': device_name} - - mapping_parts = mapping.split(':') - id = mapping_parts[0] - if len(mapping_parts) == 1: - bdm_dict['volume_id'] = id - if len(mapping_parts) > 1: - type = mapping_parts[1] - if type.startswith('snap'): - bdm_dict['snapshot_id'] = id - else: - bdm_dict['volume_id'] = id - if len(mapping_parts) > 2: - if mapping_parts[2]: - bdm_dict['volume_size'] = str(int(mapping_parts[2])) - if len(mapping_parts) > 3: - bdm_dict['delete_on_termination'] = mapping_parts[3] - bdm.append(bdm_dict) - - if nics is not None: - # NOTE(tr3buchet): nics can be an empty list - all_net_data = [] - for nic_info in nics: - net_data = {} - # if value is empty string, do not send value in body - if nic_info.get('net-id'): - net_data['uuid'] = nic_info['net-id'] - if nic_info.get('v4-fixed-ip'): - net_data['fixed_ip'] = nic_info['v4-fixed-ip'] - if nic_info.get('port-id'): - net_data['port'] = nic_info['port-id'] - all_net_data.append(net_data) - body['server']['networks'] = all_net_data - - return self._create(resource_url, body, response_key, - return_raw=return_raw, **kwargs) diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index 02a7aa338..311fa17da 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -25,7 +25,6 @@ from novaclient import base from novaclient import crypto -from novaclient.v1_1 import base as local_base REBOOT_SOFT, REBOOT_HARD = 'SOFT', 'HARD' @@ -341,7 +340,7 @@ def interface_detach(self, port_id): return self.manager.interface_detach(self, port_id) -class ServerManager(local_base.BootingManagerWithFind): +class ServerManager(base.BootingManagerWithFind): resource_class = Server def get(self, server): From 83118620d22fb26147da2455a4828b8237b9b370 Mon Sep 17 00:00:00 2001 From: Masayuki Igawa Date: Mon, 29 Jul 2013 14:36:31 +0900 Subject: [PATCH 0219/1705] Fix the help text process and the generated wrong help The wrong help text of bash-completion is generated by the process of choosing first line from the source code comment. We should get the all help text from the source code comment, and use it. So this commit removes this unnecessary process, and also fix the README.rst for proper help text. Fixes bug 1206005 Change-Id: Id4f5b6a7722197f09eb366ba8f3e643e1af91805 --- README.rst | 3 ++- novaclient/shell.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 6207d89f0..1dbcc0049 100644 --- a/README.rst +++ b/README.rst @@ -198,7 +198,8 @@ You'll find complete documentation on the shell by running x509-create-cert Create x509 cert for a user in tenant x509-get-root-cert Fetches the x509 root cert. bash-completion Prints all of the commands and options to stdout so - that the + that the nova.bash_completion script doesn't have to + hard code them. help Display help about this program or one of its subcommands. diff --git a/novaclient/shell.py b/novaclient/shell.py index 198c42fc5..7f3257155 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -469,7 +469,7 @@ def _find_actions(self, subparsers, actions_module): command = attr[3:].replace('_', '-') callback = getattr(actions_module, attr) desc = callback.__doc__ or '' - action_help = desc.strip().split('\n')[0] + action_help = desc.strip() arguments = getattr(callback, 'arguments', []) subparser = subparsers.add_parser(command, From 5fe9408d2e53ac24a578d0ed568a4cede303fe58 Mon Sep 17 00:00:00 2001 From: Christian Berendt Date: Wed, 17 Jul 2013 16:48:04 +0200 Subject: [PATCH 0220/1705] make findall in novaclient/base.py more efficient Use /resources instead of /resources/detail to resolve the resource ID by the name and load the details of the resource in a separate step. This reduces the overhead to resolve the resource ID and results in a better runtime performance. This patch does not solve the issue that the name resolving takes place on the client side. For solving this issue new Nova API methods are necessary. fixes bug #1202179 Change-Id: Ib753b1d090cb74b2d137c68f6899dad4ae2ec1ca --- novaclient/base.py | 19 +++++++++++++++--- novaclient/tests/v1_1/fakes.py | 28 +++++++++++++++++++++++++++ novaclient/tests/v1_1/test_images.py | 3 ++- novaclient/tests/v1_1/test_servers.py | 3 ++- novaclient/tests/v1_1/test_shell.py | 24 ++++++++++++++++++----- 5 files changed, 67 insertions(+), 10 deletions(-) diff --git a/novaclient/base.py b/novaclient/base.py index c37fa3486..adcf29d5a 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -22,6 +22,7 @@ import abc import contextlib import hashlib +import inspect import os import six @@ -197,11 +198,23 @@ def findall(self, **kwargs): found = [] searches = kwargs.items() - for obj in self.list(): + detailed = True + if 'detailed' in inspect.getargspec(self.list).args: + detailed = ("human_id" not in kwargs and + "name" not in kwargs and + "display_name" not in kwargs) + listing = self.list(detailed=detailed) + else: + listing = self.list() + + for obj in listing: try: if all(getattr(obj, attr) == value - for (attr, value) in searches): - found.append(obj) + for (attr, value) in searches): + if detailed: + found.append(obj) + else: + found.append(self.get(obj.id)) except AttributeError: continue diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index cc24c7d42..7673317fc 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -1720,6 +1720,34 @@ def get_volumes_detail(self, **kw): "links": ''}], "metadata": {}}]}) + def get_volumes(self, **kw): + return (200, {}, {"volumes": [ + {"display_name": "Work", + "display_description": "volume for work", + "status": "ATTACHED", + "id": "15e59938-07d5-11e1-90e3-e3dffe0c5983", + "created_at": "2011-09-09T00:00:00Z", + "attached": "2011-11-11T00:00:00Z", + "size": 1024, + "attachments": [ + {"id": "3333", + "links": ''}], + "metadata": {}}]}) + + def get_volumes_15e59938_07d5_11e1_90e3_e3dffe0c5983(self, **kw): + return (200, {}, {"volume": + {"display_name": "Work", + "display_description": "volume for work", + "status": "ATTACHED", + "id": "15e59938-07d5-11e1-90e3-e3dffe0c5983", + "created_at": "2011-09-09T00:00:00Z", + "attached": "2011-11-11T00:00:00Z", + "size": 1024, + "attachments": [ + {"id": "3333", + "links": ''}], + "metadata": {}}}) + def post_volumes(self, **kw): return (200, {}, {"volume": {"status": "creating", diff --git a/novaclient/tests/v1_1/test_images.py b/novaclient/tests/v1_1/test_images.py index b545cedd5..5f9cfacd0 100644 --- a/novaclient/tests/v1_1/test_images.py +++ b/novaclient/tests/v1_1/test_images.py @@ -45,7 +45,8 @@ def test_set_meta(self): def test_find(self): i = cs.images.find(name="CentOS 5.2") self.assertEqual(i.id, 1) - cs.assert_called('GET', '/images/detail') + cs.assert_called('GET', '/images', pos=-2) + cs.assert_called('GET', '/images/1', pos=-1) iml = cs.images.findall(status='SAVING') self.assertEqual(len(iml), 1) diff --git a/novaclient/tests/v1_1/test_servers.py b/novaclient/tests/v1_1/test_servers.py index 6fec98bac..e04e7887b 100644 --- a/novaclient/tests/v1_1/test_servers.py +++ b/novaclient/tests/v1_1/test_servers.py @@ -171,7 +171,8 @@ def test_set_server_meta(self): def test_find(self): server = cs.servers.find(name='sample-server') - cs.assert_called('GET', '/servers/detail') + cs.assert_called('GET', '/servers', pos=-2) + cs.assert_called('GET', '/servers/1234', pos=-1) self.assertEqual(server.name, 'sample-server') self.assertRaises(exceptions.NoUniqueMatch, cs.servers.find, diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index 5cf99d6e2..1ca22c62f 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -536,16 +536,21 @@ def test_reboot(self): def test_rebuild(self): self.run_command('rebuild sample-server 1') + self.assert_called('GET', '/servers', pos=-8) + self.assert_called('GET', '/servers/1234', pos=-7) + self.assert_called('GET', '/images/1', pos=-6) self.assert_called('POST', '/servers/1234/action', - {'rebuild': {'imageRef': 1}}, pos=-4) - self.assert_called('GET', '/servers/detail', pos=-3) + {'rebuild': {'imageRef': 1}}, pos=-5) self.assert_called('GET', '/flavors/1', pos=-2) self.assert_called('GET', '/images/2') self.run_command('rebuild sample-server 1 --rebuild-password asdf') + self.assert_called('GET', '/servers', pos=-8) + self.assert_called('GET', '/servers/1234', pos=-7) + self.assert_called('GET', '/images/1', pos=-6) self.assert_called('POST', '/servers/1234/action', {'rebuild': {'imageRef': 1, 'adminPass': 'asdf'}}, - pos=-4) + pos=-5) self.assert_called('GET', '/flavors/1', pos=-2) self.assert_called('GET', '/images/2') @@ -668,7 +673,11 @@ def test_delete_two_with_two_existent(self): self.assert_called('DELETE', '/servers/1234', pos=-3) self.assert_called('DELETE', '/servers/5678', pos=-1) self.run_command('delete sample-server sample-server2') - self.assert_called('DELETE', '/servers/1234', pos=-3) + self.assert_called('GET', '/servers', pos=-6) + self.assert_called('GET', '/servers/1234', pos=-5) + self.assert_called('DELETE', '/servers/1234', pos=-4) + self.assert_called('GET', '/servers', pos=-3) + self.assert_called('GET', '/servers/5678', pos=-2) self.assert_called('DELETE', '/servers/5678', pos=-1) def test_delete_two_with_one_nonexistent(self): @@ -1518,7 +1527,12 @@ def test_volume_list(self): def test_volume_show(self): self.run_command('volume-show Work') - self.assert_called('GET', '/volumes/detail') + self.assert_called('GET', '/volumes', pos=-2) + self.assert_called( + 'GET', + '/volumes/15e59938-07d5-11e1-90e3-e3dffe0c5983', + pos=-1 + ) def test_volume_create(self): self.run_command('volume-create 2 --display-name Work') From ddda3f31d86598c90e4cbdee9fca1d3bd2ff59a4 Mon Sep 17 00:00:00 2001 From: David Wittman Date: Mon, 29 Jul 2013 17:19:29 -0500 Subject: [PATCH 0221/1705] Fix net-id metavar for interface-attach The metavariable for net-id was incorrectly set to `fixed_ip`. This commit correctly sets the metavar to `net_id`. Change-Id: I9bfb442af99848cf43d1f3b78967b785506ac54f --- novaclient/v1_1/shell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 7358a87a9..b6d5eab83 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -3167,7 +3167,7 @@ def do_interface_list(cs, args): @utils.arg('server', metavar='', help='Name or ID of server.') @utils.arg('--port-id', metavar='', help='Port ID.', dest="port_id") -@utils.arg('--net-id', metavar='', help='Network ID', +@utils.arg('--net-id', metavar='', help='Network ID', default=None, dest="net_id") @utils.arg('--fixed-ip', metavar='', help='Requested fixed IP.', default=None, dest="fixed_ip") From 8b5dcee15eb82cd81d604be94e8f395627af4120 Mon Sep 17 00:00:00 2001 From: liyingjun Date: Tue, 30 Jul 2013 10:18:59 +0800 Subject: [PATCH 0222/1705] Add user quota client API support Implements blueprint user-quota-related-client-api This patch adds user arguments to the following subcommands: * quota-show * quota-update * quota-delete Change-Id: I6556de366a758f7550e9b26357f231666caae419 --- novaclient/tests/v1_1/test_quotas.py | 22 +++++++++++++++++++++ novaclient/tests/v1_1/test_shell.py | 24 +++++++++++++++++++++++ novaclient/v1_1/quotas.py | 25 ++++++++++++++++++------ novaclient/v1_1/shell.py | 29 +++++++++++++++++++++------- 4 files changed, 87 insertions(+), 13 deletions(-) diff --git a/novaclient/tests/v1_1/test_quotas.py b/novaclient/tests/v1_1/test_quotas.py index ce1c0388c..b545e6b18 100644 --- a/novaclient/tests/v1_1/test_quotas.py +++ b/novaclient/tests/v1_1/test_quotas.py @@ -26,6 +26,13 @@ def test_tenant_quotas_get(self): cs.quotas.get(tenant_id) cs.assert_called('GET', '/os-quota-sets/%s' % tenant_id) + def test_user_quotas_get(self): + tenant_id = 'test' + user_id = 'fake_user' + cs.quotas.get(tenant_id, user_id=user_id) + url = '/os-quota-sets/%s?user_id=%s' % (tenant_id, user_id) + cs.assert_called('GET', url) + def test_tenant_quotas_defaults(self): tenant_id = '97f4c221bff44578b0300df4ef119353' cs.quotas.defaults(tenant_id) @@ -37,6 +44,14 @@ def test_update_quota(self): cs.assert_called('PUT', '/os-quota-sets/97f4c221bff44578b0300df4ef119353') + def test_update_user_quota(self): + tenant_id = '97f4c221bff44578b0300df4ef119353' + user_id = 'fake_user' + q = cs.quotas.get(tenant_id) + q.update(volumes=2, user_id=user_id) + url = '/os-quota-sets/%s?user_id=%s' % (tenant_id, user_id) + cs.assert_called('PUT', url) + def test_force_update_quota(self): q = cs.quotas.get('97f4c221bff44578b0300df4ef119353') q.update(cores=2, force=True) @@ -59,3 +74,10 @@ def test_quotas_delete(self): tenant_id = 'test' cs.quotas.delete(tenant_id) cs.assert_called('DELETE', '/os-quota-sets/%s' % tenant_id) + + def test_user_quotas_delete(self): + tenant_id = 'test' + user_id = 'fake_user' + cs.quotas.delete(tenant_id, user_id=user_id) + url = '/os-quota-sets/%s?user_id=%s' % (tenant_id, user_id) + cs.assert_called('DELETE', url) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index 1ca22c62f..727a1f168 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -1213,6 +1213,12 @@ def test_quota_show(self): self.assert_called('GET', '/os-quota-sets/97f4c221bff44578b0300df4ef119353') + def test_user_quota_show(self): + self.run_command('quota-show --tenant ' + '97f4c221bff44578b0300df4ef119353 --user u1') + self.assert_called('GET', + '/os-quota-sets/97f4c221bff44578b0300df4ef119353?user_id=u1') + def test_quota_show_no_tenant(self): self.run_command('quota-show') self.assert_called('GET', '/os-quota-sets/tenant_id') @@ -1237,6 +1243,17 @@ def test_quota_update(self): {'quota_set': {'instances': 5, 'tenant_id': '97f4c221bff44578b0300df4ef119353'}}) + def test_user_quota_update(self): + self.run_command( + 'quota-update 97f4c221bff44578b0300df4ef119353' + ' --user=u1' + ' --instances=5') + self.assert_called( + 'PUT', + '/os-quota-sets/97f4c221bff44578b0300df4ef119353?user_id=u1', + {'quota_set': {'instances': 5, + 'tenant_id': '97f4c221bff44578b0300df4ef119353'}}) + def test_quota_force_update(self): self.run_command( 'quota-update 97f4c221bff44578b0300df4ef119353' @@ -1262,6 +1279,13 @@ def test_quota_delete(self): self.assert_called('DELETE', '/os-quota-sets/97f4c221bff44578b0300df4ef119353') + def test_user_quota_delete(self): + self.run_command('quota-delete --tenant ' + '97f4c221bff44578b0300df4ef119353 ' + '--user u1') + self.assert_called('DELETE', + '/os-quota-sets/97f4c221bff44578b0300df4ef119353?user_id=u1') + def test_quota_class_show(self): self.run_command('quota-class-show test') self.assert_called('GET', '/os-quota-class-sets/test') diff --git a/novaclient/v1_1/quotas.py b/novaclient/v1_1/quotas.py index e174c6f18..7c7ce54e3 100644 --- a/novaclient/v1_1/quotas.py +++ b/novaclient/v1_1/quotas.py @@ -31,17 +31,22 @@ def update(self, *args, **kwargs): class QuotaSetManager(base.Manager): resource_class = QuotaSet - def get(self, tenant_id): + def get(self, tenant_id, user_id=None): if hasattr(tenant_id, 'tenant_id'): tenant_id = tenant_id.tenant_id - return self._get("/os-quota-sets/%s" % (tenant_id), "quota_set") + if user_id: + url = '/os-quota-sets/%s?user_id=%s' % (tenant_id, user_id) + else: + url = '/os-quota-sets/%s' % tenant_id + return self._get(url, "quota_set") def update(self, tenant_id, metadata_items=None, injected_file_content_bytes=None, injected_file_path_bytes=None, volumes=None, gigabytes=None, ram=None, floating_ips=None, fixed_ips=None, instances=None, injected_files=None, cores=None, key_pairs=None, - security_groups=None, security_group_rules=None, force=None): + security_groups=None, security_group_rules=None, force=None, + user_id=None): body = {'quota_set': { 'tenant_id': tenant_id, @@ -65,11 +70,19 @@ def update(self, tenant_id, metadata_items=None, if body['quota_set'][key] is None: body['quota_set'].pop(key) - return self._update('/os-quota-sets/%s' % tenant_id, body, 'quota_set') + if user_id: + url = '/os-quota-sets/%s?user_id=%s' % (tenant_id, user_id) + else: + url = '/os-quota-sets/%s' % tenant_id + return self._update(url, body, 'quota_set') def defaults(self, tenant_id): return self._get('/os-quota-sets/%s/defaults' % tenant_id, 'quota_set') - def delete(self, tenant_id): - self._delete("/os-quota-sets/%s" % tenant_id) + def delete(self, tenant_id, user_id=None): + if user_id: + url = '/os-quota-sets/%s?user_id=%s' % (tenant_id, user_id) + else: + url = '/os-quota-sets/%s' % tenant_id + self._delete(url) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 7358a87a9..01ddadd2a 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2895,8 +2895,10 @@ def _quota_update(manager, identifier, args): # default value of force is None to make sure this client # will be compatibile with old nova server force_update = getattr(args, 'force', None) + user_id = getattr(args, 'user', None) if isinstance(manager, quotas.QuotaSetManager): - manager.update(identifier, force=force_update, **updates) + manager.update(identifier, force=force_update, user_id=user_id, + **updates) else: manager.update(identifier, **updates) @@ -2905,13 +2907,17 @@ def _quota_update(manager, identifier, args): metavar='', default=None, help='ID of tenant to list the quotas for.') +@utils.arg('--user', + metavar='', + default=None, + help='ID of user to list the quotas for.') def do_quota_show(cs, args): - """List the quotas for a tenant.""" + """List the quotas for a tenant/user.""" if not args.tenant: - _quota_show(cs.quotas.get(cs.client.tenant_id)) + _quota_show(cs.quotas.get(cs.client.tenant_id, user_id=args.user)) else: - _quota_show(cs.quotas.get(args.tenant)) + _quota_show(cs.quotas.get(args.tenant, user_id=args.user)) @utils.arg('--tenant', @@ -2930,6 +2936,10 @@ def do_quota_defaults(cs, args): @utils.arg('tenant', metavar='', help='ID of tenant to set the quotas for.') +@utils.arg('--user', + metavar='', + default=None, + help='ID of user to set the quotas for.') @utils.arg('--instances', metavar='', type=int, default=None, @@ -3014,7 +3024,7 @@ def do_quota_defaults(cs, args): help='Whether force update the quota even if the already used' ' and reserved exceeds the new quota') def do_quota_update(cs, args): - """Update the quotas for a tenant.""" + """Update the quotas for a tenant/user.""" _quota_update(cs.quotas, args.tenant, args) @@ -3022,10 +3032,15 @@ def do_quota_update(cs, args): @utils.arg('--tenant', metavar='', help='ID of tenant to delete quota for.') +@utils.arg('--user', + metavar='', + help='ID of user to delete quota for.') def do_quota_delete(cs, args): - """Delete quota for a tenant so their quota will revert back to default.""" + """Delete quota for a tenant/user so their quota will Revert + back to default. + """ - cs.quotas.delete(args.tenant) + cs.quotas.delete(args.tenant, user_id=args.user) @utils.arg('class_name', From 7fdb52ac74bd896073f4f0f7bd0a21c1c32187ea Mon Sep 17 00:00:00 2001 From: Sumanth Nagadavalli Date: Tue, 30 Jul 2013 15:09:09 +0530 Subject: [PATCH 0223/1705] Fixing host-action on V2 Changed host-actions to GET from POST for reboot, startup and shutdown, as per nova-api reference documentation. Change-Id: I863b75960b2e427fd9384a336727132ca3c130c6 Fixes: bug #1206425 --- novaclient/tests/v1_1/fakes.py | 13 +++++++++++-- novaclient/tests/v1_1/test_hosts.py | 6 +++--- novaclient/tests/v1_1/test_shell.py | 6 +++--- novaclient/v1_1/hosts.py | 5 ++--- 4 files changed, 19 insertions(+), 11 deletions(-) diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index 7673317fc..9a9686c77 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -1444,8 +1444,17 @@ def put_os_hosts_sample_host_3(self, body, **kw): 'status': 'enabled', 'maintenance_mode': 'on_maintenance'}) - def post_os_hosts_sample_host_action(self, **kw): - return (202, {}, None) + def get_os_hosts_sample_host_reboot(self, **kw): + return (200, {}, {'host': 'sample_host', + 'power_action': 'reboot'}) + + def get_os_hosts_sample_host_startup(self, **kw): + return (200, {}, {'host': 'sample_host', + 'power_action': 'startup'}) + + def get_os_hosts_sample_host_shutdown(self, **kw): + return (200, {}, {'host': 'sample_host', + 'power_action': 'shutdown'}) def put_os_hosts_sample_host(self, body, **kw): result = {'host': 'dummy'} diff --git a/novaclient/tests/v1_1/test_hosts.py b/novaclient/tests/v1_1/test_hosts.py index 348d59cc1..92d5bc965 100644 --- a/novaclient/tests/v1_1/test_hosts.py +++ b/novaclient/tests/v1_1/test_hosts.py @@ -51,16 +51,16 @@ def test_host_startup(self): host = cs.hosts.get('sample_host')[0] result = host.startup() cs.assert_called( - 'POST', '/os-hosts/sample_host/action', {'startup': None}) + 'GET', '/os-hosts/sample_host/startup') def test_host_reboot(self): host = cs.hosts.get('sample_host')[0] result = host.reboot() cs.assert_called( - 'POST', '/os-hosts/sample_host/action', {'reboot': None}) + 'GET', '/os-hosts/sample_host/reboot') def test_host_shutdown(self): host = cs.hosts.get('sample_host')[0] result = host.shutdown() cs.assert_called( - 'POST', '/os-hosts/sample_host/action', {'shutdown': None}) + 'GET', '/os-hosts/sample_host/shutdown') diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index 1ca22c62f..13ad1d1e6 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -1070,17 +1070,17 @@ def test_host_update_multiple_settings(self): def test_host_startup(self): self.run_command('host-action sample-host --action startup') self.assert_called( - 'POST', '/os-hosts/sample-host/action', {'startup': None}) + 'GET', '/os-hosts/sample-host/startup') def test_host_shutdown(self): self.run_command('host-action sample-host --action shutdown') self.assert_called( - 'POST', '/os-hosts/sample-host/action', {'shutdown': None}) + 'GET', '/os-hosts/sample-host/shutdown') def test_host_reboot(self): self.run_command('host-action sample-host --action reboot') self.assert_called( - 'POST', '/os-hosts/sample-host/action', {'reboot': None}) + 'GET', '/os-hosts/sample-host/reboot') def test_host_evacuate(self): self.run_command('host-evacuate hyper --target target_hyper') diff --git a/novaclient/v1_1/hosts.py b/novaclient/v1_1/hosts.py index 83b4c93db..af1756ad2 100644 --- a/novaclient/v1_1/hosts.py +++ b/novaclient/v1_1/hosts.py @@ -58,9 +58,8 @@ def update(self, host, values): def host_action(self, host, action): """Perform an action on a host.""" - body = {action: None} - url = '/os-hosts/%s/action' % host - return self.api.client.post(url, body=body) + url = '/os-hosts/{0}/{1}'.format(host, action) + return self.api.client.get(url) def list(self, zone=None): url = '/os-hosts' From 852e2e5f3116844ea996bec6a1c9b15a463928da Mon Sep 17 00:00:00 2001 From: Vincent Hou Date: Wed, 31 Jul 2013 05:47:53 -0400 Subject: [PATCH 0224/1705] Check whether the security group id is integer Fixed Bug 1206847. Change-Id: I2194d6538a5c5475b876ee026b5d625be2df076f --- novaclient/utils.py | 9 +++++++++ novaclient/v1_1/shell.py | 3 +-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/novaclient/utils.py b/novaclient/utils.py index 532ea976c..1bea47b6c 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -353,3 +353,12 @@ def _load_entry_point(ep_name, name=None): return ep.load() except (ImportError, pkg_resources.UnknownExtra, AttributeError): continue + + +def is_integer_like(val): + """Returns validation of a value as an integer.""" + try: + value = int(val) + return True + except (TypeError, ValueError, AttributeError): + return False diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 9b3aca5c6..c044cf49d 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -30,7 +30,6 @@ from novaclient import exceptions from novaclient.openstack.common import strutils from novaclient.openstack.common import timeutils -from novaclient.openstack.common import uuidutils from novaclient import utils from novaclient.v1_1 import availability_zones from novaclient.v1_1 import quotas @@ -1918,7 +1917,7 @@ def _print_secgroups(secgroups): def _get_secgroup(cs, secgroup): # Check secgroup is an ID - if uuidutils.is_uuid_like(strutils.safe_encode(secgroup)): + if utils.is_integer_like(strutils.safe_encode(secgroup)): try: return cs.security_groups.get(secgroup) except exceptions.NotFound: From 0d454089c9f507bf128673a07d780fbfdba981f0 Mon Sep 17 00:00:00 2001 From: Brian Elliott Date: Wed, 31 Jul 2013 15:53:26 -0500 Subject: [PATCH 0225/1705] Support programmatic use of disk config extension Allow servers to be created, resized, and rebuilt with the disk_config option: http://api.openstack.org/api-ref.html#ext-os-disk-config There is a separate extension that exists for disk config already, but it only works via the novaclient CLI interface, not via programmatic use of python-novaclient as a library. assert changes in tests/v1_1/fakes allow for other data in rebuild and resize requests. Change-Id: I8051ffb8747cf5c67b0199d22fbbe80306b01499 --- novaclient/base.py | 7 +++- novaclient/tests/v1_1/fakes.py | 5 +-- novaclient/tests/v1_1/test_servers.py | 52 +++++++++++++++++++++++++++ novaclient/v1_1/servers.py | 22 +++++++++--- 4 files changed, 79 insertions(+), 7 deletions(-) diff --git a/novaclient/base.py b/novaclient/base.py index bf309b3c9..5d4b583e4 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -231,7 +231,7 @@ def _boot(self, resource_url, response_key, name, image, flavor, max_count=None, security_groups=None, key_name=None, availability_zone=None, block_device_mapping=None, nics=None, scheduler_hints=None, config_drive=None, admin_pass=None, - **kwargs): + disk_config=None, **kwargs): """ Create (boot) a new server. @@ -264,6 +264,8 @@ def _boot(self, resource_url, response_key, name, image, flavor, :param config_drive: (optional extension) value for config drive either boolean, or volume-id :param admin_pass: admin password for the server. + :param disk_config: (optional extension) control how the disk is + partitioned when the server is created. """ body = {"server": { "name": name, @@ -360,6 +362,9 @@ def _boot(self, resource_url, response_key, name, image, flavor, all_net_data.append(net_data) body['server']['networks'] = all_net_data + if disk_config is not None: + body['server']['OS-DCF:diskConfig'] = disk_config + return self._create(resource_url, body, response_key, return_raw=return_raw, **kwargs) diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index 9a9686c77..3e1e955a2 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -473,10 +473,11 @@ def post_servers_1234_action(self, body, **kw): keys = body[action].keys() if 'adminPass' in keys: keys.remove('adminPass') - assert keys == ['imageRef'] + assert 'imageRef' in keys _body = self.get_servers_1234()[2] elif action == 'resize': - assert body[action].keys() == ['flavorRef'] + keys = body[action].keys() + assert 'flavorRef' in keys elif action == 'confirmResize': assert body[action] is None # This one method returns a different response code diff --git a/novaclient/tests/v1_1/test_servers.py b/novaclient/tests/v1_1/test_servers.py index e04e7887b..701c88126 100644 --- a/novaclient/tests/v1_1/test_servers.py +++ b/novaclient/tests/v1_1/test_servers.py @@ -135,6 +135,29 @@ def test_create_server_userdata_utf8(self): cs.assert_called('POST', '/servers') self.assertTrue(isinstance(s, servers.Server)) + def _create_disk_config(self, disk_config): + s = cs.servers.create( + name="My server", + image=1, + flavor=1, + disk_config=disk_config + ) + cs.assert_called('POST', '/servers') + self.assertTrue(isinstance(s, servers.Server)) + + # verify disk config param was used in the request: + last_request = cs.client.callstack[-1] + body = last_request[-1] + server = body['server'] + self.assertTrue('OS-DCF:diskConfig' in server) + self.assertEqual(disk_config, server['OS-DCF:diskConfig']) + + def test_create_server_disk_config_auto(self): + self._create_disk_config('AUTO') + + def test_create_server_disk_config_manual(self): + self._create_disk_config('MANUAL') + def test_update_server(self): s = cs.servers.get(1234) @@ -199,6 +222,29 @@ def test_rebuild_server(self): cs.servers.rebuild(s, image=1, password='5678') cs.assert_called('POST', '/servers/1234/action') + def _rebuild_resize_disk_config(self, disk_config, operation="rebuild"): + s = cs.servers.get(1234) + + if operation == "rebuild": + s.rebuild(image=1, disk_config=disk_config) + elif operation == "resize": + s.resize(flavor=1, disk_config=disk_config) + cs.assert_called('POST', '/servers/1234/action') + + # verify disk config param was used in the request: + last_request = cs.client.callstack[-1] + body = last_request[-1] + + d = body[operation] + self.assertTrue('OS-DCF:diskConfig' in d) + self.assertEqual(disk_config, d['OS-DCF:diskConfig']) + + def test_rebuild_server_disk_config_auto(self): + self._rebuild_resize_disk_config('AUTO') + + def test_rebuild_server_disk_config_manual(self): + self._rebuild_resize_disk_config('MANUAL') + def test_resize_server(self): s = cs.servers.get(1234) s.resize(flavor=1) @@ -206,6 +252,12 @@ def test_resize_server(self): cs.servers.resize(s, flavor=1) cs.assert_called('POST', '/servers/1234/action') + def test_resize_server_disk_config_auto(self): + self._rebuild_resize_disk_config('AUTO', 'resize') + + def test_resize_server_disk_config_manual(self): + self._rebuild_resize_disk_config('MANUAL', 'resize') + def test_confirm_resized_server(self): s = cs.servers.get(1234) s.confirm_resize() diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index fa44e9683..4f09d9df4 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -570,7 +570,7 @@ def create(self, name, image, flavor, meta=None, files=None, max_count=None, security_groups=None, userdata=None, key_name=None, availability_zone=None, block_device_mapping=None, nics=None, scheduler_hints=None, - config_drive=None, **kwargs): + config_drive=None, disk_config=None, **kwargs): # TODO(anthony): indicate in doc string if param is an extension # and/or optional """ @@ -604,6 +604,9 @@ def create(self, name, image, flavor, meta=None, files=None, specified by the client to help boot an instance :param config_drive: (optional extension) value for config drive either boolean, or volume-id + :param disk_config: (optional extension) control how the disk is + partitioned when the server is created. possible + values are 'AUTO' or 'MANUAL'. """ if not min_count: min_count = 1 @@ -620,7 +623,7 @@ def create(self, name, image, flavor, meta=None, files=None, max_count=max_count, security_groups=security_groups, key_name=key_name, availability_zone=availability_zone, scheduler_hints=scheduler_hints, config_drive=config_drive, - **kwargs) + disk_config=disk_config, **kwargs) if block_device_mapping: resource_url = "/os-volumes_boot" @@ -674,17 +677,23 @@ def reboot(self, server, reboot_type=REBOOT_SOFT): """ self._action('reboot', server, {'type': reboot_type}) - def rebuild(self, server, image, password=None, **kwargs): + def rebuild(self, server, image, password=None, disk_config=None, + **kwargs): """ Rebuild -- shut down and then re-image -- a server. :param server: The :class:`Server` (or its ID) to share onto. :param image: the :class:`Image` (or its ID) to re-image with. :param password: string to set as password on the rebuilt server. + :param disk_config: partitioning mode to use on the rebuilt server. + Valid values are 'AUTO' or 'MANUAL' """ body = {'imageRef': base.getid(image)} if password is not None: body['adminPass'] = password + if disk_config is not None: + body['OS-DCF:diskConfig'] = disk_config + _resp, body = self._action('rebuild', server, body, **kwargs) return Server(self, body['server']) @@ -696,12 +705,14 @@ def migrate(self, server): """ self._action('migrate', server) - def resize(self, server, flavor, **kwargs): + def resize(self, server, flavor, disk_config=None, **kwargs): """ Resize a server's resources. :param server: The :class:`Server` (or its ID) to share onto. :param flavor: the :class:`Flavor` (or its ID) to resize to. + :param disk_config: partitioning mode to use on the rebuilt server. + Valid values are 'AUTO' or 'MANUAL' Until a resize event is confirmed with :meth:`confirm_resize`, the old server will be kept around and you'll be able to roll back to the old @@ -709,6 +720,9 @@ def resize(self, server, flavor, **kwargs): automatically confirmed after 24 hours. """ info = {'flavorRef': base.getid(flavor)} + if disk_config is not None: + info['OS-DCF:diskConfig'] = disk_config + self._action('resize', server, info=info, **kwargs) def confirm_resize(self, server): From 1f85f57faf35e371464e157c3e77333be827111d Mon Sep 17 00:00:00 2001 From: Cyril Roelandt Date: Fri, 2 Aug 2013 15:32:12 +0000 Subject: [PATCH 0226/1705] FakeClient: fix the arguments of a string format. Fix the number of arguments in the string format in FakeClient.assert_called_anytime(). Change-Id: I9d415d6d216a6e301254ba21b63109182d71e8bd --- novaclient/tests/fakes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/tests/fakes.py b/novaclient/tests/fakes.py index 3615e4f35..f4b356985 100644 --- a/novaclient/tests/fakes.py +++ b/novaclient/tests/fakes.py @@ -53,7 +53,7 @@ def assert_called_anytime(self, method, url, body=None): found = True break - assert found, 'Expected %s %s; got %s' % \ + assert found, 'Expected %s; got %s' % \ (expected, self.client.callstack) if body is not None: try: From 930a10abf91618bf5454aea71d4429a811c34cbf Mon Sep 17 00:00:00 2001 From: Avishay Traeger Date: Thu, 1 Aug 2013 15:40:03 +0300 Subject: [PATCH 0227/1705] Add support for swap_volume Add client support for swap_volume, which allows swapping a volume currently attached to an instance with a different volume which is not attached. The contents of the old volume will be copied onto the new volume. This was added in nova commit 8f51b120b430c7c21399256f37e1d8f75d030484. Change-Id: I98323d594617c1c435e403d9f3ddc4ff4fa74da6 --- novaclient/tests/v1_1/fakes.py | 31 +++++++++ novaclient/tests/v1_1/test_shell.py | 5 ++ novaclient/tests/v1_1/test_volumes.py | 93 +++++++++++++++++++++++++++ novaclient/v1_1/shell.py | 17 +++++ novaclient/v1_1/volumes.py | 14 ++++ 5 files changed, 160 insertions(+) create mode 100644 novaclient/tests/v1_1/test_volumes.py diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index 9a9686c77..2d816887e 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -1779,6 +1779,37 @@ def post_servers_1234_os_volume_attachments(self, **kw): {"device": "/dev/vdb", "volumeId": 2}}) + def put_servers_1234_os_volume_attachments_Work(self, **kw): + return (200, {}, {"volumeAttachment": {"volumeId": 2}}) + + def get_servers_1234_os_volume_attachments(self, **kw): + return (200, {}, {"volumeAttachments": [ + {"display_name": "Work", + "display_description": "volume for work", + "status": "ATTACHED", + "id": "15e59938-07d5-11e1-90e3-e3dffe0c5983", + "created_at": "2011-09-09T00:00:00Z", + "attached": "2011-11-11T00:00:00Z", + "size": 1024, + "attachments": [ + {"id": "3333", + "links": ''}], + "metadata": {}}]}) + + def get_servers_1234_os_volume_attachments_Work(self, **kw): + return (200, {}, {"volumeAttachment": + {"display_name": "Work", + "display_description": "volume for work", + "status": "ATTACHED", + "id": "15e59938-07d5-11e1-90e3-e3dffe0c5983", + "created_at": "2011-09-09T00:00:00Z", + "attached": "2011-11-11T00:00:00Z", + "size": 1024, + "attachments": [ + {"id": "3333", + "links": ''}], + "metadata": {}}}) + def delete_servers_1234_os_volume_attachments_Work(self, **kw): return (200, {}, {}) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index 1a08c0bb8..f90c52661 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -1582,6 +1582,11 @@ def test_volume_attach(self): {'device': '/dev/vdb', 'volumeId': 'Work'}}) + def test_volume_update(self): + self.run_command('volume-update sample-server Work Work') + self.assert_called('PUT', '/servers/1234/os-volume_attachments/Work', + {'volumeAttachment': {'volumeId': 'Work'}}) + def test_volume_detach(self): self.run_command('volume-detach sample-server Work') self.assert_called('DELETE', diff --git a/novaclient/tests/v1_1/test_volumes.py b/novaclient/tests/v1_1/test_volumes.py new file mode 100644 index 000000000..131a032d7 --- /dev/null +++ b/novaclient/tests/v1_1/test_volumes.py @@ -0,0 +1,93 @@ +# Copyright 2013 IBM Corp. +# All Rights Reserved. +# +# 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 novaclient.v1_1 import volumes +from novaclient.tests import utils +from novaclient.tests.v1_1 import fakes + + +cs = fakes.FakeClient() + + +class VolumesTest(utils.TestCase): + + def test_list_servers(self): + vl = cs.volumes.list() + cs.assert_called('GET', '/volumes/detail') + [self.assertTrue(isinstance(v, volumes.Volume)) for v in vl] + + def test_list_volumes_undetailed(self): + vl = cs.volumes.list(detailed=False) + cs.assert_called('GET', '/volumes') + [self.assertTrue(isinstance(v, volumes.Volume)) for v in vl] + + def test_get_volume_details(self): + vol_id = '15e59938-07d5-11e1-90e3-e3dffe0c5983' + v = cs.volumes.get(vol_id) + cs.assert_called('GET', '/volumes/%s' % vol_id) + self.assertTrue(isinstance(v, volumes.Volume)) + self.assertEqual(v.id, vol_id) + + def test_create_volume(self): + v = cs.volumes.create( + size=2, + display_name="My volume", + display_description="My volume desc", + ) + cs.assert_called('POST', '/volumes') + self.assertTrue(isinstance(v, volumes.Volume)) + + def test_delete_volume(self): + vol_id = '15e59938-07d5-11e1-90e3-e3dffe0c5983' + v = cs.volumes.get(vol_id) + v.delete() + cs.assert_called('DELETE', '/volumes/%s' % vol_id) + cs.volumes.delete(vol_id) + cs.assert_called('DELETE', '/volumes/%s' % vol_id) + cs.volumes.delete(v) + cs.assert_called('DELETE', '/volumes/%s' % vol_id) + + def test_create_server_volume(self): + v = cs.volumes.create_server_volume( + server_id=1234, + volume_id='15e59938-07d5-11e1-90e3-e3dffe0c5983', + device='/dev/vdb' + ) + cs.assert_called('POST', '/servers/1234/os-volume_attachments') + self.assertTrue(isinstance(v, volumes.Volume)) + + def test_update_server_volume(self): + vol_id = '15e59938-07d5-11e1-90e3-e3dffe0c5983' + v = cs.volumes.update_server_volume( + server_id=1234, + attachment_id='Work', + new_volume_id=vol_id + ) + cs.assert_called('PUT', '/servers/1234/os-volume_attachments/Work') + self.assertTrue(isinstance(v, volumes.Volume)) + + def test_get_server_volume(self): + v = cs.volumes.get_server_volume(1234, 'Work') + cs.assert_called('GET', '/servers/1234/os-volume_attachments/Work') + self.assertTrue(isinstance(v, volumes.Volume)) + + def test_list_server_volumes(self): + vl = cs.volumes.get_server_volumes(1234) + cs.assert_called('GET', '/servers/1234/os-volume_attachments') + [self.assertTrue(isinstance(v, volumes.Volume)) for v in vl] + + def test_delete_server_volume(self): + cs.volumes.delete_server_volume(1234, 'Work') + cs.assert_called('DELETE', '/servers/1234/os-volume_attachments/Work') diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 9b3aca5c6..1b9a031e8 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1551,6 +1551,23 @@ def do_volume_attach(cs, args): _print_volume(volume) +@utils.arg('server', + metavar='', + help='Name or ID of server.') +@utils.arg('attachment_id', + metavar='', + help='Attachment ID of the volume.') +@utils.arg('new_volume', + metavar='', + help='ID of the volume to attach.') +def do_volume_update(cs, args): + """Update volume attachment.""" + volume = cs.volumes.update_server_volume(_find_server(cs, args.server).id, + args.attachment_id, + args.new_volume) + _print_volume(volume) + + @utils.arg('server', metavar='', help='Name or ID of server.') diff --git a/novaclient/v1_1/volumes.py b/novaclient/v1_1/volumes.py index b17457068..b744c3397 100644 --- a/novaclient/v1_1/volumes.py +++ b/novaclient/v1_1/volumes.py @@ -119,6 +119,20 @@ def create_server_volume(self, server_id, volume_id, device): return self._create("/servers/%s/os-volume_attachments" % server_id, body, "volumeAttachment") + def update_server_volume(self, server_id, attachment_id, new_volume_id): + """ + Update the volume identified by the attachment ID, that is attached to + the given server ID + + :param server_id: The ID of the server + :param attachment_id: The ID of the attachment + :param new_volume_id: The ID of the new volume to attach + :rtype: :class:`Volume` + """ + body = {'volumeAttachment': {'volumeId': new_volume_id}} + return self._update("/servers/%s/os-volume_attachments/%s" % + (server_id, attachment_id,), body, "volumeAttachment") + def get_server_volume(self, server_id, attachment_id): """ Get the volume identified by the attachment ID, that is attached to From ec5861644c3a7a77bcbbdd8cda7e33165fbb4601 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 5 Aug 2013 17:50:29 -0300 Subject: [PATCH 0228/1705] Sync with global requirements Change-Id: I8333e17d8edaa5853ac47f9d53de8c44d1c5d5cb --- requirements.txt | 9 ++++----- setup.py | 18 ++++++++---------- test-requirements.txt | 10 +++++----- tox.ini | 2 +- 4 files changed, 18 insertions(+), 21 deletions(-) diff --git a/requirements.txt b/requirements.txt index 3390adde9..3ea16a741 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,7 @@ -d2to1>=0.2.10,<0.3 -pbr>=0.5,<0.6 +pbr>=0.5.16,<0.6 argparse iso8601>=0.1.4 -prettytable>=0.6,<0.8 -requests>=0.8 -simplejson +PrettyTable>=0.6,<0.8 +requests>=1.1,<1.2.3 +simplejson>=2.0.9 six diff --git a/setup.py b/setup.py index 726cd9c6d..15f4e9d54 100644 --- a/setup.py +++ b/setup.py @@ -1,24 +1,22 @@ -# Copyright 2011 OpenStack Foundation +#!/usr/bin/env python +# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. # # 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 +# 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. +# 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. +# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT import setuptools - setuptools.setup( - setup_requires=[ - 'd2to1>=0.2.10,<0.3', - 'pbr>=0.5,<0.6' - ], - d2to1=True -) + setup_requires=['pbr>=0.5.20'], + pbr=True) diff --git a/test-requirements.txt b/test-requirements.txt index 91ffda1ba..4af426dc8 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -2,13 +2,13 @@ pep8==1.4.5 pyflakes==0.7.2 flake8==2.0 -hacking>=0.5.3,<0.6 +hacking>=0.5.6,<0.7 -coverage +coverage>=3.6 discover fixtures>=0.3.12 keyring -mock +mock>=0.8.0 sphinx>=1.1.2 -testrepository>=0.0.13 -testtools>=0.9.26 +testrepository>=0.0.17 +testtools>=0.9.32 diff --git a/tox.ini b/tox.ini index b10c1603f..e7e18c0d3 100644 --- a/tox.ini +++ b/tox.ini @@ -24,6 +24,6 @@ commands = python setup.py testr --coverage --testr-args='{posargs}' downloadcache = ~/cache/pip [flake8] -ignore = E12,E711,E721,E712,F841,F811,F821,H302,H306,H403,H404 +ignore = E12,E711,E721,E712,F841,F811,F821,H102,H302,H306,H403,H404,H501 show-source = True exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build From fe31607d1ba2596a5df7452b6c2227215e5397d3 Mon Sep 17 00:00:00 2001 From: Mark McClain Date: Tue, 6 Aug 2013 15:25:00 -0400 Subject: [PATCH 0229/1705] remove requests version max This change removes the max version specified for requests since the requirements project no longer limits it to 1.2.2. Change-Id: I96d14b4a84d975e8b08e9271db8d5e13d78019ae --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 3ea16a741..964b63d4b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,6 @@ pbr>=0.5.16,<0.6 argparse iso8601>=0.1.4 PrettyTable>=0.6,<0.8 -requests>=1.1,<1.2.3 +requests>=1.1 simplejson>=2.0.9 six From aee0a29733a1f38038fb7a038b426ca1c8fa9b97 Mon Sep 17 00:00:00 2001 From: Noorul Islam K M Date: Mon, 5 Aug 2013 07:11:57 +0530 Subject: [PATCH 0230/1705] Fix typo and grammar in docstring only Change-Id: I1e34ada679c56b996c6aa4652b43d1652f3c375f --- novaclient/v1_1/servers.py | 12 ++++++------ novaclient/v1_1/usage.py | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index b63fa09da..c7e85293a 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -319,7 +319,7 @@ def add_security_group(self, security_group): def remove_security_group(self, security_group): """ - Remova a security group from an instance. + Remove a security group from an instance. """ self.manager.remove_security_group(self, security_group) @@ -464,7 +464,7 @@ def get_password(self, server, private_key): """ Get password for an instance - Requires that openssl in installed and in the path + Requires that openssl is installed and in the path :param server: The :class:`Server` (or its ID) to add an IP to. :param private_key: The private key to decrypt password @@ -821,20 +821,20 @@ def reset_network(self, server): def add_security_group(self, server, security_group): """ - Add a Security Group to a instance + Add a Security Group to an instance :param server: ID of the instance. - :param security_grou: The name of security group to add. + :param security_group: The name of security group to add. """ self._action('addSecurityGroup', server, {'name': security_group}) def remove_security_group(self, server, security_group): """ - Add a Security Group to a instance + Add a Security Group to an instance :param server: ID of the instance. - :param security_grou: The name of security group to remove. + :param security_group: The name of security group to remove. """ self._action('removeSecurityGroup', server, {'name': security_group}) diff --git a/novaclient/v1_1/usage.py b/novaclient/v1_1/usage.py index b55f159ba..880434046 100644 --- a/novaclient/v1_1/usage.py +++ b/novaclient/v1_1/usage.py @@ -7,7 +7,7 @@ class Usage(base.Resource): """ - Usage contains infomartion about a tenants physical resource usage + Usage contains information about a tenant's physical resource usage """ def __repr__(self): return "" From f7d3948b2395fcee7e9826f567ededffc50305db Mon Sep 17 00:00:00 2001 From: Chmouel Boudjnah Date: Wed, 7 Aug 2013 14:55:53 +0200 Subject: [PATCH 0231/1705] Do not restrict flavor to only ID and integers - Flavors with strings and not just ID/UUID strings are valid. - Closes-Bug: 1209060 Change-Id: Idee389fce40f8982b263e1a4349a8565140b6584 --- novaclient/tests/v1_1/test_flavors.py | 38 +++++++++++++++++++++++++++ novaclient/v1_1/flavors.py | 7 ----- 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/novaclient/tests/v1_1/test_flavors.py b/novaclient/tests/v1_1/test_flavors.py index eb7be2a4c..9adeda8ec 100644 --- a/novaclient/tests/v1_1/test_flavors.py +++ b/novaclient/tests/v1_1/test_flavors.py @@ -1,3 +1,18 @@ +# Copyright (c) 2013, OpenStack +# All Rights Reserved. +# +# 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 novaclient import exceptions from novaclient.v1_1 import flavors from novaclient.tests import utils @@ -85,6 +100,29 @@ def test_create(self): cs.assert_called('POST', '/flavors', body) self.assertTrue(isinstance(f, flavors.Flavor)) + def test_create_with_id_as_string(self): + flavor_id = 'foobar' + f = cs.flavors.create("flavorcreate", 512, + 1, 10, flavor_id, ephemeral=10, + is_public=False) + + body = { + "flavor": { + "name": "flavorcreate", + "ram": 512, + "vcpus": 1, + "disk": 10, + "OS-FLV-EXT-DATA:ephemeral": 10, + "id": flavor_id, + "swap": 0, + "rxtx_factor": 1.0, + "os-flavor-access:is_public": False, + } + } + + cs.assert_called('POST', '/flavors', body) + self.assertTrue(isinstance(f, flavors.Flavor)) + def test_create_ephemeral_ispublic_defaults(self): f = cs.flavors.create("flavorcreate", 512, 1, 10, 1234) diff --git a/novaclient/v1_1/flavors.py b/novaclient/v1_1/flavors.py index 0f244bc18..02ca76cb4 100644 --- a/novaclient/v1_1/flavors.py +++ b/novaclient/v1_1/flavors.py @@ -7,7 +7,6 @@ from novaclient import base from novaclient import exceptions from novaclient import utils -from novaclient.openstack.common import uuidutils class Flavor(base.Resource): @@ -148,12 +147,6 @@ def create(self, name, ram, vcpus, disk, flavorid="auto", if flavorid == "auto": flavorid = None - elif not uuidutils.is_uuid_like(flavorid): - try: - flavorid = int(flavorid) - except (TypeError, ValueError): - raise exceptions.CommandError("Flavor ID must be an integer " - "or a UUID or auto.") try: swap = int(swap) From ce08598a69b7bb20c8e937e7fe8aed3b65e72919 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 7 Aug 2013 18:55:28 -0300 Subject: [PATCH 0232/1705] Updated from global requirements Change-Id: Ic4a43955526fcde313ad2f2afec8fafeb87f37a6 --- requirements.txt | 2 +- setup.py | 2 +- test-requirements.txt | 6 +----- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/requirements.txt b/requirements.txt index 964b63d4b..38ff003fc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -pbr>=0.5.16,<0.6 +pbr>=0.5.21,<1.0 argparse iso8601>=0.1.4 PrettyTable>=0.6,<0.8 diff --git a/setup.py b/setup.py index 15f4e9d54..2a0786a8b 100644 --- a/setup.py +++ b/setup.py @@ -18,5 +18,5 @@ import setuptools setuptools.setup( - setup_requires=['pbr>=0.5.20'], + setup_requires=['pbr>=0.5.21,<1.0'], pbr=True) diff --git a/test-requirements.txt b/test-requirements.txt index 4af426dc8..dcf5afb8e 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,13 +1,9 @@ -# Install bounded pep8/pyflakes first, then let flake8 install -pep8==1.4.5 -pyflakes==0.7.2 -flake8==2.0 hacking>=0.5.6,<0.7 coverage>=3.6 discover fixtures>=0.3.12 -keyring +keyring>=1.6.1 mock>=0.8.0 sphinx>=1.1.2 testrepository>=0.0.17 From 73a0e7298aeb7ff43e70a865d2350923d269db69 Mon Sep 17 00:00:00 2001 From: liuan Date: Thu, 8 Aug 2013 16:45:49 +0800 Subject: [PATCH 0233/1705] change 'Host' object's 'host' attribute to 'host_name' The function __repr__ of Host class which is located in hosts.py try to access 'host' attribute, source code is "self.host",But in fact, Host object don't has this attribute, it should be 'host_name'! We can find this attribute from nova/api/openstack/compute/contrib/hosts.py file. So I simply change 'self.host' to 'self.host_name'. Change-Id: Ie76ba04da7592a596ab728fec981e9a1dbcc6a5f Fixes: bug 1210043 --- novaclient/v1_1/hosts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/v1_1/hosts.py b/novaclient/v1_1/hosts.py index af1756ad2..0b641d44c 100644 --- a/novaclient/v1_1/hosts.py +++ b/novaclient/v1_1/hosts.py @@ -21,7 +21,7 @@ class Host(base.Resource): def __repr__(self): - return "" % self.host + return "" % self.host_name def _add_details(self, info): dico = 'resource' in info and info['resource'] or info From c0e6d24819019dfffb56ec37d8504ae4af163352 Mon Sep 17 00:00:00 2001 From: Ben Nemec Date: Tue, 6 Aug 2013 12:39:49 -0500 Subject: [PATCH 0234/1705] Begin adding v3 api support Create the v3 directory and populate it with some of the basic files from v2 that will be needed. These files are straight copies so that v3-specific changes will be easier to see in subsequent changes. Extensions will be added individually in future changes. bp v3-api Change-Id: I9538c70ccc6fb001ce2fd43ccfd41870f247c67e --- novaclient/v3/__init__.py | 17 + novaclient/v3/client.py | 171 ++ novaclient/v3/shell.py | 3252 +++++++++++++++++++++++++++++++++++++ 3 files changed, 3440 insertions(+) create mode 100644 novaclient/v3/__init__.py create mode 100644 novaclient/v3/client.py create mode 100644 novaclient/v3/shell.py diff --git a/novaclient/v3/__init__.py b/novaclient/v3/__init__.py new file mode 100644 index 000000000..19712285f --- /dev/null +++ b/novaclient/v3/__init__.py @@ -0,0 +1,17 @@ +# Copyright (c) 2012 OpenStack Foundation +# +# All Rights Reserved. +# +# 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 novaclient.v1_1.client import Client # noqa diff --git a/novaclient/v3/client.py b/novaclient/v3/client.py new file mode 100644 index 000000000..50ab46cff --- /dev/null +++ b/novaclient/v3/client.py @@ -0,0 +1,171 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright 2012 OpenStack Foundation +# Copyright 2013 IBM Corp. +# +# 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 novaclient import client +from novaclient.v1_1 import agents +from novaclient.v1_1 import certs +from novaclient.v1_1 import cloudpipe +from novaclient.v1_1 import aggregates +from novaclient.v1_1 import availability_zones +from novaclient.v1_1 import coverage_ext +from novaclient.v1_1 import flavors +from novaclient.v1_1 import flavor_access +from novaclient.v1_1 import floating_ip_dns +from novaclient.v1_1 import floating_ips +from novaclient.v1_1 import floating_ip_pools +from novaclient.v1_1 import fping +from novaclient.v1_1 import hosts +from novaclient.v1_1 import hypervisors +from novaclient.v1_1 import images +from novaclient.v1_1 import keypairs +from novaclient.v1_1 import limits +from novaclient.v1_1 import networks +from novaclient.v1_1 import quota_classes +from novaclient.v1_1 import quotas +from novaclient.v1_1 import security_group_rules +from novaclient.v1_1 import security_groups +from novaclient.v1_1 import servers +from novaclient.v1_1 import usage +from novaclient.v1_1 import virtual_interfaces +from novaclient.v1_1 import volumes +from novaclient.v1_1 import volume_snapshots +from novaclient.v1_1 import volume_types +from novaclient.v1_1 import services +from novaclient.v1_1 import fixed_ips +from novaclient.v1_1 import floating_ips_bulk + + +class Client(object): + """ + Top-level object to access the OpenStack Compute API. + + Create an instance with your creds:: + + >>> client = Client(USERNAME, PASSWORD, PROJECT_ID, AUTH_URL) + + Then call methods on its managers:: + + >>> client.servers.list() + ... + >>> client.flavors.list() + ... + + """ + + # FIXME(jesse): project_id isn't required to authenticate + def __init__(self, username, api_key, project_id, auth_url=None, + insecure=False, timeout=None, proxy_tenant_id=None, + proxy_token=None, region_name=None, + endpoint_type='publicURL', extensions=None, + service_type='compute', service_name=None, + volume_service_name=None, timings=False, + bypass_url=None, os_cache=False, no_cache=True, + http_log_debug=False, auth_system='keystone', + auth_plugin=None, + cacert=None, tenant_id=None): + # FIXME(comstud): Rename the api_key argument above when we + # know it's not being used as keyword argument + password = api_key + self.projectid = project_id + self.tenant_id = tenant_id + self.flavors = flavors.FlavorManager(self) + self.flavor_access = flavor_access.FlavorAccessManager(self) + self.images = images.ImageManager(self) + self.limits = limits.LimitsManager(self) + self.servers = servers.ServerManager(self) + + # extensions + self.agents = agents.AgentsManager(self) + self.dns_domains = floating_ip_dns.FloatingIPDNSDomainManager(self) + self.dns_entries = floating_ip_dns.FloatingIPDNSEntryManager(self) + self.cloudpipe = cloudpipe.CloudpipeManager(self) + self.certs = certs.CertificateManager(self) + self.floating_ips = floating_ips.FloatingIPManager(self) + self.floating_ip_pools = floating_ip_pools.FloatingIPPoolManager(self) + self.fping = fping.FpingManager(self) + self.volumes = volumes.VolumeManager(self) + self.volume_snapshots = volume_snapshots.SnapshotManager(self) + self.volume_types = volume_types.VolumeTypeManager(self) + self.keypairs = keypairs.KeypairManager(self) + self.networks = networks.NetworkManager(self) + self.quota_classes = quota_classes.QuotaClassSetManager(self) + self.quotas = quotas.QuotaSetManager(self) + self.security_groups = security_groups.SecurityGroupManager(self) + self.security_group_rules = \ + security_group_rules.SecurityGroupRuleManager(self) + self.usage = usage.UsageManager(self) + self.virtual_interfaces = \ + virtual_interfaces.VirtualInterfaceManager(self) + self.aggregates = aggregates.AggregateManager(self) + self.hosts = hosts.HostManager(self) + self.hypervisors = hypervisors.HypervisorManager(self) + self.services = services.ServiceManager(self) + self.fixed_ips = fixed_ips.FixedIPsManager(self) + self.floating_ips_bulk = floating_ips_bulk.FloatingIPBulkManager(self) + self.os_cache = os_cache or not no_cache + self.coverage = coverage_ext.CoverageManager(self) + self.availability_zones = \ + availability_zones.AvailabilityZoneManager(self) + + # Add in any extensions... + if extensions: + for extension in extensions: + if extension.manager_class: + setattr(self, extension.name, + extension.manager_class(self)) + + self.client = client.HTTPClient(username, + password, + projectid=project_id, + tenant_id=tenant_id, + auth_url=auth_url, + insecure=insecure, + timeout=timeout, + auth_system=auth_system, + auth_plugin=auth_plugin, + proxy_token=proxy_token, + proxy_tenant_id=proxy_tenant_id, + region_name=region_name, + endpoint_type=endpoint_type, + service_type=service_type, + service_name=service_name, + volume_service_name=volume_service_name, + timings=timings, + bypass_url=bypass_url, + os_cache=self.os_cache, + http_log_debug=http_log_debug, + cacert=cacert) + + def set_management_url(self, url): + self.client.set_management_url(url) + + def get_timings(self): + return self.client.get_timings() + + def reset_timings(self): + self.client.reset_timings() + + def authenticate(self): + """ + Authenticate against the server. + + Normally this is called automatically when you first access the API, + but you can call this method to force authentication right now. + + Returns on success; raises :exc:`exceptions.Unauthorized` if the + credentials are wrong. + """ + self.client.authenticate() diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py new file mode 100644 index 000000000..7358a87a9 --- /dev/null +++ b/novaclient/v3/shell.py @@ -0,0 +1,3252 @@ +# Copyright 2010 Jacob Kaplan-Moss + +# Copyright 2011 OpenStack Foundation +# Copyright 2013 IBM Corp. +# All Rights Reserved. +# +# 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 __future__ import print_function + +import argparse +import copy +import datetime +import getpass +import locale +import os +import sys +import time + +from novaclient import exceptions +from novaclient.openstack.common import strutils +from novaclient.openstack.common import timeutils +from novaclient.openstack.common import uuidutils +from novaclient import utils +from novaclient.v1_1 import availability_zones +from novaclient.v1_1 import quotas +from novaclient.v1_1 import servers + + +def _key_value_pairing(text): + try: + (k, v) = text.split('=', 1) + return (k, v) + except ValueError: + msg = "%r is not in the format of key=value" % text + raise argparse.ArgumentTypeError(msg) + + +def _match_image(cs, wanted_properties): + image_list = cs.images.list() + images_matched = [] + match = set(wanted_properties) + for img in image_list: + try: + if match == match.intersection(set(img.metadata.items())): + images_matched.append(img) + except AttributeError: + pass + return images_matched + + +def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): + """Boot a new server.""" + if min_count is None: + min_count = 1 + if max_count is None: + max_count = min_count + if min_count > max_count: + raise exceptions.CommandError("min_instances should be <= " + "max_instances") + if not min_count or not max_count: + raise exceptions.CommandError("min_instances nor max_instances should" + "be 0") + + if args.image: + image = _find_image(cs, args.image) + else: + image = None + + if not image and args.image_with: + images = _match_image(cs, args.image_with) + if images: + # TODO(harlowja): log a warning that we + # are selecting the first of many? + image = images[0] + + if not image and not args.block_device_mapping: + raise exceptions.CommandError("you need to specify an Image ID " + "or a block device mapping " + "or provide a set of properties to match" + " against an image") + if not args.flavor: + raise exceptions.CommandError("you need to specify a Flavor ID ") + + if args.num_instances is not None: + if args.num_instances <= 1: + raise exceptions.CommandError("num_instances should be > 1") + max_count = args.num_instances + + flavor = _find_flavor(cs, args.flavor) + + meta = dict(v.split('=', 1) for v in args.meta) + + files = {} + for f in args.files: + try: + dst, src = f.split('=', 1) + files[dst] = open(src) + except IOError as e: + raise exceptions.CommandError("Can't open '%s': %s" % (src, e)) + except ValueError as e: + raise exceptions.CommandError("Invalid file argument '%s'. File " + "arguments must be of the form '--file '" % f) + + # use the os-keypair extension + key_name = None + if args.key_name is not None: + key_name = args.key_name + + if args.user_data: + try: + userdata = open(args.user_data) + except IOError as e: + raise exceptions.CommandError("Can't open '%s': %s" % + (args.user_data, e)) + else: + userdata = None + + if args.availability_zone: + availability_zone = args.availability_zone + else: + availability_zone = None + + if args.security_groups: + security_groups = args.security_groups.split(',') + else: + security_groups = None + + block_device_mapping = {} + for bdm in args.block_device_mapping: + device_name, mapping = bdm.split('=', 1) + block_device_mapping[device_name] = mapping + + nics = [] + for nic_str in args.nics: + err_msg = ("Invalid nic argument '%s'. Nic arguments must be of the " + "form --nic , with at minimum net-id or port-id " + "specified." % nic_str) + nic_info = {"net-id": "", "v4-fixed-ip": "", "port-id": ""} + + for kv_str in nic_str.split(","): + try: + k, v = kv_str.split("=", 1) + except ValueError as e: + raise exceptions.CommandError(err_msg) + + if k in nic_info: + nic_info[k] = v + else: + raise exceptions.CommandError(err_msg) + + if not nic_info['net-id'] and not nic_info['port-id']: + raise exceptions.CommandError(err_msg) + + nics.append(nic_info) + + hints = {} + if args.scheduler_hints: + for hint in args.scheduler_hints: + key, _sep, value = hint.partition('=') + # NOTE(vish): multiple copies of the same hint will + # result in a list of values + if key in hints: + if isinstance(hints[key], basestring): + hints[key] = [hints[key]] + hints[key] += [value] + else: + hints[key] = value + boot_args = [args.name, image, flavor] + + if str(args.config_drive).lower() in ("true", "1"): + config_drive = True + elif str(args.config_drive).lower() in ("false", "0", "", "none"): + config_drive = None + else: + config_drive = args.config_drive + + boot_kwargs = dict( + meta=meta, + files=files, + key_name=key_name, + reservation_id=reservation_id, + min_count=min_count, + max_count=max_count, + userdata=userdata, + availability_zone=availability_zone, + security_groups=security_groups, + block_device_mapping=block_device_mapping, + nics=nics, + scheduler_hints=hints, + config_drive=config_drive) + + return boot_args, boot_kwargs + + +@utils.arg('--flavor', + default=None, + metavar='', + help="Flavor ID (see 'nova flavor-list').") +@utils.arg('--image', + default=None, + metavar='', + help="Image ID (see 'nova image-list'). ") +@utils.arg('--image-with', + default=[], + type=_key_value_pairing, + action='append', + metavar='', + help="Image metadata property (see 'nova image-show'). ") +@utils.arg('--num-instances', + default=None, + type=int, + metavar='', + help="boot multi instances at a time") +@utils.arg('--meta', + metavar="", + action='append', + default=[], + help="Record arbitrary key/value metadata to /meta.js " + "on the new server. Can be specified multiple times.") +@utils.arg('--file', + metavar="", + action='append', + dest='files', + default=[], + help="Store arbitrary files from locally to " + "on the new server. You may store up to 5 files.") +@utils.arg('--key-name', + metavar='', + help="Key name of keypair that should be created earlier with \ + the command keypair-add") +@utils.arg('--key_name', + help=argparse.SUPPRESS) +@utils.arg('name', metavar='', help='Name for the new server') +@utils.arg('--user-data', + default=None, + metavar='', + help="user data file to pass to be exposed by the metadata server.") +@utils.arg('--user_data', + help=argparse.SUPPRESS) +@utils.arg('--availability-zone', + default=None, + metavar='', + help="The availability zone for instance placement.") +@utils.arg('--availability_zone', + help=argparse.SUPPRESS) +@utils.arg('--security-groups', + default=None, + metavar='', + help="Comma separated list of security group names.") +@utils.arg('--security_groups', + help=argparse.SUPPRESS) +@utils.arg('--block-device-mapping', + metavar="", + action='append', + default=[], + help="Block device mapping in the format " + "=:::.") +@utils.arg('--block_device_mapping', + action='append', + help=argparse.SUPPRESS) +@utils.arg('--hint', + action='append', + dest='scheduler_hints', + default=[], + metavar='', + help="Send arbitrary key/value pairs to the scheduler for custom use.") +@utils.arg('--nic', + metavar="", + action='append', + dest='nics', + default=[], + help="Create a NIC on the server. " + "Specify option multiple times to create multiple NICs. " + "net-id: attach NIC to network with this UUID " + "(required if no port-id), " + "v4-fixed-ip: IPv4 fixed address for NIC (optional), " + "port-id: attach NIC to port with this UUID " + "(required if no net-id)") +@utils.arg('--config-drive', + metavar="", + dest='config_drive', + default=False, + help="Enable config drive") +@utils.arg('--poll', + dest='poll', + action="store_true", + default=False, + help='Blocks while instance builds so progress can be reported.') +def do_boot(cs, args): + """Boot a new server.""" + boot_args, boot_kwargs = _boot(cs, args) + + extra_boot_kwargs = utils.get_resource_manager_extra_kwargs(do_boot, args) + boot_kwargs.update(extra_boot_kwargs) + + server = cs.servers.create(*boot_args, **boot_kwargs) + + # Keep any information (like adminPass) returned by create + info = server._info + server = cs.servers.get(info['id']) + info.update(server._info) + + flavor = info.get('flavor', {}) + flavor_id = flavor.get('id', '') + info['flavor'] = _find_flavor(cs, flavor_id).name + + image = info.get('image', {}) + if image: + image_id = image.get('id', '') + info['image'] = _find_image(cs, image_id).name + else: # Booting from volume + info['image'] = "Attempt to boot from volume - no image supplied" + + info.pop('links', None) + info.pop('addresses', None) + + utils.print_dict(info) + + if args.poll: + _poll_for_status(cs.servers.get, info['id'], 'building', ['active']) + + +def do_cloudpipe_list(cs, _args): + """Print a list of all cloudpipe instances.""" + cloudpipes = cs.cloudpipe.list() + columns = ['Project Id', "Public IP", "Public Port", "Internal IP"] + utils.print_list(cloudpipes, columns) + + +@utils.arg('project', metavar='', help='Name of the project.') +def do_cloudpipe_create(cs, args): + """Create a cloudpipe instance for the given project.""" + cs.cloudpipe.create(args.project) + + +@utils.arg('address', metavar='', help='New IP Address.') +@utils.arg('port', metavar='', help='New Port.') +def do_cloudpipe_configure(cs, args): + """Update the VPN IP/port of a cloudpipe instance.""" + cs.cloudpipe.update(args.address, args.port) + + +def _poll_for_status(poll_fn, obj_id, action, final_ok_states, + poll_period=5, show_progress=True, + status_field="status", silent=False): + """Block while an action is being performed, periodically printing + progress. + """ + def print_progress(progress): + if show_progress: + msg = ('\rInstance %(action)s... %(progress)s%% complete' + % dict(action=action, progress=progress)) + else: + msg = '\rInstance %(action)s...' % dict(action=action) + + sys.stdout.write(msg) + sys.stdout.flush() + + if not silent: + print + + while True: + obj = poll_fn(obj_id) + + status = getattr(obj, status_field) + + if status: + status = status.lower() + + progress = getattr(obj, 'progress', None) or 0 + if status in final_ok_states: + if not silent: + print_progress(100) + print("\nFinished") + break + elif status == "error": + if not silent: + print("\nError %(action)s instance" % locals()) + break + + if not silent: + print_progress(progress) + + time.sleep(poll_period) + + +def _translate_keys(collection, convert): + for item in collection: + keys = item.__dict__.keys() + for from_key, to_key in convert: + if from_key in keys and to_key not in keys: + setattr(item, to_key, item._info[from_key]) + + +def _translate_extended_states(collection): + power_states = [ + 'NOSTATE', # 0x00 + 'Running', # 0x01 + '', # 0x02 + 'Paused', # 0x03 + 'Shutdown', # 0x04 + '', # 0x05 + 'Crashed', # 0x06 + 'Suspended' # 0x07 + ] + + for item in collection: + try: + setattr(item, 'power_state', + power_states[getattr(item, 'power_state')] + ) + except AttributeError: + setattr(item, 'power_state', "N/A") + try: + getattr(item, 'task_state') + except AttributeError: + setattr(item, 'task_state', "N/A") + + +def _translate_flavor_keys(collection): + _translate_keys(collection, [('ram', 'memory_mb')]) + + +def _print_flavor_extra_specs(flavor): + try: + return flavor.get_keys() + except exceptions.NotFound: + return "N/A" + + +def _print_flavor_list(flavors, show_extra_specs=False): + _translate_flavor_keys(flavors) + + headers = [ + 'ID', + 'Name', + 'Memory_MB', + 'Disk', + 'Ephemeral', + 'Swap', + 'VCPUs', + 'RXTX_Factor', + 'Is_Public', + ] + + if show_extra_specs: + formatters = {'extra_specs': _print_flavor_extra_specs} + headers.append('extra_specs') + else: + formatters = {} + + utils.print_list(flavors, headers, formatters) + + +@utils.arg('--extra-specs', + dest='extra_specs', + action='store_true', + default=False, + help='Get extra-specs of each flavor.') +@utils.arg('--all', + dest='all', + action='store_true', + default=False, + help='Display all flavors (Admin only).') +def do_flavor_list(cs, args): + """Print a list of available 'flavors' (sizes of servers).""" + if args.all: + flavors = cs.flavors.list(is_public=None) + else: + flavors = cs.flavors.list() + _print_flavor_list(flavors, args.extra_specs) + + +@utils.arg('flavor', + metavar='', + help="Name or ID of the flavor to delete") +def do_flavor_delete(cs, args): + """Delete a specific flavor""" + flavorid = _find_flavor(cs, args.flavor) + cs.flavors.delete(flavorid) + _print_flavor_list([flavorid]) + + +@utils.arg('flavor', + metavar='', + help="Name or ID of flavor") +def do_flavor_show(cs, args): + """Show details about the given flavor.""" + flavor = _find_flavor(cs, args.flavor) + _print_flavor(flavor) + + +@utils.arg('name', + metavar='', + help="Name of the new flavor") +@utils.arg('id', + metavar='', + help="Unique ID (integer or UUID) for the new flavor." + " If specifying 'auto', a UUID will be generated as id") +@utils.arg('ram', + metavar='', + help="Memory size in MB") +@utils.arg('disk', + metavar='', + help="Disk size in GB") +@utils.arg('--ephemeral', + metavar='', + help="Ephemeral space size in GB (default 0)", + default=0) +@utils.arg('vcpus', + metavar='', + help="Number of vcpus") +@utils.arg('--swap', + metavar='', + help="Swap space size in MB (default 0)", + default=0) +@utils.arg('--rxtx-factor', + metavar='', + help="RX/TX factor (default 1)", + default=1.0) +@utils.arg('--is-public', + metavar='', + help="Make flavor accessible to the public (default true)", + type=utils.bool_from_str, + default=True) +def do_flavor_create(cs, args): + """Create a new flavor""" + f = cs.flavors.create(args.name, args.ram, args.vcpus, args.disk, args.id, + args.ephemeral, args.swap, args.rxtx_factor, + args.is_public) + _print_flavor_list([f]) + + +@utils.arg('flavor', + metavar='', + help="Name or ID of flavor") +@utils.arg('action', + metavar='', + choices=['set', 'unset'], + help="Actions: 'set' or 'unset'") +@utils.arg('metadata', + metavar='', + nargs='+', + action='append', + default=[], + help='Extra_specs to set/unset (only key is necessary on unset)') +def do_flavor_key(cs, args): + """Set or unset extra_spec for a flavor.""" + flavor = _find_flavor(cs, args.flavor) + keypair = _extract_metadata(args) + + if args.action == 'set': + flavor.set_keys(keypair) + elif args.action == 'unset': + flavor.unset_keys(keypair.keys()) + + +@utils.arg('--flavor', + metavar='', + help="Filter results by flavor name or ID.") +@utils.arg('--tenant', metavar='', + help='Filter results by tenant ID.') +def do_flavor_access_list(cs, args): + """Print access information about the given flavor.""" + if args.flavor and args.tenant: + raise exceptions.CommandError("Unable to filter results by " + "both --flavor and --tenant.") + elif args.flavor: + flavor = _find_flavor(cs, args.flavor) + if flavor.is_public: + raise exceptions.CommandError("Failed to get access list " + "for public flavor type.") + kwargs = {'flavor': flavor} + elif args.tenant: + kwargs = {'tenant': args.tenant} + else: + raise exceptions.CommandError("Unable to get all access lists. " + "Specify --flavor or --tenant") + + try: + access_list = cs.flavor_access.list(**kwargs) + except NotImplementedError as e: + raise exceptions.CommandError("%s" % str(e)) + + columns = ['Flavor_ID', 'Tenant_ID'] + utils.print_list(access_list, columns) + + +@utils.arg('flavor', + metavar='', + help="Filter results by flavor name or ID.") +@utils.arg('tenant', metavar='', + help='Filter results by tenant ID.') +def do_flavor_access_add(cs, args): + """Add flavor access for the given tenant.""" + flavor = _find_flavor(cs, args.flavor) + access_list = cs.flavor_access.add_tenant_access(flavor, args.tenant) + columns = ['Flavor_ID', 'Tenant_ID'] + utils.print_list(access_list, columns) + + +@utils.arg('flavor', + metavar='', + help="Filter results by flavor name or ID.") +@utils.arg('tenant', metavar='', + help='Filter results by tenant ID.') +def do_flavor_access_remove(cs, args): + """Remove flavor access for the given tenant.""" + flavor = _find_flavor(cs, args.flavor) + access_list = cs.flavor_access.remove_tenant_access(flavor, args.tenant) + columns = ['Flavor_ID', 'Tenant_ID'] + utils.print_list(access_list, columns) + + +@utils.arg('project_id', metavar='', + help='The ID of the project.') +def do_scrub(cs, args): + """Delete data associated with the project.""" + networks_list = cs.networks.list() + networks_list = [network for network in networks_list + if getattr(network, 'project_id', '') == args.project_id] + search_opts = {'all_tenants': 1} + groups = cs.security_groups.list(search_opts) + groups = [group for group in groups + if group.tenant_id == args.project_id] + for network in networks_list: + cs.networks.disassociate(network) + for group in groups: + cs.security_groups.delete(group) + + +def do_network_list(cs, _args): + """Print a list of available networks.""" + network_list = cs.networks.list() + columns = ['ID', 'Label', 'Cidr'] + utils.print_list(network_list, columns) + + +@utils.arg('network', + metavar='', + help="uuid or label of network") +def do_network_show(cs, args): + """Show details about the given network.""" + network = utils.find_resource(cs.networks, args.network) + utils.print_dict(network._info) + + +@utils.arg('--host-only', + dest='host_only', + metavar='<0|1>', + nargs='?', + type=int, + const=1, + default=0) +@utils.arg('--project-only', + dest='project_only', + metavar='<0|1>', + nargs='?', + type=int, + const=1, + default=0) +@utils.arg('network', + metavar='', + help="uuid of network") +def do_network_disassociate(cs, args): + """Disassociate host and/or project from the given network.""" + if args.host_only: + cs.networks.disassociate(args.network, True, False) + elif args.project_only: + cs.networks.disassociate(args.network, False, True) + else: + cs.networks.disassociate(args.network, True, True) + + +@utils.arg('network', + metavar='', + help="uuid of network") +@utils.arg('host', + metavar='', + help="Name of host") +def do_network_associate_host(cs, args): + """Associate host with network.""" + cs.networks.associate_host(args.network, args.host) + + +@utils.arg('network', + metavar='', + help="uuid of network") +def do_network_associate_project(cs, args): + """Associate project with network.""" + cs.networks.associate_project(args.network) + + +def _filter_network_create_options(args): + valid_args = ['label', 'cidr', 'vlan_start', 'vpn_start', 'cidr_v6', + 'gateway', 'gateway_v6', 'bridge', 'bridge_interface', + 'multi_host', 'dns1', 'dns2', 'uuid', 'fixed_cidr', + 'project_id', 'priority'] + kwargs = {} + for k, v in args.__dict__.items(): + if k in valid_args and v is not None: + kwargs[k] = v + + return kwargs + + +@utils.arg('label', + metavar='', + help="Label for network") +@utils.arg('--fixed-range-v4', + dest='cidr', + metavar='', + help="IPv4 subnet (ex: 10.0.0.0/8)") +@utils.arg('--fixed-range-v6', + dest="cidr_v6", + help='IPv6 subnet (ex: fe80::/64') +@utils.arg('--vlan', + dest='vlan_start', + metavar='', + help="vlan id") +@utils.arg('--vpn', + dest='vpn_start', + metavar='', + help="vpn start") +@utils.arg('--gateway', + dest="gateway", + help='gateway') +@utils.arg('--gateway-v6', + dest="gateway_v6", + help='ipv6 gateway') +@utils.arg('--bridge', + dest="bridge", + metavar='', + help='VIFs on this network are connected to this bridge') +@utils.arg('--bridge-interface', + dest="bridge_interface", + metavar='', + help='the bridge is connected to this interface') +@utils.arg('--multi-host', + dest="multi_host", + metavar="<'T'|'F'>", + help='Multi host') +@utils.arg('--dns1', + dest="dns1", + metavar="", help='First DNS') +@utils.arg('--dns2', + dest="dns2", + metavar="", + help='Second DNS') +@utils.arg('--uuid', + dest="uuid", + metavar="", + help='Network UUID') +@utils.arg('--fixed-cidr', + dest="fixed_cidr", + metavar='', + help='IPv4 subnet for fixed IPS (ex: 10.20.0.0/16)') +@utils.arg('--project-id', + dest="project_id", + metavar="", + help='Project id') +@utils.arg('--priority', + dest="priority", + metavar="", + help='Network interface priority') +def do_network_create(cs, args): + """Create a network.""" + + if not (args.cidr or args.cidr_v6): + raise exceptions.CommandError( + "Must specify eith fixed_range_v4 or fixed_range_v6") + kwargs = _filter_network_create_options(args) + if args.multi_host is not None: + kwargs['multi_host'] = bool(args.multi_host == 'T' or + strutils.bool_from_string(args.multi_host)) + + cs.networks.create(**kwargs) + + +@utils.arg('--limit', + dest="limit", + metavar="", + help='number of images to return per request') +def do_image_list(cs, _args): + """Print a list of available images to boot from.""" + limit = _args.limit + image_list = cs.images.list(limit=limit) + + def parse_server_name(image): + try: + return image.server['id'] + except (AttributeError, KeyError): + return '' + + fmts = {'Server': parse_server_name} + utils.print_list(image_list, ['ID', 'Name', 'Status', 'Server'], + fmts, sortby_index=1) + + +@utils.arg('image', + metavar='', + help="Name or ID of image") +@utils.arg('action', + metavar='', + choices=['set', 'delete'], + help="Actions: 'set' or 'delete'") +@utils.arg('metadata', + metavar='', + nargs='+', + action='append', + default=[], + help='Metadata to add/update or delete (only key is necessary on delete)') +def do_image_meta(cs, args): + """Set or Delete metadata on an image.""" + image = _find_image(cs, args.image) + metadata = _extract_metadata(args) + + if args.action == 'set': + cs.images.set_meta(image, metadata) + elif args.action == 'delete': + cs.images.delete_meta(image, metadata.keys()) + + +def _extract_metadata(args): + metadata = {} + for metadatum in args.metadata[0]: + # Can only pass the key in on 'delete' + # So this doesn't have to have '=' + if metadatum.find('=') > -1: + (key, value) = metadatum.split('=', 1) + else: + key = metadatum + value = None + + metadata[key] = value + return metadata + + +def _print_image(image): + info = image._info.copy() + + # ignore links, we don't need to present those + info.pop('links') + + # try to replace a server entity to just an id + server = info.pop('server', None) + try: + info['server'] = server['id'] + except (KeyError, TypeError): + pass + + # break up metadata and display each on its own row + metadata = info.pop('metadata', {}) + try: + for key, value in metadata.items(): + _key = 'metadata %s' % key + info[_key] = value + except AttributeError: + pass + + utils.print_dict(info) + + +def _print_flavor(flavor): + info = flavor._info.copy() + # ignore links, we don't need to present those + info.pop('links') + info.update({"extra_specs": _print_flavor_extra_specs(flavor)}) + utils.print_dict(info) + + +@utils.arg('image', + metavar='', + help="Name or ID of image") +def do_image_show(cs, args): + """Show details about the given image.""" + image = _find_image(cs, args.image) + _print_image(image) + + +@utils.arg('image', metavar='', nargs='+', + help='Name or ID of image(s).') +def do_image_delete(cs, args): + """Delete specified image(s).""" + for image in args.image: + try: + _find_image(cs, image).delete() + except Exception as e: + print("Delete for image %s failed: %s" % (image, e)) + + +@utils.arg('--reservation-id', + dest='reservation_id', + metavar='', + default=None, + help='Only return instances that match reservation-id.') +@utils.arg('--reservation_id', + help=argparse.SUPPRESS) +@utils.arg('--ip', + dest='ip', + metavar='', + default=None, + help='Search with regular expression match by IP address (Admin only).') +@utils.arg('--ip6', + dest='ip6', + metavar='', + default=None, + help='Search with regular expression match by IPv6 address (Admin only).') +@utils.arg('--name', + dest='name', + metavar='', + default=None, + help='Search with regular expression match by name') +@utils.arg('--instance-name', + dest='instance_name', + metavar='', + default=None, + help='Search with regular expression match by instance name (Admin only).') +@utils.arg('--instance_name', + help=argparse.SUPPRESS) +@utils.arg('--status', + dest='status', + metavar='', + default=None, + help='Search by server status') +@utils.arg('--flavor', + dest='flavor', + metavar='', + default=None, + help='Search by flavor name or ID') +@utils.arg('--image', + dest='image', + metavar='', + default=None, + help='Search by image name or ID') +@utils.arg('--host', + dest='host', + metavar='', + default=None, + help='Search instances by hostname to which they are assigned ' + '(Admin only).') +@utils.arg('--all-tenants', + dest='all_tenants', + metavar='<0|1>', + nargs='?', + type=int, + const=1, + default=int(utils.bool_from_str(os.environ.get("ALL_TENANTS", 'false'))), + help='Display information from all tenants (Admin only).') +@utils.arg('--all_tenants', + nargs='?', + type=int, + const=1, + help=argparse.SUPPRESS) +@utils.arg('--tenant', + #nova db searches by project_id + dest='tenant', + metavar='', + nargs='?', + help='Display information from single tenant (Admin only).') +@utils.arg('--fields', + default=None, + metavar='', + help='Comma-separated list of fields to display. ' + 'Use the show command to see which fields are available.') +def do_list(cs, args): + """List active servers.""" + imageid = None + flavorid = None + if args.image: + imageid = _find_image(cs, args.image).id + if args.flavor: + flavorid = _find_flavor(cs, args.flavor).id + search_opts = { + 'all_tenants': args.all_tenants, + 'reservation_id': args.reservation_id, + 'ip': args.ip, + 'ip6': args.ip6, + 'name': args.name, + 'image': imageid, + 'flavor': flavorid, + 'status': args.status, + 'tenant_id': args.tenant, + 'host': args.host, + 'instance_name': args.instance_name} + + filters = {'flavor': lambda f: f['id'], + 'security_groups': utils._format_security_groups} + + formatters = {} + field_titles = [] + if args.fields: + for field in args.fields.split(','): + field_title, formatter = utils._make_field_formatter(field, + filters) + field_titles.append(field_title) + formatters[field_title] = formatter + + id_col = 'ID' + + servers = cs.servers.list(search_opts=search_opts) + convert = [('OS-EXT-SRV-ATTR:host', 'host'), + ('OS-EXT-STS:task_state', 'task_state'), + ('OS-EXT-SRV-ATTR:instance_name', 'instance_name'), + ('OS-EXT-STS:power_state', 'power_state'), + ('hostId', 'host_id')] + _translate_keys(servers, convert) + _translate_extended_states(servers) + if field_titles: + columns = [id_col] + field_titles + else: + columns = [ + id_col, + 'Name', + 'Status', + 'Task State', + 'Power State', + 'Networks' + ] + formatters['Networks'] = utils._format_servers_list_networks + utils.print_list(servers, columns, + formatters, sortby_index=1) + + +@utils.arg('--hard', + dest='reboot_type', + action='store_const', + const=servers.REBOOT_HARD, + default=servers.REBOOT_SOFT, + help='Perform a hard reboot (instead of a soft one).') +@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('--poll', + dest='poll', + action="store_true", + default=False, + help='Blocks while instance is rebooting.') +def do_reboot(cs, args): + """Reboot a server.""" + server = _find_server(cs, args.server) + server.reboot(args.reboot_type) + + if args.poll: + _poll_for_status(cs.servers.get, server.id, 'rebooting', ['active'], + show_progress=False) + + +@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('image', metavar='', help="Name or ID of new image.") +@utils.arg('--rebuild-password', + dest='rebuild_password', + metavar='', + default=False, + help="Set the provided password on the rebuild instance.") +@utils.arg('--rebuild_password', + help=argparse.SUPPRESS) +@utils.arg('--poll', + dest='poll', + action="store_true", + default=False, + help='Blocks while instance rebuilds so progress can be reported.') +@utils.arg('--minimal', + dest='minimal', + action="store_true", + default=False, + help='Skips flavor/image lookups when showing instances') +def do_rebuild(cs, args): + """Shutdown, re-image, and re-boot a server.""" + server = _find_server(cs, args.server) + image = _find_image(cs, args.image) + + if args.rebuild_password is not False: + _password = args.rebuild_password + else: + _password = None + + kwargs = utils.get_resource_manager_extra_kwargs(do_rebuild, args) + server.rebuild(image, _password, **kwargs) + _print_server(cs, args) + + if args.poll: + _poll_for_status(cs.servers.get, server.id, 'rebuilding', ['active']) + + +@utils.arg('server', metavar='', + help='Name (old name) or ID of server.') +@utils.arg('name', metavar='', help='New name for the server.') +def do_rename(cs, args): + """Rename a server.""" + _find_server(cs, args.server).update(name=args.name) + + +@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('flavor', metavar='', help="Name or ID of new flavor.") +@utils.arg('--poll', + dest='poll', + action="store_true", + default=False, + help='Blocks while instance resizes so progress can be reported.') +def do_resize(cs, args): + """Resize a server.""" + server = _find_server(cs, args.server) + flavor = _find_flavor(cs, args.flavor) + kwargs = utils.get_resource_manager_extra_kwargs(do_resize, args) + server.resize(flavor, **kwargs) + if args.poll: + _poll_for_status(cs.servers.get, server.id, 'resizing', + ['active', 'verify_resize']) + + +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_resize_confirm(cs, args): + """Confirm a previous resize.""" + _find_server(cs, args.server).confirm_resize() + + +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_resize_revert(cs, args): + """Revert a previous resize (and return to the previous VM).""" + _find_server(cs, args.server).revert_resize() + + +@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('--poll', + dest='poll', + action="store_true", + default=False, + help='Blocks while instance migrates so progress can be reported.') +def do_migrate(cs, args): + """Migrate a server. The new host will be selected by the scheduler.""" + server = _find_server(cs, args.server) + server.migrate() + + if args.poll: + _poll_for_status(cs.servers.get, server.id, 'migrating', + ['active', 'verify_resize']) + + +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_pause(cs, args): + """Pause a server.""" + _find_server(cs, args.server).pause() + + +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_unpause(cs, args): + """Unpause a server.""" + _find_server(cs, args.server).unpause() + + +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_stop(cs, args): + """Stop a server.""" + _find_server(cs, args.server).stop() + + +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_start(cs, args): + """Start a server.""" + _find_server(cs, args.server).start() + + +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_lock(cs, args): + """Lock a server.""" + _find_server(cs, args.server).lock() + + +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_unlock(cs, args): + """Unlock a server.""" + _find_server(cs, args.server).unlock() + + +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_suspend(cs, args): + """Suspend a server.""" + _find_server(cs, args.server).suspend() + + +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_resume(cs, args): + """Resume a server.""" + _find_server(cs, args.server).resume() + + +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_rescue(cs, args): + """Rescue a server.""" + utils.print_dict(_find_server(cs, args.server).rescue()[1]) + + +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_unrescue(cs, args): + """Unrescue a server.""" + _find_server(cs, args.server).unrescue() + + +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_diagnostics(cs, args): + """Retrieve server diagnostics.""" + server = _find_server(cs, args.server) + utils.print_dict(cs.servers.diagnostics(server)[1]) + + +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_root_password(cs, args): + """ + Change the root password for a server. + """ + server = _find_server(cs, args.server) + p1 = getpass.getpass('New password: ') + p2 = getpass.getpass('Again: ') + if p1 != p2: + raise exceptions.CommandError("Passwords do not match.") + server.change_password(p1) + + +@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('name', metavar='', help='Name of snapshot.') +@utils.arg('--poll', + dest='poll', + action="store_true", + default=False, + help='Blocks while instance snapshots so progress can be reported.') +def do_image_create(cs, args): + """Create a new image by taking a snapshot of a running server.""" + server = _find_server(cs, args.server) + image_uuid = cs.servers.create_image(server, args.name) + + if args.poll: + _poll_for_status(cs.images.get, image_uuid, 'snapshotting', + ['active']) + + # NOTE(sirp): A race-condition exists between when the image finishes + # uploading and when the servers's `task_state` is cleared. To account + # for this, we need to poll a second time to ensure the `task_state` is + # cleared before returning, ensuring that a snapshot taken immediately + # after this function returns will succeed. + # + # A better long-term solution will be to separate 'snapshotting' and + # 'image-uploading' in Nova and clear the task-state once the VM + # snapshot is complete but before the upload begins. + task_state_field = "OS-EXT-STS:task_state" + if hasattr(server, task_state_field): + _poll_for_status(cs.servers.get, server.id, 'image_snapshot', + [None], status_field=task_state_field, + show_progress=False, silent=True) + + +@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('name', metavar='', help='Name of the backup image.') +@utils.arg('backup_type', metavar='', + help='The backup type, like "daily" or "weekly".') +@utils.arg('rotation', metavar='', + help='Int parameter representing how many backups to keep around.') +def do_backup(cs, args): + """Backup a instance by create a 'backup' type snapshot.""" + _find_server(cs, args.server).backup(args.name, + args.backup_type, + args.rotation) + + +@utils.arg('server', + metavar='', + help="Name or ID of server") +@utils.arg('action', + metavar='', + choices=['set', 'delete'], + help="Actions: 'set' or 'delete'") +@utils.arg('metadata', + metavar='', + nargs='+', + action='append', + default=[], + help='Metadata to set or delete (only key is necessary on delete)') +def do_meta(cs, args): + """Set or Delete metadata on a server.""" + server = _find_server(cs, args.server) + metadata = _extract_metadata(args) + + if args.action == 'set': + cs.servers.set_meta(server, metadata) + elif args.action == 'delete': + cs.servers.delete_meta(server, metadata.keys()) + + +def _print_server(cs, args): + # By default when searching via name we will do a + # findall(name=blah) and due a REST /details which is not the same + # as a .get() and doesn't get the information about flavors and + # images. This fix it as we redo the call with the id which does a + # .get() to get all informations. + server = _find_server(cs, args.server) + + networks = server.networks + info = server._info.copy() + for network_label, address_list in networks.items(): + info['%s network' % network_label] = ', '.join(address_list) + + flavor = info.get('flavor', {}) + flavor_id = flavor.get('id', '') + if args.minimal: + info['flavor'] = flavor_id + else: + info['flavor'] = '%s (%s)' % (_find_flavor(cs, flavor_id).name, + flavor_id) + + image = info.get('image', {}) + if image: + image_id = image.get('id', '') + if args.minimal: + info['image'] = image_id + else: + try: + info['image'] = '%s (%s)' % (_find_image(cs, image_id).name, + image_id) + except Exception: + info['image'] = '%s (%s)' % ("Image not found", image_id) + else: # Booted from volume + info['image'] = "Attempt to boot from volume - no image supplied" + + info.pop('links', None) + info.pop('addresses', None) + + utils.print_dict(info) + + +@utils.arg('--minimal', + dest='minimal', + action="store_true", + default=False, + help='Skips flavor/image lookups when showing instances') +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_show(cs, args): + """Show details about the given server.""" + _print_server(cs, args) + + +@utils.arg('server', metavar='', nargs='+', + help='Name or ID of server(s).') +def do_delete(cs, args): + """Immediately shut down and delete specified server(s).""" + failure_count = 0 + + for server in args.server: + try: + _find_server(cs, server).delete() + except Exception as e: + failure_count += 1 + print(e) + + if failure_count == len(args.server): + raise exceptions.CommandError("Unable to delete any of the specified " + "servers.") + + +def _find_server(cs, server): + """Get a server by name or ID.""" + return utils.find_resource(cs.servers, server) + + +def _find_image(cs, image): + """Get an image by name or ID.""" + return utils.find_resource(cs.images, image) + + +def _find_flavor(cs, flavor): + """Get a flavor by name, ID, or RAM size.""" + try: + return utils.find_resource(cs.flavors, flavor) + except exceptions.NotFound: + return cs.flavors.find(ram=flavor) + + +@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('network_id', + metavar='', + help='Network ID.') +def do_add_fixed_ip(cs, args): + """Add new IP address on a network to server.""" + server = _find_server(cs, args.server) + server.add_fixed_ip(args.network_id) + + +@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('address', metavar='
', help='IP Address.') +def do_remove_fixed_ip(cs, args): + """Remove an IP address from a server.""" + server = _find_server(cs, args.server) + server.remove_fixed_ip(args.address) + + +def _find_volume(cs, volume): + """Get a volume by name or ID.""" + return utils.find_resource(cs.volumes, volume) + + +def _find_volume_snapshot(cs, snapshot): + """Get a volume snapshot by name or ID.""" + return utils.find_resource(cs.volume_snapshots, snapshot) + + +def _print_volume(volume): + utils.print_dict(volume._info) + + +def _print_volume_snapshot(snapshot): + utils.print_dict(snapshot._info) + + +def _translate_volume_keys(collection): + _translate_keys(collection, + [('displayName', 'display_name'), + ('volumeType', 'volume_type')]) + + +def _translate_volume_snapshot_keys(collection): + _translate_keys(collection, + [('displayName', 'display_name'), + ('volumeId', 'volume_id')]) + + +def _translate_availability_zone_keys(collection): + _translate_keys(collection, + [('zoneName', 'name'), ('zoneState', 'status')]) + + +@utils.arg('--all-tenants', + dest='all_tenants', + metavar='<0|1>', + nargs='?', + type=int, + const=1, + default=int(utils.bool_from_str(os.environ.get("ALL_TENANTS", 'false'))), + help='Display information from all tenants (Admin only).') +@utils.arg('--all_tenants', + nargs='?', + type=int, + const=1, + help=argparse.SUPPRESS) +@utils.service_type('volume') +def do_volume_list(cs, args): + """List all the volumes.""" + search_opts = {'all_tenants': args.all_tenants} + volumes = cs.volumes.list(search_opts=search_opts) + _translate_volume_keys(volumes) + + # Create a list of servers to which the volume is attached + for vol in volumes: + servers = [s.get('server_id') for s in vol.attachments] + setattr(vol, 'attached_to', ','.join(map(str, servers))) + utils.print_list(volumes, ['ID', 'Status', 'Display Name', + 'Size', 'Volume Type', 'Attached to']) + + +@utils.arg('volume', metavar='', help='Name or ID of the volume.') +@utils.service_type('volume') +def do_volume_show(cs, args): + """Show details about a volume.""" + volume = _find_volume(cs, args.volume) + _print_volume(volume) + + +@utils.arg('size', + metavar='', + type=int, + help='Size of volume in GB') +@utils.arg('--snapshot-id', + metavar='', + default=None, + help='Optional snapshot id to create the volume from. (Default=None)') +@utils.arg('--snapshot_id', + help=argparse.SUPPRESS) +@utils.arg('--image-id', + metavar='', + help='Optional image id to create the volume from. (Default=None)', + default=None) +@utils.arg('--display-name', + metavar='', + default=None, + help='Optional volume name. (Default=None)') +@utils.arg('--display_name', + help=argparse.SUPPRESS) +@utils.arg('--display-description', + metavar='', + default=None, + help='Optional volume description. (Default=None)') +@utils.arg('--display_description', + help=argparse.SUPPRESS) +@utils.arg('--volume-type', + metavar='', + default=None, + help='Optional volume type. (Default=None)') +@utils.arg('--volume_type', + help=argparse.SUPPRESS) +@utils.arg('--availability-zone', metavar='', + help='Optional Availability Zone for volume. (Default=None)', + default=None) +@utils.service_type('volume') +def do_volume_create(cs, args): + """Add a new volume.""" + volume = cs.volumes.create(args.size, + args.snapshot_id, + args.display_name, + args.display_description, + args.volume_type, + args.availability_zone, + imageRef=args.image_id) + _print_volume(volume) + + +@utils.arg('volume', + metavar='', + help='Name or ID of the volume to delete.') +@utils.service_type('volume') +def do_volume_delete(cs, args): + """Remove a volume.""" + volume = _find_volume(cs, args.volume) + volume.delete() + + +@utils.arg('server', + metavar='', + help='Name or ID of server.') +@utils.arg('volume', + metavar='', + help='ID of the volume to attach.') +@utils.arg('device', metavar='', + help='Name of the device e.g. /dev/vdb. ' + 'Use "auto" for autoassign (if supported)') +def do_volume_attach(cs, args): + """Attach a volume to a server.""" + if args.device == 'auto': + args.device = None + + volume = cs.volumes.create_server_volume(_find_server(cs, args.server).id, + args.volume, + args.device) + _print_volume(volume) + + +@utils.arg('server', + metavar='', + help='Name or ID of server.') +@utils.arg('attachment_id', + metavar='', + help='Attachment ID of the volume.') +def do_volume_detach(cs, args): + """Detach a volume from a server.""" + cs.volumes.delete_server_volume(_find_server(cs, args.server).id, + args.attachment_id) + + +@utils.service_type('volume') +def do_volume_snapshot_list(cs, _args): + """List all the snapshots.""" + snapshots = cs.volume_snapshots.list() + _translate_volume_snapshot_keys(snapshots) + utils.print_list(snapshots, ['ID', 'Volume ID', 'Status', 'Display Name', + 'Size']) + + +@utils.arg('snapshot', + metavar='', + help='Name or ID of the snapshot.') +@utils.service_type('volume') +def do_volume_snapshot_show(cs, args): + """Show details about a snapshot.""" + snapshot = _find_volume_snapshot(cs, args.snapshot) + _print_volume_snapshot(snapshot) + + +@utils.arg('volume_id', + metavar='', + help='ID of the volume to snapshot') +@utils.arg('--force', + metavar='', + help='Optional flag to indicate whether to snapshot a volume even if its ' + 'attached to an instance. (Default=False)', + default=False) +@utils.arg('--display-name', + metavar='', + default=None, + help='Optional snapshot name. (Default=None)') +@utils.arg('--display_name', + help=argparse.SUPPRESS) +@utils.arg('--display-description', + metavar='', + default=None, + help='Optional snapshot description. (Default=None)') +@utils.arg('--display_description', + help=argparse.SUPPRESS) +@utils.service_type('volume') +def do_volume_snapshot_create(cs, args): + """Add a new snapshot.""" + snapshot = cs.volume_snapshots.create(args.volume_id, + args.force, + args.display_name, + args.display_description) + _print_volume_snapshot(snapshot) + + +@utils.arg('snapshot', + metavar='', + help='Name or ID of the snapshot to delete.') +@utils.service_type('volume') +def do_volume_snapshot_delete(cs, args): + """Remove a snapshot.""" + snapshot = _find_volume_snapshot(cs, args.snapshot) + snapshot.delete() + + +def _print_volume_type_list(vtypes): + utils.print_list(vtypes, ['ID', 'Name']) + + +@utils.service_type('volume') +def do_volume_type_list(cs, args): + """Print a list of available 'volume types'.""" + vtypes = cs.volume_types.list() + _print_volume_type_list(vtypes) + + +@utils.arg('name', + metavar='', + help="Name of the new flavor") +@utils.service_type('volume') +def do_volume_type_create(cs, args): + """Create a new volume type.""" + vtype = cs.volume_types.create(args.name) + _print_volume_type_list([vtype]) + + +@utils.arg('id', + metavar='', + help="Unique ID of the volume type to delete") +@utils.service_type('volume') +def do_volume_type_delete(cs, args): + """Delete a specific flavor""" + cs.volume_types.delete(args.id) + + +@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('console_type', + metavar='', + help='Type of vnc console ("novnc" or "xvpvnc").') +def do_get_vnc_console(cs, args): + """Get a vnc console to a server.""" + server = _find_server(cs, args.server) + data = server.get_vnc_console(args.console_type) + + class VNCConsole: + def __init__(self, console_dict): + self.type = console_dict['type'] + self.url = console_dict['url'] + + utils.print_list([VNCConsole(data['console'])], ['Type', 'Url']) + + +@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('console_type', + metavar='', + help='Type of spice console ("spice-html5").') +def do_get_spice_console(cs, args): + """Get a spice console to a server.""" + server = _find_server(cs, args.server) + data = server.get_spice_console(args.console_type) + + class SPICEConsole: + def __init__(self, console_dict): + self.type = console_dict['type'] + self.url = console_dict['url'] + + utils.print_list([SPICEConsole(data['console'])], ['Type', 'Url']) + + +@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('private_key', + metavar='', + help='Private key (used locally to decrypt password).') +def do_get_password(cs, args): + """Get password for a server.""" + server = _find_server(cs, args.server) + data = server.get_password(args.private_key) + print(data) + + +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_clear_password(cs, args): + """Clear password for a server.""" + server = _find_server(cs, args.server) + server.clear_password() + + +def _print_floating_ip_list(floating_ips): + utils.print_list(floating_ips, ['Ip', 'Instance Id', 'Fixed Ip', 'Pool']) + + +@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('--length', + metavar='', + default=None, + help='Length in lines to tail.') +def do_console_log(cs, args): + """Get console log output of a server.""" + server = _find_server(cs, args.server) + data = server.get_console_output(length=args.length) + print(data) + + +@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('address', metavar='
', help='IP Address.') +@utils.arg('--fixed-address', + metavar='', + default=None, + help='Fixed IP Address to associate with.') +def do_add_floating_ip(cs, args): + """Add a floating IP address to a server.""" + server = _find_server(cs, args.server) + server.add_floating_ip(args.address, args.fixed_address) + + +@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('address', metavar='
', help='IP Address.') +def do_remove_floating_ip(cs, args): + """Remove a floating IP address from a server.""" + server = _find_server(cs, args.server) + server.remove_floating_ip(args.address) + + +@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('secgroup', metavar='', help='Name of Security Group.') +def do_add_secgroup(cs, args): + """Add a Security Group to a server.""" + server = _find_server(cs, args.server) + server.add_security_group(args.secgroup) + + +@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('secgroup', metavar='', help='Name of Security Group.') +def do_remove_secgroup(cs, args): + """Remove a Security Group from a server.""" + server = _find_server(cs, args.server) + server.remove_security_group(args.secgroup) + + +@utils.arg('pool', + metavar='', + help='Name of Floating IP Pool. (Optional)', + nargs='?', + default=None) +def do_floating_ip_create(cs, args): + """Allocate a floating IP for the current tenant.""" + _print_floating_ip_list([cs.floating_ips.create(pool=args.pool)]) + + +@utils.arg('address', metavar='
', help='IP of Floating Ip.') +def do_floating_ip_delete(cs, args): + """De-allocate a floating IP.""" + floating_ips = cs.floating_ips.list() + for floating_ip in floating_ips: + if floating_ip.ip == args.address: + return cs.floating_ips.delete(floating_ip.id) + raise exceptions.CommandError("Floating ip %s not found." % args.address) + + +def do_floating_ip_list(cs, _args): + """List floating ips for this tenant.""" + _print_floating_ip_list(cs.floating_ips.list()) + + +def do_floating_ip_pool_list(cs, _args): + """List all floating ip pools.""" + utils.print_list(cs.floating_ip_pools.list(), ['name']) + + +@utils.arg('--host', dest='host', metavar='', default=None, + help='Filter by host') +def do_floating_ip_bulk_list(cs, args): + """List all floating ips.""" + utils.print_list(cs.floating_ips_bulk.list(args.host), ['project_id', + 'address', + 'instance_uuid', + 'pool', + 'interface']) + + +@utils.arg('ip_range', metavar='', help='Address range to create') +@utils.arg('--pool', dest='pool', metavar='', default=None, + help='Pool for new Floating IPs') +@utils.arg('--interface', metavar='', default=None, + help='Interface for new Floating IPs') +def do_floating_ip_bulk_create(cs, args): + """Bulk create floating ips by range.""" + cs.floating_ips_bulk.create(args.ip_range, args.pool, args.interface) + + +@utils.arg('ip_range', metavar='', help='Address range to delete') +def do_floating_ip_bulk_delete(cs, args): + """Bulk delete floating ips by range.""" + cs.floating_ips_bulk.delete(args.ip_range) + + +def _print_dns_list(dns_entries): + utils.print_list(dns_entries, ['ip', 'name', 'domain']) + + +def _print_domain_list(domain_entries): + utils.print_list(domain_entries, ['domain', 'scope', + 'project', 'availability_zone']) + + +def do_dns_domains(cs, args): + """Print a list of available dns domains.""" + domains = cs.dns_domains.domains() + _print_domain_list(domains) + + +@utils.arg('domain', metavar='', help='DNS domain') +@utils.arg('--ip', metavar='', help='ip address', default=None) +@utils.arg('--name', metavar='', help='DNS name', default=None) +def do_dns_list(cs, args): + """List current DNS entries for domain and ip or domain and name.""" + if not (args.ip or args.name): + raise exceptions.CommandError( + "You must specify either --ip or --name") + if args.name: + entry = cs.dns_entries.get(args.domain, args.name) + _print_dns_list([entry]) + else: + entries = cs.dns_entries.get_for_ip(args.domain, + ip=args.ip) + _print_dns_list(entries) + + +@utils.arg('ip', metavar='', help='ip address') +@utils.arg('name', metavar='', help='DNS name') +@utils.arg('domain', metavar='', help='DNS domain') +@utils.arg('--type', metavar='', help='dns type (e.g. "A")', default='A') +def do_dns_create(cs, args): + """Create a DNS entry for domain, name and ip.""" + cs.dns_entries.create(args.domain, args.name, args.ip, args.type) + + +@utils.arg('domain', metavar='', help='DNS domain') +@utils.arg('name', metavar='', help='DNS name') +def do_dns_delete(cs, args): + """Delete the specified DNS entry.""" + cs.dns_entries.delete(args.domain, args.name) + + +@utils.arg('domain', metavar='', help='DNS domain') +def do_dns_delete_domain(cs, args): + """Delete the specified DNS domain.""" + cs.dns_domains.delete(args.domain) + + +@utils.arg('domain', metavar='', help='DNS domain') +@utils.arg('--availability-zone', + metavar='', + default=None, + help='Limit access to this domain to instances ' + 'in the specified availability zone.') +@utils.arg('--availability_zone', + help=argparse.SUPPRESS) +def do_dns_create_private_domain(cs, args): + """Create the specified DNS domain.""" + cs.dns_domains.create_private(args.domain, + args.availability_zone) + + +@utils.arg('domain', metavar='', help='DNS domain') +@utils.arg('--project', metavar='', + help='Limit access to this domain to users ' + 'of the specified project.', + default=None) +def do_dns_create_public_domain(cs, args): + """Create the specified DNS domain.""" + cs.dns_domains.create_public(args.domain, + args.project) + + +def _print_secgroup_rules(rules): + class FormattedRule: + def __init__(self, obj): + items = (obj if isinstance(obj, dict) else obj._info).items() + for k, v in items: + if k == 'ip_range': + v = v.get('cidr') + elif k == 'group': + k = 'source_group' + v = v.get('name') + if v is None: + v = '' + + setattr(self, k, v) + + rules = [FormattedRule(rule) for rule in rules] + utils.print_list(rules, ['IP Protocol', 'From Port', 'To Port', + 'IP Range', 'Source Group']) + + +def _print_secgroups(secgroups): + utils.print_list(secgroups, ['Id', 'Name', 'Description']) + + +def _get_secgroup(cs, secgroup): + # Check secgroup is an ID + if uuidutils.is_uuid_like(strutils.safe_encode(secgroup)): + try: + return cs.security_groups.get(secgroup) + except exceptions.NotFound: + pass + + # Check secgroup as a name + match_found = False + for s in cs.security_groups.list(): + encoding = (locale.getpreferredencoding() or + sys.stdin.encoding or + 'UTF-8') + s.name = s.name.encode(encoding) + if secgroup == s.name: + if match_found != False: + msg = ("Multiple security group matches found for name" + " '%s', use an ID to be more specific." % secgroup) + raise exceptions.NoUniqueMatch(msg) + match_found = s + if match_found is False: + raise exceptions.CommandError("Secgroup ID or name '%s' not found." + % secgroup) + return match_found + + +@utils.arg('secgroup', + metavar='', + help='ID or name of security group.') +@utils.arg('ip_proto', + metavar='', + help='IP protocol (icmp, tcp, udp).') +@utils.arg('from_port', + metavar='', + help='Port at start of range.') +@utils.arg('to_port', + metavar='', + help='Port at end of range.') +@utils.arg('cidr', metavar='', help='CIDR for address range.') +def do_secgroup_add_rule(cs, args): + """Add a rule to a security group.""" + secgroup = _get_secgroup(cs, args.secgroup) + rule = cs.security_group_rules.create(secgroup.id, + args.ip_proto, + args.from_port, + args.to_port, + args.cidr) + _print_secgroup_rules([rule]) + + +@utils.arg('secgroup', + metavar='', + help='ID or name of security group.') +@utils.arg('ip_proto', + metavar='', + help='IP protocol (icmp, tcp, udp).') +@utils.arg('from_port', + metavar='', + help='Port at start of range.') +@utils.arg('to_port', + metavar='', + help='Port at end of range.') +@utils.arg('cidr', metavar='', help='CIDR for address range.') +def do_secgroup_delete_rule(cs, args): + """Delete a rule from a security group.""" + secgroup = _get_secgroup(cs, args.secgroup) + for rule in secgroup.rules: + if (rule['ip_protocol'] and + rule['ip_protocol'].upper() == args.ip_proto.upper() and + rule['from_port'] == int(args.from_port) and + rule['to_port'] == int(args.to_port) and + rule['ip_range']['cidr'] == args.cidr): + _print_secgroup_rules([rule]) + return cs.security_group_rules.delete(rule['id']) + + raise exceptions.CommandError("Rule not found") + + +@utils.arg('name', metavar='', help='Name of security group.') +@utils.arg('description', metavar='', + help='Description of security group.') +def do_secgroup_create(cs, args): + """Create a security group.""" + secgroup = cs.security_groups.create(args.name, args.description) + _print_secgroups([secgroup]) + + +@utils.arg('secgroup', + metavar='', + help='ID or name of security group.') +@utils.arg('name', metavar='', help='Name of security group.') +@utils.arg('description', metavar='', + help='Description of security group.') +def do_secgroup_update(cs, args): + """Update a security group.""" + sg = _get_secgroup(cs, args.secgroup) + secgroup = cs.security_groups.update(sg, args.name, args.description) + _print_secgroups([secgroup]) + + +@utils.arg('secgroup', + metavar='', + help='ID or name of security group.') +def do_secgroup_delete(cs, args): + """Delete a security group.""" + secgroup = _get_secgroup(cs, args.secgroup) + cs.security_groups.delete(secgroup) + _print_secgroups([secgroup]) + + +@utils.arg('--all-tenants', + dest='all_tenants', + metavar='<0|1>', + nargs='?', + type=int, + const=1, + default=int(utils.bool_from_str(os.environ.get("ALL_TENANTS", 'false'))), + help='Display information from all tenants (Admin only).') +@utils.arg('--all_tenants', + nargs='?', + type=int, + const=1, + help=argparse.SUPPRESS) +def do_secgroup_list(cs, args): + """List security groups for the current tenant.""" + search_opts = {'all_tenants': args.all_tenants} + columns = ['Id', 'Name', 'Description'] + if args.all_tenants: + columns.append('Tenant_ID') + groups = cs.security_groups.list(search_opts=search_opts) + utils.print_list(groups, columns) + + +@utils.arg('secgroup', + metavar='', + help='ID or name of security group.') +def do_secgroup_list_rules(cs, args): + """List rules for a security group.""" + secgroup = _get_secgroup(cs, args.secgroup) + _print_secgroup_rules(secgroup.rules) + + +@utils.arg('secgroup', + metavar='', + help='ID or name of security group.') +@utils.arg('source_group', + metavar='', + help='ID or name of source group.') +@utils.arg('ip_proto', + metavar='', + help='IP protocol (icmp, tcp, udp).') +@utils.arg('from_port', + metavar='', + help='Port at start of range.') +@utils.arg('to_port', + metavar='', + help='Port at end of range.') +def do_secgroup_add_group_rule(cs, args): + """Add a source group rule to a security group.""" + secgroup = _get_secgroup(cs, args.secgroup) + source_group = _get_secgroup(cs, args.source_group) + params = {} + params['group_id'] = source_group.id + + if args.ip_proto or args.from_port or args.to_port: + if not (args.ip_proto and args.from_port and args.to_port): + raise exceptions.CommandError("ip_proto, from_port, and to_port" + " must be specified together") + params['ip_protocol'] = args.ip_proto.upper() + params['from_port'] = args.from_port + params['to_port'] = args.to_port + + rule = cs.security_group_rules.create(secgroup.id, **params) + _print_secgroup_rules([rule]) + + +@utils.arg('secgroup', + metavar='', + help='ID or name of security group.') +@utils.arg('source_group', + metavar='', + help='ID or name of source group.') +@utils.arg('ip_proto', + metavar='', + help='IP protocol (icmp, tcp, udp).') +@utils.arg('from_port', + metavar='', + help='Port at start of range.') +@utils.arg('to_port', + metavar='', + help='Port at end of range.') +def do_secgroup_delete_group_rule(cs, args): + """Delete a source group rule from a security group.""" + secgroup = _get_secgroup(cs, args.secgroup) + source_group = _get_secgroup(cs, args.source_group) + params = {} + params['group_name'] = source_group.name + + if args.ip_proto or args.from_port or args.to_port: + if not (args.ip_proto and args.from_port and args.to_port): + raise exceptions.CommandError("ip_proto, from_port, and to_port" + " must be specified together") + params['ip_protocol'] = args.ip_proto.upper() + params['from_port'] = int(args.from_port) + params['to_port'] = int(args.to_port) + + for rule in secgroup.rules: + if (rule.get('ip_protocol').upper() == params.get( + 'ip_protocol').upper() and + rule.get('from_port') == params.get('from_port') and + rule.get('to_port') == params.get('to_port') and + rule.get('group', {}).get('name') == + params.get('group_name')): + return cs.security_group_rules.delete(rule['id']) + + raise exceptions.CommandError("Rule not found") + + +@utils.arg('name', metavar='', help='Name of key.') +@utils.arg('--pub-key', + metavar='', + default=None, + help='Path to a public ssh key.') +@utils.arg('--pub_key', + help=argparse.SUPPRESS) +def do_keypair_add(cs, args): + """Create a new key pair for use with instances.""" + name = args.name + pub_key = args.pub_key + + if pub_key: + try: + with open(os.path.expanduser(pub_key)) as f: + pub_key = f.read() + except IOError as e: + raise exceptions.CommandError("Can't open or read '%s': %s" % + (pub_key, e)) + + keypair = cs.keypairs.create(name, pub_key) + + if not pub_key: + private_key = keypair.private_key + print(private_key) + + +@utils.arg('name', metavar='', help='Keypair name to delete.') +def do_keypair_delete(cs, args): + """Delete keypair given by its name.""" + name = args.name + cs.keypairs.delete(name) + + +def do_keypair_list(cs, args): + """Print a list of keypairs for a user""" + keypairs = cs.keypairs.list() + columns = ['Name', 'Fingerprint'] + utils.print_list(keypairs, columns) + + +def _print_keypair(keypair): + kp = keypair._info.copy() + pk = kp.pop('public_key') + utils.print_dict(kp) + print("Public key: %s" % pk) + + +@utils.arg('keypair', + metavar='', + help="Name or ID of keypair") +def do_keypair_show(cs, args): + """Show details about the given keypair.""" + keypair = cs.keypairs.get(args.keypair) + _print_keypair(keypair) + + +@utils.arg('--tenant', + #nova db searches by project_id + dest='tenant', + metavar='', + nargs='?', + help='Display information from single tenant (Admin only).') +@utils.arg('--reserved', + dest='reserved', + action='store_true', + default=False, + help='Include reservations count.') +def do_absolute_limits(cs, args): + """Print a list of absolute limits for a user""" + limits = cs.limits.get(args.reserved, args.tenant).absolute + columns = ['Name', 'Value'] + utils.print_list(limits, columns) + + +def do_rate_limits(cs, args): + """Print a list of rate limits for a user""" + limits = cs.limits.get().rate + columns = ['Verb', 'URI', 'Value', 'Remain', 'Unit', 'Next_Available'] + utils.print_list(limits, columns) + + +@utils.arg('--start', metavar='', + help='Usage range start date ex 2012-01-20 (default: 4 weeks ago)', + default=None) +@utils.arg('--end', metavar='', + help='Usage range end date, ex 2012-01-20 (default: tomorrow) ', + default=None) +def do_usage_list(cs, args): + """List usage data for all tenants.""" + dateformat = "%Y-%m-%d" + rows = ["Tenant ID", "Instances", "RAM MB-Hours", "CPU Hours", + "Disk GB-Hours"] + + now = timeutils.utcnow() + + if args.start: + start = datetime.datetime.strptime(args.start, dateformat) + else: + start = now - datetime.timedelta(weeks=4) + + if args.end: + end = datetime.datetime.strptime(args.end, dateformat) + else: + end = now + datetime.timedelta(days=1) + + def simplify_usage(u): + simplerows = map(lambda x: x.lower().replace(" ", "_"), rows) + + setattr(u, simplerows[0], u.tenant_id) + setattr(u, simplerows[1], "%d" % len(u.server_usages)) + setattr(u, simplerows[2], "%.2f" % u.total_memory_mb_usage) + setattr(u, simplerows[3], "%.2f" % u.total_vcpus_usage) + setattr(u, simplerows[4], "%.2f" % u.total_local_gb_usage) + + usage_list = cs.usage.list(start, end, detailed=True) + + print("Usage from %s to %s:" % (start.strftime(dateformat), + end.strftime(dateformat))) + + for usage in usage_list: + simplify_usage(usage) + + utils.print_list(usage_list, rows) + + +@utils.arg('--start', metavar='', + help='Usage range start date ex 2012-01-20 (default: 4 weeks ago)', + default=None) +@utils.arg('--end', metavar='', + help='Usage range end date, ex 2012-01-20 (default: tomorrow) ', + default=None) +@utils.arg('--tenant', metavar='', + default=None, + help='UUID or name of tenant to get usage for.') +def do_usage(cs, args): + """Show usage data for a single tenant.""" + dateformat = "%Y-%m-%d" + rows = ["Instances", "RAM MB-Hours", "CPU Hours", "Disk GB-Hours"] + + now = timeutils.utcnow() + + if args.start: + start = datetime.datetime.strptime(args.start, dateformat) + else: + start = now - datetime.timedelta(weeks=4) + + if args.end: + end = datetime.datetime.strptime(args.end, dateformat) + else: + end = now + datetime.timedelta(days=1) + + def simplify_usage(u): + simplerows = map(lambda x: x.lower().replace(" ", "_"), rows) + + setattr(u, simplerows[0], "%d" % len(u.server_usages)) + setattr(u, simplerows[1], "%.2f" % u.total_memory_mb_usage) + setattr(u, simplerows[2], "%.2f" % u.total_vcpus_usage) + setattr(u, simplerows[3], "%.2f" % u.total_local_gb_usage) + + if args.tenant: + usage = cs.usage.get(args.tenant, start, end) + else: + usage = cs.usage.get(cs.client.tenant_id, start, end) + + print("Usage from %s to %s:" % (start.strftime(dateformat), + end.strftime(dateformat))) + + if getattr(usage, 'total_vcpus_usage', None): + simplify_usage(usage) + utils.print_list([usage], rows) + else: + print('None') + + +@utils.arg('pk_filename', + metavar='', + nargs='?', + default='pk.pem', + help='Filename for the private key [Default: pk.pem]') +@utils.arg('cert_filename', + metavar='', + nargs='?', + default='cert.pem', + help='Filename for the X.509 certificate [Default: cert.pem]') +def do_x509_create_cert(cs, args): + """Create x509 cert for a user in tenant.""" + + if os.path.exists(args.pk_filename): + raise exceptions.CommandError("Unable to write privatekey - %s exists." + % args.pk_filename) + if os.path.exists(args.cert_filename): + raise exceptions.CommandError("Unable to write x509 cert - %s exists." + % args.cert_filename) + + certs = cs.certs.create() + + try: + old_umask = os.umask(0o377) + with open(args.pk_filename, 'w') as private_key: + private_key.write(certs.private_key) + print("Wrote private key to %s" % args.pk_filename) + finally: + os.umask(old_umask) + + with open(args.cert_filename, 'w') as cert: + cert.write(certs.data) + print("Wrote x509 certificate to %s" % args.cert_filename) + + +@utils.arg('filename', + metavar='', + nargs='?', + default='cacert.pem', + help='Filename to write the x509 root cert.') +def do_x509_get_root_cert(cs, args): + """Fetch the x509 root cert.""" + if os.path.exists(args.filename): + raise exceptions.CommandError("Unable to write x509 root cert - \ + %s exists." % args.filename) + + with open(args.filename, 'w') as cert: + cacert = cs.certs.get() + cert.write(cacert.data) + print("Wrote x509 root cert to %s" % args.filename) + + +@utils.arg('--hypervisor', metavar='', default=None, + help='type of hypervisor.') +def do_agent_list(cs, args): + """List all builds.""" + result = cs.agents.list(args.hypervisor) + columns = ["Agent_id", "Hypervisor", "OS", "Architecture", "Version", + 'Md5hash', 'Url'] + utils.print_list(result, columns) + + +@utils.arg('os', metavar='', help='type of os.') +@utils.arg('architecture', metavar='', + help='type of architecture') +@utils.arg('version', metavar='', help='version') +@utils.arg('url', metavar='', help='url') +@utils.arg('md5hash', metavar='', help='md5 hash') +@utils.arg('hypervisor', metavar='', default='xen', + help='type of hypervisor.') +def do_agent_create(cs, args): + """Create new agent build.""" + result = cs.agents.create(args.os, args.architecture, + args.version, args.url, + args.md5hash, args.hypervisor) + utils.print_dict(result._info.copy()) + + +@utils.arg('id', metavar='', help='id of the agent-build') +def do_agent_delete(cs, args): + """Delete existing agent build.""" + cs.agents.delete(args.id) + + +@utils.arg('id', metavar='', help='id of the agent-build') +@utils.arg('version', metavar='', help='version') +@utils.arg('url', metavar='', help='url') +@utils.arg('md5hash', metavar='', help='md5hash') +def do_agent_modify(cs, args): + """Modify existing agent build.""" + result = cs.agents.update(args.id, args.version, + args.url, args.md5hash) + utils.print_dict(result._info) + + +def _find_aggregate(cs, aggregate): + """Get a aggregate by name or ID.""" + return utils.find_resource(cs.aggregates, aggregate) + + +def do_aggregate_list(cs, args): + """Print a list of all aggregates.""" + aggregates = cs.aggregates.list() + columns = ['Id', 'Name', 'Availability Zone'] + utils.print_list(aggregates, columns) + + +@utils.arg('name', metavar='', help='Name of aggregate.') +@utils.arg('availability_zone', + metavar='', + default=None, + nargs='?', + help='The availability zone of the aggregate (optional).') +def do_aggregate_create(cs, args): + """Create a new aggregate with the specified details.""" + aggregate = cs.aggregates.create(args.name, args.availability_zone) + _print_aggregate_details(aggregate) + + +@utils.arg('aggregate', metavar='', + help='Name or ID of aggregate to delete.') +def do_aggregate_delete(cs, args): + """Delete the aggregate.""" + aggregate = _find_aggregate(cs, args.aggregate) + cs.aggregates.delete(aggregate) + print("Aggregate %s has been successfully deleted." % aggregate.id) + + +@utils.arg('aggregate', metavar='', + help='Name or ID of aggregate to update.') +@utils.arg('name', metavar='', help='Name of aggregate.') +@utils.arg('availability_zone', + metavar='', + nargs='?', + default=None, + help='The availability zone of the aggregate.') +def do_aggregate_update(cs, args): + """Update the aggregate's name and optionally availability zone.""" + aggregate = _find_aggregate(cs, args.aggregate) + updates = {"name": args.name} + if args.availability_zone: + updates["availability_zone"] = args.availability_zone + + aggregate = cs.aggregates.update(aggregate.id, updates) + print("Aggregate %s has been successfully updated." % aggregate.id) + _print_aggregate_details(aggregate) + + +@utils.arg('aggregate', metavar='', + help='Name or ID of aggregate to update.') +@utils.arg('metadata', + metavar='', + nargs='+', + action='append', + default=[], + help='Metadata to add/update to aggregate') +def do_aggregate_set_metadata(cs, args): + """Update the metadata associated with the aggregate.""" + aggregate = _find_aggregate(cs, args.aggregate) + metadata = _extract_metadata(args) + aggregate = cs.aggregates.set_metadata(aggregate.id, metadata) + print("Aggregate %s has been successfully updated." % aggregate.id) + _print_aggregate_details(aggregate) + + +@utils.arg('aggregate', metavar='', help='Name or ID of aggregate.') +@utils.arg('host', metavar='', help='The host to add to the aggregate.') +def do_aggregate_add_host(cs, args): + """Add the host to the specified aggregate.""" + aggregate = _find_aggregate(cs, args.aggregate) + aggregate = cs.aggregates.add_host(aggregate.id, args.host) + print("Aggregate %s has been successfully updated." % aggregate.id) + _print_aggregate_details(aggregate) + + +@utils.arg('aggregate', metavar='', help='Name or ID of aggregate.') +@utils.arg('host', metavar='', + help='The host to remove from the aggregate.') +def do_aggregate_remove_host(cs, args): + """Remove the specified host from the specified aggregate.""" + aggregate = _find_aggregate(cs, args.aggregate) + aggregate = cs.aggregates.remove_host(aggregate.id, args.host) + print("Aggregate %s has been successfully updated." % aggregate.id) + _print_aggregate_details(aggregate) + + +@utils.arg('aggregate', metavar='', help='Name or ID of aggregate.') +def do_aggregate_details(cs, args): + """Show details of the specified aggregate.""" + aggregate = _find_aggregate(cs, args.aggregate) + _print_aggregate_details(aggregate) + + +def _print_aggregate_details(aggregate): + columns = ['Id', 'Name', 'Availability Zone', 'Hosts', 'Metadata'] + utils.print_list([aggregate], columns) + + +@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('host', metavar='', default=None, nargs='?', + help='destination host name.') +@utils.arg('--block-migrate', + action='store_true', + dest='block_migrate', + default=False, + help='True in case of block_migration.\ + (Default=False:live_migration)') +@utils.arg('--block_migrate', + action='store_true', + help=argparse.SUPPRESS) +@utils.arg('--disk-over-commit', + action='store_true', + dest='disk_over_commit', + default=False, + help='Allow overcommit.(Default=False)') +@utils.arg('--disk_over_commit', + action='store_true', + help=argparse.SUPPRESS) +def do_live_migration(cs, args): + """Migrate running instance to a new machine.""" + _find_server(cs, args.server).live_migrate(args.host, + args.block_migrate, + args.disk_over_commit) + + +@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('--active', action='store_const', dest='state', + default='error', const='active', + help='Request the instance be reset to "active" state instead ' + 'of "error" state (the default).') +def do_reset_state(cs, args): + """Reset the state of an instance.""" + _find_server(cs, args.server).reset_state(args.state) + + +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_reset_network(cs, args): + """Reset network of an instance.""" + _find_server(cs, args.server).reset_network() + + +@utils.arg('--host', metavar='', default=None, + help='Name of host.') +@utils.arg('--binary', metavar='', default=None, + help='Service binary.') +def do_service_list(cs, args): + """Show a list of all running services. Filter by host & binary.""" + result = cs.services.list(host=args.host, binary=args.binary) + columns = ["Binary", "Host", "Zone", "Status", "State", "Updated_at"] + # NOTE(sulo): we check if the response has disabled_reason + # so as not to add the column when the extended ext is not enabled. + if hasattr(result[0], 'disabled_reason'): + columns.append("Disabled Reason") + utils.print_list(result, columns) + + +@utils.arg('host', metavar='', help='Name of host.') +@utils.arg('binary', metavar='', help='Service binary.') +def do_service_enable(cs, args): + """Enable the service.""" + result = cs.services.enable(args.host, args.binary) + utils.print_list([result], ['Host', 'Binary', 'Status']) + + +@utils.arg('host', metavar='', help='Name of host.') +@utils.arg('binary', metavar='', help='Service binary.') +@utils.arg('--reason', metavar='', + help='Reason for disabling service.') +def do_service_disable(cs, args): + """Disable the service.""" + if args.reason: + result = cs.services.disable_log_reason(args.host, args.binary, + args.reason) + utils.print_list([result], ['Host', 'Binary', 'Status', + 'Disabled Reason']) + else: + result = cs.services.disable(args.host, args.binary) + utils.print_list([result], ['Host', 'Binary', 'Status']) + + +@utils.arg('fixed_ip', metavar='', help='Fixed IP Address.') +def do_fixed_ip_get(cs, args): + """Retrieve info on a fixed ip.""" + result = cs.fixed_ips.get(args.fixed_ip) + utils.print_list([result], ['address', 'cidr', 'hostname', 'host']) + + +@utils.arg('fixed_ip', metavar='', help='Fixed IP Address.') +def do_fixed_ip_reserve(cs, args): + """Reserve a fixed IP.""" + cs.fixed_ips.reserve(args.fixed_ip) + + +@utils.arg('fixed_ip', metavar='', help='Fixed IP Address.') +def do_fixed_ip_unreserve(cs, args): + """Unreserve a fixed IP.""" + cs.fixed_ips.unreserve(args.fixed_ip) + + +@utils.arg('host', metavar='', help='Name of host.') +def do_host_describe(cs, args): + """Describe a specific host.""" + result = cs.hosts.get(args.host) + columns = ["HOST", "PROJECT", "cpu", "memory_mb", "disk_gb"] + utils.print_list(result, columns) + + +@utils.arg('--zone', metavar='', default=None, + help='Filters the list, returning only those ' + 'hosts in the availability zone .') +def do_host_list(cs, args): + """List all hosts by service.""" + columns = ["host_name", "service", "zone"] + result = cs.hosts.list(args.zone) + utils.print_list(result, columns) + + +@utils.arg('host', metavar='', help='Name of host.') +@utils.arg('--status', metavar='', default=None, dest='status', + help='Either enable or disable a host.') +@utils.arg('--maintenance', + metavar='', + default=None, + dest='maintenance', + help='Either put or resume host to/from maintenance.') +def do_host_update(cs, args): + """Update host settings.""" + updates = {} + columns = ["HOST"] + if args.status: + updates['status'] = args.status + columns.append("status") + if args.maintenance: + updates['maintenance_mode'] = args.maintenance + columns.append("maintenance_mode") + result = cs.hosts.update(args.host, updates) + utils.print_list([result], columns) + + +@utils.arg('host', metavar='', help='Name of host.') +@utils.arg('--action', metavar='', dest='action', + choices=['startup', 'shutdown', 'reboot'], + help='A power action: startup, reboot, or shutdown.') +def do_host_action(cs, args): + """Perform a power action on a host.""" + result = cs.hosts.host_action(args.host, args.action) + utils.print_list([result], ['HOST', 'power_action']) + + +@utils.arg('--combine', + dest='combine', + action="store_true", + default=False, + help='Generate a single report for all services.') +def do_coverage_start(cs, args): + """Start Nova coverage reporting.""" + cs.coverage.start(combine=args.combine) + print("Coverage collection started") + + +def do_coverage_stop(cs, args): + """Stop Nova coverage reporting.""" + out = cs.coverage.stop() + print("Coverage data file path: %s" % out[-1]['path']) + + +@utils.arg('filename', metavar='', help='report filename') +@utils.arg('--html', + dest='html', + action="store_true", + default=False, + help='Generate HTML reports instead of text ones.') +@utils.arg('--xml', + dest='xml', + action="store_true", + default=False, + help='Generate XML reports instead of text ones.') +def do_coverage_report(cs, args): + """Generate coverage report.""" + if args.html == True and args.xml == True: + raise exceptions.CommandError("--html and --xml must not be " + "specified together.") + cov = cs.coverage.report(args.filename, xml=args.xml, html=args.html) + print("Report path: %s" % cov[-1]['path']) + + +def do_coverage_reset(cs, args): + """Reset coverage data.""" + cs.coverage.reset() + print("Coverage data reset") + + +def _find_hypervisor(cs, hypervisor): + """Get a hypervisor by name or ID.""" + return utils.find_resource(cs.hypervisors, hypervisor) + + +@utils.arg('--matching', metavar='', default=None, + help='List hypervisors matching the given .') +def do_hypervisor_list(cs, args): + """List hypervisors.""" + columns = ['ID', 'Hypervisor hostname'] + if args.matching: + utils.print_list(cs.hypervisors.search(args.matching), columns) + else: + # Since we're not outputting detail data, choose + # detailed=False for server-side efficiency + utils.print_list(cs.hypervisors.list(False), columns) + + +@utils.arg('hostname', metavar='', + help='The hypervisor hostname (or pattern) to search for.') +def do_hypervisor_servers(cs, args): + """List instances belonging to specific hypervisors.""" + hypers = cs.hypervisors.search(args.hostname, servers=True) + + class InstanceOnHyper(object): + def __init__(self, **kwargs): + self.__dict__.update(kwargs) + + # Massage the result into a list to be displayed + instances = [] + for hyper in hypers: + hyper_host = hyper.hypervisor_hostname + hyper_id = hyper.id + if hasattr(hyper, 'servers'): + instances.extend([InstanceOnHyper(id=serv['uuid'], + name=serv['name'], + hypervisor_hostname=hyper_host, + hypervisor_id=hyper_id) + for serv in hyper.servers]) + + # Output the data + utils.print_list(instances, ['ID', 'Name', 'Hypervisor ID', + 'Hypervisor Hostname']) + + +@utils.arg('hypervisor', + metavar='', + help='Name or ID of the hypervisor to show the details of.') +def do_hypervisor_show(cs, args): + """Display the details of the specified hypervisor.""" + hyper = _find_hypervisor(cs, args.hypervisor) + + # Build up the dict + info = hyper._info.copy() + info['service_id'] = info['service']['id'] + info['service_host'] = info['service']['host'] + del info['service'] + + utils.print_dict(info) + + +@utils.arg('hypervisor', + metavar='', + help='Name or ID of the hypervisor to show the uptime of.') +def do_hypervisor_uptime(cs, args): + """Display the uptime of the specified hypervisor.""" + hyper = _find_hypervisor(cs, args.hypervisor) + hyper = cs.hypervisors.uptime(hyper) + + # Output the uptime information + utils.print_dict(hyper._info.copy()) + + +def do_hypervisor_stats(cs, args): + """Get hypervisor statistics over all compute nodes.""" + stats = cs.hypervisors.statistics() + utils.print_dict(stats._info.copy()) + + +def ensure_service_catalog_present(cs): + if not hasattr(cs.client, 'service_catalog'): + # Turn off token caching and re-auth + cs.client.unauthenticate() + cs.client.use_token_cache(False) + cs.client.authenticate() + + +def do_endpoints(cs, _args): + """Discover endpoints that get returned from the authenticate services.""" + ensure_service_catalog_present(cs) + catalog = cs.client.service_catalog.catalog + for e in catalog['access']['serviceCatalog']: + utils.print_dict(e['endpoints'][0], e['name']) + + +@utils.arg('--wrap', dest='wrap', metavar='', default=64, + help='wrap PKI tokens to a specified length, or 0 to disable') +def do_credentials(cs, _args): + """Show user credentials returned from auth.""" + ensure_service_catalog_present(cs) + catalog = cs.client.service_catalog.catalog + utils.print_dict(catalog['access']['user'], "User Credentials", + wrap=int(_args.wrap)) + utils.print_dict(catalog['access']['token'], "Token", wrap=int(_args.wrap)) + + +@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('--port', + dest='port', + action='store', + type=int, + default=22, + help='Optional flag to indicate which port to use for ssh. ' + '(Default=22)') +@utils.arg('--private', + dest='private', + action='store_true', + default=False, + help='Optional flag to indicate whether to use private address ' + 'attached to an instance. (Default=False)') +@utils.arg('--ipv6', + dest='ipv6', + action='store_true', + default=False, + help='Optional flag to indicate whether to use an IPv6 address ' + 'attached to an instance. (Defaults to IPv4 address)') +@utils.arg('--login', metavar='', help='Login to use.', default="root") +@utils.arg('-i', '--identity', + dest='identity', + help='Private key file, same as the -i option to the ssh command.', + default='') +@utils.arg('--extra-opts', + dest='extra', + help='Extra options to pass to ssh. see: man ssh', + default='') +def do_ssh(cs, args): + """SSH into a server.""" + addresses = _find_server(cs, args.server).addresses + address_type = "private" if args.private else "public" + version = 6 if args.ipv6 else 4 + + if address_type not in addresses: + print("ERROR: No %s addresses found for '%s'." % (address_type, + args.server)) + return + + ip_address = None + for address in addresses[address_type]: + if address['version'] == version: + ip_address = address['addr'] + break + + identity = '-i %s' % args.identity if len(args.identity) else '' + + if ip_address: + os.system("ssh -%d -p%d %s %s@%s %s" % (version, args.port, identity, + args.login, ip_address, + args.extra)) + else: + pretty_version = "IPv%d" % version + print("ERROR: No %s %s address found." % (address_type, + pretty_version)) + return + + +_quota_resources = ['instances', 'cores', 'ram', 'volumes', 'gigabytes', + 'floating_ips', 'fixed_ips', 'metadata_items', + 'injected_files', 'key_pairs', + 'injected_file_content_bytes', 'injected_file_path_bytes', + 'security_groups', 'security_group_rules'] + + +def _quota_show(quotas): + quota_dict = {} + for resource in _quota_resources: + try: + quota_dict[resource] = getattr(quotas, resource) + except AttributeError: + pass + utils.print_dict(quota_dict) + + +def _quota_update(manager, identifier, args): + updates = {} + for resource in _quota_resources: + val = getattr(args, resource, None) + if val is not None: + updates[resource] = val + + if updates: + # default value of force is None to make sure this client + # will be compatibile with old nova server + force_update = getattr(args, 'force', None) + if isinstance(manager, quotas.QuotaSetManager): + manager.update(identifier, force=force_update, **updates) + else: + manager.update(identifier, **updates) + + +@utils.arg('--tenant', + metavar='', + default=None, + help='ID of tenant to list the quotas for.') +def do_quota_show(cs, args): + """List the quotas for a tenant.""" + + if not args.tenant: + _quota_show(cs.quotas.get(cs.client.tenant_id)) + else: + _quota_show(cs.quotas.get(args.tenant)) + + +@utils.arg('--tenant', + metavar='', + default=None, + help='ID of tenant to list the default quotas for.') +def do_quota_defaults(cs, args): + """List the default quotas for a tenant.""" + + if not args.tenant: + _quota_show(cs.quotas.defaults(cs.client.tenant_id)) + else: + _quota_show(cs.quotas.defaults(args.tenant)) + + +@utils.arg('tenant', + metavar='', + help='ID of tenant to set the quotas for.') +@utils.arg('--instances', + metavar='', + type=int, default=None, + help='New value for the "instances" quota.') +@utils.arg('--cores', + metavar='', + type=int, default=None, + help='New value for the "cores" quota.') +@utils.arg('--ram', + metavar='', + type=int, default=None, + help='New value for the "ram" quota.') +@utils.arg('--volumes', + metavar='', + type=int, default=None, + help='New value for the "volumes" quota.') +@utils.arg('--gigabytes', + metavar='', + type=int, default=None, + help='New value for the "gigabytes" quota.') +@utils.arg('--floating-ips', + metavar='', + type=int, + default=None, + help='New value for the "floating-ips" quota.') +@utils.arg('--floating_ips', + type=int, + help=argparse.SUPPRESS) +@utils.arg('--fixed-ips', + metavar='', + type=int, + default=None, + help='New value for the "fixed-ips" quota.') +@utils.arg('--metadata-items', + metavar='', + type=int, + default=None, + help='New value for the "metadata-items" quota.') +@utils.arg('--metadata_items', + type=int, + help=argparse.SUPPRESS) +@utils.arg('--injected-files', + metavar='', + type=int, + default=None, + help='New value for the "injected-files" quota.') +@utils.arg('--injected_files', + type=int, + help=argparse.SUPPRESS) +@utils.arg('--injected-file-content-bytes', + metavar='', + type=int, + default=None, + help='New value for the "injected-file-content-bytes" quota.') +@utils.arg('--injected_file_content_bytes', + type=int, + help=argparse.SUPPRESS) +@utils.arg('--injected-file-path-bytes', + metavar='', + type=int, + default=None, + help='New value for the "injected-file-path-bytes" quota.') +@utils.arg('--key-pairs', + metavar='', + type=int, + default=None, + help='New value for the "key-pairs" quota.') +@utils.arg('--security-groups', + metavar='', + type=int, + default=None, + help='New value for the "security-groups" quota.') +@utils.arg('--security-group-rules', + metavar='', + type=int, + default=None, + help='New value for the "security-group-rules" quota.') +@utils.arg('--force', + dest='force', + action="store_true", + default=None, + help='Whether force update the quota even if the already used' + ' and reserved exceeds the new quota') +def do_quota_update(cs, args): + """Update the quotas for a tenant.""" + + _quota_update(cs.quotas, args.tenant, args) + + +@utils.arg('--tenant', + metavar='', + help='ID of tenant to delete quota for.') +def do_quota_delete(cs, args): + """Delete quota for a tenant so their quota will revert back to default.""" + + cs.quotas.delete(args.tenant) + + +@utils.arg('class_name', + metavar='', + help='Name of quota class to list the quotas for.') +def do_quota_class_show(cs, args): + """List the quotas for a quota class.""" + + _quota_show(cs.quota_classes.get(args.class_name)) + + +@utils.arg('class_name', + metavar='', + help='Name of quota class to set the quotas for.') +@utils.arg('--instances', + metavar='', + type=int, default=None, + help='New value for the "instances" quota.') +@utils.arg('--cores', + metavar='', + type=int, default=None, + help='New value for the "cores" quota.') +@utils.arg('--ram', + metavar='', + type=int, default=None, + help='New value for the "ram" quota.') +@utils.arg('--volumes', + metavar='', + type=int, default=None, + help='New value for the "volumes" quota.') +@utils.arg('--gigabytes', + metavar='', + type=int, default=None, + help='New value for the "gigabytes" quota.') +@utils.arg('--floating-ips', + metavar='', + type=int, + default=None, + help='New value for the "floating-ips" quota.') +@utils.arg('--floating_ips', + type=int, + help=argparse.SUPPRESS) +@utils.arg('--metadata-items', + metavar='', + type=int, + default=None, + help='New value for the "metadata-items" quota.') +@utils.arg('--metadata_items', + type=int, + help=argparse.SUPPRESS) +@utils.arg('--injected-files', + metavar='', + type=int, + default=None, + help='New value for the "injected-files" quota.') +@utils.arg('--injected_files', + type=int, + help=argparse.SUPPRESS) +@utils.arg('--injected-file-content-bytes', + metavar='', + type=int, + default=None, + help='New value for the "injected-file-content-bytes" quota.') +@utils.arg('--injected_file_content_bytes', + type=int, + help=argparse.SUPPRESS) +@utils.arg('--injected-file-path-bytes', + metavar='', + type=int, + default=None, + help='New value for the "injected-file-path-bytes" quota.') +@utils.arg('--key-pairs', + metavar='', + type=int, + default=None, + help='New value for the "key-pairs" quota.') +@utils.arg('--security-groups', + metavar='', + type=int, + default=None, + help='New value for the "security-groups" quota.') +@utils.arg('--security-group-rules', + metavar='', + type=int, + default=None, + help='New value for the "security-group-rules" quota.') +def do_quota_class_update(cs, args): + """Update the quotas for a quota class.""" + + _quota_update(cs.quota_classes, args.class_name, args) + + +@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('host', metavar='', help='Name or ID of target host.') +@utils.arg('--password', + dest='password', + metavar='', + default=None, + help="Set the provided password on the evacuated instance. Not applicable " + "with on-shared-storage flag") +@utils.arg('--on-shared-storage', + dest='on_shared_storage', + action="store_true", + default=False, + help='Specifies whether instance files located on shared storage') +def do_evacuate(cs, args): + """Evacuate server from failed host to specified one.""" + server = _find_server(cs, args.server) + + res = server.evacuate(args.host, args.on_shared_storage, args.password)[1] + if type(res) is dict: + utils.print_dict(res) + + +def _print_interfaces(interfaces): + columns = ['Port State', 'Port ID', 'Net ID', 'IP addresses', + 'MAC Addr'] + + class FormattedInterface(object): + def __init__(self, interface): + for col in columns: + key = col.lower().replace(" ", "_") + if hasattr(interface, key): + setattr(self, key, getattr(interface, key)) + self.ip_addresses = ",".join([fip['ip_address'] + for fip in interface.fixed_ips]) + utils.print_list([FormattedInterface(i) for i in interfaces], columns) + + +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_interface_list(cs, args): + """List interfaces attached to an instance.""" + server = _find_server(cs, args.server) + + res = server.interface_list() + if type(res) is list: + _print_interfaces(res) + + +@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('--port-id', metavar='', help='Port ID.', dest="port_id") +@utils.arg('--net-id', metavar='', help='Network ID', + default=None, dest="net_id") +@utils.arg('--fixed-ip', metavar='', help='Requested fixed IP.', + default=None, dest="fixed_ip") +def do_interface_attach(cs, args): + """Attach a network interface to an instance.""" + server = _find_server(cs, args.server) + + res = server.interface_attach(args.port_id, args.net_id, args.fixed_ip) + if type(res) is dict: + utils.print_dict(res) + + +@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('port_id', metavar='', help='Port ID.') +def do_interface_detach(cs, args): + """Detach a network interface from an instance.""" + server = _find_server(cs, args.server) + + res = server.interface_detach(args.port_id) + if type(res) is dict: + utils.print_dict(res) + + +def _treeizeAvailabilityZone(zone): + """Build a tree view for availability zones.""" + AvailabilityZone = availability_zones.AvailabilityZone + + az = AvailabilityZone(zone.manager, + copy.deepcopy(zone._info), zone._loaded) + result = [] + + # Zone tree view item + az.zoneName = zone.zoneName + az.zoneState = ('available' + if zone.zoneState['available'] else 'not available') + az._info['zoneName'] = az.zoneName + az._info['zoneState'] = az.zoneState + result.append(az) + + if zone.hosts is not None: + for (host, services) in zone.hosts.items(): + # Host tree view item + az = AvailabilityZone(zone.manager, + copy.deepcopy(zone._info), zone._loaded) + az.zoneName = '|- %s' % host + az.zoneState = '' + az._info['zoneName'] = az.zoneName + az._info['zoneState'] = az.zoneState + result.append(az) + + for (svc, state) in services.items(): + # Service tree view item + az = AvailabilityZone(zone.manager, + copy.deepcopy(zone._info), zone._loaded) + az.zoneName = '| |- %s' % svc + az.zoneState = '%s %s %s' % ( + 'enabled' if state['active'] else 'disabled', + ':-)' if state['available'] else 'XXX', + state['updated_at']) + az._info['zoneName'] = az.zoneName + az._info['zoneState'] = az.zoneState + result.append(az) + return result + + +@utils.service_type('compute') +def do_availability_zone_list(cs, _args): + """List all the availability zones.""" + try: + availability_zones = cs.availability_zones.list() + except exceptions.Forbidden as e: # policy doesn't allow probably + try: + availability_zones = cs.availability_zones.list(detailed=False) + except Exception: + raise e + + result = [] + for zone in availability_zones: + result += _treeizeAvailabilityZone(zone) + _translate_availability_zone_keys(result) + utils.print_list(result, ['Name', 'Status'], + sortby_index=None) From 211dfe6ea706ec861daa3138d95b70c3427e0eed Mon Sep 17 00:00:00 2001 From: Ben Nemec Date: Tue, 6 Aug 2013 12:48:24 -0500 Subject: [PATCH 0235/1705] Enable v3 api code Add mappings to enable v3 code bp v3-api Change-Id: I035e5b9d65c06ce5691573832bb9b6c4c705b121 --- novaclient/client.py | 1 + novaclient/shell.py | 5 ++++- novaclient/v3/__init__.py | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index ce0e8495e..03bdb030c 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -429,6 +429,7 @@ def get_client_class(version): version_map = { '1.1': 'novaclient.v1_1.client.Client', '2': 'novaclient.v1_1.client.Client', + '3': 'novaclient.v3.client.Client', } try: client_path = version_map[str(version)] diff --git a/novaclient/shell.py b/novaclient/shell.py index 7f3257155..8e7860879 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -56,6 +56,7 @@ from novaclient.openstack.common import strutils from novaclient import utils from novaclient.v1_1 import shell as shell_v1_1 +from novaclient.v3 import shell as shell_v3 DEFAULT_OS_COMPUTE_API_VERSION = "1.1" DEFAULT_NOVA_ENDPOINT_TYPE = 'publicURL' @@ -353,7 +354,8 @@ def get_base_parser(self): metavar='', default=utils.env('OS_COMPUTE_API_VERSION', default=DEFAULT_OS_COMPUTE_API_VERSION), - help='Accepts 1.1, defaults to env[OS_COMPUTE_API_VERSION].') + help='Accepts 1.1 or 3, ' + 'defaults to env[OS_COMPUTE_API_VERSION].') parser.add_argument('--os_compute_api_version', help=argparse.SUPPRESS) @@ -394,6 +396,7 @@ def get_subcommand_parser(self, version): actions_module = { '1.1': shell_v1_1, '2': shell_v1_1, + '3': shell_v3, }[version] except KeyError: actions_module = shell_v1_1 diff --git a/novaclient/v3/__init__.py b/novaclient/v3/__init__.py index 19712285f..a442be109 100644 --- a/novaclient/v3/__init__.py +++ b/novaclient/v3/__init__.py @@ -14,4 +14,4 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.v1_1.client import Client # noqa +from novaclient.v3.client import Client # noqa From 99d978409f0d455ba148d4d5ee8545c9f99cd126 Mon Sep 17 00:00:00 2001 From: Ben Nemec Date: Thu, 8 Aug 2013 12:33:10 -0500 Subject: [PATCH 0236/1705] Remove old references The v3 code references v1_1 code because it's a straight-across copy. Remove those in preparation for adding the actual v3 versions. bp v3-api Change-Id: I359c6c5fc70dd91fa418802cc8048e186f5d09a4 --- novaclient/v3/client.py | 77 ++--------------------------------------- 1 file changed, 2 insertions(+), 75 deletions(-) diff --git a/novaclient/v3/client.py b/novaclient/v3/client.py index 50ab46cff..aca58452a 100644 --- a/novaclient/v3/client.py +++ b/novaclient/v3/client.py @@ -15,37 +15,6 @@ # under the License. from novaclient import client -from novaclient.v1_1 import agents -from novaclient.v1_1 import certs -from novaclient.v1_1 import cloudpipe -from novaclient.v1_1 import aggregates -from novaclient.v1_1 import availability_zones -from novaclient.v1_1 import coverage_ext -from novaclient.v1_1 import flavors -from novaclient.v1_1 import flavor_access -from novaclient.v1_1 import floating_ip_dns -from novaclient.v1_1 import floating_ips -from novaclient.v1_1 import floating_ip_pools -from novaclient.v1_1 import fping -from novaclient.v1_1 import hosts -from novaclient.v1_1 import hypervisors -from novaclient.v1_1 import images -from novaclient.v1_1 import keypairs -from novaclient.v1_1 import limits -from novaclient.v1_1 import networks -from novaclient.v1_1 import quota_classes -from novaclient.v1_1 import quotas -from novaclient.v1_1 import security_group_rules -from novaclient.v1_1 import security_groups -from novaclient.v1_1 import servers -from novaclient.v1_1 import usage -from novaclient.v1_1 import virtual_interfaces -from novaclient.v1_1 import volumes -from novaclient.v1_1 import volume_snapshots -from novaclient.v1_1 import volume_types -from novaclient.v1_1 import services -from novaclient.v1_1 import fixed_ips -from novaclient.v1_1 import floating_ips_bulk class Client(object): @@ -66,7 +35,7 @@ class Client(object): """ # FIXME(jesse): project_id isn't required to authenticate - def __init__(self, username, api_key, project_id, auth_url=None, + def __init__(self, username, password, project_id, auth_url=None, insecure=False, timeout=None, proxy_tenant_id=None, proxy_token=None, region_name=None, endpoint_type='publicURL', extensions=None, @@ -76,49 +45,7 @@ def __init__(self, username, api_key, project_id, auth_url=None, http_log_debug=False, auth_system='keystone', auth_plugin=None, cacert=None, tenant_id=None): - # FIXME(comstud): Rename the api_key argument above when we - # know it's not being used as keyword argument - password = api_key - self.projectid = project_id - self.tenant_id = tenant_id - self.flavors = flavors.FlavorManager(self) - self.flavor_access = flavor_access.FlavorAccessManager(self) - self.images = images.ImageManager(self) - self.limits = limits.LimitsManager(self) - self.servers = servers.ServerManager(self) - - # extensions - self.agents = agents.AgentsManager(self) - self.dns_domains = floating_ip_dns.FloatingIPDNSDomainManager(self) - self.dns_entries = floating_ip_dns.FloatingIPDNSEntryManager(self) - self.cloudpipe = cloudpipe.CloudpipeManager(self) - self.certs = certs.CertificateManager(self) - self.floating_ips = floating_ips.FloatingIPManager(self) - self.floating_ip_pools = floating_ip_pools.FloatingIPPoolManager(self) - self.fping = fping.FpingManager(self) - self.volumes = volumes.VolumeManager(self) - self.volume_snapshots = volume_snapshots.SnapshotManager(self) - self.volume_types = volume_types.VolumeTypeManager(self) - self.keypairs = keypairs.KeypairManager(self) - self.networks = networks.NetworkManager(self) - self.quota_classes = quota_classes.QuotaClassSetManager(self) - self.quotas = quotas.QuotaSetManager(self) - self.security_groups = security_groups.SecurityGroupManager(self) - self.security_group_rules = \ - security_group_rules.SecurityGroupRuleManager(self) - self.usage = usage.UsageManager(self) - self.virtual_interfaces = \ - virtual_interfaces.VirtualInterfaceManager(self) - self.aggregates = aggregates.AggregateManager(self) - self.hosts = hosts.HostManager(self) - self.hypervisors = hypervisors.HypervisorManager(self) - self.services = services.ServiceManager(self) - self.fixed_ips = fixed_ips.FixedIPsManager(self) - self.floating_ips_bulk = floating_ips_bulk.FloatingIPBulkManager(self) - self.os_cache = os_cache or not no_cache - self.coverage = coverage_ext.CoverageManager(self) - self.availability_zones = \ - availability_zones.AvailabilityZoneManager(self) + #TODO(bnemec): Add back in v3 extensions # Add in any extensions... if extensions: From 0331a01cfc4a2d9197998b0b33d2e02dc04b0197 Mon Sep 17 00:00:00 2001 From: Yufang Zhang Date: Wed, 7 Aug 2013 17:53:05 +0800 Subject: [PATCH 0237/1705] Clean up inaccurate docstrings of server list() method Clean up the inaccurate docstrings of server list() method and refactor them to standerd format. Change-Id: I4c17aba7ce5eb430ddd10305135e11e3ce5dc446 --- novaclient/v1_1/servers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index 4f09d9df4..c656976cd 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -367,9 +367,9 @@ def get(self, server): def list(self, detailed=True, search_opts=None): """ Get a list of servers. - Optional detailed returns details server info. - Optional reservation_id only returns instances with that - reservation_id. + + :param detailed: Whether to return detailed server info (optional). + :param search_opts: Search options to filter out servers (optional). :rtype: list of :class:`Server` """ From d925f55d0c232dd2fe4f2d066f0aa2ef09444fca Mon Sep 17 00:00:00 2001 From: Ken'ichi Ohmichi Date: Mon, 12 Aug 2013 13:58:03 +0900 Subject: [PATCH 0238/1705] Fix the help messages to specify image/flavor name User can specify flavor/image name as the arguments of "nova boot" command instead of their ids, but the help messages does not explain that. This fixes the help messages. Fixes: bug #1211156 Change-Id: I47813c8ebbe5fc83d627f016e3751918b3ab11cb --- novaclient/v1_1/shell.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index c044cf49d..51cce9352 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -206,11 +206,11 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): @utils.arg('--flavor', default=None, metavar='', - help="Flavor ID (see 'nova flavor-list').") + help="Name or ID of flavor (see 'nova flavor-list').") @utils.arg('--image', default=None, metavar='', - help="Image ID (see 'nova image-list'). ") + help="Name or ID of image (see 'nova image-list'). ") @utils.arg('--image-with', default=[], type=_key_value_pairing, From 8dbd684a5cbf9fe5cdc0ab1f38930a6f6794726d Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Fri, 9 Aug 2013 15:15:04 +0000 Subject: [PATCH 0239/1705] python3: Fix traceback while running tests Fix: TypeError: Unicode-objects must be encoded before hashing while running tests. This is due to the fact in python3 hashlib.md5 works with bytes and we are passing unicode strings. Change-Id: I27a57d6afb2412e11bc2802e4559d56c5b5592a9 Signed-off-by: Chuck Short --- novaclient/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/novaclient/base.py b/novaclient/base.py index 5d4b583e4..63cef4e43 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -97,7 +97,8 @@ def completion_cache(self, cache_type, obj_class, mode): # pair username = utils.env('OS_USERNAME', 'NOVA_USERNAME') url = utils.env('OS_URL', 'NOVA_URL') - uniqifier = hashlib.md5(username + url).hexdigest() + uniqifier = hashlib.md5(username.encode('utf-8') + + url.encode('utf-8')).hexdigest() cache_dir = os.path.expanduser(os.path.join(base_dir, uniqifier)) From 476e50f4d7f7c7f3231bca8beb194776c28b2267 Mon Sep 17 00:00:00 2001 From: Yuuichi Fujioka Date: Mon, 29 Jul 2013 05:32:01 +0900 Subject: [PATCH 0240/1705] Allow name argument to flavor-access-add Administrator cannot use the flavor name in arguments of "nova flavor-access-add" and "nova flavor-access-remove" in following conditions. 1. The flavor is non-public. 2. The falvor doesn't belong administrator's tenant. This patch fixes this problem. Change-Id: Ic5e3222d0ff62667e003a61e6a94e85833da5d11 Fixes: bug #1205298 --- novaclient/base.py | 14 +++++--- novaclient/tests/v1_1/fakes.py | 47 +++++++++++++++++++++++---- novaclient/tests/v1_1/test_flavors.py | 18 ++++++++-- novaclient/tests/v1_1/test_shell.py | 19 +++++++++-- novaclient/utils.py | 4 +-- novaclient/v1_1/shell.py | 12 +++++-- 6 files changed, 93 insertions(+), 21 deletions(-) diff --git a/novaclient/base.py b/novaclient/base.py index adcf29d5a..cbe29eb51 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -199,13 +199,19 @@ def findall(self, **kwargs): searches = kwargs.items() detailed = True - if 'detailed' in inspect.getargspec(self.list).args: + list_kwargs = {} + + list_argspec = inspect.getargspec(self.list) + if 'detailed' in list_argspec.args: detailed = ("human_id" not in kwargs and "name" not in kwargs and "display_name" not in kwargs) - listing = self.list(detailed=detailed) - else: - listing = self.list() + list_kwargs['detailed'] = detailed + + if 'is_public' in list_argspec.args and 'is_public' in kwargs: + list_kwargs['is_public'] = kwargs['is_public'] + + listing = self.list(**list_kwargs) for obj in listing: try: diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index 7673317fc..bb6f866af 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -20,9 +20,11 @@ import six from novaclient import client as base_client -from novaclient.v1_1 import client +from novaclient import exceptions +from novaclient.openstack.common import strutils from novaclient.tests import fakes from novaclient.tests import utils +from novaclient.v1_1 import client class FakeClient(fakes.FakeClient, client.Client): @@ -67,6 +69,7 @@ def _cs_request(self, url, method, **kwargs): munged_url = url.rsplit('?', 1)[0] munged_url = munged_url.strip('/').replace('/', '_').replace('.', '_') munged_url = munged_url.replace('-', '_') + munged_url = munged_url.replace(' ', '_') callback = "%s_%s" % (method.lower(), munged_url) @@ -594,7 +597,7 @@ def get_flavors(self, **kw): ]}) def get_flavors_detail(self, **kw): - return (200, {}, {'flavors': [ + flavors = {'flavors': [ {'id': 1, 'name': '256 MB Server', 'ram': 256, 'disk': 10, 'OS-FLV-EXT-DATA:ephemeral': 10, 'os-flavor-access:is_public': True, @@ -607,20 +610,45 @@ def get_flavors_detail(self, **kw): 'OS-FLV-EXT-DATA:ephemeral': 0, 'os-flavor-access:is_public': True, 'links': {}} - ]}) + ]} + + if 'is_public' not in kw: + filter_is_public = True + else: + if kw['is_public'].lower() == 'none': + filter_is_public = None + else: + filter_is_public = strutils.bool_from_string(kw['is_public'], + True) + + if filter_is_public is not None: + if filter_is_public: + flavors['flavors'] = [ + v for v in flavors['flavors'] + if v['os-flavor-access:is_public'] + ] + else: + flavors['flavors'] = [ + v for v in flavors['flavors'] + if not v['os-flavor-access:is_public'] + ] + + return (200, {}, flavors) def get_flavors_1(self, **kw): return ( 200, {}, - {'flavor': self.get_flavors_detail()[2]['flavors'][0]} + {'flavor': + self.get_flavors_detail(is_public='None')[2]['flavors'][0]} ) def get_flavors_2(self, **kw): return ( 200, {}, - {'flavor': self.get_flavors_detail()[2]['flavors'][1]} + {'flavor': + self.get_flavors_detail(is_public='None')[2]['flavors'][1]} ) def get_flavors_3(self, **kw): @@ -636,12 +664,16 @@ def get_flavors_3(self, **kw): }}, ) + def get_flavors_512_MB_Server(self, **kw): + raise exceptions.NotFound('404') + def get_flavors_aa1(self, **kw): # Aplhanumeric flavor id are allowed. return ( 200, {}, - {'flavor': self.get_flavors_detail()[2]['flavors'][2]} + {'flavor': + self.get_flavors_detail(is_public='None')[2]['flavors'][2]} ) def delete_flavors_flavordelete(self, **kw): @@ -654,7 +686,8 @@ def post_flavors(self, body, **kw): return ( 202, {}, - {'flavor': self.get_flavors_detail()[2]['flavors'][0]} + {'flavor': + self.get_flavors_detail(is_public='None')[2]['flavors'][0]} ) def get_flavors_1_os_extra_specs(self, **kw): diff --git a/novaclient/tests/v1_1/test_flavors.py b/novaclient/tests/v1_1/test_flavors.py index eb7be2a4c..398e6df13 100644 --- a/novaclient/tests/v1_1/test_flavors.py +++ b/novaclient/tests/v1_1/test_flavors.py @@ -21,12 +21,24 @@ def test_list_flavors_undetailed(self): for flavor in fl: self.assertTrue(isinstance(flavor, flavors.Flavor)) - def test_list_flavors_is_public(self): + def test_list_flavors_is_public_none(self): fl = cs.flavors.list(is_public=None) cs.assert_called('GET', '/flavors/detail?is_public=None') for flavor in fl: self.assertTrue(isinstance(flavor, flavors.Flavor)) + def test_list_flavors_is_public_false(self): + fl = cs.flavors.list(is_public=False) + cs.assert_called('GET', '/flavors/detail?is_public=False') + for flavor in fl: + self.assertTrue(isinstance(flavor, flavors.Flavor)) + + def test_list_flavors_is_public_true(self): + fl = cs.flavors.list(is_public=True) + cs.assert_called('GET', '/flavors/detail') + for flavor in fl: + self.assertTrue(isinstance(flavor, flavors.Flavor)) + def test_get_flavor_details(self): f = cs.flavors.get(1) cs.assert_called('GET', '/flavors/1') @@ -59,8 +71,8 @@ def test_find(self): cs.assert_called('GET', '/flavors/detail') self.assertEqual(f.name, '256 MB Server') - f = cs.flavors.find(disk=20) - self.assertEqual(f.name, '512 MB Server') + f = cs.flavors.find(disk=0) + self.assertEqual(f.name, '128 MB Server') self.assertRaises(exceptions.NotFound, cs.flavors.find, disk=12345) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index 1ca22c62f..69f9b69ba 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -73,7 +73,10 @@ def setUp(self): @mock.patch('sys.stdout', StringIO.StringIO()) def run_command(self, cmd): - self.shell.main(cmd.split()) + if isinstance(cmd, list): + self.shell.main(cmd) + else: + self.shell.main(cmd.split()) return sys.stdout.getvalue() def assert_called(self, method, url, body=None, **kwargs): @@ -449,16 +452,26 @@ def test_flavor_access_list_no_filter(self): cmd = 'flavor-access-list' self.assertRaises(exceptions.CommandError, self.run_command, cmd) - def test_flavor_access_add(self): + def test_flavor_access_add_by_id(self): self.run_command('flavor-access-add 2 proj2') self.assert_called('POST', '/flavors/2/action', {'addTenantAccess': {'tenant': 'proj2'}}) - def test_flavor_access_remove(self): + def test_flavor_access_add_by_name(self): + self.run_command(['flavor-access-add', '512 MB Server', 'proj2']) + self.assert_called('POST', '/flavors/2/action', + {'addTenantAccess': {'tenant': 'proj2'}}) + + def test_flavor_access_remove_by_id(self): self.run_command('flavor-access-remove 2 proj2') self.assert_called('POST', '/flavors/2/action', {'removeTenantAccess': {'tenant': 'proj2'}}) + def test_flavor_access_remove_by_name(self): + self.run_command(['flavor-access-remove', '512 MB Server', 'proj2']) + self.assert_called('POST', '/flavors/2/action', + {'removeTenantAccess': {'tenant': 'proj2'}}) + def test_image_show(self): self.run_command('image-show 1') self.assert_called('GET', '/images/1') diff --git a/novaclient/utils.py b/novaclient/utils.py index 532ea976c..2167dc0bb 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -192,7 +192,7 @@ def print_dict(d, dict_property="Property", dict_value="Value", wrap=0): print(strutils.safe_encode(pt.get_string())) -def find_resource(manager, name_or_id): +def find_resource(manager, name_or_id, **find_args): """Helper for the _find_* methods.""" # first try to get entity as integer id try: @@ -216,7 +216,7 @@ def find_resource(manager, name_or_id): try: try: - return manager.find(human_id=name_or_id) + return manager.find(human_id=name_or_id, **find_args) except exceptions.NotFound: pass diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 7358a87a9..927e1f029 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -605,7 +605,7 @@ def do_flavor_access_list(cs, args): help='Filter results by tenant ID.') def do_flavor_access_add(cs, args): """Add flavor access for the given tenant.""" - flavor = _find_flavor(cs, args.flavor) + flavor = _find_flavor_for_admin(cs, args.flavor) access_list = cs.flavor_access.add_tenant_access(flavor, args.tenant) columns = ['Flavor_ID', 'Tenant_ID'] utils.print_list(access_list, columns) @@ -618,7 +618,7 @@ def do_flavor_access_add(cs, args): help='Filter results by tenant ID.') def do_flavor_access_remove(cs, args): """Remove flavor access for the given tenant.""" - flavor = _find_flavor(cs, args.flavor) + flavor = _find_flavor_for_admin(cs, args.flavor) access_list = cs.flavor_access.remove_tenant_access(flavor, args.tenant) columns = ['Flavor_ID', 'Tenant_ID'] utils.print_list(access_list, columns) @@ -1376,6 +1376,14 @@ def _find_image(cs, image): return utils.find_resource(cs.images, image) +def _find_flavor_for_admin(cs, flavor): + """Get a flavor for administrator by name, ID, or RAM size.""" + try: + return utils.find_resource(cs.flavors, flavor, is_public='None') + except exceptions.NotFound: + return cs.flavors.find(ram=flavor) + + def _find_flavor(cs, flavor): """Get a flavor by name, ID, or RAM size.""" try: From 626c4805595678875c5950d30926b126ab679bb8 Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Wed, 14 Aug 2013 15:05:52 -0400 Subject: [PATCH 0241/1705] Update oslo Update gettext, striutils, timeutils and install_venv_common from oslo. Change-Id: Ibd9067e3e2be335ef75f0e4a5e4000d143030ab7 Signed-off-by: Chuck Short --- novaclient/openstack/common/gettextutils.py | 263 +++++++++++++++++++- novaclient/openstack/common/strutils.py | 108 ++++++-- novaclient/openstack/common/timeutils.py | 32 +-- requirements.txt | 1 + tools/install_venv_common.py | 22 +- 5 files changed, 377 insertions(+), 49 deletions(-) diff --git a/novaclient/openstack/common/gettextutils.py b/novaclient/openstack/common/gettextutils.py index 8a660b057..210d2bd1e 100644 --- a/novaclient/openstack/common/gettextutils.py +++ b/novaclient/openstack/common/gettextutils.py @@ -1,6 +1,7 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright 2012 Red Hat, Inc. +# Copyright 2013 IBM Corp. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -23,18 +24,27 @@ from novaclient.openstack.common.gettextutils import _ """ +import copy import gettext +import logging.handlers import os +import re +import UserString + +from babel import localedata +import six _localedir = os.environ.get('novaclient'.upper() + '_LOCALEDIR') _t = gettext.translation('novaclient', localedir=_localedir, fallback=True) +_AVAILABLE_LANGUAGES = [] + def _(msg): return _t.ugettext(msg) -def install(domain): +def install(domain, lazy=False): """Install a _() function using the given translation domain. Given a translation domain, install a _() function using gettext's @@ -44,7 +54,252 @@ def install(domain): overriding the default localedir (e.g. /usr/share/locale) using a translation-domain-specific environment variable (e.g. NOVA_LOCALEDIR). + + :param domain: the translation domain + :param lazy: indicates whether or not to install the lazy _() function. + The lazy _() introduces a way to do deferred translation + of messages by installing a _ that builds Message objects, + instead of strings, which can then be lazily translated into + any available locale. + """ + if lazy: + # NOTE(mrodden): Lazy gettext functionality. + # + # The following introduces a deferred way to do translations on + # messages in OpenStack. We override the standard _() function + # and % (format string) operation to build Message objects that can + # later be translated when we have more information. + # + # Also included below is an example LocaleHandler that translates + # Messages to an associated locale, effectively allowing many logs, + # each with their own locale. + + def _lazy_gettext(msg): + """Create and return a Message object. + + Lazy gettext function for a given domain, it is a factory method + for a project/module to get a lazy gettext function for its own + translation domain (i.e. nova, glance, cinder, etc.) + + Message encapsulates a string so that we can translate + it later when needed. + """ + return Message(msg, domain) + + import __builtin__ + __builtin__.__dict__['_'] = _lazy_gettext + else: + localedir = '%s_LOCALEDIR' % domain.upper() + gettext.install(domain, + localedir=os.environ.get(localedir), + unicode=True) + + +class Message(UserString.UserString, object): + """Class used to encapsulate translatable messages.""" + def __init__(self, msg, domain): + # _msg is the gettext msgid and should never change + self._msg = msg + self._left_extra_msg = '' + self._right_extra_msg = '' + self.params = None + self.locale = None + self.domain = domain + + @property + def data(self): + # NOTE(mrodden): this should always resolve to a unicode string + # that best represents the state of the message currently + + localedir = os.environ.get(self.domain.upper() + '_LOCALEDIR') + if self.locale: + lang = gettext.translation(self.domain, + localedir=localedir, + languages=[self.locale], + fallback=True) + else: + # use system locale for translations + lang = gettext.translation(self.domain, + localedir=localedir, + fallback=True) + + full_msg = (self._left_extra_msg + + lang.ugettext(self._msg) + + self._right_extra_msg) + + if self.params is not None: + full_msg = full_msg % self.params + + return six.text_type(full_msg) + + def _save_dictionary_parameter(self, dict_param): + full_msg = self.data + # look for %(blah) fields in string; + # ignore %% and deal with the + # case where % is first character on the line + keys = re.findall('(?:[^%]|^)?%\((\w*)\)[a-z]', full_msg) + + # if we don't find any %(blah) blocks but have a %s + if not keys and re.findall('(?:[^%]|^)%[a-z]', full_msg): + # apparently the full dictionary is the parameter + params = copy.deepcopy(dict_param) + else: + params = {} + for key in keys: + try: + params[key] = copy.deepcopy(dict_param[key]) + except TypeError: + # cast uncopyable thing to unicode string + params[key] = unicode(dict_param[key]) + + return params + + def _save_parameters(self, other): + # we check for None later to see if + # we actually have parameters to inject, + # so encapsulate if our parameter is actually None + if other is None: + self.params = (other, ) + elif isinstance(other, dict): + self.params = self._save_dictionary_parameter(other) + else: + # fallback to casting to unicode, + # this will handle the problematic python code-like + # objects that cannot be deep-copied + try: + self.params = copy.deepcopy(other) + except TypeError: + self.params = unicode(other) + + return self + + # overrides to be more string-like + def __unicode__(self): + return self.data + + def __str__(self): + return self.data.encode('utf-8') + + def __getstate__(self): + to_copy = ['_msg', '_right_extra_msg', '_left_extra_msg', + 'domain', 'params', 'locale'] + new_dict = self.__dict__.fromkeys(to_copy) + for attr in to_copy: + new_dict[attr] = copy.deepcopy(self.__dict__[attr]) + + return new_dict + + def __setstate__(self, state): + for (k, v) in state.items(): + setattr(self, k, v) + + # operator overloads + def __add__(self, other): + copied = copy.deepcopy(self) + copied._right_extra_msg += other.__str__() + return copied + + def __radd__(self, other): + copied = copy.deepcopy(self) + copied._left_extra_msg += other.__str__() + return copied + + def __mod__(self, other): + # do a format string to catch and raise + # any possible KeyErrors from missing parameters + self.data % other + copied = copy.deepcopy(self) + return copied._save_parameters(other) + + def __mul__(self, other): + return self.data * other + + def __rmul__(self, other): + return other * self.data + + def __getitem__(self, key): + return self.data[key] + + def __getslice__(self, start, end): + return self.data.__getslice__(start, end) + + def __getattribute__(self, name): + # NOTE(mrodden): handle lossy operations that we can't deal with yet + # These override the UserString implementation, since UserString + # uses our __class__ attribute to try and build a new message + # after running the inner data string through the operation. + # At that point, we have lost the gettext message id and can just + # safely resolve to a string instead. + ops = ['capitalize', 'center', 'decode', 'encode', + 'expandtabs', 'ljust', 'lstrip', 'replace', 'rjust', 'rstrip', + 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill'] + if name in ops: + return getattr(self.data, name) + else: + return UserString.UserString.__getattribute__(self, name) + + +def get_available_languages(domain): + """Lists the available languages for the given translation domain. + + :param domain: the domain to get languages for """ - gettext.install(domain, - localedir=os.environ.get(domain.upper() + '_LOCALEDIR'), - unicode=True) + if _AVAILABLE_LANGUAGES: + return _AVAILABLE_LANGUAGES + + localedir = '%s_LOCALEDIR' % domain.upper() + find = lambda x: gettext.find(domain, + localedir=os.environ.get(localedir), + languages=[x]) + + # NOTE(mrodden): en_US should always be available (and first in case + # order matters) since our in-line message strings are en_US + _AVAILABLE_LANGUAGES.append('en_US') + # NOTE(luisg): Babel <1.0 used a function called list(), which was + # renamed to locale_identifiers() in >=1.0, the requirements master list + # requires >=0.9.6, uncapped, so defensively work with both. We can remove + # this check when the master list updates to >=1.0, and all projects udpate + list_identifiers = (getattr(localedata, 'list', None) or + getattr(localedata, 'locale_identifiers')) + locale_identifiers = list_identifiers() + for i in locale_identifiers: + if find(i) is not None: + _AVAILABLE_LANGUAGES.append(i) + return _AVAILABLE_LANGUAGES + + +def get_localized_message(message, user_locale): + """Gets a localized version of the given message in the given locale.""" + if (isinstance(message, Message)): + if user_locale: + message.locale = user_locale + return unicode(message) + else: + return message + + +class LocaleHandler(logging.Handler): + """Handler that can have a locale associated to translate Messages. + + A quick example of how to utilize the Message class above. + LocaleHandler takes a locale and a target logging.Handler object + to forward LogRecord objects to after translating the internal Message. + """ + + def __init__(self, locale, target): + """Initialize a LocaleHandler + + :param locale: locale to use for translating messages + :param target: logging.Handler object to forward + LogRecord objects to after translation + """ + logging.Handler.__init__(self) + self.locale = locale + self.target = target + + def emit(self, record): + if isinstance(record.msg, Message): + # set the locale and resolve to a string + record.msg.locale = self.locale + + self.target.emit(record) diff --git a/novaclient/openstack/common/strutils.py b/novaclient/openstack/common/strutils.py index a9805373c..ef139aaad 100644 --- a/novaclient/openstack/common/strutils.py +++ b/novaclient/openstack/common/strutils.py @@ -19,18 +19,35 @@ System-level utilities and helper functions. """ +import re import sys +import unicodedata -from novaclient.openstack.common.gettextutils import _ +import six +from novaclient.openstack.common.gettextutils import _ # noqa + + +# Used for looking up extensions of text +# to their 'multiplied' byte amount +BYTE_MULTIPLIERS = { + '': 1, + 't': 1024 ** 4, + 'g': 1024 ** 3, + 'm': 1024 ** 2, + 'k': 1024, +} +BYTE_REGEX = re.compile(r'(^-?\d+)(\D*)') TRUE_STRINGS = ('1', 't', 'true', 'on', 'y', 'yes') FALSE_STRINGS = ('0', 'f', 'false', 'off', 'n', 'no') +SLUGIFY_STRIP_RE = re.compile(r"[^\w\s-]") +SLUGIFY_HYPHENATE_RE = re.compile(r"[-\s]+") + def int_from_bool_as_string(subject): - """ - Interpret a string as a boolean and return either 1 or 0. + """Interpret a string as a boolean and return either 1 or 0. Any string value in: @@ -44,8 +61,7 @@ def int_from_bool_as_string(subject): def bool_from_string(subject, strict=False): - """ - Interpret a string as a boolean. + """Interpret a string as a boolean. A case-insensitive match is performed such that strings matching 't', 'true', 'on', 'y', 'yes', or '1' are considered True and, when @@ -57,7 +73,7 @@ def bool_from_string(subject, strict=False): ValueError which is useful when parsing values passed in from an API call. Strings yielding False are 'f', 'false', 'off', 'n', 'no', or '0'. """ - if not isinstance(subject, basestring): + if not isinstance(subject, six.string_types): subject = str(subject) lowered = subject.strip().lower() @@ -78,21 +94,19 @@ def bool_from_string(subject, strict=False): def safe_decode(text, incoming=None, errors='strict'): - """ - Decodes incoming str using `incoming` if they're - not already unicode. + """Decodes incoming str using `incoming` if they're not already unicode. :param incoming: Text's current encoding :param errors: Errors handling policy. See here for valid values http://docs.python.org/2/library/codecs.html :returns: text or a unicode `incoming` encoded representation of it. - :raises TypeError: If text is not an isntance of basestring + :raises TypeError: If text is not an isntance of str """ - if not isinstance(text, basestring): + if not isinstance(text, six.string_types): raise TypeError("%s can't be decoded" % type(text)) - if isinstance(text, unicode): + if isinstance(text, six.text_type): return text if not incoming: @@ -119,11 +133,10 @@ def safe_decode(text, incoming=None, errors='strict'): def safe_encode(text, incoming=None, encoding='utf-8', errors='strict'): - """ - Encodes incoming str/unicode using `encoding`. If - incoming is not specified, text is expected to - be encoded with current python's default encoding. - (`sys.getdefaultencoding`) + """Encodes incoming str/unicode using `encoding`. + + If incoming is not specified, text is expected to be encoded with + current python's default encoding. (`sys.getdefaultencoding`) :param incoming: Text's current encoding :param encoding: Expected encoding for text (Default UTF-8) @@ -131,16 +144,16 @@ def safe_encode(text, incoming=None, values http://docs.python.org/2/library/codecs.html :returns: text or a bytestring `encoding` encoded representation of it. - :raises TypeError: If text is not an isntance of basestring + :raises TypeError: If text is not an isntance of str """ - if not isinstance(text, basestring): + if not isinstance(text, six.string_types): raise TypeError("%s can't be encoded" % type(text)) if not incoming: incoming = (sys.stdin.encoding or sys.getdefaultencoding()) - if isinstance(text, unicode): + if isinstance(text, six.text_type): return text.encode(encoding, errors) elif text and encoding != incoming: # Decode text before encoding it with `encoding` @@ -148,3 +161,58 @@ def safe_encode(text, incoming=None, return text.encode(encoding, errors) return text + + +def to_bytes(text, default=0): + """Converts a string into an integer of bytes. + + Looks at the last characters of the text to determine + what conversion is needed to turn the input text into a byte number. + Supports "B, K(B), M(B), G(B), and T(B)". (case insensitive) + + :param text: String input for bytes size conversion. + :param default: Default return value when text is blank. + + """ + match = BYTE_REGEX.search(text) + if match: + magnitude = int(match.group(1)) + mult_key_org = match.group(2) + if not mult_key_org: + return magnitude + elif text: + msg = _('Invalid string format: %s') % text + raise TypeError(msg) + else: + return default + mult_key = mult_key_org.lower().replace('b', '', 1) + multiplier = BYTE_MULTIPLIERS.get(mult_key) + if multiplier is None: + msg = _('Unknown byte multiplier: %s') % mult_key_org + raise TypeError(msg) + return magnitude * multiplier + + +def to_slug(value, incoming=None, errors="strict"): + """Normalize string. + + Convert to lowercase, remove non-word characters, and convert spaces + to hyphens. + + Inspired by Django's `slugify` filter. + + :param value: Text to slugify + :param incoming: Text's current encoding + :param errors: Errors handling policy. See here for valid + values http://docs.python.org/2/library/codecs.html + :returns: slugified unicode representation of `value` + :raises TypeError: If text is not an instance of str + """ + value = safe_decode(value, incoming, errors) + # NOTE(aababilov): no need to use safe_(encode|decode) here: + # encodings are always "ascii", error handling is always "ignore" + # and types are always known (first: unicode; second: str) + value = unicodedata.normalize("NFKD", value).encode( + "ascii", "ignore").decode("ascii") + value = SLUGIFY_STRIP_RE.sub("", value).strip().lower() + return SLUGIFY_HYPHENATE_RE.sub("-", value) diff --git a/novaclient/openstack/common/timeutils.py b/novaclient/openstack/common/timeutils.py index 609436590..aa9f70807 100644 --- a/novaclient/openstack/common/timeutils.py +++ b/novaclient/openstack/common/timeutils.py @@ -23,6 +23,7 @@ import datetime import iso8601 +import six # ISO 8601 extended time format with microseconds @@ -32,7 +33,7 @@ def isotime(at=None, subsecond=False): - """Stringify time in ISO 8601 format""" + """Stringify time in ISO 8601 format.""" if not at: at = utcnow() st = at.strftime(_ISO8601_TIME_FORMAT @@ -44,13 +45,13 @@ def isotime(at=None, subsecond=False): def parse_isotime(timestr): - """Parse time from ISO 8601 format""" + """Parse time from ISO 8601 format.""" try: return iso8601.parse_date(timestr) except iso8601.ParseError as e: - raise ValueError(e.message) + raise ValueError(unicode(e)) except TypeError as e: - raise ValueError(e.message) + raise ValueError(unicode(e)) def strtime(at=None, fmt=PERFECT_TIME_FORMAT): @@ -66,7 +67,7 @@ def parse_strtime(timestr, fmt=PERFECT_TIME_FORMAT): def normalize_time(timestamp): - """Normalize time in arbitrary timezone to UTC naive object""" + """Normalize time in arbitrary timezone to UTC naive object.""" offset = timestamp.utcoffset() if offset is None: return timestamp @@ -75,14 +76,14 @@ def normalize_time(timestamp): def is_older_than(before, seconds): """Return True if before is older than seconds.""" - if isinstance(before, basestring): + if isinstance(before, six.string_types): before = parse_strtime(before).replace(tzinfo=None) return utcnow() - before > datetime.timedelta(seconds=seconds) def is_newer_than(after, seconds): """Return True if after is newer than seconds.""" - if isinstance(after, basestring): + if isinstance(after, six.string_types): after = parse_strtime(after).replace(tzinfo=None) return after - utcnow() > datetime.timedelta(seconds=seconds) @@ -103,7 +104,7 @@ def utcnow(): def iso8601_from_timestamp(timestamp): - """Returns a iso8601 formated date from timestamp""" + """Returns a iso8601 formated date from timestamp.""" return isotime(datetime.datetime.utcfromtimestamp(timestamp)) @@ -111,9 +112,9 @@ def iso8601_from_timestamp(timestamp): def set_time_override(override_time=datetime.datetime.utcnow()): - """ - Override utils.utcnow to return a constant time or a list thereof, - one at a time. + """Overrides utils.utcnow. + + Make it return a constant time or a list thereof, one at a time. """ utcnow.override_time = override_time @@ -141,7 +142,8 @@ def clear_time_override(): def marshall_now(now=None): """Make an rpc-safe datetime with microseconds. - Note: tzinfo is stripped, but not required for relative times.""" + Note: tzinfo is stripped, but not required for relative times. + """ if not now: now = utcnow() return dict(day=now.day, month=now.month, year=now.year, hour=now.hour, @@ -161,7 +163,8 @@ def unmarshall_time(tyme): def delta_seconds(before, after): - """ + """Return the difference between two timing objects. + Compute the difference in seconds between two date, time, or datetime objects (as a float, to microsecond resolution). """ @@ -174,8 +177,7 @@ def delta_seconds(before, after): def is_soon(dt, window): - """ - Determines if time is going to happen in the next window seconds. + """Determines if time is going to happen in the next window seconds. :params dt: the time :params window: minimum seconds to remain to consider the time not soon diff --git a/requirements.txt b/requirements.txt index 38ff003fc..c4eb2a088 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ PrettyTable>=0.6,<0.8 requests>=1.1 simplejson>=2.0.9 six +Babel>=0.9.6 diff --git a/tools/install_venv_common.py b/tools/install_venv_common.py index f428c1e02..0999e2c29 100644 --- a/tools/install_venv_common.py +++ b/tools/install_venv_common.py @@ -114,9 +114,10 @@ def install_dependencies(self): print('Installing dependencies with pip (this can take a while)...') # First things first, make sure our venv has the latest pip and - # setuptools. - self.pip_install('pip>=1.3') + # setuptools and pbr + self.pip_install('pip>=1.4') self.pip_install('setuptools') + self.pip_install('pbr') self.pip_install('-r', self.requirements) self.pip_install('-r', self.test_requirements) @@ -201,12 +202,13 @@ def post_process(self): RHEL: https://bugzilla.redhat.com/958868 """ - # Install "patch" program if it's not there - if not self.check_pkg('patch'): - self.die("Please install 'patch'.") + if os.path.exists('contrib/redhat-eventlet.patch'): + # Install "patch" program if it's not there + if not self.check_pkg('patch'): + self.die("Please install 'patch'.") - # Apply the eventlet patch - self.apply_patch(os.path.join(self.venv, 'lib', self.py_version, - 'site-packages', - 'eventlet/green/subprocess.py'), - 'contrib/redhat-eventlet.patch') + # Apply the eventlet patch + self.apply_patch(os.path.join(self.venv, 'lib', self.py_version, + 'site-packages', + 'eventlet/green/subprocess.py'), + 'contrib/redhat-eventlet.patch') From e0e5c2dfe51373f33041c2412581491175432006 Mon Sep 17 00:00:00 2001 From: Joe Gordon Date: Thu, 15 Aug 2013 19:11:28 -0400 Subject: [PATCH 0242/1705] Fix and gate on H501, no locals for string formatting Change-Id: I07e4a1a733639a343d4b3ee20927d50ba04bd7a3 --- novaclient/utils.py | 4 +++- novaclient/v1_1/shell.py | 2 +- novaclient/v3/shell.py | 2 +- tox.ini | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/novaclient/utils.py b/novaclient/utils.py index 1bea47b6c..3fd9a182f 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -88,7 +88,9 @@ def get_resource_manager_extra_kwargs(f, args, allow_conflicts=False): conflicting_keys = set(hook_kwargs.keys()) & set(extra_kwargs.keys()) if conflicting_keys and not allow_conflicts: raise Exception("Hook '%(hook_name)s' is attempting to redefine" - " attributes '%(conflicting_keys)s'" % locals()) + " attributes '%(conflicting_keys)s'" % + {'hook_name': hook_name, + 'conflicting_keys': conflicting_keys}) extra_kwargs.update(hook_kwargs) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index c044cf49d..b44107893 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -386,7 +386,7 @@ def print_progress(progress): break elif status == "error": if not silent: - print("\nError %(action)s instance" % locals()) + print("\nError %s instance" % action) break if not silent: diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 7358a87a9..39cf322ae 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -387,7 +387,7 @@ def print_progress(progress): break elif status == "error": if not silent: - print("\nError %(action)s instance" % locals()) + print("\nError %s instance" % action) break if not silent: diff --git a/tox.ini b/tox.ini index e7e18c0d3..66c8f48f5 100644 --- a/tox.ini +++ b/tox.ini @@ -24,6 +24,6 @@ commands = python setup.py testr --coverage --testr-args='{posargs}' downloadcache = ~/cache/pip [flake8] -ignore = E12,E711,E721,E712,F841,F811,F821,H102,H302,H306,H403,H404,H501 +ignore = E12,E711,E721,E712,F841,F811,F821,H102,H302,H306,H403,H404 show-source = True exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build From 3c2a817efcb6954f3c8246c32ce997b9e3a4d000 Mon Sep 17 00:00:00 2001 From: Mahesh Panchaksharaiah Date: Fri, 19 Jul 2013 14:55:23 +0530 Subject: [PATCH 0243/1705] Added 'nova migration-list' command This command lets Admin's list migrations by applying filters Implements: blueprint list-resizes-through-admin-api Change-Id: I587c62dab537186cfc8b387fbc46cdb56fb9976c --- .../tests/v1_1/contrib/test_migrations.py | 42 ++++++++++ novaclient/tests/v1_1/fakes.py | 18 ++++ novaclient/tests/v1_1/test_shell.py | 11 +++ novaclient/v1_1/contrib/migrations.py | 84 +++++++++++++++++++ 4 files changed, 155 insertions(+) create mode 100644 novaclient/tests/v1_1/contrib/test_migrations.py create mode 100644 novaclient/v1_1/contrib/migrations.py diff --git a/novaclient/tests/v1_1/contrib/test_migrations.py b/novaclient/tests/v1_1/contrib/test_migrations.py new file mode 100644 index 000000000..7b49c4cc7 --- /dev/null +++ b/novaclient/tests/v1_1/contrib/test_migrations.py @@ -0,0 +1,42 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 novaclient import extension +from novaclient.tests import utils +from novaclient.tests.v1_1 import fakes +from novaclient.v1_1.contrib import migrations + +extensions = [ + extension.Extension(migrations.__name__.split(".")[-1], + migrations), +] +cs = fakes.FakeClient(extensions=extensions) + + +class MigrationsTest(utils.TestCase): + + def test_list_migrations(self): + ml = cs.migrations.list() + cs.assert_called('GET', '/os-migrations') + for m in ml: + self.assertTrue(isinstance(m, migrations.Migration)) + + def test_list_migrations_with_filters(self): + ml = cs.migrations.list('host1', 'finished', 'child1') + + cs.assert_called('GET', + '/os-migrations?status=finished&host=host1' + '&cell_name=child1') + for m in ml: + self.assertTrue(isinstance(m, migrations.Migration)) diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index 3e1e955a2..03e2c4847 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -1843,3 +1843,21 @@ def get_os_cells_capacities(self, **kw): def get_os_cells_child_cell_capacities(self, **kw): return self.get_os_cells_capacities() + + def get_os_migrations(self, **kw): + migrations = {'migrations': + [{ + "created_at": "2012-10-29T13:42:02.000000", + "dest_compute": "compute2", + "dest_host": "1.2.3.4", + "dest_node": "node2", + "id": 1234, + "instance_uuid": "instance_id_123", + "new_instance_type_id": 2, + "old_instance_type_id": 1, + "source_compute": "compute1", + "source_node": "node1", + "status": "Done", + "updated_at": "2012-10-29T13:42:02.000000" + }]} + return (200, {}, migrations) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index 1a08c0bb8..625b17789 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -1607,3 +1607,14 @@ def test_cell_capacities_with_cell_name(self): def test_cell_capacities_without_cell_name(self): self.run_command('cell-capacities') self.assert_called('GET', '/os-cells/capacities') + + def test_migration_list(self): + self.run_command('migration-list') + self.assert_called('GET', '/os-migrations') + + def test_migration_list_with_filters(self): + self.run_command('migration-list --host host1 --cell_name child1 ' + '--status finished') + self.assert_called('GET', + '/os-migrations?status=finished&host=host1' + '&cell_name=child1') diff --git a/novaclient/v1_1/contrib/migrations.py b/novaclient/v1_1/contrib/migrations.py new file mode 100644 index 000000000..ee2f49963 --- /dev/null +++ b/novaclient/v1_1/contrib/migrations.py @@ -0,0 +1,84 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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. + +""" +migration interface +""" + +import urllib + +from novaclient import base +from novaclient import utils + + +class Migration(base.Resource): + def __repr__(self): + return "" % self.id + + +class MigrationManager(base.ManagerWithFind): + resource_class = Migration + + def list(self, host=None, status=None, cell_name=None): + """ + Get a list of migrations. + :param host: (optional) filter migrations by host name. + :param status: (optional) filter migrations by status. + :param cell_name: (optional) filter migrations for a cell. + """ + opts = {} + if host: + opts['host'] = host + if status: + opts['status'] = status + if cell_name: + opts['cell_name'] = cell_name + + query_string = "?%s" % urllib.urlencode(opts) if opts else "" + + return self._list("/os-migrations%s" % query_string, "migrations") + + +@utils.arg('--host', + dest='host', + metavar='', + help='Fetch migrations for the given host.') +@utils.arg('--status', + dest='status', + metavar='', + help='Fetch migrations for the given status.') +@utils.arg('--cell_name', + dest='cell_name', + metavar='', + help='Fetch migrations for the given cell_name.') +def do_migration_list(cs, args): + """Print a list of migrations.""" + _print_migrations(cs.migrations.list(args.host, args.status, + args.cell_name)) + + +def _print_migrations(migrations): + fields = ['Source Node', 'Dest Node', 'Source Compute', 'Dest Compute', + 'Dest Host', 'Status', 'Instance UUID', 'Old Flavor', + 'New Flavor', 'Created At', 'Updated At'] + + def old_flavor(migration): + return migration.old_instance_type_id + + def new_flavor(migration): + return migration.new_instance_type_id + + formatters = {'Old Flavor': old_flavor, 'New Flavor': new_flavor} + + utils.print_list(migrations, fields, formatters) From d0619bc64558e26dae39eb669c4bc0d699d63f8d Mon Sep 17 00:00:00 2001 From: guoqingzhang Date: Tue, 20 Aug 2013 21:48:06 -0700 Subject: [PATCH 0244/1705] Update mailmap Change-Id: Ic3050ae029b367f099346dc99e76e33bd47bdc33 --- .mailmap | 1 + 1 file changed, 1 insertion(+) diff --git a/.mailmap b/.mailmap index 2b533aa89..959e033d6 100644 --- a/.mailmap +++ b/.mailmap @@ -17,3 +17,4 @@ Nikolay Sokolov Nokolay Sokolov Nokolay Sokolov +zhangguoqing From fa2867f1bd572a7728b1b051bbf04b3846be563c Mon Sep 17 00:00:00 2001 From: hwbi Date: Tue, 20 Aug 2013 22:00:51 -0700 Subject: [PATCH 0245/1705] Update mailmap Change-Id: I0f1f8b65db311fee9303dcf1a68c8057d9d778ab --- .mailmap | 1 + 1 file changed, 1 insertion(+) diff --git a/.mailmap b/.mailmap index 2b533aa89..aecf12b34 100644 --- a/.mailmap +++ b/.mailmap @@ -13,6 +13,7 @@ Johannes Erdfelt jerdfelt termie +hwbi Nikolay Sokolov Nokolay Sokolov Nikolay Sokolov Nokolay Sokolov From 61a80639545cf44e3e4141dbaa5afab0259c574a Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Fri, 9 Aug 2013 13:59:02 +0000 Subject: [PATCH 0246/1705] Sync py3kcompat from oslo Python3 reorganized the standard library and moved several functions to different modules and combined modules. Six provides a consistent interface to the module through six.moves However urllib/urlparse is not covered by six.moves so py3kcompat adds python2/python3 compatibility layer for urllib/urlparse. Change-Id: If1436d2260f1c8b6df8c514c8730e7bcf0e648b8 Signed-off-by: Chuck Short --- .../openstack/common/py3kcompat/__init__.py | 17 +++++++ .../openstack/common/py3kcompat/urlutils.py | 47 +++++++++++++++++++ openstack-common.conf | 1 + 3 files changed, 65 insertions(+) create mode 100644 novaclient/openstack/common/py3kcompat/__init__.py create mode 100644 novaclient/openstack/common/py3kcompat/urlutils.py diff --git a/novaclient/openstack/common/py3kcompat/__init__.py b/novaclient/openstack/common/py3kcompat/__init__.py new file mode 100644 index 000000000..be894cf50 --- /dev/null +++ b/novaclient/openstack/common/py3kcompat/__init__.py @@ -0,0 +1,17 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2013 Canonical Ltd. +# All Rights Reserved. +# +# 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. +# diff --git a/novaclient/openstack/common/py3kcompat/urlutils.py b/novaclient/openstack/common/py3kcompat/urlutils.py new file mode 100644 index 000000000..447102170 --- /dev/null +++ b/novaclient/openstack/common/py3kcompat/urlutils.py @@ -0,0 +1,47 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2013 Canonical Ltd. +# All Rights Reserved. +# +# 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. +# + +""" +Python2/Python3 compatibility layer for openstack +""" + +import six + +if six.PY3: + # python3 + import urllib.parse + + urlencode = urllib.parse.urlencode + quote = urllib.parse.quote + parse_qsl = urllib.parse.parse_qsl + urlparse = urllib.parse.urlparse + urlsplit = urllib.parse.urlsplit + urlunsplit = urllib.parse.urlunsplit +else: + # python2 + import urllib + import urlparse + + urlencode = urllib.urlencode + quote = urllib.quote + + parse = urlparse + parse_qsl = parse.parse_qsl + urlparse = parse.urlparse + urlsplit = parse.urlsplit + urlunsplit = parse.urlunsplit diff --git a/openstack-common.conf b/openstack-common.conf index 33f18d3a8..8b60c5b5d 100644 --- a/openstack-common.conf +++ b/openstack-common.conf @@ -5,6 +5,7 @@ module=install_venv_common module=strutils module=timeutils module=uuidutils +module=py3kcompat # The base module to hold the copy of openstack.common base=novaclient From fe5f07e891f45886a0e0e00134db36f310566ebd Mon Sep 17 00:00:00 2001 From: Joe Gordon Date: Fri, 23 Aug 2013 16:51:46 -0400 Subject: [PATCH 0247/1705] Upgrade to Hacking 0.7 There was a bug in hacking 0.6 that broke H202, assertRaises Exception too broad, so switch to Hacking 0.7 and fix the one H202 bug. Change-Id: I0ec9532ffbb6b3c8dbd775e6da7bc879b0a6737a --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index dcf5afb8e..07409e8e2 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,4 +1,4 @@ -hacking>=0.5.6,<0.7 +hacking>=0.5.6,<0.8 coverage>=3.6 discover From c450b39828a06c99a4bdb386398d8bd29c4193fa Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Mon, 19 Aug 2013 20:51:48 -0400 Subject: [PATCH 0248/1705] python3: Fix imports for py2/py3 Python3 reorganized the standard library and moved several functions to different modules. Six provides a consistent interface to them through the fake six.moves module. However, the urlparse, urllib2, etc modules have been combined into one module which Six does not support so we do it via py3kcompat. Modules such as StringIO and CStringIO have been removed completely so we use the io module. Change-Id: I53adac11b634de2c710fc39def36bcec96366710 Signed-off-by: Chuck Short --- novaclient/client.py | 6 +++--- novaclient/tests/test_shell.py | 6 +++--- novaclient/tests/test_utils.py | 8 ++++---- novaclient/tests/v1_1/fakes.py | 4 ++-- novaclient/tests/v1_1/test_servers.py | 12 ++++++------ novaclient/tests/v1_1/test_shell.py | 4 ++-- novaclient/v1_1/flavors.py | 5 ++--- novaclient/v1_1/floating_ip_dns.py | 7 +++---- novaclient/v1_1/hypervisors.py | 5 ++--- novaclient/v1_1/images.py | 5 ++--- novaclient/v1_1/limits.py | 5 ++--- novaclient/v1_1/security_groups.py | 5 ++--- novaclient/v1_1/servers.py | 5 ++--- novaclient/v1_1/volumes.py | 5 ++--- 14 files changed, 37 insertions(+), 45 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index 03bdb030c..02d75ec18 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -10,7 +10,6 @@ import logging import os import time -import urlparse import requests @@ -22,6 +21,7 @@ from novaclient import exceptions from novaclient import service_catalog from novaclient import utils +from novaclient.openstack.common.py3kcompat import urlutils class HTTPClient(object): @@ -298,7 +298,7 @@ def _fetch_endpoints_from_auth(self, url): extract_token=False) def authenticate(self): - magic_tuple = urlparse.urlsplit(self.auth_url) + magic_tuple = urlutils.urlsplit(self.auth_url) scheme, netloc, path, query, frag = magic_tuple port = magic_tuple.port if port is None: @@ -312,7 +312,7 @@ def authenticate(self): # TODO(sandy): Assume admin endpoint is 35357 for now. # Ideally this is going to have to be provided by the service catalog. new_netloc = netloc.replace(':%d' % port, ':%d' % (35357,)) - admin_url = urlparse.urlunsplit( + admin_url = urlutils.urlunsplit( (scheme, new_netloc, path, query, frag)) # FIXME(chmouel): This is to handle backward compatibiliy when diff --git a/novaclient/tests/test_shell.py b/novaclient/tests/test_shell.py index a4d8c5bed..ae9e6283f 100644 --- a/novaclient/tests/test_shell.py +++ b/novaclient/tests/test_shell.py @@ -1,4 +1,4 @@ -import cStringIO +import io import prettytable import re import sys @@ -43,8 +43,8 @@ def shell(self, argstr, exitcodes=(0,)): orig = sys.stdout orig_stderr = sys.stderr try: - sys.stdout = cStringIO.StringIO() - sys.stderr = cStringIO.StringIO() + sys.stdout = io.BytesIO() + sys.stderr = io.BytesIO() _shell = novaclient.shell.OpenStackComputeShell() _shell.main(argstr.split()) except SystemExit: diff --git a/novaclient/tests/test_utils.py b/novaclient/tests/test_utils.py index 9d03c0390..64c97614f 100644 --- a/novaclient/tests/test_utils.py +++ b/novaclient/tests/test_utils.py @@ -1,4 +1,4 @@ -import StringIO +import io import sys import mock @@ -112,7 +112,7 @@ def __init__(self, name, value): class PrintResultTestCase(test_utils.TestCase): - @mock.patch('sys.stdout', StringIO.StringIO()) + @mock.patch('sys.stdout', io.BytesIO()) def test_print_list_sort_by_str(self): objs = [_FakeResult("k1", 1), _FakeResult("k3", 2), @@ -129,7 +129,7 @@ def test_print_list_sort_by_str(self): '| k3 | 2 |\n' '+------+-------+\n') - @mock.patch('sys.stdout', StringIO.StringIO()) + @mock.patch('sys.stdout', io.BytesIO()) def test_print_list_sort_by_integer(self): objs = [_FakeResult("k1", 1), _FakeResult("k3", 2), @@ -147,7 +147,7 @@ def test_print_list_sort_by_integer(self): '+------+-------+\n') # without sorting - @mock.patch('sys.stdout', StringIO.StringIO()) + @mock.patch('sys.stdout', io.BytesIO()) def test_print_list_sort_by_none(self): objs = [_FakeResult("k1", 1), _FakeResult("k3", 3), diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index 6055f1abe..7649b4756 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -15,13 +15,13 @@ # limitations under the License. from datetime import datetime -import urlparse import six from novaclient import client as base_client from novaclient import exceptions from novaclient.openstack.common import strutils +from novaclient.openstack.common.py3kcompat import urlutils from novaclient.tests import fakes from novaclient.tests import utils from novaclient.v1_1 import client @@ -64,7 +64,7 @@ def _cs_request(self, url, method, **kwargs): assert 'body' in kwargs # Call the method - args = urlparse.parse_qsl(urlparse.urlparse(url)[4]) + args = urlutils.parse_qsl(urlutils.urlparse(url)[4]) kwargs.update(args) munged_url = url.rsplit('?', 1)[0] munged_url = munged_url.strip('/').replace('/', '_').replace('.', '_') diff --git a/novaclient/tests/v1_1/test_servers.py b/novaclient/tests/v1_1/test_servers.py index 701c88126..0dd206043 100644 --- a/novaclient/tests/v1_1/test_servers.py +++ b/novaclient/tests/v1_1/test_servers.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -import StringIO +import io import mock import six @@ -50,7 +50,7 @@ def test_create_server(self): key_name="fakekey", files={ '/etc/passwd': 'some data', # a file - '/tmp/foo.txt': StringIO.StringIO('data'), # a stream + '/tmp/foo.txt': io.BytesIO('data'), # a stream } ) cs.assert_called('POST', '/servers') @@ -94,10 +94,10 @@ def test_create_server_userdata_file_object(self): image=1, flavor=1, meta={'foo': 'bar'}, - userdata=StringIO.StringIO('hello moto'), + userdata=io.BytesIO('hello moto'), files={ '/etc/passwd': 'some data', # a file - '/tmp/foo.txt': StringIO.StringIO('data'), # a stream + '/tmp/foo.txt': io.BytesIO('data'), # a stream }, ) cs.assert_called('POST', '/servers') @@ -113,7 +113,7 @@ def test_create_server_userdata_unicode(self): key_name="fakekey", files={ '/etc/passwd': 'some data', # a file - '/tmp/foo.txt': StringIO.StringIO('data'), # a stream + '/tmp/foo.txt': io.BytesIO('data'), # a stream }, ) cs.assert_called('POST', '/servers') @@ -129,7 +129,7 @@ def test_create_server_userdata_utf8(self): key_name="fakekey", files={ '/etc/passwd': 'some data', # a file - '/tmp/foo.txt': StringIO.StringIO('data'), # a stream + '/tmp/foo.txt': io.BytesIO('data'), # a stream }, ) cs.assert_called('POST', '/servers') diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index fc3dd9448..be168d0d5 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -17,9 +17,9 @@ # under the License. import datetime +import io import os import mock -import StringIO import sys import tempfile @@ -71,7 +71,7 @@ def setUp(self): lambda *_: fakes.FakeClient)) self.addCleanup(timeutils.clear_time_override) - @mock.patch('sys.stdout', StringIO.StringIO()) + @mock.patch('sys.stdout', io.BytesIO()) def run_command(self, cmd): if isinstance(cmd, list): self.shell.main(cmd) diff --git a/novaclient/v1_1/flavors.py b/novaclient/v1_1/flavors.py index 02ca76cb4..c94a30652 100644 --- a/novaclient/v1_1/flavors.py +++ b/novaclient/v1_1/flavors.py @@ -2,11 +2,10 @@ """ Flavor interface. """ -import urllib - from novaclient import base from novaclient import exceptions from novaclient import utils +from novaclient.openstack.common.py3kcompat import urlutils class Flavor(base.Resource): @@ -89,7 +88,7 @@ def list(self, detailed=True, is_public=True): # and flavors from their own projects only. if not is_public: qparams['is_public'] = is_public - query_string = "?%s" % urllib.urlencode(qparams) if qparams else "" + query_string = "?%s" % urlutils.urlencode(qparams) if qparams else "" detail = "" if detailed: diff --git a/novaclient/v1_1/floating_ip_dns.py b/novaclient/v1_1/floating_ip_dns.py index f9c9ea4e8..a5dfcb8d9 100644 --- a/novaclient/v1_1/floating_ip_dns.py +++ b/novaclient/v1_1/floating_ip_dns.py @@ -13,9 +13,8 @@ # License for the specific language governing permissions and limitations # under the License. -import urllib - from novaclient import base +from novaclient.openstack.common.py3kcompat import urlutils def _quote_domain(domain): @@ -25,7 +24,7 @@ def _quote_domain(domain): but Routes tends to choke on them, so we need an extra level of by-hand quoting here. """ - return urllib.quote(domain.replace('.', '%2E')) + return urlutils.quote(domain.replace('.', '%2E')) class FloatingIPDNSDomain(base.Resource): @@ -102,7 +101,7 @@ def get(self, domain, name): def get_for_ip(self, domain, ip): """Return a list of entries for the given domain and ip or name.""" qparams = {'ip': ip} - params = "?%s" % urllib.urlencode(qparams) + params = "?%s" % urlutils.urlencode(qparams) return self._list("/os-floating-ip-dns/%s/entries%s" % (_quote_domain(domain), params), diff --git a/novaclient/v1_1/hypervisors.py b/novaclient/v1_1/hypervisors.py index 4d02576d6..fae04026a 100644 --- a/novaclient/v1_1/hypervisors.py +++ b/novaclient/v1_1/hypervisors.py @@ -17,9 +17,8 @@ Hypervisors interface (1.1 extension). """ -import urllib - from novaclient import base +from novaclient.openstack.common.py3kcompat import urlutils class Hypervisor(base.Resource): @@ -49,7 +48,7 @@ def search(self, hypervisor_match, servers=False): """ target = 'servers' if servers else 'search' url = ('/os-hypervisors/%s/%s' % - (urllib.quote(hypervisor_match, safe=''), target)) + (urlutils.quote(hypervisor_match, safe=''), target)) return self._list(url, 'hypervisors') def get(self, hypervisor): diff --git a/novaclient/v1_1/images.py b/novaclient/v1_1/images.py index fcc8b547b..eff012de5 100644 --- a/novaclient/v1_1/images.py +++ b/novaclient/v1_1/images.py @@ -2,9 +2,8 @@ """ Image interface. """ -import urllib - from novaclient import base +from novaclient.openstack.common.py3kcompat import urlutils class Image(base.Resource): @@ -51,7 +50,7 @@ def list(self, detailed=True, limit=None): detail = '/detail' if limit: params['limit'] = int(limit) - query = '?%s' % urllib.urlencode(params) if params else '' + query = '?%s' % urlutils.urlencode(params) if params else '' return self._list('/images%s%s' % (detail, query), 'images') def delete(self, image): diff --git a/novaclient/v1_1/limits.py b/novaclient/v1_1/limits.py index 4b4eabc95..8394bdd6b 100644 --- a/novaclient/v1_1/limits.py +++ b/novaclient/v1_1/limits.py @@ -1,8 +1,7 @@ # Copyright 2011 OpenStack Foundation -import urllib - from novaclient import base +from novaclient.openstack.common.py3kcompat import urlutils class Limits(base.Resource): @@ -83,6 +82,6 @@ def get(self, reserved=False, tenant_id=None): opts['reserved'] = 1 if tenant_id: opts['tenant_id'] = tenant_id - query_string = "?%s" % urllib.urlencode(opts) if opts else "" + query_string = "?%s" % urlutils.urlencode(opts) if opts else "" return self._get("/limits%s" % query_string, "limits") diff --git a/novaclient/v1_1/security_groups.py b/novaclient/v1_1/security_groups.py index 85e67de7b..b929b4e20 100644 --- a/novaclient/v1_1/security_groups.py +++ b/novaclient/v1_1/security_groups.py @@ -17,11 +17,10 @@ Security group interface (1.1 extension). """ -import urllib - import six from novaclient import base +from novaclient.openstack.common.py3kcompat import urlutils class SecurityGroup(base.Resource): @@ -91,7 +90,7 @@ def list(self, search_opts=None): qparams = dict((k, v) for (k, v) in six.iteritems(search_opts) if v) - query_string = '?%s' % urllib.urlencode(qparams) if qparams else '' + query_string = '?%s' % urlutils.urlencode(qparams) if qparams else '' return self._list('/os-security-groups%s' % query_string, 'security_groups') diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index 3cfb32614..e214d5f34 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -19,12 +19,11 @@ Server interface. """ -import urllib - import six from novaclient import base from novaclient import crypto +from novaclient.openstack.common.py3kcompat import urlutils REBOOT_SOFT, REBOOT_HARD = 'SOFT', 'HARD' @@ -382,7 +381,7 @@ def list(self, detailed=True, search_opts=None): if val: qparams[opt] = val - query_string = "?%s" % urllib.urlencode(qparams) if qparams else "" + query_string = "?%s" % urlutils.urlencode(qparams) if qparams else "" detail = "" if detailed: diff --git a/novaclient/v1_1/volumes.py b/novaclient/v1_1/volumes.py index b744c3397..1c4792015 100644 --- a/novaclient/v1_1/volumes.py +++ b/novaclient/v1_1/volumes.py @@ -17,11 +17,10 @@ Volume interface (1.1 extension). """ -import urllib - import six from novaclient import base +from novaclient.openstack.common.py3kcompat import urlutils class Volume(base.Resource): @@ -90,7 +89,7 @@ def list(self, detailed=True, search_opts=None): qparams = dict((k, v) for (k, v) in six.iteritems(search_opts) if v) - query_string = '?%s' % urllib.urlencode(qparams) if qparams else '' + query_string = '?%s' % urlutils.urlencode(qparams) if qparams else '' if detailed is True: return self._list("/volumes/detail%s" % query_string, "volumes") From d9782457fcc4539a0dbe795831114b6fc756d23c Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Wed, 28 Aug 2013 09:35:28 -0700 Subject: [PATCH 0249/1705] Added support for running the tests under PyPy with tox This is a precursor to having them run under check and gate. Change-Id: I105404d45cd4be93909ea5a272fc7e7c0c6d78cb --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index e7e18c0d3..ce5ec5969 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py26,py27,py33,pep8 +envlist = py26,py27,py33,pypy,pep8 [testenv] setenv = VIRTUAL_ENV={envdir} From 12d5b9578bf7b4149ad84800fd2336d87d3311ef Mon Sep 17 00:00:00 2001 From: Noorul Islam K M Date: Mon, 5 Aug 2013 14:11:58 +0530 Subject: [PATCH 0250/1705] Add interface for listing security groups of an instance This is already available in nova but not exposed via client. * novaclient/v1_1/servers.py: New interface to list security groups of an instance. * novaclient/v1_1/shell.py: New sub command list-secgroup. * novaclient/tests/v1_1/fakes.py, novaclient/tests/v1_1/test_servers.py, novaclient/tests/v1_1/test_shell.py: Add corresponding tests. Implements: blueprint servers-list-secgroup Change-Id: I505bcffdbb15b84bfd73cae5ef5a8fb9c69bd7b9 --- novaclient/tests/v1_1/fakes.py | 10 ++++++++++ novaclient/tests/v1_1/test_servers.py | 5 +++++ novaclient/tests/v1_1/test_shell.py | 4 ++++ novaclient/v1_1/servers.py | 18 +++++++++++++++++- novaclient/v1_1/shell.py | 8 ++++++++ 5 files changed, 44 insertions(+), 1 deletion(-) diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index 7649b4756..9f669bacf 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -430,6 +430,16 @@ def delete_servers_uuid3_metadata_key1(self, **kw): def delete_servers_uuid4_metadata_key1(self, **kw): return (200, {}, {'data': 'Fake diagnostics'}) + def get_servers_1234_os_security_groups(self, **kw): + return (200, {}, { + "security_groups": [{ + 'id': 1, + 'name': 'securitygroup1', + 'description': 'FAKE_SECURITY_GROUP', + 'tenant_id': '4ffc664c198e435e9853f2538fbcd7a7', + 'rules': []}] + }) + # # Server Addresses # diff --git a/novaclient/tests/v1_1/test_servers.py b/novaclient/tests/v1_1/test_servers.py index 0dd206043..a567f9c58 100644 --- a/novaclient/tests/v1_1/test_servers.py +++ b/novaclient/tests/v1_1/test_servers.py @@ -501,6 +501,11 @@ def test_remove_security_group(self): cs.servers.remove_security_group(s, 'oldsg') cs.assert_called('POST', '/servers/1234/action') + def test_list_security_group(self): + s = cs.servers.get(1234) + s.list_security_group() + cs.assert_called('GET', '/servers/1234/os-security-groups') + def test_evacuate(self): s = cs.servers.get(1234) s.evacuate('fake_target_host', 'True') diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index be168d0d5..1542c34d6 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -1545,6 +1545,10 @@ def test_server_security_group_remove(self): self.assert_called('POST', '/servers/1234/action', {'removeSecurityGroup': {'name': 'testgroup'}}) + def test_server_security_group_list(self): + self.run_command('list-secgroup 1234') + self.assert_called('GET', '/servers/1234/os-security-groups') + def test_interface_list(self): self.run_command('interface-list 1234') self.assert_called('GET', '/servers/1234/os-interface') diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index e214d5f34..36748907d 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -24,7 +24,7 @@ from novaclient import base from novaclient import crypto from novaclient.openstack.common.py3kcompat import urlutils - +from novaclient.v1_1.security_groups import SecurityGroup REBOOT_SOFT, REBOOT_HARD = 'SOFT', 'HARD' @@ -321,6 +321,12 @@ def remove_security_group(self, security_group): """ self.manager.remove_security_group(self, security_group) + def list_security_group(self): + """ + List security group(s) of an instance. + """ + return self.manager.list_security_group(self) + def evacuate(self, host, on_shared_storage, password=None): """ Evacuate an instance from failed host to specified host. @@ -851,6 +857,16 @@ def remove_security_group(self, server, security_group): """ self._action('removeSecurityGroup', server, {'name': security_group}) + def list_security_group(self, server): + """ + List Security Group(s) of an instance + + :param server: ID of the instance. + + """ + return self._list('/servers/%s/os-security-groups' % + base.getid(server), 'security_groups', SecurityGroup) + def evacuate(self, server, host, on_shared_storage, password=None): """ Evacuate a server instance. diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index a6ba50e4a..b078ca828 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1780,6 +1780,14 @@ def do_remove_secgroup(cs, args): server.remove_security_group(args.secgroup) +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_list_secgroup(cs, args): + """List Security Group(s) of a server.""" + server = _find_server(cs, args.server) + groups = server.list_security_group() + _print_secgroups(groups) + + @utils.arg('pool', metavar='', help='Name of Floating IP Pool. (Optional)', From 756a4333e6845468ba82c2ba8be245a849bc8507 Mon Sep 17 00:00:00 2001 From: Yufang Zhang Date: Wed, 7 Aug 2013 21:37:28 +0800 Subject: [PATCH 0251/1705] Suport instance list pagination in novaclient, Part I Bug 1209242 nova-api has supported pagination for long. A marker and limit option could be passed to nova-api to get a slice of instances. It makes sense to enable this feature in novaclient, so that horizon could use it for pagination supporting. Modification to shell.py would be submitted in a separate patch. Further change will also pass 'sort_key' and 'sort_dir' to nova-api, as long as nova supports this. This is part of blueprint support-pagination-for-instance-list Change-Id: Ieb5f2c1eb31b9f7e95b62b51ea7dc338e3970d04 --- novaclient/tests/v1_1/test_servers.py | 6 ++++++ novaclient/v1_1/servers.py | 11 ++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/novaclient/tests/v1_1/test_servers.py b/novaclient/tests/v1_1/test_servers.py index 0dd206043..57e04ddf9 100644 --- a/novaclient/tests/v1_1/test_servers.py +++ b/novaclient/tests/v1_1/test_servers.py @@ -26,6 +26,12 @@ def test_list_servers_undetailed(self): cs.assert_called('GET', '/servers') [self.assertTrue(isinstance(s, servers.Server)) for s in sl] + def test_list_servers_with_marker_limit(self): + sl = cs.servers.list(marker=1234, limit=2) + cs.assert_called('GET', '/servers/detail?marker=1234&limit=2') + for s in sl: + self.assertTrue(isinstance(s, servers.Server)) + def test_get_server_details(self): s = cs.servers.get(1234) cs.assert_called('GET', '/servers/1234') diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index e214d5f34..2d8bdb74f 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -363,12 +363,15 @@ def get(self, server): """ return self._get("/servers/%s" % base.getid(server), "server") - def list(self, detailed=True, search_opts=None): + def list(self, detailed=True, search_opts=None, marker=None, limit=None): """ Get a list of servers. :param detailed: Whether to return detailed server info (optional). :param search_opts: Search options to filter out servers (optional). + :param marker: Begin returning servers that appear later in the server + list than that represented by this server id (optional). + :param limit: Maximum number of servers to return (optional). :rtype: list of :class:`Server` """ @@ -381,6 +384,12 @@ def list(self, detailed=True, search_opts=None): if val: qparams[opt] = val + if marker: + qparams['marker'] = marker + + if limit: + qparams['limit'] = limit + query_string = "?%s" % urlutils.urlencode(qparams) if qparams else "" detail = "" From 69f8de69d59084e0ca6b85834a3029193b17469b Mon Sep 17 00:00:00 2001 From: Russell Bryant Date: Thu, 29 Aug 2013 20:03:28 -0400 Subject: [PATCH 0252/1705] Add support for os-assisted-volume-snapshots This patch adds support for the assisted volume snapshots API extension. This is used by Cinder to ask Nova to perform a volume snapshot on its behalf. It's required when the volume is actually file backed (like qcow2) and the hypervisor needs to be involved in the snapshot operation. Required for blueprint qemu-assisted-snapshots Change-Id: I50ee9bf92c8de98528638d1724fe35e07bed729e --- novaclient/tests/v1_1/contrib/fakes.py | 6 +++ .../contrib/test_assisted_volume_snapshots.py | 41 ++++++++++++++++ .../v1_1/contrib/assisted_volume_snapshots.py | 48 +++++++++++++++++++ 3 files changed, 95 insertions(+) create mode 100644 novaclient/tests/v1_1/contrib/test_assisted_volume_snapshots.py create mode 100644 novaclient/v1_1/contrib/assisted_volume_snapshots.py diff --git a/novaclient/tests/v1_1/contrib/fakes.py b/novaclient/tests/v1_1/contrib/fakes.py index a7bee0429..b9e79e841 100644 --- a/novaclient/tests/v1_1/contrib/fakes.py +++ b/novaclient/tests/v1_1/contrib/fakes.py @@ -129,3 +129,9 @@ def post_os_baremetal_nodes_1_action(self, **kw): return (202, {}, {}) else: return (500, {}, {}) + + def post_os_assisted_volume_snapshots(self, **kw): + return (202, {}, {'snapshot': {'id': 'blah', 'volumeId': '1'}}) + + def delete_os_assisted_volume_snapshots_x(self, **kw): + return (202, {}, {}) diff --git a/novaclient/tests/v1_1/contrib/test_assisted_volume_snapshots.py b/novaclient/tests/v1_1/contrib/test_assisted_volume_snapshots.py new file mode 100644 index 000000000..34d958f08 --- /dev/null +++ b/novaclient/tests/v1_1/contrib/test_assisted_volume_snapshots.py @@ -0,0 +1,41 @@ +# Copyright (C) 2013, Red Hat, Inc. +# +# 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. + +""" +Assisted volume snapshots - to be used by Cinder and not end users. +""" + +from novaclient import extension +from novaclient.tests import utils +from novaclient.tests.v1_1.contrib import fakes +from novaclient.v1_1.contrib import assisted_volume_snapshots as assisted_snaps + + +extensions = [ + extension.Extension(assisted_snaps.__name__.split(".")[-1], + assisted_snaps), +] +cs = fakes.FakeClient(extensions=extensions) + + +class AssistedVolumeSnapshotsTestCase(utils.TestCase): + + def test_create_snap(self): + res = cs.assisted_volume_snapshots.create('1', {}) + cs.assert_called('POST', '/os-assisted-volume-snapshots') + + def test_delete_snap(self): + res = cs.assisted_volume_snapshots.delete('x', {}) + cs.assert_called('DELETE', + '/os-assisted-volume-snapshots/x?delete_info={}') diff --git a/novaclient/v1_1/contrib/assisted_volume_snapshots.py b/novaclient/v1_1/contrib/assisted_volume_snapshots.py new file mode 100644 index 000000000..0f1773b49 --- /dev/null +++ b/novaclient/v1_1/contrib/assisted_volume_snapshots.py @@ -0,0 +1,48 @@ +# Copyright (C) 2013, Red Hat, Inc. +# +# 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. + +""" +Assisted volume snapshots - to be used by Cinder and not end users. +""" + +import json + +from novaclient import base + + +class Snapshot(base.Resource): + def __repr__(self): + return "" % self.id + + def delete(self): + """ + Delete this snapshot. + """ + self.manager.delete(self) + + +class AssistedSnapshotManager(base.Manager): + resource_class = Snapshot + + def create(self, volume_id, create_info): + body = {'snapshot': {'volume_id': volume_id, + 'create_info': create_info}} + return self._create('/os-assisted-volume-snapshots', body, 'snapshot') + + def delete(self, snapshot, delete_info): + self._delete("/os-assisted-volume-snapshots/%s?delete_info=%s" % ( + base.getid(snapshot), json.dumps(delete_info))) + +manager_class = AssistedSnapshotManager +name = 'assisted_volume_snapshots' From 97da33fcf599efeb29d7e496d34a450c78c557f5 Mon Sep 17 00:00:00 2001 From: fujioka yuuichi Date: Mon, 2 Sep 2013 11:03:50 +0900 Subject: [PATCH 0253/1705] Allow name argument to flavor-access-add Administrator cannot use the flavor name in arguments of "nova flavor-access-add" and "nova flavor-access-remove". This patch fixes this problem. Change-Id: I68b267dc071382f1978efc2088cd8e837455e8b4 Related-Bug: #1205298 --- novaclient/base.py | 7 ++++++- novaclient/tests/v1_1/fakes.py | 12 +++++++----- novaclient/utils.py | 1 + novaclient/v1_1/shell.py | 2 +- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/novaclient/base.py b/novaclient/base.py index b26134aa1..b80f9412f 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -212,7 +212,12 @@ def findall(self, **kwargs): list_kwargs['detailed'] = detailed if 'is_public' in list_argspec.args and 'is_public' in kwargs: - list_kwargs['is_public'] = kwargs['is_public'] + is_public = kwargs['is_public'] + list_kwargs['is_public'] = is_public + if is_public is None: + tmp_kwargs = kwargs.copy() + del tmp_kwargs['is_public'] + searches = tmp_kwargs.items() listing = self.list(**list_kwargs) diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index 6055f1abe..7d9b88c4e 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -591,11 +591,13 @@ def put_os_cloudpipe_configure_project(self, **kw): # def get_flavors(self, **kw): - return (200, {}, {'flavors': [ - {'id': 1, 'name': '256 MB Server'}, - {'id': 2, 'name': '512 MB Server'}, - {'id': 'aa1', 'name': '128 MB Server'} - ]}) + status, header, flavors = self.get_flavors_detail(**kw) + for flavor in flavors['flavors']: + for k in flavor.keys(): + if k not in ['id', 'name']: + del flavor[k] + + return (200, {}, flavors) def get_flavors_detail(self, **kw): flavors = {'flavors': [ diff --git a/novaclient/utils.py b/novaclient/utils.py index 6524605f8..ae28ca664 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -225,6 +225,7 @@ def find_resource(manager, name_or_id, **find_args): resource = getattr(manager, 'resource_class', None) name_attr = resource.NAME_ATTR if resource else 'name' kwargs = {name_attr: name_or_id} + kwargs.update(find_args) return manager.find(**kwargs) except exceptions.NotFound: msg = "No %s with a name or ID of '%s' exists." % \ diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index de22ada66..62194a775 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1378,7 +1378,7 @@ def _find_image(cs, image): def _find_flavor_for_admin(cs, flavor): """Get a flavor for administrator by name, ID, or RAM size.""" try: - return utils.find_resource(cs.flavors, flavor, is_public='None') + return utils.find_resource(cs.flavors, flavor, is_public=None) except exceptions.NotFound: return cs.flavors.find(ram=flavor) From 6a85c954c53f868251413db51cc1d9616acd4d02 Mon Sep 17 00:00:00 2001 From: Xavier Queralt Date: Fri, 26 Jul 2013 09:23:19 +0200 Subject: [PATCH 0254/1705] New syntax to boot from a block device mapping Add new arguments and syntax for booting from a block device mapping that use the new os-block-device-mapping-v2-boot extension. These allow to: * boot from an image, volume or snapshot (--image, --boot-volume, --snapshot) * attach any type of block device (--block-device). * attach an swap disk on boot (--swap). * attach an ephemeral disk on boot (--ephemeral). blueprint: improve-block-device-handling DocImpact Change-Id: I1aadeafed82b3bd1febcf0d1c3e64b258d6abeda --- novaclient/base.py | 68 +++++++----- novaclient/tests/v1_1/fakes.py | 12 ++- novaclient/tests/v1_1/test_shell.py | 157 ++++++++++++++++++++++++++++ novaclient/v1_1/servers.py | 8 +- novaclient/v1_1/shell.py | 132 ++++++++++++++++++++++- 5 files changed, 343 insertions(+), 34 deletions(-) diff --git a/novaclient/base.py b/novaclient/base.py index b26134aa1..0abb3848b 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -232,13 +232,45 @@ def findall(self, **kwargs): class BootingManagerWithFind(ManagerWithFind): """Like a `ManagerWithFind`, but has the ability to boot servers.""" + + def _parse_block_device_mapping(self, block_device_mapping): + bdm = [] + + for device_name, mapping in block_device_mapping.iteritems(): + # + # The mapping is in the format: + # :[]:[]:[] + # + bdm_dict = {'device_name': device_name} + + mapping_parts = mapping.split(':') + source_id = mapping_parts[0] + if len(mapping_parts) == 1: + bdm_dict['volume_id'] = source_id + + elif len(mapping_parts) > 1: + source_type = mapping_parts[1] + if source_type.startswith('snap'): + bdm_dict['snapshot_id'] = source_id + else: + bdm_dict['volume_id'] = source_id + + if len(mapping_parts) > 2 and mapping_parts[2]: + bdm_dict['volume_size'] = str(int(mapping_parts[2])) + + if len(mapping_parts) > 3: + bdm_dict['delete_on_termination'] = mapping_parts[3] + + bdm.append(bdm_dict) + return bdm + def _boot(self, resource_url, response_key, name, image, flavor, meta=None, files=None, userdata=None, reservation_id=None, return_raw=False, min_count=None, max_count=None, security_groups=None, key_name=None, - availability_zone=None, block_device_mapping=None, nics=None, - scheduler_hints=None, config_drive=None, admin_pass=None, - disk_config=None, **kwargs): + availability_zone=None, block_device_mapping=None, + block_device_mapping_v2=None, nics=None, scheduler_hints=None, + config_drive=None, admin_pass=None, disk_config=None, **kwargs): """ Create (boot) a new server. @@ -263,6 +295,8 @@ def _boot(self, resource_url, response_key, name, image, flavor, placement. :param block_device_mapping: A dict of block device mappings for this server. + :param block_device_mapping_v2: A dict of block device mappings V2 for + this server. :param nics: (optional extension) an ordered list of nics to be added to this server, with information about connected networks, fixed ips, etc. @@ -329,30 +363,10 @@ def _boot(self, resource_url, response_key, name, image, flavor, # Block device mappings are passed as a list of dictionaries if block_device_mapping: - bdm = body['server']['block_device_mapping'] = [] - for device_name, mapping in block_device_mapping.items(): - # - # The mapping is in the format: - # :[]:[]:[] - # - bdm_dict = {'device_name': device_name} - - mapping_parts = mapping.split(':') - id = mapping_parts[0] - if len(mapping_parts) == 1: - bdm_dict['volume_id'] = id - if len(mapping_parts) > 1: - type = mapping_parts[1] - if type.startswith('snap'): - bdm_dict['snapshot_id'] = id - else: - bdm_dict['volume_id'] = id - if len(mapping_parts) > 2: - if mapping_parts[2]: - bdm_dict['volume_size'] = str(int(mapping_parts[2])) - if len(mapping_parts) > 3: - bdm_dict['delete_on_termination'] = mapping_parts[3] - bdm.append(bdm_dict) + body['server']['block_device_mapping'] = \ + self._parse_block_device_mapping(block_device_mapping) + elif block_device_mapping_v2: + body['server']['block_device_mapping_v2'] = block_device_mapping_v2 if nics is not None: # NOTE(tr3buchet): nics can be an empty list diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index bc510c5c5..545ff782d 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -364,8 +364,18 @@ def post_servers(self, body, **kw): def post_os_volumes_boot(self, body, **kw): assert set(body.keys()) <= set(['server', 'os:scheduler_hints']) fakes.assert_has_keys(body['server'], - required=['name', 'block_device_mapping', 'flavorRef'], + required=['name', 'flavorRef'], optional=['imageRef']) + + # Require one, and only one, of the keys for bdm + if 'block_device_mapping' not in body['server']: + if 'block_device_mapping_v2' not in body['server']: + raise AssertionError( + "missing required keys: 'block_device_mapping'" + ) + elif 'block_device_mapping_v2' in body['server']: + raise AssertionError("found extra keys: 'block_device_mapping'") + return (202, {}, self.get_servers_9012()[2]) def get_servers_1234(self, **kw): diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index af9cd6318..236182c57 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -289,6 +289,163 @@ def test_boot_no_image_bdms(self): }}, ) + def test_boot_image_bdms_v2(self): + self.run_command( + 'boot --flavor 1 --image 1 --block-device id=fake-id,' + 'source=volume,dest=volume,device=vda,size=1,format=ext4,' + 'type=disk,shutdown=preserve some-server' + ) + self.assert_called_anytime( + 'POST', '/os-volumes_boot', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'block_device_mapping_v2': [ + { + 'uuid': 1, + 'source_type': 'image', + 'destination_type': 'local', + 'boot_index': 0, + 'delete_on_termination': True, + }, + { + 'uuid': 'fake-id', + 'source_type': 'volume', + 'destination_type': 'volume', + 'device_name': 'vda', + 'volume_size': '1', + 'guest_format': 'ext4', + 'device_type': 'disk', + 'delete_on_termination': False, + }, + ], + 'imageRef': '1', + 'min_count': 1, + 'max_count': 1, + }}, + ) + + def test_boot_no_image_bdms_v2(self): + self.run_command( + 'boot --flavor 1 --block-device id=fake-id,source=volume,' + 'dest=volume,bus=virtio,device=vda,size=1,format=ext4,bootindex=0,' + 'type=disk,shutdown=preserve some-server' + ) + self.assert_called_anytime( + 'POST', '/os-volumes_boot', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'block_device_mapping_v2': [ + { + 'uuid': 'fake-id', + 'source_type': 'volume', + 'destination_type': 'volume', + 'disk_bus': 'virtio', + 'device_name': 'vda', + 'volume_size': '1', + 'guest_format': 'ext4', + 'boot_index': '0', + 'device_type': 'disk', + 'delete_on_termination': False, + } + ], + 'imageRef': '', + 'min_count': 1, + 'max_count': 1, + }}, + ) + + cmd = 'boot --flavor 1 --boot-volume fake-id some-server' + self.run_command(cmd) + self.assert_called_anytime( + 'POST', '/os-volumes_boot', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'block_device_mapping_v2': [ + { + 'uuid': 'fake-id', + 'source_type': 'volume', + 'destination_type': 'volume', + 'boot_index': 0, + 'delete_on_termination': False, + } + ], + 'imageRef': '', + 'min_count': 1, + 'max_count': 1, + }}, + ) + + cmd = 'boot --flavor 1 --snapshot fake-id some-server' + self.run_command(cmd) + self.assert_called_anytime( + 'POST', '/os-volumes_boot', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'block_device_mapping_v2': [ + { + 'uuid': 'fake-id', + 'source_type': 'snapshot', + 'destination_type': 'volume', + 'boot_index': 0, + 'delete_on_termination': False, + } + ], + 'imageRef': '', + 'min_count': 1, + 'max_count': 1, + }}, + ) + + self.run_command('boot --flavor 1 --swap 1 some-server') + self.assert_called_anytime( + 'POST', '/os-volumes_boot', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'block_device_mapping_v2': [ + { + 'source_type': 'blank', + 'destination_type': 'local', + 'boot_index': -1, + 'guest_format': 'swap', + 'volume_size': '1', + 'delete_on_termination': True, + } + ], + 'imageRef': '', + 'min_count': 1, + 'max_count': 1, + }}, + ) + + self.run_command( + 'boot --flavor 1 --ephemeral size=1,format=ext4 some-server' + ) + self.assert_called_anytime( + 'POST', '/os-volumes_boot', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'block_device_mapping_v2': [ + { + 'source_type': 'blank', + 'destination_type': 'local', + 'boot_index': -1, + 'guest_format': 'ext4', + 'volume_size': '1', + 'delete_on_termination': True, + } + ], + 'imageRef': '', + 'min_count': 1, + 'max_count': 1, + }}, + ) + def test_boot_metadata(self): self.run_command('boot --image 1 --flavor 1 --meta foo=bar=pants' ' --meta spam=eggs some-server ') diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index 98c6a3532..8d2df4fdc 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -583,7 +583,8 @@ def create(self, name, image, flavor, meta=None, files=None, reservation_id=None, min_count=None, max_count=None, security_groups=None, userdata=None, key_name=None, availability_zone=None, - block_device_mapping=None, nics=None, scheduler_hints=None, + block_device_mapping=None, block_device_mapping_v2=None, + nics=None, scheduler_hints=None, config_drive=None, disk_config=None, **kwargs): # TODO(anthony): indicate in doc string if param is an extension # and/or optional @@ -611,6 +612,8 @@ def create(self, name, image, flavor, meta=None, files=None, placement. :param block_device_mapping: (optional extension) A dict of block device mappings for this server. + :param block_device_mapping_v2: (optional extension) A dict of block + device mappings for this server. :param nics: (optional extension) an ordered list of nics to be added to this server, with information about connected networks, fixed ips, port etc. @@ -642,6 +645,9 @@ def create(self, name, image, flavor, meta=None, files=None, if block_device_mapping: resource_url = "/os-volumes_boot" boot_kwargs['block_device_mapping'] = block_device_mapping + elif block_device_mapping_v2: + resource_url = "/os-volumes_boot" + boot_kwargs['block_device_mapping_v2'] = block_device_mapping_v2 else: resource_url = "/servers" if nics: diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 7f805217b..55ae79d50 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -36,6 +36,20 @@ from novaclient.v1_1 import servers +CLIENT_BDM2_KEYS = { + 'id': 'uuid', + 'source': 'source_type', + 'dest': 'destination_type', + 'bus': 'disk_bus', + 'device': 'device_name', + 'size': 'volume_size', + 'format': 'guest_format', + 'bootindex': 'boot_index', + 'type': 'device_type', + 'shutdown': 'delete_on_termination', +} + + def _key_value_pairing(text): try: (k, v) = text.split('=', 1) @@ -58,6 +72,66 @@ def _match_image(cs, wanted_properties): return images_matched +def _parse_block_device_mapping_v2(args, image): + bdm = [] + + if args.boot_volume: + bdm_dict = {'uuid': args.boot_volume, 'source_type': 'volume', + 'destination_type': 'volume', 'boot_index': 0, + 'delete_on_termination': False} + bdm.append(bdm_dict) + + if args.snapshot: + bdm_dict = {'uuid': args.snapshot, 'source_type': 'snapshot', + 'destination_type': 'volume', 'boot_index': 0, + 'delete_on_termination': False} + bdm.append(bdm_dict) + + for device_spec in args.block_device: + spec_dict = dict(v.split('=') for v in device_spec.split(',')) + bdm_dict = {} + + for key, value in spec_dict.iteritems(): + bdm_dict[CLIENT_BDM2_KEYS[key]] = value + + # Convert the delete_on_termination to a boolean or set it to true by + # default for local block devices when not specified. + if 'delete_on_termination' in bdm_dict: + action = bdm_dict['delete_on_termination'] + bdm_dict['delete_on_termination'] = (action == 'remove') + elif bdm_dict.get('destination_type') == 'local': + bdm_dict['delete_on_termination'] = True + + bdm.append(bdm_dict) + + for ephemeral_spec in args.ephemeral: + bdm_dict = {'source_type': 'blank', 'destination_type': 'local', + 'boot_index': -1, 'delete_on_termination': True} + + eph_dict = dict(v.split('=') for v in ephemeral_spec.split(',')) + if 'size' in eph_dict: + bdm_dict['volume_size'] = eph_dict['size'] + if 'format' in eph_dict: + bdm_dict['guest_format'] = eph_dict['format'] + + bdm.append(bdm_dict) + + if args.swap: + bdm_dict = {'source_type': 'blank', 'destination_type': 'local', + 'boot_index': -1, 'delete_on_termination': True, + 'guest_format': 'swap', 'volume_size': args.swap} + bdm.append(bdm_dict) + + # Append the image to the list only if we have new style BDMs + if bdm and not args.block_device_mapping and image: + bdm_dict = {'uuid': image.id, 'source_type': 'image', + 'destination_type': 'local', 'boot_index': 0, + 'delete_on_termination': True} + bdm.insert(0, bdm_dict) + + return bdm + + def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): """Boot a new server.""" if min_count is None: @@ -83,11 +157,6 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): # are selecting the first of many? image = images[0] - if not image and not args.block_device_mapping: - raise exceptions.CommandError("you need to specify an Image ID " - "or a block device mapping " - "or provide a set of properties to match" - " against an image") if not args.flavor: raise exceptions.CommandError("you need to specify a Flavor ID ") @@ -140,6 +209,25 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): device_name, mapping = bdm.split('=', 1) block_device_mapping[device_name] = mapping + block_device_mapping_v2 = _parse_block_device_mapping_v2(args, image) + + n_boot_args = len(filter(None, (image, args.boot_volume, args.snapshot))) + have_bdm = block_device_mapping_v2 or block_device_mapping + + # Fail if more than one boot devices are present + # or if there is no device to boot from. + if n_boot_args > 1 or n_boot_args == 0 and not have_bdm: + raise exceptions.CommandError( + "you need to specify at least one source ID (Image, Snapshot or " + "Volume), a block device mapping or provide a set of properties " + "to match against an image") + + if block_device_mapping and block_device_mapping_v2: + raise exceptions.CommandError( + "you can't mix old block devices (--block-device-mapping) " + "with the new ones (--block-device, --boot-volume, --snapshot, " + "--ephemeral, --swap)") + nics = [] for nic_str in args.nics: err_msg = ("Invalid nic argument '%s'. Nic arguments must be of the " @@ -196,6 +284,7 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): availability_zone=availability_zone, security_groups=security_groups, block_device_mapping=block_device_mapping, + block_device_mapping_v2=block_device_mapping_v2, nics=nics, scheduler_hints=hints, config_drive=config_drive) @@ -217,6 +306,14 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): action='append', metavar='', help="Image metadata property (see 'nova image-show'). ") +@utils.arg('--boot-volume', + default=None, + metavar="", + help="Volume ID to boot from.") +@utils.arg('--snapshot', + default=None, + metavar="", + help="Sapshot ID to boot from (will create a volume).") @utils.arg('--num-instances', default=None, type=int, @@ -269,6 +366,31 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): @utils.arg('--block_device_mapping', action='append', help=argparse.SUPPRESS) +@utils.arg('--block-device', + metavar="key1=value1[,key2=value2...]", + action='append', + default=[], + help="Block device mapping with the keys: " + "id=image_id, snapshot_id or volume_id, " + "source=source type (image, snapshot, volume or blank), " + "dest=destination type of the block device (volume or local), " + "bus=device's bus, " + "device=name of the device (e.g. vda, xda, ...), " + "size=size of the block device in GB, " + "format=device will be formatted (e.g. swap, ext3, ntfs, ...), " + "bootindex=integer used for ordering the boot disks, " + "type=device type (e.g. disk, cdrom, ...) and " + "shutdown=shutdown behaviour (either preserve or remove).") +@utils.arg('--swap', + metavar="", + default=None, + help="Create and attach a local swap block device of MB.") +@utils.arg('--ephemeral', + metavar="size=[,format=]", + action='append', + default=[], + help="Create and attach a local ephemeral block device of GB " + "and format it to .") @utils.arg('--hint', action='append', dest='scheduler_hints', From bec4e51df4aef33d2c76889af56258cc00fed357 Mon Sep 17 00:00:00 2001 From: Ken'ichi Ohmichi Date: Thu, 29 Aug 2013 11:33:59 +0900 Subject: [PATCH 0255/1705] Add delete method to Flavor class In tempest, some tests use delete() method of each resource class (Server, SecurityGroup, etc.) for cleaning up when each test finishes. Now we are adding some tests, which create a Flavor instance, to tempest and we need the delete() method of Flavor class. Fixes bug #1218156 Change-Id: I210d62aa45510858346315046cf57ea7b1de7b7b --- novaclient/tests/v1_1/test_flavors.py | 10 ++++++++++ novaclient/v1_1/flavors.py | 6 ++++++ 2 files changed, 16 insertions(+) diff --git a/novaclient/tests/v1_1/test_flavors.py b/novaclient/tests/v1_1/test_flavors.py index 146f323b7..a50783bdd 100644 --- a/novaclient/tests/v1_1/test_flavors.py +++ b/novaclient/tests/v1_1/test_flavors.py @@ -182,6 +182,16 @@ def test_delete(self): cs.flavors.delete("flavordelete") cs.assert_called('DELETE', '/flavors/flavordelete') + def test_delete_with_flavor_instance(self): + f = cs.flavors.get(2) + cs.flavors.delete(f) + cs.assert_called('DELETE', '/flavors/2') + + def test_delete_with_flavor_instance_method(self): + f = cs.flavors.get(2) + f.delete() + cs.assert_called('DELETE', '/flavors/2') + def test_set_keys(self): f = cs.flavors.get(1) f.set_keys({'k1': 'v1'}) diff --git a/novaclient/v1_1/flavors.py b/novaclient/v1_1/flavors.py index c94a30652..61f615c0f 100644 --- a/novaclient/v1_1/flavors.py +++ b/novaclient/v1_1/flavors.py @@ -68,6 +68,12 @@ def unset_keys(self, keys): "/flavors/%s/os-extra_specs/%s" % ( base.getid(self), k)) + def delete(self): + """ + Delete this flavor. + """ + self.manager.delete(self) + class FlavorManager(base.ManagerWithFind): """ From cab46172253add86a5a25431a1af4066d29418bc Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Fri, 6 Sep 2013 08:02:31 -0400 Subject: [PATCH 0256/1705] Update oslo from oslo-incubator Update oslo from oslo-incubator includes various python3 fixes. Change-Id: Ie30a4c319125c3d4fb704254f8553bc8fd960eae Signed-off-by: Chuck Short --- novaclient/openstack/common/gettextutils.py | 44 ++++++++++++++----- .../openstack/common/py3kcompat/urlutils.py | 4 +- novaclient/openstack/common/timeutils.py | 6 +++ 3 files changed, 41 insertions(+), 13 deletions(-) diff --git a/novaclient/openstack/common/gettextutils.py b/novaclient/openstack/common/gettextutils.py index 210d2bd1e..01a4ce59a 100644 --- a/novaclient/openstack/common/gettextutils.py +++ b/novaclient/openstack/common/gettextutils.py @@ -26,10 +26,13 @@ import copy import gettext -import logging.handlers +import logging import os import re -import UserString +try: + import UserString as _userString +except ImportError: + import collections as _userString from babel import localedata import six @@ -37,11 +40,27 @@ _localedir = os.environ.get('novaclient'.upper() + '_LOCALEDIR') _t = gettext.translation('novaclient', localedir=_localedir, fallback=True) -_AVAILABLE_LANGUAGES = [] +_AVAILABLE_LANGUAGES = {} +USE_LAZY = False + + +def enable_lazy(): + """Convenience function for configuring _() to use lazy gettext + + Call this at the start of execution to enable the gettextutils._ + function to use lazy gettext functionality. This is useful if + your project is importing _ directly instead of using the + gettextutils.install() way of importing the _ function. + """ + global USE_LAZY + USE_LAZY = True def _(msg): - return _t.ugettext(msg) + if USE_LAZY: + return Message(msg, 'novaclient') + else: + return _t.ugettext(msg) def install(domain, lazy=False): @@ -95,7 +114,7 @@ def _lazy_gettext(msg): unicode=True) -class Message(UserString.UserString, object): +class Message(_userString.UserString, object): """Class used to encapsulate translatable messages.""" def __init__(self, msg, domain): # _msg is the gettext msgid and should never change @@ -236,7 +255,7 @@ def __getattribute__(self, name): if name in ops: return getattr(self.data, name) else: - return UserString.UserString.__getattribute__(self, name) + return _userString.UserString.__getattribute__(self, name) def get_available_languages(domain): @@ -244,8 +263,8 @@ def get_available_languages(domain): :param domain: the domain to get languages for """ - if _AVAILABLE_LANGUAGES: - return _AVAILABLE_LANGUAGES + if domain in _AVAILABLE_LANGUAGES: + return copy.copy(_AVAILABLE_LANGUAGES[domain]) localedir = '%s_LOCALEDIR' % domain.upper() find = lambda x: gettext.find(domain, @@ -254,7 +273,7 @@ def get_available_languages(domain): # NOTE(mrodden): en_US should always be available (and first in case # order matters) since our in-line message strings are en_US - _AVAILABLE_LANGUAGES.append('en_US') + language_list = ['en_US'] # NOTE(luisg): Babel <1.0 used a function called list(), which was # renamed to locale_identifiers() in >=1.0, the requirements master list # requires >=0.9.6, uncapped, so defensively work with both. We can remove @@ -264,13 +283,14 @@ def get_available_languages(domain): locale_identifiers = list_identifiers() for i in locale_identifiers: if find(i) is not None: - _AVAILABLE_LANGUAGES.append(i) - return _AVAILABLE_LANGUAGES + language_list.append(i) + _AVAILABLE_LANGUAGES[domain] = language_list + return copy.copy(language_list) def get_localized_message(message, user_locale): """Gets a localized version of the given message in the given locale.""" - if (isinstance(message, Message)): + if isinstance(message, Message): if user_locale: message.locale = user_locale return unicode(message) diff --git a/novaclient/openstack/common/py3kcompat/urlutils.py b/novaclient/openstack/common/py3kcompat/urlutils.py index 447102170..04b3418da 100644 --- a/novaclient/openstack/common/py3kcompat/urlutils.py +++ b/novaclient/openstack/common/py3kcompat/urlutils.py @@ -17,7 +17,7 @@ # """ -Python2/Python3 compatibility layer for openstack +Python2/Python3 compatibility layer for OpenStack """ import six @@ -27,6 +27,7 @@ import urllib.parse urlencode = urllib.parse.urlencode + urljoin = urllib.parse.urljoin quote = urllib.parse.quote parse_qsl = urllib.parse.parse_qsl urlparse = urllib.parse.urlparse @@ -42,6 +43,7 @@ parse = urlparse parse_qsl = parse.parse_qsl + urljoin = parse.urljoin urlparse = parse.urlparse urlsplit = parse.urlsplit urlunsplit = parse.urlunsplit diff --git a/novaclient/openstack/common/timeutils.py b/novaclient/openstack/common/timeutils.py index aa9f70807..60f02bcb9 100644 --- a/novaclient/openstack/common/timeutils.py +++ b/novaclient/openstack/common/timeutils.py @@ -21,6 +21,7 @@ import calendar import datetime +import time import iso8601 import six @@ -90,6 +91,11 @@ def is_newer_than(after, seconds): def utcnow_ts(): """Timestamp version of our utcnow function.""" + if utcnow.override_time is None: + # NOTE(kgriffs): This is several times faster + # than going through calendar.timegm(...) + return int(time.time()) + return calendar.timegm(utcnow().timetuple()) From 28f97734203a2df33805291673104742d45d34f3 Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Sat, 7 Sep 2013 12:27:46 -0400 Subject: [PATCH 0257/1705] Python3: Use six.StringIO for io.Bytes() The newer version of six (1.4.1) provides six.StringIO which is a fake file object for textual data. It's an alias for StringIO.StringIO in python2 and io.StringIO in Python3. Use the fake object where approiate. Change-Id: I364001933b4f2305ac27b293a9a4a3fec36c8a49 Signed-off-by: Chuck Short --- novaclient/tests/test_utils.py | 8 ++++---- novaclient/tests/v1_1/test_servers.py | 12 +++++------- novaclient/tests/v1_1/test_shell.py | 4 ++-- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/novaclient/tests/test_utils.py b/novaclient/tests/test_utils.py index 64c97614f..8809133ac 100644 --- a/novaclient/tests/test_utils.py +++ b/novaclient/tests/test_utils.py @@ -1,7 +1,7 @@ -import io import sys import mock +import six from novaclient import exceptions from novaclient import utils @@ -112,7 +112,7 @@ def __init__(self, name, value): class PrintResultTestCase(test_utils.TestCase): - @mock.patch('sys.stdout', io.BytesIO()) + @mock.patch('sys.stdout', six.StringIO()) def test_print_list_sort_by_str(self): objs = [_FakeResult("k1", 1), _FakeResult("k3", 2), @@ -129,7 +129,7 @@ def test_print_list_sort_by_str(self): '| k3 | 2 |\n' '+------+-------+\n') - @mock.patch('sys.stdout', io.BytesIO()) + @mock.patch('sys.stdout', six.StringIO()) def test_print_list_sort_by_integer(self): objs = [_FakeResult("k1", 1), _FakeResult("k3", 2), @@ -147,7 +147,7 @@ def test_print_list_sort_by_integer(self): '+------+-------+\n') # without sorting - @mock.patch('sys.stdout', io.BytesIO()) + @mock.patch('sys.stdout', six.StringIO()) def test_print_list_sort_by_none(self): objs = [_FakeResult("k1", 1), _FakeResult("k3", 3), diff --git a/novaclient/tests/v1_1/test_servers.py b/novaclient/tests/v1_1/test_servers.py index 19cca5bb1..658068f77 100644 --- a/novaclient/tests/v1_1/test_servers.py +++ b/novaclient/tests/v1_1/test_servers.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -import io - import mock import six @@ -56,7 +54,7 @@ def test_create_server(self): key_name="fakekey", files={ '/etc/passwd': 'some data', # a file - '/tmp/foo.txt': io.BytesIO('data'), # a stream + '/tmp/foo.txt': six.StringIO('data'), # a stream } ) cs.assert_called('POST', '/servers') @@ -100,10 +98,10 @@ def test_create_server_userdata_file_object(self): image=1, flavor=1, meta={'foo': 'bar'}, - userdata=io.BytesIO('hello moto'), + userdata=six.StringIO('hello moto'), files={ '/etc/passwd': 'some data', # a file - '/tmp/foo.txt': io.BytesIO('data'), # a stream + '/tmp/foo.txt': six.StringIO('data'), # a stream }, ) cs.assert_called('POST', '/servers') @@ -119,7 +117,7 @@ def test_create_server_userdata_unicode(self): key_name="fakekey", files={ '/etc/passwd': 'some data', # a file - '/tmp/foo.txt': io.BytesIO('data'), # a stream + '/tmp/foo.txt': six.StringIO('data'), # a stream }, ) cs.assert_called('POST', '/servers') @@ -135,7 +133,7 @@ def test_create_server_userdata_utf8(self): key_name="fakekey", files={ '/etc/passwd': 'some data', # a file - '/tmp/foo.txt': io.BytesIO('data'), # a stream + '/tmp/foo.txt': six.StringIO('data'), # a stream }, ) cs.assert_called('POST', '/servers') diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index 236182c57..d7a5e0d38 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -17,13 +17,13 @@ # under the License. import datetime -import io import os import mock import sys import tempfile import fixtures +import six import novaclient.client from novaclient import exceptions @@ -71,7 +71,7 @@ def setUp(self): lambda *_: fakes.FakeClient)) self.addCleanup(timeutils.clear_time_override) - @mock.patch('sys.stdout', io.BytesIO()) + @mock.patch('sys.stdout', six.StringIO()) def run_command(self, cmd): if isinstance(cmd, list): self.shell.main(cmd) From 9d8869e01c51f2611c187c45701033f507f40e2a Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Sat, 7 Sep 2013 12:33:31 -0400 Subject: [PATCH 0258/1705] Python3: Fix traceback while running unit tests While running the unit tests with python3 the following traceback appears: TypeError: can't use a string pattern on a bytes-like object This is due to the way that python2 and python3 handles unicodes. Change-Id: I401f1cefed69780073222cae98d8da4c3d8031a8 Signed-off-by: Chuck Short --- novaclient/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/novaclient/utils.py b/novaclient/utils.py index 69b9758c6..03c5d9d60 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -343,7 +343,8 @@ def slugify(value): import unicodedata if not isinstance(value, unicode): value = six.text_type(value) - value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore') + value = unicodedata.normalize('NFKD', value).encode('ascii', + 'ignore').decode("ascii") value = six.text_type(_slugify_strip_re.sub('', value).strip().lower()) return _slugify_hyphenate_re.sub('-', value) From 3523ba90f68d55ac20fb044d9acbe6c53fecbf1f Mon Sep 17 00:00:00 2001 From: Vitaliy Kolosov Date: Mon, 9 Sep 2013 12:11:32 +0300 Subject: [PATCH 0259/1705] Unittests added for client v1_1 Unittests test_client_set_management_url_v1_1 and test_client_get_reset_timings_v1_1 added. New methods covered: * client.set_management_url() * client.get_timings() * client.reset_timings() Change-Id: I46cac01864a11fbaffc284d26f63b8e00f2631f0 --- novaclient/tests/test_client.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/novaclient/tests/test_client.py b/novaclient/tests/test_client.py index 9f5d90f39..4fabca26a 100644 --- a/novaclient/tests/test_client.py +++ b/novaclient/tests/test_client.py @@ -122,3 +122,20 @@ def test_client_with_no_cache_disabled(self): auth_url="foo/v2", no_cache=False) self.assertEqual(True, cs.os_cache) self.assertEqual(True, cs.client.os_cache) + + def test_client_set_management_url_v1_1(self): + cs = novaclient.v1_1.client.Client("user", "password", "project_id", + auth_url="foo/v2") + cs.set_management_url("blabla") + self.assertEqual("blabla", cs.client.management_url) + + def test_client_get_reset_timings_v1_1(self): + cs = novaclient.v1_1.client.Client("user", "password", "project_id", + auth_url="foo/v2") + self.assertEqual(0, len(cs.get_timings())) + cs.client.times.append("somevalue") + self.assertEqual(1, len(cs.get_timings())) + self.assertEqual("somevalue", cs.get_timings()[0]) + + cs.reset_timings() + self.assertEqual(0, len(cs.get_timings())) From 605a7ed2f24f7ac26a9e7bd9961be144e50cc5e4 Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Fri, 6 Sep 2013 08:52:45 -0400 Subject: [PATCH 0260/1705] python3: Fix Traceback while running unit tests Python3 uses the the concepts of text and (binary) data instead of Unicode strings and 8-bit strings. In python3 all text is unicode; however encoded unicode is represented as binary data. Use the six module to use six.binary_types and six.text_types where apporiate: - six.text_type unicode() in python2 and str in python3. - six.basestring() in python2 and str in python3. blueprint python3-novaclient Change-Id: Ie7b393abe6beac22eaf127b7fc7bbc372c338211 Signed-off-by: Chuck Short --- novaclient/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/novaclient/utils.py b/novaclient/utils.py index 69b9758c6..3004b993d 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -183,7 +183,7 @@ def print_dict(d, dict_property="Property", dict_value="Value", wrap=0): v = textwrap.fill(str(v), wrap) # if value has a newline, add in multiple rows # e.g. fault with stacktrace - if v and isinstance(v, basestring) and r'\n' in v: + if v and isinstance(v, six.string_types) and r'\n' in v: lines = v.strip().split(r'\n') col1 = k for line in lines: @@ -341,7 +341,7 @@ def slugify(value): From Django's "django/template/defaultfilters.py". """ import unicodedata - if not isinstance(value, unicode): + if not isinstance(value, six.text_type): value = six.text_type(value) value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore') value = six.text_type(_slugify_strip_re.sub('', value).strip().lower()) From 4f3c2c92a461e015bf620be3b2d398a952a5a89a Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Fri, 6 Sep 2013 11:03:18 -0400 Subject: [PATCH 0261/1705] python3: Fix traceback while running unit tests While running unit tests with python3, we get the following traceback: TypeError: 'map' object is not subscriptable In python3, a map returns an iterable object of type map, and is not subscriptable. Refactor to not use indicies so that it is python2 and python3 compat. blueprint python3-novaclient Change-Id: Iaf0de19f08fa3e91ee79c174ccc86a8bbe4acdc8 Signed-off-by: Chuck Short --- novaclient/v1_1/shell.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 55ae79d50..2a3be0e45 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2396,7 +2396,7 @@ def do_usage_list(cs, args): end = now + datetime.timedelta(days=1) def simplify_usage(u): - simplerows = map(lambda x: x.lower().replace(" ", "_"), rows) + simplerows = [x.lower().replace(" ", "_") for x in rows] setattr(u, simplerows[0], u.tenant_id) setattr(u, simplerows[1], "%d" % len(u.server_usages)) @@ -2442,7 +2442,7 @@ def do_usage(cs, args): end = now + datetime.timedelta(days=1) def simplify_usage(u): - simplerows = map(lambda x: x.lower().replace(" ", "_"), rows) + simplerows = [x.lower().replace(" ", "_") for x in rows] setattr(u, simplerows[0], "%d" % len(u.server_usages)) setattr(u, simplerows[1], "%.2f" % u.total_memory_mb_usage) From 450f02efe3310ce62a2a5ffc1cfc4318b2474289 Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Sat, 7 Sep 2013 13:52:01 -0400 Subject: [PATCH 0262/1705] python3: Compatibility for iteritems differences In python3 dict.iteritems(), dict.iterkeys(), and dict.itervalues() are no longer supported. So use six.iteritems() where it is appropriate. blueprint python3-novaclient Change-Id: I8c39bfe426d08d36215b55c3245dcfc69ec72517 Signed-off-by: Chuck Short --- novaclient/base.py | 2 +- novaclient/v1_1/shell.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/novaclient/base.py b/novaclient/base.py index 0abb3848b..efa7068bc 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -236,7 +236,7 @@ class BootingManagerWithFind(ManagerWithFind): def _parse_block_device_mapping(self, block_device_mapping): bdm = [] - for device_name, mapping in block_device_mapping.iteritems(): + for device_name, mapping in six.iteritems(block_device_mapping): # # The mapping is in the format: # :[]:[]:[] diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 55ae79d50..df1a67407 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -27,6 +27,8 @@ import sys import time +import six + from novaclient import exceptions from novaclient.openstack.common import strutils from novaclient.openstack.common import timeutils @@ -91,7 +93,7 @@ def _parse_block_device_mapping_v2(args, image): spec_dict = dict(v.split('=') for v in device_spec.split(',')) bdm_dict = {} - for key, value in spec_dict.iteritems(): + for key, value in six.iteritems(spec_dict): bdm_dict[CLIENT_BDM2_KEYS[key]] = value # Convert the delete_on_termination to a boolean or set it to true by From 2e8900b6dceb0574a79c2885ea7f0291fd3e8dca Mon Sep 17 00:00:00 2001 From: Xavier Queralt Date: Fri, 13 Sep 2013 18:27:45 +0200 Subject: [PATCH 0263/1705] Add a block device for the image when using BDMv2 Right now the cli was already adding a block device when it was passed an image_id to boot from and more than one block device mapping v2. This is done because nova expects the image to be another block device mapping and would ignore it otherwise. This patch moves this functionality from the cli to the base module so users of the module can benefit from this and also to prevent some misunderstandings that may arise when using BDMv2 and the image gets ignored by nova. In the future we should handle this in the nova side assuming a new BDM when an image is provided. Fixes bug #1225061 Change-Id: I29f31c24f958cfa8b68b33edc63e0d7031aa241f --- novaclient/base.py | 7 +++++++ novaclient/v1_1/shell.py | 7 ------- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/novaclient/base.py b/novaclient/base.py index 0abb3848b..158f207a1 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -366,6 +366,13 @@ def _boot(self, resource_url, response_key, name, image, flavor, body['server']['block_device_mapping'] = \ self._parse_block_device_mapping(block_device_mapping) elif block_device_mapping_v2: + # Append the image to the list only if we have new style BDMs + if image: + bdm_dict = {'uuid': image.id, 'source_type': 'image', + 'destination_type': 'local', 'boot_index': 0, + 'delete_on_termination': True} + block_device_mapping_v2.insert(0, bdm_dict) + body['server']['block_device_mapping_v2'] = block_device_mapping_v2 if nics is not None: diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 55ae79d50..35e9152f6 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -122,13 +122,6 @@ def _parse_block_device_mapping_v2(args, image): 'guest_format': 'swap', 'volume_size': args.swap} bdm.append(bdm_dict) - # Append the image to the list only if we have new style BDMs - if bdm and not args.block_device_mapping and image: - bdm_dict = {'uuid': image.id, 'source_type': 'image', - 'destination_type': 'local', 'boot_index': 0, - 'delete_on_termination': True} - bdm.insert(0, bdm_dict) - return bdm From aeef15d6d34867556fa73824739d2bd0e07b933f Mon Sep 17 00:00:00 2001 From: Abhishek Lahiri Date: Sat, 14 Sep 2013 03:47:01 +0000 Subject: [PATCH 0264/1705] Modify --num-instances flag description to clarify limit upper bound Closes Bug:#1220703 Modified --num-instances flag description in shell.py to clarify that number of instances spawned are limited by the quota. Change-Id: I29fa175f310ab1e073dca5f453a3540dcdde933d Signed-off-by: Abhishek Lahiri --- novaclient/v1_1/shell.py | 2 +- novaclient/v3/shell.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 55ae79d50..936d787e8 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -318,7 +318,7 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): default=None, type=int, metavar='', - help="boot multi instances at a time") + help="boot multi instances at a time (limited by quota).") @utils.arg('--meta', metavar="", action='append', diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 39cf322ae..8debf1151 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -222,7 +222,7 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): default=None, type=int, metavar='', - help="boot multi instances at a time") + help="boot multi instances at a time (limited by quota).") @utils.arg('--meta', metavar="", action='append', From 7edda206b17a5ef0914c630ae10ee7e1afcc1568 Mon Sep 17 00:00:00 2001 From: Vitaliy Kolosov Date: Mon, 9 Sep 2013 16:28:12 +0300 Subject: [PATCH 0265/1705] Small bugfix for client v3 Client.__init__ method used self.os_cache variable in an internal method call though this variable had not been initialized earlier. In addition, unittests added for full coverage of client v3. Change-Id: I421f82932b65f137932d933e04d42064bec0d08d --- novaclient/tests/fakes.py | 7 +++++ novaclient/tests/test_client.py | 47 +++++++++++++++++++++++++++++++++ novaclient/v3/client.py | 2 +- 3 files changed, 55 insertions(+), 1 deletion(-) diff --git a/novaclient/tests/fakes.py b/novaclient/tests/fakes.py index f4b356985..bc3683a4a 100644 --- a/novaclient/tests/fakes.py +++ b/novaclient/tests/fakes.py @@ -6,6 +6,8 @@ places where actual behavior differs from the spec. """ +from novaclient import base + def assert_has_keys(dict, required=[], optional=[]): keys = dict.keys() @@ -71,3 +73,8 @@ def clear_callstack(self): def authenticate(self): pass + + +# Fake class that will be used as an extension +class FakeManager(base.Manager): + pass diff --git a/novaclient/tests/test_client.py b/novaclient/tests/test_client.py index 4fabca26a..3d7743166 100644 --- a/novaclient/tests/test_client.py +++ b/novaclient/tests/test_client.py @@ -18,7 +18,10 @@ import requests import novaclient.client +import novaclient.extension +import novaclient.tests.fakes as fakes import novaclient.v1_1.client +import novaclient.v3.client from novaclient.tests import utils @@ -83,6 +86,10 @@ def test_client_reauth(self): verify=mock.ANY)] self.assertEqual(mock_request.call_args_list, expected) + def test_get_client_class_v3(self): + output = novaclient.client.get_client_class('3') + self.assertEqual(output, novaclient.v3.client.Client) + def test_get_client_class_v2(self): output = novaclient.client.get_client_class('2') self.assertEqual(output, novaclient.v1_1.client.Client) @@ -139,3 +146,43 @@ def test_client_get_reset_timings_v1_1(self): cs.reset_timings() self.assertEqual(0, len(cs.get_timings())) + + def test_client_set_management_url_v3(self): + cs = novaclient.v3.client.Client("user", "password", "project_id", + auth_url="foo/v2") + cs.set_management_url("blabla") + self.assertEqual("blabla", cs.client.management_url) + + def test_client_get_reset_timings_v3(self): + cs = novaclient.v3.client.Client("user", "password", "project_id", + auth_url="foo/v2") + self.assertEqual(0, len(cs.get_timings())) + cs.client.times.append("somevalue") + self.assertEqual(["somevalue"], cs.get_timings()) + + cs.reset_timings() + self.assertEquals(0, len(cs.get_timings())) + + def test_clent_extensions_v3(self): + fake_attribute_name1 = "FakeAttribute1" + fake_attribute_name2 = "FakeAttribute2" + extensions = [ + novaclient.extension.Extension(fake_attribute_name1, + fakes), + novaclient.extension.Extension(fake_attribute_name2, + utils), + ] + + cs = novaclient.v3.client.Client("user", "password", "project_id", + auth_url="foo/v2", + extensions=extensions) + self.assertTrue(isinstance(getattr(cs, fake_attribute_name1, None), + fakes.FakeManager)) + self.assertFalse(hasattr(cs, fake_attribute_name2)) + + @mock.patch.object(novaclient.client.HTTPClient, 'authenticate') + def test_authenticate_call_v3(self, mock_authenticate): + cs = novaclient.v3.client.Client("user", "password", "project_id", + auth_url="foo/v2") + cs.authenticate() + self.assertTrue(mock_authenticate.called) diff --git a/novaclient/v3/client.py b/novaclient/v3/client.py index aca58452a..8a8d814f9 100644 --- a/novaclient/v3/client.py +++ b/novaclient/v3/client.py @@ -72,7 +72,7 @@ def __init__(self, username, password, project_id, auth_url=None, volume_service_name=volume_service_name, timings=timings, bypass_url=bypass_url, - os_cache=self.os_cache, + os_cache=os_cache, http_log_debug=http_log_debug, cacert=cacert) From 889f5680a99e9c2ed11dadec79cfb2c3ef3e7761 Mon Sep 17 00:00:00 2001 From: Chang Bo Guo Date: Mon, 16 Sep 2013 17:20:54 -0700 Subject: [PATCH 0266/1705] assertEquals is deprecated, use assertEqual assertEquals is deprecated in Python 2.7 , need drop it http://docs.python.org/2/library/unittest.html#deprecated-aliases Change-Id: I5aeeb3b289e1d29f6338fc9fd47e680909f0a133 --- novaclient/tests/test_auth_plugins.py | 4 ++-- novaclient/tests/test_http.py | 4 ++-- novaclient/tests/test_service_catalog.py | 8 ++++---- novaclient/tests/v1_1/test_security_group_rules.py | 2 +- novaclient/tests/v1_1/test_security_groups.py | 2 +- novaclient/tests/v1_1/test_servers.py | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/novaclient/tests/test_auth_plugins.py b/novaclient/tests/test_auth_plugins.py index c4b37b57d..96217991a 100644 --- a/novaclient/tests/test_auth_plugins.py +++ b/novaclient/tests/test_auth_plugins.py @@ -171,7 +171,7 @@ def test_auth_call(): auth_system="fakewithauthurl", auth_plugin=plugin) cs.client.authenticate() - self.assertEquals(cs.client.auth_url, "http://faked/v2.0") + self.assertEqual(cs.client.auth_url, "http://faked/v2.0") test_auth_call() @@ -301,7 +301,7 @@ def get_auth_url(self): cs = client.Client("username", "password", "project_id", auth_system="fakewithauthurl", auth_plugin=plugin) - self.assertEquals(cs.client.auth_url, "http://faked/v2.0") + self.assertEqual(cs.client.auth_url, "http://faked/v2.0") @mock.patch.object(pkg_resources, "iter_entry_points") def test_exception_if_no_authenticate(self, mock_iter_entry_points): diff --git a/novaclient/tests/test_http.py b/novaclient/tests/test_http.py index 015928015..f6ebb7a7e 100644 --- a/novaclient/tests/test_http.py +++ b/novaclient/tests/test_http.py @@ -115,8 +115,8 @@ def test_refused_call(): def test_client_logger(self): cl1 = client.HTTPClient("username", "password", "project_id", "auth_test", http_log_debug=True) - self.assertEquals(len(cl1._logger.handlers), 1) + self.assertEqual(len(cl1._logger.handlers), 1) cl2 = client.HTTPClient("username", "password", "project_id", "auth_test", http_log_debug=True) - self.assertEquals(len(cl2._logger.handlers), 1) + self.assertEqual(len(cl2._logger.handlers), 1) diff --git a/novaclient/tests/test_service_catalog.py b/novaclient/tests/test_service_catalog.py index 1d73c73dc..a34d82ffb 100644 --- a/novaclient/tests/test_service_catalog.py +++ b/novaclient/tests/test_service_catalog.py @@ -116,9 +116,9 @@ def test_building_a_service_catalog(self): self.assertRaises(exceptions.AmbiguousEndpoints, sc.url_for, service_type='compute') - self.assertEquals(sc.url_for('tenantId', '1', service_type='compute'), + self.assertEqual(sc.url_for('tenantId', '1', service_type='compute'), "https://compute1.host/v2/1") - self.assertEquals(sc.url_for('tenantId', '2', service_type='compute'), + self.assertEqual(sc.url_for('tenantId', '2', service_type='compute'), "https://compute1.host/v1.1/2") self.assertRaises(exceptions.EndpointNotFound, sc.url_for, @@ -135,9 +135,9 @@ def test_alternate_service_type(self): self.assertRaises(exceptions.AmbiguousEndpoints, sc.url_for, service_type='volume') - self.assertEquals(sc.url_for('tenantId', '1', service_type='volume'), + self.assertEqual(sc.url_for('tenantId', '1', service_type='volume'), "https://volume1.host/v1/1") - self.assertEquals(sc.url_for('tenantId', '2', service_type='volume'), + self.assertEqual(sc.url_for('tenantId', '2', service_type='volume'), "https://volume1.host/v1.1/2") self.assertRaises(exceptions.EndpointNotFound, sc.url_for, diff --git a/novaclient/tests/v1_1/test_security_group_rules.py b/novaclient/tests/v1_1/test_security_group_rules.py index e42592761..e1d015fa5 100644 --- a/novaclient/tests/v1_1/test_security_group_rules.py +++ b/novaclient/tests/v1_1/test_security_group_rules.py @@ -60,7 +60,7 @@ def test_invalid_parameters_create(self): def test_security_group_rule_str(self): sg = cs.security_group_rules.create(1, "tcp", 1, 65535, "10.0.0.0/16") - self.assertEquals('1', str(sg)) + self.assertEqual('1', str(sg)) def test_security_group_rule_del(self): sg = cs.security_group_rules.create(1, "tcp", 1, 65535, "10.0.0.0/16") diff --git a/novaclient/tests/v1_1/test_security_groups.py b/novaclient/tests/v1_1/test_security_groups.py index 9f3ae46bb..0040d3054 100644 --- a/novaclient/tests/v1_1/test_security_groups.py +++ b/novaclient/tests/v1_1/test_security_groups.py @@ -29,7 +29,7 @@ def test_get_security_groups(self): sg = cs.security_groups.get(1) cs.assert_called('GET', '/os-security-groups/1') self.assertTrue(isinstance(sg, security_groups.SecurityGroup)) - self.assertEquals('1', str(sg)) + self.assertEqual('1', str(sg)) def test_delete_security_group(self): sg = cs.security_groups.list()[0] diff --git a/novaclient/tests/v1_1/test_servers.py b/novaclient/tests/v1_1/test_servers.py index 658068f77..76d3e3cd8 100644 --- a/novaclient/tests/v1_1/test_servers.py +++ b/novaclient/tests/v1_1/test_servers.py @@ -42,7 +42,7 @@ def test_get_server_promote_details(self): s2 = cs.servers.list(detailed=True)[0] self.assertNotEquals(s1._info, s2._info) s1.get() - self.assertEquals(s1._info, s2._info) + self.assertEqual(s1._info, s2._info) def test_create_server(self): s = cs.servers.create( From 043ef5b7573fa2eb2aa825057a06a666764120fd Mon Sep 17 00:00:00 2001 From: Ken'ichi Ohmichi Date: Tue, 10 Sep 2013 17:21:54 +0900 Subject: [PATCH 0267/1705] Fix the print order of quota-show Current "nova qouta-show" prints quotas by random order like the following: $ nova quota-show +-----------------------------+-------+ | Property | Value | +-----------------------------+-------+ | metadata_items | 128 | | injected_file_content_bytes | 10240 | | ram | 51200 | | floating_ips | 10 | | key_pairs | 100 | | instances | 10 | | security_group_rules | 20 | | injected_files | 5 | | cores | 20 | | fixed_ips | -1 | | injected_file_path_bytes | 255 | | security_groups | 10 | +-----------------------------+-------+ $ This patch fixes its order for fitting to the option order of "nova quota-update" like the following: $ nova quota-show +-----------------------------+-------+ | Quota | Limit | +-----------------------------+-------+ | instances | 10 | | cores | 20 | | ram | 51200 | | floating_ips | 10 | | fixed_ips | -1 | | metadata_items | 128 | | injected_files | 5 | | injected_file_content_bytes | 10240 | | injected_file_path_bytes | 255 | | key_pairs | 100 | | security_groups | 10 | | security_group_rules | 20 | +-----------------------------+-------+ Fixes bug #1223233 Change-Id: I482fd26b9114ac718dc4a16753e7692213bd3690 --- novaclient/v1_1/shell.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 55ae79d50..e6085e6dd 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -3023,19 +3023,26 @@ def do_ssh(cs, args): _quota_resources = ['instances', 'cores', 'ram', 'volumes', 'gigabytes', 'floating_ips', 'fixed_ips', 'metadata_items', - 'injected_files', 'key_pairs', - 'injected_file_content_bytes', 'injected_file_path_bytes', + 'injected_files', 'injected_file_content_bytes', + 'injected_file_path_bytes', 'key_pairs', 'security_groups', 'security_group_rules'] def _quota_show(quotas): - quota_dict = {} + class FormattedQuota(object): + def __init__(self, key, value): + setattr(self, 'quota', key) + setattr(self, 'limit', value) + + quota_list = [] for resource in _quota_resources: try: - quota_dict[resource] = getattr(quotas, resource) + quota = FormattedQuota(resource, getattr(quotas, resource)) + quota_list.append(quota) except AttributeError: pass - utils.print_dict(quota_dict) + columns = ['Quota', 'Limit'] + utils.print_list(quota_list, columns) def _quota_update(manager, identifier, args): From 615ad3de54507038d276eae50e7d46fb359c57c3 Mon Sep 17 00:00:00 2001 From: Ben Nemec Date: Thu, 15 Aug 2013 17:31:37 -0500 Subject: [PATCH 0268/1705] Create v3 tests directory Create a basic framework that inherits from v1_1 and can be overridden as necessary in extension commits bp v3-api Change-Id: I611451e50a9e553a18777fa48f32e368b919605e --- novaclient/tests/v3/__init__.py | 0 novaclient/tests/v3/fakes.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 novaclient/tests/v3/__init__.py create mode 100644 novaclient/tests/v3/fakes.py diff --git a/novaclient/tests/v3/__init__.py b/novaclient/tests/v3/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/novaclient/tests/v3/fakes.py b/novaclient/tests/v3/fakes.py new file mode 100644 index 000000000..8b7f4d2d5 --- /dev/null +++ b/novaclient/tests/v3/fakes.py @@ -0,0 +1,32 @@ +# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. +# Copyright 2011 OpenStack Foundation +# Copyright 2013 IBM Corp. +# +# 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 novaclient.v3 import client +from novaclient.tests import fakes +from novaclient.tests.v1_1 import fakes as fakes_v1_1 + + +class FakeClient(fakes.FakeClient, client.Client): + + def __init__(self, *args, **kwargs): + client.Client.__init__(self, 'username', 'password', + 'project_id', 'auth_url', + extensions=kwargs.get('extensions')) + self.client = FakeHTTPClient(**kwargs) + + +class FakeHTTPClient(fakes_v1_1.FakeHTTPClient): + pass From 57a22ec99ca68dfa78878e3e6aa3e2bf9f24c06b Mon Sep 17 00:00:00 2001 From: Ben Nemec Date: Thu, 15 Aug 2013 17:13:19 -0500 Subject: [PATCH 0269/1705] Add v3 HostManager Some of the host code required changes to work with the v3 API. This change adds a v3-compatible HostManager that inherits from the 1_1 HostManager to make use of the functions that didn't need to change. bp v3-api Change-Id: I3ef7ab4619d49c20ab1bdeb1185ad28b411d37a1 --- novaclient/tests/v3/fakes.py | 28 ++++++++++- novaclient/tests/v3/test_hosts.py | 83 +++++++++++++++++++++++++++++++ novaclient/v3/client.py | 5 ++ novaclient/v3/hosts.py | 35 +++++++++++++ 4 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 novaclient/tests/v3/test_hosts.py create mode 100644 novaclient/v3/hosts.py diff --git a/novaclient/tests/v3/fakes.py b/novaclient/tests/v3/fakes.py index 8b7f4d2d5..a8c3788ab 100644 --- a/novaclient/tests/v3/fakes.py +++ b/novaclient/tests/v3/fakes.py @@ -29,4 +29,30 @@ def __init__(self, *args, **kwargs): class FakeHTTPClient(fakes_v1_1.FakeHTTPClient): - pass + # + # Hosts + # + def put_os_hosts_sample_host_1(self, body, **kw): + return (200, {}, {'host': {'host': 'sample-host_1', + 'status': 'enabled'}}) + + def put_os_hosts_sample_host_2(self, body, **kw): + return (200, {}, {'host': {'host': 'sample-host_2', + 'maintenance_mode': 'on_maintenance'}}) + + def put_os_hosts_sample_host_3(self, body, **kw): + return (200, {}, {'host': {'host': 'sample-host_3', + 'status': 'enabled', + 'maintenance_mode': 'on_maintenance'}}) + + def get_os_hosts_sample_host_reboot(self, **kw): + return (200, {}, {'host': {'host': 'sample_host', + 'power_action': 'reboot'}}) + + def get_os_hosts_sample_host_startup(self, **kw): + return (200, {}, {'host': {'host': 'sample_host', + 'power_action': 'startup'}}) + + def get_os_hosts_sample_host_shutdown(self, **kw): + return (200, {}, {'host': {'host': 'sample_host', + 'power_action': 'shutdown'}}) diff --git a/novaclient/tests/v3/test_hosts.py b/novaclient/tests/v3/test_hosts.py new file mode 100644 index 000000000..15bda0e85 --- /dev/null +++ b/novaclient/tests/v3/test_hosts.py @@ -0,0 +1,83 @@ +# Copyright 2013 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient.v3 import hosts +from novaclient.tests.v3 import fakes +from novaclient.tests import utils + + +cs = fakes.FakeClient() + + +class HostsTest(utils.TestCase): + + def test_describe_resource(self): + hs = cs.hosts.get('host') + cs.assert_called('GET', '/os-hosts/host') + for h in hs: + self.assertTrue(isinstance(h, hosts.Host)) + + def test_list_host(self): + hs = cs.hosts.list() + cs.assert_called('GET', '/os-hosts') + for h in hs: + self.assertTrue(isinstance(h, hosts.Host)) + self.assertEqual(h.zone, 'nova1') + + def test_list_host_with_zone(self): + hs = cs.hosts.list('nova') + cs.assert_called('GET', '/os-hosts?zone=nova') + for h in hs: + self.assertTrue(isinstance(h, hosts.Host)) + self.assertEqual(h.zone, 'nova') + + def test_update_enable(self): + host = cs.hosts.get('sample_host')[0] + values = {"status": "enabled"} + result = host.update(values) + cs.assert_called('PUT', '/os-hosts/sample_host', {"host": values}) + self.assertTrue(isinstance(result, hosts.Host)) + + def test_update_maintenance(self): + host = cs.hosts.get('sample_host')[0] + values = {"maintenance_mode": "enable"} + result = host.update(values) + cs.assert_called('PUT', '/os-hosts/sample_host', {"host": values}) + self.assertTrue(isinstance(result, hosts.Host)) + + def test_update_both(self): + host = cs.hosts.get('sample_host')[0] + values = {"status": "enabled", + "maintenance_mode": "enable"} + result = host.update(values) + cs.assert_called('PUT', '/os-hosts/sample_host', {"host": values}) + self.assertTrue(isinstance(result, hosts.Host)) + + def test_host_startup(self): + host = cs.hosts.get('sample_host')[0] + result = host.startup() + cs.assert_called( + 'GET', '/os-hosts/sample_host/startup') + + def test_host_reboot(self): + host = cs.hosts.get('sample_host')[0] + result = host.reboot() + cs.assert_called( + 'GET', '/os-hosts/sample_host/reboot') + + def test_host_shutdown(self): + host = cs.hosts.get('sample_host')[0] + result = host.shutdown() + cs.assert_called( + 'GET', '/os-hosts/sample_host/shutdown') diff --git a/novaclient/v3/client.py b/novaclient/v3/client.py index 8a8d814f9..091aa0901 100644 --- a/novaclient/v3/client.py +++ b/novaclient/v3/client.py @@ -15,6 +15,7 @@ # under the License. from novaclient import client +from novaclient.v3 import hosts class Client(object): @@ -45,7 +46,11 @@ def __init__(self, username, password, project_id, auth_url=None, http_log_debug=False, auth_system='keystone', auth_plugin=None, cacert=None, tenant_id=None): + self.projectid = project_id + self.tenant_id = tenant_id + self.os_cache = os_cache or not no_cache #TODO(bnemec): Add back in v3 extensions + self.hosts = hosts.HostManager(self) # Add in any extensions... if extensions: diff --git a/novaclient/v3/hosts.py b/novaclient/v3/hosts.py new file mode 100644 index 000000000..5327a2bf3 --- /dev/null +++ b/novaclient/v3/hosts.py @@ -0,0 +1,35 @@ +# Copyright 2013 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" V3 API versions of the Hosts interface. + +Inherits from the 1.1 code because a lot of the functionality is shared. +""" + +from novaclient.v1_1 import hosts + + +Host = hosts.Host + + +class HostManager(hosts.HostManager): + def update(self, host, values): + """Update status or maintenance mode for the host.""" + body = dict(host=values) + return self._update("/os-hosts/%s" % host, body, response_key='host') + + def host_action(self, host, action): + """Perform an action on a host.""" + url = '/os-hosts/{0}/{1}'.format(host, action) + return self._get(url, response_key='host') From 8deaf3769d6076851003668f4c53d478bb3ff2c5 Mon Sep 17 00:00:00 2001 From: Phil Day Date: Fri, 20 Sep 2013 13:20:36 +0000 Subject: [PATCH 0270/1705] Novaclient shell list command should support a minimal server list The Nova API supports both a basic (id and name) and detailed server list, and this is supported in the client by the "detailed=True" argument to servers.list(). However the shell do_list() method always leaves this to its default value. For administrators on a large system, or where the tenants has a lot of instances a detailed list can be painfully slow. This change adds support for a --minimal option to list. Fixes bug: 1228137 Change-Id: I3126ee6b372606a98f0d7a4e344556f8c05ae224 --- novaclient/tests/v1_1/test_shell.py | 4 ++++ novaclient/v1_1/shell.py | 16 ++++++++++++++-- novaclient/v3/shell.py | 16 ++++++++++++++-- 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index d7a5e0d38..48b08d9b1 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -679,6 +679,10 @@ def test_list(self): self.run_command('list') self.assert_called('GET', '/servers/detail') + def test_list_minimal(self): + self.run_command('list --minimal') + self.assert_called('GET', '/servers') + def test_list_with_images(self): self.run_command('list --image 1') self.assert_called('GET', '/servers/detail?image=1') diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index d74c0b04c..66eb742af 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1091,6 +1091,11 @@ def do_image_delete(cs, args): metavar='', help='Comma-separated list of fields to display. ' 'Use the show command to see which fields are available.') +@utils.arg('--minimal', + dest='minimal', + action="store_true", + default=False, + help='Get only uuid and name.') def do_list(cs, args): """List active servers.""" imageid = None @@ -1126,7 +1131,10 @@ def do_list(cs, args): id_col = 'ID' - servers = cs.servers.list(search_opts=search_opts) + detailed = not args.minimal + + servers = cs.servers.list(detailed=detailed, + search_opts=search_opts) convert = [('OS-EXT-SRV-ATTR:host', 'host'), ('OS-EXT-STS:task_state', 'task_state'), ('OS-EXT-SRV-ATTR:instance_name', 'instance_name'), @@ -1134,7 +1142,11 @@ def do_list(cs, args): ('hostId', 'host_id')] _translate_keys(servers, convert) _translate_extended_states(servers) - if field_titles: + if args.minimal: + columns = [ + id_col, + 'Name'] + elif field_titles: columns = [id_col] + field_titles else: columns = [ diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 8debf1151..7b478c79e 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -975,6 +975,11 @@ def do_image_delete(cs, args): metavar='', help='Comma-separated list of fields to display. ' 'Use the show command to see which fields are available.') +@utils.arg('--minimal', + dest='minimal', + action="store_true", + default=False, + help='Get only uuid and name.') def do_list(cs, args): """List active servers.""" imageid = None @@ -1010,7 +1015,10 @@ def do_list(cs, args): id_col = 'ID' - servers = cs.servers.list(search_opts=search_opts) + detailed = not args.minimal + + servers = cs.servers.list(detailed=detailed, + search_opts=search_opts) convert = [('OS-EXT-SRV-ATTR:host', 'host'), ('OS-EXT-STS:task_state', 'task_state'), ('OS-EXT-SRV-ATTR:instance_name', 'instance_name'), @@ -1018,7 +1026,11 @@ def do_list(cs, args): ('hostId', 'host_id')] _translate_keys(servers, convert) _translate_extended_states(servers) - if field_titles: + if args.minimal: + columns = [ + id_col, + 'Name'] + elif field_titles: columns = [id_col] + field_titles else: columns = [ From c7e62d4c5706a021c5ab566baed5048a4ee5f885 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 18 Sep 2013 08:00:08 -0500 Subject: [PATCH 0271/1705] Clean up a little cruft OpenStack runs a git service now, so instead of linking to github URLs, link to git.openstack.org. Versions for pbr in setup.py actually wind up being an error (and a very odd one) Remove them. The setup hook in setup.cfg is no longer needed. Change-Id: I8f48ac194e6c01e9bb06fa5df6f2b2bf28c01636 --- HACKING.rst | 2 +- doc/source/index.rst | 4 ++-- setup.cfg | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/HACKING.rst b/HACKING.rst index ac363673e..49eba1184 100644 --- a/HACKING.rst +++ b/HACKING.rst @@ -2,7 +2,7 @@ Nova Client Style Commandments ============================== - Step 1: Read the OpenStack Style Commandments - https://github.com/openstack-dev/hacking/blob/master/HACKING.rst + http://docs.openstack.org/developer/hacking - Step 2: Read on diff --git a/doc/source/index.rst b/doc/source/index.rst index a9b617abf..00d1a81ea 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -30,11 +30,11 @@ Contents: Contributing ============ -Code is hosted `on GitHub`_. Submit bugs to the Nova project on +Code is hosted at `git.openstack.org`_. Submit bugs to the Nova project on `Launchpad`_. Submit code to the openstack/python-novaclient project using `Gerrit`_. -.. _on GitHub: https://github.com/openstack/python-novaclient +.. _git.openstack.org: https://git.openstack.org/cgit/openstack/python-novaclient .. _Launchpad: https://launchpad.net/nova .. _Gerrit: http://wiki.openstack.org/GerritWorkflow diff --git a/setup.cfg b/setup.cfg index 0f5f94fb4..6fc426cdc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -6,7 +6,7 @@ description-file = license = Apache License, Version 2.0 author = OpenStack author-email = openstack-dev@lists.openstack.org -home-page = https://github.com/openstack/python-novaclient +home-page = https://git.openstack.org/cgit/openstack/python-novaclient classifier = Development Status :: 5 - Production/Stable Environment :: Console From ce89f7b12a6f3e80dda2573bfa96a07b8b7d7740 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 20 Sep 2013 15:55:42 -0700 Subject: [PATCH 0272/1705] Update pbr usage We've discovered that having versions in setup_requires leads to evil and death. Also, the setup-hooks in setup.cfg are completely unnecessary anymore. Change-Id: I8878e635e0828bbddb81d9c6321a9f10ad3b89fe --- setup.cfg | 4 ---- setup.py | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/setup.cfg b/setup.cfg index 6fc426cdc..3513c3bf5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -21,10 +21,6 @@ classifier = packages = novaclient -[global] -setup-hooks = - pbr.hooks.setup_hook - [entry_points] console_scripts = nova = novaclient.shell:main diff --git a/setup.py b/setup.py index 2a0786a8b..70c2b3f32 100644 --- a/setup.py +++ b/setup.py @@ -18,5 +18,5 @@ import setuptools setuptools.setup( - setup_requires=['pbr>=0.5.21,<1.0'], + setup_requires=['pbr'], pbr=True) From 0ab364a98ce82d3f552d9584692954daef16964a Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Sat, 21 Sep 2013 10:02:49 -0500 Subject: [PATCH 0273/1705] Corrected usage of len(filter(...)) filter() returns an iterable, not a list on Python 3. Change-Id: I8afb2962bcd193ab67017c98376359c0f28afc66 --- novaclient/v1_1/shell.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 66eb742af..ce7c18493 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -206,7 +206,8 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): block_device_mapping_v2 = _parse_block_device_mapping_v2(args, image) - n_boot_args = len(filter(None, (image, args.boot_volume, args.snapshot))) + n_boot_args = len(list(filter( + bool, (image, args.boot_volume, args.snapshot)))) have_bdm = block_device_mapping_v2 or block_device_mapping # Fail if more than one boot devices are present From f1c3b79c2bb1805098cf55d0aaa23faf664b48ea Mon Sep 17 00:00:00 2001 From: Kui Shi Date: Sun, 22 Sep 2013 22:06:41 +0800 Subject: [PATCH 0274/1705] py33: 'dict_keys' object does not support indexing In python 3, dict.keys() is a class of 'dict_keys', it does not support indexing. Cast the dict to list to get the "first" value of dict, which is compatible with python 2&3. Closes-Bug: #1229005 Change-Id: I561b7ada9e5ad936659a657b3161a9b93a15a788 --- novaclient/exceptions.py | 2 +- novaclient/tests/v1_1/contrib/fakes.py | 2 +- novaclient/tests/v1_1/fakes.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/novaclient/exceptions.py b/novaclient/exceptions.py index 727638bdc..b04844a2d 100644 --- a/novaclient/exceptions.py +++ b/novaclient/exceptions.py @@ -206,7 +206,7 @@ def from_response(response, body, url, method=None): details = "n/a" if hasattr(body, 'keys'): - error = body[body.keys()[0]] + error = body[list(body)[0]] message = error.get('message', None) details = error.get('details', None) diff --git a/novaclient/tests/v1_1/contrib/fakes.py b/novaclient/tests/v1_1/contrib/fakes.py index b9e79e841..1d679f352 100644 --- a/novaclient/tests/v1_1/contrib/fakes.py +++ b/novaclient/tests/v1_1/contrib/fakes.py @@ -113,7 +113,7 @@ def delete_os_baremetal_nodes_1(self, **kw): def post_os_baremetal_nodes_1_action(self, **kw): body = kw['body'] - action = body.keys()[0] + action = list(body)[0] if action == "add_interface": return ( 200, {}, { diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index 7b6bc119d..06cfce93d 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -488,7 +488,7 @@ def post_servers_1234_action(self, body, **kw): _body = None resp = 202 assert len(body.keys()) == 1 - action = body.keys()[0] + action = list(body)[0] if action == 'reboot': assert body[action].keys() == ['type'] assert body[action]['type'] in ['HARD', 'SOFT'] From 35e03a92e2543b8713ab0c2004ca4a0dcc8fd270 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Sun, 22 Sep 2013 09:04:20 -0700 Subject: [PATCH 0275/1705] Corrected several usage of keys() for Python 3 Under Python2 dict.keys() returns a list, under Python 3 it returns an iterator. Some places assumed that if they called keys() then it was safe to modify the dict in a loop. Corrected this by calling list(). Change-Id: I7638263f288dd20590bd751d09194a919b921545 --- novaclient/tests/v1_1/fakes.py | 2 +- novaclient/v1_1/quota_classes.py | 2 +- novaclient/v1_1/quotas.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index 7b6bc119d..62224a2da 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -613,7 +613,7 @@ def put_os_cloudpipe_configure_project(self, **kw): def get_flavors(self, **kw): status, header, flavors = self.get_flavors_detail(**kw) for flavor in flavors['flavors']: - for k in flavor.keys(): + for k in list(flavor): if k not in ['id', 'name']: del flavor[k] diff --git a/novaclient/v1_1/quota_classes.py b/novaclient/v1_1/quota_classes.py index 0b669bc2c..3f2908e5e 100644 --- a/novaclient/v1_1/quota_classes.py +++ b/novaclient/v1_1/quota_classes.py @@ -58,7 +58,7 @@ def update(self, class_name, metadata_items=None, 'security_groups': security_groups, 'security_group_rules': security_group_rules}} - for key in body['quota_class_set'].keys(): + for key in list(body['quota_class_set']): if body['quota_class_set'][key] is None: body['quota_class_set'].pop(key) diff --git a/novaclient/v1_1/quotas.py b/novaclient/v1_1/quotas.py index 7c7ce54e3..bd5bd76e1 100644 --- a/novaclient/v1_1/quotas.py +++ b/novaclient/v1_1/quotas.py @@ -66,7 +66,7 @@ def update(self, tenant_id, metadata_items=None, 'security_group_rules': security_group_rules, 'force': force}} - for key in body['quota_set'].keys(): + for key in list(body['quota_set']): if body['quota_set'][key] is None: body['quota_set'].pop(key) From ac7970321377c287d27d2379f061deaf3f82e8d1 Mon Sep 17 00:00:00 2001 From: Kui Shi Date: Sun, 22 Sep 2013 20:21:00 +0800 Subject: [PATCH 0276/1705] py33: dict.keys() is not a list in python3 Cast the dict.keys() to list when comparing its value with list. Closes-Bug: #1228993 Change-Id: I59d13258935de3e9286845f857eaa602a5b690fb --- novaclient/tests/v1_1/fakes.py | 48 +++++++++++++++++----------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index 7b6bc119d..472edd280 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -391,7 +391,7 @@ def get_servers_9012(self, **kw): return (200, {}, r) def put_servers_1234(self, body, **kw): - assert body.keys() == ['server'] + assert list(body) == ['server'] fakes.assert_has_keys(body['server'], optional=['name', 'adminPass']) return (204, {}, body) @@ -490,7 +490,7 @@ def post_servers_1234_action(self, body, **kw): assert len(body.keys()) == 1 action = body.keys()[0] if action == 'reboot': - assert body[action].keys() == ['type'] + assert list(body[action]) == ['type'] assert body[action]['type'] in ['HARD', 'SOFT'] elif action == 'rebuild': keys = body[action].keys() @@ -539,39 +539,39 @@ def post_servers_1234_action(self, body, **kw): elif action == 'unlock': assert body[action] is None elif action == 'addFixedIp': - assert body[action].keys() == ['networkId'] + assert list(body[action]) == ['networkId'] elif action == 'removeFixedIp': - assert body[action].keys() == ['address'] + assert list(body[action]) == ['address'] elif action == 'addFloatingIp': - assert (body[action].keys() == ['address'] or - body[action].keys() == ['fixed_address', + assert (list(body[action]) == ['address'] or + list(body[action]) == ['fixed_address', 'address']) elif action == 'removeFloatingIp': - assert body[action].keys() == ['address'] + assert list(body[action]) == ['address'] elif action == 'createImage': assert set(body[action].keys()) == set(['name', 'metadata']) _headers = dict(location="http://blah/images/456") elif action == 'changePassword': - assert body[action].keys() == ['adminPass'] + assert list(body[action]) == ['adminPass'] elif action == 'os-getConsoleOutput': - assert body[action].keys() == ['length'] + assert list(body[action]) == ['length'] return (202, {}, {'output': 'foo'}) elif action == 'os-getVNCConsole': - assert body[action].keys() == ['type'] + assert list(body[action]) == ['type'] elif action == 'os-getSPICEConsole': - assert body[action].keys() == ['type'] + assert list(body[action]) == ['type'] elif action == 'os-migrateLive': assert set(body[action].keys()) == set(['host', 'block_migration', 'disk_over_commit']) elif action == 'os-resetState': - assert body[action].keys() == ['state'] + assert list(body[action]) == ['state'] elif action == 'resetNetwork': assert body[action] is None elif action == 'addSecurityGroup': - assert body[action].keys() == ['name'] + assert list(body[action]) == ['name'] elif action == 'removeSecurityGroup': - assert body[action].keys() == ['name'] + assert list(body[action]) == ['name'] elif action == 'createBackup': assert set(body[action].keys()) == set(['name', 'backup_type', @@ -728,7 +728,7 @@ def get_flavors_aa1_os_extra_specs(self, **kw): {'extra_specs': {"k3": "v3"}}) def post_flavors_1_os_extra_specs(self, body, **kw): - assert body.keys() == ['extra_specs'] + assert list(body) == ['extra_specs'] fakes.assert_has_keys(body['extra_specs'], required=['k1']) return (200, @@ -911,12 +911,12 @@ def get_images_2(self, **kw): return (200, {}, {'image': self.get_images_detail()[2]['images'][1]}) def post_images(self, body, **kw): - assert body.keys() == ['image'] + assert list(body) == ['image'] fakes.assert_has_keys(body['image'], required=['serverId', 'name']) return (202, {}, self.get_images_1()[2]) def post_images_1_metadata(self, body, **kw): - assert body.keys() == ['metadata'] + assert list(body) == ['metadata'] fakes.assert_has_keys(body['metadata'], required=['test_key']) return (200, @@ -947,7 +947,7 @@ def delete_os_keypairs_test(self, **kw): return (202, {}, None) def post_os_keypairs(self, body, **kw): - assert body.keys() == ['keypair'] + assert list(body) == ['keypair'] fakes.assert_has_keys(body['keypair'], required=['name']) r = {'keypair': self.get_os_keypairs()[2]['keypairs'][0]} @@ -1085,7 +1085,7 @@ def get_os_quota_sets_tenant_id_defaults(self): 'security_group_rules': 1}}) def put_os_quota_sets_97f4c221bff44578b0300df4ef119353(self, body, **kw): - assert body.keys() == ['quota_set'] + assert list(body) == ['quota_set'] fakes.assert_has_keys(body['quota_set'], required=['tenant_id']) return (200, {}, {'quota_set': { @@ -1132,7 +1132,7 @@ def get_os_quota_class_sets_test(self, **kw): 'security_group_rules': 1}}) def put_os_quota_class_sets_test(self, body, **kw): - assert body.keys() == ['quota_class_set'] + assert list(body) == ['quota_class_set'] fakes.assert_has_keys(body['quota_class_set'], required=['class_name']) return (200, {}, {'quota_class_set': { @@ -1153,7 +1153,7 @@ def put_os_quota_class_sets_test(self, body, **kw): def put_os_quota_class_sets_97f4c221bff44578b0300df4ef119353(self, body, **kw): - assert body.keys() == ['quota_class_set'] + assert list(body) == ['quota_class_set'] fakes.assert_has_keys(body['quota_class_set'], required=['class_name']) return (200, {}, {'quota_class_set': { @@ -1219,7 +1219,7 @@ def delete_os_security_groups_1(self, **kw): return (202, {}, None) def post_os_security_groups(self, body, **kw): - assert body.keys() == ['security_group'] + assert list(body) == ['security_group'] fakes.assert_has_keys(body['security_group'], required=['name', 'description']) r = {'security_group': @@ -1227,7 +1227,7 @@ def post_os_security_groups(self, body, **kw): return (202, {}, r) def put_os_security_groups_1(self, body, **kw): - assert body.keys() == ['security_group'] + assert list(body) == ['security_group'] fakes.assert_has_keys(body['security_group'], required=['name', 'description']) return (205, {}, body) @@ -1252,7 +1252,7 @@ def delete_os_security_group_rules_12(self, **kw): return (202, {}, None) def post_os_security_group_rules(self, body, **kw): - assert body.keys() == ['security_group_rule'] + assert list(body) == ['security_group_rule'] fakes.assert_has_keys(body['security_group_rule'], required=['parent_group_id'], optional=['group_id', 'ip_protocol', 'from_port', From aee366cc500d8301aadaf4ecd0d234806b7b30cd Mon Sep 17 00:00:00 2001 From: Jakub Ruzicka Date: Mon, 23 Sep 2013 17:38:51 +0200 Subject: [PATCH 0277/1705] Make nova CLI use term "server" where possible nova client was using terms "server" and "instance" interchangeably which was very confusing. This patch uses "server" where possible without touching the API. Note that there are actions (instance-action, instance-action-list) and arguments (--instance-name, --num-instances, ...) with "instance" in their name which can't be changed easily. Closes bug 1156648 Change-Id: I0570aa1c4dc9ad1852af307a2a4896f16073a6ab --- novaclient/v1_1/shell.py | 72 ++++++++++++++++++++-------------------- novaclient/v3/shell.py | 72 ++++++++++++++++++++-------------------- 2 files changed, 72 insertions(+), 72 deletions(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 66eb742af..f86995c64 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -313,7 +313,7 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): default=None, type=int, metavar='', - help="boot multi instances at a time (limited by quota).") + help="boot multiple servers at a time (limited by quota).") @utils.arg('--meta', metavar="", action='append', @@ -343,7 +343,7 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): @utils.arg('--availability-zone', default=None, metavar='', - help="The availability zone for instance placement.") + help="The availability zone for server placement.") @utils.arg('--availability_zone', help=argparse.SUPPRESS) @utils.arg('--security-groups', @@ -413,7 +413,7 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): dest='poll', action="store_true", default=False, - help='Blocks while instance builds so progress can be reported.') + help='Blocks while server builds so progress can be reported.') def do_boot(cs, args): """Boot a new server.""" boot_args, boot_kwargs = _boot(cs, args) @@ -476,10 +476,10 @@ def _poll_for_status(poll_fn, obj_id, action, final_ok_states, """ def print_progress(progress): if show_progress: - msg = ('\rInstance %(action)s... %(progress)s%% complete' + msg = ('\rServer %(action)s... %(progress)s%% complete' % dict(action=action, progress=progress)) else: - msg = '\rInstance %(action)s...' % dict(action=action) + msg = '\rServer %(action)s...' % dict(action=action) sys.stdout.write(msg) sys.stdout.flush() @@ -503,7 +503,7 @@ def print_progress(progress): break elif status == "error": if not silent: - print("\nError %s instance" % action) + print("\nError %s server" % action) break if not silent: @@ -1021,7 +1021,7 @@ def do_image_delete(cs, args): dest='reservation_id', metavar='', default=None, - help='Only return instances that match reservation-id.') + help='Only return servers that match reservation-id.') @utils.arg('--reservation_id', help=argparse.SUPPRESS) @utils.arg('--ip', @@ -1043,7 +1043,7 @@ def do_image_delete(cs, args): dest='instance_name', metavar='', default=None, - help='Search with regular expression match by instance name (Admin only).') + help='Search with regular expression match by server name (Admin only).') @utils.arg('--instance_name', help=argparse.SUPPRESS) @utils.arg('--status', @@ -1065,7 +1065,7 @@ def do_image_delete(cs, args): dest='host', metavar='', default=None, - help='Search instances by hostname to which they are assigned ' + help='Search servers by hostname to which they are assigned ' '(Admin only).') @utils.arg('--all-tenants', dest='all_tenants', @@ -1173,7 +1173,7 @@ def do_list(cs, args): dest='poll', action="store_true", default=False, - help='Blocks while instance is rebooting.') + help='Blocks while server is rebooting.') def do_reboot(cs, args): """Reboot a server.""" server = _find_server(cs, args.server) @@ -1190,19 +1190,19 @@ def do_reboot(cs, args): dest='rebuild_password', metavar='', default=False, - help="Set the provided password on the rebuild instance.") + help="Set the provided password on the rebuild server.") @utils.arg('--rebuild_password', help=argparse.SUPPRESS) @utils.arg('--poll', dest='poll', action="store_true", default=False, - help='Blocks while instance rebuilds so progress can be reported.') + help='Blocks while server rebuilds so progress can be reported.') @utils.arg('--minimal', dest='minimal', action="store_true", default=False, - help='Skips flavor/image lookups when showing instances') + help='Skips flavor/image lookups when showing servers') def do_rebuild(cs, args): """Shutdown, re-image, and re-boot a server.""" server = _find_server(cs, args.server) @@ -1235,7 +1235,7 @@ def do_rename(cs, args): dest='poll', action="store_true", default=False, - help='Blocks while instance resizes so progress can be reported.') + help='Blocks while servers resizes so progress can be reported.') def do_resize(cs, args): """Resize a server.""" server = _find_server(cs, args.server) @@ -1264,7 +1264,7 @@ def do_resize_revert(cs, args): dest='poll', action="store_true", default=False, - help='Blocks while instance migrates so progress can be reported.') + help='Blocks while server migrates so progress can be reported.') def do_migrate(cs, args): """Migrate a server. The new host will be selected by the scheduler.""" server = _find_server(cs, args.server) @@ -1361,7 +1361,7 @@ def do_root_password(cs, args): dest='poll', action="store_true", default=False, - help='Blocks while instance snapshots so progress can be reported.') + help='Blocks while server snapshots so progress can be reported.') def do_image_create(cs, args): """Create a new image by taking a snapshot of a running server.""" server = _find_server(cs, args.server) @@ -1394,7 +1394,7 @@ def do_image_create(cs, args): @utils.arg('rotation', metavar='', help='Int parameter representing how many backups to keep around.') def do_backup(cs, args): - """Backup a instance by create a 'backup' type snapshot.""" + """Backup a server by creating a 'backup' type snapshot.""" _find_server(cs, args.server).backup(args.name, args.backup_type, args.rotation) @@ -1469,7 +1469,7 @@ def _print_server(cs, args): dest='minimal', action="store_true", default=False, - help='Skips flavor/image lookups when showing instances') + help='Skips flavor/image lookups when showing servers') @utils.arg('server', metavar='', help='Name or ID of server.') def do_show(cs, args): """Show details about the given server.""" @@ -1741,7 +1741,7 @@ def do_volume_snapshot_show(cs, args): @utils.arg('--force', metavar='', help='Optional flag to indicate whether to snapshot a volume even if its ' - 'attached to an instance. (Default=False)', + 'attached to a server. (Default=False)', default=False) @utils.arg('--display-name', metavar='', @@ -1858,7 +1858,7 @@ def do_clear_password(cs, args): def _print_floating_ip_list(floating_ips): - utils.print_list(floating_ips, ['Ip', 'Instance Id', 'Fixed Ip', 'Pool']) + utils.print_list(floating_ips, ['Ip', 'Server Id', 'Fixed Ip', 'Pool']) @utils.arg('server', metavar='', help='Name or ID of server.') @@ -2032,7 +2032,7 @@ def do_dns_delete_domain(cs, args): @utils.arg('--availability-zone', metavar='', default=None, - help='Limit access to this domain to instances ' + help='Limit access to this domain to servers ' 'in the specified availability zone.') @utils.arg('--availability_zone', help=argparse.SUPPRESS) @@ -2304,7 +2304,7 @@ def do_secgroup_delete_group_rule(cs, args): @utils.arg('--pub_key', help=argparse.SUPPRESS) def do_keypair_add(cs, args): - """Create a new key pair for use with instances.""" + """Create a new key pair for use with servers.""" name = args.name pub_key = args.pub_key @@ -2387,7 +2387,7 @@ def do_rate_limits(cs, args): def do_usage_list(cs, args): """List usage data for all tenants.""" dateformat = "%Y-%m-%d" - rows = ["Tenant ID", "Instances", "RAM MB-Hours", "CPU Hours", + rows = ["Tenant ID", "Servers", "RAM MB-Hours", "CPU Hours", "Disk GB-Hours"] now = timeutils.utcnow() @@ -2434,7 +2434,7 @@ def simplify_usage(u): def do_usage(cs, args): """Show usage data for a single tenant.""" dateformat = "%Y-%m-%d" - rows = ["Instances", "RAM MB-Hours", "CPU Hours", "Disk GB-Hours"] + rows = ["Servers", "RAM MB-Hours", "CPU Hours", "Disk GB-Hours"] now = timeutils.utcnow() @@ -2690,7 +2690,7 @@ def _print_aggregate_details(aggregate): action='store_true', help=argparse.SUPPRESS) def do_live_migration(cs, args): - """Migrate running instance to a new machine.""" + """Migrate running server to a new machine.""" _find_server(cs, args.server).live_migrate(args.host, args.block_migrate, args.disk_over_commit) @@ -2699,16 +2699,16 @@ def do_live_migration(cs, args): @utils.arg('server', metavar='', help='Name or ID of server.') @utils.arg('--active', action='store_const', dest='state', default='error', const='active', - help='Request the instance be reset to "active" state instead ' + help='Request the server be reset to "active" state instead ' 'of "error" state (the default).') def do_reset_state(cs, args): - """Reset the state of an instance.""" + """Reset the state of a server.""" _find_server(cs, args.server).reset_state(args.state) @utils.arg('server', metavar='', help='Name or ID of server.') def do_reset_network(cs, args): - """Reset network of an instance.""" + """Reset network of a server.""" _find_server(cs, args.server).reset_network() @@ -2884,7 +2884,7 @@ def do_hypervisor_list(cs, args): @utils.arg('hostname', metavar='', help='The hypervisor hostname (or pattern) to search for.') def do_hypervisor_servers(cs, args): - """List instances belonging to specific hypervisors.""" + """List servers belonging to specific hypervisors.""" hypers = cs.hypervisors.search(args.hostname, servers=True) class InstanceOnHyper(object): @@ -2982,13 +2982,13 @@ def do_credentials(cs, _args): action='store_true', default=False, help='Optional flag to indicate whether to use private address ' - 'attached to an instance. (Default=False)') + 'attached to a server. (Default=False)') @utils.arg('--ipv6', dest='ipv6', action='store_true', default=False, help='Optional flag to indicate whether to use an IPv6 address ' - 'attached to an instance. (Defaults to IPv4 address)') + 'attached to a server. (Defaults to IPv4 address)') @utils.arg('--login', metavar='', help='Login to use.', default="root") @utils.arg('-i', '--identity', dest='identity', @@ -3307,13 +3307,13 @@ def do_quota_class_update(cs, args): dest='password', metavar='', default=None, - help="Set the provided password on the evacuated instance. Not applicable " + help="Set the provided password on the evacuated server. Not applicable " "with on-shared-storage flag") @utils.arg('--on-shared-storage', dest='on_shared_storage', action="store_true", default=False, - help='Specifies whether instance files located on shared storage') + help='Specifies whether server files are located on shared storage') def do_evacuate(cs, args): """Evacuate server from failed host to specified one.""" server = _find_server(cs, args.server) @@ -3340,7 +3340,7 @@ def __init__(self, interface): @utils.arg('server', metavar='', help='Name or ID of server.') def do_interface_list(cs, args): - """List interfaces attached to an instance.""" + """List interfaces attached to a server.""" server = _find_server(cs, args.server) res = server.interface_list() @@ -3355,7 +3355,7 @@ def do_interface_list(cs, args): @utils.arg('--fixed-ip', metavar='', help='Requested fixed IP.', default=None, dest="fixed_ip") def do_interface_attach(cs, args): - """Attach a network interface to an instance.""" + """Attach a network interface to a server.""" server = _find_server(cs, args.server) res = server.interface_attach(args.port_id, args.net_id, args.fixed_ip) @@ -3366,7 +3366,7 @@ def do_interface_attach(cs, args): @utils.arg('server', metavar='', help='Name or ID of server.') @utils.arg('port_id', metavar='', help='Port ID.') def do_interface_detach(cs, args): - """Detach a network interface from an instance.""" + """Detach a network interface from a server.""" server = _find_server(cs, args.server) res = server.interface_detach(args.port_id) diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 7b478c79e..3044e6d9f 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -222,7 +222,7 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): default=None, type=int, metavar='', - help="boot multi instances at a time (limited by quota).") + help="boot multiple servers at a time (limited by quota).") @utils.arg('--meta', metavar="", action='append', @@ -252,7 +252,7 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): @utils.arg('--availability-zone', default=None, metavar='', - help="The availability zone for instance placement.") + help="The availability zone for server placement.") @utils.arg('--availability_zone', help=argparse.SUPPRESS) @utils.arg('--security-groups', @@ -297,7 +297,7 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): dest='poll', action="store_true", default=False, - help='Blocks while instance builds so progress can be reported.') + help='Blocks while server builds so progress can be reported.') def do_boot(cs, args): """Boot a new server.""" boot_args, boot_kwargs = _boot(cs, args) @@ -360,10 +360,10 @@ def _poll_for_status(poll_fn, obj_id, action, final_ok_states, """ def print_progress(progress): if show_progress: - msg = ('\rInstance %(action)s... %(progress)s%% complete' + msg = ('\rServer %(action)s... %(progress)s%% complete' % dict(action=action, progress=progress)) else: - msg = '\rInstance %(action)s...' % dict(action=action) + msg = '\rServer %(action)s...' % dict(action=action) sys.stdout.write(msg) sys.stdout.flush() @@ -387,7 +387,7 @@ def print_progress(progress): break elif status == "error": if not silent: - print("\nError %s instance" % action) + print("\nError %s server" % action) break if not silent: @@ -905,7 +905,7 @@ def do_image_delete(cs, args): dest='reservation_id', metavar='', default=None, - help='Only return instances that match reservation-id.') + help='Only return servers that match reservation-id.') @utils.arg('--reservation_id', help=argparse.SUPPRESS) @utils.arg('--ip', @@ -927,7 +927,7 @@ def do_image_delete(cs, args): dest='instance_name', metavar='', default=None, - help='Search with regular expression match by instance name (Admin only).') + help='Search with regular expression match by server name (Admin only).') @utils.arg('--instance_name', help=argparse.SUPPRESS) @utils.arg('--status', @@ -949,7 +949,7 @@ def do_image_delete(cs, args): dest='host', metavar='', default=None, - help='Search instances by hostname to which they are assigned ' + help='Search servers by hostname to which they are assigned ' '(Admin only).') @utils.arg('--all-tenants', dest='all_tenants', @@ -1057,7 +1057,7 @@ def do_list(cs, args): dest='poll', action="store_true", default=False, - help='Blocks while instance is rebooting.') + help='Blocks while server is rebooting.') def do_reboot(cs, args): """Reboot a server.""" server = _find_server(cs, args.server) @@ -1074,19 +1074,19 @@ def do_reboot(cs, args): dest='rebuild_password', metavar='', default=False, - help="Set the provided password on the rebuild instance.") + help="Set the provided password on the rebuild server.") @utils.arg('--rebuild_password', help=argparse.SUPPRESS) @utils.arg('--poll', dest='poll', action="store_true", default=False, - help='Blocks while instance rebuilds so progress can be reported.') + help='Blocks while server rebuilds so progress can be reported.') @utils.arg('--minimal', dest='minimal', action="store_true", default=False, - help='Skips flavor/image lookups when showing instances') + help='Skips flavor/image lookups when showing servers') def do_rebuild(cs, args): """Shutdown, re-image, and re-boot a server.""" server = _find_server(cs, args.server) @@ -1119,7 +1119,7 @@ def do_rename(cs, args): dest='poll', action="store_true", default=False, - help='Blocks while instance resizes so progress can be reported.') + help='Blocks while server resizes so progress can be reported.') def do_resize(cs, args): """Resize a server.""" server = _find_server(cs, args.server) @@ -1148,7 +1148,7 @@ def do_resize_revert(cs, args): dest='poll', action="store_true", default=False, - help='Blocks while instance migrates so progress can be reported.') + help='Blocks while server migrates so progress can be reported.') def do_migrate(cs, args): """Migrate a server. The new host will be selected by the scheduler.""" server = _find_server(cs, args.server) @@ -1245,7 +1245,7 @@ def do_root_password(cs, args): dest='poll', action="store_true", default=False, - help='Blocks while instance snapshots so progress can be reported.') + help='Blocks while server snapshots so progress can be reported.') def do_image_create(cs, args): """Create a new image by taking a snapshot of a running server.""" server = _find_server(cs, args.server) @@ -1278,7 +1278,7 @@ def do_image_create(cs, args): @utils.arg('rotation', metavar='', help='Int parameter representing how many backups to keep around.') def do_backup(cs, args): - """Backup a instance by create a 'backup' type snapshot.""" + """Backup a server by creating a 'backup' type snapshot.""" _find_server(cs, args.server).backup(args.name, args.backup_type, args.rotation) @@ -1353,7 +1353,7 @@ def _print_server(cs, args): dest='minimal', action="store_true", default=False, - help='Skips flavor/image lookups when showing instances') + help='Skips flavor/image lookups when showing servers') @utils.arg('server', metavar='', help='Name or ID of server.') def do_show(cs, args): """Show details about the given server.""" @@ -1600,7 +1600,7 @@ def do_volume_snapshot_show(cs, args): @utils.arg('--force', metavar='', help='Optional flag to indicate whether to snapshot a volume even if its ' - 'attached to an instance. (Default=False)', + 'attached to a server. (Default=False)', default=False) @utils.arg('--display-name', metavar='', @@ -1717,7 +1717,7 @@ def do_clear_password(cs, args): def _print_floating_ip_list(floating_ips): - utils.print_list(floating_ips, ['Ip', 'Instance Id', 'Fixed Ip', 'Pool']) + utils.print_list(floating_ips, ['Ip', 'Server Id', 'Fixed Ip', 'Pool']) @utils.arg('server', metavar='', help='Name or ID of server.') @@ -1883,7 +1883,7 @@ def do_dns_delete_domain(cs, args): @utils.arg('--availability-zone', metavar='', default=None, - help='Limit access to this domain to instances ' + help='Limit access to this domain to servers ' 'in the specified availability zone.') @utils.arg('--availability_zone', help=argparse.SUPPRESS) @@ -2155,7 +2155,7 @@ def do_secgroup_delete_group_rule(cs, args): @utils.arg('--pub_key', help=argparse.SUPPRESS) def do_keypair_add(cs, args): - """Create a new key pair for use with instances.""" + """Create a new key pair for use with servers.""" name = args.name pub_key = args.pub_key @@ -2238,7 +2238,7 @@ def do_rate_limits(cs, args): def do_usage_list(cs, args): """List usage data for all tenants.""" dateformat = "%Y-%m-%d" - rows = ["Tenant ID", "Instances", "RAM MB-Hours", "CPU Hours", + rows = ["Tenant ID", "Servers", "RAM MB-Hours", "CPU Hours", "Disk GB-Hours"] now = timeutils.utcnow() @@ -2285,7 +2285,7 @@ def simplify_usage(u): def do_usage(cs, args): """Show usage data for a single tenant.""" dateformat = "%Y-%m-%d" - rows = ["Instances", "RAM MB-Hours", "CPU Hours", "Disk GB-Hours"] + rows = ["Servers", "RAM MB-Hours", "CPU Hours", "Disk GB-Hours"] now = timeutils.utcnow() @@ -2541,7 +2541,7 @@ def _print_aggregate_details(aggregate): action='store_true', help=argparse.SUPPRESS) def do_live_migration(cs, args): - """Migrate running instance to a new machine.""" + """Migrate running server to a new machine.""" _find_server(cs, args.server).live_migrate(args.host, args.block_migrate, args.disk_over_commit) @@ -2550,16 +2550,16 @@ def do_live_migration(cs, args): @utils.arg('server', metavar='', help='Name or ID of server.') @utils.arg('--active', action='store_const', dest='state', default='error', const='active', - help='Request the instance be reset to "active" state instead ' + help='Request the server be reset to "active" state instead ' 'of "error" state (the default).') def do_reset_state(cs, args): - """Reset the state of an instance.""" + """Reset the state of a server.""" _find_server(cs, args.server).reset_state(args.state) @utils.arg('server', metavar='', help='Name or ID of server.') def do_reset_network(cs, args): - """Reset network of an instance.""" + """Reset network of a server.""" _find_server(cs, args.server).reset_network() @@ -2735,7 +2735,7 @@ def do_hypervisor_list(cs, args): @utils.arg('hostname', metavar='', help='The hypervisor hostname (or pattern) to search for.') def do_hypervisor_servers(cs, args): - """List instances belonging to specific hypervisors.""" + """List servers belonging to specific hypervisors.""" hypers = cs.hypervisors.search(args.hostname, servers=True) class InstanceOnHyper(object): @@ -2833,13 +2833,13 @@ def do_credentials(cs, _args): action='store_true', default=False, help='Optional flag to indicate whether to use private address ' - 'attached to an instance. (Default=False)') + 'attached to a server. (Default=False)') @utils.arg('--ipv6', dest='ipv6', action='store_true', default=False, help='Optional flag to indicate whether to use an IPv6 address ' - 'attached to an instance. (Defaults to IPv4 address)') + 'attached to a server. (Defaults to IPv4 address)') @utils.arg('--login', metavar='', help='Login to use.', default="root") @utils.arg('-i', '--identity', dest='identity', @@ -3136,13 +3136,13 @@ def do_quota_class_update(cs, args): dest='password', metavar='', default=None, - help="Set the provided password on the evacuated instance. Not applicable " + help="Set the provided password on the evacuated server. Not applicable " "with on-shared-storage flag") @utils.arg('--on-shared-storage', dest='on_shared_storage', action="store_true", default=False, - help='Specifies whether instance files located on shared storage') + help='Specifies whether server files are located on shared storage') def do_evacuate(cs, args): """Evacuate server from failed host to specified one.""" server = _find_server(cs, args.server) @@ -3169,7 +3169,7 @@ def __init__(self, interface): @utils.arg('server', metavar='', help='Name or ID of server.') def do_interface_list(cs, args): - """List interfaces attached to an instance.""" + """List interfaces attached to a server.""" server = _find_server(cs, args.server) res = server.interface_list() @@ -3184,7 +3184,7 @@ def do_interface_list(cs, args): @utils.arg('--fixed-ip', metavar='', help='Requested fixed IP.', default=None, dest="fixed_ip") def do_interface_attach(cs, args): - """Attach a network interface to an instance.""" + """Attach a network interface to a server.""" server = _find_server(cs, args.server) res = server.interface_attach(args.port_id, args.net_id, args.fixed_ip) @@ -3195,7 +3195,7 @@ def do_interface_attach(cs, args): @utils.arg('server', metavar='', help='Name or ID of server.') @utils.arg('port_id', metavar='', help='Port ID.') def do_interface_detach(cs, args): - """Detach a network interface from an instance.""" + """Detach a network interface from a server.""" server = _find_server(cs, args.server) res = server.interface_detach(args.port_id) From 991ab62ef163ce8009ffbbed89ff87226ca58a68 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Mon, 23 Sep 2013 17:55:24 -0700 Subject: [PATCH 0278/1705] Fixed several test failures on Python3 - Corrected places where `dict.keys()` was being treated as a list - Corrected usage of `urllib.urlencode()` Change-Id: I0a24b2f64fab6c905e20073ff7ebaa3c5ffe7c90 --- novaclient/tests/v1_1/fakes.py | 10 +++++----- novaclient/v1_1/contrib/migrations.py | 5 ++--- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index 81a415860..8f48ac4f5 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -493,7 +493,7 @@ def post_servers_1234_action(self, body, **kw): assert list(body[action]) == ['type'] assert body[action]['type'] in ['HARD', 'SOFT'] elif action == 'rebuild': - keys = body[action].keys() + keys = list(body[action]) if 'adminPass' in keys: keys.remove('adminPass') assert 'imageRef' in keys @@ -573,11 +573,11 @@ def post_servers_1234_action(self, body, **kw): elif action == 'removeSecurityGroup': assert list(body[action]) == ['name'] elif action == 'createBackup': - assert set(body[action].keys()) == set(['name', - 'backup_type', - 'rotation']) + assert set(body[action]) == set(['name', + 'backup_type', + 'rotation']) elif action == 'evacuate': - keys = body[action].keys() + keys = list(body[action]) if 'adminPass' in keys: keys.remove('adminPass') assert set(keys) == set(['host', 'onSharedStorage']) diff --git a/novaclient/v1_1/contrib/migrations.py b/novaclient/v1_1/contrib/migrations.py index ee2f49963..fc7c89213 100644 --- a/novaclient/v1_1/contrib/migrations.py +++ b/novaclient/v1_1/contrib/migrations.py @@ -16,9 +16,8 @@ migration interface """ -import urllib - from novaclient import base +from novaclient.openstack.common.py3kcompat import urlutils from novaclient import utils @@ -45,7 +44,7 @@ def list(self, host=None, status=None, cell_name=None): if cell_name: opts['cell_name'] = cell_name - query_string = "?%s" % urllib.urlencode(opts) if opts else "" + query_string = "?%s" % urlutils.urlencode(opts) if opts else "" return self._list("/os-migrations%s" % query_string, "migrations") From 9e8dd063c6c4215ebda25d0199cec3fb64a81837 Mon Sep 17 00:00:00 2001 From: Roman Podolyaka Date: Tue, 17 Sep 2013 10:21:03 +0300 Subject: [PATCH 0279/1705] Fix AttributeError in Keypair._add_details() _add_details() method copies keypair attributes retrieved from Nova to an instance of Keypair class. Trying to set attribute 'id' fails with AttributeError, because Keypair class has a read-only property 'id'. We can safely omit setting of attribute 'id', because it's just an implementation detail (the id of a table row in a database), and keypairs for a given tenant are uniquely identified using attribute 'name'. Fixes bug 1223934 Change-Id: I1045730e47e9e6ad31fcdfbaefdad77e2f3b2c3e --- novaclient/v1_1/keypairs.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/novaclient/v1_1/keypairs.py b/novaclient/v1_1/keypairs.py index 28bd760c1..cd9e5852f 100644 --- a/novaclient/v1_1/keypairs.py +++ b/novaclient/v1_1/keypairs.py @@ -32,7 +32,15 @@ def _add_details(self, info): dico = 'keypair' in info and \ info['keypair'] or info for (k, v) in dico.items(): - setattr(self, k, v) + # NOTE(rpodolyaka): keypair name allows us to uniquely identify + # a specific keypair, while its id attribute + # is nothing more than an implementation + # detail. We can safely omit the id attribute + # here to ensure setattr() won't raise + # AttributeError trying to set read-only + # property id + if k != 'id': + setattr(self, k, v) @property def id(self): From 8b264fc61d21fe4d0c7405914fb084f898956888 Mon Sep 17 00:00:00 2001 From: Kui Shi Date: Wed, 25 Sep 2013 06:24:14 +0800 Subject: [PATCH 0280/1705] py33: unknown encoding: base64 Edit Python 3 doesn't support str.encode('base64'), we should use the module "base64" instead. Close-Bug #1229161 Change-Id: I1432952558f8c5c3cff39761306e91916d80207f --- novaclient/base.py | 2 +- novaclient/tests/v1_1/test_shell.py | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/novaclient/base.py b/novaclient/base.py index 120c698fc..a5dad823f 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -360,7 +360,7 @@ def _boot(self, resource_url, response_key, name, image, flavor, data = file_or_string personality.append({ 'path': filepath, - 'contents': data.encode('base64'), + 'contents': base64.b64encode(data.encode('utf-8')), }) if availability_zone: diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index 48b08d9b1..93f852ec8 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -16,6 +16,7 @@ # License for the specific language governing permissions and limitations # under the License. +import base64 import datetime import os import mock @@ -174,7 +175,8 @@ def test_boot_key(self): def test_boot_user_data(self): testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt') - expected_file_data = open(testfile).read().encode('base64').strip() + data = open(testfile).read() + expected_file_data = base64.b64encode(data.encode('utf-8')) self.run_command( 'boot --flavor 1 --image 1 --user_data %s some-server' % testfile) self.assert_called_anytime( @@ -514,7 +516,8 @@ def test_boot_nics_no_netid_or_portid(self): def test_boot_files(self): testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt') - expected_file_data = open(testfile).read().encode('base64') + data = open(testfile).read() + expected_file_data = base64.b64encode(data.encode('utf-8')) cmd = ('boot some-server --flavor 1 --image 1' ' --file /tmp/foo=%s --file /tmp/bar=%s') From ac50057710453de80bb7f81824e812581bb0f219 Mon Sep 17 00:00:00 2001 From: Kui Shi Date: Wed, 25 Sep 2013 15:48:42 +0800 Subject: [PATCH 0281/1705] py33: safe_encode() returns bytes in Python 3 Python 2: str.encode = encode(...) S.encode([encoding[,errors]]) -> object Python 3: str.encode = encode(...) S.encode(encoding='utf-8', errors='strict') -> bytes In Python 3, strings are always Unicode. safe_encode() returns "bytes", the "\n" will be printed literally, it is not expected. bytes.decode() will transform bytes to str, which will be printed properly. Close-bug: #1229150 Change-Id: If926d3f7837673b4608ae667d34cea3c78e82e5d --- novaclient/utils.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/novaclient/utils.py b/novaclient/utils.py index 5abecb6c7..2d9bcdee2 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -167,9 +167,14 @@ def print_list(objs, fields, formatters={}, sortby_index=None): pt.add_row(row) if sortby is not None: - print(strutils.safe_encode(pt.get_string(sortby=sortby))) + result = strutils.safe_encode(pt.get_string(sortby=sortby)) else: - print(strutils.safe_encode(pt.get_string())) + result = strutils.safe_encode(pt.get_string()) + + if six.PY3: + result = result.decode() + + print(result) def print_dict(d, dict_property="Property", dict_value="Value", wrap=0): From 9ff60119edd9263319d8d9cc749aaf54b44863d0 Mon Sep 17 00:00:00 2001 From: Nicolas Simonds Date: Wed, 3 Jul 2013 11:08:41 -0700 Subject: [PATCH 0282/1705] if we have a valid auth token, use it instead of generating a new one If authenticating with a valid token (i.e., token auth, not username/password auth), novaclient would instruct keystone to issue a new token with an identical expiry to the one used, and pivot onto it. This makes it appear as though Nova is leaking Keystone tokens. Since there's zero benefit to issuing a new token that is ostensibly identical to the perfectly good token used to authenticate, make novaclient keep using the token it already has after successful validation, and not issue a new one. This patch set adds a new pair of tests to the AuthenticateAgainstKeystoneTests class, which parrot the current authentication pass/fail tests, but exercise the new code paths Also adds a fix by Joe Gordon to make os_cache work correctly. Change-Id: Ic4f0e6d506d8d81f0b1d46138e7e19d3b3392e1d Fixes: Bug 1197201 Related-Bug: 1214658 --- novaclient/client.py | 17 +++++-- novaclient/tests/v1_1/test_auth.py | 73 ++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 3 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index 02d75ec18..123a3e9bf 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -412,17 +412,28 @@ def _v2_auth(self, url): def _authenticate(self, url, body, **kwargs): """Authenticate and extract the service catalog.""" + method = "POST" token_url = url + "/tokens" + # if we have a valid auth token, use it instead of generating a new one + if self.auth_token: + kwargs.setdefault('headers', {})['X-Auth-Token'] = self.auth_token + token_url += "/" + self.auth_token + method = "GET" + body = None + + if self.auth_token and self.tenant_id and self.management_url: + return None + # Make sure we follow redirects when trying to reach Keystone - resp, body = self._time_request( + resp, respbody = self._time_request( token_url, - "POST", + method, body=body, allow_redirects=True, **kwargs) - return self._extract_service_catalog(url, resp, body) + return self._extract_service_catalog(url, resp, respbody) def get_client_class(version): diff --git a/novaclient/tests/v1_1/test_auth.py b/novaclient/tests/v1_1/test_auth.py index 537f59b5f..f3bd279a9 100644 --- a/novaclient/tests/v1_1/test_auth.py +++ b/novaclient/tests/v1_1/test_auth.py @@ -325,6 +325,79 @@ def test_auth_call(): test_auth_call() + def test_authenticate_with_token_success(self): + cs = client.Client("username", None, "project_id", + "auth_url/v2.0", service_type='compute') + cs.client.auth_token = "FAKE_ID" + resp = { + "access": { + "token": { + "expires": "12345", + "id": "FAKE_ID", + "tenant": { + "id": "FAKE_TENANT_ID", + } + }, + "serviceCatalog": [ + { + "type": "compute", + "endpoints": [ + { + "region": "RegionOne", + "adminURL": "http://localhost:8774/v1.1", + "internalURL": "http://localhost:8774/v1.1", + "publicURL": "http://localhost:8774/v1.1/", + }, + ], + }, + ], + }, + } + auth_response = utils.TestResponse({ + "status_code": 200, + "text": json.dumps(resp), + }) + + mock_request = mock.Mock(return_value=(auth_response)) + + with mock.patch.object(requests.Session, "request", mock_request): + cs.client.authenticate() + headers = { + 'User-Agent': cs.client.USER_AGENT, + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'X-Auth-Token': "FAKE_ID", + } + + token_url = cs.client.auth_url + "/tokens/FAKE_ID" + mock_request.assert_called_with( + "GET", + token_url, + headers=headers, + data="null", + allow_redirects=True, + **self.TEST_REQUEST_BASE) + + endpoints = resp["access"]["serviceCatalog"][0]['endpoints'] + public_url = endpoints[0]["publicURL"].rstrip('/') + self.assertEqual(cs.client.management_url, public_url) + token_id = resp["access"]["token"]["id"] + self.assertEqual(cs.client.auth_token, token_id) + + def test_authenticate_with_token_failure(self): + cs = client.Client("username", None, "project_id", "auth_url/v2.0") + cs.client.auth_token = "FAKE_ID" + resp = {"unauthorized": {"message": "Unauthorized", "code": "401"}} + auth_response = utils.TestResponse({ + "status_code": 401, + "text": json.dumps(resp), + }) + + mock_request = mock.Mock(return_value=(auth_response)) + + with mock.patch.object(requests.Session, "request", mock_request): + self.assertRaises(exceptions.Unauthorized, cs.client.authenticate) + class AuthenticationTests(utils.TestCase): def test_authenticate_success(self): From 8eed73ce20ff0808d350fc9af66d6ba55ebda017 Mon Sep 17 00:00:00 2001 From: Morgan Fainberg Date: Wed, 25 Sep 2013 19:01:13 -0700 Subject: [PATCH 0283/1705] Add-in some re-auth logic when using os_cache When using os_cache for the auth_token, if username and password are available when a token fails to validate/authenticate, attempt a reauth behind the scenes. The helper object for stashing the auth_token doesn't handle flushing the cache. This is valid behavior because the result would be the same as if a bad username/password were issued (401 Unauthorized). Fixes-bug: 1214658 Change-Id: I107fc999610bb7638cefcb5d195aeafdcd663ca6 --- novaclient/client.py | 30 +++++++-- novaclient/tests/v1_1/test_auth.py | 97 +++++++++++++++++++++++++++++- 2 files changed, 120 insertions(+), 7 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index 123a3e9bf..ad594dd32 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -395,20 +395,38 @@ def _plugin_auth(self, auth_url): def _v2_auth(self, url): """Authenticate against a v2.0 auth service.""" + body_pass = {"auth": { + "passwordCredentials": {"username": self.user, + "password": self.password}}} + body_token = {"auth": {"token": {"id": self.auth_token}}} + if self.auth_token: - body = {"auth": { - "token": {"id": self.auth_token}}} + body = body_token else: - body = {"auth": { - "passwordCredentials": {"username": self.user, - "password": self.password}}} + body = body_pass if self.tenant_id: body['auth']['tenantId'] = self.tenant_id elif self.projectid: body['auth']['tenantName'] = self.projectid - return self._authenticate(url, body) + try: + return self._authenticate(url, body) + except exceptions.Unauthorized: + # NOTE(morganfainberg): there is no actual point in flushing + # the cache out because it would result in the same behavior + # in either case, a 401 being raised/returned. The expected + # recourse in the case of a failure will be the same, reauth + # with username and password. + + if (self.os_cache and self.user and self.password and + self.keyring_saver is not None): + # If we are using a cache, and we failed, try again if we have + # the required information to do so. + self.auth_token = None + body = body_pass + return self._authenticate(url, body) + raise def _authenticate(self, url, body, **kwargs): """Authenticate and extract the service catalog.""" diff --git a/novaclient/tests/v1_1/test_auth.py b/novaclient/tests/v1_1/test_auth.py index f3bd279a9..fa802b812 100644 --- a/novaclient/tests/v1_1/test_auth.py +++ b/novaclient/tests/v1_1/test_auth.py @@ -9,6 +9,14 @@ from novaclient.tests import utils +class MockKeyringSaver(object): + def __init__(self): + self.saved = [] + + def save(self, *args): + self.saved = args + + class AuthenticateAgainstKeystoneTests(utils.TestCase): def test_authenticate_success(self): cs = client.Client("username", "password", "project_id", @@ -385,7 +393,8 @@ def test_authenticate_with_token_success(self): self.assertEqual(cs.client.auth_token, token_id) def test_authenticate_with_token_failure(self): - cs = client.Client("username", None, "project_id", "auth_url/v2.0") + cs = client.Client("username", None, "project_id", "auth_url/v2.0", + service_type='compute') cs.client.auth_token = "FAKE_ID" resp = {"unauthorized": {"message": "Unauthorized", "code": "401"}} auth_response = utils.TestResponse({ @@ -398,6 +407,92 @@ def test_authenticate_with_token_failure(self): with mock.patch.object(requests.Session, "request", mock_request): self.assertRaises(exceptions.Unauthorized, cs.client.authenticate) + def test_authenticate_with_os_cache_and_failure(self): + cs = client.Client("username", "password", "project_id", + "auth_url/v2.0", service_type='compute') + cs.client.os_cache = True + cs.client.keyring_saver = MockKeyringSaver() + cs.client.auth_token = "FAKE_ID" + + catalog_managment_url = u"http://localhost:8774/v1.1" + + headers_get = { + 'User-Agent': cs.client.USER_AGENT, + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'X-Auth-Token': "FAKE_ID"} + headers_post = { + 'User-Agent': cs.client.USER_AGENT, + 'Content-Type': 'application/json', + 'Accept': 'application/json'} + token_url_get = cs.client.auth_url + "/tokens/FAKE_ID" + token_url_post = cs.client.auth_url + "/tokens" + resp = { + "access": { + "token": { + "expires": "12345", + "id": "FAKE_ID_NEW", + "tenant": { + "id": "FAKE_TENANT_ID", + } + }, + "serviceCatalog": [ + { + "type": "compute", + "endpoints": [ + { + "region": "RegionOne", + "adminURL": catalog_managment_url, + "internalURL": catalog_managment_url, + "publicURL": catalog_managment_url, + }, + ], + }, + ], + }, + } + + auth_resp_200 = utils.TestResponse({ + "status_code": 200, + "text": json.dumps(resp), + }) + auth_resp_401 = utils.TestResponse({ + "status_code": 401, + "text": json.dumps({"unauthorized": + {"message": "Unauthorized", + "code": "401"}}), + }) + + expected_keyring = (u"FAKE_ID_NEW", + catalog_managment_url, + u"FAKE_TENANT_ID") + + post_data = {"auth": + {"passwordCredentials": + {"username": "username", + "password": "password"}}} + + mock_request = mock.Mock() + mock_request.side_effect = [auth_resp_401, auth_resp_200] + + with mock.patch.object(requests.Session, "request", mock_request): + expected = [mock.call("GET", + token_url_get, + headers=headers_get, + data="null", + allow_redirects=True, + **self.TEST_REQUEST_BASE), + mock.call("POST", + token_url_post, + headers=headers_post, + data=json.dumps(post_data), + allow_redirects=True, + **self.TEST_REQUEST_BASE), + ] + cs.client.authenticate() + self.assertEqual(expected, mock_request.call_args_list) + self.assertEqual(cs.client.keyring_saver.saved, expected_keyring) + class AuthenticationTests(utils.TestCase): def test_authenticate_success(self): From 74d03270f5e0462e8de7bc2ce8061cfb7acc3206 Mon Sep 17 00:00:00 2001 From: Kui Shi Date: Sat, 28 Sep 2013 06:08:57 +0800 Subject: [PATCH 0284/1705] py33: don't encode security_group In Python 3, str and bytes can not be mixed. str.encode() will return bytes in py33, which will induce the mismatch. Bypass the s.name.encode() in py33. Change-Id: Iee8d69aab9df4af126e94e4363f79e32fdd70fcd Close-bug: #1231868 --- novaclient/v1_1/shell.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index a417025e3..df666acf4 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -24,6 +24,7 @@ import getpass import locale import os +import six import sys import time @@ -2092,7 +2093,8 @@ def _get_secgroup(cs, secgroup): encoding = (locale.getpreferredencoding() or sys.stdin.encoding or 'UTF-8') - s.name = s.name.encode(encoding) + if not six.PY3: + s.name = s.name.encode(encoding) if secgroup == s.name: if match_found != False: msg = ("Multiple security group matches found for name" From f94c625a38810b86f9774a72a12d5960205a3c81 Mon Sep 17 00:00:00 2001 From: Kui Shi Date: Sat, 28 Sep 2013 21:52:37 +0800 Subject: [PATCH 0285/1705] py33: uuid verification in find_resource() 1. strutils.safe_encode() return "bytes" in py33. 2. find_resource() makes use of uuid.UUID() to verify the given uuid argument, uuid.UUID() will trigger TypeError when input is "bytes". so, decode the bytes before uuid.UUID() while run in Python 3. Close-bug #1232445 Change-Id: I504be7b43236b3a4843655706385e506cd559e4e --- novaclient/utils.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/novaclient/utils.py b/novaclient/utils.py index 5abecb6c7..d9f37a84f 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -204,8 +204,11 @@ def find_resource(manager, name_or_id, **find_args): # now try to get entity as uuid try: - uuid.UUID(strutils.safe_encode(name_or_id)) - return manager.get(name_or_id) + tmp_id = strutils.safe_encode(name_or_id) + if six.PY3: + tmp_id = tmp_id.decode() + uuid.UUID(tmp_id) + return manager.get(tmp_id) except (TypeError, ValueError, exceptions.NotFound): pass From ffb83222e535fb26eb10e691310880893d7d9eed Mon Sep 17 00:00:00 2001 From: Ken'ichi Ohmichi Date: Sun, 29 Sep 2013 00:04:56 +0900 Subject: [PATCH 0286/1705] Add shelve/unshelve/shelve-offload command The "shelve" API, which powers off and snapshots a vm instance, has been implemented by Nova-server side. In addition, thare are two relational API "unshelve" and "shelveOffload". This patch adds three commands for using these APIs. Fixes bug #1231827 Change-Id: Ic2ad1bb537efb40a3155e9f3c7b63e09e82be7f1 --- novaclient/tests/v1_1/fakes.py | 6 +++++ novaclient/tests/v1_1/test_shell.py | 13 +++++++++++ novaclient/v1_1/servers.py | 36 +++++++++++++++++++++++++++++ novaclient/v1_1/shell.py | 18 +++++++++++++++ 4 files changed, 73 insertions(+) diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index 8f48ac4f5..5f9873cbf 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -538,6 +538,12 @@ def post_servers_1234_action(self, body, **kw): assert body[action] is None elif action == 'unlock': assert body[action] is None + elif action == 'shelve': + assert body[action] is None + elif action == 'shelveOffload': + assert body[action] is None + elif action == 'unshelve': + assert body[action] is None elif action == 'addFixedIp': assert list(body[action]) == ['networkId'] elif action == 'removeFixedIp': diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index 48b08d9b1..a0e9e557a 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -768,6 +768,19 @@ def test_unrescue(self): self.run_command('unrescue sample-server') self.assert_called('POST', '/servers/1234/action', {'unrescue': None}) + def test_shelve(self): + self.run_command('shelve sample-server') + self.assert_called('POST', '/servers/1234/action', {'shelve': None}) + + def test_shelve_offload(self): + self.run_command('shelve-offload sample-server') + self.assert_called('POST', '/servers/1234/action', + {'shelveOffload': None}) + + def test_unshelve(self): + self.run_command('unshelve sample-server') + self.assert_called('POST', '/servers/1234/action', {'unshelve': None}) + def test_migrate(self): self.run_command('migrate sample-server') self.assert_called('POST', '/servers/1234/action', {'migrate': None}) diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index 8d2df4fdc..b47ece808 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -187,6 +187,24 @@ def unrescue(self): """ self.manager.unrescue(self) + def shelve(self): + """ + Shelve -- Shelve the server. + """ + self.manager.shelve(self) + + def shelve_offload(self): + """ + Shelve_offload -- Remove a shelved server from the compute node. + """ + self.manager.shelve_offload(self) + + def unshelve(self): + """ + Unshelve -- Unshelve the server. + """ + self.manager.unshelve(self) + def diagnostics(self): """Diagnostics -- Retrieve server diagnostics.""" return self.manager.diagnostics(self) @@ -574,6 +592,24 @@ def unrescue(self, server): """ self._action('unrescue', server, None) + def shelve(self, server): + """ + Shelve the server. + """ + self._action('shelve', server, None) + + def shelve_offload(self, server): + """ + Remove a shelved instance from the compute node. + """ + self._action('shelveOffload', server, None) + + def unshelve(self, server): + """ + Unshelve the server. + """ + self._action('unshelve', server, None) + def diagnostics(self, server): """Retrieve server diagnostics.""" return self.api.client.get("/servers/%s/diagnostics" % diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index a417025e3..292ee01eb 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1336,6 +1336,24 @@ def do_unrescue(cs, args): _find_server(cs, args.server).unrescue() +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_shelve(cs, args): + """Shelve a server.""" + _find_server(cs, args.server).shelve() + + +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_shelve_offload(cs, args): + """Remove a shelved server from the compute node.""" + _find_server(cs, args.server).shelve_offload() + + +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_unshelve(cs, args): + """Unshelve a server.""" + _find_server(cs, args.server).unshelve() + + @utils.arg('server', metavar='', help='Name or ID of server.') def do_diagnostics(cs, args): """Retrieve server diagnostics.""" From ecdd79b04aa65b5e8740c5a2a5fdca2530f2d652 Mon Sep 17 00:00:00 2001 From: Kui Shi Date: Sun, 29 Sep 2013 21:45:36 +0800 Subject: [PATCH 0287/1705] py33: align the order of parameters for urlencode() In Python 3, hash randomization is enabled by default. It causes the iteration order of dicts and sets to be unpredictable and differ across Python runs. In the test case, the fixed expecting string will not match the test result, it is relying on the dict order. This change transforms the input dict to a sequence of two-element list, with fixed order, and update the related expecitng string. Close-Bug #1231871 Change-Id: Ia998cdb6978fc024dd0d3c9bd161fbdebe68638a --- novaclient/tests/v1_1/contrib/test_migrations.py | 4 ++-- novaclient/tests/v1_1/test_shell.py | 4 ++-- novaclient/v1_1/contrib/migrations.py | 6 +++++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/novaclient/tests/v1_1/contrib/test_migrations.py b/novaclient/tests/v1_1/contrib/test_migrations.py index 7b49c4cc7..2b4d4cb03 100644 --- a/novaclient/tests/v1_1/contrib/test_migrations.py +++ b/novaclient/tests/v1_1/contrib/test_migrations.py @@ -36,7 +36,7 @@ def test_list_migrations_with_filters(self): ml = cs.migrations.list('host1', 'finished', 'child1') cs.assert_called('GET', - '/os-migrations?status=finished&host=host1' - '&cell_name=child1') + '/os-migrations?cell_name=child1&host=host1' + '&status=finished') for m in ml: self.assertTrue(isinstance(m, migrations.Migration)) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index 48b08d9b1..dec56b218 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -1799,5 +1799,5 @@ def test_migration_list_with_filters(self): self.run_command('migration-list --host host1 --cell_name child1 ' '--status finished') self.assert_called('GET', - '/os-migrations?status=finished&host=host1' - '&cell_name=child1') + '/os-migrations?cell_name=child1&host=host1' + '&status=finished') diff --git a/novaclient/v1_1/contrib/migrations.py b/novaclient/v1_1/contrib/migrations.py index fc7c89213..9322849aa 100644 --- a/novaclient/v1_1/contrib/migrations.py +++ b/novaclient/v1_1/contrib/migrations.py @@ -44,7 +44,11 @@ def list(self, host=None, status=None, cell_name=None): if cell_name: opts['cell_name'] = cell_name - query_string = "?%s" % urlutils.urlencode(opts) if opts else "" + # Transform the dict to a sequence of two-element tuples in fixed + # order, then the encoded string will be consistent in Python 2&3. + new_opts = sorted(opts.items(), key=lambda x: x[0]) + + query_string = "?%s" % urlutils.urlencode(new_opts) if new_opts else "" return self._list("/os-migrations%s" % query_string, "migrations") From ebee5e91b7c6d1aa506661e4558a99be8778bae1 Mon Sep 17 00:00:00 2001 From: Dirk Mueller Date: Mon, 30 Sep 2013 22:25:17 +0200 Subject: [PATCH 0288/1705] assertEquals is deprecated, use assertEqual Change-Id: I9b2aaed969b046e0203ffa0c7278dff206cb8cd2 --- novaclient/tests/test_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/tests/test_client.py b/novaclient/tests/test_client.py index 3d7743166..362901e21 100644 --- a/novaclient/tests/test_client.py +++ b/novaclient/tests/test_client.py @@ -161,7 +161,7 @@ def test_client_get_reset_timings_v3(self): self.assertEqual(["somevalue"], cs.get_timings()) cs.reset_timings() - self.assertEquals(0, len(cs.get_timings())) + self.assertEqual(0, len(cs.get_timings())) def test_clent_extensions_v3(self): fake_attribute_name1 = "FakeAttribute1" From 0e3278d3a8a08c0090af4c0d5ed5c989b05f7ce8 Mon Sep 17 00:00:00 2001 From: Kui Shi Date: Tue, 1 Oct 2013 05:15:55 +0800 Subject: [PATCH 0289/1705] py33: 'str' does not support the buffer interface Binary I/O expects and produces bytes objects. If sys.stdout is assigned to io.BytesIO, print() output it stdout by default, then there will be "TypeError". For this case, mock.patch() decorator can not work well casually. So this patch just update the six.StringIO() for stdout/stderr. Close-Bug #1229163 Change-Id: Ieb29ad0774c9b7f2b691628d753b210640baca8d --- novaclient/tests/test_shell.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/novaclient/tests/test_shell.py b/novaclient/tests/test_shell.py index ae9e6283f..c873966b1 100644 --- a/novaclient/tests/test_shell.py +++ b/novaclient/tests/test_shell.py @@ -1,6 +1,6 @@ -import io import prettytable import re +import six import sys from distutils.version import StrictVersion @@ -43,8 +43,8 @@ def shell(self, argstr, exitcodes=(0,)): orig = sys.stdout orig_stderr = sys.stderr try: - sys.stdout = io.BytesIO() - sys.stderr = io.BytesIO() + sys.stdout = six.StringIO() + sys.stderr = six.StringIO() _shell = novaclient.shell.OpenStackComputeShell() _shell.main(argstr.split()) except SystemExit: From f9ca39f94f15f4084110563f97f2be8dcfe642e6 Mon Sep 17 00:00:00 2001 From: OpenStack Jenkins Date: Tue, 1 Oct 2013 16:15:02 +0000 Subject: [PATCH 0290/1705] Updated from global requirements Change-Id: I32fdd184ff2c38e5bcf2ade1a79d6d9278e72ba1 --- test-requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test-requirements.txt b/test-requirements.txt index 07409e8e2..1d440816c 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -2,9 +2,9 @@ hacking>=0.5.6,<0.8 coverage>=3.6 discover -fixtures>=0.3.12 -keyring>=1.6.1 -mock>=0.8.0 +fixtures>=0.3.14 +keyring>=1.6.1,<2.0 +mock>=1.0 sphinx>=1.1.2 testrepository>=0.0.17 testtools>=0.9.32 From d21dde05880a30e86cc82b394e171f240640696a Mon Sep 17 00:00:00 2001 From: Kui Shi Date: Wed, 2 Oct 2013 03:38:39 +0800 Subject: [PATCH 0291/1705] py33: iteration order of dict is unpredictable In Python 3.3, hash randomization is enabled by default. It causes the iteration order of dicts and sets to be unpredictable and differ across Python runs. Sort the metadata.keys() in reverse order to keep the test cases as is. Close-Bug #1233405 Change-Id: I6251f18c9bc3a7842be9efeb5131c82919020e75 --- novaclient/v1_1/shell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index a417025e3..b8aef06f8 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1422,7 +1422,7 @@ def do_meta(cs, args): if args.action == 'set': cs.servers.set_meta(server, metadata) elif args.action == 'delete': - cs.servers.delete_meta(server, metadata.keys()) + cs.servers.delete_meta(server, sorted(metadata.keys(), reverse=True)) def _print_server(cs, args): From f7462f14a49790c99c6d4afb62120e62cb6998c1 Mon Sep 17 00:00:00 2001 From: Kui Shi Date: Wed, 2 Oct 2013 07:06:30 +0800 Subject: [PATCH 0292/1705] py33: sort dict for test_add_floating_ip_to_fixed In Python 3, hash randomization is enabled by default. It causes the iteration order of dicts and sets to be unpredictable and differ across Python runs. Sort the list and update the expecting result. Close-Bug #1233406 Change-Id: Ib747824dad340534d484797afb87b5e92200e426 --- novaclient/tests/v1_1/fakes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index 8f48ac4f5..86d6c5d8c 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -544,8 +544,8 @@ def post_servers_1234_action(self, body, **kw): assert list(body[action]) == ['address'] elif action == 'addFloatingIp': assert (list(body[action]) == ['address'] or - list(body[action]) == ['fixed_address', - 'address']) + sorted(list(body[action])) == ['address', + 'fixed_address']) elif action == 'removeFloatingIp': assert list(body[action]) == ['address'] elif action == 'createImage': From 2c32e71720c287ddd8ec3fbf624cfcaeb688f81c Mon Sep 17 00:00:00 2001 From: Kui Shi Date: Thu, 3 Oct 2013 06:53:53 +0800 Subject: [PATCH 0293/1705] py33: align the order of parameters for urlencode() In Python 3.3, hash randomization is enabled by default. It causes the iteration order of dicts and sets to be unpredictable and differ across Python runs. In the test case, the fixed expecting string will not match the test result, it is relying on the dict order. This change transforms the input dict to a sequence of two-element list, with fixed order, and update the related expecitng string in test case. Close-Bug #1234438 Change-Id: Ic7846279a9f508a856cd4ee70408d537088792f2 --- novaclient/tests/v1_1/test_servers.py | 2 +- novaclient/v1_1/servers.py | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/novaclient/tests/v1_1/test_servers.py b/novaclient/tests/v1_1/test_servers.py index 76d3e3cd8..08e86f5b2 100644 --- a/novaclient/tests/v1_1/test_servers.py +++ b/novaclient/tests/v1_1/test_servers.py @@ -26,7 +26,7 @@ def test_list_servers_undetailed(self): def test_list_servers_with_marker_limit(self): sl = cs.servers.list(marker=1234, limit=2) - cs.assert_called('GET', '/servers/detail?marker=1234&limit=2') + cs.assert_called('GET', '/servers/detail?limit=2&marker=1234') for s in sl: self.assertTrue(isinstance(s, servers.Server)) diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index b47ece808..a64e955b9 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -414,7 +414,13 @@ def list(self, detailed=True, search_opts=None, marker=None, limit=None): if limit: qparams['limit'] = limit - query_string = "?%s" % urlutils.urlencode(qparams) if qparams else "" + # Transform the dict to a sequence of two-element tuples in fixed + # order, then the encoded string will be consistent in Python 2&3. + if qparams: + new_qparams = sorted(qparams.items(), key=lambda x: x[0]) + query_string = "?%s" % urlutils.urlencode(new_qparams) + else: + query_string = "" detail = "" if detailed: From e5a193ac43fbae82dc01703df1e634272363fedb Mon Sep 17 00:00:00 2001 From: Kui Shi Date: Thu, 3 Oct 2013 21:32:22 +0800 Subject: [PATCH 0294/1705] py33: unify the input of request to json format In novaclient.client.HTTPClient.request, json.dumps() process the body of "POST": kwargs['data'] = json.dumps(kwargs['body']) So, in test case, we should pass the same forat for mock.call(). Also, transform the "data" variable to json format. Close-Bug #1234067 Change-Id: I3a0abaa8064b4360cb94b7b205454e26bddef8e4 --- novaclient/tests/test_client.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/novaclient/tests/test_client.py b/novaclient/tests/test_client.py index 362901e21..45c05e459 100644 --- a/novaclient/tests/test_client.py +++ b/novaclient/tests/test_client.py @@ -24,6 +24,8 @@ import novaclient.v3.client from novaclient.tests import utils +import json + class ClientTest(utils.TestCase): @@ -71,8 +73,16 @@ def test_client_reauth(self): reauth_headers = {'Content-Type': 'application/json', 'Accept': 'application/json', 'User-Agent': 'python-novaclient'} - data = ('{"auth": {"tenantName": "project", "passwordCredentials":' - ' {"username": "user", "password": "password"}}}') + data = { + "auth": { + "tenantName": "project", + "passwordCredentials": { + "username": "user", + "password": "password" + } + } + } + expected = [mock.call('GET', 'http://example.com/servers/detail', timeout=mock.ANY, @@ -82,7 +92,7 @@ def test_client_reauth(self): timeout=mock.ANY, headers=reauth_headers, allow_redirects=mock.ANY, - data=data, + data=json.dumps(data), verify=mock.ANY)] self.assertEqual(mock_request.call_args_list, expected) From ceb6cc5480fc90d7f1c2c31e659e18517374b2ea Mon Sep 17 00:00:00 2001 From: Kui Shi Date: Fri, 4 Oct 2013 04:30:53 +0800 Subject: [PATCH 0295/1705] py33: sort hosts while treeize AvailabilityZone In Python 3.3, hash randomization is enabled by default. It causes the iteration order of dicts and sets to be unpredictable and differ across Python runs. sort the hosts while treeize AvailabiltyZone. Test cmd -------- for i in {1..100}; do echo $i; \ tox -e py33 |grep test_detail_availability_zone; done Change-Id: I9ab25ef52d6d19b45a39f04cbcde864ee225b4cc Close-Bug: #1235067 --- novaclient/v1_1/shell.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 4557d1fe3..97d0bf913 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -3410,7 +3410,8 @@ def _treeizeAvailabilityZone(zone): result.append(az) if zone.hosts is not None: - for (host, services) in zone.hosts.items(): + zone_hosts = sorted(zone.hosts.items(), key=lambda x: x[0]) + for (host, services) in zone_hosts: # Host tree view item az = AvailabilityZone(zone.manager, copy.deepcopy(zone._info), zone._loaded) From f14273d1e07a6a7ffe49044d894a81cda48ed8ad Mon Sep 17 00:00:00 2001 From: Kui Shi Date: Fri, 4 Oct 2013 08:08:44 +0800 Subject: [PATCH 0296/1705] py33: sort the files parameters of "--files" In Python 3.3, hash randomization is enabled by default. It causes the iteration order of dicts and sets to be unpredictable and differ across Python runs. Sort the files parameters when multiple files are designated. Close-Bug #1235096 Change-Id: I0db645b3fb92408bd8c6780bfabe7ce8265a65a8 --- novaclient/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/novaclient/base.py b/novaclient/base.py index a5dad823f..d270b316e 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -353,7 +353,8 @@ def _boot(self, resource_url, response_key, name, image, flavor, # either an open file *or* some contents as files here. if files: personality = body['server']['personality'] = [] - for filepath, file_or_string in files.items(): + for filepath, file_or_string in sorted(files.items(), + key=lambda x: x[0]): if hasattr(file_or_string, 'read'): data = file_or_string.read() else: From 5f0363ad9d20f7d9df330afd9b9fea9f4bf455ae Mon Sep 17 00:00:00 2001 From: Kui Shi Date: Fri, 4 Oct 2013 17:44:04 +0800 Subject: [PATCH 0297/1705] py33: use six.StringIO() to mock stdout/stderr six.StringIO() is the right way to keep Python2/3 compatibiliby while mocking stdout/stderr. Close-Bug #1235086 Change-Id: I33d9223b2202896331d24339056d57d4ca9acdc5 --- novaclient/tests/v1_1/test_shell.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index 9dda45dc6..430eb99c5 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -21,7 +21,6 @@ import os import mock import sys -import tempfile import fixtures import six @@ -646,7 +645,7 @@ def test_image_meta_del(self): self.assert_called('DELETE', '/images/1/metadata/test_key') def test_image_meta_bad_action(self): - tmp = tempfile.TemporaryFile() + tmp = six.StringIO() # Suppress stdout and stderr (stdout, stderr) = (sys.stdout, sys.stderr) From b1e3753fdfea638ec8c82cddd910a0b3e0f627a6 Mon Sep 17 00:00:00 2001 From: Kui Shi Date: Tue, 8 Oct 2013 16:57:43 +0800 Subject: [PATCH 0298/1705] Align mocking pattern for test case Mock sys.stderr/sys.stdout with mock.patch() In novaclient/tests/test_shell.py, ShellTest.shell(), it is another case that should be aligned. But it may induce test error if changed. So, let's keep it as is. Close-Bug #1237093 Change-Id: I8908dc1593dbbbaf2813cc8fb034be32972140a0 --- novaclient/tests/v1_1/test_shell.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index 430eb99c5..8a1ade65c 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -644,19 +644,12 @@ def test_image_meta_del(self): self.run_command('image-meta 1 delete test_key=test_value') self.assert_called('DELETE', '/images/1/metadata/test_key') + @mock.patch('sys.stdout', six.StringIO()) + @mock.patch('sys.stderr', six.StringIO()) def test_image_meta_bad_action(self): - tmp = six.StringIO() - - # Suppress stdout and stderr - (stdout, stderr) = (sys.stdout, sys.stderr) - (sys.stdout, sys.stderr) = (tmp, tmp) - self.assertRaises(SystemExit, self.run_command, 'image-meta 1 BAD_ACTION test_key=test_value') - # Put stdout and stderr back - sys.stdout, sys.stderr = (stdout, stderr) - def test_image_list(self): self.run_command('image-list') self.assert_called('GET', '/images/detail') From cbed4887c9cd6c798da29719d66af0653cc072e8 Mon Sep 17 00:00:00 2001 From: Joe Gordon Date: Thu, 10 Oct 2013 06:01:26 +0000 Subject: [PATCH 0299/1705] Revert "Add-in some re-auth logic when using os_cache" This patch didn't do what it was supposed to do, make os_cache work. Instead it just adds complexity, so just remove it. This reverts commit 8eed73ce20ff0808d350fc9af66d6ba55ebda017. Change-Id: I7c118cbf8d8e2218e198d603f1984a9de18266c4 --- novaclient/client.py | 30 ++------- novaclient/tests/v1_1/test_auth.py | 97 +----------------------------- 2 files changed, 7 insertions(+), 120 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index ad594dd32..123a3e9bf 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -395,38 +395,20 @@ def _plugin_auth(self, auth_url): def _v2_auth(self, url): """Authenticate against a v2.0 auth service.""" - body_pass = {"auth": { - "passwordCredentials": {"username": self.user, - "password": self.password}}} - body_token = {"auth": {"token": {"id": self.auth_token}}} - if self.auth_token: - body = body_token + body = {"auth": { + "token": {"id": self.auth_token}}} else: - body = body_pass + body = {"auth": { + "passwordCredentials": {"username": self.user, + "password": self.password}}} if self.tenant_id: body['auth']['tenantId'] = self.tenant_id elif self.projectid: body['auth']['tenantName'] = self.projectid - try: - return self._authenticate(url, body) - except exceptions.Unauthorized: - # NOTE(morganfainberg): there is no actual point in flushing - # the cache out because it would result in the same behavior - # in either case, a 401 being raised/returned. The expected - # recourse in the case of a failure will be the same, reauth - # with username and password. - - if (self.os_cache and self.user and self.password and - self.keyring_saver is not None): - # If we are using a cache, and we failed, try again if we have - # the required information to do so. - self.auth_token = None - body = body_pass - return self._authenticate(url, body) - raise + return self._authenticate(url, body) def _authenticate(self, url, body, **kwargs): """Authenticate and extract the service catalog.""" diff --git a/novaclient/tests/v1_1/test_auth.py b/novaclient/tests/v1_1/test_auth.py index fa802b812..f3bd279a9 100644 --- a/novaclient/tests/v1_1/test_auth.py +++ b/novaclient/tests/v1_1/test_auth.py @@ -9,14 +9,6 @@ from novaclient.tests import utils -class MockKeyringSaver(object): - def __init__(self): - self.saved = [] - - def save(self, *args): - self.saved = args - - class AuthenticateAgainstKeystoneTests(utils.TestCase): def test_authenticate_success(self): cs = client.Client("username", "password", "project_id", @@ -393,8 +385,7 @@ def test_authenticate_with_token_success(self): self.assertEqual(cs.client.auth_token, token_id) def test_authenticate_with_token_failure(self): - cs = client.Client("username", None, "project_id", "auth_url/v2.0", - service_type='compute') + cs = client.Client("username", None, "project_id", "auth_url/v2.0") cs.client.auth_token = "FAKE_ID" resp = {"unauthorized": {"message": "Unauthorized", "code": "401"}} auth_response = utils.TestResponse({ @@ -407,92 +398,6 @@ def test_authenticate_with_token_failure(self): with mock.patch.object(requests.Session, "request", mock_request): self.assertRaises(exceptions.Unauthorized, cs.client.authenticate) - def test_authenticate_with_os_cache_and_failure(self): - cs = client.Client("username", "password", "project_id", - "auth_url/v2.0", service_type='compute') - cs.client.os_cache = True - cs.client.keyring_saver = MockKeyringSaver() - cs.client.auth_token = "FAKE_ID" - - catalog_managment_url = u"http://localhost:8774/v1.1" - - headers_get = { - 'User-Agent': cs.client.USER_AGENT, - 'Content-Type': 'application/json', - 'Accept': 'application/json', - 'X-Auth-Token': "FAKE_ID"} - headers_post = { - 'User-Agent': cs.client.USER_AGENT, - 'Content-Type': 'application/json', - 'Accept': 'application/json'} - token_url_get = cs.client.auth_url + "/tokens/FAKE_ID" - token_url_post = cs.client.auth_url + "/tokens" - resp = { - "access": { - "token": { - "expires": "12345", - "id": "FAKE_ID_NEW", - "tenant": { - "id": "FAKE_TENANT_ID", - } - }, - "serviceCatalog": [ - { - "type": "compute", - "endpoints": [ - { - "region": "RegionOne", - "adminURL": catalog_managment_url, - "internalURL": catalog_managment_url, - "publicURL": catalog_managment_url, - }, - ], - }, - ], - }, - } - - auth_resp_200 = utils.TestResponse({ - "status_code": 200, - "text": json.dumps(resp), - }) - auth_resp_401 = utils.TestResponse({ - "status_code": 401, - "text": json.dumps({"unauthorized": - {"message": "Unauthorized", - "code": "401"}}), - }) - - expected_keyring = (u"FAKE_ID_NEW", - catalog_managment_url, - u"FAKE_TENANT_ID") - - post_data = {"auth": - {"passwordCredentials": - {"username": "username", - "password": "password"}}} - - mock_request = mock.Mock() - mock_request.side_effect = [auth_resp_401, auth_resp_200] - - with mock.patch.object(requests.Session, "request", mock_request): - expected = [mock.call("GET", - token_url_get, - headers=headers_get, - data="null", - allow_redirects=True, - **self.TEST_REQUEST_BASE), - mock.call("POST", - token_url_post, - headers=headers_post, - data=json.dumps(post_data), - allow_redirects=True, - **self.TEST_REQUEST_BASE), - ] - cs.client.authenticate() - self.assertEqual(expected, mock_request.call_args_list) - self.assertEqual(cs.client.keyring_saver.saved, expected_keyring) - class AuthenticationTests(utils.TestCase): def test_authenticate_success(self): From 18ad5270f4d4b56f3ef9473f7829a7a0311c29bc Mon Sep 17 00:00:00 2001 From: Joe Gordon Date: Thu, 10 Oct 2013 08:17:40 +0000 Subject: [PATCH 0300/1705] Document and make OS_CACHE work Before the --os-no-cache option logic would overwrite any value set in env['OS_CACHE']. Since os-no-cache is deprecated, and also the default value, just remove it. Also update the help message for --os-cache to mention the environment variable. Change-Id: Ie2569d5cdf0f12f3a29e9a6f20faef75956d2209 --- novaclient/shell.py | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/novaclient/shell.py b/novaclient/shell.py index 8e7860879..3f2609614 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -243,21 +243,11 @@ def get_base_parser(self): action='store_true', help="Print debugging output") - parser.add_argument('--no-cache', - default=not utils.bool_from_str( - utils.env('OS_NO_CACHE', default='true')), - action='store_false', - dest='os_cache', - help=argparse.SUPPRESS) - parser.add_argument('--no_cache', - action='store_false', - dest='os_cache', - help=argparse.SUPPRESS) - parser.add_argument('--os-cache', - default=utils.env('OS_CACHE', default=False), + default=utils.bool_from_str(utils.env('OS_CACHE', default=False)), action='store_true', - help="Use the auth token cache.") + help="Use the auth token cache. Defaults to False if env[OS_CACHE]" + " is not set.") parser.add_argument('--timings', default=False, From 6f9d9df7df4deef00e239b7a6824c1845cb73ccc Mon Sep 17 00:00:00 2001 From: Joe Gordon Date: Thu, 10 Oct 2013 08:21:48 +0000 Subject: [PATCH 0301/1705] Make os-cache retry on an invalid token client.authenticate doesn't actually do any rest calls if a token is present, it just assumes the token is valid. Instead re-authentication is done if the request raises an Unauthorized exception, at which point we want to unauthenticate (delete the token from in-memory) and overwrite the old token in the keyring. Also if a password is present allways pass it into the client so re-authentication is possible during _cs_request. Change-Id: I86d0356d5685ffa802f1bdc97e33727ff2dd075e --- novaclient/client.py | 4 +++- novaclient/shell.py | 12 +++--------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index 123a3e9bf..8b2928f7d 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -214,9 +214,11 @@ def _cs_request(self, url, method, **kwargs): return resp, body except exceptions.Unauthorized as e: try: - # frist discard auth token, to avoid the possibly expired + # first discard auth token, to avoid the possibly expired # token being re-used in the re-authentication attempt self.unauthenticate() + # overwrite bad token + self.keyring_saved = False self.authenticate() kwargs['headers']['X-Auth-Token'] = self.auth_token resp, body = self._time_request(self.management_url + url, diff --git a/novaclient/shell.py b/novaclient/shell.py index 3f2609614..e70c1dc1c 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -629,15 +629,9 @@ def main(self, argv): self.cs.client.tenant_id = tenant_id self.cs.client.auth_token = auth_token self.cs.client.management_url = management_url - # Try to auth with the given info, if it fails - # go into password mode... - try: - self.cs.authenticate() - use_pw = False - except (exc.Unauthorized, exc.AuthorizationFailure): - # Likely it expired or just didn't work... - self.cs.client.auth_token = None - self.cs.client.management_url = None + # authenticate just sets up some values in this case, no REST + # calls + self.cs.authenticate() if use_pw: # Auth using token must have failed or not happened # at all, so now switch to password mode and save From 34887aa0d61e6ee4bab3280d06da232bf6dcc3d5 Mon Sep 17 00:00:00 2001 From: Russell Sim Date: Mon, 14 Oct 2013 15:27:01 +1100 Subject: [PATCH 0302/1705] Print dicts in alphabetical order Instead of iterating over dicts, print them in alphabetical order. Change-Id: I630c28ea36266037b1ab4098831f4f960e6c5dc5 Closes-Bug: #1239511 --- novaclient/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/utils.py b/novaclient/utils.py index 68b265b1b..2020a56f3 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -180,7 +180,7 @@ def print_list(objs, fields, formatters={}, sortby_index=None): def print_dict(d, dict_property="Property", dict_value="Value", wrap=0): pt = prettytable.PrettyTable([dict_property, dict_value], caching=False) pt.align = 'l' - for k, v in six.iteritems(d): + for k, v in sorted(d.items()): # convert dict to str to check length if isinstance(v, dict): v = str(v) From 3c5cabf94d9ce10d52db715550a5d73f94195729 Mon Sep 17 00:00:00 2001 From: Joe Gordon Date: Mon, 14 Oct 2013 15:08:00 -0700 Subject: [PATCH 0303/1705] Remove deprecated NOVA_RAX_AUTH NOVA_RAX_AUTH has been deprecated in favor of a generic pluggable auth, which in this case rackspace users can set OS_AUTH_SYSTEM=rackspace Fixes bug 966329 Change-Id: Ie55fc36a0ac1cba61efa8bdd2f20809530d3365f --- novaclient/client.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index ad594dd32..d1cc3734d 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -8,7 +8,6 @@ """ import logging -import os import time import requests @@ -315,13 +314,6 @@ def authenticate(self): admin_url = urlutils.urlunsplit( (scheme, new_netloc, path, query, frag)) - # FIXME(chmouel): This is to handle backward compatibiliy when - # we didn't have a plugin mechanism for the auth_system. This - # should be removed in the future and have people move to - # OS_AUTH_SYSTEM=rackspace instead. - if "NOVA_RAX_AUTH" in os.environ: - self.auth_system = "rackspace" - auth_url = self.auth_url if self.version == "v2.0": # FIXME(chris): This should be better. while auth_url: From 516586a6b8f48a912d9b3d090f2d0a95a267feb2 Mon Sep 17 00:00:00 2001 From: OpenStack Jenkins Date: Wed, 16 Oct 2013 12:21:34 +0000 Subject: [PATCH 0304/1705] Updated from global requirements Change-Id: I2cc2769b89bd040064408faea30c448350fa5f00 --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index c4eb2a088..a13c0124a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,5 +4,5 @@ iso8601>=0.1.4 PrettyTable>=0.6,<0.8 requests>=1.1 simplejson>=2.0.9 -six -Babel>=0.9.6 +six>=1.4.1 +Babel>=1.3 From 362cb18cb4b31da0c4a48ee47318e53a3d9f3d38 Mon Sep 17 00:00:00 2001 From: fujioka yuuichi Date: Thu, 17 Oct 2013 09:54:54 +0900 Subject: [PATCH 0305/1705] Apply six for metaclass The way to using metaclass has changed in Python3. Python 2.7 way: class Foo(object): __metaclass__ = FooMeta Python 3 way: class Foo(object, metaclass=FooMeta): ... The six.add_metaclass() decorator allows us to use one syntax that works for both Python 2.7 and Python 3. Change-Id: Ibd31a9f91bac6ec220d802f16c77ee0306e211b3 Closes-Bug: #1236648 --- novaclient/base.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/novaclient/base.py b/novaclient/base.py index d270b316e..b506286a9 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -163,13 +163,12 @@ def _update(self, url, body, response_key=None, **kwargs): return self.resource_class(self, body) +@six.add_metaclass(abc.ABCMeta) class ManagerWithFind(Manager): """ Like a `Manager`, but with additional `find()`/`findall()` methods. """ - __metaclass__ = abc.ABCMeta - @abc.abstractmethod def list(self): pass From c474cd7cec87ca0566deafd8ce33a8409a1ce976 Mon Sep 17 00:00:00 2001 From: Kieran Spear Date: Thu, 24 Oct 2013 14:52:34 +1100 Subject: [PATCH 0306/1705] Add --insecure to curl output if required Change-Id: I5762f6659a6687b86c94a6e371ccc7509d1e26f9 --- novaclient/client.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/novaclient/client.py b/novaclient/client.py index 74bb4f837..2c72d3c78 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -123,6 +123,10 @@ def http_log_req(self, args, kwargs): return string_parts = ['curl -i'] + + if not kwargs.get('verify', True): + string_parts.append(' --insecure') + for element in args: if element in ('GET', 'POST', 'DELETE', 'PUT'): string_parts.append(' -X %s' % element) @@ -156,12 +160,12 @@ def request(self, url, method, **kwargs): del kwargs['body'] if self.timeout is not None: kwargs.setdefault('timeout', self.timeout) + kwargs['verify'] = self.verify_cert self.http_log_req((url, method,), kwargs) resp = self.http.request( method, url, - verify=self.verify_cert, **kwargs) self.http_log_resp(resp) From bb214bed326c679de3be06c41d18c0ba832a89cf Mon Sep 17 00:00:00 2001 From: Kieran Spear Date: Thu, 24 Oct 2013 14:56:20 +1100 Subject: [PATCH 0307/1705] Quote URL in curl output to handle query params Copy and pasting the generated curl output into a shell doesn't work when the URL includes an ampersand, as the URL isn't quoted. This simplifies the logic in client.http_log_req to be able to output the URL and method separately and quote the URL. Change-Id: I1e497886ea9334771ab91e657e7c1121d89f7f33 --- novaclient/client.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index 2c72d3c78..652da571c 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -118,7 +118,7 @@ def get_timings(self): def reset_timings(self): self.times = [] - def http_log_req(self, args, kwargs): + def http_log_req(self, method, url, kwargs): if not self.http_log_debug: return @@ -127,11 +127,8 @@ def http_log_req(self, args, kwargs): if not kwargs.get('verify', True): string_parts.append(' --insecure') - for element in args: - if element in ('GET', 'POST', 'DELETE', 'PUT'): - string_parts.append(' -X %s' % element) - else: - string_parts.append(' %s' % element) + string_parts.append(" '%s'" % url) + string_parts.append(' -X %s' % method) for element in kwargs['headers']: header = ' -H "%s: %s"' % (element, kwargs['headers'][element]) @@ -162,7 +159,7 @@ def request(self, url, method, **kwargs): kwargs.setdefault('timeout', self.timeout) kwargs['verify'] = self.verify_cert - self.http_log_req((url, method,), kwargs) + self.http_log_req(method, url, kwargs) resp = self.http.request( method, url, From 943a3eba2d123d9ee8daa93ae9c3cde308bc3c3b Mon Sep 17 00:00:00 2001 From: Joe Gordon Date: Thu, 19 Sep 2013 19:05:46 +0000 Subject: [PATCH 0308/1705] Make 'nova ssh' automatically fall back to private address If no public IP address is available automatically revert to the private address. Change-Id: Iaf780fde77c4a1eae99c27be7e21841ca87d69dc --- novaclient/v1_1/shell.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index fbe70db7b..998d681b5 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -3002,8 +3002,9 @@ def do_credentials(cs, _args): dest='private', action='store_true', default=False, - help='Optional flag to indicate whether to use private address ' - 'attached to a server. (Default=False)') + help='Optional flag to indicate whether to only use private address ' + 'attached to an instance. (Default=False). If no public address is ' + 'found try private address') @utils.arg('--ipv6', dest='ipv6', action='store_true', @@ -3025,6 +3026,10 @@ def do_ssh(cs, args): address_type = "private" if args.private else "public" version = 6 if args.ipv6 else 4 + if (address_type == "public" and address_type not in addresses and + "private" in addresses): + address_type = "private" + if address_type not in addresses: print("ERROR: No %s addresses found for '%s'." % (address_type, args.server)) From 6374ee04a378b8a970f9e429d66eb37c969bbf7b Mon Sep 17 00:00:00 2001 From: Sahid Orentino Ferdjaoui Date: Tue, 8 Oct 2013 15:29:41 +0000 Subject: [PATCH 0309/1705] Nova aggregate-details should be more human friendly 'nova aggregate-details' shows Hosts, Availability Zone and Metadata in the python unicode notation. Change-Id: If0028347146439994265c60c429af05dd67912f9 Closes-Bug: #1132961 --- novaclient/tests/test_utils.py | 20 ++++++++++++++++++++ novaclient/utils.py | 5 +++++ novaclient/v1_1/shell.py | 12 +++++++++++- novaclient/v3/shell.py | 11 ++++++++++- 4 files changed, 46 insertions(+), 2 deletions(-) diff --git a/novaclient/tests/test_utils.py b/novaclient/tests/test_utils.py index 8809133ac..0fff7ea55 100644 --- a/novaclient/tests/test_utils.py +++ b/novaclient/tests/test_utils.py @@ -163,3 +163,23 @@ def test_print_list_sort_by_none(self): '| k3 | 3 |\n' '| k2 | 2 |\n' '+------+-------+\n') + + def test_pretty_choice_list(self): + l = [] + r = utils.pretty_choice_list(l) + self.assertEqual(r, "") + + l = ["v1", "v2", "v3"] + r = utils.pretty_choice_list(l) + self.assertEqual(r, "'v1', 'v2', 'v3'") + + def test_pretty_choice_dict(self): + d = {} + r = utils.pretty_choice_dict(d) + self.assertEqual(r, "") + + d = {"k1": "v1", + "k2": "v2", + "k3": "v3"} + r = utils.pretty_choice_dict(d) + self.assertEqual(r, "'k1=v1', 'k2=v2', 'k3=v3'") diff --git a/novaclient/utils.py b/novaclient/utils.py index 2020a56f3..cb96992f7 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -143,6 +143,11 @@ def pretty_choice_list(l): return ', '.join("'%s'" % i for i in l) +def pretty_choice_dict(d): + """Returns a formatted dict as 'key=value'.""" + return pretty_choice_list(['%s=%s' % (k, d[k]) for k in sorted(d.keys())]) + + def print_list(objs, fields, formatters={}, sortby_index=None): if sortby_index is None: sortby = None diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index fbe70db7b..4c17f871f 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2687,7 +2687,17 @@ def do_aggregate_details(cs, args): def _print_aggregate_details(aggregate): columns = ['Id', 'Name', 'Availability Zone', 'Hosts', 'Metadata'] - utils.print_list([aggregate], columns) + + parser_metadata = lambda o: utils.pretty_choice_dict( + getattr(o, 'metadata', {}) or {}) + parser_hosts = lambda o: utils.pretty_choice_list( + getattr(o, 'hosts', [])) + + formatters = { + 'Metadata': parser_metadata, + 'Hosts': parser_hosts, + } + utils.print_list([aggregate], columns, formatters=formatters) @utils.arg('server', metavar='', help='Name or ID of server.') diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 3044e6d9f..55724c2bb 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -2517,7 +2517,16 @@ def do_aggregate_details(cs, args): def _print_aggregate_details(aggregate): columns = ['Id', 'Name', 'Availability Zone', 'Hosts', 'Metadata'] - utils.print_list([aggregate], columns) + parser_metadata = lambda o: utils.pretty_choice_dict( + getattr(o, 'metadata', {}) or {}) + parser_hosts = lambda o: utils.pretty_choice_list( + getattr(o, 'hosts', [])) + + formatters = { + 'Metadata': parser_metadata, + 'Hosts': parser_hosts, + } + utils.print_list([aggregate], columns, formatters=formatters) @utils.arg('server', metavar='', help='Name or ID of server.') From c32056287704e4ce6e616bbbc37b7be98c44b218 Mon Sep 17 00:00:00 2001 From: Ryan Wilson Date: Tue, 15 Oct 2013 06:09:43 -0700 Subject: [PATCH 0310/1705] Adds locking to completion caches Previously, completion caches were not thread-safe and multiple threads could not make calls to python-novaclient simultaneously. With locking, the completion caches are now thread-safe. Closes-Bug: #1213958 Change-Id: I201c2ed0f1eb09b1c2a74de96119cbae9ed6f4b0 --- novaclient/base.py | 86 +++++++++++++++++++++++++--------------------- 1 file changed, 46 insertions(+), 40 deletions(-) diff --git a/novaclient/base.py b/novaclient/base.py index b506286a9..45bea18d8 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -25,6 +25,7 @@ import hashlib import inspect import os +import threading import six @@ -50,6 +51,7 @@ class Manager(utils.HookableMixin): etc.) and provide CRUD operations for them. """ resource_class = None + cache_lock = threading.RLock() def __init__(self, api): self.api = api @@ -90,46 +92,50 @@ def completion_cache(self, cache_type, obj_class, mode): Delete is not handled because listings are assumed to be performed often enough to keep the cache reasonably up-to-date. """ - base_dir = utils.env('NOVACLIENT_UUID_CACHE_DIR', - default="~/.novaclient") - - # NOTE(sirp): Keep separate UUID caches for each username + endpoint - # pair - username = utils.env('OS_USERNAME', 'NOVA_USERNAME') - url = utils.env('OS_URL', 'NOVA_URL') - uniqifier = hashlib.md5(username.encode('utf-8') + - url.encode('utf-8')).hexdigest() - - cache_dir = os.path.expanduser(os.path.join(base_dir, uniqifier)) - - try: - os.makedirs(cache_dir, 0o755) - except OSError: - # NOTE(kiall): This is typicaly either permission denied while - # attempting to create the directory, or the directory - # already exists. Either way, don't fail. - pass - - resource = obj_class.__name__.lower() - filename = "%s-%s-cache" % (resource, cache_type.replace('_', '-')) - path = os.path.join(cache_dir, filename) - - cache_attr = "_%s_cache" % cache_type - - try: - setattr(self, cache_attr, open(path, mode)) - except IOError: - # NOTE(kiall): This is typicaly a permission denied while - # attempting to write the cache file. - pass - - try: - yield - finally: - cache = getattr(self, cache_attr, None) - if cache: - cache.close() - delattr(self, cache_attr) + # NOTE(wryan): This lock protects read and write access to the + # completion caches + with self.cache_lock: + base_dir = utils.env('NOVACLIENT_UUID_CACHE_DIR', + default="~/.novaclient") + + # NOTE(sirp): Keep separate UUID caches for each username + + # endpoint pair + username = utils.env('OS_USERNAME', 'NOVA_USERNAME') + url = utils.env('OS_URL', 'NOVA_URL') + uniqifier = hashlib.md5(username.encode('utf-8') + + url.encode('utf-8')).hexdigest() + + cache_dir = os.path.expanduser(os.path.join(base_dir, uniqifier)) + + try: + os.makedirs(cache_dir, 0o755) + except OSError: + # NOTE(kiall): This is typicaly either permission denied while + # attempting to create the directory, or the + # directory already exists. Either way, don't + # fail. + pass + + resource = obj_class.__name__.lower() + filename = "%s-%s-cache" % (resource, cache_type.replace('_', '-')) + path = os.path.join(cache_dir, filename) + + cache_attr = "_%s_cache" % cache_type + + try: + setattr(self, cache_attr, open(path, mode)) + except IOError: + # NOTE(kiall): This is typicaly a permission denied while + # attempting to write the cache file. + pass + + try: + yield + finally: + cache = getattr(self, cache_attr, None) + if cache: + cache.close() + delattr(self, cache_attr) def write_to_completion_cache(self, cache_type, val): cache = getattr(self, "_%s_cache" % cache_type, None) From b68517ce83031898b93d10ccb6cb72df8d390ab6 Mon Sep 17 00:00:00 2001 From: Russell Sim Date: Wed, 23 Oct 2013 14:56:42 +1100 Subject: [PATCH 0311/1705] Print security groups as a human readable list Convert the security group list into a list that is more suitable for printing. Change-Id: I411d9256145986fad7659a9377494e29a6d70ced --- novaclient/tests/v1_1/fakes.py | 10 ++++++++++ novaclient/tests/v1_1/test_shell.py | 15 +++++++++++---- novaclient/v1_1/shell.py | 4 ++++ 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index 36781cf8d..8a17c61cd 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -320,6 +320,16 @@ def get_servers_detail(self, **kw): "Server Label": "DB 1" }, "OS-EXT-SRV-ATTR:host": "computenode2", + "security_groups": [{ + 'id': 1, 'name': 'securitygroup1', + 'description': 'FAKE_SECURITY_GROUP', + 'tenant_id': '4ffc664c198e435e9853f2538fbcd7a7' + }, + { + 'id': 2, 'name': 'securitygroup2', + 'description': 'ANOTHER_FAKE_SECURITY_GROUP', + 'tenant_id': '4ffc664c198e435e9853f2538fbcd7a7' + }], }, { "id": 9012, diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index 8a1ade65c..bd2caf2a8 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -20,7 +20,6 @@ import datetime import os import mock -import sys import fixtures import six @@ -71,13 +70,13 @@ def setUp(self): lambda *_: fakes.FakeClient)) self.addCleanup(timeutils.clear_time_override) - @mock.patch('sys.stdout', six.StringIO()) - def run_command(self, cmd): + @mock.patch('sys.stdout', new_callable=six.StringIO) + def run_command(self, cmd, mock_stdout): if isinstance(cmd, list): self.shell.main(cmd) else: self.shell.main(cmd.split()) - return sys.stdout.getvalue() + return mock_stdout.getvalue() def assert_called(self, method, url, body=None, **kwargs): return self.shell.cs.assert_called(method, url, body, **kwargs) @@ -830,6 +829,14 @@ def test_show_bad_id(self): self.assertRaises(exceptions.CommandError, self.run_command, 'show xxx') + @mock.patch('novaclient.v1_1.shell.utils.print_dict') + def test_print_server(self, mock_print_dict): + self.run_command('show 5678') + args, kwargs = mock_print_dict.call_args + parsed_server = args[0] + self.assertEqual('securitygroup1, securitygroup2', + parsed_server['security_groups']) + def test_delete(self): self.run_command('delete 1234') self.assert_called('DELETE', '/servers/1234') diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index a50c045c2..316fefe05 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1465,6 +1465,10 @@ def _print_server(cs, args): info['flavor'] = '%s (%s)' % (_find_flavor(cs, flavor_id).name, flavor_id) + if 'security_groups' in info: + info['security_groups'] = \ + ', '.join(group['name'] for group in info['security_groups']) + image = info.get('image', {}) if image: image_id = image.get('id', '') From e2fe20b50dd1db9a0ffb9290d6cc290ee4e7f0d7 Mon Sep 17 00:00:00 2001 From: Joe Gordon Date: Mon, 28 Oct 2013 10:22:53 +0000 Subject: [PATCH 0312/1705] Update mailmap for Joe Gordon Update mailmap to reflect that I am committing from a new email address. Change-Id: Ibf03482768a639c4c2fe57fca045daaab0233688 --- .mailmap | 1 + 1 file changed, 1 insertion(+) diff --git a/.mailmap b/.mailmap index 4675a0bb7..2df56d0d8 100644 --- a/.mailmap +++ b/.mailmap @@ -4,6 +4,7 @@ Antony Messerli root Chris Behrens comstud +Joe Gordon Johannes Erdfelt jerdfelt From 1d2263dae339590b60250793bc81ec5776845060 Mon Sep 17 00:00:00 2001 From: Sean Dague Date: Mon, 28 Oct 2013 08:11:51 -0400 Subject: [PATCH 0313/1705] Revert "Nova aggregate-details should be more human friendly" This reverts commit 6374ee04a378b8a970f9e429d66eb37c969bbf7b. Change-Id: Ib27cc6722ea7cc8cb9b133360b3ecb246a0e4a14 --- novaclient/tests/test_utils.py | 20 -------------------- novaclient/utils.py | 5 ----- novaclient/v1_1/shell.py | 12 +----------- novaclient/v3/shell.py | 11 +---------- 4 files changed, 2 insertions(+), 46 deletions(-) diff --git a/novaclient/tests/test_utils.py b/novaclient/tests/test_utils.py index 0fff7ea55..8809133ac 100644 --- a/novaclient/tests/test_utils.py +++ b/novaclient/tests/test_utils.py @@ -163,23 +163,3 @@ def test_print_list_sort_by_none(self): '| k3 | 3 |\n' '| k2 | 2 |\n' '+------+-------+\n') - - def test_pretty_choice_list(self): - l = [] - r = utils.pretty_choice_list(l) - self.assertEqual(r, "") - - l = ["v1", "v2", "v3"] - r = utils.pretty_choice_list(l) - self.assertEqual(r, "'v1', 'v2', 'v3'") - - def test_pretty_choice_dict(self): - d = {} - r = utils.pretty_choice_dict(d) - self.assertEqual(r, "") - - d = {"k1": "v1", - "k2": "v2", - "k3": "v3"} - r = utils.pretty_choice_dict(d) - self.assertEqual(r, "'k1=v1', 'k2=v2', 'k3=v3'") diff --git a/novaclient/utils.py b/novaclient/utils.py index cb96992f7..2020a56f3 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -143,11 +143,6 @@ def pretty_choice_list(l): return ', '.join("'%s'" % i for i in l) -def pretty_choice_dict(d): - """Returns a formatted dict as 'key=value'.""" - return pretty_choice_list(['%s=%s' % (k, d[k]) for k in sorted(d.keys())]) - - def print_list(objs, fields, formatters={}, sortby_index=None): if sortby_index is None: sortby = None diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index a50c045c2..998d681b5 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2687,17 +2687,7 @@ def do_aggregate_details(cs, args): def _print_aggregate_details(aggregate): columns = ['Id', 'Name', 'Availability Zone', 'Hosts', 'Metadata'] - - parser_metadata = lambda o: utils.pretty_choice_dict( - getattr(o, 'metadata', {}) or {}) - parser_hosts = lambda o: utils.pretty_choice_list( - getattr(o, 'hosts', [])) - - formatters = { - 'Metadata': parser_metadata, - 'Hosts': parser_hosts, - } - utils.print_list([aggregate], columns, formatters=formatters) + utils.print_list([aggregate], columns) @utils.arg('server', metavar='', help='Name or ID of server.') diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 55724c2bb..3044e6d9f 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -2517,16 +2517,7 @@ def do_aggregate_details(cs, args): def _print_aggregate_details(aggregate): columns = ['Id', 'Name', 'Availability Zone', 'Hosts', 'Metadata'] - parser_metadata = lambda o: utils.pretty_choice_dict( - getattr(o, 'metadata', {}) or {}) - parser_hosts = lambda o: utils.pretty_choice_list( - getattr(o, 'hosts', [])) - - formatters = { - 'Metadata': parser_metadata, - 'Hosts': parser_hosts, - } - utils.print_list([aggregate], columns, formatters=formatters) + utils.print_list([aggregate], columns) @utils.arg('server', metavar='', help='Name or ID of server.') From a89159a6e36f7cf9cc678d74a3af6847d034aa6a Mon Sep 17 00:00:00 2001 From: Russell Sim Date: Wed, 23 Oct 2013 14:43:17 +1100 Subject: [PATCH 0314/1705] Flatten hypervisor-show dictionary for printing Flatten the nested dicts so that hypervisor information can be more easily used at the command line. Change-Id: I5dfcf9d10f917a63bde09f0b5a691784a5e8f02b Closes-bug: #1243512 --- novaclient/tests/test_utils.py | 21 +++++++++++++++++ novaclient/utils.py | 42 ++++++++++++++++++++++++++++++++++ novaclient/v1_1/shell.py | 9 +------- 3 files changed, 64 insertions(+), 8 deletions(-) diff --git a/novaclient/tests/test_utils.py b/novaclient/tests/test_utils.py index 8809133ac..60b0be880 100644 --- a/novaclient/tests/test_utils.py +++ b/novaclient/tests/test_utils.py @@ -163,3 +163,24 @@ def test_print_list_sort_by_none(self): '| k3 | 3 |\n' '| k2 | 2 |\n' '+------+-------+\n') + + +class FlattenTestCase(test_utils.TestCase): + def test_flattening(self): + squashed = utils.flatten_dict( + {'a1': {'b1': 1234, + 'b2': 'string', + 'b3': set((1, 2, 3)), + 'b4': {'c1': ['l', 'l', ['l']], + 'c2': 'string'}}, + 'a2': ['l'], + 'a3': ('t',)}) + + self.assertEqual({'a1_b1': 1234, + 'a1_b2': 'string', + 'a1_b3': set([1, 2, 3]), + 'a1_b4_c1': ['l', 'l', ['l']], + 'a1_b4_c2': 'string', + 'a2': ['l'], + 'a3': ('t',)}, + squashed) diff --git a/novaclient/utils.py b/novaclient/utils.py index 2020a56f3..73dfbb112 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -1,3 +1,4 @@ +import json import os import pkg_resources import re @@ -177,6 +178,47 @@ def print_list(objs, fields, formatters={}, sortby_index=None): print(result) +def _flatten(data, prefix=None): + """Flatten a dict, using name as a prefix for the keys of dict. + + >>> _flatten('cpu_info', {'arch':'x86_64'}) + [('cpu_info_arch': 'x86_64')] + + """ + if isinstance(data, dict): + for key, value in six.iteritems(data): + new_key = '%s_%s' % (prefix, key) if prefix else key + if isinstance(value, (dict, list)): + for item in _flatten(value, new_key): + yield item + else: + yield new_key, value + else: + yield prefix, data + + +def flatten_dict(data): + """Return a new dict whose sub-dicts have been merged into the + original. Each of the parents keys are prepended to the child's + to prevent collisions. Any string elements will be JSON parsed + before flattening. + + >>> flatten_dict({'service': {'host':'cloud9@compute-068', 'id': 143}}) + {'service_host': colud9@compute-068', 'service_id': 143} + + """ + data = data.copy() + # Try and decode any nested JSON structures. + for key, value in six.iteritems(data): + if isinstance(value, six.string_types): + try: + data[key] = json.loads(value) + except ValueError: + pass + + return dict(_flatten(data)) + + def print_dict(d, dict_property="Property", dict_value="Value", wrap=0): pt = prettytable.PrettyTable([dict_property, dict_value], caching=False) pt.align = 'l' diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 998d681b5..e82b4c078 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2935,14 +2935,7 @@ def __init__(self, **kwargs): def do_hypervisor_show(cs, args): """Display the details of the specified hypervisor.""" hyper = _find_hypervisor(cs, args.hypervisor) - - # Build up the dict - info = hyper._info.copy() - info['service_id'] = info['service']['id'] - info['service_host'] = info['service']['host'] - del info['service'] - - utils.print_dict(info) + utils.print_dict(utils.flatten_dict(hyper._info)) @utils.arg('hypervisor', From 3978172012a601e3a623066fd724f5a6dd9e1caf Mon Sep 17 00:00:00 2001 From: Joe Gordon Date: Fri, 8 Nov 2013 15:42:17 +0800 Subject: [PATCH 0315/1705] Fix single H234 Bug to make Hacking 0.8 pass Jenkins patch I96b53ce825ca0f940596f53058b8e3c80463ac3c introduces hacking 0.8, this fix the one bug. Change-Id: Ib4ee60b4de32c59cb3552091f0ce50f366fe0879 --- novaclient/tests/v1_1/test_servers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/tests/v1_1/test_servers.py b/novaclient/tests/v1_1/test_servers.py index 08e86f5b2..a8b75d384 100644 --- a/novaclient/tests/v1_1/test_servers.py +++ b/novaclient/tests/v1_1/test_servers.py @@ -40,7 +40,7 @@ def test_get_server_details(self): def test_get_server_promote_details(self): s1 = cs.servers.list(detailed=False)[0] s2 = cs.servers.list(detailed=True)[0] - self.assertNotEquals(s1._info, s2._info) + self.assertNotEqual(s1._info, s2._info) s1.get() self.assertEqual(s1._info, s2._info) From f4769453fd6c103cd147cd0d6e7d8b107bdb96f2 Mon Sep 17 00:00:00 2001 From: Alvaro Lopez Garcia Date: Thu, 19 Jul 2012 09:47:40 +0200 Subject: [PATCH 0316/1705] Change "project" to "project_id" in cloudpipe-create The CLI was stating that the name of the project should be used, but the parameter needs to be the project_id. Change-Id: If0908a1b72c526f6436432ff18599423f89f7d1f --- novaclient/v1_1/cloudpipe.py | 2 +- novaclient/v1_1/shell.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/novaclient/v1_1/cloudpipe.py b/novaclient/v1_1/cloudpipe.py index 19a87d172..74fed75c7 100644 --- a/novaclient/v1_1/cloudpipe.py +++ b/novaclient/v1_1/cloudpipe.py @@ -35,7 +35,7 @@ def create(self, project): """ Launch a cloudpipe instance. - :param project: name of the project for the cloudpipe + :param project: UUID of the project (tenant) for the cloudpipe """ body = {'cloudpipe': {'project_id': project}} return self._create('/os-cloudpipe', body, 'instance_id', diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 998d681b5..8d7d6d77f 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -457,7 +457,8 @@ def do_cloudpipe_list(cs, _args): utils.print_list(cloudpipes, columns) -@utils.arg('project', metavar='', help='Name of the project.') +@utils.arg('project', metavar='', + help='UUID of the project to create the cloudpipe for.') def do_cloudpipe_create(cs, args): """Create a cloudpipe instance for the given project.""" cs.cloudpipe.create(args.project) From 6528d9944b00566d989ae532a57fd18862ecb053 Mon Sep 17 00:00:00 2001 From: "Haomeng, Wang" Date: Thu, 7 Nov 2013 21:03:54 +0800 Subject: [PATCH 0317/1705] nova security-group-* should support uuid as input If we installed neutron, the security-group request will be handled by neutron, however neutron is using uuid as key for security-group, nova uses an int value as key, for current novaclient, it only support the security-group which key is int, so will raise exception for uuid input. Change-Id: Ia230d12a1b4905434024b1a5129aa91cff7e6a5b Closes-Bug: #1247728 --- novaclient/tests/v1_1/test_shell.py | 33 +++++++++++++++++++++++++++++ novaclient/v1_1/shell.py | 6 ++++-- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index 8a1ade65c..48a1ea19f 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -31,6 +31,7 @@ import novaclient.shell from novaclient.tests.v1_1 import fakes from novaclient.tests import utils +import novaclient.v1_1.shell class ShellFixture(fixtures.Fixture): @@ -1809,3 +1810,35 @@ def test_migration_list_with_filters(self): self.assert_called('GET', '/os-migrations?cell_name=child1&host=host1' '&status=finished') + + +class GetSecgroupTest(utils.TestCase): + def test_with_integer(self): + cs = mock.Mock(**{ + 'security_groups.get.return_value': 'sec_group', + 'security_groups.list.return_value': [], + }) + result = novaclient.v1_1.shell._get_secgroup(cs, '1') + self.assertEqual(result, 'sec_group') + cs.security_groups.get.assert_called_once_with('1') + + def test_with_uuid(self): + cs = mock.Mock(**{ + 'security_groups.get.return_value': 'sec_group', + 'security_groups.list.return_value': [], + }) + result = novaclient.v1_1.shell._get_secgroup( + cs, 'c0c32459-dc5f-44dc-9a0a-473b28bac831') + self.assertEqual(result, 'sec_group') + cs.security_groups.get.assert_called_once_with( + 'c0c32459-dc5f-44dc-9a0a-473b28bac831') + + def test_with_an_nonexisting_name(self): + cs = mock.Mock(**{ + 'security_groups.get.return_value': 'sec_group', + 'security_groups.list.return_value': [], + }) + self.assertRaises(exceptions.CommandError, + novaclient.v1_1.shell._get_secgroup, + cs, + 'abc') diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 998d681b5..be17f853a 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -33,6 +33,7 @@ from novaclient import exceptions from novaclient.openstack.common import strutils from novaclient.openstack.common import timeutils +from novaclient.openstack.common import uuidutils from novaclient import utils from novaclient.v1_1 import availability_zones from novaclient.v1_1 import quotas @@ -2098,8 +2099,9 @@ def _print_secgroups(secgroups): def _get_secgroup(cs, secgroup): - # Check secgroup is an ID - if utils.is_integer_like(strutils.safe_encode(secgroup)): + # Check secgroup is an ID (nova-network) or UUID (neutron) + if (utils.is_integer_like(strutils.safe_encode(secgroup)) + or uuidutils.is_uuid_like(secgroup)): try: return cs.security_groups.get(secgroup) except exceptions.NotFound: From b8ab45f390e85fd4e1c096425c9336b003108611 Mon Sep 17 00:00:00 2001 From: Chris Buccella Date: Wed, 13 Nov 2013 12:51:01 +0000 Subject: [PATCH 0318/1705] Discrepancy between README.rst and nova help 1) --version in README should be --os-compute-api-version 2) NOVA_URL should be OS_AUTH_URL Change-Id: Ib8989e6923e2073bcc31d2a17d346eb436effdc8 Closes-Bug: 1250842 --- README.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 1dbcc0049..30131e1cd 100644 --- a/README.rst +++ b/README.rst @@ -42,13 +42,13 @@ params, but it's easier to just set them as environment variables:: export OS_TENANT_NAME=myproject You will also need to define the authentication url with ``--os-auth-url`` -and the version of the API with ``--version``. Or set them as an environment -variables as well:: +and the version of the API with ``--os-compute-api-version``. Or set them as +an environment variables as well:: export OS_AUTH_URL=http://example.com:8774/v1.1/ export OS_COMPUTE_API_VERSION=1.1 -If you are using Keystone, you need to set the NOVA_URL to the keystone +If you are using Keystone, you need to set the OS_AUTH_URL to the keystone endpoint:: export OS_AUTH_URL=http://example.com:5000/v2.0/ From 97e7ccf757dcd3a1fbe57ae01d6ebcd90a63e4cc Mon Sep 17 00:00:00 2001 From: OpenStack Jenkins Date: Fri, 15 Nov 2013 16:51:09 +0000 Subject: [PATCH 0319/1705] Updated from global requirements Change-Id: I96b53ce825ca0f940596f53058b8e3c80463ac3c --- requirements.txt | 2 +- test-requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index a13c0124a..9583c630b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ pbr>=0.5.21,<1.0 argparse -iso8601>=0.1.4 +iso8601>=0.1.8 PrettyTable>=0.6,<0.8 requests>=1.1 simplejson>=2.0.9 diff --git a/test-requirements.txt b/test-requirements.txt index 1d440816c..862590d15 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,4 +1,4 @@ -hacking>=0.5.6,<0.8 +hacking>=0.8.0,<0.9 coverage>=3.6 discover From 42772e143bf9a0539fedfefedd2556cd49a50f75 Mon Sep 17 00:00:00 2001 From: Chris Yeoh Date: Thu, 21 Nov 2013 22:36:25 +1030 Subject: [PATCH 0320/1705] Removes unnecessary pass Change-Id: Ic127765e549ccb172328b35a991b67afcdbb583a --- novaclient/client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/novaclient/client.py b/novaclient/client.py index 652da571c..b0623edbc 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -180,7 +180,6 @@ def request(self, url, method, **kwargs): try: body = json.loads(resp.text) except ValueError: - pass body = None else: body = None From 81c72dbc34dbcf96cb8c89fbe7fb91f2afdb2615 Mon Sep 17 00:00:00 2001 From: wingwj Date: Fri, 22 Nov 2013 12:09:47 +0800 Subject: [PATCH 0321/1705] Fix typo in novaclient hypen-separated --> hyphen-separated overrwrite --> overwrite typicaly --> typically sematics --> semantics Fixes bug: 1253881 Change-Id: I4aff614bb36b8f321837a9f54024b26ac0f11d40 --- novaclient/base.py | 6 +++--- novaclient/shell.py | 2 +- novaclient/utils.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/novaclient/base.py b/novaclient/base.py index 45bea18d8..38434c282 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -110,7 +110,7 @@ def completion_cache(self, cache_type, obj_class, mode): try: os.makedirs(cache_dir, 0o755) except OSError: - # NOTE(kiall): This is typicaly either permission denied while + # NOTE(kiall): This is typically either permission denied while # attempting to create the directory, or the # directory already exists. Either way, don't # fail. @@ -125,7 +125,7 @@ def completion_cache(self, cache_type, obj_class, mode): try: setattr(self, cache_attr, open(path, mode)) except IOError: - # NOTE(kiall): This is typicaly a permission denied while + # NOTE(kiall): This is typically a permission denied while # attempting to write the cache file. pass @@ -290,7 +290,7 @@ def _boot(self, resource_url, response_key, name, image, flavor, :param meta: A dict of arbitrary key/value metadata to store for this server. A maximum of five entries is allowed, and both keys and values must be 255 characters or less. - :param files: A dict of files to overrwrite on the server upon boot. + :param files: A dict of files to overwrite on the server upon boot. Keys are file names (i.e. ``/etc/passwd``) and values are the file contents (either as a string or as a file-like object). A maximum of five entries is allowed, diff --git a/novaclient/shell.py b/novaclient/shell.py index e70c1dc1c..ca0770e38 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -458,7 +458,7 @@ def _add_bash_completion_subparser(self, subparsers): def _find_actions(self, subparsers, actions_module): for attr in (a for a in dir(actions_module) if a.startswith('do_')): - # I prefer to be hypen-separated instead of underscores. + # I prefer to be hyphen-separated instead of underscores. command = attr[3:].replace('_', '-') callback = getattr(actions_module, attr) desc = callback.__doc__ or '' diff --git a/novaclient/utils.py b/novaclient/utils.py index 73dfbb112..f013726ee 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -42,7 +42,7 @@ def add_arg(f, *args, **kwargs): # NOTE(sirp): avoid dups that can occur when the module is shared across # tests. if (args, kwargs) not in f.arguments: - # Because of the sematics of decorator composition if we just append + # Because of the semantics of decorator composition if we just append # to the options list positional options will appear to be backwards. f.arguments.insert(0, (args, kwargs)) From 79070d6c5c4385c916a11b990de5c131d9c04e1d Mon Sep 17 00:00:00 2001 From: Zhenguo Niu Date: Fri, 22 Nov 2013 15:04:51 +0800 Subject: [PATCH 0322/1705] Fix inappropriate comment for flavor create api Change-Id: I7fa547925de5a194c10484d523006ca093518149 Fixes: bug #1253903 --- novaclient/v1_1/flavors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/v1_1/flavors.py b/novaclient/v1_1/flavors.py index 61f615c0f..9fe8ba271 100644 --- a/novaclient/v1_1/flavors.py +++ b/novaclient/v1_1/flavors.py @@ -123,7 +123,7 @@ def delete(self, flavor): def create(self, name, ram, vcpus, disk, flavorid="auto", ephemeral=0, swap=0, rxtx_factor=1.0, is_public=True): """ - Create (allocate) a floating ip for a tenant + Create a flavor. :param name: Descriptive name of the flavor :param ram: Memory in MB for the flavor From fabbc87bf22c1ee5025937de2eb4c78b352883be Mon Sep 17 00:00:00 2001 From: Florent Flament Date: Tue, 19 Nov 2013 15:33:14 +0100 Subject: [PATCH 0323/1705] Allows users to retrieve ciphered VM passwords This patch allows users to retrieve VM encrypted passwords using the `nova get-password` command without specifying the private key. Change-Id: I13ea132160dca912c6c1643b1006377982b778a1 Implements: blueprint retrieve-ciphered-vm-password --- novaclient/tests/idfake.pem | 27 ++++++++++++++++++++++++++ novaclient/tests/v1_1/fakes.py | 21 +++++++++++++++++++- novaclient/tests/v1_1/test_servers.py | 28 ++++++++++++++++++++++++++- novaclient/tests/v1_1/test_shell.py | 4 ++++ novaclient/v1_1/servers.py | 21 ++++++++++++++------ novaclient/v1_1/shell.py | 7 ++++++- novaclient/v3/shell.py | 7 ++++++- 7 files changed, 105 insertions(+), 10 deletions(-) create mode 100644 novaclient/tests/idfake.pem diff --git a/novaclient/tests/idfake.pem b/novaclient/tests/idfake.pem new file mode 100644 index 000000000..f7b446641 --- /dev/null +++ b/novaclient/tests/idfake.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA9QstF/7prDY7a9La7GS9TpMX+MWWXQgK6pHRLakDFp1WX1Q3 +Vly7rWitaZUGirUPMm181oJXBwkKlAxFD7hKjyHYaSswNszPYIAsVkc1+AO5epXz +g9kUBNtfg44Pg72UecwLrZ8JpmNZpJlKQOx6vF+yi7JmHrrIf6il/grIGUPzoT2L +yReimpyPoBrGtXhJYaCJ/XbKg1idRZiQdmwh1F/OmZWn9p0wunnsv08a0+qIywuw +WhG9/Zy9fjnEByfusS6gI0GIxDRL4RWzOqphd3PZzunwIBgEKFhgiki9+2DgcRVO +9I5wnDvfwQREJRZWh1uJa5ZTcfPa1EzZryVeOQIDAQABAoIBABxO3Te/cBk/7p9n +LXlPrfrszUEk+ljm+/PbQpIGy1+Kb5b1sKrebaP7ysS+vZG6lvXZZimVxx398mXm +APhu7tYYL9r+bUR3ZqGcTQLumRJ8w6mgtxANPN3Oxfr5p1stxIBJjTPSgpfhNFLq +joRvjUJDv+mZg2ibZVwyDHMLpdAdKp+3XMdyTLZcH9esqwii+natix7rHd1RuF85 +L1dfpxjkItwhgHsfdYS++5X3fRByFOhQ+Nhabh/kPQbQMcteRn1bN6zeCWBSglNb +Ka/ZrXb6ApRUc22Ji62mNO2ZPPekLJeCHk2h2E7ezYX+sGDNvvd/jHVDJJ20FjD1 +Z9KXuK0CgYEA/2vniy9yWd925QQtWbmrxgy6yj89feMH/LTv4qP298rGZ2nqxsyd +9pdBdb4NMsi4HmV5PG1hp3VRNBHl53DNh5eqzT8WEXnIF+sbrIU3KzrCVAx1kZTl ++OWKA6aVUsvvO3y85SOvInnsV+IsOGmU4/WBSjYoe39Bo7mq/YuZB9MCgYEA9ZlB +KBm6PjFdHQGNgedXahWzRcwC+ALCYqequPYqJolNzhrK4Uc2sWPSGdnldcHZ4XCQ +wbfCxUSwrMpA1oyuIQ0U4aowmOw5DjIueBWI8XBYEVRBlwvJwbXpBZ/DspGzTUDx +MBrrEwEaMadQvxhRnAzhp0rQAepatcz6Fgb1JkMCgYBMwDLiew5kfSav6JJsDMPW +DksurNQgeNEUmZYfx19V1EPMHWKj/CZXS9oqtEIpCXFyCNHmW4PlmvYcrGgmJJpN +7UAwzo0mES8UKNy2+Yy7W7u7H8dQSKrWILtZH3xtVcR8Xp4wSIm+1V40hkz9YpSP +71y7XQzLF1E1DnyYFZOVawKBgAFrmHfd5jjT2kD/sEzPBK9lXrsJmf7LLUqaw578 +NXQxmRSXDRNOcR+Hf0CNBQmwTE1EdGHaaTLw2cC2Drfu6lbgl31SmaNYwl+1pJUn +MrqKtseq4BI6jDkljypsKRqQQyQwOvTXQwLCH9+nowzn3Bj17hwkj51jOJESlWOp +OKO3AoGBALm+jjqyqX7gSnqK3FAumB8mlhv3yI1Wr1ctwe18mKfKbz17HxXRu9pF +K/6e7WMCA1p+jhoE8gj1h2WBcH0nV2qt8Ye8gJBbCi4dhI08o4AfrIV47oZx1RlO +qYcA1U9lyaODY5SL8+6PHOy5J/aYtuA+wvfEnWiCIdKQrhWetcn3 +-----END RSA PRIVATE KEY----- diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index 8a17c61cd..387ba1979 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -483,8 +483,27 @@ def delete_servers_1234_ips_public_1_2_3_4(self, **kw): # Server password # + # Testing with the following password and key + # + # Clear password: FooBar123 + # + # RSA Private Key: novaclient/tests/idfake.pem + # + # Encrypted password + # OIuEuQttO8Rk93BcKlwHQsziDAnkAm/V6V8VPToA8ZeUaUBWwS0gwo2K6Y61Z96r + # qG447iRz0uTEEYq3RAYJk1mh3mMIRVl27t8MtIecR5ggVVbz1S9AwXJQypDKl0ho + # QFvhCBcMWPohyGewDJOhDbtuN1IoFI9G55ZvFwCm5y7m7B2aVcoLeIsJZE4PLsIw + # /y5a6Z3/AoJZYGG7IH5WN88UROU3B9JZGFB2qtPLQTOvDMZLUhoPRIJeHiVSlo1N + # tI2/++UsXVg3ow6ItqCJGgdNuGG5JB+bslDHWPxROpesEIHdczk46HCpHQN8f1sk + # Hi/fmZZNQQqj1Ijq0caOIw== def get_servers_1234_os_server_password(self, **kw): - return (200, {}, {'password': ''}) + return (200, {}, {'password': + 'OIuEuQttO8Rk93BcKlwHQsziDAnkAm/V6V8VPToA8ZeUaUBWwS0gwo2K6Y61Z96r' + 'qG447iRz0uTEEYq3RAYJk1mh3mMIRVl27t8MtIecR5ggVVbz1S9AwXJQypDKl0ho' + 'QFvhCBcMWPohyGewDJOhDbtuN1IoFI9G55ZvFwCm5y7m7B2aVcoLeIsJZE4PLsIw' + '/y5a6Z3/AoJZYGG7IH5WN88UROU3B9JZGFB2qtPLQTOvDMZLUhoPRIJeHiVSlo1N' + 'tI2/++UsXVg3ow6ItqCJGgdNuGG5JB+bslDHWPxROpesEIHdczk46HCpHQN8f1sk' + 'Hi/fmZZNQQqj1Ijq0caOIw=='}) def delete_servers_1234_os_server_password(self, **kw): return (202, {}, None) diff --git a/novaclient/tests/v1_1/test_servers.py b/novaclient/tests/v1_1/test_servers.py index a8b75d384..8c4c9ea70 100644 --- a/novaclient/tests/v1_1/test_servers.py +++ b/novaclient/tests/v1_1/test_servers.py @@ -420,9 +420,35 @@ def test_get_console_output_with_length(self): self.assertEqual(cs.servers.get_console_output(s, length=50), success) cs.assert_called('POST', '/servers/1234/action') + # Testing password methods with the following password and key + # + # Clear password: FooBar123 + # + # RSA Private Key: novaclient/tests/idfake.pem + # + # Encrypted password + # OIuEuQttO8Rk93BcKlwHQsziDAnkAm/V6V8VPToA8ZeUaUBWwS0gwo2K6Y61Z96r + # qG447iRz0uTEEYq3RAYJk1mh3mMIRVl27t8MtIecR5ggVVbz1S9AwXJQypDKl0ho + # QFvhCBcMWPohyGewDJOhDbtuN1IoFI9G55ZvFwCm5y7m7B2aVcoLeIsJZE4PLsIw + # /y5a6Z3/AoJZYGG7IH5WN88UROU3B9JZGFB2qtPLQTOvDMZLUhoPRIJeHiVSlo1N + # tI2/++UsXVg3ow6ItqCJGgdNuGG5JB+bslDHWPxROpesEIHdczk46HCpHQN8f1sk + # Hi/fmZZNQQqj1Ijq0caOIw== + def test_get_password(self): s = cs.servers.get(1234) - self.assertEqual(s.get_password('/foo/id_rsa'), '') + self.assertEqual(s.get_password('novaclient/tests/idfake.pem'), + b'FooBar123') + cs.assert_called('GET', '/servers/1234/os-server-password') + + def test_get_password_without_key(self): + s = cs.servers.get(1234) + self.assertEqual(s.get_password(), + 'OIuEuQttO8Rk93BcKlwHQsziDAnkAm/V6V8VPToA8ZeUaUBWwS0gwo2K6Y61Z96r' + 'qG447iRz0uTEEYq3RAYJk1mh3mMIRVl27t8MtIecR5ggVVbz1S9AwXJQypDKl0ho' + 'QFvhCBcMWPohyGewDJOhDbtuN1IoFI9G55ZvFwCm5y7m7B2aVcoLeIsJZE4PLsIw' + '/y5a6Z3/AoJZYGG7IH5WN88UROU3B9JZGFB2qtPLQTOvDMZLUhoPRIJeHiVSlo1N' + 'tI2/++UsXVg3ow6ItqCJGgdNuGG5JB+bslDHWPxROpesEIHdczk46HCpHQN8f1sk' + 'Hi/fmZZNQQqj1Ijq0caOIw==') cs.assert_called('GET', '/servers/1234/os-server-password') def test_clear_password(self): diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index bd2caf2a8..f1dad3e5c 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -1635,6 +1635,10 @@ def test_get_password(self): self.run_command('get-password sample-server /foo/id_rsa') self.assert_called('GET', '/servers/1234/os-server-password') + def test_get_password_without_key(self): + self.run_command('get-password sample-server') + self.assert_called('GET', '/servers/1234/os-server-password') + def test_clear_password(self): self.run_command('clear-password sample-server') self.assert_called('DELETE', '/servers/1234/os-server-password') diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index a64e955b9..ef73d8669 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -74,11 +74,15 @@ def get_spice_console(self, console_type): """ return self.manager.get_spice_console(self, console_type) - def get_password(self, private_key): + def get_password(self, private_key=None): """ Get password for a Server. + Returns the clear password of an instance if private_key is + provided, returns the ciphered password otherwise. + :param private_key: Path to private key file for decryption + (optional) """ return self.manager.get_password(self, private_key) @@ -497,24 +501,29 @@ def get_spice_console(self, server, console_type): return self._action('os-getSPICEConsole', server, {'type': console_type})[1] - def get_password(self, server, private_key): + def get_password(self, server, private_key=None): """ Get password for an instance + Returns the clear password of an instance if private_key is + provided, returns the ciphered password otherwise. + Requires that openssl is installed and in the path :param server: The :class:`Server` (or its ID) to add an IP to. :param private_key: The private key to decrypt password + (optional) """ _resp, body = self.api.client.get("/servers/%s/os-server-password" % base.getid(server)) - if body and body.get('password'): + ciphered_pw = body.get('password', '') if body else '' + if private_key and ciphered_pw: try: - return crypto.decrypt_password(private_key, body['password']) + return crypto.decrypt_password(private_key, ciphered_pw) except Exception as exc: - return '%sFailed to decrypt:\n%s' % (exc, body['password']) - return '' + return '%sFailed to decrypt:\n%s' % (exc, ciphered_pw) + return ciphered_pw def clear_password(self, server): """ diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 6edbe381d..3b4449c4f 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1867,7 +1867,12 @@ def __init__(self, console_dict): @utils.arg('server', metavar='', help='Name or ID of server.') @utils.arg('private_key', metavar='', - help='Private key (used locally to decrypt password).') + help='Private key (used locally to decrypt password) (Optional). ' + 'When specified, the command displays the clear (decrypted) VM ' + 'password. When not specified, the ciphered VM password is ' + 'displayed.', + nargs='?', + default=None) def do_get_password(cs, args): """Get password for a server.""" server = _find_server(cs, args.server) diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 3044e6d9f..66b425bbf 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -1701,7 +1701,12 @@ def __init__(self, console_dict): @utils.arg('server', metavar='', help='Name or ID of server.') @utils.arg('private_key', metavar='', - help='Private key (used locally to decrypt password).') + help='Private key (used locally to decrypt password) (Optional). ' + 'When specified, the command displays the clear (decrypted) VM ' + 'password. When not specified, the ciphered VM password is ' + 'displayed.', + nargs='?', + default=None) def do_get_password(cs, args): """Get password for a server.""" server = _find_server(cs, args.server) From beb30f714ec871b6ef7c0506a7186ce739c21bd1 Mon Sep 17 00:00:00 2001 From: Zhenguo Niu Date: Thu, 28 Nov 2013 22:34:12 +0800 Subject: [PATCH 0324/1705] Updates .gitignore To ignore swap files from getting into repository currently the implemented ignore is *.swp however vim adds more swap files if these files exists, so improving this with *.sw? Change-Id: Ic3e09f8cf0be35c98d89d862d40121a09c7b2498 Closes-Bug: #1255876 --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index b237f4133..5e1b43c4c 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,7 @@ subunit.log cover *.pyc .idea -*.swp +*.sw? *~ build dist From a8756c400798ba42ac8a26df9362976ddaa90097 Mon Sep 17 00:00:00 2001 From: Chris Buccella Date: Wed, 20 Nov 2013 07:22:27 -0500 Subject: [PATCH 0325/1705] Adds a --show option to the image-create subcommand Prints info about the new image when --show is used with the image-create subcommand. Change-Id: Id1fba67df9ff86cb1bbda3e241d1d03e01afbe2c Closes-Bug: 1248128 --- novaclient/tests/v1_1/fakes.py | 3 +++ novaclient/tests/v1_1/test_shell.py | 10 ++++++++++ novaclient/v1_1/shell.py | 8 ++++++++ novaclient/v3/shell.py | 8 ++++++++ 4 files changed, 29 insertions(+) diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index 8a17c61cd..b18fe1b50 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -926,6 +926,9 @@ def get_images_1(self, **kw): def get_images_2(self, **kw): return (200, {}, {'image': self.get_images_detail()[2]['images'][1]}) + def get_images_456(self, **kw): + return (200, {}, {'image': self.get_images_detail()[2]['images'][1]}) + def post_images(self, body, **kw): assert list(body) == ['image'] fakes.assert_has_keys(body['image'], required=['serverId', 'name']) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index bd2caf2a8..acb92a9e7 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -660,6 +660,16 @@ def test_create_image(self): {'createImage': {'name': 'mysnapshot', 'metadata': {}}}, ) + def test_create_image_show(self): + output = self.run_command('image-create ' + 'sample-server mysnapshot --show') + self.assert_called_anytime( + 'POST', '/servers/1234/action', + {'createImage': {'name': 'mysnapshot', 'metadata': {}}}, + ) + self.assertIn('My Server Backup', output) + self.assertIn('SAVING', output) + def test_image_delete(self): self.run_command('image-delete 1') self.assert_called('DELETE', '/images/1') diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 6edbe381d..4ce5a606f 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1378,6 +1378,11 @@ def do_root_password(cs, args): @utils.arg('server', metavar='', help='Name or ID of server.') @utils.arg('name', metavar='', help='Name of snapshot.') +@utils.arg('--show', + dest='show', + action="store_true", + default=False, + help='Print image info.') @utils.arg('--poll', dest='poll', action="store_true", @@ -1407,6 +1412,9 @@ def do_image_create(cs, args): [None], status_field=task_state_field, show_progress=False, silent=True) + if args.show: + _print_image(cs.images.get(image_uuid)) + @utils.arg('server', metavar='', help='Name or ID of server.') @utils.arg('name', metavar='', help='Name of the backup image.') diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 3044e6d9f..e70c827d9 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -1241,6 +1241,11 @@ def do_root_password(cs, args): @utils.arg('server', metavar='', help='Name or ID of server.') @utils.arg('name', metavar='', help='Name of snapshot.') +@utils.arg('--show', + dest='show', + action="store_true", + default=False, + help='Print image info.') @utils.arg('--poll', dest='poll', action="store_true", @@ -1270,6 +1275,9 @@ def do_image_create(cs, args): [None], status_field=task_state_field, show_progress=False, silent=True) + if args.show: + _print_image(cs.images.get(image_uuid)) + @utils.arg('server', metavar='', help='Name or ID of server.') @utils.arg('name', metavar='', help='Name of the backup image.') From 8dd5ea47838b8fd4e59660a1e1c0a231e291e0c6 Mon Sep 17 00:00:00 2001 From: zhangyanzi Date: Thu, 28 Nov 2013 11:52:24 +0800 Subject: [PATCH 0326/1705] Fix the inappropriate comment for flavor Fix the inappropriate comment for delete and create flavor Change-Id: I29985e5f145cfe3d0e72d6ac684ae9c47ea89c7e Closes-Bug: #1255819 --- novaclient/v1_1/flavors.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/novaclient/v1_1/flavors.py b/novaclient/v1_1/flavors.py index 9fe8ba271..acc42b974 100644 --- a/novaclient/v1_1/flavors.py +++ b/novaclient/v1_1/flavors.py @@ -116,7 +116,6 @@ def delete(self, flavor): Delete a specific flavor. :param flavor: The ID of the :class:`Flavor` to get. - :param purge: Whether to purge record from the database """ self._delete("/flavors/%s" % base.getid(flavor)) @@ -127,7 +126,7 @@ def create(self, name, ram, vcpus, disk, flavorid="auto", :param name: Descriptive name of the flavor :param ram: Memory in MB for the flavor - :param vcpu: Number of VCPUs for the flavor + :param vcpus: Number of VCPUs for the flavor :param disk: Size of local disk in GB :param flavorid: ID for the flavor (optional). You can use the reserved value ``"auto"`` to have Nova generate a UUID for the From fe7ac8800df02aa2a4f30b3be35e43edd16b7dfa Mon Sep 17 00:00:00 2001 From: Chris Yeoh Date: Mon, 2 Dec 2013 23:09:03 +1030 Subject: [PATCH 0327/1705] Sets default service type for Nova V3 API Set the default service type when talking to the Nova V3 API to computev3 rather than compute. Although it is rather ugly to have a different service type for a different version of a service, this is necessary for the medium term because traditionally the compute service endpoint has pointed to the V2 API rather than the root and then version discovery done. This change allows progression of the V3 API support in novaclient and although devstack too currently points computev3 directly to the V3 API, the intent is to change this to point to the root and implement version discovery during the Icehouse development cycle. Longer term when the V2 API support is eventually removed we can reclaim the 'compute' service type and point it to the root. Partially implements blueprint v3-api Change-Id: If5d6a0d8af037cde7bf253d71aac2823b89f8066 --- novaclient/shell.py | 17 +++++++++++++++-- novaclient/tests/test_shell.py | 30 ++++++++++++++++++++++++++++++ novaclient/v3/client.py | 2 +- 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/novaclient/shell.py b/novaclient/shell.py index ca0770e38..a8368ab90 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -60,7 +60,13 @@ DEFAULT_OS_COMPUTE_API_VERSION = "1.1" DEFAULT_NOVA_ENDPOINT_TYPE = 'publicURL' -DEFAULT_NOVA_SERVICE_TYPE = 'compute' +# NOTE(cyeoh): Having the service type dependent on the API version +# is pretty ugly, but we have to do this because traditionally the +# catalog entry for compute points directly to the V2 API rather than +# the root, and then doing version discovery. +DEFAULT_NOVA_SERVICE_TYPE_MAP = {'1.1': 'compute', + '2': 'compute', + '3': 'computev3'} logger = logging.getLogger(__name__) @@ -557,7 +563,14 @@ def main(self, argv): endpoint_type = DEFAULT_NOVA_ENDPOINT_TYPE if not service_type: - service_type = DEFAULT_NOVA_SERVICE_TYPE + os_compute_api_version = (options.os_compute_api_version or + DEFAULT_OS_COMPUTE_API_VERSION) + try: + service_type = DEFAULT_NOVA_SERVICE_TYPE_MAP[ + os_compute_api_version] + except KeyError: + service_type = DEFAULT_NOVA_SERVICE_TYPE_MAP[ + DEFAULT_OS_COMPUTE_API_VERSION] service_type = utils.get_service_type(args.func) or service_type #FIXME(usrleon): Here should be restrict for project id same as diff --git a/novaclient/tests/test_shell.py b/novaclient/tests/test_shell.py index c873966b1..e70fdb1b0 100644 --- a/novaclient/tests/test_shell.py +++ b/novaclient/tests/test_shell.py @@ -198,3 +198,33 @@ def test_no_password(self, mock_getpass, mock_stdin): self.assertEqual(required, message.args) else: self.fail('CommandError not raised') + + def _test_service_type(self, version, service_type, mock_client): + if version is None: + cmd = 'list' + else: + cmd = '--os-compute-api-version %s list' % version + self.make_env() + self.shell(cmd) + _, client_kwargs = mock_client.call_args + self.assertEqual(service_type, client_kwargs['service_type']) + + @mock.patch('novaclient.client.Client') + def test_default_service_type(self, mock_client): + self._test_service_type(None, 'compute', mock_client) + + @mock.patch('novaclient.client.Client') + def test_v1_1_service_type(self, mock_client): + self._test_service_type('1.1', 'compute', mock_client) + + @mock.patch('novaclient.client.Client') + def test_v2_service_type(self, mock_client): + self._test_service_type('2', 'compute', mock_client) + + @mock.patch('novaclient.client.Client') + def test_v3_service_type(self, mock_client): + self._test_service_type('3', 'computev3', mock_client) + + @mock.patch('novaclient.client.Client') + def test_v_unknown_service_type(self, mock_client): + self._test_service_type('unknown', 'compute', mock_client) diff --git a/novaclient/v3/client.py b/novaclient/v3/client.py index 091aa0901..e3ab29c37 100644 --- a/novaclient/v3/client.py +++ b/novaclient/v3/client.py @@ -40,7 +40,7 @@ def __init__(self, username, password, project_id, auth_url=None, insecure=False, timeout=None, proxy_tenant_id=None, proxy_token=None, region_name=None, endpoint_type='publicURL', extensions=None, - service_type='compute', service_name=None, + service_type='computev3', service_name=None, volume_service_name=None, timings=False, bypass_url=None, os_cache=False, no_cache=True, http_log_debug=False, auth_system='keystone', From 50685437eda29150efcae3e171d3467c32fd958d Mon Sep 17 00:00:00 2001 From: Chris Yeoh Date: Fri, 6 Dec 2013 10:47:41 +1030 Subject: [PATCH 0328/1705] Enable hacking check for Apache 2.0 license Removes H102 (license header not found) check from flake8 ignore list. Adds missing apache license headers. Change-Id: I109f23c6d8b2e3efb1dac7f764bd77e0d9d335f0 --- novaclient/client.py | 13 +++++++++++++ novaclient/exceptions.py | 13 +++++++++++++ novaclient/tests/fakes.py | 13 +++++++++++++ novaclient/tests/test_base.py | 13 +++++++++++++ novaclient/tests/test_http.py | 13 +++++++++++++ novaclient/tests/test_service_catalog.py | 13 +++++++++++++ novaclient/tests/test_shell.py | 13 +++++++++++++ novaclient/tests/test_utils.py | 13 +++++++++++++ novaclient/tests/utils.py | 13 +++++++++++++ .../tests/v1_1/contrib/test_list_extensions.py | 13 +++++++++++++ novaclient/tests/v1_1/test_auth.py | 13 +++++++++++++ novaclient/tests/v1_1/test_certs.py | 13 +++++++++++++ novaclient/tests/v1_1/test_cloudpipe.py | 13 +++++++++++++ novaclient/tests/v1_1/test_floating_ip_dns.py | 13 +++++++++++++ novaclient/tests/v1_1/test_hosts.py | 13 +++++++++++++ novaclient/tests/v1_1/test_images.py | 13 +++++++++++++ novaclient/tests/v1_1/test_keypairs.py | 13 +++++++++++++ novaclient/tests/v1_1/test_limits.py | 12 ++++++++++++ novaclient/tests/v1_1/test_networks.py | 13 +++++++++++++ novaclient/tests/v1_1/test_security_group_rules.py | 13 +++++++++++++ novaclient/tests/v1_1/test_security_groups.py | 13 +++++++++++++ novaclient/tests/v1_1/test_servers.py | 12 ++++++++++++ novaclient/tests/v1_1/test_usage.py | 13 +++++++++++++ novaclient/tests/v1_1/utils.py | 13 +++++++++++++ novaclient/utils.py | 13 +++++++++++++ novaclient/v1_1/flavors.py | 13 +++++++++++++ novaclient/v1_1/images.py | 13 +++++++++++++ novaclient/v1_1/limits.py | 12 ++++++++++++ novaclient/v1_1/usage.py | 13 +++++++++++++ tox.ini | 2 +- 30 files changed, 375 insertions(+), 1 deletion(-) diff --git a/novaclient/client.py b/novaclient/client.py index b0623edbc..0cbfa8bdd 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -3,6 +3,19 @@ # Copyright 2011 Piston Cloud Computing, Inc. # All Rights Reserved. +# +# 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. + """ OpenStack Client interface. Handles the REST calls and responses. """ diff --git a/novaclient/exceptions.py b/novaclient/exceptions.py index b04844a2d..6ff93d720 100644 --- a/novaclient/exceptions.py +++ b/novaclient/exceptions.py @@ -1,4 +1,17 @@ # Copyright 2010 Jacob Kaplan-Moss +# +# 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. + """ Exception definitions. """ diff --git a/novaclient/tests/fakes.py b/novaclient/tests/fakes.py index bc3683a4a..c73f72f91 100644 --- a/novaclient/tests/fakes.py +++ b/novaclient/tests/fakes.py @@ -1,3 +1,16 @@ +# +# 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. + """ A fake server that "responds" to API methods with pre-canned responses. diff --git a/novaclient/tests/test_base.py b/novaclient/tests/test_base.py index 58aeae962..6f9ba7c15 100644 --- a/novaclient/tests/test_base.py +++ b/novaclient/tests/test_base.py @@ -1,3 +1,16 @@ +# +# 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 novaclient import base from novaclient import exceptions from novaclient.v1_1 import flavors diff --git a/novaclient/tests/test_http.py b/novaclient/tests/test_http.py index f6ebb7a7e..e2fc4fa10 100644 --- a/novaclient/tests/test_http.py +++ b/novaclient/tests/test_http.py @@ -1,3 +1,16 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + import mock import requests diff --git a/novaclient/tests/test_service_catalog.py b/novaclient/tests/test_service_catalog.py index a34d82ffb..9f224bb57 100644 --- a/novaclient/tests/test_service_catalog.py +++ b/novaclient/tests/test_service_catalog.py @@ -1,3 +1,16 @@ +# +# 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 novaclient import exceptions from novaclient import service_catalog from novaclient.tests import utils diff --git a/novaclient/tests/test_shell.py b/novaclient/tests/test_shell.py index c873966b1..f6e26c057 100644 --- a/novaclient/tests/test_shell.py +++ b/novaclient/tests/test_shell.py @@ -1,3 +1,16 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + import prettytable import re import six diff --git a/novaclient/tests/test_utils.py b/novaclient/tests/test_utils.py index 60b0be880..4400ab010 100644 --- a/novaclient/tests/test_utils.py +++ b/novaclient/tests/test_utils.py @@ -1,3 +1,16 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + import sys import mock diff --git a/novaclient/tests/utils.py b/novaclient/tests/utils.py index 4fccff9b7..9335819ff 100644 --- a/novaclient/tests/utils.py +++ b/novaclient/tests/utils.py @@ -1,3 +1,16 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + import os import fixtures diff --git a/novaclient/tests/v1_1/contrib/test_list_extensions.py b/novaclient/tests/v1_1/contrib/test_list_extensions.py index f9ede2972..2d4426fc0 100644 --- a/novaclient/tests/v1_1/contrib/test_list_extensions.py +++ b/novaclient/tests/v1_1/contrib/test_list_extensions.py @@ -1,3 +1,16 @@ +# +# 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 novaclient import extension from novaclient.v1_1.contrib import list_extensions diff --git a/novaclient/tests/v1_1/test_auth.py b/novaclient/tests/v1_1/test_auth.py index f3bd279a9..0ecfd1e5e 100644 --- a/novaclient/tests/v1_1/test_auth.py +++ b/novaclient/tests/v1_1/test_auth.py @@ -1,3 +1,16 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + import copy import json import mock diff --git a/novaclient/tests/v1_1/test_certs.py b/novaclient/tests/v1_1/test_certs.py index ff1f61121..3d1ca0907 100644 --- a/novaclient/tests/v1_1/test_certs.py +++ b/novaclient/tests/v1_1/test_certs.py @@ -1,3 +1,16 @@ +# +# 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 novaclient.v1_1 import certs from novaclient.tests import utils from novaclient.tests.v1_1 import fakes diff --git a/novaclient/tests/v1_1/test_cloudpipe.py b/novaclient/tests/v1_1/test_cloudpipe.py index e428629f2..f98c2b24b 100644 --- a/novaclient/tests/v1_1/test_cloudpipe.py +++ b/novaclient/tests/v1_1/test_cloudpipe.py @@ -1,3 +1,16 @@ +# +# 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 novaclient.v1_1 import cloudpipe from novaclient.tests import utils from novaclient.tests.v1_1 import fakes diff --git a/novaclient/tests/v1_1/test_floating_ip_dns.py b/novaclient/tests/v1_1/test_floating_ip_dns.py index 96cf90f43..a5714f4da 100644 --- a/novaclient/tests/v1_1/test_floating_ip_dns.py +++ b/novaclient/tests/v1_1/test_floating_ip_dns.py @@ -1,3 +1,16 @@ +# +# 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 novaclient.v1_1 import floating_ip_dns from novaclient.tests.v1_1 import fakes from novaclient.tests import utils diff --git a/novaclient/tests/v1_1/test_hosts.py b/novaclient/tests/v1_1/test_hosts.py index 92d5bc965..cfaeeacca 100644 --- a/novaclient/tests/v1_1/test_hosts.py +++ b/novaclient/tests/v1_1/test_hosts.py @@ -1,3 +1,16 @@ +# +# 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 novaclient.v1_1 import hosts from novaclient.tests.v1_1 import fakes from novaclient.tests import utils diff --git a/novaclient/tests/v1_1/test_images.py b/novaclient/tests/v1_1/test_images.py index 5f9cfacd0..33c8adb61 100644 --- a/novaclient/tests/v1_1/test_images.py +++ b/novaclient/tests/v1_1/test_images.py @@ -1,3 +1,16 @@ +# +# 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 novaclient.v1_1 import images from novaclient.tests import utils from novaclient.tests.v1_1 import fakes diff --git a/novaclient/tests/v1_1/test_keypairs.py b/novaclient/tests/v1_1/test_keypairs.py index 64fbc0ead..1318bd11d 100644 --- a/novaclient/tests/v1_1/test_keypairs.py +++ b/novaclient/tests/v1_1/test_keypairs.py @@ -1,3 +1,16 @@ +# +# 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 novaclient.v1_1 import keypairs from novaclient.tests import utils from novaclient.tests.v1_1 import fakes diff --git a/novaclient/tests/v1_1/test_limits.py b/novaclient/tests/v1_1/test_limits.py index 517a6f1fb..d97300f5f 100644 --- a/novaclient/tests/v1_1/test_limits.py +++ b/novaclient/tests/v1_1/test_limits.py @@ -1,3 +1,15 @@ +# +# 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 novaclient.v1_1 import limits from novaclient.tests import utils diff --git a/novaclient/tests/v1_1/test_networks.py b/novaclient/tests/v1_1/test_networks.py index d40ec53f2..cba8393fd 100644 --- a/novaclient/tests/v1_1/test_networks.py +++ b/novaclient/tests/v1_1/test_networks.py @@ -1,3 +1,16 @@ +# +# 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 novaclient.v1_1 import networks from novaclient.tests import utils from novaclient.tests.v1_1 import fakes diff --git a/novaclient/tests/v1_1/test_security_group_rules.py b/novaclient/tests/v1_1/test_security_group_rules.py index e1d015fa5..02230e9b8 100644 --- a/novaclient/tests/v1_1/test_security_group_rules.py +++ b/novaclient/tests/v1_1/test_security_group_rules.py @@ -1,3 +1,16 @@ +# +# 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 novaclient import exceptions from novaclient.v1_1 import security_group_rules from novaclient.tests import utils diff --git a/novaclient/tests/v1_1/test_security_groups.py b/novaclient/tests/v1_1/test_security_groups.py index 0040d3054..ac6e0aa75 100644 --- a/novaclient/tests/v1_1/test_security_groups.py +++ b/novaclient/tests/v1_1/test_security_groups.py @@ -1,3 +1,16 @@ +# +# 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 novaclient.v1_1 import security_groups from novaclient.tests import utils from novaclient.tests.v1_1 import fakes diff --git a/novaclient/tests/v1_1/test_servers.py b/novaclient/tests/v1_1/test_servers.py index 8c4c9ea70..4861ad2c7 100644 --- a/novaclient/tests/v1_1/test_servers.py +++ b/novaclient/tests/v1_1/test_servers.py @@ -1,4 +1,16 @@ # -*- coding: utf-8 -*- +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. import mock import six diff --git a/novaclient/tests/v1_1/test_usage.py b/novaclient/tests/v1_1/test_usage.py index 49b0baedf..09546966f 100644 --- a/novaclient/tests/v1_1/test_usage.py +++ b/novaclient/tests/v1_1/test_usage.py @@ -1,3 +1,16 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + import datetime from novaclient.v1_1 import usage diff --git a/novaclient/tests/v1_1/utils.py b/novaclient/tests/v1_1/utils.py index eaf108f55..d28392f95 100644 --- a/novaclient/tests/v1_1/utils.py +++ b/novaclient/tests/v1_1/utils.py @@ -1,3 +1,16 @@ +# +# 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 nose.tools import ok_ diff --git a/novaclient/utils.py b/novaclient/utils.py index f013726ee..6e6406037 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -1,3 +1,16 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + import json import os import pkg_resources diff --git a/novaclient/v1_1/flavors.py b/novaclient/v1_1/flavors.py index acc42b974..19a3808cd 100644 --- a/novaclient/v1_1/flavors.py +++ b/novaclient/v1_1/flavors.py @@ -1,4 +1,17 @@ # Copyright 2010 Jacob Kaplan-Moss +# +# 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. + """ Flavor interface. """ diff --git a/novaclient/v1_1/images.py b/novaclient/v1_1/images.py index eff012de5..3a1c7da27 100644 --- a/novaclient/v1_1/images.py +++ b/novaclient/v1_1/images.py @@ -1,4 +1,17 @@ # Copyright 2010 Jacob Kaplan-Moss +# +# 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. + """ Image interface. """ diff --git a/novaclient/v1_1/limits.py b/novaclient/v1_1/limits.py index 8394bdd6b..4ea80a053 100644 --- a/novaclient/v1_1/limits.py +++ b/novaclient/v1_1/limits.py @@ -1,4 +1,16 @@ # Copyright 2011 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. from novaclient import base from novaclient.openstack.common.py3kcompat import urlutils diff --git a/novaclient/v1_1/usage.py b/novaclient/v1_1/usage.py index 880434046..05ce020a0 100644 --- a/novaclient/v1_1/usage.py +++ b/novaclient/v1_1/usage.py @@ -1,3 +1,16 @@ +# +# 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. + """ Usage interface. """ diff --git a/tox.ini b/tox.ini index 6ec267623..8cf7dd71f 100644 --- a/tox.ini +++ b/tox.ini @@ -24,6 +24,6 @@ commands = python setup.py testr --coverage --testr-args='{posargs}' downloadcache = ~/cache/pip [flake8] -ignore = E12,E711,E721,E712,F841,F811,F821,H102,H302,H306,H403,H404 +ignore = E12,E711,E721,E712,F841,F811,F821,H302,H306,H403,H404 show-source = True exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build From 78ca0766934c7b1f1c45f107d627047fc57772cf Mon Sep 17 00:00:00 2001 From: zhangyanzi Date: Thu, 5 Dec 2013 15:05:21 +0800 Subject: [PATCH 0329/1705] Fix inappropriate comment for delete FloatingIP There is an inappropriate comment for delete FloatingIP, fix it. Change-Id: Ib5ddabd3b227c8580e1b912b2bb98a3294ec3288 Closes-Bug: #1258034 --- novaclient/v1_1/floating_ips.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/v1_1/floating_ips.py b/novaclient/v1_1/floating_ips.py index 503e6269c..25f5a7437 100644 --- a/novaclient/v1_1/floating_ips.py +++ b/novaclient/v1_1/floating_ips.py @@ -44,7 +44,7 @@ def delete(self, floating_ip): """ Delete (deallocate) a floating ip for a tenant - :param key: The :class:`Keypair` (or its ID) to delete. + :param floating_ip: The floating ip address to delete. """ self._delete("/os-floating-ips/%s" % base.getid(floating_ip)) From dd4bc08dd9a571637734a92c9411c32ea1f36dcf Mon Sep 17 00:00:00 2001 From: huangtianhua Date: Fri, 6 Dec 2013 17:57:51 +0800 Subject: [PATCH 0330/1705] Fix incorrect help message on flavor_access action The help message of params "flavor" and "tenant" is incorrect on do_flavor_access_add and do_flavor_access_remove. Change-Id: I66c45316181307305a19aec25acefc019cd7bdfc Closes-Bug: #1258453 --- novaclient/v1_1/shell.py | 8 ++++---- novaclient/v3/shell.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 77b32f1ba..99efac424 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -720,9 +720,9 @@ def do_flavor_access_list(cs, args): @utils.arg('flavor', metavar='', - help="Filter results by flavor name or ID.") + help="Flavor name or ID to add access for the given tenant.") @utils.arg('tenant', metavar='', - help='Filter results by tenant ID.') + help='Tenant ID to add flavor access for.') def do_flavor_access_add(cs, args): """Add flavor access for the given tenant.""" flavor = _find_flavor_for_admin(cs, args.flavor) @@ -733,9 +733,9 @@ def do_flavor_access_add(cs, args): @utils.arg('flavor', metavar='', - help="Filter results by flavor name or ID.") + help="Flavor name or ID to remove access for the given tenant.") @utils.arg('tenant', metavar='', - help='Filter results by tenant ID.') + help='Tenant ID to remove flavor access for.') def do_flavor_access_remove(cs, args): """Remove flavor access for the given tenant.""" flavor = _find_flavor_for_admin(cs, args.flavor) diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 66b425bbf..a84cc039a 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -600,9 +600,9 @@ def do_flavor_access_list(cs, args): @utils.arg('flavor', metavar='', - help="Filter results by flavor name or ID.") + help="Flavor name or ID to add access for the given tenant.") @utils.arg('tenant', metavar='', - help='Filter results by tenant ID.') + help='Tenant ID to add flavor access for.') def do_flavor_access_add(cs, args): """Add flavor access for the given tenant.""" flavor = _find_flavor(cs, args.flavor) @@ -613,9 +613,9 @@ def do_flavor_access_add(cs, args): @utils.arg('flavor', metavar='', - help="Filter results by flavor name or ID.") + help="Flavor name or ID to remove access for the given tenant.") @utils.arg('tenant', metavar='', - help='Filter results by tenant ID.') + help='Tenant ID to remove flavor access for.') def do_flavor_access_remove(cs, args): """Remove flavor access for the given tenant.""" flavor = _find_flavor(cs, args.flavor) From 879c91d9d3a6c411e198c04fd149e414be15e644 Mon Sep 17 00:00:00 2001 From: Jesse Andrews Date: Fri, 6 Dec 2013 04:27:49 -0800 Subject: [PATCH 0331/1705] add support for server set metadata item add set_meta_item to the servers python API as specified by the 1.1 API Change-Id: I01c92cde18656b23b45aedd41c50d3a474d91435 --- novaclient/tests/v1_1/fakes.py | 3 +++ novaclient/tests/v1_1/test_servers.py | 5 +++++ novaclient/v1_1/servers.py | 11 +++++++++++ 3 files changed, 19 insertions(+) diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index 387ba1979..50205e72e 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -423,6 +423,9 @@ def delete_servers_1234_metadata_key2(self, **kw): def post_servers_1234_metadata(self, **kw): return (204, {}, {'metadata': {'test_key': 'test_value'}}) + def put_servers_1234_metadata_test_key(self, **kw): + return (200, {}, {'meta': {'test_key': 'test_value'}}) + def get_servers_1234_diagnostics(self, **kw): return (200, {}, {'data': 'Fake diagnostics'}) diff --git a/novaclient/tests/v1_1/test_servers.py b/novaclient/tests/v1_1/test_servers.py index 8c4c9ea70..dacaa0cf6 100644 --- a/novaclient/tests/v1_1/test_servers.py +++ b/novaclient/tests/v1_1/test_servers.py @@ -196,6 +196,11 @@ def test_set_server_meta(self): reval = cs.assert_called('POST', '/servers/1234/metadata', {'metadata': {'test_key': 'test_value'}}) + def test_set_server_meta_item(self): + s = cs.servers.set_meta_item(1234, 'test_key', 'test_value') + reval = cs.assert_called('PUT', '/servers/1234/metadata/test_key', + {'meta': {'test_key': 'test_value'}}) + def test_find(self): server = cs.servers.find(name='sample-server') cs.assert_called('GET', '/servers', pos=-2) diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index ef73d8669..3b815e8b1 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -851,6 +851,17 @@ def set_meta(self, server, metadata): return self._create("/servers/%s/metadata" % base.getid(server), body, "metadata") + def set_meta_item(self, server, key, value): + """ + Updates an item of server metadata + :param server: The :class:`Server` to add metadata to + :param key: metadata key to update + :param value: string value + """ + body = {'meta': {key: value}} + return self._update("/servers/%s/metadata/%s" % + (base.getid(server), key), body) + def get_console_output(self, server, length=None): """ Get text console log output from Server. From beaf57cfa7d0cea0dfa3c84f78400caca97b545c Mon Sep 17 00:00:00 2001 From: huangtianhua Date: Fri, 6 Dec 2013 18:22:20 +0800 Subject: [PATCH 0332/1705] Fix docstring on novaclient Something like this: 1. Modify "`Image` to add metadata to" to ""`Image` to delete metadata" on delete_meta() 2. Modify "The security group to delete" to "The security group to update" on update() 3. Remove the "password" from the description on update() Change-Id: I0120d84cc263d4eb58b692a5eb6f20ca0ef8264d Closes-Bug: #1258461 --- novaclient/v1_1/images.py | 2 +- novaclient/v1_1/security_groups.py | 2 +- novaclient/v1_1/servers.py | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/novaclient/v1_1/images.py b/novaclient/v1_1/images.py index 3a1c7da27..ba6820b64 100644 --- a/novaclient/v1_1/images.py +++ b/novaclient/v1_1/images.py @@ -92,7 +92,7 @@ def delete_meta(self, image, keys): """ Delete metadata from an image - :param image: The :class:`Image` to add metadata to + :param image: The :class:`Image` to delete metadata :param keys: A list of metadata keys to delete from the image """ for k in keys: diff --git a/novaclient/v1_1/security_groups.py b/novaclient/v1_1/security_groups.py index b929b4e20..00e1b48f0 100644 --- a/novaclient/v1_1/security_groups.py +++ b/novaclient/v1_1/security_groups.py @@ -52,7 +52,7 @@ def update(self, group, name, description): """ Update a security group - :param group: The security group to delete (group or ID) + :param group: The security group to update (group or ID) :param name: name for the security group to update :param description: description for the security group to update :rtype: the security group object diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index ef73d8669..dbbad7b22 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -43,10 +43,9 @@ def delete(self): def update(self, name=None): """ - Update the name or the password for this server. + Update the name for this server. :param name: Update the server's name. - :param password: Update the root password. """ self.manager.update(self, name=name) From e334096aa3a4badb2930c766cb4b42bbcc9ac107 Mon Sep 17 00:00:00 2001 From: Abhishek Chanda Date: Tue, 10 Dec 2013 17:03:38 +0530 Subject: [PATCH 0333/1705] Remove the release.rst file It has not been updated in a long time and does not seem to be used Closes-Bug: #1257295 Change-Id: Ie798cc5e6c64db6afcaf22102c74e8be12d3c6a2 --- doc/source/releases.rst | 99 ----------------------------------------- 1 file changed, 99 deletions(-) delete mode 100644 doc/source/releases.rst diff --git a/doc/source/releases.rst b/doc/source/releases.rst deleted file mode 100644 index 6eb034033..000000000 --- a/doc/source/releases.rst +++ /dev/null @@ -1,99 +0,0 @@ -============= -Release notes -============= - -2.5.8 (July 11, 2011) -===================== -* returns all public/private ips, not just first one -* better 'nova list' search options - -2.5.7 - 2.5.6 = minor tweaks - -2.5.5 (June 21, 2011) -===================== -* zone-boot min/max instance count added thanks to comstud -* create for user added thanks to cerberus -* fixed tests - -2.5.3 (June 15, 2011) -===================== -* ProjectID can be None for backwards compatability. -* README/docs updated for projectId thanks to usrleon - -2.5.1 (June 10, 2011) -===================== -* ProjectID now part of authentication - -2.5.0 (June 3, 2011) -================= - -* better logging thanks to GridDynamics - -2.4.4 (June 1, 2011) -================= - -* added support for GET /servers with reservation_id (and /servers/detail) - -2.4.3 (May 27, 2011) -================= - -* added support for POST /zones/select (client only, not cmdline) - -2.4 (March 7, 2011) -================= - -* added Jacob Kaplan-Moss copyright notices to older/untouched files. - - -2.3 (March 2, 2011) -================= - -* package renamed to python-novaclient. Module to novaclient - - -2.2 (March 1, 2011) -================= - -* removed some license/copywrite notices from source that wasn't - significantly changed. - - -2.1 (Feb 28, 2011) -================= - -* shell renamed to nova from novatools - -* license changed from BSD to Apache - -2.0 (Feb 7, 2011) -================= - -* Forked from https://github.com/jacobian/python-cloudservers - -* Rebranded to python-novatools - -* Auth URL support - -* New OpenStack specific commands added (pause, suspend, etc) - -1.2 (August 15, 2010) -===================== - -* Support for Python 2.4 - 2.7. - -* Improved output of :program:`cloudservers ipgroup-list`. - -* Made ``cloudservers boot --ipgroup `` work (as well as ``--ipgroup - ``). - -1.1 (May 6, 2010) -================= - -* Added a ``--files`` option to :program:`cloudservers boot` supporting - the upload of (up to five) files at boot time. - -* Added a ``--key`` option to :program:`cloudservers boot` to key the server - with an SSH public key at boot time. This is just a shortcut for ``--files``, - but it's a useful shortcut. - -* Changed the default server image to Ubuntu 10.04 LTS. From 71a9ac78a430b09567b50583bf74596d4b84f8c0 Mon Sep 17 00:00:00 2001 From: OpenStack Jenkins Date: Tue, 10 Dec 2013 22:21:52 +0000 Subject: [PATCH 0334/1705] Updated from global requirements Change-Id: I071601a07d74711828dc961b646dc066262698da --- requirements.txt | 2 +- test-requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 9583c630b..8923e6c42 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ pbr>=0.5.21,<1.0 argparse iso8601>=0.1.8 -PrettyTable>=0.6,<0.8 +PrettyTable>=0.7,<0.8 requests>=1.1 simplejson>=2.0.9 six>=1.4.1 diff --git a/test-requirements.txt b/test-requirements.txt index 862590d15..b2788bcfe 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -5,6 +5,6 @@ discover fixtures>=0.3.14 keyring>=1.6.1,<2.0 mock>=1.0 -sphinx>=1.1.2 +sphinx>=1.1.2,<1.2 testrepository>=0.0.17 testtools>=0.9.32 From 06b28391e7ba6a2e1852ed3c5f246f3043f27e85 Mon Sep 17 00:00:00 2001 From: Sushil Kumar Date: Mon, 9 Dec 2013 14:54:09 +0000 Subject: [PATCH 0335/1705] Updates tox.ini to use new features tox 1.6 allows us to skip the sdist step, which is slow. This does that. It also allows us to override the install line. In this case, it's important as it allows us to stop getting pre-release software we were not asking for. Original patch by Monty Taylor, talked about here: http://lists.openstack.org/pipermail/openstack-dev/2013-September/015495.html Change-Id: Iddf85d33b57f256d72756b516c23492e9600bedc --- tox.ini | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tox.ini b/tox.ini index 8cf7dd71f..ad8f2882a 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,11 @@ [tox] envlist = py26,py27,py33,pypy,pep8 +minversion = 1.6 +skipsdist = True [testenv] +usedevelop = True +install_command = pip install -U {opts} {packages} setenv = VIRTUAL_ENV={envdir} LANG=en_US.UTF-8 LANGUAGE=en_US:en From 547f357b12aaffadaa6ea67d10ee3109a9a968c6 Mon Sep 17 00:00:00 2001 From: Sushil Kumar Date: Wed, 11 Dec 2013 12:02:56 +0000 Subject: [PATCH 0336/1705] Enables E711,E721,E712 pep8 rules Updates tox.ini to reduce ignored rules. Updates novaclient/v1_1/shell.py and novaclient/v3/shell.py for E712 violation. Change-Id: Ibfd6a4ed19835e65cc9e27873699c31a801e99a8 --- novaclient/v1_1/shell.py | 4 ++-- novaclient/v3/shell.py | 4 ++-- tox.ini | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 99efac424..e9009cf85 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2126,7 +2126,7 @@ def _get_secgroup(cs, secgroup): if not six.PY3: s.name = s.name.encode(encoding) if secgroup == s.name: - if match_found != False: + if match_found is not False: msg = ("Multiple security group matches found for name" " '%s', use an ID to be more specific." % secgroup) raise exceptions.NoUniqueMatch(msg) @@ -2883,7 +2883,7 @@ def do_coverage_stop(cs, args): help='Generate XML reports instead of text ones.') def do_coverage_report(cs, args): """Generate coverage report.""" - if args.html == True and args.xml == True: + if args.html is True and args.xml is True: raise exceptions.CommandError("--html and --xml must not be " "specified together.") cov = cs.coverage.report(args.filename, xml=args.xml, html=args.html) diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index a84cc039a..016f4a5ed 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -1949,7 +1949,7 @@ def _get_secgroup(cs, secgroup): 'UTF-8') s.name = s.name.encode(encoding) if secgroup == s.name: - if match_found != False: + if match_found is not False: msg = ("Multiple security group matches found for name" " '%s', use an ID to be more specific." % secgroup) raise exceptions.NoUniqueMatch(msg) @@ -2706,7 +2706,7 @@ def do_coverage_stop(cs, args): help='Generate XML reports instead of text ones.') def do_coverage_report(cs, args): """Generate coverage report.""" - if args.html == True and args.xml == True: + if args.html is True and args.xml is True: raise exceptions.CommandError("--html and --xml must not be " "specified together.") cov = cs.coverage.report(args.filename, xml=args.xml, html=args.html) diff --git a/tox.ini b/tox.ini index ad8f2882a..99286c2bd 100644 --- a/tox.ini +++ b/tox.ini @@ -28,6 +28,6 @@ commands = python setup.py testr --coverage --testr-args='{posargs}' downloadcache = ~/cache/pip [flake8] -ignore = E12,E711,E721,E712,F841,F811,F821,H302,H306,H403,H404 +ignore = E12,F841,F811,F821,H302,H306,H403,H404 show-source = True exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build From 3ecaeb416ed77de4033bf6839208788b9d5f066e Mon Sep 17 00:00:00 2001 From: Sushil Kumar Date: Wed, 11 Dec 2013 17:38:56 +0000 Subject: [PATCH 0337/1705] Enables H306 pep8 rules Updates tox.ini to reduce ignored rules. Updates code for H306 violation. Change-Id: I9beea02510a1122afb55ad3df3b37f770fe05134 --- novaclient/client.py | 2 +- novaclient/tests/test_auth_plugins.py | 2 +- novaclient/tests/test_base.py | 2 +- novaclient/tests/test_client.py | 2 +- novaclient/tests/test_discover.py | 3 ++- novaclient/tests/test_utils.py | 4 ++-- novaclient/tests/v1_1/contrib/fakes.py | 2 +- novaclient/tests/v1_1/fakes.py | 2 +- novaclient/tests/v1_1/test_agents.py | 4 ++-- novaclient/tests/v1_1/test_aggregates.py | 2 +- novaclient/tests/v1_1/test_auth.py | 4 ++-- novaclient/tests/v1_1/test_availability_zone.py | 4 ++-- novaclient/tests/v1_1/test_certs.py | 2 +- novaclient/tests/v1_1/test_cloudpipe.py | 2 +- novaclient/tests/v1_1/test_fixed_ips.py | 2 +- novaclient/tests/v1_1/test_flavor_access.py | 2 +- novaclient/tests/v1_1/test_flavors.py | 2 +- novaclient/tests/v1_1/test_floating_ip_dns.py | 4 ++-- novaclient/tests/v1_1/test_floating_ip_pools.py | 2 +- novaclient/tests/v1_1/test_floating_ips.py | 2 +- novaclient/tests/v1_1/test_floating_ips_bulk.py | 3 ++- novaclient/tests/v1_1/test_fping.py | 2 +- novaclient/tests/v1_1/test_hosts.py | 4 ++-- novaclient/tests/v1_1/test_images.py | 2 +- novaclient/tests/v1_1/test_keypairs.py | 2 +- novaclient/tests/v1_1/test_limits.py | 2 +- novaclient/tests/v1_1/test_networks.py | 2 +- .../tests/v1_1/test_security_group_rules.py | 2 +- novaclient/tests/v1_1/test_security_groups.py | 2 +- novaclient/tests/v1_1/test_servers.py | 2 +- novaclient/tests/v1_1/test_services.py | 4 ++-- novaclient/tests/v1_1/test_shell.py | 4 ++-- novaclient/tests/v1_1/test_usage.py | 2 +- novaclient/tests/v1_1/test_volumes.py | 2 +- novaclient/tests/v3/fakes.py | 2 +- novaclient/tests/v3/test_hosts.py | 4 ++-- novaclient/v1_1/client.py | 16 ++++++++-------- novaclient/v1_1/flavors.py | 2 +- tox.ini | 2 +- 39 files changed, 57 insertions(+), 55 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index 0cbfa8bdd..3cb18798d 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -31,9 +31,9 @@ import simplejson as json from novaclient import exceptions +from novaclient.openstack.common.py3kcompat import urlutils from novaclient import service_catalog from novaclient import utils -from novaclient.openstack.common.py3kcompat import urlutils class HTTPClient(object): diff --git a/novaclient/tests/test_auth_plugins.py b/novaclient/tests/test_auth_plugins.py index 96217991a..a08459416 100644 --- a/novaclient/tests/test_auth_plugins.py +++ b/novaclient/tests/test_auth_plugins.py @@ -25,8 +25,8 @@ from novaclient import auth_plugin from novaclient import exceptions -from novaclient.v1_1 import client from novaclient.tests import utils +from novaclient.v1_1 import client def mock_http_request(resp=None): diff --git a/novaclient/tests/test_base.py b/novaclient/tests/test_base.py index 6f9ba7c15..717dc6ea5 100644 --- a/novaclient/tests/test_base.py +++ b/novaclient/tests/test_base.py @@ -13,9 +13,9 @@ from novaclient import base from novaclient import exceptions -from novaclient.v1_1 import flavors from novaclient.tests import utils from novaclient.tests.v1_1 import fakes +from novaclient.v1_1 import flavors cs = fakes.FakeClient() diff --git a/novaclient/tests/test_client.py b/novaclient/tests/test_client.py index 45c05e459..ded0d9add 100644 --- a/novaclient/tests/test_client.py +++ b/novaclient/tests/test_client.py @@ -20,9 +20,9 @@ import novaclient.client import novaclient.extension import novaclient.tests.fakes as fakes +from novaclient.tests import utils import novaclient.v1_1.client import novaclient.v3.client -from novaclient.tests import utils import json diff --git a/novaclient/tests/test_discover.py b/novaclient/tests/test_discover.py index 29c9e4756..e4acf0be5 100644 --- a/novaclient/tests/test_discover.py +++ b/novaclient/tests/test_discover.py @@ -13,9 +13,10 @@ # License for the specific language governing permissions and limitations # under the License. -import mock import imp import inspect + +import mock import pkg_resources import novaclient.shell diff --git a/novaclient/tests/test_utils.py b/novaclient/tests/test_utils.py index 4400ab010..bc63c95c9 100644 --- a/novaclient/tests/test_utils.py +++ b/novaclient/tests/test_utils.py @@ -16,10 +16,10 @@ import mock import six -from novaclient import exceptions -from novaclient import utils from novaclient import base +from novaclient import exceptions from novaclient.tests import utils as test_utils +from novaclient import utils UUID = '8e8ec658-c7b0-4243-bdf8-6f7f2952c0d0' diff --git a/novaclient/tests/v1_1/contrib/fakes.py b/novaclient/tests/v1_1/contrib/fakes.py index 1d679f352..6f4608e82 100644 --- a/novaclient/tests/v1_1/contrib/fakes.py +++ b/novaclient/tests/v1_1/contrib/fakes.py @@ -12,8 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -from novaclient.v1_1 import client from novaclient.tests.v1_1 import fakes +from novaclient.v1_1 import client class FakeClient(fakes.FakeClient): diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index 6ed88a11d..a6699c0b1 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -20,8 +20,8 @@ from novaclient import client as base_client from novaclient import exceptions -from novaclient.openstack.common import strutils from novaclient.openstack.common.py3kcompat import urlutils +from novaclient.openstack.common import strutils from novaclient.tests import fakes from novaclient.tests import utils from novaclient.v1_1 import client diff --git a/novaclient/tests/v1_1/test_agents.py b/novaclient/tests/v1_1/test_agents.py index c9eaf3787..222d03153 100644 --- a/novaclient/tests/v1_1/test_agents.py +++ b/novaclient/tests/v1_1/test_agents.py @@ -15,9 +15,9 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.v1_1 import agents -from novaclient.tests.v1_1 import fakes from novaclient.tests import utils +from novaclient.tests.v1_1 import fakes +from novaclient.v1_1 import agents cs = fakes.FakeClient() diff --git a/novaclient/tests/v1_1/test_aggregates.py b/novaclient/tests/v1_1/test_aggregates.py index bbd7f2c10..3367b9850 100644 --- a/novaclient/tests/v1_1/test_aggregates.py +++ b/novaclient/tests/v1_1/test_aggregates.py @@ -13,9 +13,9 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.v1_1 import aggregates from novaclient.tests import utils from novaclient.tests.v1_1 import fakes +from novaclient.v1_1 import aggregates cs = fakes.FakeClient() diff --git a/novaclient/tests/v1_1/test_auth.py b/novaclient/tests/v1_1/test_auth.py index 0ecfd1e5e..1754f8a2a 100644 --- a/novaclient/tests/v1_1/test_auth.py +++ b/novaclient/tests/v1_1/test_auth.py @@ -13,13 +13,13 @@ import copy import json -import mock +import mock import requests -from novaclient.v1_1 import client from novaclient import exceptions from novaclient.tests import utils +from novaclient.v1_1 import client class AuthenticateAgainstKeystoneTests(utils.TestCase): diff --git a/novaclient/tests/v1_1/test_availability_zone.py b/novaclient/tests/v1_1/test_availability_zone.py index 93cb6bebf..fe9e885f4 100644 --- a/novaclient/tests/v1_1/test_availability_zone.py +++ b/novaclient/tests/v1_1/test_availability_zone.py @@ -16,10 +16,10 @@ import six +from novaclient.tests import utils +from novaclient.tests.v1_1 import fakes from novaclient.v1_1 import availability_zones from novaclient.v1_1 import shell -from novaclient.tests.v1_1 import fakes -from novaclient.tests import utils cs = fakes.FakeClient() diff --git a/novaclient/tests/v1_1/test_certs.py b/novaclient/tests/v1_1/test_certs.py index 3d1ca0907..c08ac90ed 100644 --- a/novaclient/tests/v1_1/test_certs.py +++ b/novaclient/tests/v1_1/test_certs.py @@ -11,9 +11,9 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.v1_1 import certs from novaclient.tests import utils from novaclient.tests.v1_1 import fakes +from novaclient.v1_1 import certs cs = fakes.FakeClient() diff --git a/novaclient/tests/v1_1/test_cloudpipe.py b/novaclient/tests/v1_1/test_cloudpipe.py index f98c2b24b..a54aa4f47 100644 --- a/novaclient/tests/v1_1/test_cloudpipe.py +++ b/novaclient/tests/v1_1/test_cloudpipe.py @@ -11,9 +11,9 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.v1_1 import cloudpipe from novaclient.tests import utils from novaclient.tests.v1_1 import fakes +from novaclient.v1_1 import cloudpipe cs = fakes.FakeClient() diff --git a/novaclient/tests/v1_1/test_fixed_ips.py b/novaclient/tests/v1_1/test_fixed_ips.py index 6881d1b40..550784c41 100644 --- a/novaclient/tests/v1_1/test_fixed_ips.py +++ b/novaclient/tests/v1_1/test_fixed_ips.py @@ -15,8 +15,8 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.tests.v1_1 import fakes from novaclient.tests import utils +from novaclient.tests.v1_1 import fakes cs = fakes.FakeClient() diff --git a/novaclient/tests/v1_1/test_flavor_access.py b/novaclient/tests/v1_1/test_flavor_access.py index aea57a511..1ed58f4f0 100644 --- a/novaclient/tests/v1_1/test_flavor_access.py +++ b/novaclient/tests/v1_1/test_flavor_access.py @@ -13,9 +13,9 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.v1_1 import flavor_access from novaclient.tests import utils from novaclient.tests.v1_1 import fakes +from novaclient.v1_1 import flavor_access cs = fakes.FakeClient() diff --git a/novaclient/tests/v1_1/test_flavors.py b/novaclient/tests/v1_1/test_flavors.py index a50783bdd..c10d925d3 100644 --- a/novaclient/tests/v1_1/test_flavors.py +++ b/novaclient/tests/v1_1/test_flavors.py @@ -14,9 +14,9 @@ # under the License. from novaclient import exceptions -from novaclient.v1_1 import flavors from novaclient.tests import utils from novaclient.tests.v1_1 import fakes +from novaclient.v1_1 import flavors cs = fakes.FakeClient() diff --git a/novaclient/tests/v1_1/test_floating_ip_dns.py b/novaclient/tests/v1_1/test_floating_ip_dns.py index a5714f4da..25a3a22e3 100644 --- a/novaclient/tests/v1_1/test_floating_ip_dns.py +++ b/novaclient/tests/v1_1/test_floating_ip_dns.py @@ -11,9 +11,9 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.v1_1 import floating_ip_dns -from novaclient.tests.v1_1 import fakes from novaclient.tests import utils +from novaclient.tests.v1_1 import fakes +from novaclient.v1_1 import floating_ip_dns cs = fakes.FakeClient() diff --git a/novaclient/tests/v1_1/test_floating_ip_pools.py b/novaclient/tests/v1_1/test_floating_ip_pools.py index ba0fcb517..f0d080914 100644 --- a/novaclient/tests/v1_1/test_floating_ip_pools.py +++ b/novaclient/tests/v1_1/test_floating_ip_pools.py @@ -14,9 +14,9 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.v1_1 import floating_ip_pools from novaclient.tests import utils from novaclient.tests.v1_1 import fakes +from novaclient.v1_1 import floating_ip_pools cs = fakes.FakeClient() diff --git a/novaclient/tests/v1_1/test_floating_ips.py b/novaclient/tests/v1_1/test_floating_ips.py index 04088e097..90aaaa208 100644 --- a/novaclient/tests/v1_1/test_floating_ips.py +++ b/novaclient/tests/v1_1/test_floating_ips.py @@ -14,9 +14,9 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.v1_1 import floating_ips from novaclient.tests import utils from novaclient.tests.v1_1 import fakes +from novaclient.v1_1 import floating_ips cs = fakes.FakeClient() diff --git a/novaclient/tests/v1_1/test_floating_ips_bulk.py b/novaclient/tests/v1_1/test_floating_ips_bulk.py index 27acd5c7a..8ad2ac4a8 100644 --- a/novaclient/tests/v1_1/test_floating_ips_bulk.py +++ b/novaclient/tests/v1_1/test_floating_ips_bulk.py @@ -14,9 +14,10 @@ # 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 novaclient.v1_1 import floating_ips_bulk + from novaclient.tests import utils from novaclient.tests.v1_1 import fakes +from novaclient.v1_1 import floating_ips_bulk cs = fakes.FakeClient() diff --git a/novaclient/tests/v1_1/test_fping.py b/novaclient/tests/v1_1/test_fping.py index 7f240e299..165caf57a 100644 --- a/novaclient/tests/v1_1/test_fping.py +++ b/novaclient/tests/v1_1/test_fping.py @@ -15,9 +15,9 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.v1_1 import fping from novaclient.tests import utils from novaclient.tests.v1_1 import fakes +from novaclient.v1_1 import fping cs = fakes.FakeClient() diff --git a/novaclient/tests/v1_1/test_hosts.py b/novaclient/tests/v1_1/test_hosts.py index cfaeeacca..ce18d960b 100644 --- a/novaclient/tests/v1_1/test_hosts.py +++ b/novaclient/tests/v1_1/test_hosts.py @@ -11,9 +11,9 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.v1_1 import hosts -from novaclient.tests.v1_1 import fakes from novaclient.tests import utils +from novaclient.tests.v1_1 import fakes +from novaclient.v1_1 import hosts cs = fakes.FakeClient() diff --git a/novaclient/tests/v1_1/test_images.py b/novaclient/tests/v1_1/test_images.py index 33c8adb61..481553a95 100644 --- a/novaclient/tests/v1_1/test_images.py +++ b/novaclient/tests/v1_1/test_images.py @@ -11,9 +11,9 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.v1_1 import images from novaclient.tests import utils from novaclient.tests.v1_1 import fakes +from novaclient.v1_1 import images cs = fakes.FakeClient() diff --git a/novaclient/tests/v1_1/test_keypairs.py b/novaclient/tests/v1_1/test_keypairs.py index 1318bd11d..a6b674228 100644 --- a/novaclient/tests/v1_1/test_keypairs.py +++ b/novaclient/tests/v1_1/test_keypairs.py @@ -11,9 +11,9 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.v1_1 import keypairs from novaclient.tests import utils from novaclient.tests.v1_1 import fakes +from novaclient.v1_1 import keypairs cs = fakes.FakeClient() diff --git a/novaclient/tests/v1_1/test_limits.py b/novaclient/tests/v1_1/test_limits.py index d97300f5f..92c8f7c4c 100644 --- a/novaclient/tests/v1_1/test_limits.py +++ b/novaclient/tests/v1_1/test_limits.py @@ -11,9 +11,9 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.v1_1 import limits from novaclient.tests import utils from novaclient.tests.v1_1 import fakes +from novaclient.v1_1 import limits cs = fakes.FakeClient() diff --git a/novaclient/tests/v1_1/test_networks.py b/novaclient/tests/v1_1/test_networks.py index cba8393fd..f4e973754 100644 --- a/novaclient/tests/v1_1/test_networks.py +++ b/novaclient/tests/v1_1/test_networks.py @@ -11,9 +11,9 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.v1_1 import networks from novaclient.tests import utils from novaclient.tests.v1_1 import fakes +from novaclient.v1_1 import networks cs = fakes.FakeClient() diff --git a/novaclient/tests/v1_1/test_security_group_rules.py b/novaclient/tests/v1_1/test_security_group_rules.py index 02230e9b8..a7259619d 100644 --- a/novaclient/tests/v1_1/test_security_group_rules.py +++ b/novaclient/tests/v1_1/test_security_group_rules.py @@ -12,9 +12,9 @@ # under the License. from novaclient import exceptions -from novaclient.v1_1 import security_group_rules from novaclient.tests import utils from novaclient.tests.v1_1 import fakes +from novaclient.v1_1 import security_group_rules cs = fakes.FakeClient() diff --git a/novaclient/tests/v1_1/test_security_groups.py b/novaclient/tests/v1_1/test_security_groups.py index ac6e0aa75..c06eaab2a 100644 --- a/novaclient/tests/v1_1/test_security_groups.py +++ b/novaclient/tests/v1_1/test_security_groups.py @@ -11,9 +11,9 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.v1_1 import security_groups from novaclient.tests import utils from novaclient.tests.v1_1 import fakes +from novaclient.v1_1 import security_groups cs = fakes.FakeClient() diff --git a/novaclient/tests/v1_1/test_servers.py b/novaclient/tests/v1_1/test_servers.py index 4861ad2c7..f13fa7577 100644 --- a/novaclient/tests/v1_1/test_servers.py +++ b/novaclient/tests/v1_1/test_servers.py @@ -16,9 +16,9 @@ import six from novaclient import exceptions -from novaclient.v1_1 import servers from novaclient.tests import utils from novaclient.tests.v1_1 import fakes +from novaclient.v1_1 import servers cs = fakes.FakeClient() diff --git a/novaclient/tests/v1_1/test_services.py b/novaclient/tests/v1_1/test_services.py index ddd43bc8a..85e4f345c 100644 --- a/novaclient/tests/v1_1/test_services.py +++ b/novaclient/tests/v1_1/test_services.py @@ -15,9 +15,9 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.v1_1 import services -from novaclient.tests.v1_1 import fakes from novaclient.tests import utils +from novaclient.tests.v1_1 import fakes +from novaclient.v1_1 import services cs = fakes.FakeClient() diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index c8758cff3..c02dc3dcf 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -19,17 +19,17 @@ import base64 import datetime import os -import mock import fixtures +import mock import six import novaclient.client from novaclient import exceptions from novaclient.openstack.common import timeutils import novaclient.shell -from novaclient.tests.v1_1 import fakes from novaclient.tests import utils +from novaclient.tests.v1_1 import fakes import novaclient.v1_1.shell diff --git a/novaclient/tests/v1_1/test_usage.py b/novaclient/tests/v1_1/test_usage.py index 09546966f..01df9fe70 100644 --- a/novaclient/tests/v1_1/test_usage.py +++ b/novaclient/tests/v1_1/test_usage.py @@ -13,9 +13,9 @@ import datetime -from novaclient.v1_1 import usage from novaclient.tests import utils from novaclient.tests.v1_1 import fakes +from novaclient.v1_1 import usage cs = fakes.FakeClient() diff --git a/novaclient/tests/v1_1/test_volumes.py b/novaclient/tests/v1_1/test_volumes.py index 131a032d7..6c52f6bbc 100644 --- a/novaclient/tests/v1_1/test_volumes.py +++ b/novaclient/tests/v1_1/test_volumes.py @@ -13,9 +13,9 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.v1_1 import volumes from novaclient.tests import utils from novaclient.tests.v1_1 import fakes +from novaclient.v1_1 import volumes cs = fakes.FakeClient() diff --git a/novaclient/tests/v3/fakes.py b/novaclient/tests/v3/fakes.py index a8c3788ab..a3815f5e4 100644 --- a/novaclient/tests/v3/fakes.py +++ b/novaclient/tests/v3/fakes.py @@ -14,9 +14,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -from novaclient.v3 import client from novaclient.tests import fakes from novaclient.tests.v1_1 import fakes as fakes_v1_1 +from novaclient.v3 import client class FakeClient(fakes.FakeClient, client.Client): diff --git a/novaclient/tests/v3/test_hosts.py b/novaclient/tests/v3/test_hosts.py index 15bda0e85..ff158b920 100644 --- a/novaclient/tests/v3/test_hosts.py +++ b/novaclient/tests/v3/test_hosts.py @@ -12,9 +12,9 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.v3 import hosts -from novaclient.tests.v3 import fakes from novaclient.tests import utils +from novaclient.tests.v3 import fakes +from novaclient.v3 import hosts cs = fakes.FakeClient() diff --git a/novaclient/v1_1/client.py b/novaclient/v1_1/client.py index 50ab46cff..247944510 100644 --- a/novaclient/v1_1/client.py +++ b/novaclient/v1_1/client.py @@ -16,16 +16,18 @@ from novaclient import client from novaclient.v1_1 import agents -from novaclient.v1_1 import certs -from novaclient.v1_1 import cloudpipe from novaclient.v1_1 import aggregates from novaclient.v1_1 import availability_zones +from novaclient.v1_1 import certs +from novaclient.v1_1 import cloudpipe from novaclient.v1_1 import coverage_ext -from novaclient.v1_1 import flavors +from novaclient.v1_1 import fixed_ips from novaclient.v1_1 import flavor_access +from novaclient.v1_1 import flavors from novaclient.v1_1 import floating_ip_dns -from novaclient.v1_1 import floating_ips from novaclient.v1_1 import floating_ip_pools +from novaclient.v1_1 import floating_ips +from novaclient.v1_1 import floating_ips_bulk from novaclient.v1_1 import fping from novaclient.v1_1 import hosts from novaclient.v1_1 import hypervisors @@ -38,14 +40,12 @@ from novaclient.v1_1 import security_group_rules from novaclient.v1_1 import security_groups from novaclient.v1_1 import servers +from novaclient.v1_1 import services from novaclient.v1_1 import usage from novaclient.v1_1 import virtual_interfaces -from novaclient.v1_1 import volumes from novaclient.v1_1 import volume_snapshots from novaclient.v1_1 import volume_types -from novaclient.v1_1 import services -from novaclient.v1_1 import fixed_ips -from novaclient.v1_1 import floating_ips_bulk +from novaclient.v1_1 import volumes class Client(object): diff --git a/novaclient/v1_1/flavors.py b/novaclient/v1_1/flavors.py index 19a3808cd..28ca2c7b9 100644 --- a/novaclient/v1_1/flavors.py +++ b/novaclient/v1_1/flavors.py @@ -17,8 +17,8 @@ """ from novaclient import base from novaclient import exceptions -from novaclient import utils from novaclient.openstack.common.py3kcompat import urlutils +from novaclient import utils class Flavor(base.Resource): diff --git a/tox.ini b/tox.ini index 99286c2bd..9ef68cca4 100644 --- a/tox.ini +++ b/tox.ini @@ -28,6 +28,6 @@ commands = python setup.py testr --coverage --testr-args='{posargs}' downloadcache = ~/cache/pip [flake8] -ignore = E12,F841,F811,F821,H302,H306,H403,H404 +ignore = E12,F841,F811,F821,H302,H403,H404 show-source = True exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build From 589f665e29381d7ce8df9db6f7ade5302af02ef1 Mon Sep 17 00:00:00 2001 From: Abhishek Chanda Date: Tue, 26 Nov 2013 23:00:01 -0800 Subject: [PATCH 0338/1705] Allow graceful shutdown on Ctrl+C Currently it prints a stacktrace and exits. This patch makes it print a message and exit gracefully. Closes-Bug: #1256732 Change-Id: I59ae8d09be43cd3e56dbe9bed4f6f328377dcc13 --- novaclient/shell.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/novaclient/shell.py b/novaclient/shell.py index ca0770e38..09acf6b09 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -725,6 +725,9 @@ def main(): print("ERROR: %s" % strutils.safe_encode(six.text_type(e)), file=sys.stderr) sys.exit(1) + except KeyboardInterrupt as e: + print("Shutting down novaclient", file=sys.stderr) + sys.exit(1) if __name__ == "__main__": From 953734d72752aaabe2f0c5f0526cf25a64b3a8b8 Mon Sep 17 00:00:00 2001 From: Sushil Kumar Date: Thu, 12 Dec 2013 03:55:50 +0000 Subject: [PATCH 0339/1705] Enables H403 pep8 rules Updates tox.ini to reduce ignored rules. Updates code for H403 violation. Change-Id: Iee7b34a27c62ce8cb0f26166b3c16e3386e2fecd --- novaclient/client.py | 3 ++- novaclient/crypto.py | 3 ++- novaclient/exceptions.py | 6 ++++-- novaclient/service_catalog.py | 3 ++- novaclient/v1_1/quota_classes.py | 3 ++- novaclient/v1_1/quotas.py | 3 ++- tox.ini | 2 +- 7 files changed, 15 insertions(+), 8 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index 3cb18798d..0ae8bb285 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -254,7 +254,8 @@ def delete(self, url, **kwargs): def _extract_service_catalog(self, url, resp, body, extract_token=True): """See what the auth service told us and process the response. We may get redirected to another site, fail or actually get - back a service catalog with a token and our endpoints.""" + back a service catalog with a token and our endpoints. + """ # content must always present if resp.status_code == 200 or resp.status_code == 201: diff --git a/novaclient/crypto.py b/novaclient/crypto.py index a264823ad..d817be5bb 100644 --- a/novaclient/crypto.py +++ b/novaclient/crypto.py @@ -24,7 +24,8 @@ class DecryptionFailure(Exception): def decrypt_password(private_key, password): """Base64 decodes password and unecrypts it with private key. - Requires openssl binary available in the path""" + Requires openssl binary available in the path. + """ unencoded = base64.b64decode(password) cmd = ['openssl', 'rsautl', '-decrypt', '-inkey', private_key] proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, diff --git a/novaclient/exceptions.py b/novaclient/exceptions.py index 6ff93d720..73049cda5 100644 --- a/novaclient/exceptions.py +++ b/novaclient/exceptions.py @@ -19,7 +19,8 @@ class UnsupportedVersion(Exception): """Indicates that the user is trying to use an unsupported - version of the API""" + version of the API. + """ pass @@ -46,7 +47,8 @@ def __str__(self): class NoTokenLookupException(Exception): """This form of authentication does not support looking up - endpoints from an existing token.""" + endpoints from an existing token. + """ pass diff --git a/novaclient/service_catalog.py b/novaclient/service_catalog.py index 2c4c99716..26f4ccc5c 100644 --- a/novaclient/service_catalog.py +++ b/novaclient/service_catalog.py @@ -36,7 +36,8 @@ def url_for(self, attr=None, filter_value=None, service_name=None, volume_service_name=None): """Fetch the public URL from the Compute service for a particular endpoint attribute. If none given, return - the first. See tests for sample service catalog.""" + the first. See tests for sample service catalog. + """ matching_endpoints = [] if 'endpoints' in self.catalog: # We have a bastardized service catalog. Treat it special. :/ diff --git a/novaclient/v1_1/quota_classes.py b/novaclient/v1_1/quota_classes.py index 3f2908e5e..b57d7071b 100644 --- a/novaclient/v1_1/quota_classes.py +++ b/novaclient/v1_1/quota_classes.py @@ -21,7 +21,8 @@ class QuotaClassSet(base.Resource): @property def id(self): """QuotaClassSet does not have a 'id' attribute but base.Resource - needs it to self-refresh and QuotaSet is indexed by class_name""" + needs it to self-refresh and QuotaSet is indexed by class_name. + """ return self.class_name def update(self, *args, **kwargs): diff --git a/novaclient/v1_1/quotas.py b/novaclient/v1_1/quotas.py index bd5bd76e1..601bcebe9 100644 --- a/novaclient/v1_1/quotas.py +++ b/novaclient/v1_1/quotas.py @@ -21,7 +21,8 @@ class QuotaSet(base.Resource): @property def id(self): """QuotaSet does not have a 'id' attribute but base.Resource needs it - to self-refresh and QuotaSet is indexed by tenant_id""" + to self-refresh and QuotaSet is indexed by tenant_id. + """ return self.tenant_id def update(self, *args, **kwargs): diff --git a/tox.ini b/tox.ini index 9ef68cca4..d7d2b3234 100644 --- a/tox.ini +++ b/tox.ini @@ -28,6 +28,6 @@ commands = python setup.py testr --coverage --testr-args='{posargs}' downloadcache = ~/cache/pip [flake8] -ignore = E12,F841,F811,F821,H302,H403,H404 +ignore = E12,F841,F811,F821,H302,H404 show-source = True exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build From 9329f435bfa555046c05bed8f73f02ace71c21a8 Mon Sep 17 00:00:00 2001 From: Chris Yeoh Date: Wed, 4 Dec 2013 00:34:08 +1030 Subject: [PATCH 0340/1705] Adds flavor support for Nova V3 API The first part of adding flavor support for the Nova V3 API (support for flavor-access API calls is still to come). Restructures v1_1 testcases so as much as possible so they can be reused for the V3 version of the tests. If it looks too ugly an alternative would be to just cut and paste the fakes and test from v1_1 to v3, which is simpler to understand but comes at the cost of duplicated code. The upside though is it would be much easier to remove v1_1/v2 support in the future. Partially implements blueprint v3-api Change-Id: Ic7cb3c43db02c07d37aea2675b310aaa50639c40 --- novaclient/tests/v1_1/test_flavors.py | 183 ++++++++++++-------------- novaclient/tests/v3/fakes.py | 49 +++++++ novaclient/tests/v3/test_flavors.py | 57 ++++++++ novaclient/v1_1/flavors.py | 31 +++-- novaclient/v3/client.py | 2 + novaclient/v3/flavors.py | 99 ++++++++++++++ 6 files changed, 312 insertions(+), 109 deletions(-) create mode 100644 novaclient/tests/v3/test_flavors.py create mode 100644 novaclient/v3/flavors.py diff --git a/novaclient/tests/v1_1/test_flavors.py b/novaclient/tests/v1_1/test_flavors.py index c10d925d3..eb7bf7052 100644 --- a/novaclient/tests/v1_1/test_flavors.py +++ b/novaclient/tests/v1_1/test_flavors.py @@ -19,186 +19,177 @@ from novaclient.v1_1 import flavors -cs = fakes.FakeClient() +class FlavorsTest(utils.TestCase): + def setUp(self): + super(FlavorsTest, self).setUp() + self.cs = self._get_fake_client() + self.flavor_type = self._get_flavor_type() + def _get_fake_client(self): + return fakes.FakeClient() -class FlavorsTest(utils.TestCase): + def _get_flavor_type(self): + return flavors.Flavor def test_list_flavors(self): - fl = cs.flavors.list() - cs.assert_called('GET', '/flavors/detail') + fl = self.cs.flavors.list() + self.cs.assert_called('GET', '/flavors/detail') for flavor in fl: - self.assertTrue(isinstance(flavor, flavors.Flavor)) + self.assertTrue(isinstance(flavor, self.flavor_type)) def test_list_flavors_undetailed(self): - fl = cs.flavors.list(detailed=False) - cs.assert_called('GET', '/flavors') + fl = self.cs.flavors.list(detailed=False) + self.cs.assert_called('GET', '/flavors') for flavor in fl: - self.assertTrue(isinstance(flavor, flavors.Flavor)) + self.assertTrue(isinstance(flavor, self.flavor_type)) def test_list_flavors_is_public_none(self): - fl = cs.flavors.list(is_public=None) - cs.assert_called('GET', '/flavors/detail?is_public=None') + fl = self.cs.flavors.list(is_public=None) + self.cs.assert_called('GET', '/flavors/detail?is_public=None') for flavor in fl: - self.assertTrue(isinstance(flavor, flavors.Flavor)) + self.assertTrue(isinstance(flavor, self.flavor_type)) def test_list_flavors_is_public_false(self): - fl = cs.flavors.list(is_public=False) - cs.assert_called('GET', '/flavors/detail?is_public=False') + fl = self.cs.flavors.list(is_public=False) + self.cs.assert_called('GET', '/flavors/detail?is_public=False') for flavor in fl: - self.assertTrue(isinstance(flavor, flavors.Flavor)) + self.assertTrue(isinstance(flavor, self.flavor_type)) def test_list_flavors_is_public_true(self): - fl = cs.flavors.list(is_public=True) - cs.assert_called('GET', '/flavors/detail') + fl = self.cs.flavors.list(is_public=True) + self.cs.assert_called('GET', '/flavors/detail') for flavor in fl: - self.assertTrue(isinstance(flavor, flavors.Flavor)) + self.assertTrue(isinstance(flavor, self.flavor_type)) def test_get_flavor_details(self): - f = cs.flavors.get(1) - cs.assert_called('GET', '/flavors/1') - self.assertTrue(isinstance(f, flavors.Flavor)) + f = self.cs.flavors.get(1) + self.cs.assert_called('GET', '/flavors/1') + self.assertTrue(isinstance(f, self.flavor_type)) self.assertEqual(f.ram, 256) self.assertEqual(f.disk, 10) self.assertEqual(f.ephemeral, 10) self.assertEqual(f.is_public, True) def test_get_flavor_details_alphanum_id(self): - f = cs.flavors.get('aa1') - cs.assert_called('GET', '/flavors/aa1') - self.assertTrue(isinstance(f, flavors.Flavor)) + f = self.cs.flavors.get('aa1') + self.cs.assert_called('GET', '/flavors/aa1') + self.assertTrue(isinstance(f, self.flavor_type)) self.assertEqual(f.ram, 128) self.assertEqual(f.disk, 0) self.assertEqual(f.ephemeral, 0) self.assertEqual(f.is_public, True) def test_get_flavor_details_diablo(self): - f = cs.flavors.get(3) - cs.assert_called('GET', '/flavors/3') - self.assertTrue(isinstance(f, flavors.Flavor)) + f = self.cs.flavors.get(3) + self.cs.assert_called('GET', '/flavors/3') + self.assertTrue(isinstance(f, self.flavor_type)) self.assertEqual(f.ram, 256) self.assertEqual(f.disk, 10) self.assertEqual(f.ephemeral, 'N/A') self.assertEqual(f.is_public, 'N/A') def test_find(self): - f = cs.flavors.find(ram=256) - cs.assert_called('GET', '/flavors/detail') + f = self.cs.flavors.find(ram=256) + self.cs.assert_called('GET', '/flavors/detail') self.assertEqual(f.name, '256 MB Server') - f = cs.flavors.find(disk=0) + f = self.cs.flavors.find(disk=0) self.assertEqual(f.name, '128 MB Server') - self.assertRaises(exceptions.NotFound, cs.flavors.find, disk=12345) + self.assertRaises(exceptions.NotFound, self.cs.flavors.find, + disk=12345) - def test_create(self): - f = cs.flavors.create("flavorcreate", 512, 1, 10, 1234, ephemeral=10, - is_public=False) - - body = { + def _create_body(self, name, ram, vcpus, disk, ephemeral, id, swap, + rxtx_factor, is_public): + return { "flavor": { - "name": "flavorcreate", - "ram": 512, - "vcpus": 1, - "disk": 10, - "OS-FLV-EXT-DATA:ephemeral": 10, - "id": 1234, - "swap": 0, - "rxtx_factor": 1.0, - "os-flavor-access:is_public": False, + "name": name, + "ram": ram, + "vcpus": vcpus, + "disk": disk, + "OS-FLV-EXT-DATA:ephemeral": ephemeral, + "id": id, + "swap": swap, + "rxtx_factor": rxtx_factor, + "os-flavor-access:is_public": is_public, } } - cs.assert_called('POST', '/flavors', body) - self.assertTrue(isinstance(f, flavors.Flavor)) + def test_create(self): + f = self.cs.flavors.create("flavorcreate", 512, 1, 10, 1234, + ephemeral=10, is_public=False) + + body = self._create_body("flavorcreate", 512, 1, 10, 10, 1234, 0, 1.0, + False) + + self.cs.assert_called('POST', '/flavors', body) + self.assertTrue(isinstance(f, self.flavor_type)) def test_create_with_id_as_string(self): flavor_id = 'foobar' - f = cs.flavors.create("flavorcreate", 512, + f = self.cs.flavors.create("flavorcreate", 512, 1, 10, flavor_id, ephemeral=10, is_public=False) - body = { - "flavor": { - "name": "flavorcreate", - "ram": 512, - "vcpus": 1, - "disk": 10, - "OS-FLV-EXT-DATA:ephemeral": 10, - "id": flavor_id, - "swap": 0, - "rxtx_factor": 1.0, - "os-flavor-access:is_public": False, - } - } + body = self._create_body("flavorcreate", 512, 1, 10, 10, flavor_id, 0, + 1.0, False) - cs.assert_called('POST', '/flavors', body) - self.assertTrue(isinstance(f, flavors.Flavor)) + self.cs.assert_called('POST', '/flavors', body) + self.assertTrue(isinstance(f, self.flavor_type)) def test_create_ephemeral_ispublic_defaults(self): - f = cs.flavors.create("flavorcreate", 512, 1, 10, 1234) + f = self.cs.flavors.create("flavorcreate", 512, 1, 10, 1234) - body = { - "flavor": { - "name": "flavorcreate", - "ram": 512, - "vcpus": 1, - "disk": 10, - "OS-FLV-EXT-DATA:ephemeral": 0, - "id": 1234, - "swap": 0, - "rxtx_factor": 1.0, - "os-flavor-access:is_public": True, - } - } + body = self._create_body("flavorcreate", 512, 1, 10, 0, 1234, 0, + 1.0, True) - cs.assert_called('POST', '/flavors', body) - self.assertTrue(isinstance(f, flavors.Flavor)) + self.cs.assert_called('POST', '/flavors', body) + self.assertTrue(isinstance(f, self.flavor_type)) def test_invalid_parameters_create(self): - self.assertRaises(exceptions.CommandError, cs.flavors.create, + self.assertRaises(exceptions.CommandError, self.cs.flavors.create, "flavorcreate", "invalid", 1, 10, 1234, swap=0, ephemeral=0, rxtx_factor=1.0, is_public=True) - self.assertRaises(exceptions.CommandError, cs.flavors.create, + self.assertRaises(exceptions.CommandError, self.cs.flavors.create, "flavorcreate", 512, "invalid", 10, 1234, swap=0, ephemeral=0, rxtx_factor=1.0, is_public=True) - self.assertRaises(exceptions.CommandError, cs.flavors.create, + self.assertRaises(exceptions.CommandError, self.cs.flavors.create, "flavorcreate", 512, 1, "invalid", 1234, swap=0, ephemeral=0, rxtx_factor=1.0, is_public=True) - self.assertRaises(exceptions.CommandError, cs.flavors.create, + self.assertRaises(exceptions.CommandError, self.cs.flavors.create, "flavorcreate", 512, 1, 10, 1234, swap="invalid", ephemeral=0, rxtx_factor=1.0, is_public=True) - self.assertRaises(exceptions.CommandError, cs.flavors.create, + self.assertRaises(exceptions.CommandError, self.cs.flavors.create, "flavorcreate", 512, 1, 10, 1234, swap=0, ephemeral="invalid", rxtx_factor=1.0, is_public=True) - self.assertRaises(exceptions.CommandError, cs.flavors.create, + self.assertRaises(exceptions.CommandError, self.cs.flavors.create, "flavorcreate", 512, 1, 10, 1234, swap=0, ephemeral=0, rxtx_factor="invalid", is_public=True) - self.assertRaises(exceptions.CommandError, cs.flavors.create, + self.assertRaises(exceptions.CommandError, self.cs.flavors.create, "flavorcreate", 512, 1, 10, 1234, swap=0, ephemeral=0, rxtx_factor=1.0, is_public='invalid') def test_delete(self): - cs.flavors.delete("flavordelete") - cs.assert_called('DELETE', '/flavors/flavordelete') + self.cs.flavors.delete("flavordelete") + self.cs.assert_called('DELETE', '/flavors/flavordelete') def test_delete_with_flavor_instance(self): - f = cs.flavors.get(2) - cs.flavors.delete(f) - cs.assert_called('DELETE', '/flavors/2') + f = self.cs.flavors.get(2) + self.cs.flavors.delete(f) + self.cs.assert_called('DELETE', '/flavors/2') def test_delete_with_flavor_instance_method(self): - f = cs.flavors.get(2) + f = self.cs.flavors.get(2) f.delete() - cs.assert_called('DELETE', '/flavors/2') + self.cs.assert_called('DELETE', '/flavors/2') def test_set_keys(self): - f = cs.flavors.get(1) + f = self.cs.flavors.get(1) f.set_keys({'k1': 'v1'}) - cs.assert_called('POST', '/flavors/1/os-extra_specs', + self.cs.assert_called('POST', '/flavors/1/os-extra_specs', {"extra_specs": {'k1': 'v1'}}) def test_unset_keys(self): - f = cs.flavors.get(1) + f = self.cs.flavors.get(1) f.unset_keys(['k1']) - cs.assert_called('DELETE', '/flavors/1/os-extra_specs/k1') + self.cs.assert_called('DELETE', '/flavors/1/os-extra_specs/k1') diff --git a/novaclient/tests/v3/fakes.py b/novaclient/tests/v3/fakes.py index a3815f5e4..f0f4a2a89 100644 --- a/novaclient/tests/v3/fakes.py +++ b/novaclient/tests/v3/fakes.py @@ -14,6 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from novaclient.openstack.common import strutils from novaclient.tests import fakes from novaclient.tests.v1_1 import fakes as fakes_v1_1 from novaclient.v3 import client @@ -56,3 +57,51 @@ def get_os_hosts_sample_host_startup(self, **kw): def get_os_hosts_sample_host_shutdown(self, **kw): return (200, {}, {'host': {'host': 'sample_host', 'power_action': 'shutdown'}}) + + # + # Flavors + # + post_flavors_1_flavor_extra_specs = ( + fakes_v1_1.FakeHTTPClient.post_flavors_1_os_extra_specs) + + delete_flavors_1_flavor_extra_specs_k1 = ( + fakes_v1_1.FakeHTTPClient.delete_flavors_1_os_extra_specs_k1) + + def get_flavors_detail(self, **kw): + flavors = {'flavors': [ + {'id': 1, 'name': '256 MB Server', 'ram': 256, 'disk': 10, + 'ephemeral': 10, + 'flavor-access:is_public': True, + 'links': {}}, + {'id': 2, 'name': '512 MB Server', 'ram': 512, 'disk': 20, + 'ephemeral': 20, + 'flavor-access:is_public': False, + 'links': {}}, + {'id': 'aa1', 'name': '128 MB Server', 'ram': 128, 'disk': 0, + 'ephemeral': 0, + 'flavor-access:is_public': True, + 'links': {}} + ]} + + if 'is_public' not in kw: + filter_is_public = True + else: + if kw['is_public'].lower() == 'none': + filter_is_public = None + else: + filter_is_public = strutils.bool_from_string(kw['is_public'], + True) + + if filter_is_public is not None: + if filter_is_public: + flavors['flavors'] = [ + v for v in flavors['flavors'] + if v['flavor-access:is_public'] + ] + else: + flavors['flavors'] = [ + v for v in flavors['flavors'] + if not v['flavor-access:is_public'] + ] + + return (200, {}, flavors) diff --git a/novaclient/tests/v3/test_flavors.py b/novaclient/tests/v3/test_flavors.py new file mode 100644 index 000000000..34714aef0 --- /dev/null +++ b/novaclient/tests/v3/test_flavors.py @@ -0,0 +1,57 @@ +# Copyright (c) 2013, OpenStack +# Copyright 2013 IBM Corp. +# +# 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 novaclient.tests.v1_1 import test_flavors +from novaclient.tests.v3 import fakes +from novaclient.v3 import flavors + + +class FlavorsTest(test_flavors.FlavorsTest): + def _get_fake_client(self): + return fakes.FakeClient() + + def _get_flavor_type(self): + return flavors.Flavor + + def _create_body(self, name, ram, vcpus, disk, ephemeral, id, swap, + rxtx_factor, is_public): + return { + "flavor": { + "name": name, + "ram": ram, + "vcpus": vcpus, + "disk": disk, + "ephemeral": ephemeral, + "id": id, + "swap": swap, + "rxtx_factor": rxtx_factor, + "flavor-access:is_public": is_public, + } + } + + def test_set_keys(self): + f = self.cs.flavors.get(1) + f.set_keys({'k1': 'v1'}) + self.cs.assert_called('POST', '/flavors/1/flavor-extra-specs', + {"extra_specs": {'k1': 'v1'}}) + + def test_unset_keys(self): + f = self.cs.flavors.get(1) + f.unset_keys(['k1']) + self.cs.assert_called('DELETE', '/flavors/1/flavor-extra-specs/k1') + + def test_get_flavor_details_diablo(self): + # Don't need for V3 API to work against diablo + pass diff --git a/novaclient/v1_1/flavors.py b/novaclient/v1_1/flavors.py index 28ca2c7b9..dfafdab3e 100644 --- a/novaclient/v1_1/flavors.py +++ b/novaclient/v1_1/flavors.py @@ -132,6 +132,22 @@ def delete(self, flavor): """ self._delete("/flavors/%s" % base.getid(flavor)) + def _build_body(self, name, ram, vcpus, disk, id, swap, + ephemeral, rxtx_factor, is_public): + return { + "flavor": { + "name": name, + "ram": ram, + "vcpus": vcpus, + "disk": disk, + "id": id, + "swap": swap, + "OS-FLV-EXT-DATA:ephemeral": ephemeral, + "rxtx_factor": rxtx_factor, + "os-flavor-access:is_public": is_public, + } + } + def create(self, name, ram, vcpus, disk, flavorid="auto", ephemeral=0, swap=0, rxtx_factor=1.0, is_public=True): """ @@ -183,18 +199,7 @@ def create(self, name, ram, vcpus, disk, flavorid="auto", except Exception: raise exceptions.CommandError("is_public must be a boolean.") - body = { - "flavor": { - "name": name, - "ram": ram, - "vcpus": vcpus, - "disk": disk, - "id": flavorid, - "swap": swap, - "OS-FLV-EXT-DATA:ephemeral": ephemeral, - "rxtx_factor": rxtx_factor, - "os-flavor-access:is_public": is_public, - } - } + body = self._build_body(name, ram, vcpus, disk, flavorid, swap, + ephemeral, rxtx_factor, is_public) return self._create("/flavors", body, "flavor") diff --git a/novaclient/v3/client.py b/novaclient/v3/client.py index e3ab29c37..1ee1cdccc 100644 --- a/novaclient/v3/client.py +++ b/novaclient/v3/client.py @@ -15,6 +15,7 @@ # under the License. from novaclient import client +from novaclient.v3 import flavors from novaclient.v3 import hosts @@ -51,6 +52,7 @@ def __init__(self, username, password, project_id, auth_url=None, self.os_cache = os_cache or not no_cache #TODO(bnemec): Add back in v3 extensions self.hosts = hosts.HostManager(self) + self.flavors = flavors.FlavorManager(self) # Add in any extensions... if extensions: diff --git a/novaclient/v3/flavors.py b/novaclient/v3/flavors.py new file mode 100644 index 000000000..120574aef --- /dev/null +++ b/novaclient/v3/flavors.py @@ -0,0 +1,99 @@ +# Copyright 2010 Jacob Kaplan-Moss +# Copyright 2013 IBM Corp. +# +# 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. + +""" +Flavor interface. +""" +from novaclient import base +from novaclient.v1_1 import flavors + + +class Flavor(base.Resource): + """ + A flavor is an available hardware configuration for a server. + """ + HUMAN_ID = True + + def __repr__(self): + return "" % self.name + + @property + def is_public(self): + """ + Provide a user-friendly accessor to flavor-access:is_public + """ + return self._info.get("flavor-access:is_public", 'N/A') + + def get_keys(self): + """ + Get extra specs from a flavor. + + :param flavor: The :class:`Flavor` to get extra specs from + """ + _resp, body = self.manager.api.client.get( + "/flavors/%s/flavor-extra-specs" % + base.getid(self)) + return body["extra_specs"] + + def set_keys(self, metadata): + """ + Set extra specs on a flavor. + + :param flavor: The :class:`Flavor` to set extra spec on + :param metadata: A dict of key/value pairs to be set + """ + body = {'extra_specs': metadata} + return self.manager._create( + "/flavors/%s/flavor-extra-specs" % + base.getid(self), body, "extra_specs", + return_raw=True) + + def unset_keys(self, keys): + """ + Unset extra specs on a flavor. + + :param flavor: The :class:`Flavor` to unset extra spec on + :param keys: A list of keys to be unset + """ + for k in keys: + return self.manager._delete( + "/flavors/%s/flavor-extra-specs/%s" % ( + base.getid(self), k)) + + def delete(self): + """ + Delete this flavor. + """ + self.manager.delete(self) + + +class FlavorManager(flavors.FlavorManager): + resource_class = Flavor + + def _build_body(self, name, ram, vcpus, disk, id, swap, + ephemeral, rxtx_factor, is_public): + return { + "flavor": { + "name": name, + "ram": ram, + "vcpus": vcpus, + "disk": disk, + "id": id, + "swap": swap, + "ephemeral": ephemeral, + "rxtx_factor": rxtx_factor, + "flavor-access:is_public": is_public, + } + } From 72b1862c97afbfc5bcd90a2568afc0c7aacf4d79 Mon Sep 17 00:00:00 2001 From: Chris Yeoh Date: Wed, 4 Dec 2013 23:09:57 +1030 Subject: [PATCH 0341/1705] Adds flavor access support for Nova V3 API Adds the remaining flavors support for the Nova V3 API. Partially implements blueprint v3-api Change-Id: I46cdec8999f74af37a9ca4280d123907124bad39 --- novaclient/tests/v3/fakes.py | 6 +++ novaclient/tests/v3/test_flavor_access.py | 63 +++++++++++++++++++++++ novaclient/v3/client.py | 2 + novaclient/v3/flavor_access.py | 45 ++++++++++++++++ 4 files changed, 116 insertions(+) create mode 100644 novaclient/tests/v3/test_flavor_access.py create mode 100644 novaclient/v3/flavor_access.py diff --git a/novaclient/tests/v3/fakes.py b/novaclient/tests/v3/fakes.py index f0f4a2a89..4069776ff 100644 --- a/novaclient/tests/v3/fakes.py +++ b/novaclient/tests/v3/fakes.py @@ -105,3 +105,9 @@ def get_flavors_detail(self, **kw): ] return (200, {}, flavors) + + # + # Flavor access + # + get_flavors_2_flavor_access = ( + fakes_v1_1.FakeHTTPClient.get_flavors_2_os_flavor_access) diff --git a/novaclient/tests/v3/test_flavor_access.py b/novaclient/tests/v3/test_flavor_access.py new file mode 100644 index 000000000..5b64887d1 --- /dev/null +++ b/novaclient/tests/v3/test_flavor_access.py @@ -0,0 +1,63 @@ +# Copyright 2012 OpenStack Foundation +# Copyright 2013 IBM Corp. +# All Rights Reserved. +# +# 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 novaclient.tests import utils +from novaclient.tests.v3 import fakes +from novaclient.v3 import flavor_access + + +cs = fakes.FakeClient() + + +class FlavorAccessTest(utils.TestCase): + + def test_list_access_by_flavor_private(self): + kwargs = {'flavor': cs.flavors.get(2)} + r = cs.flavor_access.list(**kwargs) + cs.assert_called('GET', '/flavors/2/flavor-access') + for access_list in r: + self.assertTrue(isinstance(access_list, + flavor_access.FlavorAccess)) + + def test_add_tenant_access(self): + flavor = cs.flavors.get(2) + tenant = 'proj2' + r = cs.flavor_access.add_tenant_access(flavor, tenant) + + body = { + "add_tenant_access": { + "tenant_id": "proj2" + } + } + + cs.assert_called('POST', '/flavors/2/action', body) + for a in r: + self.assertTrue(isinstance(a, flavor_access.FlavorAccess)) + + def test_remove_tenant_access(self): + flavor = cs.flavors.get(2) + tenant = 'proj2' + r = cs.flavor_access.remove_tenant_access(flavor, tenant) + + body = { + "remove_tenant_access": { + "tenant_id": "proj2" + } + } + + cs.assert_called('POST', '/flavors/2/action', body) + for a in r: + self.assertTrue(isinstance(a, flavor_access.FlavorAccess)) diff --git a/novaclient/v3/client.py b/novaclient/v3/client.py index 1ee1cdccc..8aba63f34 100644 --- a/novaclient/v3/client.py +++ b/novaclient/v3/client.py @@ -15,6 +15,7 @@ # under the License. from novaclient import client +from novaclient.v3 import flavor_access from novaclient.v3 import flavors from novaclient.v3 import hosts @@ -53,6 +54,7 @@ def __init__(self, username, password, project_id, auth_url=None, #TODO(bnemec): Add back in v3 extensions self.hosts = hosts.HostManager(self) self.flavors = flavors.FlavorManager(self) + self.flavor_access = flavor_access.FlavorAccessManager(self) # Add in any extensions... if extensions: diff --git a/novaclient/v3/flavor_access.py b/novaclient/v3/flavor_access.py new file mode 100644 index 000000000..6896dac36 --- /dev/null +++ b/novaclient/v3/flavor_access.py @@ -0,0 +1,45 @@ +# Copyright 2012 OpenStack Foundation +# Copyright 2013 IBM Corp. +# All Rights Reserved. +# +# 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. + +"""Flavor access interface.""" + +from novaclient import base +from novaclient.v1_1 import flavor_access + + +class FlavorAccess(flavor_access.FlavorAccess): + pass + + +class FlavorAccessManager(flavor_access.FlavorAccessManager): + """ + Manage :class:`FlavorAccess` resources. + """ + resource_class = FlavorAccess + + def _list_by_flavor(self, flavor): + return self._list('/flavors/%s/flavor-access' % base.getid(flavor), + 'flavor_access') + + def add_tenant_access(self, flavor, tenant): + """Add a tenant to the given flavor access list.""" + info = {'tenant_id': tenant} + return self._action('add_tenant_access', flavor, info) + + def remove_tenant_access(self, flavor, tenant): + """Remove a tenant from the given flavor access list.""" + info = {'tenant_id': tenant} + return self._action('remove_tenant_access', flavor, info) From 59cf27aa3f00652b9cff2668c985d3394b7e8a64 Mon Sep 17 00:00:00 2001 From: Chris Yeoh Date: Fri, 6 Dec 2013 12:08:49 +1030 Subject: [PATCH 0342/1705] Adds agent support for Nova V3 API Adds support and tests for the os-agents extension for the Nova V3 API Partially implements blueprint v3-api Change-Id: I53f8a1cbab6f4dee10fb823bc51ae90baa97fa7a --- novaclient/tests/v1_1/test_agents.py | 52 +++++++++++++++++----------- novaclient/tests/v3/test_agents.py | 34 ++++++++++++++++++ novaclient/v1_1/agents.py | 11 +++--- novaclient/v3/agents.py | 36 +++++++++++++++++++ novaclient/v3/client.py | 2 ++ 5 files changed, 111 insertions(+), 24 deletions(-) create mode 100644 novaclient/tests/v3/test_agents.py create mode 100644 novaclient/v3/agents.py diff --git a/novaclient/tests/v1_1/test_agents.py b/novaclient/tests/v1_1/test_agents.py index 222d03153..6671282b8 100644 --- a/novaclient/tests/v1_1/test_agents.py +++ b/novaclient/tests/v1_1/test_agents.py @@ -20,25 +20,34 @@ from novaclient.v1_1 import agents -cs = fakes.FakeClient() +class AgentsTest(utils.TestCase): + def setUp(self): + super(AgentsTest, self).setUp() + self.cs = self._get_fake_client() + self.agent_type = self._get_agent_type() + def _get_fake_client(self): + return fakes.FakeClient() -class AgentsTest(utils.TestCase): + def _get_agent_type(self): + return agents.Agent def test_list_agents(self): - ags = cs.agents.list() - cs.assert_called('GET', '/os-agents') - [self.assertTrue(isinstance(a, agents.Agent)) for a in ags] - [self.assertEqual(a.hypervisor, 'kvm') for a in ags] + ags = self.cs.agents.list() + self.cs.assert_called('GET', '/os-agents') + for a in ags: + self.assertTrue(isinstance(a, self.agent_type)) + self.assertEqual(a.hypervisor, 'kvm') def test_list_agents_with_hypervisor(self): - ags = cs.agents.list('xen') - cs.assert_called('GET', '/os-agents?hypervisor=xen') - [self.assertTrue(isinstance(a, agents.Agent)) for a in ags] - [self.assertEqual(a.hypervisor, 'xen') for a in ags] + ags = self.cs.agents.list('xen') + self.cs.assert_called('GET', '/os-agents?hypervisor=xen') + for a in ags: + self.assertTrue(isinstance(a, self.agent_type)) + self.assertEqual(a.hypervisor, 'xen') def test_agents_create(self): - ag = cs.agents.create('win', 'x86', '7.0', + ag = self.cs.agents.create('win', 'x86', '7.0', '/xxx/xxx/xxx', 'add6bb58e139be103324d04d82d8f546', 'xen') @@ -49,20 +58,23 @@ def test_agents_create(self): 'version': '7.0', 'architecture': 'x86', 'os': 'win'}} - cs.assert_called('POST', '/os-agents', body) + self.cs.assert_called('POST', '/os-agents', body) self.assertEqual(1, ag._info.copy()['id']) def test_agents_delete(self): - cs.agents.delete('1') - cs.assert_called('DELETE', '/os-agents/1') + self.cs.agents.delete('1') + self.cs.assert_called('DELETE', '/os-agents/1') + + def _build_example_update_body(self): + return {"para": { + "url": "/yyy/yyyy/yyyy", + "version": "8.0", + "md5hash": "add6bb58e139be103324d04d82d8f546"}} def test_agents_modify(self): - ag = cs.agents.update('1', '8.0', + ag = self.cs.agents.update('1', '8.0', '/yyy/yyyy/yyyy', 'add6bb58e139be103324d04d82d8f546') - body = {"para": { - "url": "/yyy/yyyy/yyyy", - "version": "8.0", - "md5hash": "add6bb58e139be103324d04d82d8f546"}} - cs.assert_called('PUT', '/os-agents/1', body) + body = self._build_example_update_body() + self.cs.assert_called('PUT', '/os-agents/1', body) self.assertEqual(1, ag.id) diff --git a/novaclient/tests/v3/test_agents.py b/novaclient/tests/v3/test_agents.py new file mode 100644 index 000000000..55b482f7c --- /dev/null +++ b/novaclient/tests/v3/test_agents.py @@ -0,0 +1,34 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 IBM Corp. +# All Rights Reserved. +# +# 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 novaclient.tests.v1_1 import test_agents +from novaclient.tests.v3 import fakes +from novaclient.v3 import agents + + +class AgentsTest(test_agents.AgentsTest): + def _build_example_update_body(self): + return {"agent": { + "url": "/yyy/yyyy/yyyy", + "version": "8.0", + "md5hash": "add6bb58e139be103324d04d82d8f546"}} + + def _get_fake_client(self): + return fakes.FakeClient() + + def _get_agent_type(self): + return agents.Agent diff --git a/novaclient/v1_1/agents.py b/novaclient/v1_1/agents.py index 550d47ab6..51b8ff05a 100644 --- a/novaclient/v1_1/agents.py +++ b/novaclient/v1_1/agents.py @@ -42,13 +42,16 @@ def list(self, hypervisor=None): url = "/os-agents?hypervisor=%s" % hypervisor return self._list(url, "agents") - def update(self, id, version, - url, md5hash): - """Update an existing agent build.""" - body = {'para': { + def _build_update_body(self, version, url, md5hash): + return {'para': { 'version': version, 'url': url, 'md5hash': md5hash}} + + def update(self, id, version, + url, md5hash): + """Update an existing agent build.""" + body = self._build_update_body(version, url, md5hash) return self._update('/os-agents/%s' % id, body, 'agent') def create(self, os, architecture, version, diff --git a/novaclient/v3/agents.py b/novaclient/v3/agents.py new file mode 100644 index 000000000..95e781508 --- /dev/null +++ b/novaclient/v3/agents.py @@ -0,0 +1,36 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 IBM Corp. +# All Rights Reserved. +# +# 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. + +""" +agent interface +""" + +from novaclient.v1_1 import agents + + +class Agent(agents.Agent): + pass + + +class AgentsManager(agents.AgentsManager): + resource_class = Agent + + def _build_update_body(self, version, url, md5hash): + return {'agent': { + 'version': version, + 'url': url, + 'md5hash': md5hash}} diff --git a/novaclient/v3/client.py b/novaclient/v3/client.py index 8aba63f34..31fd0eac7 100644 --- a/novaclient/v3/client.py +++ b/novaclient/v3/client.py @@ -15,6 +15,7 @@ # under the License. from novaclient import client +from novaclient.v3 import agents from novaclient.v3 import flavor_access from novaclient.v3 import flavors from novaclient.v3 import hosts @@ -52,6 +53,7 @@ def __init__(self, username, password, project_id, auth_url=None, self.tenant_id = tenant_id self.os_cache = os_cache or not no_cache #TODO(bnemec): Add back in v3 extensions + self.agents = agents.AgentsManager(self) self.hosts = hosts.HostManager(self) self.flavors = flavors.FlavorManager(self) self.flavor_access = flavor_access.FlavorAccessManager(self) From 2e5a5a81c303298d4dc2a2b4f8907c79ed20d314 Mon Sep 17 00:00:00 2001 From: Chris Yeoh Date: Mon, 9 Dec 2013 14:53:22 +1030 Subject: [PATCH 0343/1705] Remove commands not supported by Nova V3 API Remove commands which are no longer supported by the Nova V3 API and that we will definitely not be proxying to another service. Removes: - cloudpipe-list - cloudpipe-create - cloudpipe-configure - coverage-start - coverage-stop - coverage-report - coverage-reset Partially implements blueprint v3-api Change-Id: I366cd0233977c1b506d55de280188ecccda8188f --- novaclient/v3/shell.py | 63 ------------------------------------------ 1 file changed, 63 deletions(-) diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index bc123b29e..4dd5df8a9 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -332,26 +332,6 @@ def do_boot(cs, args): _poll_for_status(cs.servers.get, info['id'], 'building', ['active']) -def do_cloudpipe_list(cs, _args): - """Print a list of all cloudpipe instances.""" - cloudpipes = cs.cloudpipe.list() - columns = ['Project Id', "Public IP", "Public Port", "Internal IP"] - utils.print_list(cloudpipes, columns) - - -@utils.arg('project', metavar='', help='Name of the project.') -def do_cloudpipe_create(cs, args): - """Create a cloudpipe instance for the given project.""" - cs.cloudpipe.create(args.project) - - -@utils.arg('address', metavar='', help='New IP Address.') -@utils.arg('port', metavar='', help='New Port.') -def do_cloudpipe_configure(cs, args): - """Update the VPN IP/port of a cloudpipe instance.""" - cs.cloudpipe.update(args.address, args.port) - - def _poll_for_status(poll_fn, obj_id, action, final_ok_states, poll_period=5, show_progress=True, status_field="status", silent=False): @@ -2684,49 +2664,6 @@ def do_host_action(cs, args): utils.print_list([result], ['HOST', 'power_action']) -@utils.arg('--combine', - dest='combine', - action="store_true", - default=False, - help='Generate a single report for all services.') -def do_coverage_start(cs, args): - """Start Nova coverage reporting.""" - cs.coverage.start(combine=args.combine) - print("Coverage collection started") - - -def do_coverage_stop(cs, args): - """Stop Nova coverage reporting.""" - out = cs.coverage.stop() - print("Coverage data file path: %s" % out[-1]['path']) - - -@utils.arg('filename', metavar='', help='report filename') -@utils.arg('--html', - dest='html', - action="store_true", - default=False, - help='Generate HTML reports instead of text ones.') -@utils.arg('--xml', - dest='xml', - action="store_true", - default=False, - help='Generate XML reports instead of text ones.') -def do_coverage_report(cs, args): - """Generate coverage report.""" - if args.html is True and args.xml is True: - raise exceptions.CommandError("--html and --xml must not be " - "specified together.") - cov = cs.coverage.report(args.filename, xml=args.xml, html=args.html) - print("Report path: %s" % cov[-1]['path']) - - -def do_coverage_reset(cs, args): - """Reset coverage data.""" - cs.coverage.reset() - print("Coverage data reset") - - def _find_hypervisor(cs, hypervisor): """Get a hypervisor by name or ID.""" return utils.find_resource(cs.hypervisors, hypervisor) From 76f926fc10e7c023845789e89726860b48a4124b Mon Sep 17 00:00:00 2001 From: Chris Yeoh Date: Mon, 9 Dec 2013 17:08:30 +1030 Subject: [PATCH 0344/1705] Adds images support for Nova V3 API Adds support for basic image querying from the image server rather than the Nova API. The Nova V3 API no longer supports image querying directly, but in order to support convenience functions such as specifying images by name rather than ID, it is necessary to have some basic image query support. image delete and image meta manipulation is no longer supported by the client as these features can be accessed directly through the glance client Partially implements blueprint v3-api Change-Id: I9050845d631e9dfc2e110327221d154b8924cd65 --- novaclient/tests/v3/fakes.py | 16 +++++ novaclient/tests/v3/test_images.py | 57 ++++++++++++++++ novaclient/v3/client.py | 2 + novaclient/v3/images.py | 104 +++++++++++++++++++++++++++++ novaclient/v3/shell.py | 11 ++- 5 files changed, 184 insertions(+), 6 deletions(-) create mode 100644 novaclient/tests/v3/test_images.py create mode 100644 novaclient/v3/images.py diff --git a/novaclient/tests/v3/fakes.py b/novaclient/tests/v3/fakes.py index 4069776ff..afe73ce51 100644 --- a/novaclient/tests/v3/fakes.py +++ b/novaclient/tests/v3/fakes.py @@ -111,3 +111,19 @@ def get_flavors_detail(self, **kw): # get_flavors_2_flavor_access = ( fakes_v1_1.FakeHTTPClient.get_flavors_2_os_flavor_access) + + # + # Images + # + get_v1_images_detail = fakes_v1_1.FakeHTTPClient.get_images_detail + get_v1_images = fakes_v1_1.FakeHTTPClient.get_images + + def head_v1_images_1(self, **kw): + headers = { + 'x-image-meta-id': '1', + 'x-image-meta-name': 'CentOS 5.2', + 'x-image-meta-updated': '2010-10-10T12:00:00Z', + 'x-image-meta-created': '2010-10-10T12:00:00Z', + 'x-image-meta-status': 'ACTIVE', + 'x-image-meta-property-test_key': 'test_value'} + return 200, headers, '' diff --git a/novaclient/tests/v3/test_images.py b/novaclient/tests/v3/test_images.py new file mode 100644 index 000000000..082eb52b3 --- /dev/null +++ b/novaclient/tests/v3/test_images.py @@ -0,0 +1,57 @@ +# Copyright 2013 IBM Corp. +# +# 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 novaclient.tests import utils +from novaclient.tests.v3 import fakes +from novaclient.v3 import images + + +cs = fakes.FakeClient() + + +class ImagesTest(utils.TestCase): + + def test_list_images(self): + il = cs.images.list() + cs.assert_called('GET', '/v1/images/detail') + for i in il: + self.assertTrue(isinstance(i, images.Image)) + + def test_list_images_undetailed(self): + il = cs.images.list(detailed=False) + cs.assert_called('GET', '/v1/images') + for i in il: + self.assertTrue(isinstance(i, images.Image)) + + def test_list_images_with_limit(self): + il = cs.images.list(limit=4) + cs.assert_called('GET', '/v1/images/detail?limit=4') + + def test_get_image_details(self): + i = cs.images.get(1) + cs.assert_called('HEAD', '/v1/images/1') + self.assertTrue(isinstance(i, images.Image)) + self.assertEqual(i.id, '1') + self.assertEqual(i.name, 'CentOS 5.2') + + def test_find(self): + i = cs.images.find(name="CentOS 5.2") + self.assertEqual(i.id, '1') + cs.assert_called('GET', '/v1/images', pos=-2) + cs.assert_called('HEAD', '/v1/images/1', pos=-1) + + iml = cs.images.findall(status='SAVING') + self.assertEqual(len(iml), 1) + self.assertEqual(iml[0].name, 'My Server Backup') diff --git a/novaclient/v3/client.py b/novaclient/v3/client.py index 31fd0eac7..c8af7202d 100644 --- a/novaclient/v3/client.py +++ b/novaclient/v3/client.py @@ -19,6 +19,7 @@ from novaclient.v3 import flavor_access from novaclient.v3 import flavors from novaclient.v3 import hosts +from novaclient.v3 import images class Client(object): @@ -57,6 +58,7 @@ def __init__(self, username, password, project_id, auth_url=None, self.hosts = hosts.HostManager(self) self.flavors = flavors.FlavorManager(self) self.flavor_access = flavor_access.FlavorAccessManager(self) + self.images = images.ImageManager(self) # Add in any extensions... if extensions: diff --git a/novaclient/v3/images.py b/novaclient/v3/images.py new file mode 100644 index 000000000..2e1b34aa8 --- /dev/null +++ b/novaclient/v3/images.py @@ -0,0 +1,104 @@ +# Copyright 2010 Jacob Kaplan-Moss +# Copyright 2013 IBM Corp. +# +# 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. + +""" +Image interface. +""" +from novaclient import base +from novaclient.openstack.common.py3kcompat import urlutils +from novaclient.openstack.common import strutils + + +class Image(base.Resource): + """ + An image is a collection of files used to create or rebuild a server. + """ + HUMAN_ID = True + + def __repr__(self): + return "" % self.name + + def delete(self): + """ + Delete this image. + """ + self.manager.delete(self) + + +class ImageManager(base.ManagerWithFind): + """ + Manage :class:`Image` resources. + """ + resource_class = Image + # NOTE(cyeoh): Eventually we'll want novaclient to be smart + # enough to do version discovery, but for now we just request + # the v1 image API + image_api_prefix = '/v1' + + def _image_meta_from_headers(self, headers): + meta = {'properties': {}} + safe_decode = strutils.safe_decode + for key, value in headers.items(): + value = safe_decode(value, incoming='utf-8') + if key.startswith('x-image-meta-property-'): + _key = safe_decode(key[22:], incoming='utf-8') + meta['properties'][_key] = value + elif key.startswith('x-image-meta-'): + _key = safe_decode(key[13:], incoming='utf-8') + meta[_key] = value + + for key in ['is_public', 'protected', 'deleted']: + if key in meta: + meta[key] = strutils.bool_from_string(meta[key]) + + return self._format_image_meta_for_user(meta) + + @staticmethod + def _format_image_meta_for_user(meta): + for key in ['size', 'min_ram', 'min_disk']: + if key in meta: + try: + meta[key] = int(meta[key]) + except ValueError: + pass + return meta + + def get(self, image): + """ + Get an image. + + :param image: The ID of the image to get. + :rtype: :class:`Image` + """ + url = "%s/images/%s" % (self.image_api_prefix, base.getid(image)) + resp, _ = self.api.client._cs_request(url, 'HEAD') + foo = self._image_meta_from_headers(resp.headers) + return Image(self, foo) + + def list(self, detailed=True, limit=None): + """ + Get a list of all images. + + :rtype: list of :class:`Image` + :param limit: maximum number of images to return. + """ + params = {} + detail = '' + if detailed: + detail = '/detail' + if limit: + params['limit'] = int(limit) + query = '?%s' % urlutils.urlencode(params) if params else '' + return self._list('/v1/images%s%s' % (detail, query), 'images') diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 4dd5df8a9..9d36099b8 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -773,6 +773,7 @@ def do_network_create(cs, args): dest="limit", metavar="", help='number of images to return per request') +@utils.service_type('image') def do_image_list(cs, _args): """Print a list of available images to boot from.""" limit = _args.limit @@ -831,9 +832,6 @@ def _extract_metadata(args): def _print_image(image): info = image._info.copy() - # ignore links, we don't need to present those - info.pop('links') - # try to replace a server entity to just an id server = info.pop('server', None) try: @@ -842,10 +840,10 @@ def _print_image(image): pass # break up metadata and display each on its own row - metadata = info.pop('metadata', {}) + properties = info.pop('properties', {}) try: - for key, value in metadata.items(): - _key = 'metadata %s' % key + for key, value in properties.items(): + _key = 'Property %s' % key info[_key] = value except AttributeError: pass @@ -864,6 +862,7 @@ def _print_flavor(flavor): @utils.arg('image', metavar='', help="Name or ID of image") +@utils.service_type('image') def do_image_show(cs, args): """Show details about the given image.""" image = _find_image(cs, args.image) From 572f9a985efff593ff59daa02c6b4585b1804c1a Mon Sep 17 00:00:00 2001 From: Joe Gordon Date: Mon, 25 Nov 2013 11:19:30 -0800 Subject: [PATCH 0345/1705] Stop using deprecated keyring backends keyring.backend.* was deprecated in python-keyring 1.1 and we already require keyring 1.6.1. This also makes novaclient compatible with python-keyring 3.0 which removes the deprecated paths. http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=728470#10. Change-Id: I4919ac515e589fddc6044c0cd2f4cbc06c2ec91f Co-Authored-By: Sebastian Ramacher --- novaclient/shell.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/novaclient/shell.py b/novaclient/shell.py index ca0770e38..e489eafe4 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -37,14 +37,6 @@ try: import keyring HAS_KEYRING = True - try: - if isinstance(keyring.get_keyring(), keyring.backend.GnomeKeyring): - import gnomekeyring - all_errors = (ValueError, - gnomekeyring.IOError, - gnomekeyring.NoKeyringDaemonError) - except Exception: - pass except ImportError: pass From 13cf07da402814e3eeeff83404a5091648e97523 Mon Sep 17 00:00:00 2001 From: Roman Podoliaka Date: Mon, 2 Dec 2013 17:10:15 +0200 Subject: [PATCH 0346/1705] Expose the rebuild preserve-ephemeral extension This new extension permits preserving the ephemeral partition if the Nova hypervisor supports that. This is primarily useful when Cinder is not available to provide preservation of state while replacing the image in use. One common situation for that is Nova Baremetal. DocImpact Blueprint: baremetal-preserve-ephemeral Partial-Bug: #1174154 Co-Authored-By: Robert Collins Change-Id: Ib1511653904d4f95ab03fb471669175127004582 --- novaclient/tests/v1_1/test_servers.py | 9 +++++++++ novaclient/tests/v1_1/test_shell.py | 21 +++++++++++++++++++++ novaclient/v1_1/servers.py | 14 +++++++++++--- novaclient/v1_1/shell.py | 5 +++++ 4 files changed, 46 insertions(+), 3 deletions(-) diff --git a/novaclient/tests/v1_1/test_servers.py b/novaclient/tests/v1_1/test_servers.py index f13fa7577..2a812f856 100644 --- a/novaclient/tests/v1_1/test_servers.py +++ b/novaclient/tests/v1_1/test_servers.py @@ -261,6 +261,15 @@ def test_rebuild_server_disk_config_auto(self): def test_rebuild_server_disk_config_manual(self): self._rebuild_resize_disk_config('MANUAL') + def test_rebuild_server_preserve_ephemeral(self): + s = cs.servers.get(1234) + s.rebuild(image=1, preserve_ephemeral=True) + cs.assert_called('POST', '/servers/1234/action') + body = cs.client.callstack[-1][-1] + d = body['rebuild'] + self.assertIn('preserve_ephemeral', d) + self.assertEqual(d['preserve_ephemeral'], True) + def test_resize_server(self): s = cs.servers.get(1234) s.resize(flavor=1) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index c02dc3dcf..9e29a1425 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -733,6 +733,27 @@ def test_rebuild(self): self.assert_called('GET', '/flavors/1', pos=-2) self.assert_called('GET', '/images/2') + def test_rebuild_preserve_ephemeral(self): + self.run_command('rebuild sample-server 1 --preserve-ephemeral') + self.assert_called('GET', '/servers', pos=-8) + self.assert_called('GET', '/servers/1234', pos=-7) + self.assert_called('GET', '/images/1', pos=-6) + self.assert_called('POST', '/servers/1234/action', + {'rebuild': {'imageRef': 1, + 'preserve_ephemeral': True}}, pos=-5) + self.assert_called('GET', '/flavors/1', pos=-2) + self.assert_called('GET', '/images/2') + + self.run_command('rebuild sample-server 1 --rebuild-password asdf') + self.assert_called('GET', '/servers', pos=-8) + self.assert_called('GET', '/servers/1234', pos=-7) + self.assert_called('GET', '/images/1', pos=-6) + self.assert_called('POST', '/servers/1234/action', + {'rebuild': {'imageRef': 1, 'adminPass': 'asdf'}}, + pos=-5) + self.assert_called('GET', '/flavors/1', pos=-2) + self.assert_called('GET', '/images/2') + def test_start(self): self.run_command('start sample-server') self.assert_called('POST', '/servers/1234/action', {'os-start': None}) diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index dbbad7b22..9133d4057 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -241,14 +241,18 @@ def reboot(self, reboot_type=REBOOT_SOFT): """ self.manager.reboot(self, reboot_type) - def rebuild(self, image, password=None, **kwargs): + def rebuild(self, image, password=None, preserve_ephemeral=False, + **kwargs): """ Rebuild -- shut down and then re-image -- this server. :param image: the :class:`Image` (or its ID) to re-image with. :param password: string to set as password on the rebuilt server. + :param preserve_ephemeral: If True, request that any ephemeral device + be preserved when rebuilding the instance. Defaults to False. """ - return self.manager.rebuild(self, image, password=password, **kwargs) + return self.manager.rebuild(self, image, password=password, + preserve_ephemeral=preserve_ephemeral, **kwargs) def resize(self, flavor, **kwargs): """ @@ -748,7 +752,7 @@ def reboot(self, server, reboot_type=REBOOT_SOFT): self._action('reboot', server, {'type': reboot_type}) def rebuild(self, server, image, password=None, disk_config=None, - **kwargs): + preserve_ephemeral=False, **kwargs): """ Rebuild -- shut down and then re-image -- a server. @@ -757,12 +761,16 @@ def rebuild(self, server, image, password=None, disk_config=None, :param password: string to set as password on the rebuilt server. :param disk_config: partitioning mode to use on the rebuilt server. Valid values are 'AUTO' or 'MANUAL' + :param preserve_ephemeral: If True, request that any ephemeral device + be preserved when rebuilding the instance. Defaults to False. """ body = {'imageRef': base.getid(image)} if password is not None: body['adminPass'] = password if disk_config is not None: body['OS-DCF:diskConfig'] = disk_config + if preserve_ephemeral is not False: + body['preserve_ephemeral'] = True _resp, body = self._action('rebuild', server, body, **kwargs) return Server(self, body['server']) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 890fd0daa..97a377fc4 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1207,6 +1207,10 @@ def do_reboot(cs, args): action="store_true", default=False, help='Skips flavor/image lookups when showing servers') +@utils.arg('--preserve-ephemeral', + action="store_true", + default=False, + help='Preserve the default ephemeral storage partition on rebuild.') def do_rebuild(cs, args): """Shutdown, re-image, and re-boot a server.""" server = _find_server(cs, args.server) @@ -1218,6 +1222,7 @@ def do_rebuild(cs, args): _password = None kwargs = utils.get_resource_manager_extra_kwargs(do_rebuild, args) + kwargs['preserve_ephemeral'] = args.preserve_ephemeral server.rebuild(image, _password, **kwargs) _print_server(cs, args) From f3d6f1d22ca178b4f18406702b8ad9e8082e413e Mon Sep 17 00:00:00 2001 From: JUN JIE NAN Date: Mon, 16 Dec 2013 14:07:24 +0800 Subject: [PATCH 0347/1705] Fixed autodoc can't import/find class error The right module name should be `novaclient.v1_1.servers' instead of `novaclient'. Similar fix on `images', `flavors' and `exceptions'. Removed `ipgroups' and `backup_schedules' since they do not exist any more. Closes-Bug: #1056478 Change-Id: Id35f9c275fb36f3651a0bb2b0eb03c43f0aaeec1 --- doc/source/api.rst | 38 ++++------------ doc/source/conf.py | 10 ++++- doc/source/ref/backup_schedules.rst | 60 ------------------------- doc/source/ref/exceptions.rst | 16 +++---- doc/source/ref/flavors.rst | 2 +- doc/source/ref/images.rst | 4 +- doc/source/ref/index.rst | 10 ++--- doc/source/ref/ipgroups.rst | 46 ------------------- doc/source/ref/servers.rst | 70 +++-------------------------- 9 files changed, 34 insertions(+), 222 deletions(-) delete mode 100644 doc/source/ref/backup_schedules.rst delete mode 100644 doc/source/ref/ipgroups.rst diff --git a/doc/source/api.rst b/doc/source/api.rst index 6e2b1002a..78c0a5c32 100644 --- a/doc/source/api.rst +++ b/doc/source/api.rst @@ -9,39 +9,14 @@ The :mod:`novaclient` Python API Usage ----- -First create an instance of :class:`OpenStack` with your credentials:: +First create a client instance with your credentials:: - >>> from novaclient import OpenStack - >>> nova = OpenStack(USERNAME, PASSWORD, AUTH_URL) + >>> from novaclient.client import Client + >>> nova = Client(VERSION, USERNAME, PASSWORD, PROJECT_ID, AUTH_URL) -Then call methods on the :class:`OpenStack` object: +Here ``VERSION`` can be: ``1.1``, ``2`` and ``3``. -.. class:: OpenStack - - .. attribute:: backup_schedules - - A :class:`BackupScheduleManager` -- manage automatic backup images. - - .. attribute:: flavors - - A :class:`FlavorManager` -- query available "flavors" (hardware - configurations). - - .. attribute:: images - - An :class:`ImageManager` -- query and create server disk images. - - .. attribute:: ipgroups - - A :class:`IPGroupManager` -- manage shared public IP addresses. - - .. attribute:: servers - - A :class:`ServerManager` -- start, stop, and manage virtual machines. - - .. automethod:: authenticate - -For example:: +Then call methods on its managers:: >>> nova.servers.list() [] @@ -59,6 +34,9 @@ For example:: >>> nova.servers.create("my-server", flavor=fl) +Reference +--------- + For more information, see the reference: .. toctree:: diff --git a/doc/source/conf.py b/doc/source/conf.py index 4583d15e5..2d93c02bb 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -18,6 +18,14 @@ # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.append(os.path.abspath('.')) +import os +import sys + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.abspath(os.path.join(BASE_DIR, "..", "..")) + +sys.path.insert(0, ROOT) +sys.path.insert(0, BASE_DIR) # -- General configuration ---------------------------------------------------- @@ -130,7 +138,7 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +# html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. diff --git a/doc/source/ref/backup_schedules.rst b/doc/source/ref/backup_schedules.rst deleted file mode 100644 index 28163480b..000000000 --- a/doc/source/ref/backup_schedules.rst +++ /dev/null @@ -1,60 +0,0 @@ -Backup schedules -================ - -.. currentmodule:: novaclient - -Rackspace allows scheduling of weekly and/or daily backups for virtual -servers. You can access these backup schedules either off the API object as -:attr:`OpenStack.backup_schedules`, or directly off a particular -:class:`Server` instance as :attr:`Server.backup_schedule`. - -Classes -------- - -.. autoclass:: BackupScheduleManager - :members: create, delete, update, get - -.. autoclass:: BackupSchedule - :members: update, delete - - .. attribute:: enabled - - Is this backup enabled? (boolean) - - .. attribute:: weekly - - The day of week upon which to perform a weekly backup. - - .. attribute:: daily - - The daily time period during which to perform a daily backup. - -Constants ---------- - -Constants for selecting weekly backup days: - - .. data:: BACKUP_WEEKLY_DISABLED - .. data:: BACKUP_WEEKLY_SUNDAY - .. data:: BACKUP_WEEKLY_MONDAY - .. data:: BACKUP_WEEKLY_TUESDAY - .. data:: BACKUP_WEEKLY_WEDNESDA - .. data:: BACKUP_WEEKLY_THURSDAY - .. data:: BACKUP_WEEKLY_FRIDAY - .. data:: BACKUP_WEEKLY_SATURDAY - -Constants for selecting hourly backup windows: - - .. data:: BACKUP_DAILY_DISABLED - .. data:: BACKUP_DAILY_H_0000_0200 - .. data:: BACKUP_DAILY_H_0200_0400 - .. data:: BACKUP_DAILY_H_0400_0600 - .. data:: BACKUP_DAILY_H_0600_0800 - .. data:: BACKUP_DAILY_H_0800_1000 - .. data:: BACKUP_DAILY_H_1000_1200 - .. data:: BACKUP_DAILY_H_1200_1400 - .. data:: BACKUP_DAILY_H_1400_1600 - .. data:: BACKUP_DAILY_H_1600_1800 - .. data:: BACKUP_DAILY_H_1800_2000 - .. data:: BACKUP_DAILY_H_2000_2200 - .. data:: BACKUP_DAILY_H_2200_0000 diff --git a/doc/source/ref/exceptions.rst b/doc/source/ref/exceptions.rst index 0744edc22..5f593c73e 100644 --- a/doc/source/ref/exceptions.rst +++ b/doc/source/ref/exceptions.rst @@ -1,14 +1,8 @@ Exceptions ========== -.. currentmodule:: novaclient - -Exceptions ----------- - -Exceptions that the API might throw: - -.. automodule:: novaclient - :members: OpenStackException, BadRequest, Unauthorized, Forbidden, - NotFound, OverLimit - +.. automodule:: novaclient.exceptions + :members: + :undoc-members: + :show-inheritance: + :noindex: diff --git a/doc/source/ref/flavors.rst b/doc/source/ref/flavors.rst index 6b281a0e4..1e04fe752 100644 --- a/doc/source/ref/flavors.rst +++ b/doc/source/ref/flavors.rst @@ -10,7 +10,7 @@ From Rackspace's API documentation: Classes ------- -.. currentmodule:: novaclient +.. currentmodule:: novaclient.v1_1.flavors .. autoclass:: FlavorManager :members: get, list, find, findall diff --git a/doc/source/ref/images.rst b/doc/source/ref/images.rst index 7d1ceab70..6f58c0ae1 100644 --- a/doc/source/ref/images.rst +++ b/doc/source/ref/images.rst @@ -1,7 +1,7 @@ Images ====== -.. currentmodule:: novaclient +.. currentmodule:: novaclient.v1_1.images An "image" is a snapshot from which you can create new server instances. @@ -18,7 +18,7 @@ Classes ------- .. autoclass:: ImageManager - :members: get, list, find, findall, create, delete + :members: get, list, find, findall, delete .. autoclass:: Image :members: delete diff --git a/doc/source/ref/index.rst b/doc/source/ref/index.rst index c1fe136bb..2db23af4c 100644 --- a/doc/source/ref/index.rst +++ b/doc/source/ref/index.rst @@ -1,12 +1,10 @@ -API Reference -============= +Version 1.1, Version 2 API Reference +======================================= .. toctree:: :maxdepth: 1 - - backup_schedules + exceptions flavors images - ipgroups - servers \ No newline at end of file + servers diff --git a/doc/source/ref/ipgroups.rst b/doc/source/ref/ipgroups.rst deleted file mode 100644 index a08d0668f..000000000 --- a/doc/source/ref/ipgroups.rst +++ /dev/null @@ -1,46 +0,0 @@ -Shared IP addresses -=================== - -From the Rackspace API guide: - - Public IP addresses can be shared across multiple servers for use in - various high availability scenarios. When an IP address is shared to - another server, the cloud network restrictions are modified to allow each - server to listen to and respond on that IP address (you may optionally - specify that the target server network configuration be modified). Shared - IP addresses can be used with many standard heartbeat facilities (e.g. - ``keepalived``) that monitor for failure and manage IP failover. - - A shared IP group is a collection of servers that can share IPs with other - members of the group. Any server in a group can share one or more public - IPs with any other server in the group. With the exception of the first - server in a shared IP group, servers must be launched into shared IP - groups. A server may only be a member of one shared IP group. - -.. seealso:: - - Use :meth:`Server.share_ip` and `Server.unshare_ip` to share and unshare - IPs in a group. - -Classes -------- - -.. currentmodule:: novaclient - -.. autoclass:: IPGroupManager - :members: get, list, find, findall, create, delete - -.. autoclass:: IPGroup - :members: delete - - .. attribute:: id - - Shared group ID. - - .. attribute:: name - - Name of the group. - - .. attribute:: servers - - A list of server IDs in this group. diff --git a/doc/source/ref/servers.rst b/doc/source/ref/servers.rst index 529b807d8..3a8e723d1 100644 --- a/doc/source/ref/servers.rst +++ b/doc/source/ref/servers.rst @@ -6,68 +6,8 @@ A virtual machine instance. Classes ------- -.. currentmodule:: novaclient - -.. autoclass:: ServerManager - :members: get, list, find, findall, create, update, delete, share_ip, - unshare_ip, reboot, rebuild, resize, confirm_resize, - revert_resize - -.. autoclass:: Server - :members: update, delete, share_ip, unshare_ip, reboot, rebuild, resize, - confirm_resize, revert_resize - - .. attribute:: id - - This server's ID. - - .. attribute:: name - - The name you gave the server when you booted it. - - .. attribute:: imageId - - The :class:`Image` this server was booted with. - - .. attribute:: flavorId - - This server's current :class:`Flavor`. - - .. attribute:: hostId - - Rackspace doesn't document this value. It appears to be SHA1 hash. - - .. attribute:: status - - The server's status (``BOOTING``, ``ACTIVE``, etc). - - .. attribute:: progress - - When booting, resizing, updating, etc., this will be set to a - value between 0 and 100 giving a rough estimate of the progress - of the current operation. - - .. attribute:: addresses - - The public and private IP addresses of this server. This'll be a dict - of the form:: - - { - "public" : ["67.23.10.138"], - "private" : ["10.176.42.19"] - } - - You *can* get more than one public/private IP provisioned, but not - directly from the API; you'll need to open a support ticket. - - .. attribute:: metadata - - The metadata dict you gave when creating the server. - -Constants ---------- - -Reboot types: - -.. data:: REBOOT_SOFT -.. data:: REBOOT_HARD +.. automodule:: novaclient.v1_1.servers + :members: + :undoc-members: + :show-inheritance: + :noindex: From 35935db64766a595430ac4fa2ea4b2e4e1afa059 Mon Sep 17 00:00:00 2001 From: Xiao Chen Date: Wed, 18 Dec 2013 15:40:27 +0800 Subject: [PATCH 0348/1705] Allow multiple volume delete from cli like Cinder In cinder project, we can use cli like follow: cinder delete xxx yyy zzz But in nova, we can not use follow command: nova volume-delete xxx yyy zzz This patch will add a feature in nova to allow multiple volume delete from cli like cinder. Change-Id: I4dd182673e58406a57d30c572ec3116f9f4ce315 Closes-Bug: #1262061 --- novaclient/tests/v1_1/fakes.py | 91 ++++++++++++++++++----------- novaclient/tests/v1_1/test_shell.py | 9 +++ novaclient/v1_1/shell.py | 13 +++-- novaclient/v3/shell.py | 13 +++-- 4 files changed, 82 insertions(+), 44 deletions(-) diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index a6699c0b1..f178db214 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -1811,45 +1811,65 @@ def delete_servers_1234_os_interface_port_id(self, **kw): def get_volumes_detail(self, **kw): return (200, {}, {"volumes": [ - {"display_name": "Work", - "display_description": "volume for work", - "status": "ATTACHED", - "id": "15e59938-07d5-11e1-90e3-e3dffe0c5983", - "created_at": "2011-09-09T00:00:00Z", - "attached": "2011-11-11T00:00:00Z", - "size": 1024, - "attachments": [ - {"id": "3333", - "links": ''}], - "metadata": {}}]}) + { + "display_name": "Work", + "display_description": "volume for work", + "status": "ATTACHED", + "id": "15e59938-07d5-11e1-90e3-e3dffe0c5983", + "created_at": "2011-09-09T00:00:00Z", + "attached": "2011-11-11T00:00:00Z", + "size": 1024, + "attachments": [ + {"id": "3333", + "links": ''}], + "metadata": {}}, + { + "display_name": "Work2", + "display_description": "volume for work2", + "status": "ATTACHED", + "id": "15e59938-07d5-11e1-90e3-ee32ba30feaa", + "created_at": "2011-09-09T00:00:00Z", + "attached": "2011-11-11T00:00:00Z", + "size": 1024, + "attachments": [ + {"id": "2222", + "links": ''}], + "metadata": {}}]}) def get_volumes(self, **kw): return (200, {}, {"volumes": [ - {"display_name": "Work", - "display_description": "volume for work", - "status": "ATTACHED", - "id": "15e59938-07d5-11e1-90e3-e3dffe0c5983", - "created_at": "2011-09-09T00:00:00Z", - "attached": "2011-11-11T00:00:00Z", - "size": 1024, - "attachments": [ - {"id": "3333", - "links": ''}], - "metadata": {}}]}) + { + "display_name": "Work", + "display_description": "volume for work", + "status": "ATTACHED", + "id": "15e59938-07d5-11e1-90e3-e3dffe0c5983", + "created_at": "2011-09-09T00:00:00Z", + "attached": "2011-11-11T00:00:00Z", + "size": 1024, + "attachments": [ + {"id": "3333", + "links": ''}], + "metadata": {}}, + { + "display_name": "Work2", + "display_description": "volume for work2", + "status": "ATTACHED", + "id": "15e59938-07d5-11e1-90e3-ee32ba30feaa", + "created_at": "2011-09-09T00:00:00Z", + "attached": "2011-11-11T00:00:00Z", + "size": 1024, + "attachments": [ + {"id": "2222", + "links": ''}], + "metadata": {}}]}) def get_volumes_15e59938_07d5_11e1_90e3_e3dffe0c5983(self, **kw): - return (200, {}, {"volume": - {"display_name": "Work", - "display_description": "volume for work", - "status": "ATTACHED", - "id": "15e59938-07d5-11e1-90e3-e3dffe0c5983", - "created_at": "2011-09-09T00:00:00Z", - "attached": "2011-11-11T00:00:00Z", - "size": 1024, - "attachments": [ - {"id": "3333", - "links": ''}], - "metadata": {}}}) + return (200, {}, { + "volume": self.get_volumes_detail()[2]['volumes'][0]}) + + def get_volumes_15e59938_07d5_11e1_90e3_ee32ba30feaa(self, **kw): + return (200, {}, { + "volume": self.get_volumes_detail()[2]['volumes'][1]}) def post_volumes(self, **kw): return (200, {}, {"volume": @@ -1868,6 +1888,9 @@ def post_volumes(self, **kw): def delete_volumes_15e59938_07d5_11e1_90e3_e3dffe0c5983(self, **kw): return (200, {}, {}) + def delete_volumes_15e59938_07d5_11e1_90e3_ee32ba30feaa(self, **kw): + return (200, {}, {}) + def post_servers_1234_os_volume_attachments(self, **kw): return (200, {}, {"volumeAttachment": {"device": "/dev/vdb", diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index c02dc3dcf..3455ae536 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -1783,6 +1783,15 @@ def test_volume_delete(self): self.assert_called('DELETE', '/volumes/15e59938-07d5-11e1-90e3-e3dffe0c5983') + def test_volume_delete_multiple(self): + self.run_command('volume-delete Work Work2') + self.assert_called('DELETE', + '/volumes/15e59938-07d5-11e1-90e3-e3dffe0c5983', + pos=-5) + self.assert_called('DELETE', + '/volumes/15e59938-07d5-11e1-90e3-ee32ba30feaa', + pos=-1) + def test_volume_attach(self): self.run_command('volume-attach sample-server Work /dev/vdb') self.assert_called('POST', '/servers/1234/os-volume_attachments', diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 890fd0daa..e8d823e3a 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1692,13 +1692,16 @@ def do_volume_create(cs, args): @utils.arg('volume', - metavar='', - help='Name or ID of the volume to delete.') + metavar='', nargs='+', + help='Name or ID of the volume(s) to delete.') @utils.service_type('volume') def do_volume_delete(cs, args): - """Remove a volume.""" - volume = _find_volume(cs, args.volume) - volume.delete() + """Remove volume(s).""" + for volume in args.volume: + try: + _find_volume(cs, volume).delete() + except Exception as e: + print("Delete for volume %s failed: %s" % (volume, e)) @utils.arg('server', diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 4dd5df8a9..1a1dae4fb 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -1522,13 +1522,16 @@ def do_volume_create(cs, args): @utils.arg('volume', - metavar='', - help='Name or ID of the volume to delete.') + metavar='', nargs='+', + help='Name or ID of the volume(s) to delete.') @utils.service_type('volume') def do_volume_delete(cs, args): - """Remove a volume.""" - volume = _find_volume(cs, args.volume) - volume.delete() + """Remove volume(s).""" + for volume in args.volume: + try: + _find_volume(cs, volume).delete() + except Exception as e: + print("Delete for volume %s failed: %s" % (volume, e)) @utils.arg('server', From 7e50eae5ac662609d0cecb4ede4fe38d256b5cd2 Mon Sep 17 00:00:00 2001 From: Sean Dague Date: Wed, 18 Dec 2013 16:15:26 -0500 Subject: [PATCH 0349/1705] remove duplicate six import apparently we had a duplicate import of six, noticed by pyflakes in my buffer when loading up the file. Change-Id: I2f5e03d1862711c7214c1e33b4500693aaabf567 --- novaclient/v1_1/shell.py | 1 - 1 file changed, 1 deletion(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 890fd0daa..a4dd87045 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -24,7 +24,6 @@ import getpass import locale import os -import six import sys import time From 6ea070d62e252bf181145ccc78923a60bb6a0c2b Mon Sep 17 00:00:00 2001 From: Sean Dague Date: Wed, 18 Dec 2013 17:35:42 -0500 Subject: [PATCH 0350/1705] add support for nova ssh user@host the nova ssh command is convenient, but it is lacking some basic niceties that everyone is used to with ssh, for instance actually doing user@host to connect to an environment. This adds support for "nova ssh user@host" to work as one would expect. The functionality is added to both v1.1 and v3 clients. Tests for this, and other ssh behavior are added to the v1.1 tree (the v3 test_shell.py has not synced over yet). Change-Id: Ic4081f85c848507ebdc5e228ac345faf19127168 --- novaclient/tests/v1_1/test_shell.py | 19 +++++++++++++++++++ novaclient/v1_1/shell.py | 5 +++++ novaclient/v3/shell.py | 5 +++++ 3 files changed, 29 insertions(+) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index c02dc3dcf..6885e96d5 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -1832,6 +1832,25 @@ def test_migration_list_with_filters(self): '/os-migrations?cell_name=child1&host=host1' '&status=finished') + @mock.patch('novaclient.v1_1.shell._find_server') + @mock.patch('os.system') + def test_ssh(self, mock_system, mock_find_server): + class FakeResources(object): + addresses = { + "private": [{'version': 4, 'addr': "1.1.1.1"}], + "public": [{'version': 4, 'addr': "2.2.2.2"}] + } + mock_find_server.return_value = FakeResources() + + self.run_command("ssh --login bob server") + mock_system.assert_any_call("ssh -4 -p22 bob@2.2.2.2 ") + self.run_command("ssh alice@server") + mock_system.assert_any_call("ssh -4 -p22 alice@2.2.2.2 ") + self.run_command("ssh --port 202 server") + mock_system.assert_any_call("ssh -4 -p202 root@2.2.2.2 ") + self.run_command("ssh --private server") + mock_system.assert_any_call("ssh -4 -p22 root@1.1.1.1 ") + class GetSecgroupTest(utils.TestCase): def test_with_integer(self): diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index a4dd87045..8479429ea 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -3034,6 +3034,11 @@ def do_credentials(cs, _args): default='') def do_ssh(cs, args): """SSH into a server.""" + if '@' in args.server: + user, server = args.server.split('@', 1) + args.login = user + args.server = server + addresses = _find_server(cs, args.server).addresses address_type = "private" if args.private else "public" version = 6 if args.ipv6 else 4 diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 4dd5df8a9..f91fdfce2 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -2801,6 +2801,11 @@ def do_credentials(cs, _args): default='') def do_ssh(cs, args): """SSH into a server.""" + if '@' in args.server: + user, server = args.server.split('@', 1) + args.login = user + args.server = server + addresses = _find_server(cs, args.server).addresses address_type = "private" if args.private else "public" version = 6 if args.ipv6 else 4 From 1746bbac26fc112794d70af16d6bd0ad00f6cd71 Mon Sep 17 00:00:00 2001 From: Chris Yeoh Date: Tue, 10 Dec 2013 15:49:05 +1030 Subject: [PATCH 0351/1705] Adds basic servers support for the Nova V3 API Ports servers support from the v1_1 directory omitting functionality which is no longer supported directly in the V3 API such as: - floating ip - add/remove/list of security groups - disk config related functionality Makes the appropriate modifications required for parameter name changes between the V2 and V3 API Booting an instance is not yet supported as this will require more extensive changes as image listing services are no longer proxied through the Nova V3 API Partially implements blueprint v3-api Change-Id: I1116e8d8ebc73176223f4135168cc4ab5d904356 --- novaclient/tests/v3/fakes.py | 101 ++++ novaclient/tests/v3/test_servers.py | 408 +++++++++++++ novaclient/v3/client.py | 2 + novaclient/v3/servers.py | 892 ++++++++++++++++++++++++++++ novaclient/v3/shell.py | 14 +- 5 files changed, 1410 insertions(+), 7 deletions(-) create mode 100644 novaclient/tests/v3/test_servers.py create mode 100644 novaclient/v3/servers.py diff --git a/novaclient/tests/v3/fakes.py b/novaclient/tests/v3/fakes.py index afe73ce51..c2fd0c8cf 100644 --- a/novaclient/tests/v3/fakes.py +++ b/novaclient/tests/v3/fakes.py @@ -127,3 +127,104 @@ def head_v1_images_1(self, **kw): 'x-image-meta-status': 'ACTIVE', 'x-image-meta-property-test_key': 'test_value'} return 200, headers, '' + + # + # Servers + # + get_servers_1234_os_server_diagnostics = ( + fakes_v1_1.FakeHTTPClient.get_servers_1234_diagnostics) + + delete_servers_1234_os_attach_interfaces_port_id = ( + fakes_v1_1.FakeHTTPClient.delete_servers_1234_os_interface_port_id) + + def get_servers_1234_os_attach_interfaces(self, **kw): + return (200, {}, {"interface_attachments": [ + {"port_state": "ACTIVE", + "net_id": "net-id-1", + "port_id": "port-id-1", + "mac_address": "aa:bb:cc:dd:ee:ff", + "fixed_ips": [{"ip_address": "1.2.3.4"}], + }, + {"port_state": "ACTIVE", + "net_id": "net-id-1", + "port_id": "port-id-1", + "mac_address": "aa:bb:cc:dd:ee:ff", + "fixed_ips": [{"ip_address": "1.2.3.4"}], + }]}) + + def post_servers_1234_os_attach_interfaces(self, **kw): + return (200, {}, {'interface_attachment': {}}) + + # + # Server Actions + # + def post_servers_1234_action(self, body, **kw): + _headers = None + resp = 202 + body_is_none_list = [ + 'revert_resize', 'migrate', 'stop', 'start', 'force_delete', + 'restore', 'pause', 'unpause', 'lock', 'unlock', 'unrescue', + 'resume', 'suspend', 'lock', 'unlock', 'shelve', 'shelve_offload', + 'unshelve', 'reset_network', 'rescue', 'confirm_resize'] + body_return_map = { + 'rescue': {'admin_password': 'RescuePassword'}, + 'get_console_output': {'output': 'foo'}, + 'rebuild': self.get_servers_1234()[2], + } + body_param_check_exists = { + 'rebuild': 'image_ref', + 'resize': 'flavor_ref'} + body_params_check_exact = { + 'reboot': ['type'], + 'add_fixed_ip': ['network_id'], + 'evacuate': ['host', 'on_shared_storage'], + 'remove_fixed_ip': ['address'], + 'change_password': ['admin_password'], + 'get_console_output': ['length'], + 'get_vnc_console': ['type'], + 'get_spice_console': ['type'], + 'reset_state': ['state'], + 'create_image': ['name', 'metadata'], + 'migrate_live': ['host', 'block_migration', 'disk_over_commit'], + 'create_backup': ['name', 'backup_type', 'rotation']} + + assert len(body.keys()) == 1 + action = list(body)[0] + _body = body_return_map.get(action) + + if action in body_is_none_list: + assert body[action] is None + + if action in body_param_check_exists: + assert body_param_check_exists[action] in body[action] + + if action == 'evacuate': + body[action].pop('admin_password', None) + + if action in body_params_check_exact: + assert set(body[action]) == set(body_params_check_exact[action]) + + if action == 'reboot': + assert body[action]['type'] in ['HARD', 'SOFT'] + elif action == 'confirm_resize': + # This one method returns a different response code + resp = 204 + elif action == 'create_image': + _headers = dict(location="http://blah/images/456") + + if action not in set.union(set(body_is_none_list), + set(body_params_check_exact.keys()), + set(body_param_check_exists.keys())): + raise AssertionError("Unexpected server action: %s" % action) + + return (resp, _headers, _body) + + # + # Server password + # + + def get_servers_1234_os_server_password(self, **kw): + return (200, {}, {'password': ''}) + + def delete_servers_1234_os_server_password(self, **kw): + return (202, {}, None) diff --git a/novaclient/tests/v3/test_servers.py b/novaclient/tests/v3/test_servers.py new file mode 100644 index 000000000..e51b3fd9c --- /dev/null +++ b/novaclient/tests/v3/test_servers.py @@ -0,0 +1,408 @@ +# -*- coding: utf-8 -*- +# Copyright 2013 IBM Corp. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import six + +from novaclient import exceptions +from novaclient.tests import utils +from novaclient.tests.v3 import fakes +from novaclient.v3 import servers + + +cs = fakes.FakeClient() + + +class ServersTest(utils.TestCase): + + def test_list_servers(self): + sl = cs.servers.list() + cs.assert_called('GET', '/servers/detail') + for s in sl: + self.assertTrue(isinstance(s, servers.Server)) + + def test_list_servers_undetailed(self): + sl = cs.servers.list(detailed=False) + cs.assert_called('GET', '/servers') + for s in sl: + self.assertTrue(isinstance(s, servers.Server)) + + def test_list_servers_with_marker_limit(self): + sl = cs.servers.list(marker=1234, limit=2) + cs.assert_called('GET', '/servers/detail?limit=2&marker=1234') + for s in sl: + self.assertTrue(isinstance(s, servers.Server)) + + def test_get_server_details(self): + s = cs.servers.get(1234) + cs.assert_called('GET', '/servers/1234') + self.assertTrue(isinstance(s, servers.Server)) + self.assertEqual(s.id, 1234) + self.assertEqual(s.status, 'BUILD') + + def test_get_server_promote_details(self): + s1 = cs.servers.list(detailed=False)[0] + s2 = cs.servers.list(detailed=True)[0] + self.assertNotEqual(s1._info, s2._info) + s1.get() + self.assertEqual(s1._info, s2._info) + + def test_create_server(self): + s = cs.servers.create( + name="My server", + image=1, + flavor=1, + meta={'foo': 'bar'}, + userdata="hello moto", + key_name="fakekey", + files={ + '/etc/passwd': 'some data', # a file + '/tmp/foo.txt': six.StringIO('data'), # a stream + } + ) + cs.assert_called('POST', '/servers') + self.assertTrue(isinstance(s, servers.Server)) + + def test_create_server_userdata_file_object(self): + s = cs.servers.create( + name="My server", + image=1, + flavor=1, + meta={'foo': 'bar'}, + userdata=six.StringIO('hello moto'), + files={ + '/etc/passwd': 'some data', # a file + '/tmp/foo.txt': six.StringIO('data'), # a stream + }, + ) + cs.assert_called('POST', '/servers') + self.assertTrue(isinstance(s, servers.Server)) + + def test_create_server_userdata_unicode(self): + s = cs.servers.create( + name="My server", + image=1, + flavor=1, + meta={'foo': 'bar'}, + userdata=six.u('こんにちは'), + key_name="fakekey", + files={ + '/etc/passwd': 'some data', # a file + '/tmp/foo.txt': six.StringIO('data'), # a stream + }, + ) + cs.assert_called('POST', '/servers') + self.assertTrue(isinstance(s, servers.Server)) + + def test_create_server_userdata_utf8(self): + s = cs.servers.create( + name="My server", + image=1, + flavor=1, + meta={'foo': 'bar'}, + userdata='こんにちは', + key_name="fakekey", + files={ + '/etc/passwd': 'some data', # a file + '/tmp/foo.txt': six.StringIO('data'), # a stream + }, + ) + cs.assert_called('POST', '/servers') + self.assertTrue(isinstance(s, servers.Server)) + + def test_update_server(self): + s = cs.servers.get(1234) + + # Update via instance + s.update(name='hi') + cs.assert_called('PUT', '/servers/1234') + s.update(name='hi') + cs.assert_called('PUT', '/servers/1234') + + # Silly, but not an error + s.update() + + # Update via manager + cs.servers.update(s, name='hi') + cs.assert_called('PUT', '/servers/1234') + + def test_delete_server(self): + s = cs.servers.get(1234) + s.delete() + cs.assert_called('DELETE', '/servers/1234') + cs.servers.delete(1234) + cs.assert_called('DELETE', '/servers/1234') + cs.servers.delete(s) + cs.assert_called('DELETE', '/servers/1234') + + def test_delete_server_meta(self): + s = cs.servers.delete_meta(1234, ['test_key']) + cs.assert_called('DELETE', '/servers/1234/metadata/test_key') + + def test_set_server_meta(self): + s = cs.servers.set_meta(1234, {'test_key': 'test_value'}) + reval = cs.assert_called('POST', '/servers/1234/metadata', + {'metadata': {'test_key': 'test_value'}}) + + def test_find(self): + server = cs.servers.find(name='sample-server') + cs.assert_called('GET', '/servers', pos=-2) + cs.assert_called('GET', '/servers/1234', pos=-1) + self.assertEqual(server.name, 'sample-server') + + self.assertRaises(exceptions.NoUniqueMatch, cs.servers.find, + flavor={"id": 1, "name": "256 MB Server"}) + + sl = cs.servers.findall(flavor={"id": 1, "name": "256 MB Server"}) + self.assertEqual([s.id for s in sl], [1234, 5678, 9012]) + + def test_reboot_server(self): + s = cs.servers.get(1234) + s.reboot() + cs.assert_called('POST', '/servers/1234/action') + cs.servers.reboot(s, reboot_type='HARD') + cs.assert_called('POST', '/servers/1234/action') + + def test_rebuild_server(self): + s = cs.servers.get(1234) + s.rebuild(image=1) + cs.assert_called('POST', '/servers/1234/action') + cs.servers.rebuild(s, image=1) + cs.assert_called('POST', '/servers/1234/action') + s.rebuild(image=1, password='5678') + cs.assert_called('POST', '/servers/1234/action') + cs.servers.rebuild(s, image=1, password='5678') + cs.assert_called('POST', '/servers/1234/action') + + def test_resize_server(self): + s = cs.servers.get(1234) + s.resize(flavor=1) + cs.assert_called('POST', '/servers/1234/action') + cs.servers.resize(s, flavor=1) + cs.assert_called('POST', '/servers/1234/action') + + def test_confirm_resized_server(self): + s = cs.servers.get(1234) + s.confirm_resize() + cs.assert_called('POST', '/servers/1234/action') + cs.servers.confirm_resize(s) + cs.assert_called('POST', '/servers/1234/action') + + def test_revert_resized_server(self): + s = cs.servers.get(1234) + s.revert_resize() + cs.assert_called('POST', '/servers/1234/action') + cs.servers.revert_resize(s) + cs.assert_called('POST', '/servers/1234/action') + + def test_migrate_server(self): + s = cs.servers.get(1234) + s.migrate() + cs.assert_called('POST', '/servers/1234/action') + cs.servers.migrate(s) + cs.assert_called('POST', '/servers/1234/action') + + def test_add_fixed_ip(self): + s = cs.servers.get(1234) + s.add_fixed_ip(1) + cs.assert_called('POST', '/servers/1234/action') + cs.servers.add_fixed_ip(s, 1) + cs.assert_called('POST', '/servers/1234/action') + + def test_remove_fixed_ip(self): + s = cs.servers.get(1234) + s.remove_fixed_ip('10.0.0.1') + cs.assert_called('POST', '/servers/1234/action') + cs.servers.remove_fixed_ip(s, '10.0.0.1') + cs.assert_called('POST', '/servers/1234/action') + + def test_stop(self): + s = cs.servers.get(1234) + s.stop() + cs.assert_called('POST', '/servers/1234/action') + cs.servers.stop(s) + cs.assert_called('POST', '/servers/1234/action') + + def test_force_delete(self): + s = cs.servers.get(1234) + s.force_delete() + cs.assert_called('POST', '/servers/1234/action') + cs.servers.force_delete(s) + cs.assert_called('POST', '/servers/1234/action') + + def test_restore(self): + s = cs.servers.get(1234) + s.restore() + cs.assert_called('POST', '/servers/1234/action') + cs.servers.restore(s) + cs.assert_called('POST', '/servers/1234/action') + + def test_start(self): + s = cs.servers.get(1234) + s.start() + cs.assert_called('POST', '/servers/1234/action') + cs.servers.start(s) + cs.assert_called('POST', '/servers/1234/action') + + def test_rescue(self): + s = cs.servers.get(1234) + s.rescue() + cs.assert_called('POST', '/servers/1234/action') + cs.servers.rescue(s) + cs.assert_called('POST', '/servers/1234/action') + + def test_unrescue(self): + s = cs.servers.get(1234) + s.unrescue() + cs.assert_called('POST', '/servers/1234/action') + cs.servers.unrescue(s) + cs.assert_called('POST', '/servers/1234/action') + + def test_lock(self): + s = cs.servers.get(1234) + s.lock() + cs.assert_called('POST', '/servers/1234/action') + cs.servers.lock(s) + cs.assert_called('POST', '/servers/1234/action') + + def test_unlock(self): + s = cs.servers.get(1234) + s.unlock() + cs.assert_called('POST', '/servers/1234/action') + cs.servers.unlock(s) + cs.assert_called('POST', '/servers/1234/action') + + def test_backup(self): + s = cs.servers.get(1234) + s.backup('back1', 'daily', 1) + cs.assert_called('POST', '/servers/1234/action') + cs.servers.backup(s, 'back1', 'daily', 2) + cs.assert_called('POST', '/servers/1234/action') + + def test_get_console_output_without_length(self): + success = 'foo' + s = cs.servers.get(1234) + s.get_console_output() + self.assertEqual(s.get_console_output(), success) + cs.assert_called('POST', '/servers/1234/action') + + cs.servers.get_console_output(s) + self.assertEqual(cs.servers.get_console_output(s), success) + cs.assert_called('POST', '/servers/1234/action') + + def test_get_console_output_with_length(self): + success = 'foo' + + s = cs.servers.get(1234) + s.get_console_output(length=50) + self.assertEqual(s.get_console_output(length=50), success) + cs.assert_called('POST', '/servers/1234/action') + + cs.servers.get_console_output(s, length=50) + self.assertEqual(cs.servers.get_console_output(s, length=50), success) + cs.assert_called('POST', '/servers/1234/action') + + def test_get_password(self): + s = cs.servers.get(1234) + self.assertEqual(s.get_password('/foo/id_rsa'), '') + cs.assert_called('GET', '/servers/1234/os-server-password') + + def test_clear_password(self): + s = cs.servers.get(1234) + s.clear_password() + cs.assert_called('DELETE', '/servers/1234/os-server-password') + + def test_get_server_diagnostics(self): + s = cs.servers.get(1234) + diagnostics = s.diagnostics() + self.assertTrue(diagnostics is not None) + cs.assert_called('GET', '/servers/1234/os-server-diagnostics') + + diagnostics_from_manager = cs.servers.diagnostics(1234) + self.assertTrue(diagnostics_from_manager is not None) + cs.assert_called('GET', '/servers/1234/os-server-diagnostics') + + self.assertEqual(diagnostics, diagnostics_from_manager) + + def test_get_vnc_console(self): + s = cs.servers.get(1234) + s.get_vnc_console('fake') + cs.assert_called('POST', '/servers/1234/action') + + cs.servers.get_vnc_console(s, 'fake') + cs.assert_called('POST', '/servers/1234/action') + + def test_get_spice_console(self): + s = cs.servers.get(1234) + s.get_spice_console('fake') + cs.assert_called('POST', '/servers/1234/action') + + cs.servers.get_spice_console(s, 'fake') + cs.assert_called('POST', '/servers/1234/action') + + def test_create_image(self): + s = cs.servers.get(1234) + s.create_image('123') + cs.assert_called('POST', '/servers/1234/action') + s.create_image('123', {}) + cs.assert_called('POST', '/servers/1234/action') + cs.servers.create_image(s, '123') + cs.assert_called('POST', '/servers/1234/action') + cs.servers.create_image(s, '123', {}) + + def test_live_migrate_server(self): + s = cs.servers.get(1234) + s.live_migrate(host='hostname', block_migration=False, + disk_over_commit=False) + cs.assert_called('POST', '/servers/1234/action') + cs.servers.live_migrate(s, host='hostname', block_migration=False, + disk_over_commit=False) + cs.assert_called('POST', '/servers/1234/action') + + def test_reset_state(self): + s = cs.servers.get(1234) + s.reset_state('newstate') + cs.assert_called('POST', '/servers/1234/action') + cs.servers.reset_state(s, 'newstate') + cs.assert_called('POST', '/servers/1234/action') + + def test_reset_network(self): + s = cs.servers.get(1234) + s.reset_network() + cs.assert_called('POST', '/servers/1234/action') + cs.servers.reset_network(s) + cs.assert_called('POST', '/servers/1234/action') + + def test_evacuate(self): + s = cs.servers.get(1234) + s.evacuate('fake_target_host', 'True') + cs.assert_called('POST', '/servers/1234/action') + cs.servers.evacuate(s, 'fake_target_host', 'False', 'NewAdminPassword') + cs.assert_called('POST', '/servers/1234/action') + + def test_interface_list(self): + s = cs.servers.get(1234) + s.interface_list() + cs.assert_called('GET', '/servers/1234/os-attach-interfaces') + + def test_interface_attach(self): + s = cs.servers.get(1234) + s.interface_attach(None, None, None) + cs.assert_called('POST', '/servers/1234/os-attach-interfaces') + + def test_interface_detach(self): + s = cs.servers.get(1234) + s.interface_detach('port-id') + cs.assert_called('DELETE', + '/servers/1234/os-attach-interfaces/port-id') diff --git a/novaclient/v3/client.py b/novaclient/v3/client.py index c8af7202d..4cbf8d3e7 100644 --- a/novaclient/v3/client.py +++ b/novaclient/v3/client.py @@ -20,6 +20,7 @@ from novaclient.v3 import flavors from novaclient.v3 import hosts from novaclient.v3 import images +from novaclient.v3 import servers class Client(object): @@ -59,6 +60,7 @@ def __init__(self, username, password, project_id, auth_url=None, self.flavors = flavors.FlavorManager(self) self.flavor_access = flavor_access.FlavorAccessManager(self) self.images = images.ImageManager(self) + self.servers = servers.ServerManager(self) # Add in any extensions... if extensions: diff --git a/novaclient/v3/servers.py b/novaclient/v3/servers.py new file mode 100644 index 000000000..f27b2643d --- /dev/null +++ b/novaclient/v3/servers.py @@ -0,0 +1,892 @@ +# Copyright 2010 Jacob Kaplan-Moss + +# Copyright 2011 OpenStack Foundation +# All Rights Reserved. +# +# 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. + +""" +Server interface. +""" + +import six + +from novaclient import base +from novaclient import crypto +from novaclient.openstack.common.py3kcompat import urlutils + +REBOOT_SOFT, REBOOT_HARD = 'SOFT', 'HARD' + + +class Server(base.Resource): + HUMAN_ID = True + + def __repr__(self): + return "" % self.name + + def delete(self): + """ + Delete (i.e. shut down and delete the image) this server. + """ + self.manager.delete(self) + + def update(self, name=None): + """ + Update the name or the password for this server. + + :param name: Update the server's name. + :param password: Update the root password. + """ + self.manager.update(self, name=name) + + def get_console_output(self, length=None): + """ + Get text console log output from Server. + + :param length: The number of lines you would like to retrieve (as int) + """ + return self.manager.get_console_output(self, length) + + def get_vnc_console(self, console_type): + """ + Get vnc console for a Server. + + :param console_type: Type of console ('novnc' or 'xvpvnc') + """ + return self.manager.get_vnc_console(self, console_type) + + def get_spice_console(self, console_type): + """ + Get spice console for a Server. + + :param console_type: Type of console ('spice-html5') + """ + return self.manager.get_spice_console(self, console_type) + + def get_password(self, private_key): + """ + Get password for a Server. + + :param private_key: Path to private key file for decryption + """ + return self.manager.get_password(self, private_key) + + def clear_password(self): + """ + Get password for a Server. + + """ + return self.manager.clear_password(self) + + def add_fixed_ip(self, network_id): + """ + Add an IP address on a network. + + :param network_id: The ID of the network the IP should be on. + """ + self.manager.add_fixed_ip(self, network_id) + + def remove_floating_ip(self, address): + """ + Remove floating IP from an instance + + :param address: The ip address or FloatingIP to remove + """ + self.manager.remove_floating_ip(self, address) + + def stop(self): + """ + Stop -- Stop the running server. + """ + self.manager.stop(self) + + def force_delete(self): + """ + Force delete -- Force delete a server. + """ + self.manager.force_delete(self) + + def restore(self): + """ + Restore -- Restore a server in 'soft-deleted' state. + """ + self.manager.restore(self) + + def start(self): + """ + Start -- Start the paused server. + """ + self.manager.start(self) + + def pause(self): + """ + Pause -- Pause the running server. + """ + self.manager.pause(self) + + def unpause(self): + """ + Unpause -- Unpause the paused server. + """ + self.manager.unpause(self) + + def lock(self): + """ + Lock -- Lock the instance from certain operations. + """ + self.manager.lock(self) + + def unlock(self): + """ + Unlock -- Remove instance lock. + """ + self.manager.unlock(self) + + def suspend(self): + """ + Suspend -- Suspend the running server. + """ + self.manager.suspend(self) + + def resume(self): + """ + Resume -- Resume the suspended server. + """ + self.manager.resume(self) + + def rescue(self): + """ + Rescue -- Rescue the problematic server. + """ + return self.manager.rescue(self) + + def unrescue(self): + """ + Unrescue -- Unrescue the rescued server. + """ + self.manager.unrescue(self) + + def shelve(self): + """ + Shelve -- Shelve the server. + """ + self.manager.shelve(self) + + def shelve_offload(self): + """ + Shelve_offload -- Remove a shelved server from the compute node. + """ + self.manager.shelve_offload(self) + + def unshelve(self): + """ + Unshelve -- Unshelve the server. + """ + self.manager.unshelve(self) + + def diagnostics(self): + """Diagnostics -- Retrieve server diagnostics.""" + return self.manager.diagnostics(self) + + def migrate(self): + """ + Migrate a server to a new host. + """ + self.manager.migrate(self) + + def remove_fixed_ip(self, address): + """ + Remove an IP address. + + :param address: The IP address to remove. + """ + self.manager.remove_fixed_ip(self, address) + + def change_password(self, password): + """ + Update the password for a server. + """ + self.manager.change_password(self, password) + + def reboot(self, reboot_type=REBOOT_SOFT): + """ + Reboot the server. + + :param reboot_type: either :data:`REBOOT_SOFT` for a software-level + reboot, or `REBOOT_HARD` for a virtual power cycle hard reboot. + """ + self.manager.reboot(self, reboot_type) + + def rebuild(self, image, password=None, **kwargs): + """ + Rebuild -- shut down and then re-image -- this server. + + :param image: the :class:`Image` (or its ID) to re-image with. + :param password: string to set as password on the rebuilt server. + """ + return self.manager.rebuild(self, image, password=password, **kwargs) + + def resize(self, flavor, **kwargs): + """ + Resize the server's resources. + + :param flavor: the :class:`Flavor` (or its ID) to resize to. + + Until a resize event is confirmed with :meth:`confirm_resize`, the old + server will be kept around and you'll be able to roll back to the old + flavor quickly with :meth:`revert_resize`. All resizes are + automatically confirmed after 24 hours. + """ + self.manager.resize(self, flavor, **kwargs) + + def create_image(self, image_name, metadata=None): + """ + Create an image based on this server. + + :param image_name: The name to assign the newly create image. + :param metadata: Metadata to assign to the image. + """ + return self.manager.create_image(self, image_name, metadata) + + def backup(self, backup_name, backup_type, rotation): + """ + Backup a server instance. + + :param backup_name: Name of the backup image + :param backup_type: The backup type, like 'daily' or 'weekly' + :param rotation: Int parameter representing how many backups to + keep around. + """ + self.manager.backup(self, backup_name, backup_type, rotation) + + def confirm_resize(self): + """ + Confirm that the resize worked, thus removing the original server. + """ + self.manager.confirm_resize(self) + + def revert_resize(self): + """ + Revert a previous resize, switching back to the old server. + """ + self.manager.revert_resize(self) + + @property + def networks(self): + """ + Generate a simplified list of addresses + """ + networks = {} + try: + for network_label, address_list in self.addresses.items(): + networks[network_label] = [a['addr'] for a in address_list] + return networks + except Exception: + return {} + + def live_migrate(self, host=None, + block_migration=False, + disk_over_commit=False): + """ + Migrates a running instance to a new machine. + """ + self.manager.live_migrate(self, host, + block_migration, + disk_over_commit) + + def reset_state(self, state='error'): + """ + Reset the state of an instance to active or error. + """ + self.manager.reset_state(self, state) + + def reset_network(self): + """ + Reset network of an instance. + """ + self.manager.reset_network(self) + + def evacuate(self, host, on_shared_storage, password=None): + """ + Evacuate an instance from failed host to specified host. + + :param host: Name of the target host + :param on_shared_storage: Specifies whether instance files located + on shared storage + :param password: string to set as password on the evacuated server. + """ + return self.manager.evacuate(self, host, on_shared_storage, password) + + def interface_list(self): + """ + List interfaces attached to an instance. + """ + return self.manager.interface_list(self) + + def interface_attach(self, port_id, net_id, fixed_ip): + """ + Attach a network interface to an instance. + """ + return self.manager.interface_attach(self, port_id, net_id, fixed_ip) + + def interface_detach(self, port_id): + """ + Detach a network interface from an instance. + """ + return self.manager.interface_detach(self, port_id) + + +class ServerManager(base.BootingManagerWithFind): + resource_class = Server + + def get(self, server): + """ + Get a server. + + :param server: ID of the :class:`Server` to get. + :rtype: :class:`Server` + """ + return self._get("/servers/%s" % base.getid(server), "server") + + def list(self, detailed=True, search_opts=None, marker=None, limit=None): + """ + Get a list of servers. + + :param detailed: Whether to return detailed server info (optional). + :param search_opts: Search options to filter out servers (optional). + :param marker: Begin returning servers that appear later in the server + list than that represented by this server id (optional). + :param limit: Maximum number of servers to return (optional). + + :rtype: list of :class:`Server` + """ + if search_opts is None: + search_opts = {} + + qparams = {} + + for opt, val in six.iteritems(search_opts): + if val: + qparams[opt] = val + + if marker: + qparams['marker'] = marker + + if limit: + qparams['limit'] = limit + + # Transform the dict to a sequence of two-element tuples in fixed + # order, then the encoded string will be consistent in Python 2&3. + if qparams: + new_qparams = sorted(qparams.items(), key=lambda x: x[0]) + query_string = "?%s" % urlutils.urlencode(new_qparams) + else: + query_string = "" + + detail = "" + if detailed: + detail = "/detail" + return self._list("/servers%s%s" % (detail, query_string), "servers") + + def add_fixed_ip(self, server, network_id): + """ + Add an IP address on a network. + + :param server: The :class:`Server` (or its ID) to add an IP to. + :param network_id: The ID of the network the IP should be on. + """ + self._action('add_fixed_ip', server, {'network_id': network_id}) + + def remove_fixed_ip(self, server, address): + """ + Remove an IP address. + + :param server: The :class:`Server` (or its ID) to add an IP to. + :param address: The IP address to remove. + """ + self._action('remove_fixed_ip', server, {'address': address}) + + def get_vnc_console(self, server, console_type): + """ + Get a vnc console for an instance + + :param server: The :class:`Server` (or its ID) to add an IP to. + :param console_type: Type of vnc console to get ('novnc' or 'xvpvnc') + """ + + return self._action('get_vnc_console', server, + {'type': console_type})[1] + + def get_spice_console(self, server, console_type): + """ + Get a spice console for an instance + + :param server: The :class:`Server` (or its ID) to add an IP to. + :param console_type: Type of spice console to get ('spice-html5') + """ + + return self._action('get_spice_console', server, + {'type': console_type})[1] + + def get_password(self, server, private_key): + """ + Get password for an instance + + Requires that openssl is installed and in the path + + :param server: The :class:`Server` (or its ID) to add an IP to. + :param private_key: The private key to decrypt password + """ + + _resp, body = self.api.client.get("/servers/%s/os-server-password" + % base.getid(server)) + if body and body.get('password'): + try: + return crypto.decrypt_password(private_key, body['password']) + except Exception as exc: + return '%sFailed to decrypt:\n%s' % (exc, body['password']) + return '' + + def clear_password(self, server): + """ + Clear password for an instance + + :param server: The :class:`Server` (or its ID) to add an IP to. + """ + + return self._delete("/servers/%s/os-server-password" + % base.getid(server)) + + def stop(self, server): + """ + Stop the server. + """ + return self._action('stop', server, None) + + def force_delete(self, server): + """ + Force delete the server. + """ + return self._action('force_delete', server, None) + + def restore(self, server): + """ + Restore soft-deleted server. + """ + return self._action('restore', server, None) + + def start(self, server): + """ + Start the server. + """ + self._action('start', server, None) + + def pause(self, server): + """ + Pause the server. + """ + self._action('pause', server, None) + + def unpause(self, server): + """ + Unpause the server. + """ + self._action('unpause', server, None) + + def lock(self, server): + """ + Lock the server. + """ + self._action('lock', server, None) + + def unlock(self, server): + """ + Unlock the server. + """ + self._action('unlock', server, None) + + def suspend(self, server): + """ + Suspend the server. + """ + self._action('suspend', server, None) + + def resume(self, server): + """ + Resume the server. + """ + self._action('resume', server, None) + + def rescue(self, server): + """ + Rescue the server. + """ + return self._action('rescue', server, None) + + def unrescue(self, server): + """ + Unrescue the server. + """ + self._action('unrescue', server, None) + + def shelve(self, server): + """ + Shelve the server. + """ + self._action('shelve', server, None) + + def shelve_offload(self, server): + """ + Remove a shelved instance from the compute node. + """ + self._action('shelve_offload', server, None) + + def unshelve(self, server): + """ + Unshelve the server. + """ + self._action('unshelve', server, None) + + def diagnostics(self, server): + """Retrieve server diagnostics.""" + return self.api.client.get("/servers/%s/os-server-diagnostics" % + base.getid(server)) + + def create(self, name, image, flavor, meta=None, files=None, + reservation_id=None, min_count=None, + max_count=None, security_groups=None, userdata=None, + key_name=None, availability_zone=None, + block_device_mapping=None, block_device_mapping_v2=None, + nics=None, scheduler_hints=None, + config_drive=None, **kwargs): + # TODO(anthony): indicate in doc string if param is an extension + # and/or optional + """ + Create (boot) a new server. + + :param name: Something to name the server. + :param image: The :class:`Image` to boot with. + :param flavor: The :class:`Flavor` to boot onto. + :param meta: A dict of arbitrary key/value metadata to store for this + server. A maximum of five entries is allowed, and both + keys and values must be 255 characters or less. + :param files: A dict of files to overrwrite on the server upon boot. + Keys are file names (i.e. ``/etc/passwd``) and values + are the file contents (either as a string or as a + file-like object). A maximum of five entries is allowed, + and each file must be 10k or less. + :param userdata: user data to pass to be exposed by the metadata + server this can be a file type object as well or a + string. + :param reservation_id: a UUID for the set of servers being requested. + :param key_name: (optional extension) name of previously created + keypair to inject into the instance. + :param availability_zone: Name of the availability zone for instance + placement. + :param block_device_mapping: (optional extension) A dict of block + device mappings for this server. + :param block_device_mapping_v2: (optional extension) A dict of block + device mappings for this server. + :param nics: (optional extension) an ordered list of nics to be + added to this server, with information about + connected networks, fixed ips, port etc. + :param scheduler_hints: (optional extension) arbitrary key-value pairs + specified by the client to help boot an instance + :param config_drive: (optional extension) value for config drive + either boolean, or volume-id + """ + if not min_count: + min_count = 1 + if not max_count: + max_count = min_count + if min_count > max_count: + min_count = max_count + + boot_args = [name, image, flavor] + + boot_kwargs = dict( + meta=meta, files=files, userdata=userdata, + reservation_id=reservation_id, min_count=min_count, + max_count=max_count, security_groups=security_groups, + key_name=key_name, availability_zone=availability_zone, + scheduler_hints=scheduler_hints, config_drive=config_drive, + **kwargs) + + if block_device_mapping: + resource_url = "/os-volumes_boot" + boot_kwargs['block_device_mapping'] = block_device_mapping + elif block_device_mapping_v2: + resource_url = "/os-volumes_boot" + boot_kwargs['block_device_mapping_v2'] = block_device_mapping_v2 + else: + resource_url = "/servers" + if nics: + boot_kwargs['nics'] = nics + + response_key = "server" + return self._boot(resource_url, response_key, *boot_args, + **boot_kwargs) + + def update(self, server, name=None): + """ + Update the name or the password for a server. + + :param server: The :class:`Server` (or its ID) to update. + :param name: Update the server's name. + """ + if name is None: + return + + body = { + "server": { + "name": name, + }, + } + + return self._update("/servers/%s" % base.getid(server), body, "server") + + def change_password(self, server, password): + """ + Update the password for a server. + """ + self._action("change_password", server, {"admin_password": password}) + + def delete(self, server): + """ + Delete (i.e. shut down and delete the image) this server. + """ + self._delete("/servers/%s" % base.getid(server)) + + def reboot(self, server, reboot_type=REBOOT_SOFT): + """ + Reboot a server. + + :param server: The :class:`Server` (or its ID) to share onto. + :param reboot_type: either :data:`REBOOT_SOFT` for a software-level + reboot, or `REBOOT_HARD` for a virtual power cycle hard reboot. + """ + self._action('reboot', server, {'type': reboot_type}) + + def rebuild(self, server, image, password=None, **kwargs): + """ + Rebuild -- shut down and then re-image -- a server. + + :param server: The :class:`Server` (or its ID) to share onto. + :param image: the :class:`Image` (or its ID) to re-image with. + :param password: string to set as password on the rebuilt server. + """ + body = {'image_ref': base.getid(image)} + if password is not None: + body['admin_password'] = password + + _resp, body = self._action('rebuild', server, body, **kwargs) + return Server(self, body['server']) + + def migrate(self, server): + """ + Migrate a server to a new host. + + :param server: The :class:`Server` (or its ID). + """ + self._action('migrate', server) + + def resize(self, server, flavor, **kwargs): + """ + Resize a server's resources. + + :param server: The :class:`Server` (or its ID) to share onto. + :param flavor: the :class:`Flavor` (or its ID) to resize to. + + Until a resize event is confirmed with :meth:`confirm_resize`, the old + server will be kept around and you'll be able to roll back to the old + flavor quickly with :meth:`revert_resize`. All resizes are + automatically confirmed after 24 hours. + """ + info = {'flavor_ref': base.getid(flavor)} + + self._action('resize', server, info=info, **kwargs) + + def confirm_resize(self, server): + """ + Confirm that the resize worked, thus removing the original server. + + :param server: The :class:`Server` (or its ID) to share onto. + """ + self._action('confirm_resize', server) + + def revert_resize(self, server): + """ + Revert a previous resize, switching back to the old server. + + :param server: The :class:`Server` (or its ID) to share onto. + """ + self._action('revert_resize', server) + + def create_image(self, server, image_name, metadata=None): + """ + Snapshot a server. + + :param server: The :class:`Server` (or its ID) to share onto. + :param image_name: Name to give the snapshot image + :param meta: Metadata to give newly-created image entity + """ + body = {'name': image_name, 'metadata': metadata or {}} + resp = self._action('create_image', server, body)[0] + location = resp.headers['location'] + image_uuid = location.split('/')[-1] + return image_uuid + + def backup(self, server, backup_name, backup_type, rotation): + """ + Backup a server instance. + + :param server: The :class:`Server` (or its ID) to share onto. + :param backup_name: Name of the backup image + :param backup_type: The backup type, like 'daily' or 'weekly' + :param rotation: Int parameter representing how many backups to + keep around. + """ + body = {'name': backup_name, + 'backup_type': backup_type, + 'rotation': rotation} + self._action('create_backup', server, body) + + def set_meta(self, server, metadata): + """ + Set a servers metadata + :param server: The :class:`Server` to add metadata to + :param metadata: A dict of metadata to add to the server + """ + body = {'metadata': metadata} + return self._create("/servers/%s/metadata" % base.getid(server), + body, "metadata") + + def get_console_output(self, server, length=None): + """ + Get text console log output from Server. + + :param server: The :class:`Server` (or its ID) whose console output + you would like to retrieve. + :param length: The number of tail loglines you would like to retrieve. + """ + return self._action('get_console_output', + server, {'length': length})[1]['output'] + + def delete_meta(self, server, keys): + """ + Delete metadata from an server + :param server: The :class:`Server` to add metadata to + :param keys: A list of metadata keys to delete from the server + """ + for k in keys: + self._delete("/servers/%s/metadata/%s" % (base.getid(server), k)) + + def live_migrate(self, server, host, block_migration, disk_over_commit): + """ + Migrates a running instance to a new machine. + + :param server: instance id which comes from nova list. + :param host: destination host name. + :param block_migration: if True, do block_migration. + :param disk_over_commit: if True, Allow overcommit. + + """ + self._action('migrate_live', server, + {'host': host, + 'block_migration': block_migration, + 'disk_over_commit': disk_over_commit}) + + def reset_state(self, server, state='error'): + """ + Reset the state of an instance to active or error. + + :param server: ID of the instance to reset the state of. + :param state: Desired state; either 'active' or 'error'. + Defaults to 'error'. + """ + self._action('reset_state', server, dict(state=state)) + + def reset_network(self, server): + """ + Reset network of an instance. + """ + self._action('reset_network', server) + + def evacuate(self, server, host, on_shared_storage, password=None): + """ + Evacuate a server instance. + + :param server: The :class:`Server` (or its ID) to share onto. + :param host: Name of the target host. + :param on_shared_storage: Specifies whether instance files located + on shared storage + :param password: string to set as password on the evacuated server. + """ + body = { + 'host': host, + 'on_shared_storage': on_shared_storage, + } + + if password is not None: + body['admin_password'] = password + + return self._action('evacuate', server, body) + + def interface_list(self, server): + """ + List attached network interfaces + + :param server: The :class:`Server` (or its ID) to query. + """ + return self._list('/servers/%s/os-attach-interfaces' + % base.getid(server), 'interface_attachments') + + def interface_attach(self, server, port_id, net_id, fixed_ip): + """ + Attach a network_interface to an instance. + + :param server: The :class:`Server` (or its ID) to attach to. + :param port_id: The port to attach. + """ + + body = {'interface_attachment': {}} + if port_id: + body['interface_attachment']['port_id'] = port_id + if net_id: + body['interface_attachment']['net_id'] = net_id + if fixed_ip: + body['interface_attachment']['fixed_ips'] = [ + {'ip_address': fixed_ip}] + + return self._create('/servers/%s/os-attach-interfaces' + % base.getid(server), + body, 'interface_attachment') + + def interface_detach(self, server, port_id): + """ + Detach a network_interface from an instance. + + :param server: The :class:`Server` (or its ID) to detach from. + :param port_id: The port to detach. + """ + self._delete('/servers/%s/os-attach-interfaces/%s' + % (base.getid(server), port_id)) + + def _action(self, action, server, info=None, **kwargs): + """ + Perform a server "action" -- reboot/rebuild/resize/etc. + """ + body = {action: info} + self.run_hooks('modify_body_for_action', body, **kwargs) + url = '/servers/%s/action' % base.getid(server) + return self.api.client.post(url, body=body) diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 9d36099b8..52aa45936 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -34,7 +34,7 @@ from novaclient import utils from novaclient.v1_1 import availability_zones from novaclient.v1_1 import quotas -from novaclient.v1_1 import servers +from novaclient.v3 import servers def _key_value_pairing(text): @@ -307,7 +307,7 @@ def do_boot(cs, args): server = cs.servers.create(*boot_args, **boot_kwargs) - # Keep any information (like adminPass) returned by create + # Keep any information (like admin_password) returned by create info = server._info server = cs.servers.get(info['id']) info.update(server._info) @@ -998,11 +998,11 @@ def do_list(cs, args): servers = cs.servers.list(detailed=detailed, search_opts=search_opts) - convert = [('OS-EXT-SRV-ATTR:host', 'host'), - ('OS-EXT-STS:task_state', 'task_state'), - ('OS-EXT-SRV-ATTR:instance_name', 'instance_name'), - ('OS-EXT-STS:power_state', 'power_state'), - ('hostId', 'host_id')] + convert = [('os-extended-server-attributes:hypervisor_hostname', 'host'), + ('os-extended-status:task_state', 'task_state'), + ('os-extended-server-attributes:instance_name', + 'instance_name'), + ('os-extended-status:power_state', 'power_state')] _translate_keys(servers, convert) _translate_extended_states(servers) if args.minimal: From 40a1c128283f64ed7d30f37004112429bc1f0628 Mon Sep 17 00:00:00 2001 From: Chris Yeoh Date: Wed, 11 Dec 2013 13:24:56 +1030 Subject: [PATCH 0352/1705] Adds availability zone support for Nova V3 API Adds support and tests for the os-availability-zones extension for the Nova V3 API. Also implements sorting for zone host display which was applied to the v1_1 version, but not the v3 version in I9ab25ef52d6d19b45a39f04cbcde864ee225b4cc Partially implements blueprint v3-api Change-Id: I8daa2503a2dc8767e9157bdfa6c9adaedfc8f3c0 --- .../tests/v1_1/test_availability_zone.py | 38 ++++++++++------ novaclient/tests/v3/fakes.py | 43 +++++++++++++++++++ novaclient/tests/v3/test_availability_zone.py | 38 ++++++++++++++++ novaclient/v1_1/availability_zones.py | 6 ++- novaclient/v3/availability_zones.py | 33 ++++++++++++++ novaclient/v3/client.py | 3 ++ novaclient/v3/shell.py | 34 +++++++-------- 7 files changed, 162 insertions(+), 33 deletions(-) create mode 100644 novaclient/tests/v3/test_availability_zone.py create mode 100644 novaclient/v3/availability_zones.py diff --git a/novaclient/tests/v1_1/test_availability_zone.py b/novaclient/tests/v1_1/test_availability_zone.py index fe9e885f4..b248b6f0e 100644 --- a/novaclient/tests/v1_1/test_availability_zone.py +++ b/novaclient/tests/v1_1/test_availability_zone.py @@ -19,33 +19,43 @@ from novaclient.tests import utils from novaclient.tests.v1_1 import fakes from novaclient.v1_1 import availability_zones -from novaclient.v1_1 import shell -cs = fakes.FakeClient() +class AvailabilityZoneTest(utils.TestCase): + # NOTE(cyeoh): import shell here so the V3 version of + # this class can inherit off the v3 version of shell + from novaclient.v1_1 import shell # noqa + def setUp(self): + super(AvailabilityZoneTest, self).setUp() + self.cs = self._get_fake_client() + self.availability_zone_type = self._get_availability_zone_type() -class AvailabilityZoneTest(utils.TestCase): + def _get_fake_client(self): + return fakes.FakeClient() + + def _get_availability_zone_type(self): + return availability_zones.AvailabilityZone def _assertZone(self, zone, name, status): self.assertEqual(zone.zoneName, name) self.assertEqual(zone.zoneState, status) def test_list_availability_zone(self): - zones = cs.availability_zones.list(detailed=False) - cs.assert_called('GET', '/os-availability-zone') + zones = self.cs.availability_zones.list(detailed=False) + self.cs.assert_called('GET', '/os-availability-zone') for zone in zones: self.assertTrue(isinstance(zone, - availability_zones.AvailabilityZone)) + self.availability_zone_type)) self.assertEqual(2, len(zones)) l0 = [six.u('zone-1'), six.u('available')] l1 = [six.u('zone-2'), six.u('not available')] - z0 = shell._treeizeAvailabilityZone(zones[0]) - z1 = shell._treeizeAvailabilityZone(zones[1]) + z0 = self.shell._treeizeAvailabilityZone(zones[0]) + z1 = self.shell._treeizeAvailabilityZone(zones[1]) self.assertEqual((len(z0), len(z1)), (1, 1)) @@ -53,12 +63,12 @@ def test_list_availability_zone(self): self._assertZone(z1[0], l1[0], l1[1]) def test_detail_availability_zone(self): - zones = cs.availability_zones.list(detailed=True) - cs.assert_called('GET', '/os-availability-zone/detail') + zones = self.cs.availability_zones.list(detailed=True) + self.cs.assert_called('GET', '/os-availability-zone/detail') for zone in zones: self.assertTrue(isinstance(zone, - availability_zones.AvailabilityZone)) + self.availability_zone_type)) self.assertEqual(3, len(zones)) @@ -75,9 +85,9 @@ def test_detail_availability_zone(self): six.u('enabled XXX 2012-12-26 14:45:24')] l8 = [six.u('zone-2'), six.u('not available')] - z0 = shell._treeizeAvailabilityZone(zones[0]) - z1 = shell._treeizeAvailabilityZone(zones[1]) - z2 = shell._treeizeAvailabilityZone(zones[2]) + z0 = self.shell._treeizeAvailabilityZone(zones[0]) + z1 = self.shell._treeizeAvailabilityZone(zones[1]) + z2 = self.shell._treeizeAvailabilityZone(zones[2]) self.assertEqual((len(z0), len(z1), len(z2)), (3, 5, 1)) diff --git a/novaclient/tests/v3/fakes.py b/novaclient/tests/v3/fakes.py index c2fd0c8cf..e1f5dbb2c 100644 --- a/novaclient/tests/v3/fakes.py +++ b/novaclient/tests/v3/fakes.py @@ -14,6 +14,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from datetime import datetime + from novaclient.openstack.common import strutils from novaclient.tests import fakes from novaclient.tests.v1_1 import fakes as fakes_v1_1 @@ -228,3 +230,44 @@ def get_servers_1234_os_server_password(self, **kw): def delete_servers_1234_os_server_password(self, **kw): return (202, {}, None) + + # + # Availability Zones + # + def get_os_availability_zone(self, **kw): + return (200, {}, {"availability_zone_info": [ + {"zone_name": "zone-1", + "zone_state": {"available": True}, + "hosts": None}, + {"zone_name": "zone-2", + "zone_state": {"available": False}, + "hosts": None}]}) + + def get_os_availability_zone_detail(self, **kw): + return (200, {}, {"availability_zone_info": [ + {"zone_name": "zone-1", + "zone_state": {"available": True}, + "hosts": { + "fake_host-1": { + "nova-compute": {"active": True, + "available": True, + "updated_at": + datetime(2012, 12, 26, 14, 45, 25, 0)}}}}, + {"zone_name": "internal", + "zone_state": {"available": True}, + "hosts": { + "fake_host-1": { + "nova-sched": { + "active": True, + "available": True, + "updated_at": + datetime(2012, 12, 26, 14, 45, 25, 0)}}, + "fake_host-2": { + "nova-network": { + "active": True, + "available": False, + "updated_at": + datetime(2012, 12, 26, 14, 45, 24, 0)}}}}, + {"zone_name": "zone-2", + "zone_state": {"available": False}, + "hosts": None}]}) diff --git a/novaclient/tests/v3/test_availability_zone.py b/novaclient/tests/v3/test_availability_zone.py new file mode 100644 index 000000000..004ba95fa --- /dev/null +++ b/novaclient/tests/v3/test_availability_zone.py @@ -0,0 +1,38 @@ +# Copyright 2011 OpenStack Foundation +# Copyright 2013 IBM Corp. +# All Rights Reserved. +# +# 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 novaclient.tests.v1_1 import test_availability_zone +from novaclient.tests.v3 import fakes +from novaclient.v3 import availability_zones + + +class AvailabilityZoneTest(test_availability_zone.AvailabilityZoneTest): + from novaclient.v3 import shell # noqa + + def setUp(self): + super(AvailabilityZoneTest, self).setUp() + self.cs = self._get_fake_client() + self.availability_zone_type = self._get_availability_zone_type() + + def _assertZone(self, zone, name, status): + self.assertEqual(zone.zone_name, name) + self.assertEqual(zone.zone_state, status) + + def _get_fake_client(self): + return fakes.FakeClient() + + def _get_availability_zone_type(self): + return availability_zones.AvailabilityZone diff --git a/novaclient/v1_1/availability_zones.py b/novaclient/v1_1/availability_zones.py index 41362aa25..bf5903785 100644 --- a/novaclient/v1_1/availability_zones.py +++ b/novaclient/v1_1/availability_zones.py @@ -36,6 +36,7 @@ class AvailabilityZoneManager(base.ManagerWithFind): Manage :class:`AvailabilityZone` resources. """ resource_class = AvailabilityZone + return_parameter_name = "availabilityZoneInfo" def list(self, detailed=True): """ @@ -45,6 +46,7 @@ def list(self, detailed=True): """ if detailed is True: return self._list("/os-availability-zone/detail", - "availabilityZoneInfo") + self.return_parameter_name) else: - return self._list("/os-availability-zone", "availabilityZoneInfo") + return self._list("/os-availability-zone", + self.return_parameter_name) diff --git a/novaclient/v3/availability_zones.py b/novaclient/v3/availability_zones.py new file mode 100644 index 000000000..bd2a9d239 --- /dev/null +++ b/novaclient/v3/availability_zones.py @@ -0,0 +1,33 @@ +# Copyright 2011 OpenStack Foundation +# Copyright 2013 IBM Corp. +# All Rights Reserved. +# +# 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. + +""" +Availability Zone interface. +""" + +from novaclient.v1_1 import availability_zones + + +class AvailabilityZone(availability_zones.AvailabilityZone): + pass + + +class AvailabilityZoneManager(availability_zones.AvailabilityZoneManager): + """ + Manage :class:`AvailabilityZone` resources. + """ + resource_class = AvailabilityZone + return_parameter_name = 'availability_zone_info' diff --git a/novaclient/v3/client.py b/novaclient/v3/client.py index 4cbf8d3e7..8bfb2bb09 100644 --- a/novaclient/v3/client.py +++ b/novaclient/v3/client.py @@ -16,6 +16,7 @@ from novaclient import client from novaclient.v3 import agents +from novaclient.v3 import availability_zones from novaclient.v3 import flavor_access from novaclient.v3 import flavors from novaclient.v3 import hosts @@ -56,6 +57,8 @@ def __init__(self, username, password, project_id, auth_url=None, self.os_cache = os_cache or not no_cache #TODO(bnemec): Add back in v3 extensions self.agents = agents.AgentsManager(self) + self.availability_zones = \ + availability_zones.AvailabilityZoneManager(self) self.hosts = hosts.HostManager(self) self.flavors = flavors.FlavorManager(self) self.flavor_access = flavor_access.FlavorAccessManager(self) diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 52aa45936..8638224dc 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -32,8 +32,8 @@ from novaclient.openstack.common import timeutils from novaclient.openstack.common import uuidutils from novaclient import utils -from novaclient.v1_1 import availability_zones from novaclient.v1_1 import quotas +from novaclient.v3 import availability_zones from novaclient.v3 import servers @@ -1433,7 +1433,7 @@ def _translate_volume_snapshot_keys(collection): def _translate_availability_zone_keys(collection): _translate_keys(collection, - [('zoneName', 'name'), ('zoneState', 'status')]) + [('zone_name', 'name'), ('zone_state', 'status')]) @utils.arg('--all-tenants', @@ -3161,40 +3161,40 @@ def _treeizeAvailabilityZone(zone): result = [] # Zone tree view item - az.zoneName = zone.zoneName - az.zoneState = ('available' - if zone.zoneState['available'] else 'not available') - az._info['zoneName'] = az.zoneName - az._info['zoneState'] = az.zoneState + az.zone_name = zone.zone_name + az.zone_state = ('available' + if zone.zone_state['available'] else 'not available') + az._info['zone_name'] = az.zone_name + az._info['zone_state'] = az.zone_state result.append(az) if zone.hosts is not None: - for (host, services) in zone.hosts.items(): + zone_hosts = sorted(zone.hosts.items(), key=lambda x: x[0]) + for (host, services) in zone_hosts: # Host tree view item az = AvailabilityZone(zone.manager, copy.deepcopy(zone._info), zone._loaded) - az.zoneName = '|- %s' % host - az.zoneState = '' - az._info['zoneName'] = az.zoneName - az._info['zoneState'] = az.zoneState + az.zone_name = '|- %s' % host + az.zone_state = '' + az._info['zone_name'] = az.zone_name + az._info['zone_state'] = az.zone_state result.append(az) for (svc, state) in services.items(): # Service tree view item az = AvailabilityZone(zone.manager, copy.deepcopy(zone._info), zone._loaded) - az.zoneName = '| |- %s' % svc - az.zoneState = '%s %s %s' % ( + az.zone_name = '| |- %s' % svc + az.zone_state = '%s %s %s' % ( 'enabled' if state['active'] else 'disabled', ':-)' if state['available'] else 'XXX', state['updated_at']) - az._info['zoneName'] = az.zoneName - az._info['zoneState'] = az.zoneState + az._info['zone_name'] = az.zone_name + az._info['zone_state'] = az.zone_state result.append(az) return result -@utils.service_type('compute') def do_availability_zone_list(cs, _args): """List all the availability zones.""" try: From cfd38a7ef67c1495128a495a6cd832ca63e65a24 Mon Sep 17 00:00:00 2001 From: Chris Yeoh Date: Wed, 11 Dec 2013 17:49:25 +1030 Subject: [PATCH 0353/1705] Adds first part of quotas support for Nova V3 API Adds support and tests for the os-quotas extension for the Nova V3 API. Note that compared to the V2 version this removes the ability to set quotas which are not relevant to the V3 API (eg injected file quotas are not relevant because the os-personalities extension has been removed) Partially implements blueprint v3-api Change-Id: Ifa1c77428424bedf7fb88ef6d7b3843376799d24 --- novaclient/tests/v1_1/test_quotas.py | 44 +++++++++++++----------- novaclient/tests/v3/fakes.py | 21 ++++++++++++ novaclient/tests/v3/test_quotas.py | 33 ++++++++++++++++++ novaclient/v1_1/quotas.py | 31 ++++------------- novaclient/v3/client.py | 2 ++ novaclient/v3/quotas.py | 27 +++++++++++++++ novaclient/v3/shell.py | 51 ++-------------------------- 7 files changed, 116 insertions(+), 93 deletions(-) create mode 100644 novaclient/tests/v3/test_quotas.py create mode 100644 novaclient/v3/quotas.py diff --git a/novaclient/tests/v1_1/test_quotas.py b/novaclient/tests/v1_1/test_quotas.py index b545e6b18..1c679389e 100644 --- a/novaclient/tests/v1_1/test_quotas.py +++ b/novaclient/tests/v1_1/test_quotas.py @@ -16,54 +16,58 @@ from novaclient.tests import utils from novaclient.tests.v1_1 import fakes -cs = fakes.FakeClient() - class QuotaSetsTest(utils.TestCase): + def setUp(self): + super(QuotaSetsTest, self).setUp() + self.cs = self._get_fake_client() + + def _get_fake_client(self): + return fakes.FakeClient() def test_tenant_quotas_get(self): tenant_id = 'test' - cs.quotas.get(tenant_id) - cs.assert_called('GET', '/os-quota-sets/%s' % tenant_id) + self.cs.quotas.get(tenant_id) + self.cs.assert_called('GET', '/os-quota-sets/%s' % tenant_id) def test_user_quotas_get(self): tenant_id = 'test' user_id = 'fake_user' - cs.quotas.get(tenant_id, user_id=user_id) + self.cs.quotas.get(tenant_id, user_id=user_id) url = '/os-quota-sets/%s?user_id=%s' % (tenant_id, user_id) - cs.assert_called('GET', url) + self.cs.assert_called('GET', url) def test_tenant_quotas_defaults(self): tenant_id = '97f4c221bff44578b0300df4ef119353' - cs.quotas.defaults(tenant_id) - cs.assert_called('GET', '/os-quota-sets/%s/defaults' % tenant_id) + self.cs.quotas.defaults(tenant_id) + self.cs.assert_called('GET', '/os-quota-sets/%s/defaults' % tenant_id) def test_update_quota(self): - q = cs.quotas.get('97f4c221bff44578b0300df4ef119353') + q = self.cs.quotas.get('97f4c221bff44578b0300df4ef119353') q.update(volumes=2) - cs.assert_called('PUT', + self.cs.assert_called('PUT', '/os-quota-sets/97f4c221bff44578b0300df4ef119353') def test_update_user_quota(self): tenant_id = '97f4c221bff44578b0300df4ef119353' user_id = 'fake_user' - q = cs.quotas.get(tenant_id) + q = self.cs.quotas.get(tenant_id) q.update(volumes=2, user_id=user_id) url = '/os-quota-sets/%s?user_id=%s' % (tenant_id, user_id) - cs.assert_called('PUT', url) + self.cs.assert_called('PUT', url) def test_force_update_quota(self): - q = cs.quotas.get('97f4c221bff44578b0300df4ef119353') + q = self.cs.quotas.get('97f4c221bff44578b0300df4ef119353') q.update(cores=2, force=True) - cs.assert_called( + self.cs.assert_called( 'PUT', '/os-quota-sets/97f4c221bff44578b0300df4ef119353', {'quota_set': {'force': True, 'cores': 2, 'tenant_id': '97f4c221bff44578b0300df4ef119353'}}) def test_refresh_quota(self): - q = cs.quotas.get('test') - q2 = cs.quotas.get('test') + q = self.cs.quotas.get('test') + q2 = self.cs.quotas.get('test') self.assertEqual(q.volumes, q2.volumes) q2.volumes = 0 self.assertNotEqual(q.volumes, q2.volumes) @@ -72,12 +76,12 @@ def test_refresh_quota(self): def test_quotas_delete(self): tenant_id = 'test' - cs.quotas.delete(tenant_id) - cs.assert_called('DELETE', '/os-quota-sets/%s' % tenant_id) + self.cs.quotas.delete(tenant_id) + self.cs.assert_called('DELETE', '/os-quota-sets/%s' % tenant_id) def test_user_quotas_delete(self): tenant_id = 'test' user_id = 'fake_user' - cs.quotas.delete(tenant_id, user_id=user_id) + self.cs.quotas.delete(tenant_id, user_id=user_id) url = '/os-quota-sets/%s?user_id=%s' % (tenant_id, user_id) - cs.assert_called('DELETE', url) + self.cs.assert_called('DELETE', url) diff --git a/novaclient/tests/v3/fakes.py b/novaclient/tests/v3/fakes.py index e1f5dbb2c..f856ce406 100644 --- a/novaclient/tests/v3/fakes.py +++ b/novaclient/tests/v3/fakes.py @@ -271,3 +271,24 @@ def get_os_availability_zone_detail(self, **kw): {"zone_name": "zone-2", "zone_state": {"available": False}, "hosts": None}]}) + + # + # Quotas + # + def put_os_quota_sets_97f4c221bff44578b0300df4ef119353(self, body, **kw): + assert list(body) == ['quota_set'] + return (200, {}, {'quota_set': { + 'tenant_id': '97f4c221bff44578b0300df4ef119353', + 'metadata_items': [], + 'injected_file_content_bytes': 1, + 'injected_file_path_bytes': 1, + 'volumes': 2, + 'gigabytes': 1, + 'ram': 1, + 'floating_ips': 1, + 'instances': 1, + 'injected_files': 1, + 'cores': 1, + 'keypairs': 1, + 'security_groups': 1, + 'security_group_rules': 1}}) diff --git a/novaclient/tests/v3/test_quotas.py b/novaclient/tests/v3/test_quotas.py new file mode 100644 index 000000000..0f361a49f --- /dev/null +++ b/novaclient/tests/v3/test_quotas.py @@ -0,0 +1,33 @@ +# Copyright IBM Corp. 2013 +# +# 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 novaclient.tests.v1_1 import test_quotas +from novaclient.tests.v3 import fakes + + +class QuotaSetsTest(test_quotas.QuotaSetsTest): + def setUp(self): + super(QuotaSetsTest, self).setUp() + self.cs = self._get_fake_client() + + def _get_fake_client(self): + return fakes.FakeClient() + + def test_force_update_quota(self): + q = self.cs.quotas.get('97f4c221bff44578b0300df4ef119353') + q.update(cores=2, force=True) + self.cs.assert_called( + 'PUT', '/os-quota-sets/97f4c221bff44578b0300df4ef119353', + {'quota_set': {'force': True, + 'cores': 2}}) diff --git a/novaclient/v1_1/quotas.py b/novaclient/v1_1/quotas.py index 601bcebe9..cd16042b4 100644 --- a/novaclient/v1_1/quotas.py +++ b/novaclient/v1_1/quotas.py @@ -41,31 +41,14 @@ def get(self, tenant_id, user_id=None): url = '/os-quota-sets/%s' % tenant_id return self._get(url, "quota_set") - def update(self, tenant_id, metadata_items=None, - injected_file_content_bytes=None, injected_file_path_bytes=None, - volumes=None, gigabytes=None, - ram=None, floating_ips=None, fixed_ips=None, instances=None, - injected_files=None, cores=None, key_pairs=None, - security_groups=None, security_group_rules=None, force=None, - user_id=None): + def _update_body(self, tenant_id, **kwargs): + kwargs['tenant_id'] = tenant_id + return {'quota_set': kwargs} - body = {'quota_set': { - 'tenant_id': tenant_id, - 'metadata_items': metadata_items, - 'key_pairs': key_pairs, - 'injected_file_content_bytes': injected_file_content_bytes, - 'injected_file_path_bytes': injected_file_path_bytes, - 'volumes': volumes, - 'gigabytes': gigabytes, - 'ram': ram, - 'floating_ips': floating_ips, - 'fixed_ips': fixed_ips, - 'instances': instances, - 'injected_files': injected_files, - 'cores': cores, - 'security_groups': security_groups, - 'security_group_rules': security_group_rules, - 'force': force}} + def update(self, tenant_id, **kwargs): + + user_id = kwargs.pop('user_id', None) + body = self._update_body(tenant_id, **kwargs) for key in list(body['quota_set']): if body['quota_set'][key] is None: diff --git a/novaclient/v3/client.py b/novaclient/v3/client.py index 8bfb2bb09..f20a8a18e 100644 --- a/novaclient/v3/client.py +++ b/novaclient/v3/client.py @@ -21,6 +21,7 @@ from novaclient.v3 import flavors from novaclient.v3 import hosts from novaclient.v3 import images +from novaclient.v3 import quotas from novaclient.v3 import servers @@ -63,6 +64,7 @@ def __init__(self, username, password, project_id, auth_url=None, self.flavors = flavors.FlavorManager(self) self.flavor_access = flavor_access.FlavorAccessManager(self) self.images = images.ImageManager(self) + self.quotas = quotas.QuotaSetManager(self) self.servers = servers.ServerManager(self) # Add in any extensions... diff --git a/novaclient/v3/quotas.py b/novaclient/v3/quotas.py new file mode 100644 index 000000000..19723fafa --- /dev/null +++ b/novaclient/v3/quotas.py @@ -0,0 +1,27 @@ +# Copyright 2011 OpenStack Foundation +# All Rights Reserved. +# +# 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 novaclient.v1_1 import quotas + + +class QuotaSet(quotas.QuotaSet): + pass + + +class QuotaSetManager(quotas.QuotaSetManager): + resource_class = QuotaSet + + def _update_body(self, tenant_id, **kwargs): + return {'quota_set': kwargs} diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 8638224dc..bca8110a0 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -32,7 +32,6 @@ from novaclient.openstack.common import timeutils from novaclient.openstack.common import uuidutils from novaclient import utils -from novaclient.v1_1 import quotas from novaclient.v3 import availability_zones from novaclient.v3 import servers @@ -2829,10 +2828,7 @@ def do_ssh(cs, args): _quota_resources = ['instances', 'cores', 'ram', 'volumes', 'gigabytes', - 'floating_ips', 'fixed_ips', 'metadata_items', - 'injected_files', 'key_pairs', - 'injected_file_content_bytes', 'injected_file_path_bytes', - 'security_groups', 'security_group_rules'] + 'fixed_ips', 'metadata_items', 'key_pairs'] def _quota_show(quotas): @@ -2855,11 +2851,7 @@ def _quota_update(manager, identifier, args): if updates: # default value of force is None to make sure this client # will be compatibile with old nova server - force_update = getattr(args, 'force', None) - if isinstance(manager, quotas.QuotaSetManager): - manager.update(identifier, force=force_update, **updates) - else: - manager.update(identifier, **updates) + manager.update(identifier, **updates) @utils.arg('--tenant', @@ -2911,14 +2903,6 @@ def do_quota_defaults(cs, args): metavar='', type=int, default=None, help='New value for the "gigabytes" quota.') -@utils.arg('--floating-ips', - metavar='', - type=int, - default=None, - help='New value for the "floating-ips" quota.') -@utils.arg('--floating_ips', - type=int, - help=argparse.SUPPRESS) @utils.arg('--fixed-ips', metavar='', type=int, @@ -2932,42 +2916,11 @@ def do_quota_defaults(cs, args): @utils.arg('--metadata_items', type=int, help=argparse.SUPPRESS) -@utils.arg('--injected-files', - metavar='', - type=int, - default=None, - help='New value for the "injected-files" quota.') -@utils.arg('--injected_files', - type=int, - help=argparse.SUPPRESS) -@utils.arg('--injected-file-content-bytes', - metavar='', - type=int, - default=None, - help='New value for the "injected-file-content-bytes" quota.') -@utils.arg('--injected_file_content_bytes', - type=int, - help=argparse.SUPPRESS) -@utils.arg('--injected-file-path-bytes', - metavar='', - type=int, - default=None, - help='New value for the "injected-file-path-bytes" quota.') @utils.arg('--key-pairs', metavar='', type=int, default=None, help='New value for the "key-pairs" quota.') -@utils.arg('--security-groups', - metavar='', - type=int, - default=None, - help='New value for the "security-groups" quota.') -@utils.arg('--security-group-rules', - metavar='', - type=int, - default=None, - help='New value for the "security-group-rules" quota.') @utils.arg('--force', dest='force', action="store_true", From 6a9c81843f05074d2c153fb957aaa74b975cc543 Mon Sep 17 00:00:00 2001 From: Chris Yeoh Date: Wed, 11 Dec 2013 20:51:01 +1030 Subject: [PATCH 0354/1705] Adds second part of quotas support for Nova V3 API Adds support and tests for the os-quota-class-sets extension for the Nova V3 API. Note that compared to the V2 version this removes the ability to set quotas which are not relevant to the V3 API (eg injected file quotas are not relevant because the os-personalities extension has been removed). Partially implements blueprint v3-api Change-Id: I3e7f36407f2f59737ecbce2c8ce014cef623ecdc --- novaclient/tests/v3/test_quota_classes.py | 25 +++++++++++++++ novaclient/v1_1/quota_classes.py | 26 +++------------ novaclient/v3/client.py | 2 ++ novaclient/v3/quota_classes.py | 23 +++++++++++++ novaclient/v3/shell.py | 39 ----------------------- 5 files changed, 55 insertions(+), 60 deletions(-) create mode 100644 novaclient/tests/v3/test_quota_classes.py create mode 100644 novaclient/v3/quota_classes.py diff --git a/novaclient/tests/v3/test_quota_classes.py b/novaclient/tests/v3/test_quota_classes.py new file mode 100644 index 000000000..2e0ceb9f0 --- /dev/null +++ b/novaclient/tests/v3/test_quota_classes.py @@ -0,0 +1,25 @@ +# Copyright IBM Corp. 2013 +# +# 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 novaclient.tests.v1_1 import test_quota_classes +from novaclient.tests.v3 import fakes + + +class QuotaClassSetsTest(test_quota_classes.QuotaClassSetsTest): + def setUp(self): + super(QuotaClassSetsTest, self).setUp() + self.cs = self._get_fake_client() + + def _get_fake_client(self): + return fakes.FakeClient() diff --git a/novaclient/v1_1/quota_classes.py b/novaclient/v1_1/quota_classes.py index b57d7071b..50314efd9 100644 --- a/novaclient/v1_1/quota_classes.py +++ b/novaclient/v1_1/quota_classes.py @@ -36,28 +36,12 @@ def get(self, class_name): return self._get("/os-quota-class-sets/%s" % (class_name), "quota_class_set") - def update(self, class_name, metadata_items=None, - injected_file_content_bytes=None, injected_file_path_bytes=None, - volumes=None, gigabytes=None, - ram=None, floating_ips=None, instances=None, - injected_files=None, cores=None, key_pairs=None, - security_groups=None, security_group_rules=None): + def _update_body(self, **kwargs): + return {'quota_class_set': kwargs} - body = {'quota_class_set': { - 'class_name': class_name, - 'metadata_items': metadata_items, - 'key_pairs': key_pairs, - 'injected_file_content_bytes': injected_file_content_bytes, - 'injected_file_path_bytes': injected_file_path_bytes, - 'volumes': volumes, - 'gigabytes': gigabytes, - 'ram': ram, - 'floating_ips': floating_ips, - 'instances': instances, - 'injected_files': injected_files, - 'cores': cores, - 'security_groups': security_groups, - 'security_group_rules': security_group_rules}} + def update(self, class_name, **kwargs): + kwargs['class_name'] = class_name + body = self._update_body(**kwargs) for key in list(body['quota_class_set']): if body['quota_class_set'][key] is None: diff --git a/novaclient/v3/client.py b/novaclient/v3/client.py index f20a8a18e..20d67bd96 100644 --- a/novaclient/v3/client.py +++ b/novaclient/v3/client.py @@ -21,6 +21,7 @@ from novaclient.v3 import flavors from novaclient.v3 import hosts from novaclient.v3 import images +from novaclient.v3 import quota_classes from novaclient.v3 import quotas from novaclient.v3 import servers @@ -65,6 +66,7 @@ def __init__(self, username, password, project_id, auth_url=None, self.flavor_access = flavor_access.FlavorAccessManager(self) self.images = images.ImageManager(self) self.quotas = quotas.QuotaSetManager(self) + self.quota_classes = quota_classes.QuotaClassSetManager(self) self.servers = servers.ServerManager(self) # Add in any extensions... diff --git a/novaclient/v3/quota_classes.py b/novaclient/v3/quota_classes.py new file mode 100644 index 000000000..e12209eb1 --- /dev/null +++ b/novaclient/v3/quota_classes.py @@ -0,0 +1,23 @@ +# Copyright IBM Corp. 2013 +# +# 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 novaclient.v1_1 import quota_classes + + +class QuotaClassSet(quota_classes.QuotaClassSet): + pass + + +class QuotaClassSetManager(quota_classes.QuotaClassSetManager): + resource_class = QuotaClassSet diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index bca8110a0..46da5b4c4 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -2974,14 +2974,6 @@ def do_quota_class_show(cs, args): metavar='', type=int, default=None, help='New value for the "gigabytes" quota.') -@utils.arg('--floating-ips', - metavar='', - type=int, - default=None, - help='New value for the "floating-ips" quota.') -@utils.arg('--floating_ips', - type=int, - help=argparse.SUPPRESS) @utils.arg('--metadata-items', metavar='', type=int, @@ -2990,42 +2982,11 @@ def do_quota_class_show(cs, args): @utils.arg('--metadata_items', type=int, help=argparse.SUPPRESS) -@utils.arg('--injected-files', - metavar='', - type=int, - default=None, - help='New value for the "injected-files" quota.') -@utils.arg('--injected_files', - type=int, - help=argparse.SUPPRESS) -@utils.arg('--injected-file-content-bytes', - metavar='', - type=int, - default=None, - help='New value for the "injected-file-content-bytes" quota.') -@utils.arg('--injected_file_content_bytes', - type=int, - help=argparse.SUPPRESS) -@utils.arg('--injected-file-path-bytes', - metavar='', - type=int, - default=None, - help='New value for the "injected-file-path-bytes" quota.') @utils.arg('--key-pairs', metavar='', type=int, default=None, help='New value for the "key-pairs" quota.') -@utils.arg('--security-groups', - metavar='', - type=int, - default=None, - help='New value for the "security-groups" quota.') -@utils.arg('--security-group-rules', - metavar='', - type=int, - default=None, - help='New value for the "security-group-rules" quota.') def do_quota_class_update(cs, args): """Update the quotas for a quota class.""" From 94b40dd2f87d4d335a61844c0def5a92525a8dec Mon Sep 17 00:00:00 2001 From: Chris Yeoh Date: Thu, 12 Dec 2013 16:18:51 +1030 Subject: [PATCH 0355/1705] Adds services support for Nova V3 API Adds support and tests for the os-services extension for the Nova V3 API Partially implements blueprint v3-api Change-Id: Ic64e465e24a6e840c567677d4f2ea9d63b742ccd --- novaclient/tests/v1_1/test_services.py | 91 ++++++++++++++++---------- novaclient/tests/v3/test_services.py | 40 +++++++++++ novaclient/v1_1/services.py | 13 +++- novaclient/v3/client.py | 2 + novaclient/v3/services.py | 36 ++++++++++ 5 files changed, 143 insertions(+), 39 deletions(-) create mode 100644 novaclient/tests/v3/test_services.py create mode 100644 novaclient/v3/services.py diff --git a/novaclient/tests/v1_1/test_services.py b/novaclient/tests/v1_1/test_services.py index 85e4f345c..8f9b7eb33 100644 --- a/novaclient/tests/v1_1/test_services.py +++ b/novaclient/tests/v1_1/test_services.py @@ -20,58 +20,77 @@ from novaclient.v1_1 import services -cs = fakes.FakeClient() +class ServicesTest(utils.TestCase): + def setUp(self): + super(ServicesTest, self).setUp() + self.cs = self._get_fake_client() + self.service_type = self._get_service_type() + def _get_fake_client(self): + return fakes.FakeClient() -class ServicesTest(utils.TestCase): + def _get_service_type(self): + return services.Service def test_list_services(self): - svs = cs.services.list() - cs.assert_called('GET', '/os-services') - [self.assertTrue(isinstance(s, services.Service)) for s in svs] - [self.assertEqual(s.binary, 'nova-compute') for s in svs] - [self.assertEqual(s.host, 'host1') for s in svs] + svs = self.cs.services.list() + self.cs.assert_called('GET', '/os-services') + for s in svs: + self.assertTrue(isinstance(s, self._get_service_type())) + self.assertEqual(s.binary, 'nova-compute') + self.assertEqual(s.host, 'host1') def test_list_services_with_hostname(self): - svs = cs.services.list(host='host2') - cs.assert_called('GET', '/os-services?host=host2') - [self.assertTrue(isinstance(s, services.Service)) for s in svs] - [self.assertEqual(s.binary, 'nova-compute') for s in svs] - [self.assertEqual(s.host, 'host2') for s in svs] + svs = self.cs.services.list(host='host2') + self.cs.assert_called('GET', '/os-services?host=host2') + for s in svs: + self.assertTrue(isinstance(s, self._get_service_type())) + self.assertEqual(s.binary, 'nova-compute') + self.assertEqual(s.host, 'host2') def test_list_services_with_binary(self): - svs = cs.services.list(binary='nova-cert') - cs.assert_called('GET', '/os-services?binary=nova-cert') - [self.assertTrue(isinstance(s, services.Service)) for s in svs] - [self.assertEqual(s.binary, 'nova-cert') for s in svs] - [self.assertEqual(s.host, 'host1') for s in svs] + svs = self.cs.services.list(binary='nova-cert') + self.cs.assert_called('GET', '/os-services?binary=nova-cert') + for s in svs: + self.assertTrue(isinstance(s, self._get_service_type())) + self.assertEqual(s.binary, 'nova-cert') + self.assertEqual(s.host, 'host1') def test_list_services_with_host_binary(self): - svs = cs.services.list(host='host2', binary='nova-cert') - cs.assert_called('GET', '/os-services?host=host2&binary=nova-cert') - [self.assertTrue(isinstance(s, services.Service)) for s in svs] - [self.assertEqual(s.binary, 'nova-cert') for s in svs] - [self.assertEqual(s.host, 'host2') for s in svs] + svs = self.cs.services.list(host='host2', binary='nova-cert') + self.cs.assert_called('GET', + '/os-services?host=host2&binary=nova-cert') + for s in svs: + self.assertTrue(isinstance(s, self._get_service_type())) + self.assertEqual(s.binary, 'nova-cert') + self.assertEqual(s.host, 'host2') + + def _update_body(self, host, binary, disabled_reason=None): + body = {"host": host, + "binary": binary} + if disabled_reason is not None: + body["disabled_reason"] = disabled_reason + return body def test_services_enable(self): - service = cs.services.enable('host1', 'nova-cert') - values = {"host": "host1", 'binary': 'nova-cert'} - cs.assert_called('PUT', '/os-services/enable', values) - self.assertTrue(isinstance(service, services.Service)) + service = self.cs.services.enable('host1', 'nova-cert') + values = self._update_body("host1", "nova-cert") + self.cs.assert_called('PUT', '/os-services/enable', values) + self.assertTrue(isinstance(service, self._get_service_type())) self.assertEqual(service.status, 'enabled') def test_services_disable(self): - service = cs.services.disable('host1', 'nova-cert') - values = {"host": "host1", 'binary': 'nova-cert'} - cs.assert_called('PUT', '/os-services/disable', values) - self.assertTrue(isinstance(service, services.Service)) + service = self.cs.services.disable('host1', 'nova-cert') + values = self._update_body("host1", "nova-cert") + self.cs.assert_called('PUT', '/os-services/disable', values) + self.assertTrue(isinstance(service, self._get_service_type())) self.assertEqual(service.status, 'disabled') def test_services_disable_log_reason(self): - service = cs.services.disable_log_reason('compute1', 'nova-compute', - 'disable bad host') - values = {'host': 'compute1', 'binary': 'nova-compute', - 'disabled_reason': 'disable bad host'} - cs.assert_called('PUT', '/os-services/disable-log-reason', values) - self.assertTrue(isinstance(service, services.Service)) + service = self.cs.services.disable_log_reason( + 'compute1', 'nova-compute', 'disable bad host') + values = self._update_body("compute1", "nova-compute", + "disable bad host") + self.cs.assert_called('PUT', '/os-services/disable-log-reason', values) + self.assertTrue(isinstance(service, self._get_service_type())) self.assertEqual(service.status, 'disabled') diff --git a/novaclient/tests/v3/test_services.py b/novaclient/tests/v3/test_services.py new file mode 100644 index 000000000..2ad385489 --- /dev/null +++ b/novaclient/tests/v3/test_services.py @@ -0,0 +1,40 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 IBM Corp. +# All Rights Reserved. +# +# 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 novaclient.tests import utils +from novaclient.tests.v3 import fakes +from novaclient.v3 import services + + +class ServicesTest(utils.TestCase): + def setUp(self): + super(ServicesTest, self).setUp() + self.cs = self._get_fake_client() + self.service_type = self._get_service_type() + + def _get_fake_client(self): + return fakes.FakeClient() + + def _get_service_type(self): + return services.Service + + def _update_body(self, host, binary, disabled_reason=None): + body = {"host": host, + "binary": binary} + if disabled_reason is not None: + body["disabled_reason"] = disabled_reason + return body diff --git a/novaclient/v1_1/services.py b/novaclient/v1_1/services.py index 03424026e..e37123601 100644 --- a/novaclient/v1_1/services.py +++ b/novaclient/v1_1/services.py @@ -50,17 +50,24 @@ def list(self, host=None, binary=None): url = "%s?%s" % (url, "&".join(filters)) return self._list(url, "services") + def _update_body(self, host, binary, disabled_reason=None): + body = {"host": host, + "binary": binary} + if disabled_reason is not None: + body["disabled_reason"] = disabled_reason + return body + def enable(self, host, binary): """Enable the service specified by hostname and binary.""" - body = {"host": host, "binary": binary} + body = self._update_body(host, binary) return self._update("/os-services/enable", body, "service") def disable(self, host, binary): """Disable the service specified by hostname and binary.""" - body = {"host": host, "binary": binary} + body = self._update_body(host, binary) return self._update("/os-services/disable", body, "service") def disable_log_reason(self, host, binary, reason): """Disable the service with reason.""" - body = {"host": host, "binary": binary, "disabled_reason": reason} + body = self._update_body(host, binary, reason) return self._update("/os-services/disable-log-reason", body, "service") diff --git a/novaclient/v3/client.py b/novaclient/v3/client.py index 20d67bd96..e4b51866b 100644 --- a/novaclient/v3/client.py +++ b/novaclient/v3/client.py @@ -24,6 +24,7 @@ from novaclient.v3 import quota_classes from novaclient.v3 import quotas from novaclient.v3 import servers +from novaclient.v3 import services class Client(object): @@ -68,6 +69,7 @@ def __init__(self, username, password, project_id, auth_url=None, self.quotas = quotas.QuotaSetManager(self) self.quota_classes = quota_classes.QuotaClassSetManager(self) self.servers = servers.ServerManager(self) + self.services = services.ServiceManager(self) # Add in any extensions... if extensions: diff --git a/novaclient/v3/services.py b/novaclient/v3/services.py new file mode 100644 index 000000000..9415755b2 --- /dev/null +++ b/novaclient/v3/services.py @@ -0,0 +1,36 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 IBM Corp. +# +# 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. + +""" +service interface +""" +from novaclient.v1_1 import services + + +class Service(services.Service): + pass + + +class ServiceManager(services.ServiceManager): + resource_class = Service + + def _update_body(self, host, binary, disabled_reason=None): + body = {"service": + {"host": host, + "binary": binary}} + if disabled_reason is not None: + body["service"]["disabled_reason"] = disabled_reason + return body From 3c46b26666f34ad46ad9a2cba6153380d1f072c1 Mon Sep 17 00:00:00 2001 From: Chris Yeoh Date: Fri, 13 Dec 2013 21:23:27 +1030 Subject: [PATCH 0356/1705] Adds hypervisor support for Nova V3 API Adds support and tests for the os-hypervisors extension for the Nova V3 API. Note that compared to the V2 API it separates the search function into search by hypervisor name and a new list servers by hypervisor as this better first the changes in the V3 API. The listing of servers by hypervisor command is preserved, though reworked to use the new interfaces. Partially implements blueprint v3-api Change-Id: I472f9acad895dcf842e93d2de27530652f73867e --- novaclient/tests/v1_1/test_hypervisors.py | 36 ++++++++-------- novaclient/tests/v3/fakes.py | 19 +++++++++ novaclient/tests/v3/test_hypervisors.py | 50 +++++++++++++++++++++++ novaclient/v3/client.py | 2 + novaclient/v3/hypervisors.py | 48 ++++++++++++++++++++++ novaclient/v3/shell.py | 19 +++++---- 6 files changed, 151 insertions(+), 23 deletions(-) create mode 100644 novaclient/tests/v3/test_hypervisors.py create mode 100644 novaclient/v3/hypervisors.py diff --git a/novaclient/tests/v1_1/test_hypervisors.py b/novaclient/tests/v1_1/test_hypervisors.py index 8d59d5984..3cf66c783 100644 --- a/novaclient/tests/v1_1/test_hypervisors.py +++ b/novaclient/tests/v1_1/test_hypervisors.py @@ -17,10 +17,14 @@ from novaclient.tests.v1_1 import fakes -cs = fakes.FakeClient() +class HypervisorsTest(utils.TestCase): + def setUp(self): + super(HypervisorsTest, self).setUp() + self.cs = self._get_fake_client() + def _get_fake_client(self): + return fakes.FakeClient() -class HypervisorsTest(utils.TestCase): def compare_to_expected(self, expected, hyper): for key, value in expected.items(): self.assertEqual(getattr(hyper, key), value) @@ -31,8 +35,8 @@ def test_hypervisor_index(self): dict(id=5678, hypervisor_hostname='hyper2'), ] - result = cs.hypervisors.list(False) - cs.assert_called('GET', '/os-hypervisors') + result = self.cs.hypervisors.list(False) + self.cs.assert_called('GET', '/os-hypervisors') for idx, hyper in enumerate(result): self.compare_to_expected(expected[idx], hyper) @@ -74,8 +78,8 @@ def test_hypervisor_detail(self): cpu_info='cpu_info', disk_available_least=100)] - result = cs.hypervisors.list() - cs.assert_called('GET', '/os-hypervisors/detail') + result = self.cs.hypervisors.list() + self.cs.assert_called('GET', '/os-hypervisors/detail') for idx, hyper in enumerate(result): self.compare_to_expected(expected[idx], hyper) @@ -86,8 +90,8 @@ def test_hypervisor_search(self): dict(id=5678, hypervisor_hostname='hyper2'), ] - result = cs.hypervisors.search('hyper') - cs.assert_called('GET', '/os-hypervisors/hyper/search') + result = self.cs.hypervisors.search('hyper') + self.cs.assert_called('GET', '/os-hypervisors/hyper/search') for idx, hyper in enumerate(result): self.compare_to_expected(expected[idx], hyper) @@ -106,8 +110,8 @@ def test_hypervisor_servers(self): dict(name='inst4', uuid='uuid4')]), ] - result = cs.hypervisors.search('hyper', True) - cs.assert_called('GET', '/os-hypervisors/hyper/servers') + result = self.cs.hypervisors.search('hyper', True) + self.cs.assert_called('GET', '/os-hypervisors/hyper/servers') for idx, hyper in enumerate(result): self.compare_to_expected(expected[idx], hyper) @@ -132,8 +136,8 @@ def test_hypervisor_get(self): cpu_info='cpu_info', disk_available_least=100) - result = cs.hypervisors.get(1234) - cs.assert_called('GET', '/os-hypervisors/1234') + result = self.cs.hypervisors.get(1234) + self.cs.assert_called('GET', '/os-hypervisors/1234') self.compare_to_expected(expected, result) @@ -143,8 +147,8 @@ def test_hypervisor_uptime(self): hypervisor_hostname="hyper1", uptime="fake uptime") - result = cs.hypervisors.uptime(1234) - cs.assert_called('GET', '/os-hypervisors/1234/uptime') + result = self.cs.hypervisors.uptime(1234) + self.cs.assert_called('GET', '/os-hypervisors/1234/uptime') self.compare_to_expected(expected, result) @@ -164,7 +168,7 @@ def test_hypervisor_statistics(self): disk_available_least=200, ) - result = cs.hypervisors.statistics() - cs.assert_called('GET', '/os-hypervisors/statistics') + result = self.cs.hypervisors.statistics() + self.cs.assert_called('GET', '/os-hypervisors/statistics') self.compare_to_expected(expected, result) diff --git a/novaclient/tests/v3/fakes.py b/novaclient/tests/v3/fakes.py index f856ce406..eee30ef08 100644 --- a/novaclient/tests/v3/fakes.py +++ b/novaclient/tests/v3/fakes.py @@ -292,3 +292,22 @@ def put_os_quota_sets_97f4c221bff44578b0300df4ef119353(self, body, **kw): 'keypairs': 1, 'security_groups': 1, 'security_group_rules': 1}}) + + # + # Hypervisors + # + def get_os_hypervisors_search(self, **kw): + return (200, {}, {'hypervisors': [ + {'id': 1234, 'hypervisor_hostname': 'hyper1'}, + {'id': 5678, 'hypervisor_hostname': 'hyper2'} + ]}) + + def get_os_hypervisors_1234_servers(self, **kw): + return (200, {}, {'hypervisor': + {'id': 1234, + 'hypervisor_hostname': 'hyper1', + 'servers': [ + {'name': 'inst1', 'uuid': 'uuid1'}, + {'name': 'inst2', 'uuid': 'uuid2'} + ]}, + }) diff --git a/novaclient/tests/v3/test_hypervisors.py b/novaclient/tests/v3/test_hypervisors.py new file mode 100644 index 000000000..d2cbbb9ac --- /dev/null +++ b/novaclient/tests/v3/test_hypervisors.py @@ -0,0 +1,50 @@ +# Copyright 2012 OpenStack Foundation +# All Rights Reserved. +# +# 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 novaclient.tests.v1_1 import test_hypervisors +from novaclient.tests.v3 import fakes + + +class HypervisorsTest(test_hypervisors.HypervisorsTest): + def setUp(self): + super(HypervisorsTest, self).setUp() + self.cs = self._get_fake_client() + + def _get_fake_client(self): + return fakes.FakeClient() + + def test_hypervisor_search(self): + expected = [ + dict(id=1234, hypervisor_hostname='hyper1'), + dict(id=5678, hypervisor_hostname='hyper2'), + ] + + result = self.cs.hypervisors.search('hyper') + self.cs.assert_called('GET', '/os-hypervisors/search?query=hyper') + + for idx, hyper in enumerate(result): + self.compare_to_expected(expected[idx], hyper) + + def test_hypervisor_servers(self): + expected = dict(id=1234, + hypervisor_hostname='hyper1', + servers=[ + dict(name='inst1', uuid='uuid1'), + dict(name='inst2', uuid='uuid2')]) + + result = self.cs.hypervisors.servers('1234') + self.cs.assert_called('GET', '/os-hypervisors/1234/servers') + + self.compare_to_expected(expected, result) diff --git a/novaclient/v3/client.py b/novaclient/v3/client.py index e4b51866b..c28e0e8a4 100644 --- a/novaclient/v3/client.py +++ b/novaclient/v3/client.py @@ -20,6 +20,7 @@ from novaclient.v3 import flavor_access from novaclient.v3 import flavors from novaclient.v3 import hosts +from novaclient.v3 import hypervisors from novaclient.v3 import images from novaclient.v3 import quota_classes from novaclient.v3 import quotas @@ -65,6 +66,7 @@ def __init__(self, username, password, project_id, auth_url=None, self.hosts = hosts.HostManager(self) self.flavors = flavors.FlavorManager(self) self.flavor_access = flavor_access.FlavorAccessManager(self) + self.hypervisors = hypervisors.HypervisorManager(self) self.images = images.ImageManager(self) self.quotas = quotas.QuotaSetManager(self) self.quota_classes = quota_classes.QuotaClassSetManager(self) diff --git a/novaclient/v3/hypervisors.py b/novaclient/v3/hypervisors.py new file mode 100644 index 000000000..0542895fb --- /dev/null +++ b/novaclient/v3/hypervisors.py @@ -0,0 +1,48 @@ +# Copyright 2013 IBM Corp +# All Rights Reserved. +# +# 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. + +""" +Hypervisors interface +""" + +from novaclient.openstack.common.py3kcompat import urlutils +from novaclient.v1_1 import hypervisors + + +class Hypervisor(hypervisors.Hypervisor): + pass + + +class HypervisorManager(hypervisors.HypervisorManager): + resource_class = Hypervisor + + def search(self, hypervisor_match): + """ + Get a list of matching hypervisors. + + :param servers: If True, server information is also retrieved. + """ + url = ('/os-hypervisors/search?query=%s' % + urlutils.quote(hypervisor_match, safe='')) + return self._list(url, 'hypervisors') + + def servers(self, hypervisor): + """ + Get servers for a specific hypervisor + + :param hypervisor: ID of hypervisor to get list of servers for. + """ + return self._get('/os-hypervisors/%s/servers' % hypervisor, + 'hypervisor') diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 46da5b4c4..4eaa8a037 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -2684,27 +2684,32 @@ def do_hypervisor_list(cs, args): help='The hypervisor hostname (or pattern) to search for.') def do_hypervisor_servers(cs, args): """List servers belonging to specific hypervisors.""" - hypers = cs.hypervisors.search(args.hostname, servers=True) + # Get a list of hypervisors first + hypers = cs.hypervisors.search(args.hostname) class InstanceOnHyper(object): def __init__(self, **kwargs): self.__dict__.update(kwargs) # Massage the result into a list to be displayed - instances = [] + servers = [] for hyper in hypers: + # Get a list of servers for each hypervisor hyper_host = hyper.hypervisor_hostname hyper_id = hyper.id - if hasattr(hyper, 'servers'): - instances.extend([InstanceOnHyper(id=serv['uuid'], + + hyper_servers = cs.hypervisors.servers(hyper_id) + if hasattr(hyper_servers, 'servers'): + print(hyper_servers.servers) + servers.extend([InstanceOnHyper(id=serv['id'], name=serv['name'], hypervisor_hostname=hyper_host, hypervisor_id=hyper_id) - for serv in hyper.servers]) + for serv in hyper_servers.servers]) # Output the data - utils.print_list(instances, ['ID', 'Name', 'Hypervisor ID', - 'Hypervisor Hostname']) + utils.print_list(servers, ['ID', 'Name', 'Hypervisor ID', + 'Hypervisor Hostname']) @utils.arg('hypervisor', From 4c9cc001ba78f82411d693345cb406995d2ac702 Mon Sep 17 00:00:00 2001 From: Chris Yeoh Date: Mon, 16 Dec 2013 21:59:26 +1030 Subject: [PATCH 0357/1705] Adds aggregates support for Nova V3 API Adds support and tests for the os-aggregates extension for the Nova V3 API. Partially implements blueprint v3-api Change-Id: Ibe251f083ea444fb46aac9cc5cf08784b5826ad7 --- novaclient/tests/v1_1/test_aggregates.py | 97 +++++++++++++----------- novaclient/tests/v3/test_aggregates.py | 30 ++++++++ novaclient/v3/aggregates.py | 26 +++++++ novaclient/v3/client.py | 2 + 4 files changed, 110 insertions(+), 45 deletions(-) create mode 100644 novaclient/tests/v3/test_aggregates.py create mode 100644 novaclient/v3/aggregates.py diff --git a/novaclient/tests/v1_1/test_aggregates.py b/novaclient/tests/v1_1/test_aggregates.py index 3367b9850..1e3247cfe 100644 --- a/novaclient/tests/v1_1/test_aggregates.py +++ b/novaclient/tests/v1_1/test_aggregates.py @@ -18,121 +18,128 @@ from novaclient.v1_1 import aggregates -cs = fakes.FakeClient() +class AggregatesTest(utils.TestCase): + def setUp(self): + super(AggregatesTest, self).setUp() + self.cs = self._get_fake_client() + self.aggregate_type = self._get_aggregate_type() + def _get_fake_client(self): + return fakes.FakeClient() -class AggregatesTest(utils.TestCase): + def _get_aggregate_type(self): + return aggregates.Aggregate def test_list_aggregates(self): - result = cs.aggregates.list() - cs.assert_called('GET', '/os-aggregates') + result = self.cs.aggregates.list() + self.cs.assert_called('GET', '/os-aggregates') for aggregate in result: self.assertTrue(isinstance(aggregate, aggregates.Aggregate)) def test_create_aggregate(self): body = {"aggregate": {"name": "test", "availability_zone": "nova1"}} - aggregate = cs.aggregates.create("test", "nova1") - cs.assert_called('POST', '/os-aggregates', body) + aggregate = self.cs.aggregates.create("test", "nova1") + self.cs.assert_called('POST', '/os-aggregates', body) self.assertTrue(isinstance(aggregate, aggregates.Aggregate)) def test_get(self): - aggregate = cs.aggregates.get("1") - cs.assert_called('GET', '/os-aggregates/1') + aggregate = self.cs.aggregates.get("1") + self.cs.assert_called('GET', '/os-aggregates/1') self.assertTrue(isinstance(aggregate, aggregates.Aggregate)) - aggregate2 = cs.aggregates.get(aggregate) - cs.assert_called('GET', '/os-aggregates/1') + aggregate2 = self.cs.aggregates.get(aggregate) + self.cs.assert_called('GET', '/os-aggregates/1') self.assertTrue(isinstance(aggregate2, aggregates.Aggregate)) def test_get_details(self): - aggregate = cs.aggregates.get_details("1") - cs.assert_called('GET', '/os-aggregates/1') + aggregate = self.cs.aggregates.get_details("1") + self.cs.assert_called('GET', '/os-aggregates/1') self.assertTrue(isinstance(aggregate, aggregates.Aggregate)) - aggregate2 = cs.aggregates.get_details(aggregate) - cs.assert_called('GET', '/os-aggregates/1') + aggregate2 = self.cs.aggregates.get_details(aggregate) + self.cs.assert_called('GET', '/os-aggregates/1') self.assertTrue(isinstance(aggregate2, aggregates.Aggregate)) def test_update(self): - aggregate = cs.aggregates.get("1") + aggregate = self.cs.aggregates.get("1") values = {"name": "foo"} body = {"aggregate": values} result1 = aggregate.update(values) - cs.assert_called('PUT', '/os-aggregates/1', body) + self.cs.assert_called('PUT', '/os-aggregates/1', body) self.assertTrue(isinstance(result1, aggregates.Aggregate)) - result2 = cs.aggregates.update(2, values) - cs.assert_called('PUT', '/os-aggregates/2', body) + result2 = self.cs.aggregates.update(2, values) + self.cs.assert_called('PUT', '/os-aggregates/2', body) self.assertTrue(isinstance(result2, aggregates.Aggregate)) def test_update_with_availability_zone(self): - aggregate = cs.aggregates.get("1") + aggregate = self.cs.aggregates.get("1") values = {"name": "foo", "availability_zone": "new_zone"} body = {"aggregate": values} - result3 = cs.aggregates.update(aggregate, values) - cs.assert_called('PUT', '/os-aggregates/1', body) + result3 = self.cs.aggregates.update(aggregate, values) + self.cs.assert_called('PUT', '/os-aggregates/1', body) self.assertTrue(isinstance(result3, aggregates.Aggregate)) def test_add_host(self): - aggregate = cs.aggregates.get("1") + aggregate = self.cs.aggregates.get("1") host = "host1" body = {"add_host": {"host": "host1"}} result1 = aggregate.add_host(host) - cs.assert_called('POST', '/os-aggregates/1/action', body) + self.cs.assert_called('POST', '/os-aggregates/1/action', body) self.assertTrue(isinstance(result1, aggregates.Aggregate)) - result2 = cs.aggregates.add_host("2", host) - cs.assert_called('POST', '/os-aggregates/2/action', body) + result2 = self.cs.aggregates.add_host("2", host) + self.cs.assert_called('POST', '/os-aggregates/2/action', body) self.assertTrue(isinstance(result2, aggregates.Aggregate)) - result3 = cs.aggregates.add_host(aggregate, host) - cs.assert_called('POST', '/os-aggregates/1/action', body) + result3 = self.cs.aggregates.add_host(aggregate, host) + self.cs.assert_called('POST', '/os-aggregates/1/action', body) self.assertTrue(isinstance(result3, aggregates.Aggregate)) def test_remove_host(self): - aggregate = cs.aggregates.get("1") + aggregate = self.cs.aggregates.get("1") host = "host1" body = {"remove_host": {"host": "host1"}} result1 = aggregate.remove_host(host) - cs.assert_called('POST', '/os-aggregates/1/action', body) + self.cs.assert_called('POST', '/os-aggregates/1/action', body) self.assertTrue(isinstance(result1, aggregates.Aggregate)) - result2 = cs.aggregates.remove_host("2", host) - cs.assert_called('POST', '/os-aggregates/2/action', body) + result2 = self.cs.aggregates.remove_host("2", host) + self.cs.assert_called('POST', '/os-aggregates/2/action', body) self.assertTrue(isinstance(result2, aggregates.Aggregate)) - result3 = cs.aggregates.remove_host(aggregate, host) - cs.assert_called('POST', '/os-aggregates/1/action', body) + result3 = self.cs.aggregates.remove_host(aggregate, host) + self.cs.assert_called('POST', '/os-aggregates/1/action', body) self.assertTrue(isinstance(result3, aggregates.Aggregate)) def test_set_metadata(self): - aggregate = cs.aggregates.get("1") + aggregate = self.cs.aggregates.get("1") metadata = {"foo": "bar"} body = {"set_metadata": {"metadata": metadata}} result1 = aggregate.set_metadata(metadata) - cs.assert_called('POST', '/os-aggregates/1/action', body) + self.cs.assert_called('POST', '/os-aggregates/1/action', body) self.assertTrue(isinstance(result1, aggregates.Aggregate)) - result2 = cs.aggregates.set_metadata(2, metadata) - cs.assert_called('POST', '/os-aggregates/2/action', body) + result2 = self.cs.aggregates.set_metadata(2, metadata) + self.cs.assert_called('POST', '/os-aggregates/2/action', body) self.assertTrue(isinstance(result2, aggregates.Aggregate)) - result3 = cs.aggregates.set_metadata(aggregate, metadata) - cs.assert_called('POST', '/os-aggregates/1/action', body) + result3 = self.cs.aggregates.set_metadata(aggregate, metadata) + self.cs.assert_called('POST', '/os-aggregates/1/action', body) self.assertTrue(isinstance(result3, aggregates.Aggregate)) def test_delete_aggregate(self): - aggregate = cs.aggregates.list()[0] + aggregate = self.cs.aggregates.list()[0] aggregate.delete() - cs.assert_called('DELETE', '/os-aggregates/1') + self.cs.assert_called('DELETE', '/os-aggregates/1') - cs.aggregates.delete('1') - cs.assert_called('DELETE', '/os-aggregates/1') + self.cs.aggregates.delete('1') + self.cs.assert_called('DELETE', '/os-aggregates/1') - cs.aggregates.delete(aggregate) - cs.assert_called('DELETE', '/os-aggregates/1') + self.cs.aggregates.delete(aggregate) + self.cs.assert_called('DELETE', '/os-aggregates/1') diff --git a/novaclient/tests/v3/test_aggregates.py b/novaclient/tests/v3/test_aggregates.py new file mode 100644 index 000000000..f4ce8218a --- /dev/null +++ b/novaclient/tests/v3/test_aggregates.py @@ -0,0 +1,30 @@ +# Copyright 2013 IBM Corp. +# +# 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 novaclient.tests.v1_1 import test_aggregates +from novaclient.tests.v3 import fakes +from novaclient.v3 import aggregates + + +class AggregatesTest(test_aggregates.AggregatesTest): + def setUp(self): + super(AggregatesTest, self).setUp() + self.cs = self._get_fake_client() + self.aggregate_type = self._get_aggregate_type() + + def _get_fake_client(self): + return fakes.FakeClient() + + def _get_aggregate_type(self): + return aggregates.Aggregate diff --git a/novaclient/v3/aggregates.py b/novaclient/v3/aggregates.py new file mode 100644 index 000000000..b2728d6d1 --- /dev/null +++ b/novaclient/v3/aggregates.py @@ -0,0 +1,26 @@ +# Copyright 2012 OpenStack Foundation +# All Rights Reserved. +# +# 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. + +"""Aggregate interface.""" + +from novaclient.v1_1 import aggregates + + +class Aggregate(aggregates.Aggregate): + pass + + +class AggregateManager(aggregates.AggregateManager): + resource_class = Aggregate diff --git a/novaclient/v3/client.py b/novaclient/v3/client.py index c28e0e8a4..5768617a4 100644 --- a/novaclient/v3/client.py +++ b/novaclient/v3/client.py @@ -16,6 +16,7 @@ from novaclient import client from novaclient.v3 import agents +from novaclient.v3 import aggregates from novaclient.v3 import availability_zones from novaclient.v3 import flavor_access from novaclient.v3 import flavors @@ -61,6 +62,7 @@ def __init__(self, username, password, project_id, auth_url=None, self.os_cache = os_cache or not no_cache #TODO(bnemec): Add back in v3 extensions self.agents = agents.AgentsManager(self) + self.aggregates = aggregates.AggregateManager(self) self.availability_zones = \ availability_zones.AvailabilityZoneManager(self) self.hosts = hosts.HostManager(self) From e0d7c490434af36bbabc4c850a2969404a058aac Mon Sep 17 00:00:00 2001 From: Chris Yeoh Date: Mon, 16 Dec 2013 23:32:03 +1030 Subject: [PATCH 0358/1705] Adds certificates support for Nova V3 API Adds support and tests for the os-certificates extension for the Nova V3 API Partially implements blueprint v3-api Change-Id: I9a75bf04a7047af6c501af2df72de4b8ce3462b0 --- novaclient/tests/v1_1/test_certs.py | 19 ++++++++++++------ novaclient/tests/v3/test_certs.py | 29 ++++++++++++++++++++++++++++ novaclient/v3/certs.py | 30 +++++++++++++++++++++++++++++ novaclient/v3/client.py | 2 ++ 4 files changed, 74 insertions(+), 6 deletions(-) create mode 100644 novaclient/tests/v3/test_certs.py create mode 100644 novaclient/v3/certs.py diff --git a/novaclient/tests/v1_1/test_certs.py b/novaclient/tests/v1_1/test_certs.py index c08ac90ed..b6728369c 100644 --- a/novaclient/tests/v1_1/test_certs.py +++ b/novaclient/tests/v1_1/test_certs.py @@ -16,17 +16,24 @@ from novaclient.v1_1 import certs -cs = fakes.FakeClient() +class CertsTest(utils.TestCase): + def setUp(self): + super(CertsTest, self).setUp() + self.cs = self._get_fake_client() + self.cert_type = self._get_cert_type() + def _get_fake_client(self): + return fakes.FakeClient() -class FlavorsTest(utils.TestCase): + def _get_cert_type(self): + return certs.Certificate def test_create_cert(self): - cert = cs.certs.create() - cs.assert_called('POST', '/os-certificates') + cert = self.cs.certs.create() + self.cs.assert_called('POST', '/os-certificates') self.assertTrue(isinstance(cert, certs.Certificate)) def test_get_root_cert(self): - cert = cs.certs.get() - cs.assert_called('GET', '/os-certificates/root') + cert = self.cs.certs.get() + self.cs.assert_called('GET', '/os-certificates/root') self.assertTrue(isinstance(cert, certs.Certificate)) diff --git a/novaclient/tests/v3/test_certs.py b/novaclient/tests/v3/test_certs.py new file mode 100644 index 000000000..1e293adc0 --- /dev/null +++ b/novaclient/tests/v3/test_certs.py @@ -0,0 +1,29 @@ +# +# 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 novaclient.tests.v1_1 import fakes +from novaclient.tests.v1_1 import test_certs +from novaclient.v3 import certs + + +class CertsTest(test_certs.CertsTest): + def setUp(self): + super(CertsTest, self).setUp() + self.cs = self._get_fake_client() + self.cert_type = self._get_cert_type() + + def _get_fake_client(self): + return fakes.FakeClient() + + def _get_cert_type(self): + return certs.Certificate diff --git a/novaclient/v3/certs.py b/novaclient/v3/certs.py new file mode 100644 index 000000000..5e6785108 --- /dev/null +++ b/novaclient/v3/certs.py @@ -0,0 +1,30 @@ +# Copyright 2010 Jacob Kaplan-Moss + +# Copyright 2011 OpenStack Foundation +# All Rights Reserved. +# +# 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. + +""" +Certificate interface. +""" + +from novaclient.v1_1 import certs + + +class Certificate(certs.Certificate): + pass + + +class CertificateManager(certs.CertificateManager): + pass diff --git a/novaclient/v3/client.py b/novaclient/v3/client.py index 5768617a4..5ef4b1dae 100644 --- a/novaclient/v3/client.py +++ b/novaclient/v3/client.py @@ -18,6 +18,7 @@ from novaclient.v3 import agents from novaclient.v3 import aggregates from novaclient.v3 import availability_zones +from novaclient.v3 import certs from novaclient.v3 import flavor_access from novaclient.v3 import flavors from novaclient.v3 import hosts @@ -65,6 +66,7 @@ def __init__(self, username, password, project_id, auth_url=None, self.aggregates = aggregates.AggregateManager(self) self.availability_zones = \ availability_zones.AvailabilityZoneManager(self) + self.certs = certs.CertificateManager(self) self.hosts = hosts.HostManager(self) self.flavors = flavors.FlavorManager(self) self.flavor_access = flavor_access.FlavorAccessManager(self) From a29b7c7cc76155b04fd51c5f3aea8b14d5041255 Mon Sep 17 00:00:00 2001 From: Chris Yeoh Date: Tue, 17 Dec 2013 12:45:20 +1030 Subject: [PATCH 0359/1705] Adds keypairs support for the Nova V3 API Adds support and tests for the keypairs extension for the Nova V3 API. The V3 version of the keypair extension has been made part of the core V3 API and as a result no longer has the "os-" prefix in the url. Differences between the V2 and V3 API are described here: https://wiki.openstack.org/wiki/NovaAPIv2tov3 Partially implements blueprint v3-api Change-Id: Id4a77e1e4565f63ecdf7753d3c224975519fc07c --- novaclient/tests/v1_1/test_keypairs.py | 40 +++++++++++++++----------- novaclient/tests/v3/fakes.py | 8 ++++++ novaclient/tests/v3/test_keypairs.py | 31 ++++++++++++++++++++ novaclient/v1_1/keypairs.py | 10 ++++--- novaclient/v3/client.py | 2 ++ novaclient/v3/keypairs.py | 28 ++++++++++++++++++ 6 files changed, 99 insertions(+), 20 deletions(-) create mode 100644 novaclient/tests/v3/test_keypairs.py create mode 100644 novaclient/v3/keypairs.py diff --git a/novaclient/tests/v1_1/test_keypairs.py b/novaclient/tests/v1_1/test_keypairs.py index a6b674228..d4ffc4e20 100644 --- a/novaclient/tests/v1_1/test_keypairs.py +++ b/novaclient/tests/v1_1/test_keypairs.py @@ -16,37 +16,45 @@ from novaclient.v1_1 import keypairs -cs = fakes.FakeClient() +class KeypairsTest(utils.TestCase): + def setUp(self): + super(KeypairsTest, self).setUp() + self.cs = self._get_fake_client() + self.keypair_type = self._get_keypair_type() + self.keypair_prefix = keypairs.KeypairManager.keypair_prefix + def _get_fake_client(self): + return fakes.FakeClient() -class KeypairsTest(utils.TestCase): + def _get_keypair_type(self): + return keypairs.Keypair def test_get_keypair(self): - kp = cs.keypairs.get('test') - cs.assert_called('GET', '/os-keypairs/test') + kp = self.cs.keypairs.get('test') + self.cs.assert_called('GET', '/%s/test' % self.keypair_prefix) self.assertTrue(isinstance(kp, keypairs.Keypair)) self.assertEqual(kp.name, 'test') def test_list_keypairs(self): - kps = cs.keypairs.list() - cs.assert_called('GET', '/os-keypairs') + kps = self.cs.keypairs.list() + self.cs.assert_called('GET', '/%s' % self.keypair_prefix) [self.assertTrue(isinstance(kp, keypairs.Keypair)) for kp in kps] def test_delete_keypair(self): - kp = cs.keypairs.list()[0] + kp = self.cs.keypairs.list()[0] kp.delete() - cs.assert_called('DELETE', '/os-keypairs/test') - cs.keypairs.delete('test') - cs.assert_called('DELETE', '/os-keypairs/test') - cs.keypairs.delete(kp) - cs.assert_called('DELETE', '/os-keypairs/test') + self.cs.assert_called('DELETE', '/%s/test' % self.keypair_prefix) + self.cs.keypairs.delete('test') + self.cs.assert_called('DELETE', '/%s/test' % self.keypair_prefix) + self.cs.keypairs.delete(kp) + self.cs.assert_called('DELETE', '/%s/test' % self.keypair_prefix) def test_create_keypair(self): - kp = cs.keypairs.create("foo") - cs.assert_called('POST', '/os-keypairs') + kp = self.cs.keypairs.create("foo") + self.cs.assert_called('POST', '/%s' % self.keypair_prefix) self.assertTrue(isinstance(kp, keypairs.Keypair)) def test_import_keypair(self): - kp = cs.keypairs.create("foo", "fake-public-key") - cs.assert_called('POST', '/os-keypairs') + kp = self.cs.keypairs.create("foo", "fake-public-key") + self.cs.assert_called('POST', '/%s' % self.keypair_prefix) self.assertTrue(isinstance(kp, keypairs.Keypair)) diff --git a/novaclient/tests/v3/fakes.py b/novaclient/tests/v3/fakes.py index eee30ef08..72422f5cf 100644 --- a/novaclient/tests/v3/fakes.py +++ b/novaclient/tests/v3/fakes.py @@ -311,3 +311,11 @@ def get_os_hypervisors_1234_servers(self, **kw): {'name': 'inst2', 'uuid': 'uuid2'} ]}, }) + + # + # Keypairs + # + get_keypairs_test = fakes_v1_1.FakeHTTPClient.get_os_keypairs_test + get_keypairs = fakes_v1_1.FakeHTTPClient.get_os_keypairs + delete_keypairs_test = fakes_v1_1.FakeHTTPClient.delete_os_keypairs_test + post_keypairs = fakes_v1_1.FakeHTTPClient.post_os_keypairs diff --git a/novaclient/tests/v3/test_keypairs.py b/novaclient/tests/v3/test_keypairs.py new file mode 100644 index 000000000..fd1e4bac4 --- /dev/null +++ b/novaclient/tests/v3/test_keypairs.py @@ -0,0 +1,31 @@ +# Copyright 2013 IBM Corp. +# +# 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 novaclient.tests.v1_1 import test_keypairs +from novaclient.tests.v3 import fakes +from novaclient.v3 import keypairs + + +class KeypairsTest(test_keypairs.KeypairsTest): + def setUp(self): + super(KeypairsTest, self).setUp() + self.cs = self._get_fake_client() + self.keypair_type = self._get_keypair_type() + self.keypair_prefix = keypairs.KeypairManager.keypair_prefix + + def _get_fake_client(self): + return fakes.FakeClient() + + def _get_keypair_type(self): + return keypairs.Keypair diff --git a/novaclient/v1_1/keypairs.py b/novaclient/v1_1/keypairs.py index cd9e5852f..3a102a43d 100644 --- a/novaclient/v1_1/keypairs.py +++ b/novaclient/v1_1/keypairs.py @@ -52,6 +52,7 @@ def delete(self): class KeypairManager(base.ManagerWithFind): resource_class = Keypair + keypair_prefix = "os-keypairs" def get(self, keypair): """ @@ -60,7 +61,8 @@ def get(self, keypair): :param keypair: The ID of the keypair to get. :rtype: :class:`Keypair` """ - return self._get("/os-keypairs/%s" % base.getid(keypair), "keypair") + return self._get("/%s/%s" % (self.keypair_prefix, base.getid(keypair)), + "keypair") def create(self, name, public_key=None): """ @@ -72,7 +74,7 @@ def create(self, name, public_key=None): body = {'keypair': {'name': name}} if public_key: body['keypair']['public_key'] = public_key - return self._create('/os-keypairs', body, 'keypair') + return self._create('/%s' % self.keypair_prefix, body, 'keypair') def delete(self, key): """ @@ -80,10 +82,10 @@ def delete(self, key): :param key: The :class:`Keypair` (or its ID) to delete. """ - self._delete('/os-keypairs/%s' % (base.getid(key))) + self._delete('/%s/%s' % (self.keypair_prefix, base.getid(key))) def list(self): """ Get a list of keypairs. """ - return self._list('/os-keypairs', 'keypairs') + return self._list('/%s' % self.keypair_prefix, 'keypairs') diff --git a/novaclient/v3/client.py b/novaclient/v3/client.py index 5ef4b1dae..fcbbdc66d 100644 --- a/novaclient/v3/client.py +++ b/novaclient/v3/client.py @@ -24,6 +24,7 @@ from novaclient.v3 import hosts from novaclient.v3 import hypervisors from novaclient.v3 import images +from novaclient.v3 import keypairs from novaclient.v3 import quota_classes from novaclient.v3 import quotas from novaclient.v3 import servers @@ -72,6 +73,7 @@ def __init__(self, username, password, project_id, auth_url=None, self.flavor_access = flavor_access.FlavorAccessManager(self) self.hypervisors = hypervisors.HypervisorManager(self) self.images = images.ImageManager(self) + self.keypairs = keypairs.KeypairManager(self) self.quotas = quotas.QuotaSetManager(self) self.quota_classes = quota_classes.QuotaClassSetManager(self) self.servers = servers.ServerManager(self) diff --git a/novaclient/v3/keypairs.py b/novaclient/v3/keypairs.py new file mode 100644 index 000000000..f1ef8ac58 --- /dev/null +++ b/novaclient/v3/keypairs.py @@ -0,0 +1,28 @@ +# Copyright 2013 IBM Corp. +# +# 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. + +""" +Keypair interface +""" + +from novaclient.v1_1 import keypairs + + +class Keypair(keypairs.Keypair): + pass + + +class KeypairManager(keypairs.KeypairManager): + resource_class = Keypair + keypair_prefix = "keypairs" From 3b5b7b9da3a36269a1999dea6cda04095bf390cd Mon Sep 17 00:00:00 2001 From: Chris Yeoh Date: Tue, 17 Dec 2013 16:21:41 +1030 Subject: [PATCH 0360/1705] Adds simple tenant usage support for the Nova V3 API Adds support and tests for the os-simple-tenant-usage extension for the Nova V3 API. There are no differences between the V2 and V3 API and this change adds the client code to enable the usage command. Note that with nomenclature change from tenant to project occuring in Nova there will be a difference between the V2 and V3 API in the future. Differences between the V2 and V3 API are described here: https://wiki.openstack.org/wiki/NovaAPIv2tov3 Partially implements blueprint v3-api Change-Id: Iddf6211c9c87cbb191d9a5d5a65710b343c30e67 --- novaclient/tests/v1_1/test_usage.py | 19 ++++++++++++------ novaclient/tests/v3/test_usage.py | 30 +++++++++++++++++++++++++++++ novaclient/v3/client.py | 2 ++ novaclient/v3/usage.py | 27 ++++++++++++++++++++++++++ 4 files changed, 72 insertions(+), 6 deletions(-) create mode 100644 novaclient/tests/v3/test_usage.py create mode 100644 novaclient/v3/usage.py diff --git a/novaclient/tests/v1_1/test_usage.py b/novaclient/tests/v1_1/test_usage.py index 01df9fe70..ef842f4f1 100644 --- a/novaclient/tests/v1_1/test_usage.py +++ b/novaclient/tests/v1_1/test_usage.py @@ -18,16 +18,23 @@ from novaclient.v1_1 import usage -cs = fakes.FakeClient() +class UsageTest(utils.TestCase): + def setUp(self): + super(UsageTest, self).setUp() + self.cs = self._get_fake_client() + self.usage_type = self._get_usage_type() + def _get_fake_client(self): + return fakes.FakeClient() -class UsageTest(utils.TestCase): + def _get_usage_type(self): + return usage.Usage def test_usage_list(self, detailed=False): now = datetime.datetime.now() - usages = cs.usage.list(now, now, detailed) + usages = self.cs.usage.list(now, now, detailed) - cs.assert_called('GET', + self.cs.assert_called('GET', "/os-simple-tenant-usage?" + ("start=%s&" % now.isoformat()) + ("end=%s&" % now.isoformat()) + @@ -39,9 +46,9 @@ def test_usage_list_detailed(self): def test_usage_get(self): now = datetime.datetime.now() - u = cs.usage.get("tenantfoo", now, now) + u = self.cs.usage.get("tenantfoo", now, now) - cs.assert_called('GET', + self.cs.assert_called('GET', "/os-simple-tenant-usage/tenantfoo?" + ("start=%s&" % now.isoformat()) + ("end=%s" % now.isoformat())) diff --git a/novaclient/tests/v3/test_usage.py b/novaclient/tests/v3/test_usage.py new file mode 100644 index 000000000..b4ffab79e --- /dev/null +++ b/novaclient/tests/v3/test_usage.py @@ -0,0 +1,30 @@ +# Copyright 2013 IBM Corp. +# +# 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 novaclient.tests.v1_1 import fakes +from novaclient.tests.v1_1 import test_usage +from novaclient.v3 import usage + + +class UsageTest(test_usage.UsageTest): + def setUp(self): + super(UsageTest, self).setUp() + self.cs = self._get_fake_client() + self.usage_type = self._get_usage_type() + + def _get_fake_client(self): + return fakes.FakeClient() + + def _get_usage_type(self): + return usage.Usage diff --git a/novaclient/v3/client.py b/novaclient/v3/client.py index fcbbdc66d..0d8e8c3f7 100644 --- a/novaclient/v3/client.py +++ b/novaclient/v3/client.py @@ -29,6 +29,7 @@ from novaclient.v3 import quotas from novaclient.v3 import servers from novaclient.v3 import services +from novaclient.v3 import usage class Client(object): @@ -78,6 +79,7 @@ def __init__(self, username, password, project_id, auth_url=None, self.quota_classes = quota_classes.QuotaClassSetManager(self) self.servers = servers.ServerManager(self) self.services = services.ServiceManager(self) + self.usage = usage.UsageManager(self) # Add in any extensions... if extensions: diff --git a/novaclient/v3/usage.py b/novaclient/v3/usage.py new file mode 100644 index 000000000..dd16997c6 --- /dev/null +++ b/novaclient/v3/usage.py @@ -0,0 +1,27 @@ +# Copyright 2013 IBM Corp. +# +# 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. + +""" +Usage interface. +""" + +from novaclient.v1_1 import usage + + +class Usage(usage.Usage): + pass + + +class UsageManager(usage.UsageManager): + pass From 5b773adada2e1baa76032f7a38e0d4d624eb67d6 Mon Sep 17 00:00:00 2001 From: huangtianhua Date: Fri, 6 Dec 2013 11:41:44 +0800 Subject: [PATCH 0361/1705] Fix "device" as the optional para on volume-attach Attach a volume to a server without device name, the client returned the error msg: "error: too few arguments". But for server, we can attach a volume to a server without the device name. Change-Id: Ia2743c6e3956cae6591ecd414baf89b0dbcf532b Closes-Bug: #1257137 --- novaclient/tests/v1_1/test_shell.py | 7 +++++++ novaclient/v1_1/shell.py | 2 +- novaclient/v3/shell.py | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index c02dc3dcf..4eb2c7d41 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -1790,6 +1790,13 @@ def test_volume_attach(self): {'device': '/dev/vdb', 'volumeId': 'Work'}}) + def test_volume_attach_without_device(self): + self.run_command('volume-attach sample-server Work') + self.assert_called('POST', '/servers/1234/os-volume_attachments', + {'volumeAttachment': + {'device': None, + 'volumeId': 'Work'}}) + def test_volume_update(self): self.run_command('volume-update sample-server Work Work') self.assert_called('PUT', '/servers/1234/os-volume_attachments/Work', diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 890fd0daa..b3de5808d 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1707,7 +1707,7 @@ def do_volume_delete(cs, args): @utils.arg('volume', metavar='', help='ID of the volume to attach.') -@utils.arg('device', metavar='', +@utils.arg('device', metavar='', default=None, nargs='?', help='Name of the device e.g. /dev/vdb. ' 'Use "auto" for autoassign (if supported)') def do_volume_attach(cs, args): diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index bc123b29e..39ad3d2ed 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -1557,7 +1557,7 @@ def do_volume_delete(cs, args): @utils.arg('volume', metavar='', help='ID of the volume to attach.') -@utils.arg('device', metavar='', +@utils.arg('device', metavar='', default=None, nargs='?', help='Name of the device e.g. /dev/vdb. ' 'Use "auto" for autoassign (if supported)') def do_volume_attach(cs, args): From 6f7ac333de3aa98f717e251c77220598a310d280 Mon Sep 17 00:00:00 2001 From: Michael Davies Date: Fri, 20 Dec 2013 17:30:59 +1030 Subject: [PATCH 0362/1705] Adding additional tests for novaclient ssh There's a few gaps in the testing regime for do_ssh, specifically around IPv6, specifying identity files, and additional args. This patch attempts to address these. Change-Id: I590fd7ffe1e3470c8507d7f7fb2abcfdb6162561 --- novaclient/tests/v1_1/test_shell.py | 36 ++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index 6885e96d5..9b3c9b05e 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -1837,19 +1837,43 @@ def test_migration_list_with_filters(self): def test_ssh(self, mock_system, mock_find_server): class FakeResources(object): addresses = { - "private": [{'version': 4, 'addr': "1.1.1.1"}], - "public": [{'version': 4, 'addr': "2.2.2.2"}] + "private": [{'version': 4, 'addr': "1.1.1.1"}, + {'version': 6, 'addr': "2607:f0d0:1002::4"}], + "public": [{'version': 4, 'addr': "2.2.2.2"}, + {'version': 6, 'addr': "7612:a1b2:2004::6"}] } mock_find_server.return_value = FakeResources() self.run_command("ssh --login bob server") - mock_system.assert_any_call("ssh -4 -p22 bob@2.2.2.2 ") + mock_system.assert_called_with("ssh -4 -p22 bob@2.2.2.2 ") self.run_command("ssh alice@server") - mock_system.assert_any_call("ssh -4 -p22 alice@2.2.2.2 ") + mock_system.assert_called_with("ssh -4 -p22 alice@2.2.2.2 ") self.run_command("ssh --port 202 server") - mock_system.assert_any_call("ssh -4 -p202 root@2.2.2.2 ") + mock_system.assert_called_with("ssh -4 -p202 root@2.2.2.2 ") self.run_command("ssh --private server") - mock_system.assert_any_call("ssh -4 -p22 root@1.1.1.1 ") + mock_system.assert_called_with("ssh -4 -p22 root@1.1.1.1 ") + self.run_command("ssh -i ~/my_rsa_key server --private") + mock_system.assert_called_with("ssh -4 -p22 -i ~/my_rsa_key " + "root@1.1.1.1 ") + self.run_command("ssh --extra-opts -1 server") + mock_system.assert_called_with("ssh -4 -p22 root@2.2.2.2 -1") + + self.run_command("ssh --ipv6 --login carol server") + mock_system.assert_called_with("ssh -6 -p22 carol@7612:a1b2:2004::6 ") + self.run_command("ssh --ipv6 dan@server") + mock_system.assert_called_with("ssh -6 -p22 dan@7612:a1b2:2004::6 ") + self.run_command("ssh --ipv6 --port 2022 server") + mock_system.assert_called_with("ssh -6 -p2022 " + "root@7612:a1b2:2004::6 ") + self.run_command("ssh --ipv6 --private server") + mock_system.assert_called_with("ssh -6 -p22 root@2607:f0d0:1002::4 ") + self.run_command("ssh --ipv6 --identity /home/me/my_dsa_key " + "--private server") + mock_system.assert_called_with("ssh -6 -p22 -i /home/me/my_dsa_key " + "root@2607:f0d0:1002::4 ") + self.run_command("ssh --ipv6 --private --extra-opts -1 server") + mock_system.assert_called_with("ssh -6 -p22 " + "root@2607:f0d0:1002::4 -1") class GetSecgroupTest(utils.TestCase): From 7bd5b78b225a431d33907cdca64dbcb582255a51 Mon Sep 17 00:00:00 2001 From: JUN JIE NAN Date: Mon, 23 Dec 2013 10:36:35 +0800 Subject: [PATCH 0363/1705] Removed duplicated import sys is imported twice so removed the second import. Change-Id: I50531edf856d42ec9b10732eff18a97d0cb3e9cc --- doc/source/conf.py | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index 2d93c02bb..ee0b6837f 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -19,7 +19,6 @@ # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.append(os.path.abspath('.')) import os -import sys BASE_DIR = os.path.dirname(os.path.abspath(__file__)) ROOT = os.path.abspath(os.path.join(BASE_DIR, "..", "..")) From 2c54f5a037c72c18248a99504f38bfd583b7b4d7 Mon Sep 17 00:00:00 2001 From: Sahid Orentino Ferdjaoui Date: Wed, 13 Nov 2013 12:59:18 +0000 Subject: [PATCH 0364/1705] Nova aggregate-details should be more human friendly 'nova aggregate-details' shows Hosts, Availability Zone and Metadata in the python unicode notation. + Adds some tests about aggregates ported from v1 to the v3 shell Change-Id: I69d80b2e76833d78248dee782ae5e53f42a4f4a9 Closes-Bug: #1132961 --- novaclient/tests/test_utils.py | 20 ++++ novaclient/tests/v3/test_shell.py | 168 ++++++++++++++++++++++++++++++ novaclient/utils.py | 5 + novaclient/v1_1/shell.py | 13 ++- novaclient/v3/shell.py | 13 ++- 5 files changed, 217 insertions(+), 2 deletions(-) create mode 100644 novaclient/tests/v3/test_shell.py diff --git a/novaclient/tests/test_utils.py b/novaclient/tests/test_utils.py index bc63c95c9..14003c815 100644 --- a/novaclient/tests/test_utils.py +++ b/novaclient/tests/test_utils.py @@ -197,3 +197,23 @@ def test_flattening(self): 'a2': ['l'], 'a3': ('t',)}, squashed) + + def test_pretty_choice_list(self): + l = [] + r = utils.pretty_choice_list(l) + self.assertEqual(r, "") + + l = ["v1", "v2", "v3"] + r = utils.pretty_choice_list(l) + self.assertEqual(r, "'v1', 'v2', 'v3'") + + def test_pretty_choice_dict(self): + d = {} + r = utils.pretty_choice_dict(d) + self.assertEqual(r, "") + + d = {"k1": "v1", + "k2": "v2", + "k3": "v3"} + r = utils.pretty_choice_dict(d) + self.assertEqual(r, "'k1=v1', 'k2=v2', 'k3=v3'") diff --git a/novaclient/tests/v3/test_shell.py b/novaclient/tests/v3/test_shell.py new file mode 100644 index 000000000..07eec1aa0 --- /dev/null +++ b/novaclient/tests/v3/test_shell.py @@ -0,0 +1,168 @@ +# Copyright 2013 Cloudwatt +# Copyright 2010 Jacob Kaplan-Moss +# Copyright 2011 OpenStack Foundation +# Copyright 2012 IBM Corp. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import fixtures +import mock +import six + +from novaclient.openstack.common import timeutils +import novaclient.shell +from novaclient.tests import utils +from novaclient.tests.v3 import fakes + + +class ShellFixture(fixtures.Fixture): + def setUp(self): + super(ShellFixture, self).setUp() + self.shell = novaclient.shell.OpenStackComputeShell() + + def tearDown(self): + # For some method like test_image_meta_bad_action we are + # testing a SystemExit to be thrown and object self.shell has + # no time to get instantatiated which is OK in this case, so + # we make sure the method is there before launching it. + if hasattr(self.shell, 'cs'): + self.shell.cs.clear_callstack() + super(ShellFixture, self).tearDown() + + +class ShellTest(utils.TestCase): + FAKE_ENV = { + 'NOVA_USERNAME': 'username', + 'NOVA_PASSWORD': 'password', + 'NOVA_PROJECT_ID': 'project_id', + 'OS_COMPUTE_API_VERSION': '3', + 'NOVA_URL': 'http://no.where', + } + + def setUp(self): + """Run before each test.""" + super(ShellTest, self).setUp() + + for var in self.FAKE_ENV: + self.useFixture(fixtures.EnvironmentVariable(var, + self.FAKE_ENV[var])) + self.shell = self.useFixture(ShellFixture()).shell + + self.useFixture(fixtures.MonkeyPatch( + 'novaclient.client.get_client_class', + lambda *_: fakes.FakeClient)) + self.addCleanup(timeutils.clear_time_override) + + @mock.patch('sys.stdout', new_callable=six.StringIO) + def run_command(self, cmd, mock_stdout): + if isinstance(cmd, list): + self.shell.main(cmd) + else: + self.shell.main(cmd.split()) + return mock_stdout.getvalue() + + def assert_called(self, method, url, body=None, **kwargs): + return self.shell.cs.assert_called(method, url, body, **kwargs) + + def assert_called_anytime(self, method, url, body=None): + return self.shell.cs.assert_called_anytime(method, url, body) + + def test_aggregate_list(self): + self.run_command('aggregate-list') + self.assert_called('GET', '/os-aggregates') + + def test_aggregate_create(self): + self.run_command('aggregate-create test_name nova1') + body = {"aggregate": {"name": "test_name", + "availability_zone": "nova1"}} + self.assert_called('POST', '/os-aggregates', body, pos=-2) + self.assert_called('GET', '/os-aggregates/1', pos=-1) + + def test_aggregate_delete_by_id(self): + self.run_command('aggregate-delete 1') + self.assert_called('DELETE', '/os-aggregates/1') + + def test_aggregate_delete_by_name(self): + self.run_command('aggregate-delete test') + self.assert_called('DELETE', '/os-aggregates/1') + + def test_aggregate_update_by_id(self): + self.run_command('aggregate-update 1 new_name') + body = {"aggregate": {"name": "new_name"}} + self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) + self.assert_called('GET', '/os-aggregates/1', pos=-1) + + def test_aggregate_update_by_name(self): + self.run_command('aggregate-update test new_name') + body = {"aggregate": {"name": "new_name"}} + self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) + self.assert_called('GET', '/os-aggregates/1', pos=-1) + + def test_aggregate_update_with_availability_zone_by_id(self): + self.run_command('aggregate-update 1 foo new_zone') + body = {"aggregate": {"name": "foo", "availability_zone": "new_zone"}} + self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) + self.assert_called('GET', '/os-aggregates/1', pos=-1) + + def test_aggregate_update_with_availability_zone_by_name(self): + self.run_command('aggregate-update test foo new_zone') + body = {"aggregate": {"name": "foo", "availability_zone": "new_zone"}} + self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) + self.assert_called('GET', '/os-aggregates/1', pos=-1) + + def test_aggregate_set_metadata_by_id(self): + self.run_command('aggregate-set-metadata 1 foo=bar delete_key') + body = {"set_metadata": {"metadata": {"foo": "bar", + "delete_key": None}}} + self.assert_called('POST', '/os-aggregates/1/action', body, pos=-2) + self.assert_called('GET', '/os-aggregates/1', pos=-1) + + def test_aggregate_set_metadata_by_name(self): + self.run_command('aggregate-set-metadata test foo=bar delete_key') + body = {"set_metadata": {"metadata": {"foo": "bar", + "delete_key": None}}} + self.assert_called('POST', '/os-aggregates/1/action', body, pos=-2) + self.assert_called('GET', '/os-aggregates/1', pos=-1) + + def test_aggregate_add_host_by_id(self): + self.run_command('aggregate-add-host 1 host1') + body = {"add_host": {"host": "host1"}} + self.assert_called('POST', '/os-aggregates/1/action', body, pos=-2) + self.assert_called('GET', '/os-aggregates/1', pos=-1) + + def test_aggregate_add_host_by_name(self): + self.run_command('aggregate-add-host test host1') + body = {"add_host": {"host": "host1"}} + self.assert_called('POST', '/os-aggregates/1/action', body, pos=-2) + self.assert_called('GET', '/os-aggregates/1', pos=-1) + + def test_aggregate_remove_host_by_id(self): + self.run_command('aggregate-remove-host 1 host1') + body = {"remove_host": {"host": "host1"}} + self.assert_called('POST', '/os-aggregates/1/action', body, pos=-2) + self.assert_called('GET', '/os-aggregates/1', pos=-1) + + def test_aggregate_remove_host_by_name(self): + self.run_command('aggregate-remove-host test host1') + body = {"remove_host": {"host": "host1"}} + self.assert_called('POST', '/os-aggregates/1/action', body, pos=-2) + self.assert_called('GET', '/os-aggregates/1', pos=-1) + + def test_aggregate_details_by_id(self): + self.run_command('aggregate-details 1') + self.assert_called('GET', '/os-aggregates/1') + + def test_aggregate_details_by_name(self): + self.run_command('aggregate-details test') + self.assert_called('GET', '/os-aggregates') diff --git a/novaclient/utils.py b/novaclient/utils.py index 6e6406037..be64f0290 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -157,6 +157,11 @@ def pretty_choice_list(l): return ', '.join("'%s'" % i for i in l) +def pretty_choice_dict(d): + """Returns a formatted dict as 'key=value'.""" + return pretty_choice_list(['%s=%s' % (k, d[k]) for k in sorted(d.keys())]) + + def print_list(objs, fields, formatters={}, sortby_index=None): if sortby_index is None: sortby = None diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 8479429ea..9d332cab2 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2706,7 +2706,18 @@ def do_aggregate_details(cs, args): def _print_aggregate_details(aggregate): columns = ['Id', 'Name', 'Availability Zone', 'Hosts', 'Metadata'] - utils.print_list([aggregate], columns) + + def parser_metadata(fields): + return utils.pretty_choice_dict(getattr(fields, 'metadata', {}) or {}) + + def parser_hosts(fields): + return utils.pretty_choice_list(getattr(fields, 'hosts', [])) + + formatters = { + 'Metadata': parser_metadata, + 'Hosts': parser_hosts, + } + utils.print_list([aggregate], columns, formatters=formatters) @utils.arg('server', metavar='', help='Name or ID of server.') diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 76f16e7b9..eed85cf08 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -2508,7 +2508,18 @@ def do_aggregate_details(cs, args): def _print_aggregate_details(aggregate): columns = ['Id', 'Name', 'Availability Zone', 'Hosts', 'Metadata'] - utils.print_list([aggregate], columns) + + def parser_metadata(fields): + return utils.pretty_choice_dict(getattr(fields, 'metadata', {}) or {}) + + def parser_hosts(fields): + return utils.pretty_choice_list(getattr(fields, 'hosts', [])) + + formatters = { + 'Metadata': parser_metadata, + 'Hosts': parser_hosts, + } + utils.print_list([aggregate], columns, formatters=formatters) @utils.arg('server', metavar='', help='Name or ID of server.') From 7dc2ff4a49fc1c9666cc1e7ac630f69d521de3a3 Mon Sep 17 00:00:00 2001 From: Clint Byrum Date: Thu, 12 Dec 2013 15:44:44 -0800 Subject: [PATCH 0365/1705] Allow empty response in service-list If a user runs 'nova service-list --binary something-weird', where something-weird is a binary that is not listed the result is a confusing IndexError. That is because the result is expected to always have at least one result. Part of the reason for that is that the fake client does not make it easy to simulate an empty response in tests. That is why there is no regression test associated with this bug-fix. That is also why I am marking thas as Partial-Bug, as this corrects the symptom, but it does not prevent it from regressing. Change-Id: I3e6798d63a662c245196b3bf95bb7e1b0ea9c3a4 Partial-Bug: #1260504 --- novaclient/v1_1/shell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 890fd0daa..ce2790cc0 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2763,7 +2763,7 @@ def do_service_list(cs, args): columns = ["Binary", "Host", "Zone", "Status", "State", "Updated_at"] # NOTE(sulo): we check if the response has disabled_reason # so as not to add the column when the extended ext is not enabled. - if hasattr(result[0], 'disabled_reason'): + if result and hasattr(result[0], 'disabled_reason'): columns.append("Disabled Reason") utils.print_list(result, columns) From 669cdc4ffd22e5bee5aabfb8118f4449a48c2f30 Mon Sep 17 00:00:00 2001 From: Matthew Farrellee Date: Sun, 22 Dec 2013 08:01:36 -0500 Subject: [PATCH 0366/1705] Replace some utils.bool_from_str with strutils Instead of using novaclient.utils' bool_from_str, use bool_from_string from the oslo incubating strutils Notes: 0. utils.bool_from_str was strict, only accepted a small set of values, while strutils.bool_from_string is not strict by default, anything not true is false 1. bool_from_string accepts on/off, which bool_from_str did not Change-Id: I04744844b55697819289def081d3c9117ed0713f --- novaclient/shell.py | 6 ++++-- novaclient/utils.py | 15 --------------- novaclient/v1_1/flavors.py | 4 ++-- novaclient/v1_1/shell.py | 11 +++++++---- novaclient/v3/shell.py | 11 +++++++---- 5 files changed, 20 insertions(+), 27 deletions(-) diff --git a/novaclient/shell.py b/novaclient/shell.py index a7615b48c..c4a0c9ec3 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -145,7 +145,8 @@ def save(self, auth_token, management_url, tenant_id): def password(self): if self._validate_string(self.args.os_password): return self.args.os_password - verify_pass = utils.bool_from_str(utils.env("OS_VERIFY_PASSWORD")) + verify_pass = strutils.bool_from_string( + utils.env("OS_VERIFY_PASSWORD", default=False), True) return self._prompt_password(verify_pass) @property @@ -242,7 +243,8 @@ def get_base_parser(self): help="Print debugging output") parser.add_argument('--os-cache', - default=utils.bool_from_str(utils.env('OS_CACHE', default=False)), + default=strutils.bool_from_string( + utils.env('OS_CACHE', default=False), True), action='store_true', help="Use the auth token cache. Defaults to False if env[OS_CACHE]" " is not set.") diff --git a/novaclient/utils.py b/novaclient/utils.py index 6e6406037..8a57255be 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -60,21 +60,6 @@ def add_arg(f, *args, **kwargs): f.arguments.insert(0, (args, kwargs)) -def bool_from_str(val): - """Convert a string representation of a bool into a bool value.""" - - if not val: - return False - try: - return bool(int(val)) - except ValueError: - if val.lower() in ['true', 'yes', 'y']: - return True - if val.lower() in ['false', 'no', 'n']: - return False - raise - - def add_resource_manager_extra_kwargs_hook(f, hook): """Add hook to bind CLI arguments to ResourceManager calls. diff --git a/novaclient/v1_1/flavors.py b/novaclient/v1_1/flavors.py index dfafdab3e..cfb06d1ed 100644 --- a/novaclient/v1_1/flavors.py +++ b/novaclient/v1_1/flavors.py @@ -18,7 +18,7 @@ from novaclient import base from novaclient import exceptions from novaclient.openstack.common.py3kcompat import urlutils -from novaclient import utils +from novaclient.openstack.common import strutils class Flavor(base.Resource): @@ -195,7 +195,7 @@ def create(self, name, ram, vcpus, disk, flavorid="auto", raise exceptions.CommandError("rxtx_factor must be a float.") try: - is_public = utils.bool_from_str(is_public) + is_public = strutils.bool_from_string(is_public, True) except Exception: raise exceptions.CommandError("is_public must be a boolean.") diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 0f7ffc241..dfa9c155d 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -652,7 +652,7 @@ def do_flavor_show(cs, args): @utils.arg('--is-public', metavar='', help="Make flavor accessible to the public (default true)", - type=utils.bool_from_str, + type=lambda v: strutils.bool_from_string(v, True), default=True) def do_flavor_create(cs, args): """Create a new flavor""" @@ -1076,7 +1076,8 @@ def do_image_delete(cs, args): nargs='?', type=int, const=1, - default=int(utils.bool_from_str(os.environ.get("ALL_TENANTS", 'false'))), + default=int(strutils.bool_from_string( + os.environ.get("ALL_TENANTS", 'false'), True)), help='Display information from all tenants (Admin only).') @utils.arg('--all_tenants', nargs='?', @@ -1612,7 +1613,8 @@ def _translate_availability_zone_keys(collection): nargs='?', type=int, const=1, - default=int(utils.bool_from_str(os.environ.get("ALL_TENANTS", 'false'))), + default=int(strutils.bool_from_string( + os.environ.get("ALL_TENANTS", 'false'), True)), help='Display information from all tenants (Admin only).') @utils.arg('--all_tenants', nargs='?', @@ -2234,7 +2236,8 @@ def do_secgroup_delete(cs, args): nargs='?', type=int, const=1, - default=int(utils.bool_from_str(os.environ.get("ALL_TENANTS", 'false'))), + default=int(strutils.bool_from_string( + os.environ.get("ALL_TENANTS", 'false'), True)), help='Display information from all tenants (Admin only).') @utils.arg('--all_tenants', nargs='?', diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 8eaf2cb1d..7648e62fe 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -512,7 +512,7 @@ def do_flavor_show(cs, args): @utils.arg('--is-public', metavar='', help="Make flavor accessible to the public (default true)", - type=utils.bool_from_str, + type=lambda v: strutils.bool_from_string(v, True), default=True) def do_flavor_create(cs, args): """Create a new flavor""" @@ -935,7 +935,8 @@ def do_image_delete(cs, args): nargs='?', type=int, const=1, - default=int(utils.bool_from_str(os.environ.get("ALL_TENANTS", 'false'))), + default=int(strutils.bool_from_string( + os.environ.get("ALL_TENANTS", 'false'), True)), help='Display information from all tenants (Admin only).') @utils.arg('--all_tenants', nargs='?', @@ -1441,7 +1442,8 @@ def _translate_availability_zone_keys(collection): nargs='?', type=int, const=1, - default=int(utils.bool_from_str(os.environ.get("ALL_TENANTS", 'false'))), + default=int(strutils.bool_from_string( + os.environ.get("ALL_TENANTS", 'false'), True)), help='Display information from all tenants (Admin only).') @utils.arg('--all_tenants', nargs='?', @@ -2036,7 +2038,8 @@ def do_secgroup_delete(cs, args): nargs='?', type=int, const=1, - default=int(utils.bool_from_str(os.environ.get("ALL_TENANTS", 'false'))), + default=int(strutils.bool_from_string( + os.environ.get("ALL_TENANTS", 'false'), True)), help='Display information from all tenants (Admin only).') @utils.arg('--all_tenants', nargs='?', From 5c62630a9d5142c391eff1117dd86c6a25f20254 Mon Sep 17 00:00:00 2001 From: Gary Kotton Date: Mon, 30 Dec 2013 07:37:35 -0800 Subject: [PATCH 0367/1705] Ensure that nova client prints dictionaries and arrays correctly Print the dictionaries and arrays without the unicode tags. The patch also updated tests that did not return valid data. Change-Id: Ia787f98a9510b68beb3ceaf00c285ca5c934f5c0 Closes-bug: #1265002 --- novaclient/openstack/common/importutils.py | 66 ++++++++ novaclient/openstack/common/jsonutils.py | 182 +++++++++++++++++++++ novaclient/tests/test_utils.py | 33 ++++ novaclient/tests/v1_1/fakes.py | 4 +- novaclient/utils.py | 11 +- openstack-common.conf | 1 + 6 files changed, 292 insertions(+), 5 deletions(-) create mode 100644 novaclient/openstack/common/importutils.py create mode 100644 novaclient/openstack/common/jsonutils.py diff --git a/novaclient/openstack/common/importutils.py b/novaclient/openstack/common/importutils.py new file mode 100644 index 000000000..4fd9ae2bc --- /dev/null +++ b/novaclient/openstack/common/importutils.py @@ -0,0 +1,66 @@ +# Copyright 2011 OpenStack Foundation. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Import related utilities and helper functions. +""" + +import sys +import traceback + + +def import_class(import_str): + """Returns a class from a string including module and class.""" + mod_str, _sep, class_str = import_str.rpartition('.') + try: + __import__(mod_str) + return getattr(sys.modules[mod_str], class_str) + except (ValueError, AttributeError): + raise ImportError('Class %s cannot be found (%s)' % + (class_str, + traceback.format_exception(*sys.exc_info()))) + + +def import_object(import_str, *args, **kwargs): + """Import a class and return an instance of it.""" + return import_class(import_str)(*args, **kwargs) + + +def import_object_ns(name_space, import_str, *args, **kwargs): + """Tries to import object from default namespace. + + Imports a class and return an instance of it, first by trying + to find the class in a default namespace, then failing back to + a full path if not found in the default namespace. + """ + import_value = "%s.%s" % (name_space, import_str) + try: + return import_class(import_value)(*args, **kwargs) + except ImportError: + return import_class(import_str)(*args, **kwargs) + + +def import_module(import_str): + """Import a module.""" + __import__(import_str) + return sys.modules[import_str] + + +def try_import(import_str, default=None): + """Try to import a module and if it fails return default.""" + try: + return import_module(import_str) + except ImportError: + return default diff --git a/novaclient/openstack/common/jsonutils.py b/novaclient/openstack/common/jsonutils.py new file mode 100644 index 000000000..5595d0b2e --- /dev/null +++ b/novaclient/openstack/common/jsonutils.py @@ -0,0 +1,182 @@ +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# Copyright 2011 Justin Santa Barbara +# All Rights Reserved. +# +# 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. + +''' +JSON related utilities. + +This module provides a few things: + + 1) A handy function for getting an object down to something that can be + JSON serialized. See to_primitive(). + + 2) Wrappers around loads() and dumps(). The dumps() wrapper will + automatically use to_primitive() for you if needed. + + 3) This sets up anyjson to use the loads() and dumps() wrappers if anyjson + is available. +''' + + +import datetime +import functools +import inspect +import itertools +import json +try: + import xmlrpclib +except ImportError: + # NOTE(jaypipes): xmlrpclib was renamed to xmlrpc.client in Python3 + # however the function and object call signatures + # remained the same. This whole try/except block should + # be removed and replaced with a call to six.moves once + # six 1.4.2 is released. See http://bit.ly/1bqrVzu + import xmlrpc.client as xmlrpclib + +import six + +from novaclient.openstack.common import gettextutils +from novaclient.openstack.common import importutils +from novaclient.openstack.common import timeutils + +netaddr = importutils.try_import("netaddr") + +_nasty_type_tests = [inspect.ismodule, inspect.isclass, inspect.ismethod, + inspect.isfunction, inspect.isgeneratorfunction, + inspect.isgenerator, inspect.istraceback, inspect.isframe, + inspect.iscode, inspect.isbuiltin, inspect.isroutine, + inspect.isabstract] + +_simple_types = (six.string_types + six.integer_types + + (type(None), bool, float)) + + +def to_primitive(value, convert_instances=False, convert_datetime=True, + level=0, max_depth=3): + """Convert a complex object into primitives. + + Handy for JSON serialization. We can optionally handle instances, + but since this is a recursive function, we could have cyclical + data structures. + + To handle cyclical data structures we could track the actual objects + visited in a set, but not all objects are hashable. Instead we just + track the depth of the object inspections and don't go too deep. + + Therefore, convert_instances=True is lossy ... be aware. + + """ + # handle obvious types first - order of basic types determined by running + # full tests on nova project, resulting in the following counts: + # 572754 + # 460353 + # 379632 + # 274610 + # 199918 + # 114200 + # 51817 + # 26164 + # 6491 + # 283 + # 19 + if isinstance(value, _simple_types): + return value + + if isinstance(value, datetime.datetime): + if convert_datetime: + return timeutils.strtime(value) + else: + return value + + # value of itertools.count doesn't get caught by nasty_type_tests + # and results in infinite loop when list(value) is called. + if type(value) == itertools.count: + return six.text_type(value) + + # FIXME(vish): Workaround for LP bug 852095. Without this workaround, + # tests that raise an exception in a mocked method that + # has a @wrap_exception with a notifier will fail. If + # we up the dependency to 0.5.4 (when it is released) we + # can remove this workaround. + if getattr(value, '__module__', None) == 'mox': + return 'mock' + + if level > max_depth: + return '?' + + # The try block may not be necessary after the class check above, + # but just in case ... + try: + recursive = functools.partial(to_primitive, + convert_instances=convert_instances, + convert_datetime=convert_datetime, + level=level, + max_depth=max_depth) + if isinstance(value, dict): + return dict((k, recursive(v)) for k, v in six.iteritems(value)) + elif isinstance(value, (list, tuple)): + return [recursive(lv) for lv in value] + + # It's not clear why xmlrpclib created their own DateTime type, but + # for our purposes, make it a datetime type which is explicitly + # handled + if isinstance(value, xmlrpclib.DateTime): + value = datetime.datetime(*tuple(value.timetuple())[:6]) + + if convert_datetime and isinstance(value, datetime.datetime): + return timeutils.strtime(value) + elif isinstance(value, gettextutils.Message): + return value.data + elif hasattr(value, 'iteritems'): + return recursive(dict(value.iteritems()), level=level + 1) + elif hasattr(value, '__iter__'): + return recursive(list(value)) + elif convert_instances and hasattr(value, '__dict__'): + # Likely an instance of something. Watch for cycles. + # Ignore class member vars. + return recursive(value.__dict__, level=level + 1) + elif netaddr and isinstance(value, netaddr.IPAddress): + return six.text_type(value) + else: + if any(test(value) for test in _nasty_type_tests): + return six.text_type(value) + return value + except TypeError: + # Class objects are tricky since they may define something like + # __iter__ defined but it isn't callable as list(). + return six.text_type(value) + + +def dumps(value, default=to_primitive, **kwargs): + return json.dumps(value, default=default, **kwargs) + + +def loads(s): + return json.loads(s) + + +def load(s): + return json.load(s) + + +try: + import anyjson +except ImportError: + pass +else: + anyjson._modules.append((__name__, 'dumps', TypeError, + 'loads', ValueError, 'load')) + anyjson.force_implementation(__name__) diff --git a/novaclient/tests/test_utils.py b/novaclient/tests/test_utils.py index bc63c95c9..d85175591 100644 --- a/novaclient/tests/test_utils.py +++ b/novaclient/tests/test_utils.py @@ -177,6 +177,39 @@ def test_print_list_sort_by_none(self): '| k2 | 2 |\n' '+------+-------+\n') + @mock.patch('sys.stdout', six.StringIO()) + def test_print_dict_dictionary(self): + dict = {'k': {'foo': 'bar'}} + utils.print_dict(dict) + self.assertEqual(sys.stdout.getvalue(), + '+----------+----------------+\n' + '| Property | Value |\n' + '+----------+----------------+\n' + '| k | {"foo": "bar"} |\n' + '+----------+----------------+\n') + + @mock.patch('sys.stdout', six.StringIO()) + def test_print_dict_list_dictionary(self): + dict = {'k': [{'foo': 'bar'}]} + utils.print_dict(dict) + self.assertEqual(sys.stdout.getvalue(), + '+----------+------------------+\n' + '| Property | Value |\n' + '+----------+------------------+\n' + '| k | [{"foo": "bar"}] |\n' + '+----------+------------------+\n') + + @mock.patch('sys.stdout', six.StringIO()) + def test_print_dict_list(self): + dict = {'k': ['foo', 'bar']} + utils.print_dict(dict) + self.assertEqual(sys.stdout.getvalue(), + '+----------+----------------+\n' + '| Property | Value |\n' + '+----------+----------------+\n' + '| k | ["foo", "bar"] |\n' + '+----------+----------------+\n') + class FlattenTestCase(test_utils.TestCase): def test_flattening(self): diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index a6699c0b1..9dbf8ebff 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -1944,7 +1944,7 @@ def get_os_cells_child_cell(self, **kw): 'username': 'cell1_user', 'name': 'cell1', 'rpc_host': '10.0.1.10', - '_info': { + 'info': { 'username': 'cell1_user', 'rpc_host': '10.0.1.10', 'type': 'child', @@ -1953,7 +1953,7 @@ def get_os_cells_child_cell(self, **kw): }, 'type': 'child', 'rpc_port': 5673, - '_loaded': True + 'loaded': True }} return (200, {}, cell) diff --git a/novaclient/utils.py b/novaclient/utils.py index 6e6406037..39e713ca5 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -23,6 +23,7 @@ import six from novaclient import exceptions +from novaclient.openstack.common import jsonutils from novaclient.openstack.common import strutils @@ -237,8 +238,8 @@ def print_dict(d, dict_property="Property", dict_value="Value", wrap=0): pt.align = 'l' for k, v in sorted(d.items()): # convert dict to str to check length - if isinstance(v, dict): - v = str(v) + if isinstance(v, (dict, list)): + v = jsonutils.dumps(v) if wrap > 0: v = textwrap.fill(str(v), wrap) # if value has a newline, add in multiple rows @@ -251,7 +252,11 @@ def print_dict(d, dict_property="Property", dict_value="Value", wrap=0): col1 = '' else: pt.add_row([k, v]) - print(strutils.safe_encode(pt.get_string())) + + result = strutils.safe_encode(pt.get_string()) + if six.PY3: + result = result.decode() + print(result) def find_resource(manager, name_or_id, **find_args): diff --git a/openstack-common.conf b/openstack-common.conf index 8b60c5b5d..e7fe529f1 100644 --- a/openstack-common.conf +++ b/openstack-common.conf @@ -2,6 +2,7 @@ # The list of modules to copy from openstack-common module=install_venv_common +module=jsonutils module=strutils module=timeutils module=uuidutils From 3582d6f1be6ee652abdc6ee7e8896fade240ffb1 Mon Sep 17 00:00:00 2001 From: JUN JIE NAN Date: Wed, 18 Dec 2013 16:04:58 +0800 Subject: [PATCH 0368/1705] Generate interfaces reference doc Add gen_ref in conf.py to generate reference doc to avoid maintaining these boilerplates. Change-Id: I6683c41d39685dcb7f7a619a36210a987c9e9825 --- doc/.gitignore | 1 + doc/source/api.rst | 1 + doc/source/conf.py | 42 +++++++++++++++++++++++++++ doc/source/index.rst | 1 + doc/source/ref/exceptions.rst | 8 ------ doc/source/ref/flavors.rst | 35 ----------------------- doc/source/ref/images.rst | 54 ----------------------------------- doc/source/ref/index.rst | 10 ------- doc/source/ref/servers.rst | 13 --------- 9 files changed, 45 insertions(+), 120 deletions(-) delete mode 100644 doc/source/ref/exceptions.rst delete mode 100644 doc/source/ref/flavors.rst delete mode 100644 doc/source/ref/images.rst delete mode 100644 doc/source/ref/index.rst delete mode 100644 doc/source/ref/servers.rst diff --git a/doc/.gitignore b/doc/.gitignore index 567609b12..8e0be80f5 100644 --- a/doc/.gitignore +++ b/doc/.gitignore @@ -1 +1,2 @@ build/ +source/ref/ diff --git a/doc/source/api.rst b/doc/source/api.rst index 78c0a5c32..4ba9631c9 100644 --- a/doc/source/api.rst +++ b/doc/source/api.rst @@ -43,3 +43,4 @@ For more information, see the reference: :maxdepth: 2 ref/index + ref/v1_1/index diff --git a/doc/source/conf.py b/doc/source/conf.py index ee0b6837f..a4f48bd02 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -26,6 +26,48 @@ sys.path.insert(0, ROOT) sys.path.insert(0, BASE_DIR) + +def gen_ref(ver, title, names): + refdir = os.path.join(BASE_DIR, "ref") + pkg = "novaclient" + if ver: + pkg = "%s.%s" % (pkg, ver) + refdir = os.path.join(refdir, ver) + if not os.path.exists(refdir): + os.makedirs(refdir) + idxpath = os.path.join(refdir, "index.rst") + with open(idxpath, "w") as idx: + idx.write(("%(title)s\n" + "%(signs)s\n" + "\n" + ".. toctree::\n" + " :maxdepth: 1\n" + "\n") % {"title": title, "signs": "=" * len(title)}) + for name in names: + idx.write(" %s\n" % name) + rstpath = os.path.join(refdir, "%s.rst" % name) + with open(rstpath, "w") as rst: + rst.write(("%(title)s\n" + "%(signs)s\n" + "\n" + ".. automodule:: %(pkg)s.%(name)s\n" + " :members:\n" + " :undoc-members:\n" + " :show-inheritance:\n" + " :noindex:\n") + % {"title": name.capitalize(), + "signs": "=" * len(name), + "pkg": pkg, "name": name}) + +gen_ref(None, "Exceptions", ["exceptions"]) +gen_ref("v1_1", "Version 1.1, Version 2 API Reference", + ["flavors", "images", "servers", "hosts", "agents", "aggregates", + "availability_zones", "certs", "fixed_ips", "floating_ip_pools", + "floating_ips", "hypervisors", "keypairs", "limits", "networks", + "quota_classes", "quotas", "security_group_rules", + "security_groups", "services", "virtual_interfaces", + "volume_snapshots", "volumes", "volume_types"]) + # -- General configuration ---------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be diff --git a/doc/source/index.rst b/doc/source/index.rst index 00d1a81ea..c99841988 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -25,6 +25,7 @@ Contents: shell api ref/index + ref/v1_1/index releases Contributing diff --git a/doc/source/ref/exceptions.rst b/doc/source/ref/exceptions.rst deleted file mode 100644 index 5f593c73e..000000000 --- a/doc/source/ref/exceptions.rst +++ /dev/null @@ -1,8 +0,0 @@ -Exceptions -========== - -.. automodule:: novaclient.exceptions - :members: - :undoc-members: - :show-inheritance: - :noindex: diff --git a/doc/source/ref/flavors.rst b/doc/source/ref/flavors.rst deleted file mode 100644 index 1e04fe752..000000000 --- a/doc/source/ref/flavors.rst +++ /dev/null @@ -1,35 +0,0 @@ -Flavors -======= - -From Rackspace's API documentation: - - A flavor is an available hardware configuration for a server. Each flavor - has a unique combination of disk space, memory capacity and priority for - CPU time. - -Classes -------- - -.. currentmodule:: novaclient.v1_1.flavors - -.. autoclass:: FlavorManager - :members: get, list, find, findall - -.. autoclass:: Flavor - :members: - - .. attribute:: id - - This flavor's ID. - - .. attribute:: name - - A human-readable name for this flavor. - - .. attribute:: ram - - The amount of RAM this flavor has, in MB. - - .. attribute:: disk - - The amount of disk space this flavor has, in MB diff --git a/doc/source/ref/images.rst b/doc/source/ref/images.rst deleted file mode 100644 index 6f58c0ae1..000000000 --- a/doc/source/ref/images.rst +++ /dev/null @@ -1,54 +0,0 @@ -Images -====== - -.. currentmodule:: novaclient.v1_1.images - -An "image" is a snapshot from which you can create new server instances. - -From Rackspace's own API documentation: - - An image is a collection of files used to create or rebuild a server. - Rackspace provides a number of pre-built OS images by default. You may - also create custom images from cloud servers you have launched. These - custom images are useful for backup purposes or for producing "gold" - server images if you plan to deploy a particular server configuration - frequently. - -Classes -------- - -.. autoclass:: ImageManager - :members: get, list, find, findall, delete - -.. autoclass:: Image - :members: delete - - .. attribute:: id - - This image's ID. - - .. attribute:: name - - This image's name. - - .. attribute:: created - - The date/time this image was created. - - .. attribute:: updated - - The date/time this instance was updated. - - .. attribute:: status - - The status of this image (usually ``"SAVING"`` or ``ACTIVE``). - - .. attribute:: progress - - During saving of an image this'll be set to something between - 0 and 100, representing a rough percentage done. - - .. attribute:: serverId - - If this image was created from a :class:`Server` then this attribute - will be set to the ID of the server whence this image came. diff --git a/doc/source/ref/index.rst b/doc/source/ref/index.rst deleted file mode 100644 index 2db23af4c..000000000 --- a/doc/source/ref/index.rst +++ /dev/null @@ -1,10 +0,0 @@ -Version 1.1, Version 2 API Reference -======================================= - -.. toctree:: - :maxdepth: 1 - - exceptions - flavors - images - servers diff --git a/doc/source/ref/servers.rst b/doc/source/ref/servers.rst deleted file mode 100644 index 3a8e723d1..000000000 --- a/doc/source/ref/servers.rst +++ /dev/null @@ -1,13 +0,0 @@ -Servers -======= - -A virtual machine instance. - -Classes -------- - -.. automodule:: novaclient.v1_1.servers - :members: - :undoc-members: - :show-inheritance: - :noindex: From 25041fe6fa18c78fe0216423c307af65f1b2006c Mon Sep 17 00:00:00 2001 From: JUN JIE NAN Date: Sat, 21 Dec 2013 11:28:12 +0800 Subject: [PATCH 0369/1705] Enable pep8 check for config.py in doc Directory doc is excluded from pep8 check in tox.ini, since we modified file doc/source/config.py, we should enable the check. Change-Id: I057f1e6cb1c5f9a6a07043056078e4475f66f288 --- doc/source/conf.py | 11 +++++++++++ tox.ini | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index a4f48bd02..b3f54b607 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -1,4 +1,15 @@ # -*- coding: utf-8 -*- +# 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. # # python-novaclient documentation build configuration file, created by # sphinx-quickstart on Sun Dec 6 14:19:25 2009. diff --git a/tox.ini b/tox.ini index d7d2b3234..be96a7984 100644 --- a/tox.ini +++ b/tox.ini @@ -30,4 +30,4 @@ downloadcache = ~/cache/pip [flake8] ignore = E12,F841,F811,F821,H302,H404 show-source = True -exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build +exclude=.venv,.git,.tox,dist,*openstack/common*,*lib/python*,*egg,build From 77fe8288700e07ae9fd23dfe12ca60fbd02e41ec Mon Sep 17 00:00:00 2001 From: JUN JIE NAN Date: Wed, 18 Dec 2013 16:29:50 +0800 Subject: [PATCH 0370/1705] Added v3 interfaces in reference doc Added hosts, agents, flavors and so on into reference doc Change-Id: I32198a0e65551f6ea3c67b72a8d7c6dff7da9f91 --- doc/source/api.rst | 1 + doc/source/conf.py | 4 ++++ doc/source/index.rst | 1 + 3 files changed, 6 insertions(+) diff --git a/doc/source/api.rst b/doc/source/api.rst index 4ba9631c9..305ff8801 100644 --- a/doc/source/api.rst +++ b/doc/source/api.rst @@ -44,3 +44,4 @@ For more information, see the reference: ref/index ref/v1_1/index + ref/v3/index diff --git a/doc/source/conf.py b/doc/source/conf.py index b3f54b607..30945d2b3 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -78,6 +78,10 @@ def gen_ref(ver, title, names): "quota_classes", "quotas", "security_group_rules", "security_groups", "services", "virtual_interfaces", "volume_snapshots", "volumes", "volume_types"]) +gen_ref("v3", "Version 3 API Reference", + ["flavors", "hosts", "agents", "aggregates", "availability_zones", + "certs", "hypervisors", "images", "keypairs", "quotas", + "quotas_classes", "servers", "services"]) # -- General configuration ---------------------------------------------------- diff --git a/doc/source/index.rst b/doc/source/index.rst index c99841988..c17d19aac 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -26,6 +26,7 @@ Contents: api ref/index ref/v1_1/index + ref/v3/index releases Contributing From 3dbab161bed7659eb976f0961ae5226da34b16f9 Mon Sep 17 00:00:00 2001 From: Chris Buccella Date: Sat, 4 Jan 2014 22:56:09 +0000 Subject: [PATCH 0371/1705] Code cleanup: use oslo's to_slug() instead of slugify() The bash completion code is the sole user of the slugify() function in utils, which is substantially similar to to_slug() provided in strutils from oslo. Change-Id: Ib4eb7e4c0fa0e9bc5c4a0856f6391911d8f8fd3b Closes-bug: #1266118 --- novaclient/base.py | 2 +- novaclient/utils.py | 22 ---------------------- 2 files changed, 1 insertion(+), 23 deletions(-) diff --git a/novaclient/base.py b/novaclient/base.py index 38434c282..517d13779 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -442,7 +442,7 @@ def human_id(self): for bash completion. """ if self.NAME_ATTR in self.__dict__ and self.HUMAN_ID: - return utils.slugify(getattr(self, self.NAME_ATTR)) + return strutils.to_slug(getattr(self, self.NAME_ATTR)) return None def _add_details(self, info): diff --git a/novaclient/utils.py b/novaclient/utils.py index e7e848173..7668ebfd0 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -14,7 +14,6 @@ import json import os import pkg_resources -import re import sys import textwrap import uuid @@ -381,27 +380,6 @@ def import_class(import_str): __import__(mod_str) return getattr(sys.modules[mod_str], class_str) -_slugify_strip_re = re.compile(r'[^\w\s-]') -_slugify_hyphenate_re = re.compile(r'[-\s]+') - - -# http://code.activestate.com/recipes/ -# 577257-slugify-make-a-string-usable-in-a-url-or-filename/ -def slugify(value): - """ - Normalizes string, converts to lowercase, removes non-alpha characters, - and converts spaces to hyphens. - - From Django's "django/template/defaultfilters.py". - """ - import unicodedata - if not isinstance(value, six.text_type): - value = six.text_type(value) - value = unicodedata.normalize('NFKD', value).encode('ascii', - 'ignore').decode("ascii") - value = six.text_type(_slugify_strip_re.sub('', value).strip().lower()) - return _slugify_hyphenate_re.sub('-', value) - def _load_entry_point(ep_name, name=None): """Try to load the entry point ep_name that matches name.""" From 875cf42fffc6f08e84d28a896c966deaa1ab5089 Mon Sep 17 00:00:00 2001 From: Gary Kotton Date: Mon, 6 Jan 2014 01:26:58 -0800 Subject: [PATCH 0372/1705] Ensure that the diagnostics are user friendly In the case when the value is a dictionary or array or longer than the line then the diagnostics output will automatically wrap around to ensure that the output is user friendly. Part of the blueprint v3-diagnostics Change-Id: Ia158fd99aeb0e6296fb232e881d0f01a13407dfc Closes-bug: #1266402 --- novaclient/tests/test_utils.py | 25 +++++++++++++++++++++++++ novaclient/v1_1/shell.py | 2 +- novaclient/v3/shell.py | 2 +- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/novaclient/tests/test_utils.py b/novaclient/tests/test_utils.py index d85175591..489be359a 100644 --- a/novaclient/tests/test_utils.py +++ b/novaclient/tests/test_utils.py @@ -125,6 +125,31 @@ def __init__(self, name, value): class PrintResultTestCase(test_utils.TestCase): + @mock.patch('sys.stdout', six.StringIO()) + def test_print_dict(self): + dict = {'key': 'value'} + utils.print_dict(dict) + self.assertEqual(sys.stdout.getvalue(), + '+----------+-------+\n' + '| Property | Value |\n' + '+----------+-------+\n' + '| key | value |\n' + '+----------+-------+\n') + + @mock.patch('sys.stdout', six.StringIO()) + def test_print_dict_wrap(self): + dict = {'key1': 'not wrapped', + 'key2': 'this will be wrapped'} + utils.print_dict(dict, wrap=16) + self.assertEqual(sys.stdout.getvalue(), + '+----------+--------------+\n' + '| Property | Value |\n' + '+----------+--------------+\n' + '| key1 | not wrapped |\n' + '| key2 | this will be |\n' + '| | wrapped |\n' + '+----------+--------------+\n') + @mock.patch('sys.stdout', six.StringIO()) def test_print_list_sort_by_str(self): objs = [_FakeResult("k1", 1), diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 0f7ffc241..806839c29 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1360,7 +1360,7 @@ def do_unshelve(cs, args): def do_diagnostics(cs, args): """Retrieve server diagnostics.""" server = _find_server(cs, args.server) - utils.print_dict(cs.servers.diagnostics(server)[1]) + utils.print_dict(cs.servers.diagnostics(server)[1], wrap=80) @utils.arg('server', metavar='', help='Name or ID of server.') diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 8eaf2cb1d..65d80ca5e 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -1201,7 +1201,7 @@ def do_unrescue(cs, args): def do_diagnostics(cs, args): """Retrieve server diagnostics.""" server = _find_server(cs, args.server) - utils.print_dict(cs.servers.diagnostics(server)[1]) + utils.print_dict(cs.servers.diagnostics(server)[1], wrap=80) @utils.arg('server', metavar='', help='Name or ID of server.') From d664f0f672b3989b43586d6b88949c4133b9ba24 Mon Sep 17 00:00:00 2001 From: Ken'ichi Ohmichi Date: Tue, 7 Jan 2014 03:46:11 +0900 Subject: [PATCH 0373/1705] Remove class_name parameter from quota_class class_name parameter is not used in both Nova v1.1/v3 API. Nova considers the part of url as class_name. For example, bar is considered as class_name in the following case: curl -i 'http://localhost:8774/v3/os-quota-class-sets/bar' -X PUT .. This patch removes this unused paramter from quota_class. Change-Id: Id4eba5b2a17506bc04ea23ef1097ef9cdb9caf8a --- novaclient/tests/v1_1/fakes.py | 4 ---- novaclient/v1_1/quota_classes.py | 1 - 2 files changed, 5 deletions(-) diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index a6699c0b1..e2a1cfbf5 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -1171,8 +1171,6 @@ def get_os_quota_class_sets_test(self, **kw): def put_os_quota_class_sets_test(self, body, **kw): assert list(body) == ['quota_class_set'] - fakes.assert_has_keys(body['quota_class_set'], - required=['class_name']) return (200, {}, {'quota_class_set': { 'class_name': 'test', 'metadata_items': [], @@ -1192,8 +1190,6 @@ def put_os_quota_class_sets_test(self, body, **kw): def put_os_quota_class_sets_97f4c221bff44578b0300df4ef119353(self, body, **kw): assert list(body) == ['quota_class_set'] - fakes.assert_has_keys(body['quota_class_set'], - required=['class_name']) return (200, {}, {'quota_class_set': { 'class_name': '97f4c221bff44578b0300df4ef119353', 'metadata_items': [], diff --git a/novaclient/v1_1/quota_classes.py b/novaclient/v1_1/quota_classes.py index 50314efd9..a37632c13 100644 --- a/novaclient/v1_1/quota_classes.py +++ b/novaclient/v1_1/quota_classes.py @@ -40,7 +40,6 @@ def _update_body(self, **kwargs): return {'quota_class_set': kwargs} def update(self, class_name, **kwargs): - kwargs['class_name'] = class_name body = self._update_body(**kwargs) for key in list(body['quota_class_set']): From f196ed01763aa9fe16d1bd724d00ca44fda01d49 Mon Sep 17 00:00:00 2001 From: Rick Harris Date: Tue, 7 Jan 2014 01:40:03 -0600 Subject: [PATCH 0374/1705] Fix tab-completion of --flags under OS X --flags were not tab-completing under OS X. This was due to the whitespace collapsing regular expression using the '+' token (meaning one-or-more), which isn't supported in OS X's version of sed. Using the more portable ' *' instead of '[ ]+' fixes this. Additional cleanups * Remove unecessary embedding of tabs in the file (nova bash-complete doesn't emit tabs, so the regular expressions don't have to handle them) * Restore logic to exclude -h from tab-completion (was lost in last tab-completion cleanup) * Add similar exclude logic for -i Fixes bug 1266667 Change-Id: I7e1fe8382d9b5295d0bbc1cde2b89550d5a4e21c --- tools/nova.bash_completion | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/nova.bash_completion b/tools/nova.bash_completion index fefa79831..3c58d3493 100644 --- a/tools/nova.bash_completion +++ b/tools/nova.bash_completion @@ -9,10 +9,10 @@ _nova() prev="${COMP_WORDS[COMP_CWORD-1]}" if [ "x$_nova_opts" == "x" ] ; then - nbc="`nova bash-completion | sed -e "s/[ ]+/ /"`" - _nova_opts="`echo "$nbc" | sed -e "s/--[a-z0-9_-]*//g" -e "s/[ ]+/ /g"`" - _nova_flags="`echo " $nbc" | sed -e "s/ [^-][^-][a-z0-9_-]*//g" -e "s/[ ]+/ /g"`" - _nova_opts_exp="`echo "$_nova_opts" | sed -e "s/[ ]/|/g"`" + nbc="`nova bash-completion | sed -e "s/ *-h */ /" -e "s/ *-i */ /"`" + _nova_opts="`echo "$nbc" | sed -e "s/--[a-z0-9_-]*//g" -e "s/ */ /g"`" + _nova_flags="`echo " $nbc" | sed -e "s/ [^-][^-][a-z0-9_-]*//g" -e "s/ */ /g"`" + _nova_opts_exp="`echo "$_nova_opts" | tr ' ' '|'`" fi if [[ " ${COMP_WORDS[@]} " =~ " "($_nova_opts_exp)" " && "$prev" != "help" ]] ; then From db6c58b009e561fd4191b0e47ad18ce53d7b4dcc Mon Sep 17 00:00:00 2001 From: Chris Yeoh Date: Tue, 17 Dec 2013 21:46:58 +1030 Subject: [PATCH 0375/1705] Adds quota usage support for the V3 API Adds the ability to do a detailed query from the os-quota-sets V3 API extension. This was an interface added in V3 to allow for the querying of current quota usage. Adds a shell command quota-usage which uses this interface to show current quota usage as well as the limit. The absolute limits response was removed from the limits extension in a9e29b7e9fbe14ab42f24802a6d2a457a6317ba3 and so the corresponding novaclient command is also removed. The "quota-show-usage" command now shows the same information. Differences between the V2 and V3 API are described here: https://wiki.openstack.org/wiki/NovaAPIv2tov3 Partially implements blueprint v3-api Change-Id: I5db72d42120f4ad7a86fbfce20382988f6bbf5d3 --- novaclient/tests/v3/fakes.py | 6 +++ novaclient/tests/v3/test_quotas.py | 12 ++++++ novaclient/v3/quotas.py | 15 ++++++++ novaclient/v3/shell.py | 59 +++++++++++++++++------------- 4 files changed, 67 insertions(+), 25 deletions(-) diff --git a/novaclient/tests/v3/fakes.py b/novaclient/tests/v3/fakes.py index 72422f5cf..4291b13cb 100644 --- a/novaclient/tests/v3/fakes.py +++ b/novaclient/tests/v3/fakes.py @@ -293,6 +293,12 @@ def put_os_quota_sets_97f4c221bff44578b0300df4ef119353(self, body, **kw): 'security_groups': 1, 'security_group_rules': 1}}) + def get_os_quota_sets_test_detail(self, **kw): + return (200, {}, {'quota_set': { + 'cores': {'reserved': 0, 'in_use': 0, 'limit': 10}, + 'instances': {'reserved': 0, 'in_use': 4, 'limit': 50}, + 'ram': {'reserved': 0, 'in_use': 1024, 'limit': 51200}}}) + # # Hypervisors # diff --git a/novaclient/tests/v3/test_quotas.py b/novaclient/tests/v3/test_quotas.py index 0f361a49f..1e4eef19b 100644 --- a/novaclient/tests/v3/test_quotas.py +++ b/novaclient/tests/v3/test_quotas.py @@ -31,3 +31,15 @@ def test_force_update_quota(self): 'PUT', '/os-quota-sets/97f4c221bff44578b0300df4ef119353', {'quota_set': {'force': True, 'cores': 2}}) + + def test_tenant_quotas_get_detail(self): + tenant_id = 'test' + self.cs.quotas.get(tenant_id, detail=True) + self.cs.assert_called('GET', '/os-quota-sets/%s/detail' % tenant_id) + + def test_user_quotas_get_detail(self): + tenant_id = 'test' + user_id = 'fake_user' + self.cs.quotas.get(tenant_id, user_id=user_id, detail=True) + url = '/os-quota-sets/%s/detail?user_id=%s' % (tenant_id, user_id) + self.cs.assert_called('GET', url) diff --git a/novaclient/v3/quotas.py b/novaclient/v3/quotas.py index 19723fafa..b55842daa 100644 --- a/novaclient/v3/quotas.py +++ b/novaclient/v3/quotas.py @@ -23,5 +23,20 @@ class QuotaSet(quotas.QuotaSet): class QuotaSetManager(quotas.QuotaSetManager): resource_class = QuotaSet + def get(self, tenant_id, user_id=None, detail=False): + if detail: + detail_string = '/detail' + else: + detail_string = '' + + if hasattr(tenant_id, 'tenant_id'): + tenant_id = tenant_id.tenant_id + if user_id: + url = '/os-quota-sets/%s%s?user_id=%s' % (tenant_id, detail_string, + user_id) + else: + url = '/os-quota-sets/%s%s' % (tenant_id, detail_string) + return self._get(url, "quota_set") + def _update_body(self, tenant_id, **kwargs): return {'quota_set': kwargs} diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 0750b45f7..b183860b4 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -27,6 +27,8 @@ import sys import time +import six + from novaclient import exceptions from novaclient.openstack.common import strutils from novaclient.openstack.common import timeutils @@ -2198,31 +2200,6 @@ def do_keypair_show(cs, args): _print_keypair(keypair) -@utils.arg('--tenant', - #nova db searches by project_id - dest='tenant', - metavar='', - nargs='?', - help='Display information from single tenant (Admin only).') -@utils.arg('--reserved', - dest='reserved', - action='store_true', - default=False, - help='Include reservations count.') -def do_absolute_limits(cs, args): - """Print a list of absolute limits for a user""" - limits = cs.limits.get(args.reserved, args.tenant).absolute - columns = ['Name', 'Value'] - utils.print_list(limits, columns) - - -def do_rate_limits(cs, args): - """Print a list of rate limits for a user""" - limits = cs.limits.get().rate - columns = ['Verb', 'URI', 'Value', 'Remain', 'Unit', 'Next_Available'] - utils.print_list(limits, columns) - - @utils.arg('--start', metavar='', help='Usage range start date ex 2012-01-20 (default: 4 weeks ago)', default=None) @@ -2865,6 +2842,23 @@ def _quota_show(quotas): utils.print_dict(quota_dict) +def _quota_usage(quotas): + class QuotaObj(object): + def __init__(self, resource, quota_dict): + setattr(self, 'resource', resource) + for (k, v) in six.iteritems(quota_dict): + setattr(self, k, v) + + quota_list = [] + for resource in _quota_resources: + try: + quota_list.append(QuotaObj(resource, getattr(quotas, resource))) + except AttributeError: + pass + utils.print_list(quota_list, ['resource', 'in use', 'limit'], + sortby_index=0) + + def _quota_update(manager, identifier, args): updates = {} for resource in _quota_resources: @@ -2891,6 +2885,21 @@ def do_quota_show(cs, args): _quota_show(cs.quotas.get(args.tenant)) +@utils.arg('--tenant', + metavar='', + default=None, + help='ID of tenant to list the quotas for.') +@utils.arg('--user', + metavar='', + default=None, + help='ID of user to list the quotas for.') +def do_quota_usage(cs, args): + """List the quotas for a tenant.""" + + tenant = args.tenant or cs.client.tenant_id + _quota_usage(cs.quotas.get(tenant, user_id=args.user, detail=True)) + + @utils.arg('--tenant', metavar='', default=None, From 0ddcf117327b540b234414881beba28a0d9043e1 Mon Sep 17 00:00:00 2001 From: Sahid Orentino Ferdjaoui Date: Thu, 2 Jan 2014 09:26:19 +0100 Subject: [PATCH 0376/1705] Removes vim configuration headers - Removes vim headers: It's not needed to set tabstop tons of times, this can be done by setting vimrc. - I did not update files in common/* and install_venv_common.py because these files are sync with oslo. Note: http://lists.openstack.org/pipermail/openstack-dev/2013-October/017353.html Closes-Bug: #1265474 Change-Id: Ia09dc2c908187a756bf55eaba74655484304517d --- novaclient/tests/v1_1/contrib/test_migrations.py | 2 -- novaclient/tests/v1_1/contrib/test_tenant_networks.py | 2 -- novaclient/tests/v1_1/test_agents.py | 2 -- novaclient/tests/v1_1/test_coverage_ext.py | 2 -- novaclient/tests/v1_1/test_fixed_ips.py | 2 -- novaclient/tests/v1_1/test_floating_ips_bulk.py | 2 -- novaclient/tests/v1_1/test_fping.py | 2 -- novaclient/tests/v1_1/test_services.py | 2 -- novaclient/tests/v3/test_agents.py | 2 -- novaclient/tests/v3/test_services.py | 2 -- novaclient/v1_1/agents.py | 2 -- novaclient/v1_1/client.py | 1 - novaclient/v1_1/contrib/migrations.py | 2 -- novaclient/v1_1/fixed_ips.py | 2 -- novaclient/v1_1/floating_ips_bulk.py | 2 -- novaclient/v1_1/fping.py | 2 -- novaclient/v1_1/services.py | 2 -- novaclient/v3/agents.py | 2 -- novaclient/v3/client.py | 1 - novaclient/v3/services.py | 2 -- tools/install_venv.py | 2 -- 21 files changed, 40 deletions(-) diff --git a/novaclient/tests/v1_1/contrib/test_migrations.py b/novaclient/tests/v1_1/contrib/test_migrations.py index 2b4d4cb03..95356de9d 100644 --- a/novaclient/tests/v1_1/contrib/test_migrations.py +++ b/novaclient/tests/v1_1/contrib/test_migrations.py @@ -1,5 +1,3 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - # 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 diff --git a/novaclient/tests/v1_1/contrib/test_tenant_networks.py b/novaclient/tests/v1_1/contrib/test_tenant_networks.py index bb2cbaf8c..f2a077005 100644 --- a/novaclient/tests/v1_1/contrib/test_tenant_networks.py +++ b/novaclient/tests/v1_1/contrib/test_tenant_networks.py @@ -1,5 +1,3 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - # Copyright 2012 OpenStack Foundation # All Rights Reserved. # diff --git a/novaclient/tests/v1_1/test_agents.py b/novaclient/tests/v1_1/test_agents.py index 6671282b8..9c5c6d928 100644 --- a/novaclient/tests/v1_1/test_agents.py +++ b/novaclient/tests/v1_1/test_agents.py @@ -1,5 +1,3 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - # Copyright 2012 IBM Corp. # All Rights Reserved. # diff --git a/novaclient/tests/v1_1/test_coverage_ext.py b/novaclient/tests/v1_1/test_coverage_ext.py index de23ee7b4..8c9870d43 100644 --- a/novaclient/tests/v1_1/test_coverage_ext.py +++ b/novaclient/tests/v1_1/test_coverage_ext.py @@ -1,5 +1,3 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - # Copyright 2012 IBM Corp. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/novaclient/tests/v1_1/test_fixed_ips.py b/novaclient/tests/v1_1/test_fixed_ips.py index 550784c41..42e856c96 100644 --- a/novaclient/tests/v1_1/test_fixed_ips.py +++ b/novaclient/tests/v1_1/test_fixed_ips.py @@ -1,5 +1,3 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - # Copyright 2012 IBM Corp. # All Rights Reserved. # diff --git a/novaclient/tests/v1_1/test_floating_ips_bulk.py b/novaclient/tests/v1_1/test_floating_ips_bulk.py index 8ad2ac4a8..5b28f1b07 100644 --- a/novaclient/tests/v1_1/test_floating_ips_bulk.py +++ b/novaclient/tests/v1_1/test_floating_ips_bulk.py @@ -1,5 +1,3 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - # Copyright 2012 IBM Corp. # All Rights Reserved. # diff --git a/novaclient/tests/v1_1/test_fping.py b/novaclient/tests/v1_1/test_fping.py index 165caf57a..4760d5be1 100644 --- a/novaclient/tests/v1_1/test_fping.py +++ b/novaclient/tests/v1_1/test_fping.py @@ -1,5 +1,3 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - # Copyright 2012 OpenStack Foundation # All Rights Reserved. # diff --git a/novaclient/tests/v1_1/test_services.py b/novaclient/tests/v1_1/test_services.py index 8f9b7eb33..a9e1b07d3 100644 --- a/novaclient/tests/v1_1/test_services.py +++ b/novaclient/tests/v1_1/test_services.py @@ -1,5 +1,3 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - # Copyright 2012 IBM Corp. # All Rights Reserved. # diff --git a/novaclient/tests/v3/test_agents.py b/novaclient/tests/v3/test_agents.py index 55b482f7c..06534c3a5 100644 --- a/novaclient/tests/v3/test_agents.py +++ b/novaclient/tests/v3/test_agents.py @@ -1,5 +1,3 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - # Copyright 2012 IBM Corp. # All Rights Reserved. # diff --git a/novaclient/tests/v3/test_services.py b/novaclient/tests/v3/test_services.py index 2ad385489..0fd2fc0cf 100644 --- a/novaclient/tests/v3/test_services.py +++ b/novaclient/tests/v3/test_services.py @@ -1,5 +1,3 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - # Copyright 2012 IBM Corp. # All Rights Reserved. # diff --git a/novaclient/v1_1/agents.py b/novaclient/v1_1/agents.py index 51b8ff05a..a58675ade 100644 --- a/novaclient/v1_1/agents.py +++ b/novaclient/v1_1/agents.py @@ -1,5 +1,3 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - # Copyright 2012 IBM Corp. # All Rights Reserved. # diff --git a/novaclient/v1_1/client.py b/novaclient/v1_1/client.py index 247944510..a89581eac 100644 --- a/novaclient/v1_1/client.py +++ b/novaclient/v1_1/client.py @@ -1,4 +1,3 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright 2012 OpenStack Foundation # Copyright 2013 IBM Corp. # diff --git a/novaclient/v1_1/contrib/migrations.py b/novaclient/v1_1/contrib/migrations.py index 9322849aa..87fdd1930 100644 --- a/novaclient/v1_1/contrib/migrations.py +++ b/novaclient/v1_1/contrib/migrations.py @@ -1,5 +1,3 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - # 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 diff --git a/novaclient/v1_1/fixed_ips.py b/novaclient/v1_1/fixed_ips.py index fd8f3917a..4e7c0e951 100644 --- a/novaclient/v1_1/fixed_ips.py +++ b/novaclient/v1_1/fixed_ips.py @@ -1,5 +1,3 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - # Copyright 2012 IBM Corp. # All Rights Reserved. # diff --git a/novaclient/v1_1/floating_ips_bulk.py b/novaclient/v1_1/floating_ips_bulk.py index 1eeaaaa23..fb59a19e7 100644 --- a/novaclient/v1_1/floating_ips_bulk.py +++ b/novaclient/v1_1/floating_ips_bulk.py @@ -1,5 +1,3 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - # Copyright 2012 IBM Corp. # All Rights Reserved. # diff --git a/novaclient/v1_1/fping.py b/novaclient/v1_1/fping.py index 36ecac63a..ac958d4c6 100644 --- a/novaclient/v1_1/fping.py +++ b/novaclient/v1_1/fping.py @@ -1,5 +1,3 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - # Copyright 2012 OpenStack Foundation # All Rights Reserved. # diff --git a/novaclient/v1_1/services.py b/novaclient/v1_1/services.py index e37123601..45dafbc51 100644 --- a/novaclient/v1_1/services.py +++ b/novaclient/v1_1/services.py @@ -1,5 +1,3 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - # Copyright 2012 IBM Corp. # All Rights Reserved. # diff --git a/novaclient/v3/agents.py b/novaclient/v3/agents.py index 95e781508..f26bba3fa 100644 --- a/novaclient/v3/agents.py +++ b/novaclient/v3/agents.py @@ -1,5 +1,3 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - # Copyright 2012 IBM Corp. # All Rights Reserved. # diff --git a/novaclient/v3/client.py b/novaclient/v3/client.py index 0d8e8c3f7..16c5a33b3 100644 --- a/novaclient/v3/client.py +++ b/novaclient/v3/client.py @@ -1,4 +1,3 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright 2012 OpenStack Foundation # Copyright 2013 IBM Corp. # diff --git a/novaclient/v3/services.py b/novaclient/v3/services.py index 9415755b2..fe0f3a6a4 100644 --- a/novaclient/v3/services.py +++ b/novaclient/v3/services.py @@ -1,5 +1,3 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - # Copyright 2013 IBM Corp. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/tools/install_venv.py b/tools/install_venv.py index 0011a8be1..4d8feeae8 100644 --- a/tools/install_venv.py +++ b/tools/install_venv.py @@ -1,5 +1,3 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - # Copyright 2010 United States Government as represented by the # Administrator of the National Aeronautics and Space Administration. # All Rights Reserved. From 4f92f7ba02c5eb2d418de9e1b50030cf68acc6d7 Mon Sep 17 00:00:00 2001 From: Sahid Orentino Ferdjaoui Date: Tue, 7 Jan 2014 09:13:53 +0000 Subject: [PATCH 0377/1705] Using floating-ip-{associate|disassociate} This patch implements a blueprint to add more consistency into nova command, especially for the subcommand "floating-ip-*" Currently when we want to associate or disassociate an ip with nova we have to use add-floating-ip and remove-floating-ip. This is not consitent with the actual scheme of the subcommands: example: nova image-* nova flavor-* nova floating-ip-* + In the client v1.1 this patch displays a deprecated message when printing the help message for add-floating-ip and remove-floating-ip. + In the client v3 this patch do nothing because all floating ip commands are being removed. $:~/python-novaclient$ nova help | grep floating add-floating-ip DEPRECATED, use floating-ip-associate instead. floating-ip-associate Associate a floating IP address to a server. floating-ip-bulk-create Bulk create floating ips by range. floating-ip-bulk-delete Bulk delete floating ips by range. floating-ip-bulk-list List all floating ips. floating-ip-create Allocate a floating IP for the current tenant. floating-ip-delete De-allocate a floating IP. floating-ip-disassociate Remove a floating IP address from a server. floating-ip-list List floating ips for this tenant. floating-ip-pool-list List all floating ip pools. remove-floating-ip DEPRECATED, use floating-ip-disassociate instead. Implements: blueprint commands-floating-ip Change-Id: I5337d0f1ce5ec4826da6ecd2b6ae4ae7b97801e0 --- novaclient/tests/v1_1/test_shell.py | 10 ++++++++++ novaclient/v1_1/shell.py | 30 +++++++++++++++++++++++++++-- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index fa012a557..91f60c970 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -1054,6 +1054,16 @@ def test_server_floating_ip_remove(self): self.assert_called('POST', '/servers/1234/action', {'removeFloatingIp': {'address': '11.0.0.1'}}) + def test_server_floating_ip_associate(self): + self.run_command('floating-ip-associate sample-server 11.0.0.1') + self.assert_called('POST', '/servers/1234/action', + {'addFloatingIp': {'address': '11.0.0.1'}}) + + def test_server_floating_ip_disassociate(self): + self.run_command('floating-ip-disassociate sample-server 11.0.0.1') + self.assert_called('POST', '/servers/1234/action', + {'removeFloatingIp': {'address': '11.0.0.1'}}) + def test_usage_list(self): self.run_command('usage-list --start 2000-01-20 --end 2005-02-01') self.assert_called('GET', diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index e6b3160a3..62e3f5066 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1925,7 +1925,22 @@ def do_console_log(cs, args): default=None, help='Fixed IP Address to associate with.') def do_add_floating_ip(cs, args): - """Add a floating IP address to a server.""" + """DEPRECATED, use floating-ip-associate instead.""" + _associate_floating_ip(cs, args) + + +@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('address', metavar='
', help='IP Address.') +@utils.arg('--fixed-address', + metavar='', + default=None, + help='Fixed IP Address to associate with.') +def do_floating_ip_associate(cs, args): + """Associate a floating IP address to a server.""" + _associate_floating_ip(cs, args) + + +def _associate_floating_ip(cs, args): server = _find_server(cs, args.server) server.add_floating_ip(args.address, args.fixed_address) @@ -1933,7 +1948,18 @@ def do_add_floating_ip(cs, args): @utils.arg('server', metavar='', help='Name or ID of server.') @utils.arg('address', metavar='
', help='IP Address.') def do_remove_floating_ip(cs, args): - """Remove a floating IP address from a server.""" + """DEPRECATED, use floating-ip-disassociate instead.""" + _disassociate_floating_ip(cs, args) + + +@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('address', metavar='
', help='IP Address.') +def do_floating_ip_disassociate(cs, args): + """Disassociate a floating IP address from a server.""" + _disassociate_floating_ip(cs, args) + + +def _disassociate_floating_ip(cs, args): server = _find_server(cs, args.server) server.remove_floating_ip(args.address) From 0d00603f124e8d86871247ce419229329488dff8 Mon Sep 17 00:00:00 2001 From: gtt116 Date: Tue, 7 Jan 2014 04:16:06 +0000 Subject: [PATCH 0378/1705] Support list deleted servers for admin Nova api supported a parameter named 'deleted' which allow Admin to list deleted servers. This patch make novaclient support this. Change-Id: Ifc492fd0ba9d885e37c04c165d6397832ae6ebe0 Closes-Bug: #1266631 --- novaclient/tests/v1_1/test_shell.py | 4 ++++ novaclient/tests/v3/test_shell.py | 4 ++++ novaclient/v1_1/shell.py | 6 ++++++ novaclient/v3/shell.py | 6 ++++++ 4 files changed, 20 insertions(+) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index fa012a557..026ffc7c0 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -688,6 +688,10 @@ def test_list_minimal(self): self.run_command('list --minimal') self.assert_called('GET', '/servers') + def test_list_deleted(self): + self.run_command('list --deleted') + self.assert_called('GET', '/servers/detail?deleted=True') + def test_list_with_images(self): self.run_command('list --image 1') self.assert_called('GET', '/servers/detail?image=1') diff --git a/novaclient/tests/v3/test_shell.py b/novaclient/tests/v3/test_shell.py index 07eec1aa0..1c39ad7fd 100644 --- a/novaclient/tests/v3/test_shell.py +++ b/novaclient/tests/v3/test_shell.py @@ -78,6 +78,10 @@ def assert_called(self, method, url, body=None, **kwargs): def assert_called_anytime(self, method, url, body=None): return self.shell.cs.assert_called_anytime(method, url, body) + def test_list_deleted(self): + self.run_command('list --deleted') + self.assert_called('GET', '/servers/detail?deleted=True') + def test_aggregate_list(self): self.run_command('aggregate-list') self.assert_called('GET', '/os-aggregates') diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index e6b3160a3..31b0ac209 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1090,6 +1090,11 @@ def do_image_delete(cs, args): metavar='', nargs='?', help='Display information from single tenant (Admin only).') +@utils.arg('--deleted', + dest='deleted', + action="store_true", + default=False, + help='Only display deleted servers (Admin only).') @utils.arg('--fields', default=None, metavar='', @@ -1119,6 +1124,7 @@ def do_list(cs, args): 'status': args.status, 'tenant_id': args.tenant, 'host': args.host, + 'deleted': args.deleted, 'instance_name': args.instance_name} filters = {'flavor': lambda f: f['id'], diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 0750b45f7..e81da085c 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -954,6 +954,11 @@ def do_image_delete(cs, args): metavar='', help='Comma-separated list of fields to display. ' 'Use the show command to see which fields are available.') +@utils.arg('--deleted', + dest='deleted', + action="store_true", + default=False, + help='Only display deleted servers (Admin only).') @utils.arg('--minimal', dest='minimal', action="store_true", @@ -978,6 +983,7 @@ def do_list(cs, args): 'status': args.status, 'tenant_id': args.tenant, 'host': args.host, + 'deleted': args.deleted, 'instance_name': args.instance_name} filters = {'flavor': lambda f: f['id'], From 1627908b182ca9afd54ce695c820cd56d70a44df Mon Sep 17 00:00:00 2001 From: Shrirang Phadke Date: Tue, 7 Jan 2014 22:08:28 +0530 Subject: [PATCH 0379/1705] Fixes ambiguous cli output between "None" and NoneType Print hyphen(-) in python client output when attribute value is not set(NoneType) so that it is distinguishable between "None" string and NoneType value. Change-Id: I5ec0f39f13aca45fa95ddb59472c20207c63bf91 Closes-Bug: #1266414 --- novaclient/utils.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/novaclient/utils.py b/novaclient/utils.py index 7668ebfd0..6dcd3dec4 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -166,6 +166,8 @@ def print_list(objs, fields, formatters={}, sortby_index=None): else: field_name = field.lower().replace(' ', '_') data = getattr(o, field_name, '') + if data is None: + data = '-' row.append(data) pt.add_row(row) @@ -239,6 +241,8 @@ def print_dict(d, dict_property="Property", dict_value="Value", wrap=0): pt.add_row([col1, line]) col1 = '' else: + if v is None: + v = '-' pt.add_row([k, v]) print(strutils.safe_encode(pt.get_string())) From 61d88463da5eb65734edbbdad13378af6ce0f641 Mon Sep 17 00:00:00 2001 From: Chris Yeoh Date: Tue, 24 Dec 2013 00:28:13 +1030 Subject: [PATCH 0380/1705] Adds volume support for the V3 API Adds the ability to attach, detach and swap volumes on servers. There is no code shared with the v1_1 version because for V3 the volumes interface is completely different and the attach/detach/swap functionality is simply a server action rather than something accessed through a special resource. Partially implements blueprint v3-api Change-Id: Ib405f821fe557745d11cff9db08381fc15233fe5 --- novaclient/tests/v3/fakes.py | 5 ++- novaclient/tests/v3/test_volumes.py | 47 ++++++++++++++++++++ novaclient/v3/client.py | 2 + novaclient/v3/shell.py | 19 +++++++- novaclient/v3/volumes.py | 68 +++++++++++++++++++++++++++++ 5 files changed, 138 insertions(+), 3 deletions(-) create mode 100644 novaclient/tests/v3/test_volumes.py create mode 100644 novaclient/v3/volumes.py diff --git a/novaclient/tests/v3/fakes.py b/novaclient/tests/v3/fakes.py index 4291b13cb..772141411 100644 --- a/novaclient/tests/v3/fakes.py +++ b/novaclient/tests/v3/fakes.py @@ -188,7 +188,10 @@ def post_servers_1234_action(self, body, **kw): 'reset_state': ['state'], 'create_image': ['name', 'metadata'], 'migrate_live': ['host', 'block_migration', 'disk_over_commit'], - 'create_backup': ['name', 'backup_type', 'rotation']} + 'create_backup': ['name', 'backup_type', 'rotation'], + 'attach': ['volume_id', 'device'], + 'detach': ['volume_id'], + 'swap_volume_attachment': ['old_volume_id', 'new_volume_id']} assert len(body.keys()) == 1 action = list(body)[0] diff --git a/novaclient/tests/v3/test_volumes.py b/novaclient/tests/v3/test_volumes.py new file mode 100644 index 000000000..82f6d54a0 --- /dev/null +++ b/novaclient/tests/v3/test_volumes.py @@ -0,0 +1,47 @@ +# Copyright 2013 IBM Corp. +# All Rights Reserved. +# +# 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 novaclient.tests import utils +from novaclient.tests.v3 import fakes + + +class VolumesTest(utils.TestCase): + def setUp(self): + super(VolumesTest, self).setUp() + self.cs = self._get_fake_client() + + def _get_fake_client(self): + return fakes.FakeClient() + + def test_attach_server_volume(self): + v = self.cs.volumes.attach_server_volume( + server=1234, + volume_id='15e59938-07d5-11e1-90e3-e3dffe0c5983', + device='/dev/vdb' + ) + self.cs.assert_called('POST', '/servers/1234/action') + + def test_update_server_volume(self): + vol_id = '15e59938-07d5-11e1-90e3-e3dffe0c5983' + v = self.cs.volumes.update_server_volume( + server=1234, + old_volume_id='Work', + new_volume_id=vol_id + ) + self.cs.assert_called('POST', '/servers/1234/action') + + def test_delete_server_volume(self): + self.cs.volumes.delete_server_volume(1234, 'Work') + self.cs.assert_called('POST', '/servers/1234/action') diff --git a/novaclient/v3/client.py b/novaclient/v3/client.py index 0d8e8c3f7..07641d181 100644 --- a/novaclient/v3/client.py +++ b/novaclient/v3/client.py @@ -30,6 +30,7 @@ from novaclient.v3 import servers from novaclient.v3 import services from novaclient.v3 import usage +from novaclient.v3 import volumes class Client(object): @@ -80,6 +81,7 @@ def __init__(self, username, password, project_id, auth_url=None, self.servers = servers.ServerManager(self) self.services = services.ServiceManager(self) self.usage = usage.UsageManager(self) + self.volumes = volumes.VolumeManager(self) # Add in any extensions... if extensions: diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index b183860b4..2516086ed 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -1547,10 +1547,25 @@ def do_volume_attach(cs, args): if args.device == 'auto': args.device = None - volume = cs.volumes.create_server_volume(_find_server(cs, args.server).id, + volume = cs.volumes.attach_server_volume(_find_server(cs, args.server).id, args.volume, args.device) - _print_volume(volume) + + +@utils.arg('server', + metavar='', + help='Name or ID of server.') +@utils.arg('attachment_id', + metavar='', + help='Attachment ID of the volume.') +@utils.arg('new_volume', + metavar='', + help='ID of the volume to attach.') +def do_volume_update(cs, args): + """Update volume attachment.""" + volume = cs.volumes.update_server_volume(_find_server(cs, args.server).id, + args.attachment_id, + args.new_volume) @utils.arg('server', diff --git a/novaclient/v3/volumes.py b/novaclient/v3/volumes.py new file mode 100644 index 000000000..ec061a319 --- /dev/null +++ b/novaclient/v3/volumes.py @@ -0,0 +1,68 @@ +# Copyright 2013 IBM Corp. +# +# 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. + +""" +Volume interface +""" + +from novaclient import base + + +class VolumeManager(base.Manager): + """ + Manage :class:`Volume` resources. + """ + + def attach_server_volume(self, server, volume_id, device): + """ + Attach a volume identified by the volume ID to the given server ID + + :param server: The server (or it's ID) + :param volume_id: The ID of the volume to attach. + :param device: The device name + :rtype: :class:`Volume` + """ + body = {'volume_id': volume_id, 'device': device} + return self._action('attach', server, body) + + def update_server_volume(self, server, old_volume_id, new_volume_id): + """ + Update the volume identified by the attachment ID, that is attached to + the given server ID + + :param server_id: The server (or it's ID) + :param old_volume_id: The ID of the attachment + :param new_volume_id: The ID of the new volume to attach + :rtype: :class:`Volume` + """ + body = {'new_volume_id': new_volume_id, 'old_volume_id': old_volume_id} + return self._action('swap_volume_attachment', server, body) + + def delete_server_volume(self, server, volume_id): + """ + Detach a volume identified by the attachment ID from the given server + + :param server_id: The ID of the server + :param volume_id: The ID of the attachment + """ + return self._action('detach', server, {'volume_id': volume_id}) + + def _action(self, action, server, info=None, **kwargs): + """ + Perform a server "action" -- reboot/rebuild/resize/etc. + """ + body = {action: info} + self.run_hooks('modify_body_for_action', body, **kwargs) + url = '/servers/%s/action' % base.getid(server) + return self.api.client.post(url, body=body) From 3c9a3d8d32491814869f663cc40729bcb4e4687c Mon Sep 17 00:00:00 2001 From: Roman Rader Date: Thu, 9 Jan 2014 13:12:29 +0200 Subject: [PATCH 0381/1705] Don't slugify() None names If image has no name, human_id can't be built. Also slugify raises ValueError if None argument passed. It affects Docker images. Closes-Bug: #1267429 Closes-Bug: #1267130 Change-Id: Ib975775b441917eef2a650049cee9991d10c50d7 --- novaclient/base.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/novaclient/base.py b/novaclient/base.py index 517d13779..1a022b93a 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -442,7 +442,9 @@ def human_id(self): for bash completion. """ if self.NAME_ATTR in self.__dict__ and self.HUMAN_ID: - return strutils.to_slug(getattr(self, self.NAME_ATTR)) + name = getattr(self, self.NAME_ATTR) + if name is not None: + return strutils.to_slug(name) return None def _add_details(self, info): From 96eea0f1566e10b9de98752aea4f0716c881c993 Mon Sep 17 00:00:00 2001 From: Sahid Orentino Ferdjaoui Date: Thu, 2 Jan 2014 15:56:53 +0100 Subject: [PATCH 0382/1705] shell: refactor boot to use _print_server In the shell command when we use the command boot, at the end we use code to print some server information. We should use the already defined function _print_server. Change-Id: I133d7706458c71974ba464ab686cf3679a562592 Closes-Bug: #1265534 --- novaclient/v1_1/shell.py | 33 ++++++++------------------------- novaclient/v3/shell.py | 33 ++++++++------------------------- 2 files changed, 16 insertions(+), 50 deletions(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index d83a01447..2891637cc 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -424,27 +424,7 @@ def do_boot(cs, args): boot_kwargs.update(extra_boot_kwargs) server = cs.servers.create(*boot_args, **boot_kwargs) - - # Keep any information (like adminPass) returned by create - info = server._info - server = cs.servers.get(info['id']) - info.update(server._info) - - flavor = info.get('flavor', {}) - flavor_id = flavor.get('id', '') - info['flavor'] = _find_flavor(cs, flavor_id).name - - image = info.get('image', {}) - if image: - image_id = image.get('id', '') - info['image'] = _find_image(cs, image_id).name - else: # Booting from volume - info['image'] = "Attempt to boot from volume - no image supplied" - - info.pop('links', None) - info.pop('addresses', None) - - utils.print_dict(info) + _print_server(cs, args, server) if args.poll: _poll_for_status(cs.servers.get, info['id'], 'building', ['active']) @@ -1465,13 +1445,16 @@ def do_meta(cs, args): cs.servers.delete_meta(server, sorted(metadata.keys(), reverse=True)) -def _print_server(cs, args): +def _print_server(cs, args, server=None): # By default when searching via name we will do a # findall(name=blah) and due a REST /details which is not the same # as a .get() and doesn't get the information about flavors and # images. This fix it as we redo the call with the id which does a # .get() to get all informations. - server = _find_server(cs, args.server) + if not server: + server = _find_server(cs, args.server) + + minimal = getattr(args, "minimal", False) networks = server.networks info = server._info.copy() @@ -1480,7 +1463,7 @@ def _print_server(cs, args): flavor = info.get('flavor', {}) flavor_id = flavor.get('id', '') - if args.minimal: + if minimal: info['flavor'] = flavor_id else: info['flavor'] = '%s (%s)' % (_find_flavor(cs, flavor_id).name, @@ -1493,7 +1476,7 @@ def _print_server(cs, args): image = info.get('image', {}) if image: image_id = image.get('id', '') - if args.minimal: + if minimal: info['image'] = image_id else: try: diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index c11e1fcbf..612e62c35 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -307,27 +307,7 @@ def do_boot(cs, args): boot_kwargs.update(extra_boot_kwargs) server = cs.servers.create(*boot_args, **boot_kwargs) - - # Keep any information (like admin_password) returned by create - info = server._info - server = cs.servers.get(info['id']) - info.update(server._info) - - flavor = info.get('flavor', {}) - flavor_id = flavor.get('id', '') - info['flavor'] = _find_flavor(cs, flavor_id).name - - image = info.get('image', {}) - if image: - image_id = image.get('id', '') - info['image'] = _find_image(cs, image_id).name - else: # Booting from volume - info['image'] = "Attempt to boot from volume - no image supplied" - - info.pop('links', None) - info.pop('addresses', None) - - utils.print_dict(info) + _print_server(cs, args, server) if args.poll: _poll_for_status(cs.servers.get, info['id'], 'building', ['active']) @@ -1303,13 +1283,16 @@ def do_meta(cs, args): cs.servers.delete_meta(server, metadata.keys()) -def _print_server(cs, args): +def _print_server(cs, args, server=None): # By default when searching via name we will do a # findall(name=blah) and due a REST /details which is not the same # as a .get() and doesn't get the information about flavors and # images. This fix it as we redo the call with the id which does a # .get() to get all informations. - server = _find_server(cs, args.server) + if not server: + server = _find_server(cs, args.server) + + minimal = getattr(args, "minimal", False) networks = server.networks info = server._info.copy() @@ -1318,7 +1301,7 @@ def _print_server(cs, args): flavor = info.get('flavor', {}) flavor_id = flavor.get('id', '') - if args.minimal: + if minimal: info['flavor'] = flavor_id else: info['flavor'] = '%s (%s)' % (_find_flavor(cs, flavor_id).name, @@ -1327,7 +1310,7 @@ def _print_server(cs, args): image = info.get('image', {}) if image: image_id = image.get('id', '') - if args.minimal: + if minimal: info['image'] = image_id else: try: From 8e9c038f15f9f3440ed8c430a682d7bdd48f313c Mon Sep 17 00:00:00 2001 From: Masayuki Igawa Date: Fri, 10 Jan 2014 11:43:32 +0900 Subject: [PATCH 0383/1705] Remove the coverage extension code The coverage extension has been removed from Nova V2 API by the I07d798129ee277a6f7691c25f88c07a5204c0943 This commit remove the code. Change-Id: I430a8b17be11bb961dc5b89b2d098f373e0a27fa --- novaclient/tests/v1_1/fakes.py | 18 ------- novaclient/tests/v1_1/test_coverage_ext.py | 43 ---------------- novaclient/tests/v1_1/test_shell.py | 32 ------------ novaclient/v1_1/client.py | 2 - novaclient/v1_1/coverage_ext.py | 60 ---------------------- novaclient/v1_1/shell.py | 43 ---------------- 6 files changed, 198 deletions(-) delete mode 100644 novaclient/tests/v1_1/test_coverage_ext.py delete mode 100644 novaclient/v1_1/coverage_ext.py diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index fbd701298..3ac805884 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -1712,14 +1712,6 @@ def get_os_fping_1(self, **kw): } ) - def post_os_coverage_action(self, body, **kw): - if 'report' not in body: - return (200, {}, None) - else: - return (200, {}, { - 'path': '/tmp/tmpdir/' + body['report']['file'] - }) - def post_os_networks_1_action(self, **kw): return (202, {}, None) @@ -1729,16 +1721,6 @@ def post_os_networks_networktest_action(self, **kw): def post_os_networks_2_action(self, **kw): return (202, {}, None) - def post_os_coverage_action(self, body, **kw): - if 'start' in body or 'reset' in body: - return (200, {}, None) - elif 'stop' in body: - return (200, {}, {'path': '/tmp/tmpdir/'}) - else: - return (200, {}, { - 'path': '/tmp/tmpdir/' + body['report']['file'] - }) - def get_os_availability_zone(self, **kw): return (200, {}, {"availabilityZoneInfo": [ {"zoneName": "zone-1", diff --git a/novaclient/tests/v1_1/test_coverage_ext.py b/novaclient/tests/v1_1/test_coverage_ext.py deleted file mode 100644 index 8c9870d43..000000000 --- a/novaclient/tests/v1_1/test_coverage_ext.py +++ /dev/null @@ -1,43 +0,0 @@ -# Copyright 2012 IBM Corp. -# -# 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. - -# See: http://wiki.openstack.org/Nova/CoverageExtension for more information -# and usage explanation for this API extension - -from novaclient.tests import utils -from novaclient.tests.v1_1 import fakes - - -cs = fakes.FakeClient() - - -class CoverageTest(utils.TestCase): - - def test_start_coverage(self): - c = cs.coverage.start() - cs.assert_called('POST', '/os-coverage/action') - - def test_stop_coverage(self): - c = cs.coverage.stop() - return_dict = {'path': '/tmp/tmpdir/report'} - cs.assert_called_anytime('POST', '/os-coverage/action') - - def test_report_coverage(self): - c = cs.coverage.report('report') - return_dict = {'path': '/tmp/tmpdir/report'} - cs.assert_called_anytime('POST', '/os-coverage/action') - - def test_reset_coverage(self): - c = cs.coverage.reset() - cs.assert_called_anytime('POST', '/os-coverage/action') diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index 61f0f0285..75af786a0 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -1378,38 +1378,6 @@ def test_host_servers_migrate(self): self.assert_called('POST', '/servers/uuid4/action', {'migrate': None}, pos=4) - def test_coverage_start(self): - self.run_command('coverage-start') - self.assert_called('POST', '/os-coverage/action') - - def test_coverage_start_with_combine(self): - self.run_command('coverage-start --combine') - body = {'start': {'combine': True}} - self.assert_called('POST', '/os-coverage/action', body) - - def test_coverage_stop(self): - self.run_command('coverage-stop') - self.assert_called_anytime('POST', '/os-coverage/action') - - def test_coverage_report(self): - self.run_command('coverage-report report') - self.assert_called_anytime('POST', '/os-coverage/action') - - def test_coverage_report_with_html(self): - self.run_command('coverage-report report --html') - body = {'report': {'html': True, 'file': 'report'}} - self.assert_called_anytime('POST', '/os-coverage/action', body) - - def test_coverage_report_with_xml(self): - self.run_command('coverage-report report --xml') - body = {'report': {'xml': True, 'file': 'report'}} - self.assert_called_anytime('POST', '/os-coverage/action', body) - - def test_coverage_reset(self): - self.run_command('coverage-reset') - body = {'reset': {}} - self.assert_called_anytime('POST', '/os-coverage/action', body) - def test_hypervisor_list(self): self.run_command('hypervisor-list') self.assert_called('GET', '/os-hypervisors') diff --git a/novaclient/v1_1/client.py b/novaclient/v1_1/client.py index a89581eac..c7e3d65fb 100644 --- a/novaclient/v1_1/client.py +++ b/novaclient/v1_1/client.py @@ -19,7 +19,6 @@ from novaclient.v1_1 import availability_zones from novaclient.v1_1 import certs from novaclient.v1_1 import cloudpipe -from novaclient.v1_1 import coverage_ext from novaclient.v1_1 import fixed_ips from novaclient.v1_1 import flavor_access from novaclient.v1_1 import flavors @@ -115,7 +114,6 @@ def __init__(self, username, api_key, project_id, auth_url=None, self.fixed_ips = fixed_ips.FixedIPsManager(self) self.floating_ips_bulk = floating_ips_bulk.FloatingIPBulkManager(self) self.os_cache = os_cache or not no_cache - self.coverage = coverage_ext.CoverageManager(self) self.availability_zones = \ availability_zones.AvailabilityZoneManager(self) diff --git a/novaclient/v1_1/coverage_ext.py b/novaclient/v1_1/coverage_ext.py deleted file mode 100644 index 92fc5a880..000000000 --- a/novaclient/v1_1/coverage_ext.py +++ /dev/null @@ -1,60 +0,0 @@ -# Copyright 2012 IBM Corp. -# All Rights Reserved. -# -# 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 novaclient import base - - -class Coverage(base.Resource): - def __repr__(self): - return "" % self.name - - -class CoverageManager(base.Manager): - - resource_class = Coverage - - def start(self, combine=False, **kwargs): - body = {'start': {}} - if combine: - body['start'] = {'combine': True} - self.run_hooks('modify_body_for_action', body) - url = '/os-coverage/action' - return self.api.client.post(url, body=body) - - def stop(self): - body = {'stop': {}} - self.run_hooks('modify_body_for_action', body) - url = '/os-coverage/action' - return self.api.client.post(url, body=body) - - def report(self, filename, xml=False, html=False): - body = { - 'report': { - 'file': filename, - } - } - if xml: - body['report']['xml'] = True - elif html: - body['report']['html'] = True - self.run_hooks('modify_body_for_action', body) - url = '/os-coverage/action' - return self.api.client.post(url, body=body) - - def reset(self): - body = {'reset': {}} - self.run_hooks('modify_body_for_action', body) - url = '/os-coverage/action' - return self.api.client.post(url, body=body) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index d83a01447..07c62d6cd 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2911,49 +2911,6 @@ def do_host_action(cs, args): utils.print_list([result], ['HOST', 'power_action']) -@utils.arg('--combine', - dest='combine', - action="store_true", - default=False, - help='Generate a single report for all services.') -def do_coverage_start(cs, args): - """Start Nova coverage reporting.""" - cs.coverage.start(combine=args.combine) - print("Coverage collection started") - - -def do_coverage_stop(cs, args): - """Stop Nova coverage reporting.""" - out = cs.coverage.stop() - print("Coverage data file path: %s" % out[-1]['path']) - - -@utils.arg('filename', metavar='', help='report filename') -@utils.arg('--html', - dest='html', - action="store_true", - default=False, - help='Generate HTML reports instead of text ones.') -@utils.arg('--xml', - dest='xml', - action="store_true", - default=False, - help='Generate XML reports instead of text ones.') -def do_coverage_report(cs, args): - """Generate coverage report.""" - if args.html is True and args.xml is True: - raise exceptions.CommandError("--html and --xml must not be " - "specified together.") - cov = cs.coverage.report(args.filename, xml=args.xml, html=args.html) - print("Report path: %s" % cov[-1]['path']) - - -def do_coverage_reset(cs, args): - """Reset coverage data.""" - cs.coverage.reset() - print("Coverage data reset") - - def _find_hypervisor(cs, hypervisor): """Get a hypervisor by name or ID.""" return utils.find_resource(cs.hypervisors, hypervisor) From 40e433469d9a26396320b0e9f605b6041b79d2a1 Mon Sep 17 00:00:00 2001 From: lizheming Date: Wed, 15 Jan 2014 10:42:19 +0800 Subject: [PATCH 0384/1705] assertTrue(isinstance) replace by assertIsInstance some of tests use different method of assertTrue(isinstance(A, B)) or assertEqual(type(A), B). The correct way is to use assertIsInstance(A, B) provided by testtools Closes-Bug: #1268480 Change-Id: Ie3b3e49ea3cc4357a65605ad54ff4ee1fbde12c7 --- novaclient/tests/test_client.py | 4 +-- .../tests/v1_1/contrib/test_baremetal.py | 8 ++--- .../tests/v1_1/contrib/test_migrations.py | 4 +-- novaclient/tests/v1_1/test_agents.py | 4 +-- novaclient/tests/v1_1/test_aggregates.py | 36 +++++++++---------- .../tests/v1_1/test_availability_zone.py | 6 ++-- novaclient/tests/v1_1/test_certs.py | 4 +-- novaclient/tests/v1_1/test_cloudpipe.py | 4 +-- novaclient/tests/v1_1/test_flavor_access.py | 6 ++-- novaclient/tests/v1_1/test_flavors.py | 22 ++++++------ novaclient/tests/v1_1/test_floating_ip_dns.py | 10 +++--- .../tests/v1_1/test_floating_ip_pools.py | 2 +- novaclient/tests/v1_1/test_floating_ips.py | 6 ++-- .../tests/v1_1/test_floating_ips_bulk.py | 4 +-- novaclient/tests/v1_1/test_fping.py | 10 +++--- novaclient/tests/v1_1/test_hosts.py | 12 +++---- novaclient/tests/v1_1/test_images.py | 6 ++-- novaclient/tests/v1_1/test_keypairs.py | 8 ++--- novaclient/tests/v1_1/test_limits.py | 4 +-- novaclient/tests/v1_1/test_networks.py | 8 ++--- novaclient/tests/v1_1/test_security_groups.py | 8 ++--- novaclient/tests/v1_1/test_servers.py | 20 +++++------ novaclient/tests/v1_1/test_services.py | 14 ++++---- novaclient/tests/v1_1/test_usage.py | 4 +-- novaclient/tests/v1_1/test_volumes.py | 16 ++++----- novaclient/tests/v3/test_flavor_access.py | 8 ++--- novaclient/tests/v3/test_hosts.py | 12 +++---- novaclient/tests/v3/test_images.py | 6 ++-- novaclient/tests/v3/test_servers.py | 16 ++++----- 29 files changed, 135 insertions(+), 137 deletions(-) diff --git a/novaclient/tests/test_client.py b/novaclient/tests/test_client.py index ded0d9add..ee223c393 100644 --- a/novaclient/tests/test_client.py +++ b/novaclient/tests/test_client.py @@ -186,8 +186,8 @@ def test_clent_extensions_v3(self): cs = novaclient.v3.client.Client("user", "password", "project_id", auth_url="foo/v2", extensions=extensions) - self.assertTrue(isinstance(getattr(cs, fake_attribute_name1, None), - fakes.FakeManager)) + self.assertIsInstance(getattr(cs, fake_attribute_name1, None), + fakes.FakeManager) self.assertFalse(hasattr(cs, fake_attribute_name2)) @mock.patch.object(novaclient.client.HTTPClient, 'authenticate') diff --git a/novaclient/tests/v1_1/contrib/test_baremetal.py b/novaclient/tests/v1_1/contrib/test_baremetal.py index 88b129f0f..6e2399f24 100644 --- a/novaclient/tests/v1_1/contrib/test_baremetal.py +++ b/novaclient/tests/v1_1/contrib/test_baremetal.py @@ -33,18 +33,18 @@ def test_list_nodes(self): nl = cs.baremetal.list() cs.assert_called('GET', '/os-baremetal-nodes') for n in nl: - self.assertTrue(isinstance(n, baremetal.BareMetalNode)) + self.assertIsInstance(n, baremetal.BareMetalNode) def test_get_node(self): n = cs.baremetal.get(1) cs.assert_called('GET', '/os-baremetal-nodes/1') - self.assertTrue(isinstance(n, baremetal.BareMetalNode)) + self.assertIsInstance(n, baremetal.BareMetalNode) def test_create_node(self): n = cs.baremetal.create("service_host", 1, 1024, 2048, "aa:bb:cc:dd:ee:ff") cs.assert_called('POST', '/os-baremetal-nodes') - self.assertTrue(isinstance(n, baremetal.BareMetalNode)) + self.assertIsInstance(n, baremetal.BareMetalNode) def test_delete_node(self): n = cs.baremetal.get(1) @@ -54,7 +54,7 @@ def test_delete_node(self): def test_node_add_interface(self): i = cs.baremetal.add_interface(1, "bb:cc:dd:ee:ff:aa", 1, 2) cs.assert_called('POST', '/os-baremetal-nodes/1/action') - self.assertTrue(isinstance(i, baremetal.BareMetalNodeInterface)) + self.assertIsInstance(i, baremetal.BareMetalNodeInterface) def test_node_remove_interface(self): cs.baremetal.remove_interface(1, "bb:cc:dd:ee:ff:aa") diff --git a/novaclient/tests/v1_1/contrib/test_migrations.py b/novaclient/tests/v1_1/contrib/test_migrations.py index 95356de9d..5337502a1 100644 --- a/novaclient/tests/v1_1/contrib/test_migrations.py +++ b/novaclient/tests/v1_1/contrib/test_migrations.py @@ -28,7 +28,7 @@ def test_list_migrations(self): ml = cs.migrations.list() cs.assert_called('GET', '/os-migrations') for m in ml: - self.assertTrue(isinstance(m, migrations.Migration)) + self.assertIsInstance(m, migrations.Migration) def test_list_migrations_with_filters(self): ml = cs.migrations.list('host1', 'finished', 'child1') @@ -37,4 +37,4 @@ def test_list_migrations_with_filters(self): '/os-migrations?cell_name=child1&host=host1' '&status=finished') for m in ml: - self.assertTrue(isinstance(m, migrations.Migration)) + self.assertIsInstance(m, migrations.Migration) diff --git a/novaclient/tests/v1_1/test_agents.py b/novaclient/tests/v1_1/test_agents.py index 9c5c6d928..e89da9e8a 100644 --- a/novaclient/tests/v1_1/test_agents.py +++ b/novaclient/tests/v1_1/test_agents.py @@ -34,14 +34,14 @@ def test_list_agents(self): ags = self.cs.agents.list() self.cs.assert_called('GET', '/os-agents') for a in ags: - self.assertTrue(isinstance(a, self.agent_type)) + self.assertIsInstance(a, self.agent_type) self.assertEqual(a.hypervisor, 'kvm') def test_list_agents_with_hypervisor(self): ags = self.cs.agents.list('xen') self.cs.assert_called('GET', '/os-agents?hypervisor=xen') for a in ags: - self.assertTrue(isinstance(a, self.agent_type)) + self.assertIsInstance(a, self.agent_type) self.assertEqual(a.hypervisor, 'xen') def test_agents_create(self): diff --git a/novaclient/tests/v1_1/test_aggregates.py b/novaclient/tests/v1_1/test_aggregates.py index 1e3247cfe..ef0305166 100644 --- a/novaclient/tests/v1_1/test_aggregates.py +++ b/novaclient/tests/v1_1/test_aggregates.py @@ -34,31 +34,31 @@ def test_list_aggregates(self): result = self.cs.aggregates.list() self.cs.assert_called('GET', '/os-aggregates') for aggregate in result: - self.assertTrue(isinstance(aggregate, aggregates.Aggregate)) + self.assertIsInstance(aggregate, aggregates.Aggregate) def test_create_aggregate(self): body = {"aggregate": {"name": "test", "availability_zone": "nova1"}} aggregate = self.cs.aggregates.create("test", "nova1") self.cs.assert_called('POST', '/os-aggregates', body) - self.assertTrue(isinstance(aggregate, aggregates.Aggregate)) + self.assertIsInstance(aggregate, aggregates.Aggregate) def test_get(self): aggregate = self.cs.aggregates.get("1") self.cs.assert_called('GET', '/os-aggregates/1') - self.assertTrue(isinstance(aggregate, aggregates.Aggregate)) + self.assertIsInstance(aggregate, aggregates.Aggregate) aggregate2 = self.cs.aggregates.get(aggregate) self.cs.assert_called('GET', '/os-aggregates/1') - self.assertTrue(isinstance(aggregate2, aggregates.Aggregate)) + self.assertIsInstance(aggregate2, aggregates.Aggregate) def test_get_details(self): aggregate = self.cs.aggregates.get_details("1") self.cs.assert_called('GET', '/os-aggregates/1') - self.assertTrue(isinstance(aggregate, aggregates.Aggregate)) + self.assertIsInstance(aggregate, aggregates.Aggregate) aggregate2 = self.cs.aggregates.get_details(aggregate) self.cs.assert_called('GET', '/os-aggregates/1') - self.assertTrue(isinstance(aggregate2, aggregates.Aggregate)) + self.assertIsInstance(aggregate2, aggregates.Aggregate) def test_update(self): aggregate = self.cs.aggregates.get("1") @@ -67,11 +67,11 @@ def test_update(self): result1 = aggregate.update(values) self.cs.assert_called('PUT', '/os-aggregates/1', body) - self.assertTrue(isinstance(result1, aggregates.Aggregate)) + self.assertIsInstance(result1, aggregates.Aggregate) result2 = self.cs.aggregates.update(2, values) self.cs.assert_called('PUT', '/os-aggregates/2', body) - self.assertTrue(isinstance(result2, aggregates.Aggregate)) + self.assertIsInstance(result2, aggregates.Aggregate) def test_update_with_availability_zone(self): aggregate = self.cs.aggregates.get("1") @@ -80,7 +80,7 @@ def test_update_with_availability_zone(self): result3 = self.cs.aggregates.update(aggregate, values) self.cs.assert_called('PUT', '/os-aggregates/1', body) - self.assertTrue(isinstance(result3, aggregates.Aggregate)) + self.assertIsInstance(result3, aggregates.Aggregate) def test_add_host(self): aggregate = self.cs.aggregates.get("1") @@ -89,15 +89,15 @@ def test_add_host(self): result1 = aggregate.add_host(host) self.cs.assert_called('POST', '/os-aggregates/1/action', body) - self.assertTrue(isinstance(result1, aggregates.Aggregate)) + self.assertIsInstance(result1, aggregates.Aggregate) result2 = self.cs.aggregates.add_host("2", host) self.cs.assert_called('POST', '/os-aggregates/2/action', body) - self.assertTrue(isinstance(result2, aggregates.Aggregate)) + self.assertIsInstance(result2, aggregates.Aggregate) result3 = self.cs.aggregates.add_host(aggregate, host) self.cs.assert_called('POST', '/os-aggregates/1/action', body) - self.assertTrue(isinstance(result3, aggregates.Aggregate)) + self.assertIsInstance(result3, aggregates.Aggregate) def test_remove_host(self): aggregate = self.cs.aggregates.get("1") @@ -106,15 +106,15 @@ def test_remove_host(self): result1 = aggregate.remove_host(host) self.cs.assert_called('POST', '/os-aggregates/1/action', body) - self.assertTrue(isinstance(result1, aggregates.Aggregate)) + self.assertIsInstance(result1, aggregates.Aggregate) result2 = self.cs.aggregates.remove_host("2", host) self.cs.assert_called('POST', '/os-aggregates/2/action', body) - self.assertTrue(isinstance(result2, aggregates.Aggregate)) + self.assertIsInstance(result2, aggregates.Aggregate) result3 = self.cs.aggregates.remove_host(aggregate, host) self.cs.assert_called('POST', '/os-aggregates/1/action', body) - self.assertTrue(isinstance(result3, aggregates.Aggregate)) + self.assertIsInstance(result3, aggregates.Aggregate) def test_set_metadata(self): aggregate = self.cs.aggregates.get("1") @@ -123,15 +123,15 @@ def test_set_metadata(self): result1 = aggregate.set_metadata(metadata) self.cs.assert_called('POST', '/os-aggregates/1/action', body) - self.assertTrue(isinstance(result1, aggregates.Aggregate)) + self.assertIsInstance(result1, aggregates.Aggregate) result2 = self.cs.aggregates.set_metadata(2, metadata) self.cs.assert_called('POST', '/os-aggregates/2/action', body) - self.assertTrue(isinstance(result2, aggregates.Aggregate)) + self.assertIsInstance(result2, aggregates.Aggregate) result3 = self.cs.aggregates.set_metadata(aggregate, metadata) self.cs.assert_called('POST', '/os-aggregates/1/action', body) - self.assertTrue(isinstance(result3, aggregates.Aggregate)) + self.assertIsInstance(result3, aggregates.Aggregate) def test_delete_aggregate(self): aggregate = self.cs.aggregates.list()[0] diff --git a/novaclient/tests/v1_1/test_availability_zone.py b/novaclient/tests/v1_1/test_availability_zone.py index b248b6f0e..6d742b1b6 100644 --- a/novaclient/tests/v1_1/test_availability_zone.py +++ b/novaclient/tests/v1_1/test_availability_zone.py @@ -46,8 +46,7 @@ def test_list_availability_zone(self): self.cs.assert_called('GET', '/os-availability-zone') for zone in zones: - self.assertTrue(isinstance(zone, - self.availability_zone_type)) + self.assertIsInstance(zone, self.availability_zone_type) self.assertEqual(2, len(zones)) @@ -67,8 +66,7 @@ def test_detail_availability_zone(self): self.cs.assert_called('GET', '/os-availability-zone/detail') for zone in zones: - self.assertTrue(isinstance(zone, - self.availability_zone_type)) + self.assertIsInstance(zone, self.availability_zone_type) self.assertEqual(3, len(zones)) diff --git a/novaclient/tests/v1_1/test_certs.py b/novaclient/tests/v1_1/test_certs.py index b6728369c..87e1d09f0 100644 --- a/novaclient/tests/v1_1/test_certs.py +++ b/novaclient/tests/v1_1/test_certs.py @@ -31,9 +31,9 @@ def _get_cert_type(self): def test_create_cert(self): cert = self.cs.certs.create() self.cs.assert_called('POST', '/os-certificates') - self.assertTrue(isinstance(cert, certs.Certificate)) + self.assertIsInstance(cert, certs.Certificate) def test_get_root_cert(self): cert = self.cs.certs.get() self.cs.assert_called('GET', '/os-certificates/root') - self.assertTrue(isinstance(cert, certs.Certificate)) + self.assertIsInstance(cert, certs.Certificate) diff --git a/novaclient/tests/v1_1/test_cloudpipe.py b/novaclient/tests/v1_1/test_cloudpipe.py index a54aa4f47..8cb96aa42 100644 --- a/novaclient/tests/v1_1/test_cloudpipe.py +++ b/novaclient/tests/v1_1/test_cloudpipe.py @@ -24,14 +24,14 @@ class CloudpipeTest(utils.TestCase): def test_list_cloudpipes(self): cp = cs.cloudpipe.list() cs.assert_called('GET', '/os-cloudpipe') - [self.assertTrue(isinstance(c, cloudpipe.Cloudpipe)) for c in cp] + [self.assertIsInstance(c, cloudpipe.Cloudpipe) for c in cp] def test_create(self): project = "test" cp = cs.cloudpipe.create(project) body = {'cloudpipe': {'project_id': project}} cs.assert_called('POST', '/os-cloudpipe', body) - self.assertTrue(isinstance(cp, str)) + self.assertIsInstance(cp, str) def test_update(self): cs.cloudpipe.update("192.168.1.1", 2345) diff --git a/novaclient/tests/v1_1/test_flavor_access.py b/novaclient/tests/v1_1/test_flavor_access.py index 1ed58f4f0..ec937d01c 100644 --- a/novaclient/tests/v1_1/test_flavor_access.py +++ b/novaclient/tests/v1_1/test_flavor_access.py @@ -27,7 +27,7 @@ def test_list_access_by_flavor_private(self): kwargs = {'flavor': cs.flavors.get(2)} r = cs.flavor_access.list(**kwargs) cs.assert_called('GET', '/flavors/2/os-flavor-access') - [self.assertTrue(isinstance(a, flavor_access.FlavorAccess)) for a in r] + [self.assertIsInstance(a, flavor_access.FlavorAccess) for a in r] def test_add_tenant_access(self): flavor = cs.flavors.get(2) @@ -41,7 +41,7 @@ def test_add_tenant_access(self): } cs.assert_called('POST', '/flavors/2/action', body) - [self.assertTrue(isinstance(a, flavor_access.FlavorAccess)) for a in r] + [self.assertIsInstance(a, flavor_access.FlavorAccess) for a in r] def test_remove_tenant_access(self): flavor = cs.flavors.get(2) @@ -55,4 +55,4 @@ def test_remove_tenant_access(self): } cs.assert_called('POST', '/flavors/2/action', body) - [self.assertTrue(isinstance(a, flavor_access.FlavorAccess)) for a in r] + [self.assertIsInstance(a, flavor_access.FlavorAccess) for a in r] diff --git a/novaclient/tests/v1_1/test_flavors.py b/novaclient/tests/v1_1/test_flavors.py index eb7bf7052..d1762949e 100644 --- a/novaclient/tests/v1_1/test_flavors.py +++ b/novaclient/tests/v1_1/test_flavors.py @@ -35,36 +35,36 @@ def test_list_flavors(self): fl = self.cs.flavors.list() self.cs.assert_called('GET', '/flavors/detail') for flavor in fl: - self.assertTrue(isinstance(flavor, self.flavor_type)) + self.assertIsInstance(flavor, self.flavor_type) def test_list_flavors_undetailed(self): fl = self.cs.flavors.list(detailed=False) self.cs.assert_called('GET', '/flavors') for flavor in fl: - self.assertTrue(isinstance(flavor, self.flavor_type)) + self.assertIsInstance(flavor, self.flavor_type) def test_list_flavors_is_public_none(self): fl = self.cs.flavors.list(is_public=None) self.cs.assert_called('GET', '/flavors/detail?is_public=None') for flavor in fl: - self.assertTrue(isinstance(flavor, self.flavor_type)) + self.assertIsInstance(flavor, self.flavor_type) def test_list_flavors_is_public_false(self): fl = self.cs.flavors.list(is_public=False) self.cs.assert_called('GET', '/flavors/detail?is_public=False') for flavor in fl: - self.assertTrue(isinstance(flavor, self.flavor_type)) + self.assertIsInstance(flavor, self.flavor_type) def test_list_flavors_is_public_true(self): fl = self.cs.flavors.list(is_public=True) self.cs.assert_called('GET', '/flavors/detail') for flavor in fl: - self.assertTrue(isinstance(flavor, self.flavor_type)) + self.assertIsInstance(flavor, self.flavor_type) def test_get_flavor_details(self): f = self.cs.flavors.get(1) self.cs.assert_called('GET', '/flavors/1') - self.assertTrue(isinstance(f, self.flavor_type)) + self.assertIsInstance(f, self.flavor_type) self.assertEqual(f.ram, 256) self.assertEqual(f.disk, 10) self.assertEqual(f.ephemeral, 10) @@ -73,7 +73,7 @@ def test_get_flavor_details(self): def test_get_flavor_details_alphanum_id(self): f = self.cs.flavors.get('aa1') self.cs.assert_called('GET', '/flavors/aa1') - self.assertTrue(isinstance(f, self.flavor_type)) + self.assertIsInstance(f, self.flavor_type) self.assertEqual(f.ram, 128) self.assertEqual(f.disk, 0) self.assertEqual(f.ephemeral, 0) @@ -82,7 +82,7 @@ def test_get_flavor_details_alphanum_id(self): def test_get_flavor_details_diablo(self): f = self.cs.flavors.get(3) self.cs.assert_called('GET', '/flavors/3') - self.assertTrue(isinstance(f, self.flavor_type)) + self.assertIsInstance(f, self.flavor_type) self.assertEqual(f.ram, 256) self.assertEqual(f.disk, 10) self.assertEqual(f.ephemeral, 'N/A') @@ -123,7 +123,7 @@ def test_create(self): False) self.cs.assert_called('POST', '/flavors', body) - self.assertTrue(isinstance(f, self.flavor_type)) + self.assertIsInstance(f, self.flavor_type) def test_create_with_id_as_string(self): flavor_id = 'foobar' @@ -135,7 +135,7 @@ def test_create_with_id_as_string(self): 1.0, False) self.cs.assert_called('POST', '/flavors', body) - self.assertTrue(isinstance(f, self.flavor_type)) + self.assertIsInstance(f, self.flavor_type) def test_create_ephemeral_ispublic_defaults(self): f = self.cs.flavors.create("flavorcreate", 512, 1, 10, 1234) @@ -144,7 +144,7 @@ def test_create_ephemeral_ispublic_defaults(self): 1.0, True) self.cs.assert_called('POST', '/flavors', body) - self.assertTrue(isinstance(f, self.flavor_type)) + self.assertIsInstance(f, self.flavor_type) def test_invalid_parameters_create(self): self.assertRaises(exceptions.CommandError, self.cs.flavors.create, diff --git a/novaclient/tests/v1_1/test_floating_ip_dns.py b/novaclient/tests/v1_1/test_floating_ip_dns.py index 25a3a22e3..ecaae4069 100644 --- a/novaclient/tests/v1_1/test_floating_ip_dns.py +++ b/novaclient/tests/v1_1/test_floating_ip_dns.py @@ -28,8 +28,8 @@ def test_dns_domains(self): self.assertEqual(len(domainlist), 2) for entry in domainlist: - self.assertTrue(isinstance(entry, - floating_ip_dns.FloatingIPDNSDomain)) + self.assertIsInstance(entry, + floating_ip_dns.FloatingIPDNSDomain) self.assertEqual(domainlist[1].domain, 'example.com') @@ -61,8 +61,8 @@ def test_get_dns_entries_by_ip(self): self.assertEqual(len(entries), 2) for entry in entries: - self.assertTrue(isinstance(entry, - floating_ip_dns.FloatingIPDNSEntry)) + self.assertIsInstance(entry, + floating_ip_dns.FloatingIPDNSEntry) self.assertEqual(entries[1].dns_entry['name'], 'host2') self.assertEqual(entries[1].dns_entry['ip'], self.testip) @@ -70,7 +70,7 @@ def test_get_dns_entries_by_ip(self): def test_get_dns_entry_by_name(self): entry = cs.dns_entries.get(self.testdomain, self.testname) - self.assertTrue(isinstance(entry, floating_ip_dns.FloatingIPDNSEntry)) + self.assertIsInstance(entry, floating_ip_dns.FloatingIPDNSEntry) self.assertEqual(entry.name, self.testname) def test_create_entry(self): diff --git a/novaclient/tests/v1_1/test_floating_ip_pools.py b/novaclient/tests/v1_1/test_floating_ip_pools.py index f0d080914..51124cfc9 100644 --- a/novaclient/tests/v1_1/test_floating_ip_pools.py +++ b/novaclient/tests/v1_1/test_floating_ip_pools.py @@ -27,5 +27,5 @@ class TestFloatingIPPools(utils.TestCase): def test_list_floating_ips(self): fl = cs.floating_ip_pools.list() cs.assert_called('GET', '/os-floating-ip-pools') - [self.assertTrue(isinstance(f, floating_ip_pools.FloatingIPPool)) + [self.assertIsInstance(f, floating_ip_pools.FloatingIPPool) for f in fl] diff --git a/novaclient/tests/v1_1/test_floating_ips.py b/novaclient/tests/v1_1/test_floating_ips.py index 90aaaa208..337db3e21 100644 --- a/novaclient/tests/v1_1/test_floating_ips.py +++ b/novaclient/tests/v1_1/test_floating_ips.py @@ -27,7 +27,7 @@ class FloatingIPsTest(utils.TestCase): def test_list_floating_ips(self): fl = cs.floating_ips.list() cs.assert_called('GET', '/os-floating-ips') - [self.assertTrue(isinstance(f, floating_ips.FloatingIP)) for f in fl] + [self.assertIsInstance(f, floating_ips.FloatingIP) for f in fl] def test_delete_floating_ip(self): fl = cs.floating_ips.list()[0] @@ -42,10 +42,10 @@ def test_create_floating_ip(self): fl = cs.floating_ips.create() cs.assert_called('POST', '/os-floating-ips') self.assertEqual(fl.pool, None) - self.assertTrue(isinstance(fl, floating_ips.FloatingIP)) + self.assertIsInstance(fl, floating_ips.FloatingIP) def test_create_floating_ip_with_pool(self): fl = cs.floating_ips.create('foo') cs.assert_called('POST', '/os-floating-ips') self.assertEqual(fl.pool, 'nova') - self.assertTrue(isinstance(fl, floating_ips.FloatingIP)) + self.assertIsInstance(fl, floating_ips.FloatingIP) diff --git a/novaclient/tests/v1_1/test_floating_ips_bulk.py b/novaclient/tests/v1_1/test_floating_ips_bulk.py index 5b28f1b07..01fb83725 100644 --- a/novaclient/tests/v1_1/test_floating_ips_bulk.py +++ b/novaclient/tests/v1_1/test_floating_ips_bulk.py @@ -26,13 +26,13 @@ class FloatingIPsBulkTest(utils.TestCase): def test_list_floating_ips_bulk(self): fl = cs.floating_ips_bulk.list() cs.assert_called('GET', '/os-floating-ips-bulk') - [self.assertTrue(isinstance(f, floating_ips_bulk.FloatingIP)) + [self.assertIsInstance(f, floating_ips_bulk.FloatingIP) for f in fl] def test_list_floating_ips_bulk_host_filter(self): fl = cs.floating_ips_bulk.list('testHost') cs.assert_called('GET', '/os-floating-ips-bulk/testHost') - [self.assertTrue(isinstance(f, floating_ips_bulk.FloatingIP)) + [self.assertIsInstance(f, floating_ips_bulk.FloatingIP) for f in fl] def test_create_floating_ips_bulk(self): diff --git a/novaclient/tests/v1_1/test_fping.py b/novaclient/tests/v1_1/test_fping.py index 4760d5be1..aaeb9b389 100644 --- a/novaclient/tests/v1_1/test_fping.py +++ b/novaclient/tests/v1_1/test_fping.py @@ -31,31 +31,31 @@ def test_list_fpings(self): fl = cs.fping.list() cs.assert_called('GET', '/os-fping') for f in fl: - self.assertTrue(isinstance(f, fping.Fping)) + self.assertIsInstance(f, fping.Fping) self.assertEqual(f.project_id, "fake-project") self.assertEqual(f.alive, True) def test_list_fpings_all_tenants(self): fl = cs.fping.list(all_tenants=True) for f in fl: - self.assertTrue(isinstance(f, fping.Fping)) + self.assertIsInstance(f, fping.Fping) cs.assert_called('GET', '/os-fping?all_tenants=1') def test_list_fpings_exclude(self): fl = cs.fping.list(exclude=['1']) for f in fl: - self.assertTrue(isinstance(f, fping.Fping)) + self.assertIsInstance(f, fping.Fping) cs.assert_called('GET', '/os-fping?exclude=1') def test_list_fpings_include(self): fl = cs.fping.list(include=['1']) for f in fl: - self.assertTrue(isinstance(f, fping.Fping)) + self.assertIsInstance(f, fping.Fping) cs.assert_called('GET', '/os-fping?include=1') def test_get_fping(self): f = cs.fping.get(1) cs.assert_called('GET', '/os-fping/1') - self.assertTrue(isinstance(f, fping.Fping)) + self.assertIsInstance(f, fping.Fping) self.assertEqual(f.project_id, "fake-project") self.assertEqual(f.alive, True) diff --git a/novaclient/tests/v1_1/test_hosts.py b/novaclient/tests/v1_1/test_hosts.py index ce18d960b..2c243662d 100644 --- a/novaclient/tests/v1_1/test_hosts.py +++ b/novaclient/tests/v1_1/test_hosts.py @@ -24,18 +24,18 @@ class HostsTest(utils.TestCase): def test_describe_resource(self): hs = cs.hosts.get('host') cs.assert_called('GET', '/os-hosts/host') - [self.assertTrue(isinstance(h, hosts.Host)) for h in hs] + [self.assertIsInstance(h, hosts.Host) for h in hs] def test_list_host(self): hs = cs.hosts.list() cs.assert_called('GET', '/os-hosts') - [self.assertTrue(isinstance(h, hosts.Host)) for h in hs] + [self.assertIsInstance(h, hosts.Host) for h in hs] [self.assertEqual(h.zone, 'nova1') for h in hs] def test_list_host_with_zone(self): hs = cs.hosts.list('nova') cs.assert_called('GET', '/os-hosts?zone=nova') - [self.assertTrue(isinstance(h, hosts.Host)) for h in hs] + [self.assertIsInstance(h, hosts.Host) for h in hs] [self.assertEqual(h.zone, 'nova') for h in hs] def test_update_enable(self): @@ -43,14 +43,14 @@ def test_update_enable(self): values = {"status": "enabled"} result = host.update(values) cs.assert_called('PUT', '/os-hosts/sample_host', values) - self.assertTrue(isinstance(result, hosts.Host)) + self.assertIsInstance(result, hosts.Host) def test_update_maintenance(self): host = cs.hosts.get('sample_host')[0] values = {"maintenance_mode": "enable"} result = host.update(values) cs.assert_called('PUT', '/os-hosts/sample_host', values) - self.assertTrue(isinstance(result, hosts.Host)) + self.assertIsInstance(result, hosts.Host) def test_update_both(self): host = cs.hosts.get('sample_host')[0] @@ -58,7 +58,7 @@ def test_update_both(self): "maintenance_mode": "enable"} result = host.update(values) cs.assert_called('PUT', '/os-hosts/sample_host', values) - self.assertTrue(isinstance(result, hosts.Host)) + self.assertIsInstance(result, hosts.Host) def test_host_startup(self): host = cs.hosts.get('sample_host')[0] diff --git a/novaclient/tests/v1_1/test_images.py b/novaclient/tests/v1_1/test_images.py index 481553a95..e3968aa48 100644 --- a/novaclient/tests/v1_1/test_images.py +++ b/novaclient/tests/v1_1/test_images.py @@ -24,12 +24,12 @@ class ImagesTest(utils.TestCase): def test_list_images(self): il = cs.images.list() cs.assert_called('GET', '/images/detail') - [self.assertTrue(isinstance(i, images.Image)) for i in il] + [self.assertIsInstance(i, images.Image) for i in il] def test_list_images_undetailed(self): il = cs.images.list(detailed=False) cs.assert_called('GET', '/images') - [self.assertTrue(isinstance(i, images.Image)) for i in il] + [self.assertIsInstance(i, images.Image) for i in il] def test_list_images_with_limit(self): il = cs.images.list(limit=4) @@ -38,7 +38,7 @@ def test_list_images_with_limit(self): def test_get_image_details(self): i = cs.images.get(1) cs.assert_called('GET', '/images/1') - self.assertTrue(isinstance(i, images.Image)) + self.assertIsInstance(i, images.Image) self.assertEqual(i.id, 1) self.assertEqual(i.name, 'CentOS 5.2') diff --git a/novaclient/tests/v1_1/test_keypairs.py b/novaclient/tests/v1_1/test_keypairs.py index d4ffc4e20..90a610924 100644 --- a/novaclient/tests/v1_1/test_keypairs.py +++ b/novaclient/tests/v1_1/test_keypairs.py @@ -32,13 +32,13 @@ def _get_keypair_type(self): def test_get_keypair(self): kp = self.cs.keypairs.get('test') self.cs.assert_called('GET', '/%s/test' % self.keypair_prefix) - self.assertTrue(isinstance(kp, keypairs.Keypair)) + self.assertIsInstance(kp, keypairs.Keypair) self.assertEqual(kp.name, 'test') def test_list_keypairs(self): kps = self.cs.keypairs.list() self.cs.assert_called('GET', '/%s' % self.keypair_prefix) - [self.assertTrue(isinstance(kp, keypairs.Keypair)) for kp in kps] + [self.assertIsInstance(kp, keypairs.Keypair) for kp in kps] def test_delete_keypair(self): kp = self.cs.keypairs.list()[0] @@ -52,9 +52,9 @@ def test_delete_keypair(self): def test_create_keypair(self): kp = self.cs.keypairs.create("foo") self.cs.assert_called('POST', '/%s' % self.keypair_prefix) - self.assertTrue(isinstance(kp, keypairs.Keypair)) + self.assertIsInstance(kp, keypairs.Keypair) def test_import_keypair(self): kp = self.cs.keypairs.create("foo", "fake-public-key") self.cs.assert_called('POST', '/%s' % self.keypair_prefix) - self.assertTrue(isinstance(kp, keypairs.Keypair)) + self.assertIsInstance(kp, keypairs.Keypair) diff --git a/novaclient/tests/v1_1/test_limits.py b/novaclient/tests/v1_1/test_limits.py index 92c8f7c4c..7561aa4ff 100644 --- a/novaclient/tests/v1_1/test_limits.py +++ b/novaclient/tests/v1_1/test_limits.py @@ -24,12 +24,12 @@ class LimitsTest(utils.TestCase): def test_get_limits(self): obj = cs.limits.get() cs.assert_called('GET', '/limits') - self.assertTrue(isinstance(obj, limits.Limits)) + self.assertIsInstance(obj, limits.Limits) def test_get_limits_for_a_tenant(self): obj = cs.limits.get(tenant_id=1234) cs.assert_called('GET', '/limits?tenant_id=1234') - self.assertTrue(isinstance(obj, limits.Limits)) + self.assertIsInstance(obj, limits.Limits) def test_absolute_limits(self): obj = cs.limits.get() diff --git a/novaclient/tests/v1_1/test_networks.py b/novaclient/tests/v1_1/test_networks.py index f4e973754..369772ea8 100644 --- a/novaclient/tests/v1_1/test_networks.py +++ b/novaclient/tests/v1_1/test_networks.py @@ -24,12 +24,12 @@ class NetworksTest(utils.TestCase): def test_list_networks(self): fl = cs.networks.list() cs.assert_called('GET', '/os-networks') - [self.assertTrue(isinstance(f, networks.Network)) for f in fl] + [self.assertIsInstance(f, networks.Network) for f in fl] def test_get_network(self): f = cs.networks.get(1) cs.assert_called('GET', '/os-networks/1') - self.assertTrue(isinstance(f, networks.Network)) + self.assertIsInstance(f, networks.Network) def test_delete(self): cs.networks.delete('networkdelete') @@ -39,7 +39,7 @@ def test_create(self): f = cs.networks.create(label='foo') cs.assert_called('POST', '/os-networks', {'network': {'label': 'foo'}}) - self.assertTrue(isinstance(f, networks.Network)) + self.assertIsInstance(f, networks.Network) def test_create_allparams(self): params = { @@ -62,7 +62,7 @@ def test_create_allparams(self): f = cs.networks.create(**params) cs.assert_called('POST', '/os-networks', {'network': params}) - self.assertTrue(isinstance(f, networks.Network)) + self.assertIsInstance(f, networks.Network) def test_associate_project(self): cs.networks.associate_project('networktest') diff --git a/novaclient/tests/v1_1/test_security_groups.py b/novaclient/tests/v1_1/test_security_groups.py index c06eaab2a..a46533d9a 100644 --- a/novaclient/tests/v1_1/test_security_groups.py +++ b/novaclient/tests/v1_1/test_security_groups.py @@ -24,7 +24,7 @@ def _do_test_list_security_groups(self, search_opts, path): sgs = cs.security_groups.list(search_opts=search_opts) cs.assert_called('GET', path) for sg in sgs: - self.assertTrue(isinstance(sg, security_groups.SecurityGroup)) + self.assertIsInstance(sg, security_groups.SecurityGroup) def test_list_security_groups_all_tenants_on(self): self._do_test_list_security_groups( @@ -41,7 +41,7 @@ def test_list_security_groups_all_tenants_off(self): def test_get_security_groups(self): sg = cs.security_groups.get(1) cs.assert_called('GET', '/os-security-groups/1') - self.assertTrue(isinstance(sg, security_groups.SecurityGroup)) + self.assertIsInstance(sg, security_groups.SecurityGroup) self.assertEqual('1', str(sg)) def test_delete_security_group(self): @@ -56,13 +56,13 @@ def test_delete_security_group(self): def test_create_security_group(self): sg = cs.security_groups.create("foo", "foo barr") cs.assert_called('POST', '/os-security-groups') - self.assertTrue(isinstance(sg, security_groups.SecurityGroup)) + self.assertIsInstance(sg, security_groups.SecurityGroup) def test_update_security_group(self): sg = cs.security_groups.list()[0] secgroup = cs.security_groups.update(sg, "update", "update") cs.assert_called('PUT', '/os-security-groups/1') - self.assertTrue(isinstance(secgroup, security_groups.SecurityGroup)) + self.assertIsInstance(secgroup, security_groups.SecurityGroup) def test_refresh_security_group(self): sg = cs.security_groups.get(1) diff --git a/novaclient/tests/v1_1/test_servers.py b/novaclient/tests/v1_1/test_servers.py index 2a812f856..1f1771748 100644 --- a/novaclient/tests/v1_1/test_servers.py +++ b/novaclient/tests/v1_1/test_servers.py @@ -29,23 +29,23 @@ class ServersTest(utils.TestCase): def test_list_servers(self): sl = cs.servers.list() cs.assert_called('GET', '/servers/detail') - [self.assertTrue(isinstance(s, servers.Server)) for s in sl] + [self.assertIsInstance(s, servers.Server) for s in sl] def test_list_servers_undetailed(self): sl = cs.servers.list(detailed=False) cs.assert_called('GET', '/servers') - [self.assertTrue(isinstance(s, servers.Server)) for s in sl] + [self.assertIsInstance(s, servers.Server) for s in sl] def test_list_servers_with_marker_limit(self): sl = cs.servers.list(marker=1234, limit=2) cs.assert_called('GET', '/servers/detail?limit=2&marker=1234') for s in sl: - self.assertTrue(isinstance(s, servers.Server)) + self.assertIsInstance(s, servers.Server) def test_get_server_details(self): s = cs.servers.get(1234) cs.assert_called('GET', '/servers/1234') - self.assertTrue(isinstance(s, servers.Server)) + self.assertIsInstance(s, servers.Server) self.assertEqual(s.id, 1234) self.assertEqual(s.status, 'BUILD') @@ -70,7 +70,7 @@ def test_create_server(self): } ) cs.assert_called('POST', '/servers') - self.assertTrue(isinstance(s, servers.Server)) + self.assertIsInstance(s, servers.Server) def test_create_server_boot_from_volume_with_nics(self): old_boot = cs.servers._boot @@ -100,7 +100,7 @@ def test_create_server_from_volume(): nics=nics ) cs.assert_called('POST', '/os-volumes_boot') - self.assertTrue(isinstance(s, servers.Server)) + self.assertIsInstance(s, servers.Server) test_create_server_from_volume() @@ -117,7 +117,7 @@ def test_create_server_userdata_file_object(self): }, ) cs.assert_called('POST', '/servers') - self.assertTrue(isinstance(s, servers.Server)) + self.assertIsInstance(s, servers.Server) def test_create_server_userdata_unicode(self): s = cs.servers.create( @@ -133,7 +133,7 @@ def test_create_server_userdata_unicode(self): }, ) cs.assert_called('POST', '/servers') - self.assertTrue(isinstance(s, servers.Server)) + self.assertIsInstance(s, servers.Server) def test_create_server_userdata_utf8(self): s = cs.servers.create( @@ -149,7 +149,7 @@ def test_create_server_userdata_utf8(self): }, ) cs.assert_called('POST', '/servers') - self.assertTrue(isinstance(s, servers.Server)) + self.assertIsInstance(s, servers.Server) def _create_disk_config(self, disk_config): s = cs.servers.create( @@ -159,7 +159,7 @@ def _create_disk_config(self, disk_config): disk_config=disk_config ) cs.assert_called('POST', '/servers') - self.assertTrue(isinstance(s, servers.Server)) + self.assertIsInstance(s, servers.Server) # verify disk config param was used in the request: last_request = cs.client.callstack[-1] diff --git a/novaclient/tests/v1_1/test_services.py b/novaclient/tests/v1_1/test_services.py index a9e1b07d3..36f82e40a 100644 --- a/novaclient/tests/v1_1/test_services.py +++ b/novaclient/tests/v1_1/test_services.py @@ -34,7 +34,7 @@ def test_list_services(self): svs = self.cs.services.list() self.cs.assert_called('GET', '/os-services') for s in svs: - self.assertTrue(isinstance(s, self._get_service_type())) + self.assertIsInstance(s, self._get_service_type()) self.assertEqual(s.binary, 'nova-compute') self.assertEqual(s.host, 'host1') @@ -42,7 +42,7 @@ def test_list_services_with_hostname(self): svs = self.cs.services.list(host='host2') self.cs.assert_called('GET', '/os-services?host=host2') for s in svs: - self.assertTrue(isinstance(s, self._get_service_type())) + self.assertIsInstance(s, self._get_service_type()) self.assertEqual(s.binary, 'nova-compute') self.assertEqual(s.host, 'host2') @@ -50,7 +50,7 @@ def test_list_services_with_binary(self): svs = self.cs.services.list(binary='nova-cert') self.cs.assert_called('GET', '/os-services?binary=nova-cert') for s in svs: - self.assertTrue(isinstance(s, self._get_service_type())) + self.assertIsInstance(s, self._get_service_type()) self.assertEqual(s.binary, 'nova-cert') self.assertEqual(s.host, 'host1') @@ -59,7 +59,7 @@ def test_list_services_with_host_binary(self): self.cs.assert_called('GET', '/os-services?host=host2&binary=nova-cert') for s in svs: - self.assertTrue(isinstance(s, self._get_service_type())) + self.assertIsInstance(s, self._get_service_type()) self.assertEqual(s.binary, 'nova-cert') self.assertEqual(s.host, 'host2') @@ -74,14 +74,14 @@ def test_services_enable(self): service = self.cs.services.enable('host1', 'nova-cert') values = self._update_body("host1", "nova-cert") self.cs.assert_called('PUT', '/os-services/enable', values) - self.assertTrue(isinstance(service, self._get_service_type())) + self.assertIsInstance(service, self._get_service_type()) self.assertEqual(service.status, 'enabled') def test_services_disable(self): service = self.cs.services.disable('host1', 'nova-cert') values = self._update_body("host1", "nova-cert") self.cs.assert_called('PUT', '/os-services/disable', values) - self.assertTrue(isinstance(service, self._get_service_type())) + self.assertIsInstance(service, self._get_service_type()) self.assertEqual(service.status, 'disabled') def test_services_disable_log_reason(self): @@ -90,5 +90,5 @@ def test_services_disable_log_reason(self): values = self._update_body("compute1", "nova-compute", "disable bad host") self.cs.assert_called('PUT', '/os-services/disable-log-reason', values) - self.assertTrue(isinstance(service, self._get_service_type())) + self.assertIsInstance(service, self._get_service_type()) self.assertEqual(service.status, 'disabled') diff --git a/novaclient/tests/v1_1/test_usage.py b/novaclient/tests/v1_1/test_usage.py index ef842f4f1..99d40c088 100644 --- a/novaclient/tests/v1_1/test_usage.py +++ b/novaclient/tests/v1_1/test_usage.py @@ -39,7 +39,7 @@ def test_usage_list(self, detailed=False): ("start=%s&" % now.isoformat()) + ("end=%s&" % now.isoformat()) + ("detailed=%s" % int(bool(detailed)))) - [self.assertTrue(isinstance(u, usage.Usage)) for u in usages] + [self.assertIsInstance(u, usage.Usage) for u in usages] def test_usage_list_detailed(self): self.test_usage_list(True) @@ -52,4 +52,4 @@ def test_usage_get(self): "/os-simple-tenant-usage/tenantfoo?" + ("start=%s&" % now.isoformat()) + ("end=%s" % now.isoformat())) - self.assertTrue(isinstance(u, usage.Usage)) + self.assertIsInstance(u, usage.Usage) diff --git a/novaclient/tests/v1_1/test_volumes.py b/novaclient/tests/v1_1/test_volumes.py index 6c52f6bbc..61a215341 100644 --- a/novaclient/tests/v1_1/test_volumes.py +++ b/novaclient/tests/v1_1/test_volumes.py @@ -26,18 +26,18 @@ class VolumesTest(utils.TestCase): def test_list_servers(self): vl = cs.volumes.list() cs.assert_called('GET', '/volumes/detail') - [self.assertTrue(isinstance(v, volumes.Volume)) for v in vl] + [self.assertIsInstance(v, volumes.Volume) for v in vl] def test_list_volumes_undetailed(self): vl = cs.volumes.list(detailed=False) cs.assert_called('GET', '/volumes') - [self.assertTrue(isinstance(v, volumes.Volume)) for v in vl] + [self.assertIsInstance(v, volumes.Volume) for v in vl] def test_get_volume_details(self): vol_id = '15e59938-07d5-11e1-90e3-e3dffe0c5983' v = cs.volumes.get(vol_id) cs.assert_called('GET', '/volumes/%s' % vol_id) - self.assertTrue(isinstance(v, volumes.Volume)) + self.assertIsInstance(v, volumes.Volume) self.assertEqual(v.id, vol_id) def test_create_volume(self): @@ -47,7 +47,7 @@ def test_create_volume(self): display_description="My volume desc", ) cs.assert_called('POST', '/volumes') - self.assertTrue(isinstance(v, volumes.Volume)) + self.assertIsInstance(v, volumes.Volume) def test_delete_volume(self): vol_id = '15e59938-07d5-11e1-90e3-e3dffe0c5983' @@ -66,7 +66,7 @@ def test_create_server_volume(self): device='/dev/vdb' ) cs.assert_called('POST', '/servers/1234/os-volume_attachments') - self.assertTrue(isinstance(v, volumes.Volume)) + self.assertIsInstance(v, volumes.Volume) def test_update_server_volume(self): vol_id = '15e59938-07d5-11e1-90e3-e3dffe0c5983' @@ -76,17 +76,17 @@ def test_update_server_volume(self): new_volume_id=vol_id ) cs.assert_called('PUT', '/servers/1234/os-volume_attachments/Work') - self.assertTrue(isinstance(v, volumes.Volume)) + self.assertIsInstance(v, volumes.Volume) def test_get_server_volume(self): v = cs.volumes.get_server_volume(1234, 'Work') cs.assert_called('GET', '/servers/1234/os-volume_attachments/Work') - self.assertTrue(isinstance(v, volumes.Volume)) + self.assertIsInstance(v, volumes.Volume) def test_list_server_volumes(self): vl = cs.volumes.get_server_volumes(1234) cs.assert_called('GET', '/servers/1234/os-volume_attachments') - [self.assertTrue(isinstance(v, volumes.Volume)) for v in vl] + [self.assertIsInstance(v, volumes.Volume) for v in vl] def test_delete_server_volume(self): cs.volumes.delete_server_volume(1234, 'Work') diff --git a/novaclient/tests/v3/test_flavor_access.py b/novaclient/tests/v3/test_flavor_access.py index 5b64887d1..cd5914012 100644 --- a/novaclient/tests/v3/test_flavor_access.py +++ b/novaclient/tests/v3/test_flavor_access.py @@ -29,8 +29,8 @@ def test_list_access_by_flavor_private(self): r = cs.flavor_access.list(**kwargs) cs.assert_called('GET', '/flavors/2/flavor-access') for access_list in r: - self.assertTrue(isinstance(access_list, - flavor_access.FlavorAccess)) + self.assertIsInstance(access_list, + flavor_access.FlavorAccess) def test_add_tenant_access(self): flavor = cs.flavors.get(2) @@ -45,7 +45,7 @@ def test_add_tenant_access(self): cs.assert_called('POST', '/flavors/2/action', body) for a in r: - self.assertTrue(isinstance(a, flavor_access.FlavorAccess)) + self.assertIsInstance(a, flavor_access.FlavorAccess) def test_remove_tenant_access(self): flavor = cs.flavors.get(2) @@ -60,4 +60,4 @@ def test_remove_tenant_access(self): cs.assert_called('POST', '/flavors/2/action', body) for a in r: - self.assertTrue(isinstance(a, flavor_access.FlavorAccess)) + self.assertIsInstance(a, flavor_access.FlavorAccess) diff --git a/novaclient/tests/v3/test_hosts.py b/novaclient/tests/v3/test_hosts.py index ff158b920..6ce5b56e9 100644 --- a/novaclient/tests/v3/test_hosts.py +++ b/novaclient/tests/v3/test_hosts.py @@ -26,20 +26,20 @@ def test_describe_resource(self): hs = cs.hosts.get('host') cs.assert_called('GET', '/os-hosts/host') for h in hs: - self.assertTrue(isinstance(h, hosts.Host)) + self.assertIsInstance(h, hosts.Host) def test_list_host(self): hs = cs.hosts.list() cs.assert_called('GET', '/os-hosts') for h in hs: - self.assertTrue(isinstance(h, hosts.Host)) + self.assertIsInstance(h, hosts.Host) self.assertEqual(h.zone, 'nova1') def test_list_host_with_zone(self): hs = cs.hosts.list('nova') cs.assert_called('GET', '/os-hosts?zone=nova') for h in hs: - self.assertTrue(isinstance(h, hosts.Host)) + self.assertIsInstance(h, hosts.Host) self.assertEqual(h.zone, 'nova') def test_update_enable(self): @@ -47,14 +47,14 @@ def test_update_enable(self): values = {"status": "enabled"} result = host.update(values) cs.assert_called('PUT', '/os-hosts/sample_host', {"host": values}) - self.assertTrue(isinstance(result, hosts.Host)) + self.assertIsInstance(result, hosts.Host) def test_update_maintenance(self): host = cs.hosts.get('sample_host')[0] values = {"maintenance_mode": "enable"} result = host.update(values) cs.assert_called('PUT', '/os-hosts/sample_host', {"host": values}) - self.assertTrue(isinstance(result, hosts.Host)) + self.assertIsInstance(result, hosts.Host) def test_update_both(self): host = cs.hosts.get('sample_host')[0] @@ -62,7 +62,7 @@ def test_update_both(self): "maintenance_mode": "enable"} result = host.update(values) cs.assert_called('PUT', '/os-hosts/sample_host', {"host": values}) - self.assertTrue(isinstance(result, hosts.Host)) + self.assertIsInstance(result, hosts.Host) def test_host_startup(self): host = cs.hosts.get('sample_host')[0] diff --git a/novaclient/tests/v3/test_images.py b/novaclient/tests/v3/test_images.py index 082eb52b3..c1e756514 100644 --- a/novaclient/tests/v3/test_images.py +++ b/novaclient/tests/v3/test_images.py @@ -27,13 +27,13 @@ def test_list_images(self): il = cs.images.list() cs.assert_called('GET', '/v1/images/detail') for i in il: - self.assertTrue(isinstance(i, images.Image)) + self.assertIsInstance(i, images.Image) def test_list_images_undetailed(self): il = cs.images.list(detailed=False) cs.assert_called('GET', '/v1/images') for i in il: - self.assertTrue(isinstance(i, images.Image)) + self.assertIsInstance(i, images.Image) def test_list_images_with_limit(self): il = cs.images.list(limit=4) @@ -42,7 +42,7 @@ def test_list_images_with_limit(self): def test_get_image_details(self): i = cs.images.get(1) cs.assert_called('HEAD', '/v1/images/1') - self.assertTrue(isinstance(i, images.Image)) + self.assertIsInstance(i, images.Image) self.assertEqual(i.id, '1') self.assertEqual(i.name, 'CentOS 5.2') diff --git a/novaclient/tests/v3/test_servers.py b/novaclient/tests/v3/test_servers.py index e51b3fd9c..9d0abcd62 100644 --- a/novaclient/tests/v3/test_servers.py +++ b/novaclient/tests/v3/test_servers.py @@ -30,24 +30,24 @@ def test_list_servers(self): sl = cs.servers.list() cs.assert_called('GET', '/servers/detail') for s in sl: - self.assertTrue(isinstance(s, servers.Server)) + self.assertIsInstance(s, servers.Server) def test_list_servers_undetailed(self): sl = cs.servers.list(detailed=False) cs.assert_called('GET', '/servers') for s in sl: - self.assertTrue(isinstance(s, servers.Server)) + self.assertIsInstance(s, servers.Server) def test_list_servers_with_marker_limit(self): sl = cs.servers.list(marker=1234, limit=2) cs.assert_called('GET', '/servers/detail?limit=2&marker=1234') for s in sl: - self.assertTrue(isinstance(s, servers.Server)) + self.assertIsInstance(s, servers.Server) def test_get_server_details(self): s = cs.servers.get(1234) cs.assert_called('GET', '/servers/1234') - self.assertTrue(isinstance(s, servers.Server)) + self.assertIsInstance(s, servers.Server) self.assertEqual(s.id, 1234) self.assertEqual(s.status, 'BUILD') @@ -72,7 +72,7 @@ def test_create_server(self): } ) cs.assert_called('POST', '/servers') - self.assertTrue(isinstance(s, servers.Server)) + self.assertIsInstance(s, servers.Server) def test_create_server_userdata_file_object(self): s = cs.servers.create( @@ -87,7 +87,7 @@ def test_create_server_userdata_file_object(self): }, ) cs.assert_called('POST', '/servers') - self.assertTrue(isinstance(s, servers.Server)) + self.assertIsInstance(s, servers.Server) def test_create_server_userdata_unicode(self): s = cs.servers.create( @@ -103,7 +103,7 @@ def test_create_server_userdata_unicode(self): }, ) cs.assert_called('POST', '/servers') - self.assertTrue(isinstance(s, servers.Server)) + self.assertIsInstance(s, servers.Server) def test_create_server_userdata_utf8(self): s = cs.servers.create( @@ -119,7 +119,7 @@ def test_create_server_userdata_utf8(self): }, ) cs.assert_called('POST', '/servers') - self.assertTrue(isinstance(s, servers.Server)) + self.assertIsInstance(s, servers.Server) def test_update_server(self): s = cs.servers.get(1234) From ee0401585d98608cff9483788c6f36c0c41a44b9 Mon Sep 17 00:00:00 2001 From: Masayuki Igawa Date: Thu, 9 Jan 2014 10:20:48 +0900 Subject: [PATCH 0385/1705] Fix QuotaClassSet and their tests Some parameters of quota_class_set are not used in Nova v1.1/v2 API. And some items have wrong type and key name. QuotaClassSet class has id property originally. But 'id' comes from Nova API currently. So we can just use it as its id. This commit fixes and cleanups them. Change-Id: Ib963ff82e3107d7b78a3a63a2fc1cd6b6bbe47b0 --- novaclient/tests/v1_1/fakes.py | 22 +++++++-------------- novaclient/tests/v1_1/test_quota_classes.py | 10 +++++----- novaclient/v1_1/quota_classes.py | 9 +-------- 3 files changed, 13 insertions(+), 28 deletions(-) diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index fbd701298..ff5308aa7 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -1154,36 +1154,31 @@ def delete_os_quota_sets_97f4c221bff44578b0300df4ef119353(self, **kw): def get_os_quota_class_sets_test(self, **kw): return (200, {}, {'quota_class_set': { - 'class_name': 'test', - 'metadata_items': [], + 'id': 'test', + 'metadata_items': 1, 'injected_file_content_bytes': 1, 'injected_file_path_bytes': 1, - 'volumes': 1, - 'gigabytes': 1, 'ram': 1, 'floating_ips': 1, 'instances': 1, 'injected_files': 1, 'cores': 1, - 'keypairs': 1, + 'key_pairs': 1, 'security_groups': 1, 'security_group_rules': 1}}) def put_os_quota_class_sets_test(self, body, **kw): assert list(body) == ['quota_class_set'] return (200, {}, {'quota_class_set': { - 'class_name': 'test', - 'metadata_items': [], + 'metadata_items': 1, 'injected_file_content_bytes': 1, 'injected_file_path_bytes': 1, - 'volumes': 2, - 'gigabytes': 1, 'ram': 1, 'floating_ips': 1, 'instances': 1, 'injected_files': 1, 'cores': 1, - 'keypairs': 1, + 'key_pairs': 1, 'security_groups': 1, 'security_group_rules': 1}}) @@ -1191,18 +1186,15 @@ def put_os_quota_class_sets_97f4c221bff44578b0300df4ef119353(self, body, **kw): assert list(body) == ['quota_class_set'] return (200, {}, {'quota_class_set': { - 'class_name': '97f4c221bff44578b0300df4ef119353', - 'metadata_items': [], + 'metadata_items': 1, 'injected_file_content_bytes': 1, 'injected_file_path_bytes': 1, - 'volumes': 2, - 'gigabytes': 1, 'ram': 1, 'floating_ips': 1, 'instances': 1, 'injected_files': 1, 'cores': 1, - 'keypairs': 1, + 'key_pairs': 1, 'security_groups': 1, 'security_group_rules': 1}}) diff --git a/novaclient/tests/v1_1/test_quota_classes.py b/novaclient/tests/v1_1/test_quota_classes.py index eceb606da..338549bfa 100644 --- a/novaclient/tests/v1_1/test_quota_classes.py +++ b/novaclient/tests/v1_1/test_quota_classes.py @@ -29,14 +29,14 @@ def test_class_quotas_get(self): def test_update_quota(self): q = cs.quota_classes.get('test') - q.update(volumes=2) + q.update(cores=2) cs.assert_called('PUT', '/os-quota-class-sets/test') def test_refresh_quota(self): q = cs.quota_classes.get('test') q2 = cs.quota_classes.get('test') - self.assertEqual(q.volumes, q2.volumes) - q2.volumes = 0 - self.assertNotEqual(q.volumes, q2.volumes) + self.assertEqual(q.cores, q2.cores) + q2.cores = 0 + self.assertNotEqual(q.cores, q2.cores) q2.get() - self.assertEqual(q.volumes, q2.volumes) + self.assertEqual(q.cores, q2.cores) diff --git a/novaclient/v1_1/quota_classes.py b/novaclient/v1_1/quota_classes.py index a37632c13..4a38a970c 100644 --- a/novaclient/v1_1/quota_classes.py +++ b/novaclient/v1_1/quota_classes.py @@ -18,15 +18,8 @@ class QuotaClassSet(base.Resource): - @property - def id(self): - """QuotaClassSet does not have a 'id' attribute but base.Resource - needs it to self-refresh and QuotaSet is indexed by class_name. - """ - return self.class_name - def update(self, *args, **kwargs): - return self.manager.update(self.class_name, *args, **kwargs) + return self.manager.update(self.id, *args, **kwargs) class QuotaClassSetManager(base.Manager): From 6cf101e9ee66fe7a3ce6aa26659915c936dcc812 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Thu, 19 Dec 2013 14:43:09 +0200 Subject: [PATCH 0386/1705] Sync apiclient from oslo In the process of unification of the clients code we should reuse common functionality from Oslo. Related to blueprint common-client-library-2 Change-Id: I078b7be864f34596c846832d6201fee9b18c42f8 --- .../openstack/common/apiclient/__init__.py | 14 + novaclient/openstack/common/apiclient/auth.py | 225 ++++++++ novaclient/openstack/common/apiclient/base.py | 491 ++++++++++++++++++ .../openstack/common/apiclient/client.py | 358 +++++++++++++ .../openstack/common/apiclient/exceptions.py | 439 ++++++++++++++++ .../openstack/common/apiclient/fake_client.py | 174 +++++++ openstack-common.conf | 2 + 7 files changed, 1703 insertions(+) create mode 100644 novaclient/openstack/common/apiclient/__init__.py create mode 100644 novaclient/openstack/common/apiclient/auth.py create mode 100644 novaclient/openstack/common/apiclient/base.py create mode 100644 novaclient/openstack/common/apiclient/client.py create mode 100644 novaclient/openstack/common/apiclient/exceptions.py create mode 100644 novaclient/openstack/common/apiclient/fake_client.py diff --git a/novaclient/openstack/common/apiclient/__init__.py b/novaclient/openstack/common/apiclient/__init__.py new file mode 100644 index 000000000..f3d0cdefd --- /dev/null +++ b/novaclient/openstack/common/apiclient/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2013 OpenStack Foundation +# All Rights Reserved. +# +# 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. diff --git a/novaclient/openstack/common/apiclient/auth.py b/novaclient/openstack/common/apiclient/auth.py new file mode 100644 index 000000000..81af9aa0f --- /dev/null +++ b/novaclient/openstack/common/apiclient/auth.py @@ -0,0 +1,225 @@ +# Copyright 2013 OpenStack Foundation +# Copyright 2013 Spanish National Research Council. +# All Rights Reserved. +# +# 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. + +# E0202: An attribute inherited from %s hide this method +# pylint: disable=E0202 + +import abc +import argparse +import logging +import os + +import six +from stevedore import extension + +from novaclient.openstack.common.apiclient import exceptions + + +logger = logging.getLogger(__name__) + + +_discovered_plugins = {} + + +def discover_auth_systems(): + """Discover the available auth-systems. + + This won't take into account the old style auth-systems. + """ + global _discovered_plugins + _discovered_plugins = {} + + def add_plugin(ext): + _discovered_plugins[ext.name] = ext.plugin + + ep_namespace = "novaclient.openstack.common.apiclient.auth" + mgr = extension.ExtensionManager(ep_namespace) + mgr.map(add_plugin) + + +def load_auth_system_opts(parser): + """Load options needed by the available auth-systems into a parser. + + This function will try to populate the parser with options from the + available plugins. + """ + group = parser.add_argument_group("Common auth options") + BaseAuthPlugin.add_common_opts(group) + for name, auth_plugin in six.iteritems(_discovered_plugins): + group = parser.add_argument_group( + "Auth-system '%s' options" % name, + conflict_handler="resolve") + auth_plugin.add_opts(group) + + +def load_plugin(auth_system): + try: + plugin_class = _discovered_plugins[auth_system] + except KeyError: + raise exceptions.AuthSystemNotFound(auth_system) + return plugin_class(auth_system=auth_system) + + +def load_plugin_from_args(args): + """Load required plugin and populate it with options. + + Try to guess auth system if it is not specified. Systems are tried in + alphabetical order. + + :type args: argparse.Namespace + :raises: AuthorizationFailure + """ + auth_system = args.os_auth_system + if auth_system: + plugin = load_plugin(auth_system) + plugin.parse_opts(args) + plugin.sufficient_options() + return plugin + + for plugin_auth_system in sorted(six.iterkeys(_discovered_plugins)): + plugin_class = _discovered_plugins[plugin_auth_system] + plugin = plugin_class() + plugin.parse_opts(args) + try: + plugin.sufficient_options() + except exceptions.AuthPluginOptionsMissing: + continue + return plugin + raise exceptions.AuthPluginOptionsMissing(["auth_system"]) + + +@six.add_metaclass(abc.ABCMeta) +class BaseAuthPlugin(object): + """Base class for authentication plugins. + + An authentication plugin needs to override at least the authenticate + method to be a valid plugin. + """ + + auth_system = None + opt_names = [] + common_opt_names = [ + "auth_system", + "username", + "password", + "tenant_name", + "token", + "auth_url", + ] + + def __init__(self, auth_system=None, **kwargs): + self.auth_system = auth_system or self.auth_system + self.opts = dict((name, kwargs.get(name)) + for name in self.opt_names) + + @staticmethod + def _parser_add_opt(parser, opt): + """Add an option to parser in two variants. + + :param opt: option name (with underscores) + """ + dashed_opt = opt.replace("_", "-") + env_var = "OS_%s" % opt.upper() + arg_default = os.environ.get(env_var, "") + arg_help = "Defaults to env[%s]." % env_var + parser.add_argument( + "--os-%s" % dashed_opt, + metavar="<%s>" % dashed_opt, + default=arg_default, + help=arg_help) + parser.add_argument( + "--os_%s" % opt, + metavar="<%s>" % dashed_opt, + help=argparse.SUPPRESS) + + @classmethod + def add_opts(cls, parser): + """Populate the parser with the options for this plugin. + """ + for opt in cls.opt_names: + # use `BaseAuthPlugin.common_opt_names` since it is never + # changed in child classes + if opt not in BaseAuthPlugin.common_opt_names: + cls._parser_add_opt(parser, opt) + + @classmethod + def add_common_opts(cls, parser): + """Add options that are common for several plugins. + """ + for opt in cls.common_opt_names: + cls._parser_add_opt(parser, opt) + + @staticmethod + def get_opt(opt_name, args): + """Return option name and value. + + :param opt_name: name of the option, e.g., "username" + :param args: parsed arguments + """ + return (opt_name, getattr(args, "os_%s" % opt_name, None)) + + def parse_opts(self, args): + """Parse the actual auth-system options if any. + + This method is expected to populate the attribute `self.opts` with a + dict containing the options and values needed to make authentication. + """ + self.opts.update(dict(self.get_opt(opt_name, args) + for opt_name in self.opt_names)) + + def authenticate(self, http_client): + """Authenticate using plugin defined method. + + The method usually analyses `self.opts` and performs + a request to authentication server. + + :param http_client: client object that needs authentication + :type http_client: HTTPClient + :raises: AuthorizationFailure + """ + self.sufficient_options() + self._do_authenticate(http_client) + + @abc.abstractmethod + def _do_authenticate(self, http_client): + """Protected method for authentication. + """ + + def sufficient_options(self): + """Check if all required options are present. + + :raises: AuthPluginOptionsMissing + """ + missing = [opt + for opt in self.opt_names + if not self.opts.get(opt)] + if missing: + raise exceptions.AuthPluginOptionsMissing(missing) + + @abc.abstractmethod + def token_and_endpoint(self, endpoint_type, service_type): + """Return token and endpoint. + + :param service_type: Service type of the endpoint + :type service_type: string + :param endpoint_type: Type of endpoint. + Possible values: public or publicURL, + internal or internalURL, + admin or adminURL + :type endpoint_type: string + :returns: tuple of token and endpoint strings + :raises: EndpointException + """ diff --git a/novaclient/openstack/common/apiclient/base.py b/novaclient/openstack/common/apiclient/base.py new file mode 100644 index 000000000..dcb4a000c --- /dev/null +++ b/novaclient/openstack/common/apiclient/base.py @@ -0,0 +1,491 @@ +# Copyright 2010 Jacob Kaplan-Moss +# Copyright 2011 OpenStack Foundation +# Copyright 2012 Grid Dynamics +# Copyright 2013 OpenStack Foundation +# All Rights Reserved. +# +# 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. + +""" +Base utilities to build API operation managers and objects on top of. +""" + +# E1102: %s is not callable +# pylint: disable=E1102 + +import abc + +import six + +from novaclient.openstack.common.apiclient import exceptions +from novaclient.openstack.common.py3kcompat import urlutils +from novaclient.openstack.common import strutils + + +def getid(obj): + """Return id if argument is a Resource. + + Abstracts the common pattern of allowing both an object or an object's ID + (UUID) as a parameter when dealing with relationships. + """ + try: + if obj.uuid: + return obj.uuid + except AttributeError: + pass + try: + return obj.id + except AttributeError: + return obj + + +# TODO(aababilov): call run_hooks() in HookableMixin's child classes +class HookableMixin(object): + """Mixin so classes can register and run hooks.""" + _hooks_map = {} + + @classmethod + def add_hook(cls, hook_type, hook_func): + """Add a new hook of specified type. + + :param cls: class that registers hooks + :param hook_type: hook type, e.g., '__pre_parse_args__' + :param hook_func: hook function + """ + if hook_type not in cls._hooks_map: + cls._hooks_map[hook_type] = [] + + cls._hooks_map[hook_type].append(hook_func) + + @classmethod + def run_hooks(cls, hook_type, *args, **kwargs): + """Run all hooks of specified type. + + :param cls: class that registers hooks + :param hook_type: hook type, e.g., '__pre_parse_args__' + :param **args: args to be passed to every hook function + :param **kwargs: kwargs to be passed to every hook function + """ + hook_funcs = cls._hooks_map.get(hook_type) or [] + for hook_func in hook_funcs: + hook_func(*args, **kwargs) + + +class BaseManager(HookableMixin): + """Basic manager type providing common operations. + + Managers interact with a particular type of API (servers, flavors, images, + etc.) and provide CRUD operations for them. + """ + resource_class = None + + def __init__(self, client): + """Initializes BaseManager with `client`. + + :param client: instance of BaseClient descendant for HTTP requests + """ + super(BaseManager, self).__init__() + self.client = client + + def _list(self, url, response_key, obj_class=None, json=None): + """List the collection. + + :param url: a partial URL, e.g., '/servers' + :param response_key: the key to be looked up in response dictionary, + e.g., 'servers' + :param obj_class: class for constructing the returned objects + (self.resource_class will be used by default) + :param json: data that will be encoded as JSON and passed in POST + request (GET will be sent by default) + """ + if json: + body = self.client.post(url, json=json).json() + else: + body = self.client.get(url).json() + + if obj_class is None: + obj_class = self.resource_class + + data = body[response_key] + # NOTE(ja): keystone returns values as list as {'values': [ ... ]} + # unlike other services which just return the list... + try: + data = data['values'] + except (KeyError, TypeError): + pass + + return [obj_class(self, res, loaded=True) for res in data if res] + + def _get(self, url, response_key): + """Get an object from collection. + + :param url: a partial URL, e.g., '/servers' + :param response_key: the key to be looked up in response dictionary, + e.g., 'server' + """ + body = self.client.get(url).json() + return self.resource_class(self, body[response_key], loaded=True) + + def _head(self, url): + """Retrieve request headers for an object. + + :param url: a partial URL, e.g., '/servers' + """ + resp = self.client.head(url) + return resp.status_code == 204 + + def _post(self, url, json, response_key, return_raw=False): + """Create an object. + + :param url: a partial URL, e.g., '/servers' + :param json: data that will be encoded as JSON and passed in POST + request (GET will be sent by default) + :param response_key: the key to be looked up in response dictionary, + e.g., 'servers' + :param return_raw: flag to force returning raw JSON instead of + Python object of self.resource_class + """ + body = self.client.post(url, json=json).json() + if return_raw: + return body[response_key] + return self.resource_class(self, body[response_key]) + + def _put(self, url, json=None, response_key=None): + """Update an object with PUT method. + + :param url: a partial URL, e.g., '/servers' + :param json: data that will be encoded as JSON and passed in POST + request (GET will be sent by default) + :param response_key: the key to be looked up in response dictionary, + e.g., 'servers' + """ + resp = self.client.put(url, json=json) + # PUT requests may not return a body + if resp.content: + body = resp.json() + if response_key is not None: + return self.resource_class(self, body[response_key]) + else: + return self.resource_class(self, body) + + def _patch(self, url, json=None, response_key=None): + """Update an object with PATCH method. + + :param url: a partial URL, e.g., '/servers' + :param json: data that will be encoded as JSON and passed in POST + request (GET will be sent by default) + :param response_key: the key to be looked up in response dictionary, + e.g., 'servers' + """ + body = self.client.patch(url, json=json).json() + if response_key is not None: + return self.resource_class(self, body[response_key]) + else: + return self.resource_class(self, body) + + def _delete(self, url): + """Delete an object. + + :param url: a partial URL, e.g., '/servers/my-server' + """ + return self.client.delete(url) + + +@six.add_metaclass(abc.ABCMeta) +class ManagerWithFind(BaseManager): + """Manager with additional `find()`/`findall()` methods.""" + + @abc.abstractmethod + def list(self): + pass + + def find(self, **kwargs): + """Find a single item with attributes matching ``**kwargs``. + + This isn't very efficient: it loads the entire list then filters on + the Python side. + """ + matches = self.findall(**kwargs) + num_matches = len(matches) + if num_matches == 0: + msg = "No %s matching %s." % (self.resource_class.__name__, kwargs) + raise exceptions.NotFound(msg) + elif num_matches > 1: + raise exceptions.NoUniqueMatch() + else: + return matches[0] + + def findall(self, **kwargs): + """Find all items with attributes matching ``**kwargs``. + + This isn't very efficient: it loads the entire list then filters on + the Python side. + """ + found = [] + searches = kwargs.items() + + for obj in self.list(): + try: + if all(getattr(obj, attr) == value + for (attr, value) in searches): + found.append(obj) + except AttributeError: + continue + + return found + + +class CrudManager(BaseManager): + """Base manager class for manipulating entities. + + Children of this class are expected to define a `collection_key` and `key`. + + - `collection_key`: Usually a plural noun by convention (e.g. `entities`); + used to refer collections in both URL's (e.g. `/v3/entities`) and JSON + objects containing a list of member resources (e.g. `{'entities': [{}, + {}, {}]}`). + - `key`: Usually a singular noun by convention (e.g. `entity`); used to + refer to an individual member of the collection. + + """ + collection_key = None + key = None + + def build_url(self, base_url=None, **kwargs): + """Builds a resource URL for the given kwargs. + + Given an example collection where `collection_key = 'entities'` and + `key = 'entity'`, the following URL's could be generated. + + By default, the URL will represent a collection of entities, e.g.:: + + /entities + + If kwargs contains an `entity_id`, then the URL will represent a + specific member, e.g.:: + + /entities/{entity_id} + + :param base_url: if provided, the generated URL will be appended to it + """ + url = base_url if base_url is not None else '' + + url += '/%s' % self.collection_key + + # do we have a specific entity? + entity_id = kwargs.get('%s_id' % self.key) + if entity_id is not None: + url += '/%s' % entity_id + + return url + + def _filter_kwargs(self, kwargs): + """Drop null values and handle ids.""" + for key, ref in six.iteritems(kwargs.copy()): + if ref is None: + kwargs.pop(key) + else: + if isinstance(ref, Resource): + kwargs.pop(key) + kwargs['%s_id' % key] = getid(ref) + return kwargs + + def create(self, **kwargs): + kwargs = self._filter_kwargs(kwargs) + return self._post( + self.build_url(**kwargs), + {self.key: kwargs}, + self.key) + + def get(self, **kwargs): + kwargs = self._filter_kwargs(kwargs) + return self._get( + self.build_url(**kwargs), + self.key) + + def head(self, **kwargs): + kwargs = self._filter_kwargs(kwargs) + return self._head(self.build_url(**kwargs)) + + def list(self, base_url=None, **kwargs): + """List the collection. + + :param base_url: if provided, the generated URL will be appended to it + """ + kwargs = self._filter_kwargs(kwargs) + + return self._list( + '%(base_url)s%(query)s' % { + 'base_url': self.build_url(base_url=base_url, **kwargs), + 'query': '?%s' % urlutils.urlencode(kwargs) if kwargs else '', + }, + self.collection_key) + + def put(self, base_url=None, **kwargs): + """Update an element. + + :param base_url: if provided, the generated URL will be appended to it + """ + kwargs = self._filter_kwargs(kwargs) + + return self._put(self.build_url(base_url=base_url, **kwargs)) + + def update(self, **kwargs): + kwargs = self._filter_kwargs(kwargs) + params = kwargs.copy() + params.pop('%s_id' % self.key) + + return self._patch( + self.build_url(**kwargs), + {self.key: params}, + self.key) + + def delete(self, **kwargs): + kwargs = self._filter_kwargs(kwargs) + + return self._delete( + self.build_url(**kwargs)) + + def find(self, base_url=None, **kwargs): + """Find a single item with attributes matching ``**kwargs``. + + :param base_url: if provided, the generated URL will be appended to it + """ + kwargs = self._filter_kwargs(kwargs) + + rl = self._list( + '%(base_url)s%(query)s' % { + 'base_url': self.build_url(base_url=base_url, **kwargs), + 'query': '?%s' % urlutils.urlencode(kwargs) if kwargs else '', + }, + self.collection_key) + num = len(rl) + + if num == 0: + msg = "No %s matching %s." % (self.resource_class.__name__, kwargs) + raise exceptions.NotFound(404, msg) + elif num > 1: + raise exceptions.NoUniqueMatch + else: + return rl[0] + + +class Extension(HookableMixin): + """Extension descriptor.""" + + SUPPORTED_HOOKS = ('__pre_parse_args__', '__post_parse_args__') + manager_class = None + + def __init__(self, name, module): + super(Extension, self).__init__() + self.name = name + self.module = module + self._parse_extension_module() + + def _parse_extension_module(self): + self.manager_class = None + for attr_name, attr_value in self.module.__dict__.items(): + if attr_name in self.SUPPORTED_HOOKS: + self.add_hook(attr_name, attr_value) + else: + try: + if issubclass(attr_value, BaseManager): + self.manager_class = attr_value + except TypeError: + pass + + def __repr__(self): + return "" % self.name + + +class Resource(object): + """Base class for OpenStack resources (tenant, user, etc.). + + This is pretty much just a bag for attributes. + """ + + HUMAN_ID = False + NAME_ATTR = 'name' + + def __init__(self, manager, info, loaded=False): + """Populate and bind to a manager. + + :param manager: BaseManager object + :param info: dictionary representing resource attributes + :param loaded: prevent lazy-loading if set to True + """ + self.manager = manager + self._info = info + self._add_details(info) + self._loaded = loaded + + def __repr__(self): + reprkeys = sorted(k + for k in self.__dict__.keys() + if k[0] != '_' and k != 'manager') + info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys) + return "<%s %s>" % (self.__class__.__name__, info) + + @property + def human_id(self): + """Human-readable ID which can be used for bash completion. + """ + if self.NAME_ATTR in self.__dict__ and self.HUMAN_ID: + return strutils.to_slug(getattr(self, self.NAME_ATTR)) + return None + + def _add_details(self, info): + for (k, v) in six.iteritems(info): + try: + setattr(self, k, v) + self._info[k] = v + except AttributeError: + # In this case we already defined the attribute on the class + pass + + def __getattr__(self, k): + if k not in self.__dict__: + #NOTE(bcwaldon): disallow lazy-loading if already loaded once + if not self.is_loaded(): + self.get() + return self.__getattr__(k) + + raise AttributeError(k) + else: + return self.__dict__[k] + + def get(self): + # set_loaded() first ... so if we have to bail, we know we tried. + self.set_loaded(True) + if not hasattr(self.manager, 'get'): + return + + new = self.manager.get(self.id) + if new: + self._add_details(new._info) + + def __eq__(self, other): + if not isinstance(other, Resource): + return NotImplemented + # two resources of different types are not equal + if not isinstance(other, self.__class__): + return False + if hasattr(self, 'id') and hasattr(other, 'id'): + return self.id == other.id + return self._info == other._info + + def is_loaded(self): + return self._loaded + + def set_loaded(self, val): + self._loaded = val diff --git a/novaclient/openstack/common/apiclient/client.py b/novaclient/openstack/common/apiclient/client.py new file mode 100644 index 000000000..f573c7b52 --- /dev/null +++ b/novaclient/openstack/common/apiclient/client.py @@ -0,0 +1,358 @@ +# Copyright 2010 Jacob Kaplan-Moss +# Copyright 2011 OpenStack Foundation +# Copyright 2011 Piston Cloud Computing, Inc. +# Copyright 2013 Alessio Ababilov +# Copyright 2013 Grid Dynamics +# Copyright 2013 OpenStack Foundation +# All Rights Reserved. +# +# 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. + +""" +OpenStack Client interface. Handles the REST calls and responses. +""" + +# E0202: An attribute inherited from %s hide this method +# pylint: disable=E0202 + +import logging +import time + +try: + import simplejson as json +except ImportError: + import json + +import requests + +from novaclient.openstack.common.apiclient import exceptions +from novaclient.openstack.common import importutils + + +_logger = logging.getLogger(__name__) + + +class HTTPClient(object): + """This client handles sending HTTP requests to OpenStack servers. + + Features: + - share authentication information between several clients to different + services (e.g., for compute and image clients); + - reissue authentication request for expired tokens; + - encode/decode JSON bodies; + - raise exceptions on HTTP errors; + - pluggable authentication; + - store authentication information in a keyring; + - store time spent for requests; + - register clients for particular services, so one can use + `http_client.identity` or `http_client.compute`; + - log requests and responses in a format that is easy to copy-and-paste + into terminal and send the same request with curl. + """ + + user_agent = "novaclient.openstack.common.apiclient" + + def __init__(self, + auth_plugin, + region_name=None, + endpoint_type="publicURL", + original_ip=None, + verify=True, + cert=None, + timeout=None, + timings=False, + keyring_saver=None, + debug=False, + user_agent=None, + http=None): + self.auth_plugin = auth_plugin + + self.endpoint_type = endpoint_type + self.region_name = region_name + + self.original_ip = original_ip + self.timeout = timeout + self.verify = verify + self.cert = cert + + self.keyring_saver = keyring_saver + self.debug = debug + self.user_agent = user_agent or self.user_agent + + self.times = [] # [("item", starttime, endtime), ...] + self.timings = timings + + # requests within the same session can reuse TCP connections from pool + self.http = http or requests.Session() + + self.cached_token = None + + def _http_log_req(self, method, url, kwargs): + if not self.debug: + return + + string_parts = [ + "curl -i", + "-X '%s'" % method, + "'%s'" % url, + ] + + for element in kwargs['headers']: + header = "-H '%s: %s'" % (element, kwargs['headers'][element]) + string_parts.append(header) + + _logger.debug("REQ: %s" % " ".join(string_parts)) + if 'data' in kwargs: + _logger.debug("REQ BODY: %s\n" % (kwargs['data'])) + + def _http_log_resp(self, resp): + if not self.debug: + return + _logger.debug( + "RESP: [%s] %s\n", + resp.status_code, + resp.headers) + if resp._content_consumed: + _logger.debug( + "RESP BODY: %s\n", + resp.text) + + def serialize(self, kwargs): + if kwargs.get('json') is not None: + kwargs['headers']['Content-Type'] = 'application/json' + kwargs['data'] = json.dumps(kwargs['json']) + try: + del kwargs['json'] + except KeyError: + pass + + def get_timings(self): + return self.times + + def reset_timings(self): + self.times = [] + + def request(self, method, url, **kwargs): + """Send an http request with the specified characteristics. + + Wrapper around `requests.Session.request` to handle tasks such as + setting headers, JSON encoding/decoding, and error handling. + + :param method: method of HTTP request + :param url: URL of HTTP request + :param kwargs: any other parameter that can be passed to +' requests.Session.request (such as `headers`) or `json` + that will be encoded as JSON and used as `data` argument + """ + kwargs.setdefault("headers", kwargs.get("headers", {})) + kwargs["headers"]["User-Agent"] = self.user_agent + if self.original_ip: + kwargs["headers"]["Forwarded"] = "for=%s;by=%s" % ( + self.original_ip, self.user_agent) + if self.timeout is not None: + kwargs.setdefault("timeout", self.timeout) + kwargs.setdefault("verify", self.verify) + if self.cert is not None: + kwargs.setdefault("cert", self.cert) + self.serialize(kwargs) + + self._http_log_req(method, url, kwargs) + if self.timings: + start_time = time.time() + resp = self.http.request(method, url, **kwargs) + if self.timings: + self.times.append(("%s %s" % (method, url), + start_time, time.time())) + self._http_log_resp(resp) + + if resp.status_code >= 400: + _logger.debug( + "Request returned failure status: %s", + resp.status_code) + raise exceptions.from_response(resp, method, url) + + return resp + + @staticmethod + def concat_url(endpoint, url): + """Concatenate endpoint and final URL. + + E.g., "http://keystone/v2.0/" and "/tokens" are concatenated to + "http://keystone/v2.0/tokens". + + :param endpoint: the base URL + :param url: the final URL + """ + return "%s/%s" % (endpoint.rstrip("/"), url.strip("/")) + + def client_request(self, client, method, url, **kwargs): + """Send an http request using `client`'s endpoint and specified `url`. + + If request was rejected as unauthorized (possibly because the token is + expired), issue one authorization attempt and send the request once + again. + + :param client: instance of BaseClient descendant + :param method: method of HTTP request + :param url: URL of HTTP request + :param kwargs: any other parameter that can be passed to +' `HTTPClient.request` + """ + + filter_args = { + "endpoint_type": client.endpoint_type or self.endpoint_type, + "service_type": client.service_type, + } + token, endpoint = (self.cached_token, client.cached_endpoint) + just_authenticated = False + if not (token and endpoint): + try: + token, endpoint = self.auth_plugin.token_and_endpoint( + **filter_args) + except exceptions.EndpointException: + pass + if not (token and endpoint): + self.authenticate() + just_authenticated = True + token, endpoint = self.auth_plugin.token_and_endpoint( + **filter_args) + if not (token and endpoint): + raise exceptions.AuthorizationFailure( + "Cannot find endpoint or token for request") + + old_token_endpoint = (token, endpoint) + kwargs.setdefault("headers", {})["X-Auth-Token"] = token + self.cached_token = token + client.cached_endpoint = endpoint + # Perform the request once. If we get Unauthorized, then it + # might be because the auth token expired, so try to + # re-authenticate and try again. If it still fails, bail. + try: + return self.request( + method, self.concat_url(endpoint, url), **kwargs) + except exceptions.Unauthorized as unauth_ex: + if just_authenticated: + raise + self.cached_token = None + client.cached_endpoint = None + self.authenticate() + try: + token, endpoint = self.auth_plugin.token_and_endpoint( + **filter_args) + except exceptions.EndpointException: + raise unauth_ex + if (not (token and endpoint) or + old_token_endpoint == (token, endpoint)): + raise unauth_ex + self.cached_token = token + client.cached_endpoint = endpoint + kwargs["headers"]["X-Auth-Token"] = token + return self.request( + method, self.concat_url(endpoint, url), **kwargs) + + def add_client(self, base_client_instance): + """Add a new instance of :class:`BaseClient` descendant. + + `self` will store a reference to `base_client_instance`. + + Example: + + >>> def test_clients(): + ... from keystoneclient.auth import keystone + ... from openstack.common.apiclient import client + ... auth = keystone.KeystoneAuthPlugin( + ... username="user", password="pass", tenant_name="tenant", + ... auth_url="http://auth:5000/v2.0") + ... openstack_client = client.HTTPClient(auth) + ... # create nova client + ... from novaclient.v1_1 import client + ... client.Client(openstack_client) + ... # create keystone client + ... from keystoneclient.v2_0 import client + ... client.Client(openstack_client) + ... # use them + ... openstack_client.identity.tenants.list() + ... openstack_client.compute.servers.list() + """ + service_type = base_client_instance.service_type + if service_type and not hasattr(self, service_type): + setattr(self, service_type, base_client_instance) + + def authenticate(self): + self.auth_plugin.authenticate(self) + # Store the authentication results in the keyring for later requests + if self.keyring_saver: + self.keyring_saver.save(self) + + +class BaseClient(object): + """Top-level object to access the OpenStack API. + + This client uses :class:`HTTPClient` to send requests. :class:`HTTPClient` + will handle a bunch of issues such as authentication. + """ + + service_type = None + endpoint_type = None # "publicURL" will be used + cached_endpoint = None + + def __init__(self, http_client, extensions=None): + self.http_client = http_client + http_client.add_client(self) + + # Add in any extensions... + if extensions: + for extension in extensions: + if extension.manager_class: + setattr(self, extension.name, + extension.manager_class(self)) + + def client_request(self, method, url, **kwargs): + return self.http_client.client_request( + self, method, url, **kwargs) + + def head(self, url, **kwargs): + return self.client_request("HEAD", url, **kwargs) + + def get(self, url, **kwargs): + return self.client_request("GET", url, **kwargs) + + def post(self, url, **kwargs): + return self.client_request("POST", url, **kwargs) + + def put(self, url, **kwargs): + return self.client_request("PUT", url, **kwargs) + + def delete(self, url, **kwargs): + return self.client_request("DELETE", url, **kwargs) + + def patch(self, url, **kwargs): + return self.client_request("PATCH", url, **kwargs) + + @staticmethod + def get_class(api_name, version, version_map): + """Returns the client class for the requested API version + + :param api_name: the name of the API, e.g. 'compute', 'image', etc + :param version: the requested API version + :param version_map: a dict of client classes keyed by version + :rtype: a client class for the requested API version + """ + try: + client_path = version_map[str(version)] + except (KeyError, ValueError): + msg = "Invalid %s client version '%s'. must be one of: %s" % ( + (api_name, version, ', '.join(version_map.keys()))) + raise exceptions.UnsupportedVersion(msg) + + return importutils.import_class(client_path) diff --git a/novaclient/openstack/common/apiclient/exceptions.py b/novaclient/openstack/common/apiclient/exceptions.py new file mode 100644 index 000000000..b364d60dc --- /dev/null +++ b/novaclient/openstack/common/apiclient/exceptions.py @@ -0,0 +1,439 @@ +# Copyright 2010 Jacob Kaplan-Moss +# Copyright 2011 Nebula, Inc. +# Copyright 2013 Alessio Ababilov +# Copyright 2013 OpenStack Foundation +# All Rights Reserved. +# +# 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. + +""" +Exception definitions. +""" + +import inspect +import sys + +import six + + +class ClientException(Exception): + """The base exception class for all exceptions this library raises. + """ + pass + + +class MissingArgs(ClientException): + """Supplied arguments are not sufficient for calling a function.""" + def __init__(self, missing): + self.missing = missing + msg = "Missing argument(s): %s" % ", ".join(missing) + super(MissingArgs, self).__init__(msg) + + +class ValidationError(ClientException): + """Error in validation on API client side.""" + pass + + +class UnsupportedVersion(ClientException): + """User is trying to use an unsupported version of the API.""" + pass + + +class CommandError(ClientException): + """Error in CLI tool.""" + pass + + +class AuthorizationFailure(ClientException): + """Cannot authorize API client.""" + pass + + +class AuthPluginOptionsMissing(AuthorizationFailure): + """Auth plugin misses some options.""" + def __init__(self, opt_names): + super(AuthPluginOptionsMissing, self).__init__( + "Authentication failed. Missing options: %s" % + ", ".join(opt_names)) + self.opt_names = opt_names + + +class AuthSystemNotFound(AuthorizationFailure): + """User has specified a AuthSystem that is not installed.""" + def __init__(self, auth_system): + super(AuthSystemNotFound, self).__init__( + "AuthSystemNotFound: %s" % repr(auth_system)) + self.auth_system = auth_system + + +class NoUniqueMatch(ClientException): + """Multiple entities found instead of one.""" + pass + + +class EndpointException(ClientException): + """Something is rotten in Service Catalog.""" + pass + + +class EndpointNotFound(EndpointException): + """Could not find requested endpoint in Service Catalog.""" + pass + + +class AmbiguousEndpoints(EndpointException): + """Found more than one matching endpoint in Service Catalog.""" + def __init__(self, endpoints=None): + super(AmbiguousEndpoints, self).__init__( + "AmbiguousEndpoints: %s" % repr(endpoints)) + self.endpoints = endpoints + + +class HttpError(ClientException): + """The base exception class for all HTTP exceptions. + """ + http_status = 0 + message = "HTTP Error" + + def __init__(self, message=None, details=None, + response=None, request_id=None, + url=None, method=None, http_status=None): + self.http_status = http_status or self.http_status + self.message = message or self.message + self.details = details + self.request_id = request_id + self.response = response + self.url = url + self.method = method + formatted_string = "%s (HTTP %s)" % (self.message, self.http_status) + if request_id: + formatted_string += " (Request-ID: %s)" % request_id + super(HttpError, self).__init__(formatted_string) + + +class HTTPClientError(HttpError): + """Client-side HTTP error. + + Exception for cases in which the client seems to have erred. + """ + message = "HTTP Client Error" + + +class HttpServerError(HttpError): + """Server-side HTTP error. + + Exception for cases in which the server is aware that it has + erred or is incapable of performing the request. + """ + message = "HTTP Server Error" + + +class BadRequest(HTTPClientError): + """HTTP 400 - Bad Request. + + The request cannot be fulfilled due to bad syntax. + """ + http_status = 400 + message = "Bad Request" + + +class Unauthorized(HTTPClientError): + """HTTP 401 - Unauthorized. + + Similar to 403 Forbidden, but specifically for use when authentication + is required and has failed or has not yet been provided. + """ + http_status = 401 + message = "Unauthorized" + + +class PaymentRequired(HTTPClientError): + """HTTP 402 - Payment Required. + + Reserved for future use. + """ + http_status = 402 + message = "Payment Required" + + +class Forbidden(HTTPClientError): + """HTTP 403 - Forbidden. + + The request was a valid request, but the server is refusing to respond + to it. + """ + http_status = 403 + message = "Forbidden" + + +class NotFound(HTTPClientError): + """HTTP 404 - Not Found. + + The requested resource could not be found but may be available again + in the future. + """ + http_status = 404 + message = "Not Found" + + +class MethodNotAllowed(HTTPClientError): + """HTTP 405 - Method Not Allowed. + + A request was made of a resource using a request method not supported + by that resource. + """ + http_status = 405 + message = "Method Not Allowed" + + +class NotAcceptable(HTTPClientError): + """HTTP 406 - Not Acceptable. + + The requested resource is only capable of generating content not + acceptable according to the Accept headers sent in the request. + """ + http_status = 406 + message = "Not Acceptable" + + +class ProxyAuthenticationRequired(HTTPClientError): + """HTTP 407 - Proxy Authentication Required. + + The client must first authenticate itself with the proxy. + """ + http_status = 407 + message = "Proxy Authentication Required" + + +class RequestTimeout(HTTPClientError): + """HTTP 408 - Request Timeout. + + The server timed out waiting for the request. + """ + http_status = 408 + message = "Request Timeout" + + +class Conflict(HTTPClientError): + """HTTP 409 - Conflict. + + Indicates that the request could not be processed because of conflict + in the request, such as an edit conflict. + """ + http_status = 409 + message = "Conflict" + + +class Gone(HTTPClientError): + """HTTP 410 - Gone. + + Indicates that the resource requested is no longer available and will + not be available again. + """ + http_status = 410 + message = "Gone" + + +class LengthRequired(HTTPClientError): + """HTTP 411 - Length Required. + + The request did not specify the length of its content, which is + required by the requested resource. + """ + http_status = 411 + message = "Length Required" + + +class PreconditionFailed(HTTPClientError): + """HTTP 412 - Precondition Failed. + + The server does not meet one of the preconditions that the requester + put on the request. + """ + http_status = 412 + message = "Precondition Failed" + + +class RequestEntityTooLarge(HTTPClientError): + """HTTP 413 - Request Entity Too Large. + + The request is larger than the server is willing or able to process. + """ + http_status = 413 + message = "Request Entity Too Large" + + def __init__(self, *args, **kwargs): + try: + self.retry_after = int(kwargs.pop('retry_after')) + except (KeyError, ValueError): + self.retry_after = 0 + + super(RequestEntityTooLarge, self).__init__(*args, **kwargs) + + +class RequestUriTooLong(HTTPClientError): + """HTTP 414 - Request-URI Too Long. + + The URI provided was too long for the server to process. + """ + http_status = 414 + message = "Request-URI Too Long" + + +class UnsupportedMediaType(HTTPClientError): + """HTTP 415 - Unsupported Media Type. + + The request entity has a media type which the server or resource does + not support. + """ + http_status = 415 + message = "Unsupported Media Type" + + +class RequestedRangeNotSatisfiable(HTTPClientError): + """HTTP 416 - Requested Range Not Satisfiable. + + The client has asked for a portion of the file, but the server cannot + supply that portion. + """ + http_status = 416 + message = "Requested Range Not Satisfiable" + + +class ExpectationFailed(HTTPClientError): + """HTTP 417 - Expectation Failed. + + The server cannot meet the requirements of the Expect request-header field. + """ + http_status = 417 + message = "Expectation Failed" + + +class UnprocessableEntity(HTTPClientError): + """HTTP 422 - Unprocessable Entity. + + The request was well-formed but was unable to be followed due to semantic + errors. + """ + http_status = 422 + message = "Unprocessable Entity" + + +class InternalServerError(HttpServerError): + """HTTP 500 - Internal Server Error. + + A generic error message, given when no more specific message is suitable. + """ + http_status = 500 + message = "Internal Server Error" + + +# NotImplemented is a python keyword. +class HttpNotImplemented(HttpServerError): + """HTTP 501 - Not Implemented. + + The server either does not recognize the request method, or it lacks + the ability to fulfill the request. + """ + http_status = 501 + message = "Not Implemented" + + +class BadGateway(HttpServerError): + """HTTP 502 - Bad Gateway. + + The server was acting as a gateway or proxy and received an invalid + response from the upstream server. + """ + http_status = 502 + message = "Bad Gateway" + + +class ServiceUnavailable(HttpServerError): + """HTTP 503 - Service Unavailable. + + The server is currently unavailable. + """ + http_status = 503 + message = "Service Unavailable" + + +class GatewayTimeout(HttpServerError): + """HTTP 504 - Gateway Timeout. + + The server was acting as a gateway or proxy and did not receive a timely + response from the upstream server. + """ + http_status = 504 + message = "Gateway Timeout" + + +class HttpVersionNotSupported(HttpServerError): + """HTTP 505 - HttpVersion Not Supported. + + The server does not support the HTTP protocol version used in the request. + """ + http_status = 505 + message = "HTTP Version Not Supported" + + +# _code_map contains all the classes that have http_status attribute. +_code_map = dict( + (getattr(obj, 'http_status', None), obj) + for name, obj in six.iteritems(vars(sys.modules[__name__])) + if inspect.isclass(obj) and getattr(obj, 'http_status', False) +) + + +def from_response(response, method, url): + """Returns an instance of :class:`HttpError` or subclass based on response. + + :param response: instance of `requests.Response` class + :param method: HTTP method used for request + :param url: URL used for request + """ + kwargs = { + "http_status": response.status_code, + "response": response, + "method": method, + "url": url, + "request_id": response.headers.get("x-compute-request-id"), + } + if "retry-after" in response.headers: + kwargs["retry_after"] = response.headers["retry-after"] + + content_type = response.headers.get("Content-Type", "") + if content_type.startswith("application/json"): + try: + body = response.json() + except ValueError: + pass + else: + if hasattr(body, "keys"): + error = body[body.keys()[0]] + kwargs["message"] = error.get("message", None) + kwargs["details"] = error.get("details", None) + elif content_type.startswith("text/"): + kwargs["details"] = response.text + + try: + cls = _code_map[response.status_code] + except KeyError: + if 500 <= response.status_code < 600: + cls = HttpServerError + elif 400 <= response.status_code < 500: + cls = HTTPClientError + else: + cls = HttpError + return cls(**kwargs) diff --git a/novaclient/openstack/common/apiclient/fake_client.py b/novaclient/openstack/common/apiclient/fake_client.py new file mode 100644 index 000000000..05f550b91 --- /dev/null +++ b/novaclient/openstack/common/apiclient/fake_client.py @@ -0,0 +1,174 @@ +# Copyright 2013 OpenStack Foundation +# All Rights Reserved. +# +# 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. + +""" +A fake server that "responds" to API methods with pre-canned responses. + +All of these responses come from the spec, so if for some reason the spec's +wrong the tests might raise AssertionError. I've indicated in comments the +places where actual behavior differs from the spec. +""" + +# W0102: Dangerous default value %s as argument +# pylint: disable=W0102 + +import json + +import requests +import six + +from novaclient.openstack.common.apiclient import client +from novaclient.openstack.common.py3kcompat import urlutils +from novaclient.openstack.common import strutils + + +def assert_has_keys(dct, required=[], optional=[]): + for k in required: + try: + assert k in dct + except AssertionError: + extra_keys = set(dct.keys()).difference(set(required + optional)) + raise AssertionError("found unexpected keys: %s" % + list(extra_keys)) + + +class TestResponse(requests.Response): + """Wrap requests.Response and provide a convenient initialization. + """ + + def __init__(self, data): + super(TestResponse, self).__init__() + self._content_consumed = True + if isinstance(data, dict): + self.status_code = data.get('status_code', 200) + # Fake the text attribute to streamline Response creation + text = data.get('text', "") + if isinstance(text, (dict, list)): + self._content = json.dumps(text) + default_headers = { + "Content-Type": "application/json", + } + else: + self._content = text + default_headers = {} + if six.PY3 and isinstance(self._content, six.string_types): + self._content = strutils.safe_encode(self._content) + self.headers = data.get('headers') or default_headers + else: + self.status_code = data + + def __eq__(self, other): + return (self.status_code == other.status_code and + self.headers == other.headers and + self._content == other._content) + + +class FakeHTTPClient(client.HTTPClient): + + def __init__(self, *args, **kwargs): + self.callstack = [] + self.fixtures = kwargs.pop("fixtures", None) or {} + if not args and not "auth_plugin" in kwargs: + args = (None, ) + super(FakeHTTPClient, self).__init__(*args, **kwargs) + + def assert_called(self, method, url, body=None, pos=-1): + """Assert than an API method was just called. + """ + expected = (method, url) + called = self.callstack[pos][0:2] + assert self.callstack, \ + "Expected %s %s but no calls were made." % expected + + assert expected == called, 'Expected %s %s; got %s %s' % \ + (expected + called) + + if body is not None: + if self.callstack[pos][3] != body: + raise AssertionError('%r != %r' % + (self.callstack[pos][3], body)) + + def assert_called_anytime(self, method, url, body=None): + """Assert than an API method was called anytime in the test. + """ + expected = (method, url) + + assert self.callstack, \ + "Expected %s %s but no calls were made." % expected + + found = False + entry = None + for entry in self.callstack: + if expected == entry[0:2]: + found = True + break + + assert found, 'Expected %s %s; got %s' % \ + (method, url, self.callstack) + if body is not None: + assert entry[3] == body, "%s != %s" % (entry[3], body) + + self.callstack = [] + + def clear_callstack(self): + self.callstack = [] + + def authenticate(self): + pass + + def client_request(self, client, method, url, **kwargs): + # Check that certain things are called correctly + if method in ["GET", "DELETE"]: + assert "json" not in kwargs + + # Note the call + self.callstack.append( + (method, + url, + kwargs.get("headers") or {}, + kwargs.get("json") or kwargs.get("data"))) + try: + fixture = self.fixtures[url][method] + except KeyError: + pass + else: + return TestResponse({"headers": fixture[0], + "text": fixture[1]}) + + # Call the method + args = urlutils.parse_qsl(urlutils.urlparse(url)[4]) + kwargs.update(args) + munged_url = url.rsplit('?', 1)[0] + munged_url = munged_url.strip('/').replace('/', '_').replace('.', '_') + munged_url = munged_url.replace('-', '_') + + callback = "%s_%s" % (method.lower(), munged_url) + + if not hasattr(self, callback): + raise AssertionError('Called unknown API method: %s %s, ' + 'expected fakes method name: %s' % + (method, url, callback)) + + resp = getattr(self, callback)(**kwargs) + if len(resp) == 3: + status, headers, body = resp + else: + status, body = resp + headers = {} + return TestResponse({ + "status_code": status, + "text": body, + "headers": headers, + }) diff --git a/openstack-common.conf b/openstack-common.conf index e7fe529f1..7d478d03f 100644 --- a/openstack-common.conf +++ b/openstack-common.conf @@ -7,6 +7,8 @@ module=strutils module=timeutils module=uuidutils module=py3kcompat +module=apiclient +module=importutils # The base module to hold the copy of openstack.common base=novaclient From 871c5fc1bee31ff9c06b778d468d209b91daa5fe Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Fri, 27 Dec 2013 16:29:23 +0200 Subject: [PATCH 0387/1705] Sync cliutils from oslo In the process of unification of the clients code we should reuse common functionality from Oslo. Related to blueprint common-client-library-2 Change-Id: Ia5e4e60f07561849f75d88b8a2ea3d23d6d5ff6d --- novaclient/openstack/common/cliutils.py | 213 ++++++++++++++++++++++++ openstack-common.conf | 1 + 2 files changed, 214 insertions(+) create mode 100644 novaclient/openstack/common/cliutils.py diff --git a/novaclient/openstack/common/cliutils.py b/novaclient/openstack/common/cliutils.py new file mode 100644 index 000000000..907159886 --- /dev/null +++ b/novaclient/openstack/common/cliutils.py @@ -0,0 +1,213 @@ +# Copyright 2012 Red Hat, Inc. +# +# 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. + +# W0603: Using the global statement +# W0621: Redefining name %s from outer scope +# pylint: disable=W0603,W0621 + +import getpass +import inspect +import os +import sys +import textwrap + +import prettytable +import six +from six import moves + +from novaclient.openstack.common.apiclient import exceptions +from novaclient.openstack.common import strutils + + +def validate_args(fn, *args, **kwargs): + """Check that the supplied args are sufficient for calling a function. + + >>> validate_args(lambda a: None) + Traceback (most recent call last): + ... + MissingArgs: Missing argument(s): a + >>> validate_args(lambda a, b, c, d: None, 0, c=1) + Traceback (most recent call last): + ... + MissingArgs: Missing argument(s): b, d + + :param fn: the function to check + :param arg: the positional arguments supplied + :param kwargs: the keyword arguments supplied + """ + argspec = inspect.getargspec(fn) + + num_defaults = len(argspec.defaults or []) + required_args = argspec.args[:len(argspec.args) - num_defaults] + + def isbound(method): + return getattr(method, 'im_self', None) is not None + + if isbound(fn): + required_args.pop(0) + + missing = [arg for arg in required_args if arg not in kwargs] + missing = missing[len(args):] + if missing: + raise exceptions.MissingArgs(missing) + + +def arg(*args, **kwargs): + """Decorator for CLI args. + + Example: + + >>> @arg("name", help="Name of the new entity") + ... def entity_create(args): + ... pass + """ + def _decorator(func): + add_arg(func, *args, **kwargs) + return func + return _decorator + + +def env(*args, **kwargs): + """Returns the first environment variable set. + + If all are empty, defaults to '' or keyword arg `default`. + """ + for arg in args: + value = os.environ.get(arg, None) + if value: + return value + return kwargs.get('default', '') + + +def add_arg(func, *args, **kwargs): + """Bind CLI arguments to a shell.py `do_foo` function.""" + + if not hasattr(func, 'arguments'): + func.arguments = [] + + # NOTE(sirp): avoid dups that can occur when the module is shared across + # tests. + if (args, kwargs) not in func.arguments: + # Because of the semantics of decorator composition if we just append + # to the options list positional options will appear to be backwards. + func.arguments.insert(0, (args, kwargs)) + + +def unauthenticated(func): + """Adds 'unauthenticated' attribute to decorated function. + + Usage: + + >>> @unauthenticated + ... def mymethod(f): + ... pass + """ + func.unauthenticated = True + return func + + +def isunauthenticated(func): + """Checks if the function does not require authentication. + + Mark such functions with the `@unauthenticated` decorator. + + :returns: bool + """ + return getattr(func, 'unauthenticated', False) + + +def print_list(objs, fields, formatters=None, sortby_index=0, + mixed_case_fields=None): + """Print a list or objects as a table, one row per object. + + :param objs: iterable of :class:`Resource` + :param fields: attributes that correspond to columns, in order + :param formatters: `dict` of callables for field formatting + :param sortby_index: index of the field for sorting table rows + :param mixed_case_fields: fields corresponding to object attributes that + have mixed case names (e.g., 'serverId') + """ + formatters = formatters or {} + mixed_case_fields = mixed_case_fields or [] + if sortby_index is None: + kwargs = {} + else: + kwargs = {'sortby': fields[sortby_index]} + pt = prettytable.PrettyTable(fields, caching=False) + pt.align = 'l' + + for o in objs: + row = [] + for field in fields: + if field in formatters: + row.append(formatters[field](o)) + else: + if field in mixed_case_fields: + field_name = field.replace(' ', '_') + else: + field_name = field.lower().replace(' ', '_') + data = getattr(o, field_name, '') + row.append(data) + pt.add_row(row) + + print(strutils.safe_encode(pt.get_string(**kwargs))) + + +def print_dict(dct, dict_property="Property", wrap=0): + """Print a `dict` as a table of two columns. + + :param dct: `dict` to print + :param dict_property: name of the first column + :param wrap: wrapping for the second column + """ + pt = prettytable.PrettyTable([dict_property, 'Value'], caching=False) + pt.align = 'l' + for k, v in six.iteritems(dct): + # convert dict to str to check length + if isinstance(v, dict): + v = str(v) + if wrap > 0: + v = textwrap.fill(str(v), wrap) + # if value has a newline, add in multiple rows + # e.g. fault with stacktrace + if v and isinstance(v, six.string_types) and r'\n' in v: + lines = v.strip().split(r'\n') + col1 = k + for line in lines: + pt.add_row([col1, line]) + col1 = '' + else: + pt.add_row([k, v]) + print(strutils.safe_encode(pt.get_string())) + + +def get_password(max_password_prompts=3): + """Read password from TTY.""" + verify = strutils.bool_from_string(env("OS_VERIFY_PASSWORD")) + pw = None + if hasattr(sys.stdin, "isatty") and sys.stdin.isatty(): + # Check for Ctrl-D + try: + for _ in moves.range(max_password_prompts): + pw1 = getpass.getpass("OS Password: ") + if verify: + pw2 = getpass.getpass("Please verify: ") + else: + pw2 = pw1 + if pw1 == pw2 and pw1: + pw = pw1 + break + except EOFError: + pass + return pw diff --git a/openstack-common.conf b/openstack-common.conf index 7d478d03f..150814715 100644 --- a/openstack-common.conf +++ b/openstack-common.conf @@ -9,6 +9,7 @@ module=uuidutils module=py3kcompat module=apiclient module=importutils +module=cliutils # The base module to hold the copy of openstack.common base=novaclient From bd9ebc5d27f00f30b86b387bb191bfc82e0780f2 Mon Sep 17 00:00:00 2001 From: Alexis Lee Date: Thu, 19 Dec 2013 19:26:06 +0000 Subject: [PATCH 0388/1705] Don't call CS if a token + URL are provided Adds --os-auth-token (matching Glance client at least) to accept a pre-obtained authentication token. CS will be called iff: One or both of token and URL are not provided; AND the cache is empty/disabled or we don't have a tenant-id. Removed some code altering the auth request to a GET if a token is supplied - did not work for me and I think the path was dead previously. Fixed test to account for this change. Completes blueprint token-endpoint-instantiation Change-Id: I67410b80e506bb80c152223cd113b7139a62a536 --- novaclient/client.py | 42 ++++++----- novaclient/shell.py | 108 ++++++++++++++++++----------- novaclient/tests/test_client.py | 20 ++++++ novaclient/tests/v1_1/test_auth.py | 15 ++-- novaclient/v1_1/client.py | 3 +- novaclient/v3/client.py | 3 +- 6 files changed, 127 insertions(+), 64 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index 0ae8bb285..d89331322 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -48,13 +48,19 @@ def __init__(self, user, password, projectid=None, auth_url=None, timings=False, bypass_url=None, os_cache=False, no_cache=True, http_log_debug=False, auth_system='keystone', - auth_plugin=None, + auth_plugin=None, auth_token=None, cacert=None, tenant_id=None): self.user = user self.password = password self.projectid = projectid self.tenant_id = tenant_id + # This will be called by #_get_password if self.password is None. + # EG if a password can only be obtained by prompting the user, but a + # token is available, you don't want to prompt until the token has + # been proven invalid + self.password_func = None + if auth_system and auth_system != 'keystone' and not auth_plugin: raise exceptions.AuthSystemNotFound(auth_system) @@ -80,8 +86,8 @@ def __init__(self, user, password, projectid=None, auth_url=None, self.times = [] # [("item", starttime, endtime), ...] - self.management_url = None - self.auth_token = None + self.management_url = self.bypass_url or None + self.auth_token = auth_token self.proxy_token = proxy_token self.proxy_tenant_id = proxy_tenant_id self.keyring_saver = None @@ -239,6 +245,11 @@ def _cs_request(self, url, method, **kwargs): except exceptions.Unauthorized: raise e + def _get_password(self): + if not self.password and self.password_func: + self.password = self.password_func() + return self.password + def get(self, url, **kwargs): return self._cs_request(url, 'GET', **kwargs) @@ -324,6 +335,10 @@ def authenticate(self): self.version = part break + if self.auth_token and self.management_url: + self._save_keys() + return + # TODO(sandy): Assume admin endpoint is 35357 for now. # Ideally this is going to have to be provided by the service catalog. new_netloc = netloc.replace(':%d' % port, ':%d' % (35357,)) @@ -367,8 +382,13 @@ def authenticate(self): elif not self.management_url: raise exceptions.Unauthorized('Nova Client') + self._save_keys() + + def _save_keys(self): # Store the token/mgmt url in the keyring for later requests. - if self.keyring_saver and self.os_cache and not self.keyring_saved: + if (self.keyring_saver and self.os_cache and not self.keyring_saved + and self.auth_token and self.management_url + and self.tenant_id): self.keyring_saver.save(self.auth_token, self.management_url, self.tenant_id) @@ -380,7 +400,7 @@ def _v1_auth(self, url): raise exceptions.NoTokenLookupException() headers = {'X-Auth-User': self.user, - 'X-Auth-Key': self.password} + 'X-Auth-Key': self._get_password()} if self.projectid: headers['X-Auth-Project-Id'] = self.projectid @@ -409,7 +429,7 @@ def _v2_auth(self, url): else: body = {"auth": { "passwordCredentials": {"username": self.user, - "password": self.password}}} + "password": self._get_password()}}} if self.tenant_id: body['auth']['tenantId'] = self.tenant_id @@ -423,16 +443,6 @@ def _authenticate(self, url, body, **kwargs): method = "POST" token_url = url + "/tokens" - # if we have a valid auth token, use it instead of generating a new one - if self.auth_token: - kwargs.setdefault('headers', {})['X-Auth-Token'] = self.auth_token - token_url += "/" + self.auth_token - method = "GET" - body = None - - if self.auth_token and self.tenant_id and self.management_url: - return None - # Make sure we follow redirects when trying to reach Keystone resp, respbody = self._time_request( token_url, diff --git a/novaclient/shell.py b/novaclient/shell.py index c4a0c9ec3..a4254c929 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -82,6 +82,7 @@ def __init__(self, args, client): self.args = args self.client = client self.key = None + self._password = None def _validate_string(self, text): if text is None or len(text) == 0: @@ -143,11 +144,21 @@ def save(self, auth_token, management_url, tenant_id): @property def password(self): - if self._validate_string(self.args.os_password): - return self.args.os_password - verify_pass = strutils.bool_from_string( - utils.env("OS_VERIFY_PASSWORD", default=False), True) - return self._prompt_password(verify_pass) + # Cache password so we prompt user at most once + if self._password: + pass + elif self._validate_string(self.args.os_password): + self._password = self.args.os_password + else: + verify_pass = strutils.bool_from_string( + utils.env("OS_VERIFY_PASSWORD", default=False), True) + self._password = self._prompt_password(verify_pass) + if not self._password: + raise exc.CommandError( + 'Expecting a password provided via either ' + '--os-password, env[OS_PASSWORD], or ' + 'prompted response') + return self._password @property def management_url(self): @@ -260,6 +271,10 @@ def get_base_parser(self): type=positive_non_zero_float, help="Set HTTP call timeout (in seconds)") + parser.add_argument('--os-auth-token', + default=utils.env('OS_AUTH_TOKEN'), + help='Defaults to env[OS_AUTH_TOKEN]') + parser.add_argument('--os-username', metavar='', default=utils.env('OS_USERNAME', 'NOVA_USERNAME'), @@ -491,7 +506,6 @@ def setup_debugging(self, debug): format=streamformat) def main(self, argv): - # Parse args once to find version and debug settings parser = self.get_base_parser() (options, args) = parser.parse_known_args(argv) @@ -532,27 +546,37 @@ def main(self, argv): self.do_bash_completion(args) return 0 - (os_username, os_tenant_name, os_tenant_id, os_auth_url, - os_region_name, os_auth_system, endpoint_type, insecure, - service_type, service_name, volume_service_name, - bypass_url, os_cache, cacert, timeout) = ( - args.os_username, - args.os_tenant_name, args.os_tenant_id, - args.os_auth_url, - args.os_region_name, args.os_auth_system, - args.endpoint_type, args.insecure, args.service_type, - args.service_name, args.volume_service_name, - args.bypass_url, args.os_cache, - args.os_cacert, args.timeout) + os_username = args.os_username + os_password = None # Fetched and set later as needed + os_tenant_name = args.os_tenant_name + os_tenant_id = args.os_tenant_id + os_auth_url = args.os_auth_url + os_region_name = args.os_region_name + os_auth_system = args.os_auth_system + endpoint_type = args.endpoint_type + insecure = args.insecure + service_type = args.service_type + service_name = args.service_name + volume_service_name = args.volume_service_name + bypass_url = args.bypass_url + os_cache = args.os_cache + cacert = args.os_cacert + timeout = args.timeout + + # We may have either, both or none of these. + # If we have both, we don't need USERNAME, PASSWORD etc. + # Fill in the blanks from the SecretsHelper if possible. + # Finally, authenticate unless we have both. + # Note if we don't auth we probably don't have a tenant ID so we can't + # cache the token. + auth_token = args.os_auth_token if args.os_auth_token else None + management_url = bypass_url if bypass_url else None if os_auth_system and os_auth_system != "keystone": auth_plugin = novaclient.auth_plugin.load_plugin(os_auth_system) else: auth_plugin = None - # Fetched and set later as needed - os_password = None - if not endpoint_type: endpoint_type = DEFAULT_NOVA_ENDPOINT_TYPE @@ -567,9 +591,14 @@ def main(self, argv): DEFAULT_OS_COMPUTE_API_VERSION] service_type = utils.get_service_type(args.func) or service_type + # If we have an auth token but no management_url, we must auth anyway. + # Expired tokens are handled by client.py:_cs_request + must_auth = not (utils.isunauthenticated(args.func) + or (auth_token and management_url)) + #FIXME(usrleon): Here should be restrict for project id same as # for os_username or os_password but for compatibility it is not. - if not utils.isunauthenticated(args.func): + if must_auth: if auth_plugin: auth_plugin.parse_opts(args) @@ -613,7 +642,7 @@ def main(self, argv): region_name=os_region_name, endpoint_type=endpoint_type, extensions=self.extensions, service_type=service_type, service_name=service_name, auth_system=os_auth_system, - auth_plugin=auth_plugin, + auth_plugin=auth_plugin, auth_token=auth_token, volume_service_name=volume_service_name, timings=args.timings, bypass_url=bypass_url, os_cache=os_cache, http_log_debug=options.debug, @@ -621,7 +650,7 @@ def main(self, argv): # Now check for the password/token of which pieces of the # identifying keyring key can come from the underlying client - if not utils.isunauthenticated(args.func): + if must_auth: helper = SecretsHelper(args, self.cs.client) if (auth_plugin and auth_plugin.opts and "os_password" not in auth_plugin.opts): @@ -629,31 +658,26 @@ def main(self, argv): else: use_pw = True - tenant_id, auth_token, management_url = (helper.tenant_id, - helper.auth_token, - helper.management_url) + tenant_id = helper.tenant_id + # Allow commandline to override cache + if not auth_token: + auth_token = helper.auth_token + if not management_url: + management_url = helper.management_url if tenant_id and auth_token and management_url: self.cs.client.tenant_id = tenant_id self.cs.client.auth_token = auth_token self.cs.client.management_url = management_url - # authenticate just sets up some values in this case, no REST - # calls - self.cs.authenticate() - if use_pw: - # Auth using token must have failed or not happened - # at all, so now switch to password mode and save - # the token when its gotten... using our keyring - # saver - os_password = helper.password - if not os_password: - raise exc.CommandError( - 'Expecting a password provided via either ' - '--os-password, env[OS_PASSWORD], or ' - 'prompted response') - self.cs.client.password = os_password + self.cs.client.password_fun = lambda: helper.password + elif use_pw: + # We're missing something, so auth with user/pass and save + # the result in our helper. + self.cs.client.password = helper.password self.cs.client.keyring_saver = helper try: + # This does a couple of bits which are useful even if we've + # got the token + service URL already. It exits fast in that case. if not utils.isunauthenticated(args.func): self.cs.authenticate() except exc.Unauthorized: diff --git a/novaclient/tests/test_client.py b/novaclient/tests/test_client.py index ded0d9add..0d2cc4cf7 100644 --- a/novaclient/tests/test_client.py +++ b/novaclient/tests/test_client.py @@ -196,3 +196,23 @@ def test_authenticate_call_v3(self, mock_authenticate): auth_url="foo/v2") cs.authenticate() self.assertTrue(mock_authenticate.called) + + def test_get_password_simple(self): + cs = novaclient.client.HTTPClient("user", "password", "", "") + cs.password_func = mock.Mock() + self.assertEqual(cs._get_password(), "password") + self.assertFalse(cs.password_func.called) + + def test_get_password_none(self): + cs = novaclient.client.HTTPClient("user", None, "", "") + self.assertEqual(cs._get_password(), None) + + def test_get_password_func(self): + cs = novaclient.client.HTTPClient("user", None, "", "") + cs.password_func = mock.Mock(return_value="password") + self.assertEqual(cs._get_password(), "password") + cs.password_func.assert_called_once_with() + + cs.password_func = mock.Mock() + self.assertEqual(cs._get_password(), "password") + self.assertFalse(cs.password_func.called) diff --git a/novaclient/tests/v1_1/test_auth.py b/novaclient/tests/v1_1/test_auth.py index 1754f8a2a..7344bc76e 100644 --- a/novaclient/tests/v1_1/test_auth.py +++ b/novaclient/tests/v1_1/test_auth.py @@ -379,15 +379,22 @@ def test_authenticate_with_token_success(self): 'User-Agent': cs.client.USER_AGENT, 'Content-Type': 'application/json', 'Accept': 'application/json', - 'X-Auth-Token': "FAKE_ID", + } + body = { + 'auth': { + 'token': { + 'id': cs.client.auth_token, + }, + 'tenantName': cs.client.projectid, + }, } - token_url = cs.client.auth_url + "/tokens/FAKE_ID" + token_url = cs.client.auth_url + "/tokens" mock_request.assert_called_with( - "GET", + "POST", token_url, headers=headers, - data="null", + data=json.dumps(body), allow_redirects=True, **self.TEST_REQUEST_BASE) diff --git a/novaclient/v1_1/client.py b/novaclient/v1_1/client.py index a89581eac..382929dcc 100644 --- a/novaclient/v1_1/client.py +++ b/novaclient/v1_1/client.py @@ -73,7 +73,7 @@ def __init__(self, username, api_key, project_id, auth_url=None, volume_service_name=None, timings=False, bypass_url=None, os_cache=False, no_cache=True, http_log_debug=False, auth_system='keystone', - auth_plugin=None, + auth_plugin=None, auth_token=None, cacert=None, tenant_id=None): # FIXME(comstud): Rename the api_key argument above when we # know it's not being used as keyword argument @@ -131,6 +131,7 @@ def __init__(self, username, api_key, project_id, auth_url=None, projectid=project_id, tenant_id=tenant_id, auth_url=auth_url, + auth_token=auth_token, insecure=insecure, timeout=timeout, auth_system=auth_system, diff --git a/novaclient/v3/client.py b/novaclient/v3/client.py index 16c5a33b3..37489616f 100644 --- a/novaclient/v3/client.py +++ b/novaclient/v3/client.py @@ -57,7 +57,7 @@ def __init__(self, username, password, project_id, auth_url=None, volume_service_name=None, timings=False, bypass_url=None, os_cache=False, no_cache=True, http_log_debug=False, auth_system='keystone', - auth_plugin=None, + auth_plugin=None, auth_token=None, cacert=None, tenant_id=None): self.projectid = project_id self.tenant_id = tenant_id @@ -96,6 +96,7 @@ def __init__(self, username, password, project_id, auth_url=None, timeout=timeout, auth_system=auth_system, auth_plugin=auth_plugin, + auth_token=auth_token, proxy_token=proxy_token, proxy_tenant_id=proxy_tenant_id, region_name=region_name, From e650ffa5e87079e22e138e700435bd0451824884 Mon Sep 17 00:00:00 2001 From: Sascha Peilicke Date: Thu, 16 Jan 2014 09:20:52 +0100 Subject: [PATCH 0389/1705] Sync with global requirements Change-Id: I239e319045433b57610dfd996b837a76124e15af --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index b2788bcfe..20a8506c2 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -3,7 +3,7 @@ hacking>=0.8.0,<0.9 coverage>=3.6 discover fixtures>=0.3.14 -keyring>=1.6.1,<2.0 +keyring>=1.6.1,<2.0,>=2.1 mock>=1.0 sphinx>=1.1.2,<1.2 testrepository>=0.0.17 From 27de89af5fbbbd7c4f9e5be3554de96515b5cc57 Mon Sep 17 00:00:00 2001 From: Masayuki Igawa Date: Fri, 10 Jan 2014 11:43:32 +0900 Subject: [PATCH 0390/1705] Fix logic for "nova flavor-show 0#" For perviously logic, if flavor id is start with "0Number", int(name_or_id) will convert it to "Number". If flavor id "Number" exist in DB, it will show up for query with "0Number". This fix enter flavor search logic at the beginning. Also add one more UT for this function. Change-Id: Ic48ff4275978404064c0c6fce6f98181660aa84a Fixes: bug 1268456 --- novaclient/tests/test_utils.py | 13 ++++++++++++- novaclient/utils.py | 16 ++++++++-------- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/novaclient/tests/test_utils.py b/novaclient/tests/test_utils.py index ad0dff5e7..a7660b8b5 100644 --- a/novaclient/tests/test_utils.py +++ b/novaclient/tests/test_utils.py @@ -42,9 +42,15 @@ class FakeManager(base.ManagerWithFind): resources = [ FakeResource('1234', {'name': 'entity_one'}), FakeResource(UUID, {'name': 'entity_two'}), - FakeResource('5678', {'name': '9876'}) + FakeResource('5678', {'name': '9876'}), + FakeResource('01234', {'name': 'entity_three'}) ] + is_alphanum_id_allowed = None + + def __init__(self, alphanum_id_allowed=False): + self.is_alphanum_id_allowed = alphanum_id_allowed + def get(self, resource_id): for resource in self.resources: if resource.id == str(resource_id): @@ -117,6 +123,11 @@ def test_find_by_str_displayname(self): output = utils.find_resource(display_manager, 'entity_three') self.assertEqual(output, display_manager.get('4242')) + def test_find_in_alphanum_allowd_manager_by_str_id_(self): + alphanum_manager = FakeManager(True) + output = utils.find_resource(alphanum_manager, '01234') + self.assertEqual(output, alphanum_manager.get('01234')) + class _FakeResult(object): def __init__(self, name, value): diff --git a/novaclient/utils.py b/novaclient/utils.py index 0ce95f650..73b31af10 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -250,7 +250,14 @@ def print_dict(d, dict_property="Property", dict_value="Value", wrap=0): def find_resource(manager, name_or_id, **find_args): """Helper for the _find_* methods.""" - # first try to get entity as integer id + # for str id which is not uuid (for Flavor search currently) + if getattr(manager, 'is_alphanum_id_allowed', False): + try: + return manager.get(name_or_id) + except exceptions.NotFound: + pass + + # try to get entity as integer id try: return manager.get(int(name_or_id)) except (TypeError, ValueError, exceptions.NotFound): @@ -266,13 +273,6 @@ def find_resource(manager, name_or_id, **find_args): except (TypeError, ValueError, exceptions.NotFound): pass - # for str id which is not uuid (for Flavor search currently) - if getattr(manager, 'is_alphanum_id_allowed', False): - try: - return manager.get(name_or_id) - except exceptions.NotFound: - pass - try: try: return manager.find(human_id=name_or_id, **find_args) From c71131a50994d95ae869442fc25e7c3511a5ddbc Mon Sep 17 00:00:00 2001 From: Zhongyue Luo Date: Thu, 9 Jan 2014 10:24:34 +0800 Subject: [PATCH 0391/1705] Removes use of timeutils.set_time_override The set_time_override function in timeutils was written as a helper function to mock utcnow for unittests before 'mock' was generally used. Now that we have mock and fixture, we no longer need to use it. Change-Id: I809825560b0324498010bd93aa1ceef552554375 Partial-Bug: #1266962 --- novaclient/tests/v1_1/test_shell.py | 7 +++---- novaclient/tests/v3/test_shell.py | 2 -- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index 75af786a0..70d978f60 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -26,7 +26,6 @@ import novaclient.client from novaclient import exceptions -from novaclient.openstack.common import timeutils import novaclient.shell from novaclient.tests import utils from novaclient.tests.v1_1 import fakes @@ -69,7 +68,6 @@ def setUp(self): self.useFixture(fixtures.MonkeyPatch( 'novaclient.client.get_client_class', lambda *_: fakes.FakeClient)) - self.addCleanup(timeutils.clear_time_override) @mock.patch('sys.stdout', new_callable=six.StringIO) def run_command(self, cmd, mock_stdout): @@ -1076,8 +1074,9 @@ def test_usage_list(self): 'end=2005-02-01T00:00:00&' + 'detailed=1') - def test_usage_list_no_args(self): - timeutils.set_time_override(datetime.datetime(2005, 2, 1, 0, 0)) + @mock.patch('novaclient.openstack.common.timeutils.utcnow') + def test_usage_list_no_args(self, mock_utcnow): + mock_utcnow.return_value = datetime.datetime(2005, 2, 1, 0, 0) self.run_command('usage-list') self.assert_called('GET', '/os-simple-tenant-usage?' + diff --git a/novaclient/tests/v3/test_shell.py b/novaclient/tests/v3/test_shell.py index 1c39ad7fd..b95e3820a 100644 --- a/novaclient/tests/v3/test_shell.py +++ b/novaclient/tests/v3/test_shell.py @@ -20,7 +20,6 @@ import mock import six -from novaclient.openstack.common import timeutils import novaclient.shell from novaclient.tests import utils from novaclient.tests.v3 import fakes @@ -62,7 +61,6 @@ def setUp(self): self.useFixture(fixtures.MonkeyPatch( 'novaclient.client.get_client_class', lambda *_: fakes.FakeClient)) - self.addCleanup(timeutils.clear_time_override) @mock.patch('sys.stdout', new_callable=six.StringIO) def run_command(self, cmd, mock_stdout): From 99e289ea86bac352be68af14d90ee3acc6974d86 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Tue, 21 Jan 2014 18:03:00 +0200 Subject: [PATCH 0392/1705] Replace basestring by six.string_types Method shell._boot uses class basestring instead of six.string_types. Tests don't check this method with several hints, so we didn't have NameError in py33 env. Change-Id: I9b35cd7ba162140a15929f9fc8a6d21755b3570c --- novaclient/tests/v1_1/test_shell.py | 5 +++-- novaclient/v1_1/shell.py | 2 +- novaclient/v3/shell.py | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index 70d978f60..ff4836244 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -461,7 +461,8 @@ def test_boot_metadata(self): ) def test_boot_hints(self): - self.run_command('boot --image 1 --flavor 1 --hint a=b=c some-server ') + self.run_command('boot --image 1 --flavor 1 ' + '--hint a=b0=c0 --hint a=b1=c1 some-server ') self.assert_called_anytime( 'POST', '/servers', { @@ -472,7 +473,7 @@ def test_boot_hints(self): 'min_count': 1, 'max_count': 1, }, - 'os:scheduler_hints': {'a': 'b=c'}, + 'os:scheduler_hints': {'a': ['b0=c0', 'b1=c1']}, }, ) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 4aa068964..1a67ce63d 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -256,7 +256,7 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): # NOTE(vish): multiple copies of the same hint will # result in a list of values if key in hints: - if isinstance(hints[key], basestring): + if isinstance(hints[key], six.string_types): hints[key] = [hints[key]] hints[key] += [value] else: diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 612e62c35..681aed4de 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -173,7 +173,7 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): # NOTE(vish): multiple copies of the same hint will # result in a list of values if key in hints: - if isinstance(hints[key], basestring): + if isinstance(hints[key], six.string_types): hints[key] = [hints[key]] hints[key] += [value] else: From 6ff02390a2df6d72177dbaef30df4759df4f0f34 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Tue, 21 Jan 2014 18:22:51 +0200 Subject: [PATCH 0393/1705] Add tests for boot method of v3 shell Methods `do_boot` and `_boot` in v3.shell don't have tests at all. Partially implements bp v3-api Change-Id: Ic231f829459f22f26d1262d208cc4fc9cbb8676e --- novaclient/tests/v3/test_shell.py | 332 ++++++++++++++++++++++++++++++ 1 file changed, 332 insertions(+) diff --git a/novaclient/tests/v3/test_shell.py b/novaclient/tests/v3/test_shell.py index b95e3820a..eb125c3ca 100644 --- a/novaclient/tests/v3/test_shell.py +++ b/novaclient/tests/v3/test_shell.py @@ -16,10 +16,14 @@ # License for the specific language governing permissions and limitations # under the License. +import base64 +import os + import fixtures import mock import six +from novaclient import exceptions import novaclient.shell from novaclient.tests import utils from novaclient.tests.v3 import fakes @@ -168,3 +172,331 @@ def test_aggregate_details_by_id(self): def test_aggregate_details_by_name(self): self.run_command('aggregate-details test') self.assert_called('GET', '/os-aggregates') + + def test_boot(self): + self.run_command('boot --flavor 1 --image 1 some-server') + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': '1', + 'min_count': 1, + 'max_count': 1, + }}, + ) + + def test_boot_multiple(self): + self.run_command('boot --flavor 1 --image 1' + ' --num-instances 3 some-server') + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': '1', + 'min_count': 1, + 'max_count': 3, + }}, + ) + + def test_boot_image_with(self): + self.run_command("boot --flavor 1" + " --image-with test_key=test_value some-server") + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': '1', + 'min_count': 1, + 'max_count': 1, + }}, + ) + + def test_boot_key(self): + self.run_command('boot --flavor 1 --image 1 --key_name 1 some-server') + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': '1', + 'key_name': '1', + 'min_count': 1, + 'max_count': 1, + }}, + ) + + def test_boot_user_data(self): + file_text = 'text' + + with mock.patch('novaclient.v3.shell.open', create=True) as mock_open: + mock_open.return_value = file_text + testfile = 'some_dir/some_file.txt' + + self.run_command('boot --flavor 1 --image 1 --user_data %s ' + 'some-server' % testfile) + + mock_open.assert_called_once_with(testfile) + + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': '1', + 'min_count': 1, + 'max_count': 1, + 'user_data': base64.b64encode(file_text.encode('utf-8')) + }}, + ) + + def test_boot_avzone(self): + self.run_command( + 'boot --flavor 1 --image 1 --availability-zone avzone ' + 'some-server') + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': '1', + 'availability_zone': 'avzone', + 'min_count': 1, + 'max_count': 1 + }}, + ) + + def test_boot_secgroup(self): + self.run_command( + 'boot --flavor 1 --image 1 --security-groups secgroup1,' + 'secgroup2 some-server') + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'security_groups': [{'name': 'secgroup1'}, + {'name': 'secgroup2'}], + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': '1', + 'min_count': 1, + 'max_count': 1, + }}, + ) + + def test_boot_config_drive(self): + self.run_command( + 'boot --flavor 1 --image 1 --config-drive 1 some-server') + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': '1', + 'min_count': 1, + 'max_count': 1, + 'config_drive': True + }}, + ) + + def test_boot_config_drive_custom(self): + self.run_command( + 'boot --flavor 1 --image 1 --config-drive /dev/hda some-server') + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': '1', + 'min_count': 1, + 'max_count': 1, + 'config_drive': '/dev/hda' + }}, + ) + + def test_boot_invalid_user_data(self): + invalid_file = os.path.join(os.path.dirname(__file__), + 'no_such_file') + cmd = ('boot some-server --flavor 1 --image 1' + ' --user_data %s' % invalid_file) + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + def test_boot_no_image_no_bdms(self): + cmd = 'boot --flavor 1 some-server' + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + def test_boot_no_flavor(self): + cmd = 'boot --image 1 some-server' + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + def test_boot_no_image_bdms(self): + self.run_command( + 'boot --flavor 1 --block_device_mapping vda=blah:::0 some-server' + ) + self.assert_called_anytime( + 'POST', '/os-volumes_boot', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'block_device_mapping': [ + { + 'volume_id': 'blah', + 'delete_on_termination': '0', + 'device_name': 'vda' + } + ], + 'imageRef': '', + 'min_count': 1, + 'max_count': 1, + }}, + ) + + def test_boot_image_bdms(self): + self.run_command( + 'boot --flavor 1 --image 1 --block-device id=fake-id,' + 'source=volume,dest=volume,device=vda,size=1,format=ext4,' + 'type=disk,shutdown=preserve some-server' + ) + self.assert_called_anytime( + 'POST', '/os-volumes_boot', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'block_device_mapping': [ + {'device_name': 'id', 'volume_id': + 'fake-id,source=volume,dest=volume,device=vda,size=1,' + 'format=ext4,type=disk,shutdown=preserve'}], + 'imageRef': '1', + 'min_count': 1, + 'max_count': 1, + }}, + ) + + def test_boot_metadata(self): + self.run_command('boot --image 1 --flavor 1 --meta foo=bar=pants' + ' --meta spam=eggs some-server ') + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': '1', + 'metadata': {'foo': 'bar=pants', 'spam': 'eggs'}, + 'min_count': 1, + 'max_count': 1, + }}, + ) + + def test_boot_hints(self): + self.run_command('boot --image 1 --flavor 1 ' + '--hint a=b1=c1 --hint a2=b2=c2 --hint a=b0=c0 ' + 'some-server') + self.assert_called_anytime( + 'POST', '/servers', + { + 'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': '1', + 'min_count': 1, + 'max_count': 1, + }, + 'os:scheduler_hints': {'a': ['b1=c1', 'b0=c0'], 'a2': 'b2=c2'}, + }, + ) + + def test_boot_nics(self): + cmd = ('boot --image 1 --flavor 1 ' + '--nic net-id=a=c,v4-fixed-ip=10.0.0.1 some-server') + self.run_command(cmd) + self.assert_called_anytime( + 'POST', '/servers', + { + 'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': '1', + 'min_count': 1, + 'max_count': 1, + 'networks': [ + {'uuid': 'a=c', 'fixed_ip': '10.0.0.1'}, + ], + }, + }, + ) + + def tets_boot_nics_no_value(self): + cmd = ('boot --image 1 --flavor 1 ' + '--nic net-id some-server') + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + def test_boot_nics_random_key(self): + cmd = ('boot --image 1 --flavor 1 ' + '--nic net-id=a=c,v4-fixed-ip=10.0.0.1,foo=bar some-server') + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + def test_boot_nics_no_netid_or_portid(self): + cmd = ('boot --image 1 --flavor 1 ' + '--nic v4-fixed-ip=10.0.0.1 some-server') + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + def test_boot_files(self): + file_text = 'text' + + with mock.patch('novaclient.v3.shell.open', create=True) as mock_open: + mock_open.return_value = file_text + testfile = 'some_dir/some_file.txt' + + self.run_command('boot --flavor 1 --image 1 --user_data %s ' + 'some-server' % testfile) + + cmd = ('boot some-server --flavor 1 --image 1' + ' --file /tmp/foo=%s --file /tmp/bar=%s') + self.run_command(cmd % (testfile, testfile)) + + mock_open.assert_called_with(testfile) + + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': '1', + 'min_count': 1, + 'max_count': 1, + 'personality': [ + {'path': '/tmp/bar', + 'contents': base64.b64encode(file_text.encode('utf-8'))}, + {'path': '/tmp/foo', + 'contents': base64.b64encode(file_text.encode('utf-8'))}, + ] + }}, + ) + + def test_boot_invalid_files(self): + invalid_file = os.path.join(os.path.dirname(__file__), + 'asdfasdfasdfasdf') + cmd = ('boot some-server --flavor 1 --image 1' + ' --file /foo=%s' % invalid_file) + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + def test_boot_num_instances(self): + self.run_command('boot --image 1 --flavor 1 --num-instances 3 server') + self.assert_called_anytime( + 'POST', '/servers', + { + 'server': { + 'flavorRef': '1', + 'name': 'server', + 'imageRef': '1', + 'min_count': 1, + 'max_count': 3, + } + }) + + def test_boot_invalid_num_instances(self): + cmd = 'boot --image 1 --flavor 1 --num-instances 1 server' + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + cmd = 'boot --image 1 --flavor 1 --num-instances 0 server' + self.assertRaises(exceptions.CommandError, self.run_command, cmd) From da11e62216ffb219bcfe30fc647a49ebbb403bb3 Mon Sep 17 00:00:00 2001 From: Sahid Orentino Ferdjaoui Date: Wed, 8 Jan 2014 12:07:30 +0100 Subject: [PATCH 0394/1705] Using common methods from oslo cliutils There are some common methods in cliutils we can use in novaclient: arg, env, unauthenticated, isunauthenticated. + Replaces utils.env to add alias env from cliutils. + Replaces utils.arg to add alias arg from cliutils. + Removes unused methods: add_arg, unauthenticated, isunauthenticated To use methods from clituils. Related to blueprint common-client-library-2 Change-Id: Ic7c132c37d6a91cf3eae55530300efd153c31903 --- novaclient/shell.py | 5 +-- novaclient/tests/test_shell.py | 3 +- novaclient/utils.py | 57 ++-------------------------------- 3 files changed, 8 insertions(+), 57 deletions(-) diff --git a/novaclient/shell.py b/novaclient/shell.py index a4254c929..c434857d4 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -45,6 +45,7 @@ from novaclient import client from novaclient import exceptions as exc import novaclient.extension +from novaclient.openstack.common import cliutils from novaclient.openstack.common import strutils from novaclient import utils from novaclient.v1_1 import shell as shell_v1_1 @@ -593,7 +594,7 @@ def main(self, argv): # If we have an auth token but no management_url, we must auth anyway. # Expired tokens are handled by client.py:_cs_request - must_auth = not (utils.isunauthenticated(args.func) + must_auth = not (cliutils.isunauthenticated(args.func) or (auth_token and management_url)) #FIXME(usrleon): Here should be restrict for project id same as @@ -678,7 +679,7 @@ def main(self, argv): try: # This does a couple of bits which are useful even if we've # got the token + service URL already. It exits fast in that case. - if not utils.isunauthenticated(args.func): + if not cliutils.isunauthenticated(args.func): self.cs.authenticate() except exc.Unauthorized: raise exc.CommandError("Invalid OpenStack Nova credentials.") diff --git a/novaclient/tests/test_shell.py b/novaclient/tests/test_shell.py index cb40f9988..ab0581727 100644 --- a/novaclient/tests/test_shell.py +++ b/novaclient/tests/test_shell.py @@ -49,7 +49,8 @@ def setUp(self): self.useFixture(fixtures.MonkeyPatch( 'novaclient.client.get_client_class', mock.MagicMock)) - self.nc_util = mock.patch('novaclient.utils.isunauthenticated').start() + self.nc_util = mock.patch( + 'novaclient.openstack.common.cliutils.isunauthenticated').start() self.nc_util.return_value = False def shell(self, argstr, exitcodes=(0,)): diff --git a/novaclient/utils.py b/novaclient/utils.py index c7f7a15f9..6203b0668 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -12,7 +12,6 @@ # under the License. import json -import os import pkg_resources import sys import textwrap @@ -22,42 +21,13 @@ import six from novaclient import exceptions +from novaclient.openstack.common import cliutils from novaclient.openstack.common import jsonutils from novaclient.openstack.common import strutils -def arg(*args, **kwargs): - """Decorator for CLI args.""" - def _decorator(func): - add_arg(func, *args, **kwargs) - return func - return _decorator - - -def env(*args, **kwargs): - """ - returns the first environment variable set - if none are non-empty, defaults to '' or keyword arg default - """ - for arg in args: - value = os.environ.get(arg, None) - if value: - return value - return kwargs.get('default', '') - - -def add_arg(f, *args, **kwargs): - """Bind CLI arguments to a shell.py `do_foo` function.""" - - if not hasattr(f, 'arguments'): - f.arguments = [] - - # NOTE(sirp): avoid dups that can occur when the module is shared across - # tests. - if (args, kwargs) not in f.arguments: - # Because of the semantics of decorator composition if we just append - # to the options list positional options will appear to be backwards. - f.arguments.insert(0, (args, kwargs)) +arg = cliutils.arg +env = cliutils.env def add_resource_manager_extra_kwargs_hook(f, hook): @@ -96,27 +66,6 @@ def get_resource_manager_extra_kwargs(f, args, allow_conflicts=False): return extra_kwargs -def unauthenticated(f): - """ - Adds 'unauthenticated' attribute to decorated function. - Usage: - @unauthenticated - def mymethod(f): - ... - """ - f.unauthenticated = True - return f - - -def isunauthenticated(f): - """ - Checks to see if the function is marked as not requiring authentication - with the @unauthenticated decorator. Returns True if decorator is - set to True, False otherwise. - """ - return getattr(f, 'unauthenticated', False) - - def service_type(stype): """ Adds 'service_type' attribute to decorated function. From a5195c5033eb8eeb3aede15d569f4f66a4318004 Mon Sep 17 00:00:00 2001 From: Sahid Orentino Ferdjaoui Date: Mon, 13 Jan 2014 12:24:55 +0100 Subject: [PATCH 0395/1705] Updates nova client to use the latest oslo files This patchset updates modules based on the config file: openstack-common.conf Notes: Some corrections has been added to work with new files. + utils.py: The method safe_decode from strutils.py was updated and it is now not necessary to check for decode string with py33. + base.py: base64 needs a 8-bit string for py33 + test_shell.py: stdin.encoding is needed for strutils Change-Id: Iebe474f1226f8b5faa7fb5722e65f41b80d1973c Related to blueprint common-client-library-2 Closes-Bug: #1265473 --- novaclient/base.py | 6 +- novaclient/openstack/common/gettextutils.py | 432 +++++++++++------- .../openstack/common/py3kcompat/__init__.py | 17 - .../openstack/common/py3kcompat/urlutils.py | 20 +- novaclient/openstack/common/strutils.py | 24 +- novaclient/openstack/common/timeutils.py | 32 +- novaclient/openstack/common/uuidutils.py | 2 - novaclient/tests/test_shell.py | 2 + novaclient/utils.py | 8 +- tools/install_venv_common.py | 44 +- 10 files changed, 330 insertions(+), 257 deletions(-) diff --git a/novaclient/base.py b/novaclient/base.py index 1a022b93a..3759320f5 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -327,7 +327,11 @@ def _boot(self, resource_url, response_key, name, image, flavor, if hasattr(userdata, 'read'): userdata = userdata.read() - userdata = strutils.safe_encode(userdata) + if six.PY3: + userdata = userdata.encode("utf-8") + else: + userdata = strutils.safe_encode(userdata) + body["server"]["user_data"] = base64.b64encode(userdata) if meta: body["server"]["metadata"] = meta diff --git a/novaclient/openstack/common/gettextutils.py b/novaclient/openstack/common/gettextutils.py index 01a4ce59a..27f7ec426 100644 --- a/novaclient/openstack/common/gettextutils.py +++ b/novaclient/openstack/common/gettextutils.py @@ -1,5 +1,3 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - # Copyright 2012 Red Hat, Inc. # Copyright 2013 IBM Corp. # All Rights Reserved. @@ -26,13 +24,10 @@ import copy import gettext -import logging +import locale +from logging import handlers import os import re -try: - import UserString as _userString -except ImportError: - import collections as _userString from babel import localedata import six @@ -58,8 +53,10 @@ def enable_lazy(): def _(msg): if USE_LAZY: - return Message(msg, 'novaclient') + return Message(msg, domain='novaclient') else: + if six.PY3: + return _t.gettext(msg) return _t.ugettext(msg) @@ -88,11 +85,6 @@ def install(domain, lazy=False): # messages in OpenStack. We override the standard _() function # and % (format string) operation to build Message objects that can # later be translated when we have more information. - # - # Also included below is an example LocaleHandler that translates - # Messages to an associated locale, effectively allowing many logs, - # each with their own locale. - def _lazy_gettext(msg): """Create and return a Message object. @@ -103,159 +95,183 @@ def _lazy_gettext(msg): Message encapsulates a string so that we can translate it later when needed. """ - return Message(msg, domain) + return Message(msg, domain=domain) - import __builtin__ - __builtin__.__dict__['_'] = _lazy_gettext + from six import moves + moves.builtins.__dict__['_'] = _lazy_gettext else: localedir = '%s_LOCALEDIR' % domain.upper() - gettext.install(domain, - localedir=os.environ.get(localedir), - unicode=True) - - -class Message(_userString.UserString, object): - """Class used to encapsulate translatable messages.""" - def __init__(self, msg, domain): - # _msg is the gettext msgid and should never change - self._msg = msg - self._left_extra_msg = '' - self._right_extra_msg = '' - self.params = None - self.locale = None - self.domain = domain - - @property - def data(self): - # NOTE(mrodden): this should always resolve to a unicode string - # that best represents the state of the message currently - - localedir = os.environ.get(self.domain.upper() + '_LOCALEDIR') - if self.locale: - lang = gettext.translation(self.domain, - localedir=localedir, - languages=[self.locale], - fallback=True) + if six.PY3: + gettext.install(domain, + localedir=os.environ.get(localedir)) else: - # use system locale for translations - lang = gettext.translation(self.domain, - localedir=localedir, - fallback=True) - - full_msg = (self._left_extra_msg + - lang.ugettext(self._msg) + - self._right_extra_msg) - - if self.params is not None: - full_msg = full_msg % self.params - - return six.text_type(full_msg) - - def _save_dictionary_parameter(self, dict_param): - full_msg = self.data - # look for %(blah) fields in string; - # ignore %% and deal with the - # case where % is first character on the line - keys = re.findall('(?:[^%]|^)?%\((\w*)\)[a-z]', full_msg) - - # if we don't find any %(blah) blocks but have a %s - if not keys and re.findall('(?:[^%]|^)%[a-z]', full_msg): - # apparently the full dictionary is the parameter - params = copy.deepcopy(dict_param) + gettext.install(domain, + localedir=os.environ.get(localedir), + unicode=True) + + +class Message(six.text_type): + """A Message object is a unicode object that can be translated. + + Translation of Message is done explicitly using the translate() method. + For all non-translation intents and purposes, a Message is simply unicode, + and can be treated as such. + """ + + def __new__(cls, msgid, msgtext=None, params=None, domain='novaclient', *args): + """Create a new Message object. + + In order for translation to work gettext requires a message ID, this + msgid will be used as the base unicode text. It is also possible + for the msgid and the base unicode text to be different by passing + the msgtext parameter. + """ + # If the base msgtext is not given, we use the default translation + # of the msgid (which is in English) just in case the system locale is + # not English, so that the base text will be in that locale by default. + if not msgtext: + msgtext = Message._translate_msgid(msgid, domain) + # We want to initialize the parent unicode with the actual object that + # would have been plain unicode if 'Message' was not enabled. + msg = super(Message, cls).__new__(cls, msgtext) + msg.msgid = msgid + msg.domain = domain + msg.params = params + return msg + + def translate(self, desired_locale=None): + """Translate this message to the desired locale. + + :param desired_locale: The desired locale to translate the message to, + if no locale is provided the message will be + translated to the system's default locale. + + :returns: the translated message in unicode + """ + + translated_message = Message._translate_msgid(self.msgid, + self.domain, + desired_locale) + if self.params is None: + # No need for more translation + return translated_message + + # This Message object may have been formatted with one or more + # Message objects as substitution arguments, given either as a single + # argument, part of a tuple, or as one or more values in a dictionary. + # When translating this Message we need to translate those Messages too + translated_params = _translate_args(self.params, desired_locale) + + translated_message = translated_message % translated_params + + return translated_message + + @staticmethod + def _translate_msgid(msgid, domain, desired_locale=None): + if not desired_locale: + system_locale = locale.getdefaultlocale() + # If the system locale is not available to the runtime use English + if not system_locale[0]: + desired_locale = 'en_US' + else: + desired_locale = system_locale[0] + + locale_dir = os.environ.get(domain.upper() + '_LOCALEDIR') + lang = gettext.translation(domain, + localedir=locale_dir, + languages=[desired_locale], + fallback=True) + if six.PY3: + translator = lang.gettext else: - params = {} - for key in keys: - try: - params[key] = copy.deepcopy(dict_param[key]) - except TypeError: - # cast uncopyable thing to unicode string - params[key] = unicode(dict_param[key]) + translator = lang.ugettext - return params + translated_message = translator(msgid) + return translated_message - def _save_parameters(self, other): - # we check for None later to see if - # we actually have parameters to inject, - # so encapsulate if our parameter is actually None + def __mod__(self, other): + # When we mod a Message we want the actual operation to be performed + # by the parent class (i.e. unicode()), the only thing we do here is + # save the original msgid and the parameters in case of a translation + params = self._sanitize_mod_params(other) + unicode_mod = super(Message, self).__mod__(params) + modded = Message(self.msgid, + msgtext=unicode_mod, + params=params, + domain=self.domain) + return modded + + def _sanitize_mod_params(self, other): + """Sanitize the object being modded with this Message. + + - Add support for modding 'None' so translation supports it + - Trim the modded object, which can be a large dictionary, to only + those keys that would actually be used in a translation + - Snapshot the object being modded, in case the message is + translated, it will be used as it was when the Message was created + """ if other is None: - self.params = (other, ) + params = (other,) elif isinstance(other, dict): - self.params = self._save_dictionary_parameter(other) + params = self._trim_dictionary_parameters(other) else: - # fallback to casting to unicode, - # this will handle the problematic python code-like - # objects that cannot be deep-copied - try: - self.params = copy.deepcopy(other) - except TypeError: - self.params = unicode(other) - - return self - - # overrides to be more string-like - def __unicode__(self): - return self.data - - def __str__(self): - return self.data.encode('utf-8') + params = self._copy_param(other) + return params - def __getstate__(self): - to_copy = ['_msg', '_right_extra_msg', '_left_extra_msg', - 'domain', 'params', 'locale'] - new_dict = self.__dict__.fromkeys(to_copy) - for attr in to_copy: - new_dict[attr] = copy.deepcopy(self.__dict__[attr]) + def _trim_dictionary_parameters(self, dict_param): + """Return a dict that only has matching entries in the msgid.""" + # NOTE(luisg): Here we trim down the dictionary passed as parameters + # to avoid carrying a lot of unnecessary weight around in the message + # object, for example if someone passes in Message() % locals() but + # only some params are used, and additionally we prevent errors for + # non-deepcopyable objects by unicoding() them. + + # Look for %(param) keys in msgid; + # Skip %% and deal with the case where % is first character on the line + keys = re.findall('(?:[^%]|^)?%\((\w*)\)[a-z]', self.msgid) + + # If we don't find any %(param) keys but have a %s + if not keys and re.findall('(?:[^%]|^)%[a-z]', self.msgid): + # Apparently the full dictionary is the parameter + params = self._copy_param(dict_param) + else: + params = {} + # Save our existing parameters as defaults to protect + # ourselves from losing values if we are called through an + # (erroneous) chain that builds a valid Message with + # arguments, and then does something like "msg % kwds" + # where kwds is an empty dictionary. + src = {} + if isinstance(self.params, dict): + src.update(self.params) + src.update(dict_param) + for key in keys: + params[key] = self._copy_param(src[key]) - return new_dict + return params - def __setstate__(self, state): - for (k, v) in state.items(): - setattr(self, k, v) + def _copy_param(self, param): + try: + return copy.deepcopy(param) + except TypeError: + # Fallback to casting to unicode this will handle the + # python code-like objects that can't be deep-copied + return six.text_type(param) - # operator overloads def __add__(self, other): - copied = copy.deepcopy(self) - copied._right_extra_msg += other.__str__() - return copied + msg = _('Message objects do not support addition.') + raise TypeError(msg) def __radd__(self, other): - copied = copy.deepcopy(self) - copied._left_extra_msg += other.__str__() - return copied + return self.__add__(other) - def __mod__(self, other): - # do a format string to catch and raise - # any possible KeyErrors from missing parameters - self.data % other - copied = copy.deepcopy(self) - return copied._save_parameters(other) - - def __mul__(self, other): - return self.data * other - - def __rmul__(self, other): - return other * self.data - - def __getitem__(self, key): - return self.data[key] - - def __getslice__(self, start, end): - return self.data.__getslice__(start, end) - - def __getattribute__(self, name): - # NOTE(mrodden): handle lossy operations that we can't deal with yet - # These override the UserString implementation, since UserString - # uses our __class__ attribute to try and build a new message - # after running the inner data string through the operation. - # At that point, we have lost the gettext message id and can just - # safely resolve to a string instead. - ops = ['capitalize', 'center', 'decode', 'encode', - 'expandtabs', 'ljust', 'lstrip', 'replace', 'rjust', 'rstrip', - 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill'] - if name in ops: - return getattr(self.data, name) - else: - return _userString.UserString.__getattribute__(self, name) + def __str__(self): + # NOTE(luisg): Logging in python 2.6 tries to str() log records, + # and it expects specifically a UnicodeError in order to proceed. + msg = _('Message objects do not support str() because they may ' + 'contain non-ascii characters. ' + 'Please use unicode() or translate() instead.') + raise UnicodeError(msg) def get_available_languages(domain): @@ -277,7 +293,7 @@ def get_available_languages(domain): # NOTE(luisg): Babel <1.0 used a function called list(), which was # renamed to locale_identifiers() in >=1.0, the requirements master list # requires >=0.9.6, uncapped, so defensively work with both. We can remove - # this check when the master list updates to >=1.0, and all projects udpate + # this check when the master list updates to >=1.0, and update all projects list_identifiers = (getattr(localedata, 'list', None) or getattr(localedata, 'locale_identifiers')) locale_identifiers = list_identifiers() @@ -288,38 +304,118 @@ def get_available_languages(domain): return copy.copy(language_list) -def get_localized_message(message, user_locale): - """Gets a localized version of the given message in the given locale.""" +def translate(obj, desired_locale=None): + """Gets the translated unicode representation of the given object. + + If the object is not translatable it is returned as-is. + If the locale is None the object is translated to the system locale. + + :param obj: the object to translate + :param desired_locale: the locale to translate the message to, if None the + default system locale will be used + :returns: the translated object in unicode, or the original object if + it could not be translated + """ + message = obj + if not isinstance(message, Message): + # If the object to translate is not already translatable, + # let's first get its unicode representation + message = six.text_type(obj) if isinstance(message, Message): - if user_locale: - message.locale = user_locale - return unicode(message) - else: - return message + # Even after unicoding() we still need to check if we are + # running with translatable unicode before translating + return message.translate(desired_locale) + return obj + +def _translate_args(args, desired_locale=None): + """Translates all the translatable elements of the given arguments object. -class LocaleHandler(logging.Handler): - """Handler that can have a locale associated to translate Messages. + This method is used for translating the translatable values in method + arguments which include values of tuples or dictionaries. + If the object is not a tuple or a dictionary the object itself is + translated if it is translatable. - A quick example of how to utilize the Message class above. - LocaleHandler takes a locale and a target logging.Handler object - to forward LogRecord objects to after translating the internal Message. + If the locale is None the object is translated to the system locale. + + :param args: the args to translate + :param desired_locale: the locale to translate the args to, if None the + default system locale will be used + :returns: a new args object with the translated contents of the original """ + if isinstance(args, tuple): + return tuple(translate(v, desired_locale) for v in args) + if isinstance(args, dict): + translated_dict = {} + for (k, v) in six.iteritems(args): + translated_v = translate(v, desired_locale) + translated_dict[k] = translated_v + return translated_dict + return translate(args, desired_locale) + + +class TranslationHandler(handlers.MemoryHandler): + """Handler that translates records before logging them. + + The TranslationHandler takes a locale and a target logging.Handler object + to forward LogRecord objects to after translating them. This handler + depends on Message objects being logged, instead of regular strings. + + The handler can be configured declaratively in the logging.conf as follows: + + [handlers] + keys = translatedlog, translator - def __init__(self, locale, target): - """Initialize a LocaleHandler + [handler_translatedlog] + class = handlers.WatchedFileHandler + args = ('/var/log/api-localized.log',) + formatter = context + + [handler_translator] + class = openstack.common.log.TranslationHandler + target = translatedlog + args = ('zh_CN',) + + If the specified locale is not available in the system, the handler will + log in the default locale. + """ + + def __init__(self, locale=None, target=None): + """Initialize a TranslationHandler :param locale: locale to use for translating messages :param target: logging.Handler object to forward LogRecord objects to after translation """ - logging.Handler.__init__(self) + # NOTE(luisg): In order to allow this handler to be a wrapper for + # other handlers, such as a FileHandler, and still be able to + # configure it using logging.conf, this handler has to extend + # MemoryHandler because only the MemoryHandlers' logging.conf + # parsing is implemented such that it accepts a target handler. + handlers.MemoryHandler.__init__(self, capacity=0, target=target) self.locale = locale - self.target = target + + def setFormatter(self, fmt): + self.target.setFormatter(fmt) def emit(self, record): - if isinstance(record.msg, Message): - # set the locale and resolve to a string - record.msg.locale = self.locale + # We save the message from the original record to restore it + # after translation, so other handlers are not affected by this + original_msg = record.msg + original_args = record.args + + try: + self._translate_and_log_record(record) + finally: + record.msg = original_msg + record.args = original_args + + def _translate_and_log_record(self, record): + record.msg = translate(record.msg, self.locale) + + # In addition to translating the message, we also need to translate + # arguments that were passed to the log method that were not part + # of the main message e.g., log.info(_('Some message %s'), this_one)) + record.args = _translate_args(record.args, self.locale) self.target.emit(record) diff --git a/novaclient/openstack/common/py3kcompat/__init__.py b/novaclient/openstack/common/py3kcompat/__init__.py index be894cf50..e69de29bb 100644 --- a/novaclient/openstack/common/py3kcompat/__init__.py +++ b/novaclient/openstack/common/py3kcompat/__init__.py @@ -1,17 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 -# -# Copyright 2013 Canonical Ltd. -# All Rights Reserved. -# -# 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. -# diff --git a/novaclient/openstack/common/py3kcompat/urlutils.py b/novaclient/openstack/common/py3kcompat/urlutils.py index 04b3418da..84e457a44 100644 --- a/novaclient/openstack/common/py3kcompat/urlutils.py +++ b/novaclient/openstack/common/py3kcompat/urlutils.py @@ -1,4 +1,3 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 # # Copyright 2013 Canonical Ltd. # All Rights Reserved. @@ -24,22 +23,36 @@ if six.PY3: # python3 + import urllib.error import urllib.parse + import urllib.request urlencode = urllib.parse.urlencode urljoin = urllib.parse.urljoin quote = urllib.parse.quote + quote_plus = urllib.parse.quote_plus parse_qsl = urllib.parse.parse_qsl + unquote = urllib.parse.unquote + unquote_plus = urllib.parse.unquote_plus urlparse = urllib.parse.urlparse urlsplit = urllib.parse.urlsplit urlunsplit = urllib.parse.urlunsplit + SplitResult = urllib.parse.SplitResult + + urlopen = urllib.request.urlopen + URLError = urllib.error.URLError + pathname2url = urllib.request.pathname2url else: # python2 import urllib + import urllib2 import urlparse urlencode = urllib.urlencode quote = urllib.quote + quote_plus = urllib.quote_plus + unquote = urllib.unquote + unquote_plus = urllib.unquote_plus parse = urlparse parse_qsl = parse.parse_qsl @@ -47,3 +60,8 @@ urlparse = parse.urlparse urlsplit = parse.urlsplit urlunsplit = parse.urlunsplit + SplitResult = parse.SplitResult + + urlopen = urllib2.urlopen + URLError = urllib2.URLError + pathname2url = urllib.pathname2url diff --git a/novaclient/openstack/common/strutils.py b/novaclient/openstack/common/strutils.py index ef139aaad..65b485448 100644 --- a/novaclient/openstack/common/strutils.py +++ b/novaclient/openstack/common/strutils.py @@ -1,5 +1,3 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - # Copyright 2011 OpenStack Foundation. # All Rights Reserved. # @@ -25,7 +23,7 @@ import six -from novaclient.openstack.common.gettextutils import _ # noqa +from novaclient.openstack.common.gettextutils import _ # Used for looking up extensions of text @@ -60,12 +58,12 @@ def int_from_bool_as_string(subject): return bool_from_string(subject) and 1 or 0 -def bool_from_string(subject, strict=False): +def bool_from_string(subject, strict=False, default=False): """Interpret a string as a boolean. A case-insensitive match is performed such that strings matching 't', 'true', 'on', 'y', 'yes', or '1' are considered True and, when - `strict=False`, anything else is considered False. + `strict=False`, anything else returns the value specified by 'default'. Useful for JSON-decoded stuff and config file parsing. @@ -90,7 +88,7 @@ def bool_from_string(subject, strict=False): 'acceptable': acceptable} raise ValueError(msg) else: - return False + return default def safe_decode(text, incoming=None, errors='strict'): @@ -101,7 +99,7 @@ def safe_decode(text, incoming=None, errors='strict'): values http://docs.python.org/2/library/codecs.html :returns: text or a unicode `incoming` encoded representation of it. - :raises TypeError: If text is not an isntance of str + :raises TypeError: If text is not an instance of str """ if not isinstance(text, six.string_types): raise TypeError("%s can't be decoded" % type(text)) @@ -144,7 +142,7 @@ def safe_encode(text, incoming=None, values http://docs.python.org/2/library/codecs.html :returns: text or a bytestring `encoding` encoded representation of it. - :raises TypeError: If text is not an isntance of str + :raises TypeError: If text is not an instance of str """ if not isinstance(text, six.string_types): raise TypeError("%s can't be encoded" % type(text)) @@ -154,11 +152,17 @@ def safe_encode(text, incoming=None, sys.getdefaultencoding()) if isinstance(text, six.text_type): - return text.encode(encoding, errors) + if six.PY3: + return text.encode(encoding, errors).decode(incoming) + else: + return text.encode(encoding, errors) elif text and encoding != incoming: # Decode text before encoding it with `encoding` text = safe_decode(text, incoming, errors) - return text.encode(encoding, errors) + if six.PY3: + return text.encode(encoding, errors).decode(incoming) + else: + return text.encode(encoding, errors) return text diff --git a/novaclient/openstack/common/timeutils.py b/novaclient/openstack/common/timeutils.py index 60f02bcb9..d5ed81d3e 100644 --- a/novaclient/openstack/common/timeutils.py +++ b/novaclient/openstack/common/timeutils.py @@ -1,5 +1,3 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - # Copyright 2011 OpenStack Foundation. # All Rights Reserved. # @@ -50,9 +48,9 @@ def parse_isotime(timestr): try: return iso8601.parse_date(timestr) except iso8601.ParseError as e: - raise ValueError(unicode(e)) + raise ValueError(six.text_type(e)) except TypeError as e: - raise ValueError(unicode(e)) + raise ValueError(six.text_type(e)) def strtime(at=None, fmt=PERFECT_TIME_FORMAT): @@ -79,6 +77,9 @@ def is_older_than(before, seconds): """Return True if before is older than seconds.""" if isinstance(before, six.string_types): before = parse_strtime(before).replace(tzinfo=None) + else: + before = before.replace(tzinfo=None) + return utcnow() - before > datetime.timedelta(seconds=seconds) @@ -86,6 +87,9 @@ def is_newer_than(after, seconds): """Return True if after is newer than seconds.""" if isinstance(after, six.string_types): after = parse_strtime(after).replace(tzinfo=None) + else: + after = after.replace(tzinfo=None) + return after - utcnow() > datetime.timedelta(seconds=seconds) @@ -117,12 +121,15 @@ def iso8601_from_timestamp(timestamp): utcnow.override_time = None -def set_time_override(override_time=datetime.datetime.utcnow()): +def set_time_override(override_time=None): """Overrides utils.utcnow. Make it return a constant time or a list thereof, one at a time. + + :param override_time: datetime instance or list thereof. If not + given, defaults to the current UTC time. """ - utcnow.override_time = override_time + utcnow.override_time = override_time or datetime.datetime.utcnow() def advance_time_delta(timedelta): @@ -175,6 +182,15 @@ def delta_seconds(before, after): datetime objects (as a float, to microsecond resolution). """ delta = after - before + return total_seconds(delta) + + +def total_seconds(delta): + """Return the total seconds of datetime.timedelta object. + + Compute total seconds of datetime.timedelta, datetime.timedelta + doesn't have method total_seconds in Python2.6, calculate it manually. + """ try: return delta.total_seconds() except AttributeError: @@ -185,8 +201,8 @@ def delta_seconds(before, after): def is_soon(dt, window): """Determines if time is going to happen in the next window seconds. - :params dt: the time - :params window: minimum seconds to remain to consider the time not soon + :param dt: the time + :param window: minimum seconds to remain to consider the time not soon :return: True if expiration is within the given duration """ diff --git a/novaclient/openstack/common/uuidutils.py b/novaclient/openstack/common/uuidutils.py index 7608acb94..234b880c9 100644 --- a/novaclient/openstack/common/uuidutils.py +++ b/novaclient/openstack/common/uuidutils.py @@ -1,5 +1,3 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - # Copyright (c) 2012 Intel Corporation. # All Rights Reserved. # diff --git a/novaclient/tests/test_shell.py b/novaclient/tests/test_shell.py index cb40f9988..4d62cd8b1 100644 --- a/novaclient/tests/test_shell.py +++ b/novaclient/tests/test_shell.py @@ -182,6 +182,8 @@ def test_no_auth_url(self): @mock.patch('sys.stdin', side_effect=mock.MagicMock) @mock.patch('getpass.getpass', return_value='password') def test_password(self, mock_getpass, mock_stdin): + mock_stdin.encoding = "utf-8" + # default output of empty tables differs depending between prettytable # versions if (hasattr(prettytable, '__version__') and diff --git a/novaclient/utils.py b/novaclient/utils.py index c7f7a15f9..5bcf47f17 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -177,9 +177,6 @@ def print_list(objs, fields, formatters={}, sortby_index=None): else: result = strutils.safe_encode(pt.get_string()) - if six.PY3: - result = result.decode() - print(result) @@ -247,8 +244,7 @@ def print_dict(d, dict_property="Property", dict_value="Value", wrap=0): pt.add_row([k, v]) result = strutils.safe_encode(pt.get_string()) - if six.PY3: - result = result.decode() + print(result) @@ -270,8 +266,6 @@ def find_resource(manager, name_or_id, **find_args): # now try to get entity as uuid try: tmp_id = strutils.safe_encode(name_or_id) - if six.PY3: - tmp_id = tmp_id.decode() uuid.UUID(tmp_id) return manager.get(tmp_id) except (TypeError, ValueError, exceptions.NotFound): diff --git a/tools/install_venv_common.py b/tools/install_venv_common.py index 0999e2c29..46822e329 100644 --- a/tools/install_venv_common.py +++ b/tools/install_venv_common.py @@ -1,5 +1,3 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - # Copyright 2013 OpenStack Foundation # Copyright 2013 IBM Corp. # @@ -119,11 +117,7 @@ def install_dependencies(self): self.pip_install('setuptools') self.pip_install('pbr') - self.pip_install('-r', self.requirements) - self.pip_install('-r', self.test_requirements) - - def post_process(self): - self.get_distro().post_process() + self.pip_install('-r', self.requirements, '-r', self.test_requirements) def parse_args(self, argv): """Parses command-line arguments.""" @@ -157,14 +151,6 @@ def install_virtualenv(self): ' requires virtualenv, please install it using your' ' favorite package management tool' % self.project) - def post_process(self): - """Any distribution-specific post-processing gets done here. - - In particular, this is useful for applying patches to code inside - the venv. - """ - pass - class Fedora(Distro): """This covers all Fedora-based distributions. @@ -176,10 +162,6 @@ def check_pkg(self, pkg): return self.run_command_with_code(['rpm', '-q', pkg], check_exit_code=False)[1] == 0 - def apply_patch(self, originalfile, patchfile): - self.run_command(['patch', '-N', originalfile, patchfile], - check_exit_code=False) - def install_virtualenv(self): if self.check_cmd('virtualenv'): return @@ -188,27 +170,3 @@ def install_virtualenv(self): self.die("Please install 'python-virtualenv'.") super(Fedora, self).install_virtualenv() - - def post_process(self): - """Workaround for a bug in eventlet. - - This currently affects RHEL6.1, but the fix can safely be - applied to all RHEL and Fedora distributions. - - This can be removed when the fix is applied upstream. - - Nova: https://bugs.launchpad.net/nova/+bug/884915 - Upstream: https://bitbucket.org/eventlet/eventlet/issue/89 - RHEL: https://bugzilla.redhat.com/958868 - """ - - if os.path.exists('contrib/redhat-eventlet.patch'): - # Install "patch" program if it's not there - if not self.check_pkg('patch'): - self.die("Please install 'patch'.") - - # Apply the eventlet patch - self.apply_patch(os.path.join(self.venv, 'lib', self.py_version, - 'site-packages', - 'eventlet/green/subprocess.py'), - 'contrib/redhat-eventlet.patch') From 4045979418e6c4807caf03f83ca164fb817355cc Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Sun, 26 Jan 2014 14:21:58 +0200 Subject: [PATCH 0396/1705] Reuse Resource from oslo All methods of Resource class novaclient is equal to common code. In the process of unification of the clients code we should reuse common functionality from Oslo. Related to blueprint common-client-library-2 Change-Id: I2b72c6b34a44dec3572ca8ad70e41e42a32d48c0 --- novaclient/base.py | 92 ++-------------------------------------------- 1 file changed, 3 insertions(+), 89 deletions(-) diff --git a/novaclient/base.py b/novaclient/base.py index 3759320f5..92671348e 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -30,9 +30,12 @@ import six from novaclient import exceptions +from novaclient.openstack.common.apiclient import base from novaclient.openstack.common import strutils from novaclient import utils +Resource = base.Resource + def getid(obj): """ @@ -410,92 +413,3 @@ def _boot(self, resource_url, response_key, name, image, flavor, return self._create(resource_url, body, response_key, return_raw=return_raw, **kwargs) - - -class Resource(object): - """ - A resource represents a particular instance of an object (server, flavor, - etc). This is pretty much just a bag for attributes. - - :param manager: Manager object - :param info: dictionary representing resource attributes - :param loaded: prevent lazy-loading if set to True - """ - HUMAN_ID = False - NAME_ATTR = 'name' - - def __init__(self, manager, info, loaded=False): - self.manager = manager - self._info = info - self._add_details(info) - self._loaded = loaded - - # NOTE(sirp): ensure `id` is already present because if it isn't we'll - # enter an infinite loop of __getattr__ -> get -> __init__ -> - # __getattr__ -> ... - if 'id' in self.__dict__ and len(str(self.id)) == 36: - self.manager.write_to_completion_cache('uuid', self.id) - - human_id = self.human_id - if human_id: - self.manager.write_to_completion_cache('human_id', human_id) - - @property - def human_id(self): - """Subclasses may override this provide a pretty ID which can be used - for bash completion. - """ - if self.NAME_ATTR in self.__dict__ and self.HUMAN_ID: - name = getattr(self, self.NAME_ATTR) - if name is not None: - return strutils.to_slug(name) - return None - - def _add_details(self, info): - for (k, v) in six.iteritems(info): - try: - setattr(self, k, v) - self._info[k] = v - except AttributeError: - # In this case we already defined the attribute on the class - pass - - def __getattr__(self, k): - if k not in self.__dict__: - #NOTE(bcwaldon): disallow lazy-loading if already loaded once - if not self.is_loaded(): - self.get() - return self.__getattr__(k) - - raise AttributeError(k) - else: - return self.__dict__[k] - - def __repr__(self): - reprkeys = sorted(k for k in self.__dict__.keys() if k[0] != '_' and - k != 'manager') - info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys) - return "<%s %s>" % (self.__class__.__name__, info) - - def get(self): - # set_loaded() first ... so if we have to bail, we know we tried. - self.set_loaded(True) - if not hasattr(self.manager, 'get'): - return - - new = self.manager.get(self.id) - if new: - self._add_details(new._info) - - def __eq__(self, other): - if not isinstance(other, self.__class__): - return False - if hasattr(self, 'id') and hasattr(other, 'id'): - return self.id == other.id - return self._info == other._info - - def is_loaded(self): - return self._loaded - - def set_loaded(self, val): - self._loaded = val From 1151c8ec5da951ecb449a95671947164069de721 Mon Sep 17 00:00:00 2001 From: Christopher Yeoh Date: Sat, 11 Jan 2014 14:28:56 +0800 Subject: [PATCH 0397/1705] Removes unsupported volume commands from V3 API support Removes the volume commands which are not supported by the V3 API. They are removed only from the V3 part of novaclient and so the V2 support remains the same. Partially implements blueprint v3-api Change-Id: I343885c72e7cd020060249ece61135eacab68039 --- novaclient/v3/shell.py | 217 ----------------------------------------- 1 file changed, 217 deletions(-) diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 00d77f5fb..f093469af 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -1392,139 +1392,11 @@ def do_remove_fixed_ip(cs, args): server.remove_fixed_ip(args.address) -def _find_volume(cs, volume): - """Get a volume by name or ID.""" - return utils.find_resource(cs.volumes, volume) - - -def _find_volume_snapshot(cs, snapshot): - """Get a volume snapshot by name or ID.""" - return utils.find_resource(cs.volume_snapshots, snapshot) - - -def _print_volume(volume): - utils.print_dict(volume._info) - - -def _print_volume_snapshot(snapshot): - utils.print_dict(snapshot._info) - - -def _translate_volume_keys(collection): - _translate_keys(collection, - [('displayName', 'display_name'), - ('volumeType', 'volume_type')]) - - -def _translate_volume_snapshot_keys(collection): - _translate_keys(collection, - [('displayName', 'display_name'), - ('volumeId', 'volume_id')]) - - def _translate_availability_zone_keys(collection): _translate_keys(collection, [('zone_name', 'name'), ('zone_state', 'status')]) -@utils.arg('--all-tenants', - dest='all_tenants', - metavar='<0|1>', - nargs='?', - type=int, - const=1, - default=int(strutils.bool_from_string( - os.environ.get("ALL_TENANTS", 'false'), True)), - help='Display information from all tenants (Admin only).') -@utils.arg('--all_tenants', - nargs='?', - type=int, - const=1, - help=argparse.SUPPRESS) -@utils.service_type('volume') -def do_volume_list(cs, args): - """List all the volumes.""" - search_opts = {'all_tenants': args.all_tenants} - volumes = cs.volumes.list(search_opts=search_opts) - _translate_volume_keys(volumes) - - # Create a list of servers to which the volume is attached - for vol in volumes: - servers = [s.get('server_id') for s in vol.attachments] - setattr(vol, 'attached_to', ','.join(map(str, servers))) - utils.print_list(volumes, ['ID', 'Status', 'Display Name', - 'Size', 'Volume Type', 'Attached to']) - - -@utils.arg('volume', metavar='', help='Name or ID of the volume.') -@utils.service_type('volume') -def do_volume_show(cs, args): - """Show details about a volume.""" - volume = _find_volume(cs, args.volume) - _print_volume(volume) - - -@utils.arg('size', - metavar='', - type=int, - help='Size of volume in GB') -@utils.arg('--snapshot-id', - metavar='', - default=None, - help='Optional snapshot id to create the volume from. (Default=None)') -@utils.arg('--snapshot_id', - help=argparse.SUPPRESS) -@utils.arg('--image-id', - metavar='', - help='Optional image id to create the volume from. (Default=None)', - default=None) -@utils.arg('--display-name', - metavar='', - default=None, - help='Optional volume name. (Default=None)') -@utils.arg('--display_name', - help=argparse.SUPPRESS) -@utils.arg('--display-description', - metavar='', - default=None, - help='Optional volume description. (Default=None)') -@utils.arg('--display_description', - help=argparse.SUPPRESS) -@utils.arg('--volume-type', - metavar='', - default=None, - help='Optional volume type. (Default=None)') -@utils.arg('--volume_type', - help=argparse.SUPPRESS) -@utils.arg('--availability-zone', metavar='', - help='Optional Availability Zone for volume. (Default=None)', - default=None) -@utils.service_type('volume') -def do_volume_create(cs, args): - """Add a new volume.""" - volume = cs.volumes.create(args.size, - args.snapshot_id, - args.display_name, - args.display_description, - args.volume_type, - args.availability_zone, - imageRef=args.image_id) - _print_volume(volume) - - -@utils.arg('volume', - metavar='', nargs='+', - help='Name or ID of the volume(s) to delete.') -@utils.service_type('volume') -def do_volume_delete(cs, args): - """Remove volume(s).""" - for volume in args.volume: - try: - _find_volume(cs, volume).delete() - except Exception as e: - print("Delete for volume %s failed: %s" % (volume, e)) - - @utils.arg('server', metavar='', help='Name or ID of server.') @@ -1572,95 +1444,6 @@ def do_volume_detach(cs, args): args.attachment_id) -@utils.service_type('volume') -def do_volume_snapshot_list(cs, _args): - """List all the snapshots.""" - snapshots = cs.volume_snapshots.list() - _translate_volume_snapshot_keys(snapshots) - utils.print_list(snapshots, ['ID', 'Volume ID', 'Status', 'Display Name', - 'Size']) - - -@utils.arg('snapshot', - metavar='', - help='Name or ID of the snapshot.') -@utils.service_type('volume') -def do_volume_snapshot_show(cs, args): - """Show details about a snapshot.""" - snapshot = _find_volume_snapshot(cs, args.snapshot) - _print_volume_snapshot(snapshot) - - -@utils.arg('volume_id', - metavar='', - help='ID of the volume to snapshot') -@utils.arg('--force', - metavar='', - help='Optional flag to indicate whether to snapshot a volume even if its ' - 'attached to a server. (Default=False)', - default=False) -@utils.arg('--display-name', - metavar='', - default=None, - help='Optional snapshot name. (Default=None)') -@utils.arg('--display_name', - help=argparse.SUPPRESS) -@utils.arg('--display-description', - metavar='', - default=None, - help='Optional snapshot description. (Default=None)') -@utils.arg('--display_description', - help=argparse.SUPPRESS) -@utils.service_type('volume') -def do_volume_snapshot_create(cs, args): - """Add a new snapshot.""" - snapshot = cs.volume_snapshots.create(args.volume_id, - args.force, - args.display_name, - args.display_description) - _print_volume_snapshot(snapshot) - - -@utils.arg('snapshot', - metavar='', - help='Name or ID of the snapshot to delete.') -@utils.service_type('volume') -def do_volume_snapshot_delete(cs, args): - """Remove a snapshot.""" - snapshot = _find_volume_snapshot(cs, args.snapshot) - snapshot.delete() - - -def _print_volume_type_list(vtypes): - utils.print_list(vtypes, ['ID', 'Name']) - - -@utils.service_type('volume') -def do_volume_type_list(cs, args): - """Print a list of available 'volume types'.""" - vtypes = cs.volume_types.list() - _print_volume_type_list(vtypes) - - -@utils.arg('name', - metavar='', - help="Name of the new flavor") -@utils.service_type('volume') -def do_volume_type_create(cs, args): - """Create a new volume type.""" - vtype = cs.volume_types.create(args.name) - _print_volume_type_list([vtype]) - - -@utils.arg('id', - metavar='', - help="Unique ID of the volume type to delete") -@utils.service_type('volume') -def do_volume_type_delete(cs, args): - """Delete a specific flavor""" - cs.volume_types.delete(args.id) - - @utils.arg('server', metavar='', help='Name or ID of server.') @utils.arg('console_type', metavar='', From c8ad3157633b26ae324c352a73bb599eb084c115 Mon Sep 17 00:00:00 2001 From: Chris Yeoh Date: Tue, 24 Dec 2013 21:34:22 +1030 Subject: [PATCH 0398/1705] Adds ability to boot a server via the Nova V3 API Creates an images client when attached to the the servers client. This is necessary because the Nova V3 API no longer proxies image queries to glance but when preparing a request to boot a server it is necessary to retreive information about images so we need to talk to both Nova and Glance in the same command. This is a bit ugly, but not much more than the already existing ugliness of using the client class designed to talk to Nova to talk to Glance and Cinder. The long term clean solution is probably to a unified client that is designed to talk to multiple openstack services. Differences between the V2 and V3 API are described here: https://wiki.openstack.org/wiki/NovaAPIv2tov3 Partially implements blueprint v3-api Change-Id: Ib43682f38cd7a3e0f910b75e96685591246e7f67 --- novaclient/base.py | 139 ---------------------- novaclient/shell.py | 19 +++ novaclient/tests/test_shell.py | 2 +- novaclient/tests/v3/fakes.py | 8 ++ novaclient/tests/v3/test_shell.py | 184 ++++++++++++------------------ novaclient/v1_1/servers.py | 140 +++++++++++++++++++++++ novaclient/v3/servers.py | 123 +++++++++++++++++++- novaclient/v3/shell.py | 4 +- 8 files changed, 362 insertions(+), 257 deletions(-) diff --git a/novaclient/base.py b/novaclient/base.py index 92671348e..11a6d6f80 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -20,7 +20,6 @@ """ import abc -import base64 import contextlib import hashlib import inspect @@ -31,7 +30,6 @@ from novaclient import exceptions from novaclient.openstack.common.apiclient import base -from novaclient.openstack.common import strutils from novaclient import utils Resource = base.Resource @@ -276,140 +274,3 @@ def _parse_block_device_mapping(self, block_device_mapping): bdm.append(bdm_dict) return bdm - - def _boot(self, resource_url, response_key, name, image, flavor, - meta=None, files=None, userdata=None, - reservation_id=None, return_raw=False, min_count=None, - max_count=None, security_groups=None, key_name=None, - availability_zone=None, block_device_mapping=None, - block_device_mapping_v2=None, nics=None, scheduler_hints=None, - config_drive=None, admin_pass=None, disk_config=None, **kwargs): - """ - Create (boot) a new server. - - :param name: Something to name the server. - :param image: The :class:`Image` to boot with. - :param flavor: The :class:`Flavor` to boot onto. - :param meta: A dict of arbitrary key/value metadata to store for this - server. A maximum of five entries is allowed, and both - keys and values must be 255 characters or less. - :param files: A dict of files to overwrite on the server upon boot. - Keys are file names (i.e. ``/etc/passwd``) and values - are the file contents (either as a string or as a - file-like object). A maximum of five entries is allowed, - and each file must be 10k or less. - :param reservation_id: a UUID for the set of servers being requested. - :param return_raw: If True, don't try to coearse the result into - a Resource object. - :param security_groups: list of security group names - :param key_name: (optional extension) name of keypair to inject into - the instance - :param availability_zone: Name of the availability zone for instance - placement. - :param block_device_mapping: A dict of block device mappings for this - server. - :param block_device_mapping_v2: A dict of block device mappings V2 for - this server. - :param nics: (optional extension) an ordered list of nics to be - added to this server, with information about - connected networks, fixed ips, etc. - :param scheduler_hints: (optional extension) arbitrary key-value pairs - specified by the client to help boot an instance. - :param config_drive: (optional extension) value for config drive - either boolean, or volume-id - :param admin_pass: admin password for the server. - :param disk_config: (optional extension) control how the disk is - partitioned when the server is created. - """ - body = {"server": { - "name": name, - "imageRef": str(getid(image)) if image else '', - "flavorRef": str(getid(flavor)), - }} - if userdata: - if hasattr(userdata, 'read'): - userdata = userdata.read() - - if six.PY3: - userdata = userdata.encode("utf-8") - else: - userdata = strutils.safe_encode(userdata) - - body["server"]["user_data"] = base64.b64encode(userdata) - if meta: - body["server"]["metadata"] = meta - if reservation_id: - body["server"]["reservation_id"] = reservation_id - if key_name: - body["server"]["key_name"] = key_name - if scheduler_hints: - body['os:scheduler_hints'] = scheduler_hints - if config_drive: - body["server"]["config_drive"] = config_drive - if admin_pass: - body["server"]["adminPass"] = admin_pass - if not min_count: - min_count = 1 - if not max_count: - max_count = min_count - body["server"]["min_count"] = min_count - body["server"]["max_count"] = max_count - - if security_groups: - body["server"]["security_groups"] =\ - [{'name': sg} for sg in security_groups] - - # Files are a slight bit tricky. They're passed in a "personality" - # list to the POST. Each item is a dict giving a file name and the - # base64-encoded contents of the file. We want to allow passing - # either an open file *or* some contents as files here. - if files: - personality = body['server']['personality'] = [] - for filepath, file_or_string in sorted(files.items(), - key=lambda x: x[0]): - if hasattr(file_or_string, 'read'): - data = file_or_string.read() - else: - data = file_or_string - personality.append({ - 'path': filepath, - 'contents': base64.b64encode(data.encode('utf-8')), - }) - - if availability_zone: - body["server"]["availability_zone"] = availability_zone - - # Block device mappings are passed as a list of dictionaries - if block_device_mapping: - body['server']['block_device_mapping'] = \ - self._parse_block_device_mapping(block_device_mapping) - elif block_device_mapping_v2: - # Append the image to the list only if we have new style BDMs - if image: - bdm_dict = {'uuid': image.id, 'source_type': 'image', - 'destination_type': 'local', 'boot_index': 0, - 'delete_on_termination': True} - block_device_mapping_v2.insert(0, bdm_dict) - - body['server']['block_device_mapping_v2'] = block_device_mapping_v2 - - if nics is not None: - # NOTE(tr3buchet): nics can be an empty list - all_net_data = [] - for nic_info in nics: - net_data = {} - # if value is empty string, do not send value in body - if nic_info.get('net-id'): - net_data['uuid'] = nic_info['net-id'] - if nic_info.get('v4-fixed-ip'): - net_data['fixed_ip'] = nic_info['v4-fixed-ip'] - if nic_info.get('port-id'): - net_data['port'] = nic_info['port-id'] - all_net_data.append(net_data) - body['server']['networks'] = all_net_data - - if disk_config is not None: - body['server']['OS-DCF:diskConfig'] = disk_config - - return self._create(resource_url, body, response_key, - return_raw=return_raw, **kwargs) diff --git a/novaclient/shell.py b/novaclient/shell.py index c434857d4..3f17091f6 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -686,6 +686,25 @@ def main(self, argv): except exc.AuthorizationFailure: raise exc.CommandError("Unable to authorize user") + if os_compute_api_version == "3" and service_type != 'image': + # NOTE(cyeoh): create an image based client because the + # images api is no longer proxied by the V3 API and we + # sometimes need to be able to look up images information + # via glance when connected to the nova api. + image_service_type = 'image' + self.cs.image_cs = client.Client( + options.os_compute_api_version, os_username, + os_password, os_tenant_name, tenant_id=os_tenant_id, + auth_url=os_auth_url, insecure=insecure, + region_name=os_region_name, endpoint_type=endpoint_type, + extensions=self.extensions, service_type=image_service_type, + service_name=service_name, auth_system=os_auth_system, + auth_plugin=auth_plugin, + volume_service_name=volume_service_name, + timings=args.timings, bypass_url=bypass_url, + os_cache=os_cache, http_log_debug=options.debug, + cacert=cacert, timeout=timeout) + args.func(self.cs, args) if args.timings: diff --git a/novaclient/tests/test_shell.py b/novaclient/tests/test_shell.py index 1b53f2626..7cbb89b9b 100644 --- a/novaclient/tests/test_shell.py +++ b/novaclient/tests/test_shell.py @@ -222,7 +222,7 @@ def _test_service_type(self, version, service_type, mock_client): cmd = '--os-compute-api-version %s list' % version self.make_env() self.shell(cmd) - _, client_kwargs = mock_client.call_args + _, client_kwargs = mock_client.call_args_list[0] self.assertEqual(service_type, client_kwargs['service_type']) @mock.patch('novaclient.client.Client') diff --git a/novaclient/tests/v3/fakes.py b/novaclient/tests/v3/fakes.py index 772141411..ffc7e6056 100644 --- a/novaclient/tests/v3/fakes.py +++ b/novaclient/tests/v3/fakes.py @@ -157,6 +157,14 @@ def get_servers_1234_os_attach_interfaces(self, **kw): def post_servers_1234_os_attach_interfaces(self, **kw): return (200, {}, {'interface_attachment': {}}) + def post_servers(self, body, **kw): + assert set(body.keys()) <= set(['server']) + fakes.assert_has_keys(body['server'], + required=['name', 'image_ref', 'flavor_ref'], + optional=['metadata', 'personality', + 'os-scheduler-hints:scheduler_hints']) + return (202, {}, self.get_servers_1234()[2]) + # # Server Actions # diff --git a/novaclient/tests/v3/test_shell.py b/novaclient/tests/v3/test_shell.py index eb125c3ca..615a80a5d 100644 --- a/novaclient/tests/v3/test_shell.py +++ b/novaclient/tests/v3/test_shell.py @@ -178,11 +178,11 @@ def test_boot(self): self.assert_called_anytime( 'POST', '/servers', {'server': { - 'flavorRef': '1', + 'flavor_ref': '1', 'name': 'some-server', - 'imageRef': '1', - 'min_count': 1, - 'max_count': 1, + 'image_ref': '1', + 'os-multiple-create:min_count': 1, + 'os-multiple-create:max_count': 1, }}, ) @@ -192,11 +192,11 @@ def test_boot_multiple(self): self.assert_called_anytime( 'POST', '/servers', {'server': { - 'flavorRef': '1', + 'flavor_ref': '1', 'name': 'some-server', - 'imageRef': '1', - 'min_count': 1, - 'max_count': 3, + 'image_ref': '1', + 'os-multiple-create:min_count': 1, + 'os-multiple-create:max_count': 3, }}, ) @@ -206,11 +206,11 @@ def test_boot_image_with(self): self.assert_called_anytime( 'POST', '/servers', {'server': { - 'flavorRef': '1', + 'flavor_ref': '1', 'name': 'some-server', - 'imageRef': '1', - 'min_count': 1, - 'max_count': 1, + 'image_ref': '1', + 'os-multiple-create:min_count': 1, + 'os-multiple-create:max_count': 1, }}, ) @@ -219,12 +219,12 @@ def test_boot_key(self): self.assert_called_anytime( 'POST', '/servers', {'server': { - 'flavorRef': '1', + 'flavor_ref': '1', 'name': 'some-server', - 'imageRef': '1', + 'image_ref': '1', 'key_name': '1', - 'min_count': 1, - 'max_count': 1, + 'os-multiple-create:min_count': 1, + 'os-multiple-create:max_count': 1, }}, ) @@ -243,12 +243,13 @@ def test_boot_user_data(self): self.assert_called_anytime( 'POST', '/servers', {'server': { - 'flavorRef': '1', + 'flavor_ref': '1', 'name': 'some-server', - 'imageRef': '1', - 'min_count': 1, - 'max_count': 1, - 'user_data': base64.b64encode(file_text.encode('utf-8')) + 'image_ref': '1', + 'os-multiple-create:min_count': 1, + 'os-multiple-create:max_count': 1, + 'os-user-data:user_data': base64.b64encode( + file_text.encode('utf-8')) }}, ) @@ -259,12 +260,12 @@ def test_boot_avzone(self): self.assert_called_anytime( 'POST', '/servers', {'server': { - 'flavorRef': '1', + 'flavor_ref': '1', 'name': 'some-server', - 'imageRef': '1', - 'availability_zone': 'avzone', - 'min_count': 1, - 'max_count': 1 + 'image_ref': '1', + 'os-availability-zone:availability_zone': 'avzone', + 'os-multiple-create:min_count': 1, + 'os-multiple-create:max_count': 1 }}, ) @@ -275,13 +276,13 @@ def test_boot_secgroup(self): self.assert_called_anytime( 'POST', '/servers', {'server': { - 'security_groups': [{'name': 'secgroup1'}, - {'name': 'secgroup2'}], - 'flavorRef': '1', + 'os-security-groups:security_groups': [{'name': 'secgroup1'}, + {'name': 'secgroup2'}], + 'flavor_ref': '1', 'name': 'some-server', - 'imageRef': '1', - 'min_count': 1, - 'max_count': 1, + 'image_ref': '1', + 'os-multiple-create:min_count': 1, + 'os-multiple-create:max_count': 1, }}, ) @@ -291,12 +292,12 @@ def test_boot_config_drive(self): self.assert_called_anytime( 'POST', '/servers', {'server': { - 'flavorRef': '1', + 'flavor_ref': '1', 'name': 'some-server', - 'imageRef': '1', - 'min_count': 1, - 'max_count': 1, - 'config_drive': True + 'image_ref': '1', + 'os-multiple-create:min_count': 1, + 'os-multiple-create:max_count': 1, + 'os-config-drive:config_drive': True }}, ) @@ -306,12 +307,12 @@ def test_boot_config_drive_custom(self): self.assert_called_anytime( 'POST', '/servers', {'server': { - 'flavorRef': '1', + 'flavor_ref': '1', 'name': 'some-server', - 'imageRef': '1', - 'min_count': 1, - 'max_count': 1, - 'config_drive': '/dev/hda' + 'image_ref': '1', + 'os-multiple-create:min_count': 1, + 'os-multiple-create:max_count': 1, + 'os-config-drive:config_drive': '/dev/hda' }}, ) @@ -335,20 +336,20 @@ def test_boot_no_image_bdms(self): 'boot --flavor 1 --block_device_mapping vda=blah:::0 some-server' ) self.assert_called_anytime( - 'POST', '/os-volumes_boot', + 'POST', '/servers', {'server': { - 'flavorRef': '1', + 'flavor_ref': '1', 'name': 'some-server', - 'block_device_mapping': [ + 'os-block-device-mapping:block_device_mapping': [ { 'volume_id': 'blah', 'delete_on_termination': '0', 'device_name': 'vda' } ], - 'imageRef': '', - 'min_count': 1, - 'max_count': 1, + 'image_ref': '', + 'os-multiple-create:min_count': 1, + 'os-multiple-create:max_count': 1, }}, ) @@ -359,17 +360,17 @@ def test_boot_image_bdms(self): 'type=disk,shutdown=preserve some-server' ) self.assert_called_anytime( - 'POST', '/os-volumes_boot', + 'POST', '/servers', {'server': { - 'flavorRef': '1', + 'flavor_ref': '1', 'name': 'some-server', - 'block_device_mapping': [ + 'os-block-device-mapping:block_device_mapping': [ {'device_name': 'id', 'volume_id': 'fake-id,source=volume,dest=volume,device=vda,size=1,' 'format=ext4,type=disk,shutdown=preserve'}], - 'imageRef': '1', - 'min_count': 1, - 'max_count': 1, + 'image_ref': '1', + 'os-multiple-create:min_count': 1, + 'os-multiple-create:max_count': 1, }}, ) @@ -379,12 +380,12 @@ def test_boot_metadata(self): self.assert_called_anytime( 'POST', '/servers', {'server': { - 'flavorRef': '1', + 'flavor_ref': '1', 'name': 'some-server', - 'imageRef': '1', + 'image_ref': '1', 'metadata': {'foo': 'bar=pants', 'spam': 'eggs'}, - 'min_count': 1, - 'max_count': 1, + 'os-multiple-create:min_count': 1, + 'os-multiple-create:max_count': 1, }}, ) @@ -396,13 +397,14 @@ def test_boot_hints(self): 'POST', '/servers', { 'server': { - 'flavorRef': '1', + 'flavor_ref': '1', 'name': 'some-server', - 'imageRef': '1', - 'min_count': 1, - 'max_count': 1, + 'image_ref': '1', + 'os-multiple-create:min_count': 1, + 'os-multiple-create:max_count': 1, + 'os-scheduler-hints:scheduler_hints': { + 'a': ['b1=c1', 'b0=c0'], 'a2': 'b2=c2'}, }, - 'os:scheduler_hints': {'a': ['b1=c1', 'b0=c0'], 'a2': 'b2=c2'}, }, ) @@ -414,11 +416,11 @@ def test_boot_nics(self): 'POST', '/servers', { 'server': { - 'flavorRef': '1', + 'flavor_ref': '1', 'name': 'some-server', - 'imageRef': '1', - 'min_count': 1, - 'max_count': 1, + 'image_ref': '1', + 'os-multiple-create:min_count': 1, + 'os-multiple-create:max_count': 1, 'networks': [ {'uuid': 'a=c', 'fixed_ip': '10.0.0.1'}, ], @@ -441,57 +443,17 @@ def test_boot_nics_no_netid_or_portid(self): '--nic v4-fixed-ip=10.0.0.1 some-server') self.assertRaises(exceptions.CommandError, self.run_command, cmd) - def test_boot_files(self): - file_text = 'text' - - with mock.patch('novaclient.v3.shell.open', create=True) as mock_open: - mock_open.return_value = file_text - testfile = 'some_dir/some_file.txt' - - self.run_command('boot --flavor 1 --image 1 --user_data %s ' - 'some-server' % testfile) - - cmd = ('boot some-server --flavor 1 --image 1' - ' --file /tmp/foo=%s --file /tmp/bar=%s') - self.run_command(cmd % (testfile, testfile)) - - mock_open.assert_called_with(testfile) - - self.assert_called_anytime( - 'POST', '/servers', - {'server': { - 'flavorRef': '1', - 'name': 'some-server', - 'imageRef': '1', - 'min_count': 1, - 'max_count': 1, - 'personality': [ - {'path': '/tmp/bar', - 'contents': base64.b64encode(file_text.encode('utf-8'))}, - {'path': '/tmp/foo', - 'contents': base64.b64encode(file_text.encode('utf-8'))}, - ] - }}, - ) - - def test_boot_invalid_files(self): - invalid_file = os.path.join(os.path.dirname(__file__), - 'asdfasdfasdfasdf') - cmd = ('boot some-server --flavor 1 --image 1' - ' --file /foo=%s' % invalid_file) - self.assertRaises(exceptions.CommandError, self.run_command, cmd) - def test_boot_num_instances(self): self.run_command('boot --image 1 --flavor 1 --num-instances 3 server') self.assert_called_anytime( 'POST', '/servers', { 'server': { - 'flavorRef': '1', + 'flavor_ref': '1', 'name': 'server', - 'imageRef': '1', - 'min_count': 1, - 'max_count': 3, + 'image_ref': '1', + 'os-multiple-create:min_count': 1, + 'os-multiple-create:max_count': 3, } }) diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index abbe31215..0553f1401 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -19,11 +19,14 @@ Server interface. """ +import base64 + import six from novaclient import base from novaclient import crypto from novaclient.openstack.common.py3kcompat import urlutils +from novaclient.openstack.common import strutils from novaclient.v1_1.security_groups import SecurityGroup REBOOT_SOFT, REBOOT_HARD = 'SOFT', 'HARD' @@ -385,6 +388,143 @@ def interface_detach(self, port_id): class ServerManager(base.BootingManagerWithFind): resource_class = Server + def _boot(self, resource_url, response_key, name, image, flavor, + meta=None, files=None, userdata=None, + reservation_id=None, return_raw=False, min_count=None, + max_count=None, security_groups=None, key_name=None, + availability_zone=None, block_device_mapping=None, + block_device_mapping_v2=None, nics=None, scheduler_hints=None, + config_drive=None, admin_pass=None, disk_config=None, **kwargs): + """ + Create (boot) a new server. + + :param name: Something to name the server. + :param image: The :class:`Image` to boot with. + :param flavor: The :class:`Flavor` to boot onto. + :param meta: A dict of arbitrary key/value metadata to store for this + server. A maximum of five entries is allowed, and both + keys and values must be 255 characters or less. + :param files: A dict of files to overwrite on the server upon boot. + Keys are file names (i.e. ``/etc/passwd``) and values + are the file contents (either as a string or as a + file-like object). A maximum of five entries is allowed, + and each file must be 10k or less. + :param reservation_id: a UUID for the set of servers being requested. + :param return_raw: If True, don't try to coearse the result into + a Resource object. + :param security_groups: list of security group names + :param key_name: (optional extension) name of keypair to inject into + the instance + :param availability_zone: Name of the availability zone for instance + placement. + :param block_device_mapping: A dict of block device mappings for this + server. + :param block_device_mapping_v2: A dict of block device mappings V2 for + this server. + :param nics: (optional extension) an ordered list of nics to be + added to this server, with information about + connected networks, fixed ips, etc. + :param scheduler_hints: (optional extension) arbitrary key-value pairs + specified by the client to help boot an instance. + :param config_drive: (optional extension) value for config drive + either boolean, or volume-id + :param admin_pass: admin password for the server. + :param disk_config: (optional extension) control how the disk is + partitioned when the server is created. + """ + body = {"server": { + "name": name, + "imageRef": str(base.getid(image)) if image else '', + "flavorRef": str(base.getid(flavor)), + }} + if userdata: + if hasattr(userdata, 'read'): + userdata = userdata.read() + + if six.PY3: + userdata = userdata.encode("utf-8") + else: + userdata = strutils.safe_encode(userdata) + + body["server"]["user_data"] = base64.b64encode(userdata) + if meta: + body["server"]["metadata"] = meta + if reservation_id: + body["server"]["reservation_id"] = reservation_id + if key_name: + body["server"]["key_name"] = key_name + if scheduler_hints: + body['os:scheduler_hints'] = scheduler_hints + if config_drive: + body["server"]["config_drive"] = config_drive + if admin_pass: + body["server"]["adminPass"] = admin_pass + if not min_count: + min_count = 1 + if not max_count: + max_count = min_count + body["server"]["min_count"] = min_count + body["server"]["max_count"] = max_count + + if security_groups: + body["server"]["security_groups"] =\ + [{'name': sg} for sg in security_groups] + + # Files are a slight bit tricky. They're passed in a "personality" + # list to the POST. Each item is a dict giving a file name and the + # base64-encoded contents of the file. We want to allow passing + # either an open file *or* some contents as files here. + if files: + personality = body['server']['personality'] = [] + for filepath, file_or_string in sorted(files.items(), + key=lambda x: x[0]): + if hasattr(file_or_string, 'read'): + data = file_or_string.read() + else: + data = file_or_string + personality.append({ + 'path': filepath, + 'contents': base64.b64encode(data.encode('utf-8')), + }) + + if availability_zone: + body["server"]["availability_zone"] = availability_zone + + # Block device mappings are passed as a list of dictionaries + if block_device_mapping: + body['server']['block_device_mapping'] = \ + self._parse_block_device_mapping(block_device_mapping) + elif block_device_mapping_v2: + # Append the image to the list only if we have new style BDMs + if image: + bdm_dict = {'uuid': image.id, 'source_type': 'image', + 'destination_type': 'local', 'boot_index': 0, + 'delete_on_termination': True} + block_device_mapping_v2.insert(0, bdm_dict) + + body['server']['block_device_mapping_v2'] = block_device_mapping_v2 + + if nics is not None: + # NOTE(tr3buchet): nics can be an empty list + all_net_data = [] + for nic_info in nics: + net_data = {} + # if value is empty string, do not send value in body + if nic_info.get('net-id'): + net_data['uuid'] = nic_info['net-id'] + if nic_info.get('v4-fixed-ip'): + net_data['fixed_ip'] = nic_info['v4-fixed-ip'] + if nic_info.get('port-id'): + net_data['port'] = nic_info['port-id'] + all_net_data.append(net_data) + body['server']['networks'] = all_net_data + + if disk_config is not None: + body['server']['OS-DCF:diskConfig'] = disk_config + + return self._create(resource_url, body, response_key, + return_raw=return_raw, **kwargs) + def get(self, server): """ Get a server. diff --git a/novaclient/v3/servers.py b/novaclient/v3/servers.py index f27b2643d..b097ffe65 100644 --- a/novaclient/v3/servers.py +++ b/novaclient/v3/servers.py @@ -19,11 +19,14 @@ Server interface. """ +import base64 + import six from novaclient import base from novaclient import crypto from novaclient.openstack.common.py3kcompat import urlutils +from novaclient.openstack.common import strutils REBOOT_SOFT, REBOOT_HARD = 'SOFT', 'HARD' @@ -349,6 +352,121 @@ def interface_detach(self, port_id): class ServerManager(base.BootingManagerWithFind): resource_class = Server + def _boot(self, resource_url, response_key, name, image, flavor, + meta=None, userdata=None, + reservation_id=None, return_raw=False, min_count=None, + max_count=None, security_groups=None, key_name=None, + availability_zone=None, block_device_mapping=None, + block_device_mapping_v2=None, nics=None, scheduler_hints=None, + config_drive=None, admin_pass=None, **kwargs): + """ + Create (boot) a new server. + + :param name: Something to name the server. + :param image: The :class:`Image` to boot with. + :param flavor: The :class:`Flavor` to boot onto. + :param meta: A dict of arbitrary key/value metadata to store for this + server. A maximum of five entries is allowed, and both + keys and values must be 255 characters or less. + :param reservation_id: a UUID for the set of servers being requested. + :param return_raw: If True, don't try to coearse the result into + a Resource object. + :param security_groups: list of security group names + :param key_name: (optional extension) name of keypair to inject into + the instance + :param availability_zone: Name of the availability zone for instance + placement. + :param block_device_mapping: A dict of block device mappings for this + server. + :param block_device_mapping_v2: A dict of block device mappings V2 for + this server. + :param nics: (optional extension) an ordered list of nics to be + added to this server, with information about + connected networks, fixed ips, etc. + :param scheduler_hints: (optional extension) arbitrary key-value pairs + specified by the client to help boot an instance. + :param config_drive: (optional extension) value for config drive + either boolean, or volume-id + :param admin_pass: admin password for the server. + """ + body = {"server": { + "name": name, + "image_ref": str(base.getid(image)) if image else '', + "flavor_ref": str(base.getid(flavor)), + }} + if userdata: + if hasattr(userdata, 'read'): + userdata = userdata.read() + + if six.PY3: + userdata = userdata.encode("utf-8") + else: + userdata = strutils.safe_encode(userdata) + + body["server"][ + "os-user-data:user_data"] = base64.b64encode(userdata) + if meta: + body["server"]["metadata"] = meta + if reservation_id: + body["server"][ + "os-multiple-create:reservation_id"] = reservation_id + if key_name: + body["server"]["key_name"] = key_name + if scheduler_hints: + body["server"][ + "os-scheduler-hints:scheduler_hints"] = scheduler_hints + if config_drive: + body["server"]["os-config-drive:config_drive"] = config_drive + if admin_pass: + body["server"]["admin_password"] = admin_pass + if not min_count: + min_count = 1 + if not max_count: + max_count = min_count + body["server"]["os-multiple-create:min_count"] = min_count + body["server"]["os-multiple-create:max_count"] = max_count + + if security_groups: + body["server"]["os-security-groups:security_groups"] = \ + [{'name': sg} for sg in security_groups] + + if availability_zone: + body["server"][ + "os-availability-zone:availability_zone"] = availability_zone + + # Block device mappings are passed as a list of dictionaries + if block_device_mapping: + bdm_param = 'os-block-device-mapping:block_device_mapping' + body['server'][bdm_param] = \ + self._parse_block_device_mapping(block_device_mapping) + elif block_device_mapping_v2: + # Append the image to the list only if we have new style BDMs + if image: + bdm_dict = {'uuid': image.id, 'source_type': 'image', + 'destination_type': 'local', 'boot_index': 0, + 'delete_on_termination': True} + block_device_mapping_v2.insert(0, bdm_dict) + + body['server'][bdm_param] = block_device_mapping_v2 + + if nics is not None: + # NOTE(tr3buchet): nics can be an empty list + all_net_data = [] + for nic_info in nics: + net_data = {} + # if value is empty string, do not send value in body + if nic_info.get('net-id'): + net_data['uuid'] = nic_info['net-id'] + if nic_info.get('v4-fixed-ip'): + net_data['fixed_ip'] = nic_info['v4-fixed-ip'] + if nic_info.get('port-id'): + net_data['port'] = nic_info['port-id'] + all_net_data.append(net_data) + body['server']['networks'] = all_net_data + + return self._create(resource_url, body, response_key, + return_raw=return_raw, **kwargs) + def get(self, server): """ Get a server. @@ -623,13 +741,10 @@ def create(self, name, image, flavor, meta=None, files=None, **kwargs) if block_device_mapping: - resource_url = "/os-volumes_boot" boot_kwargs['block_device_mapping'] = block_device_mapping elif block_device_mapping_v2: - resource_url = "/os-volumes_boot" boot_kwargs['block_device_mapping_v2'] = block_device_mapping_v2 - else: - resource_url = "/servers" + resource_url = "/servers" if nics: boot_kwargs['nics'] = nics diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index f093469af..89062ee92 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -74,12 +74,12 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): "be 0") if args.image: - image = _find_image(cs, args.image) + image = _find_image(cs.image_cs, args.image) else: image = None if not image and args.image_with: - images = _match_image(cs, args.image_with) + images = _match_image(cs.image_cs, args.image_with) if images: # TODO(harlowja): log a warning that we # are selecting the first of many? From 6b070c82d429ce53338b28189ec34a01a44748e4 Mon Sep 17 00:00:00 2001 From: Sergio Cazzolato Date: Thu, 12 Dec 2013 23:17:28 -0500 Subject: [PATCH 0399/1705] Fix i18n messages in novaclient, part I This change make all the text visible by the user i18n. The messages changes are in prints, logs, exceptions, helps, etc Pep8 errors about "Multiple positional placeholders" also fixed Change-Id: I731afea790baddbc34d059b93a35e3d275fc1df8 --- novaclient/auth_plugin.py | 5 +- novaclient/client.py | 24 +++--- novaclient/openstack/common/strutils.py | 4 +- novaclient/shell.py | 76 ++++++++++--------- novaclient/utils.py | 17 +++-- novaclient/v1_1/contrib/baremetal.py | 32 ++++---- novaclient/v1_1/contrib/cells.py | 10 ++- novaclient/v1_1/contrib/host_evacuate.py | 8 +- .../v1_1/contrib/host_servers_migrate.py | 3 +- novaclient/v1_1/contrib/instance_action.py | 7 +- .../v1_1/contrib/metadata_extensions.py | 8 +- novaclient/v1_1/contrib/migrations.py | 7 +- novaclient/v1_1/contrib/tenant_networks.py | 7 +- novaclient/v1_1/flavor_access.py | 5 +- novaclient/v1_1/flavors.py | 15 ++-- novaclient/v1_1/networks.py | 3 +- novaclient/v1_1/security_group_rules.py | 9 ++- 17 files changed, 132 insertions(+), 108 deletions(-) diff --git a/novaclient/auth_plugin.py b/novaclient/auth_plugin.py index 843489788..1c7227fdc 100644 --- a/novaclient/auth_plugin.py +++ b/novaclient/auth_plugin.py @@ -15,11 +15,12 @@ # under the License. import logging -import pkg_resources +import pkg_resources import six from novaclient import exceptions +from novaclient.openstack.common.gettextutils import _ from novaclient import utils @@ -39,7 +40,7 @@ def discover_auth_systems(): try: auth_plugin = ep.load() except (ImportError, pkg_resources.UnknownExtra, AttributeError) as e: - logger.debug("ERROR: Cannot load auth plugin %s" % ep.name) + logger.debug(_("ERROR: Cannot load auth plugin %s") % ep.name) logger.debug(e, exc_info=1) else: _discovered_plugins[ep.name] = auth_plugin diff --git a/novaclient/client.py b/novaclient/client.py index d89331322..105d8075e 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -31,6 +31,7 @@ import simplejson as json from novaclient import exceptions +from novaclient.openstack.common.gettextutils import _ from novaclient.openstack.common.py3kcompat import urlutils from novaclient import service_catalog from novaclient import utils @@ -160,11 +161,10 @@ def http_log_req(self, method, url, kwargs): def http_log_resp(self, resp): if not self.http_log_debug: return - self._logger.debug( - "RESP: [%s] %s\nRESP BODY: %s\n", - resp.status_code, - resp.headers, - resp.text) + self._logger.debug(_("RESP: [%(status)s] %(headers)s\nRESP BODY: " + "%(text)s\n"), {'status': resp.status_code, + 'headers': resp.headers, + 'text': resp.text}) def request(self, url, method, **kwargs): kwargs.setdefault('headers', kwargs.get('headers', {})) @@ -288,13 +288,14 @@ def _extract_service_catalog(self, url, resp, body, extract_token=True): self.management_url = management_url.rstrip('/') return None except exceptions.AmbiguousEndpoints: - print("Found more than one valid endpoint. Use a more " - "restrictive filter") + print(_("Found more than one valid endpoint. Use a more " + "restrictive filter")) raise except KeyError: raise exceptions.AuthorizationFailure() except exceptions.EndpointNotFound: - print("Could not find any suitable endpoint. Correct region?") + print(_("Could not find any suitable endpoint. Correct " + "region?")) raise elif resp.status_code == 305: @@ -317,7 +318,7 @@ def _fetch_endpoints_from_auth(self, url): # GET ...:5001/v2.0/tokens/#####/endpoints url = '/'.join([url, 'tokens', '%s?belongsTo=%s' % (self.proxy_token, self.proxy_tenant_id)]) - self._logger.debug("Using Endpoint URL: %s" % url) + self._logger.debug(_("Using Endpoint URL: %s") % url) resp, body = self._time_request( url, "GET", headers={'X-Auth-Token': self.auth_token}) return self._extract_service_catalog(url, resp, body, @@ -463,8 +464,9 @@ def get_client_class(version): try: client_path = version_map[str(version)] except (KeyError, ValueError): - msg = "Invalid client version '%s'. must be one of: %s" % ( - (version, ', '.join(version_map.keys()))) + msg = _("Invalid client version '%(version)s'. must be one of: " + "%(keys)s") % {'version': version, + 'keys': ''.join(version_map.keys())} raise exceptions.UnsupportedVersion(msg) return utils.import_class(client_path) diff --git a/novaclient/openstack/common/strutils.py b/novaclient/openstack/common/strutils.py index 65b485448..c01de557a 100644 --- a/novaclient/openstack/common/strutils.py +++ b/novaclient/openstack/common/strutils.py @@ -102,7 +102,7 @@ def safe_decode(text, incoming=None, errors='strict'): :raises TypeError: If text is not an instance of str """ if not isinstance(text, six.string_types): - raise TypeError("%s can't be decoded" % type(text)) + raise TypeError(_("%s can't be decoded") % type(text)) if isinstance(text, six.text_type): return text @@ -145,7 +145,7 @@ def safe_encode(text, incoming=None, :raises TypeError: If text is not an instance of str """ if not isinstance(text, six.string_types): - raise TypeError("%s can't be encoded" % type(text)) + raise TypeError(_("%s can't be encoded") % type(text)) if not incoming: incoming = (sys.stdin.encoding or diff --git a/novaclient/shell.py b/novaclient/shell.py index c434857d4..bea376d63 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -46,6 +46,7 @@ from novaclient import exceptions as exc import novaclient.extension from novaclient.openstack.common import cliutils +from novaclient.openstack.common.gettextutils import _ from novaclient.openstack.common import strutils from novaclient import utils from novaclient.v1_1 import shell as shell_v1_1 @@ -70,10 +71,10 @@ def positive_non_zero_float(text): try: value = float(text) except ValueError: - msg = "%s must be a float" % text + msg = _("%s must be a float") % text raise argparse.ArgumentTypeError(msg) if value <= 0: - msg = "%s must be greater than 0" % text + msg = _("%s must be greater than 0") % text raise argparse.ArgumentTypeError(msg) return value @@ -137,7 +138,8 @@ def save(self, auth_token, management_url, tenant_id): # Nothing changed.... return if not all([management_url, auth_token, tenant_id]): - raise ValueError("Unable to save empty management url/auth token") + raise ValueError(_("Unable to save empty management url/auth " + "token")) value = "|".join([str(auth_token), str(management_url), str(tenant_id)]) @@ -220,8 +222,8 @@ def error(self, message): #FIXME(lzyeval): if changes occur in argparse.ArgParser._check_value choose_from = ' (choose from' progparts = self.prog.partition(' ') - self.exit(2, "error: %(errmsg)s\nTry '%(mainp)s help %(subp)s'" - " for more information.\n" % + self.exit(2, _("error: %(errmsg)s\nTry '%(mainp)s help %(subp)s'" + " for more information.\n") % {'errmsg': message.split(choose_from)[0], 'mainp': progparts[0], 'subp': progparts[2]}) @@ -252,25 +254,25 @@ def get_base_parser(self): parser.add_argument('--debug', default=False, action='store_true', - help="Print debugging output") + help=_("Print debugging output")) parser.add_argument('--os-cache', default=strutils.bool_from_string( utils.env('OS_CACHE', default=False), True), action='store_true', - help="Use the auth token cache. Defaults to False if env[OS_CACHE]" - " is not set.") + help=_("Use the auth token cache. Defaults to False if " + "env[OS_CACHE] is not set.")) parser.add_argument('--timings', default=False, action='store_true', - help="Print call timing info") + help=_("Print call timing info")) parser.add_argument('--timeout', default=600, metavar='', type=positive_non_zero_float, - help="Set HTTP call timeout (in seconds)") + help=_("Set HTTP call timeout (in seconds)")) parser.add_argument('--os-auth-token', default=utils.env('OS_AUTH_TOKEN'), @@ -279,40 +281,40 @@ def get_base_parser(self): parser.add_argument('--os-username', metavar='', default=utils.env('OS_USERNAME', 'NOVA_USERNAME'), - help='Defaults to env[OS_USERNAME].') + help=_('Defaults to env[OS_USERNAME].')) parser.add_argument('--os_username', help=argparse.SUPPRESS) parser.add_argument('--os-password', metavar='', default=utils.env('OS_PASSWORD', 'NOVA_PASSWORD'), - help='Defaults to env[OS_PASSWORD].') + help=_('Defaults to env[OS_PASSWORD].')) parser.add_argument('--os_password', help=argparse.SUPPRESS) parser.add_argument('--os-tenant-name', metavar='', default=utils.env('OS_TENANT_NAME', 'NOVA_PROJECT_ID'), - help='Defaults to env[OS_TENANT_NAME].') + help=_('Defaults to env[OS_TENANT_NAME].')) parser.add_argument('--os_tenant_name', help=argparse.SUPPRESS) parser.add_argument('--os-tenant-id', metavar='', default=utils.env('OS_TENANT_ID'), - help='Defaults to env[OS_TENANT_ID].') + help=_('Defaults to env[OS_TENANT_ID].')) parser.add_argument('--os-auth-url', metavar='', default=utils.env('OS_AUTH_URL', 'NOVA_URL'), - help='Defaults to env[OS_AUTH_URL].') + help=_('Defaults to env[OS_AUTH_URL].')) parser.add_argument('--os_auth_url', help=argparse.SUPPRESS) parser.add_argument('--os-region-name', metavar='', default=utils.env('OS_REGION_NAME', 'NOVA_REGION_NAME'), - help='Defaults to env[OS_REGION_NAME].') + help=_('Defaults to env[OS_REGION_NAME].')) parser.add_argument('--os_region_name', help=argparse.SUPPRESS) @@ -325,21 +327,21 @@ def get_base_parser(self): parser.add_argument('--service-type', metavar='', - help='Defaults to compute for most actions') + help=_('Defaults to compute for most actions')) parser.add_argument('--service_type', help=argparse.SUPPRESS) parser.add_argument('--service-name', metavar='', default=utils.env('NOVA_SERVICE_NAME'), - help='Defaults to env[NOVA_SERVICE_NAME]') + help=_('Defaults to env[NOVA_SERVICE_NAME]')) parser.add_argument('--service_name', help=argparse.SUPPRESS) parser.add_argument('--volume-service-name', metavar='', default=utils.env('NOVA_VOLUME_SERVICE_NAME'), - help='Defaults to env[NOVA_VOLUME_SERVICE_NAME]') + help=_('Defaults to env[NOVA_VOLUME_SERVICE_NAME]')) parser.add_argument('--volume_service_name', help=argparse.SUPPRESS) @@ -347,7 +349,7 @@ def get_base_parser(self): metavar='', default=utils.env('NOVA_ENDPOINT_TYPE', default=DEFAULT_NOVA_ENDPOINT_TYPE), - help='Defaults to env[NOVA_ENDPOINT_TYPE] or ' + help=_('Defaults to env[NOVA_ENDPOINT_TYPE] or ') + DEFAULT_NOVA_ENDPOINT_TYPE + '.') # NOTE(dtroyer): We can't add --endpoint_type here due to argparse # thinking usage-list --end is ambiguous; but it @@ -360,8 +362,8 @@ def get_base_parser(self): metavar='', default=utils.env('OS_COMPUTE_API_VERSION', default=DEFAULT_OS_COMPUTE_API_VERSION), - help='Accepts 1.1 or 3, ' - 'defaults to env[OS_COMPUTE_API_VERSION].') + help=_('Accepts 1.1 or 3, ' + 'defaults to env[OS_COMPUTE_API_VERSION].')) parser.add_argument('--os_compute_api_version', help=argparse.SUPPRESS) @@ -375,10 +377,10 @@ def get_base_parser(self): parser.add_argument('--insecure', default=utils.env('NOVACLIENT_INSECURE', default=False), action='store_true', - help="Explicitly allow novaclient to perform \"insecure\" " + help=_("Explicitly allow novaclient to perform \"insecure\" " "SSL (https) requests. The server's certificate will " "not be verified against any certificate authorities. " - "This option should be used with caution.") + "This option should be used with caution.")) parser.add_argument('--bypass-url', metavar='', @@ -605,37 +607,37 @@ def main(self, argv): if not auth_plugin or not auth_plugin.opts: if not os_username: - raise exc.CommandError("You must provide a username " - "via either --os-username or env[OS_USERNAME]") + raise exc.CommandError(_("You must provide a username " + "via either --os-username or env[OS_USERNAME]")) if not os_tenant_name and not os_tenant_id: - raise exc.CommandError("You must provide a tenant name " + raise exc.CommandError(_("You must provide a tenant name " "or tenant id via --os-tenant-name, " "--os-tenant-id, env[OS_TENANT_NAME] " - "or env[OS_TENANT_ID]") + "or env[OS_TENANT_ID]")) if not os_auth_url: if os_auth_system and os_auth_system != 'keystone': os_auth_url = auth_plugin.get_auth_url() if not os_auth_url: - raise exc.CommandError("You must provide an auth url " + raise exc.CommandError(_("You must provide an auth url " "via either --os-auth-url or env[OS_AUTH_URL] " "or specify an auth_system which defines a " "default url with --os-auth-system " - "or env[OS_AUTH_SYSTEM]") + "or env[OS_AUTH_SYSTEM]")) if (options.os_compute_api_version and options.os_compute_api_version != '1.0'): if not os_tenant_name and not os_tenant_id: - raise exc.CommandError("You must provide a tenant name " + raise exc.CommandError(_("You must provide a tenant name " "or tenant id via --os-tenant-name, " "--os-tenant-id, env[OS_TENANT_NAME] " - "or env[OS_TENANT_ID]") + "or env[OS_TENANT_ID]")) if not os_auth_url: - raise exc.CommandError("You must provide an auth url " - "via either --os-auth-url or env[OS_AUTH_URL]") + raise exc.CommandError(_("You must provide an auth url " + "via either --os-auth-url or env[OS_AUTH_URL]")) self.cs = client.Client(options.os_compute_api_version, os_username, os_password, os_tenant_name, tenant_id=os_tenant_id, @@ -682,9 +684,9 @@ def main(self, argv): if not cliutils.isunauthenticated(args.func): self.cs.authenticate() except exc.Unauthorized: - raise exc.CommandError("Invalid OpenStack Nova credentials.") + raise exc.CommandError(_("Invalid OpenStack Nova credentials.")) except exc.AuthorizationFailure: - raise exc.CommandError("Unable to authorize user") + raise exc.CommandError(_("Unable to authorize user")) args.func(self.cs, args) @@ -734,7 +736,7 @@ def do_help(self, args): if args.command in self.subcommands: self.subcommands[args.command].print_help() else: - raise exc.CommandError("'%s' is not a valid subcommand" % + raise exc.CommandError(_("'%s' is not a valid subcommand") % args.command) else: self.parser.print_help() diff --git a/novaclient/utils.py b/novaclient/utils.py index 812cbfa15..9004863c0 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -22,6 +22,7 @@ from novaclient import exceptions from novaclient.openstack.common import cliutils +from novaclient.openstack.common.gettextutils import _ from novaclient.openstack.common import jsonutils from novaclient.openstack.common import strutils @@ -56,8 +57,8 @@ def get_resource_manager_extra_kwargs(f, args, allow_conflicts=False): conflicting_keys = set(hook_kwargs.keys()) & set(extra_kwargs.keys()) if conflicting_keys and not allow_conflicts: - raise Exception("Hook '%(hook_name)s' is attempting to redefine" - " attributes '%(conflicting_keys)s'" % + raise Exception(_("Hook '%(hook_name)s' is attempting to redefine" + " attributes '%(conflicting_keys)s'") % {'hook_name': hook_name, 'conflicting_keys': conflicting_keys}) @@ -234,13 +235,15 @@ def find_resource(manager, name_or_id, **find_args): kwargs.update(find_args) return manager.find(**kwargs) except exceptions.NotFound: - msg = "No %s with a name or ID of '%s' exists." % \ - (manager.resource_class.__name__.lower(), name_or_id) + msg = _("No %(class)s with a name or ID of '%(name)s' exists.") % \ + {'class': manager.resource_class.__name__.lower(), + 'name': name_or_id} raise exceptions.CommandError(msg) except exceptions.NoUniqueMatch: - msg = ("Multiple %s matches found for '%s', use an ID to be more" - " specific." % (manager.resource_class.__name__.lower(), - name_or_id)) + msg = (_("Multiple %(class)s matches found for '%(name)s', use an ID " + "to be more specific.") % + {'class': manager.resource_class.__name__.lower(), + 'name': name_or_id}) raise exceptions.CommandError(msg) diff --git a/novaclient/v1_1/contrib/baremetal.py b/novaclient/v1_1/contrib/baremetal.py index db1f54977..825eedc9c 100644 --- a/novaclient/v1_1/contrib/baremetal.py +++ b/novaclient/v1_1/contrib/baremetal.py @@ -17,6 +17,7 @@ Baremetal interface (v2 extension). """ from novaclient import base +from novaclient.openstack.common.gettextutils import _ from novaclient import utils @@ -151,35 +152,36 @@ def list_interfaces(self, node_id): @utils.arg('service_host', metavar='', - help='Name of nova compute host which will control this baremetal node') + help=_('Name of nova compute host which will control this baremetal ' + 'node')) @utils.arg('cpus', metavar='', type=int, - help='Number of CPUs in the node') + help=_('Number of CPUs in the node')) @utils.arg('memory_mb', metavar='', type=int, - help='Megabytes of RAM in the node') + help=_('Megabytes of RAM in the node')) @utils.arg('local_gb', metavar='', type=int, - help='Gigabytes of local storage in the node') + help=_('Gigabytes of local storage in the node')) @utils.arg('prov_mac_address', metavar='', - help='MAC address to provision the node') + help=_('MAC address to provision the node')) @utils.arg('--pm_address', default=None, metavar='', - help='Power management IP for the node') + help=_('Power management IP for the node')) @utils.arg('--pm_user', default=None, metavar='', - help='Username for the node\'s power management') + help=_('Username for the node\'s power management')) @utils.arg('--pm_password', default=None, metavar='', - help='Password for the node\'s power management') + help=_('Password for the node\'s power management')) @utils.arg('--terminal_port', default=None, metavar='', type=int, - help='ShellInABox port?') + help=_('ShellInABox port?')) def do_baremetal_node_create(cs, args): """Create a baremetal node.""" node = cs.baremetal.create(args.service_host, args.cpus, @@ -270,18 +272,18 @@ def do_baremetal_node_show(cs, args): @utils.arg('node', metavar='', - help="ID of node") + help=_("ID of node")) @utils.arg('address', metavar='
', - help="MAC address of interface") + help=_("MAC address of interface")) @utils.arg('--datapath_id', default=0, metavar='', - help="OpenFlow Datapath ID of interface") + help=_("OpenFlow Datapath ID of interface")) @utils.arg('--port_no', default=0, metavar='', - help="OpenFlow port number of interface") + help=_("OpenFlow port number of interface")) def do_baremetal_interface_add(cs, args): """Add a network interface to a baremetal node.""" bmif = cs.baremetal.add_interface(args.node, args.address, @@ -289,8 +291,8 @@ def do_baremetal_interface_add(cs, args): _print_baremetal_resource(bmif) -@utils.arg('node', metavar='', help="ID of node") -@utils.arg('address', metavar='
', help="MAC address of interface") +@utils.arg('node', metavar='', help=_("ID of node")) +@utils.arg('address', metavar='
', help=_("MAC address of interface")) def do_baremetal_interface_remove(cs, args): """Remove a network interface from a baremetal node.""" cs.baremetal.remove_interface(args.node, args.address) diff --git a/novaclient/v1_1/contrib/cells.py b/novaclient/v1_1/contrib/cells.py index a6cd78a4e..7c59400ef 100644 --- a/novaclient/v1_1/contrib/cells.py +++ b/novaclient/v1_1/contrib/cells.py @@ -14,6 +14,7 @@ # under the License. from novaclient import base +from novaclient.openstack.common.gettextutils import _ from novaclient import utils @@ -47,7 +48,7 @@ def capacities(self, cell_name=None): @utils.arg('cell', metavar='', - help='Name of the cell.') + help=_('Name of the cell.')) def do_cell_show(cs, args): """Show details of a given cell.""" cell = cs.cells.get(args.cell) @@ -56,14 +57,15 @@ def do_cell_show(cs, args): @utils.arg('--cell', metavar='', - help="Name of the cell to get the capacities.", + help=_("Name of the cell to get the capacities."), default=None) def do_cell_capacities(cs, args): """Get cell capacities for all cells or a given cell.""" cell = cs.cells.capacities(args.cell) - print("Ram Available: %s MB" % cell.capacities['ram_free']['total_mb']) + print(_("Ram Available: %s MB") % cell.capacities['ram_free']['total_mb']) utils.print_dict(cell.capacities['ram_free']['units_by_mb'], dict_property='Ram(MB)', dict_value="Units") - print("\nDisk Available: %s MB" % cell.capacities['disk_free']['total_mb']) + print(_("\nDisk Available: %s MB") % + cell.capacities['disk_free']['total_mb']) utils.print_dict(cell.capacities['disk_free']['units_by_mb'], dict_property='Disk(MB)', dict_value="Units") diff --git a/novaclient/v1_1/contrib/host_evacuate.py b/novaclient/v1_1/contrib/host_evacuate.py index c8acef8b5..dd1aef267 100644 --- a/novaclient/v1_1/contrib/host_evacuate.py +++ b/novaclient/v1_1/contrib/host_evacuate.py @@ -14,6 +14,7 @@ # under the License. from novaclient import base +from novaclient.openstack.common.gettextutils import _ from novaclient import utils @@ -29,7 +30,7 @@ def _server_evacuate(cs, server, args): args.on_shared_storage) except Exception as e: success = False - error_message = "Error while evacuating instance: %s" % e + error_message = _("Error while evacuating instance: %s") % e return EvacuateHostResponse(base.Manager, {"server_uuid": server['uuid'], "evacuate_accepted": success, @@ -40,12 +41,13 @@ def _server_evacuate(cs, server, args): @utils.arg('--target_host', metavar='', default=None, - help='Name of target host.') + help=_('Name of target host.')) @utils.arg('--on-shared-storage', dest='on_shared_storage', action="store_true", default=False, - help='Specifies whether all instances files are on shared storage') + help=_('Specifies whether all instances files are on shared ' + ' storage')) def do_host_evacuate(cs, args): """Evacuate all instances from failed host to specified one.""" hypervisors = cs.hypervisors.search(args.host, servers=True) diff --git a/novaclient/v1_1/contrib/host_servers_migrate.py b/novaclient/v1_1/contrib/host_servers_migrate.py index 3076dc51d..ac5238f42 100644 --- a/novaclient/v1_1/contrib/host_servers_migrate.py +++ b/novaclient/v1_1/contrib/host_servers_migrate.py @@ -14,6 +14,7 @@ # under the License. from novaclient import base +from novaclient.openstack.common.gettextutils import _ from novaclient import utils @@ -28,7 +29,7 @@ def _server_migrate(cs, server): cs.servers.migrate(server['uuid']) except Exception as e: success = False - error_message = "Error while migrating instance: %s" % e + error_message = _("Error while migrating instance: %s") % e return HostServersMigrateResponse(base.Manager, {"server_uuid": server['uuid'], "migration_accepted": success, diff --git a/novaclient/v1_1/contrib/instance_action.py b/novaclient/v1_1/contrib/instance_action.py index 6e64d0b50..cddc30012 100644 --- a/novaclient/v1_1/contrib/instance_action.py +++ b/novaclient/v1_1/contrib/instance_action.py @@ -16,6 +16,7 @@ import pprint from novaclient import base +from novaclient.openstack.common.gettextutils import _ from novaclient import utils @@ -41,10 +42,10 @@ def list(self, server): @utils.arg('server', metavar='', - help='Name or UUID of the server to show an action for.') + help=_('Name or UUID of the server to show an action for.')) @utils.arg('request_id', metavar='', - help='Request ID of the action to get.') + help=_('Request ID of the action to get.')) def do_instance_action(cs, args): """Show an action.""" server = utils.find_resource(cs.servers, args.server) @@ -57,7 +58,7 @@ def do_instance_action(cs, args): @utils.arg('server', metavar='', - help='Name or UUID of the server to list actions for.') + help=_('Name or UUID of the server to list actions for.')) def do_instance_action_list(cs, args): """List actions on a server.""" server = utils.find_resource(cs.servers, args.server) diff --git a/novaclient/v1_1/contrib/metadata_extensions.py b/novaclient/v1_1/contrib/metadata_extensions.py index 16e4ad13d..27b864f7a 100644 --- a/novaclient/v1_1/contrib/metadata_extensions.py +++ b/novaclient/v1_1/contrib/metadata_extensions.py @@ -13,23 +13,25 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient.openstack.common.gettextutils import _ from novaclient import utils from novaclient.v1_1 import shell @utils.arg('host', metavar='', - help='Name of host.') + help=_('Name of host.')) @utils.arg('action', metavar='', choices=['set', 'delete'], - help="Actions: 'set' or 'delete'") + help=_("Actions: 'set' or 'delete'")) @utils.arg('metadata', metavar='', nargs='+', action='append', default=[], - help='Metadata to set or delete (only key is necessary on delete)') + help=_('Metadata to set or delete (only key is necessary on ' + 'delete)')) def do_host_meta(cs, args): """Set or Delete metadata on all instances of a host.""" hypervisors = cs.hypervisors.search(args.host, servers=True) diff --git a/novaclient/v1_1/contrib/migrations.py b/novaclient/v1_1/contrib/migrations.py index 87fdd1930..507ebc9eb 100644 --- a/novaclient/v1_1/contrib/migrations.py +++ b/novaclient/v1_1/contrib/migrations.py @@ -15,6 +15,7 @@ """ from novaclient import base +from novaclient.openstack.common.gettextutils import _ from novaclient.openstack.common.py3kcompat import urlutils from novaclient import utils @@ -54,15 +55,15 @@ def list(self, host=None, status=None, cell_name=None): @utils.arg('--host', dest='host', metavar='', - help='Fetch migrations for the given host.') + help=_('Fetch migrations for the given host.')) @utils.arg('--status', dest='status', metavar='', - help='Fetch migrations for the given status.') + help=_('Fetch migrations for the given status.')) @utils.arg('--cell_name', dest='cell_name', metavar='', - help='Fetch migrations for the given cell_name.') + help=_('Fetch migrations for the given cell_name.')) def do_migration_list(cs, args): """Print a list of migrations.""" _print_migrations(cs.migrations.list(args.host, args.status, diff --git a/novaclient/v1_1/contrib/tenant_networks.py b/novaclient/v1_1/contrib/tenant_networks.py index 9ac97f110..8c9b22ebc 100644 --- a/novaclient/v1_1/contrib/tenant_networks.py +++ b/novaclient/v1_1/contrib/tenant_networks.py @@ -13,6 +13,7 @@ # limitations under the License. from novaclient import base +from novaclient.openstack.common.gettextutils import _ from novaclient import utils @@ -57,10 +58,10 @@ def do_net_list(cs, args): @utils.arg('label', metavar='', - help='Network label (ex. my_new_network)') + help=_('Network label (ex. my_new_network)')) @utils.arg('cidr', metavar='', - help='IP block to allocate from (ex. 172.16.0.0/24 or ' - '2001:DB8::/64)') + help=_('IP block to allocate from (ex. 172.16.0.0/24 or ' + '2001:DB8::/64)')) def do_net_create(cs, args): """ Create a network diff --git a/novaclient/v1_1/flavor_access.py b/novaclient/v1_1/flavor_access.py index b314040fe..d847d061f 100644 --- a/novaclient/v1_1/flavor_access.py +++ b/novaclient/v1_1/flavor_access.py @@ -16,6 +16,7 @@ """Flavor access interface.""" from novaclient import base +from novaclient.openstack.common.gettextutils import _ class FlavorAccess(base.Resource): @@ -35,7 +36,7 @@ def list(self, **kwargs): elif kwargs.get('tenant', None): return self._list_by_tenant(kwargs['tenant']) else: - raise NotImplementedError('Unknown list options.') + raise NotImplementedError(_('Unknown list options.')) def _list_by_flavor(self, flavor): return self._list('/flavors/%s/os-flavor-access' % base.getid(flavor), @@ -45,7 +46,7 @@ def _list_by_tenant(self, tenant): """Print flavor list shared with the given tenant.""" # TODO(uni): need to figure out a proper URI for list_by_tenant # since current API already provided current tenant_id information - raise NotImplementedError('Sorry, query by tenant not supported.') + raise NotImplementedError(_('Sorry, query by tenant not supported.')) def add_tenant_access(self, flavor, tenant): """Add a tenant to the given flavor access list.""" diff --git a/novaclient/v1_1/flavors.py b/novaclient/v1_1/flavors.py index cfb06d1ed..6d1634971 100644 --- a/novaclient/v1_1/flavors.py +++ b/novaclient/v1_1/flavors.py @@ -17,6 +17,7 @@ """ from novaclient import base from novaclient import exceptions +from novaclient.openstack.common.gettextutils import _ from novaclient.openstack.common.py3kcompat import urlutils from novaclient.openstack.common import strutils @@ -168,15 +169,15 @@ def create(self, name, ram, vcpus, disk, flavorid="auto", try: ram = int(ram) except (TypeError, ValueError): - raise exceptions.CommandError("Ram must be an integer.") + raise exceptions.CommandError(_("Ram must be an integer.")) try: vcpus = int(vcpus) except (TypeError, ValueError): - raise exceptions.CommandError("VCPUs must be an integer.") + raise exceptions.CommandError(_("VCPUs must be an integer.")) try: disk = int(disk) except (TypeError, ValueError): - raise exceptions.CommandError("Disk must be an integer.") + raise exceptions.CommandError(_("Disk must be an integer.")) if flavorid == "auto": flavorid = None @@ -184,20 +185,20 @@ def create(self, name, ram, vcpus, disk, flavorid="auto", try: swap = int(swap) except (TypeError, ValueError): - raise exceptions.CommandError("Swap must be an integer.") + raise exceptions.CommandError(_("Swap must be an integer.")) try: ephemeral = int(ephemeral) except (TypeError, ValueError): - raise exceptions.CommandError("Ephemeral must be an integer.") + raise exceptions.CommandError(_("Ephemeral must be an integer.")) try: rxtx_factor = float(rxtx_factor) except (TypeError, ValueError): - raise exceptions.CommandError("rxtx_factor must be a float.") + raise exceptions.CommandError(_("rxtx_factor must be a float.")) try: is_public = strutils.bool_from_string(is_public, True) except Exception: - raise exceptions.CommandError("is_public must be a boolean.") + raise exceptions.CommandError(_("is_public must be a boolean.")) body = self._build_body(name, ram, vcpus, disk, flavorid, swap, ephemeral, rxtx_factor, is_public) diff --git a/novaclient/v1_1/networks.py b/novaclient/v1_1/networks.py index 18c5bd53e..d29faf26d 100644 --- a/novaclient/v1_1/networks.py +++ b/novaclient/v1_1/networks.py @@ -19,6 +19,7 @@ from novaclient import base from novaclient import exceptions +from novaclient.openstack.common.gettextutils import _ class Network(base.Resource): @@ -110,7 +111,7 @@ def disassociate(self, network, disassociate_host=True, body = {"disassociate_host": None} else: raise exceptions.CommandError( - "Must disassociate either host or project or both") + _("Must disassociate either host or project or both")) self.api.client.post("/os-networks/%s/action" % base.getid(network), body=body) diff --git a/novaclient/v1_1/security_group_rules.py b/novaclient/v1_1/security_group_rules.py index 1ebf809ed..f1348920a 100644 --- a/novaclient/v1_1/security_group_rules.py +++ b/novaclient/v1_1/security_group_rules.py @@ -19,6 +19,7 @@ from novaclient import base from novaclient import exceptions +from novaclient.openstack.common.gettextutils import _ class SecurityGroupRule(base.Resource): @@ -48,14 +49,14 @@ def create(self, parent_group_id, ip_protocol=None, from_port=None, try: from_port = int(from_port) except (TypeError, ValueError): - raise exceptions.CommandError("From port must be an integer.") + raise exceptions.CommandError(_("From port must be an integer.")) try: to_port = int(to_port) except (TypeError, ValueError): - raise exceptions.CommandError("To port must be an integer.") + raise exceptions.CommandError(_("To port must be an integer.")) if ip_protocol.upper() not in ['TCP', 'UDP', 'ICMP']: - raise exceptions.CommandError("Ip protocol must be 'tcp', 'udp', " - "or 'icmp'.") + raise exceptions.CommandError(_("Ip protocol must be 'tcp', 'udp'" + ", or 'icmp'.")) body = {"security_group_rule": { "ip_protocol": ip_protocol, From 0194492059920fd8d9d91849c341b2de0b561089 Mon Sep 17 00:00:00 2001 From: Victor Morales Date: Fri, 31 Jan 2014 11:33:54 -0600 Subject: [PATCH 0400/1705] Removed undefined method in install_env.py file There was a call from install_venv.py file to post_process method of InstallVenv class, but this method is not defined in this class as result it was raising an error. Even when this error doesn't affect dependencies installation, it is not allowing to display user information. Change-Id: I499c0399eb961588d7e1491e1481412ffdda38b8 Closes-Bug: #1275025 --- tools/install_venv.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tools/install_venv.py b/tools/install_venv.py index 4d8feeae8..cc2184378 100644 --- a/tools/install_venv.py +++ b/tools/install_venv.py @@ -68,7 +68,6 @@ def main(argv): install.check_dependencies() install.create_virtualenv(no_site_packages=options.no_site_packages) install.install_dependencies() - install.post_process() print_help(project, venv, root) if __name__ == '__main__': From fc8579dfa8f2d897ce4ea9f6acfeea3ca170d6f0 Mon Sep 17 00:00:00 2001 From: Sergio Cazzolato Date: Tue, 30 Nov 1999 22:24:27 -0500 Subject: [PATCH 0401/1705] Flavor ExtraSpecs containing '/' cannot be deleted This change applies a regular expression in order to filter flavor extraspects keys with invalid characters. The characters allowed are: letters, numbers, underscores, periods, colons, spaces and hyphens. A new test flavor has been created which doesn't check the keys in the post body. This flavor has been created in the third place (instead of in the last) in order to keep working existent test cases which depend on the last flavor received in the get method. Change-Id: Ifd86bed23a05a5946ae8b9ba6f6c9bf4b24b1d4c Partial-Bug: #1256119 --- novaclient/tests/test_utils.py | 16 ++++++++++++++++ novaclient/tests/v1_1/fakes.py | 24 ++++++++++++++++++++++++ novaclient/tests/v1_1/test_flavors.py | 17 +++++++++++++++++ novaclient/tests/v3/fakes.py | 7 +++++++ novaclient/tests/v3/test_flavors.py | 10 ++++++++++ novaclient/utils.py | 14 ++++++++++++++ novaclient/v1_1/flavors.py | 4 ++++ novaclient/v3/flavors.py | 4 ++++ 8 files changed, 96 insertions(+) diff --git a/novaclient/tests/test_utils.py b/novaclient/tests/test_utils.py index a7660b8b5..cce2db029 100644 --- a/novaclient/tests/test_utils.py +++ b/novaclient/tests/test_utils.py @@ -286,3 +286,19 @@ def test_pretty_choice_dict(self): "k3": "v3"} r = utils.pretty_choice_dict(d) self.assertEqual(r, "'k1=v1', 'k2=v2', 'k3=v3'") + + +class ValidationsTestCase(test_utils.TestCase): + def test_validate_flavor_metadata_keys_with_valid_keys(self): + valid_keys = ['key1', 'month.price', 'I-Am:AK-ey.01-', 'spaces and _'] + for key in valid_keys: + utils.validate_flavor_metadata_keys(valid_keys) + + def test_validate_flavor_metadata_keys_with_invalid_keys(self): + invalid_keys = ['/1', '?1', '%1', '<', '>', '\1'] + for key in invalid_keys: + try: + utils.validate_flavor_metadata_keys([key]) + self.assertFail() + except exceptions.CommandError as ce: + self.assertTrue(key in str(ce)) diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index 8590ce4f1..d00dd0785 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -667,6 +667,10 @@ def get_flavors_detail(self, **kw): 'OS-FLV-EXT-DATA:ephemeral': 20, 'os-flavor-access:is_public': False, 'links': {}}, + {'id': 4, 'name': '1024 MB Server', 'ram': 1024, 'disk': 10, + 'OS-FLV-EXT-DATA:ephemeral': 10, + 'os-flavor-access:is_public': True, + 'links': {}}, {'id': 'aa1', 'name': '128 MB Server', 'ram': 128, 'disk': 0, 'OS-FLV-EXT-DATA:ephemeral': 0, 'os-flavor-access:is_public': True, @@ -730,6 +734,14 @@ def get_flavors_512_MB_Server(self, **kw): def get_flavors_aa1(self, **kw): # Aplhanumeric flavor id are allowed. + return ( + 200, + {}, + {'flavor': + self.get_flavors_detail(is_public='None')[2]['flavors'][3]} + ) + + def get_flavors_4(self, **kw): return ( 200, {}, @@ -765,6 +777,11 @@ def get_flavors_aa1_os_extra_specs(self, **kw): return (200, {}, {'extra_specs': {"k3": "v3"}}) + def get_flavors_4_os_extra_specs(self, **kw): + return (200, + {}, + {'extra_specs': {"k4": "v4"}}) + def post_flavors_1_os_extra_specs(self, body, **kw): assert list(body) == ['extra_specs'] fakes.assert_has_keys(body['extra_specs'], @@ -773,6 +790,13 @@ def post_flavors_1_os_extra_specs(self, body, **kw): {}, {'extra_specs': {"k1": "v1"}}) + def post_flavors_4_os_extra_specs(self, body, **kw): + assert list(body) == ['extra_specs'] + + return (200, + {}, + body) + def delete_flavors_1_os_extra_specs_k1(self, **kw): return (204, {}, None) diff --git a/novaclient/tests/v1_1/test_flavors.py b/novaclient/tests/v1_1/test_flavors.py index d1762949e..c18305401 100644 --- a/novaclient/tests/v1_1/test_flavors.py +++ b/novaclient/tests/v1_1/test_flavors.py @@ -189,6 +189,23 @@ def test_set_keys(self): self.cs.assert_called('POST', '/flavors/1/os-extra_specs', {"extra_specs": {'k1': 'v1'}}) + def test_set_with_valid_keys(self): + valid_keys = ['key4', 'month.price', 'I-Am:AK-ey.44-', + 'key with spaces and _'] + + f = self.cs.flavors.get(4) + for key in valid_keys: + f.set_keys({key: 'v4'}) + self.cs.assert_called('POST', '/flavors/4/os-extra_specs', + {"extra_specs": {key: 'v4'}}) + + def test_set_with_invalid_keys(self): + invalid_keys = ['/1', '?1', '%1', '<', '>'] + + f = self.cs.flavors.get(1) + for key in invalid_keys: + self.assertRaises(exceptions.CommandError, f.set_keys, {key: 'v1'}) + def test_unset_keys(self): f = self.cs.flavors.get(1) f.unset_keys(['k1']) diff --git a/novaclient/tests/v3/fakes.py b/novaclient/tests/v3/fakes.py index ffc7e6056..60063f3e9 100644 --- a/novaclient/tests/v3/fakes.py +++ b/novaclient/tests/v3/fakes.py @@ -66,6 +66,9 @@ def get_os_hosts_sample_host_shutdown(self, **kw): post_flavors_1_flavor_extra_specs = ( fakes_v1_1.FakeHTTPClient.post_flavors_1_os_extra_specs) + post_flavors_4_flavor_extra_specs = ( + fakes_v1_1.FakeHTTPClient.post_flavors_4_os_extra_specs) + delete_flavors_1_flavor_extra_specs_k1 = ( fakes_v1_1.FakeHTTPClient.delete_flavors_1_os_extra_specs_k1) @@ -79,6 +82,10 @@ def get_flavors_detail(self, **kw): 'ephemeral': 20, 'flavor-access:is_public': False, 'links': {}}, + {'id': 4, 'name': '1024 MB Server', 'ram': 1024, 'disk': 10, + 'ephemeral': 10, + 'flavor-access:is_public': True, + 'links': {}}, {'id': 'aa1', 'name': '128 MB Server', 'ram': 128, 'disk': 0, 'ephemeral': 0, 'flavor-access:is_public': True, diff --git a/novaclient/tests/v3/test_flavors.py b/novaclient/tests/v3/test_flavors.py index 34714aef0..ea76988a0 100644 --- a/novaclient/tests/v3/test_flavors.py +++ b/novaclient/tests/v3/test_flavors.py @@ -47,6 +47,16 @@ def test_set_keys(self): self.cs.assert_called('POST', '/flavors/1/flavor-extra-specs', {"extra_specs": {'k1': 'v1'}}) + def test_set_with_valid_keys(self): + valid_keys = ['key4', 'month.price', 'I-Am:AK-ey.44-', + 'key with spaces and _'] + + f = self.cs.flavors.get(4) + for key in valid_keys: + f.set_keys({key: 'v4'}) + self.cs.assert_called('POST', '/flavors/4/flavor-extra-specs', + {"extra_specs": {key: 'v4'}}) + def test_unset_keys(self): f = self.cs.flavors.get(1) f.unset_keys(['k1']) diff --git a/novaclient/utils.py b/novaclient/utils.py index 812cbfa15..f4c8b8dd0 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -13,6 +13,7 @@ import json import pkg_resources +import re import sys import textwrap import uuid @@ -22,6 +23,7 @@ from novaclient import exceptions from novaclient.openstack.common import cliutils +from novaclient.openstack.common.gettextutils import _ from novaclient.openstack.common import jsonutils from novaclient.openstack.common import strutils @@ -29,6 +31,8 @@ arg = cliutils.arg env = cliutils.env +VALID_KEY_REGEX = re.compile(r"[\w\.\- :]+$", re.UNICODE) + def add_resource_manager_extra_kwargs_hook(f, hook): """Add hook to bind CLI arguments to ResourceManager calls. @@ -349,3 +353,13 @@ def is_integer_like(val): return True except (TypeError, ValueError, AttributeError): return False + + +def validate_flavor_metadata_keys(keys): + for key in keys: + valid_name = VALID_KEY_REGEX.match(key) + if not valid_name: + msg = _('Invalid key: "%s". Keys may only contain letters, ' + 'numbers, spaces, underscores, periods, colons and ' + 'hyphens.') + raise exceptions.CommandError(msg % key) diff --git a/novaclient/v1_1/flavors.py b/novaclient/v1_1/flavors.py index cfb06d1ed..91c879bf5 100644 --- a/novaclient/v1_1/flavors.py +++ b/novaclient/v1_1/flavors.py @@ -15,10 +15,12 @@ """ Flavor interface. """ + from novaclient import base from novaclient import exceptions from novaclient.openstack.common.py3kcompat import urlutils from novaclient.openstack.common import strutils +from novaclient import utils class Flavor(base.Resource): @@ -62,6 +64,8 @@ def set_keys(self, metadata): :param flavor: The :class:`Flavor` to set extra spec on :param metadata: A dict of key/value pairs to be set """ + utils.validate_flavor_metadata_keys(metadata.keys()) + body = {'extra_specs': metadata} return self.manager._create( "/flavors/%s/os-extra_specs" % base.getid(self), diff --git a/novaclient/v3/flavors.py b/novaclient/v3/flavors.py index 120574aef..38f288504 100644 --- a/novaclient/v3/flavors.py +++ b/novaclient/v3/flavors.py @@ -16,7 +16,9 @@ """ Flavor interface. """ + from novaclient import base +from novaclient import utils from novaclient.v1_1 import flavors @@ -54,6 +56,8 @@ def set_keys(self, metadata): :param flavor: The :class:`Flavor` to set extra spec on :param metadata: A dict of key/value pairs to be set """ + utils.validate_flavor_metadata_keys(metadata.keys()) + body = {'extra_specs': metadata} return self.manager._create( "/flavors/%s/flavor-extra-specs" % From d218088c22760bac7de98c0a77d9d723fdc840f0 Mon Sep 17 00:00:00 2001 From: Andrew Lazarev Date: Tue, 4 Feb 2014 14:39:04 -0800 Subject: [PATCH 0402/1705] Fixed super constructor call for TestResponse class Change-Id: Idbd436d63af152d4a73e747bf00a82fc0308a036 --- novaclient/tests/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/tests/utils.py b/novaclient/tests/utils.py index 9335819ff..982b953ab 100644 --- a/novaclient/tests/utils.py +++ b/novaclient/tests/utils.py @@ -42,8 +42,8 @@ class TestResponse(requests.Response): """ def __init__(self, data): + super(TestResponse, self).__init__() self._text = None - super(TestResponse, self) if isinstance(data, dict): self.status_code = data.get('status_code', None) self.headers = data.get('headers', None) From 788e05c0df1fb5e94ae355b2929000445006d8f2 Mon Sep 17 00:00:00 2001 From: Andrew Lazarev Date: Tue, 4 Feb 2014 15:09:35 -0800 Subject: [PATCH 0403/1705] Fixed multi validation and wrong fail calls in unit tests * Positive test was calling the same check several times. Replaced with one call. * Negative test was failing with "'object has no attribute 'assertFail'" error message in case of failure. Replaced with appropriate message. Change-Id: Id03a452f7735d6b4d13f54036f1bd3ae50cb487a --- novaclient/tests/test_utils.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/novaclient/tests/test_utils.py b/novaclient/tests/test_utils.py index cce2db029..ba6801198 100644 --- a/novaclient/tests/test_utils.py +++ b/novaclient/tests/test_utils.py @@ -291,14 +291,13 @@ def test_pretty_choice_dict(self): class ValidationsTestCase(test_utils.TestCase): def test_validate_flavor_metadata_keys_with_valid_keys(self): valid_keys = ['key1', 'month.price', 'I-Am:AK-ey.01-', 'spaces and _'] - for key in valid_keys: - utils.validate_flavor_metadata_keys(valid_keys) + utils.validate_flavor_metadata_keys(valid_keys) def test_validate_flavor_metadata_keys_with_invalid_keys(self): invalid_keys = ['/1', '?1', '%1', '<', '>', '\1'] for key in invalid_keys: try: utils.validate_flavor_metadata_keys([key]) - self.assertFail() + self.fail("Invalid key passed validation: %s" % key) except exceptions.CommandError as ce: self.assertTrue(key in str(ce)) From bd09342779addecd3f395e7dc8e4bc334f8a6e30 Mon Sep 17 00:00:00 2001 From: Andrew Lazarev Date: Tue, 4 Feb 2014 16:13:33 -0800 Subject: [PATCH 0404/1705] [UT] Removed duplicate key from dict in fake baremetal_node Python takes only the last value if several keys with the same name listed. Removed the first one to make value evident. Change-Id: I6cef783ff245073b4c0fde0e4d60ff69fb02fc22 --- novaclient/tests/v1_1/contrib/fakes.py | 1 - 1 file changed, 1 deletion(-) diff --git a/novaclient/tests/v1_1/contrib/fakes.py b/novaclient/tests/v1_1/contrib/fakes.py index 6f4608e82..fd25910a5 100644 --- a/novaclient/tests/v1_1/contrib/fakes.py +++ b/novaclient/tests/v1_1/contrib/fakes.py @@ -50,7 +50,6 @@ def get_os_baremetal_nodes(self, **kw): { "id": 1, "instance_uuid": None, - "pm_address": "1.2.3.4", "interfaces": [], "cpus": 2, "local_gb": 10, From d97975470459973ad2687fe1188494029c56b57a Mon Sep 17 00:00:00 2001 From: Andrew Lazarev Date: Tue, 4 Feb 2014 16:20:03 -0800 Subject: [PATCH 0405/1705] [UT] Fixed floating_ip_pools fake return to expected one It seems that author wanted to return two pools. But because of typo, method now returns only one of them. Fixed typo to return two pools. Change-Id: Ia77a8529fd4db1b06860073ebf33bd54b43438cd --- novaclient/tests/v1_1/fakes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index d00dd0785..aa8f2414a 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -824,7 +824,7 @@ def get_os_floating_ip_pools(self): return ( 200, {}, - {'floating_ip_pools': [{'name': 'foo', 'name': 'bar'}]} + {'floating_ip_pools': [{'name': 'foo'}, {'name': 'bar'}]} ) def get_os_floating_ips(self, **kw): From 5e3a3a193bb55cd9f0622b0adb76ffa54b98b906 Mon Sep 17 00:00:00 2001 From: Masayuki Igawa Date: Tue, 4 Feb 2014 15:53:16 +0900 Subject: [PATCH 0406/1705] Fix Serivce class AttributeError When we str(service_object), an AttributeError occurred like this: ======================== File "/opt/stack/python-novaclient/novaclient/v1_1/services.py", line 24, in __repr__ return "" % self.service File "/opt/stack/python-novaclient/novaclient/openstack/common/apiclient/base.py", line 463, in __getattr__ raise AttributeError(k) AttributeError: service ======================== This commit fixes it. Change-Id: I496d522591273bf1b1e7dbadf19afaf5e64e41e3 Closes-Bug: #1276408 --- novaclient/tests/v1_1/test_services.py | 1 + novaclient/v1_1/services.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/novaclient/tests/v1_1/test_services.py b/novaclient/tests/v1_1/test_services.py index 36f82e40a..d66a11107 100644 --- a/novaclient/tests/v1_1/test_services.py +++ b/novaclient/tests/v1_1/test_services.py @@ -37,6 +37,7 @@ def test_list_services(self): self.assertIsInstance(s, self._get_service_type()) self.assertEqual(s.binary, 'nova-compute') self.assertEqual(s.host, 'host1') + self.assertTrue(str(s).startswith('" % self.service + return "" % self.binary def _add_details(self, info): dico = 'resource' in info and info['resource'] or info From 81794c14f87dd580d373c2d941f0baa49cc46064 Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Wed, 5 Feb 2014 19:37:15 +0000 Subject: [PATCH 0407/1705] Update my mailmap Using new email address. Change-Id: Ib2303680e6219b4fb8bdd67c016b14dd515d6d05 --- .mailmap | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.mailmap b/.mailmap index 2df56d0d8..64cb5eada 100644 --- a/.mailmap +++ b/.mailmap @@ -14,6 +14,8 @@ Johannes Erdfelt jerdfelt termie + + hwbi Nikolay Sokolov Nokolay Sokolov Nikolay Sokolov Nokolay Sokolov From 6e116a1532fd56cd9a2b4c8e68a870227bc23f01 Mon Sep 17 00:00:00 2001 From: Andrew Lazarev Date: Tue, 4 Feb 2014 15:42:04 -0800 Subject: [PATCH 0408/1705] Fixed polling after boot in shell + Added unit test to test that poll method is called. Testing of poll method itself is out of this CR scope. Change-Id: I57adb80bacd76b0831ea63f74182f60a2033ab11 --- novaclient/tests/v1_1/test_shell.py | 18 ++++++++++++++++++ novaclient/tests/v3/test_shell.py | 18 ++++++++++++++++++ novaclient/v1_1/shell.py | 2 +- novaclient/v3/shell.py | 2 +- 4 files changed, 38 insertions(+), 2 deletions(-) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index 32f99f171..a4c4ca19c 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -563,6 +563,24 @@ def test_boot_invalid_num_instances(self): cmd = 'boot --image 1 --flavor 1 --num-instances 0 server' self.assertRaises(exceptions.CommandError, self.run_command, cmd) + @mock.patch('novaclient.v1_1.shell._poll_for_status') + def test_boot_with_poll(self, poll_method): + self.run_command('boot --flavor 1 --image 1 some-server --poll') + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': '1', + 'min_count': 1, + 'max_count': 1, + }}, + ) + self.assertEqual(poll_method.call_count, 1) + poll_method.assert_has_calls( + [mock.call(self.shell.cs.servers.get, 1234, 'building', + ['active'])]) + def test_flavor_list(self): self.run_command('flavor-list') self.assert_called_anytime('GET', '/flavors/detail') diff --git a/novaclient/tests/v3/test_shell.py b/novaclient/tests/v3/test_shell.py index 615a80a5d..85a839b49 100644 --- a/novaclient/tests/v3/test_shell.py +++ b/novaclient/tests/v3/test_shell.py @@ -462,3 +462,21 @@ def test_boot_invalid_num_instances(self): self.assertRaises(exceptions.CommandError, self.run_command, cmd) cmd = 'boot --image 1 --flavor 1 --num-instances 0 server' self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + @mock.patch('novaclient.v3.shell._poll_for_status') + def test_boot_with_poll(self, poll_method): + self.run_command('boot --flavor 1 --image 1 some-server --poll') + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavor_ref': '1', + 'name': 'some-server', + 'image_ref': '1', + 'os-multiple-create:min_count': 1, + 'os-multiple-create:max_count': 1, + }}, + ) + self.assertEqual(poll_method.call_count, 1) + poll_method.assert_has_calls( + [mock.call(self.shell.cs.servers.get, 1234, 'building', + ['active'])]) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index dbc77ad3c..9fee3ebbe 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -427,7 +427,7 @@ def do_boot(cs, args): _print_server(cs, args, server) if args.poll: - _poll_for_status(cs.servers.get, info['id'], 'building', ['active']) + _poll_for_status(cs.servers.get, server.id, 'building', ['active']) def do_cloudpipe_list(cs, _args): diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 89062ee92..7b0dcfee1 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -310,7 +310,7 @@ def do_boot(cs, args): _print_server(cs, args, server) if args.poll: - _poll_for_status(cs.servers.get, info['id'], 'building', ['active']) + _poll_for_status(cs.servers.get, server.id, 'building', ['active']) def _poll_for_status(poll_fn, obj_id, action, final_ok_states, From 935501b0e4f80a5b50bcf5baf680470c211a4a34 Mon Sep 17 00:00:00 2001 From: Sascha Peilicke Date: Tue, 19 Nov 2013 10:31:21 +0100 Subject: [PATCH 0409/1705] Support building wheels (PEP-427) Universal is used to identify pure-Python module(by bdist_wheel). For these, it is sufficient to build a wheel with _any_ Python ABI version and publish that to PyPI (by whatever means). Change-Id: I900a7d2e3777a5c151778679f3049771047855e6 --- setup.cfg | 3 +++ 1 file changed, 3 insertions(+) diff --git a/setup.cfg b/setup.cfg index 3513c3bf5..e8cb0e382 100644 --- a/setup.cfg +++ b/setup.cfg @@ -32,3 +32,6 @@ all_files = 1 [upload_sphinx] upload-dir = doc/build/html + +[wheel] +universal = 1 From 55249f777cc13e7e6cf3440023cf543ccdca176d Mon Sep 17 00:00:00 2001 From: Alessandro Pilotti Date: Thu, 29 Aug 2013 12:25:57 +0300 Subject: [PATCH 0410/1705] Adds support for the get_rdp_console API Hyper-V employs RDP to access virtual machine consoles, unlike most other hypervisors which support VNC. In order to support this scenario, the get_rdp_console API has been added to Nova. This commit adds the corresponding client side feature, implemented in a way consistent with existing VNC and SPICE console support. Nova Gerrit commit: https://review.openstack.org/#/c/43502/ Change-Id: I86b814797d234f1eb49a7fa67ed27a9bcda034ae Implements: blueprint hyper-v-rdp-console --- novaclient/tests/v1_1/fakes.py | 2 ++ novaclient/tests/v1_1/test_servers.py | 8 ++++++++ novaclient/v1_1/servers.py | 19 +++++++++++++++++++ novaclient/v1_1/shell.py | 17 +++++++++++++++++ novaclient/v3/shell.py | 17 +++++++++++++++++ 5 files changed, 63 insertions(+) diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index d00dd0785..e10b8eb67 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -598,6 +598,8 @@ def post_servers_1234_action(self, body, **kw): assert list(body[action]) == ['type'] elif action == 'os-getSPICEConsole': assert list(body[action]) == ['type'] + elif action == 'os-getRDPConsole': + assert list(body[action]) == ['type'] elif action == 'os-migrateLive': assert set(body[action].keys()) == set(['host', 'block_migration', diff --git a/novaclient/tests/v1_1/test_servers.py b/novaclient/tests/v1_1/test_servers.py index ac30df18e..a48204c19 100644 --- a/novaclient/tests/v1_1/test_servers.py +++ b/novaclient/tests/v1_1/test_servers.py @@ -510,6 +510,14 @@ def test_get_spice_console(self): cs.servers.get_spice_console(s, 'fake') cs.assert_called('POST', '/servers/1234/action') + def test_get_rdp_console(self): + s = cs.servers.get(1234) + s.get_rdp_console('fake') + cs.assert_called('POST', '/servers/1234/action') + + cs.servers.get_rdp_console(s, 'fake') + cs.assert_called('POST', '/servers/1234/action') + def test_create_image(self): s = cs.servers.get(1234) s.create_image('123') diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index 0553f1401..0ae5d1663 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -76,6 +76,14 @@ def get_spice_console(self, console_type): """ return self.manager.get_spice_console(self, console_type) + def get_rdp_console(self, console_type): + """ + Get rdp console for a Server. + + :param console_type: Type of console ('rdp-html5') + """ + return self.manager.get_rdp_console(self, console_type) + def get_password(self, private_key=None): """ Get password for a Server. @@ -644,6 +652,17 @@ def get_spice_console(self, server, console_type): return self._action('os-getSPICEConsole', server, {'type': console_type})[1] + def get_rdp_console(self, server, console_type): + """ + Get a rdp console for an instance + + :param server: The :class:`Server` (or its ID) to add an IP to. + :param console_type: Type of rdp console to get ('rdp-html5') + """ + + return self._action('os-getRDPConsole', server, + {'type': console_type})[1] + def get_password(self, server, private_key=None): """ Get password for an instance diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index dbc77ad3c..ae59bfcba 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1871,6 +1871,23 @@ def __init__(self, console_dict): utils.print_list([SPICEConsole(data['console'])], ['Type', 'Url']) +@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('console_type', + metavar='', + help='Type of rdp console ("rdp-html5").') +def do_get_rdp_console(cs, args): + """Get a rdp console to a server.""" + server = _find_server(cs, args.server) + data = server.get_rdp_console(args.console_type) + + class RDPConsole: + def __init__(self, console_dict): + self.type = console_dict['type'] + self.url = console_dict['url'] + + utils.print_list([RDPConsole(data['console'])], ['Type', 'Url']) + + @utils.arg('server', metavar='', help='Name or ID of server.') @utils.arg('private_key', metavar='', diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 89062ee92..49c8768f7 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -1478,6 +1478,23 @@ def __init__(self, console_dict): utils.print_list([SPICEConsole(data['console'])], ['Type', 'Url']) +@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('console_type', + metavar='', + help='Type of rdp console ("rdp-html5").') +def do_get_rdp_console(cs, args): + """Get a rdp console to a server.""" + server = _find_server(cs, args.server) + data = server.get_rdp_console(args.console_type) + + class RDPConsole: + def __init__(self, console_dict): + self.type = console_dict['type'] + self.url = console_dict['url'] + + utils.print_list([RDPConsole(data['console'])], ['Type', 'Url']) + + @utils.arg('server', metavar='', help='Name or ID of server.') @utils.arg('private_key', metavar='', From f6fdff657f079c83b2c3702380218c2b546ee632 Mon Sep 17 00:00:00 2001 From: Russell Bryant Date: Sun, 9 Feb 2014 17:49:29 -0500 Subject: [PATCH 0411/1705] Fix python 3.3 unit test job Change-Id: Ibb253e51ec2412d71d04a1994cef4fb60e77f234 Closes-bug: 1277495 --- tox.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/tox.ini b/tox.ini index be96a7984..b7d694945 100644 --- a/tox.ini +++ b/tox.ini @@ -9,7 +9,6 @@ install_command = pip install -U {opts} {packages} setenv = VIRTUAL_ENV={envdir} LANG=en_US.UTF-8 LANGUAGE=en_US:en - LC_ALL=C deps = -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt From 09bef81e9c766586985da48cf1fbc89cf19577fd Mon Sep 17 00:00:00 2001 From: Jeremy Stanley Date: Mon, 10 Feb 2014 03:21:02 +0000 Subject: [PATCH 0412/1705] Remove tox locale overrides * tox.ini: The LANG and LANGUAGE environment overrides were introduced originally during the testr migration in an attempt to be conservative about the possibility that locale settings in the calling environment could cause consistency problems for test runs. In actuality, this should be unnecessary and any place where it does cause issues ought to be considered an actual bug. Change-Id: I0f30b45033145c61ad2a9b232cb1178c48c7fc97 --- tox.ini | 2 -- 1 file changed, 2 deletions(-) diff --git a/tox.ini b/tox.ini index b7d694945..b85bd2938 100644 --- a/tox.ini +++ b/tox.ini @@ -7,8 +7,6 @@ skipsdist = True usedevelop = True install_command = pip install -U {opts} {packages} setenv = VIRTUAL_ENV={envdir} - LANG=en_US.UTF-8 - LANGUAGE=en_US:en deps = -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt From ff815fb4d01a3fe69a036293ddb899dbc6d99df0 Mon Sep 17 00:00:00 2001 From: shihanzhang Date: Tue, 11 Feb 2014 14:50:54 +0800 Subject: [PATCH 0413/1705] Remove invalid parameter of quota-update nova quota-update doesn't support update '--gigabytes' and '--volumes', but CLI include these two parameter, so it should remove these two parameter from novaclient Change-Id: I9db39dd397ba3368f214377f37b56ac4f4f3864c Closes-bug: #1277673 --- novaclient/tests/v1_1/fakes.py | 16 ---------------- novaclient/tests/v1_1/test_quotas.py | 23 ----------------------- novaclient/tests/v3/fakes.py | 2 -- novaclient/v1_1/shell.py | 18 +----------------- novaclient/v3/shell.py | 18 +----------------- 5 files changed, 2 insertions(+), 75 deletions(-) diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index aa8f2414a..594a9f2d4 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -1036,8 +1036,6 @@ def get_os_quota_sets_test(self, **kw): 'metadata_items': [], 'injected_file_content_bytes': 1, 'injected_file_path_bytes': 1, - 'volumes': 1, - 'gigabytes': 1, 'ram': 1, 'floating_ips': 1, 'instances': 1, @@ -1053,8 +1051,6 @@ def get_os_quota_sets_tenant_id(self, **kw): 'metadata_items': [], 'injected_file_content_bytes': 1, 'injected_file_path_bytes': 1, - 'volumes': 1, - 'gigabytes': 1, 'ram': 1, 'floating_ips': 1, 'instances': 1, @@ -1070,8 +1066,6 @@ def get_os_quota_sets_97f4c221bff44578b0300df4ef119353(self, **kw): 'metadata_items': [], 'injected_file_content_bytes': 1, 'injected_file_path_bytes': 1, - 'volumes': 1, - 'gigabytes': 1, 'ram': 1, 'floating_ips': 1, 'instances': 1, @@ -1087,8 +1081,6 @@ def put_os_quota_sets_97f4c221_bff4_4578_b030_0df4ef119353(self, **kw): 'metadata_items': [], 'injected_file_content_bytes': 1, 'injected_file_path_bytes': 1, - 'volumes': 1, - 'gigabytes': 1, 'ram': 1, 'floating_ips': 1, 'instances': 1, @@ -1104,8 +1096,6 @@ def get_os_quota_sets_97f4c221_bff4_4578_b030_0df4ef119353(self, **kw): 'metadata_items': [], 'injected_file_content_bytes': 1, 'injected_file_path_bytes': 1, - 'volumes': 1, - 'gigabytes': 1, 'ram': 1, 'floating_ips': 1, 'instances': 1, @@ -1121,8 +1111,6 @@ def get_os_quota_sets_97f4c221bff44578b0300df4ef119353_defaults(self): 'metadata_items': [], 'injected_file_content_bytes': 1, 'injected_file_path_bytes': 1, - 'volumes': 1, - 'gigabytes': 1, 'ram': 1, 'floating_ips': 1, 'instances': 1, @@ -1138,8 +1126,6 @@ def get_os_quota_sets_tenant_id_defaults(self): 'metadata_items': [], 'injected_file_content_bytes': 1, 'injected_file_path_bytes': 1, - 'volumes': 1, - 'gigabytes': 1, 'ram': 1, 'floating_ips': 1, 'instances': 1, @@ -1158,8 +1144,6 @@ def put_os_quota_sets_97f4c221bff44578b0300df4ef119353(self, body, **kw): 'metadata_items': [], 'injected_file_content_bytes': 1, 'injected_file_path_bytes': 1, - 'volumes': 2, - 'gigabytes': 1, 'ram': 1, 'floating_ips': 1, 'instances': 1, diff --git a/novaclient/tests/v1_1/test_quotas.py b/novaclient/tests/v1_1/test_quotas.py index 1c679389e..06d7461d3 100644 --- a/novaclient/tests/v1_1/test_quotas.py +++ b/novaclient/tests/v1_1/test_quotas.py @@ -42,20 +42,6 @@ def test_tenant_quotas_defaults(self): self.cs.quotas.defaults(tenant_id) self.cs.assert_called('GET', '/os-quota-sets/%s/defaults' % tenant_id) - def test_update_quota(self): - q = self.cs.quotas.get('97f4c221bff44578b0300df4ef119353') - q.update(volumes=2) - self.cs.assert_called('PUT', - '/os-quota-sets/97f4c221bff44578b0300df4ef119353') - - def test_update_user_quota(self): - tenant_id = '97f4c221bff44578b0300df4ef119353' - user_id = 'fake_user' - q = self.cs.quotas.get(tenant_id) - q.update(volumes=2, user_id=user_id) - url = '/os-quota-sets/%s?user_id=%s' % (tenant_id, user_id) - self.cs.assert_called('PUT', url) - def test_force_update_quota(self): q = self.cs.quotas.get('97f4c221bff44578b0300df4ef119353') q.update(cores=2, force=True) @@ -65,15 +51,6 @@ def test_force_update_quota(self): 'cores': 2, 'tenant_id': '97f4c221bff44578b0300df4ef119353'}}) - def test_refresh_quota(self): - q = self.cs.quotas.get('test') - q2 = self.cs.quotas.get('test') - self.assertEqual(q.volumes, q2.volumes) - q2.volumes = 0 - self.assertNotEqual(q.volumes, q2.volumes) - q2.get() - self.assertEqual(q.volumes, q2.volumes) - def test_quotas_delete(self): tenant_id = 'test' self.cs.quotas.delete(tenant_id) diff --git a/novaclient/tests/v3/fakes.py b/novaclient/tests/v3/fakes.py index 60063f3e9..1013910fc 100644 --- a/novaclient/tests/v3/fakes.py +++ b/novaclient/tests/v3/fakes.py @@ -300,8 +300,6 @@ def put_os_quota_sets_97f4c221bff44578b0300df4ef119353(self, body, **kw): 'metadata_items': [], 'injected_file_content_bytes': 1, 'injected_file_path_bytes': 1, - 'volumes': 2, - 'gigabytes': 1, 'ram': 1, 'floating_ips': 1, 'instances': 1, diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 9fee3ebbe..af26be311 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -3065,7 +3065,7 @@ def do_ssh(cs, args): return -_quota_resources = ['instances', 'cores', 'ram', 'volumes', 'gigabytes', +_quota_resources = ['instances', 'cores', 'ram', 'floating_ips', 'fixed_ips', 'metadata_items', 'injected_files', 'injected_file_content_bytes', 'injected_file_path_bytes', 'key_pairs', @@ -3157,14 +3157,6 @@ def do_quota_defaults(cs, args): metavar='', type=int, default=None, help='New value for the "ram" quota.') -@utils.arg('--volumes', - metavar='', - type=int, default=None, - help='New value for the "volumes" quota.') -@utils.arg('--gigabytes', - metavar='', - type=int, default=None, - help='New value for the "gigabytes" quota.') @utils.arg('--floating-ips', metavar='', type=int, @@ -3272,14 +3264,6 @@ def do_quota_class_show(cs, args): metavar='', type=int, default=None, help='New value for the "ram" quota.') -@utils.arg('--volumes', - metavar='', - type=int, default=None, - help='New value for the "volumes" quota.') -@utils.arg('--gigabytes', - metavar='', - type=int, default=None, - help='New value for the "gigabytes" quota.') @utils.arg('--floating-ips', metavar='', type=int, diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 7b0dcfee1..9854c1e6a 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -2618,7 +2618,7 @@ def do_ssh(cs, args): return -_quota_resources = ['instances', 'cores', 'ram', 'volumes', 'gigabytes', +_quota_resources = ['instances', 'cores', 'ram', 'fixed_ips', 'metadata_items', 'key_pairs'] @@ -2718,14 +2718,6 @@ def do_quota_defaults(cs, args): metavar='', type=int, default=None, help='New value for the "ram" quota.') -@utils.arg('--volumes', - metavar='', - type=int, default=None, - help='New value for the "volumes" quota.') -@utils.arg('--gigabytes', - metavar='', - type=int, default=None, - help='New value for the "gigabytes" quota.') @utils.arg('--fixed-ips', metavar='', type=int, @@ -2789,14 +2781,6 @@ def do_quota_class_show(cs, args): metavar='', type=int, default=None, help='New value for the "ram" quota.') -@utils.arg('--volumes', - metavar='', - type=int, default=None, - help='New value for the "volumes" quota.') -@utils.arg('--gigabytes', - metavar='', - type=int, default=None, - help='New value for the "gigabytes" quota.') @utils.arg('--metadata-items', metavar='', type=int, From adcdf116611e8cebd5713ff06b34ae0192c0f5fe Mon Sep 17 00:00:00 2001 From: huangtianhua Date: Wed, 12 Feb 2014 16:00:30 +0800 Subject: [PATCH 0414/1705] Fix copy/paste errors in print messages "Aggregate %s has been successfully updated." as the print message of do_aggregate_set_metadata/do_aggregate_add_host/do_aggregate_remove_host is incorrect. Change-Id: I18c1c3a4403a8ebe3062ef7a695b0e643619dc74 --- novaclient/v1_1/shell.py | 9 ++++++--- novaclient/v3/shell.py | 9 ++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 9fee3ebbe..ee667957e 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2698,7 +2698,8 @@ def do_aggregate_set_metadata(cs, args): aggregate = _find_aggregate(cs, args.aggregate) metadata = _extract_metadata(args) aggregate = cs.aggregates.set_metadata(aggregate.id, metadata) - print("Aggregate %s has been successfully updated." % aggregate.id) + print("Metadata has been successfully updated for aggregate %s." % + aggregate.id) _print_aggregate_details(aggregate) @@ -2708,7 +2709,8 @@ def do_aggregate_add_host(cs, args): """Add the host to the specified aggregate.""" aggregate = _find_aggregate(cs, args.aggregate) aggregate = cs.aggregates.add_host(aggregate.id, args.host) - print("Aggregate %s has been successfully updated." % aggregate.id) + print("Host %s has been successfully added for aggregate %s " % + (args.host, aggregate.id)) _print_aggregate_details(aggregate) @@ -2719,7 +2721,8 @@ def do_aggregate_remove_host(cs, args): """Remove the specified host from the specified aggregate.""" aggregate = _find_aggregate(cs, args.aggregate) aggregate = cs.aggregates.remove_host(aggregate.id, args.host) - print("Aggregate %s has been successfully updated." % aggregate.id) + print("Host %s has been successfully removed from aggregate %s " % + (args.host, aggregate.id)) _print_aggregate_details(aggregate) diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 7b0dcfee1..f81264eda 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -2244,7 +2244,8 @@ def do_aggregate_set_metadata(cs, args): aggregate = _find_aggregate(cs, args.aggregate) metadata = _extract_metadata(args) aggregate = cs.aggregates.set_metadata(aggregate.id, metadata) - print("Aggregate %s has been successfully updated." % aggregate.id) + print("Metadata has been successfully updated for aggregate %s." % + aggregate.id) _print_aggregate_details(aggregate) @@ -2254,7 +2255,8 @@ def do_aggregate_add_host(cs, args): """Add the host to the specified aggregate.""" aggregate = _find_aggregate(cs, args.aggregate) aggregate = cs.aggregates.add_host(aggregate.id, args.host) - print("Aggregate %s has been successfully updated." % aggregate.id) + print("Host %s has been successfully added for aggregate %s " % + (args.host, aggregate.id)) _print_aggregate_details(aggregate) @@ -2265,7 +2267,8 @@ def do_aggregate_remove_host(cs, args): """Remove the specified host from the specified aggregate.""" aggregate = _find_aggregate(cs, args.aggregate) aggregate = cs.aggregates.remove_host(aggregate.id, args.host) - print("Aggregate %s has been successfully updated." % aggregate.id) + print("Host %s has been successfully removed from aggregate %s " % + (args.host, aggregate.id)) _print_aggregate_details(aggregate) From 8edc9b6a6ab6ce6aa9a23b531cae92e637d1104d Mon Sep 17 00:00:00 2001 From: Haiwei Xu Date: Thu, 13 Feb 2014 02:45:51 +0900 Subject: [PATCH 0415/1705] Fix spelling miss of password_func variable In novaclient/shell.py there is a variable called 'password_fun' which isn't used anywhere. In fact it's a spelling miss, and should be modified to 'password_func' which is used in novaclient/client.py This patch fixes this bug. Change-Id: Ibb8e95a2efc77575dcc8544584c708c5c62b7dea --- novaclient/shell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/shell.py b/novaclient/shell.py index 3f17091f6..e10b58239 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -669,7 +669,7 @@ def main(self, argv): self.cs.client.tenant_id = tenant_id self.cs.client.auth_token = auth_token self.cs.client.management_url = management_url - self.cs.client.password_fun = lambda: helper.password + self.cs.client.password_func = lambda: helper.password elif use_pw: # We're missing something, so auth with user/pass and save # the result in our helper. From dacbe0b070764054679e40e05339f51c74f3c4b3 Mon Sep 17 00:00:00 2001 From: Eric Guo Date: Thu, 13 Feb 2014 23:17:34 +0800 Subject: [PATCH 0416/1705] Update broken command line reference link The old reference link of nova command line was broken, update it with latest one. Change-Id: I4ab8a4750c7090ba20def6e47d1c4fe905a8256d --- doc/source/man/nova.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/man/nova.rst b/doc/source/man/nova.rst index a502b4393..78093c7ef 100644 --- a/doc/source/man/nova.rst +++ b/doc/source/man/nova.rst @@ -76,7 +76,7 @@ Terminate an instance:: SEE ALSO ======== -OpenStack Nova CLI Guide: http://docs.openstack.org/cli/quick-start/content/nova-cli-reference.html +OpenStack Nova CLI Guide: http://docs.openstack.org/cli-reference/content/novaclient_commands.html BUGS From 4af3cb133cf0527ffaf40f3252b888bafe57fd5f Mon Sep 17 00:00:00 2001 From: Sergio Cazzolato Date: Tue, 11 Feb 2014 12:07:42 -0300 Subject: [PATCH 0417/1705] Fix i18n messages in novaclient, part II This change make all the text visible by the user i18n. The messages changes are in prints, logs, exceptions, helps, etc Pep8 errors about "Multiple positional placeholders" also fixed. Change-Id: Ia69c86bb2ee8be3fa8af734f793a81eefebe362d --- novaclient/v1_1/shell.py | 907 ++++++++++++++++++++------------------- 1 file changed, 467 insertions(+), 440 deletions(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 989a565e5..5f0858b55 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -30,6 +30,7 @@ import six from novaclient import exceptions +from novaclient.openstack.common.gettextutils import _ from novaclient.openstack.common import strutils from novaclient.openstack.common import timeutils from novaclient.openstack.common import uuidutils @@ -135,11 +136,11 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): if max_count is None: max_count = min_count if min_count > max_count: - raise exceptions.CommandError("min_instances should be <= " - "max_instances") + raise exceptions.CommandError(_("min_instances should be <= " + "max_instances")) if not min_count or not max_count: - raise exceptions.CommandError("min_instances nor max_instances should" - "be 0") + raise exceptions.CommandError(_("min_instances nor max_instances " + "should be 0")) if args.image: image = _find_image(cs, args.image) @@ -154,11 +155,11 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): image = images[0] if not args.flavor: - raise exceptions.CommandError("you need to specify a Flavor ID ") + raise exceptions.CommandError(_("you need to specify a Flavor ID ")) if args.num_instances is not None: if args.num_instances <= 1: - raise exceptions.CommandError("num_instances should be > 1") + raise exceptions.CommandError(_("num_instances should be > 1")) max_count = args.num_instances flavor = _find_flavor(cs, args.flavor) @@ -171,10 +172,13 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): dst, src = f.split('=', 1) files[dst] = open(src) except IOError as e: - raise exceptions.CommandError("Can't open '%s': %s" % (src, e)) + raise exceptions.CommandError(_("Can't open '%(src)s': %(exc)s") % + {'src': src, 'exc': e}) except ValueError as e: - raise exceptions.CommandError("Invalid file argument '%s'. File " - "arguments must be of the form '--file '" % f) + raise exceptions.CommandError(_("Invalid file argument '%s'. " + "File arguments must be of the " + "form '--file " + "'") % f) # use the os-keypair extension key_name = None @@ -185,8 +189,10 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): try: userdata = open(args.user_data) except IOError as e: - raise exceptions.CommandError("Can't open '%s': %s" % - (args.user_data, e)) + raise exceptions.CommandError(_("Can't open '%(user_data)s': " + "%(exc)s") % + {'user_data': args.user_data, + 'exc': e}) else: userdata = None @@ -215,22 +221,22 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): # or if there is no device to boot from. if n_boot_args > 1 or n_boot_args == 0 and not have_bdm: raise exceptions.CommandError( - "you need to specify at least one source ID (Image, Snapshot or " - "Volume), a block device mapping or provide a set of properties " - "to match against an image") + _("you need to specify at least one source ID (Image, Snapshot, " + "or Volume), a block device mapping or provide a set of " + "properties to match against an image")) if block_device_mapping and block_device_mapping_v2: raise exceptions.CommandError( - "you can't mix old block devices (--block-device-mapping) " + _("you can't mix old block devices (--block-device-mapping) " "with the new ones (--block-device, --boot-volume, --snapshot, " - "--ephemeral, --swap)") + "--ephemeral, --swap)")) nics = [] for nic_str in args.nics: - err_msg = ("Invalid nic argument '%s'. Nic arguments must be of the " - "form --nic , with at minimum net-id or port-id " - "specified." % nic_str) + "specified.") % nic_str) nic_info = {"net-id": "", "v4-fixed-ip": "", "port-id": ""} for kv_str in nic_str.split(","): @@ -292,74 +298,74 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): @utils.arg('--flavor', default=None, metavar='', - help="Name or ID of flavor (see 'nova flavor-list').") + help=_("Name or ID of flavor (see 'nova flavor-list').")) @utils.arg('--image', default=None, metavar='', - help="Name or ID of image (see 'nova image-list'). ") + help=_("Name or ID of image (see 'nova image-list'). ")) @utils.arg('--image-with', default=[], type=_key_value_pairing, action='append', metavar='', - help="Image metadata property (see 'nova image-show'). ") + help=_("Image metadata property (see 'nova image-show'). ")) @utils.arg('--boot-volume', default=None, metavar="", - help="Volume ID to boot from.") + help=_("Volume ID to boot from.")) @utils.arg('--snapshot', default=None, metavar="", - help="Sapshot ID to boot from (will create a volume).") + help=_("Sapshot ID to boot from (will create a volume).")) @utils.arg('--num-instances', default=None, type=int, metavar='', - help="boot multiple servers at a time (limited by quota).") + help=_("boot multiple servers at a time (limited by quota).")) @utils.arg('--meta', metavar="", action='append', default=[], - help="Record arbitrary key/value metadata to /meta.js " - "on the new server. Can be specified multiple times.") + help=_("Record arbitrary key/value metadata to /meta.js " + "on the new server. Can be specified multiple times.")) @utils.arg('--file', metavar="", action='append', dest='files', default=[], - help="Store arbitrary files from locally to " - "on the new server. You may store up to 5 files.") + help=_("Store arbitrary files from locally to " + "on the new server. You may store up to 5 files.")) @utils.arg('--key-name', metavar='', - help="Key name of keypair that should be created earlier with \ - the command keypair-add") + help=_("Key name of keypair that should be created earlier with \ + the command keypair-add")) @utils.arg('--key_name', help=argparse.SUPPRESS) -@utils.arg('name', metavar='', help='Name for the new server') +@utils.arg('name', metavar='', help=_('Name for the new server')) @utils.arg('--user-data', default=None, metavar='', - help="user data file to pass to be exposed by the metadata server.") + help=_("user data file to pass to be exposed by the metadata server.")) @utils.arg('--user_data', help=argparse.SUPPRESS) @utils.arg('--availability-zone', default=None, metavar='', - help="The availability zone for server placement.") + help=_("The availability zone for server placement.")) @utils.arg('--availability_zone', help=argparse.SUPPRESS) @utils.arg('--security-groups', default=None, metavar='', - help="Comma separated list of security group names.") + help=_("Comma separated list of security group names.")) @utils.arg('--security_groups', help=argparse.SUPPRESS) @utils.arg('--block-device-mapping', metavar="", action='append', default=[], - help="Block device mapping in the format " - "=:::.") + help=_("Block device mapping in the format " + "=:::.")) @utils.arg('--block_device_mapping', action='append', help=argparse.SUPPRESS) @@ -367,7 +373,7 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): metavar="key1=value1[,key2=value2...]", action='append', default=[], - help="Block device mapping with the keys: " + help=_("Block device mapping with the keys: " "id=image_id, snapshot_id or volume_id, " "source=source type (image, snapshot, volume or blank), " "dest=destination type of the block device (volume or local), " @@ -377,45 +383,46 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): "format=device will be formatted (e.g. swap, ext3, ntfs, ...), " "bootindex=integer used for ordering the boot disks, " "type=device type (e.g. disk, cdrom, ...) and " - "shutdown=shutdown behaviour (either preserve or remove).") + "shutdown=shutdown behaviour (either preserve or remove).")) @utils.arg('--swap', metavar="", default=None, - help="Create and attach a local swap block device of MB.") + help=_("Create and attach a local swap block device of MB.")) @utils.arg('--ephemeral', metavar="size=[,format=]", action='append', default=[], - help="Create and attach a local ephemeral block device of GB " - "and format it to .") + help=_("Create and attach a local ephemeral block device of GB " + "and format it to .")) @utils.arg('--hint', action='append', dest='scheduler_hints', default=[], metavar='', - help="Send arbitrary key/value pairs to the scheduler for custom use.") + help=_("Send arbitrary key/value pairs to the scheduler for custom " + "use.")) @utils.arg('--nic', metavar="", action='append', dest='nics', default=[], - help="Create a NIC on the server. " + help=_("Create a NIC on the server. " "Specify option multiple times to create multiple NICs. " "net-id: attach NIC to network with this UUID " "(required if no port-id), " "v4-fixed-ip: IPv4 fixed address for NIC (optional), " "port-id: attach NIC to port with this UUID " - "(required if no net-id)") + "(required if no net-id)")) @utils.arg('--config-drive', metavar="", dest='config_drive', default=False, - help="Enable config drive") + help=_("Enable config drive")) @utils.arg('--poll', dest='poll', action="store_true", default=False, - help='Blocks while server builds so progress can be reported.') + help=_('Blocks while server builds so progress can be reported.')) def do_boot(cs, args): """Boot a new server.""" boot_args, boot_kwargs = _boot(cs, args) @@ -438,13 +445,13 @@ def do_cloudpipe_list(cs, _args): @utils.arg('project', metavar='', - help='UUID of the project to create the cloudpipe for.') + help=_('UUID of the project to create the cloudpipe for.')) def do_cloudpipe_create(cs, args): """Create a cloudpipe instance for the given project.""" cs.cloudpipe.create(args.project) -@utils.arg('address', metavar='', help='New IP Address.') +@utils.arg('address', metavar='', help=_('New IP Address.')) @utils.arg('port', metavar='', help='New Port.') def do_cloudpipe_configure(cs, args): """Update the VPN IP/port of a cloudpipe instance.""" @@ -459,10 +466,10 @@ def _poll_for_status(poll_fn, obj_id, action, final_ok_states, """ def print_progress(progress): if show_progress: - msg = ('\rServer %(action)s... %(progress)s%% complete' + msg = (_('\rServer %(action)s... %(progress)s%% complete') % dict(action=action, progress=progress)) else: - msg = '\rServer %(action)s...' % dict(action=action) + msg = _('\rServer %(action)s...') % dict(action=action) sys.stdout.write(msg) sys.stdout.flush() @@ -482,11 +489,11 @@ def print_progress(progress): if status in final_ok_states: if not silent: print_progress(100) - print("\nFinished") + print(_("\nFinished")) break elif status == "error": if not silent: - print("\nError %s server" % action) + print(_("\nError %s server") % action) break if not silent: @@ -567,12 +574,12 @@ def _print_flavor_list(flavors, show_extra_specs=False): dest='extra_specs', action='store_true', default=False, - help='Get extra-specs of each flavor.') + help=_('Get extra-specs of each flavor.')) @utils.arg('--all', dest='all', action='store_true', default=False, - help='Display all flavors (Admin only).') + help=_('Display all flavors (Admin only).')) def do_flavor_list(cs, args): """Print a list of available 'flavors' (sizes of servers).""" if args.all: @@ -584,7 +591,7 @@ def do_flavor_list(cs, args): @utils.arg('flavor', metavar='', - help="Name or ID of the flavor to delete") + help=_("Name or ID of the flavor to delete")) def do_flavor_delete(cs, args): """Delete a specific flavor""" flavorid = _find_flavor(cs, args.flavor) @@ -594,7 +601,7 @@ def do_flavor_delete(cs, args): @utils.arg('flavor', metavar='', - help="Name or ID of flavor") + help=_("Name or ID of flavor")) def do_flavor_show(cs, args): """Show details about the given flavor.""" flavor = _find_flavor(cs, args.flavor) @@ -603,35 +610,35 @@ def do_flavor_show(cs, args): @utils.arg('name', metavar='', - help="Name of the new flavor") + help=_("Name of the new flavor")) @utils.arg('id', metavar='', - help="Unique ID (integer or UUID) for the new flavor." - " If specifying 'auto', a UUID will be generated as id") + help=_("Unique ID (integer or UUID) for the new flavor." + " If specifying 'auto', a UUID will be generated as id")) @utils.arg('ram', metavar='', - help="Memory size in MB") + help=_("Memory size in MB")) @utils.arg('disk', metavar='', - help="Disk size in GB") + help=_("Disk size in GB")) @utils.arg('--ephemeral', metavar='', - help="Ephemeral space size in GB (default 0)", + help=_("Ephemeral space size in GB (default 0)"), default=0) @utils.arg('vcpus', metavar='', - help="Number of vcpus") + help=_("Number of vcpus")) @utils.arg('--swap', metavar='', - help="Swap space size in MB (default 0)", + help=_("Swap space size in MB (default 0)"), default=0) @utils.arg('--rxtx-factor', metavar='', - help="RX/TX factor (default 1)", + help=_("RX/TX factor (default 1)"), default=1.0) @utils.arg('--is-public', metavar='', - help="Make flavor accessible to the public (default true)", + help=_("Make flavor accessible to the public (default true)"), type=lambda v: strutils.bool_from_string(v, True), default=True) def do_flavor_create(cs, args): @@ -644,17 +651,17 @@ def do_flavor_create(cs, args): @utils.arg('flavor', metavar='', - help="Name or ID of flavor") + help=_("Name or ID of flavor")) @utils.arg('action', metavar='', choices=['set', 'unset'], - help="Actions: 'set' or 'unset'") + help=_("Actions: 'set' or 'unset'")) @utils.arg('metadata', metavar='', nargs='+', action='append', default=[], - help='Extra_specs to set/unset (only key is necessary on unset)') + help=_('Extra_specs to set/unset (only key is necessary on unset)')) def do_flavor_key(cs, args): """Set or unset extra_spec for a flavor.""" flavor = _find_flavor(cs, args.flavor) @@ -668,25 +675,25 @@ def do_flavor_key(cs, args): @utils.arg('--flavor', metavar='', - help="Filter results by flavor name or ID.") + help=_("Filter results by flavor name or ID.")) @utils.arg('--tenant', metavar='', - help='Filter results by tenant ID.') + help=_('Filter results by tenant ID.')) def do_flavor_access_list(cs, args): """Print access information about the given flavor.""" if args.flavor and args.tenant: - raise exceptions.CommandError("Unable to filter results by " - "both --flavor and --tenant.") + raise exceptions.CommandError(_("Unable to filter results by " + "both --flavor and --tenant.")) elif args.flavor: flavor = _find_flavor(cs, args.flavor) if flavor.is_public: - raise exceptions.CommandError("Failed to get access list " - "for public flavor type.") + raise exceptions.CommandError(_("Failed to get access list " + "for public flavor type.")) kwargs = {'flavor': flavor} elif args.tenant: kwargs = {'tenant': args.tenant} else: - raise exceptions.CommandError("Unable to get all access lists. " - "Specify --flavor or --tenant") + raise exceptions.CommandError(_("Unable to get all access lists. " + "Specify --flavor or --tenant")) try: access_list = cs.flavor_access.list(**kwargs) @@ -699,9 +706,9 @@ def do_flavor_access_list(cs, args): @utils.arg('flavor', metavar='', - help="Flavor name or ID to add access for the given tenant.") + help=_("Flavor name or ID to add access for the given tenant.")) @utils.arg('tenant', metavar='', - help='Tenant ID to add flavor access for.') + help=_('Tenant ID to add flavor access for.')) def do_flavor_access_add(cs, args): """Add flavor access for the given tenant.""" flavor = _find_flavor_for_admin(cs, args.flavor) @@ -712,9 +719,9 @@ def do_flavor_access_add(cs, args): @utils.arg('flavor', metavar='', - help="Flavor name or ID to remove access for the given tenant.") + help=_("Flavor name or ID to remove access for the given tenant.")) @utils.arg('tenant', metavar='', - help='Tenant ID to remove flavor access for.') + help=_('Tenant ID to remove flavor access for.')) def do_flavor_access_remove(cs, args): """Remove flavor access for the given tenant.""" flavor = _find_flavor_for_admin(cs, args.flavor) @@ -724,7 +731,7 @@ def do_flavor_access_remove(cs, args): @utils.arg('project_id', metavar='', - help='The ID of the project.') + help=_('The ID of the project.')) def do_scrub(cs, args): """Delete data associated with the project.""" networks_list = cs.networks.list() @@ -749,7 +756,7 @@ def do_network_list(cs, _args): @utils.arg('network', metavar='', - help="uuid or label of network") + help=_("uuid or label of network")) def do_network_show(cs, args): """Show details about the given network.""" network = utils.find_resource(cs.networks, args.network) @@ -817,69 +824,69 @@ def _filter_network_create_options(args): @utils.arg('label', metavar='', - help="Label for network") + help=_("Label for network")) @utils.arg('--fixed-range-v4', dest='cidr', metavar='', - help="IPv4 subnet (ex: 10.0.0.0/8)") + help=_("IPv4 subnet (ex: 10.0.0.0/8)")) @utils.arg('--fixed-range-v6', dest="cidr_v6", - help='IPv6 subnet (ex: fe80::/64') + help=_('IPv6 subnet (ex: fe80::/64')) @utils.arg('--vlan', dest='vlan_start', metavar='', - help="vlan id") + help=_("vlan id")) @utils.arg('--vpn', dest='vpn_start', metavar='', - help="vpn start") + help=_("vpn start")) @utils.arg('--gateway', dest="gateway", - help='gateway') + help=_('gateway')) @utils.arg('--gateway-v6', dest="gateway_v6", - help='ipv6 gateway') + help=_('ipv6 gateway')) @utils.arg('--bridge', dest="bridge", metavar='', - help='VIFs on this network are connected to this bridge') + help=_('VIFs on this network are connected to this bridge')) @utils.arg('--bridge-interface', dest="bridge_interface", metavar='', - help='the bridge is connected to this interface') + help=_('the bridge is connected to this interface')) @utils.arg('--multi-host', dest="multi_host", metavar="<'T'|'F'>", - help='Multi host') + help=_('Multi host')) @utils.arg('--dns1', dest="dns1", metavar="", help='First DNS') @utils.arg('--dns2', dest="dns2", metavar="", - help='Second DNS') + help=_('Second DNS')) @utils.arg('--uuid', dest="uuid", metavar="", - help='Network UUID') + help=_('Network UUID')) @utils.arg('--fixed-cidr', dest="fixed_cidr", metavar='', - help='IPv4 subnet for fixed IPS (ex: 10.20.0.0/16)') + help=_('IPv4 subnet for fixed IPS (ex: 10.20.0.0/16)')) @utils.arg('--project-id', dest="project_id", metavar="", - help='Project id') + help=_('Project id')) @utils.arg('--priority', dest="priority", metavar="", - help='Network interface priority') + help=_('Network interface priority')) def do_network_create(cs, args): """Create a network.""" if not (args.cidr or args.cidr_v6): raise exceptions.CommandError( - "Must specify eith fixed_range_v4 or fixed_range_v6") + _("Must specify eith fixed_range_v4 or fixed_range_v6")) kwargs = _filter_network_create_options(args) if args.multi_host is not None: kwargs['multi_host'] = bool(args.multi_host == 'T' or @@ -891,7 +898,7 @@ def do_network_create(cs, args): @utils.arg('--limit', dest="limit", metavar="", - help='number of images to return per request') + help=_('number of images to return per request')) def do_image_list(cs, _args): """Print a list of available images to boot from.""" limit = _args.limit @@ -910,17 +917,18 @@ def parse_server_name(image): @utils.arg('image', metavar='', - help="Name or ID of image") + help=_("Name or ID of image")) @utils.arg('action', metavar='', choices=['set', 'delete'], - help="Actions: 'set' or 'delete'") + help=_("Actions: 'set' or 'delete'")) @utils.arg('metadata', metavar='', nargs='+', action='append', default=[], - help='Metadata to add/update or delete (only key is necessary on delete)') + help=_('Metadata to add/update or delete (only key is necessary on ' + 'delete)')) def do_image_meta(cs, args): """Set or Delete metadata on an image.""" image = _find_image(cs, args.image) @@ -982,7 +990,7 @@ def _print_flavor(flavor): @utils.arg('image', metavar='', - help="Name or ID of image") + help=_("Name or ID of image")) def do_image_show(cs, args): """Show details about the given image.""" image = _find_image(cs, args.image) @@ -990,66 +998,69 @@ def do_image_show(cs, args): @utils.arg('image', metavar='', nargs='+', - help='Name or ID of image(s).') + help=_('Name or ID of image(s).')) def do_image_delete(cs, args): """Delete specified image(s).""" for image in args.image: try: _find_image(cs, image).delete() except Exception as e: - print("Delete for image %s failed: %s" % (image, e)) + print(_("Delete for image %(image)s failed: %(e)s") % + {'image': image, 'e': e}) @utils.arg('--reservation-id', dest='reservation_id', metavar='', default=None, - help='Only return servers that match reservation-id.') + help=_('Only return servers that match reservation-id.')) @utils.arg('--reservation_id', help=argparse.SUPPRESS) @utils.arg('--ip', dest='ip', metavar='', default=None, - help='Search with regular expression match by IP address (Admin only).') + help=_('Search with regular expression match by IP address (Admin only).')) @utils.arg('--ip6', dest='ip6', metavar='', default=None, - help='Search with regular expression match by IPv6 address (Admin only).') + help=_('Search with regular expression match by IPv6 address ' + '(Admin only).')) @utils.arg('--name', dest='name', metavar='', default=None, - help='Search with regular expression match by name') + help=_('Search with regular expression match by name')) @utils.arg('--instance-name', dest='instance_name', metavar='', default=None, - help='Search with regular expression match by server name (Admin only).') + help=_('Search with regular expression match by server name ' + '(Admin only).')) @utils.arg('--instance_name', help=argparse.SUPPRESS) @utils.arg('--status', dest='status', metavar='', default=None, - help='Search by server status') + help=_('Search by server status')) @utils.arg('--flavor', dest='flavor', metavar='', default=None, - help='Search by flavor name or ID') + help=_('Search by flavor name or ID')) @utils.arg('--image', dest='image', metavar='', default=None, - help='Search by image name or ID') + help=_('Search by image name or ID')) @utils.arg('--host', dest='host', metavar='', default=None, - help='Search servers by hostname to which they are assigned ' - '(Admin only).') + help=_('Search servers by hostname to which they are assigned (Admin ' + 'only).')) @utils.arg('--all-tenants', dest='all_tenants', metavar='<0|1>', @@ -1058,7 +1069,7 @@ def do_image_delete(cs, args): const=1, default=int(strutils.bool_from_string( os.environ.get("ALL_TENANTS", 'false'), True)), - help='Display information from all tenants (Admin only).') + help=_('Display information from all tenants (Admin only).')) @utils.arg('--all_tenants', nargs='?', type=int, @@ -1069,7 +1080,7 @@ def do_image_delete(cs, args): dest='tenant', metavar='', nargs='?', - help='Display information from single tenant (Admin only).') + help=_('Display information from single tenant (Admin only).')) @utils.arg('--deleted', dest='deleted', action="store_true", @@ -1078,13 +1089,13 @@ def do_image_delete(cs, args): @utils.arg('--fields', default=None, metavar='', - help='Comma-separated list of fields to display. ' - 'Use the show command to see which fields are available.') + help=_('Comma-separated list of fields to display. ' + 'Use the show command to see which fields are available.')) @utils.arg('--minimal', dest='minimal', action="store_true", default=False, - help='Get only uuid and name.') + help=_('Get only uuid and name.')) def do_list(cs, args): """List active servers.""" imageid = None @@ -1157,13 +1168,13 @@ def do_list(cs, args): action='store_const', const=servers.REBOOT_HARD, default=servers.REBOOT_SOFT, - help='Perform a hard reboot (instead of a soft one).') -@utils.arg('server', metavar='', help='Name or ID of server.') + help=_('Perform a hard reboot (instead of a soft one).')) +@utils.arg('server', metavar='', help=_('Name or ID of server.')) @utils.arg('--poll', dest='poll', action="store_true", default=False, - help='Blocks while server is rebooting.') + help=_('Blocks while server is rebooting.')) def do_reboot(cs, args): """Reboot a server.""" server = _find_server(cs, args.server) @@ -1174,25 +1185,25 @@ def do_reboot(cs, args): show_progress=False) -@utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg('image', metavar='', help="Name or ID of new image.") +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('image', metavar='', help=_("Name or ID of new image.")) @utils.arg('--rebuild-password', dest='rebuild_password', metavar='', default=False, - help="Set the provided password on the rebuild server.") + help=_("Set the provided password on the rebuild server.")) @utils.arg('--rebuild_password', help=argparse.SUPPRESS) @utils.arg('--poll', dest='poll', action="store_true", default=False, - help='Blocks while server rebuilds so progress can be reported.') + help=_('Blocks while server rebuilds so progress can be reported.')) @utils.arg('--minimal', dest='minimal', action="store_true", default=False, - help='Skips flavor/image lookups when showing servers') + help=_('Skips flavor/image lookups when showing servers')) @utils.arg('--preserve-ephemeral', action="store_true", default=False, @@ -1217,20 +1228,20 @@ def do_rebuild(cs, args): @utils.arg('server', metavar='', - help='Name (old name) or ID of server.') -@utils.arg('name', metavar='', help='New name for the server.') + help=_('Name (old name) or ID of server.')) +@utils.arg('name', metavar='', help=_('New name for the server.')) def do_rename(cs, args): """Rename a server.""" _find_server(cs, args.server).update(name=args.name) -@utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg('flavor', metavar='', help="Name or ID of new flavor.") +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('flavor', metavar='', help=_("Name or ID of new flavor.")) @utils.arg('--poll', dest='poll', action="store_true", default=False, - help='Blocks while servers resizes so progress can be reported.') + help=_('Blocks while servers resizes so progress can be reported.')) def do_resize(cs, args): """Resize a server.""" server = _find_server(cs, args.server) @@ -1242,24 +1253,24 @@ def do_resize(cs, args): ['active', 'verify_resize']) -@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('server', metavar='', help=_('Name or ID of server.')) def do_resize_confirm(cs, args): """Confirm a previous resize.""" _find_server(cs, args.server).confirm_resize() -@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('server', metavar='', help=_('Name or ID of server.')) def do_resize_revert(cs, args): """Revert a previous resize (and return to the previous VM).""" _find_server(cs, args.server).revert_resize() -@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('server', metavar='', help=_('Name or ID of server.')) @utils.arg('--poll', dest='poll', action="store_true", default=False, - help='Blocks while server migrates so progress can be reported.') + help=_('Blocks while server migrates so progress can be reported.')) def do_migrate(cs, args): """Migrate a server. The new host will be selected by the scheduler.""" server = _find_server(cs, args.server) @@ -1270,92 +1281,92 @@ def do_migrate(cs, args): ['active', 'verify_resize']) -@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('server', metavar='', help=_('Name or ID of server.')) def do_pause(cs, args): """Pause a server.""" _find_server(cs, args.server).pause() -@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('server', metavar='', help=_('Name or ID of server.')) def do_unpause(cs, args): """Unpause a server.""" _find_server(cs, args.server).unpause() -@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('server', metavar='', help=_('Name or ID of server.')) def do_stop(cs, args): """Stop a server.""" _find_server(cs, args.server).stop() -@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('server', metavar='', help=_('Name or ID of server.')) def do_start(cs, args): """Start a server.""" _find_server(cs, args.server).start() -@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('server', metavar='', help=_('Name or ID of server.')) def do_lock(cs, args): """Lock a server.""" _find_server(cs, args.server).lock() -@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('server', metavar='', help=_('Name or ID of server.')) def do_unlock(cs, args): """Unlock a server.""" _find_server(cs, args.server).unlock() -@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('server', metavar='', help=_('Name or ID of server.')) def do_suspend(cs, args): """Suspend a server.""" _find_server(cs, args.server).suspend() -@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('server', metavar='', help=_('Name or ID of server.')) def do_resume(cs, args): """Resume a server.""" _find_server(cs, args.server).resume() -@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('server', metavar='', help=_('Name or ID of server.')) def do_rescue(cs, args): """Rescue a server.""" utils.print_dict(_find_server(cs, args.server).rescue()[1]) -@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('server', metavar='', help=_('Name or ID of server.')) def do_unrescue(cs, args): """Unrescue a server.""" _find_server(cs, args.server).unrescue() -@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('server', metavar='', help=_('Name or ID of server.')) def do_shelve(cs, args): """Shelve a server.""" _find_server(cs, args.server).shelve() -@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('server', metavar='', help=_('Name or ID of server.')) def do_shelve_offload(cs, args): """Remove a shelved server from the compute node.""" _find_server(cs, args.server).shelve_offload() -@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('server', metavar='', help=_('Name or ID of server.')) def do_unshelve(cs, args): """Unshelve a server.""" _find_server(cs, args.server).unshelve() -@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('server', metavar='', help=_('Name or ID of server.')) def do_diagnostics(cs, args): """Retrieve server diagnostics.""" server = _find_server(cs, args.server) utils.print_dict(cs.servers.diagnostics(server)[1], wrap=80) -@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('server', metavar='', help=_('Name or ID of server.')) def do_root_password(cs, args): """ Change the root password for a server. @@ -1364,22 +1375,22 @@ def do_root_password(cs, args): p1 = getpass.getpass('New password: ') p2 = getpass.getpass('Again: ') if p1 != p2: - raise exceptions.CommandError("Passwords do not match.") + raise exceptions.CommandError(_("Passwords do not match.")) server.change_password(p1) -@utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg('name', metavar='', help='Name of snapshot.') +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('name', metavar='', help=_('Name of snapshot.')) @utils.arg('--show', dest='show', action="store_true", default=False, - help='Print image info.') + help=_('Print image info.')) @utils.arg('--poll', dest='poll', action="store_true", default=False, - help='Blocks while server snapshots so progress can be reported.') + help=_('Blocks while server snapshots so progress can be reported.')) def do_image_create(cs, args): """Create a new image by taking a snapshot of a running server.""" server = _find_server(cs, args.server) @@ -1408,12 +1419,13 @@ def do_image_create(cs, args): _print_image(cs.images.get(image_uuid)) -@utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg('name', metavar='', help='Name of the backup image.') +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('name', metavar='', help=_('Name of the backup image.')) @utils.arg('backup_type', metavar='', - help='The backup type, like "daily" or "weekly".') + help=_('The backup type, like "daily" or "weekly".')) @utils.arg('rotation', metavar='', - help='Int parameter representing how many backups to keep around.') + help=_('Int parameter representing how many backups to keep ' + 'around.')) def do_backup(cs, args): """Backup a server by creating a 'backup' type snapshot.""" _find_server(cs, args.server).backup(args.name, @@ -1423,17 +1435,17 @@ def do_backup(cs, args): @utils.arg('server', metavar='', - help="Name or ID of server") + help=_("Name or ID of server")) @utils.arg('action', metavar='', choices=['set', 'delete'], - help="Actions: 'set' or 'delete'") + help=_("Actions: 'set' or 'delete'")) @utils.arg('metadata', metavar='', nargs='+', action='append', default=[], - help='Metadata to set or delete (only key is necessary on delete)') + help=_('Metadata to set or delete (only key is necessary on delete)')) def do_meta(cs, args): """Set or Delete metadata on a server.""" server = _find_server(cs, args.server) @@ -1483,9 +1495,9 @@ def _print_server(cs, args, server=None): info['image'] = '%s (%s)' % (_find_image(cs, image_id).name, image_id) except Exception: - info['image'] = '%s (%s)' % ("Image not found", image_id) + info['image'] = '%s (%s)' % (_("Image not found"), image_id) else: # Booted from volume - info['image'] = "Attempt to boot from volume - no image supplied" + info['image'] = _("Attempt to boot from volume - no image supplied") info.pop('links', None) info.pop('addresses', None) @@ -1497,15 +1509,15 @@ def _print_server(cs, args, server=None): dest='minimal', action="store_true", default=False, - help='Skips flavor/image lookups when showing servers') -@utils.arg('server', metavar='', help='Name or ID of server.') + help=_('Skips flavor/image lookups when showing servers')) +@utils.arg('server', metavar='', help=_('Name or ID of server.')) def do_show(cs, args): """Show details about the given server.""" _print_server(cs, args) @utils.arg('server', metavar='', nargs='+', - help='Name or ID of server(s).') + help=_('Name or ID of server(s).')) def do_delete(cs, args): """Immediately shut down and delete specified server(s).""" failure_count = 0 @@ -1518,8 +1530,8 @@ def do_delete(cs, args): print(e) if failure_count == len(args.server): - raise exceptions.CommandError("Unable to delete any of the specified " - "servers.") + raise exceptions.CommandError(_("Unable to delete any of the " + "specified servers.")) def _find_server(cs, server): @@ -1548,7 +1560,7 @@ def _find_flavor(cs, flavor): return cs.flavors.find(ram=flavor) -@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('server', metavar='', help=_('Name or ID of server.')) @utils.arg('network_id', metavar='', help='Network ID.') @@ -1558,8 +1570,8 @@ def do_add_fixed_ip(cs, args): server.add_fixed_ip(args.network_id) -@utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg('address', metavar='
', help='IP Address.') +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('address', metavar='
', help=_('IP Address.')) def do_remove_fixed_ip(cs, args): """Remove an IP address from a server.""" server = _find_server(cs, args.server) @@ -1609,7 +1621,7 @@ def _translate_availability_zone_keys(collection): const=1, default=int(strutils.bool_from_string( os.environ.get("ALL_TENANTS", 'false'), True)), - help='Display information from all tenants (Admin only).') + help=_('Display information from all tenants (Admin only).')) @utils.arg('--all_tenants', nargs='?', type=int, @@ -1630,7 +1642,7 @@ def do_volume_list(cs, args): 'Size', 'Volume Type', 'Attached to']) -@utils.arg('volume', metavar='', help='Name or ID of the volume.') +@utils.arg('volume', metavar='', help=_('Name or ID of the volume.')) @utils.service_type('volume') def do_volume_show(cs, args): """Show details about a volume.""" @@ -1641,37 +1653,37 @@ def do_volume_show(cs, args): @utils.arg('size', metavar='', type=int, - help='Size of volume in GB') + help=_('Size of volume in GB')) @utils.arg('--snapshot-id', metavar='', default=None, - help='Optional snapshot id to create the volume from. (Default=None)') + help=_('Optional snapshot id to create the volume from. (Default=None)')) @utils.arg('--snapshot_id', help=argparse.SUPPRESS) @utils.arg('--image-id', metavar='', - help='Optional image id to create the volume from. (Default=None)', + help=_('Optional image id to create the volume from. (Default=None)'), default=None) @utils.arg('--display-name', metavar='', default=None, - help='Optional volume name. (Default=None)') + help=_('Optional volume name. (Default=None)')) @utils.arg('--display_name', help=argparse.SUPPRESS) @utils.arg('--display-description', metavar='', default=None, - help='Optional volume description. (Default=None)') + help=_('Optional volume description. (Default=None)')) @utils.arg('--display_description', help=argparse.SUPPRESS) @utils.arg('--volume-type', metavar='', default=None, - help='Optional volume type. (Default=None)') + help=_('Optional volume type. (Default=None)')) @utils.arg('--volume_type', help=argparse.SUPPRESS) @utils.arg('--availability-zone', metavar='', - help='Optional Availability Zone for volume. (Default=None)', + help=_('Optional Availability Zone for volume. (Default=None)'), default=None) @utils.service_type('volume') def do_volume_create(cs, args): @@ -1688,7 +1700,7 @@ def do_volume_create(cs, args): @utils.arg('volume', metavar='', nargs='+', - help='Name or ID of the volume(s) to delete.') + help=_('Name or ID of the volume(s) to delete.')) @utils.service_type('volume') def do_volume_delete(cs, args): """Remove volume(s).""" @@ -1696,18 +1708,19 @@ def do_volume_delete(cs, args): try: _find_volume(cs, volume).delete() except Exception as e: - print("Delete for volume %s failed: %s" % (volume, e)) + print(_("Delete for volume %(volume)s failed: %(e)s") % + {'volume': volume, 'e': e}) @utils.arg('server', metavar='', - help='Name or ID of server.') + help=_('Name or ID of server.')) @utils.arg('volume', metavar='', - help='ID of the volume to attach.') + help=_('ID of the volume to attach.')) @utils.arg('device', metavar='', default=None, nargs='?', - help='Name of the device e.g. /dev/vdb. ' - 'Use "auto" for autoassign (if supported)') + help=_('Name of the device e.g. /dev/vdb. ' + 'Use "auto" for autoassign (if supported)')) def do_volume_attach(cs, args): """Attach a volume to a server.""" if args.device == 'auto': @@ -1721,13 +1734,13 @@ def do_volume_attach(cs, args): @utils.arg('server', metavar='', - help='Name or ID of server.') + help=_('Name or ID of server.')) @utils.arg('attachment_id', metavar='', - help='Attachment ID of the volume.') + help=_('Attachment ID of the volume.')) @utils.arg('new_volume', metavar='', - help='ID of the volume to attach.') + help=_('ID of the volume to attach.')) def do_volume_update(cs, args): """Update volume attachment.""" volume = cs.volumes.update_server_volume(_find_server(cs, args.server).id, @@ -1738,10 +1751,10 @@ def do_volume_update(cs, args): @utils.arg('server', metavar='', - help='Name or ID of server.') + help=_('Name or ID of server.')) @utils.arg('attachment_id', metavar='', - help='Attachment ID of the volume.') + help=_('Attachment ID of the volume.')) def do_volume_detach(cs, args): """Detach a volume from a server.""" cs.volumes.delete_server_volume(_find_server(cs, args.server).id, @@ -1759,7 +1772,7 @@ def do_volume_snapshot_list(cs, _args): @utils.arg('snapshot', metavar='', - help='Name or ID of the snapshot.') + help=_('Name or ID of the snapshot.')) @utils.service_type('volume') def do_volume_snapshot_show(cs, args): """Show details about a snapshot.""" @@ -1769,22 +1782,22 @@ def do_volume_snapshot_show(cs, args): @utils.arg('volume_id', metavar='', - help='ID of the volume to snapshot') + help=_('ID of the volume to snapshot')) @utils.arg('--force', metavar='', - help='Optional flag to indicate whether to snapshot a volume even if its ' - 'attached to a server. (Default=False)', + help=_('Optional flag to indicate whether to snapshot a volume even if ' + 'its attached to a server. (Default=False)'), default=False) @utils.arg('--display-name', metavar='', default=None, - help='Optional snapshot name. (Default=None)') + help=_('Optional snapshot name. (Default=None)')) @utils.arg('--display_name', help=argparse.SUPPRESS) @utils.arg('--display-description', metavar='', default=None, - help='Optional snapshot description. (Default=None)') + help=_('Optional snapshot description. (Default=None)')) @utils.arg('--display_description', help=argparse.SUPPRESS) @utils.service_type('volume') @@ -1799,7 +1812,7 @@ def do_volume_snapshot_create(cs, args): @utils.arg('snapshot', metavar='', - help='Name or ID of the snapshot to delete.') + help=_('Name or ID of the snapshot to delete.')) @utils.service_type('volume') def do_volume_snapshot_delete(cs, args): """Remove a snapshot.""" @@ -1820,7 +1833,7 @@ def do_volume_type_list(cs, args): @utils.arg('name', metavar='', - help="Name of the new flavor") + help=_("Name of the new flavor")) @utils.service_type('volume') def do_volume_type_create(cs, args): """Create a new volume type.""" @@ -1830,17 +1843,17 @@ def do_volume_type_create(cs, args): @utils.arg('id', metavar='', - help="Unique ID of the volume type to delete") + help=_("Unique ID of the volume type to delete")) @utils.service_type('volume') def do_volume_type_delete(cs, args): """Delete a specific flavor""" cs.volume_types.delete(args.id) -@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('server', metavar='', help=_('Name or ID of server.')) @utils.arg('console_type', metavar='', - help='Type of vnc console ("novnc" or "xvpvnc").') + help=_('Type of vnc console ("novnc" or "xvpvnc").')) def do_get_vnc_console(cs, args): """Get a vnc console to a server.""" server = _find_server(cs, args.server) @@ -1854,10 +1867,10 @@ def __init__(self, console_dict): utils.print_list([VNCConsole(data['console'])], ['Type', 'Url']) -@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('server', metavar='', help=_('Name or ID of server.')) @utils.arg('console_type', metavar='', - help='Type of spice console ("spice-html5").') + help=_('Type of spice console ("spice-html5").')) def do_get_spice_console(cs, args): """Get a spice console to a server.""" server = _find_server(cs, args.server) @@ -1871,7 +1884,7 @@ def __init__(self, console_dict): utils.print_list([SPICEConsole(data['console'])], ['Type', 'Url']) -@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('server', metavar='', help=_('Name or ID of server.')) @utils.arg('console_type', metavar='', help='Type of rdp console ("rdp-html5").') @@ -1891,10 +1904,10 @@ def __init__(self, console_dict): @utils.arg('server', metavar='', help='Name or ID of server.') @utils.arg('private_key', metavar='', - help='Private key (used locally to decrypt password) (Optional). ' + help=_('Private key (used locally to decrypt password) (Optional). ' 'When specified, the command displays the clear (decrypted) VM ' 'password. When not specified, the ciphered VM password is ' - 'displayed.', + 'displayed.'), nargs='?', default=None) def do_get_password(cs, args): @@ -1904,7 +1917,7 @@ def do_get_password(cs, args): print(data) -@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('server', metavar='', help=_('Name or ID of server.')) def do_clear_password(cs, args): """Clear password for a server.""" server = _find_server(cs, args.server) @@ -1915,11 +1928,11 @@ def _print_floating_ip_list(floating_ips): utils.print_list(floating_ips, ['Ip', 'Server Id', 'Fixed Ip', 'Pool']) -@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('server', metavar='', help=_('Name or ID of server.')) @utils.arg('--length', metavar='', default=None, - help='Length in lines to tail.') + help=_('Length in lines to tail.')) def do_console_log(cs, args): """Get console log output of a server.""" server = _find_server(cs, args.server) @@ -1927,12 +1940,12 @@ def do_console_log(cs, args): print(data) -@utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg('address', metavar='
', help='IP Address.') +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('address', metavar='
', help=_('IP Address.')) @utils.arg('--fixed-address', metavar='', default=None, - help='Fixed IP Address to associate with.') + help=_('Fixed IP Address to associate with.')) def do_add_floating_ip(cs, args): """DEPRECATED, use floating-ip-associate instead.""" _associate_floating_ip(cs, args) @@ -1954,8 +1967,8 @@ def _associate_floating_ip(cs, args): server.add_floating_ip(args.address, args.fixed_address) -@utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg('address', metavar='
', help='IP Address.') +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('address', metavar='
', help=_('IP Address.')) def do_remove_floating_ip(cs, args): """DEPRECATED, use floating-ip-disassociate instead.""" _disassociate_floating_ip(cs, args) @@ -1973,23 +1986,23 @@ def _disassociate_floating_ip(cs, args): server.remove_floating_ip(args.address) -@utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg('secgroup', metavar='', help='Name of Security Group.') +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('secgroup', metavar='', help=_('Name of Security Group.')) def do_add_secgroup(cs, args): """Add a Security Group to a server.""" server = _find_server(cs, args.server) server.add_security_group(args.secgroup) -@utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg('secgroup', metavar='', help='Name of Security Group.') +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('secgroup', metavar='', help=_('Name of Security Group.')) def do_remove_secgroup(cs, args): """Remove a Security Group from a server.""" server = _find_server(cs, args.server) server.remove_security_group(args.secgroup) -@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('server', metavar='', help=_('Name or ID of server.')) def do_list_secgroup(cs, args): """List Security Group(s) of a server.""" server = _find_server(cs, args.server) @@ -1999,7 +2012,7 @@ def do_list_secgroup(cs, args): @utils.arg('pool', metavar='', - help='Name of Floating IP Pool. (Optional)', + help=_('Name of Floating IP Pool. (Optional)'), nargs='?', default=None) def do_floating_ip_create(cs, args): @@ -2007,14 +2020,15 @@ def do_floating_ip_create(cs, args): _print_floating_ip_list([cs.floating_ips.create(pool=args.pool)]) -@utils.arg('address', metavar='
', help='IP of Floating Ip.') +@utils.arg('address', metavar='
', help=_('IP of Floating Ip.')) def do_floating_ip_delete(cs, args): """De-allocate a floating IP.""" floating_ips = cs.floating_ips.list() for floating_ip in floating_ips: if floating_ip.ip == args.address: return cs.floating_ips.delete(floating_ip.id) - raise exceptions.CommandError("Floating ip %s not found." % args.address) + raise exceptions.CommandError(_("Floating ip %s not found.") % + args.address) def do_floating_ip_list(cs, _args): @@ -2028,7 +2042,7 @@ def do_floating_ip_pool_list(cs, _args): @utils.arg('--host', dest='host', metavar='', default=None, - help='Filter by host') + help=_('Filter by host')) def do_floating_ip_bulk_list(cs, args): """List all floating ips.""" utils.print_list(cs.floating_ips_bulk.list(args.host), ['project_id', @@ -2038,17 +2052,17 @@ def do_floating_ip_bulk_list(cs, args): 'interface']) -@utils.arg('ip_range', metavar='', help='Address range to create') +@utils.arg('ip_range', metavar='', help=_('Address range to create')) @utils.arg('--pool', dest='pool', metavar='', default=None, - help='Pool for new Floating IPs') + help=_('Pool for new Floating IPs')) @utils.arg('--interface', metavar='', default=None, - help='Interface for new Floating IPs') + help=_('Interface for new Floating IPs')) def do_floating_ip_bulk_create(cs, args): """Bulk create floating ips by range.""" cs.floating_ips_bulk.create(args.ip_range, args.pool, args.interface) -@utils.arg('ip_range', metavar='', help='Address range to delete') +@utils.arg('ip_range', metavar='', help=_('Address range to delete')) def do_floating_ip_bulk_delete(cs, args): """Bulk delete floating ips by range.""" cs.floating_ips_bulk.delete(args.ip_range) @@ -2069,14 +2083,14 @@ def do_dns_domains(cs, args): _print_domain_list(domains) -@utils.arg('domain', metavar='', help='DNS domain') -@utils.arg('--ip', metavar='', help='ip address', default=None) -@utils.arg('--name', metavar='', help='DNS name', default=None) +@utils.arg('domain', metavar='', help=_('DNS domain')) +@utils.arg('--ip', metavar='', help=_('ip address'), default=None) +@utils.arg('--name', metavar='', help=_('DNS name'), default=None) def do_dns_list(cs, args): """List current DNS entries for domain and ip or domain and name.""" if not (args.ip or args.name): raise exceptions.CommandError( - "You must specify either --ip or --name") + _("You must specify either --ip or --name")) if args.name: entry = cs.dns_entries.get(args.domain, args.name) _print_dns_list([entry]) @@ -2086,34 +2100,35 @@ def do_dns_list(cs, args): _print_dns_list(entries) -@utils.arg('ip', metavar='', help='ip address') -@utils.arg('name', metavar='', help='DNS name') -@utils.arg('domain', metavar='', help='DNS domain') -@utils.arg('--type', metavar='', help='dns type (e.g. "A")', default='A') +@utils.arg('ip', metavar='', help=_('ip address')) +@utils.arg('name', metavar='', help=_('DNS name')) +@utils.arg('domain', metavar='', help=_('DNS domain')) +@utils.arg('--type', metavar='', help=_('dns type (e.g. "A")'), + default='A') def do_dns_create(cs, args): """Create a DNS entry for domain, name and ip.""" cs.dns_entries.create(args.domain, args.name, args.ip, args.type) -@utils.arg('domain', metavar='', help='DNS domain') -@utils.arg('name', metavar='', help='DNS name') +@utils.arg('domain', metavar='', help=_('DNS domain')) +@utils.arg('name', metavar='', help=_('DNS name')) def do_dns_delete(cs, args): """Delete the specified DNS entry.""" cs.dns_entries.delete(args.domain, args.name) -@utils.arg('domain', metavar='', help='DNS domain') +@utils.arg('domain', metavar='', help=_('DNS domain')) def do_dns_delete_domain(cs, args): """Delete the specified DNS domain.""" cs.dns_domains.delete(args.domain) -@utils.arg('domain', metavar='', help='DNS domain') +@utils.arg('domain', metavar='', help=_('DNS domain')) @utils.arg('--availability-zone', metavar='', default=None, - help='Limit access to this domain to servers ' - 'in the specified availability zone.') + help=_('Limit access to this domain to servers ' + 'in the specified availability zone.')) @utils.arg('--availability_zone', help=argparse.SUPPRESS) def do_dns_create_private_domain(cs, args): @@ -2122,10 +2137,10 @@ def do_dns_create_private_domain(cs, args): args.availability_zone) -@utils.arg('domain', metavar='', help='DNS domain') +@utils.arg('domain', metavar='', help=_('DNS domain')) @utils.arg('--project', metavar='', - help='Limit access to this domain to users ' - 'of the specified project.', + help=_('Limit access to this domain to users ' + 'of the specified project.'), default=None) def do_dns_create_public_domain(cs, args): """Create the specified DNS domain.""" @@ -2176,29 +2191,29 @@ def _get_secgroup(cs, secgroup): s.name = s.name.encode(encoding) if secgroup == s.name: if match_found is not False: - msg = ("Multiple security group matches found for name" - " '%s', use an ID to be more specific." % secgroup) + msg = (_("Multiple security group matches found for name '%s'" + ", use an ID to be more specific.") % secgroup) raise exceptions.NoUniqueMatch(msg) match_found = s if match_found is False: - raise exceptions.CommandError("Secgroup ID or name '%s' not found." + raise exceptions.CommandError(_("Secgroup ID or name '%s' not found.") % secgroup) return match_found @utils.arg('secgroup', metavar='', - help='ID or name of security group.') + help=_('ID or name of security group.')) @utils.arg('ip_proto', metavar='', - help='IP protocol (icmp, tcp, udp).') + help=_('IP protocol (icmp, tcp, udp).')) @utils.arg('from_port', metavar='', - help='Port at start of range.') + help=_('Port at start of range.')) @utils.arg('to_port', metavar='', - help='Port at end of range.') -@utils.arg('cidr', metavar='', help='CIDR for address range.') + help=_('Port at end of range.')) +@utils.arg('cidr', metavar='', help=_('CIDR for address range.')) def do_secgroup_add_rule(cs, args): """Add a rule to a security group.""" secgroup = _get_secgroup(cs, args.secgroup) @@ -2212,17 +2227,17 @@ def do_secgroup_add_rule(cs, args): @utils.arg('secgroup', metavar='', - help='ID or name of security group.') + help=_('ID or name of security group.')) @utils.arg('ip_proto', metavar='', - help='IP protocol (icmp, tcp, udp).') + help=_('IP protocol (icmp, tcp, udp).')) @utils.arg('from_port', metavar='', - help='Port at start of range.') + help=_('Port at start of range.')) @utils.arg('to_port', metavar='', - help='Port at end of range.') -@utils.arg('cidr', metavar='', help='CIDR for address range.') + help=_('Port at end of range.')) +@utils.arg('cidr', metavar='', help=_('CIDR for address range.')) def do_secgroup_delete_rule(cs, args): """Delete a rule from a security group.""" secgroup = _get_secgroup(cs, args.secgroup) @@ -2235,12 +2250,12 @@ def do_secgroup_delete_rule(cs, args): _print_secgroup_rules([rule]) return cs.security_group_rules.delete(rule['id']) - raise exceptions.CommandError("Rule not found") + raise exceptions.CommandError(_("Rule not found")) -@utils.arg('name', metavar='', help='Name of security group.') +@utils.arg('name', metavar='', help=_('Name of security group.')) @utils.arg('description', metavar='', - help='Description of security group.') + help=_('Description of security group.')) def do_secgroup_create(cs, args): """Create a security group.""" secgroup = cs.security_groups.create(args.name, args.description) @@ -2249,10 +2264,10 @@ def do_secgroup_create(cs, args): @utils.arg('secgroup', metavar='', - help='ID or name of security group.') -@utils.arg('name', metavar='', help='Name of security group.') + help=_('ID or name of security group.')) +@utils.arg('name', metavar='', help=_('Name of security group.')) @utils.arg('description', metavar='', - help='Description of security group.') + help=_('Description of security group.')) def do_secgroup_update(cs, args): """Update a security group.""" sg = _get_secgroup(cs, args.secgroup) @@ -2262,7 +2277,7 @@ def do_secgroup_update(cs, args): @utils.arg('secgroup', metavar='', - help='ID or name of security group.') + help=_('ID or name of security group.')) def do_secgroup_delete(cs, args): """Delete a security group.""" secgroup = _get_secgroup(cs, args.secgroup) @@ -2278,7 +2293,7 @@ def do_secgroup_delete(cs, args): const=1, default=int(strutils.bool_from_string( os.environ.get("ALL_TENANTS", 'false'), True)), - help='Display information from all tenants (Admin only).') + help=_('Display information from all tenants (Admin only).')) @utils.arg('--all_tenants', nargs='?', type=int, @@ -2296,7 +2311,7 @@ def do_secgroup_list(cs, args): @utils.arg('secgroup', metavar='', - help='ID or name of security group.') + help=_('ID or name of security group.')) def do_secgroup_list_rules(cs, args): """List rules for a security group.""" secgroup = _get_secgroup(cs, args.secgroup) @@ -2305,19 +2320,19 @@ def do_secgroup_list_rules(cs, args): @utils.arg('secgroup', metavar='', - help='ID or name of security group.') + help=_('ID or name of security group.')) @utils.arg('source_group', metavar='', - help='ID or name of source group.') + help=_('ID or name of source group.')) @utils.arg('ip_proto', metavar='', - help='IP protocol (icmp, tcp, udp).') + help=_('IP protocol (icmp, tcp, udp).')) @utils.arg('from_port', metavar='', - help='Port at start of range.') + help=_('Port at start of range.')) @utils.arg('to_port', metavar='', - help='Port at end of range.') + help=_('Port at end of range.')) def do_secgroup_add_group_rule(cs, args): """Add a source group rule to a security group.""" secgroup = _get_secgroup(cs, args.secgroup) @@ -2327,8 +2342,8 @@ def do_secgroup_add_group_rule(cs, args): if args.ip_proto or args.from_port or args.to_port: if not (args.ip_proto and args.from_port and args.to_port): - raise exceptions.CommandError("ip_proto, from_port, and to_port" - " must be specified together") + raise exceptions.CommandError(_("ip_proto, from_port, and to_port" + " must be specified together")) params['ip_protocol'] = args.ip_proto.upper() params['from_port'] = args.from_port params['to_port'] = args.to_port @@ -2339,19 +2354,19 @@ def do_secgroup_add_group_rule(cs, args): @utils.arg('secgroup', metavar='', - help='ID or name of security group.') + help=_('ID or name of security group.')) @utils.arg('source_group', metavar='', - help='ID or name of source group.') + help=_('ID or name of source group.')) @utils.arg('ip_proto', metavar='', - help='IP protocol (icmp, tcp, udp).') + help=_('IP protocol (icmp, tcp, udp).')) @utils.arg('from_port', metavar='', - help='Port at start of range.') + help=_('Port at start of range.')) @utils.arg('to_port', metavar='', - help='Port at end of range.') + help=_('Port at end of range.')) def do_secgroup_delete_group_rule(cs, args): """Delete a source group rule from a security group.""" secgroup = _get_secgroup(cs, args.secgroup) @@ -2361,8 +2376,8 @@ def do_secgroup_delete_group_rule(cs, args): if args.ip_proto or args.from_port or args.to_port: if not (args.ip_proto and args.from_port and args.to_port): - raise exceptions.CommandError("ip_proto, from_port, and to_port" - " must be specified together") + raise exceptions.CommandError(_("ip_proto, from_port, and to_port" + " must be specified together")) params['ip_protocol'] = args.ip_proto.upper() params['from_port'] = int(args.from_port) params['to_port'] = int(args.to_port) @@ -2376,14 +2391,14 @@ def do_secgroup_delete_group_rule(cs, args): params.get('group_name')): return cs.security_group_rules.delete(rule['id']) - raise exceptions.CommandError("Rule not found") + raise exceptions.CommandError(_("Rule not found")) -@utils.arg('name', metavar='', help='Name of key.') +@utils.arg('name', metavar='', help=_('Name of key.')) @utils.arg('--pub-key', metavar='', default=None, - help='Path to a public ssh key.') + help=_('Path to a public ssh key.')) @utils.arg('--pub_key', help=argparse.SUPPRESS) def do_keypair_add(cs, args): @@ -2396,8 +2411,9 @@ def do_keypair_add(cs, args): with open(os.path.expanduser(pub_key)) as f: pub_key = f.read() except IOError as e: - raise exceptions.CommandError("Can't open or read '%s': %s" % - (pub_key, e)) + raise exceptions.CommandError(_("Can't open or read '%(key)s': " + "%(exc)s") % {'key': pub_key, + 'exc': e}) keypair = cs.keypairs.create(name, pub_key) @@ -2406,7 +2422,7 @@ def do_keypair_add(cs, args): print(private_key) -@utils.arg('name', metavar='', help='Keypair name to delete.') +@utils.arg('name', metavar='', help=_('Keypair name to delete.')) def do_keypair_delete(cs, args): """Delete keypair given by its name.""" name = args.name @@ -2424,12 +2440,12 @@ def _print_keypair(keypair): kp = keypair._info.copy() pk = kp.pop('public_key') utils.print_dict(kp) - print("Public key: %s" % pk) + print(_("Public key: %s") % pk) @utils.arg('keypair', metavar='', - help="Name or ID of keypair") + help=_("Name or ID of keypair")) def do_keypair_show(cs, args): """Show details about the given keypair.""" keypair = cs.keypairs.get(args.keypair) @@ -2441,12 +2457,12 @@ def do_keypair_show(cs, args): dest='tenant', metavar='', nargs='?', - help='Display information from single tenant (Admin only).') + help=_('Display information from single tenant (Admin only).')) @utils.arg('--reserved', dest='reserved', action='store_true', default=False, - help='Include reservations count.') + help=_('Include reservations count.')) def do_absolute_limits(cs, args): """Print a list of absolute limits for a user""" limits = cs.limits.get(args.reserved, args.tenant).absolute @@ -2462,10 +2478,11 @@ def do_rate_limits(cs, args): @utils.arg('--start', metavar='', - help='Usage range start date ex 2012-01-20 (default: 4 weeks ago)', + help=_('Usage range start date ex 2012-01-20 (default: 4 weeks ' + 'ago)'), default=None) @utils.arg('--end', metavar='', - help='Usage range end date, ex 2012-01-20 (default: tomorrow) ', + help=_('Usage range end date, ex 2012-01-20 (default: tomorrow)'), default=None) def do_usage_list(cs, args): """List usage data for all tenants.""" @@ -2496,8 +2513,9 @@ def simplify_usage(u): usage_list = cs.usage.list(start, end, detailed=True) - print("Usage from %s to %s:" % (start.strftime(dateformat), - end.strftime(dateformat))) + print(_("Usage from %(start)s to %(end)s:") % + {'start': start.strftime(dateformat), + 'end': end.strftime(dateformat)}) for usage in usage_list: simplify_usage(usage) @@ -2506,14 +2524,15 @@ def simplify_usage(u): @utils.arg('--start', metavar='', - help='Usage range start date ex 2012-01-20 (default: 4 weeks ago)', + help=_('Usage range start date ex 2012-01-20 (default: 4 weeks ' + 'ago)'), default=None) @utils.arg('--end', metavar='', - help='Usage range end date, ex 2012-01-20 (default: tomorrow) ', + help=_('Usage range end date, ex 2012-01-20 (default: tomorrow)'), default=None) @utils.arg('--tenant', metavar='', default=None, - help='UUID or name of tenant to get usage for.') + help=_('UUID or name of tenant to get usage for.')) def do_usage(cs, args): """Show usage data for a single tenant.""" dateformat = "%Y-%m-%d" @@ -2544,35 +2563,36 @@ def simplify_usage(u): else: usage = cs.usage.get(cs.client.tenant_id, start, end) - print("Usage from %s to %s:" % (start.strftime(dateformat), - end.strftime(dateformat))) + print(_("Usage from %(start)s to %(end)s:") % + {'start': start.strftime(dateformat), + 'end': end.strftime(dateformat)}) if getattr(usage, 'total_vcpus_usage', None): simplify_usage(usage) utils.print_list([usage], rows) else: - print('None') + print(_('None')) @utils.arg('pk_filename', metavar='', nargs='?', default='pk.pem', - help='Filename for the private key [Default: pk.pem]') + help=_('Filename for the private key [Default: pk.pem]')) @utils.arg('cert_filename', metavar='', nargs='?', default='cert.pem', - help='Filename for the X.509 certificate [Default: cert.pem]') + help=_('Filename for the X.509 certificate [Default: cert.pem]')) def do_x509_create_cert(cs, args): """Create x509 cert for a user in tenant.""" if os.path.exists(args.pk_filename): - raise exceptions.CommandError("Unable to write privatekey - %s exists." - % args.pk_filename) + raise exceptions.CommandError(_("Unable to write privatekey - %s " + "exists.") % args.pk_filename) if os.path.exists(args.cert_filename): - raise exceptions.CommandError("Unable to write x509 cert - %s exists." - % args.cert_filename) + raise exceptions.CommandError(_("Unable to write x509 cert - %s " + "exists.") % args.cert_filename) certs = cs.certs.create() @@ -2580,34 +2600,34 @@ def do_x509_create_cert(cs, args): old_umask = os.umask(0o377) with open(args.pk_filename, 'w') as private_key: private_key.write(certs.private_key) - print("Wrote private key to %s" % args.pk_filename) + print(_("Wrote private key to %s") % args.pk_filename) finally: os.umask(old_umask) with open(args.cert_filename, 'w') as cert: cert.write(certs.data) - print("Wrote x509 certificate to %s" % args.cert_filename) + print(_("Wrote x509 certificate to %s") % args.cert_filename) @utils.arg('filename', metavar='', nargs='?', default='cacert.pem', - help='Filename to write the x509 root cert.') + help=_('Filename to write the x509 root cert.')) def do_x509_get_root_cert(cs, args): """Fetch the x509 root cert.""" if os.path.exists(args.filename): - raise exceptions.CommandError("Unable to write x509 root cert - \ - %s exists." % args.filename) + raise exceptions.CommandError(_("Unable to write x509 root cert - \ + %s exists.") % args.filename) with open(args.filename, 'w') as cert: cacert = cs.certs.get() cert.write(cacert.data) - print("Wrote x509 root cert to %s" % args.filename) + print(_("Wrote x509 root cert to %s") % args.filename) @utils.arg('--hypervisor', metavar='', default=None, - help='type of hypervisor.') + help=_('type of hypervisor.')) def do_agent_list(cs, args): """List all builds.""" result = cs.agents.list(args.hypervisor) @@ -2616,14 +2636,14 @@ def do_agent_list(cs, args): utils.print_list(result, columns) -@utils.arg('os', metavar='', help='type of os.') +@utils.arg('os', metavar='', help=_('type of os.')) @utils.arg('architecture', metavar='', - help='type of architecture') -@utils.arg('version', metavar='', help='version') -@utils.arg('url', metavar='', help='url') -@utils.arg('md5hash', metavar='', help='md5 hash') + help=_('type of architecture')) +@utils.arg('version', metavar='', help=_('version')) +@utils.arg('url', metavar='', help=_('url')) +@utils.arg('md5hash', metavar='', help=_('md5 hash')) @utils.arg('hypervisor', metavar='', default='xen', - help='type of hypervisor.') + help=_('type of hypervisor.')) def do_agent_create(cs, args): """Create new agent build.""" result = cs.agents.create(args.os, args.architecture, @@ -2632,16 +2652,16 @@ def do_agent_create(cs, args): utils.print_dict(result._info.copy()) -@utils.arg('id', metavar='', help='id of the agent-build') +@utils.arg('id', metavar='', help=_('id of the agent-build')) def do_agent_delete(cs, args): """Delete existing agent build.""" cs.agents.delete(args.id) -@utils.arg('id', metavar='', help='id of the agent-build') -@utils.arg('version', metavar='', help='version') -@utils.arg('url', metavar='', help='url') -@utils.arg('md5hash', metavar='', help='md5hash') +@utils.arg('id', metavar='', help=_('id of the agent-build')) +@utils.arg('version', metavar='', help=_('version')) +@utils.arg('url', metavar='', help=_('url')) +@utils.arg('md5hash', metavar='', help=_('md5hash')) def do_agent_modify(cs, args): """Modify existing agent build.""" result = cs.agents.update(args.id, args.version, @@ -2661,12 +2681,12 @@ def do_aggregate_list(cs, args): utils.print_list(aggregates, columns) -@utils.arg('name', metavar='', help='Name of aggregate.') +@utils.arg('name', metavar='', help=_('Name of aggregate.')) @utils.arg('availability_zone', metavar='', default=None, nargs='?', - help='The availability zone of the aggregate (optional).') + help=_('The availability zone of the aggregate (optional).')) def do_aggregate_create(cs, args): """Create a new aggregate with the specified details.""" aggregate = cs.aggregates.create(args.name, args.availability_zone) @@ -2674,22 +2694,22 @@ def do_aggregate_create(cs, args): @utils.arg('aggregate', metavar='', - help='Name or ID of aggregate to delete.') + help=_('Name or ID of aggregate to delete.')) def do_aggregate_delete(cs, args): """Delete the aggregate.""" aggregate = _find_aggregate(cs, args.aggregate) cs.aggregates.delete(aggregate) - print("Aggregate %s has been successfully deleted." % aggregate.id) + print(_("Aggregate %s has been successfully deleted.") % aggregate.id) @utils.arg('aggregate', metavar='', - help='Name or ID of aggregate to update.') -@utils.arg('name', metavar='', help='Name of aggregate.') + help=_('Name or ID of aggregate to update.')) +@utils.arg('name', metavar='', help=_('Name of aggregate.')) @utils.arg('availability_zone', metavar='', nargs='?', default=None, - help='The availability zone of the aggregate.') + help=_('The availability zone of the aggregate.')) def do_aggregate_update(cs, args): """Update the aggregate's name and optionally availability zone.""" aggregate = _find_aggregate(cs, args.aggregate) @@ -2698,52 +2718,58 @@ def do_aggregate_update(cs, args): updates["availability_zone"] = args.availability_zone aggregate = cs.aggregates.update(aggregate.id, updates) - print("Aggregate %s has been successfully updated." % aggregate.id) + print(_("Aggregate %s has been successfully updated.") % aggregate.id) _print_aggregate_details(aggregate) @utils.arg('aggregate', metavar='', - help='Name or ID of aggregate to update.') + help=_('Name or ID of aggregate to update.')) @utils.arg('metadata', metavar='', nargs='+', action='append', default=[], - help='Metadata to add/update to aggregate') + help=_('Metadata to add/update to aggregate')) def do_aggregate_set_metadata(cs, args): """Update the metadata associated with the aggregate.""" aggregate = _find_aggregate(cs, args.aggregate) metadata = _extract_metadata(args) aggregate = cs.aggregates.set_metadata(aggregate.id, metadata) - print("Metadata has been successfully updated for aggregate %s." % + print(_("Metadata has been successfully updated for aggregate %s.") % aggregate.id) _print_aggregate_details(aggregate) -@utils.arg('aggregate', metavar='', help='Name or ID of aggregate.') -@utils.arg('host', metavar='', help='The host to add to the aggregate.') +@utils.arg('aggregate', metavar='', + help=_('Name or ID of aggregate.')) +@utils.arg('host', metavar='', + help=_('The host to add to the aggregate.')) def do_aggregate_add_host(cs, args): """Add the host to the specified aggregate.""" aggregate = _find_aggregate(cs, args.aggregate) aggregate = cs.aggregates.add_host(aggregate.id, args.host) - print("Host %s has been successfully added for aggregate %s " % - (args.host, aggregate.id)) + print(_("Host %(host)s has been successfully added for aggregate " + "%(aggregate_id)s ") % {'host': args.host, + 'aggregate_id': aggregate.id}) _print_aggregate_details(aggregate) -@utils.arg('aggregate', metavar='', help='Name or ID of aggregate.') +@utils.arg('aggregate', metavar='', + help=_('Name or ID of aggregate.')) @utils.arg('host', metavar='', - help='The host to remove from the aggregate.') + help=_('The host to remove from the aggregate.')) def do_aggregate_remove_host(cs, args): """Remove the specified host from the specified aggregate.""" aggregate = _find_aggregate(cs, args.aggregate) aggregate = cs.aggregates.remove_host(aggregate.id, args.host) - print("Host %s has been successfully removed from aggregate %s " % - (args.host, aggregate.id)) + print(_("Host %(host)s has been successfully removed from aggregate " + "%(aggregate_id)s ") % {'host': args.host, + 'aggregate_id': aggregate.id}) _print_aggregate_details(aggregate) -@utils.arg('aggregate', metavar='', help='Name or ID of aggregate.') +@utils.arg('aggregate', metavar='', + help=_('Name or ID of aggregate.')) def do_aggregate_details(cs, args): """Show details of the specified aggregate.""" aggregate = _find_aggregate(cs, args.aggregate) @@ -2766,15 +2792,14 @@ def parser_hosts(fields): utils.print_list([aggregate], columns, formatters=formatters) -@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('server', metavar='', help=_('Name or ID of server.')) @utils.arg('host', metavar='', default=None, nargs='?', - help='destination host name.') + help=_('destination host name.')) @utils.arg('--block-migrate', action='store_true', dest='block_migrate', default=False, - help='True in case of block_migration.\ - (Default=False:live_migration)') + help=_('True in case of block_migration. (Default=False:live_migration)')) @utils.arg('--block_migrate', action='store_true', help=argparse.SUPPRESS) @@ -2782,7 +2807,7 @@ def parser_hosts(fields): action='store_true', dest='disk_over_commit', default=False, - help='Allow overcommit.(Default=False)') + help=_('Allow overcommit.(Default=False)')) @utils.arg('--disk_over_commit', action='store_true', help=argparse.SUPPRESS) @@ -2793,26 +2818,26 @@ def do_live_migration(cs, args): args.disk_over_commit) -@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('server', metavar='', help=_('Name or ID of server.')) @utils.arg('--active', action='store_const', dest='state', default='error', const='active', - help='Request the server be reset to "active" state instead ' - 'of "error" state (the default).') + help=_('Request the server be reset to "active" state instead ' + 'of "error" state (the default).')) def do_reset_state(cs, args): """Reset the state of a server.""" _find_server(cs, args.server).reset_state(args.state) -@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('server', metavar='', help=_('Name or ID of server.')) def do_reset_network(cs, args): """Reset network of a server.""" _find_server(cs, args.server).reset_network() @utils.arg('--host', metavar='', default=None, - help='Name of host.') + help=_('Name of host.')) @utils.arg('--binary', metavar='', default=None, - help='Service binary.') + help=_('Service binary.')) def do_service_list(cs, args): """Show a list of all running services. Filter by host & binary.""" result = cs.services.list(host=args.host, binary=args.binary) @@ -2824,18 +2849,18 @@ def do_service_list(cs, args): utils.print_list(result, columns) -@utils.arg('host', metavar='', help='Name of host.') -@utils.arg('binary', metavar='', help='Service binary.') +@utils.arg('host', metavar='', help=_('Name of host.')) +@utils.arg('binary', metavar='', help=_('Service binary.')) def do_service_enable(cs, args): """Enable the service.""" result = cs.services.enable(args.host, args.binary) utils.print_list([result], ['Host', 'Binary', 'Status']) -@utils.arg('host', metavar='', help='Name of host.') -@utils.arg('binary', metavar='', help='Service binary.') +@utils.arg('host', metavar='', help=_('Name of host.')) +@utils.arg('binary', metavar='', help=_('Service binary.')) @utils.arg('--reason', metavar='', - help='Reason for disabling service.') + help=_('Reason for disabling service.')) def do_service_disable(cs, args): """Disable the service.""" if args.reason: @@ -2848,26 +2873,26 @@ def do_service_disable(cs, args): utils.print_list([result], ['Host', 'Binary', 'Status']) -@utils.arg('fixed_ip', metavar='', help='Fixed IP Address.') +@utils.arg('fixed_ip', metavar='', help=_('Fixed IP Address.')) def do_fixed_ip_get(cs, args): """Retrieve info on a fixed ip.""" result = cs.fixed_ips.get(args.fixed_ip) utils.print_list([result], ['address', 'cidr', 'hostname', 'host']) -@utils.arg('fixed_ip', metavar='', help='Fixed IP Address.') +@utils.arg('fixed_ip', metavar='', help=_('Fixed IP Address.')) def do_fixed_ip_reserve(cs, args): """Reserve a fixed IP.""" cs.fixed_ips.reserve(args.fixed_ip) -@utils.arg('fixed_ip', metavar='', help='Fixed IP Address.') +@utils.arg('fixed_ip', metavar='', help=_('Fixed IP Address.')) def do_fixed_ip_unreserve(cs, args): """Unreserve a fixed IP.""" cs.fixed_ips.unreserve(args.fixed_ip) -@utils.arg('host', metavar='', help='Name of host.') +@utils.arg('host', metavar='', help=_('Name of host.')) def do_host_describe(cs, args): """Describe a specific host.""" result = cs.hosts.get(args.host) @@ -2876,8 +2901,8 @@ def do_host_describe(cs, args): @utils.arg('--zone', metavar='', default=None, - help='Filters the list, returning only those ' - 'hosts in the availability zone .') + help=_('Filters the list, returning only those ' + 'hosts in the availability zone .')) def do_host_list(cs, args): """List all hosts by service.""" columns = ["host_name", "service", "zone"] @@ -2887,12 +2912,12 @@ def do_host_list(cs, args): @utils.arg('host', metavar='', help='Name of host.') @utils.arg('--status', metavar='', default=None, dest='status', - help='Either enable or disable a host.') + help=_('Either enable or disable a host.')) @utils.arg('--maintenance', metavar='', default=None, dest='maintenance', - help='Either put or resume host to/from maintenance.') + help=_('Either put or resume host to/from maintenance.')) def do_host_update(cs, args): """Update host settings.""" updates = {} @@ -2910,7 +2935,7 @@ def do_host_update(cs, args): @utils.arg('host', metavar='', help='Name of host.') @utils.arg('--action', metavar='', dest='action', choices=['startup', 'shutdown', 'reboot'], - help='A power action: startup, reboot, or shutdown.') + help=_('A power action: startup, reboot, or shutdown.')) def do_host_action(cs, args): """Perform a power action on a host.""" result = cs.hosts.host_action(args.host, args.action) @@ -2923,7 +2948,7 @@ def _find_hypervisor(cs, hypervisor): @utils.arg('--matching', metavar='', default=None, - help='List hypervisors matching the given .') + help=_('List hypervisors matching the given .')) def do_hypervisor_list(cs, args): """List hypervisors.""" columns = ['ID', 'Hypervisor hostname'] @@ -2936,7 +2961,7 @@ def do_hypervisor_list(cs, args): @utils.arg('hostname', metavar='', - help='The hypervisor hostname (or pattern) to search for.') + help=_('The hypervisor hostname (or pattern) to search for.')) def do_hypervisor_servers(cs, args): """List servers belonging to specific hypervisors.""" hypers = cs.hypervisors.search(args.hostname, servers=True) @@ -2964,7 +2989,7 @@ def __init__(self, **kwargs): @utils.arg('hypervisor', metavar='', - help='Name or ID of the hypervisor to show the details of.') + help=_('Name or ID of the hypervisor to show the details of.')) def do_hypervisor_show(cs, args): """Display the details of the specified hypervisor.""" hyper = _find_hypervisor(cs, args.hypervisor) @@ -2973,7 +2998,7 @@ def do_hypervisor_show(cs, args): @utils.arg('hypervisor', metavar='', - help='Name or ID of the hypervisor to show the uptime of.') + help=_('Name or ID of the hypervisor to show the uptime of.')) def do_hypervisor_uptime(cs, args): """Display the uptime of the specified hypervisor.""" hyper = _find_hypervisor(cs, args.hypervisor) @@ -3006,7 +3031,7 @@ def do_endpoints(cs, _args): @utils.arg('--wrap', dest='wrap', metavar='', default=64, - help='wrap PKI tokens to a specified length, or 0 to disable') + help=_('wrap PKI tokens to a specified length, or 0 to disable')) def do_credentials(cs, _args): """Show user credentials returned from auth.""" ensure_service_catalog_present(cs) @@ -3016,35 +3041,36 @@ def do_credentials(cs, _args): utils.print_dict(catalog['access']['token'], "Token", wrap=int(_args.wrap)) -@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('server', metavar='', help=_('Name or ID of server.')) @utils.arg('--port', dest='port', action='store', type=int, default=22, - help='Optional flag to indicate which port to use for ssh. ' - '(Default=22)') + help=_('Optional flag to indicate which port to use for ssh. ' + '(Default=22)')) @utils.arg('--private', dest='private', action='store_true', default=False, - help='Optional flag to indicate whether to only use private address ' + help=_('Optional flag to indicate whether to only use private address ' 'attached to an instance. (Default=False). If no public address is ' - 'found try private address') + 'found try private address')) @utils.arg('--ipv6', dest='ipv6', action='store_true', default=False, - help='Optional flag to indicate whether to use an IPv6 address ' - 'attached to a server. (Defaults to IPv4 address)') -@utils.arg('--login', metavar='', help='Login to use.', default="root") + help=_('Optional flag to indicate whether to use an IPv6 address ' + 'attached to a server. (Defaults to IPv4 address)')) +@utils.arg('--login', metavar='', help=_('Login to use.'), + default="root") @utils.arg('-i', '--identity', dest='identity', - help='Private key file, same as the -i option to the ssh command.', + help=_('Private key file, same as the -i option to the ssh command.'), default='') @utils.arg('--extra-opts', dest='extra', - help='Extra options to pass to ssh. see: man ssh', + help=_('Extra options to pass to ssh. see: man ssh'), default='') def do_ssh(cs, args): """SSH into a server.""" @@ -3062,8 +3088,8 @@ def do_ssh(cs, args): address_type = "private" if address_type not in addresses: - print("ERROR: No %s addresses found for '%s'." % (address_type, - args.server)) + print(_("ERROR: No %(addr_type)s addresses found for '%(server)s'.") % + {'addr_type': address_type, 'server': args.server}) return ip_address = None @@ -3080,8 +3106,8 @@ def do_ssh(cs, args): args.extra)) else: pretty_version = "IPv%d" % version - print("ERROR: No %s %s address found." % (address_type, - pretty_version)) + print(_("ERROR: No %(addr_type)s %(pretty_version)s address found.") % + {'addr_type': address_type, 'pretty_version': pretty_version}) return @@ -3131,11 +3157,11 @@ def _quota_update(manager, identifier, args): @utils.arg('--tenant', metavar='', default=None, - help='ID of tenant to list the quotas for.') + help=_('ID of tenant to list the quotas for.')) @utils.arg('--user', metavar='', default=None, - help='ID of user to list the quotas for.') + help=_('ID of user to list the quotas for.')) def do_quota_show(cs, args): """List the quotas for a tenant/user.""" @@ -3148,7 +3174,7 @@ def do_quota_show(cs, args): @utils.arg('--tenant', metavar='', default=None, - help='ID of tenant to list the default quotas for.') + help=_('ID of tenant to list the default quotas for.')) def do_quota_defaults(cs, args): """List the default quotas for a tenant.""" @@ -3160,28 +3186,28 @@ def do_quota_defaults(cs, args): @utils.arg('tenant', metavar='', - help='ID of tenant to set the quotas for.') + help=_('ID of tenant to set the quotas for.')) @utils.arg('--user', metavar='', default=None, - help='ID of user to set the quotas for.') + help=_('ID of user to set the quotas for.')) @utils.arg('--instances', metavar='', type=int, default=None, - help='New value for the "instances" quota.') + help=_('New value for the "instances" quota.')) @utils.arg('--cores', metavar='', type=int, default=None, - help='New value for the "cores" quota.') + help=_('New value for the "cores" quota.')) @utils.arg('--ram', metavar='', type=int, default=None, - help='New value for the "ram" quota.') + help=_('New value for the "ram" quota.')) @utils.arg('--floating-ips', metavar='', type=int, default=None, - help='New value for the "floating-ips" quota.') + help=_('New value for the "floating-ips" quota.')) @utils.arg('--floating_ips', type=int, help=argparse.SUPPRESS) @@ -3189,12 +3215,12 @@ def do_quota_defaults(cs, args): metavar='', type=int, default=None, - help='New value for the "fixed-ips" quota.') + help=_('New value for the "fixed-ips" quota.')) @utils.arg('--metadata-items', metavar='', type=int, default=None, - help='New value for the "metadata-items" quota.') + help=_('New value for the "metadata-items" quota.')) @utils.arg('--metadata_items', type=int, help=argparse.SUPPRESS) @@ -3202,7 +3228,7 @@ def do_quota_defaults(cs, args): metavar='', type=int, default=None, - help='New value for the "injected-files" quota.') + help=_('New value for the "injected-files" quota.')) @utils.arg('--injected_files', type=int, help=argparse.SUPPRESS) @@ -3210,7 +3236,7 @@ def do_quota_defaults(cs, args): metavar='', type=int, default=None, - help='New value for the "injected-file-content-bytes" quota.') + help=_('New value for the "injected-file-content-bytes" quota.')) @utils.arg('--injected_file_content_bytes', type=int, help=argparse.SUPPRESS) @@ -3218,28 +3244,28 @@ def do_quota_defaults(cs, args): metavar='', type=int, default=None, - help='New value for the "injected-file-path-bytes" quota.') + help=_('New value for the "injected-file-path-bytes" quota.')) @utils.arg('--key-pairs', metavar='', type=int, default=None, - help='New value for the "key-pairs" quota.') + help=_('New value for the "key-pairs" quota.')) @utils.arg('--security-groups', metavar='', type=int, default=None, - help='New value for the "security-groups" quota.') + help=_('New value for the "security-groups" quota.')) @utils.arg('--security-group-rules', metavar='', type=int, default=None, - help='New value for the "security-group-rules" quota.') + help=_('New value for the "security-group-rules" quota.')) @utils.arg('--force', dest='force', action="store_true", default=None, - help='Whether force update the quota even if the already used' - ' and reserved exceeds the new quota') + help=_('Whether force update the quota even if the already used' + ' and reserved exceeds the new quota')) def do_quota_update(cs, args): """Update the quotas for a tenant/user.""" @@ -3248,10 +3274,10 @@ def do_quota_update(cs, args): @utils.arg('--tenant', metavar='', - help='ID of tenant to delete quota for.') + help=_('ID of tenant to delete quota for.')) @utils.arg('--user', metavar='', - help='ID of user to delete quota for.') + help=_('ID of user to delete quota for.')) def do_quota_delete(cs, args): """Delete quota for a tenant/user so their quota will Revert back to default. @@ -3262,7 +3288,7 @@ def do_quota_delete(cs, args): @utils.arg('class_name', metavar='', - help='Name of quota class to list the quotas for.') + help=_('Name of quota class to list the quotas for.')) def do_quota_class_show(cs, args): """List the quotas for a quota class.""" @@ -3271,24 +3297,24 @@ def do_quota_class_show(cs, args): @utils.arg('class_name', metavar='', - help='Name of quota class to set the quotas for.') + help=_('Name of quota class to set the quotas for.')) @utils.arg('--instances', metavar='', type=int, default=None, - help='New value for the "instances" quota.') + help=_('New value for the "instances" quota.')) @utils.arg('--cores', metavar='', type=int, default=None, - help='New value for the "cores" quota.') + help=_('New value for the "cores" quota.')) @utils.arg('--ram', metavar='', type=int, default=None, - help='New value for the "ram" quota.') + help=_('New value for the "ram" quota.')) @utils.arg('--floating-ips', metavar='', type=int, default=None, - help='New value for the "floating-ips" quota.') + help=_('New value for the "floating-ips" quota.')) @utils.arg('--floating_ips', type=int, help=argparse.SUPPRESS) @@ -3296,7 +3322,7 @@ def do_quota_class_show(cs, args): metavar='', type=int, default=None, - help='New value for the "metadata-items" quota.') + help=_('New value for the "metadata-items" quota.')) @utils.arg('--metadata_items', type=int, help=argparse.SUPPRESS) @@ -3304,7 +3330,7 @@ def do_quota_class_show(cs, args): metavar='', type=int, default=None, - help='New value for the "injected-files" quota.') + help=_('New value for the "injected-files" quota.')) @utils.arg('--injected_files', type=int, help=argparse.SUPPRESS) @@ -3312,7 +3338,7 @@ def do_quota_class_show(cs, args): metavar='', type=int, default=None, - help='New value for the "injected-file-content-bytes" quota.') + help=_('New value for the "injected-file-content-bytes" quota.')) @utils.arg('--injected_file_content_bytes', type=int, help=argparse.SUPPRESS) @@ -3320,41 +3346,41 @@ def do_quota_class_show(cs, args): metavar='', type=int, default=None, - help='New value for the "injected-file-path-bytes" quota.') + help=_('New value for the "injected-file-path-bytes" quota.')) @utils.arg('--key-pairs', metavar='', type=int, default=None, - help='New value for the "key-pairs" quota.') + help=_('New value for the "key-pairs" quota.')) @utils.arg('--security-groups', metavar='', type=int, default=None, - help='New value for the "security-groups" quota.') + help=_('New value for the "security-groups" quota.')) @utils.arg('--security-group-rules', metavar='', type=int, default=None, - help='New value for the "security-group-rules" quota.') + help=_('New value for the "security-group-rules" quota.')) def do_quota_class_update(cs, args): """Update the quotas for a quota class.""" _quota_update(cs.quota_classes, args.class_name, args) -@utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg('host', metavar='', help='Name or ID of target host.') +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('host', metavar='', help=_('Name or ID of target host.')) @utils.arg('--password', dest='password', metavar='', default=None, - help="Set the provided password on the evacuated server. Not applicable " - "with on-shared-storage flag") + help=_("Set the provided password on the evacuated server. Not applicable " + "with on-shared-storage flag")) @utils.arg('--on-shared-storage', dest='on_shared_storage', action="store_true", default=False, - help='Specifies whether server files are located on shared storage') + help=_('Specifies whether server files are located on shared storage')) def do_evacuate(cs, args): """Evacuate server from failed host to specified one.""" server = _find_server(cs, args.server) @@ -3379,7 +3405,7 @@ def __init__(self, interface): utils.print_list([FormattedInterface(i) for i in interfaces], columns) -@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('server', metavar='', help=_('Name or ID of server.')) def do_interface_list(cs, args): """List interfaces attached to a server.""" server = _find_server(cs, args.server) @@ -3389,11 +3415,12 @@ def do_interface_list(cs, args): _print_interfaces(res) -@utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg('--port-id', metavar='', help='Port ID.', dest="port_id") -@utils.arg('--net-id', metavar='', help='Network ID', +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('--port-id', metavar='', help=_('Port ID.'), + dest="port_id") +@utils.arg('--net-id', metavar='', help=_('Network ID'), default=None, dest="net_id") -@utils.arg('--fixed-ip', metavar='', help='Requested fixed IP.', +@utils.arg('--fixed-ip', metavar='', help=_('Requested fixed IP.'), default=None, dest="fixed_ip") def do_interface_attach(cs, args): """Attach a network interface to a server.""" @@ -3404,8 +3431,8 @@ def do_interface_attach(cs, args): utils.print_dict(res) -@utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg('port_id', metavar='', help='Port ID.') +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('port_id', metavar='', help=_('Port ID.')) def do_interface_detach(cs, args): """Detach a network interface from a server.""" server = _find_server(cs, args.server) From aec2b6172a2ad78a80a1346f87cdb6b29b7acc12 Mon Sep 17 00:00:00 2001 From: Haiwei Xu Date: Tue, 18 Feb 2014 04:09:44 +0900 Subject: [PATCH 0418/1705] Fix element miss in client request body Currently the client sends a request with wrong element name 'os-multiple-create:reservation_id' in the body. The correct element should be 'os-multiple-create:return_reservation_id'. The information can be got in this website. https://wiki.openstack.org/wiki/NovaAPIv2tov3 Change-Id: I7d2f006ef6dbbcd6b41c8adbeee0c0e048d35448 Closes-Bug: #1281342 --- novaclient/tests/v3/test_servers.py | 20 ++++++++++++++++++++ novaclient/v3/servers.py | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/novaclient/tests/v3/test_servers.py b/novaclient/tests/v3/test_servers.py index 9d0abcd62..b25c33801 100644 --- a/novaclient/tests/v3/test_servers.py +++ b/novaclient/tests/v3/test_servers.py @@ -121,6 +121,26 @@ def test_create_server_userdata_utf8(self): cs.assert_called('POST', '/servers') self.assertIsInstance(s, servers.Server) + def test_create_server_return_reservation_id(self): + s = cs.servers.create( + name="My server", + image=1, + flavor=1, + reservation_id=True + ) + expected_body = { + 'server': { + 'name': 'My server', + 'image_ref': '1', + 'flavor_ref': '1', + 'os-multiple-create:min_count': 1, + 'os-multiple-create:max_count': 1, + 'os-multiple-create:return_reservation_id': True, + } + } + cs.assert_called('POST', '/servers', expected_body) + self.assertIsInstance(s, servers.Server) + def test_update_server(self): s = cs.servers.get(1234) diff --git a/novaclient/v3/servers.py b/novaclient/v3/servers.py index b097ffe65..eba0f7275 100644 --- a/novaclient/v3/servers.py +++ b/novaclient/v3/servers.py @@ -409,7 +409,7 @@ def _boot(self, resource_url, response_key, name, image, flavor, body["server"]["metadata"] = meta if reservation_id: body["server"][ - "os-multiple-create:reservation_id"] = reservation_id + "os-multiple-create:return_reservation_id"] = reservation_id if key_name: body["server"]["key_name"] = key_name if scheduler_hints: From 93f9e68ca22d19fdc636b7d50a4d08e4ea07d80e Mon Sep 17 00:00:00 2001 From: zhang-jinnan Date: Mon, 17 Feb 2014 20:09:28 +0800 Subject: [PATCH 0419/1705] Replace assertEqual(None, *) with assertIsNone in tests Replace assertEqual(None, *) with assertIsNone in tests to have more clear messages in case of failure. Change-Id: I6deee90c31adf61d80e2678a5f29ba9e187281c9 --- novaclient/tests/test_client.py | 2 +- novaclient/tests/v1_1/test_floating_ips.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/novaclient/tests/test_client.py b/novaclient/tests/test_client.py index 581776cac..d58670240 100644 --- a/novaclient/tests/test_client.py +++ b/novaclient/tests/test_client.py @@ -205,7 +205,7 @@ def test_get_password_simple(self): def test_get_password_none(self): cs = novaclient.client.HTTPClient("user", None, "", "") - self.assertEqual(cs._get_password(), None) + self.assertIsNone(cs._get_password()) def test_get_password_func(self): cs = novaclient.client.HTTPClient("user", None, "", "") diff --git a/novaclient/tests/v1_1/test_floating_ips.py b/novaclient/tests/v1_1/test_floating_ips.py index 337db3e21..0b2313f51 100644 --- a/novaclient/tests/v1_1/test_floating_ips.py +++ b/novaclient/tests/v1_1/test_floating_ips.py @@ -41,7 +41,7 @@ def test_delete_floating_ip(self): def test_create_floating_ip(self): fl = cs.floating_ips.create() cs.assert_called('POST', '/os-floating-ips') - self.assertEqual(fl.pool, None) + self.assertIsNone(fl.pool) self.assertIsInstance(fl, floating_ips.FloatingIP) def test_create_floating_ip_with_pool(self): From ca721d6e9b5013093e4324899b4f69e83aeb43d3 Mon Sep 17 00:00:00 2001 From: zhang-jinnan Date: Wed, 19 Feb 2014 21:02:11 +0800 Subject: [PATCH 0420/1705] Remove None for dict.get() Because If no default value is specified it defaults to None already. Change-Id: I3caad9f17840347e30465c7bd4c9c4fe53d991e3 --- novaclient/exceptions.py | 4 ++-- novaclient/openstack/common/apiclient/exceptions.py | 4 ++-- novaclient/openstack/common/cliutils.py | 2 +- novaclient/tests/utils.py | 6 +++--- novaclient/tests/v1_1/fakes.py | 2 +- novaclient/v1_1/flavor_access.py | 4 ++-- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/novaclient/exceptions.py b/novaclient/exceptions.py index 73049cda5..9f22a9e3e 100644 --- a/novaclient/exceptions.py +++ b/novaclient/exceptions.py @@ -222,8 +222,8 @@ def from_response(response, body, url, method=None): if hasattr(body, 'keys'): error = body[list(body)[0]] - message = error.get('message', None) - details = error.get('details', None) + message = error.get('message') + details = error.get('details') kwargs['message'] = message kwargs['details'] = details diff --git a/novaclient/openstack/common/apiclient/exceptions.py b/novaclient/openstack/common/apiclient/exceptions.py index b364d60dc..45a70e0a7 100644 --- a/novaclient/openstack/common/apiclient/exceptions.py +++ b/novaclient/openstack/common/apiclient/exceptions.py @@ -422,8 +422,8 @@ def from_response(response, method, url): else: if hasattr(body, "keys"): error = body[body.keys()[0]] - kwargs["message"] = error.get("message", None) - kwargs["details"] = error.get("details", None) + kwargs["message"] = error.get("message") + kwargs["details"] = error.get("details") elif content_type.startswith("text/"): kwargs["details"] = response.text diff --git a/novaclient/openstack/common/cliutils.py b/novaclient/openstack/common/cliutils.py index 907159886..710f8f8fb 100644 --- a/novaclient/openstack/common/cliutils.py +++ b/novaclient/openstack/common/cliutils.py @@ -84,7 +84,7 @@ def env(*args, **kwargs): If all are empty, defaults to '' or keyword arg `default`. """ for arg in args: - value = os.environ.get(arg, None) + value = os.environ.get(arg) if value: return value return kwargs.get('default', '') diff --git a/novaclient/tests/utils.py b/novaclient/tests/utils.py index 982b953ab..11bfa827b 100644 --- a/novaclient/tests/utils.py +++ b/novaclient/tests/utils.py @@ -45,10 +45,10 @@ def __init__(self, data): super(TestResponse, self).__init__() self._text = None if isinstance(data, dict): - self.status_code = data.get('status_code', None) - self.headers = data.get('headers', None) + self.status_code = data.get('status_code') + self.headers = data.get('headers') # Fake the text attribute to streamline Response creation - self._text = data.get('text', None) + self._text = data.get('text') else: self.status_code = data diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index 983f849aa..3604312b3 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -79,7 +79,7 @@ def _cs_request(self, url, method, **kwargs): (method, url, callback)) # Note the call - self.callstack.append((method, url, kwargs.get('body', None))) + self.callstack.append((method, url, kwargs.get('body'))) status, headers, body = getattr(self, callback)(**kwargs) r = utils.TestResponse({ diff --git a/novaclient/v1_1/flavor_access.py b/novaclient/v1_1/flavor_access.py index d847d061f..29c1f84f5 100644 --- a/novaclient/v1_1/flavor_access.py +++ b/novaclient/v1_1/flavor_access.py @@ -31,9 +31,9 @@ class FlavorAccessManager(base.ManagerWithFind): resource_class = FlavorAccess def list(self, **kwargs): - if kwargs.get('flavor', None): + if kwargs.get('flavor'): return self._list_by_flavor(kwargs['flavor']) - elif kwargs.get('tenant', None): + elif kwargs.get('tenant'): return self._list_by_tenant(kwargs['tenant']) else: raise NotImplementedError(_('Unknown list options.')) From 2caf682bcdc2f66755a67c91b5b7210c09bb9b44 Mon Sep 17 00:00:00 2001 From: "Leandro I. Costantino" Date: Wed, 19 Feb 2014 13:20:36 -0300 Subject: [PATCH 0421/1705] Invalid client version message unclear The error message when using an invalid os-compute-api-version is created by joining all the available versions without space. This patch will add ', ' between versions. Ex: nova --os-compute-api-version v3 list ERROR: Invalid client version 'v3'. must be one of: 321.1 Now: nova --os-compute-api-version v3 list ERROR: Invalid client version 'v3'. must be one of: 3, 2, 1.1 Change-Id: I825df89d38adc9e4bd3fb900cd0199f159c04a6b --- novaclient/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/client.py b/novaclient/client.py index 105d8075e..8ec471cf9 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -466,7 +466,7 @@ def get_client_class(version): except (KeyError, ValueError): msg = _("Invalid client version '%(version)s'. must be one of: " "%(keys)s") % {'version': version, - 'keys': ''.join(version_map.keys())} + 'keys': ', '.join(version_map.keys())} raise exceptions.UnsupportedVersion(msg) return utils.import_class(client_path) From c6f2331c15ba250a19770e7ec81e984916433b72 Mon Sep 17 00:00:00 2001 From: OpenStack Jenkins Date: Sun, 23 Feb 2014 09:31:50 +0000 Subject: [PATCH 0422/1705] Updated from global requirements Change-Id: I0456dea975c2ce3d45caa9c1813a8ac9e966976a --- requirements.txt | 2 +- test-requirements.txt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 8923e6c42..954ac4e37 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -pbr>=0.5.21,<1.0 +pbr>=0.6,<1.0 argparse iso8601>=0.1.8 PrettyTable>=0.7,<0.8 diff --git a/test-requirements.txt b/test-requirements.txt index 20a8506c2..868d84cf5 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -6,5 +6,5 @@ fixtures>=0.3.14 keyring>=1.6.1,<2.0,>=2.1 mock>=1.0 sphinx>=1.1.2,<1.2 -testrepository>=0.0.17 -testtools>=0.9.32 +testrepository>=0.0.18 +testtools>=0.9.34 From d44e5986925d3129ca0d34599109ad2019295362 Mon Sep 17 00:00:00 2001 From: Eric Guo Date: Fri, 14 Feb 2014 08:43:12 +0800 Subject: [PATCH 0423/1705] Remove usage of module py3kcompat Module py3kcompat was removed from oslo-incubator. We need remove its usage in client side firstly. This make us move smoothly when sync oslo-incubator code. Change-Id: I8b07c32c9852e747579a23685f3c8a07ac13ec01 Partial-Bug: #1280033 --- novaclient/client.py | 7 ++++--- novaclient/tests/v1_1/fakes.py | 4 ++-- novaclient/v1_1/contrib/migrations.py | 5 +++-- novaclient/v1_1/flavors.py | 5 +++-- novaclient/v1_1/floating_ip_dns.py | 7 ++++--- novaclient/v1_1/hypervisors.py | 5 +++-- novaclient/v1_1/images.py | 6 ++++-- novaclient/v1_1/limits.py | 5 +++-- novaclient/v1_1/security_groups.py | 4 ++-- novaclient/v1_1/servers.py | 4 ++-- novaclient/v1_1/volumes.py | 4 ++-- novaclient/v3/hypervisors.py | 5 +++-- novaclient/v3/images.py | 6 ++++-- novaclient/v3/servers.py | 4 ++-- 14 files changed, 41 insertions(+), 30 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index 8ec471cf9..bab36301b 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -30,9 +30,10 @@ except ImportError: import simplejson as json +from six.moves.urllib import parse + from novaclient import exceptions from novaclient.openstack.common.gettextutils import _ -from novaclient.openstack.common.py3kcompat import urlutils from novaclient import service_catalog from novaclient import utils @@ -325,7 +326,7 @@ def _fetch_endpoints_from_auth(self, url): extract_token=False) def authenticate(self): - magic_tuple = urlutils.urlsplit(self.auth_url) + magic_tuple = parse.urlsplit(self.auth_url) scheme, netloc, path, query, frag = magic_tuple port = magic_tuple.port if port is None: @@ -343,7 +344,7 @@ def authenticate(self): # TODO(sandy): Assume admin endpoint is 35357 for now. # Ideally this is going to have to be provided by the service catalog. new_netloc = netloc.replace(':%d' % port, ':%d' % (35357,)) - admin_url = urlutils.urlunsplit( + admin_url = parse.urlunsplit( (scheme, new_netloc, path, query, frag)) auth_url = self.auth_url diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index 3604312b3..44330550d 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -17,10 +17,10 @@ from datetime import datetime import six +from six.moves.urllib import parse from novaclient import client as base_client from novaclient import exceptions -from novaclient.openstack.common.py3kcompat import urlutils from novaclient.openstack.common import strutils from novaclient.tests import fakes from novaclient.tests import utils @@ -64,7 +64,7 @@ def _cs_request(self, url, method, **kwargs): assert 'body' in kwargs # Call the method - args = urlutils.parse_qsl(urlutils.urlparse(url)[4]) + args = parse.parse_qsl(parse.urlparse(url)[4]) kwargs.update(args) munged_url = url.rsplit('?', 1)[0] munged_url = munged_url.strip('/').replace('/', '_').replace('.', '_') diff --git a/novaclient/v1_1/contrib/migrations.py b/novaclient/v1_1/contrib/migrations.py index 507ebc9eb..16be844ab 100644 --- a/novaclient/v1_1/contrib/migrations.py +++ b/novaclient/v1_1/contrib/migrations.py @@ -14,9 +14,10 @@ migration interface """ +from six.moves.urllib import parse + from novaclient import base from novaclient.openstack.common.gettextutils import _ -from novaclient.openstack.common.py3kcompat import urlutils from novaclient import utils @@ -47,7 +48,7 @@ def list(self, host=None, status=None, cell_name=None): # order, then the encoded string will be consistent in Python 2&3. new_opts = sorted(opts.items(), key=lambda x: x[0]) - query_string = "?%s" % urlutils.urlencode(new_opts) if new_opts else "" + query_string = "?%s" % parse.urlencode(new_opts) if new_opts else "" return self._list("/os-migrations%s" % query_string, "migrations") diff --git a/novaclient/v1_1/flavors.py b/novaclient/v1_1/flavors.py index 8aa875ff0..c47c47633 100644 --- a/novaclient/v1_1/flavors.py +++ b/novaclient/v1_1/flavors.py @@ -16,10 +16,11 @@ Flavor interface. """ +from six.moves.urllib import parse + from novaclient import base from novaclient import exceptions from novaclient.openstack.common.gettextutils import _ -from novaclient.openstack.common.py3kcompat import urlutils from novaclient.openstack.common import strutils from novaclient import utils @@ -112,7 +113,7 @@ def list(self, detailed=True, is_public=True): # and flavors from their own projects only. if not is_public: qparams['is_public'] = is_public - query_string = "?%s" % urlutils.urlencode(qparams) if qparams else "" + query_string = "?%s" % parse.urlencode(qparams) if qparams else "" detail = "" if detailed: diff --git a/novaclient/v1_1/floating_ip_dns.py b/novaclient/v1_1/floating_ip_dns.py index a5dfcb8d9..449c7e11e 100644 --- a/novaclient/v1_1/floating_ip_dns.py +++ b/novaclient/v1_1/floating_ip_dns.py @@ -13,8 +13,9 @@ # License for the specific language governing permissions and limitations # under the License. +from six.moves.urllib import parse + from novaclient import base -from novaclient.openstack.common.py3kcompat import urlutils def _quote_domain(domain): @@ -24,7 +25,7 @@ def _quote_domain(domain): but Routes tends to choke on them, so we need an extra level of by-hand quoting here. """ - return urlutils.quote(domain.replace('.', '%2E')) + return parse.quote(domain.replace('.', '%2E')) class FloatingIPDNSDomain(base.Resource): @@ -101,7 +102,7 @@ def get(self, domain, name): def get_for_ip(self, domain, ip): """Return a list of entries for the given domain and ip or name.""" qparams = {'ip': ip} - params = "?%s" % urlutils.urlencode(qparams) + params = "?%s" % parse.urlencode(qparams) return self._list("/os-floating-ip-dns/%s/entries%s" % (_quote_domain(domain), params), diff --git a/novaclient/v1_1/hypervisors.py b/novaclient/v1_1/hypervisors.py index fae04026a..4a4f2b93a 100644 --- a/novaclient/v1_1/hypervisors.py +++ b/novaclient/v1_1/hypervisors.py @@ -17,8 +17,9 @@ Hypervisors interface (1.1 extension). """ +from six.moves.urllib import parse + from novaclient import base -from novaclient.openstack.common.py3kcompat import urlutils class Hypervisor(base.Resource): @@ -48,7 +49,7 @@ def search(self, hypervisor_match, servers=False): """ target = 'servers' if servers else 'search' url = ('/os-hypervisors/%s/%s' % - (urlutils.quote(hypervisor_match, safe=''), target)) + (parse.quote(hypervisor_match, safe=''), target)) return self._list(url, 'hypervisors') def get(self, hypervisor): diff --git a/novaclient/v1_1/images.py b/novaclient/v1_1/images.py index ba6820b64..f355b894f 100644 --- a/novaclient/v1_1/images.py +++ b/novaclient/v1_1/images.py @@ -15,8 +15,10 @@ """ Image interface. """ + +from six.moves.urllib import parse + from novaclient import base -from novaclient.openstack.common.py3kcompat import urlutils class Image(base.Resource): @@ -63,7 +65,7 @@ def list(self, detailed=True, limit=None): detail = '/detail' if limit: params['limit'] = int(limit) - query = '?%s' % urlutils.urlencode(params) if params else '' + query = '?%s' % parse.urlencode(params) if params else '' return self._list('/images%s%s' % (detail, query), 'images') def delete(self, image): diff --git a/novaclient/v1_1/limits.py b/novaclient/v1_1/limits.py index 4ea80a053..46d77def8 100644 --- a/novaclient/v1_1/limits.py +++ b/novaclient/v1_1/limits.py @@ -12,8 +12,9 @@ # License for the specific language governing permissions and limitations # under the License. +from six.moves.urllib import parse + from novaclient import base -from novaclient.openstack.common.py3kcompat import urlutils class Limits(base.Resource): @@ -94,6 +95,6 @@ def get(self, reserved=False, tenant_id=None): opts['reserved'] = 1 if tenant_id: opts['tenant_id'] = tenant_id - query_string = "?%s" % urlutils.urlencode(opts) if opts else "" + query_string = "?%s" % parse.urlencode(opts) if opts else "" return self._get("/limits%s" % query_string, "limits") diff --git a/novaclient/v1_1/security_groups.py b/novaclient/v1_1/security_groups.py index 00e1b48f0..40d1e7ff7 100644 --- a/novaclient/v1_1/security_groups.py +++ b/novaclient/v1_1/security_groups.py @@ -18,9 +18,9 @@ """ import six +from six.moves.urllib import parse from novaclient import base -from novaclient.openstack.common.py3kcompat import urlutils class SecurityGroup(base.Resource): @@ -90,7 +90,7 @@ def list(self, search_opts=None): qparams = dict((k, v) for (k, v) in six.iteritems(search_opts) if v) - query_string = '?%s' % urlutils.urlencode(qparams) if qparams else '' + query_string = '?%s' % parse.urlencode(qparams) if qparams else '' return self._list('/os-security-groups%s' % query_string, 'security_groups') diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index 0ae5d1663..cf284d94b 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -22,10 +22,10 @@ import base64 import six +from six.moves.urllib import parse from novaclient import base from novaclient import crypto -from novaclient.openstack.common.py3kcompat import urlutils from novaclient.openstack.common import strutils from novaclient.v1_1.security_groups import SecurityGroup @@ -573,7 +573,7 @@ def list(self, detailed=True, search_opts=None, marker=None, limit=None): # order, then the encoded string will be consistent in Python 2&3. if qparams: new_qparams = sorted(qparams.items(), key=lambda x: x[0]) - query_string = "?%s" % urlutils.urlencode(new_qparams) + query_string = "?%s" % parse.urlencode(new_qparams) else: query_string = "" diff --git a/novaclient/v1_1/volumes.py b/novaclient/v1_1/volumes.py index 1c4792015..77746dd30 100644 --- a/novaclient/v1_1/volumes.py +++ b/novaclient/v1_1/volumes.py @@ -18,9 +18,9 @@ """ import six +from six.moves.urllib import parse from novaclient import base -from novaclient.openstack.common.py3kcompat import urlutils class Volume(base.Resource): @@ -89,7 +89,7 @@ def list(self, detailed=True, search_opts=None): qparams = dict((k, v) for (k, v) in six.iteritems(search_opts) if v) - query_string = '?%s' % urlutils.urlencode(qparams) if qparams else '' + query_string = '?%s' % parse.urlencode(qparams) if qparams else '' if detailed is True: return self._list("/volumes/detail%s" % query_string, "volumes") diff --git a/novaclient/v3/hypervisors.py b/novaclient/v3/hypervisors.py index 0542895fb..b04a60080 100644 --- a/novaclient/v3/hypervisors.py +++ b/novaclient/v3/hypervisors.py @@ -17,7 +17,8 @@ Hypervisors interface """ -from novaclient.openstack.common.py3kcompat import urlutils +from six.moves.urllib import parse + from novaclient.v1_1 import hypervisors @@ -35,7 +36,7 @@ def search(self, hypervisor_match): :param servers: If True, server information is also retrieved. """ url = ('/os-hypervisors/search?query=%s' % - urlutils.quote(hypervisor_match, safe='')) + parse.quote(hypervisor_match, safe='')) return self._list(url, 'hypervisors') def servers(self, hypervisor): diff --git a/novaclient/v3/images.py b/novaclient/v3/images.py index 2e1b34aa8..b78d9af55 100644 --- a/novaclient/v3/images.py +++ b/novaclient/v3/images.py @@ -16,8 +16,10 @@ """ Image interface. """ + +from six.moves.urllib import parse + from novaclient import base -from novaclient.openstack.common.py3kcompat import urlutils from novaclient.openstack.common import strutils @@ -100,5 +102,5 @@ def list(self, detailed=True, limit=None): detail = '/detail' if limit: params['limit'] = int(limit) - query = '?%s' % urlutils.urlencode(params) if params else '' + query = '?%s' % parse.urlencode(params) if params else '' return self._list('/v1/images%s%s' % (detail, query), 'images') diff --git a/novaclient/v3/servers.py b/novaclient/v3/servers.py index b097ffe65..d8ebd0a13 100644 --- a/novaclient/v3/servers.py +++ b/novaclient/v3/servers.py @@ -22,10 +22,10 @@ import base64 import six +from six.moves.urllib import parse from novaclient import base from novaclient import crypto -from novaclient.openstack.common.py3kcompat import urlutils from novaclient.openstack.common import strutils REBOOT_SOFT, REBOOT_HARD = 'SOFT', 'HARD' @@ -507,7 +507,7 @@ def list(self, detailed=True, search_opts=None, marker=None, limit=None): # order, then the encoded string will be consistent in Python 2&3. if qparams: new_qparams = sorted(qparams.items(), key=lambda x: x[0]) - query_string = "?%s" % urlutils.urlencode(new_qparams) + query_string = "?%s" % parse.urlencode(new_qparams) else: query_string = "" From 5461af9c6ca0a56267b28b83930503a9d60214a9 Mon Sep 17 00:00:00 2001 From: wingwj Date: Tue, 25 Feb 2014 14:17:35 +0800 Subject: [PATCH 0424/1705] Fix typo in novaclient sapshot --> snapshot messanges --> messages Change-Id: I21d9f3bd02c435f92f8ba20e85898d7a11e6a0af --- novaclient/client.py | 2 +- novaclient/v1_1/shell.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index 8ec471cf9..a80ffb285 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -116,7 +116,7 @@ def __init__(self, user, password, projectid=None, auth_url=None, rql.addHandler(ch) # Since we have already setup the root logger on debug, we # have to set it up here on WARNING (its original level) - # otherwise we will get all the requests logging messanges + # otherwise we will get all the requests logging messages rql.setLevel(logging.WARNING) # requests within the same session can reuse TCP connections from pool self.http = requests.Session() diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 5f0858b55..c7a520980 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -316,7 +316,7 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): @utils.arg('--snapshot', default=None, metavar="", - help=_("Sapshot ID to boot from (will create a volume).")) + help=_("Snapshot ID to boot from (will create a volume).")) @utils.arg('--num-instances', default=None, type=int, From a2a1ef8f36644c3487bdaf00b2e70bcc6b948fb4 Mon Sep 17 00:00:00 2001 From: huangtianhua Date: Fri, 14 Feb 2014 16:01:07 +0800 Subject: [PATCH 0425/1705] 'name' should as be optional param on aggregate-update 'name' should be an optional parameter not required on aggregate-update, so we can update 'availability_zone' only. Change-Id: I23e669a3362e0bea44adc88744eed823ec1e7ebb Closes-Bug: #1280118 --- novaclient/tests/v1_1/test_shell.py | 28 ++++++++++++++++++++++------ novaclient/tests/v3/test_shell.py | 28 ++++++++++++++++++++++------ novaclient/v1_1/shell.py | 12 ++++++++---- novaclient/v3/shell.py | 12 ++++++++---- 4 files changed, 60 insertions(+), 20 deletions(-) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index a4c4ca19c..feeba4870 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -1149,26 +1149,42 @@ def test_aggregate_delete_by_name(self): self.assert_called('DELETE', '/os-aggregates/1') def test_aggregate_update_by_id(self): - self.run_command('aggregate-update 1 new_name') + self.run_command('aggregate-update 1 --name new_name') body = {"aggregate": {"name": "new_name"}} self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) self.assert_called('GET', '/os-aggregates/1', pos=-1) def test_aggregate_update_by_name(self): - self.run_command('aggregate-update test new_name') + self.run_command('aggregate-update test --name new_name') body = {"aggregate": {"name": "new_name"}} self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) self.assert_called('GET', '/os-aggregates/1', pos=-1) def test_aggregate_update_with_availability_zone_by_id(self): - self.run_command('aggregate-update 1 foo new_zone') - body = {"aggregate": {"name": "foo", "availability_zone": "new_zone"}} + self.run_command('aggregate-update 1 --availability_zone new_zone') + body = {"aggregate": {"availability_zone": "new_zone"}} self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) self.assert_called('GET', '/os-aggregates/1', pos=-1) def test_aggregate_update_with_availability_zone_by_name(self): - self.run_command('aggregate-update test foo new_zone') - body = {"aggregate": {"name": "foo", "availability_zone": "new_zone"}} + self.run_command('aggregate-update test --availability_zone new_zone') + body = {"aggregate": {"availability_zone": "new_zone"}} + self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) + self.assert_called('GET', '/os-aggregates/1', pos=-1) + + def test_aggregate_update_all_parameters_by_id(self): + self.run_command('aggregate-update 1 --name new_name ' + '--availability_zone new_zone') + body = {"aggregate": {"name": "new_name", + "availability_zone": "new_zone"}} + self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) + self.assert_called('GET', '/os-aggregates/1', pos=-1) + + def test_aggregate_update_all_parameters_by_name(self): + self.run_command('aggregate-update test --name new_name ' + '--availability_zone new_zone') + body = {"aggregate": {"name": "new_name", + "availability_zone": "new_zone"}} self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) self.assert_called('GET', '/os-aggregates/1', pos=-1) diff --git a/novaclient/tests/v3/test_shell.py b/novaclient/tests/v3/test_shell.py index 85a839b49..796e73c6c 100644 --- a/novaclient/tests/v3/test_shell.py +++ b/novaclient/tests/v3/test_shell.py @@ -104,26 +104,42 @@ def test_aggregate_delete_by_name(self): self.assert_called('DELETE', '/os-aggregates/1') def test_aggregate_update_by_id(self): - self.run_command('aggregate-update 1 new_name') + self.run_command('aggregate-update 1 --name new_name') body = {"aggregate": {"name": "new_name"}} self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) self.assert_called('GET', '/os-aggregates/1', pos=-1) def test_aggregate_update_by_name(self): - self.run_command('aggregate-update test new_name') + self.run_command('aggregate-update test --name new_name') body = {"aggregate": {"name": "new_name"}} self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) self.assert_called('GET', '/os-aggregates/1', pos=-1) def test_aggregate_update_with_availability_zone_by_id(self): - self.run_command('aggregate-update 1 foo new_zone') - body = {"aggregate": {"name": "foo", "availability_zone": "new_zone"}} + self.run_command('aggregate-update 1 --availability_zone new_zone') + body = {"aggregate": {"availability_zone": "new_zone"}} self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) self.assert_called('GET', '/os-aggregates/1', pos=-1) def test_aggregate_update_with_availability_zone_by_name(self): - self.run_command('aggregate-update test foo new_zone') - body = {"aggregate": {"name": "foo", "availability_zone": "new_zone"}} + self.run_command('aggregate-update test --availability_zone new_zone') + body = {"aggregate": {"availability_zone": "new_zone"}} + self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) + self.assert_called('GET', '/os-aggregates/1', pos=-1) + + def test_aggregate_update_all_parameters_by_id(self): + self.run_command('aggregate-update 1 --name new_name ' + '--availability_zone new_zone') + body = {"aggregate": {"name": "new_name", + "availability_zone": "new_zone"}} + self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) + self.assert_called('GET', '/os-aggregates/1', pos=-1) + + def test_aggregate_update_all_parameters_by_name(self): + self.run_command('aggregate-update test --name new_name ' + '--availability_zone new_zone') + body = {"aggregate": {"name": "new_name", + "availability_zone": "new_zone"}} self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) self.assert_called('GET', '/os-aggregates/1', pos=-1) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index c7a520980..24317642c 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2704,18 +2704,22 @@ def do_aggregate_delete(cs, args): @utils.arg('aggregate', metavar='', help=_('Name or ID of aggregate to update.')) -@utils.arg('name', metavar='', help=_('Name of aggregate.')) -@utils.arg('availability_zone', +@utils.arg('--name', metavar='', help=_('New name of aggregate.')) +@utils.arg('--availability_zone', metavar='', - nargs='?', default=None, help=_('The availability zone of the aggregate.')) def do_aggregate_update(cs, args): """Update the aggregate's name and optionally availability zone.""" aggregate = _find_aggregate(cs, args.aggregate) - updates = {"name": args.name} + updates = {} + if args.name: + updates["name"] = args.name if args.availability_zone: updates["availability_zone"] = args.availability_zone + if not updates: + raise exceptions.CommandError(_("Must supply name " + "or availability_zone.")) aggregate = cs.aggregates.update(aggregate.id, updates) print(_("Aggregate %s has been successfully updated.") % aggregate.id) diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 10a6975d8..02706c359 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -2230,18 +2230,22 @@ def do_aggregate_delete(cs, args): @utils.arg('aggregate', metavar='', help='Name or ID of aggregate to update.') -@utils.arg('name', metavar='', help='Name of aggregate.') -@utils.arg('availability_zone', +@utils.arg('--name', metavar='', help='New name of aggregate.') +@utils.arg('--availability_zone', metavar='', - nargs='?', default=None, help='The availability zone of the aggregate.') def do_aggregate_update(cs, args): """Update the aggregate's name and optionally availability zone.""" aggregate = _find_aggregate(cs, args.aggregate) - updates = {"name": args.name} + updates = {} + if args.name: + updates["name"] = args.name if args.availability_zone: updates["availability_zone"] = args.availability_zone + if not updates: + raise exceptions.CommandError("Must supply name " + "or availability_zone.") aggregate = cs.aggregates.update(aggregate.id, updates) print("Aggregate %s has been successfully updated." % aggregate.id) From a77f6850d543cee02abcbfb01c5e2d41ccf77833 Mon Sep 17 00:00:00 2001 From: OpenStack Jenkins Date: Wed, 26 Feb 2014 23:34:59 +0000 Subject: [PATCH 0426/1705] Updated from global requirements Change-Id: I870165ea9be249b1cda937307a32ab29d8270d94 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 954ac4e37..bafbb386d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,5 +4,5 @@ iso8601>=0.1.8 PrettyTable>=0.7,<0.8 requests>=1.1 simplejson>=2.0.9 -six>=1.4.1 +six>=1.5.2 Babel>=1.3 From e0272b0578d2a4f5bd75e68baee1b56f49931266 Mon Sep 17 00:00:00 2001 From: Joe Gordon Date: Wed, 26 Feb 2014 12:11:25 -0800 Subject: [PATCH 0427/1705] oslo-sync of low hanging fruit Generated with: ./update.sh --base novaclient --config-file ../python-novaclient/openstack-common.conf --dest-dir ../python-novaclient/ Skipped modules: apiclient and cliutils due to oslo issue in apiclient (cliutils depends on apiclient). https://review.openstack.org/#/c/76718/ Removed non-existent modules from openstack-commit.conf: py3kcompat I06b90f789ae21f2ef8b8071b4298bfc0406482a6 intall_venv_common I84267f3c6726cb2e750f615e107c48b12c6ed353 Synced patches: __init__.py [first sync]: c178e56 Add basic Python 3 tests 12bcdb7 Remove vim header 547ab34 Fix Copyright Headers - Rename LLC to Foundation 96b9a54 Rajaram/Vinkesh|Added nova's serializaiton classes into common c85e1f7 Initial skeleton project gettextutils: 6d55e26 Add support for translating log levels separately afdbc0a Fix E501 in individual openstack projects 6b2d15f Merge "Add support for locales missing from babel" 9d529dd Make Message keep string interpolation args importutils: 885828a Deleted duplicated method in cliutils. strutils: bec3a5e Implements SI/IEC unit system conversion to bytes timeutils: d815087 Merge "Fix spelling errors in comments" 71208fe Fix spelling errors in comments Change-Id: I8f82acb63e61a64eeb6caba9d2d9c81d9cb766d8 --- novaclient/openstack/common/__init__.py | 2 + novaclient/openstack/common/gettextutils.py | 55 +++++++++++++- novaclient/openstack/common/importutils.py | 7 ++ novaclient/openstack/common/strutils.py | 84 +++++++++++++-------- novaclient/openstack/common/timeutils.py | 2 +- openstack-common.conf | 2 - 6 files changed, 117 insertions(+), 35 deletions(-) diff --git a/novaclient/openstack/common/__init__.py b/novaclient/openstack/common/__init__.py index e69de29bb..2a00f3bc4 100644 --- a/novaclient/openstack/common/__init__.py +++ b/novaclient/openstack/common/__init__.py @@ -0,0 +1,2 @@ +import six +six.add_move(six.MovedModule('mox', 'mox', 'mox3.mox')) diff --git a/novaclient/openstack/common/gettextutils.py b/novaclient/openstack/common/gettextutils.py index 27f7ec426..982348274 100644 --- a/novaclient/openstack/common/gettextutils.py +++ b/novaclient/openstack/common/gettextutils.py @@ -23,6 +23,7 @@ """ import copy +import functools import gettext import locale from logging import handlers @@ -35,6 +36,17 @@ _localedir = os.environ.get('novaclient'.upper() + '_LOCALEDIR') _t = gettext.translation('novaclient', localedir=_localedir, fallback=True) +# We use separate translation catalogs for each log level, so set up a +# mapping between the log level name and the translator. The domain +# for the log level is project_name + "-log-" + log_level so messages +# for each level end up in their own catalog. +_t_log_levels = dict( + (level, gettext.translation('novaclient' + '-log-' + level, + localedir=_localedir, + fallback=True)) + for level in ['info', 'warning', 'error', 'critical'] +) + _AVAILABLE_LANGUAGES = {} USE_LAZY = False @@ -60,6 +72,28 @@ def _(msg): return _t.ugettext(msg) +def _log_translation(msg, level): + """Build a single translation of a log message + """ + if USE_LAZY: + return Message(msg, domain='novaclient' + '-log-' + level) + else: + translator = _t_log_levels[level] + if six.PY3: + return translator.gettext(msg) + return translator.ugettext(msg) + +# Translators for log levels. +# +# The abbreviated names are meant to reflect the usual use of a short +# name like '_'. The "L" is for "log" and the other letter comes from +# the level. +_LI = functools.partial(_log_translation, level='info') +_LW = functools.partial(_log_translation, level='warning') +_LE = functools.partial(_log_translation, level='error') +_LC = functools.partial(_log_translation, level='critical') + + def install(domain, lazy=False): """Install a _() function using the given translation domain. @@ -118,7 +152,8 @@ class Message(six.text_type): and can be treated as such. """ - def __new__(cls, msgid, msgtext=None, params=None, domain='novaclient', *args): + def __new__(cls, msgid, msgtext=None, params=None, + domain='novaclient', *args): """Create a new Message object. In order for translation to work gettext requires a message ID, this @@ -297,9 +332,27 @@ def get_available_languages(domain): list_identifiers = (getattr(localedata, 'list', None) or getattr(localedata, 'locale_identifiers')) locale_identifiers = list_identifiers() + for i in locale_identifiers: if find(i) is not None: language_list.append(i) + + # NOTE(luisg): Babel>=1.0,<1.3 has a bug where some OpenStack supported + # locales (e.g. 'zh_CN', and 'zh_TW') aren't supported even though they + # are perfectly legitimate locales: + # https://github.com/mitsuhiko/babel/issues/37 + # In Babel 1.3 they fixed the bug and they support these locales, but + # they are still not explicitly "listed" by locale_identifiers(). + # That is why we add the locales here explicitly if necessary so that + # they are listed as supported. + aliases = {'zh': 'zh_CN', + 'zh_Hant_HK': 'zh_HK', + 'zh_Hant': 'zh_TW', + 'fil': 'tl_PH'} + for (locale, alias) in six.iteritems(aliases): + if locale in language_list and alias not in language_list: + language_list.append(alias) + _AVAILABLE_LANGUAGES[domain] = language_list return copy.copy(language_list) diff --git a/novaclient/openstack/common/importutils.py b/novaclient/openstack/common/importutils.py index 4fd9ae2bc..e35b088af 100644 --- a/novaclient/openstack/common/importutils.py +++ b/novaclient/openstack/common/importutils.py @@ -58,6 +58,13 @@ def import_module(import_str): return sys.modules[import_str] +def import_versioned_module(version, submodule=None): + module = 'novaclient.v%s' % version + if submodule: + module = '.'.join((module, submodule)) + return import_module(module) + + def try_import(import_str, default=None): """Try to import a module and if it fails return default.""" try: diff --git a/novaclient/openstack/common/strutils.py b/novaclient/openstack/common/strutils.py index c01de557a..1383fbb7e 100644 --- a/novaclient/openstack/common/strutils.py +++ b/novaclient/openstack/common/strutils.py @@ -17,6 +17,7 @@ System-level utilities and helper functions. """ +import math import re import sys import unicodedata @@ -26,16 +27,21 @@ from novaclient.openstack.common.gettextutils import _ -# Used for looking up extensions of text -# to their 'multiplied' byte amount -BYTE_MULTIPLIERS = { - '': 1, - 't': 1024 ** 4, - 'g': 1024 ** 3, - 'm': 1024 ** 2, - 'k': 1024, +UNIT_PREFIX_EXPONENT = { + 'k': 1, + 'K': 1, + 'Ki': 1, + 'M': 2, + 'Mi': 2, + 'G': 3, + 'Gi': 3, + 'T': 4, + 'Ti': 4, +} +UNIT_SYSTEM_INFO = { + 'IEC': (1024, re.compile(r'(^[-+]?\d*\.?\d+)([KMGT]i?)?(b|bit|B)$')), + 'SI': (1000, re.compile(r'(^[-+]?\d*\.?\d+)([kMGT])?(b|bit|B)$')), } -BYTE_REGEX = re.compile(r'(^-?\d+)(\D*)') TRUE_STRINGS = ('1', 't', 'true', 'on', 'y', 'yes') FALSE_STRINGS = ('0', 'f', 'false', 'off', 'n', 'no') @@ -102,7 +108,7 @@ def safe_decode(text, incoming=None, errors='strict'): :raises TypeError: If text is not an instance of str """ if not isinstance(text, six.string_types): - raise TypeError(_("%s can't be decoded") % type(text)) + raise TypeError("%s can't be decoded" % type(text)) if isinstance(text, six.text_type): return text @@ -145,7 +151,7 @@ def safe_encode(text, incoming=None, :raises TypeError: If text is not an instance of str """ if not isinstance(text, six.string_types): - raise TypeError(_("%s can't be encoded") % type(text)) + raise TypeError("%s can't be encoded" % type(text)) if not incoming: incoming = (sys.stdin.encoding or @@ -167,34 +173,50 @@ def safe_encode(text, incoming=None, return text -def to_bytes(text, default=0): - """Converts a string into an integer of bytes. +def string_to_bytes(text, unit_system='IEC', return_int=False): + """Converts a string into an float representation of bytes. + + The units supported for IEC :: + + Kb(it), Kib(it), Mb(it), Mib(it), Gb(it), Gib(it), Tb(it), Tib(it) + KB, KiB, MB, MiB, GB, GiB, TB, TiB + + The units supported for SI :: + + kb(it), Mb(it), Gb(it), Tb(it) + kB, MB, GB, TB - Looks at the last characters of the text to determine - what conversion is needed to turn the input text into a byte number. - Supports "B, K(B), M(B), G(B), and T(B)". (case insensitive) + Note that the SI unit system does not support capital letter 'K' :param text: String input for bytes size conversion. - :param default: Default return value when text is blank. + :param unit_system: Unit system for byte size conversion. + :param return_int: If True, returns integer representation of text + in bytes. (default: decimal) + :returns: Numerical representation of text in bytes. + :raises ValueError: If text has an invalid value. """ - match = BYTE_REGEX.search(text) + try: + base, reg_ex = UNIT_SYSTEM_INFO[unit_system] + except KeyError: + msg = _('Invalid unit system: "%s"') % unit_system + raise ValueError(msg) + match = reg_ex.match(text) if match: - magnitude = int(match.group(1)) - mult_key_org = match.group(2) - if not mult_key_org: - return magnitude - elif text: + magnitude = float(match.group(1)) + unit_prefix = match.group(2) + if match.group(3) in ['b', 'bit']: + magnitude /= 8 + else: msg = _('Invalid string format: %s') % text - raise TypeError(msg) + raise ValueError(msg) + if not unit_prefix: + res = magnitude else: - return default - mult_key = mult_key_org.lower().replace('b', '', 1) - multiplier = BYTE_MULTIPLIERS.get(mult_key) - if multiplier is None: - msg = _('Unknown byte multiplier: %s') % mult_key_org - raise TypeError(msg) - return magnitude * multiplier + res = magnitude * pow(base, UNIT_PREFIX_EXPONENT[unit_prefix]) + if return_int: + return int(math.ceil(res)) + return res def to_slug(value, incoming=None, errors="strict"): diff --git a/novaclient/openstack/common/timeutils.py b/novaclient/openstack/common/timeutils.py index d5ed81d3e..52688a026 100644 --- a/novaclient/openstack/common/timeutils.py +++ b/novaclient/openstack/common/timeutils.py @@ -114,7 +114,7 @@ def utcnow(): def iso8601_from_timestamp(timestamp): - """Returns a iso8601 formated date from timestamp.""" + """Returns a iso8601 formatted date from timestamp.""" return isotime(datetime.datetime.utcfromtimestamp(timestamp)) diff --git a/openstack-common.conf b/openstack-common.conf index 150814715..e65430638 100644 --- a/openstack-common.conf +++ b/openstack-common.conf @@ -1,12 +1,10 @@ [DEFAULT] # The list of modules to copy from openstack-common -module=install_venv_common module=jsonutils module=strutils module=timeutils module=uuidutils -module=py3kcompat module=apiclient module=importutils module=cliutils From c1d918b2cf223d231d8a52d924c742e04a63714b Mon Sep 17 00:00:00 2001 From: gtt116 Date: Wed, 26 Feb 2014 05:02:22 +0000 Subject: [PATCH 0428/1705] Add service-list show `id` column In nova this patch https://review.openstack.org/#/c/39998/ can show service's id. Then user can use the id to delete service. This patch support show service's id Change-Id: I9fae88743ab7faea5e3fb64643f0bc9171e5b009 Implements: blueprint support-delete-service --- novaclient/v1_1/shell.py | 6 ++++++ novaclient/v3/shell.py | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 24317642c..382a8983c 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2850,6 +2850,12 @@ def do_service_list(cs, args): # so as not to add the column when the extended ext is not enabled. if result and hasattr(result[0], 'disabled_reason'): columns.append("Disabled Reason") + + # NOTE(gtt): After https://review.openstack.org/#/c/39998/ nova will + # show id in response. + if result and hasattr(result[0], 'id'): + columns.insert(0, "Id") + utils.print_list(result, columns) diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 02706c359..792247678 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -2371,6 +2371,12 @@ def do_service_list(cs, args): # so as not to add the column when the extended ext is not enabled. if hasattr(result[0], 'disabled_reason'): columns.append("Disabled Reason") + + # NOTE(gtt): After https://review.openstack.org/#/c/39998/ nova will + # show id in response. + if result and hasattr(result[0], 'id'): + columns.insert(0, "Id") + utils.print_list(result, columns) From 360a3393f4d52746a9e87ac0011cc700a51965ad Mon Sep 17 00:00:00 2001 From: Alan Pevec Date: Thu, 27 Feb 2014 13:12:41 +0100 Subject: [PATCH 0429/1705] Revert "'name' should as be optional param on aggregate-update" This reverts commit a2a1ef8f36644c3487bdaf00b2e70bcc6b948fb4. Change-Id: I43bdc254e391c4b20254b8732c772fc9e728310c Reopens-bug: #1280118 Partial-bug: #1281416 --- novaclient/tests/v1_1/test_shell.py | 28 ++++++---------------------- novaclient/tests/v3/test_shell.py | 28 ++++++---------------------- novaclient/v1_1/shell.py | 12 ++++-------- novaclient/v3/shell.py | 12 ++++-------- 4 files changed, 20 insertions(+), 60 deletions(-) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index feeba4870..a4c4ca19c 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -1149,42 +1149,26 @@ def test_aggregate_delete_by_name(self): self.assert_called('DELETE', '/os-aggregates/1') def test_aggregate_update_by_id(self): - self.run_command('aggregate-update 1 --name new_name') + self.run_command('aggregate-update 1 new_name') body = {"aggregate": {"name": "new_name"}} self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) self.assert_called('GET', '/os-aggregates/1', pos=-1) def test_aggregate_update_by_name(self): - self.run_command('aggregate-update test --name new_name') + self.run_command('aggregate-update test new_name') body = {"aggregate": {"name": "new_name"}} self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) self.assert_called('GET', '/os-aggregates/1', pos=-1) def test_aggregate_update_with_availability_zone_by_id(self): - self.run_command('aggregate-update 1 --availability_zone new_zone') - body = {"aggregate": {"availability_zone": "new_zone"}} + self.run_command('aggregate-update 1 foo new_zone') + body = {"aggregate": {"name": "foo", "availability_zone": "new_zone"}} self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) self.assert_called('GET', '/os-aggregates/1', pos=-1) def test_aggregate_update_with_availability_zone_by_name(self): - self.run_command('aggregate-update test --availability_zone new_zone') - body = {"aggregate": {"availability_zone": "new_zone"}} - self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) - self.assert_called('GET', '/os-aggregates/1', pos=-1) - - def test_aggregate_update_all_parameters_by_id(self): - self.run_command('aggregate-update 1 --name new_name ' - '--availability_zone new_zone') - body = {"aggregate": {"name": "new_name", - "availability_zone": "new_zone"}} - self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) - self.assert_called('GET', '/os-aggregates/1', pos=-1) - - def test_aggregate_update_all_parameters_by_name(self): - self.run_command('aggregate-update test --name new_name ' - '--availability_zone new_zone') - body = {"aggregate": {"name": "new_name", - "availability_zone": "new_zone"}} + self.run_command('aggregate-update test foo new_zone') + body = {"aggregate": {"name": "foo", "availability_zone": "new_zone"}} self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) self.assert_called('GET', '/os-aggregates/1', pos=-1) diff --git a/novaclient/tests/v3/test_shell.py b/novaclient/tests/v3/test_shell.py index 796e73c6c..85a839b49 100644 --- a/novaclient/tests/v3/test_shell.py +++ b/novaclient/tests/v3/test_shell.py @@ -104,42 +104,26 @@ def test_aggregate_delete_by_name(self): self.assert_called('DELETE', '/os-aggregates/1') def test_aggregate_update_by_id(self): - self.run_command('aggregate-update 1 --name new_name') + self.run_command('aggregate-update 1 new_name') body = {"aggregate": {"name": "new_name"}} self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) self.assert_called('GET', '/os-aggregates/1', pos=-1) def test_aggregate_update_by_name(self): - self.run_command('aggregate-update test --name new_name') + self.run_command('aggregate-update test new_name') body = {"aggregate": {"name": "new_name"}} self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) self.assert_called('GET', '/os-aggregates/1', pos=-1) def test_aggregate_update_with_availability_zone_by_id(self): - self.run_command('aggregate-update 1 --availability_zone new_zone') - body = {"aggregate": {"availability_zone": "new_zone"}} + self.run_command('aggregate-update 1 foo new_zone') + body = {"aggregate": {"name": "foo", "availability_zone": "new_zone"}} self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) self.assert_called('GET', '/os-aggregates/1', pos=-1) def test_aggregate_update_with_availability_zone_by_name(self): - self.run_command('aggregate-update test --availability_zone new_zone') - body = {"aggregate": {"availability_zone": "new_zone"}} - self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) - self.assert_called('GET', '/os-aggregates/1', pos=-1) - - def test_aggregate_update_all_parameters_by_id(self): - self.run_command('aggregate-update 1 --name new_name ' - '--availability_zone new_zone') - body = {"aggregate": {"name": "new_name", - "availability_zone": "new_zone"}} - self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) - self.assert_called('GET', '/os-aggregates/1', pos=-1) - - def test_aggregate_update_all_parameters_by_name(self): - self.run_command('aggregate-update test --name new_name ' - '--availability_zone new_zone') - body = {"aggregate": {"name": "new_name", - "availability_zone": "new_zone"}} + self.run_command('aggregate-update test foo new_zone') + body = {"aggregate": {"name": "foo", "availability_zone": "new_zone"}} self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) self.assert_called('GET', '/os-aggregates/1', pos=-1) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 24317642c..c7a520980 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2704,22 +2704,18 @@ def do_aggregate_delete(cs, args): @utils.arg('aggregate', metavar='', help=_('Name or ID of aggregate to update.')) -@utils.arg('--name', metavar='', help=_('New name of aggregate.')) -@utils.arg('--availability_zone', +@utils.arg('name', metavar='', help=_('Name of aggregate.')) +@utils.arg('availability_zone', metavar='', + nargs='?', default=None, help=_('The availability zone of the aggregate.')) def do_aggregate_update(cs, args): """Update the aggregate's name and optionally availability zone.""" aggregate = _find_aggregate(cs, args.aggregate) - updates = {} - if args.name: - updates["name"] = args.name + updates = {"name": args.name} if args.availability_zone: updates["availability_zone"] = args.availability_zone - if not updates: - raise exceptions.CommandError(_("Must supply name " - "or availability_zone.")) aggregate = cs.aggregates.update(aggregate.id, updates) print(_("Aggregate %s has been successfully updated.") % aggregate.id) diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 02706c359..10a6975d8 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -2230,22 +2230,18 @@ def do_aggregate_delete(cs, args): @utils.arg('aggregate', metavar='', help='Name or ID of aggregate to update.') -@utils.arg('--name', metavar='', help='New name of aggregate.') -@utils.arg('--availability_zone', +@utils.arg('name', metavar='', help='Name of aggregate.') +@utils.arg('availability_zone', metavar='', + nargs='?', default=None, help='The availability zone of the aggregate.') def do_aggregate_update(cs, args): """Update the aggregate's name and optionally availability zone.""" aggregate = _find_aggregate(cs, args.aggregate) - updates = {} - if args.name: - updates["name"] = args.name + updates = {"name": args.name} if args.availability_zone: updates["availability_zone"] = args.availability_zone - if not updates: - raise exceptions.CommandError("Must supply name " - "or availability_zone.") aggregate = cs.aggregates.update(aggregate.id, updates) print("Aggregate %s has been successfully updated." % aggregate.id) From 36db3b95f556d5f57a2bf49303b24a0b25b4b7e8 Mon Sep 17 00:00:00 2001 From: Tihomir Trifonov Date: Thu, 20 Feb 2014 23:11:34 +0200 Subject: [PATCH 0430/1705] Fix in in novaclient, to avoid excessive conns The current client creates new .Session() on each request, but since Horizon is a stateless app, each Session creates new HttpAdapter, which itself has its own connection pool, and each connection there is used (almost) once and then is being kept in the pool(with Keep-Alive) for a certain amount of time(waiting for inactivity timeout). The problem is that the connection cannot be used anymore from next Django calls - they create new connection pool with new connections, etc. This keeps lots of open connections on the server. Now the client will store an HTTPAdapter for each URL into a singleton object, and will reuse its connections between Django calls, but still taking advantage of Sessions during a single page load(although we do not fully use this). Note: the default pool behavior is non-blocking, which means that if the max_pool_size is reached, a new connection will still be opened, and when released - will be discarded. It could be useful to add max_pool_size param into settings, for performance fine-tuning. The default max_pool_size is 10. Since python-novaclient is also used from non-Django projects, I'd expect feedback from more people on the impact this change could have over other projects. Patch Set 3: Removed explicit connection closing, leaving connections open in the pool. Change-Id: Icc9dc2fa2863d0e0e26a86c8180f2e0fbcd1fcff Closes-Bug: #1247056 --- novaclient/client.py | 38 +++++++++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index 7a7c06fe6..0b9aeeecc 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -24,6 +24,7 @@ import time import requests +from requests import adapters try: import json @@ -38,8 +39,20 @@ from novaclient import utils -class HTTPClient(object): +_ADAPTERS = {} + + +def _adapter_pool(url): + """ + Store and reuse HTTP adapters per Service URL. + """ + if url not in _ADAPTERS: + _ADAPTERS[url] = adapters.HTTPAdapter() + return _ADAPTERS[url] + + +class HTTPClient(object): USER_AGENT = 'python-novaclient' def __init__(self, user, password, projectid=None, auth_url=None, @@ -105,8 +118,10 @@ def __init__(self, user, password, projectid=None, auth_url=None, self.auth_system = auth_system self.auth_plugin = auth_plugin - + self._current_url = None + self._http = None self._logger = logging.getLogger(__name__) + if self.http_log_debug and not self._logger.handlers: # Logging level is already set on the root logger ch = logging.StreamHandler() @@ -119,8 +134,6 @@ def __init__(self, user, password, projectid=None, auth_url=None, # have to set it up here on WARNING (its original level) # otherwise we will get all the requests logging messages rql.setLevel(logging.WARNING) - # requests within the same session can reuse TCP connections from pool - self.http = requests.Session() def use_token_cache(self, use_it): self.os_cache = use_it @@ -167,6 +180,20 @@ def http_log_resp(self, resp): 'headers': resp.headers, 'text': resp.text}) + def http(self, url): + magic_tuple = parse.urlsplit(url) + scheme, netloc, path, query, frag = magic_tuple + service_url = '%s://%s' % (scheme, netloc) + if self._current_url != service_url: + # Invalidate Session object in case the url is somehow changed + if self._http: + self._http.close() + self._current_url = service_url + self._logger.debug("New session created for: (%s)" % service_url) + self._http = requests.Session() + self._http.mount(service_url, _adapter_pool(service_url)) + return self._http + def request(self, url, method, **kwargs): kwargs.setdefault('headers', kwargs.get('headers', {})) kwargs['headers']['User-Agent'] = self.USER_AGENT @@ -180,10 +207,11 @@ def request(self, url, method, **kwargs): kwargs['verify'] = self.verify_cert self.http_log_req(method, url, kwargs) - resp = self.http.request( + resp = self.http(url).request( method, url, **kwargs) + self.http_log_resp(resp) if resp.text: From 64043442bbafa48f9042b669d30292b1db00db4f Mon Sep 17 00:00:00 2001 From: Joe Gordon Date: Wed, 26 Feb 2014 21:00:23 -0800 Subject: [PATCH 0431/1705] oslo sync apiclient and cliutils Generated with: ./update.sh --base novaclient --config-file ../python-novaclient/openstack-common.conf --dest-dir ../python-novaclient/ Synced patches: apiclient: 04a1abe Revert "Removed set_loaded() method from Resource class" 5e76477 Merge "Handle 300 status code in common HTTPClient" 492fe2c Merge "py3kcompat: remove" 466ad64 Merge "Fix usage of dict.keys in apiclient.exceptions" 8630a44 Merge "Removed set_loaded() method from Resource class" 41fbfea Merge "Add to_dict() method to apiclient Resource" 6650435 Fix usage of dict.keys in apiclient.exceptions 35dc1d7 py3kcompat: remove 9f1e7eb Correct docstring in load_plugin_from_args 0c4d2c7 Removed set_loaded() method from Resource class 3b248dd Add to_dict() method to apiclient Resource 71c22e9 Handle 300 status code in common HTTPClient cliutils: 9a7f2f8 Merge "Deleted duplicated method in cliutils." 8f2effd Use `six.text_type` instead of `str` in cliutils 885828a Deleted duplicated method in cliutils. 71a2d90 Add common methods to cliutils Change-Id: I0c8849d8d5dd71f34aa5dbcd2c0875c164706d70 --- .../openstack/common/apiclient/__init__.py | 14 --- novaclient/openstack/common/apiclient/auth.py | 6 +- novaclient/openstack/common/apiclient/base.py | 15 ++- .../openstack/common/apiclient/exceptions.py | 24 ++++- .../openstack/common/apiclient/fake_client.py | 7 +- novaclient/openstack/common/cliutils.py | 102 +++++++++++++++++- 6 files changed, 137 insertions(+), 31 deletions(-) diff --git a/novaclient/openstack/common/apiclient/__init__.py b/novaclient/openstack/common/apiclient/__init__.py index f3d0cdefd..e69de29bb 100644 --- a/novaclient/openstack/common/apiclient/__init__.py +++ b/novaclient/openstack/common/apiclient/__init__.py @@ -1,14 +0,0 @@ -# Copyright 2013 OpenStack Foundation -# All Rights Reserved. -# -# 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. diff --git a/novaclient/openstack/common/apiclient/auth.py b/novaclient/openstack/common/apiclient/auth.py index 81af9aa0f..ee1348afe 100644 --- a/novaclient/openstack/common/apiclient/auth.py +++ b/novaclient/openstack/common/apiclient/auth.py @@ -19,7 +19,6 @@ import abc import argparse -import logging import os import six @@ -28,9 +27,6 @@ from novaclient.openstack.common.apiclient import exceptions -logger = logging.getLogger(__name__) - - _discovered_plugins = {} @@ -80,7 +76,7 @@ def load_plugin_from_args(args): alphabetical order. :type args: argparse.Namespace - :raises: AuthorizationFailure + :raises: AuthPluginOptionsMissing """ auth_system = args.os_auth_system if auth_system: diff --git a/novaclient/openstack/common/apiclient/base.py b/novaclient/openstack/common/apiclient/base.py index dcb4a000c..647f4a720 100644 --- a/novaclient/openstack/common/apiclient/base.py +++ b/novaclient/openstack/common/apiclient/base.py @@ -24,11 +24,12 @@ # pylint: disable=E1102 import abc +import copy import six +from six.moves.urllib import parse from novaclient.openstack.common.apiclient import exceptions -from novaclient.openstack.common.py3kcompat import urlutils from novaclient.openstack.common import strutils @@ -327,7 +328,7 @@ def list(self, base_url=None, **kwargs): return self._list( '%(base_url)s%(query)s' % { 'base_url': self.build_url(base_url=base_url, **kwargs), - 'query': '?%s' % urlutils.urlencode(kwargs) if kwargs else '', + 'query': '?%s' % parse.urlencode(kwargs) if kwargs else '', }, self.collection_key) @@ -366,7 +367,7 @@ def find(self, base_url=None, **kwargs): rl = self._list( '%(base_url)s%(query)s' % { 'base_url': self.build_url(base_url=base_url, **kwargs), - 'query': '?%s' % urlutils.urlencode(kwargs) if kwargs else '', + 'query': '?%s' % parse.urlencode(kwargs) if kwargs else '', }, self.collection_key) num = len(rl) @@ -465,6 +466,11 @@ def __getattr__(self, k): return self.__dict__[k] def get(self): + """Support for lazy loading details. + + Some clients, such as novaclient have the option to lazy load the + details, details which can be loaded with this function. + """ # set_loaded() first ... so if we have to bail, we know we tried. self.set_loaded(True) if not hasattr(self.manager, 'get'): @@ -489,3 +495,6 @@ def is_loaded(self): def set_loaded(self, val): self._loaded = val + + def to_dict(self): + return copy.deepcopy(self._info) diff --git a/novaclient/openstack/common/apiclient/exceptions.py b/novaclient/openstack/common/apiclient/exceptions.py index 45a70e0a7..ada1344f5 100644 --- a/novaclient/openstack/common/apiclient/exceptions.py +++ b/novaclient/openstack/common/apiclient/exceptions.py @@ -60,6 +60,11 @@ class AuthorizationFailure(ClientException): pass +class ConnectionRefused(ClientException): + """Cannot connect to API service.""" + pass + + class AuthPluginOptionsMissing(AuthorizationFailure): """Auth plugin misses some options.""" def __init__(self, opt_names): @@ -122,6 +127,11 @@ def __init__(self, message=None, details=None, super(HttpError, self).__init__(formatted_string) +class HTTPRedirection(HttpError): + """HTTP Redirection.""" + message = "HTTP Redirection" + + class HTTPClientError(HttpError): """Client-side HTTP error. @@ -139,6 +149,16 @@ class HttpServerError(HttpError): message = "HTTP Server Error" +class MultipleChoices(HTTPRedirection): + """HTTP 300 - Multiple Choices. + + Indicates multiple options for the resource that the client may follow. + """ + + http_status = 300 + message = "Multiple Choices" + + class BadRequest(HTTPClientError): """HTTP 400 - Bad Request. @@ -420,8 +440,8 @@ def from_response(response, method, url): except ValueError: pass else: - if hasattr(body, "keys"): - error = body[body.keys()[0]] + if isinstance(body, dict): + error = list(body.values())[0] kwargs["message"] = error.get("message") kwargs["details"] = error.get("details") elif content_type.startswith("text/"): diff --git a/novaclient/openstack/common/apiclient/fake_client.py b/novaclient/openstack/common/apiclient/fake_client.py index 05f550b91..cdb3cc1c9 100644 --- a/novaclient/openstack/common/apiclient/fake_client.py +++ b/novaclient/openstack/common/apiclient/fake_client.py @@ -28,10 +28,9 @@ import requests import six +from six.moves.urllib import parse from novaclient.openstack.common.apiclient import client -from novaclient.openstack.common.py3kcompat import urlutils -from novaclient.openstack.common import strutils def assert_has_keys(dct, required=[], optional=[]): @@ -64,7 +63,7 @@ def __init__(self, data): self._content = text default_headers = {} if six.PY3 and isinstance(self._content, six.string_types): - self._content = strutils.safe_encode(self._content) + self._content = self._content.encode('utf-8', 'strict') self.headers = data.get('headers') or default_headers else: self.status_code = data @@ -148,7 +147,7 @@ def client_request(self, client, method, url, **kwargs): "text": fixture[1]}) # Call the method - args = urlutils.parse_qsl(urlutils.urlparse(url)[4]) + args = parse.parse_qsl(parse.urlparse(url)[4]) kwargs.update(args) munged_url = url.rsplit('?', 1)[0] munged_url = munged_url.strip('/').replace('/', '_').replace('.', '_') diff --git a/novaclient/openstack/common/cliutils.py b/novaclient/openstack/common/cliutils.py index 710f8f8fb..6a96da57b 100644 --- a/novaclient/openstack/common/cliutils.py +++ b/novaclient/openstack/common/cliutils.py @@ -16,6 +16,8 @@ # W0621: Redefining name %s from outer scope # pylint: disable=W0603,W0621 +from __future__ import print_function + import getpass import inspect import os @@ -27,7 +29,9 @@ from six import moves from novaclient.openstack.common.apiclient import exceptions +from novaclient.openstack.common.gettextutils import _ from novaclient.openstack.common import strutils +from novaclient.openstack.common import uuidutils def validate_args(fn, *args, **kwargs): @@ -176,9 +180,9 @@ def print_dict(dct, dict_property="Property", wrap=0): for k, v in six.iteritems(dct): # convert dict to str to check length if isinstance(v, dict): - v = str(v) + v = six.text_type(v) if wrap > 0: - v = textwrap.fill(str(v), wrap) + v = textwrap.fill(six.text_type(v), wrap) # if value has a newline, add in multiple rows # e.g. fault with stacktrace if v and isinstance(v, six.string_types) and r'\n' in v: @@ -199,7 +203,7 @@ def get_password(max_password_prompts=3): if hasattr(sys.stdin, "isatty") and sys.stdin.isatty(): # Check for Ctrl-D try: - for _ in moves.range(max_password_prompts): + for __ in moves.range(max_password_prompts): pw1 = getpass.getpass("OS Password: ") if verify: pw2 = getpass.getpass("Please verify: ") @@ -211,3 +215,95 @@ def get_password(max_password_prompts=3): except EOFError: pass return pw + + +def find_resource(manager, name_or_id, **find_args): + """Look for resource in a given manager. + + Used as a helper for the _find_* methods. + Example: + + def _find_hypervisor(cs, hypervisor): + #Get a hypervisor by name or ID. + return cliutils.find_resource(cs.hypervisors, hypervisor) + """ + # first try to get entity as integer id + try: + return manager.get(int(name_or_id)) + except (TypeError, ValueError, exceptions.NotFound): + pass + + # now try to get entity as uuid + try: + tmp_id = strutils.safe_encode(name_or_id) + + if uuidutils.is_uuid_like(tmp_id): + return manager.get(tmp_id) + except (TypeError, ValueError, exceptions.NotFound): + pass + + # for str id which is not uuid + if getattr(manager, 'is_alphanum_id_allowed', False): + try: + return manager.get(name_or_id) + except exceptions.NotFound: + pass + + try: + try: + return manager.find(human_id=name_or_id, **find_args) + except exceptions.NotFound: + pass + + # finally try to find entity by name + try: + resource = getattr(manager, 'resource_class', None) + name_attr = resource.NAME_ATTR if resource else 'name' + kwargs = {name_attr: name_or_id} + kwargs.update(find_args) + return manager.find(**kwargs) + except exceptions.NotFound: + msg = _("No %(name)s with a name or " + "ID of '%(name_or_id)s' exists.") % \ + { + "name": manager.resource_class.__name__.lower(), + "name_or_id": name_or_id + } + raise exceptions.CommandError(msg) + except exceptions.NoUniqueMatch: + msg = _("Multiple %(name)s matches found for " + "'%(name_or_id)s', use an ID to be more specific.") % \ + { + "name": manager.resource_class.__name__.lower(), + "name_or_id": name_or_id + } + raise exceptions.CommandError(msg) + + +def service_type(stype): + """Adds 'service_type' attribute to decorated function. + + Usage: + @service_type('volume') + def mymethod(f): + ... + """ + def inner(f): + f.service_type = stype + return f + return inner + + +def get_service_type(f): + """Retrieves service type from function.""" + return getattr(f, 'service_type', None) + + +def pretty_choice_list(l): + return ', '.join("'%s'" % i for i in l) + + +def exit(msg=''): + if msg: + print (msg, file=sys.stderr) + sys.exit(1) From 1023c85906e2113b91ee1a85836833b66a4c6ba0 Mon Sep 17 00:00:00 2001 From: Joe Gordon Date: Fri, 28 Feb 2014 10:11:12 -0800 Subject: [PATCH 0432/1705] Re-add install_venv_common to openstack-common.conf Mistakenly removed it in e0272b0578d2a4f5bd75e68baee1b56f49931266. Was thrown off by the warning saying module not found, the module is found on the third try (found in tools). Change-Id: Iaf5a7ba0259d352cb824b2cdb22291ffb7883efc --- openstack-common.conf | 1 + 1 file changed, 1 insertion(+) diff --git a/openstack-common.conf b/openstack-common.conf index e65430638..9b72a78e9 100644 --- a/openstack-common.conf +++ b/openstack-common.conf @@ -1,6 +1,7 @@ [DEFAULT] # The list of modules to copy from openstack-common +module=install_venv_common module=jsonutils module=strutils module=timeutils From 7c11b06bd4e896d891ed3f50c6480bd76a1023bf Mon Sep 17 00:00:00 2001 From: Masayuki Igawa Date: Mon, 3 Mar 2014 14:30:56 +0900 Subject: [PATCH 0433/1705] Remove quota-class subcommand quota-classes API was already removed. Change-Id: I1110022d6f628d03aaf363da707f2d2ef1600437 This patch removed quota-class subcommand from nova client. Change-Id: I18bf7c255fabdb52c8ce8159f68c3e5c70e54993 --- novaclient/tests/v1_1/fakes.py | 50 ------------- novaclient/tests/v1_1/test_quota_classes.py | 42 ----------- novaclient/tests/v1_1/test_shell.py | 11 --- novaclient/tests/v3/test_quota_classes.py | 25 ------- novaclient/v1_1/client.py | 2 - novaclient/v1_1/quota_classes.py | 44 ----------- novaclient/v1_1/shell.py | 82 --------------------- novaclient/v3/client.py | 2 - novaclient/v3/quota_classes.py | 23 ------ novaclient/v3/shell.py | 43 ----------- 10 files changed, 324 deletions(-) delete mode 100644 novaclient/tests/v1_1/test_quota_classes.py delete mode 100644 novaclient/tests/v3/test_quota_classes.py delete mode 100644 novaclient/v1_1/quota_classes.py delete mode 100644 novaclient/v3/quota_classes.py diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index 44330550d..6795501dd 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -1161,56 +1161,6 @@ def delete_os_quota_sets_test(self, **kw): def delete_os_quota_sets_97f4c221bff44578b0300df4ef119353(self, **kw): return (202, {}, {}) - # - # Quota Classes - # - - def get_os_quota_class_sets_test(self, **kw): - return (200, {}, {'quota_class_set': { - 'id': 'test', - 'metadata_items': 1, - 'injected_file_content_bytes': 1, - 'injected_file_path_bytes': 1, - 'ram': 1, - 'floating_ips': 1, - 'instances': 1, - 'injected_files': 1, - 'cores': 1, - 'key_pairs': 1, - 'security_groups': 1, - 'security_group_rules': 1}}) - - def put_os_quota_class_sets_test(self, body, **kw): - assert list(body) == ['quota_class_set'] - return (200, {}, {'quota_class_set': { - 'metadata_items': 1, - 'injected_file_content_bytes': 1, - 'injected_file_path_bytes': 1, - 'ram': 1, - 'floating_ips': 1, - 'instances': 1, - 'injected_files': 1, - 'cores': 1, - 'key_pairs': 1, - 'security_groups': 1, - 'security_group_rules': 1}}) - - def put_os_quota_class_sets_97f4c221bff44578b0300df4ef119353(self, - body, **kw): - assert list(body) == ['quota_class_set'] - return (200, {}, {'quota_class_set': { - 'metadata_items': 1, - 'injected_file_content_bytes': 1, - 'injected_file_path_bytes': 1, - 'ram': 1, - 'floating_ips': 1, - 'instances': 1, - 'injected_files': 1, - 'cores': 1, - 'key_pairs': 1, - 'security_groups': 1, - 'security_group_rules': 1}}) - # # Security Groups # diff --git a/novaclient/tests/v1_1/test_quota_classes.py b/novaclient/tests/v1_1/test_quota_classes.py deleted file mode 100644 index 338549bfa..000000000 --- a/novaclient/tests/v1_1/test_quota_classes.py +++ /dev/null @@ -1,42 +0,0 @@ -# Copyright 2011 OpenStack Foundation -# All Rights Reserved. -# -# 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 novaclient.tests import utils -from novaclient.tests.v1_1 import fakes - - -cs = fakes.FakeClient() - - -class QuotaClassSetsTest(utils.TestCase): - - def test_class_quotas_get(self): - class_name = 'test' - cs.quota_classes.get(class_name) - cs.assert_called('GET', '/os-quota-class-sets/%s' % class_name) - - def test_update_quota(self): - q = cs.quota_classes.get('test') - q.update(cores=2) - cs.assert_called('PUT', '/os-quota-class-sets/test') - - def test_refresh_quota(self): - q = cs.quota_classes.get('test') - q2 = cs.quota_classes.get('test') - self.assertEqual(q.cores, q2.cores) - q2.cores = 0 - self.assertNotEqual(q.cores, q2.cores) - q2.get() - self.assertEqual(q.cores, q2.cores) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index a4c4ca19c..836e5c631 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -1507,17 +1507,6 @@ def test_user_quota_delete(self): self.assert_called('DELETE', '/os-quota-sets/97f4c221bff44578b0300df4ef119353?user_id=u1') - def test_quota_class_show(self): - self.run_command('quota-class-show test') - self.assert_called('GET', '/os-quota-class-sets/test') - - def test_quota_class_update(self): - self.run_command('quota-class-update 97f4c221bff44578b0300df4ef119353' - ' --instances=5') - self.assert_called('PUT', - '/os-quota-class-sets/97f4c221bff44578b0300' - 'df4ef119353') - def test_network_list(self): self.run_command('network-list') self.assert_called('GET', '/os-networks') diff --git a/novaclient/tests/v3/test_quota_classes.py b/novaclient/tests/v3/test_quota_classes.py deleted file mode 100644 index 2e0ceb9f0..000000000 --- a/novaclient/tests/v3/test_quota_classes.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright IBM Corp. 2013 -# -# 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 novaclient.tests.v1_1 import test_quota_classes -from novaclient.tests.v3 import fakes - - -class QuotaClassSetsTest(test_quota_classes.QuotaClassSetsTest): - def setUp(self): - super(QuotaClassSetsTest, self).setUp() - self.cs = self._get_fake_client() - - def _get_fake_client(self): - return fakes.FakeClient() diff --git a/novaclient/v1_1/client.py b/novaclient/v1_1/client.py index efeb5c993..e0a72373d 100644 --- a/novaclient/v1_1/client.py +++ b/novaclient/v1_1/client.py @@ -33,7 +33,6 @@ from novaclient.v1_1 import keypairs from novaclient.v1_1 import limits from novaclient.v1_1 import networks -from novaclient.v1_1 import quota_classes from novaclient.v1_1 import quotas from novaclient.v1_1 import security_group_rules from novaclient.v1_1 import security_groups @@ -99,7 +98,6 @@ def __init__(self, username, api_key, project_id, auth_url=None, self.volume_types = volume_types.VolumeTypeManager(self) self.keypairs = keypairs.KeypairManager(self) self.networks = networks.NetworkManager(self) - self.quota_classes = quota_classes.QuotaClassSetManager(self) self.quotas = quotas.QuotaSetManager(self) self.security_groups = security_groups.SecurityGroupManager(self) self.security_group_rules = \ diff --git a/novaclient/v1_1/quota_classes.py b/novaclient/v1_1/quota_classes.py deleted file mode 100644 index 4a38a970c..000000000 --- a/novaclient/v1_1/quota_classes.py +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright 2012 OpenStack Foundation -# All Rights Reserved. -# -# 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 novaclient import base - - -class QuotaClassSet(base.Resource): - - def update(self, *args, **kwargs): - return self.manager.update(self.id, *args, **kwargs) - - -class QuotaClassSetManager(base.Manager): - resource_class = QuotaClassSet - - def get(self, class_name): - return self._get("/os-quota-class-sets/%s" % (class_name), - "quota_class_set") - - def _update_body(self, **kwargs): - return {'quota_class_set': kwargs} - - def update(self, class_name, **kwargs): - body = self._update_body(**kwargs) - - for key in list(body['quota_class_set']): - if body['quota_class_set'][key] is None: - body['quota_class_set'].pop(key) - - return self._update('/os-quota-class-sets/%s' % (class_name), - body, - 'quota_class_set') diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index c7a520980..122cea8bd 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -3286,88 +3286,6 @@ def do_quota_delete(cs, args): cs.quotas.delete(args.tenant, user_id=args.user) -@utils.arg('class_name', - metavar='', - help=_('Name of quota class to list the quotas for.')) -def do_quota_class_show(cs, args): - """List the quotas for a quota class.""" - - _quota_show(cs.quota_classes.get(args.class_name)) - - -@utils.arg('class_name', - metavar='', - help=_('Name of quota class to set the quotas for.')) -@utils.arg('--instances', - metavar='', - type=int, default=None, - help=_('New value for the "instances" quota.')) -@utils.arg('--cores', - metavar='', - type=int, default=None, - help=_('New value for the "cores" quota.')) -@utils.arg('--ram', - metavar='', - type=int, default=None, - help=_('New value for the "ram" quota.')) -@utils.arg('--floating-ips', - metavar='', - type=int, - default=None, - help=_('New value for the "floating-ips" quota.')) -@utils.arg('--floating_ips', - type=int, - help=argparse.SUPPRESS) -@utils.arg('--metadata-items', - metavar='', - type=int, - default=None, - help=_('New value for the "metadata-items" quota.')) -@utils.arg('--metadata_items', - type=int, - help=argparse.SUPPRESS) -@utils.arg('--injected-files', - metavar='', - type=int, - default=None, - help=_('New value for the "injected-files" quota.')) -@utils.arg('--injected_files', - type=int, - help=argparse.SUPPRESS) -@utils.arg('--injected-file-content-bytes', - metavar='', - type=int, - default=None, - help=_('New value for the "injected-file-content-bytes" quota.')) -@utils.arg('--injected_file_content_bytes', - type=int, - help=argparse.SUPPRESS) -@utils.arg('--injected-file-path-bytes', - metavar='', - type=int, - default=None, - help=_('New value for the "injected-file-path-bytes" quota.')) -@utils.arg('--key-pairs', - metavar='', - type=int, - default=None, - help=_('New value for the "key-pairs" quota.')) -@utils.arg('--security-groups', - metavar='', - type=int, - default=None, - help=_('New value for the "security-groups" quota.')) -@utils.arg('--security-group-rules', - metavar='', - type=int, - default=None, - help=_('New value for the "security-group-rules" quota.')) -def do_quota_class_update(cs, args): - """Update the quotas for a quota class.""" - - _quota_update(cs.quota_classes, args.class_name, args) - - @utils.arg('server', metavar='', help=_('Name or ID of server.')) @utils.arg('host', metavar='', help=_('Name or ID of target host.')) @utils.arg('--password', diff --git a/novaclient/v3/client.py b/novaclient/v3/client.py index f26fdcf99..b3af4cb78 100644 --- a/novaclient/v3/client.py +++ b/novaclient/v3/client.py @@ -24,7 +24,6 @@ from novaclient.v3 import hypervisors from novaclient.v3 import images from novaclient.v3 import keypairs -from novaclient.v3 import quota_classes from novaclient.v3 import quotas from novaclient.v3 import servers from novaclient.v3 import services @@ -76,7 +75,6 @@ def __init__(self, username, password, project_id, auth_url=None, self.images = images.ImageManager(self) self.keypairs = keypairs.KeypairManager(self) self.quotas = quotas.QuotaSetManager(self) - self.quota_classes = quota_classes.QuotaClassSetManager(self) self.servers = servers.ServerManager(self) self.services = services.ServiceManager(self) self.usage = usage.UsageManager(self) diff --git a/novaclient/v3/quota_classes.py b/novaclient/v3/quota_classes.py deleted file mode 100644 index e12209eb1..000000000 --- a/novaclient/v3/quota_classes.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright IBM Corp. 2013 -# -# 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 novaclient.v1_1 import quota_classes - - -class QuotaClassSet(quota_classes.QuotaClassSet): - pass - - -class QuotaClassSetManager(quota_classes.QuotaClassSetManager): - resource_class = QuotaClassSet diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 10a6975d8..b9240905a 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -2777,49 +2777,6 @@ def do_quota_delete(cs, args): cs.quotas.delete(args.tenant) -@utils.arg('class_name', - metavar='', - help='Name of quota class to list the quotas for.') -def do_quota_class_show(cs, args): - """List the quotas for a quota class.""" - - _quota_show(cs.quota_classes.get(args.class_name)) - - -@utils.arg('class_name', - metavar='', - help='Name of quota class to set the quotas for.') -@utils.arg('--instances', - metavar='', - type=int, default=None, - help='New value for the "instances" quota.') -@utils.arg('--cores', - metavar='', - type=int, default=None, - help='New value for the "cores" quota.') -@utils.arg('--ram', - metavar='', - type=int, default=None, - help='New value for the "ram" quota.') -@utils.arg('--metadata-items', - metavar='', - type=int, - default=None, - help='New value for the "metadata-items" quota.') -@utils.arg('--metadata_items', - type=int, - help=argparse.SUPPRESS) -@utils.arg('--key-pairs', - metavar='', - type=int, - default=None, - help='New value for the "key-pairs" quota.') -def do_quota_class_update(cs, args): - """Update the quotas for a quota class.""" - - _quota_update(cs.quota_classes, args.class_name, args) - - @utils.arg('server', metavar='', help='Name or ID of server.') @utils.arg('host', metavar='', help='Name or ID of target host.') @utils.arg('--password', From 2f8af3de76146c908f0247a41017fee0d82a3994 Mon Sep 17 00:00:00 2001 From: Cyril Roelandt Date: Mon, 24 Feb 2014 20:18:30 +0100 Subject: [PATCH 0434/1705] Add classifiers for specific versions of Python These are used by automated tools, such as caniusepython3 (https://github.com/brettcannon/caniusepython3) to check on what version of Python the Nova client works. Change-Id: Ie2a652a8c4797b1e1a69c91673e6009f36772667 --- setup.cfg | 3 +++ 1 file changed, 3 insertions(+) diff --git a/setup.cfg b/setup.cfg index 3513c3bf5..66349f7a8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,6 +16,9 @@ classifier = License :: OSI Approved :: Apache Software License Operating System :: OS Independent Programming Language :: Python + Programming Language :: Python :: 2.6 + Programming Language :: Python :: 2.7 + Programming Language :: Python :: 3.3 [files] packages = From e385cc47168a8b346a78098c2570dc5cd8cd4974 Mon Sep 17 00:00:00 2001 From: shihanzhang Date: Thu, 6 Mar 2014 09:29:07 +0800 Subject: [PATCH 0435/1705] Fix some spelling mistakes Change-Id: I8da3b73d108b11dc5bb3080215f16486fec95333 --- novaclient/tests/v1_1/test_shell.py | 2 +- novaclient/tests/v3/test_shell.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index a4c4ca19c..5a1299fe1 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -497,7 +497,7 @@ def test_boot_nics(self): }, ) - def tets_boot_nics_no_value(self): + def test_boot_nics_no_value(self): cmd = ('boot --image 1 --flavor 1 ' '--nic net-id some-server') self.assertRaises(exceptions.CommandError, self.run_command, cmd) diff --git a/novaclient/tests/v3/test_shell.py b/novaclient/tests/v3/test_shell.py index 85a839b49..d3300bf70 100644 --- a/novaclient/tests/v3/test_shell.py +++ b/novaclient/tests/v3/test_shell.py @@ -428,7 +428,7 @@ def test_boot_nics(self): }, ) - def tets_boot_nics_no_value(self): + def test_boot_nics_no_value(self): cmd = ('boot --image 1 --flavor 1 ' '--nic net-id some-server') self.assertRaises(exceptions.CommandError, self.run_command, cmd) From 04a123cdee0644e2319e7d50f9f2bf1105199ba7 Mon Sep 17 00:00:00 2001 From: Dan Smith Date: Wed, 19 Feb 2014 08:07:56 -0800 Subject: [PATCH 0436/1705] Add os-server-external-events support This adds support for the os-server-external-events extension in nova, which allows other services to deliver events to nova. It also adds a shell command to trigger the "network-changed" event manually, which will cause nova to refresh its network cache from neutron. Related to blueprint admin-event-callback-api Change-Id: I1a302a43b6b7a6d8bdc03965a8f4c1a151bcab88 --- README.rst | 1 + novaclient/tests/v1_1/contrib/fakes.py | 11 +++++ .../contrib/test_server_external_events.py | 44 +++++++++++++++++++ novaclient/tests/v1_1/fakes.py | 5 +++ novaclient/tests/v1_1/test_shell.py | 6 +++ .../v1_1/contrib/server_external_events.py | 43 ++++++++++++++++++ novaclient/v1_1/shell.py | 10 +++++ 7 files changed, 120 insertions(+) create mode 100644 novaclient/tests/v1_1/contrib/test_server_external_events.py create mode 100644 novaclient/v1_1/contrib/server_external_events.py diff --git a/README.rst b/README.rst index 30131e1cd..b155bf54e 100644 --- a/README.rst +++ b/README.rst @@ -146,6 +146,7 @@ You'll find complete documentation on the shell by running rate-limits Print a list of rate limits for a user reboot Reboot a server. rebuild Shutdown, re-image, and re-boot a server. + refresh-network Refresh server network information. remove-fixed-ip Remove an IP address from a server. remove-floating-ip Remove a floating IP address from a server. rename Rename a server. diff --git a/novaclient/tests/v1_1/contrib/fakes.py b/novaclient/tests/v1_1/contrib/fakes.py index fd25910a5..3b859362e 100644 --- a/novaclient/tests/v1_1/contrib/fakes.py +++ b/novaclient/tests/v1_1/contrib/fakes.py @@ -134,3 +134,14 @@ def post_os_assisted_volume_snapshots(self, **kw): def delete_os_assisted_volume_snapshots_x(self, **kw): return (202, {}, {}) + + def post_os_server_external_events(self, **kw): + return (200, {}, {'events': [ + {'name': 'test-event', + 'status': 'completed', + 'tag': 'tag', + 'server_uuid': 'fake-uuid1'}, + {'name': 'test-event', + 'status': 'completed', + 'tag': 'tag', + 'server_uuid': 'fake-uuid2'}]}) diff --git a/novaclient/tests/v1_1/contrib/test_server_external_events.py b/novaclient/tests/v1_1/contrib/test_server_external_events.py new file mode 100644 index 000000000..c92ba34e0 --- /dev/null +++ b/novaclient/tests/v1_1/contrib/test_server_external_events.py @@ -0,0 +1,44 @@ +# Copyright (C) 2014, Red Hat, Inc. +# +# 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. + +""" +External event triggering for servers, not to be used by users. +""" + +from novaclient import extension +from novaclient.tests import utils +from novaclient.tests.v1_1.contrib import fakes +from novaclient.v1_1.contrib import server_external_events as ext_events + + +extensions = [ + extension.Extension(ext_events.__name__.split(".")[-1], + ext_events), +] +cs = fakes.FakeClient(extensions=extensions) + + +class ServerExternalEventsTestCase(utils.TestCase): + def test_external_event(self): + events = [{'server_uuid': 'fake-uuid1', + 'name': 'test-event', + 'status': 'completed', + 'tag': 'tag'}, + {'server_uuid': 'fake-uuid2', + 'name': 'test-event', + 'status': 'completed', + 'tag': 'tag'}] + result = cs.server_external_events.create(events) + self.assertEqual(events, result) + cs.assert_called('POST', '/os-server-external-events') diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index 3604312b3..43caf367e 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -1991,3 +1991,8 @@ def get_os_migrations(self, **kw): "updated_at": "2012-10-29T13:42:02.000000" }]} return (200, {}, migrations) + + def post_os_server_external_events(self, **kw): + return (200, {}, {'events': [ + {'name': 'network-changed', + 'server_uuid': '1234'}]}) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index a4c4ca19c..580d2ed55 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -946,6 +946,12 @@ def test_diagnostics(self): self.run_command('diagnostics sample-server') self.assert_called('GET', '/servers/1234/diagnostics') + def test_refresh_network(self): + self.run_command('refresh-network 1234') + self.assert_called('POST', '/os-server-external-events', + {'events': [{'name': 'network-changed', + 'server_uuid': 1234}]}) + def test_set_meta_set(self): self.run_command('meta 1234 set key1=val1 key2=val2') self.assert_called('POST', '/servers/1234/metadata', diff --git a/novaclient/v1_1/contrib/server_external_events.py b/novaclient/v1_1/contrib/server_external_events.py new file mode 100644 index 000000000..a45914b55 --- /dev/null +++ b/novaclient/v1_1/contrib/server_external_events.py @@ -0,0 +1,43 @@ +# Copyright (C) 2014, Red Hat, Inc. +# +# 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. + +""" +External event triggering for servers, not to be used by users. +""" + +from novaclient import base + + +class Event(base.Resource): + def __repr__(self): + return "" % self.name + + +class ServerExternalEventManager(base.Manager): + resource_class = Event + + def create(self, events): + """Create one or more server events. + + :param:events: A list of dictionaries containing 'server_uuid', 'name', + 'status', and 'tag' (which may be absent) + """ + + body = {'events': events} + return self._create('/os-server-external-events', body, 'events', + return_raw=True) + + +manager_class = ServerExternalEventManager +name = 'server_external_events' diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 5f0858b55..ea0d4dff9 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1366,6 +1366,16 @@ def do_diagnostics(cs, args): utils.print_dict(cs.servers.diagnostics(server)[1], wrap=80) +@utils.arg('server', metavar='', + help=_('Name or ID of a server for which the network cache should ' + 'be refreshed from neutron (Admin only).')) +def do_refresh_network(cs, args): + """Refresh server network information.""" + server = _find_server(cs, args.server) + cs.server_external_events.create([{'server_uuid': server.id, + 'name': 'network-changed'}]) + + @utils.arg('server', metavar='', help=_('Name or ID of server.')) def do_root_password(cs, args): """ From dd8bde71ff094b55e9b002ec3de0c152fde843f6 Mon Sep 17 00:00:00 2001 From: Phil Day Date: Thu, 6 Mar 2014 12:37:12 +0000 Subject: [PATCH 0437/1705] Allow user ID for authentication In Keystone V3 user names are no longer necessarily unique accross domains. A user can still authenticate a user in the non default domain via the V2 API providng they use IDs instead of names. Tenant_ID is already supported, this change adds support for user ID Change-Id: I36ba75f3e67c8cdb959e31923d5e557414ab6f9b --- novaclient/client.py | 7 ++++++- novaclient/shell.py | 16 ++++++++++++---- novaclient/tests/test_shell.py | 25 +++++++++++++++++++------ novaclient/v1_1/client.py | 4 +++- novaclient/v3/client.py | 4 +++- 5 files changed, 43 insertions(+), 13 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index 0b9aeeecc..ecb755065 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -64,8 +64,9 @@ def __init__(self, user, password, projectid=None, auth_url=None, os_cache=False, no_cache=True, http_log_debug=False, auth_system='keystone', auth_plugin=None, auth_token=None, - cacert=None, tenant_id=None): + cacert=None, tenant_id=None, user_id=None): self.user = user + self.user_id = user_id self.password = password self.projectid = projectid self.tenant_id = tenant_id @@ -456,6 +457,10 @@ def _v2_auth(self, url): if self.auth_token: body = {"auth": { "token": {"id": self.auth_token}}} + elif self.user_id: + body = {"auth": { + "passwordCredentials": {"userId": self.user_id, + "password": self._get_password()}}} else: body = {"auth": { "passwordCredentials": {"username": self.user, diff --git a/novaclient/shell.py b/novaclient/shell.py index 919aa21ba..fcbf83e3d 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -285,6 +285,11 @@ def get_base_parser(self): parser.add_argument('--os_username', help=argparse.SUPPRESS) + parser.add_argument('--os-user-id', + metavar='', + default=utils.env('OS_USER_ID'), + help=_('Defaults to env[OS_USER_ID].')) + parser.add_argument('--os-password', metavar='', default=utils.env('OS_PASSWORD', 'NOVA_PASSWORD'), @@ -550,6 +555,7 @@ def main(self, argv): return 0 os_username = args.os_username + os_user_id = args.os_user_id os_password = None # Fetched and set later as needed os_tenant_name = args.os_tenant_name os_tenant_id = args.os_tenant_id @@ -606,9 +612,10 @@ def main(self, argv): auth_plugin.parse_opts(args) if not auth_plugin or not auth_plugin.opts: - if not os_username: + if not os_username and not os_user_id: raise exc.CommandError(_("You must provide a username " - "via either --os-username or env[OS_USERNAME]")) + "or user id via --os-username, --os-user-id, " + "env[OS_USERNAME] or env[OS_USER_ID]")) if not os_tenant_name and not os_tenant_id: raise exc.CommandError(_("You must provide a tenant name " @@ -639,8 +646,9 @@ def main(self, argv): raise exc.CommandError(_("You must provide an auth url " "via either --os-auth-url or env[OS_AUTH_URL]")) - self.cs = client.Client(options.os_compute_api_version, os_username, - os_password, os_tenant_name, tenant_id=os_tenant_id, + self.cs = client.Client(options.os_compute_api_version, + os_username, os_password, os_tenant_name, + tenant_id=os_tenant_id, user_id=os_user_id, auth_url=os_auth_url, insecure=insecure, region_name=os_region_name, endpoint_type=endpoint_type, extensions=self.extensions, service_type=service_type, diff --git a/novaclient/tests/test_shell.py b/novaclient/tests/test_shell.py index 7cbb89b9b..8994cb34b 100644 --- a/novaclient/tests/test_shell.py +++ b/novaclient/tests/test_shell.py @@ -32,7 +32,7 @@ 'OS_TENANT_NAME': 'tenant_name', 'OS_AUTH_URL': 'http://no.where'} -FAKE_ENV2 = {'OS_USERNAME': 'username', +FAKE_ENV2 = {'OS_USER_ID': 'user_id', 'OS_PASSWORD': 'password', 'OS_TENANT_ID': 'tenant_id', 'OS_AUTH_URL': 'http://no.where'} @@ -133,25 +133,38 @@ def test_bash_completion(self): matchers.MatchesRegex(r, re.DOTALL | re.MULTILINE)) def test_no_username(self): - required = ('You must provide a username' - ' via either --os-username or env[OS_USERNAME]',) + required = ('You must provide a username or user id' + ' via --os-username, --os-user-id,' + ' env[OS_USERNAME] or env[OS_USER_ID]') self.make_env(exclude='OS_USERNAME') try: self.shell('list') except exceptions.CommandError as message: - self.assertEqual(required, message.args) + self.assertEqual(required, message.args[0]) + else: + self.fail('CommandError not raised') + + def test_no_user_id(self): + required = ('You must provide a username or user id' + ' via --os-username, --os-user-id,' + ' env[OS_USERNAME] or env[OS_USER_ID]') + self.make_env(exclude='OS_USER_ID', fake_env=FAKE_ENV2) + try: + self.shell('list') + except exceptions.CommandError as message: + self.assertEqual(required, message.args[0]) else: self.fail('CommandError not raised') def test_no_tenant_name(self): required = ('You must provide a tenant name or tenant id' ' via --os-tenant-name, --os-tenant-id,' - ' env[OS_TENANT_NAME] or env[OS_TENANT_ID]',) + ' env[OS_TENANT_NAME] or env[OS_TENANT_ID]') self.make_env(exclude='OS_TENANT_NAME') try: self.shell('list') except exceptions.CommandError as message: - self.assertEqual(required, message.args) + self.assertEqual(required, message.args[0]) else: self.fail('CommandError not raised') diff --git a/novaclient/v1_1/client.py b/novaclient/v1_1/client.py index efeb5c993..70c90f8b0 100644 --- a/novaclient/v1_1/client.py +++ b/novaclient/v1_1/client.py @@ -73,12 +73,13 @@ def __init__(self, username, api_key, project_id, auth_url=None, bypass_url=None, os_cache=False, no_cache=True, http_log_debug=False, auth_system='keystone', auth_plugin=None, auth_token=None, - cacert=None, tenant_id=None): + cacert=None, tenant_id=None, user_id=None): # FIXME(comstud): Rename the api_key argument above when we # know it's not being used as keyword argument password = api_key self.projectid = project_id self.tenant_id = tenant_id + self.user_id = user_id self.flavors = flavors.FlavorManager(self) self.flavor_access = flavor_access.FlavorAccessManager(self) self.images = images.ImageManager(self) @@ -126,6 +127,7 @@ def __init__(self, username, api_key, project_id, auth_url=None, self.client = client.HTTPClient(username, password, + user_id=user_id, projectid=project_id, tenant_id=tenant_id, auth_url=auth_url, diff --git a/novaclient/v3/client.py b/novaclient/v3/client.py index f26fdcf99..74d2f9eb5 100644 --- a/novaclient/v3/client.py +++ b/novaclient/v3/client.py @@ -59,9 +59,10 @@ def __init__(self, username, password, project_id, auth_url=None, bypass_url=None, os_cache=False, no_cache=True, http_log_debug=False, auth_system='keystone', auth_plugin=None, auth_token=None, - cacert=None, tenant_id=None): + cacert=None, tenant_id=None, user_id=None): self.projectid = project_id self.tenant_id = tenant_id + self.user_id = user_id self.os_cache = os_cache or not no_cache #TODO(bnemec): Add back in v3 extensions self.agents = agents.AgentsManager(self) @@ -91,6 +92,7 @@ def __init__(self, username, password, project_id, auth_url=None, self.client = client.HTTPClient(username, password, + user_id=user_id, projectid=project_id, tenant_id=tenant_id, auth_url=auth_url, From 02a091cc6c5b19c0e97a7b5771379cef20b612d5 Mon Sep 17 00:00:00 2001 From: Haiwei Xu Date: Sat, 8 Feb 2014 03:45:47 +0900 Subject: [PATCH 0438/1705] Fix authentication bug when booting an server in V3 Currently when booting a server with V3, novaclient sends an empty os_password to image_cs. This will cause 401(Unauthorized: Invalid user/password) when trying to find image. This is is a result of changes nova's V3 API: nova is no longer used as a proxy for the image service. So novaclient uses two Client instances: one for nova, the other for image service. This patch checks os_password before creating the image Client and assigns it if it's empty. Change-Id: Ic54cef93e9b823fb98b1edd78776c9a1fc06ba46 Closes-Bug: #1277425 --- novaclient/shell.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/novaclient/shell.py b/novaclient/shell.py index 919aa21ba..f751f2128 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -694,6 +694,12 @@ def main(self, argv): # sometimes need to be able to look up images information # via glance when connected to the nova api. image_service_type = 'image' + # NOTE(hdd): the password is needed again because creating a new + # Client without specifying bypass_url will force authentication. + # We can't reuse self.cs's bypass_url, because that's the URL for + # the nova service; we need to get glance's URL for this Client + if not os_password: + os_password = helper.password self.cs.image_cs = client.Client( options.os_compute_api_version, os_username, os_password, os_tenant_name, tenant_id=os_tenant_id, From ec6a5e803c83eaeada1d3b8b61cb36545507af52 Mon Sep 17 00:00:00 2001 From: Chris Yeoh Date: Thu, 13 Mar 2014 21:15:56 +1030 Subject: [PATCH 0439/1705] Removes copy of output of 'nova help' from README Removes the copy of the otuput of 'nova help' from the README.rst file. It's out of date and given its not autogenerated is rather prone to getting out of date again soon anyway. Just leave the command that people need to run in order to print the help message. Change-Id: I3cd069838302913bc69cc0ded5d632c7f8c3f408 --- README.rst | 181 +---------------------------------------------------- 1 file changed, 1 insertion(+), 180 deletions(-) diff --git a/README.rst b/README.rst index b155bf54e..1fef0d4ba 100644 --- a/README.rst +++ b/README.rst @@ -58,186 +58,7 @@ can specify the one you want with ``--os-region-name`` (or ``export OS_REGION_NAME``). It defaults to the first in the list returned. You'll find complete documentation on the shell by running -``nova help``:: - - usage: nova [--debug] [--no-cache] [--timings] - [--os-username ] [--os-password ] - [--os-tenant-name ] [--os-auth-url ] - [--os-region-name ] [--os-auth-system ] - [--service-type ] [--service-name ] - [--volume-service-name ] - [--endpoint-type ] - [--os-compute-api-version ] [--insecure] - [--bypass-url ] - ... - - Command-line interface to the OpenStack Nova API. - - Positional arguments: - - absolute-limits Print a list of absolute limits for a user - actions Retrieve server actions. - add-fixed-ip Add new IP address to network. - add-floating-ip Add a floating IP address to a server. - aggregate-add-host Add the host to the specified aggregate. - aggregate-create Create a new aggregate with the specified details. - aggregate-delete Delete the aggregate by its id. - aggregate-details Show details of the specified aggregate. - aggregate-list Print a list of all aggregates. - aggregate-remove-host - Remove the specified host from the specified aggregate. - aggregate-set-metadata - Update the metadata associated with the aggregate. - aggregate-update Update the aggregate's name and optionally - availability zone. - boot Boot a new server. - console-log Get console log output of a server. - credentials Show user credentials returned from auth - delete Immediately shut down and delete a server. - describe-resource Show details about a resource - diagnostics Retrieve server diagnostics. - dns-create Create a DNS entry for domain, name and ip. - dns-create-private-domain - Create the specified DNS domain. - dns-create-public-domain - Create the specified DNS domain. - dns-delete Delete the specified DNS entry. - dns-delete-domain Delete the specified DNS domain. - dns-domains Print a list of available dns domains. - dns-list List current DNS entries for domain and ip or domain - and name. - endpoints Discover endpoints that get returned from the - authenticate services - evacuate Evacuate a server from failed host - flavor-create Create a new flavor. - flavor-delete Delete a specific flavor. - flavor-list Print a list of available 'flavors' (sizes of - servers). - flavor-show Show details about the given flavor. - flavor-key Set or unset extra_spec for a flavor. - flavor-access-list Print access information about the given flavor. - flavor-access-add Add flavor access for the given tenant. - flavor-access-remove - Remove flavor access for the given tenant. - floating-ip-create Allocate a floating IP for the current tenant. - floating-ip-delete De-allocate a floating IP. - floating-ip-list List floating ips for this tenant. - floating-ip-pool-list - List all floating ip pools. - get-vnc-console Get a vnc console to a server. - get-spice-console Get a spice console to a server. - host-action Perform a power action on a host. - host-update Update host settings. - image-create Create a new image by taking a snapshot of a running - server. - image-delete Delete an image. - image-list Print a list of available images to boot from. - image-meta Set or Delete metadata on an image. - image-show Show details about the given image. - keypair-add Create a new key pair for use with instances - keypair-delete Delete keypair by its name - keypair-list Print a list of keypairs for a user - list List active servers. - live-migration Migrates a running instance to a new machine. - lock Lock a server. - meta Set or Delete metadata on a server. - migrate Migrate a server. - pause Pause a server. - rate-limits Print a list of rate limits for a user - reboot Reboot a server. - rebuild Shutdown, re-image, and re-boot a server. - refresh-network Refresh server network information. - remove-fixed-ip Remove an IP address from a server. - remove-floating-ip Remove a floating IP address from a server. - rename Rename a server. - rescue Rescue a server. - resize Resize a server. - resize-confirm Confirm a previous resize. - resize-revert Revert a previous resize (and return to the previous - VM). - resume Resume a server. - root-password Change the root password for a server. - secgroup-add-group-rule - Add a source group rule to a security group. - secgroup-add-rule Add a rule to a security group. - secgroup-create Create a security group. - secgroup-update Update a security group. - secgroup-delete Delete a security group. - secgroup-delete-group-rule - Delete a source group rule from a security group. - secgroup-delete-rule - Delete a rule from a security group. - secgroup-list List security groups for the curent tenant. - secgroup-list-rules - List rules for a security group. - show Show details about the given server. - ssh SSH into a server. - start Start a server. - stop Stop a server. - suspend Suspend a server. - unlock Unlock a server. - unpause Unpause a server. - unrescue Unrescue a server. - usage-list List usage data for all tenants - volume-attach Attach a volume to a server. - volume-create Add a new volume. - volume-delete Remove a volume. - volume-detach Detach a volume from a server. - volume-list List all the volumes. - volume-show Show details about a volume. - volume-snapshot-create - Add a new snapshot. - volume-snapshot-delete - Remove a snapshot. - volume-snapshot-list - List all the snapshots. - volume-snapshot-show - Show details about a snapshot. - volume-type-create Create a new volume type. - volume-type-delete Delete a specific flavor - volume-type-list Print a list of available 'volume types'. - x509-create-cert Create x509 cert for a user in tenant - x509-get-root-cert Fetches the x509 root cert. - bash-completion Prints all of the commands and options to stdout so - that the nova.bash_completion script doesn't have to - hard code them. - help Display help about this program or one of its - subcommands. - - Optional arguments: - --debug Print debugging output - --no-cache Don't use the auth token cache. - --timings Print call timing info - --os-username - Defaults to env[OS_USERNAME]. - --os-password - Defaults to env[OS_PASSWORD]. - --os-tenant-name - Defaults to env[OS_TENANT_NAME]. - --os-auth-url - Defaults to env[OS_AUTH_URL]. - --os-region-name - Defaults to env[OS_REGION_NAME]. - --os-auth-system - Defaults to env[OS_AUTH_SYSTEM]. - --service-type - Defaults to compute for most actions - --service-name - Defaults to env[NOVA_SERVICE_NAME] - --volume-service-name - Defaults to env[NOVA_VOLUME_SERVICE_NAME] - --endpoint-type - Defaults to env[NOVA_ENDPOINT_TYPE] or publicURL. - --os-compute-api-version - Accepts 1.1, defaults to env[OS_COMPUTE_API_VERSION]. --username USERNAME Deprecated - --insecure Explicitly allow novaclient to perform "insecure" SSL - (https) requests. The server's certificate will not be - verified against any certificate authorities. This - option should be used with caution. - --bypass-url - Use this API endpoint instead of the Service Catalog - - See "nova help COMMAND" for help on a specific command. +``nova help`` Python API ---------- From c40891b2824805458db40067ab669d961ecfdfed Mon Sep 17 00:00:00 2001 From: Gary Kotton Date: Thu, 13 Jun 2013 15:25:39 +0000 Subject: [PATCH 0440/1705] Nova CLI for server groups CLI support for blueprint instance-group-api-extension REST API support:- https://review.openstack.org/#/c/62557/ DocImpact - supports create, list, get and delete - only V2 is supported Change-Id: Iaa5a2922b9a0eed9f682b7584c2acf582379b422 --- novaclient/tests/v1_1/fakes.py | 45 +++++++++++++ novaclient/tests/v1_1/test_server_groups.py | 52 +++++++++++++++ novaclient/v1_1/client.py | 2 + novaclient/v1_1/server_groups.py | 71 +++++++++++++++++++++ novaclient/v1_1/shell.py | 39 +++++++++++ 5 files changed, 209 insertions(+) create mode 100644 novaclient/tests/v1_1/test_server_groups.py create mode 100644 novaclient/v1_1/server_groups.py diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index dd8fdccc4..50658c144 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -1996,3 +1996,48 @@ def post_os_server_external_events(self, **kw): return (200, {}, {'events': [ {'name': 'network-changed', 'server_uuid': '1234'}]}) + + # + # Server Groups + # + + def get_os_server_groups(self, *kw): + return (200, {}, + {"server_groups": [ + {"members": [], "metadata": {}, + "id": "2cbd51f4-fafe-4cdb-801b-cf913a6f288b", + "policies": [], "name": "ig1"}, + {"members": [], "metadata": {}, + "id": "4473bb03-4370-4bfb-80d3-dc8cffc47d94", + "policies": ["anti-affinity"], "name": "ig2"}, + {"members": [], "metadata": {"key": "value"}, + "id": "31ab9bdb-55e1-4ac3-b094-97eeb1b65cc4", + "policies": [], "name": "ig3"}, + {"members": ["2dccb4a1-02b9-482a-aa23-5799490d6f5d"], + "metadata": {}, + "id": "4890bb03-7070-45fb-8453-d34556c87d94", + "policies": ["anti-affinity"], "name": "ig2"}]}) + + def _return_server_group(self): + r = {'server_group': + self.get_os_server_groups()[2]['server_groups'][0]} + return (200, {}, r) + + def post_os_server_groups(self, body, **kw): + return self._return_server_group() + + def get_os_server_groups_2cbd51f4_fafe_4cdb_801b_cf913a6f288b(self, + **kw): + return self._return_server_group() + + def put_os_server_groups_2cbd51f4_fafe_4cdb_801b_cf913a6f288b(self, + **kw): + return self._return_server_group() + + def post_os_server_groups_2cbd51f4_fafe_4cdb_801b_cf913a6f288b_action( + self, body, **kw): + return self._return_server_group() + + def delete_os_server_groups_2cbd51f4_fafe_4cdb_801b_cf913a6f288b( + self, **kw): + return (202, {}, None) diff --git a/novaclient/tests/v1_1/test_server_groups.py b/novaclient/tests/v1_1/test_server_groups.py new file mode 100644 index 000000000..fde5def2e --- /dev/null +++ b/novaclient/tests/v1_1/test_server_groups.py @@ -0,0 +1,52 @@ +# Copyright (c) 2014 VMware, Inc. +# All Rights Reserved. +# +# 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 novaclient.tests import utils +from novaclient.tests.v1_1 import fakes +from novaclient.v1_1 import server_groups + + +cs = fakes.FakeClient() + + +class ServerGroupsTest(utils.TestCase): + + def test_list_server_groups(self): + result = cs.server_groups.list() + cs.assert_called('GET', '/os-server-groups') + for server_group in result: + self.assertTrue(isinstance(server_group, + server_groups.ServerGroup)) + + def test_create_server_group(self): + kwargs = {'name': 'ig1', + 'policies': ['anti-affinity']} + server_group = cs.server_groups.create(**kwargs) + body = {'server_group': kwargs} + cs.assert_called('POST', '/os-server-groups', body) + self.assertTrue(isinstance(server_group, + server_groups.ServerGroup)) + + def test_get_server_group(self): + id = '2cbd51f4-fafe-4cdb-801b-cf913a6f288b' + server_group = cs.server_groups.get(id) + cs.assert_called('GET', '/os-server-groups/%s' % id) + self.assertTrue(isinstance(server_group, + server_groups.ServerGroup)) + + def test_delete_server_group(self): + id = '2cbd51f4-fafe-4cdb-801b-cf913a6f288b' + cs.server_groups.delete(id) + cs.assert_called('DELETE', '/os-server-groups/%s' % id) diff --git a/novaclient/v1_1/client.py b/novaclient/v1_1/client.py index efeb5c993..79a700d8c 100644 --- a/novaclient/v1_1/client.py +++ b/novaclient/v1_1/client.py @@ -37,6 +37,7 @@ from novaclient.v1_1 import quotas from novaclient.v1_1 import security_group_rules from novaclient.v1_1 import security_groups +from novaclient.v1_1 import server_groups from novaclient.v1_1 import servers from novaclient.v1_1 import services from novaclient.v1_1 import usage @@ -116,6 +117,7 @@ def __init__(self, username, api_key, project_id, auth_url=None, self.os_cache = os_cache or not no_cache self.availability_zones = \ availability_zones.AvailabilityZoneManager(self) + self.server_groups = server_groups.ServerGroupsManager(self) # Add in any extensions... if extensions: diff --git a/novaclient/v1_1/server_groups.py b/novaclient/v1_1/server_groups.py new file mode 100644 index 000000000..be6ff8eef --- /dev/null +++ b/novaclient/v1_1/server_groups.py @@ -0,0 +1,71 @@ +# Copyright (c) 2014 VMware, Inc. +# All Rights Reserved. +# +# 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. + +""" +Server group interface. +""" + +from novaclient import base + + +class ServerGroup(base.Resource): + """ + A server group. + """ + NAME_ATTR = 'server_group_name' + + def __repr__(self): + return '' % self.id + + def delete(self): + self.manager.delete(self) + + +class ServerGroupsManager(base.ManagerWithFind): + """ + Manage :class:`ServerGroup` resources. + """ + resource_class = ServerGroup + + def list(self): + """Get a list of all server groups. + + :rtype: list of :class:`ServerGroup`. + """ + return self._list('/os-server-groups', 'server_groups') + + def get(self, id): + """Get a specific server group. + + :param id: The ID of the :class:`ServerGroup` to get. + :rtype: :class:`ServerGroup` + """ + return self._get('/os-server-groups/%s' % id, + 'server_group') + + def delete(self, id): + """Delete a specific server group. + + :param id: The ID of the :class:`ServerGroup` to delete. + """ + self._delete('/os-server-groups/%s' % id) + + def create(self, **kwargs): + """Create (allocate) a server group. + + :rtype: list of :class:`ServerGroup` + """ + body = {'server_group': kwargs} + return self._create('/os-server-groups', body, 'server_group') diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index a043d3c09..c1754c986 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -3512,3 +3512,42 @@ def do_availability_zone_list(cs, _args): _translate_availability_zone_keys(result) utils.print_list(result, ['Name', 'Status'], sortby_index=None) + + +def _print_server_group_details(server_group): + columns = ['Id', 'Name', 'Policies', 'Members', 'Metadata'] + utils.print_list(server_group, columns) + + +def do_server_group_list(cs, args): + """Print a list of all server groups.""" + server_groups = cs.server_groups.list() + _print_server_group_details(server_groups) + + +@utils.arg('name', metavar='', help='Server group name.') +@utils.arg('--policy', metavar='', action='append', + dest='policies', default=[], type=str, + help='Policies for the server groups') +def do_server_group_create(cs, args): + """Create a new server group with the specified details.""" + kwargs = {'name': args.name, + 'policies': args.policies} + server_group = cs.server_groups.create(**kwargs) + _print_server_group_details([server_group]) + + +@utils.arg('id', metavar='', + help="Unique ID of the server group to delete") +def do_server_group_delete(cs, args): + """Delete a specific server group.""" + cs.server_groups.delete(args.id) + print("Instance group %s has been successfully deleted." % args.id) + + +@utils.arg('id', metavar='', + help="Unique ID of the server group to get") +def do_server_group_get(cs, args): + """Get a specific server group.""" + server_group = cs.server_groups.get(args.id) + _print_server_group_details([server_group]) From 99aa0273dc0bccf093aa5a4fc3ca6ffadd23edfd Mon Sep 17 00:00:00 2001 From: gtt116 Date: Fri, 14 Mar 2014 09:40:33 +0800 Subject: [PATCH 0441/1705] Explain how to delete a metadata in aggregate-set-metadata nova aggregate-set-metadata not given value of a key means delete the metadata, so explain this usage in help message. Change-Id: Ie8215bd8b47ed00e3d4686edab9e3052ae84774d --- novaclient/v1_1/shell.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index c1754c986..ed7d06d9d 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2739,7 +2739,8 @@ def do_aggregate_update(cs, args): nargs='+', action='append', default=[], - help=_('Metadata to add/update to aggregate')) + help=_('Metadata to add/update to aggregate. ' + 'Specify only the key to delete a metadata item.')) def do_aggregate_set_metadata(cs, args): """Update the metadata associated with the aggregate.""" aggregate = _find_aggregate(cs, args.aggregate) From 4cfaa4a61cdb2411dcf5bca3e157d8bc651b2b1e Mon Sep 17 00:00:00 2001 From: Arata Notsu Date: Tue, 18 Mar 2014 00:21:21 +0900 Subject: [PATCH 0442/1705] Do auth_url.rstrip('/') only if auth_url is set auth_url can be None, for example, when we use bypass_url. Also, add rstrip('/') for bypass_url (and management_url), which is done when management_url is gotten from service catalog. Change-Id: I4f59cc405386a15f8a266d279b27f279eacdb7f1 --- novaclient/client.py | 4 ++-- novaclient/tests/test_client.py | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index 0b9aeeecc..94b66348e 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -83,7 +83,7 @@ def __init__(self, user, password, projectid=None, auth_url=None, auth_url = auth_plugin.get_auth_url() if not auth_url: raise exceptions.EndpointNotFound() - self.auth_url = auth_url.rstrip('/') + self.auth_url = auth_url.rstrip('/') if auth_url else auth_url self.version = 'v1.1' self.region_name = region_name self.endpoint_type = endpoint_type @@ -91,7 +91,7 @@ def __init__(self, user, password, projectid=None, auth_url=None, self.service_name = service_name self.volume_service_name = volume_service_name self.timings = timings - self.bypass_url = bypass_url + self.bypass_url = bypass_url.rstrip('/') if bypass_url else bypass_url self.os_cache = os_cache or not no_cache self.http_log_debug = http_log_debug if timeout is not None: diff --git a/novaclient/tests/test_client.py b/novaclient/tests/test_client.py index d58670240..8a307a363 100644 --- a/novaclient/tests/test_client.py +++ b/novaclient/tests/test_client.py @@ -216,3 +216,17 @@ def test_get_password_func(self): cs.password_func = mock.Mock() self.assertEqual(cs._get_password(), "password") self.assertFalse(cs.password_func.called) + + def test_auth_url_rstrip_slash(self): + cs = novaclient.client.HTTPClient("user", "password", "project_id", + auth_url="foo/v2/") + self.assertEqual(cs.auth_url, "foo/v2") + + def test_token_and_bypass_url(self): + cs = novaclient.client.HTTPClient(None, None, None, + auth_token="12345", + bypass_url="compute/v100/") + self.assertIsNone(cs.auth_url) + self.assertEqual(cs.auth_token, "12345") + self.assertEqual(cs.bypass_url, "compute/v100") + self.assertEqual(cs.management_url, "compute/v100") From 4e8dea674558dadeb9e53c3aec94be7b40ff4710 Mon Sep 17 00:00:00 2001 From: OpenStack Jenkins Date: Tue, 18 Mar 2014 08:52:05 +0000 Subject: [PATCH 0443/1705] Updated from global requirements Change-Id: Ibb039ec440a513a49cbcf05daa69ac8785770035 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index bafbb386d..a12aaf27c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ pbr>=0.6,<1.0 argparse -iso8601>=0.1.8 +iso8601>=0.1.9 PrettyTable>=0.7,<0.8 requests>=1.1 simplejson>=2.0.9 From 91c138c51fff43779a4fe4a9f12cd1e2c38c689c Mon Sep 17 00:00:00 2001 From: shihanzhang Date: Sun, 9 Mar 2014 16:19:17 +0800 Subject: [PATCH 0444/1705] Support IPv6 when booting instances When nova uses neutron, IPv6 addresses may be allocated. This ensures those addresses are supported.the patch to modify nova is https://review.openstack.org/#/c/74252/ Change-Id: I2468d3bcaca9122e5d3f9c98044d57f623630dc3 Closes-bug: #1267685 --- novaclient/tests/v1_1/test_servers.py | 22 +++++++++++++ novaclient/tests/v1_1/test_shell.py | 26 ++++++++++++++++ novaclient/tests/v3/test_servers.py | 45 +++++++++++++++++++++++++++ novaclient/tests/v3/test_shell.py | 26 ++++++++++++++++ novaclient/v1_1/servers.py | 10 +++++- novaclient/v1_1/shell.py | 11 ++++--- novaclient/v3/servers.py | 10 +++++- novaclient/v3/shell.py | 11 ++++--- 8 files changed, 151 insertions(+), 10 deletions(-) diff --git a/novaclient/tests/v1_1/test_servers.py b/novaclient/tests/v1_1/test_servers.py index a48204c19..521cca832 100644 --- a/novaclient/tests/v1_1/test_servers.py +++ b/novaclient/tests/v1_1/test_servers.py @@ -104,6 +104,28 @@ def test_create_server_from_volume(): test_create_server_from_volume() + def test_create_server_boot_with_nics_ipv6(self): + old_boot = cs.servers._boot + nics = [{'net-id': '11111111-1111-1111-1111-111111111111', + 'v6-fixed-ip': '2001:db9:0:1::10'}] + + def wrapped_boot(url, key, *boot_args, **boot_kwargs): + self.assertEqual(boot_kwargs['nics'], nics) + return old_boot(url, key, *boot_args, **boot_kwargs) + + with mock.patch.object(cs.servers, '_boot', wrapped_boot): + s = cs.servers.create( + name="My server", + image=1, + flavor=1, + meta={'foo': 'bar'}, + userdata="hello moto", + key_name="fakekey", + nics=nics + ) + cs.assert_called('POST', '/servers') + self.assertIsInstance(s, servers.Server) + def test_create_server_userdata_file_object(self): s = cs.servers.create( name="My server", diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index 58c50b64d..adac6c096 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -497,6 +497,32 @@ def test_boot_nics(self): }, ) + def test_boot_nics_ipv6(self): + cmd = ('boot --image 1 --flavor 1 ' + '--nic net-id=a=c,v6-fixed-ip=2001:db9:0:1::10 some-server') + self.run_command(cmd) + self.assert_called_anytime( + 'POST', '/servers', + { + 'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': '1', + 'min_count': 1, + 'max_count': 1, + 'networks': [ + {'uuid': 'a=c', 'fixed_ip': '2001:db9:0:1::10'}, + ], + }, + }, + ) + + def test_boot_nics_both_ipv4_and_ipv6(self): + cmd = ('boot --image 1 --flavor 1 ' + '--nic net-id=a=c,v4-fixed-ip=10.0.0.1,' + 'v6-fixed-ip=2001:db9:0:1::10 some-server') + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + def test_boot_nics_no_value(self): cmd = ('boot --image 1 --flavor 1 ' '--nic net-id some-server') diff --git a/novaclient/tests/v3/test_servers.py b/novaclient/tests/v3/test_servers.py index 9d0abcd62..bde6893ca 100644 --- a/novaclient/tests/v3/test_servers.py +++ b/novaclient/tests/v3/test_servers.py @@ -13,6 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. +import mock import six from novaclient import exceptions @@ -74,6 +75,50 @@ def test_create_server(self): cs.assert_called('POST', '/servers') self.assertIsInstance(s, servers.Server) + def test_create_server_boot_with_nics_ipv4(self): + old_boot = cs.servers._boot + nics = [{'net-id': '11111111-1111-1111-1111-111111111111', + 'v4-fixed-ip': '10.10.0.7'}] + + def wrapped_boot(url, key, *boot_args, **boot_kwargs): + self.assertEqual(boot_kwargs['nics'], nics) + return old_boot(url, key, *boot_args, **boot_kwargs) + + with mock.patch.object(cs.servers, '_boot', wrapped_boot): + s = cs.servers.create( + name="My server", + image=1, + flavor=1, + meta={'foo': 'bar'}, + userdata="hello moto", + key_name="fakekey", + nics=nics + ) + cs.assert_called('POST', '/servers') + self.assertIsInstance(s, servers.Server) + + def test_create_server_boot_with_nics_ipv6(self): + old_boot = cs.servers._boot + nics = [{'net-id': '11111111-1111-1111-1111-111111111111', + 'v6-fixed-ip': '2001:db9:0:1::10'}] + + def wrapped_boot(url, key, *boot_args, **boot_kwargs): + self.assertEqual(boot_kwargs['nics'], nics) + return old_boot(url, key, *boot_args, **boot_kwargs) + + with mock.patch.object(cs.servers, '_boot', wrapped_boot): + s = cs.servers.create( + name="My server", + image=1, + flavor=1, + meta={'foo': 'bar'}, + userdata="hello moto", + key_name="fakekey", + nics=nics + ) + cs.assert_called('POST', '/servers') + self.assertIsInstance(s, servers.Server) + def test_create_server_userdata_file_object(self): s = cs.servers.create( name="My server", diff --git a/novaclient/tests/v3/test_shell.py b/novaclient/tests/v3/test_shell.py index d3300bf70..e0c28ac74 100644 --- a/novaclient/tests/v3/test_shell.py +++ b/novaclient/tests/v3/test_shell.py @@ -428,6 +428,32 @@ def test_boot_nics(self): }, ) + def test_boot_nics_ipv6(self): + cmd = ('boot --image 1 --flavor 1 ' + '--nic net-id=a=c,v6-fixed-ip=2001:db9:0:1::10 some-server') + self.run_command(cmd) + self.assert_called_anytime( + 'POST', '/servers', + { + 'server': { + 'flavor_ref': '1', + 'name': 'some-server', + 'image_ref': '1', + 'os-multiple-create:min_count': 1, + 'os-multiple-create:max_count': 1, + 'networks': [ + {'uuid': 'a=c', 'fixed_ip': '2001:db9:0:1::10'}, + ], + }, + }, + ) + + def test_boot_nics_both_ipv4_and_ipv6(self): + cmd = ('boot --image 1 --flavor 1 ' + '--nic net-id=a=c,v4-fixed-ip=10.0.0.1,' + 'v6-fixed-ip=2001:db9:0:1::10 some-server') + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + def test_boot_nics_no_value(self): cmd = ('boot --image 1 --flavor 1 ' '--nic net-id some-server') diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index cf284d94b..e5778b2ad 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -26,6 +26,7 @@ from novaclient import base from novaclient import crypto +from novaclient.openstack.common.gettextutils import _ from novaclient.openstack.common import strutils from novaclient.v1_1.security_groups import SecurityGroup @@ -520,8 +521,15 @@ def _boot(self, resource_url, response_key, name, image, flavor, # if value is empty string, do not send value in body if nic_info.get('net-id'): net_data['uuid'] = nic_info['net-id'] - if nic_info.get('v4-fixed-ip'): + if (nic_info.get('v4-fixed-ip') and + nic_info.get('v6-fixed-ip')): + raise base.exceptions.CommandError(_( + "Only one of 'v4-fixed-ip' and 'v6-fixed-ip' may be" + " provided.")) + elif nic_info.get('v4-fixed-ip'): net_data['fixed_ip'] = nic_info['v4-fixed-ip'] + elif nic_info.get('v6-fixed-ip'): + net_data['fixed_ip'] = nic_info['v6-fixed-ip'] if nic_info.get('port-id'): net_data['port'] = nic_info['port-id'] all_net_data.append(net_data) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index c1754c986..3e3d6ec45 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -235,9 +235,10 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): for nic_str in args.nics: err_msg = (_("Invalid nic argument '%s'. Nic arguments must be of " "the form --nic , with at minimum net-id or port-id " - "specified.") % nic_str) - nic_info = {"net-id": "", "v4-fixed-ip": "", "port-id": ""} + "v6-fixed-ip=ip-addr,port-id=port-uuid>, with at minimum " + "net-id or port-id specified.") % nic_str) + nic_info = {"net-id": "", "v4-fixed-ip": "", "v6-fixed-ip": "", + "port-id": ""} for kv_str in nic_str.split(","): try: @@ -402,7 +403,8 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): help=_("Send arbitrary key/value pairs to the scheduler for custom " "use.")) @utils.arg('--nic', - metavar="", + metavar="", action='append', dest='nics', default=[], @@ -411,6 +413,7 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): "net-id: attach NIC to network with this UUID " "(required if no port-id), " "v4-fixed-ip: IPv4 fixed address for NIC (optional), " + "v6-fixed-ip: IPv6 fixed address for NIC (optional), " "port-id: attach NIC to port with this UUID " "(required if no net-id)")) @utils.arg('--config-drive', diff --git a/novaclient/v3/servers.py b/novaclient/v3/servers.py index d8ebd0a13..75e0d6990 100644 --- a/novaclient/v3/servers.py +++ b/novaclient/v3/servers.py @@ -26,6 +26,7 @@ from novaclient import base from novaclient import crypto +from novaclient.openstack.common.gettextutils import _ from novaclient.openstack.common import strutils REBOOT_SOFT, REBOOT_HARD = 'SOFT', 'HARD' @@ -457,8 +458,15 @@ def _boot(self, resource_url, response_key, name, image, flavor, # if value is empty string, do not send value in body if nic_info.get('net-id'): net_data['uuid'] = nic_info['net-id'] - if nic_info.get('v4-fixed-ip'): + if (nic_info.get('v4-fixed-ip') and + nic_info.get('v6-fixed-ip')): + raise base.exceptions.CommandError(_( + "Only one of 'v4-fixed-ip' and 'v6-fixed-ip' may be" + " provided.")) + elif nic_info.get('v4-fixed-ip'): net_data['fixed_ip'] = nic_info['v4-fixed-ip'] + elif nic_info.get('v6-fixed-ip'): + net_data['fixed_ip'] = nic_info['v6-fixed-ip'] if nic_info.get('port-id'): net_data['port'] = nic_info['port-id'] all_net_data.append(net_data) diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 10a6975d8..61c9a8cb5 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -146,9 +146,10 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): for nic_str in args.nics: err_msg = ("Invalid nic argument '%s'. Nic arguments must be of the " "form --nic , with at minimum net-id or port-id " - "specified." % nic_str) - nic_info = {"net-id": "", "v4-fixed-ip": "", "port-id": ""} + "v6-fixed-ip=ip-addr,port-id=port-uuid>, with at minimum " + "net-id or port-id specified." % nic_str) + nic_info = {"net-id": "", "v4-fixed-ip": "", "v6-fixed-ip": "", + "port-id": ""} for kv_str in nic_str.split(","): try: @@ -278,7 +279,8 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): metavar='', help="Send arbitrary key/value pairs to the scheduler for custom use.") @utils.arg('--nic', - metavar="", + metavar="", action='append', dest='nics', default=[], @@ -287,6 +289,7 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): "net-id: attach NIC to network with this UUID " "(required if no port-id), " "v4-fixed-ip: IPv4 fixed address for NIC (optional), " + "v6-fixed-ip: IPv6 fixed address for NIC (optional), " "port-id: attach NIC to port with this UUID " "(required if no net-id)") @utils.arg('--config-drive', From 8c7524b8bf6a5eb1f0bc77515e9ad95bdee826b0 Mon Sep 17 00:00:00 2001 From: "Kevin L. Mitchell" Date: Tue, 18 Mar 2014 11:37:57 -0500 Subject: [PATCH 0445/1705] Work around pypy testing issue A recent release of setuptools appears to have introduced a bug that causes the pypy gate tests to fail. This applies a temporary workaround that should restore pypy testing while the root cause of the problem is researched. Change-Id: I66597e40c1d2e57cca1292844d228af85214197d Partial-Bug: 1290562 --- tox.ini | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tox.ini b/tox.ini index b85bd2938..a8d9956d4 100644 --- a/tox.ini +++ b/tox.ini @@ -12,6 +12,11 @@ deps = -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt commands = python setup.py testr --testr-args='{posargs}' +[testenv:pypy] +deps = setuptools<3.2 + -r{toxinidir}/requirements.txt + -r{toxinidir}/test-requirements.txt + [testenv:pep8] commands = flake8 From 0b1a29e7b0f749276cdb8827aa38530cc2be61c9 Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Tue, 18 Mar 2014 15:55:58 -0400 Subject: [PATCH 0446/1705] Show Exception Name in Shell Output Currently, when the CLI encounters an exception, only the exception message is shown. However, the name of the exception can be quite informative as well. For instance, an error message might read "Cannot do xyz", which simply indicates the operation was unsucessful. However, the attached exception name (HTTPNotImplemented, for instance) indicates the *reason* that the call was unsuccessful. Change-Id: I0298477bd9d40d98c95bb797c68c47dbc952b345 --- novaclient/shell.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/novaclient/shell.py b/novaclient/shell.py index 919aa21ba..ce10bfcbf 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -775,7 +775,9 @@ def main(): except Exception as e: logger.debug(e, exc_info=1) - print("ERROR: %s" % strutils.safe_encode(six.text_type(e)), + details = {'name': strutils.safe_encode(e.__class__.__name__), + 'msg': strutils.safe_encode(six.text_type(e))} + print("ERROR (%(name)s): %(msg)s" % details, file=sys.stderr) sys.exit(1) except KeyboardInterrupt as e: From e43825bd1c67d22f012978c5de6feff028c75d40 Mon Sep 17 00:00:00 2001 From: Johannes Erdfelt Date: Thu, 20 Mar 2014 11:11:12 -0700 Subject: [PATCH 0447/1705] Print a useful message for unknown server errors If a server error is returned that has an unknown code, novaclient will end up printing something similar to: ClientException: Setting a message for ClientException will ensure that something more useful than that is printed. Change-Id: I43a2a33017f9a5c1b79d7fd8af4153e91d296f7b Closes-bug: 1295293 --- novaclient/exceptions.py | 2 ++ novaclient/tests/test_http.py | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/novaclient/exceptions.py b/novaclient/exceptions.py index 9f22a9e3e..a987c562c 100644 --- a/novaclient/exceptions.py +++ b/novaclient/exceptions.py @@ -81,6 +81,8 @@ class ClientException(Exception): """ The base exception class for all exceptions this library raises. """ + message = 'Unknown Error' + def __init__(self, code, message=None, details=None, request_id=None, url=None, method=None): self.code = code diff --git a/novaclient/tests/test_http.py b/novaclient/tests/test_http.py index e2fc4fa10..e7092781d 100644 --- a/novaclient/tests/test_http.py +++ b/novaclient/tests/test_http.py @@ -13,6 +13,7 @@ import mock import requests +import six from novaclient import client from novaclient import exceptions @@ -37,6 +38,12 @@ }) bad_req_mock_request = mock.Mock(return_value=(bad_req_response)) +unknown_error_response = utils.TestResponse({ + "status_code": 503, + "text": '', +}) +unknown_error_mock_request = mock.Mock(return_value=unknown_error_response) + def get_client(): cl = client.HTTPClient("username", "password", @@ -133,3 +140,16 @@ def test_client_logger(self): cl2 = client.HTTPClient("username", "password", "project_id", "auth_test", http_log_debug=True) self.assertEqual(len(cl2._logger.handlers), 1) + + @mock.patch.object(requests.Session, 'request', unknown_error_mock_request) + def test_unknown_server_error(self): + cl = get_client() + # This would be cleaner with the context manager version of + # assertRaises or assertRaisesRegexp, but both only appeared in + # Python 2.7 and testtools doesn't match that implementation yet + try: + cl.get('/hi') + except exceptions.ClientException as exc: + self.assertIn('Unknown Error', six.text_type(exc)) + else: + self.fail('Expected exceptions.ClientException') From 596d7d56758e445046e2429f3b6d61fbe245d292 Mon Sep 17 00:00:00 2001 From: liu-sheng Date: Fri, 21 Mar 2014 15:28:45 +0800 Subject: [PATCH 0448/1705] Correct the help sting of volume-type-delete Change-Id: I10b1bd6d42e4085dc73572f05ed893385ca3e139 --- novaclient/v1_1/shell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index c1754c986..e3171976d 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1856,7 +1856,7 @@ def do_volume_type_create(cs, args): help=_("Unique ID of the volume type to delete")) @utils.service_type('volume') def do_volume_type_delete(cs, args): - """Delete a specific flavor""" + """Delete a specific volume type.""" cs.volume_types.delete(args.id) From d01b3abdb007eb97ebb563801dee7ae9b458805d Mon Sep 17 00:00:00 2001 From: Ryan Hallisey Date: Fri, 14 Mar 2014 15:32:10 -0400 Subject: [PATCH 0449/1705] Raise exception when poll returns error state When running nova boot --poll, any error states that occur will be seen but, an exit code of 0 will be returned indicating success. This would be problematic for scripts looking to see if an operation completed successfully. To fix this, an exception is raised after the nova api indicates to the poll code that the vm is in an error state. The exceptions signals the shell main function to exit with code 1. Change-Id: I48f6b1c82e2f8b221dda898dcb804b0196018505 Fixes-Bug: #1292669 --- novaclient/exceptions.py | 5 +++++ novaclient/tests/v1_1/fakes.py | 12 +++++++++++- novaclient/tests/v1_1/test_shell.py | 4 ++++ novaclient/tests/v3/fakes.py | 5 ++++- novaclient/tests/v3/test_shell.py | 4 ++++ novaclient/v1_1/shell.py | 2 +- novaclient/v3/shell.py | 2 +- 7 files changed, 30 insertions(+), 4 deletions(-) diff --git a/novaclient/exceptions.py b/novaclient/exceptions.py index 9f22a9e3e..51ab41153 100644 --- a/novaclient/exceptions.py +++ b/novaclient/exceptions.py @@ -77,6 +77,11 @@ def __str__(self): return "ConnectionRefused: %s" % repr(self.response) +class InstanceInErrorState(Exception): + """Instance is in the error state.""" + pass + + class ClientException(Exception): """ The base exception class for all exceptions this library raises. diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index 50658c144..734c5600c 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -369,7 +369,10 @@ def post_servers(self, body, **kw): if 'personality' in body['server']: for pfile in body['server']['personality']: fakes.assert_has_keys(pfile, required=['path', 'contents']) - return (202, {}, self.get_servers_1234()[2]) + if body['server']['name'] == 'some-bad-server': + return (202, {}, self.get_servers_1235()[2]) + else: + return (202, {}, self.get_servers_1234()[2]) def post_os_volumes_boot(self, body, **kw): assert set(body.keys()) <= set(['server', 'os:scheduler_hints']) @@ -392,6 +395,13 @@ def get_servers_1234(self, **kw): r = {'server': self.get_servers_detail()[2]['servers'][0]} return (200, {}, r) + def get_servers_1235(self, **kw): + r = {'server': self.get_servers_detail()[2]['servers'][0]} + r['server']['id'] = 1235 + r['server']['status'] = 'error' + r['server']['fault'] = {'message': 'something went wrong!'} + return (200, {}, r) + def get_servers_5678(self, **kw): r = {'server': self.get_servers_detail()[2]['servers'][1]} return (200, {}, r) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index 58c50b64d..06f469fef 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -581,6 +581,10 @@ def test_boot_with_poll(self, poll_method): [mock.call(self.shell.cs.servers.get, 1234, 'building', ['active'])]) + def test_boot_with_poll_to_check_VM_state_error(self): + self.assertRaises(exceptions.InstanceInErrorState, self.run_command, + 'boot --flavor 1 --image 1 some-bad-server --poll') + def test_flavor_list(self): self.run_command('flavor-list') self.assert_called_anytime('GET', '/flavors/detail') diff --git a/novaclient/tests/v3/fakes.py b/novaclient/tests/v3/fakes.py index 1013910fc..be3727452 100644 --- a/novaclient/tests/v3/fakes.py +++ b/novaclient/tests/v3/fakes.py @@ -170,7 +170,10 @@ def post_servers(self, body, **kw): required=['name', 'image_ref', 'flavor_ref'], optional=['metadata', 'personality', 'os-scheduler-hints:scheduler_hints']) - return (202, {}, self.get_servers_1234()[2]) + if body['server']['name'] == 'some-bad-server': + return (202, {}, self.get_servers_1235()[2]) + else: + return (202, {}, self.get_servers_1234()[2]) # # Server Actions diff --git a/novaclient/tests/v3/test_shell.py b/novaclient/tests/v3/test_shell.py index d3300bf70..322b4b755 100644 --- a/novaclient/tests/v3/test_shell.py +++ b/novaclient/tests/v3/test_shell.py @@ -480,3 +480,7 @@ def test_boot_with_poll(self, poll_method): poll_method.assert_has_calls( [mock.call(self.shell.cs.servers.get, 1234, 'building', ['active'])]) + + def test_boot_with_poll_to_check_VM_state_error(self): + self.assertRaises(exceptions.InstanceInErrorState, self.run_command, + 'boot --flavor 1 --image 1 some-bad-server --poll') diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index c1754c986..a7774bb69 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -494,7 +494,7 @@ def print_progress(progress): elif status == "error": if not silent: print(_("\nError %s server") % action) - break + raise exceptions.InstanceInErrorState(obj.fault['message']) if not silent: print_progress(progress) diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 10a6975d8..71887089e 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -349,7 +349,7 @@ def print_progress(progress): elif status == "error": if not silent: print("\nError %s server" % action) - break + raise exceptions.InstanceInErrorState(obj.fault['message']) if not silent: print_progress(progress) From 6fbddcb4b629608a6b5ff556990c13563a36f858 Mon Sep 17 00:00:00 2001 From: ZhiQiang Fan Date: Mon, 24 Mar 2014 15:29:01 +0800 Subject: [PATCH 0450/1705] Avoid AttributeError in servers.Server.__repr__ servers.Server represents various object now, and some of them may don't have attribute 'name', for example, the interface_list() result object. It will cause AttributeError when we try to format string with such object, so I add a check for the 'name' attribute in __repr__ method, it will use 'unknown-name' instead when 'name' is not found. Change-Id: If4757d5d73721774543d58a4cc875710a6013f34 Closes-Bug: #1280453 --- novaclient/tests/v1_1/test_servers.py | 26 ++++++++++++++++++++++++++ novaclient/v1_1/servers.py | 2 +- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/novaclient/tests/v1_1/test_servers.py b/novaclient/tests/v1_1/test_servers.py index a48204c19..b70964a50 100644 --- a/novaclient/tests/v1_1/test_servers.py +++ b/novaclient/tests/v1_1/test_servers.py @@ -582,6 +582,32 @@ def test_interface_list(self): s.interface_list() cs.assert_called('GET', '/servers/1234/os-interface') + def test_interface_list_result_string_representable(self): + """Test for bugs.launchpad.net/python-novaclient/+bug/1280453.""" + # According to https://github.com/openstack/nova/blob/master/ + # nova/api/openstack/compute/contrib/attach_interfaces.py#L33, + # the attach_interface extension get method will return a json + # object partly like this: + interface_list = [{ + 'net_id': 'd7745cf5-63f9-4883-b0ae-983f061e4f23', + 'port_id': 'f35079da-36d5-4513-8ec1-0298d703f70e', + 'mac_addr': 'fa:16:3e:4c:37:c8', + 'port_state': 'ACTIVE', + 'fixed_ips': [{ + 'subnet_id': 'f1ad93ad-2967-46ba-b403-e8cbbe65f7fa', + 'ip_address': '10.2.0.96' + }] + }] + # If server is not string representable, it will raise an exception, + # because attribute named 'name' cannot be found. + # Parameter 'loaded' must be True or it will try to get attribute + # 'id' then fails (lazy load detail), this is exactly same as + # novaclient.base.Manager._list() + s = servers.Server(servers.ServerManager, interface_list[0], + loaded=True) + # Trigger the __repr__ magic method + self.assertEqual('', '%r' % s) + def test_interface_attach(self): s = cs.servers.get(1234) s.interface_attach(None, None, None) diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index cf284d94b..d4ac6a794 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -36,7 +36,7 @@ class Server(base.Resource): HUMAN_ID = True def __repr__(self): - return "" % self.name + return '' % getattr(self, 'name', 'unknown-name') def delete(self): """ From 656cf91f898ff1a20986f358bedc64f410f5ee4a Mon Sep 17 00:00:00 2001 From: Johannes Erdfelt Date: Thu, 20 Mar 2014 13:29:10 -0700 Subject: [PATCH 0451/1705] Fix error when run with no arguments on Python 3 Python 3 changed the map built-in to return an iterable instead of a list. When tested in a boolean context, this always returns True, even if it would not return anything when iterated. Instead of the usage being printed, this error was printed: ERROR: 'Namespace' object has no attribute 'func' Use list comprehension instead to ensure that an iterable isn't returned Change-Id: Ie15f2fa8ee93ab26490e371133fa0f944430737b Closes-bug: 1295356 --- novaclient/shell.py | 3 ++- novaclient/tests/test_shell.py | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/novaclient/shell.py b/novaclient/shell.py index 63715d939..770f5bb48 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -785,7 +785,8 @@ def start_section(self, heading): def main(): try: - OpenStackComputeShell().main(map(strutils.safe_decode, sys.argv[1:])) + argv = [strutils.safe_decode(a) for a in sys.argv[1:]] + OpenStackComputeShell().main(argv) except Exception as e: logger.debug(e, exc_info=1) diff --git a/novaclient/tests/test_shell.py b/novaclient/tests/test_shell.py index 8994cb34b..a9f060b34 100644 --- a/novaclient/tests/test_shell.py +++ b/novaclient/tests/test_shell.py @@ -257,3 +257,17 @@ def test_v3_service_type(self, mock_client): @mock.patch('novaclient.client.Client') def test_v_unknown_service_type(self, mock_client): self._test_service_type('unknown', 'compute', mock_client) + + @mock.patch('sys.argv', ['nova']) + @mock.patch('sys.stdout', six.StringIO()) + @mock.patch('sys.stderr', six.StringIO()) + def test_main_noargs(self): + # Ensure that main works with no command-line arguments + try: + novaclient.shell.main() + except SystemExit as exc: + self.fail('Unexpected SystemExit') + + # We expect the normal usage as a result + self.assertIn('Command-line interface to the OpenStack Nova API', + sys.stdout.getvalue()) From 21f166b623415ba136e8ea0a95635d545db9aeed Mon Sep 17 00:00:00 2001 From: Sergey Lukjanov Date: Wed, 26 Mar 2014 15:41:37 +0400 Subject: [PATCH 0452/1705] Start using oslosphinx theme for docs Change-Id: Ic327d9752e9c77e6543f23b032c8b77abd6ed479 --- doc/source/conf.py | 2 +- test-requirements.txt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index 30945d2b3..e37b8b888 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -88,7 +88,7 @@ def gen_ref(ver, title, names): # Add any Sphinx extension module names here, as strings. They can be # extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx'] +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'oslosphinx'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] diff --git a/test-requirements.txt b/test-requirements.txt index 868d84cf5..bc59d2eca 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -6,5 +6,6 @@ fixtures>=0.3.14 keyring>=1.6.1,<2.0,>=2.1 mock>=1.0 sphinx>=1.1.2,<1.2 +oslosphinx testrepository>=0.0.18 testtools>=0.9.34 From 32d84dc7868206690752a02f3b65ab40aa74cdb3 Mon Sep 17 00:00:00 2001 From: Davanum Srinivas Date: Thu, 27 Mar 2014 20:12:54 +0000 Subject: [PATCH 0453/1705] Sync with Oslo-Incubator oslo-incubator HEAD was 2eab986ef3c43f8d1e25065e3cbc1307860c25c7 This change brings in a few minor updates to python3 and a bug fix for deep copy failure in gettextutils. Change-Id: Id360f3b43d1ad2f67b328206990dd6bdb53c1cd7 --- novaclient/openstack/common/__init__.py | 15 +++++++ novaclient/openstack/common/gettextutils.py | 44 +++++---------------- novaclient/openstack/common/jsonutils.py | 10 +---- novaclient/openstack/common/strutils.py | 9 +++-- 4 files changed, 30 insertions(+), 48 deletions(-) diff --git a/novaclient/openstack/common/__init__.py b/novaclient/openstack/common/__init__.py index 2a00f3bc4..d1223eaf7 100644 --- a/novaclient/openstack/common/__init__.py +++ b/novaclient/openstack/common/__init__.py @@ -1,2 +1,17 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + import six + + six.add_move(six.MovedModule('mox', 'mox', 'mox3.mox')) diff --git a/novaclient/openstack/common/gettextutils.py b/novaclient/openstack/common/gettextutils.py index 982348274..e76b61a11 100644 --- a/novaclient/openstack/common/gettextutils.py +++ b/novaclient/openstack/common/gettextutils.py @@ -28,7 +28,6 @@ import locale from logging import handlers import os -import re from babel import localedata import six @@ -248,47 +247,22 @@ def _sanitize_mod_params(self, other): if other is None: params = (other,) elif isinstance(other, dict): - params = self._trim_dictionary_parameters(other) - else: - params = self._copy_param(other) - return params - - def _trim_dictionary_parameters(self, dict_param): - """Return a dict that only has matching entries in the msgid.""" - # NOTE(luisg): Here we trim down the dictionary passed as parameters - # to avoid carrying a lot of unnecessary weight around in the message - # object, for example if someone passes in Message() % locals() but - # only some params are used, and additionally we prevent errors for - # non-deepcopyable objects by unicoding() them. - - # Look for %(param) keys in msgid; - # Skip %% and deal with the case where % is first character on the line - keys = re.findall('(?:[^%]|^)?%\((\w*)\)[a-z]', self.msgid) - - # If we don't find any %(param) keys but have a %s - if not keys and re.findall('(?:[^%]|^)%[a-z]', self.msgid): - # Apparently the full dictionary is the parameter - params = self._copy_param(dict_param) - else: + # Merge the dictionaries + # Copy each item in case one does not support deep copy. params = {} - # Save our existing parameters as defaults to protect - # ourselves from losing values if we are called through an - # (erroneous) chain that builds a valid Message with - # arguments, and then does something like "msg % kwds" - # where kwds is an empty dictionary. - src = {} if isinstance(self.params, dict): - src.update(self.params) - src.update(dict_param) - for key in keys: - params[key] = self._copy_param(src[key]) - + for key, val in self.params.items(): + params[key] = self._copy_param(val) + for key, val in other.items(): + params[key] = self._copy_param(val) + else: + params = self._copy_param(other) return params def _copy_param(self, param): try: return copy.deepcopy(param) - except TypeError: + except Exception: # Fallback to casting to unicode this will handle the # python code-like objects that can't be deep-copied return six.text_type(param) diff --git a/novaclient/openstack/common/jsonutils.py b/novaclient/openstack/common/jsonutils.py index 5595d0b2e..f8738f911 100644 --- a/novaclient/openstack/common/jsonutils.py +++ b/novaclient/openstack/common/jsonutils.py @@ -36,17 +36,9 @@ import inspect import itertools import json -try: - import xmlrpclib -except ImportError: - # NOTE(jaypipes): xmlrpclib was renamed to xmlrpc.client in Python3 - # however the function and object call signatures - # remained the same. This whole try/except block should - # be removed and replaced with a call to six.moves once - # six 1.4.2 is released. See http://bit.ly/1bqrVzu - import xmlrpc.client as xmlrpclib import six +import six.moves.xmlrpc_client as xmlrpclib from novaclient.openstack.common import gettextutils from novaclient.openstack.common import importutils diff --git a/novaclient/openstack/common/strutils.py b/novaclient/openstack/common/strutils.py index 1383fbb7e..c22063d90 100644 --- a/novaclient/openstack/common/strutils.py +++ b/novaclient/openstack/common/strutils.py @@ -98,7 +98,8 @@ def bool_from_string(subject, strict=False, default=False): def safe_decode(text, incoming=None, errors='strict'): - """Decodes incoming str using `incoming` if they're not already unicode. + """Decodes incoming text/bytes string using `incoming` if they're not + already unicode. :param incoming: Text's current encoding :param errors: Errors handling policy. See here for valid @@ -107,7 +108,7 @@ def safe_decode(text, incoming=None, errors='strict'): representation of it. :raises TypeError: If text is not an instance of str """ - if not isinstance(text, six.string_types): + if not isinstance(text, (six.string_types, six.binary_type)): raise TypeError("%s can't be decoded" % type(text)) if isinstance(text, six.text_type): @@ -137,7 +138,7 @@ def safe_decode(text, incoming=None, errors='strict'): def safe_encode(text, incoming=None, encoding='utf-8', errors='strict'): - """Encodes incoming str/unicode using `encoding`. + """Encodes incoming text/bytes string using `encoding`. If incoming is not specified, text is expected to be encoded with current python's default encoding. (`sys.getdefaultencoding`) @@ -150,7 +151,7 @@ def safe_encode(text, incoming=None, representation of it. :raises TypeError: If text is not an instance of str """ - if not isinstance(text, six.string_types): + if not isinstance(text, (six.string_types, six.binary_type)): raise TypeError("%s can't be encoded" % type(text)) if not incoming: From 8475ada28c661527d75c35db13fbe1cd503dd2a1 Mon Sep 17 00:00:00 2001 From: Shuangtai Tian Date: Fri, 28 Mar 2014 09:55:44 +0800 Subject: [PATCH 0454/1705] typo in novaclient Change-Id: If30543ed7c94cd965f1d6b7f0d4af26b23914ba2 --- novaclient/v1_1/certs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/v1_1/certs.py b/novaclient/v1_1/certs.py index b33d52134..232d7c1fd 100644 --- a/novaclient/v1_1/certs.py +++ b/novaclient/v1_1/certs.py @@ -37,7 +37,7 @@ class CertificateManager(base.Manager): def create(self): """ - Create a x509 certificates for a user in tenant. + Create a x509 certificate for a user in tenant. """ return self._create('/os-certificates', {}, 'certificate') From 17a8b78eecf4a69299f9550008e743d01c57f738 Mon Sep 17 00:00:00 2001 From: Johannes Erdfelt Date: Tue, 25 Mar 2014 10:25:06 -0700 Subject: [PATCH 0455/1705] Remove unused arguments to _boot() reservation_id, min_count, max_count are never specified by any callers. Remove the arguments and the code that checks the values. Change-Id: I3794fd1eabbfb999c422c701af1ff64cd667313f --- novaclient/v1_1/shell.py | 16 +++------------- novaclient/v3/shell.py | 16 +++------------- 2 files changed, 6 insertions(+), 26 deletions(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index e3171976d..a2a50bbab 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -129,19 +129,8 @@ def _parse_block_device_mapping_v2(args, image): return bdm -def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): +def _boot(cs, args): """Boot a new server.""" - if min_count is None: - min_count = 1 - if max_count is None: - max_count = min_count - if min_count > max_count: - raise exceptions.CommandError(_("min_instances should be <= " - "max_instances")) - if not min_count or not max_count: - raise exceptions.CommandError(_("min_instances nor max_instances " - "should be 0")) - if args.image: image = _find_image(cs, args.image) else: @@ -157,6 +146,8 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): if not args.flavor: raise exceptions.CommandError(_("you need to specify a Flavor ID ")) + min_count = 1 + max_count = 1 if args.num_instances is not None: if args.num_instances <= 1: raise exceptions.CommandError(_("num_instances should be > 1")) @@ -280,7 +271,6 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): meta=meta, files=files, key_name=key_name, - reservation_id=reservation_id, min_count=min_count, max_count=max_count, userdata=userdata, diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 10a6975d8..cbf25051a 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -60,19 +60,8 @@ def _match_image(cs, wanted_properties): return images_matched -def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): +def _boot(cs, args): """Boot a new server.""" - if min_count is None: - min_count = 1 - if max_count is None: - max_count = min_count - if min_count > max_count: - raise exceptions.CommandError("min_instances should be <= " - "max_instances") - if not min_count or not max_count: - raise exceptions.CommandError("min_instances nor max_instances should" - "be 0") - if args.image: image = _find_image(cs.image_cs, args.image) else: @@ -93,6 +82,8 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): if not args.flavor: raise exceptions.CommandError("you need to specify a Flavor ID ") + min_count = 1 + max_count = 1 if args.num_instances is not None: if args.num_instances <= 1: raise exceptions.CommandError("num_instances should be > 1") @@ -191,7 +182,6 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): meta=meta, files=files, key_name=key_name, - reservation_id=reservation_id, min_count=min_count, max_count=max_count, userdata=userdata, From 9344f02649df65af26ca13b4d3443be73beb5d4e Mon Sep 17 00:00:00 2001 From: Davanum Srinivas Date: Thu, 27 Mar 2014 20:14:48 +0000 Subject: [PATCH 0456/1705] Fix for invalid literal ValueError parsing ipv6 url(s) Switch to using network_utils for splitting the URL. The code in oslo-incubator supports ipv6 urls HEAD of oslo-incubator is bb52a3fc49f033b9f36238231ca56e754a78cf4b Updated openstack-common.conf to pick up the new dependency from oslo-incubator Change-Id: Ifa3dec384e85942a191260d17e8141030d31ff84 Closes-Bug: #1298137 --- novaclient/client.py | 3 +- novaclient/openstack/common/network_utils.py | 108 +++++++++++++++++++ novaclient/tests/test_auth_plugins.py | 6 +- novaclient/tests/test_http.py | 3 +- novaclient/tests/utils.py | 4 + novaclient/tests/v1_1/test_auth.py | 26 +++-- openstack-common.conf | 1 + 7 files changed, 135 insertions(+), 16 deletions(-) create mode 100644 novaclient/openstack/common/network_utils.py diff --git a/novaclient/client.py b/novaclient/client.py index 54a9e815a..53bf2f479 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -35,6 +35,7 @@ from novaclient import exceptions from novaclient.openstack.common.gettextutils import _ +from novaclient.openstack.common import network_utils from novaclient import service_catalog from novaclient import utils @@ -355,7 +356,7 @@ def _fetch_endpoints_from_auth(self, url): extract_token=False) def authenticate(self): - magic_tuple = parse.urlsplit(self.auth_url) + magic_tuple = network_utils.urlsplit(self.auth_url) scheme, netloc, path, query, frag = magic_tuple port = magic_tuple.port if port is None: diff --git a/novaclient/openstack/common/network_utils.py b/novaclient/openstack/common/network_utils.py new file mode 100644 index 000000000..fa812b29f --- /dev/null +++ b/novaclient/openstack/common/network_utils.py @@ -0,0 +1,108 @@ +# Copyright 2012 OpenStack Foundation. +# All Rights Reserved. +# +# 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. + +""" +Network-related utilities and helper functions. +""" + +# TODO(jd) Use six.moves once +# https://bitbucket.org/gutworth/six/pull-request/28 +# is merged +try: + import urllib.parse + SplitResult = urllib.parse.SplitResult +except ImportError: + import urlparse + SplitResult = urlparse.SplitResult + +from six.moves.urllib import parse + + +def parse_host_port(address, default_port=None): + """Interpret a string as a host:port pair. + + An IPv6 address MUST be escaped if accompanied by a port, + because otherwise ambiguity ensues: 2001:db8:85a3::8a2e:370:7334 + means both [2001:db8:85a3::8a2e:370:7334] and + [2001:db8:85a3::8a2e:370]:7334. + + >>> parse_host_port('server01:80') + ('server01', 80) + >>> parse_host_port('server01') + ('server01', None) + >>> parse_host_port('server01', default_port=1234) + ('server01', 1234) + >>> parse_host_port('[::1]:80') + ('::1', 80) + >>> parse_host_port('[::1]') + ('::1', None) + >>> parse_host_port('[::1]', default_port=1234) + ('::1', 1234) + >>> parse_host_port('2001:db8:85a3::8a2e:370:7334', default_port=1234) + ('2001:db8:85a3::8a2e:370:7334', 1234) + + """ + if address[0] == '[': + # Escaped ipv6 + _host, _port = address[1:].split(']') + host = _host + if ':' in _port: + port = _port.split(':')[1] + else: + port = default_port + else: + if address.count(':') == 1: + host, port = address.split(':') + else: + # 0 means ipv4, >1 means ipv6. + # We prohibit unescaped ipv6 addresses with port. + host = address + port = default_port + + return (host, None if port is None else int(port)) + + +class ModifiedSplitResult(SplitResult): + """Split results class for urlsplit.""" + + # NOTE(dims): The functions below are needed for Python 2.6.x. + # We can remove these when we drop support for 2.6.x. + @property + def hostname(self): + netloc = self.netloc.split('@', 1)[-1] + host, port = parse_host_port(netloc) + return host + + @property + def port(self): + netloc = self.netloc.split('@', 1)[-1] + host, port = parse_host_port(netloc) + return port + + +def urlsplit(url, scheme='', allow_fragments=True): + """Parse a URL using urlparse.urlsplit(), splitting query and fragments. + This function papers over Python issue9374 when needed. + + The parameters are the same as urlparse.urlsplit. + """ + scheme, netloc, path, query, fragment = parse.urlsplit( + url, scheme, allow_fragments) + if allow_fragments and '#' in path: + path, fragment = path.split('#', 1) + if '?' in path: + path, query = path.split('?', 1) + return ModifiedSplitResult(scheme, netloc, + path, query, fragment) diff --git a/novaclient/tests/test_auth_plugins.py b/novaclient/tests/test_auth_plugins.py index a08459416..06c16a3a2 100644 --- a/novaclient/tests/test_auth_plugins.py +++ b/novaclient/tests/test_auth_plugins.py @@ -96,7 +96,7 @@ def mock_iter_entry_points(_type, name): def test_auth_call(): plugin = auth_plugin.DeprecatedAuthPlugin("fake") cs = client.Client("username", "password", "project_id", - "auth_url/v2.0", auth_system="fake", + utils.AUTH_URL_V2, auth_system="fake", auth_plugin=plugin) cs.client.authenticate() @@ -126,7 +126,7 @@ def test_auth_call(): auth_plugin.discover_auth_systems() plugin = auth_plugin.DeprecatedAuthPlugin("notexists") cs = client.Client("username", "password", "project_id", - "auth_url/v2.0", auth_system="notexists", + utils.AUTH_URL_V2, auth_system="notexists", auth_plugin=plugin) self.assertRaises(exceptions.AuthSystemNotFound, cs.client.authenticate) @@ -217,7 +217,7 @@ def authenticate(self, cls, auth_url): auth_plugin.discover_auth_systems() plugin = auth_plugin.load_plugin("fake") cs = client.Client("username", "password", "project_id", - "auth_url/v2.0", auth_system="fake", + utils.AUTH_URL_V2, auth_system="fake", auth_plugin=plugin) cs.client.authenticate() diff --git a/novaclient/tests/test_http.py b/novaclient/tests/test_http.py index e7092781d..69e1c9709 100644 --- a/novaclient/tests/test_http.py +++ b/novaclient/tests/test_http.py @@ -47,7 +47,8 @@ def get_client(): cl = client.HTTPClient("username", "password", - "project_id", "auth_test") + "project_id", + utils.AUTH_URL_V2) return cl diff --git a/novaclient/tests/utils.py b/novaclient/tests/utils.py index 11bfa827b..cb8ded810 100644 --- a/novaclient/tests/utils.py +++ b/novaclient/tests/utils.py @@ -17,6 +17,10 @@ import requests import testtools +AUTH_URL = "http://localhost:5002/auth_url" +AUTH_URL_V1 = "http://localhost:5002/auth_url/v1.0" +AUTH_URL_V2 = "http://localhost:5002/auth_url/v2.0" + class TestCase(testtools.TestCase): TEST_REQUEST_BASE = { diff --git a/novaclient/tests/v1_1/test_auth.py b/novaclient/tests/v1_1/test_auth.py index 7344bc76e..7b1a441a8 100644 --- a/novaclient/tests/v1_1/test_auth.py +++ b/novaclient/tests/v1_1/test_auth.py @@ -25,7 +25,7 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase): def test_authenticate_success(self): cs = client.Client("username", "password", "project_id", - "auth_url/v2.0", service_type='compute') + utils.AUTH_URL_V2, service_type='compute') resp = { "access": { "token": { @@ -94,7 +94,7 @@ def test_auth_call(): def test_authenticate_failure(self): cs = client.Client("username", "password", "project_id", - "auth_url/v2.0") + utils.AUTH_URL_V2) resp = {"unauthorized": {"message": "Unauthorized", "code": "401"}} auth_response = utils.TestResponse({ "status_code": 401, @@ -111,7 +111,7 @@ def test_auth_call(): def test_v1_auth_redirect(self): cs = client.Client("username", "password", "project_id", - "auth_url/v1.0", service_type='compute') + utils.AUTH_URL_V1, service_type='compute') dict_correct_response = { "access": { "token": { @@ -199,7 +199,7 @@ def test_auth_call(): def test_v2_auth_redirect(self): cs = client.Client("username", "password", "project_id", - "auth_url/v2.0", service_type='compute') + utils.AUTH_URL_V2, service_type='compute') dict_correct_response = { "access": { "token": { @@ -287,7 +287,7 @@ def test_auth_call(): def test_ambiguous_endpoints(self): cs = client.Client("username", "password", "project_id", - "auth_url/v2.0", service_type='compute') + utils.AUTH_URL_V2, service_type='compute') resp = { "access": { "token": { @@ -340,7 +340,7 @@ def test_auth_call(): def test_authenticate_with_token_success(self): cs = client.Client("username", None, "project_id", - "auth_url/v2.0", service_type='compute') + utils.AUTH_URL_V2, service_type='compute') cs.client.auth_token = "FAKE_ID" resp = { "access": { @@ -405,7 +405,7 @@ def test_authenticate_with_token_success(self): self.assertEqual(cs.client.auth_token, token_id) def test_authenticate_with_token_failure(self): - cs = client.Client("username", None, "project_id", "auth_url/v2.0") + cs = client.Client("username", None, "project_id", utils.AUTH_URL_V2) cs.client.auth_token = "FAKE_ID" resp = {"unauthorized": {"message": "Unauthorized", "code": "401"}} auth_response = utils.TestResponse({ @@ -421,7 +421,8 @@ def test_authenticate_with_token_failure(self): class AuthenticationTests(utils.TestCase): def test_authenticate_success(self): - cs = client.Client("username", "password", "project_id", "auth_url") + cs = client.Client("username", "password", + "project_id", utils.AUTH_URL) management_url = 'https://localhost/v1.1/443470' auth_response = utils.TestResponse({ 'status_code': 204, @@ -456,7 +457,8 @@ def test_auth_call(): test_auth_call() def test_authenticate_failure(self): - cs = client.Client("username", "password", "project_id", "auth_url") + cs = client.Client("username", "password", + "project_id", utils.AUTH_URL) auth_response = utils.TestResponse({'status_code': 401}) mock_request = mock.Mock(return_value=(auth_response)) @@ -467,7 +469,8 @@ def test_auth_call(): test_auth_call() def test_auth_automatic(self): - cs = client.Client("username", "password", "project_id", "auth_url") + cs = client.Client("username", "password", + "project_id", utils.AUTH_URL) http_client = cs.client http_client.management_url = '' mock_request = mock.Mock(return_value=(None, None)) @@ -482,7 +485,8 @@ def test_auth_call(m): test_auth_call() def test_auth_manual(self): - cs = client.Client("username", "password", "project_id", "auth_url") + cs = client.Client("username", "password", + "project_id", utils.AUTH_URL) @mock.patch.object(cs.client, 'authenticate') def test_auth_call(m): diff --git a/openstack-common.conf b/openstack-common.conf index 9b72a78e9..23c09bc0d 100644 --- a/openstack-common.conf +++ b/openstack-common.conf @@ -9,6 +9,7 @@ module=uuidutils module=apiclient module=importutils module=cliutils +module=network_utils # The base module to hold the copy of openstack.common base=novaclient From 02328d33373479b33f4fee8112d47c1ae29e8bd4 Mon Sep 17 00:00:00 2001 From: liu-sheng Date: Fri, 28 Mar 2014 11:32:39 +0800 Subject: [PATCH 0457/1705] Allow admin user to get all tenant's floating IPs When getting floatingips with Nova API, the results will be filtered with the 'tenant_id'. So, we can only get the floatingips belonging to the tenant of the current context. When ceilometer invokes novaclient to list floatingips, it will get an empty list because the tenant is 'service'. we should allow an admin user to index all tenants's floatingip by adding a parameter 'all_tenants'. This patch provides CLI support Change-Id: I35a2155401247d49017bf3c03341b082cb87750d Closes-bug: #1262124 --- novaclient/tests/v1_1/test_floating_ips.py | 11 +++++++++-- novaclient/tests/v1_1/test_shell.py | 4 ++++ novaclient/v1_1/floating_ips.py | 9 ++++++--- novaclient/v1_1/shell.py | 10 +++++++--- 4 files changed, 26 insertions(+), 8 deletions(-) diff --git a/novaclient/tests/v1_1/test_floating_ips.py b/novaclient/tests/v1_1/test_floating_ips.py index 0b2313f51..5a9147fa0 100644 --- a/novaclient/tests/v1_1/test_floating_ips.py +++ b/novaclient/tests/v1_1/test_floating_ips.py @@ -25,9 +25,16 @@ class FloatingIPsTest(utils.TestCase): def test_list_floating_ips(self): - fl = cs.floating_ips.list() + fips = cs.floating_ips.list() cs.assert_called('GET', '/os-floating-ips') - [self.assertIsInstance(f, floating_ips.FloatingIP) for f in fl] + for fip in fips: + self.assertIsInstance(fip, floating_ips.FloatingIP) + + def test_list_floating_ips_all_tenants(self): + fips = cs.floating_ips.list(all_tenants=True) + cs.assert_called('GET', '/os-floating-ips?all_tenants=1') + for fip in fips: + self.assertIsInstance(fip, floating_ips.FloatingIP) def test_delete_floating_ip(self): fl = cs.floating_ips.list()[0] diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index 58c50b64d..bfff2e34e 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -1039,6 +1039,10 @@ def test_floating_ip_list(self): self.run_command('floating-ip-list') self.assert_called('GET', '/os-floating-ips') + def test_floating_ip_list_all_tenants(self): + self.run_command('floating-ip-list --all-tenants') + self.assert_called('GET', '/os-floating-ips?all_tenants=1') + def test_floating_ip_create(self): self.run_command('floating-ip-create') self.assert_called('GET', '/os-floating-ips/1') diff --git a/novaclient/v1_1/floating_ips.py b/novaclient/v1_1/floating_ips.py index 25f5a7437..54979371f 100644 --- a/novaclient/v1_1/floating_ips.py +++ b/novaclient/v1_1/floating_ips.py @@ -28,11 +28,14 @@ def delete(self): class FloatingIPManager(base.ManagerWithFind): resource_class = FloatingIP - def list(self): + def list(self, all_tenants=False): """ - List floating ips for a tenant + List floating ips """ - return self._list("/os-floating-ips", "floating_ips") + url = '/os-floating-ips' + if all_tenants: + url += '?all_tenants=1' + return self._list(url, "floating_ips") def create(self, pool=None): """ diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index a2a50bbab..7b99f1ecf 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2031,9 +2031,13 @@ def do_floating_ip_delete(cs, args): args.address) -def do_floating_ip_list(cs, _args): - """List floating ips for this tenant.""" - _print_floating_ip_list(cs.floating_ips.list()) +@utils.arg('--all-tenants', + action='store_true', + default=False, + help=_('Display floatingips from all tenants (Admin only).')) +def do_floating_ip_list(cs, args): + """List floating ips.""" + _print_floating_ip_list(cs.floating_ips.list(args.all_tenants)) def do_floating_ip_pool_list(cs, _args): From 9162a5fe8fa4a5c9d59648aa1e9fbd48dec079f9 Mon Sep 17 00:00:00 2001 From: Johannes Erdfelt Date: Fri, 4 Apr 2014 10:57:40 -0700 Subject: [PATCH 0458/1705] Split test_rebuild() into two tests The method tests rebuilds with and without passwords, but the tests are independent and should be in two seperate tests. Also, remove duplication of the rebuild with password test from test_rebuild_preserve_ephemeral Change-Id: I40d0376fabf319dbe703d593de8fb4cdcf0aeab1 --- novaclient/tests/v1_1/test_shell.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index f8fca15d9..fb0a6e613 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -748,6 +748,7 @@ def test_rebuild(self): self.assert_called('GET', '/flavors/1', pos=-2) self.assert_called('GET', '/images/2') + def test_rebuild_password(self): self.run_command('rebuild sample-server 1 --rebuild-password asdf') self.assert_called('GET', '/servers', pos=-8) self.assert_called('GET', '/servers/1234', pos=-7) @@ -769,16 +770,6 @@ def test_rebuild_preserve_ephemeral(self): self.assert_called('GET', '/flavors/1', pos=-2) self.assert_called('GET', '/images/2') - self.run_command('rebuild sample-server 1 --rebuild-password asdf') - self.assert_called('GET', '/servers', pos=-8) - self.assert_called('GET', '/servers/1234', pos=-7) - self.assert_called('GET', '/images/1', pos=-6) - self.assert_called('POST', '/servers/1234/action', - {'rebuild': {'imageRef': 1, 'adminPass': 'asdf'}}, - pos=-5) - self.assert_called('GET', '/flavors/1', pos=-2) - self.assert_called('GET', '/images/2') - def test_start(self): self.run_command('start sample-server') self.assert_called('POST', '/servers/1234/action', {'os-start': None}) From 04fdf97b356e6ae76fecd34f56f523d428a661f3 Mon Sep 17 00:00:00 2001 From: Johannes Erdfelt Date: Fri, 4 Apr 2014 12:31:40 -0700 Subject: [PATCH 0459/1705] Remove py3kcompat It's not used by novaclient anymore and isn't used by any other openstack common code either. Change-Id: I96ebf030d0a96436a8a75c6937c354003ee32c58 Closes-bug: 1280033 --- .../openstack/common/py3kcompat/__init__.py | 0 .../openstack/common/py3kcompat/urlutils.py | 67 ------------------- 2 files changed, 67 deletions(-) delete mode 100644 novaclient/openstack/common/py3kcompat/__init__.py delete mode 100644 novaclient/openstack/common/py3kcompat/urlutils.py diff --git a/novaclient/openstack/common/py3kcompat/__init__.py b/novaclient/openstack/common/py3kcompat/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/novaclient/openstack/common/py3kcompat/urlutils.py b/novaclient/openstack/common/py3kcompat/urlutils.py deleted file mode 100644 index 84e457a44..000000000 --- a/novaclient/openstack/common/py3kcompat/urlutils.py +++ /dev/null @@ -1,67 +0,0 @@ -# -# Copyright 2013 Canonical Ltd. -# All Rights Reserved. -# -# 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. -# - -""" -Python2/Python3 compatibility layer for OpenStack -""" - -import six - -if six.PY3: - # python3 - import urllib.error - import urllib.parse - import urllib.request - - urlencode = urllib.parse.urlencode - urljoin = urllib.parse.urljoin - quote = urllib.parse.quote - quote_plus = urllib.parse.quote_plus - parse_qsl = urllib.parse.parse_qsl - unquote = urllib.parse.unquote - unquote_plus = urllib.parse.unquote_plus - urlparse = urllib.parse.urlparse - urlsplit = urllib.parse.urlsplit - urlunsplit = urllib.parse.urlunsplit - SplitResult = urllib.parse.SplitResult - - urlopen = urllib.request.urlopen - URLError = urllib.error.URLError - pathname2url = urllib.request.pathname2url -else: - # python2 - import urllib - import urllib2 - import urlparse - - urlencode = urllib.urlencode - quote = urllib.quote - quote_plus = urllib.quote_plus - unquote = urllib.unquote - unquote_plus = urllib.unquote_plus - - parse = urlparse - parse_qsl = parse.parse_qsl - urljoin = parse.urljoin - urlparse = parse.urlparse - urlsplit = parse.urlsplit - urlunsplit = parse.urlunsplit - SplitResult = parse.SplitResult - - urlopen = urllib2.urlopen - URLError = urllib2.URLError - pathname2url = urllib.pathname2url From 496e39a260b75432958f7d8a8d1629e425795c4e Mon Sep 17 00:00:00 2001 From: Johannes Erdfelt Date: Fri, 4 Apr 2014 10:53:37 -0700 Subject: [PATCH 0460/1705] Print adminPass when rebuilding from shell The 'nova rebuild' command did not print the adminPass returned by the server. This is because it discards the instance information returned by the rebuild action and fetchs the information again. This would lose the adminPass and it would not be printed for the user. This patch reuses the instance information returned by the rebuild action which ensures that the adminPass is printed by the shell. Change-Id: I144bfa298bef529c78ca04042cd37a763537f09e Closes-bug: 1302696 --- novaclient/tests/v1_1/fakes.py | 8 ++++---- novaclient/tests/v1_1/test_shell.py | 31 ++++++++++++++++------------- novaclient/v1_1/shell.py | 4 ++-- novaclient/v3/shell.py | 4 ++-- 4 files changed, 25 insertions(+), 22 deletions(-) diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index 4457d0b94..89a1d60fb 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -535,11 +535,11 @@ def post_servers_1234_action(self, body, **kw): assert list(body[action]) == ['type'] assert body[action]['type'] in ['HARD', 'SOFT'] elif action == 'rebuild': - keys = list(body[action]) - if 'adminPass' in keys: - keys.remove('adminPass') - assert 'imageRef' in keys + body = body[action] + adminPass = body.get('adminPass', 'randompassword') + assert 'imageRef' in body _body = self.get_servers_1234()[2] + _body['server']['adminPass'] = adminPass elif action == 'resize': keys = body[action].keys() assert 'flavorRef' in keys diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index fb0a6e613..d86180267 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -739,34 +739,37 @@ def test_reboot(self): {'reboot': {'type': 'HARD'}}) def test_rebuild(self): - self.run_command('rebuild sample-server 1') - self.assert_called('GET', '/servers', pos=-8) - self.assert_called('GET', '/servers/1234', pos=-7) - self.assert_called('GET', '/images/1', pos=-6) + output = self.run_command('rebuild sample-server 1') + self.assert_called('GET', '/servers', pos=-6) + self.assert_called('GET', '/servers/1234', pos=-5) + self.assert_called('GET', '/images/1', pos=-4) self.assert_called('POST', '/servers/1234/action', - {'rebuild': {'imageRef': 1}}, pos=-5) + {'rebuild': {'imageRef': 1}}, pos=-3) self.assert_called('GET', '/flavors/1', pos=-2) self.assert_called('GET', '/images/2') + self.assertIn('adminPass', output) def test_rebuild_password(self): - self.run_command('rebuild sample-server 1 --rebuild-password asdf') - self.assert_called('GET', '/servers', pos=-8) - self.assert_called('GET', '/servers/1234', pos=-7) - self.assert_called('GET', '/images/1', pos=-6) + output = self.run_command('rebuild sample-server 1' + ' --rebuild-password asdf') + self.assert_called('GET', '/servers', pos=-6) + self.assert_called('GET', '/servers/1234', pos=-5) + self.assert_called('GET', '/images/1', pos=-4) self.assert_called('POST', '/servers/1234/action', {'rebuild': {'imageRef': 1, 'adminPass': 'asdf'}}, - pos=-5) + pos=-3) self.assert_called('GET', '/flavors/1', pos=-2) self.assert_called('GET', '/images/2') + self.assertIn('adminPass', output) def test_rebuild_preserve_ephemeral(self): self.run_command('rebuild sample-server 1 --preserve-ephemeral') - self.assert_called('GET', '/servers', pos=-8) - self.assert_called('GET', '/servers/1234', pos=-7) - self.assert_called('GET', '/images/1', pos=-6) + self.assert_called('GET', '/servers', pos=-6) + self.assert_called('GET', '/servers/1234', pos=-5) + self.assert_called('GET', '/images/1', pos=-4) self.assert_called('POST', '/servers/1234/action', {'rebuild': {'imageRef': 1, - 'preserve_ephemeral': True}}, pos=-5) + 'preserve_ephemeral': True}}, pos=-3) self.assert_called('GET', '/flavors/1', pos=-2) self.assert_called('GET', '/images/2') diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index f2dac6224..1c6b28c21 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1210,8 +1210,8 @@ def do_rebuild(cs, args): kwargs = utils.get_resource_manager_extra_kwargs(do_rebuild, args) kwargs['preserve_ephemeral'] = args.preserve_ephemeral - server.rebuild(image, _password, **kwargs) - _print_server(cs, args) + server = server.rebuild(image, _password, **kwargs) + _print_server(cs, args, server) if args.poll: _poll_for_status(cs.servers.get, server.id, 'rebuilding', ['active']) diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 75e435985..b9a03ba4f 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -1055,8 +1055,8 @@ def do_rebuild(cs, args): _password = None kwargs = utils.get_resource_manager_extra_kwargs(do_rebuild, args) - server.rebuild(image, _password, **kwargs) - _print_server(cs, args) + server = server.rebuild(image, _password, **kwargs) + _print_server(cs, args, server) if args.poll: _poll_for_status(cs.servers.get, server.id, 'rebuilding', ['active']) From 98934d7bf1464afe0f7fe98efd2a591d95ac9c41 Mon Sep 17 00:00:00 2001 From: Boris Pavlovic Date: Wed, 26 Mar 2014 15:22:03 +0400 Subject: [PATCH 0461/1705] Fix session handling in novaclient Prior to this patch, novaclient was handling sessions in an inconsistent manner. Every time we created a client instance, it would use a global connection pool, which made it difficult to use in a process that is meant to be forked. Obviously sessions like the ones provided by the requests library that will automatically cause connections to be kept alive should not be implicit. This patch moves the novaclient back to the age of a single session-less request call by default, but also adds two more resource-reuse friendly options that a user needs to be explicit about. The first one is that both v1_1 and v3 clients can now be used as context managers,. where the session will be kept open (and thus the connection kept-alive) for the duration of the with block. This is far more ideal for a web worker use-case as the session can be made request-long. The second one is the per-instance session. This is very similar to what we had up until now, except it is not a global object so forking is possible as long as each child instantiates it's own client. The session once created will be kept open for the duration of the client object lifetime. Please note: client instances are not thread safe. As can be seen from above forking example - if you wish to use threading/multiprocessing, you *must not* share client instances. DocImpact Related-bug: #1247056 Closes-Bug: #1297796 Co-authored-by: Nikola Dipanov Change-Id: Id59e48f61bb3f3c6223302355c849e1e99673410 --- novaclient/client.py | 74 ++++++++++++++------- novaclient/tests/test_auth_plugins.py | 8 +-- novaclient/tests/test_client.py | 96 ++++++++++++++++++++++++++- novaclient/tests/test_http.py | 10 +-- novaclient/tests/v1_1/test_auth.py | 12 ++-- novaclient/v1_1/client.py | 27 +++++++- novaclient/v3/client.py | 27 +++++++- 7 files changed, 208 insertions(+), 46 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index 53bf2f479..6c1ad13e7 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -40,17 +40,19 @@ from novaclient import utils -_ADAPTERS = {} +class _ClientConnectionPool(object): + def __init__(self): + self._adapters = {} -def _adapter_pool(url): - """ - Store and reuse HTTP adapters per Service URL. - """ - if url not in _ADAPTERS: - _ADAPTERS[url] = adapters.HTTPAdapter() + def get(self, url): + """ + Store and reuse HTTP adapters per Service URL. + """ + if url not in self._adapters: + self._adapters[url] = adapters.HTTPAdapter() - return _ADAPTERS[url] + return self._adapters[url] class HTTPClient(object): @@ -65,13 +67,17 @@ def __init__(self, user, password, projectid=None, auth_url=None, os_cache=False, no_cache=True, http_log_debug=False, auth_system='keystone', auth_plugin=None, auth_token=None, - cacert=None, tenant_id=None, user_id=None): + cacert=None, tenant_id=None, user_id=None, + connection_pool=False): self.user = user self.user_id = user_id self.password = password self.projectid = projectid self.tenant_id = tenant_id + self._connection_pool = (_ClientConnectionPool() + if connection_pool else None) + # This will be called by #_get_password if self.password is None. # EG if a password can only be obtained by prompting the user, but a # token is available, you don't want to prompt until the token has @@ -120,8 +126,8 @@ def __init__(self, user, password, projectid=None, auth_url=None, self.auth_system = auth_system self.auth_plugin = auth_plugin + self._session = None self._current_url = None - self._http = None self._logger = logging.getLogger(__name__) if self.http_log_debug and not self._logger.handlers: @@ -182,19 +188,33 @@ def http_log_resp(self, resp): 'headers': resp.headers, 'text': resp.text}) - def http(self, url): - magic_tuple = parse.urlsplit(url) - scheme, netloc, path, query, frag = magic_tuple - service_url = '%s://%s' % (scheme, netloc) - if self._current_url != service_url: - # Invalidate Session object in case the url is somehow changed - if self._http: - self._http.close() - self._current_url = service_url - self._logger.debug("New session created for: (%s)" % service_url) - self._http = requests.Session() - self._http.mount(service_url, _adapter_pool(service_url)) - return self._http + def open_session(self): + if not self._connection_pool: + self._session = requests.Session() + + def close_session(self): + if self._session and not self._connection_pool: + self._session.close() + self._session = None + + def _get_session(self, url): + if self._connection_pool: + magic_tuple = parse.urlsplit(url) + scheme, netloc, path, query, frag = magic_tuple + service_url = '%s://%s' % (scheme, netloc) + if self._current_url != service_url: + # Invalidate Session object in case the url is somehow changed + if self._session: + self._session.close() + self._current_url = service_url + self._logger.debug( + "New session created for: (%s)" % service_url) + self._session = requests.Session() + self._session.mount(service_url, + self._connection_pool.get(service_url)) + return self._session + elif self._session: + return self._session def request(self, url, method, **kwargs): kwargs.setdefault('headers', kwargs.get('headers', {})) @@ -209,7 +229,13 @@ def request(self, url, method, **kwargs): kwargs['verify'] = self.verify_cert self.http_log_req(method, url, kwargs) - resp = self.http(url).request( + + request_func = requests.request + session = self._get_session(url) + if session: + request_func = session.request + + resp = request_func( method, url, **kwargs) diff --git a/novaclient/tests/test_auth_plugins.py b/novaclient/tests/test_auth_plugins.py index 06c16a3a2..34e83baca 100644 --- a/novaclient/tests/test_auth_plugins.py +++ b/novaclient/tests/test_auth_plugins.py @@ -92,7 +92,7 @@ def mock_iter_entry_points(_type, name): @mock.patch.object(pkg_resources, "iter_entry_points", mock_iter_entry_points) - @mock.patch.object(requests.Session, "request", mock_request) + @mock.patch.object(requests, "request", mock_request) def test_auth_call(): plugin = auth_plugin.DeprecatedAuthPlugin("fake") cs = client.Client("username", "password", "project_id", @@ -121,7 +121,7 @@ def mock_iter_entry_points(_t, name=None): @mock.patch.object(pkg_resources, "iter_entry_points", mock_iter_entry_points) - @mock.patch.object(requests.Session, "request", mock_request) + @mock.patch.object(requests, "request", mock_request) def test_auth_call(): auth_plugin.discover_auth_systems() plugin = auth_plugin.DeprecatedAuthPlugin("notexists") @@ -164,7 +164,7 @@ def mock_iter_entry_points(_type, name): @mock.patch.object(pkg_resources, "iter_entry_points", mock_iter_entry_points) - @mock.patch.object(requests.Session, "request", mock_request) + @mock.patch.object(requests, "request", mock_request) def test_auth_call(): plugin = auth_plugin.DeprecatedAuthPlugin("fakewithauthurl") cs = client.Client("username", "password", "project_id", @@ -197,7 +197,7 @@ def auth_url(self): class AuthPluginTest(utils.TestCase): - @mock.patch.object(requests.Session, "request") + @mock.patch.object(requests, "request") @mock.patch.object(pkg_resources, "iter_entry_points") def test_auth_system_success(self, mock_iter_entry_points, mock_request): """Test that we can authenticate using the auth system.""" diff --git a/novaclient/tests/test_client.py b/novaclient/tests/test_client.py index 8a307a363..96901b9d2 100644 --- a/novaclient/tests/test_client.py +++ b/novaclient/tests/test_client.py @@ -27,6 +27,16 @@ import json +class ClientConnectionPoolTest(utils.TestCase): + + @mock.patch("novaclient.client.adapters.HTTPAdapter") + def test_get(self, mock_http_adapter): + mock_http_adapter.side_effect = lambda: mock.Mock() + pool = novaclient.client._ClientConnectionPool() + self.assertEqual(pool.get("abc"), pool.get("abc")) + self.assertNotEqual(pool.get("abc"), pool.get("def")) + + class ClientTest(utils.TestCase): def test_client_with_timeout(self): @@ -43,9 +53,9 @@ def test_client_with_timeout(self): 'x-server-management-url': 'blah.com', 'x-auth-token': 'blah', } - with mock.patch('requests.Session.request', mock_request): + with mock.patch('requests.request', mock_request): instance.authenticate() - requests.Session.request.assert_called_with(mock.ANY, mock.ANY, + requests.request.assert_called_with(mock.ANY, mock.ANY, timeout=2, headers=mock.ANY, verify=mock.ANY) @@ -61,7 +71,7 @@ def test_client_reauth(self): instance.version = 'v2.0' mock_request = mock.Mock() mock_request.side_effect = novaclient.exceptions.Unauthorized(401) - with mock.patch('requests.Session.request', mock_request): + with mock.patch('requests.request', mock_request): try: instance.get('/servers/detail') except Exception: @@ -197,6 +207,26 @@ def test_authenticate_call_v3(self, mock_authenticate): cs.authenticate() self.assertTrue(mock_authenticate.called) + @mock.patch('novaclient.client.HTTPClient') + def test_contextmanager_v1_1(self, mock_http_client): + fake_client = mock.Mock() + mock_http_client.return_value = fake_client + with novaclient.v1_1.client.Client("user", "password", "project_id", + auth_url="foo/v2") as client: + pass + self.assertTrue(fake_client.open_session.called) + self.assertTrue(fake_client.close_session.called) + + @mock.patch('novaclient.client.HTTPClient') + def test_contextmanager_v3(self, mock_http_client): + fake_client = mock.Mock() + mock_http_client.return_value = fake_client + with novaclient.v3.client.Client("user", "password", "project_id", + auth_url="foo/v2") as client: + pass + self.assertTrue(fake_client.open_session.called) + self.assertTrue(fake_client.close_session.called) + def test_get_password_simple(self): cs = novaclient.client.HTTPClient("user", "password", "", "") cs.password_func = mock.Mock() @@ -230,3 +260,63 @@ def test_token_and_bypass_url(self): self.assertEqual(cs.auth_token, "12345") self.assertEqual(cs.bypass_url, "compute/v100") self.assertEqual(cs.management_url, "compute/v100") + + @mock.patch("novaclient.client.requests.Session") + def test_session(self, mock_session): + fake_session = mock.Mock() + mock_session.return_value = fake_session + cs = novaclient.client.HTTPClient("user", None, "", "") + cs.open_session() + self.assertEqual(cs._session, fake_session) + cs.close_session() + self.assertIsNone(cs._session) + + def test_session_connection_pool(self): + cs = novaclient.client.HTTPClient("user", None, "", + "", connection_pool=True) + cs.open_session() + self.assertIsNone(cs._session) + cs.close_session() + self.assertIsNone(cs._session) + + def test_get_session(self): + cs = novaclient.client.HTTPClient("user", None, "", "") + self.assertIsNone(cs._get_session("http://nooooooooo.com")) + + @mock.patch("novaclient.client.requests.Session") + def test_get_session_open_session(self, mock_session): + fake_session = mock.Mock() + mock_session.return_value = fake_session + cs = novaclient.client.HTTPClient("user", None, "", "") + cs.open_session() + self.assertEqual(fake_session, cs._get_session("http://example.com")) + + @mock.patch("novaclient.client.requests.Session") + @mock.patch("novaclient.client._ClientConnectionPool") + def test_get_session_connection_pool(self, mock_pool, mock_session): + service_url = "http://example.com" + + pool = mock.MagicMock() + pool.get.return_value = "http_adapter" + mock_pool.return_value = pool + cs = novaclient.client.HTTPClient("user", None, "", + "", connection_pool=True) + cs._current_url = "http://another.com" + + session = cs._get_session(service_url) + self.assertEqual(session, mock_session.return_value) + pool.get.assert_called_once_with(service_url) + mock_session().mount.assert_called_once_with(service_url, + 'http_adapter') + + def test_init_without_connection_pool(self): + cs = novaclient.client.HTTPClient("user", None, "", "") + self.assertIsNone(cs._connection_pool) + + @mock.patch("novaclient.client._ClientConnectionPool") + def test_init_with_proper_connection_pool(self, mock_pool): + fake_pool = mock.Mock() + mock_pool.return_value = fake_pool + cs = novaclient.client.HTTPClient("user", None, "", + connection_pool=True) + self.assertEqual(cs._connection_pool, fake_pool) diff --git a/novaclient/tests/test_http.py b/novaclient/tests/test_http.py index 69e1c9709..2797a438f 100644 --- a/novaclient/tests/test_http.py +++ b/novaclient/tests/test_http.py @@ -64,7 +64,7 @@ class ClientTest(utils.TestCase): def test_get(self): cl = get_authed_client() - @mock.patch.object(requests.Session, "request", mock_request) + @mock.patch.object(requests, "request", mock_request) @mock.patch('time.time', mock.Mock(return_value=1234)) def test_get_call(): resp, body = cl.get("/hi") @@ -86,7 +86,7 @@ def test_get_call(): def test_post(self): cl = get_authed_client() - @mock.patch.object(requests.Session, "request", mock_request) + @mock.patch.object(requests, "request", mock_request) def test_post_call(): cl.post("/hi", body=[1, 2, 3]) headers = { @@ -118,7 +118,7 @@ def test_auth_call(): def test_connection_refused(self): cl = get_client() - @mock.patch.object(requests.Session, "request", refused_mock_request) + @mock.patch.object(requests, "request", refused_mock_request) def test_refused_call(): self.assertRaises(exceptions.ConnectionRefused, cl.get, "/hi") @@ -127,7 +127,7 @@ def test_refused_call(): def test_bad_request(self): cl = get_client() - @mock.patch.object(requests.Session, "request", bad_req_mock_request) + @mock.patch.object(requests, "request", bad_req_mock_request) def test_refused_call(): self.assertRaises(exceptions.BadRequest, cl.get, "/hi") @@ -142,7 +142,7 @@ def test_client_logger(self): "auth_test", http_log_debug=True) self.assertEqual(len(cl2._logger.handlers), 1) - @mock.patch.object(requests.Session, 'request', unknown_error_mock_request) + @mock.patch.object(requests, 'request', unknown_error_mock_request) def test_unknown_server_error(self): cl = get_client() # This would be cleaner with the context manager version of diff --git a/novaclient/tests/v1_1/test_auth.py b/novaclient/tests/v1_1/test_auth.py index 7b1a441a8..c3df530b4 100644 --- a/novaclient/tests/v1_1/test_auth.py +++ b/novaclient/tests/v1_1/test_auth.py @@ -57,7 +57,7 @@ def test_authenticate_success(self): mock_request = mock.Mock(return_value=(auth_response)) - @mock.patch.object(requests.Session, "request", mock_request) + @mock.patch.object(requests, "request", mock_request) def test_auth_call(): cs.client.authenticate() headers = { @@ -160,7 +160,7 @@ def side_effect(*args, **kwargs): mock_request = mock.Mock(side_effect=side_effect) - @mock.patch.object(requests.Session, "request", mock_request) + @mock.patch.object(requests, "request", mock_request) def test_auth_call(): cs.client.authenticate() headers = { @@ -248,7 +248,7 @@ def side_effect(*args, **kwargs): mock_request = mock.Mock(side_effect=side_effect) - @mock.patch.object(requests.Session, "request", mock_request) + @mock.patch.object(requests, "request", mock_request) def test_auth_call(): cs.client.authenticate() headers = { @@ -373,7 +373,7 @@ def test_authenticate_with_token_success(self): mock_request = mock.Mock(return_value=(auth_response)) - with mock.patch.object(requests.Session, "request", mock_request): + with mock.patch.object(requests, "request", mock_request): cs.client.authenticate() headers = { 'User-Agent': cs.client.USER_AGENT, @@ -433,7 +433,7 @@ def test_authenticate_success(self): }) mock_request = mock.Mock(return_value=(auth_response)) - @mock.patch.object(requests.Session, "request", mock_request) + @mock.patch.object(requests, "request", mock_request) def test_auth_call(): cs.client.authenticate() headers = { @@ -462,7 +462,7 @@ def test_authenticate_failure(self): auth_response = utils.TestResponse({'status_code': 401}) mock_request = mock.Mock(return_value=(auth_response)) - @mock.patch.object(requests.Session, "request", mock_request) + @mock.patch.object(requests, "request", mock_request) def test_auth_call(): self.assertRaises(exceptions.Unauthorized, cs.client.authenticate) diff --git a/novaclient/v1_1/client.py b/novaclient/v1_1/client.py index d4ebee629..6b59ee642 100644 --- a/novaclient/v1_1/client.py +++ b/novaclient/v1_1/client.py @@ -61,6 +61,20 @@ class Client(object): >>> client.flavors.list() ... + It is also possible to use an instance as a context manager in which + case there will be a session kept alive for the duration of the with + statement:: + + >>> with Client(USERNAME, PASSWORD, PROJECT_ID, AUTH_URL) as client: + ... client.servers.list() + ... client.flavors.list() + ... + + It is also possible to have a permanent (process-long) connection pool, + by passing a connection_pool=True:: + + >>> client = Client(USERNAME, PASSWORD, PROJECT_ID, + ... AUTH_URL, connection_pool=True) """ # FIXME(jesse): project_id isn't required to authenticate @@ -73,7 +87,8 @@ def __init__(self, username, api_key, project_id, auth_url=None, bypass_url=None, os_cache=False, no_cache=True, http_log_debug=False, auth_system='keystone', auth_plugin=None, auth_token=None, - cacert=None, tenant_id=None, user_id=None): + cacert=None, tenant_id=None, user_id=None, + connection_pool=False): # FIXME(comstud): Rename the api_key argument above when we # know it's not being used as keyword argument password = api_key @@ -147,7 +162,15 @@ def __init__(self, username, api_key, project_id, auth_url=None, bypass_url=bypass_url, os_cache=self.os_cache, http_log_debug=http_log_debug, - cacert=cacert) + cacert=cacert, + connection_pool=connection_pool) + + def __enter__(self): + self.client.open_session() + return self + + def __exit__(self, t, v, tb): + self.client.close_session() def set_management_url(self, url): self.client.set_management_url(url) diff --git a/novaclient/v3/client.py b/novaclient/v3/client.py index e9c5c6f5e..4f454e220 100644 --- a/novaclient/v3/client.py +++ b/novaclient/v3/client.py @@ -46,6 +46,20 @@ class Client(object): >>> client.flavors.list() ... + It is also possible to use an instance as a context manager in which + case there will be a session kept alive for the duration of the with + statement:: + + >>> with Client(USERNAME, PASSWORD, PROJECT_ID, AUTH_URL) as client: + ... client.servers.list() + ... client.flavors.list() + ... + + It is also possible to have a permanent (process-long) connection pool, + by passing a connection_pool=True:: + + >>> client = Client(USERNAME, PASSWORD, PROJECT_ID, + ... AUTH_URL, connection_pool=True) """ # FIXME(jesse): project_id isn't required to authenticate @@ -58,7 +72,8 @@ def __init__(self, username, password, project_id, auth_url=None, bypass_url=None, os_cache=False, no_cache=True, http_log_debug=False, auth_system='keystone', auth_plugin=None, auth_token=None, - cacert=None, tenant_id=None, user_id=None): + cacert=None, tenant_id=None, user_id=None, + connection_pool=False): self.projectid = project_id self.tenant_id = tenant_id self.user_id = user_id @@ -110,7 +125,15 @@ def __init__(self, username, password, project_id, auth_url=None, bypass_url=bypass_url, os_cache=os_cache, http_log_debug=http_log_debug, - cacert=cacert) + cacert=cacert, + connection_pool=connection_pool) + + def __enter__(self): + self.client.open_session() + return self + + def __exit__(self, t, v, tb): + self.client.close_session() def set_management_url(self, url): self.client.set_management_url(url) From f6cda643fe2c6fbea8361843617694c0793b5e9e Mon Sep 17 00:00:00 2001 From: OpenStack Jenkins Date: Mon, 7 Apr 2014 21:27:22 +0000 Subject: [PATCH 0462/1705] Updated from global requirements Change-Id: I123a065193ea330d71afe2fd2ee3fdcbcbd4a88b --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index bc59d2eca..388f0fd8b 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -3,7 +3,7 @@ hacking>=0.8.0,<0.9 coverage>=3.6 discover fixtures>=0.3.14 -keyring>=1.6.1,<2.0,>=2.1 +keyring>=2.1 mock>=1.0 sphinx>=1.1.2,<1.2 oslosphinx From 91caad97d37b5aad9d04b218b07d4075dbf3704b Mon Sep 17 00:00:00 2001 From: gtt116 Date: Wed, 26 Feb 2014 06:57:11 +0000 Subject: [PATCH 0463/1705] Add service-delete subcommand to delete a service In nova https://review.openstack.org/#/c/39998/ expose an API to delete a service. Both in V1.1 and V3. This patch make novaclient support it. Change-Id: I4aaabc866b464c046b46eda68734f37223c6d6a0 Implements: blueprint support-delete-service --- novaclient/tests/v1_1/fakes.py | 3 +++ novaclient/tests/v1_1/test_services.py | 4 ++++ novaclient/tests/v1_1/test_shell.py | 4 ++++ novaclient/v1_1/services.py | 4 ++++ novaclient/v1_1/shell.py | 6 ++++++ novaclient/v3/shell.py | 6 ++++++ 6 files changed, 27 insertions(+) diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index 89a1d60fb..983357619 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -1446,6 +1446,9 @@ def put_os_services_disable_log_reason(self, body, **kw): 'status': 'disabled', 'disabled_reason': body['disabled_reason']}}) + def delete_os_services_1(self, **kw): + return (204, {}, None) + # # Fixed IPs # diff --git a/novaclient/tests/v1_1/test_services.py b/novaclient/tests/v1_1/test_services.py index d66a11107..5ee33432e 100644 --- a/novaclient/tests/v1_1/test_services.py +++ b/novaclient/tests/v1_1/test_services.py @@ -78,6 +78,10 @@ def test_services_enable(self): self.assertIsInstance(service, self._get_service_type()) self.assertEqual(service.status, 'enabled') + def test_services_delete(self): + service = self.cs.services.delete('1') + self.cs.assert_called('DELETE', '/os-services/1') + def test_services_disable(self): service = self.cs.services.disable('host1', 'nova-cert') values = self._update_body("host1", "nova-cert") diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index d86180267..44538f252 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -1286,6 +1286,10 @@ def test_services_disable_with_reason(self): 'disabled_reason': 'no_reason'} self.assert_called('PUT', '/os-services/disable-log-reason', body) + def test_services_delete(self): + self.run_command('service-delete 1') + self.assert_called('DELETE', '/os-services/1') + def test_fixed_ips_get(self): self.run_command('fixed-ip-get 192.168.1.1') self.assert_called('GET', '/os-fixed-ips/192.168.1.1') diff --git a/novaclient/v1_1/services.py b/novaclient/v1_1/services.py index f2588eaa1..d51fa3ebf 100644 --- a/novaclient/v1_1/services.py +++ b/novaclient/v1_1/services.py @@ -69,3 +69,7 @@ def disable_log_reason(self, host, binary, reason): """Disable the service with reason.""" body = self._update_body(host, binary, reason) return self._update("/os-services/disable-log-reason", body, "service") + + def delete(self, service_id): + """Delete a service.""" + return self._delete("/os-services/%s" % service_id) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 1c6b28c21..74a3f7c77 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2880,6 +2880,12 @@ def do_service_disable(cs, args): utils.print_list([result], ['Host', 'Binary', 'Status']) +@utils.arg('id', metavar='', help=_('Id of service.')) +def do_service_delete(cs, args): + """Delete the service.""" + cs.services.delete(args.id) + + @utils.arg('fixed_ip', metavar='', help=_('Fixed IP Address.')) def do_fixed_ip_get(cs, args): """Retrieve info on a fixed ip.""" diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index b9a03ba4f..aade7489e 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -2390,6 +2390,12 @@ def do_service_disable(cs, args): utils.print_list([result], ['Host', 'Binary', 'Status']) +@utils.arg('id', metavar='', help='Id of service.') +def do_service_delete(cs, args): + """Delete the service.""" + cs.services.delete(args.id) + + @utils.arg('fixed_ip', metavar='', help='Fixed IP Address.') def do_fixed_ip_get(cs, args): """Retrieve info on a fixed ip.""" From 7df6579f7ac9283916182d270c780ebc32545a24 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Tue, 8 Apr 2014 12:52:17 -0700 Subject: [PATCH 0464/1705] Fix name arg help for volume-type-create Similar to change I10b1bd6d but for help on an argument rather than the command itself. Closes-Bug: #1304570 Change-Id: I4f08ffb6af248ccd7446012765d17c41567f4b38 --- novaclient/v1_1/shell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 1c6b28c21..cd38cb7f3 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1833,7 +1833,7 @@ def do_volume_type_list(cs, args): @utils.arg('name', metavar='', - help=_("Name of the new flavor")) + help=_("Name of the new volume type")) @utils.service_type('volume') def do_volume_type_create(cs, args): """Create a new volume type.""" From 119a480fc1dcaf57f10a30b0f07870c6936628bc Mon Sep 17 00:00:00 2001 From: Jamie Lennox Date: Tue, 4 Mar 2014 16:26:24 +1000 Subject: [PATCH 0465/1705] Convert aggregates testing to use httpretty This is the first in the series and so contains some setup classes. In general the original data will not be able to be removed from fakes until the very end because testing shell depends on a lot of it. Change-Id: I499fe968923b9452f60c1b2ca50d5749e89827de blueprint: httpretty-testing --- novaclient/tests/fixture_data/__init__.py | 0 novaclient/tests/fixture_data/aggregates.py | 52 ++++++++++ novaclient/tests/fixture_data/base.py | 32 ++++++ novaclient/tests/fixture_data/client.py | 109 ++++++++++++++++++++ novaclient/tests/utils.py | 38 +++++++ novaclient/tests/v1_1/test_aggregates.py | 58 +++++------ novaclient/tests/v3/test_aggregates.py | 14 +-- test-requirements.txt | 1 + 8 files changed, 260 insertions(+), 44 deletions(-) create mode 100644 novaclient/tests/fixture_data/__init__.py create mode 100644 novaclient/tests/fixture_data/aggregates.py create mode 100644 novaclient/tests/fixture_data/base.py create mode 100644 novaclient/tests/fixture_data/client.py diff --git a/novaclient/tests/fixture_data/__init__.py b/novaclient/tests/fixture_data/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/novaclient/tests/fixture_data/aggregates.py b/novaclient/tests/fixture_data/aggregates.py new file mode 100644 index 000000000..33e30905f --- /dev/null +++ b/novaclient/tests/fixture_data/aggregates.py @@ -0,0 +1,52 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import httpretty + +from novaclient.openstack.common import jsonutils +from novaclient.tests.fixture_data import base + + +class Fixture(base.Fixture): + + base_url = 'os-aggregates' + + def setUp(self): + super(Fixture, self).setUp() + + get_os_aggregates = {"aggregates": [ + {'id': '1', + 'name': 'test', + 'availability_zone': 'nova1'}, + {'id': '2', + 'name': 'test2', + 'availability_zone': 'nova1'}, + ]} + + httpretty.register_uri(httpretty.GET, self.url(), + body=jsonutils.dumps(get_os_aggregates), + content_type='application/json') + + r = jsonutils.dumps({'aggregate': get_os_aggregates['aggregates'][0]}) + + httpretty.register_uri(httpretty.POST, self.url(), body=r, + content_type='application/json') + + for agg_id in (1, 2): + for method in (httpretty.GET, httpretty.PUT): + httpretty.register_uri(method, self.url(agg_id), body=r, + content_type='application/json') + + httpretty.register_uri(httpretty.POST, self.url(agg_id, 'action'), + body=r, content_type='application/json') + + httpretty.register_uri(httpretty.DELETE, self.url(1), status=202) diff --git a/novaclient/tests/fixture_data/base.py b/novaclient/tests/fixture_data/base.py new file mode 100644 index 000000000..3c96133b6 --- /dev/null +++ b/novaclient/tests/fixture_data/base.py @@ -0,0 +1,32 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import fixtures + +COMPUTE_URL = 'http://compute.host' + + +class Fixture(fixtures.Fixture): + + base_url = None + + def __init__(self, compute_url=COMPUTE_URL): + super(Fixture, self).__init__() + self.compute_url = compute_url + + def url(self, *args): + url_args = [self.compute_url] + + if self.base_url: + url_args.append(self.base_url) + + return '/'.join(str(a).strip('/') for a in tuple(url_args) + args) diff --git a/novaclient/tests/fixture_data/client.py b/novaclient/tests/fixture_data/client.py new file mode 100644 index 000000000..a221b9bd1 --- /dev/null +++ b/novaclient/tests/fixture_data/client.py @@ -0,0 +1,109 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import fixtures +import httpretty + +from novaclient.openstack.common import jsonutils +from novaclient.v1_1 import client as v1_1client +from novaclient.v3 import client as v3client + +IDENTITY_URL = 'http://identityserver:5000/v2.0' +COMPUTE_URL = 'http://compute.host' + + +class V1(fixtures.Fixture): + + def __init__(self, compute_url=COMPUTE_URL, identity_url=IDENTITY_URL): + super(V1, self).__init__() + self.identity_url = identity_url + self.compute_url = compute_url + self.client = None + + self.token = { + 'access': { + "token": { + "id": "ab48a9efdfedb23ty3494", + "expires": "2010-11-01T03:32:15-05:00", + "tenant": { + "id": "345", + "name": "My Project" + } + }, + "user": { + "id": "123", + "name": "jqsmith", + "roles": [ + { + "id": "234", + "name": "compute:admin", + }, + { + "id": "235", + "name": "object-store:admin", + "tenantId": "1", + } + ], + "roles_links": [], + }, + "serviceCatalog": [ + { + "name": "Cloud Servers", + "type": "compute", + "endpoints": [ + { + "publicURL": self.compute_url, + "internalURL": "https://compute1.host/v1/1", + }, + ], + "endpoints_links": [], + }, + { + "name": "Cloud Servers", + "type": "computev3", + "endpoints": [ + { + "publicURL": self.compute_url, + "internalURL": "https://compute1.host/v1/1", + }, + ], + "endpoints_links": [], + }, + ], + } + } + + def setUp(self): + super(V1, self).setUp() + httpretty.enable() + self.addCleanup(httpretty.disable) + + auth_url = '%s/tokens' % self.identity_url + httpretty.register_uri(httpretty.POST, auth_url, + body=jsonutils.dumps(self.token), + content_type='application/json') + self.client = self.new_client() + + def new_client(self): + return v1_1client.Client(username='xx', + api_key='xx', + project_id='xx', + auth_url=self.identity_url) + + +class V3(V1): + + def new_client(self): + return v3client.Client(username='xx', + password='xx', + project_id='xx', + auth_url=self.identity_url) diff --git a/novaclient/tests/utils.py b/novaclient/tests/utils.py index cb8ded810..e544d7510 100644 --- a/novaclient/tests/utils.py +++ b/novaclient/tests/utils.py @@ -14,9 +14,13 @@ import os import fixtures +import httpretty import requests +import six import testtools +from novaclient.openstack.common import jsonutils + AUTH_URL = "http://localhost:5002/auth_url" AUTH_URL_V1 = "http://localhost:5002/auth_url/v1.0" AUTH_URL_V2 = "http://localhost:5002/auth_url/v2.0" @@ -39,6 +43,40 @@ def setUp(self): self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr)) +class FixturedTestCase(TestCase): + + client_fixture_class = None + data_fixture_class = None + + def setUp(self): + super(FixturedTestCase, self).setUp() + + httpretty.reset() + self.data_fixture = None + self.client_fixture = None + self.cs = None + + if self.client_fixture_class: + self.client_fixture = self.useFixture(self.client_fixture_class()) + self.cs = self.client_fixture.client + + if self.data_fixture_class: + self.data_fixture = self.useFixture(self.data_fixture_class()) + + def assert_called(self, method, path, body=None): + self.assertEqual(httpretty.last_request().method, method) + self.assertEqual(httpretty.last_request().path, path) + + if body: + req_data = httpretty.last_request().body + if isinstance(req_data, six.binary_type): + req_data = req_data.decode('utf-8') + if not isinstance(body, six.string_types): + # json load if the input body to match against is not a string + req_data = jsonutils.loads(req_data) + self.assertEqual(req_data, body) + + class TestResponse(requests.Response): """ Class used to wrap requests.Response and provide some diff --git a/novaclient/tests/v1_1/test_aggregates.py b/novaclient/tests/v1_1/test_aggregates.py index ef0305166..8d41de30d 100644 --- a/novaclient/tests/v1_1/test_aggregates.py +++ b/novaclient/tests/v1_1/test_aggregates.py @@ -13,51 +13,45 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient.tests.fixture_data import aggregates as data +from novaclient.tests.fixture_data import client from novaclient.tests import utils -from novaclient.tests.v1_1 import fakes from novaclient.v1_1 import aggregates -class AggregatesTest(utils.TestCase): - def setUp(self): - super(AggregatesTest, self).setUp() - self.cs = self._get_fake_client() - self.aggregate_type = self._get_aggregate_type() +class AggregatesTest(utils.FixturedTestCase): - def _get_fake_client(self): - return fakes.FakeClient() - - def _get_aggregate_type(self): - return aggregates.Aggregate + client_fixture_class = client.V1 + data_fixture_class = data.Fixture def test_list_aggregates(self): result = self.cs.aggregates.list() - self.cs.assert_called('GET', '/os-aggregates') + self.assert_called('GET', '/os-aggregates') for aggregate in result: self.assertIsInstance(aggregate, aggregates.Aggregate) def test_create_aggregate(self): body = {"aggregate": {"name": "test", "availability_zone": "nova1"}} aggregate = self.cs.aggregates.create("test", "nova1") - self.cs.assert_called('POST', '/os-aggregates', body) + self.assert_called('POST', '/os-aggregates', body) self.assertIsInstance(aggregate, aggregates.Aggregate) def test_get(self): aggregate = self.cs.aggregates.get("1") - self.cs.assert_called('GET', '/os-aggregates/1') + self.assert_called('GET', '/os-aggregates/1') self.assertIsInstance(aggregate, aggregates.Aggregate) aggregate2 = self.cs.aggregates.get(aggregate) - self.cs.assert_called('GET', '/os-aggregates/1') + self.assert_called('GET', '/os-aggregates/1') self.assertIsInstance(aggregate2, aggregates.Aggregate) def test_get_details(self): aggregate = self.cs.aggregates.get_details("1") - self.cs.assert_called('GET', '/os-aggregates/1') + self.assert_called('GET', '/os-aggregates/1') self.assertIsInstance(aggregate, aggregates.Aggregate) aggregate2 = self.cs.aggregates.get_details(aggregate) - self.cs.assert_called('GET', '/os-aggregates/1') + self.assert_called('GET', '/os-aggregates/1') self.assertIsInstance(aggregate2, aggregates.Aggregate) def test_update(self): @@ -66,11 +60,11 @@ def test_update(self): body = {"aggregate": values} result1 = aggregate.update(values) - self.cs.assert_called('PUT', '/os-aggregates/1', body) + self.assert_called('PUT', '/os-aggregates/1', body) self.assertIsInstance(result1, aggregates.Aggregate) result2 = self.cs.aggregates.update(2, values) - self.cs.assert_called('PUT', '/os-aggregates/2', body) + self.assert_called('PUT', '/os-aggregates/2', body) self.assertIsInstance(result2, aggregates.Aggregate) def test_update_with_availability_zone(self): @@ -79,7 +73,7 @@ def test_update_with_availability_zone(self): body = {"aggregate": values} result3 = self.cs.aggregates.update(aggregate, values) - self.cs.assert_called('PUT', '/os-aggregates/1', body) + self.assert_called('PUT', '/os-aggregates/1', body) self.assertIsInstance(result3, aggregates.Aggregate) def test_add_host(self): @@ -88,15 +82,15 @@ def test_add_host(self): body = {"add_host": {"host": "host1"}} result1 = aggregate.add_host(host) - self.cs.assert_called('POST', '/os-aggregates/1/action', body) + self.assert_called('POST', '/os-aggregates/1/action', body) self.assertIsInstance(result1, aggregates.Aggregate) result2 = self.cs.aggregates.add_host("2", host) - self.cs.assert_called('POST', '/os-aggregates/2/action', body) + self.assert_called('POST', '/os-aggregates/2/action', body) self.assertIsInstance(result2, aggregates.Aggregate) result3 = self.cs.aggregates.add_host(aggregate, host) - self.cs.assert_called('POST', '/os-aggregates/1/action', body) + self.assert_called('POST', '/os-aggregates/1/action', body) self.assertIsInstance(result3, aggregates.Aggregate) def test_remove_host(self): @@ -105,15 +99,15 @@ def test_remove_host(self): body = {"remove_host": {"host": "host1"}} result1 = aggregate.remove_host(host) - self.cs.assert_called('POST', '/os-aggregates/1/action', body) + self.assert_called('POST', '/os-aggregates/1/action', body) self.assertIsInstance(result1, aggregates.Aggregate) result2 = self.cs.aggregates.remove_host("2", host) - self.cs.assert_called('POST', '/os-aggregates/2/action', body) + self.assert_called('POST', '/os-aggregates/2/action', body) self.assertIsInstance(result2, aggregates.Aggregate) result3 = self.cs.aggregates.remove_host(aggregate, host) - self.cs.assert_called('POST', '/os-aggregates/1/action', body) + self.assert_called('POST', '/os-aggregates/1/action', body) self.assertIsInstance(result3, aggregates.Aggregate) def test_set_metadata(self): @@ -122,24 +116,24 @@ def test_set_metadata(self): body = {"set_metadata": {"metadata": metadata}} result1 = aggregate.set_metadata(metadata) - self.cs.assert_called('POST', '/os-aggregates/1/action', body) + self.assert_called('POST', '/os-aggregates/1/action', body) self.assertIsInstance(result1, aggregates.Aggregate) result2 = self.cs.aggregates.set_metadata(2, metadata) - self.cs.assert_called('POST', '/os-aggregates/2/action', body) + self.assert_called('POST', '/os-aggregates/2/action', body) self.assertIsInstance(result2, aggregates.Aggregate) result3 = self.cs.aggregates.set_metadata(aggregate, metadata) - self.cs.assert_called('POST', '/os-aggregates/1/action', body) + self.assert_called('POST', '/os-aggregates/1/action', body) self.assertIsInstance(result3, aggregates.Aggregate) def test_delete_aggregate(self): aggregate = self.cs.aggregates.list()[0] aggregate.delete() - self.cs.assert_called('DELETE', '/os-aggregates/1') + self.assert_called('DELETE', '/os-aggregates/1') self.cs.aggregates.delete('1') - self.cs.assert_called('DELETE', '/os-aggregates/1') + self.assert_called('DELETE', '/os-aggregates/1') self.cs.aggregates.delete(aggregate) - self.cs.assert_called('DELETE', '/os-aggregates/1') + self.assert_called('DELETE', '/os-aggregates/1') diff --git a/novaclient/tests/v3/test_aggregates.py b/novaclient/tests/v3/test_aggregates.py index f4ce8218a..7f64fb68f 100644 --- a/novaclient/tests/v3/test_aggregates.py +++ b/novaclient/tests/v3/test_aggregates.py @@ -12,19 +12,9 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient.tests.fixture_data import client from novaclient.tests.v1_1 import test_aggregates -from novaclient.tests.v3 import fakes -from novaclient.v3 import aggregates class AggregatesTest(test_aggregates.AggregatesTest): - def setUp(self): - super(AggregatesTest, self).setUp() - self.cs = self._get_fake_client() - self.aggregate_type = self._get_aggregate_type() - - def _get_fake_client(self): - return fakes.FakeClient() - - def _get_aggregate_type(self): - return aggregates.Aggregate + client_fixture = client.V3 diff --git a/test-requirements.txt b/test-requirements.txt index 388f0fd8b..717eaa2ba 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -3,6 +3,7 @@ hacking>=0.8.0,<0.9 coverage>=3.6 discover fixtures>=0.3.14 +httpretty>=0.8.0 keyring>=2.1 mock>=1.0 sphinx>=1.1.2,<1.2 From 5ff5e05facf7253cb6aa467f4e39cd3750b0ab08 Mon Sep 17 00:00:00 2001 From: OpenStack Jenkins Date: Fri, 11 Apr 2014 04:17:06 +0000 Subject: [PATCH 0466/1705] Updated from global requirements Change-Id: I85276146bd621f905cf57388147d410bd3c56986 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a12aaf27c..2b4e76452 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -pbr>=0.6,<1.0 +pbr>=0.6,!=0.7,<1.0 argparse iso8601>=0.1.9 PrettyTable>=0.7,<0.8 From a4db0feab43d0ed0416bb03247d62e0d3bae776a Mon Sep 17 00:00:00 2001 From: Phil Day Date: Tue, 15 Apr 2014 17:46:01 +0000 Subject: [PATCH 0467/1705] Fix mac address and task_state in baremetal-node-list Currently nova baremetal-node-list doesn't display the list of MAC addresses for each node because this is part of the interfaces object, and not a top level field. It also doesn't display the task state, which is very useful when trying to find which nodes have failed to build. This change fixes both issues Change-Id: If7a3bcab1220be400519ac811bd727ca8ff85827 Closes-Bug: #1308147 --- novaclient/v1_1/contrib/baremetal.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/novaclient/v1_1/contrib/baremetal.py b/novaclient/v1_1/contrib/baremetal.py index 825eedc9c..769472efb 100644 --- a/novaclient/v1_1/contrib/baremetal.py +++ b/novaclient/v1_1/contrib/baremetal.py @@ -219,10 +219,22 @@ def _translate_baremetal_node_keys(collection): def _print_baremetal_nodes_list(nodes): """Print the list of baremetal nodes.""" + + def _parse_address(fields): + macs = [] + for interface in fields.interfaces: + macs.append(interface['address']) + return ', '.join("%s" % i for i in macs) + + formatters = { + 'MAC Address': _parse_address + } + _translate_baremetal_node_keys(nodes) utils.print_list(nodes, [ 'ID', 'Host', + 'Task State', 'CPUs', 'Memory_MB', 'Disk_GB', @@ -231,7 +243,7 @@ def _print_baremetal_nodes_list(nodes): 'PM Username', 'PM Password', 'Terminal Port', - ]) + ], formatters=formatters) def do_baremetal_node_list(cs, _args): From 7acbc5f1f8d6d8079f7aaf5e60c8b7a2ee3a5932 Mon Sep 17 00:00:00 2001 From: Ken'ichi Ohmichi Date: Tue, 15 Apr 2014 09:47:51 +0900 Subject: [PATCH 0468/1705] Fix the unlimited length of console-log Since the commit Idf88a238d1b0e545ebab5be872269b1b1030cc56 of Nova, the unlimited length has been changed to -1 for API consistency. This patch applies it to novaclient. Change-Id: I581609a55f081184ad9d791ba38d78fa30d298a6 Closes-Bug: #1295426 --- novaclient/tests/v3/test_servers.py | 9 ++++++--- novaclient/v3/servers.py | 5 +++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/novaclient/tests/v3/test_servers.py b/novaclient/tests/v3/test_servers.py index fdeb02721..c29b7e1c4 100644 --- a/novaclient/tests/v3/test_servers.py +++ b/novaclient/tests/v3/test_servers.py @@ -364,7 +364,8 @@ def test_get_console_output_without_length(self): cs.servers.get_console_output(s) self.assertEqual(cs.servers.get_console_output(s), success) - cs.assert_called('POST', '/servers/1234/action') + cs.assert_called('POST', '/servers/1234/action', + {'get_console_output': {'length': -1}}) def test_get_console_output_with_length(self): success = 'foo' @@ -372,11 +373,13 @@ def test_get_console_output_with_length(self): s = cs.servers.get(1234) s.get_console_output(length=50) self.assertEqual(s.get_console_output(length=50), success) - cs.assert_called('POST', '/servers/1234/action') + cs.assert_called('POST', '/servers/1234/action', + {'get_console_output': {'length': 50}}) cs.servers.get_console_output(s, length=50) self.assertEqual(cs.servers.get_console_output(s, length=50), success) - cs.assert_called('POST', '/servers/1234/action') + cs.assert_called('POST', '/servers/1234/action', + {'get_console_output': {'length': 50}}) def test_get_password(self): s = cs.servers.get(1234) diff --git a/novaclient/v3/servers.py b/novaclient/v3/servers.py index 92dffcfd1..076a6bf35 100644 --- a/novaclient/v3/servers.py +++ b/novaclient/v3/servers.py @@ -902,6 +902,11 @@ def get_console_output(self, server, length=None): you would like to retrieve. :param length: The number of tail loglines you would like to retrieve. """ + if length is None: + # NOTE: On v3 get_console_output API, -1 means an unlimited length. + # Here translates None, which means an unlimited in the internal + # implementation, to -1. + length = -1 return self._action('get_console_output', server, {'length': length})[1]['output'] From 02ee4fc79e25b31e8a12094cfa3f1839bcc497cd Mon Sep 17 00:00:00 2001 From: Zhengguang Date: Sat, 19 Apr 2014 10:44:54 -0400 Subject: [PATCH 0469/1705] Add unit test for keypair's api Add unit test for keypair's api in test_shell.py, just for v1.1, not for v3, because of v3 inherits from v1.1. Change-Id: Ibbad199449431b328091ef4b5e4b955ffcddc303 Closes-Bug: #1309986 --- novaclient/tests/v1_1/test_shell.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index 0e1080a5d..564b55ec5 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -23,6 +23,7 @@ import fixtures import mock import six +from six.moves import builtins import novaclient.client from novaclient import exceptions @@ -1939,6 +1940,33 @@ class FakeResources(object): mock_system.assert_called_with("ssh -6 -p22 " "root@2607:f0d0:1002::4 -1") + def test_keypair_add(self): + self.run_command('keypair-add test') + self.assert_called('POST', '/os-keypairs', + {'keypair': + {'name': 'test'}}) + + @mock.patch.object(builtins, 'open', + mock.mock_open(read_data='FAKE_PUBLIC_KEY')) + def test_keypair_import(self): + self.run_command('keypair-add --pub-key test.pub test') + self.assert_called('POST', '/os-keypairs', + {'keypair': + {'public_key': 'FAKE_PUBLIC_KEY', + 'name': 'test'}}) + + def test_keypair_list(self): + self.run_command('keypair-list') + self.assert_called('GET', '/os-keypairs') + + def test_keypair_show(self): + self.run_command('keypair-show test') + self.assert_called('GET', '/os-keypairs/test') + + def test_keypair_delete(self): + self.run_command('keypair-delete test') + self.assert_called('DELETE', '/os-keypairs/test') + class GetSecgroupTest(utils.TestCase): def test_with_integer(self): From d0dfafea57425bcdd3c440317a76fcbdae1eed0b Mon Sep 17 00:00:00 2001 From: Ala Rezmerita Date: Fri, 18 Apr 2014 15:52:24 +0200 Subject: [PATCH 0470/1705] Fix wrong fake return values for Nova V3 client tests The response format for the hypervisor servers list action in Nova API V3 has changed. This patch corrects the fake return values used for client testing. Change-Id: I645607b5adcfe3fa1c1d1efc071c87d2bb2b37b5 Closes-Bug: #1309542 --- novaclient/tests/v3/fakes.py | 4 ++-- novaclient/tests/v3/test_hypervisors.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/novaclient/tests/v3/fakes.py b/novaclient/tests/v3/fakes.py index be3727452..43b5e05e7 100644 --- a/novaclient/tests/v3/fakes.py +++ b/novaclient/tests/v3/fakes.py @@ -332,8 +332,8 @@ def get_os_hypervisors_1234_servers(self, **kw): {'id': 1234, 'hypervisor_hostname': 'hyper1', 'servers': [ - {'name': 'inst1', 'uuid': 'uuid1'}, - {'name': 'inst2', 'uuid': 'uuid2'} + {'name': 'inst1', 'id': 'uuid1'}, + {'name': 'inst2', 'id': 'uuid2'} ]}, }) diff --git a/novaclient/tests/v3/test_hypervisors.py b/novaclient/tests/v3/test_hypervisors.py index d2cbbb9ac..97e69a8a1 100644 --- a/novaclient/tests/v3/test_hypervisors.py +++ b/novaclient/tests/v3/test_hypervisors.py @@ -41,8 +41,8 @@ def test_hypervisor_servers(self): expected = dict(id=1234, hypervisor_hostname='hyper1', servers=[ - dict(name='inst1', uuid='uuid1'), - dict(name='inst2', uuid='uuid2')]) + dict(name='inst1', id='uuid1'), + dict(name='inst2', id='uuid2')]) result = self.cs.hypervisors.servers('1234') self.cs.assert_called('GET', '/os-hypervisors/1234/servers') From fdd59e142a69d02123ad5b8522ba0e75c731e02f Mon Sep 17 00:00:00 2001 From: Zhengguang Date: Fri, 18 Apr 2014 15:15:48 +0800 Subject: [PATCH 0471/1705] Fix the incorrect return messages in keypair show and delete When we delete or show a keypair, if the keypair doesn't exist, we'll get "The resource could not be found.(HTTP 404)", this patch will change it to "ERROR: No keypair with a name or ID of 'keypair_name' exists." Change-Id: Ifebd8d2213c327f3d3fdd672207170aebbe1bb40 Closes-Bug: #1307338 --- novaclient/tests/v1_1/fakes.py | 20 ++++++++++++++++---- novaclient/utils.py | 2 +- novaclient/v1_1/keypairs.py | 1 + novaclient/v1_1/shell.py | 9 +++++++-- novaclient/v3/shell.py | 9 +++++++-- 5 files changed, 32 insertions(+), 9 deletions(-) diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index 983357619..cfd0f24ec 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -1013,12 +1013,24 @@ def delete_images_1_metadata_test_key(self, **kw): # Keypairs # def get_os_keypairs_test(self, *kw): - return (200, {}, {'keypair': self.get_os_keypairs()[2]['keypairs'][0]}) + return (200, {}, {'keypair': + self.get_os_keypairs()[2]['keypairs'][0]['keypair']}) def get_os_keypairs(self, *kw): return (200, {}, {"keypairs": [ - {'fingerprint': 'FAKE_KEYPAIR', 'name': 'test'} - ]}) + {"keypair": { + "public_key": "FAKE_SSH_RSA", + "private_key": "FAKE_PRIVATE_KEY", + "user_id": + "81e373b596d6466e99c4896826abaa46", + "name": "test", + "deleted": False, + "created_at": "2014-04-19T02:16:44.000000", + "updated_at": "2014-04-19T10:12:3.000000", + "figerprint": "FAKE_KEYPAIR", + "deleted_at": None, + "id": 4} + }]}) def delete_os_keypairs_test(self, **kw): return (202, {}, None) @@ -1027,7 +1039,7 @@ def post_os_keypairs(self, body, **kw): assert list(body) == ['keypair'] fakes.assert_has_keys(body['keypair'], required=['name']) - r = {'keypair': self.get_os_keypairs()[2]['keypairs'][0]} + r = {'keypair': self.get_os_keypairs()[2]['keypairs'][0]['keypair']} return (202, {}, r) # diff --git a/novaclient/utils.py b/novaclient/utils.py index c10d5c3b2..6d93f26ae 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -203,7 +203,7 @@ def print_dict(d, dict_property="Property", dict_value="Value", wrap=0): def find_resource(manager, name_or_id, **find_args): """Helper for the _find_* methods.""" - # for str id which is not uuid (for Flavor search currently) + # for str id which is not uuid (for Flavor and Keypair search currently) if getattr(manager, 'is_alphanum_id_allowed', False): try: return manager.get(name_or_id) diff --git a/novaclient/v1_1/keypairs.py b/novaclient/v1_1/keypairs.py index 3a102a43d..96caff618 100644 --- a/novaclient/v1_1/keypairs.py +++ b/novaclient/v1_1/keypairs.py @@ -53,6 +53,7 @@ def delete(self): class KeypairManager(base.ManagerWithFind): resource_class = Keypair keypair_prefix = "os-keypairs" + is_alphanum_id_allowed = True def get(self, keypair): """ diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index fd1feba86..df1e2ac62 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2432,7 +2432,7 @@ def do_keypair_add(cs, args): @utils.arg('name', metavar='', help=_('Keypair name to delete.')) def do_keypair_delete(cs, args): """Delete keypair given by its name.""" - name = args.name + name = _find_keypair(cs, args.name) cs.keypairs.delete(name) @@ -2455,10 +2455,15 @@ def _print_keypair(keypair): help=_("Name or ID of keypair")) def do_keypair_show(cs, args): """Show details about the given keypair.""" - keypair = cs.keypairs.get(args.keypair) + keypair = _find_keypair(cs, args.keypair) _print_keypair(keypair) +def _find_keypair(cs, keypair): + """Get a keypair by name or ID.""" + return utils.find_resource(cs.keypairs, keypair) + + @utils.arg('--tenant', #nova db searches by project_id dest='tenant', diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index eb5cda1b3..2b8ac5acf 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -1973,7 +1973,7 @@ def do_keypair_add(cs, args): @utils.arg('name', metavar='', help='Keypair name to delete.') def do_keypair_delete(cs, args): """Delete keypair given by its name.""" - name = args.name + name = _find_keypair(cs, args.name) cs.keypairs.delete(name) @@ -1996,10 +1996,15 @@ def _print_keypair(keypair): help="Name or ID of keypair") def do_keypair_show(cs, args): """Show details about the given keypair.""" - keypair = cs.keypairs.get(args.keypair) + keypair = _find_keypair(cs, args.keypair) _print_keypair(keypair) +def _find_keypair(cs, keypair): + """Get a keypair by name or ID.""" + return utils.find_resource(cs.keypairs, keypair) + + @utils.arg('--start', metavar='', help='Usage range start date ex 2012-01-20 (default: 4 weeks ago)', default=None) From d5e61bd0f75385cfc5f40cca51d4bc48f9901f49 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Tue, 22 Apr 2014 01:55:55 +0000 Subject: [PATCH 0472/1705] Updated from global requirements Change-Id: I1f5be823bd33ff05b055c58eac5a1f7706429453 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2b4e76452..e98257c9a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,5 +4,5 @@ iso8601>=0.1.9 PrettyTable>=0.7,<0.8 requests>=1.1 simplejson>=2.0.9 -six>=1.5.2 +six>=1.6.0 Babel>=1.3 From 3bde9c3f427110dae3480644752f49b79ac66ec5 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Wed, 29 Jan 2014 13:03:26 +0200 Subject: [PATCH 0473/1705] Reuse exceptions from Oslo - Remove unused exceptions from novaclient.exceptions - Reuse exceptions from common code OpenStack clients which already use exception from common code: - ironicclient - I784007f36dd6a63fc4c77e0d0f0f000e05eb792f - keystoneclient - Iedf4e5d753d4278d81751ba0f55fdef3566b56de - saharaclient - tuskarclient - Iacbf219168fae62d864c6594dd41e0737e848cfa Similar patches in other clients related to reusing exceptions from common code: - cinderclient - I07f04ba5357e0196fe43b510e1d2fff5afa02faa - glanceclient - I9838391cff7e450f08d27668564b02e63259186c Related to blueprint common-client-library-2 Change-Id: Ia35d65d57a0047f88a609070f8b4c76215cbd3e9 --- novaclient/exceptions.py | 174 +++------------------------------- novaclient/tests/test_http.py | 6 +- 2 files changed, 18 insertions(+), 162 deletions(-) diff --git a/novaclient/exceptions.py b/novaclient/exceptions.py index f1102ac2c..cba8ea165 100644 --- a/novaclient/exceptions.py +++ b/novaclient/exceptions.py @@ -16,161 +16,31 @@ Exception definitions. """ +import inspect +import sys -class UnsupportedVersion(Exception): - """Indicates that the user is trying to use an unsupported - version of the API. - """ - pass - - -class CommandError(Exception): - pass +import six +from novaclient.openstack.common.apiclient.exceptions import * # noqa -class AuthorizationFailure(Exception): - pass - - -class NoUniqueMatch(Exception): - pass +# NOTE(akurilin): Since v.2.17.0 this alias should be left here to support +# backwards compatibility. +OverLimit = RequestEntityTooLarge -class AuthSystemNotFound(Exception): - """When the user specify a AuthSystem but not installed.""" - def __init__(self, auth_system): - self.auth_system = auth_system - - def __str__(self): - return "AuthSystemNotFound: %s" % repr(self.auth_system) - - -class NoTokenLookupException(Exception): +class NoTokenLookupException(ClientException): """This form of authentication does not support looking up endpoints from an existing token. """ pass -class EndpointNotFound(Exception): - """Could not find Service or Region in Service Catalog.""" - pass - - -class AmbiguousEndpoints(Exception): - """Found more than one matching endpoint in Service Catalog.""" - def __init__(self, endpoints=None): - self.endpoints = endpoints - - def __str__(self): - return "AmbiguousEndpoints: %s" % repr(self.endpoints) - - -class ConnectionRefused(Exception): - """ - Connection refused: the server refused the connection. - """ - def __init__(self, response=None): - self.response = response - - def __str__(self): - return "ConnectionRefused: %s" % repr(self.response) - - class InstanceInErrorState(Exception): """Instance is in the error state.""" pass -class ClientException(Exception): - """ - The base exception class for all exceptions this library raises. - """ - message = 'Unknown Error' - - def __init__(self, code, message=None, details=None, request_id=None, - url=None, method=None): - self.code = code - self.message = message or self.__class__.message - self.details = details - self.request_id = request_id - self.url = url - self.method = method - - def __str__(self): - formatted_string = "%s (HTTP %s)" % (self.message, self.code) - if self.request_id: - formatted_string += " (Request-ID: %s)" % self.request_id - - return formatted_string - - -class BadRequest(ClientException): - """ - HTTP 400 - Bad request: you sent some malformed data. - """ - http_status = 400 - message = "Bad request" - - -class Unauthorized(ClientException): - """ - HTTP 401 - Unauthorized: bad credentials. - """ - http_status = 401 - message = "Unauthorized" - - -class Forbidden(ClientException): - """ - HTTP 403 - Forbidden: your credentials don't give you access to this - resource. - """ - http_status = 403 - message = "Forbidden" - - -class NotFound(ClientException): - """ - HTTP 404 - Not found - """ - http_status = 404 - message = "Not found" - - -class MethodNotAllowed(ClientException): - """ - HTTP 405 - Method Not Allowed - """ - http_status = 405 - message = "Method Not Allowed" - - -class Conflict(ClientException): - """ - HTTP 409 - Conflict - """ - http_status = 409 - message = "Conflict" - - -class OverLimit(ClientException): - """ - HTTP 413 - Over limit: you're over the API limits for this time period. - """ - http_status = 413 - message = "Over limit" - - def __init__(self, *args, **kwargs): - try: - self.retry_after = int(kwargs.pop('retry_after')) - except (KeyError, ValueError): - self.retry_after = 0 - - super(OverLimit, self).__init__(*args, **kwargs) - - -class RateLimit(OverLimit): +class RateLimit(RequestEntityTooLarge): """ HTTP 429 - Rate limit: you've sent too many requests for this time period. """ @@ -178,25 +48,11 @@ class RateLimit(OverLimit): message = "Rate limit" -# NotImplemented is a python keyword. -class HTTPNotImplemented(ClientException): - """ - HTTP 501 - Not Implemented: the server does not support this operation. - """ - http_status = 501 - message = "Not Implemented" - - -# In Python 2.4 Exception is old-style and thus doesn't have a __subclasses__() -# so we can do this: -# _code_map = dict((c.http_status, c) -# for c in ClientException.__subclasses__()) -# -# Instead, we have to hardcode it: -_error_classes = [BadRequest, Unauthorized, Forbidden, NotFound, - MethodNotAllowed, Conflict, OverLimit, RateLimit, - HTTPNotImplemented] -_code_map = dict((c.http_status, c) for c in _error_classes) +_code_map = dict( + (getattr(obj, 'http_status', None), obj) + for name, obj in six.iteritems(vars(sys.modules[__name__])) + if inspect.isclass(obj) and getattr(obj, 'http_status', False) +) def from_response(response, body, url, method=None): @@ -211,7 +67,7 @@ def from_response(response, body, url, method=None): raise exception_from_response(resp, rest.text) """ kwargs = { - 'code': response.status_code, + 'http_status': response.status_code, 'method': method, 'url': url, 'request_id': None, diff --git a/novaclient/tests/test_http.py b/novaclient/tests/test_http.py index 2797a438f..824bf4b5f 100644 --- a/novaclient/tests/test_http.py +++ b/novaclient/tests/test_http.py @@ -150,7 +150,7 @@ def test_unknown_server_error(self): # Python 2.7 and testtools doesn't match that implementation yet try: cl.get('/hi') - except exceptions.ClientException as exc: - self.assertIn('Unknown Error', six.text_type(exc)) + except exceptions.ServiceUnavailable as exc: + self.assertIn('Service Unavailable (HTTP 503)', six.text_type(exc)) else: - self.fail('Expected exceptions.ClientException') + self.fail('Expected exceptions.ServiceUnavailable') From 4c5f5458719bc475f46c22d4ad7f2997c58fcab8 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Fri, 25 Apr 2014 21:25:27 -0700 Subject: [PATCH 0474/1705] Fixed a typo in a comment Found using: https://github.com/intgr/topy Change-Id: I1fba0b8c8e978f286905eeb96ae0a2e47762f413 --- novaclient/tests/v1_1/fakes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index cfd0f24ec..5ce4166be 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -745,7 +745,7 @@ def get_flavors_512_MB_Server(self, **kw): raise exceptions.NotFound('404') def get_flavors_aa1(self, **kw): - # Aplhanumeric flavor id are allowed. + # Alphanumeric flavor id are allowed. return ( 200, {}, From 0a1f9c168fe0562ca1e7d5137c015e0975f6173c Mon Sep 17 00:00:00 2001 From: Veronica Musso Date: Mon, 28 Apr 2014 11:30:03 -0300 Subject: [PATCH 0475/1705] Fix for "nova help list" command The output of "nova help list" shows several argument as "Admin Only", when they are not. This patch modifies help notes for --ip, --ip6 and --instance-name, which can be used by regular users. DocImpact Change-Id: Iac541ac8e698a27348b437ad53910bd09ea7f257 Closes-Bug: #1295126 --- novaclient/v1_1/shell.py | 8 +++----- novaclient/v3/shell.py | 6 +++--- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index df1e2ac62..030a1fb9d 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1013,13 +1013,12 @@ def do_image_delete(cs, args): dest='ip', metavar='', default=None, - help=_('Search with regular expression match by IP address (Admin only).')) + help=_('Search with regular expression match by IP address.')) @utils.arg('--ip6', dest='ip6', metavar='', default=None, - help=_('Search with regular expression match by IPv6 address ' - '(Admin only).')) + help=_('Search with regular expression match by IPv6 address.')) @utils.arg('--name', dest='name', metavar='', @@ -1029,8 +1028,7 @@ def do_image_delete(cs, args): dest='instance_name', metavar='', default=None, - help=_('Search with regular expression match by server name ' - '(Admin only).')) + help=_('Search with regular expression match by server name.')) @utils.arg('--instance_name', help=argparse.SUPPRESS) @utils.arg('--status', diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 2b8ac5acf..f3710354c 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -865,12 +865,12 @@ def do_image_delete(cs, args): dest='ip', metavar='', default=None, - help='Search with regular expression match by IP address (Admin only).') + help='Search with regular expression match by IP address.') @utils.arg('--ip6', dest='ip6', metavar='', default=None, - help='Search with regular expression match by IPv6 address (Admin only).') + help='Search with regular expression match by IPv6 address.') @utils.arg('--name', dest='name', metavar='', @@ -880,7 +880,7 @@ def do_image_delete(cs, args): dest='instance_name', metavar='', default=None, - help='Search with regular expression match by server name (Admin only).') + help='Search with regular expression match by server name.') @utils.arg('--instance_name', help=argparse.SUPPRESS) @utils.arg('--status', From 8e0e25f6ceac9525cf0fd6fc36e6295874081d5a Mon Sep 17 00:00:00 2001 From: Ihar Hrachyshka Date: Tue, 29 Apr 2014 17:26:19 +0200 Subject: [PATCH 0476/1705] Synced jsonutils from oslo-incubator The sync includes change that drastically enhances performance on Python 2.6 with fresh simplejson library installed. The latest commit in oslo-incubator: - 732bdb6297eb9de81667f7713ebcb1ccc2ee45a7 Change-Id: Ib3dc0b713ed90396919feba018772243b3b9c90f Closes-Bug: 1314129 --- novaclient/openstack/common/jsonutils.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/novaclient/openstack/common/jsonutils.py b/novaclient/openstack/common/jsonutils.py index f8738f911..0767e1e59 100644 --- a/novaclient/openstack/common/jsonutils.py +++ b/novaclient/openstack/common/jsonutils.py @@ -35,7 +35,17 @@ import functools import inspect import itertools -import json +import sys + +if sys.version_info < (2, 7): + # On Python <= 2.6, json module is not C boosted, so try to use + # simplejson module if available + try: + import simplejson as json + except ImportError: + import json +else: + import json import six import six.moves.xmlrpc_client as xmlrpclib From adf6c351a05d205573be4f00db1e53aa2615a593 Mon Sep 17 00:00:00 2001 From: Jason Dunsmore Date: Tue, 29 Apr 2014 15:40:03 -0500 Subject: [PATCH 0477/1705] Fix documentation for config_drive boot parameter The "config_drive" boot parameter only accepts a Boolean parameter, but the documentation says otherwise. Change-Id: I7cbf5aefbd006d8b5425cd8592bacb5d8435b3c9 Closes-Bug: #1314395 --- novaclient/v1_1/servers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index a243300eb..41f04fd63 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -435,8 +435,8 @@ def _boot(self, resource_url, response_key, name, image, flavor, connected networks, fixed ips, etc. :param scheduler_hints: (optional extension) arbitrary key-value pairs specified by the client to help boot an instance. - :param config_drive: (optional extension) value for config drive - either boolean, or volume-id + :param config_drive: (optional extension) If True, enable config drive + on the server. :param admin_pass: admin password for the server. :param disk_config: (optional extension) control how the disk is partitioned when the server is created. From 0ca568242cb60ef86b7752ee2e2749f532027421 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Wed, 30 Apr 2014 02:46:46 +0000 Subject: [PATCH 0478/1705] Updated from global requirements Change-Id: I2f3f0b9f09e458eec4eb1e7a3b83e49043a8cb11 --- setup.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/setup.py b/setup.py index 70c2b3f32..736375744 100644 --- a/setup.py +++ b/setup.py @@ -17,6 +17,14 @@ # THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT import setuptools +# In python < 2.7.4, a lazy loading of package `pbr` will break +# setuptools if some other modules registered functions in `atexit`. +# solution from: http://bugs.python.org/issue15881#msg170215 +try: + import multiprocessing # noqa +except ImportError: + pass + setuptools.setup( setup_requires=['pbr'], pbr=True) From b23c97d634ff770f643a198308a4c962c9924517 Mon Sep 17 00:00:00 2001 From: liyingjun Date: Fri, 25 Apr 2014 23:02:11 +0800 Subject: [PATCH 0479/1705] Print message if instance is successfully deleted Change-Id: I0057a1c906083e1a379a0d15a3c783823f49bf4c Closes-bug: 1312087 --- novaclient/v1_1/shell.py | 1 + 1 file changed, 1 insertion(+) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 030a1fb9d..2de2ed963 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1526,6 +1526,7 @@ def do_delete(cs, args): for server in args.server: try: _find_server(cs, server).delete() + print(_("Server %s has been successfully deleted.") % server) except Exception as e: failure_count += 1 print(e) From b165900838413e1bd70d5e600ebf87e00c719912 Mon Sep 17 00:00:00 2001 From: Dirk Mueller Date: Tue, 15 Apr 2014 10:34:07 +0200 Subject: [PATCH 0480/1705] Make help description of rescue/unrescue more useful The docstring was not helping users in understanding what the command does. Change-Id: I858a43157c582067992cb80080ed440d6aba3ac3 --- novaclient/v1_1/shell.py | 6 ++++-- novaclient/v3/shell.py | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index df1e2ac62..7aab77da1 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1324,13 +1324,15 @@ def do_resume(cs, args): @utils.arg('server', metavar='', help=_('Name or ID of server.')) def do_rescue(cs, args): - """Rescue a server.""" + """Reboots a server into rescue mode, which starts the machine + from the initial image, attaching the current boot disk as secondary. + """ utils.print_dict(_find_server(cs, args.server).rescue()[1]) @utils.arg('server', metavar='', help=_('Name or ID of server.')) def do_unrescue(cs, args): - """Unrescue a server.""" + """Restart the server from normal boot disk again.""" _find_server(cs, args.server).unrescue() diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 2b8ac5acf..8e802aee3 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -1169,13 +1169,15 @@ def do_resume(cs, args): @utils.arg('server', metavar='', help='Name or ID of server.') def do_rescue(cs, args): - """Rescue a server.""" + """Reboots a server into rescue mode, which starts the machine + from the initial image, attaching the current boot disk as secondary. + """ utils.print_dict(_find_server(cs, args.server).rescue()[1]) @utils.arg('server', metavar='', help='Name or ID of server.') def do_unrescue(cs, args): - """Unrescue a server.""" + """Restart the server from normal boot disk again.""" _find_server(cs, args.server).unrescue() From 1e7ad37b8a842094963d3296cf127614f49e10fe Mon Sep 17 00:00:00 2001 From: Joe Gordon Date: Mon, 28 Apr 2014 22:17:50 +0000 Subject: [PATCH 0481/1705] Deprecate num-instances in favor of min/max count For 'nova boot', num_instances only set max_count, which will try to launch up to max_count instances. But sometimes users want to either launch min_count instances or nothing. So deprecate --num-instances and use --min-count and --max-count instead. DocImpact: deprecate '--num-instances' for 'nova boot' Change-Id: If0088aef554a528258799b22c6e793541474cb0d Closes-bug: #1309244 --- novaclient/tests/v1_1/test_shell.py | 45 +++++++++++++++++++++++++++++ novaclient/tests/v3/test_shell.py | 45 +++++++++++++++++++++++++++++ novaclient/v1_1/shell.py | 29 +++++++++++++++++-- novaclient/v3/shell.py | 29 +++++++++++++++++-- 4 files changed, 144 insertions(+), 4 deletions(-) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index 564b55ec5..d01155b24 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -590,6 +590,51 @@ def test_boot_invalid_num_instances(self): cmd = 'boot --image 1 --flavor 1 --num-instances 0 server' self.assertRaises(exceptions.CommandError, self.run_command, cmd) + def test_boot_num_instances_and_count(self): + cmd = 'boot --image 1 --flavor 1 --num-instances 3 --min-count 3 serv' + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + cmd = 'boot --image 1 --flavor 1 --num-instances 3 --max-count 3 serv' + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + def test_boot_min_max_count(self): + self.run_command('boot --image 1 --flavor 1 --max-count 3 server') + self.assert_called_anytime( + 'POST', '/servers', + { + 'server': { + 'flavorRef': '1', + 'name': 'server', + 'imageRef': '1', + 'min_count': 1, + 'max_count': 3, + } + }) + self.run_command('boot --image 1 --flavor 1 --min-count 3 server') + self.assert_called_anytime( + 'POST', '/servers', + { + 'server': { + 'flavorRef': '1', + 'name': 'server', + 'imageRef': '1', + 'min_count': 3, + 'max_count': 3, + } + }) + self.run_command('boot --image 1 --flavor 1 ' + '--min-count 3 --max-count 5 server') + self.assert_called_anytime( + 'POST', '/servers', + { + 'server': { + 'flavorRef': '1', + 'name': 'server', + 'imageRef': '1', + 'min_count': 3, + 'max_count': 5, + } + }) + @mock.patch('novaclient.v1_1.shell._poll_for_status') def test_boot_with_poll(self, poll_method): self.run_command('boot --flavor 1 --image 1 some-server --poll') diff --git a/novaclient/tests/v3/test_shell.py b/novaclient/tests/v3/test_shell.py index 144e85712..a18d97d10 100644 --- a/novaclient/tests/v3/test_shell.py +++ b/novaclient/tests/v3/test_shell.py @@ -489,6 +489,51 @@ def test_boot_invalid_num_instances(self): cmd = 'boot --image 1 --flavor 1 --num-instances 0 server' self.assertRaises(exceptions.CommandError, self.run_command, cmd) + def test_boot_num_instances_and_count(self): + cmd = 'boot --image 1 --flavor 1 --num-instances 3 --min-count 3 serv' + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + cmd = 'boot --image 1 --flavor 1 --num-instances 3 --max-count 3 serv' + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + def test_boot_min_max_count(self): + self.run_command('boot --image 1 --flavor 1 --max-count 3 server') + self.assert_called_anytime( + 'POST', '/servers', + { + 'server': { + 'flavor_ref': '1', + 'name': 'server', + 'image_ref': '1', + 'os-multiple-create:min_count': 1, + 'os-multiple-create:max_count': 3, + } + }) + self.run_command('boot --image 1 --flavor 1 --min-count 3 server') + self.assert_called_anytime( + 'POST', '/servers', + { + 'server': { + 'flavor_ref': '1', + 'name': 'server', + 'image_ref': '1', + 'os-multiple-create:min_count': 3, + 'os-multiple-create:max_count': 3, + } + }) + self.run_command('boot --image 1 --flavor 1 ' + '--min-count 3 --max-count 5 server') + self.assert_called_anytime( + 'POST', '/servers', + { + 'server': { + 'flavor_ref': '1', + 'name': 'server', + 'image_ref': '1', + 'os-multiple-create:min_count': 3, + 'os-multiple-create:max_count': 5, + } + }) + @mock.patch('novaclient.v3.shell._poll_for_status') def test_boot_with_poll(self, poll_method): self.run_command('boot --flavor 1 --image 1 some-server --poll') diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index df1e2ac62..54774b6c4 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -148,10 +148,25 @@ def _boot(cs, args): min_count = 1 max_count = 1 - if args.num_instances is not None: + # Don't let user mix num_instances and max_count/min_count. + if (args.num_instances is not None and + args.min_count is None and args.max_count is None): if args.num_instances <= 1: raise exceptions.CommandError(_("num_instances should be > 1")) max_count = args.num_instances + elif (args.num_instances is not None and + (args.min_count is not None or args.max_count is not None)): + raise exceptions.CommandError(_("Don't mix num-instances and " + "max/min-count")) + if args.min_count is not None: + if args.min_count <= 1: + raise exceptions.CommandError(_("min_count should be > 1")) + min_count = args.min_count + max_count = min_count + if args.max_count is not None: + if args.max_count <= 1: + raise exceptions.CommandError(_("max_count should be > 1")) + max_count = args.max_count flavor = _find_flavor(cs, args.flavor) @@ -312,7 +327,17 @@ def _boot(cs, args): default=None, type=int, metavar='', - help=_("boot multiple servers at a time (limited by quota).")) + help=argparse.SUPPRESS) +@utils.arg('--min-count', + default=None, + type=int, + metavar='', + help=_("Boot at least servers (limited by quota).")) +@utils.arg('--max-count', + default=None, + type=int, + metavar='', + help=_("Boot up to servers (limited by quota).")) @utils.arg('--meta', metavar="", action='append', diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 2b8ac5acf..1d9c76c6d 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -84,10 +84,25 @@ def _boot(cs, args): min_count = 1 max_count = 1 - if args.num_instances is not None: + # Don't let user mix num_instances and max_count/min_count. + if (args.num_instances is not None and + args.min_count is None and args.max_count is None): if args.num_instances <= 1: raise exceptions.CommandError("num_instances should be > 1") max_count = args.num_instances + elif (args.num_instances is not None and + (args.min_count is not None or args.max_count is not None)): + raise exceptions.CommandError("Don't mix num-instances and " + "max/min-count") + if args.min_count is not None: + if args.min_count <= 1: + raise exceptions.CommandError("min_count should be > 1") + min_count = args.min_count + max_count = min_count + if args.max_count is not None: + if args.max_count <= 1: + raise exceptions.CommandError("max_count should be > 1") + max_count = args.max_count flavor = _find_flavor(cs, args.flavor) @@ -214,7 +229,17 @@ def _boot(cs, args): default=None, type=int, metavar='', - help="boot multiple servers at a time (limited by quota).") + help=argparse.SUPPRESS) +@utils.arg('--min-count', + default=None, + type=int, + metavar='', + help="Boot at least servers (limited by quota).") +@utils.arg('--max-count', + default=None, + type=int, + metavar='', + help="Boot up to servers (limited by quota).") @utils.arg('--meta', metavar="", action='append', From 8c8448d035c16d33e0ba9c7e9187d5ebf3999ef5 Mon Sep 17 00:00:00 2001 From: Joe Gordon Date: Mon, 28 Apr 2014 16:28:46 -0700 Subject: [PATCH 0482/1705] Remove duplicate test test_boot_multiple test_boot_multiple is a duplicate if test_boot_num_instances. Change-Id: I03a4def85b79109e6a950587542b53741b06bd70 --- novaclient/tests/v1_1/test_shell.py | 14 -------------- novaclient/tests/v3/test_shell.py | 14 -------------- 2 files changed, 28 deletions(-) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index d01155b24..c7e98a70b 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -129,20 +129,6 @@ def test_boot(self): }}, ) - def test_boot_multiple(self): - self.run_command('boot --flavor 1 --image 1' - ' --num-instances 3 some-server') - self.assert_called_anytime( - 'POST', '/servers', - {'server': { - 'flavorRef': '1', - 'name': 'some-server', - 'imageRef': '1', - 'min_count': 1, - 'max_count': 3, - }}, - ) - def test_boot_image_with(self): self.run_command("boot --flavor 1" " --image-with test_key=test_value some-server") diff --git a/novaclient/tests/v3/test_shell.py b/novaclient/tests/v3/test_shell.py index a18d97d10..a7afb7cb8 100644 --- a/novaclient/tests/v3/test_shell.py +++ b/novaclient/tests/v3/test_shell.py @@ -186,20 +186,6 @@ def test_boot(self): }}, ) - def test_boot_multiple(self): - self.run_command('boot --flavor 1 --image 1' - ' --num-instances 3 some-server') - self.assert_called_anytime( - 'POST', '/servers', - {'server': { - 'flavor_ref': '1', - 'name': 'some-server', - 'image_ref': '1', - 'os-multiple-create:min_count': 1, - 'os-multiple-create:max_count': 3, - }}, - ) - def test_boot_image_with(self): self.run_command("boot --flavor 1" " --image-with test_key=test_value some-server") From 9f6679ebd3c7436989b03b81c52b908657363a72 Mon Sep 17 00:00:00 2001 From: Pavlo Shchelokovskyy Date: Tue, 29 Apr 2014 18:56:04 +0300 Subject: [PATCH 0483/1705] Make port-id and net-id keys mutually exclusive These keys are never used together, and port-id always takes precedence in Nova API when present. This patch makes these parameters mutually exclusive and updates help strings accordingly. Change-Id: I207dc637cb69d9ab3ff6024a657d2c1890057b4d Closes-Bug: #1314097 --- novaclient/tests/v1_1/test_shell.py | 5 +++++ novaclient/tests/v3/test_shell.py | 5 +++++ novaclient/v1_1/shell.py | 8 ++++---- novaclient/v3/shell.py | 8 ++++---- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index 564b55ec5..df633e855 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -539,6 +539,11 @@ def test_boot_nics_no_netid_or_portid(self): '--nic v4-fixed-ip=10.0.0.1 some-server') self.assertRaises(exceptions.CommandError, self.run_command, cmd) + def test_boot_nics_netid_and_portid(self): + cmd = ('boot --image 1 --flavor 1 ' + '--nic port-id=some=port,net-id=some=net some-server') + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + def test_boot_files(self): testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt') data = open(testfile).read() diff --git a/novaclient/tests/v3/test_shell.py b/novaclient/tests/v3/test_shell.py index 144e85712..48bfc6e18 100644 --- a/novaclient/tests/v3/test_shell.py +++ b/novaclient/tests/v3/test_shell.py @@ -469,6 +469,11 @@ def test_boot_nics_no_netid_or_portid(self): '--nic v4-fixed-ip=10.0.0.1 some-server') self.assertRaises(exceptions.CommandError, self.run_command, cmd) + def test_boot_nics_netid_and_portid(self): + cmd = ('boot --image 1 --flavor 1 ' + '--nic port-id=some=port,net-id=some=net some-server') + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + def test_boot_num_instances(self): self.run_command('boot --image 1 --flavor 1 --num-instances 3 server') self.assert_called_anytime( diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index df1e2ac62..16eb2758c 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -227,7 +227,7 @@ def _boot(cs, args): err_msg = (_("Invalid nic argument '%s'. Nic arguments must be of " "the form --nic , with at minimum " - "net-id or port-id specified.") % nic_str) + "net-id or port-id (but not both) specified.") % nic_str) nic_info = {"net-id": "", "v4-fixed-ip": "", "v6-fixed-ip": "", "port-id": ""} @@ -242,7 +242,7 @@ def _boot(cs, args): else: raise exceptions.CommandError(err_msg) - if not nic_info['net-id'] and not nic_info['port-id']: + if bool(nic_info['net-id']) == bool(nic_info['port-id']): raise exceptions.CommandError(err_msg) nics.append(nic_info) @@ -401,11 +401,11 @@ def _boot(cs, args): help=_("Create a NIC on the server. " "Specify option multiple times to create multiple NICs. " "net-id: attach NIC to network with this UUID " - "(required if no port-id), " + "(either port-id or net-id must be provided), " "v4-fixed-ip: IPv4 fixed address for NIC (optional), " "v6-fixed-ip: IPv6 fixed address for NIC (optional), " "port-id: attach NIC to port with this UUID " - "(required if no net-id)")) + "(either port-id or net-id must be provided).")) @utils.arg('--config-drive', metavar="", dest='config_drive', diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 2b8ac5acf..7e0633f2f 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -138,7 +138,7 @@ def _boot(cs, args): err_msg = ("Invalid nic argument '%s'. Nic arguments must be of the " "form --nic , with at minimum " - "net-id or port-id specified." % nic_str) + "net-id or port-id (but not both) specified." % nic_str) nic_info = {"net-id": "", "v4-fixed-ip": "", "v6-fixed-ip": "", "port-id": ""} @@ -153,7 +153,7 @@ def _boot(cs, args): else: raise exceptions.CommandError(err_msg) - if not nic_info['net-id'] and not nic_info['port-id']: + if bool(nic_info['net-id']) == bool(nic_info['port-id']): raise exceptions.CommandError(err_msg) nics.append(nic_info) @@ -277,11 +277,11 @@ def _boot(cs, args): help="Create a NIC on the server. " "Specify option multiple times to create multiple NICs. " "net-id: attach NIC to network with this UUID " - "(required if no port-id), " + "(either port-id or net-id must be provided), " "v4-fixed-ip: IPv4 fixed address for NIC (optional), " "v6-fixed-ip: IPv6 fixed address for NIC (optional), " "port-id: attach NIC to port with this UUID " - "(required if no net-id)") + "(either port-id or net-id must be provided).") @utils.arg('--config-drive', metavar="", dest='config_drive', From afbf4e4be06c005dee874a15765d25870317cd3b Mon Sep 17 00:00:00 2001 From: Jay Lau Date: Sun, 6 Apr 2014 13:46:53 +0800 Subject: [PATCH 0484/1705] Enable delete multiple server groups in one request Currently, "nova server-group-delete" can only delete one server group in one request, this patch was enabling nova client support removing multiple server groups in one request. Change-Id: I373151bc27cbe8617e2023ba99f6fb3f0108d592 Closes-Bug: #1302954 --- novaclient/tests/v1_1/fakes.py | 6 ++++++ novaclient/tests/v1_1/test_shell.py | 5 +++++ novaclient/v1_1/shell.py | 21 ++++++++++++++++----- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index 5ce4166be..52c03ad3a 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -415,6 +415,12 @@ def put_servers_1234(self, body, **kw): fakes.assert_has_keys(body['server'], optional=['name', 'adminPass']) return (204, {}, body) + def delete_os_server_groups_12345(self, **kw): + return (202, {}, None) + + def delete_os_server_groups_56789(self, **kw): + return (202, {}, None) + def delete_servers_1234(self, **kw): return (202, {}, None) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index 564b55ec5..7f0535db5 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -1967,6 +1967,11 @@ def test_keypair_delete(self): self.run_command('keypair-delete test') self.assert_called('DELETE', '/os-keypairs/test') + def test_delete_multi_server_groups(self): + self.run_command('server-group-delete 12345 56789') + self.assert_called('DELETE', '/os-server-groups/56789') + self.assert_called('DELETE', '/os-server-groups/12345', pos=-2) + class GetSecgroupTest(utils.TestCase): def test_with_integer(self): diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 2de2ed963..62124aff5 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -3469,12 +3469,23 @@ def do_server_group_create(cs, args): _print_server_group_details([server_group]) -@utils.arg('id', metavar='', - help="Unique ID of the server group to delete") +@utils.arg('id', metavar='', nargs='+', + help="Unique ID(s) of the server group to delete") def do_server_group_delete(cs, args): - """Delete a specific server group.""" - cs.server_groups.delete(args.id) - print("Instance group %s has been successfully deleted." % args.id) + """Delete specific server group(s).""" + failure_count = 0 + + for sg in args.id: + try: + cs.server_groups.delete(sg) + print(_("Server group %s has been successfully deleted.") % sg) + except Exception as e: + failure_count += 1 + print(_("Delete for server group %(sg)s failed: %(e)s") % + {'sg': sg, 'e': e}) + if failure_count == len(args.id): + raise exceptions.CommandError(_("Unable to delete any of the " + "specified server groups.")) @utils.arg('id', metavar='', From ee7fafbfa738b31c521ebb77ee1b426dd385e966 Mon Sep 17 00:00:00 2001 From: liyingjun Date: Tue, 6 May 2014 01:14:42 +0800 Subject: [PATCH 0485/1705] Print message if instance is successfully deleted v3 Also change the message for v1_1. Change-Id: I35eaf1a7670de79e3b75acaffe7ae13018b0a762 Closes-bug: 1312087 --- novaclient/v1_1/shell.py | 2 +- novaclient/v3/shell.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 2de2ed963..234c86dd0 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1526,7 +1526,7 @@ def do_delete(cs, args): for server in args.server: try: _find_server(cs, server).delete() - print(_("Server %s has been successfully deleted.") % server) + print(_("Request to delete server %s has been accepted.") % server) except Exception as e: failure_count += 1 print(e) diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index f3710354c..09923dbe4 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -1340,6 +1340,7 @@ def do_delete(cs, args): for server in args.server: try: _find_server(cs, server).delete() + print("Request to delete server %s has been accepted." % server) except Exception as e: failure_count += 1 print(e) From b277dd1c1c8e058653a3d019a17e7d381de184b3 Mon Sep 17 00:00:00 2001 From: Chris Buccella Date: Tue, 6 May 2014 21:37:17 +0000 Subject: [PATCH 0486/1705] Some Help Messages Missing Translation Support v1_1/contrib/baremetal is missing translation support for some help messages. Fix this. Change-Id: Iea354fef1c7f41361b66ed4e143b3f1f5e562d88 --- novaclient/v1_1/contrib/baremetal.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/novaclient/v1_1/contrib/baremetal.py b/novaclient/v1_1/contrib/baremetal.py index 769472efb..d13018ce8 100644 --- a/novaclient/v1_1/contrib/baremetal.py +++ b/novaclient/v1_1/contrib/baremetal.py @@ -194,7 +194,7 @@ def do_baremetal_node_create(cs, args): @utils.arg('node', metavar='', - help='ID of the node to delete.') + help=_('ID of the node to delete.')) def do_baremetal_node_delete(cs, args): """Remove a baremetal node and any associated interfaces.""" node = _find_baremetal_node(cs, args.node) @@ -275,7 +275,7 @@ def _print_baremetal_node_interfaces(interfaces): @utils.arg('node', metavar='', - help="ID of node") + help=_("ID of node")) def do_baremetal_node_show(cs, args): """Show information about a baremetal node.""" node = _find_baremetal_node(cs, args.node) @@ -310,7 +310,7 @@ def do_baremetal_interface_remove(cs, args): cs.baremetal.remove_interface(args.node, args.address) -@utils.arg('node', metavar='', help="ID of node") +@utils.arg('node', metavar='', help=_("ID of node")) def do_baremetal_interface_list(cs, args): """List network interfaces associated with a baremetal node.""" interfaces = cs.baremetal.list_interfaces(args.node) From 8e49df62439dce02e49e2c86880b64f17c9d782d Mon Sep 17 00:00:00 2001 From: Morgan Fainberg Date: Wed, 7 May 2014 12:16:41 -0700 Subject: [PATCH 0487/1705] Add mailmap entry Add mailmap entry for my email address Change-Id: Ia46c28c86ef3f440556b1b027d2bf0a7f3e721c5 --- .mailmap | 1 + 1 file changed, 1 insertion(+) diff --git a/.mailmap b/.mailmap index 64cb5eada..ec3c43d93 100644 --- a/.mailmap +++ b/.mailmap @@ -14,6 +14,7 @@ Johannes Erdfelt jerdfelt termie + hwbi From 661231817d93f937af31700593ed707017ab48f7 Mon Sep 17 00:00:00 2001 From: Ken'ichi Ohmichi Date: Sat, 3 May 2014 13:33:39 +0900 Subject: [PATCH 0488/1705] Add extension-list command for v3 API Nova returns available extension list through its REST API. The command of v2 API has been implemented, and this patch adds the one of v3 API. Note: The command name of v2 is "nova list-extensions" but the one of v3, which this patch adds, is "nova extension-list" because the name fits to the other command names. Change-Id: Ibee712b0aa5898a94e362f20be71e8863500b195 --- novaclient/tests/v3/fakes.py | 28 +++++++++++++++++ novaclient/tests/v3/test_list_extensions.py | 33 +++++++++++++++++++++ novaclient/v3/client.py | 2 ++ novaclient/v3/list_extensions.py | 26 ++++++++++++++++ novaclient/v3/shell.py | 9 ++++++ 5 files changed, 98 insertions(+) create mode 100644 novaclient/tests/v3/test_list_extensions.py create mode 100644 novaclient/v3/list_extensions.py diff --git a/novaclient/tests/v3/fakes.py b/novaclient/tests/v3/fakes.py index 43b5e05e7..9aad01716 100644 --- a/novaclient/tests/v3/fakes.py +++ b/novaclient/tests/v3/fakes.py @@ -344,3 +344,31 @@ def get_os_hypervisors_1234_servers(self, **kw): get_keypairs = fakes_v1_1.FakeHTTPClient.get_os_keypairs delete_keypairs_test = fakes_v1_1.FakeHTTPClient.delete_os_keypairs_test post_keypairs = fakes_v1_1.FakeHTTPClient.post_os_keypairs + + # + # List all extensions + # + def get_extensions(self, **kw): + exts = [ + { + "alias": "os-multinic", + "description": "Multiple network support", + "name": "Multinic", + "version": 1, + }, + { + "alias": "os-extended-server-attributes", + "description": "Extended Server Attributes support.", + "name": "ExtendedServerAttributes", + "version": 1, + }, + { + "alias": "os-extended-status", + "description": "Extended Status support", + "name": "ExtendedStatus", + "version": 1, + }, + ] + return (200, {}, { + "extensions": exts, + }) diff --git a/novaclient/tests/v3/test_list_extensions.py b/novaclient/tests/v3/test_list_extensions.py new file mode 100644 index 000000000..61d387c3c --- /dev/null +++ b/novaclient/tests/v3/test_list_extensions.py @@ -0,0 +1,33 @@ +# Copyright 2014 NEC Corporation. All rights reserved. +# +# 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 novaclient import extension +from novaclient.tests import utils +from novaclient.tests.v3 import fakes +from novaclient.v3 import list_extensions + + +extensions = [ + extension.Extension("list_extensions", list_extensions), +] +cs = fakes.FakeClient(extensions=extensions) + + +class ListExtensionsTests(utils.TestCase): + def test_list_extensions(self): + all_exts = cs.list_extensions.show_all() + cs.assert_called('GET', '/extensions') + self.assertTrue(len(all_exts) > 0) + for r in all_exts: + self.assertTrue(len(r.summary) > 0) diff --git a/novaclient/v3/client.py b/novaclient/v3/client.py index 4f454e220..7af2e1f2b 100644 --- a/novaclient/v3/client.py +++ b/novaclient/v3/client.py @@ -24,6 +24,7 @@ from novaclient.v3 import hypervisors from novaclient.v3 import images from novaclient.v3 import keypairs +from novaclient.v3 import list_extensions from novaclient.v3 import quotas from novaclient.v3 import servers from novaclient.v3 import services @@ -84,6 +85,7 @@ def __init__(self, username, password, project_id, auth_url=None, self.availability_zones = \ availability_zones.AvailabilityZoneManager(self) self.certs = certs.CertificateManager(self) + self.list_extensions = list_extensions.ListExtManager(self) self.hosts = hosts.HostManager(self) self.flavors = flavors.FlavorManager(self) self.flavor_access = flavor_access.FlavorAccessManager(self) diff --git a/novaclient/v3/list_extensions.py b/novaclient/v3/list_extensions.py new file mode 100644 index 000000000..bcc187494 --- /dev/null +++ b/novaclient/v3/list_extensions.py @@ -0,0 +1,26 @@ +# Copyright 2014 NEC Corporation. All rights reserved. +# +# 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. +""" +extension interface +""" + +from novaclient.v1_1.contrib import list_extensions + + +class ListExtResource(list_extensions.ListExtResource): + pass + + +class ListExtManager(list_extensions.ListExtManager): + pass diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index f3710354c..3736bcb85 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -2584,6 +2584,15 @@ def do_credentials(cs, _args): utils.print_dict(catalog['access']['token'], "Token", wrap=int(_args.wrap)) +def do_extension_list(cs, _args): + """ + List all the os-api extensions that are available. + """ + extensions = cs.list_extensions.show_all() + fields = ["Name", "Summary", "Alias", "Version"] + utils.print_list(extensions, fields) + + @utils.arg('server', metavar='', help='Name or ID of server.') @utils.arg('--port', dest='port', From 28c89580af7230d40ed994b405b8b956560f23e8 Mon Sep 17 00:00:00 2001 From: Joe Gordon Date: Thu, 1 May 2014 11:27:00 -0700 Subject: [PATCH 0489/1705] Allow the default value of 1 to be set for boot multiple Allow one to call the following with their default values of 1: * nova boot --num-instances 1 * nova boot --min-count 1 * nova boot --max-count 1 Don't allow min_count>max_count. This is the only place where if a user passes in the default value the CLI will return an error. Change-Id: I805b3f0c778e9c70c5817624fb696a746dfe3d5b --- novaclient/tests/v1_1/test_shell.py | 17 +++++++++++++++-- novaclient/tests/v3/test_shell.py | 17 +++++++++++++++-- novaclient/v1_1/shell.py | 16 ++++++++++------ novaclient/v3/shell.py | 16 ++++++++++------ 4 files changed, 50 insertions(+), 16 deletions(-) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index d01155b24..883ac90c8 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -585,8 +585,6 @@ def test_boot_num_instances(self): }) def test_boot_invalid_num_instances(self): - cmd = 'boot --image 1 --flavor 1 --num-instances 1 server' - self.assertRaises(exceptions.CommandError, self.run_command, cmd) cmd = 'boot --image 1 --flavor 1 --num-instances 0 server' self.assertRaises(exceptions.CommandError, self.run_command, cmd) @@ -610,6 +608,19 @@ def test_boot_min_max_count(self): } }) self.run_command('boot --image 1 --flavor 1 --min-count 3 server') + self.assert_called_anytime( + 'POST', '/servers', + { + 'server': { + 'flavorRef': '1', + 'name': 'server', + 'imageRef': '1', + 'min_count': 3, + 'max_count': 3, + } + }) + self.run_command('boot --image 1 --flavor 1 ' + '--min-count 3 --max-count 3 server') self.assert_called_anytime( 'POST', '/servers', { @@ -634,6 +645,8 @@ def test_boot_min_max_count(self): 'max_count': 5, } }) + cmd = 'boot --image 1 --flavor 1 --min-count 3 --max-count 1 serv' + self.assertRaises(exceptions.CommandError, self.run_command, cmd) @mock.patch('novaclient.v1_1.shell._poll_for_status') def test_boot_with_poll(self, poll_method): diff --git a/novaclient/tests/v3/test_shell.py b/novaclient/tests/v3/test_shell.py index a18d97d10..a62d1df55 100644 --- a/novaclient/tests/v3/test_shell.py +++ b/novaclient/tests/v3/test_shell.py @@ -484,8 +484,6 @@ def test_boot_num_instances(self): }) def test_boot_invalid_num_instances(self): - cmd = 'boot --image 1 --flavor 1 --num-instances 1 server' - self.assertRaises(exceptions.CommandError, self.run_command, cmd) cmd = 'boot --image 1 --flavor 1 --num-instances 0 server' self.assertRaises(exceptions.CommandError, self.run_command, cmd) @@ -509,6 +507,19 @@ def test_boot_min_max_count(self): } }) self.run_command('boot --image 1 --flavor 1 --min-count 3 server') + self.assert_called_anytime( + 'POST', '/servers', + { + 'server': { + 'flavor_ref': '1', + 'name': 'server', + 'image_ref': '1', + 'os-multiple-create:min_count': 3, + 'os-multiple-create:max_count': 3, + } + }) + self.run_command('boot --image 1 --flavor 1 ' + '--min-count 3 --max-count 3 server') self.assert_called_anytime( 'POST', '/servers', { @@ -533,6 +544,8 @@ def test_boot_min_max_count(self): 'os-multiple-create:max_count': 5, } }) + cmd = 'boot --image 1 --flavor 1 --min-count 3 --max-count 1 serv' + self.assertRaises(exceptions.CommandError, self.run_command, cmd) @mock.patch('novaclient.v3.shell._poll_for_status') def test_boot_with_poll(self, poll_method): diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 54774b6c4..0142cb802 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -151,22 +151,26 @@ def _boot(cs, args): # Don't let user mix num_instances and max_count/min_count. if (args.num_instances is not None and args.min_count is None and args.max_count is None): - if args.num_instances <= 1: - raise exceptions.CommandError(_("num_instances should be > 1")) + if args.num_instances < 1: + raise exceptions.CommandError(_("num_instances should be >= 1")) max_count = args.num_instances elif (args.num_instances is not None and (args.min_count is not None or args.max_count is not None)): raise exceptions.CommandError(_("Don't mix num-instances and " "max/min-count")) if args.min_count is not None: - if args.min_count <= 1: - raise exceptions.CommandError(_("min_count should be > 1")) + if args.min_count < 1: + raise exceptions.CommandError(_("min_count should be >= 1")) min_count = args.min_count max_count = min_count if args.max_count is not None: - if args.max_count <= 1: - raise exceptions.CommandError(_("max_count should be > 1")) + if args.max_count < 1: + raise exceptions.CommandError(_("max_count should be >= 1")) max_count = args.max_count + if (args.min_count is not None and args.max_count is not None and + args.min_count > args.max_count): + raise exceptions.CommandError(_( + "min_count should be <= max_count")) flavor = _find_flavor(cs, args.flavor) diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 1d9c76c6d..967a893b8 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -87,22 +87,26 @@ def _boot(cs, args): # Don't let user mix num_instances and max_count/min_count. if (args.num_instances is not None and args.min_count is None and args.max_count is None): - if args.num_instances <= 1: - raise exceptions.CommandError("num_instances should be > 1") + if args.num_instances < 1: + raise exceptions.CommandError("num_instances should be >= 1") max_count = args.num_instances elif (args.num_instances is not None and (args.min_count is not None or args.max_count is not None)): raise exceptions.CommandError("Don't mix num-instances and " "max/min-count") if args.min_count is not None: - if args.min_count <= 1: - raise exceptions.CommandError("min_count should be > 1") + if args.min_count < 1: + raise exceptions.CommandError("min_count should be >= 1") min_count = args.min_count max_count = min_count if args.max_count is not None: - if args.max_count <= 1: - raise exceptions.CommandError("max_count should be > 1") + if args.max_count < 1: + raise exceptions.CommandError("max_count should be >= 1") max_count = args.max_count + if (args.min_count is not None and args.max_count is not None and + args.min_count > args.max_count): + raise exceptions.CommandError( + "min_count should be <= max_count") flavor = _find_flavor(cs, args.flavor) From ced071b78b16ce41e3bd599094afe50ddeb135c0 Mon Sep 17 00:00:00 2001 From: "ChangBo Guo(gcb)" Date: Mon, 12 May 2014 16:49:06 +0800 Subject: [PATCH 0490/1705] Add missing dependent module gettextutils gettextutils was used in novaclient, but wasn't recorded. This patch also let the modules in alphabetical order. Change-Id: I725c3e8be126b8acd5e42ccaca776f57235ecdc7 --- openstack-common.conf | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/openstack-common.conf b/openstack-common.conf index 23c09bc0d..f21e5b551 100644 --- a/openstack-common.conf +++ b/openstack-common.conf @@ -1,15 +1,16 @@ [DEFAULT] -# The list of modules to copy from openstack-common +# The list of modules to copy from oslo-incubator +module=apiclient +module=cliutils +module=gettextutils module=install_venv_common +module=importutils module=jsonutils +module=network_utils module=strutils module=timeutils module=uuidutils -module=apiclient -module=importutils -module=cliutils -module=network_utils # The base module to hold the copy of openstack.common base=novaclient From 22277e6d0b92e5674e2602610dc30f9a9262caf8 Mon Sep 17 00:00:00 2001 From: Christian Berendt Date: Mon, 12 May 2014 19:09:50 +0200 Subject: [PATCH 0491/1705] debug level logs should not be translated According to the OpenStack translation policy available at https://wiki.openstack.org/wiki/LoggingStandards debug messages should not be translated. Like mentioned in several changes in Nova by garyk this is to help prioritize log translation. Change-Id: I43c5d4ecd2286f1a622b6822603ba62881a317a2 --- novaclient/auth_plugin.py | 3 +-- novaclient/client.py | 10 +++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/novaclient/auth_plugin.py b/novaclient/auth_plugin.py index 1c7227fdc..da2c07b26 100644 --- a/novaclient/auth_plugin.py +++ b/novaclient/auth_plugin.py @@ -20,7 +20,6 @@ import six from novaclient import exceptions -from novaclient.openstack.common.gettextutils import _ from novaclient import utils @@ -40,7 +39,7 @@ def discover_auth_systems(): try: auth_plugin = ep.load() except (ImportError, pkg_resources.UnknownExtra, AttributeError) as e: - logger.debug(_("ERROR: Cannot load auth plugin %s") % ep.name) + logger.debug("ERROR: Cannot load auth plugin %s" % ep.name) logger.debug(e, exc_info=1) else: _discovered_plugins[ep.name] = auth_plugin diff --git a/novaclient/client.py b/novaclient/client.py index 6c1ad13e7..8ac0354b5 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -183,10 +183,10 @@ def http_log_req(self, method, url, kwargs): def http_log_resp(self, resp): if not self.http_log_debug: return - self._logger.debug(_("RESP: [%(status)s] %(headers)s\nRESP BODY: " - "%(text)s\n"), {'status': resp.status_code, - 'headers': resp.headers, - 'text': resp.text}) + self._logger.debug("RESP: [%(status)s] %(headers)s\nRESP BODY: " + "%(text)s\n", {'status': resp.status_code, + 'headers': resp.headers, + 'text': resp.text}) def open_session(self): if not self._connection_pool: @@ -375,7 +375,7 @@ def _fetch_endpoints_from_auth(self, url): # GET ...:5001/v2.0/tokens/#####/endpoints url = '/'.join([url, 'tokens', '%s?belongsTo=%s' % (self.proxy_token, self.proxy_tenant_id)]) - self._logger.debug(_("Using Endpoint URL: %s") % url) + self._logger.debug("Using Endpoint URL: %s" % url) resp, body = self._time_request( url, "GET", headers={'X-Auth-Token': self.auth_token}) return self._extract_service_catalog(url, resp, body, From 0f4f831474c36f279878be72c26d2f2ca87f2d56 Mon Sep 17 00:00:00 2001 From: Jamie Lennox Date: Tue, 4 Mar 2014 19:39:29 +1000 Subject: [PATCH 0492/1705] Convert agent testing to httpretty Change-Id: Ifd598c3b2d4c26b3557349046822a8ab2108aca7 blueprint: httpretty-testing --- novaclient/tests/fixture_data/agents.py | 57 +++++++++++++++++++++++ novaclient/tests/v1_1/test_agents.py | 61 ++++++++++++++++++------- novaclient/tests/v3/test_agents.py | 12 ++--- 3 files changed, 105 insertions(+), 25 deletions(-) create mode 100644 novaclient/tests/fixture_data/agents.py diff --git a/novaclient/tests/fixture_data/agents.py b/novaclient/tests/fixture_data/agents.py new file mode 100644 index 000000000..a21f473de --- /dev/null +++ b/novaclient/tests/fixture_data/agents.py @@ -0,0 +1,57 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import httpretty + +from novaclient.openstack.common import jsonutils +from novaclient.tests.fixture_data import base + + +class Fixture(base.Fixture): + + base_url = 'os-agents' + + def setUp(self): + super(Fixture, self).setUp() + + post_os_agents = { + 'agent': { + 'url': '/xxx/xxx/xxx', + 'hypervisor': 'kvm', + 'md5hash': 'add6bb58e139be103324d04d82d8f546', + 'version': '7.0', + 'architecture': 'x86', + 'os': 'win', + 'id': 1 + } + } + + httpretty.register_uri(httpretty.POST, self.url(), + body=jsonutils.dumps(post_os_agents), + content_type='application/json') + + put_os_agents_1 = { + "agent": { + "url": "/yyy/yyyy/yyyy", + "version": "8.0", + "md5hash": "add6bb58e139be103324d04d82d8f546", + 'id': 1 + } + } + + httpretty.register_uri(httpretty.PUT, self.url(1), + body=jsonutils.dumps(put_os_agents_1), + content_type='application/json') + + httpretty.register_uri(httpretty.DELETE, self.url(1), + content_type='application/json', + status=202) diff --git a/novaclient/tests/v1_1/test_agents.py b/novaclient/tests/v1_1/test_agents.py index e89da9e8a..910400467 100644 --- a/novaclient/tests/v1_1/test_agents.py +++ b/novaclient/tests/v1_1/test_agents.py @@ -13,35 +13,62 @@ # License for the specific language governing permissions and limitations # under the License. +import httpretty + +from novaclient.openstack.common import jsonutils +from novaclient.tests.fixture_data import agents as data +from novaclient.tests.fixture_data import client from novaclient.tests import utils -from novaclient.tests.v1_1 import fakes from novaclient.v1_1 import agents -class AgentsTest(utils.TestCase): - def setUp(self): - super(AgentsTest, self).setUp() - self.cs = self._get_fake_client() - self.agent_type = self._get_agent_type() +class AgentsTest(utils.FixturedTestCase): + + client_fixture_class = client.V1 + data_fixture_class = data.Fixture - def _get_fake_client(self): - return fakes.FakeClient() + def stub_hypervisors(self, hypervisor='kvm'): + get_os_agents = {'agents': + [ + { + 'hypervisor': hypervisor, + 'os': 'win', + 'architecture': 'x86', + 'version': '7.0', + 'url': 'xxx://xxxx/xxx/xxx', + 'md5hash': 'add6bb58e139be103324d04d82d8f545', + 'id': 1 + }, + { + 'hypervisor': hypervisor, + 'os': 'linux', + 'architecture': 'x86', + 'version': '16.0', + 'url': 'xxx://xxxx/xxx/xxx1', + 'md5hash': 'add6bb58e139be103324d04d82d8f546', + 'id': 2 + }, + ] + } - def _get_agent_type(self): - return agents.Agent + httpretty.register_uri(httpretty.GET, self.data_fixture.url(), + body=jsonutils.dumps(get_os_agents), + content_type='application/json') def test_list_agents(self): + self.stub_hypervisors() ags = self.cs.agents.list() - self.cs.assert_called('GET', '/os-agents') + self.assert_called('GET', '/os-agents') for a in ags: - self.assertIsInstance(a, self.agent_type) + self.assertIsInstance(a, agents.Agent) self.assertEqual(a.hypervisor, 'kvm') def test_list_agents_with_hypervisor(self): + self.stub_hypervisors('xen') ags = self.cs.agents.list('xen') - self.cs.assert_called('GET', '/os-agents?hypervisor=xen') + self.assert_called('GET', '/os-agents?hypervisor=xen') for a in ags: - self.assertIsInstance(a, self.agent_type) + self.assertIsInstance(a, agents.Agent) self.assertEqual(a.hypervisor, 'xen') def test_agents_create(self): @@ -56,12 +83,12 @@ def test_agents_create(self): 'version': '7.0', 'architecture': 'x86', 'os': 'win'}} - self.cs.assert_called('POST', '/os-agents', body) + self.assert_called('POST', '/os-agents', body) self.assertEqual(1, ag._info.copy()['id']) def test_agents_delete(self): self.cs.agents.delete('1') - self.cs.assert_called('DELETE', '/os-agents/1') + self.assert_called('DELETE', '/os-agents/1') def _build_example_update_body(self): return {"para": { @@ -74,5 +101,5 @@ def test_agents_modify(self): '/yyy/yyyy/yyyy', 'add6bb58e139be103324d04d82d8f546') body = self._build_example_update_body() - self.cs.assert_called('PUT', '/os-agents/1', body) + self.assert_called('PUT', '/os-agents/1', body) self.assertEqual(1, ag.id) diff --git a/novaclient/tests/v3/test_agents.py b/novaclient/tests/v3/test_agents.py index 06534c3a5..6aaee08c5 100644 --- a/novaclient/tests/v3/test_agents.py +++ b/novaclient/tests/v3/test_agents.py @@ -13,20 +13,16 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient.tests.fixture_data import client from novaclient.tests.v1_1 import test_agents -from novaclient.tests.v3 import fakes -from novaclient.v3 import agents class AgentsTest(test_agents.AgentsTest): + + client_fixture_class = client.V3 + def _build_example_update_body(self): return {"agent": { "url": "/yyy/yyyy/yyyy", "version": "8.0", "md5hash": "add6bb58e139be103324d04d82d8f546"}} - - def _get_fake_client(self): - return fakes.FakeClient() - - def _get_agent_type(self): - return agents.Agent From b4628ae313e9ea5d5e1077a9b585c548ff277f91 Mon Sep 17 00:00:00 2001 From: Jamie Lennox Date: Tue, 4 Mar 2014 20:22:40 +1000 Subject: [PATCH 0493/1705] Convert Availability Zone testing to httpretty Change-Id: I1de0a1f488f1a59d9041a2788762ddf8361a6b41 blueprint: httpretty-testing --- .../tests/fixture_data/availability_zones.py | 99 +++++++++++++++++++ .../tests/v1_1/test_availability_zone.py | 16 +-- novaclient/tests/v3/test_availability_zone.py | 12 +-- 3 files changed, 111 insertions(+), 16 deletions(-) create mode 100644 novaclient/tests/fixture_data/availability_zones.py diff --git a/novaclient/tests/fixture_data/availability_zones.py b/novaclient/tests/fixture_data/availability_zones.py new file mode 100644 index 000000000..c23788f51 --- /dev/null +++ b/novaclient/tests/fixture_data/availability_zones.py @@ -0,0 +1,99 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import httpretty + +from novaclient.openstack.common import jsonutils +from novaclient.tests.fixture_data import base + + +class V1(base.Fixture): + + base_url = 'os-availability-zone' + + zone_info_key = 'availabilityZoneInfo' + zone_name_key = 'zoneName' + zone_state_key = 'zoneState' + + def setUp(self): + super(V1, self).setUp() + + get_os_availability_zone = { + self.zone_info_key: [ + { + self.zone_name_key: "zone-1", + self.zone_state_key: {"available": True}, + "hosts": None + }, + { + self.zone_name_key: "zone-2", + self.zone_state_key: {"available": False}, + "hosts": None + } + ] + } + httpretty.register_uri(httpretty.GET, self.url(), + body=jsonutils.dumps(get_os_availability_zone), + content_type='application/json') + + get_os_zone_detail = { + self.zone_info_key: [ + { + self.zone_name_key: "zone-1", + self.zone_state_key: {"available": True}, + "hosts": { + "fake_host-1": { + "nova-compute": { + "active": True, + "available": True, + "updated_at": '2012-12-26 14:45:25' + } + } + } + }, + { + self.zone_name_key: "internal", + self.zone_state_key: {"available": True}, + "hosts": { + "fake_host-1": { + "nova-sched": { + "active": True, + "available": True, + "updated_at": '2012-12-26 14:45:25' + } + }, + "fake_host-2": { + "nova-network": { + "active": True, + "available": False, + "updated_at": '2012-12-26 14:45:24' + } + } + } + }, + { + self.zone_name_key: "zone-2", + self.zone_state_key: {"available": False}, + "hosts": None + } + ] + } + + httpretty.register_uri(httpretty.GET, self.url('detail'), + body=jsonutils.dumps(get_os_zone_detail), + content_type='application/json') + + +class V3(V1): + zone_info_key = 'availability_zone_info' + zone_name_key = 'zone_name' + zone_state_key = 'zone_state' diff --git a/novaclient/tests/v1_1/test_availability_zone.py b/novaclient/tests/v1_1/test_availability_zone.py index 6d742b1b6..6cd5fafa1 100644 --- a/novaclient/tests/v1_1/test_availability_zone.py +++ b/novaclient/tests/v1_1/test_availability_zone.py @@ -16,24 +16,24 @@ import six +from novaclient.tests.fixture_data import availability_zones as data +from novaclient.tests.fixture_data import client from novaclient.tests import utils -from novaclient.tests.v1_1 import fakes from novaclient.v1_1 import availability_zones -class AvailabilityZoneTest(utils.TestCase): +class AvailabilityZoneTest(utils.FixturedTestCase): # NOTE(cyeoh): import shell here so the V3 version of # this class can inherit off the v3 version of shell from novaclient.v1_1 import shell # noqa + client_fixture_class = client.V1 + data_fixture_class = data.V1 + def setUp(self): super(AvailabilityZoneTest, self).setUp() - self.cs = self._get_fake_client() self.availability_zone_type = self._get_availability_zone_type() - def _get_fake_client(self): - return fakes.FakeClient() - def _get_availability_zone_type(self): return availability_zones.AvailabilityZone @@ -43,7 +43,7 @@ def _assertZone(self, zone, name, status): def test_list_availability_zone(self): zones = self.cs.availability_zones.list(detailed=False) - self.cs.assert_called('GET', '/os-availability-zone') + self.assert_called('GET', '/os-availability-zone') for zone in zones: self.assertIsInstance(zone, self.availability_zone_type) @@ -63,7 +63,7 @@ def test_list_availability_zone(self): def test_detail_availability_zone(self): zones = self.cs.availability_zones.list(detailed=True) - self.cs.assert_called('GET', '/os-availability-zone/detail') + self.assert_called('GET', '/os-availability-zone/detail') for zone in zones: self.assertIsInstance(zone, self.availability_zone_type) diff --git a/novaclient/tests/v3/test_availability_zone.py b/novaclient/tests/v3/test_availability_zone.py index 004ba95fa..ad7f3a715 100644 --- a/novaclient/tests/v3/test_availability_zone.py +++ b/novaclient/tests/v3/test_availability_zone.py @@ -14,25 +14,21 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient.tests.fixture_data import availability_zones as data +from novaclient.tests.fixture_data import client from novaclient.tests.v1_1 import test_availability_zone -from novaclient.tests.v3 import fakes from novaclient.v3 import availability_zones class AvailabilityZoneTest(test_availability_zone.AvailabilityZoneTest): from novaclient.v3 import shell # noqa - def setUp(self): - super(AvailabilityZoneTest, self).setUp() - self.cs = self._get_fake_client() - self.availability_zone_type = self._get_availability_zone_type() + client_fixture_class = client.V3 + data_fixture_class = data.V3 def _assertZone(self, zone, name, status): self.assertEqual(zone.zone_name, name) self.assertEqual(zone.zone_state, status) - def _get_fake_client(self): - return fakes.FakeClient() - def _get_availability_zone_type(self): return availability_zones.AvailabilityZone From 4e1ee661083bd672a884fd13fdb1ffaadea5252a Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Mon, 19 May 2014 13:48:29 +0300 Subject: [PATCH 0494/1705] Set default http-based exception as `HttpError` Since exceptions from oslo is used, ClientException doesn't have any attributes related to HTTP, so `HttpError` should be set as default http-based exception. Also, attribute `code` is used in several project, so it should be returned and marked as deprecated. Partial-Bug: #1322183 Change-Id: I3c71e2d25d6e36b5bac0f2b3add74d6747cf7c25 --- novaclient/base.py | 2 +- novaclient/exceptions.py | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/novaclient/base.py b/novaclient/base.py index 11a6d6f80..c54367c1f 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -191,7 +191,7 @@ def find(self, **kwargs): num_matches = len(matches) if num_matches == 0: msg = "No %s matching %s." % (self.resource_class.__name__, kwargs) - raise exceptions.NotFound(404, msg) + raise exceptions.NotFound(msg) elif num_matches > 1: raise exceptions.NoUniqueMatch else: diff --git a/novaclient/exceptions.py b/novaclient/exceptions.py index cba8ea165..c2a6bf6ed 100644 --- a/novaclient/exceptions.py +++ b/novaclient/exceptions.py @@ -28,6 +28,15 @@ OverLimit = RequestEntityTooLarge +def _deprecate_code_attribute(slf): + import warnings + warnings.warn("'code' attribute is deprecated since v.2.17.0. Use " + "'http_status' instead of this one.", UserWarning) + return slf.http_status + +HttpError.code = property(_deprecate_code_attribute) + + class NoTokenLookupException(ClientException): """This form of authentication does not support looking up endpoints from an existing token. @@ -57,7 +66,7 @@ class RateLimit(RequestEntityTooLarge): def from_response(response, body, url, method=None): """ - Return an instance of an ClientException or subclass + Return an instance of an HttpError or subclass based on an requests response. Usage:: @@ -91,5 +100,6 @@ def from_response(response, body, url, method=None): kwargs['message'] = message kwargs['details'] = details - cls = _code_map.get(response.status_code, ClientException) + cls = _code_map.get(response.status_code, HttpError) + return cls(**kwargs) From 43181edda1a86d069429cd112d84e146b294dc2b Mon Sep 17 00:00:00 2001 From: Jamie Lennox Date: Fri, 9 May 2014 14:07:42 +1000 Subject: [PATCH 0495/1705] Convert certificate tests to httpretty Change-Id: Ic7eacb75f90b383ecacf01269033c0a37bd7528c blueprint: httpretty-testing --- novaclient/tests/fixture_data/certs.py | 58 ++++++++++++++++++++++++++ novaclient/tests/v1_1/test_certs.py | 25 +++++------ novaclient/tests/v3/test_certs.py | 13 +----- 3 files changed, 70 insertions(+), 26 deletions(-) create mode 100644 novaclient/tests/fixture_data/certs.py diff --git a/novaclient/tests/fixture_data/certs.py b/novaclient/tests/fixture_data/certs.py new file mode 100644 index 000000000..3ce0f0f07 --- /dev/null +++ b/novaclient/tests/fixture_data/certs.py @@ -0,0 +1,58 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import httpretty + +from novaclient.openstack.common import jsonutils +from novaclient.tests.fixture_data import base + + +class Fixture(base.Fixture): + + base_url = 'os-certificates' + + def get_os_certificates_root(self, **kw): + return ( + 200, + {}, + {'certificate': {'private_key': None, 'data': 'foo'}} + ) + + def post_os_certificates(self, **kw): + return ( + 200, + {}, + {'certificate': {'private_key': 'foo', 'data': 'bar'}} + ) + + def setUp(self): + super(Fixture, self).setUp() + + get_os_certificate = { + 'certificate': { + 'private_key': None, + 'data': 'foo' + } + } + httpretty.register_uri(httpretty.GET, self.url('root'), + body=jsonutils.dumps(get_os_certificate), + content_type='application/json') + + post_os_certificates = { + 'certificate': { + 'private_key': 'foo', + 'data': 'bar' + } + } + httpretty.register_uri(httpretty.POST, self.url(), + body=jsonutils.dumps(post_os_certificates), + content_type='application/json') diff --git a/novaclient/tests/v1_1/test_certs.py b/novaclient/tests/v1_1/test_certs.py index 87e1d09f0..0519254c0 100644 --- a/novaclient/tests/v1_1/test_certs.py +++ b/novaclient/tests/v1_1/test_certs.py @@ -11,29 +11,24 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient.tests.fixture_data import certs as data +from novaclient.tests.fixture_data import client from novaclient.tests import utils -from novaclient.tests.v1_1 import fakes from novaclient.v1_1 import certs -class CertsTest(utils.TestCase): - def setUp(self): - super(CertsTest, self).setUp() - self.cs = self._get_fake_client() - self.cert_type = self._get_cert_type() +class CertsTest(utils.FixturedTestCase): - def _get_fake_client(self): - return fakes.FakeClient() - - def _get_cert_type(self): - return certs.Certificate + client_fixture_class = client.V1 + data_fixture_class = data.Fixture + cert_type = certs.Certificate def test_create_cert(self): cert = self.cs.certs.create() - self.cs.assert_called('POST', '/os-certificates') - self.assertIsInstance(cert, certs.Certificate) + self.assert_called('POST', '/os-certificates') + self.assertIsInstance(cert, self.cert_type) def test_get_root_cert(self): cert = self.cs.certs.get() - self.cs.assert_called('GET', '/os-certificates/root') - self.assertIsInstance(cert, certs.Certificate) + self.assert_called('GET', '/os-certificates/root') + self.assertIsInstance(cert, self.cert_type) diff --git a/novaclient/tests/v3/test_certs.py b/novaclient/tests/v3/test_certs.py index 1e293adc0..cf09e58db 100644 --- a/novaclient/tests/v3/test_certs.py +++ b/novaclient/tests/v3/test_certs.py @@ -11,19 +11,10 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.tests.v1_1 import fakes +from novaclient.tests.fixture_data import client from novaclient.tests.v1_1 import test_certs -from novaclient.v3 import certs class CertsTest(test_certs.CertsTest): - def setUp(self): - super(CertsTest, self).setUp() - self.cs = self._get_fake_client() - self.cert_type = self._get_cert_type() - def _get_fake_client(self): - return fakes.FakeClient() - - def _get_cert_type(self): - return certs.Certificate + client_fixture_data = client.V3 From ecebc308b3394d4847cf32b37f93fd08c646a39d Mon Sep 17 00:00:00 2001 From: Jamie Lennox Date: Tue, 27 May 2014 10:30:47 +1000 Subject: [PATCH 0496/1705] In Py3 decode the output of base64.decode The return from a base64.encode in python 3 is bytes. Bytes cannot be json.dumps() so when we base64 encode some data we need to convert it to a string before passing it through to the request layer. This isn't shown by unit tests because the fakeclient mocking layer intercepts the request before it gets to the json encoding code. Closes-Bug: #1323450 Change-Id: Ibbbb24f64c17069178e3bf0ee9998b806bc629ff --- novaclient/tests/v1_1/test_shell.py | 10 +++++----- novaclient/tests/v3/test_shell.py | 5 ++--- novaclient/v1_1/servers.py | 7 +++++-- novaclient/v3/servers.py | 4 ++-- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index 05254a7de..e7f6c4df7 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -159,8 +159,8 @@ def test_boot_key(self): def test_boot_user_data(self): testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt') - data = open(testfile).read() - expected_file_data = base64.b64encode(data.encode('utf-8')) + data = open(testfile).read().encode('utf-8') + expected_file_data = base64.b64encode(data).decode('utf-8') self.run_command( 'boot --flavor 1 --image 1 --user_data %s some-server' % testfile) self.assert_called_anytime( @@ -533,7 +533,7 @@ def test_boot_nics_netid_and_portid(self): def test_boot_files(self): testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt') data = open(testfile).read() - expected_file_data = base64.b64encode(data.encode('utf-8')) + expected = base64.b64encode(data.encode('utf-8')).decode('utf-8') cmd = ('boot some-server --flavor 1 --image 1' ' --file /tmp/foo=%s --file /tmp/bar=%s') @@ -548,8 +548,8 @@ def test_boot_files(self): 'min_count': 1, 'max_count': 1, 'personality': [ - {'path': '/tmp/bar', 'contents': expected_file_data}, - {'path': '/tmp/foo', 'contents': expected_file_data}, + {'path': '/tmp/bar', 'contents': expected}, + {'path': '/tmp/foo', 'contents': expected}, ] }}, ) diff --git a/novaclient/tests/v3/test_shell.py b/novaclient/tests/v3/test_shell.py index 30e733969..fbed7e11a 100644 --- a/novaclient/tests/v3/test_shell.py +++ b/novaclient/tests/v3/test_shell.py @@ -226,6 +226,7 @@ def test_boot_user_data(self): mock_open.assert_called_once_with(testfile) + user_data = base64.b64encode(file_text.encode('utf-8')).decode('utf-8') self.assert_called_anytime( 'POST', '/servers', {'server': { @@ -234,9 +235,7 @@ def test_boot_user_data(self): 'image_ref': '1', 'os-multiple-create:min_count': 1, 'os-multiple-create:max_count': 1, - 'os-user-data:user_data': base64.b64encode( - file_text.encode('utf-8')) - }}, + 'os-user-data:user_data': user_data}}, ) def test_boot_avzone(self): diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index 41f04fd63..400999049 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -455,7 +455,8 @@ def _boot(self, resource_url, response_key, name, image, flavor, else: userdata = strutils.safe_encode(userdata) - body["server"]["user_data"] = base64.b64encode(userdata) + userdata_b64 = base64.b64encode(userdata).decode('utf-8') + body["server"]["user_data"] = userdata_b64 if meta: body["server"]["metadata"] = meta if reservation_id: @@ -491,9 +492,11 @@ def _boot(self, resource_url, response_key, name, image, flavor, data = file_or_string.read() else: data = file_or_string + + cont = base64.b64encode(data.encode('utf-8')).decode('utf-8') personality.append({ 'path': filepath, - 'contents': base64.b64encode(data.encode('utf-8')), + 'contents': cont, }) if availability_zone: diff --git a/novaclient/v3/servers.py b/novaclient/v3/servers.py index 076a6bf35..0e028b5da 100644 --- a/novaclient/v3/servers.py +++ b/novaclient/v3/servers.py @@ -404,8 +404,8 @@ def _boot(self, resource_url, response_key, name, image, flavor, else: userdata = strutils.safe_encode(userdata) - body["server"][ - "os-user-data:user_data"] = base64.b64encode(userdata) + data = base64.b64encode(userdata).decode('utf-8') + body["server"]["os-user-data:user_data"] = data if meta: body["server"]["metadata"] = meta if reservation_id: From 0ffa94ef8a1d288affb5b01aaa414f4a03481b62 Mon Sep 17 00:00:00 2001 From: Jamie Lennox Date: Fri, 9 May 2014 14:26:50 +1000 Subject: [PATCH 0497/1705] Convert Cloud Pipe tests to httpretty Change-Id: I0a737a5c66831edafbd671bfd49017bc1274fca3 blueprint: httpretty-testing --- novaclient/tests/fixture_data/cloudpipe.py | 40 ++++++++++++++++++++++ novaclient/tests/v1_1/test_cloudpipe.py | 25 ++++++++------ 2 files changed, 54 insertions(+), 11 deletions(-) create mode 100644 novaclient/tests/fixture_data/cloudpipe.py diff --git a/novaclient/tests/fixture_data/cloudpipe.py b/novaclient/tests/fixture_data/cloudpipe.py new file mode 100644 index 000000000..fffd2a1fe --- /dev/null +++ b/novaclient/tests/fixture_data/cloudpipe.py @@ -0,0 +1,40 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import httpretty + +from novaclient.openstack.common import jsonutils +from novaclient.tests.fixture_data import base + + +class Fixture(base.Fixture): + + base_url = 'os-cloudpipe' + + def setUp(self): + super(Fixture, self).setUp() + + get_os_cloudpipe = {'cloudpipes': [{'project_id': 1}]} + httpretty.register_uri(httpretty.GET, self.url(), + body=jsonutils.dumps(get_os_cloudpipe), + content_type='application/json') + + instance_id = '9d5824aa-20e6-4b9f-b967-76a699fc51fd' + post_os_cloudpipe = {'instance_id': instance_id} + httpretty.register_uri(httpretty.POST, self.url(), + body=jsonutils.dumps(post_os_cloudpipe), + content_type='application/json', + status=202) + + httpretty.register_uri(httpretty.PUT, self.url('configure-project'), + content_type='application/json', + status=202) diff --git a/novaclient/tests/v1_1/test_cloudpipe.py b/novaclient/tests/v1_1/test_cloudpipe.py index 8cb96aa42..d2183c47e 100644 --- a/novaclient/tests/v1_1/test_cloudpipe.py +++ b/novaclient/tests/v1_1/test_cloudpipe.py @@ -11,30 +11,33 @@ # License for the specific language governing permissions and limitations # under the License. +import six + +from novaclient.tests.fixture_data import client +from novaclient.tests.fixture_data import cloudpipe as data from novaclient.tests import utils -from novaclient.tests.v1_1 import fakes from novaclient.v1_1 import cloudpipe -cs = fakes.FakeClient() - +class CloudpipeTest(utils.FixturedTestCase): -class CloudpipeTest(utils.TestCase): + client_fixture_class = client.V1 + data_fixture_class = data.Fixture def test_list_cloudpipes(self): - cp = cs.cloudpipe.list() - cs.assert_called('GET', '/os-cloudpipe') + cp = self.cs.cloudpipe.list() + self.assert_called('GET', '/os-cloudpipe') [self.assertIsInstance(c, cloudpipe.Cloudpipe) for c in cp] def test_create(self): project = "test" - cp = cs.cloudpipe.create(project) + cp = self.cs.cloudpipe.create(project) body = {'cloudpipe': {'project_id': project}} - cs.assert_called('POST', '/os-cloudpipe', body) - self.assertIsInstance(cp, str) + self.assert_called('POST', '/os-cloudpipe', body) + self.assertIsInstance(cp, six.string_types) def test_update(self): - cs.cloudpipe.update("192.168.1.1", 2345) + self.cs.cloudpipe.update("192.168.1.1", 2345) body = {'configure_project': {'vpn_ip': "192.168.1.1", 'vpn_port': 2345}} - cs.assert_called('PUT', '/os-cloudpipe/configure-project', body) + self.assert_called('PUT', '/os-cloudpipe/configure-project', body) From 873f958f79b3734133226d6c34907abbf19e6046 Mon Sep 17 00:00:00 2001 From: Jamie Lennox Date: Fri, 9 May 2014 14:32:55 +1000 Subject: [PATCH 0498/1705] Convert Fixed IP tests to httpretty Change-Id: I90a14b51dba6c0b64f0a264e762a0795a2566ae6 blueprint: httpretty-testing --- novaclient/tests/fixture_data/fixedips.py | 41 +++++++++++++++++++++++ novaclient/tests/v1_1/test_fixed_ips.py | 20 ++++++----- 2 files changed, 52 insertions(+), 9 deletions(-) create mode 100644 novaclient/tests/fixture_data/fixedips.py diff --git a/novaclient/tests/fixture_data/fixedips.py b/novaclient/tests/fixture_data/fixedips.py new file mode 100644 index 000000000..64a36d213 --- /dev/null +++ b/novaclient/tests/fixture_data/fixedips.py @@ -0,0 +1,41 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import httpretty + +from novaclient.openstack.common import jsonutils +from novaclient.tests.fixture_data import base + + +class Fixture(base.Fixture): + + base_url = 'os-fixed-ips' + + def setUp(self): + super(Fixture, self).setUp() + + get_os_fixed_ips = { + "fixed_ip": { + 'cidr': '192.168.1.0/24', + 'address': '192.168.1.1', + 'hostname': 'foo', + 'host': 'bar' + } + } + httpretty.register_uri(httpretty.GET, self.url('192.168.1.1'), + body=jsonutils.dumps(get_os_fixed_ips), + content_type='application/json') + + httpretty.register_uri(httpretty.POST, + self.url('192.168.1.1', 'action'), + content_type='application/json', + status=202) diff --git a/novaclient/tests/v1_1/test_fixed_ips.py b/novaclient/tests/v1_1/test_fixed_ips.py index 42e856c96..ca2b3fc5f 100644 --- a/novaclient/tests/v1_1/test_fixed_ips.py +++ b/novaclient/tests/v1_1/test_fixed_ips.py @@ -13,17 +13,19 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient.tests.fixture_data import client +from novaclient.tests.fixture_data import fixedips as data from novaclient.tests import utils -from novaclient.tests.v1_1 import fakes -cs = fakes.FakeClient() +class FixedIpsTest(utils.FixturedTestCase): -class FixedIpsTest(utils.TestCase): + client_fixture_class = client.V1 + data_fixture_class = data.Fixture def test_get_fixed_ip(self): - info = cs.fixed_ips.get(fixed_ip='192.168.1.1') - cs.assert_called('GET', '/os-fixed-ips/192.168.1.1') + info = self.cs.fixed_ips.get(fixed_ip='192.168.1.1') + self.assert_called('GET', '/os-fixed-ips/192.168.1.1') self.assertEqual(info.cidr, '192.168.1.0/24') self.assertEqual(info.address, '192.168.1.1') self.assertEqual(info.hostname, 'foo') @@ -31,10 +33,10 @@ def test_get_fixed_ip(self): def test_reserve_fixed_ip(self): body = {"reserve": None} - res = cs.fixed_ips.reserve(fixed_ip='192.168.1.1') - cs.assert_called('POST', '/os-fixed-ips/192.168.1.1/action', body) + res = self.cs.fixed_ips.reserve(fixed_ip='192.168.1.1') + self.assert_called('POST', '/os-fixed-ips/192.168.1.1/action', body) def test_unreserve_fixed_ip(self): body = {"unreserve": None} - res = cs.fixed_ips.unreserve(fixed_ip='192.168.1.1') - cs.assert_called('POST', '/os-fixed-ips/192.168.1.1/action', body) + res = self.cs.fixed_ips.unreserve(fixed_ip='192.168.1.1') + self.assert_called('POST', '/os-fixed-ips/192.168.1.1/action', body) From bf39b0c3973c11d30c72addfefe9de5565c883cd Mon Sep 17 00:00:00 2001 From: Jamie Lennox Date: Fri, 9 May 2014 16:43:16 +1000 Subject: [PATCH 0499/1705] Convert fPing tests to httpretty Change-Id: I9e3b152e5916215c98cd218bf500f9864c763d63 blueprint: httpretty-testing --- novaclient/tests/fixture_data/fping.py | 49 ++++++++++++++++++++++++++ novaclient/tests/v1_1/test_fping.py | 31 ++++++++-------- 2 files changed, 65 insertions(+), 15 deletions(-) create mode 100644 novaclient/tests/fixture_data/fping.py diff --git a/novaclient/tests/fixture_data/fping.py b/novaclient/tests/fixture_data/fping.py new file mode 100644 index 000000000..9542f83d4 --- /dev/null +++ b/novaclient/tests/fixture_data/fping.py @@ -0,0 +1,49 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import httpretty + +from novaclient.openstack.common import jsonutils +from novaclient.tests.fixture_data import base + + +class Fixture(base.Fixture): + + base_url = 'os-fping' + + def setUp(self): + super(Fixture, self).setUp() + + get_os_fping_1 = { + 'server': { + "id": "1", + "project_id": "fake-project", + "alive": True, + } + } + httpretty.register_uri(httpretty.GET, self.url(1), + body=jsonutils.dumps(get_os_fping_1), + content_type='application/json') + + get_os_fping = { + 'servers': [ + get_os_fping_1['server'], + { + "id": "2", + "project_id": "fake-project", + "alive": True, + }, + ] + } + httpretty.register_uri(httpretty.GET, self.url(), + body=jsonutils.dumps(get_os_fping), + content_type='application/json') diff --git a/novaclient/tests/v1_1/test_fping.py b/novaclient/tests/v1_1/test_fping.py index aaeb9b389..87f9af29e 100644 --- a/novaclient/tests/v1_1/test_fping.py +++ b/novaclient/tests/v1_1/test_fping.py @@ -13,49 +13,50 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient.tests.fixture_data import client +from novaclient.tests.fixture_data import fping as data from novaclient.tests import utils -from novaclient.tests.v1_1 import fakes from novaclient.v1_1 import fping -cs = fakes.FakeClient() +class FpingTest(utils.FixturedTestCase): - -class FpingTest(utils.TestCase): + client_fixture_class = client.V1 + data_fixture_class = data.Fixture def test_fping_repr(self): - r = cs.fping.get(1) + r = self.cs.fping.get(1) self.assertEqual(repr(r), "") def test_list_fpings(self): - fl = cs.fping.list() - cs.assert_called('GET', '/os-fping') + fl = self.cs.fping.list() + self.assert_called('GET', '/os-fping') for f in fl: self.assertIsInstance(f, fping.Fping) self.assertEqual(f.project_id, "fake-project") self.assertEqual(f.alive, True) def test_list_fpings_all_tenants(self): - fl = cs.fping.list(all_tenants=True) + fl = self.cs.fping.list(all_tenants=True) for f in fl: self.assertIsInstance(f, fping.Fping) - cs.assert_called('GET', '/os-fping?all_tenants=1') + self.assert_called('GET', '/os-fping?all_tenants=1') def test_list_fpings_exclude(self): - fl = cs.fping.list(exclude=['1']) + fl = self.cs.fping.list(exclude=['1']) for f in fl: self.assertIsInstance(f, fping.Fping) - cs.assert_called('GET', '/os-fping?exclude=1') + self.assert_called('GET', '/os-fping?exclude=1') def test_list_fpings_include(self): - fl = cs.fping.list(include=['1']) + fl = self.cs.fping.list(include=['1']) for f in fl: self.assertIsInstance(f, fping.Fping) - cs.assert_called('GET', '/os-fping?include=1') + self.assert_called('GET', '/os-fping?include=1') def test_get_fping(self): - f = cs.fping.get(1) - cs.assert_called('GET', '/os-fping/1') + f = self.cs.fping.get(1) + self.assert_called('GET', '/os-fping/1') self.assertIsInstance(f, fping.Fping) self.assertEqual(f.project_id, "fake-project") self.assertEqual(f.alive, True) From 1e40b41a9e3bdbfbf485ddbace55d47e78e66a1b Mon Sep 17 00:00:00 2001 From: Ihar Hrachyshka Date: Tue, 27 May 2014 10:22:49 +0200 Subject: [PATCH 0500/1705] Synced jsonutils from oslo-incubator The sync includes change that makes sure we get unicode-only dicts from jsonutils no matter which json module implementation is selected. The latest commit in oslo-incubator: - 0f4586c0076183c6356eec682c8a593648125abd Change-Id: Ic815ca3df94c33edec9104172048b2cd94b92e3f Closes-Bug: 1314129 --- novaclient/openstack/common/jsonutils.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/novaclient/openstack/common/jsonutils.py b/novaclient/openstack/common/jsonutils.py index 0767e1e59..b08a679c5 100644 --- a/novaclient/openstack/common/jsonutils.py +++ b/novaclient/openstack/common/jsonutils.py @@ -31,6 +31,7 @@ ''' +import codecs import datetime import functools import inspect @@ -52,6 +53,7 @@ from novaclient.openstack.common import gettextutils from novaclient.openstack.common import importutils +from novaclient.openstack.common import strutils from novaclient.openstack.common import timeutils netaddr = importutils.try_import("netaddr") @@ -166,12 +168,12 @@ def dumps(value, default=to_primitive, **kwargs): return json.dumps(value, default=default, **kwargs) -def loads(s): - return json.loads(s) +def loads(s, encoding='utf-8'): + return json.loads(strutils.safe_decode(s, encoding)) -def load(s): - return json.load(s) +def load(fp, encoding='utf-8'): + return json.load(codecs.getreader(encoding)(fp)) try: From d05da4e985036fa354cc1f2666e39c4aa3213609 Mon Sep 17 00:00:00 2001 From: Sergio Cazzolato Date: Thu, 29 May 2014 16:48:32 -0300 Subject: [PATCH 0501/1705] Revert "Remove quota-class subcommand" This reverts commit 7c11b06bd4e896d891ed3f50c6480bd76a1023bf. The quota_class subcommand was used to set default quota values so it shouldn't have been removed. Related mailing list thread on the topic: http://lists.openstack.org/pipermail/openstack-dev/2014-May/035383.html Change-Id: I8f392f8e54bef52c1c950f75377e6bb93a41996d Partial-Bug: #1299517 --- novaclient/tests/v1_1/fakes.py | 50 +++++++++++++ novaclient/tests/v1_1/test_quota_classes.py | 42 +++++++++++ novaclient/tests/v1_1/test_shell.py | 11 +++ novaclient/tests/v3/test_quota_classes.py | 25 +++++++ novaclient/v1_1/client.py | 2 + novaclient/v1_1/quota_classes.py | 44 +++++++++++ novaclient/v1_1/shell.py | 82 +++++++++++++++++++++ novaclient/v3/client.py | 2 + novaclient/v3/quota_classes.py | 23 ++++++ novaclient/v3/shell.py | 43 +++++++++++ 10 files changed, 324 insertions(+) create mode 100644 novaclient/tests/v1_1/test_quota_classes.py create mode 100644 novaclient/tests/v3/test_quota_classes.py create mode 100644 novaclient/v1_1/quota_classes.py create mode 100644 novaclient/v3/quota_classes.py diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index 52c03ad3a..35fd44d60 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -1189,6 +1189,56 @@ def delete_os_quota_sets_test(self, **kw): def delete_os_quota_sets_97f4c221bff44578b0300df4ef119353(self, **kw): return (202, {}, {}) + # + # Quota Classes + # + + def get_os_quota_class_sets_test(self, **kw): + return (200, {}, {'quota_class_set': { + 'id': 'test', + 'metadata_items': 1, + 'injected_file_content_bytes': 1, + 'injected_file_path_bytes': 1, + 'ram': 1, + 'floating_ips': 1, + 'instances': 1, + 'injected_files': 1, + 'cores': 1, + 'key_pairs': 1, + 'security_groups': 1, + 'security_group_rules': 1}}) + + def put_os_quota_class_sets_test(self, body, **kw): + assert list(body) == ['quota_class_set'] + return (200, {}, {'quota_class_set': { + 'metadata_items': 1, + 'injected_file_content_bytes': 1, + 'injected_file_path_bytes': 1, + 'ram': 1, + 'floating_ips': 1, + 'instances': 1, + 'injected_files': 1, + 'cores': 1, + 'key_pairs': 1, + 'security_groups': 1, + 'security_group_rules': 1}}) + + def put_os_quota_class_sets_97f4c221bff44578b0300df4ef119353(self, + body, **kw): + assert list(body) == ['quota_class_set'] + return (200, {}, {'quota_class_set': { + 'metadata_items': 1, + 'injected_file_content_bytes': 1, + 'injected_file_path_bytes': 1, + 'ram': 1, + 'floating_ips': 1, + 'instances': 1, + 'injected_files': 1, + 'cores': 1, + 'key_pairs': 1, + 'security_groups': 1, + 'security_group_rules': 1}}) + # # Security Groups # diff --git a/novaclient/tests/v1_1/test_quota_classes.py b/novaclient/tests/v1_1/test_quota_classes.py new file mode 100644 index 000000000..338549bfa --- /dev/null +++ b/novaclient/tests/v1_1/test_quota_classes.py @@ -0,0 +1,42 @@ +# Copyright 2011 OpenStack Foundation +# All Rights Reserved. +# +# 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 novaclient.tests import utils +from novaclient.tests.v1_1 import fakes + + +cs = fakes.FakeClient() + + +class QuotaClassSetsTest(utils.TestCase): + + def test_class_quotas_get(self): + class_name = 'test' + cs.quota_classes.get(class_name) + cs.assert_called('GET', '/os-quota-class-sets/%s' % class_name) + + def test_update_quota(self): + q = cs.quota_classes.get('test') + q.update(cores=2) + cs.assert_called('PUT', '/os-quota-class-sets/test') + + def test_refresh_quota(self): + q = cs.quota_classes.get('test') + q2 = cs.quota_classes.get('test') + self.assertEqual(q.cores, q2.cores) + q2.cores = 0 + self.assertNotEqual(q.cores, q2.cores) + q2.get() + self.assertEqual(q.cores, q2.cores) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index 05254a7de..76c72057d 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -1595,6 +1595,17 @@ def test_user_quota_delete(self): self.assert_called('DELETE', '/os-quota-sets/97f4c221bff44578b0300df4ef119353?user_id=u1') + def test_quota_class_show(self): + self.run_command('quota-class-show test') + self.assert_called('GET', '/os-quota-class-sets/test') + + def test_quota_class_update(self): + self.run_command('quota-class-update 97f4c221bff44578b0300df4ef119353' + ' --instances=5') + self.assert_called('PUT', + '/os-quota-class-sets/97f4c221bff44578b0300' + 'df4ef119353') + def test_network_list(self): self.run_command('network-list') self.assert_called('GET', '/os-networks') diff --git a/novaclient/tests/v3/test_quota_classes.py b/novaclient/tests/v3/test_quota_classes.py new file mode 100644 index 000000000..2e0ceb9f0 --- /dev/null +++ b/novaclient/tests/v3/test_quota_classes.py @@ -0,0 +1,25 @@ +# Copyright IBM Corp. 2013 +# +# 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 novaclient.tests.v1_1 import test_quota_classes +from novaclient.tests.v3 import fakes + + +class QuotaClassSetsTest(test_quota_classes.QuotaClassSetsTest): + def setUp(self): + super(QuotaClassSetsTest, self).setUp() + self.cs = self._get_fake_client() + + def _get_fake_client(self): + return fakes.FakeClient() diff --git a/novaclient/v1_1/client.py b/novaclient/v1_1/client.py index 6b59ee642..b15155094 100644 --- a/novaclient/v1_1/client.py +++ b/novaclient/v1_1/client.py @@ -33,6 +33,7 @@ from novaclient.v1_1 import keypairs from novaclient.v1_1 import limits from novaclient.v1_1 import networks +from novaclient.v1_1 import quota_classes from novaclient.v1_1 import quotas from novaclient.v1_1 import security_group_rules from novaclient.v1_1 import security_groups @@ -115,6 +116,7 @@ def __init__(self, username, api_key, project_id, auth_url=None, self.volume_types = volume_types.VolumeTypeManager(self) self.keypairs = keypairs.KeypairManager(self) self.networks = networks.NetworkManager(self) + self.quota_classes = quota_classes.QuotaClassSetManager(self) self.quotas = quotas.QuotaSetManager(self) self.security_groups = security_groups.SecurityGroupManager(self) self.security_group_rules = \ diff --git a/novaclient/v1_1/quota_classes.py b/novaclient/v1_1/quota_classes.py new file mode 100644 index 000000000..4a38a970c --- /dev/null +++ b/novaclient/v1_1/quota_classes.py @@ -0,0 +1,44 @@ +# Copyright 2012 OpenStack Foundation +# All Rights Reserved. +# +# 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 novaclient import base + + +class QuotaClassSet(base.Resource): + + def update(self, *args, **kwargs): + return self.manager.update(self.id, *args, **kwargs) + + +class QuotaClassSetManager(base.Manager): + resource_class = QuotaClassSet + + def get(self, class_name): + return self._get("/os-quota-class-sets/%s" % (class_name), + "quota_class_set") + + def _update_body(self, **kwargs): + return {'quota_class_set': kwargs} + + def update(self, class_name, **kwargs): + body = self._update_body(**kwargs) + + for key in list(body['quota_class_set']): + if body['quota_class_set'][key] is None: + body['quota_class_set'].pop(key) + + return self._update('/os-quota-class-sets/%s' % (class_name), + body, + 'quota_class_set') diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 6f987b8cf..3429164b5 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -3341,6 +3341,88 @@ def do_quota_delete(cs, args): cs.quotas.delete(args.tenant, user_id=args.user) +@utils.arg('class_name', + metavar='', + help=_('Name of quota class to list the quotas for.')) +def do_quota_class_show(cs, args): + """List the quotas for a quota class.""" + + _quota_show(cs.quota_classes.get(args.class_name)) + + +@utils.arg('class_name', + metavar='', + help=_('Name of quota class to set the quotas for.')) +@utils.arg('--instances', + metavar='', + type=int, default=None, + help=_('New value for the "instances" quota.')) +@utils.arg('--cores', + metavar='', + type=int, default=None, + help=_('New value for the "cores" quota.')) +@utils.arg('--ram', + metavar='', + type=int, default=None, + help=_('New value for the "ram" quota.')) +@utils.arg('--floating-ips', + metavar='', + type=int, + default=None, + help=_('New value for the "floating-ips" quota.')) +@utils.arg('--floating_ips', + type=int, + help=argparse.SUPPRESS) +@utils.arg('--metadata-items', + metavar='', + type=int, + default=None, + help=_('New value for the "metadata-items" quota.')) +@utils.arg('--metadata_items', + type=int, + help=argparse.SUPPRESS) +@utils.arg('--injected-files', + metavar='', + type=int, + default=None, + help=_('New value for the "injected-files" quota.')) +@utils.arg('--injected_files', + type=int, + help=argparse.SUPPRESS) +@utils.arg('--injected-file-content-bytes', + metavar='', + type=int, + default=None, + help=_('New value for the "injected-file-content-bytes" quota.')) +@utils.arg('--injected_file_content_bytes', + type=int, + help=argparse.SUPPRESS) +@utils.arg('--injected-file-path-bytes', + metavar='', + type=int, + default=None, + help=_('New value for the "injected-file-path-bytes" quota.')) +@utils.arg('--key-pairs', + metavar='', + type=int, + default=None, + help=_('New value for the "key-pairs" quota.')) +@utils.arg('--security-groups', + metavar='', + type=int, + default=None, + help=_('New value for the "security-groups" quota.')) +@utils.arg('--security-group-rules', + metavar='', + type=int, + default=None, + help=_('New value for the "security-group-rules" quota.')) +def do_quota_class_update(cs, args): + """Update the quotas for a quota class.""" + + _quota_update(cs.quota_classes, args.class_name, args) + + @utils.arg('server', metavar='', help=_('Name or ID of server.')) @utils.arg('host', metavar='', help=_('Name or ID of target host.')) @utils.arg('--password', diff --git a/novaclient/v3/client.py b/novaclient/v3/client.py index 7af2e1f2b..8d265de5a 100644 --- a/novaclient/v3/client.py +++ b/novaclient/v3/client.py @@ -25,6 +25,7 @@ from novaclient.v3 import images from novaclient.v3 import keypairs from novaclient.v3 import list_extensions +from novaclient.v3 import quota_classes from novaclient.v3 import quotas from novaclient.v3 import servers from novaclient.v3 import services @@ -93,6 +94,7 @@ def __init__(self, username, password, project_id, auth_url=None, self.images = images.ImageManager(self) self.keypairs = keypairs.KeypairManager(self) self.quotas = quotas.QuotaSetManager(self) + self.quota_classes = quota_classes.QuotaClassSetManager(self) self.servers = servers.ServerManager(self) self.services = services.ServiceManager(self) self.usage = usage.UsageManager(self) diff --git a/novaclient/v3/quota_classes.py b/novaclient/v3/quota_classes.py new file mode 100644 index 000000000..e12209eb1 --- /dev/null +++ b/novaclient/v3/quota_classes.py @@ -0,0 +1,23 @@ +# Copyright IBM Corp. 2013 +# +# 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 novaclient.v1_1 import quota_classes + + +class QuotaClassSet(quota_classes.QuotaClassSet): + pass + + +class QuotaClassSetManager(quota_classes.QuotaClassSetManager): + resource_class = QuotaClassSet diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 9ad52d16b..ea1feff86 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -2828,6 +2828,49 @@ def do_quota_delete(cs, args): cs.quotas.delete(args.tenant) +@utils.arg('class_name', + metavar='', + help='Name of quota class to list the quotas for.') +def do_quota_class_show(cs, args): + """List the quotas for a quota class.""" + + _quota_show(cs.quota_classes.get(args.class_name)) + + +@utils.arg('class_name', + metavar='', + help='Name of quota class to set the quotas for.') +@utils.arg('--instances', + metavar='', + type=int, default=None, + help='New value for the "instances" quota.') +@utils.arg('--cores', + metavar='', + type=int, default=None, + help='New value for the "cores" quota.') +@utils.arg('--ram', + metavar='', + type=int, default=None, + help='New value for the "ram" quota.') +@utils.arg('--metadata-items', + metavar='', + type=int, + default=None, + help='New value for the "metadata-items" quota.') +@utils.arg('--metadata_items', + type=int, + help=argparse.SUPPRESS) +@utils.arg('--key-pairs', + metavar='', + type=int, + default=None, + help='New value for the "key-pairs" quota.') +def do_quota_class_update(cs, args): + """Update the quotas for a quota class.""" + + _quota_update(cs.quota_classes, args.class_name, args) + + @utils.arg('server', metavar='', help='Name or ID of server.') @utils.arg('host', metavar='', help='Name or ID of target host.') @utils.arg('--password', From 32a5084b3db882ebc78e4846e12a8e715040c81b Mon Sep 17 00:00:00 2001 From: Veronica Musso Date: Thu, 29 May 2014 16:42:03 -0300 Subject: [PATCH 0502/1705] Change help for --poll option in Nova commands This patch clarifies the help about --poll option in these nova commands: image-create, boot, migrate, resize, rebuild and reboot Change-Id: Ia877fe5d07f2e9e60104f117c4a6032b6c419282 Closes-bug: #1312747 --- novaclient/v1_1/shell.py | 13 +++++++------ novaclient/v3/shell.py | 13 +++++++------ 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 6f987b8cf..6e3137ccf 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -444,7 +444,7 @@ def _boot(cs, args): dest='poll', action="store_true", default=False, - help=_('Blocks while server builds so progress can be reported.')) + help=_('Report the new server boot progress until it completes.')) def do_boot(cs, args): """Boot a new server.""" boot_args, boot_kwargs = _boot(cs, args) @@ -1194,7 +1194,7 @@ def do_list(cs, args): dest='poll', action="store_true", default=False, - help=_('Blocks while server is rebooting.')) + help=_('Poll until reboot is complete.')) def do_reboot(cs, args): """Reboot a server.""" server = _find_server(cs, args.server) @@ -1218,7 +1218,7 @@ def do_reboot(cs, args): dest='poll', action="store_true", default=False, - help=_('Blocks while server rebuilds so progress can be reported.')) + help=_('Report the server rebuild progress until it completes.')) @utils.arg('--minimal', dest='minimal', action="store_true", @@ -1261,7 +1261,7 @@ def do_rename(cs, args): dest='poll', action="store_true", default=False, - help=_('Blocks while servers resizes so progress can be reported.')) + help=_('Report the server resize progress until it completes.')) def do_resize(cs, args): """Resize a server.""" server = _find_server(cs, args.server) @@ -1290,7 +1290,7 @@ def do_resize_revert(cs, args): dest='poll', action="store_true", default=False, - help=_('Blocks while server migrates so progress can be reported.')) + help=_('Report the server migration progress until it completes.')) def do_migrate(cs, args): """Migrate a server. The new host will be selected by the scheduler.""" server = _find_server(cs, args.server) @@ -1422,7 +1422,8 @@ def do_root_password(cs, args): dest='poll', action="store_true", default=False, - help=_('Blocks while server snapshots so progress can be reported.')) + help=_('Report the snapshot progress and poll until image creation is ' + 'complete.')) def do_image_create(cs, args): """Create a new image by taking a snapshot of a running server.""" server = _find_server(cs, args.server) diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 9ad52d16b..ef2bf777e 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -320,7 +320,7 @@ def _boot(cs, args): dest='poll', action="store_true", default=False, - help='Blocks while server builds so progress can be reported.') + help='Report the new server boot progress until it completes.') def do_boot(cs, args): """Boot a new server.""" boot_args, boot_kwargs = _boot(cs, args) @@ -1046,7 +1046,7 @@ def do_list(cs, args): dest='poll', action="store_true", default=False, - help='Blocks while server is rebooting.') + help='Poll until reboot is complete.') def do_reboot(cs, args): """Reboot a server.""" server = _find_server(cs, args.server) @@ -1070,7 +1070,7 @@ def do_reboot(cs, args): dest='poll', action="store_true", default=False, - help='Blocks while server rebuilds so progress can be reported.') + help='Report the server rebuild progress until it completes.') @utils.arg('--minimal', dest='minimal', action="store_true", @@ -1108,7 +1108,7 @@ def do_rename(cs, args): dest='poll', action="store_true", default=False, - help='Blocks while server resizes so progress can be reported.') + help='Report the server resize progress until it completes.') def do_resize(cs, args): """Resize a server.""" server = _find_server(cs, args.server) @@ -1137,7 +1137,7 @@ def do_resize_revert(cs, args): dest='poll', action="store_true", default=False, - help='Blocks while server migrates so progress can be reported.') + help='Report the server migration progress until it completes.') def do_migrate(cs, args): """Migrate a server. The new host will be selected by the scheduler.""" server = _find_server(cs, args.server) @@ -1241,7 +1241,8 @@ def do_root_password(cs, args): dest='poll', action="store_true", default=False, - help='Blocks while server snapshots so progress can be reported.') + help='Report the snapshot progress and poll until image creation is ' + 'complete.') def do_image_create(cs, args): """Create a new image by taking a snapshot of a running server.""" server = _find_server(cs, args.server) From cbbfc6904e221d05827bdd08af6f013c7d539b65 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Sat, 31 May 2014 21:40:59 -0700 Subject: [PATCH 0503/1705] Removed now unnecesary workaround for PyPy Change-Id: Id23fdf62352a1575653d2277f7b87d371b0480c8 --- tox.ini | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tox.ini b/tox.ini index a8d9956d4..b85bd2938 100644 --- a/tox.ini +++ b/tox.ini @@ -12,11 +12,6 @@ deps = -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt commands = python setup.py testr --testr-args='{posargs}' -[testenv:pypy] -deps = setuptools<3.2 - -r{toxinidir}/requirements.txt - -r{toxinidir}/test-requirements.txt - [testenv:pep8] commands = flake8 From abd4e8986bca481a562096da61f2b243f4d16718 Mon Sep 17 00:00:00 2001 From: zhangjl Date: Wed, 28 May 2014 19:24:35 +0800 Subject: [PATCH 0504/1705] Logical error in flavors unset_keys method When we want to unset multi keys of flavor extra data, only first key can be removed because of the key word named 'return'. To fix it, remove the return key word Change-Id: I70d1f2e84a2bfcea1db60b26138f84421f9a7309 Closes-bug: #1324077 --- novaclient/tests/v1_1/test_flavors.py | 13 ++++++++++--- novaclient/tests/v3/test_flavors.py | 13 ++++++++++--- novaclient/v1_1/flavors.py | 6 +++--- novaclient/v3/flavors.py | 6 +++--- 4 files changed, 26 insertions(+), 12 deletions(-) diff --git a/novaclient/tests/v1_1/test_flavors.py b/novaclient/tests/v1_1/test_flavors.py index c18305401..8028cf0ee 100644 --- a/novaclient/tests/v1_1/test_flavors.py +++ b/novaclient/tests/v1_1/test_flavors.py @@ -13,6 +13,8 @@ # License for the specific language governing permissions and limitations # under the License. +import mock + from novaclient import exceptions from novaclient.tests import utils from novaclient.tests.v1_1 import fakes @@ -206,7 +208,12 @@ def test_set_with_invalid_keys(self): for key in invalid_keys: self.assertRaises(exceptions.CommandError, f.set_keys, {key: 'v1'}) - def test_unset_keys(self): + @mock.patch.object(flavors.FlavorManager, '_delete') + def test_unset_keys(self, mock_delete): f = self.cs.flavors.get(1) - f.unset_keys(['k1']) - self.cs.assert_called('DELETE', '/flavors/1/os-extra_specs/k1') + keys = ['k1', 'k2'] + f.unset_keys(keys) + mock_delete.assert_has_calls([ + mock.call("/flavors/1/os-extra_specs/k1"), + mock.call("/flavors/1/os-extra_specs/k2") + ]) diff --git a/novaclient/tests/v3/test_flavors.py b/novaclient/tests/v3/test_flavors.py index ea76988a0..cf3e86513 100644 --- a/novaclient/tests/v3/test_flavors.py +++ b/novaclient/tests/v3/test_flavors.py @@ -13,6 +13,8 @@ # License for the specific language governing permissions and limitations # under the License. +import mock + from novaclient.tests.v1_1 import test_flavors from novaclient.tests.v3 import fakes from novaclient.v3 import flavors @@ -57,10 +59,15 @@ def test_set_with_valid_keys(self): self.cs.assert_called('POST', '/flavors/4/flavor-extra-specs', {"extra_specs": {key: 'v4'}}) - def test_unset_keys(self): + @mock.patch.object(flavors.FlavorManager, '_delete') + def test_unset_keys(self, mock_delete): f = self.cs.flavors.get(1) - f.unset_keys(['k1']) - self.cs.assert_called('DELETE', '/flavors/1/flavor-extra-specs/k1') + keys = ['k1', 'k2'] + f.unset_keys(keys) + mock_delete.assert_has_calls([ + mock.call("/flavors/1/flavor-extra-specs/k1"), + mock.call("/flavors/1/flavor-extra-specs/k2") + ]) def test_get_flavor_details_diablo(self): # Don't need for V3 API to work against diablo diff --git a/novaclient/v1_1/flavors.py b/novaclient/v1_1/flavors.py index c47c47633..30536fc68 100644 --- a/novaclient/v1_1/flavors.py +++ b/novaclient/v1_1/flavors.py @@ -83,9 +83,9 @@ def unset_keys(self, keys): :param keys: A list of keys to be unset """ for k in keys: - return self.manager._delete( - "/flavors/%s/os-extra_specs/%s" % ( - base.getid(self), k)) + self.manager._delete( + "/flavors/%s/os-extra_specs/%s" % ( + base.getid(self), k)) def delete(self): """ diff --git a/novaclient/v3/flavors.py b/novaclient/v3/flavors.py index 38f288504..60553b2f5 100644 --- a/novaclient/v3/flavors.py +++ b/novaclient/v3/flavors.py @@ -72,9 +72,9 @@ def unset_keys(self, keys): :param keys: A list of keys to be unset """ for k in keys: - return self.manager._delete( - "/flavors/%s/flavor-extra-specs/%s" % ( - base.getid(self), k)) + self.manager._delete( + "/flavors/%s/flavor-extra-specs/%s" % ( + base.getid(self), k)) def delete(self): """ From 85faf9cbf0de6103c4d4dd7839ea6588eb2c0645 Mon Sep 17 00:00:00 2001 From: Rick Harris Date: Tue, 3 Jun 2014 15:33:15 -0500 Subject: [PATCH 0505/1705] Add NOVACLIENT_BYPASS_URL env variable Currently, in order to use a bypass URL, you need to specify it using the `--bypass-url` command-line option each time, which is inconvenient if you plan on always using a bypass URL. This patch adds a `NOVACLIENT_BYPASS_URL` environment variable which can be placed in the `novarc` to persistently set this option. Change-Id: I49e67ee1fc3570a43e49dc1d78d8ca0b26945bc8 Closes-Bug: 1326134 --- novaclient/shell.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/novaclient/shell.py b/novaclient/shell.py index 770f5bb48..7a1413ea1 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -390,7 +390,9 @@ def get_base_parser(self): parser.add_argument('--bypass-url', metavar='', dest='bypass_url', - help="Use this API endpoint instead of the Service Catalog") + default=utils.env('NOVACLIENT_BYPASS_URL'), + help="Use this API endpoint instead of the Service Catalog. " + "Defaults to env[NOVACLIENT_BYPASS_URL]") parser.add_argument('--bypass_url', help=argparse.SUPPRESS) From 54040875e036679c330548c46946f31a248ae6f4 Mon Sep 17 00:00:00 2001 From: liyingjun Date: Wed, 4 Jun 2014 07:26:37 +0800 Subject: [PATCH 0506/1705] Change help message for volume-update `nova volume-update ` to `nova volume-update ` Change-Id: Ie820843c3e8d23659075c1d6b4fdd47f639b3b58 Closes-bug: 1326613 --- novaclient/v1_1/shell.py | 2 +- novaclient/v3/shell.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 3429164b5..b28814977 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1769,7 +1769,7 @@ def do_volume_attach(cs, args): metavar='', help=_('Name or ID of server.')) @utils.arg('attachment_id', - metavar='', + metavar='', help=_('Attachment ID of the volume.')) @utils.arg('new_volume', metavar='', diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index ea1feff86..dbdb24815 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -1445,7 +1445,7 @@ def do_volume_attach(cs, args): metavar='', help='Name or ID of server.') @utils.arg('attachment_id', - metavar='', + metavar='', help='Attachment ID of the volume.') @utils.arg('new_volume', metavar='', From 1490001c3ee9b2c935c5f81df97d46ae585c4dfc Mon Sep 17 00:00:00 2001 From: Rui Zang Date: Wed, 4 Jun 2014 22:30:33 +0800 Subject: [PATCH 0507/1705] Add swap measurement unit (MB) to CLI output The measurement unit of the swap memory in flavor is MB. If not clearly labeled, some may confuse it with other disk's units which are GB. Closes-Bug: #1323541 Change-Id: I29b4c8e14bcdb3505d5c0bae8bcf653c96b27413 --- novaclient/v1_1/shell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 3429164b5..2ede1a6da 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -577,7 +577,7 @@ def _print_flavor_list(flavors, show_extra_specs=False): 'Memory_MB', 'Disk', 'Ephemeral', - 'Swap', + 'Swap_MB', 'VCPUs', 'RXTX_Factor', 'Is_Public', From 8bef1e11107076660c3ea204d1419eb755b56b70 Mon Sep 17 00:00:00 2001 From: Christian Berendt Date: Wed, 4 Jun 2014 20:11:17 +0200 Subject: [PATCH 0508/1705] Overwrite HelpFormatter constructur to extend argument column Overwrite the HelpFormatter constructor in the class OpenStackHelpFormatter to modify the default value of the max_help_position to extend the width of the argument column in the help output. example output before this patch: volume-snapshot-create Add a new snapshot. volume-snapshot-delete Remove a snapshot. volume-snapshot-list List all the snapshots. volume-snapshot-show Show details about a snapshot. example output after this patch: volume-snapshot-create Add a new snapshot. volume-snapshot-delete Remove a snapshot. volume-snapshot-list List all the snapshots. volume-snapshot-show Show details about a snapshot. Change-Id: I517098748947e7bded03160a15e778bf79475d0d Closes-Bug: #1326471 --- novaclient/shell.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/novaclient/shell.py b/novaclient/shell.py index 770f5bb48..309bcc38f 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -777,6 +777,11 @@ def do_help(self, args): # I'm picky about my shell help. class OpenStackHelpFormatter(argparse.HelpFormatter): + def __init__(self, prog, indent_increment=2, max_help_position=32, + width=None): + super(OpenStackHelpFormatter, self).__init__(prog, indent_increment, + max_help_position, width) + def start_section(self, heading): # Title-case the headings heading = '%s%s' % (heading[0].upper(), heading[1:]) From 782a2595f8224ed96fd582a8f49f9a0cf29a6fbc Mon Sep 17 00:00:00 2001 From: Jamie Lennox Date: Fri, 9 May 2014 15:34:26 +1000 Subject: [PATCH 0509/1705] Convert Floating IP tests to httpretty Change-Id: I0f6816868580af32ee93c0bfdf2c113c6c6ecd13 blueprint: httpretty-testing --- novaclient/tests/fixture_data/floatingips.py | 52 ++++++++++++++++++++ novaclient/tests/v1_1/test_floating_ips.py | 37 +++++++------- 2 files changed, 71 insertions(+), 18 deletions(-) create mode 100644 novaclient/tests/fixture_data/floatingips.py diff --git a/novaclient/tests/fixture_data/floatingips.py b/novaclient/tests/fixture_data/floatingips.py new file mode 100644 index 000000000..66c9958cb --- /dev/null +++ b/novaclient/tests/fixture_data/floatingips.py @@ -0,0 +1,52 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import httpretty + +from novaclient.openstack.common import jsonutils +from novaclient.tests.fixture_data import base + + +class FloatingFixture(base.Fixture): + + base_url = 'os-floating-ips' + + def setUp(self): + super(FloatingFixture, self).setUp() + + floating_ips = [{'id': 1, 'fixed_ip': '10.0.0.1', 'ip': '11.0.0.1'}, + {'id': 2, 'fixed_ip': '10.0.0.2', 'ip': '11.0.0.2'}] + + get_os_floating_ips = {'floating_ips': floating_ips} + httpretty.register_uri(httpretty.GET, self.url(), + body=jsonutils.dumps(get_os_floating_ips), + content_type='application/json') + + for ip in floating_ips: + get_os_floating_ip = {'floating_ip': ip} + httpretty.register_uri(httpretty.GET, self.url(ip['id']), + body=jsonutils.dumps(get_os_floating_ip), + content_type='application/json') + + httpretty.register_uri(httpretty.DELETE, self.url(ip['id']), + content_type='application/json', + status=204) + + def post_os_floating_ips(request, url, headers): + body = jsonutils.loads(request.body.decode('utf-8')) + ip = floating_ips[0].copy() + ip['pool'] = body.get('pool') + ip = jsonutils.dumps({'floating_ip': ip}) + return 200, headers, ip + httpretty.register_uri(httpretty.POST, self.url(), + body=post_os_floating_ips, + content_type='application/json') diff --git a/novaclient/tests/v1_1/test_floating_ips.py b/novaclient/tests/v1_1/test_floating_ips.py index 5a9147fa0..d14f8a710 100644 --- a/novaclient/tests/v1_1/test_floating_ips.py +++ b/novaclient/tests/v1_1/test_floating_ips.py @@ -14,45 +14,46 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient.tests.fixture_data import client +from novaclient.tests.fixture_data import floatingips as data from novaclient.tests import utils -from novaclient.tests.v1_1 import fakes from novaclient.v1_1 import floating_ips -cs = fakes.FakeClient() +class FloatingIPsTest(utils.FixturedTestCase): - -class FloatingIPsTest(utils.TestCase): + client_fixture_class = client.V1 + data_fixture_class = data.FloatingFixture def test_list_floating_ips(self): - fips = cs.floating_ips.list() - cs.assert_called('GET', '/os-floating-ips') + fips = self.cs.floating_ips.list() + self.assert_called('GET', '/os-floating-ips') for fip in fips: self.assertIsInstance(fip, floating_ips.FloatingIP) def test_list_floating_ips_all_tenants(self): - fips = cs.floating_ips.list(all_tenants=True) - cs.assert_called('GET', '/os-floating-ips?all_tenants=1') + fips = self.cs.floating_ips.list(all_tenants=True) + self.assert_called('GET', '/os-floating-ips?all_tenants=1') for fip in fips: self.assertIsInstance(fip, floating_ips.FloatingIP) def test_delete_floating_ip(self): - fl = cs.floating_ips.list()[0] + fl = self.cs.floating_ips.list()[0] fl.delete() - cs.assert_called('DELETE', '/os-floating-ips/1') - cs.floating_ips.delete(1) - cs.assert_called('DELETE', '/os-floating-ips/1') - cs.floating_ips.delete(fl) - cs.assert_called('DELETE', '/os-floating-ips/1') + self.assert_called('DELETE', '/os-floating-ips/1') + self.cs.floating_ips.delete(1) + self.assert_called('DELETE', '/os-floating-ips/1') + self.cs.floating_ips.delete(fl) + self.assert_called('DELETE', '/os-floating-ips/1') def test_create_floating_ip(self): - fl = cs.floating_ips.create() - cs.assert_called('POST', '/os-floating-ips') + fl = self.cs.floating_ips.create() + self.assert_called('POST', '/os-floating-ips') self.assertIsNone(fl.pool) self.assertIsInstance(fl, floating_ips.FloatingIP) def test_create_floating_ip_with_pool(self): - fl = cs.floating_ips.create('foo') - cs.assert_called('POST', '/os-floating-ips') + fl = self.cs.floating_ips.create('nova') + self.assert_called('POST', '/os-floating-ips') self.assertEqual(fl.pool, 'nova') self.assertIsInstance(fl, floating_ips.FloatingIP) From 851c7a2d97567d4cc2d6f2863dcbcf8d26d67eb9 Mon Sep 17 00:00:00 2001 From: Jamie Lennox Date: Fri, 9 May 2014 16:12:47 +1000 Subject: [PATCH 0510/1705] Convert Floating IPs DNS tests to httpretty Change-Id: I19dfadbe9103145a6b80d8ce10d2b6d3ce326ffa blueprint: httpretty-testing --- novaclient/tests/fixture_data/floatingips.py | 97 +++++++++++++++++++ novaclient/tests/v1_1/test_floating_ip_dns.py | 59 +++++------ 2 files changed, 128 insertions(+), 28 deletions(-) diff --git a/novaclient/tests/fixture_data/floatingips.py b/novaclient/tests/fixture_data/floatingips.py index 66c9958cb..20acde705 100644 --- a/novaclient/tests/fixture_data/floatingips.py +++ b/novaclient/tests/fixture_data/floatingips.py @@ -13,6 +13,7 @@ import httpretty from novaclient.openstack.common import jsonutils +from novaclient.tests import fakes from novaclient.tests.fixture_data import base @@ -50,3 +51,99 @@ def post_os_floating_ips(request, url, headers): httpretty.register_uri(httpretty.POST, self.url(), body=post_os_floating_ips, content_type='application/json') + + +class DNSFixture(base.Fixture): + + base_url = 'os-floating-ip-dns' + + def setUp(self): + super(DNSFixture, self).setUp() + + get_os_floating_ip_dns = { + 'domain_entries': [ + {'domain': 'example.org'}, + {'domain': 'example.com'} + ] + } + httpretty.register_uri(httpretty.GET, self.url(), + body=jsonutils.dumps(get_os_floating_ip_dns), + content_type='application/json', + status=205) + + get_dns_testdomain_entries_testname = { + 'dns_entry': { + 'ip': "10.10.10.10", + 'name': 'testname', + 'type': "A", + 'domain': 'testdomain' + } + } + url = self.url('testdomain', 'entries', 'testname') + body = jsonutils.dumps(get_dns_testdomain_entries_testname) + httpretty.register_uri(httpretty.GET, url, + body=body, + content_type='application/json', + status=205) + + httpretty.register_uri(httpretty.DELETE, self.url('testdomain'), + status=200) + + url = self.url('testdomain', 'entries', 'testname') + httpretty.register_uri(httpretty.DELETE, url, status=200) + + def put_dns_testdomain_entries_testname(request, url, headers): + body = jsonutils.loads(request.body.decode('utf-8')) + fakes.assert_has_keys(body['dns_entry'], + required=['ip', 'dns_type']) + return 205, headers, request.body + httpretty.register_uri(httpretty.PUT, url, + body=put_dns_testdomain_entries_testname, + content_type='application/json') + + url = self.url('testdomain', 'entries') + httpretty.register_uri(httpretty.GET, url, status=404) + + get_os_floating_ip_dns_testdomain_entries = { + 'dns_entries': [ + { + 'dns_entry': { + 'ip': '1.2.3.4', + 'name': "host1", + 'type': "A", + 'domain': 'testdomain' + } + }, + { + 'dns_entry': { + 'ip': '1.2.3.4', + 'name': "host2", + 'type': "A", + 'domain': 'testdomain' + } + }, + ] + } + body = jsonutils.dumps(get_os_floating_ip_dns_testdomain_entries) + httpretty.register_uri(httpretty.GET, url + '?ip=1.2.3.4', + body=body, + status=205, + content_type='application/json') + + def put_os_floating_ip_dns_testdomain(request, url, headers): + body = jsonutils.loads(request.body.decode('utf-8')) + if body['domain_entry']['scope'] == 'private': + fakes.assert_has_keys(body['domain_entry'], + required=['availability_zone', 'scope']) + elif body['domain_entry']['scope'] == 'public': + fakes.assert_has_keys(body['domain_entry'], + required=['project', 'scope']) + else: + fakes.assert_has_keys(body['domain_entry'], + required=['project', 'scope']) + + headers['Content-Type'] = 'application/json' + return (205, headers, request.body) + + httpretty.register_uri(httpretty.PUT, self.url('testdomain'), + body=put_os_floating_ip_dns_testdomain) diff --git a/novaclient/tests/v1_1/test_floating_ip_dns.py b/novaclient/tests/v1_1/test_floating_ip_dns.py index ecaae4069..df310b269 100644 --- a/novaclient/tests/v1_1/test_floating_ip_dns.py +++ b/novaclient/tests/v1_1/test_floating_ip_dns.py @@ -11,20 +11,20 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient.tests.fixture_data import client +from novaclient.tests.fixture_data import floatingips as data from novaclient.tests import utils -from novaclient.tests.v1_1 import fakes from novaclient.v1_1 import floating_ip_dns -cs = fakes.FakeClient() - - -class FloatingIPDNSDomainTest(utils.TestCase): +class FloatingIPDNSDomainTest(utils.FixturedTestCase): testdomain = "testdomain" + client_fixture_class = client.V1 + data_fixture_class = data.DNSFixture def test_dns_domains(self): - domainlist = cs.dns_domains.domains() + domainlist = self.cs.dns_domains.domains() self.assertEqual(len(domainlist), 2) for entry in domainlist: @@ -34,30 +34,33 @@ def test_dns_domains(self): self.assertEqual(domainlist[1].domain, 'example.com') def test_create_private_domain(self): - cs.dns_domains.create_private(self.testdomain, 'test_avzone') - cs.assert_called('PUT', '/os-floating-ip-dns/%s' % - self.testdomain) + self.cs.dns_domains.create_private(self.testdomain, 'test_avzone') + self.assert_called('PUT', '/os-floating-ip-dns/%s' % + self.testdomain) def test_create_public_domain(self): - cs.dns_domains.create_public(self.testdomain, 'test_project') - cs.assert_called('PUT', '/os-floating-ip-dns/%s' % - self.testdomain) + self.cs.dns_domains.create_public(self.testdomain, 'test_project') + self.assert_called('PUT', '/os-floating-ip-dns/%s' % + self.testdomain) def test_delete_domain(self): - cs.dns_domains.delete(self.testdomain) - cs.assert_called('DELETE', '/os-floating-ip-dns/%s' % - self.testdomain) + self.cs.dns_domains.delete(self.testdomain) + self.assert_called('DELETE', '/os-floating-ip-dns/%s' % + self.testdomain) -class FloatingIPDNSEntryTest(utils.TestCase): +class FloatingIPDNSEntryTest(utils.FixturedTestCase): testname = "testname" testip = "1.2.3.4" testdomain = "testdomain" testtype = "A" + client_fixture_class = client.V1 + data_fixture_class = data.DNSFixture def test_get_dns_entries_by_ip(self): - entries = cs.dns_entries.get_for_ip(self.testdomain, ip=self.testip) + entries = self.cs.dns_entries.get_for_ip(self.testdomain, + ip=self.testip) self.assertEqual(len(entries), 2) for entry in entries: @@ -68,21 +71,21 @@ def test_get_dns_entries_by_ip(self): self.assertEqual(entries[1].dns_entry['ip'], self.testip) def test_get_dns_entry_by_name(self): - entry = cs.dns_entries.get(self.testdomain, - self.testname) + entry = self.cs.dns_entries.get(self.testdomain, + self.testname) self.assertIsInstance(entry, floating_ip_dns.FloatingIPDNSEntry) self.assertEqual(entry.name, self.testname) def test_create_entry(self): - cs.dns_entries.create(self.testdomain, - self.testname, - self.testip, - self.testtype) + self.cs.dns_entries.create(self.testdomain, + self.testname, + self.testip, + self.testtype) - cs.assert_called('PUT', '/os-floating-ip-dns/%s/entries/%s' % - (self.testdomain, self.testname)) + self.assert_called('PUT', '/os-floating-ip-dns/%s/entries/%s' % + (self.testdomain, self.testname)) def test_delete_entry(self): - cs.dns_entries.delete(self.testdomain, self.testname) - cs.assert_called('DELETE', '/os-floating-ip-dns/%s/entries/%s' % - (self.testdomain, self.testname)) + self.cs.dns_entries.delete(self.testdomain, self.testname) + self.assert_called('DELETE', '/os-floating-ip-dns/%s/entries/%s' % + (self.testdomain, self.testname)) From 858a83023a95ce8625102daa7b2c673ea5be4572 Mon Sep 17 00:00:00 2001 From: Jamie Lennox Date: Fri, 9 May 2014 16:28:17 +1000 Subject: [PATCH 0511/1705] Convert Floating IP bulk tests to httpretty Change-Id: I4b81963ab6a9924c5f2262f49a42baa56326964d blueprint: httpretty-testing --- novaclient/tests/fixture_data/floatingips.py | 49 +++++++++++++++++++ .../tests/v1_1/test_floating_ips_bulk.py | 31 ++++++------ 2 files changed, 65 insertions(+), 15 deletions(-) diff --git a/novaclient/tests/fixture_data/floatingips.py b/novaclient/tests/fixture_data/floatingips.py index 20acde705..a74b82913 100644 --- a/novaclient/tests/fixture_data/floatingips.py +++ b/novaclient/tests/fixture_data/floatingips.py @@ -147,3 +147,52 @@ def put_os_floating_ip_dns_testdomain(request, url, headers): httpretty.register_uri(httpretty.PUT, self.url('testdomain'), body=put_os_floating_ip_dns_testdomain) + + +class BulkFixture(base.Fixture): + + base_url = 'os-floating-ips-bulk' + + def setUp(self): + super(BulkFixture, self).setUp() + + get_os_floating_ips_bulk = { + 'floating_ip_info': [ + {'id': 1, 'fixed_ip': '10.0.0.1', 'ip': '11.0.0.1'}, + {'id': 2, 'fixed_ip': '10.0.0.2', 'ip': '11.0.0.2'}, + ] + } + httpretty.register_uri(httpretty.GET, self.url(), + body=jsonutils.dumps(get_os_floating_ips_bulk), + content_type='application/json') + httpretty.register_uri(httpretty.GET, self.url('testHost'), + body=jsonutils.dumps(get_os_floating_ips_bulk), + content_type='application/json') + + def put_os_floating_ips_bulk_delete(request, url, headers): + body = jsonutils.loads(request.body.decode('utf-8')) + ip_range = body.get('ip_range') + data = {'floating_ips_bulk_delete': ip_range} + return 200, headers, jsonutils.dumps(data) + + httpretty.register_uri(httpretty.PUT, self.url('delete'), + body=put_os_floating_ips_bulk_delete, + content_type='application/json') + + def post_os_floating_ips_bulk(request, url, headers): + body = jsonutils.loads(request.body.decode('utf-8')) + params = body.get('floating_ips_bulk_create') + pool = params.get('pool', 'defaultPool') + interface = params.get('interface', 'defaultInterface') + data = { + 'floating_ips_bulk_create': { + 'ip_range': '192.168.1.0/30', + 'pool': pool, + 'interface': interface + } + } + return 200, headers, jsonutils.dumps(data) + + httpretty.register_uri(httpretty.POST, self.url(), + body=post_os_floating_ips_bulk, + content_type='application/json') diff --git a/novaclient/tests/v1_1/test_floating_ips_bulk.py b/novaclient/tests/v1_1/test_floating_ips_bulk.py index 01fb83725..dd4f823c7 100644 --- a/novaclient/tests/v1_1/test_floating_ips_bulk.py +++ b/novaclient/tests/v1_1/test_floating_ips_bulk.py @@ -13,42 +13,43 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient.tests.fixture_data import client +from novaclient.tests.fixture_data import floatingips as data from novaclient.tests import utils -from novaclient.tests.v1_1 import fakes from novaclient.v1_1 import floating_ips_bulk -cs = fakes.FakeClient() +class FloatingIPsBulkTest(utils.FixturedTestCase): - -class FloatingIPsBulkTest(utils.TestCase): + client_fixture_class = client.V1 + data_fixture_class = data.BulkFixture def test_list_floating_ips_bulk(self): - fl = cs.floating_ips_bulk.list() - cs.assert_called('GET', '/os-floating-ips-bulk') + fl = self.cs.floating_ips_bulk.list() + self.assert_called('GET', '/os-floating-ips-bulk') [self.assertIsInstance(f, floating_ips_bulk.FloatingIP) for f in fl] def test_list_floating_ips_bulk_host_filter(self): - fl = cs.floating_ips_bulk.list('testHost') - cs.assert_called('GET', '/os-floating-ips-bulk/testHost') + fl = self.cs.floating_ips_bulk.list('testHost') + self.assert_called('GET', '/os-floating-ips-bulk/testHost') [self.assertIsInstance(f, floating_ips_bulk.FloatingIP) for f in fl] def test_create_floating_ips_bulk(self): - fl = cs.floating_ips_bulk.create('192.168.1.0/30') + fl = self.cs.floating_ips_bulk.create('192.168.1.0/30') body = {'floating_ips_bulk_create': {'ip_range': '192.168.1.0/30'}} - cs.assert_called('POST', '/os-floating-ips-bulk', body) + self.assert_called('POST', '/os-floating-ips-bulk', body) self.assertEqual(fl.ip_range, body['floating_ips_bulk_create']['ip_range']) def test_create_floating_ips_bulk_with_pool_and_host(self): - fl = cs.floating_ips_bulk.create('192.168.1.0/30', 'poolTest', - 'interfaceTest') + fl = self.cs.floating_ips_bulk.create('192.168.1.0/30', 'poolTest', + 'interfaceTest') body = {'floating_ips_bulk_create': {'ip_range': '192.168.1.0/30', 'pool': 'poolTest', 'interface': 'interfaceTest'}} - cs.assert_called('POST', '/os-floating-ips-bulk', body) + self.assert_called('POST', '/os-floating-ips-bulk', body) self.assertEqual(fl.ip_range, body['floating_ips_bulk_create']['ip_range']) self.assertEqual(fl.pool, @@ -57,7 +58,7 @@ def test_create_floating_ips_bulk_with_pool_and_host(self): body['floating_ips_bulk_create']['interface']) def test_delete_floating_ips_bulk(self): - fl = cs.floating_ips_bulk.delete('192.168.1.0/30') + fl = self.cs.floating_ips_bulk.delete('192.168.1.0/30') body = {'ip_range': '192.168.1.0/30'} - cs.assert_called('PUT', '/os-floating-ips-bulk/delete', body) + self.assert_called('PUT', '/os-floating-ips-bulk/delete', body) self.assertEqual(fl.floating_ips_bulk_delete, body['ip_range']) From 313a2f8a28fc38ec308ba755df61c896c12c99cb Mon Sep 17 00:00:00 2001 From: Rick Harris Date: Fri, 6 Jun 2014 15:53:25 -0500 Subject: [PATCH 0512/1705] Add way to specify key-name from environ If you're using a standard SSH keypair, it becomes tedious to have to type `--key-name` for every novaclient command. This patch introduces a new environment variable `NOVACLIENT_DEFAULT_KEY_NAME` which can be used to set this value persistently. Change-Id: I1a1180b7d474d1a4e039dbb7044cd8eaf04cc9ad Closes-Bug: 1327418 --- novaclient/v1_1/shell.py | 1 + novaclient/v3/shell.py | 1 + 2 files changed, 2 insertions(+) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 3429164b5..14cb68de9 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -356,6 +356,7 @@ def _boot(cs, args): help=_("Store arbitrary files from locally to " "on the new server. You may store up to 5 files.")) @utils.arg('--key-name', + default=os.environ.get('NOVACLIENT_DEFAULT_KEY_NAME'), metavar='', help=_("Key name of keypair that should be created earlier with \ the command keypair-add")) diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index ea1feff86..9bbbec963 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -258,6 +258,7 @@ def _boot(cs, args): help="Store arbitrary files from locally to " "on the new server. You may store up to 5 files.") @utils.arg('--key-name', + default=os.environ.get('NOVACLIENT_DEFAULT_KEY_NAME'), metavar='', help="Key name of keypair that should be created earlier with \ the command keypair-add") From 3588994cdd55fcb2cf3c702a2c2423622a1c06b0 Mon Sep 17 00:00:00 2001 From: liyingjun Date: Wed, 4 Jun 2014 11:39:21 +0800 Subject: [PATCH 0513/1705] Remove _print_volume from volume-update Since the response of volume-update do not contain a body, just a status code 202, so when calling _print_volume(volume), there will be an error: `ERROR (AttributeError): 'NoneType' object has no attribute '_info'`. Change-Id: I950ae7dca5e847beae7295d72d009f2d07d07c51 Closes-bug: 1321205 --- novaclient/v1_1/shell.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 3429164b5..60089162f 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1776,10 +1776,9 @@ def do_volume_attach(cs, args): help=_('ID of the volume to attach.')) def do_volume_update(cs, args): """Update volume attachment.""" - volume = cs.volumes.update_server_volume(_find_server(cs, args.server).id, - args.attachment_id, - args.new_volume) - _print_volume(volume) + cs.volumes.update_server_volume(_find_server(cs, args.server).id, + args.attachment_id, + args.new_volume) @utils.arg('server', From 40d814b02e3fcb5cceca57ef30bba0d505d11d8e Mon Sep 17 00:00:00 2001 From: Sean Dague Date: Mon, 9 Jun 2014 10:20:31 -0400 Subject: [PATCH 0514/1705] mask keystone token in debug output replaying the keystone token in debug output is both completely burdensome to read (given it's unbounded size), but it's also potentially a security issue given that it could be used in replay attacks. Instead, filter the headers to sha1 sensitive things, with X-Auth-Token being the only one listed so far. The sha1 will at least give us the understanding of tokens being the same or different (and an administrator could use that to figure out if they were valid later). This also removes some extra '\n' that were being injected into the debug logs, because they were not helping with readability. Lastly, actually test logging. This introduces the first tests of the logging path using the logging fixture. It's pretty basic, but does verify that requests are logged at debug, that the SHA1 format works, and that headers which are not listed get shown straight through. DocImpact because this changes the curl dumped strings which people may have been using. They can still do that as long as they generate their own keystone token. Change-Id: I1edb94785705c3b6a05f118b77d3aeb07461cd44 --- novaclient/client.py | 22 +++++++++++++++++--- novaclient/tests/test_client.py | 36 +++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index 8ac0354b5..0a1aa0075 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -20,6 +20,7 @@ OpenStack Client interface. Handles the REST calls and responses. """ +import hashlib import logging import time @@ -39,6 +40,8 @@ from novaclient import service_catalog from novaclient import utils +SENSITIVE_HEADERS = ('X-Auth-Token') + class _ClientConnectionPool(object): @@ -160,6 +163,16 @@ def get_timings(self): def reset_timings(self): self.times = [] + def safe_header(self, name, value): + if name in SENSITIVE_HEADERS: + # because in python3 byte string handling is ... ug + v = value.encode('utf-8') + h = hashlib.sha1(v) + d = h.hexdigest() + return name, "SHA1(%s)" % d + else: + return name, value + def http_log_req(self, method, url, kwargs): if not self.http_log_debug: return @@ -172,13 +185,16 @@ def http_log_req(self, method, url, kwargs): string_parts.append(" '%s'" % url) string_parts.append(' -X %s' % method) - for element in kwargs['headers']: - header = ' -H "%s: %s"' % (element, kwargs['headers'][element]) + # because dict ordering changes from 2 to 3 + keys = sorted(kwargs['headers'].keys()) + for name in keys: + value = kwargs['headers'][name] + header = ' -H "%s: %s"' % self.safe_header(name, value) string_parts.append(header) if 'data' in kwargs: string_parts.append(" -d '%s'" % (kwargs['data'])) - self._logger.debug("\nREQ: %s\n" % "".join(string_parts)) + self._logger.debug("REQ: %s" % "".join(string_parts)) def http_log_resp(self, resp): if not self.http_log_debug: diff --git a/novaclient/tests/test_client.py b/novaclient/tests/test_client.py index 96901b9d2..3568210ba 100644 --- a/novaclient/tests/test_client.py +++ b/novaclient/tests/test_client.py @@ -14,9 +14,12 @@ # under the License. +import logging import mock import requests +import fixtures + import novaclient.client import novaclient.extension import novaclient.tests.fakes as fakes @@ -320,3 +323,36 @@ def test_init_with_proper_connection_pool(self, mock_pool): cs = novaclient.client.HTTPClient("user", None, "", connection_pool=True) self.assertEqual(cs._connection_pool, fake_pool) + + def test_log_req(self): + self.logger = self.useFixture( + fixtures.FakeLogger( + format="%(message)s", + level=logging.DEBUG, + nuke_handlers=True + ) + ) + cs = novaclient.client.HTTPClient("user", None, "", + connection_pool=True) + cs.http_log_debug = True + cs.http_log_req('GET', '/foo', {'headers': {}}) + cs.http_log_req('GET', '/foo', {'headers': + {'X-Auth-Token': 'totally_bogus'} + }) + cs.http_log_req('GET', '/foo', {'headers': + {'X-Foo': 'bar', + 'X-Auth-Token': 'totally_bogus'} + }) + + output = self.logger.output.split('\n') + + self.assertIn("REQ: curl -i '/foo' -X GET", output) + self.assertIn( + "REQ: curl -i '/foo' -X GET -H " + '"X-Auth-Token: SHA1(b42162b6ffdbd7c3c37b7c95b7ba9f51dda0236d)"', + output) + self.assertIn( + "REQ: curl -i '/foo' -X GET -H " + '"X-Auth-Token: SHA1(b42162b6ffdbd7c3c37b7c95b7ba9f51dda0236d)"' + ' -H "X-Foo: bar"', + output) From 9520fbbfeab6cce5013ca1356cf35b274871757f Mon Sep 17 00:00:00 2001 From: Sean Dague Date: Mon, 9 Jun 2014 10:22:10 -0400 Subject: [PATCH 0515/1705] add tox target for python 3.4 Ubuntu 14.04 has python 3.4 so add a 3.4 target. Note that this will fail unless you have new enough virtualenv. Recommend you install the packaged version of 1.11.4 and get rid of any pip installed ones. Change-Id: Ib54dc49fca2463f1c5cd788b48d06bd85be52655 --- tox.ini | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index b85bd2938..54a05caba 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,6 @@ +# noted to use py34 you need virtualenv >= 1.11.4 [tox] -envlist = py26,py27,py33,pypy,pep8 +envlist = py26,py27,py33,py34,pypy,pep8 minversion = 1.6 skipsdist = True From 762bf69c3657d327ce694714c8a900ae36ebd768 Mon Sep 17 00:00:00 2001 From: neetu Date: Thu, 5 Jun 2014 14:39:28 -0500 Subject: [PATCH 0516/1705] Adding cornercases for set_metadata Aggregate_set_metadata leaves users confused because it currently does not check for duplicate key=value parameters for addition or a missing key for deletion. All that user sees is that "metadata has been updated" more details in bug1292572 This patch checks for above mentioned corner cases and informs users when and why there was no change made in above corner cases Adding better testcases also. Closes-Bug: #1292572 Change-Id: I820d793ac44680295df8b2a3f58a0834f7019875 --- novaclient/tests/v1_1/fakes.py | 16 +++++++++++++++ novaclient/tests/v1_1/test_shell.py | 30 ++++++++++++++++++++--------- novaclient/tests/v3/test_shell.py | 30 ++++++++++++++++++++--------- novaclient/v1_1/shell.py | 8 ++++++++ novaclient/v3/shell.py | 8 ++++++++ 5 files changed, 74 insertions(+), 18 deletions(-) diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index 35fd44d60..6b418e0c4 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -1450,15 +1450,25 @@ def get_os_aggregates(self, *kw): {'id': '2', 'name': 'test2', 'availability_zone': 'nova1'}, + {'id': '3', + 'name': 'test3', + 'metadata': {'test': "dup", "none_key": "Nine"}}, ]}) def _return_aggregate(self): r = {'aggregate': self.get_os_aggregates()[2]['aggregates'][0]} return (200, {}, r) + def _return_aggregate_3(self): + r = {'aggregate': self.get_os_aggregates()[2]['aggregates'][2]} + return (200, {}, r) + def get_os_aggregates_1(self, **kw): return self._return_aggregate() + def get_os_aggregates_3(self, **kw): + return self._return_aggregate_3() + def post_os_aggregates(self, body, **kw): return self._return_aggregate() @@ -1468,12 +1478,18 @@ def put_os_aggregates_1(self, body, **kw): def put_os_aggregates_2(self, body, **kw): return self._return_aggregate() + def put_os_aggregates_3(self, body, **kw): + return self._return_aggregate_3() + def post_os_aggregates_1_action(self, body, **kw): return self._return_aggregate() def post_os_aggregates_2_action(self, body, **kw): return self._return_aggregate() + def post_os_aggregates_3_action(self, body, **kw): + return self._return_aggregate_3() + def delete_os_aggregates_1(self, **kw): return (202, {}, None) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index 38c721552..67600350b 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -1256,17 +1256,29 @@ def test_aggregate_update_with_availability_zone_by_name(self): self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) self.assert_called('GET', '/os-aggregates/1', pos=-1) - def test_aggregate_set_metadata_by_id(self): - self.run_command('aggregate-set-metadata 1 foo=bar delete_key') - body = {"set_metadata": {"metadata": {"foo": "bar", - "delete_key": None}}} - self.assert_called('POST', '/os-aggregates/1/action', body, pos=-2) - self.assert_called('GET', '/os-aggregates/1', pos=-1) + def test_aggregate_set_metadata_add_by_id(self): + self.run_command('aggregate-set-metadata 3 foo=bar') + body = {"set_metadata": {"metadata": {"foo": "bar"}}} + self.assert_called('POST', '/os-aggregates/3/action', body, pos=-2) + self.assert_called('GET', '/os-aggregates/3', pos=-1) + + def test_aggregate_set_metadata_add_duplicate_by_id(self): + cmd = 'aggregate-set-metadata 3 test=dup' + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + def test_aggregate_set_metadata_delete_by_id(self): + self.run_command('aggregate-set-metadata 3 none_key') + body = {"set_metadata": {"metadata": {"none_key": None}}} + self.assert_called('POST', '/os-aggregates/3/action', body, pos=-2) + self.assert_called('GET', '/os-aggregates/3', pos=-1) + + def test_aggregate_set_metadata_delete_missing_by_id(self): + cmd = 'aggregate-set-metadata 3 delete_key2' + self.assertRaises(exceptions.CommandError, self.run_command, cmd) def test_aggregate_set_metadata_by_name(self): - self.run_command('aggregate-set-metadata test foo=bar delete_key') - body = {"set_metadata": {"metadata": {"foo": "bar", - "delete_key": None}}} + self.run_command('aggregate-set-metadata test foo=bar') + body = {"set_metadata": {"metadata": {"foo": "bar"}}} self.assert_called('POST', '/os-aggregates/1/action', body, pos=-2) self.assert_called('GET', '/os-aggregates/1', pos=-1) diff --git a/novaclient/tests/v3/test_shell.py b/novaclient/tests/v3/test_shell.py index fbed7e11a..2d71fc6f9 100644 --- a/novaclient/tests/v3/test_shell.py +++ b/novaclient/tests/v3/test_shell.py @@ -127,17 +127,29 @@ def test_aggregate_update_with_availability_zone_by_name(self): self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) self.assert_called('GET', '/os-aggregates/1', pos=-1) - def test_aggregate_set_metadata_by_id(self): - self.run_command('aggregate-set-metadata 1 foo=bar delete_key') - body = {"set_metadata": {"metadata": {"foo": "bar", - "delete_key": None}}} - self.assert_called('POST', '/os-aggregates/1/action', body, pos=-2) - self.assert_called('GET', '/os-aggregates/1', pos=-1) + def test_aggregate_set_metadata_add_by_id(self): + self.run_command('aggregate-set-metadata 3 foo=bar') + body = {"set_metadata": {"metadata": {"foo": "bar"}}} + self.assert_called('POST', '/os-aggregates/3/action', body, pos=-2) + self.assert_called('GET', '/os-aggregates/3', pos=-1) + + def test_aggregate_set_metadata_add_duplicate_by_id(self): + cmd = 'aggregate-set-metadata 3 test=dup' + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + def test_aggregate_set_metadata_delete_by_id(self): + self.run_command('aggregate-set-metadata 3 none_key') + body = {"set_metadata": {"metadata": {"none_key": None}}} + self.assert_called('POST', '/os-aggregates/3/action', body, pos=-2) + self.assert_called('GET', '/os-aggregates/3', pos=-1) + + def test_aggregate_set_metadata_delete_missing_by_id(self): + cmd = 'aggregate-set-metadata 3 delete_key2' + self.assertRaises(exceptions.CommandError, self.run_command, cmd) def test_aggregate_set_metadata_by_name(self): - self.run_command('aggregate-set-metadata test foo=bar delete_key') - body = {"set_metadata": {"metadata": {"foo": "bar", - "delete_key": None}}} + self.run_command('aggregate-set-metadata test foo=bar') + body = {"set_metadata": {"metadata": {"foo": "bar"}}} self.assert_called('POST', '/os-aggregates/1/action', body, pos=-2) self.assert_called('GET', '/os-aggregates/1', pos=-1) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 3429164b5..a7e2c5ab8 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2777,6 +2777,14 @@ def do_aggregate_set_metadata(cs, args): """Update the metadata associated with the aggregate.""" aggregate = _find_aggregate(cs, args.aggregate) metadata = _extract_metadata(args) + currentmetadata = getattr(aggregate, 'metadata', {}) + if set(metadata.items()) & set(currentmetadata.items()): + raise exceptions.CommandError(_("metadata already exists")) + for key, value in metadata.items(): + if value is None and key not in currentmetadata: + raise exceptions.CommandError(_("metadata key %s does not exist" + " hence can not be deleted") + % key) aggregate = cs.aggregates.set_metadata(aggregate.id, metadata) print(_("Metadata has been successfully updated for aggregate %s.") % aggregate.id) diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index ea1feff86..0f4de0087 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -2290,6 +2290,14 @@ def do_aggregate_set_metadata(cs, args): """Update the metadata associated with the aggregate.""" aggregate = _find_aggregate(cs, args.aggregate) metadata = _extract_metadata(args) + currentmetadata = getattr(aggregate, 'metadata', {}) + if set(metadata.items()) & set(currentmetadata.items()): + raise exceptions.CommandError("metadata already exists") + for key, value in metadata.items(): + if value is None and key not in currentmetadata: + raise exceptions.CommandError("metadata key %s does not exist" + " hence can not be deleted" + % key) aggregate = cs.aggregates.set_metadata(aggregate.id, metadata) print("Metadata has been successfully updated for aggregate %s." % aggregate.id) From 376fd9f5bb825e2ce01fc9141c7477f25ac10101 Mon Sep 17 00:00:00 2001 From: wingwj Date: Thu, 29 May 2014 15:31:27 +0800 Subject: [PATCH 0517/1705] 'policy' should be required in server_group_create The 'policies' param is a required param in nova api now. So, we also need to change it in cli. After the modification, the 'policy' param will be required now, and the old '--policy' style will still be available. Change-Id: I97921eef0f635c8d50a04bbdaf3ef4f1bfaa3e9d Closes-Bug: #1323909 --- novaclient/tests/v1_1/test_shell.py | 7 +++++++ novaclient/v1_1/shell.py | 28 ++++++++++++++++++++++++---- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index 38c721552..5d1625090 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -2027,6 +2027,13 @@ def test_keypair_delete(self): self.run_command('keypair-delete test') self.assert_called('DELETE', '/os-keypairs/test') + def test_create_server_group(self): + self.run_command('server-group-create wjsg affinity') + self.assert_called('POST', '/os-server-groups', + {'server_group': + {'name': 'wjsg', + 'policies': ['affinity']}}) + def test_delete_multi_server_groups(self): self.run_command('server-group-delete 12345 56789') self.assert_called('DELETE', '/os-server-groups/56789') diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index a4f5c4042..a9e65e274 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -3572,13 +3572,33 @@ def do_server_group_list(cs, args): @utils.arg('name', metavar='', help='Server group name.') -@utils.arg('--policy', metavar='', action='append', - dest='policies', default=[], type=str, - help='Policies for the server groups') +# NOTE(wingwj): The '--policy' way is still reserved here for preserving +# the backwards compatibility of CLI, even if a user won't get this usage +# in '--help' description. It will be deprecated after an suitable deprecation +# period(probably 2 coordinated releases or so). +# +# Moreover, we imagine that a given user will use only positional parameters or +# only the "--policy" option. So we don't need to properly handle +# the possibility that they might mix them here. That usage is unsupported. +# The related discussion can be found in +# https://review.openstack.org/#/c/96382/2/. +@utils.arg('policy', + metavar='', + default=argparse.SUPPRESS, + nargs='*', + help='Policies for the server groups ' + '("affinity" or "anti-affinity")') +@utils.arg('--policy', + default=[], + action='append', + help=argparse.SUPPRESS) def do_server_group_create(cs, args): """Create a new server group with the specified details.""" + if not args.policy: + raise exceptions.CommandError(_("at least one policy must be " + "specified")) kwargs = {'name': args.name, - 'policies': args.policies} + 'policies': args.policy} server_group = cs.server_groups.create(**kwargs) _print_server_group_details([server_group]) From ef91a5732abb6c1516135048afe7aa9647fb466f Mon Sep 17 00:00:00 2001 From: Sean Dague Date: Wed, 11 Jun 2014 19:40:14 -0400 Subject: [PATCH 0518/1705] adjust to {SHA1} convention for token based on openstack-dev mailing list thread, support the {SHA1} convention for headers that is generally agreed on. Also fix a minor issue that Johannes found after the review landed, which was doing substring matching instead of tuple matching. Change-Id: I80a456c765026ac6179fd424ead01715798c1dac --- novaclient/client.py | 4 ++-- novaclient/tests/test_client.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index 0a1aa0075..eef6a6edb 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -40,7 +40,7 @@ from novaclient import service_catalog from novaclient import utils -SENSITIVE_HEADERS = ('X-Auth-Token') +SENSITIVE_HEADERS = ('X-Auth-Token',) class _ClientConnectionPool(object): @@ -169,7 +169,7 @@ def safe_header(self, name, value): v = value.encode('utf-8') h = hashlib.sha1(v) d = h.hexdigest() - return name, "SHA1(%s)" % d + return name, "{SHA1}%s" % d else: return name, value diff --git a/novaclient/tests/test_client.py b/novaclient/tests/test_client.py index 3568210ba..7c9f50e38 100644 --- a/novaclient/tests/test_client.py +++ b/novaclient/tests/test_client.py @@ -349,10 +349,10 @@ def test_log_req(self): self.assertIn("REQ: curl -i '/foo' -X GET", output) self.assertIn( "REQ: curl -i '/foo' -X GET -H " - '"X-Auth-Token: SHA1(b42162b6ffdbd7c3c37b7c95b7ba9f51dda0236d)"', + '"X-Auth-Token: {SHA1}b42162b6ffdbd7c3c37b7c95b7ba9f51dda0236d"', output) self.assertIn( "REQ: curl -i '/foo' -X GET -H " - '"X-Auth-Token: SHA1(b42162b6ffdbd7c3c37b7c95b7ba9f51dda0236d)"' + '"X-Auth-Token: {SHA1}b42162b6ffdbd7c3c37b7c95b7ba9f51dda0236d"' ' -H "X-Foo: bar"', output) From 7be7d883428a0d956a6751ae786495fa5cb2a155 Mon Sep 17 00:00:00 2001 From: liyingjun Date: Fri, 13 Jun 2014 10:44:46 +0800 Subject: [PATCH 0519/1705] Fixes wrong value description for volume-detach MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The description of the argument is not proper, the argument is indeed "ID of the volume". “Attachment ID" is the internal term of nova/cinder. So change description 'Attachment ID of the volume.' to 'ID of the volume to detach.' Change-Id: I262d72f6f676e260a58d45b750dbd60fc7c3dcf5 Closes-bug: 1321073 --- novaclient/v1_1/shell.py | 4 ++-- novaclient/v3/shell.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 77ad1986e..d8260cc89 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1787,11 +1787,11 @@ def do_volume_update(cs, args): help=_('Name or ID of server.')) @utils.arg('attachment_id', metavar='', - help=_('Attachment ID of the volume.')) + help=_('ID of the volume to detach.')) def do_volume_detach(cs, args): """Detach a volume from a server.""" cs.volumes.delete_server_volume(_find_server(cs, args.server).id, - args.attachment_id) + args.attachment_id) @utils.service_type('volume') diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 35177122e..8e8f575e7 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -1463,11 +1463,11 @@ def do_volume_update(cs, args): help='Name or ID of server.') @utils.arg('attachment_id', metavar='', - help='Attachment ID of the volume.') + help='ID of the volume to detach.') def do_volume_detach(cs, args): """Detach a volume from a server.""" cs.volumes.delete_server_volume(_find_server(cs, args.server).id, - args.attachment_id) + args.attachment_id) @utils.arg('server', metavar='', help='Name or ID of server.') From b4d0280f03cc5c172351a4d526d0438371c7a773 Mon Sep 17 00:00:00 2001 From: Joe Gordon Date: Sun, 15 Jun 2014 23:42:31 -0700 Subject: [PATCH 0520/1705] Add posargs support to flake8 call Add posargs to flake8 call in tox.ini, with this you can pass arguments directly into flake8 using the following notation tox -epep8 -- --FLAKE8-ARG This can be used to pass arguments like '--statistics' to flake8 Change-Id: Ia820d62c4a1f428d60d88b04d8a4d9b1442baa09 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index b85bd2938..384db6b26 100644 --- a/tox.ini +++ b/tox.ini @@ -13,7 +13,7 @@ deps = -r{toxinidir}/requirements.txt commands = python setup.py testr --testr-args='{posargs}' [testenv:pep8] -commands = flake8 +commands = flake8 {posargs} [testenv:venv] commands = {posargs} From 9f92301983a0a7218396cf14bd2b8fcd4b3b9e48 Mon Sep 17 00:00:00 2001 From: Joe Gordon Date: Sun, 15 Jun 2014 23:50:37 -0700 Subject: [PATCH 0521/1705] Bump hacking to 0.9.x series In order to keep this patch to just a requirements bump, ignore new and stricter hacking rules that are being triggered. Fixing up the code and turning these on is out of scope of this patch and is for future patches. Change-Id: Ia3517e705f894b48585b957c21e7a3a6ed90e586 --- test-requirements.txt | 2 +- tox.ini | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/test-requirements.txt b/test-requirements.txt index 717eaa2ba..ab8aa6072 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,4 +1,4 @@ -hacking>=0.8.0,<0.9 +hacking>=0.9.2,<0.10 coverage>=3.6 discover diff --git a/tox.ini b/tox.ini index 384db6b26..a3220eaf4 100644 --- a/tox.ini +++ b/tox.ini @@ -25,6 +25,8 @@ commands = python setup.py testr --coverage --testr-args='{posargs}' downloadcache = ~/cache/pip [flake8] -ignore = E12,F841,F811,F821,H302,H404 +# TODO fix following rules from hacking 0.9 +# E131,E265,H233,H305,H307,H402,H405,H904 +ignore = E12,E131,E265,F841,F811,F821,H233,H302,H305,H307,H402,H404,H405,H904 show-source = True exclude=.venv,.git,.tox,dist,*openstack/common*,*lib/python*,*egg,build From b78c0d4f469a7e257962ede60cff472bf118e3d4 Mon Sep 17 00:00:00 2001 From: Jyotsna Date: Fri, 13 Jun 2014 17:23:27 +0530 Subject: [PATCH 0522/1705] "nova boot" should not log an error if subsidiary commands fail Fix: Intially the cli was raising "CommadError" in case the requested flavor or image were not present. This error category was not approrpiate as it signifies an error in the command syntax. When the requested resource (flavour/image) does not exist, a ResourceNotFound error should be raised. So, added a new error category "ResourceNotFound" to cater for this scenario and updated the code to raise this new error. "nova show " command has also been updated to raise "ResourceNotFound" error when the requested vm for which details have to be displayed does not exist. Closes-Bug: #1258488 Change-Id: If64a087944586ef5792efe3baa62e455b9bbaa07 --- novaclient/openstack/common/apiclient/exceptions.py | 5 +++++ novaclient/tests/test_utils.py | 6 +++--- novaclient/tests/v1_1/test_shell.py | 2 +- novaclient/utils.py | 2 +- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/novaclient/openstack/common/apiclient/exceptions.py b/novaclient/openstack/common/apiclient/exceptions.py index ada1344f5..68f49c0ce 100644 --- a/novaclient/openstack/common/apiclient/exceptions.py +++ b/novaclient/openstack/common/apiclient/exceptions.py @@ -55,6 +55,11 @@ class CommandError(ClientException): pass +class ResourceNotFound(ClientException): + """Error in getting the resource.""" + pass + + class AuthorizationFailure(ClientException): """Cannot authorize API client.""" pass diff --git a/novaclient/tests/test_utils.py b/novaclient/tests/test_utils.py index ba6801198..87c8c208f 100644 --- a/novaclient/tests/test_utils.py +++ b/novaclient/tests/test_utils.py @@ -89,15 +89,15 @@ def setUp(self): def test_find_none(self): """Test a few non-valid inputs.""" - self.assertRaises(exceptions.CommandError, + self.assertRaises(exceptions.ResourceNotFound, utils.find_resource, self.manager, 'asdf') - self.assertRaises(exceptions.CommandError, + self.assertRaises(exceptions.ResourceNotFound, utils.find_resource, self.manager, None) - self.assertRaises(exceptions.CommandError, + self.assertRaises(exceptions.ResourceNotFound, utils.find_resource, self.manager, {}) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index 38c721552..d2e4c7e70 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -953,7 +953,7 @@ def test_show_no_image(self): self.assert_called('GET', '/flavors/1', pos=-1) def test_show_bad_id(self): - self.assertRaises(exceptions.CommandError, + self.assertRaises(exceptions.ResourceNotFound, self.run_command, 'show xxx') @mock.patch('novaclient.v1_1.shell.utils.print_dict') diff --git a/novaclient/utils.py b/novaclient/utils.py index 6d93f26ae..13f87d6ad 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -241,7 +241,7 @@ def find_resource(manager, name_or_id, **find_args): msg = _("No %(class)s with a name or ID of '%(name)s' exists.") % \ {'class': manager.resource_class.__name__.lower(), 'name': name_or_id} - raise exceptions.CommandError(msg) + raise exceptions.ResourceNotFound(msg) except exceptions.NoUniqueMatch: msg = (_("Multiple %(class)s matches found for '%(name)s', use an ID " "to be more specific.") % From aac82c4097f0de13a34deb09b071cf688da18c0e Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Tue, 17 Jun 2014 13:57:02 +0000 Subject: [PATCH 0523/1705] Updated from global requirements Change-Id: I8b166da9845a1d791ca19ff6713769b83c0b4137 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index ab8aa6072..8e71a3cda 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -6,7 +6,7 @@ fixtures>=0.3.14 httpretty>=0.8.0 keyring>=2.1 mock>=1.0 -sphinx>=1.1.2,<1.2 +sphinx>=1.1.2,!=1.2.0,<1.3 oslosphinx testrepository>=0.0.18 testtools>=0.9.34 From bdbc3afc517a08da42209cb5eab920a75c4333a0 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Tue, 17 Jun 2014 11:11:54 -0700 Subject: [PATCH 0524/1705] Remove quota-class-* commands from v3 shell Commit 1b15b23b0a629e00913a40c5def42e5ca887071c removed the os-quota-class-sets v3 API from Nova in Icehouse. This change removes the corresponding client code. Closes-Bug: #1331095 Change-Id: Iaa2f4063e0f671da5a54ff89d0f1c1780a2687c4 --- novaclient/tests/v3/test_quota_classes.py | 25 ------------- novaclient/v3/client.py | 2 -- novaclient/v3/quota_classes.py | 23 ------------ novaclient/v3/shell.py | 43 ----------------------- 4 files changed, 93 deletions(-) delete mode 100644 novaclient/tests/v3/test_quota_classes.py delete mode 100644 novaclient/v3/quota_classes.py diff --git a/novaclient/tests/v3/test_quota_classes.py b/novaclient/tests/v3/test_quota_classes.py deleted file mode 100644 index 2e0ceb9f0..000000000 --- a/novaclient/tests/v3/test_quota_classes.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright IBM Corp. 2013 -# -# 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 novaclient.tests.v1_1 import test_quota_classes -from novaclient.tests.v3 import fakes - - -class QuotaClassSetsTest(test_quota_classes.QuotaClassSetsTest): - def setUp(self): - super(QuotaClassSetsTest, self).setUp() - self.cs = self._get_fake_client() - - def _get_fake_client(self): - return fakes.FakeClient() diff --git a/novaclient/v3/client.py b/novaclient/v3/client.py index 8d265de5a..7af2e1f2b 100644 --- a/novaclient/v3/client.py +++ b/novaclient/v3/client.py @@ -25,7 +25,6 @@ from novaclient.v3 import images from novaclient.v3 import keypairs from novaclient.v3 import list_extensions -from novaclient.v3 import quota_classes from novaclient.v3 import quotas from novaclient.v3 import servers from novaclient.v3 import services @@ -94,7 +93,6 @@ def __init__(self, username, password, project_id, auth_url=None, self.images = images.ImageManager(self) self.keypairs = keypairs.KeypairManager(self) self.quotas = quotas.QuotaSetManager(self) - self.quota_classes = quota_classes.QuotaClassSetManager(self) self.servers = servers.ServerManager(self) self.services = services.ServiceManager(self) self.usage = usage.UsageManager(self) diff --git a/novaclient/v3/quota_classes.py b/novaclient/v3/quota_classes.py deleted file mode 100644 index e12209eb1..000000000 --- a/novaclient/v3/quota_classes.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright IBM Corp. 2013 -# -# 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 novaclient.v1_1 import quota_classes - - -class QuotaClassSet(quota_classes.QuotaClassSet): - pass - - -class QuotaClassSetManager(quota_classes.QuotaClassSetManager): - resource_class = QuotaClassSet diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 8e8f575e7..706dc7bc2 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -2829,49 +2829,6 @@ def do_quota_delete(cs, args): cs.quotas.delete(args.tenant) -@utils.arg('class_name', - metavar='', - help='Name of quota class to list the quotas for.') -def do_quota_class_show(cs, args): - """List the quotas for a quota class.""" - - _quota_show(cs.quota_classes.get(args.class_name)) - - -@utils.arg('class_name', - metavar='', - help='Name of quota class to set the quotas for.') -@utils.arg('--instances', - metavar='', - type=int, default=None, - help='New value for the "instances" quota.') -@utils.arg('--cores', - metavar='', - type=int, default=None, - help='New value for the "cores" quota.') -@utils.arg('--ram', - metavar='', - type=int, default=None, - help='New value for the "ram" quota.') -@utils.arg('--metadata-items', - metavar='', - type=int, - default=None, - help='New value for the "metadata-items" quota.') -@utils.arg('--metadata_items', - type=int, - help=argparse.SUPPRESS) -@utils.arg('--key-pairs', - metavar='', - type=int, - default=None, - help='New value for the "key-pairs" quota.') -def do_quota_class_update(cs, args): - """Update the quotas for a quota class.""" - - _quota_update(cs.quota_classes, args.class_name, args) - - @utils.arg('server', metavar='', help='Name or ID of server.') @utils.arg('host', metavar='', help='Name or ID of target host.') @utils.arg('--password', From e6a51f47c409adfa56e8b1e16425ac3fc26e08ab Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Wed, 18 Jun 2014 14:10:23 +0000 Subject: [PATCH 0525/1705] Updated from global requirements Change-Id: Iee04e0f6b62cb97824bef2217a3dbd42c15754f8 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e98257c9a..b08504104 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,5 +4,5 @@ iso8601>=0.1.9 PrettyTable>=0.7,<0.8 requests>=1.1 simplejson>=2.0.9 -six>=1.6.0 +six>=1.7.0 Babel>=1.3 From 88d0b6f2d957cd2e7abc3fc11fc533017c5f46bd Mon Sep 17 00:00:00 2001 From: "ChangBo Guo(gcb)" Date: Tue, 17 Jun 2014 15:07:04 +0800 Subject: [PATCH 0526/1705] Enable F841 F841 detects local variable is assigned to but never used. This commit fix the violations and enable F841 in gate. Change-Id: I52419f5e17db70e511ff2d4d61c85458c958e9c3 --- novaclient/tests/test_client.py | 4 ++-- novaclient/tests/test_shell.py | 2 +- .../v1_1/contrib/test_assisted_volume_snapshots.py | 4 ++-- novaclient/tests/v1_1/test_fixed_ips.py | 4 ++-- novaclient/tests/v1_1/test_hosts.py | 6 +++--- novaclient/tests/v1_1/test_images.py | 2 +- novaclient/tests/v1_1/test_servers.py | 10 +++++----- novaclient/tests/v1_1/test_services.py | 2 +- novaclient/tests/v3/test_hosts.py | 6 +++--- novaclient/tests/v3/test_images.py | 2 +- novaclient/tests/v3/test_servers.py | 6 +++--- novaclient/tests/v3/test_volumes.py | 4 ++-- novaclient/utils.py | 2 +- novaclient/v3/shell.py | 10 ++++------ tox.ini | 2 +- 15 files changed, 32 insertions(+), 34 deletions(-) diff --git a/novaclient/tests/test_client.py b/novaclient/tests/test_client.py index 7c9f50e38..bf868f821 100644 --- a/novaclient/tests/test_client.py +++ b/novaclient/tests/test_client.py @@ -215,7 +215,7 @@ def test_contextmanager_v1_1(self, mock_http_client): fake_client = mock.Mock() mock_http_client.return_value = fake_client with novaclient.v1_1.client.Client("user", "password", "project_id", - auth_url="foo/v2") as client: + auth_url="foo/v2"): pass self.assertTrue(fake_client.open_session.called) self.assertTrue(fake_client.close_session.called) @@ -225,7 +225,7 @@ def test_contextmanager_v3(self, mock_http_client): fake_client = mock.Mock() mock_http_client.return_value = fake_client with novaclient.v3.client.Client("user", "password", "project_id", - auth_url="foo/v2") as client: + auth_url="foo/v2"): pass self.assertTrue(fake_client.open_session.called) self.assertTrue(fake_client.close_session.called) diff --git a/novaclient/tests/test_shell.py b/novaclient/tests/test_shell.py index a9f060b34..fdcc78e68 100644 --- a/novaclient/tests/test_shell.py +++ b/novaclient/tests/test_shell.py @@ -265,7 +265,7 @@ def test_main_noargs(self): # Ensure that main works with no command-line arguments try: novaclient.shell.main() - except SystemExit as exc: + except SystemExit: self.fail('Unexpected SystemExit') # We expect the normal usage as a result diff --git a/novaclient/tests/v1_1/contrib/test_assisted_volume_snapshots.py b/novaclient/tests/v1_1/contrib/test_assisted_volume_snapshots.py index 34d958f08..c3e47b458 100644 --- a/novaclient/tests/v1_1/contrib/test_assisted_volume_snapshots.py +++ b/novaclient/tests/v1_1/contrib/test_assisted_volume_snapshots.py @@ -32,10 +32,10 @@ class AssistedVolumeSnapshotsTestCase(utils.TestCase): def test_create_snap(self): - res = cs.assisted_volume_snapshots.create('1', {}) + cs.assisted_volume_snapshots.create('1', {}) cs.assert_called('POST', '/os-assisted-volume-snapshots') def test_delete_snap(self): - res = cs.assisted_volume_snapshots.delete('x', {}) + cs.assisted_volume_snapshots.delete('x', {}) cs.assert_called('DELETE', '/os-assisted-volume-snapshots/x?delete_info={}') diff --git a/novaclient/tests/v1_1/test_fixed_ips.py b/novaclient/tests/v1_1/test_fixed_ips.py index ca2b3fc5f..38fa31c13 100644 --- a/novaclient/tests/v1_1/test_fixed_ips.py +++ b/novaclient/tests/v1_1/test_fixed_ips.py @@ -33,10 +33,10 @@ def test_get_fixed_ip(self): def test_reserve_fixed_ip(self): body = {"reserve": None} - res = self.cs.fixed_ips.reserve(fixed_ip='192.168.1.1') + self.cs.fixed_ips.reserve(fixed_ip='192.168.1.1') self.assert_called('POST', '/os-fixed-ips/192.168.1.1/action', body) def test_unreserve_fixed_ip(self): body = {"unreserve": None} - res = self.cs.fixed_ips.unreserve(fixed_ip='192.168.1.1') + self.cs.fixed_ips.unreserve(fixed_ip='192.168.1.1') self.assert_called('POST', '/os-fixed-ips/192.168.1.1/action', body) diff --git a/novaclient/tests/v1_1/test_hosts.py b/novaclient/tests/v1_1/test_hosts.py index 2c243662d..1ab64d514 100644 --- a/novaclient/tests/v1_1/test_hosts.py +++ b/novaclient/tests/v1_1/test_hosts.py @@ -62,18 +62,18 @@ def test_update_both(self): def test_host_startup(self): host = cs.hosts.get('sample_host')[0] - result = host.startup() + host.startup() cs.assert_called( 'GET', '/os-hosts/sample_host/startup') def test_host_reboot(self): host = cs.hosts.get('sample_host')[0] - result = host.reboot() + host.reboot() cs.assert_called( 'GET', '/os-hosts/sample_host/reboot') def test_host_shutdown(self): host = cs.hosts.get('sample_host')[0] - result = host.shutdown() + host.shutdown() cs.assert_called( 'GET', '/os-hosts/sample_host/shutdown') diff --git a/novaclient/tests/v1_1/test_images.py b/novaclient/tests/v1_1/test_images.py index e3968aa48..937bea1e6 100644 --- a/novaclient/tests/v1_1/test_images.py +++ b/novaclient/tests/v1_1/test_images.py @@ -32,7 +32,7 @@ def test_list_images_undetailed(self): [self.assertIsInstance(i, images.Image) for i in il] def test_list_images_with_limit(self): - il = cs.images.list(limit=4) + cs.images.list(limit=4) cs.assert_called('GET', '/images/detail?limit=4') def test_get_image_details(self): diff --git a/novaclient/tests/v1_1/test_servers.py b/novaclient/tests/v1_1/test_servers.py index f38c14fd0..728732474 100644 --- a/novaclient/tests/v1_1/test_servers.py +++ b/novaclient/tests/v1_1/test_servers.py @@ -222,17 +222,17 @@ def test_delete_server(self): cs.assert_called('DELETE', '/servers/1234') def test_delete_server_meta(self): - s = cs.servers.delete_meta(1234, ['test_key']) + cs.servers.delete_meta(1234, ['test_key']) cs.assert_called('DELETE', '/servers/1234/metadata/test_key') def test_set_server_meta(self): - s = cs.servers.set_meta(1234, {'test_key': 'test_value'}) - reval = cs.assert_called('POST', '/servers/1234/metadata', + cs.servers.set_meta(1234, {'test_key': 'test_value'}) + cs.assert_called('POST', '/servers/1234/metadata', {'metadata': {'test_key': 'test_value'}}) def test_set_server_meta_item(self): - s = cs.servers.set_meta_item(1234, 'test_key', 'test_value') - reval = cs.assert_called('PUT', '/servers/1234/metadata/test_key', + cs.servers.set_meta_item(1234, 'test_key', 'test_value') + cs.assert_called('PUT', '/servers/1234/metadata/test_key', {'meta': {'test_key': 'test_value'}}) def test_find(self): diff --git a/novaclient/tests/v1_1/test_services.py b/novaclient/tests/v1_1/test_services.py index 5ee33432e..d5242e52b 100644 --- a/novaclient/tests/v1_1/test_services.py +++ b/novaclient/tests/v1_1/test_services.py @@ -79,7 +79,7 @@ def test_services_enable(self): self.assertEqual(service.status, 'enabled') def test_services_delete(self): - service = self.cs.services.delete('1') + self.cs.services.delete('1') self.cs.assert_called('DELETE', '/os-services/1') def test_services_disable(self): diff --git a/novaclient/tests/v3/test_hosts.py b/novaclient/tests/v3/test_hosts.py index 6ce5b56e9..19a5060df 100644 --- a/novaclient/tests/v3/test_hosts.py +++ b/novaclient/tests/v3/test_hosts.py @@ -66,18 +66,18 @@ def test_update_both(self): def test_host_startup(self): host = cs.hosts.get('sample_host')[0] - result = host.startup() + host.startup() cs.assert_called( 'GET', '/os-hosts/sample_host/startup') def test_host_reboot(self): host = cs.hosts.get('sample_host')[0] - result = host.reboot() + host.reboot() cs.assert_called( 'GET', '/os-hosts/sample_host/reboot') def test_host_shutdown(self): host = cs.hosts.get('sample_host')[0] - result = host.shutdown() + host.shutdown() cs.assert_called( 'GET', '/os-hosts/sample_host/shutdown') diff --git a/novaclient/tests/v3/test_images.py b/novaclient/tests/v3/test_images.py index c1e756514..c3e8991a9 100644 --- a/novaclient/tests/v3/test_images.py +++ b/novaclient/tests/v3/test_images.py @@ -36,7 +36,7 @@ def test_list_images_undetailed(self): self.assertIsInstance(i, images.Image) def test_list_images_with_limit(self): - il = cs.images.list(limit=4) + cs.images.list(limit=4) cs.assert_called('GET', '/v1/images/detail?limit=4') def test_get_image_details(self): diff --git a/novaclient/tests/v3/test_servers.py b/novaclient/tests/v3/test_servers.py index c29b7e1c4..e03fc5f40 100644 --- a/novaclient/tests/v3/test_servers.py +++ b/novaclient/tests/v3/test_servers.py @@ -212,12 +212,12 @@ def test_delete_server(self): cs.assert_called('DELETE', '/servers/1234') def test_delete_server_meta(self): - s = cs.servers.delete_meta(1234, ['test_key']) + cs.servers.delete_meta(1234, ['test_key']) cs.assert_called('DELETE', '/servers/1234/metadata/test_key') def test_set_server_meta(self): - s = cs.servers.set_meta(1234, {'test_key': 'test_value'}) - reval = cs.assert_called('POST', '/servers/1234/metadata', + cs.servers.set_meta(1234, {'test_key': 'test_value'}) + cs.assert_called('POST', '/servers/1234/metadata', {'metadata': {'test_key': 'test_value'}}) def test_find(self): diff --git a/novaclient/tests/v3/test_volumes.py b/novaclient/tests/v3/test_volumes.py index 82f6d54a0..773d2bb1d 100644 --- a/novaclient/tests/v3/test_volumes.py +++ b/novaclient/tests/v3/test_volumes.py @@ -26,7 +26,7 @@ def _get_fake_client(self): return fakes.FakeClient() def test_attach_server_volume(self): - v = self.cs.volumes.attach_server_volume( + self.cs.volumes.attach_server_volume( server=1234, volume_id='15e59938-07d5-11e1-90e3-e3dffe0c5983', device='/dev/vdb' @@ -35,7 +35,7 @@ def test_attach_server_volume(self): def test_update_server_volume(self): vol_id = '15e59938-07d5-11e1-90e3-e3dffe0c5983' - v = self.cs.volumes.update_server_volume( + self.cs.volumes.update_server_volume( server=1234, old_volume_id='Work', new_volume_id=vol_id diff --git a/novaclient/utils.py b/novaclient/utils.py index 6d93f26ae..36e732038 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -351,7 +351,7 @@ def _load_entry_point(ep_name, name=None): def is_integer_like(val): """Returns validation of a value as an integer.""" try: - value = int(val) + int(val) return True except (TypeError, ValueError, AttributeError): return False diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 706dc7bc2..f206f9263 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -1437,9 +1437,8 @@ def do_volume_attach(cs, args): if args.device == 'auto': args.device = None - volume = cs.volumes.attach_server_volume(_find_server(cs, args.server).id, - args.volume, - args.device) + cs.volumes.attach_server_volume(_find_server(cs, args.server).id, + args.volume, args.device) @utils.arg('server', @@ -1453,9 +1452,8 @@ def do_volume_attach(cs, args): help='ID of the volume to attach.') def do_volume_update(cs, args): """Update volume attachment.""" - volume = cs.volumes.update_server_volume(_find_server(cs, args.server).id, - args.attachment_id, - args.new_volume) + cs.volumes.update_server_volume(_find_server(cs, args.server).id, + args.attachment_id, args.new_volume) @utils.arg('server', diff --git a/tox.ini b/tox.ini index f6da55846..b9f8c9f08 100644 --- a/tox.ini +++ b/tox.ini @@ -28,6 +28,6 @@ downloadcache = ~/cache/pip [flake8] # TODO fix following rules from hacking 0.9 # E131,E265,H233,H305,H307,H402,H405,H904 -ignore = E12,E131,E265,F841,F811,F821,H233,H302,H305,H307,H402,H404,H405,H904 +ignore = E12,E131,E265,F811,F821,H233,H302,H305,H307,H402,H404,H405,H904 show-source = True exclude=.venv,.git,.tox,dist,*openstack/common*,*lib/python*,*egg,build From d17253b29a5b7b422538c6496239cf713be627a3 Mon Sep 17 00:00:00 2001 From: "Daniel P. Berrange" Date: Fri, 6 Jun 2014 14:23:17 +0100 Subject: [PATCH 0527/1705] Look for all accessible flavors by default, not just public ones The Nova server will restrict unprivileged user accounts to just public images, while allowing administrator accounts access to all. The Nova client shouldn't force the flavor name lookup to be restricted to just public images, since that breaks the ability to the flavor name when booting an instance Fixes bug #1327212 Change-Id: I949aec52660242249b8cba51d77bfdc1acaf31d2 --- novaclient/tests/v1_1/fakes.py | 3 +++ novaclient/tests/v1_1/test_shell.py | 37 +++++++++++++++++++++++++++++ novaclient/tests/v3/fakes.py | 4 ++++ novaclient/tests/v3/test_shell.py | 36 ++++++++++++++++++++++++++++ novaclient/v1_1/shell.py | 14 +++-------- novaclient/v3/shell.py | 2 +- 6 files changed, 84 insertions(+), 12 deletions(-) diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index 35fd44d60..d387e17c4 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -750,6 +750,9 @@ def get_flavors_3(self, **kw): def get_flavors_512_MB_Server(self, **kw): raise exceptions.NotFound('404') + def get_flavors_128_MB_Server(self, **kw): + raise exceptions.NotFound('404') + def get_flavors_aa1(self, **kw): # Alphanumeric flavor id are allowed. return ( diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index 38c721552..aa3b9b494 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -661,6 +661,27 @@ def test_boot_with_poll_to_check_VM_state_error(self): self.assertRaises(exceptions.InstanceInErrorState, self.run_command, 'boot --flavor 1 --image 1 some-bad-server --poll') + def test_boot_named_flavor(self): + self.run_command(["boot", "--image", "1", + "--flavor", "512 MB Server", + "--max-count", "3", "server"]) + self.assert_called('GET', '/images/1', pos=0) + self.assert_called('GET', '/flavors/512 MB Server', pos=1) + self.assert_called('GET', '/flavors?is_public=None', pos=2) + self.assert_called('GET', '/flavors?is_public=None', pos=3) + self.assert_called('GET', '/flavors/2', pos=4) + self.assert_called( + 'POST', '/servers', + { + 'server': { + 'flavorRef': '2', + 'name': 'server', + 'imageRef': '1', + 'min_count': 1, + 'max_count': 3, + } + }, pos=5) + def test_flavor_list(self): self.run_command('flavor-list') self.assert_called_anytime('GET', '/flavors/detail') @@ -682,6 +703,22 @@ def test_flavor_show_with_alphanum_id(self): self.run_command('flavor-show aa1') self.assert_called_anytime('GET', '/flavors/aa1') + def test_flavor_show_by_name(self): + self.run_command(['flavor-show', '128 MB Server']) + self.assert_called('GET', '/flavors/128 MB Server', pos=0) + self.assert_called('GET', '/flavors?is_public=None', pos=1) + self.assert_called('GET', '/flavors?is_public=None', pos=2) + self.assert_called('GET', '/flavors/aa1', pos=3) + self.assert_called('GET', '/flavors/aa1/os-extra_specs', pos=4) + + def test_flavor_show_by_name_priv(self): + self.run_command(['flavor-show', '512 MB Server']) + self.assert_called('GET', '/flavors/512 MB Server', pos=0) + self.assert_called('GET', '/flavors?is_public=None', pos=1) + self.assert_called('GET', '/flavors?is_public=None', pos=2) + self.assert_called('GET', '/flavors/2', pos=3) + self.assert_called('GET', '/flavors/2/os-extra_specs', pos=4) + def test_flavor_key_set(self): self.run_command('flavor-key 1 set k1=v1') self.assert_called('POST', '/flavors/1/os-extra_specs', diff --git a/novaclient/tests/v3/fakes.py b/novaclient/tests/v3/fakes.py index 9aad01716..64af4b747 100644 --- a/novaclient/tests/v3/fakes.py +++ b/novaclient/tests/v3/fakes.py @@ -120,6 +120,10 @@ def get_flavors_detail(self, **kw): # get_flavors_2_flavor_access = ( fakes_v1_1.FakeHTTPClient.get_flavors_2_os_flavor_access) + get_flavors_2_flavor_extra_specs = ( + fakes_v1_1.FakeHTTPClient.get_flavors_2_os_extra_specs) + get_flavors_aa1_flavor_extra_specs = ( + fakes_v1_1.FakeHTTPClient.get_flavors_aa1_os_extra_specs) # # Images diff --git a/novaclient/tests/v3/test_shell.py b/novaclient/tests/v3/test_shell.py index fbed7e11a..3d9c2b598 100644 --- a/novaclient/tests/v3/test_shell.py +++ b/novaclient/tests/v3/test_shell.py @@ -558,3 +558,39 @@ def test_boot_with_poll(self, poll_method): def test_boot_with_poll_to_check_VM_state_error(self): self.assertRaises(exceptions.InstanceInErrorState, self.run_command, 'boot --flavor 1 --image 1 some-bad-server --poll') + + def test_boot_named_flavor(self): + self.run_command(["boot", "--image", "1", + "--flavor", "512 MB Server", + "--max-count", "3", "server"]) + self.assert_called('GET', '/flavors/512 MB Server', pos=0) + self.assert_called('GET', '/flavors?is_public=None', pos=1) + self.assert_called('GET', '/flavors?is_public=None', pos=2) + self.assert_called('GET', '/flavors/2', pos=3) + self.assert_called( + 'POST', '/servers', + { + 'server': { + 'flavor_ref': '2', + 'name': 'server', + 'image_ref': '1', + 'os-multiple-create:min_count': 1, + 'os-multiple-create:max_count': 3, + } + }, pos=4) + + def test_flavor_show_by_name(self): + self.run_command(['flavor-show', '128 MB Server']) + self.assert_called('GET', '/flavors/128 MB Server', pos=0) + self.assert_called('GET', '/flavors?is_public=None', pos=1) + self.assert_called('GET', '/flavors?is_public=None', pos=2) + self.assert_called('GET', '/flavors/aa1', pos=3) + self.assert_called('GET', '/flavors/aa1/flavor-extra-specs', pos=4) + + def test_flavor_show_by_name_priv(self): + self.run_command(['flavor-show', '512 MB Server']) + self.assert_called('GET', '/flavors/512 MB Server', pos=0) + self.assert_called('GET', '/flavors?is_public=None', pos=1) + self.assert_called('GET', '/flavors?is_public=None', pos=2) + self.assert_called('GET', '/flavors/2', pos=3) + self.assert_called('GET', '/flavors/2/flavor-extra-specs', pos=4) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index d8260cc89..29f5d70b7 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -733,7 +733,7 @@ def do_flavor_access_list(cs, args): help=_('Tenant ID to add flavor access for.')) def do_flavor_access_add(cs, args): """Add flavor access for the given tenant.""" - flavor = _find_flavor_for_admin(cs, args.flavor) + flavor = _find_flavor(cs, args.flavor) access_list = cs.flavor_access.add_tenant_access(flavor, args.tenant) columns = ['Flavor_ID', 'Tenant_ID'] utils.print_list(access_list, columns) @@ -746,7 +746,7 @@ def do_flavor_access_add(cs, args): help=_('Tenant ID to remove flavor access for.')) def do_flavor_access_remove(cs, args): """Remove flavor access for the given tenant.""" - flavor = _find_flavor_for_admin(cs, args.flavor) + flavor = _find_flavor(cs, args.flavor) access_list = cs.flavor_access.remove_tenant_access(flavor, args.tenant) columns = ['Flavor_ID', 'Tenant_ID'] utils.print_list(access_list, columns) @@ -1578,18 +1578,10 @@ def _find_image(cs, image): return utils.find_resource(cs.images, image) -def _find_flavor_for_admin(cs, flavor): - """Get a flavor for administrator by name, ID, or RAM size.""" - try: - return utils.find_resource(cs.flavors, flavor, is_public=None) - except exceptions.NotFound: - return cs.flavors.find(ram=flavor) - - def _find_flavor(cs, flavor): """Get a flavor by name, ID, or RAM size.""" try: - return utils.find_resource(cs.flavors, flavor) + return utils.find_resource(cs.flavors, flavor, is_public=None) except exceptions.NotFound: return cs.flavors.find(ram=flavor) diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 8e8f575e7..a1aaf638a 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -1395,7 +1395,7 @@ def _find_image(cs, image): def _find_flavor(cs, flavor): """Get a flavor by name, ID, or RAM size.""" try: - return utils.find_resource(cs.flavors, flavor) + return utils.find_resource(cs.flavors, flavor, is_public=None) except exceptions.NotFound: return cs.flavors.find(ram=flavor) From 3441007062eea006fe0fa1a90692c3e85eee9df8 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Tue, 17 Jun 2014 11:01:18 -0700 Subject: [PATCH 0528/1705] Allow updating fixed_ips quotas in quota-class-update (v2 shell only) The quota-class-update command wasn't allowing updating quotas for fixed_ips which is a supported key for the os-quota-class-sets API. This change adds the support to update qoutas for fixed_ips and updates the existing test to be more robust in (a) the quota keys it's testing and (b) the request body it's asserting. This is a v2-only change since the os-quota-class-sets API was removed from the nova v3 API in Icehouse. Closes-Bug: #1330571 Change-Id: Ifcca6d00551619b993b96f05134980a4ca2f2acf --- novaclient/tests/v1_1/test_shell.py | 21 ++++++++++++++++----- novaclient/v1_1/shell.py | 5 +++++ 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index 38c721552..4121e4a77 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -1600,11 +1600,22 @@ def test_quota_class_show(self): self.assert_called('GET', '/os-quota-class-sets/test') def test_quota_class_update(self): - self.run_command('quota-class-update 97f4c221bff44578b0300df4ef119353' - ' --instances=5') - self.assert_called('PUT', - '/os-quota-class-sets/97f4c221bff44578b0300' - 'df4ef119353') + # The list of args we can update. + args = ( + '--instances', '--cores', '--ram', '--floating-ips', '--fixed-ips', + '--metadata-items', '--injected-files', + '--injected-file-content-bytes', '--injected-file-path-bytes', + '--key-pairs', '--security-groups', '--security-group-rules' + ) + for arg in args: + self.run_command('quota-class-update ' + '97f4c221bff44578b0300df4ef119353 ' + '%s=5' % arg) + request_param = arg[2:].replace('-', '_') + body = {'quota_class_set': {request_param: 5}} + self.assert_called( + 'PUT', '/os-quota-class-sets/97f4c221bff44578b0300df4ef119353', + body) def test_network_list(self): self.run_command('network-list') diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index d8260cc89..928da577c 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -3373,6 +3373,11 @@ def do_quota_class_show(cs, args): @utils.arg('--floating_ips', type=int, help=argparse.SUPPRESS) +@utils.arg('--fixed-ips', + metavar='', + type=int, + default=None, + help=_('New value for the "fixed-ips" quota.')) @utils.arg('--metadata-items', metavar='', type=int, From fce7da49342845ae03004cc5b22e6041d4814db5 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 20 Jun 2014 03:38:52 +0000 Subject: [PATCH 0529/1705] Updated from global requirements Change-Id: I7645c469147b0ff235aaf96fd6f6f2df68505ea4 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 8e71a3cda..5f294a3c9 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -3,7 +3,7 @@ hacking>=0.9.2,<0.10 coverage>=3.6 discover fixtures>=0.3.14 -httpretty>=0.8.0 +httpretty>=0.8.0,!=0.8.1,!=0.8.2 keyring>=2.1 mock>=1.0 sphinx>=1.1.2,!=1.2.0,<1.3 From a20284ebe8973beb5f80bb54458c2c817e184c42 Mon Sep 17 00:00:00 2001 From: Abhishek Chanda Date: Fri, 13 Dec 2013 20:34:25 +0530 Subject: [PATCH 0530/1705] Add some security group tests for the V1_1 API This patch adds a test for NoUniqueMatch exception to the v1_1 API Change-Id: Ic9947eaed34be100c529f021432f0c388a7c3ddb Closes-Bug: #1250501 --- novaclient/tests/v1_1/test_shell.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index 38c721552..083d6cf5a 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -2063,3 +2063,15 @@ def test_with_an_nonexisting_name(self): novaclient.v1_1.shell._get_secgroup, cs, 'abc') + + def test_with_non_unique_name(self): + group_one = mock.MagicMock() + group_one.name = 'group_one' + cs = mock.Mock(**{ + 'security_groups.get.return_value': 'sec_group', + 'security_groups.list.return_value': [group_one, group_one], + }) + self.assertRaises(exceptions.NoUniqueMatch, + novaclient.v1_1.shell._get_secgroup, + cs, + 'group_one') From 4c8cefb98a425382204df2f38f24e6b5b71520dd Mon Sep 17 00:00:00 2001 From: Rick Harris Date: Thu, 19 Jun 2014 18:34:17 -0500 Subject: [PATCH 0531/1705] Overhaul bash-completion to support non-UUID based IDs There are a few things currently wrong with bash-completion as it stands now: 1) IDs are currently required to be UUIDs. This is an arbitrary limitation and doesn't make sense for certain kinds of objects, like `Flavors` where a valid ID could be `performance-16gb`. 2) The code is spread out between Oslo's `Resource` and Novaclient's `Manager` class. This makes it difficult to improve the code because it requires changes to two separate projects. We should centralize the code in Novaclient until the API is stable, then import the code into Oslo in its entirety, not partially like it is now. 3) The completion code is handled by the `Manager` of which there is one per Resource-type. In the interest of centralizing this functionality, we should create a `CompletionCache` class and hang it off of `Client` of which there is one-per-session. 4) The completion-code currently runs by default even in headless mode (e.g. novaclient without the shell). It'd be much more efficient to only write to the completion cache if we're accessing the `Client` from the novaclient shell. We can make this an option to support third-party CLI clients that want to use the completion-cache as well. NOTE: * The corresponding Oslo patch is here: https://review.openstack.org/#/c/101376/ * This patch was tested in multithreaded mode to prevent any regression from: https://bugs.launchpad.net/python-novaclient/+bug/1213958. Change-Id: Idada83de103358974b739f81d4f392574f9e1237 Closes-Bug: 1332270 --- novaclient/base.py | 96 +++++++++------------------------------ novaclient/client.py | 74 ++++++++++++++++++++++++++++++ novaclient/shell.py | 5 +- novaclient/v1_1/client.py | 12 ++++- novaclient/v3/client.py | 12 ++++- 5 files changed, 122 insertions(+), 77 deletions(-) diff --git a/novaclient/base.py b/novaclient/base.py index c54367c1f..6b000f8fe 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -20,11 +20,7 @@ """ import abc -import contextlib -import hashlib import inspect -import os -import threading import six @@ -52,11 +48,18 @@ class Manager(utils.HookableMixin): etc.) and provide CRUD operations for them. """ resource_class = None - cache_lock = threading.RLock() def __init__(self, api): self.api = api + def _write_object_to_completion_cache(self, obj): + if hasattr(self.api, 'write_object_to_completion_cache'): + self.api.write_object_to_completion_cache(obj) + + def _clear_completion_cache_for_class(self, obj_class): + if hasattr(self.api, 'clear_completion_cache_for_class'): + self.api.clear_completion_cache_for_class(obj_class) + def _list(self, url, response_key, obj_class=None, body=None): if body: _resp, body = self.api.client.post(url, body=body) @@ -75,77 +78,22 @@ def _list(self, url, response_key, obj_class=None, body=None): except KeyError: pass - with self.completion_cache('human_id', obj_class, mode="w"): - with self.completion_cache('uuid', obj_class, mode="w"): - return [obj_class(self, res, loaded=True) - for res in data if res] - - @contextlib.contextmanager - def completion_cache(self, cache_type, obj_class, mode): - """ - The completion cache store items that can be used for bash - autocompletion, like UUIDs or human-friendly IDs. - - A resource listing will clear and repopulate the cache. + self._clear_completion_cache_for_class(obj_class) - A resource create will append to the cache. + objs = [] + for res in data: + if res: + obj = obj_class(self, res, loaded=True) + self._write_object_to_completion_cache(obj) + objs.append(obj) - Delete is not handled because listings are assumed to be performed - often enough to keep the cache reasonably up-to-date. - """ - # NOTE(wryan): This lock protects read and write access to the - # completion caches - with self.cache_lock: - base_dir = utils.env('NOVACLIENT_UUID_CACHE_DIR', - default="~/.novaclient") - - # NOTE(sirp): Keep separate UUID caches for each username + - # endpoint pair - username = utils.env('OS_USERNAME', 'NOVA_USERNAME') - url = utils.env('OS_URL', 'NOVA_URL') - uniqifier = hashlib.md5(username.encode('utf-8') + - url.encode('utf-8')).hexdigest() - - cache_dir = os.path.expanduser(os.path.join(base_dir, uniqifier)) - - try: - os.makedirs(cache_dir, 0o755) - except OSError: - # NOTE(kiall): This is typically either permission denied while - # attempting to create the directory, or the - # directory already exists. Either way, don't - # fail. - pass - - resource = obj_class.__name__.lower() - filename = "%s-%s-cache" % (resource, cache_type.replace('_', '-')) - path = os.path.join(cache_dir, filename) - - cache_attr = "_%s_cache" % cache_type - - try: - setattr(self, cache_attr, open(path, mode)) - except IOError: - # NOTE(kiall): This is typically a permission denied while - # attempting to write the cache file. - pass - - try: - yield - finally: - cache = getattr(self, cache_attr, None) - if cache: - cache.close() - delattr(self, cache_attr) - - def write_to_completion_cache(self, cache_type, val): - cache = getattr(self, "_%s_cache" % cache_type, None) - if cache: - cache.write("%s\n" % val) + return objs def _get(self, url, response_key): _resp, body = self.api.client.get(url) - return self.resource_class(self, body[response_key], loaded=True) + obj = self.resource_class(self, body[response_key], loaded=True) + self._write_object_to_completion_cache(obj) + return obj def _create(self, url, body, response_key, return_raw=False, **kwargs): self.run_hooks('modify_body_for_create', body, **kwargs) @@ -153,9 +101,9 @@ def _create(self, url, body, response_key, return_raw=False, **kwargs): if return_raw: return body[response_key] - with self.completion_cache('human_id', self.resource_class, mode="a"): - with self.completion_cache('uuid', self.resource_class, mode="a"): - return self.resource_class(self, body[response_key]) + obj = self.resource_class(self, body[response_key]) + self._write_object_to_completion_cache(obj) + return obj def _delete(self, url): _resp, _body = self.api.client.delete(url) diff --git a/novaclient/client.py b/novaclient/client.py index eef6a6edb..8d2d3f678 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -20,8 +20,11 @@ OpenStack Client interface. Handles the REST calls and responses. """ +import errno +import glob import hashlib import logging +import os import time import requests @@ -58,6 +61,77 @@ def get(self, url): return self._adapters[url] +class CompletionCache(object): + """The completion cache is how we support tab-completion with novaclient. + + The `Manager` writes object IDs and Human-IDs to the completion-cache on + object-show, object-list, and object-create calls. + + The `nova.bash_completion` script then uses these files to provide the + actual tab-completion. + + The cache directory layout is: + + ~/.novaclient/ + / + -id-cache + -human-id-cache + """ + def __init__(self, username, auth_url, attributes=('id', 'human_id')): + self.directory = self._make_directory_name(username, auth_url) + self.attributes = attributes + + def _make_directory_name(self, username, auth_url): + """Creates a unique directory name based on the auth_url and username + of the current user. + """ + uniqifier = hashlib.md5(username.encode('utf-8') + + auth_url.encode('utf-8')).hexdigest() + base_dir = utils.env('NOVACLIENT_UUID_CACHE_DIR', + default="~/.novaclient") + return os.path.expanduser(os.path.join(base_dir, uniqifier)) + + def _prepare_directory(self): + try: + os.makedirs(self.directory, 0o755) + except OSError: + # NOTE(kiall): This is typically either permission denied while + # attempting to create the directory, or the + # directory already exists. Either way, don't + # fail. + pass + + def clear_class(self, obj_class): + self._prepare_directory() + + resource = obj_class.__name__.lower() + resource_glob = os.path.join(self.directory, "%s-*-cache" % resource) + + for filename in glob.iglob(resource_glob): + try: + os.unlink(filename) + except OSError as e: + if e.errno != errno.ENOENT: + raise + + def _write_attribute(self, resource, attribute, value): + self._prepare_directory() + + filename = "%s-%s-cache" % (resource, attribute.replace('_', '-')) + path = os.path.join(self.directory, filename) + + with open(path, 'a') as f: + f.write("%s\n" % value) + + def write_object(self, obj): + resource = obj.__class__.__name__.lower() + + for attribute in self.attributes: + value = getattr(obj, attribute, None) + if value: + self._write_attribute(resource, attribute, value) + + class HTTPClient(object): USER_AGENT = 'python-novaclient' diff --git a/novaclient/shell.py b/novaclient/shell.py index 5729aa1e1..08f5e8c40 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -648,6 +648,8 @@ def main(self, argv): raise exc.CommandError(_("You must provide an auth url " "via either --os-auth-url or env[OS_AUTH_URL]")) + completion_cache = client.CompletionCache(os_username, os_auth_url) + self.cs = client.Client(options.os_compute_api_version, os_username, os_password, os_tenant_name, tenant_id=os_tenant_id, user_id=os_user_id, @@ -659,7 +661,8 @@ def main(self, argv): volume_service_name=volume_service_name, timings=args.timings, bypass_url=bypass_url, os_cache=os_cache, http_log_debug=options.debug, - cacert=cacert, timeout=timeout) + cacert=cacert, timeout=timeout, + completion_cache=completion_cache) # Now check for the password/token of which pieces of the # identifying keyring key can come from the underlying client diff --git a/novaclient/v1_1/client.py b/novaclient/v1_1/client.py index b15155094..2cf62e947 100644 --- a/novaclient/v1_1/client.py +++ b/novaclient/v1_1/client.py @@ -89,7 +89,7 @@ def __init__(self, username, api_key, project_id, auth_url=None, http_log_debug=False, auth_system='keystone', auth_plugin=None, auth_token=None, cacert=None, tenant_id=None, user_id=None, - connection_pool=False): + connection_pool=False, completion_cache=None): # FIXME(comstud): Rename the api_key argument above when we # know it's not being used as keyword argument password = api_key @@ -167,6 +167,16 @@ def __init__(self, username, api_key, project_id, auth_url=None, cacert=cacert, connection_pool=connection_pool) + self.completion_cache = completion_cache + + def write_object_to_completion_cache(self, obj): + if self.completion_cache: + self.completion_cache.write_object(obj) + + def clear_completion_cache_for_class(self, obj_class): + if self.completion_cache: + self.completion_cache.clear_class(obj_class) + def __enter__(self): self.client.open_session() return self diff --git a/novaclient/v3/client.py b/novaclient/v3/client.py index 7af2e1f2b..5274c7aee 100644 --- a/novaclient/v3/client.py +++ b/novaclient/v3/client.py @@ -74,7 +74,7 @@ def __init__(self, username, password, project_id, auth_url=None, http_log_debug=False, auth_system='keystone', auth_plugin=None, auth_token=None, cacert=None, tenant_id=None, user_id=None, - connection_pool=False): + connection_pool=False, completion_cache=None): self.projectid = project_id self.tenant_id = tenant_id self.user_id = user_id @@ -130,6 +130,16 @@ def __init__(self, username, password, project_id, auth_url=None, cacert=cacert, connection_pool=connection_pool) + self.completion_cache = completion_cache + + def write_object_to_completion_cache(self, obj): + if self.completion_cache: + self.completion_cache.write_object(obj) + + def clear_completion_cache_for_class(self, obj_class): + if self.completion_cache: + self.completion_cache.clear_class(obj_class) + def __enter__(self): self.client.open_session() return self From 298b06f7e0d378d2d53b130a2f954900cda61810 Mon Sep 17 00:00:00 2001 From: Rick Harris Date: Fri, 20 Jun 2014 11:28:27 -0500 Subject: [PATCH 0532/1705] Sync Oslo's apiclient Oslo's version of apiclient fixes a bug where if `human_id` is `None` it causes `novaclient` to crash, so lets sync it over to fix that bug. Change-Id: I53f174a1d3356c4038dcbdf88f4f9c4ea179418c References-Bug: 1288397 --- novaclient/openstack/common/apiclient/auth.py | 4 +- novaclient/openstack/common/apiclient/base.py | 21 +++-- .../openstack/common/apiclient/client.py | 16 ++-- .../openstack/common/apiclient/exceptions.py | 79 ++++++++++--------- 4 files changed, 71 insertions(+), 49 deletions(-) diff --git a/novaclient/openstack/common/apiclient/auth.py b/novaclient/openstack/common/apiclient/auth.py index ee1348afe..67a1bf7dd 100644 --- a/novaclient/openstack/common/apiclient/auth.py +++ b/novaclient/openstack/common/apiclient/auth.py @@ -213,8 +213,8 @@ def token_and_endpoint(self, endpoint_type, service_type): :type service_type: string :param endpoint_type: Type of endpoint. Possible values: public or publicURL, - internal or internalURL, - admin or adminURL + internal or internalURL, + admin or adminURL :type endpoint_type: string :returns: tuple of token and endpoint strings :raises: EndpointException diff --git a/novaclient/openstack/common/apiclient/base.py b/novaclient/openstack/common/apiclient/base.py index 647f4a720..1f90b88e9 100644 --- a/novaclient/openstack/common/apiclient/base.py +++ b/novaclient/openstack/common/apiclient/base.py @@ -30,6 +30,7 @@ from six.moves.urllib import parse from novaclient.openstack.common.apiclient import exceptions +from novaclient.openstack.common.gettextutils import _ from novaclient.openstack.common import strutils @@ -74,8 +75,8 @@ def run_hooks(cls, hook_type, *args, **kwargs): :param cls: class that registers hooks :param hook_type: hook type, e.g., '__pre_parse_args__' - :param **args: args to be passed to every hook function - :param **kwargs: kwargs to be passed to every hook function + :param args: args to be passed to every hook function + :param kwargs: kwargs to be passed to every hook function """ hook_funcs = cls._hooks_map.get(hook_type) or [] for hook_func in hook_funcs: @@ -219,7 +220,10 @@ def find(self, **kwargs): matches = self.findall(**kwargs) num_matches = len(matches) if num_matches == 0: - msg = "No %s matching %s." % (self.resource_class.__name__, kwargs) + msg = _("No %(name)s matching %(args)s.") % { + 'name': self.resource_class.__name__, + 'args': kwargs + } raise exceptions.NotFound(msg) elif num_matches > 1: raise exceptions.NoUniqueMatch() @@ -373,7 +377,10 @@ def find(self, base_url=None, **kwargs): num = len(rl) if num == 0: - msg = "No %s matching %s." % (self.resource_class.__name__, kwargs) + msg = _("No %(name)s matching %(args)s.") % { + 'name': self.resource_class.__name__, + 'args': kwargs + } raise exceptions.NotFound(404, msg) elif num > 1: raise exceptions.NoUniqueMatch @@ -441,8 +448,10 @@ def __repr__(self): def human_id(self): """Human-readable ID which can be used for bash completion. """ - if self.NAME_ATTR in self.__dict__ and self.HUMAN_ID: - return strutils.to_slug(getattr(self, self.NAME_ATTR)) + if self.HUMAN_ID: + name = getattr(self, self.NAME_ATTR, None) + if name is not None: + return strutils.to_slug(name) return None def _add_details(self, info): diff --git a/novaclient/openstack/common/apiclient/client.py b/novaclient/openstack/common/apiclient/client.py index f573c7b52..a3666daf0 100644 --- a/novaclient/openstack/common/apiclient/client.py +++ b/novaclient/openstack/common/apiclient/client.py @@ -36,6 +36,7 @@ import requests from novaclient.openstack.common.apiclient import exceptions +from novaclient.openstack.common.gettextutils import _ from novaclient.openstack.common import importutils @@ -46,6 +47,7 @@ class HTTPClient(object): """This client handles sending HTTP requests to OpenStack servers. Features: + - share authentication information between several clients to different services (e.g., for compute and image clients); - reissue authentication request for expired tokens; @@ -151,7 +153,7 @@ def request(self, method, url, **kwargs): :param method: method of HTTP request :param url: URL of HTTP request :param kwargs: any other parameter that can be passed to -' requests.Session.request (such as `headers`) or `json` + requests.Session.request (such as `headers`) or `json` that will be encoded as JSON and used as `data` argument """ kwargs.setdefault("headers", kwargs.get("headers", {})) @@ -206,7 +208,7 @@ def client_request(self, client, method, url, **kwargs): :param method: method of HTTP request :param url: URL of HTTP request :param kwargs: any other parameter that can be passed to -' `HTTPClient.request` + `HTTPClient.request` """ filter_args = { @@ -228,7 +230,7 @@ def client_request(self, client, method, url, **kwargs): **filter_args) if not (token and endpoint): raise exceptions.AuthorizationFailure( - "Cannot find endpoint or token for request") + _("Cannot find endpoint or token for request")) old_token_endpoint = (token, endpoint) kwargs.setdefault("headers", {})["X-Auth-Token"] = token @@ -351,8 +353,12 @@ def get_class(api_name, version, version_map): try: client_path = version_map[str(version)] except (KeyError, ValueError): - msg = "Invalid %s client version '%s'. must be one of: %s" % ( - (api_name, version, ', '.join(version_map.keys()))) + msg = _("Invalid %(api_name)s client version '%(version)s'. " + "Must be one of: %(version_map)s") % { + 'api_name': api_name, + 'version': version, + 'version_map': ', '.join(version_map.keys()) + } raise exceptions.UnsupportedVersion(msg) return importutils.import_class(client_path) diff --git a/novaclient/openstack/common/apiclient/exceptions.py b/novaclient/openstack/common/apiclient/exceptions.py index ada1344f5..fc36334ad 100644 --- a/novaclient/openstack/common/apiclient/exceptions.py +++ b/novaclient/openstack/common/apiclient/exceptions.py @@ -25,6 +25,8 @@ import six +from novaclient.openstack.common.gettextutils import _ + class ClientException(Exception): """The base exception class for all exceptions this library raises. @@ -36,7 +38,7 @@ class MissingArgs(ClientException): """Supplied arguments are not sufficient for calling a function.""" def __init__(self, missing): self.missing = missing - msg = "Missing argument(s): %s" % ", ".join(missing) + msg = _("Missing arguments: %s") % ", ".join(missing) super(MissingArgs, self).__init__(msg) @@ -69,16 +71,16 @@ class AuthPluginOptionsMissing(AuthorizationFailure): """Auth plugin misses some options.""" def __init__(self, opt_names): super(AuthPluginOptionsMissing, self).__init__( - "Authentication failed. Missing options: %s" % + _("Authentication failed. Missing options: %s") % ", ".join(opt_names)) self.opt_names = opt_names class AuthSystemNotFound(AuthorizationFailure): - """User has specified a AuthSystem that is not installed.""" + """User has specified an AuthSystem that is not installed.""" def __init__(self, auth_system): super(AuthSystemNotFound, self).__init__( - "AuthSystemNotFound: %s" % repr(auth_system)) + _("AuthSystemNotFound: %s") % repr(auth_system)) self.auth_system = auth_system @@ -101,7 +103,7 @@ class AmbiguousEndpoints(EndpointException): """Found more than one matching endpoint in Service Catalog.""" def __init__(self, endpoints=None): super(AmbiguousEndpoints, self).__init__( - "AmbiguousEndpoints: %s" % repr(endpoints)) + _("AmbiguousEndpoints: %s") % repr(endpoints)) self.endpoints = endpoints @@ -109,7 +111,7 @@ class HttpError(ClientException): """The base exception class for all HTTP exceptions. """ http_status = 0 - message = "HTTP Error" + message = _("HTTP Error") def __init__(self, message=None, details=None, response=None, request_id=None, @@ -129,7 +131,7 @@ def __init__(self, message=None, details=None, class HTTPRedirection(HttpError): """HTTP Redirection.""" - message = "HTTP Redirection" + message = _("HTTP Redirection") class HTTPClientError(HttpError): @@ -137,7 +139,7 @@ class HTTPClientError(HttpError): Exception for cases in which the client seems to have erred. """ - message = "HTTP Client Error" + message = _("HTTP Client Error") class HttpServerError(HttpError): @@ -146,7 +148,7 @@ class HttpServerError(HttpError): Exception for cases in which the server is aware that it has erred or is incapable of performing the request. """ - message = "HTTP Server Error" + message = _("HTTP Server Error") class MultipleChoices(HTTPRedirection): @@ -156,7 +158,7 @@ class MultipleChoices(HTTPRedirection): """ http_status = 300 - message = "Multiple Choices" + message = _("Multiple Choices") class BadRequest(HTTPClientError): @@ -165,7 +167,7 @@ class BadRequest(HTTPClientError): The request cannot be fulfilled due to bad syntax. """ http_status = 400 - message = "Bad Request" + message = _("Bad Request") class Unauthorized(HTTPClientError): @@ -175,7 +177,7 @@ class Unauthorized(HTTPClientError): is required and has failed or has not yet been provided. """ http_status = 401 - message = "Unauthorized" + message = _("Unauthorized") class PaymentRequired(HTTPClientError): @@ -184,7 +186,7 @@ class PaymentRequired(HTTPClientError): Reserved for future use. """ http_status = 402 - message = "Payment Required" + message = _("Payment Required") class Forbidden(HTTPClientError): @@ -194,7 +196,7 @@ class Forbidden(HTTPClientError): to it. """ http_status = 403 - message = "Forbidden" + message = _("Forbidden") class NotFound(HTTPClientError): @@ -204,7 +206,7 @@ class NotFound(HTTPClientError): in the future. """ http_status = 404 - message = "Not Found" + message = _("Not Found") class MethodNotAllowed(HTTPClientError): @@ -214,7 +216,7 @@ class MethodNotAllowed(HTTPClientError): by that resource. """ http_status = 405 - message = "Method Not Allowed" + message = _("Method Not Allowed") class NotAcceptable(HTTPClientError): @@ -224,7 +226,7 @@ class NotAcceptable(HTTPClientError): acceptable according to the Accept headers sent in the request. """ http_status = 406 - message = "Not Acceptable" + message = _("Not Acceptable") class ProxyAuthenticationRequired(HTTPClientError): @@ -233,7 +235,7 @@ class ProxyAuthenticationRequired(HTTPClientError): The client must first authenticate itself with the proxy. """ http_status = 407 - message = "Proxy Authentication Required" + message = _("Proxy Authentication Required") class RequestTimeout(HTTPClientError): @@ -242,7 +244,7 @@ class RequestTimeout(HTTPClientError): The server timed out waiting for the request. """ http_status = 408 - message = "Request Timeout" + message = _("Request Timeout") class Conflict(HTTPClientError): @@ -252,7 +254,7 @@ class Conflict(HTTPClientError): in the request, such as an edit conflict. """ http_status = 409 - message = "Conflict" + message = _("Conflict") class Gone(HTTPClientError): @@ -262,7 +264,7 @@ class Gone(HTTPClientError): not be available again. """ http_status = 410 - message = "Gone" + message = _("Gone") class LengthRequired(HTTPClientError): @@ -272,7 +274,7 @@ class LengthRequired(HTTPClientError): required by the requested resource. """ http_status = 411 - message = "Length Required" + message = _("Length Required") class PreconditionFailed(HTTPClientError): @@ -282,7 +284,7 @@ class PreconditionFailed(HTTPClientError): put on the request. """ http_status = 412 - message = "Precondition Failed" + message = _("Precondition Failed") class RequestEntityTooLarge(HTTPClientError): @@ -291,7 +293,7 @@ class RequestEntityTooLarge(HTTPClientError): The request is larger than the server is willing or able to process. """ http_status = 413 - message = "Request Entity Too Large" + message = _("Request Entity Too Large") def __init__(self, *args, **kwargs): try: @@ -308,7 +310,7 @@ class RequestUriTooLong(HTTPClientError): The URI provided was too long for the server to process. """ http_status = 414 - message = "Request-URI Too Long" + message = _("Request-URI Too Long") class UnsupportedMediaType(HTTPClientError): @@ -318,7 +320,7 @@ class UnsupportedMediaType(HTTPClientError): not support. """ http_status = 415 - message = "Unsupported Media Type" + message = _("Unsupported Media Type") class RequestedRangeNotSatisfiable(HTTPClientError): @@ -328,7 +330,7 @@ class RequestedRangeNotSatisfiable(HTTPClientError): supply that portion. """ http_status = 416 - message = "Requested Range Not Satisfiable" + message = _("Requested Range Not Satisfiable") class ExpectationFailed(HTTPClientError): @@ -337,7 +339,7 @@ class ExpectationFailed(HTTPClientError): The server cannot meet the requirements of the Expect request-header field. """ http_status = 417 - message = "Expectation Failed" + message = _("Expectation Failed") class UnprocessableEntity(HTTPClientError): @@ -347,7 +349,7 @@ class UnprocessableEntity(HTTPClientError): errors. """ http_status = 422 - message = "Unprocessable Entity" + message = _("Unprocessable Entity") class InternalServerError(HttpServerError): @@ -356,7 +358,7 @@ class InternalServerError(HttpServerError): A generic error message, given when no more specific message is suitable. """ http_status = 500 - message = "Internal Server Error" + message = _("Internal Server Error") # NotImplemented is a python keyword. @@ -367,7 +369,7 @@ class HttpNotImplemented(HttpServerError): the ability to fulfill the request. """ http_status = 501 - message = "Not Implemented" + message = _("Not Implemented") class BadGateway(HttpServerError): @@ -377,7 +379,7 @@ class BadGateway(HttpServerError): response from the upstream server. """ http_status = 502 - message = "Bad Gateway" + message = _("Bad Gateway") class ServiceUnavailable(HttpServerError): @@ -386,7 +388,7 @@ class ServiceUnavailable(HttpServerError): The server is currently unavailable. """ http_status = 503 - message = "Service Unavailable" + message = _("Service Unavailable") class GatewayTimeout(HttpServerError): @@ -396,7 +398,7 @@ class GatewayTimeout(HttpServerError): response from the upstream server. """ http_status = 504 - message = "Gateway Timeout" + message = _("Gateway Timeout") class HttpVersionNotSupported(HttpServerError): @@ -405,7 +407,7 @@ class HttpVersionNotSupported(HttpServerError): The server does not support the HTTP protocol version used in the request. """ http_status = 505 - message = "HTTP Version Not Supported" + message = _("HTTP Version Not Supported") # _code_map contains all the classes that have http_status attribute. @@ -423,12 +425,17 @@ def from_response(response, method, url): :param method: HTTP method used for request :param url: URL used for request """ + + req_id = response.headers.get("x-openstack-request-id") + #NOTE(hdd) true for older versions of nova and cinder + if not req_id: + req_id = response.headers.get("x-compute-request-id") kwargs = { "http_status": response.status_code, "response": response, "method": method, "url": url, - "request_id": response.headers.get("x-compute-request-id"), + "request_id": req_id, } if "retry-after" in response.headers: kwargs["retry_after"] = response.headers["retry-after"] From a3d0057da9d97b6ad9d533e0af464898658b780d Mon Sep 17 00:00:00 2001 From: Dirk Mueller Date: Fri, 30 May 2014 12:29:09 +0200 Subject: [PATCH 0533/1705] Fix listing of Server in floating-ip-list Add a translation from instance_id to server_id to accommodate for the adjusted user output. Closes-Bug: #1324857 Change-Id: Ifaa15f6907b5efae7431eb1741b7fae4b3540a8c --- novaclient/v1_1/shell.py | 3 +++ novaclient/v3/shell.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 928da577c..69335d7f5 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1958,6 +1958,9 @@ def do_clear_password(cs, args): def _print_floating_ip_list(floating_ips): + convert = [('instance_id', 'server_id')] + _translate_keys(floating_ips, convert) + utils.print_list(floating_ips, ['Ip', 'Server Id', 'Fixed Ip', 'Pool']) diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index f206f9263..d0407e344 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -1543,6 +1543,9 @@ def do_clear_password(cs, args): def _print_floating_ip_list(floating_ips): + convert = [('instance_id', 'server_id')] + _translate_keys(floating_ips, convert) + utils.print_list(floating_ips, ['Ip', 'Server Id', 'Fixed Ip', 'Pool']) From 2b54bbc8bb7e6cce49e570b534ba78baeb811e75 Mon Sep 17 00:00:00 2001 From: vagrant Date: Thu, 12 Jun 2014 06:45:55 +0000 Subject: [PATCH 0534/1705] add disk bus and device type to volume attach This change adds the optional parameters: disk bus and device type to the volume-attach command for the V3 API. DocImpact Closes-Bug: #1303875 Change-Id: I4fd07726887d08e5b669139b559bdb2c2d21826e --- novaclient/tests/v3/fakes.py | 9 +++++++-- novaclient/tests/v3/test_volumes.py | 17 +++++++++++++++++ novaclient/v3/shell.py | 13 ++++++++++++- novaclient/v3/volumes.py | 9 ++++++++- 4 files changed, 44 insertions(+), 4 deletions(-) diff --git a/novaclient/tests/v3/fakes.py b/novaclient/tests/v3/fakes.py index 64af4b747..8bb9ffbd1 100644 --- a/novaclient/tests/v3/fakes.py +++ b/novaclient/tests/v3/fakes.py @@ -211,9 +211,10 @@ def post_servers_1234_action(self, body, **kw): 'create_image': ['name', 'metadata'], 'migrate_live': ['host', 'block_migration', 'disk_over_commit'], 'create_backup': ['name', 'backup_type', 'rotation'], - 'attach': ['volume_id', 'device'], 'detach': ['volume_id'], 'swap_volume_attachment': ['old_volume_id', 'new_volume_id']} + body_params_check_superset = { + 'attach': ['volume_id', 'device']} assert len(body.keys()) == 1 action = list(body)[0] @@ -231,6 +232,9 @@ def post_servers_1234_action(self, body, **kw): if action in body_params_check_exact: assert set(body[action]) == set(body_params_check_exact[action]) + if action in body_params_check_superset: + assert set(body[action]) >= set(body_params_check_superset[action]) + if action == 'reboot': assert body[action]['type'] in ['HARD', 'SOFT'] elif action == 'confirm_resize': @@ -241,7 +245,8 @@ def post_servers_1234_action(self, body, **kw): if action not in set.union(set(body_is_none_list), set(body_params_check_exact.keys()), - set(body_param_check_exists.keys())): + set(body_param_check_exists.keys()), + set(body_params_check_superset.keys())): raise AssertionError("Unexpected server action: %s" % action) return (resp, _headers, _body) diff --git a/novaclient/tests/v3/test_volumes.py b/novaclient/tests/v3/test_volumes.py index 773d2bb1d..cfe6d9eb3 100644 --- a/novaclient/tests/v3/test_volumes.py +++ b/novaclient/tests/v3/test_volumes.py @@ -33,6 +33,23 @@ def test_attach_server_volume(self): ) self.cs.assert_called('POST', '/servers/1234/action') + def test_attach_server_volume_disk_bus_device_type(self): + volume_id = '15e59938-07d5-11e1-90e3-e3dffe0c5983' + device = '/dev/vdb' + disk_bus = 'ide' + device_type = 'cdrom' + self.cs.volumes.attach_server_volume(server=1234, + volume_id=volume_id, + device=device, + disk_bus=disk_bus, + device_type=device_type) + body_params = {'volume_id': volume_id, + 'device': device, + 'disk_bus': disk_bus, + 'device_type': device_type} + body = {'attach': body_params} + self.cs.assert_called('POST', '/servers/1234/action', body) + def test_update_server_volume(self): vol_id = '15e59938-07d5-11e1-90e3-e3dffe0c5983' self.cs.volumes.update_server_volume( diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 4a12eaaa4..1c4d09080 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -1432,13 +1432,24 @@ def _translate_availability_zone_keys(collection): @utils.arg('device', metavar='', default=None, nargs='?', help='Name of the device e.g. /dev/vdb. ' 'Use "auto" for autoassign (if supported)') +@utils.arg('disk_bus', + metavar='', + default=None, + nargs='?', + help='The disk bus e.g. ide of the volume (optional).') +@utils.arg('device_type', + metavar='', + default=None, + nargs='?', + help='The device type e.g. cdrom of the volume (optional).') def do_volume_attach(cs, args): """Attach a volume to a server.""" if args.device == 'auto': args.device = None cs.volumes.attach_server_volume(_find_server(cs, args.server).id, - args.volume, args.device) + args.volume, args.device, args.disk_bus, + args.device_type) @utils.arg('server', diff --git a/novaclient/v3/volumes.py b/novaclient/v3/volumes.py index ec061a319..53cc05f6c 100644 --- a/novaclient/v3/volumes.py +++ b/novaclient/v3/volumes.py @@ -24,16 +24,23 @@ class VolumeManager(base.Manager): Manage :class:`Volume` resources. """ - def attach_server_volume(self, server, volume_id, device): + def attach_server_volume(self, server, volume_id, device, + disk_bus=None, device_type=None): """ Attach a volume identified by the volume ID to the given server ID :param server: The server (or it's ID) :param volume_id: The ID of the volume to attach. :param device: The device name + :param disk_bus: The disk bus of the volume + :param device_type: The device type of the volume :rtype: :class:`Volume` """ body = {'volume_id': volume_id, 'device': device} + if disk_bus: + body['disk_bus'] = disk_bus + if device_type: + body['device_type'] = device_type return self._action('attach', server, body) def update_server_volume(self, server, old_volume_id, new_volume_id): From 98ef56332d7fdb551f8a4beb6d411ce141583c10 Mon Sep 17 00:00:00 2001 From: Jamie Lennox Date: Fri, 9 May 2014 16:34:09 +1000 Subject: [PATCH 0535/1705] Convert floating IP pool tests to httpretty Change-Id: Ic9332ddd30a0bc3ffc5ed7c046f60ded3f940e85 blueprint: httpretty-testing --- novaclient/tests/fixture_data/floatingips.py | 18 ++++++++++++++++++ .../tests/v1_1/test_floating_ip_pools.py | 13 +++++++------ 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/novaclient/tests/fixture_data/floatingips.py b/novaclient/tests/fixture_data/floatingips.py index a74b82913..8dd590427 100644 --- a/novaclient/tests/fixture_data/floatingips.py +++ b/novaclient/tests/fixture_data/floatingips.py @@ -196,3 +196,21 @@ def post_os_floating_ips_bulk(request, url, headers): httpretty.register_uri(httpretty.POST, self.url(), body=post_os_floating_ips_bulk, content_type='application/json') + + +class PoolsFixture(base.Fixture): + + base_url = 'os-floating-ip-pools' + + def setUp(self): + super(PoolsFixture, self).setUp() + + get_os_floating_ip_pools = { + 'floating_ip_pools': [ + {'name': 'foo'}, + {'name': 'bar'} + ] + } + httpretty.register_uri(httpretty.GET, self.url(), + body=jsonutils.dumps(get_os_floating_ip_pools), + content_type='application/json') diff --git a/novaclient/tests/v1_1/test_floating_ip_pools.py b/novaclient/tests/v1_1/test_floating_ip_pools.py index 51124cfc9..efdfabf30 100644 --- a/novaclient/tests/v1_1/test_floating_ip_pools.py +++ b/novaclient/tests/v1_1/test_floating_ip_pools.py @@ -14,18 +14,19 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient.tests.fixture_data import client +from novaclient.tests.fixture_data import floatingips as data from novaclient.tests import utils -from novaclient.tests.v1_1 import fakes from novaclient.v1_1 import floating_ip_pools -cs = fakes.FakeClient() +class TestFloatingIPPools(utils.FixturedTestCase): - -class TestFloatingIPPools(utils.TestCase): + client_fixture_class = client.V1 + data_fixture_class = data.PoolsFixture def test_list_floating_ips(self): - fl = cs.floating_ip_pools.list() - cs.assert_called('GET', '/os-floating-ip-pools') + fl = self.cs.floating_ip_pools.list() + self.assert_called('GET', '/os-floating-ip-pools') [self.assertIsInstance(f, floating_ip_pools.FloatingIPPool) for f in fl] From 2acfb9ba7eab3b63758dfdbb065ca3d5335c3485 Mon Sep 17 00:00:00 2001 From: Jamie Lennox Date: Mon, 26 May 2014 16:33:16 +1000 Subject: [PATCH 0536/1705] Convert server tests to httpretty Change-Id: If18980fbe94cee389b7dab9925fdad8f920f9885 blueprint: httpretty-testing --- novaclient/tests/fixture_data/servers.py | 610 +++++++++++++++++++++++ novaclient/tests/v1_1/test_servers.py | 466 ++++++++--------- novaclient/tests/v3/test_servers.py | 370 +++++++------- 3 files changed, 1032 insertions(+), 414 deletions(-) create mode 100644 novaclient/tests/fixture_data/servers.py diff --git a/novaclient/tests/fixture_data/servers.py b/novaclient/tests/fixture_data/servers.py new file mode 100644 index 000000000..e862b4f69 --- /dev/null +++ b/novaclient/tests/fixture_data/servers.py @@ -0,0 +1,610 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import httpretty + +from novaclient.openstack.common import jsonutils +from novaclient.tests import fakes +from novaclient.tests.fixture_data import base + + +class Base(base.Fixture): + + base_url = 'servers' + + def setUp(self): + super(Base, self).setUp() + + get_servers = { + "servers": [ + {'id': 1234, 'name': 'sample-server'}, + {'id': 5678, 'name': 'sample-server2'} + ] + } + + httpretty.register_uri(httpretty.GET, self.url(), + body=jsonutils.dumps(get_servers), + content_type='application/json') + + self.server_1234 = { + "id": 1234, + "name": "sample-server", + "image": { + "id": 2, + "name": "sample image", + }, + "flavor": { + "id": 1, + "name": "256 MB Server", + }, + "hostId": "e4d909c290d0fb1ca068ffaddf22cbd0", + "status": "BUILD", + "progress": 60, + "addresses": { + "public": [{ + "version": 4, + "addr": "1.2.3.4", + }, + { + "version": 4, + "addr": "5.6.7.8", + }], + "private": [{ + "version": 4, + "addr": "10.11.12.13", + }], + }, + "metadata": { + "Server Label": "Web Head 1", + "Image Version": "2.1" + }, + "OS-EXT-SRV-ATTR:host": "computenode1", + "security_groups": [{ + 'id': 1, 'name': 'securitygroup1', + 'description': 'FAKE_SECURITY_GROUP', + 'tenant_id': '4ffc664c198e435e9853f2538fbcd7a7' + }], + "OS-EXT-MOD:some_thing": "mod_some_thing_value", + } + + self.server_5678 = { + "id": 5678, + "name": "sample-server2", + "image": { + "id": 2, + "name": "sample image", + }, + "flavor": { + "id": 1, + "name": "256 MB Server", + }, + "hostId": "9e107d9d372bb6826bd81d3542a419d6", + "status": "ACTIVE", + "addresses": { + "public": [{ + "version": 4, + "addr": "4.5.6.7", + }, + { + "version": 4, + "addr": "5.6.9.8", + }], + "private": [{ + "version": 4, + "addr": "10.13.12.13", + }], + }, + "metadata": { + "Server Label": "DB 1" + }, + "OS-EXT-SRV-ATTR:host": "computenode2", + "security_groups": [{ + 'id': 1, 'name': 'securitygroup1', + 'description': 'FAKE_SECURITY_GROUP', + 'tenant_id': '4ffc664c198e435e9853f2538fbcd7a7' + }, + { + 'id': 2, 'name': 'securitygroup2', + 'description': 'ANOTHER_FAKE_SECURITY_GROUP', + 'tenant_id': '4ffc664c198e435e9853f2538fbcd7a7' + }], + } + + self.server_9012 = { + "id": 9012, + "name": "sample-server3", + "image": "", + "flavor": { + "id": 1, + "name": "256 MB Server", + }, + "hostId": "9e107d9d372bb6826bd81d3542a419d6", + "status": "ACTIVE", + "addresses": { + "public": [{ + "version": 4, + "addr": "4.5.6.7", + }, + { + "version": 4, + "addr": "5.6.9.8", + }], + "private": [{ + "version": 4, + "addr": "10.13.12.13", + }], + }, + "metadata": { + "Server Label": "DB 1" + } + } + + servers = [self.server_1234, self.server_5678, self.server_9012] + get_servers_detail = {"servers": servers} + + httpretty.register_uri(httpretty.GET, self.url('detail'), + body=jsonutils.dumps(get_servers_detail), + content_type='application/json') + + self.server_1235 = self.server_1234.copy() + self.server_1235['id'] = 1235 + self.server_1235['status'] = 'error' + self.server_1235['fault'] = {'message': 'something went wrong!'} + servers.append(self.server_1235) + + for s in servers: + httpretty.register_uri(httpretty.GET, self.url(s['id']), + body=jsonutils.dumps({'server': s}), + content_type='application/json') + + for s in (1234, 5678): + httpretty.register_uri(httpretty.DELETE, self.url(s), status=202) + + for k in ('test_key', 'key1', 'key2'): + httpretty.register_uri(httpretty.DELETE, + self.url(1234, 'metadata', k), + status=204) + + metadata1 = jsonutils.dumps({'metadata': {'test_key': 'test_value'}}) + httpretty.register_uri(httpretty.POST, self.url(1234, 'metadata'), + body=metadata1, status=200, + content_type='application/json') + httpretty.register_uri(httpretty.PUT, + self.url(1234, 'metadata', 'test_key'), + body=metadata1, status=200, + content_type='application/json') + + self.diagnostic = jsonutils.dumps({'data': 'Fake diagnostics'}) + + metadata2 = jsonutils.dumps({'metadata': {'key1': 'val1'}}) + for u in ('uuid1', 'uuid2', 'uuid3', 'uuid4'): + httpretty.register_uri(httpretty.POST, self.url(u, 'metadata'), + body=metadata2, status=204) + httpretty.register_uri(httpretty.DELETE, + self.url(u, 'metadata', 'key1'), + body=self.diagnostic, + content_type='application/json') + + get_security_groups = { + "security_groups": [{ + 'id': 1, + 'name': 'securitygroup1', + 'description': 'FAKE_SECURITY_GROUP', + 'tenant_id': '4ffc664c198e435e9853f2538fbcd7a7', + 'rules': []}] + } + + httpretty.register_uri(httpretty.GET, + self.url('1234', 'os-security-groups'), + body=jsonutils.dumps(get_security_groups), + status=200) + + httpretty.register_uri(httpretty.POST, self.url(), + body=self.post_servers, + content_type='application/json') + + httpretty.register_uri(httpretty.POST, self.url('1234', 'action'), + body=self.post_servers_1234_action, + content_type='application/json') + + get_os_interface = { + "interfaceAttachments": [ + { + "port_state": "ACTIVE", + "net_id": "net-id-1", + "port_id": "port-id-1", + "mac_address": "aa:bb:cc:dd:ee:ff", + "fixed_ips": [{"ip_address": "1.2.3.4"}], + }, + { + "port_state": "ACTIVE", + "net_id": "net-id-1", + "port_id": "port-id-1", + "mac_address": "aa:bb:cc:dd:ee:ff", + "fixed_ips": [{"ip_address": "1.2.3.4"}], + } + ] + } + + httpretty.register_uri(httpretty.GET, + self.url('1234', 'os-interface'), + body=jsonutils.dumps(get_os_interface), + content_type='application/json') + + interface_data = {'interfaceAttachment': {}} + httpretty.register_uri(httpretty.POST, + self.url('1234', 'os-interface'), + body=jsonutils.dumps(interface_data), + content_type='application/json') + + def put_servers_1234(request, url, headers): + body = jsonutils.loads(request.body.decode('utf-8')) + assert list(body) == ['server'] + fakes.assert_has_keys(body['server'], + optional=['name', 'adminPass']) + return 204, headers, request.body + + httpretty.register_uri(httpretty.PUT, self.url(1234), + body=put_servers_1234, + content_type='application/json') + + def post_os_volumes_boot(request, url, headers): + body = jsonutils.loads(request.body.decode('utf-8')) + assert (set(body.keys()) <= + set(['server', 'os:scheduler_hints'])) + + fakes.assert_has_keys(body['server'], + required=['name', 'flavorRef'], + optional=['imageRef']) + + data = body['server'] + + # Require one, and only one, of the keys for bdm + if 'block_device_mapping' not in data: + if 'block_device_mapping_v2' not in data: + msg = "missing required keys: 'block_device_mapping'" + raise AssertionError(msg) + elif 'block_device_mapping_v2' in data: + msg = "found extra keys: 'block_device_mapping'" + raise AssertionError(msg) + + return 202, headers, jsonutils.dumps({'server': self.server_9012}) + + # NOTE(jamielennox): hack to make os_volumes mock go to the right place + base_url = self.base_url + self.base_url = None + httpretty.register_uri(httpretty.POST, self.url('os-volumes_boot'), + body=post_os_volumes_boot, + content_type='application/json') + self.base_url = base_url + + # + # Server password + # + + httpretty.register_uri(httpretty.DELETE, + self.url(1234, 'os-server-password'), + status=202) + + +class V1(Base): + + def setUp(self): + super(V1, self).setUp() + + # + # Server Addresses + # + + add = self.server_1234['addresses'] + httpretty.register_uri(httpretty.GET, self.url(1234, 'ips'), + jsonutils.dumps({'addresses': add}), + content_type='application/json') + + httpretty.register_uri(httpretty.GET, self.url(1234, 'ips', 'public'), + jsonutils.dumps({'public': add['public']}), + content_type='application/json') + + httpretty.register_uri(httpretty.GET, self.url(1234, 'ips', 'private'), + jsonutils.dumps({'private': add['private']}), + content_type='application/json') + + httpretty.register_uri(httpretty.DELETE, + self.url(1234, 'ips', 'public', '1.2.3.4'), + status=202) + + httpretty.register_uri(httpretty.GET, + self.url('1234', 'diagnostics'), + body=self.diagnostic, + status=200) + + httpretty.register_uri(httpretty.DELETE, + self.url('1234', 'os-interface', 'port-id')) + + # Testing with the following password and key + # + # Clear password: FooBar123 + # + # RSA Private Key: novaclient/tests/idfake.pem + # + # Encrypted password + # OIuEuQttO8Rk93BcKlwHQsziDAnkAm/V6V8VPToA8ZeUaUBWwS0gwo2K6Y61Z96r + # qG447iRz0uTEEYq3RAYJk1mh3mMIRVl27t8MtIecR5ggVVbz1S9AwXJQypDKl0ho + # QFvhCBcMWPohyGewDJOhDbtuN1IoFI9G55ZvFwCm5y7m7B2aVcoLeIsJZE4PLsIw + # /y5a6Z3/AoJZYGG7IH5WN88UROU3B9JZGFB2qtPLQTOvDMZLUhoPRIJeHiVSlo1N + # tI2/++UsXVg3ow6ItqCJGgdNuGG5JB+bslDHWPxROpesEIHdczk46HCpHQN8f1sk + # Hi/fmZZNQQqj1Ijq0caOIw== + + get_server_password = {'password': + 'OIuEuQttO8Rk93BcKlwHQsziDAnkAm/V6V8VPToA8ZeUaUBWwS0gwo2K6Y61Z96r' + 'qG447iRz0uTEEYq3RAYJk1mh3mMIRVl27t8MtIecR5ggVVbz1S9AwXJQypDKl0ho' + 'QFvhCBcMWPohyGewDJOhDbtuN1IoFI9G55ZvFwCm5y7m7B2aVcoLeIsJZE4PLsIw' + '/y5a6Z3/AoJZYGG7IH5WN88UROU3B9JZGFB2qtPLQTOvDMZLUhoPRIJeHiVSlo1N' + 'tI2/++UsXVg3ow6ItqCJGgdNuGG5JB+bslDHWPxROpesEIHdczk46HCpHQN8f1sk' + 'Hi/fmZZNQQqj1Ijq0caOIw=='} + httpretty.register_uri(httpretty.GET, + self.url(1234, 'os-server-password'), + jsonutils.dumps(get_server_password)) + + def post_servers(self, request, url, headers): + body = jsonutils.loads(request.body.decode('utf-8')) + assert (set(body.keys()) <= + set(['server', 'os:scheduler_hints'])) + fakes.assert_has_keys(body['server'], + required=['name', 'imageRef', 'flavorRef'], + optional=['metadata', 'personality']) + if 'personality' in body['server']: + for pfile in body['server']['personality']: + fakes.assert_has_keys(pfile, required=['path', 'contents']) + if body['server']['name'] == 'some-bad-server': + body = self.server_1235 + else: + body = self.server_1234 + + return 202, headers, jsonutils.dumps({'server': body}) + + def post_servers_1234_action(self, request, url, headers): + _body = '' + body = jsonutils.loads(request.body.decode('utf-8')) + resp = 202 + assert len(body.keys()) == 1 + action = list(body)[0] + if action == 'reboot': + assert list(body[action]) == ['type'] + assert body[action]['type'] in ['HARD', 'SOFT'] + elif action == 'rebuild': + body = body[action] + adminPass = body.get('adminPass', 'randompassword') + assert 'imageRef' in body + _body = self.server_1234.copy() + _body['adminPass'] = adminPass + elif action == 'resize': + keys = body[action].keys() + assert 'flavorRef' in keys + elif action == 'confirmResize': + assert body[action] is None + # This one method returns a different response code + return 204, headers, '' + elif action == 'revertResize': + assert body[action] is None + elif action == 'migrate': + assert body[action] is None + elif action == 'os-stop': + assert body[action] is None + elif action == 'os-start': + assert body[action] is None + elif action == 'forceDelete': + assert body[action] is None + elif action == 'restore': + assert body[action] is None + elif action == 'pause': + assert body[action] is None + elif action == 'unpause': + assert body[action] is None + elif action == 'lock': + assert body[action] is None + elif action == 'unlock': + assert body[action] is None + elif action == 'rescue': + assert body[action] is None + _body = {'Password': 'RescuePassword'} + elif action == 'unrescue': + assert body[action] is None + elif action == 'resume': + assert body[action] is None + elif action == 'suspend': + assert body[action] is None + elif action == 'lock': + assert body[action] is None + elif action == 'unlock': + assert body[action] is None + elif action == 'shelve': + assert body[action] is None + elif action == 'shelveOffload': + assert body[action] is None + elif action == 'unshelve': + assert body[action] is None + elif action == 'addFixedIp': + assert list(body[action]) == ['networkId'] + elif action == 'removeFixedIp': + assert list(body[action]) == ['address'] + elif action == 'addFloatingIp': + assert (list(body[action]) == ['address'] or + sorted(list(body[action])) == ['address', + 'fixed_address']) + elif action == 'removeFloatingIp': + assert list(body[action]) == ['address'] + elif action == 'createImage': + assert set(body[action].keys()) == set(['name', 'metadata']) + headers['location'] = "http://blah/images/456" + elif action == 'changePassword': + assert list(body[action]) == ['adminPass'] + elif action == 'os-getConsoleOutput': + assert list(body[action]) == ['length'] + return 202, headers, jsonutils.dumps({'output': 'foo'}) + elif action == 'os-getVNCConsole': + assert list(body[action]) == ['type'] + elif action == 'os-getSPICEConsole': + assert list(body[action]) == ['type'] + elif action == 'os-getRDPConsole': + assert list(body[action]) == ['type'] + elif action == 'os-migrateLive': + assert set(body[action].keys()) == set(['host', + 'block_migration', + 'disk_over_commit']) + elif action == 'os-resetState': + assert list(body[action]) == ['state'] + elif action == 'resetNetwork': + assert body[action] is None + elif action == 'addSecurityGroup': + assert list(body[action]) == ['name'] + elif action == 'removeSecurityGroup': + assert list(body[action]) == ['name'] + elif action == 'createBackup': + assert set(body[action]) == set(['name', + 'backup_type', + 'rotation']) + elif action == 'evacuate': + keys = list(body[action]) + if 'adminPass' in keys: + keys.remove('adminPass') + assert set(keys) == set(['host', 'onSharedStorage']) + else: + raise AssertionError("Unexpected server action: %s" % action) + return resp, headers, jsonutils.dumps({'server': _body}) + + +class V3(Base): + + def setUp(self): + super(V3, self).setUp() + + get_interfaces = { + "interface_attachments": [ + { + "port_state": "ACTIVE", + "net_id": "net-id-1", + "port_id": "port-id-1", + "mac_address": "aa:bb:cc:dd:ee:ff", + "fixed_ips": [{"ip_address": "1.2.3.4"}], + }, + { + "port_state": "ACTIVE", + "net_id": "net-id-1", + "port_id": "port-id-1", + "mac_address": "aa:bb:cc:dd:ee:ff", + "fixed_ips": [{"ip_address": "1.2.3.4"}], + } + ] + } + + httpretty.register_uri(httpretty.GET, + self.url('1234', 'os-attach-interfaces'), + body=jsonutils.dumps(get_interfaces), + content_type='application/json') + + attach_body = {'interface_attachment': {}} + httpretty.register_uri(httpretty.POST, + self.url('1234', 'os-attach-interfaces'), + body=jsonutils.dumps(attach_body), + content_type='application/json') + + httpretty.register_uri(httpretty.GET, + self.url('1234', 'os-server-diagnostics'), + body=self.diagnostic, + status=200) + + httpretty.register_uri(httpretty.DELETE, + self.url('1234', 'os-attach-interfaces', + 'port-id')) + + httpretty.register_uri(httpretty.GET, + self.url(1234, 'os-server-password'), + jsonutils.dumps({'password': ''})) + + def post_servers(self, request, url, headers): + body = jsonutils.loads(request.body.decode('utf-8')) + assert set(body.keys()) <= set(['server']) + fakes.assert_has_keys(body['server'], + required=['name', 'image_ref', 'flavor_ref'], + optional=['metadata', 'personality', + 'os-scheduler-hints:scheduler_hints']) + if body['server']['name'] == 'some-bad-server': + body = self.server_1235 + else: + body = self.server_1234 + + return 202, headers, jsonutils.dumps({'server': body}) + + def post_servers_1234_action(self, request, url, headers): + resp = 202 + body_is_none_list = [ + 'revert_resize', 'migrate', 'stop', 'start', 'force_delete', + 'restore', 'pause', 'unpause', 'lock', 'unlock', 'unrescue', + 'resume', 'suspend', 'lock', 'unlock', 'shelve', 'shelve_offload', + 'unshelve', 'reset_network', 'rescue', 'confirm_resize'] + body_return_map = { + 'rescue': {'admin_password': 'RescuePassword'}, + 'get_console_output': {'output': 'foo'}, + 'rebuild': {'server': self.server_1234}, + } + body_param_check_exists = { + 'rebuild': 'image_ref', + 'resize': 'flavor_ref'} + body_params_check_exact = { + 'reboot': ['type'], + 'add_fixed_ip': ['network_id'], + 'evacuate': ['host', 'on_shared_storage'], + 'remove_fixed_ip': ['address'], + 'change_password': ['admin_password'], + 'get_console_output': ['length'], + 'get_vnc_console': ['type'], + 'get_spice_console': ['type'], + 'reset_state': ['state'], + 'create_image': ['name', 'metadata'], + 'migrate_live': ['host', 'block_migration', 'disk_over_commit'], + 'create_backup': ['name', 'backup_type', 'rotation'], + 'attach': ['volume_id', 'device'], + 'detach': ['volume_id'], + 'swap_volume_attachment': ['old_volume_id', 'new_volume_id']} + + body = jsonutils.loads(request.body.decode('utf-8')) + assert len(body.keys()) == 1 + action = list(body)[0] + _body = body_return_map.get(action, '') + + if action in body_is_none_list: + assert body[action] is None + + if action in body_param_check_exists: + assert body_param_check_exists[action] in body[action] + + if action == 'evacuate': + body[action].pop('admin_password', None) + + if action in body_params_check_exact: + assert set(body[action]) == set(body_params_check_exact[action]) + + if action == 'reboot': + assert body[action]['type'] in ['HARD', 'SOFT'] + elif action == 'confirm_resize': + # This one method returns a different response code + resp = 204 + elif action == 'create_image': + headers['location'] = "http://blah/images/456" + + if action not in set.union(set(body_is_none_list), + set(body_params_check_exact.keys()), + set(body_param_check_exists.keys())): + raise AssertionError("Unexpected server action: %s" % action) + + return resp, headers, jsonutils.dumps(_body) diff --git a/novaclient/tests/v1_1/test_servers.py b/novaclient/tests/v1_1/test_servers.py index 728732474..e7c38ddd6 100644 --- a/novaclient/tests/v1_1/test_servers.py +++ b/novaclient/tests/v1_1/test_servers.py @@ -12,52 +12,59 @@ # License for the specific language governing permissions and limitations # under the License. +import httpretty import mock import six from novaclient import exceptions +from novaclient.tests.fixture_data import client +from novaclient.tests.fixture_data import floatingips +from novaclient.tests.fixture_data import servers as data from novaclient.tests import utils -from novaclient.tests.v1_1 import fakes from novaclient.v1_1 import servers -cs = fakes.FakeClient() +class ServersTest(utils.FixturedTestCase): + client_fixture_class = client.V1 + data_fixture_class = data.V1 -class ServersTest(utils.TestCase): + def setUp(self): + super(ServersTest, self).setUp() + self.useFixture(floatingips.FloatingFixture()) def test_list_servers(self): - sl = cs.servers.list() - cs.assert_called('GET', '/servers/detail') + sl = self.cs.servers.list() + self.assert_called('GET', '/servers/detail') [self.assertIsInstance(s, servers.Server) for s in sl] def test_list_servers_undetailed(self): - sl = cs.servers.list(detailed=False) - cs.assert_called('GET', '/servers') + sl = self.cs.servers.list(detailed=False) + self.assert_called('GET', '/servers') [self.assertIsInstance(s, servers.Server) for s in sl] def test_list_servers_with_marker_limit(self): - sl = cs.servers.list(marker=1234, limit=2) - cs.assert_called('GET', '/servers/detail?limit=2&marker=1234') + sl = self.cs.servers.list(marker=1234, limit=2) + self.assert_called('GET', '/servers/detail?limit=2&marker=1234') for s in sl: self.assertIsInstance(s, servers.Server) def test_get_server_details(self): - s = cs.servers.get(1234) - cs.assert_called('GET', '/servers/1234') + s = self.cs.servers.get(1234) + self.assert_called('GET', '/servers/1234') self.assertIsInstance(s, servers.Server) self.assertEqual(s.id, 1234) self.assertEqual(s.status, 'BUILD') def test_get_server_promote_details(self): - s1 = cs.servers.list(detailed=False)[0] - s2 = cs.servers.list(detailed=True)[0] + s1 = self.cs.servers.list(detailed=False)[0] + s2 = self.cs.servers.list(detailed=True)[0] self.assertNotEqual(s1._info, s2._info) s1.get() self.assertEqual(s1._info, s2._info) def test_create_server(self): - s = cs.servers.create( + s = self.cs.servers.create( name="My server", image=1, flavor=1, @@ -69,11 +76,11 @@ def test_create_server(self): '/tmp/foo.txt': six.StringIO('data'), # a stream } ) - cs.assert_called('POST', '/servers') + self.assert_called('POST', '/servers') self.assertIsInstance(s, servers.Server) def test_create_server_boot_from_volume_with_nics(self): - old_boot = cs.servers._boot + old_boot = self.cs.servers._boot nics = [{'net-id': '11111111-1111-1111-1111-111111111111', 'v4-fixed-ip': '10.0.0.7'}] @@ -87,9 +94,9 @@ def wrapped_boot(url, key, *boot_args, **boot_kwargs): self.assertEqual(boot_kwargs['nics'], nics) return old_boot(url, key, *boot_args, **boot_kwargs) - @mock.patch.object(cs.servers, '_boot', wrapped_boot) + @mock.patch.object(self.cs.servers, '_boot', wrapped_boot) def test_create_server_from_volume(): - s = cs.servers.create( + s = self.cs.servers.create( name="My server", image=1, flavor=1, @@ -99,13 +106,13 @@ def test_create_server_from_volume(): block_device_mapping=bdm, nics=nics ) - cs.assert_called('POST', '/os-volumes_boot') + self.assert_called('POST', '/os-volumes_boot') self.assertIsInstance(s, servers.Server) test_create_server_from_volume() def test_create_server_boot_with_nics_ipv6(self): - old_boot = cs.servers._boot + old_boot = self.cs.servers._boot nics = [{'net-id': '11111111-1111-1111-1111-111111111111', 'v6-fixed-ip': '2001:db9:0:1::10'}] @@ -113,8 +120,8 @@ def wrapped_boot(url, key, *boot_args, **boot_kwargs): self.assertEqual(boot_kwargs['nics'], nics) return old_boot(url, key, *boot_args, **boot_kwargs) - with mock.patch.object(cs.servers, '_boot', wrapped_boot): - s = cs.servers.create( + with mock.patch.object(self.cs.servers, '_boot', wrapped_boot): + s = self.cs.servers.create( name="My server", image=1, flavor=1, @@ -123,11 +130,11 @@ def wrapped_boot(url, key, *boot_args, **boot_kwargs): key_name="fakekey", nics=nics ) - cs.assert_called('POST', '/servers') + self.assert_called('POST', '/servers') self.assertIsInstance(s, servers.Server) def test_create_server_userdata_file_object(self): - s = cs.servers.create( + s = self.cs.servers.create( name="My server", image=1, flavor=1, @@ -138,11 +145,11 @@ def test_create_server_userdata_file_object(self): '/tmp/foo.txt': six.StringIO('data'), # a stream }, ) - cs.assert_called('POST', '/servers') + self.assert_called('POST', '/servers') self.assertIsInstance(s, servers.Server) def test_create_server_userdata_unicode(self): - s = cs.servers.create( + s = self.cs.servers.create( name="My server", image=1, flavor=1, @@ -154,11 +161,11 @@ def test_create_server_userdata_unicode(self): '/tmp/foo.txt': six.StringIO('data'), # a stream }, ) - cs.assert_called('POST', '/servers') + self.assert_called('POST', '/servers') self.assertIsInstance(s, servers.Server) def test_create_server_userdata_utf8(self): - s = cs.servers.create( + s = self.cs.servers.create( name="My server", image=1, flavor=1, @@ -170,22 +177,21 @@ def test_create_server_userdata_utf8(self): '/tmp/foo.txt': six.StringIO('data'), # a stream }, ) - cs.assert_called('POST', '/servers') + self.assert_called('POST', '/servers') self.assertIsInstance(s, servers.Server) def _create_disk_config(self, disk_config): - s = cs.servers.create( + s = self.cs.servers.create( name="My server", image=1, flavor=1, disk_config=disk_config ) - cs.assert_called('POST', '/servers') + self.assert_called('POST', '/servers') self.assertIsInstance(s, servers.Server) # verify disk config param was used in the request: - last_request = cs.client.callstack[-1] - body = last_request[-1] + body = httpretty.last_request().parsed_body server = body['server'] self.assertTrue('OS-DCF:diskConfig' in server) self.assertEqual(disk_config, server['OS-DCF:diskConfig']) @@ -197,86 +203,84 @@ def test_create_server_disk_config_manual(self): self._create_disk_config('MANUAL') def test_update_server(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) # Update via instance s.update(name='hi') - cs.assert_called('PUT', '/servers/1234') + self.assert_called('PUT', '/servers/1234') s.update(name='hi') - cs.assert_called('PUT', '/servers/1234') + self.assert_called('PUT', '/servers/1234') # Silly, but not an error s.update() # Update via manager - cs.servers.update(s, name='hi') - cs.assert_called('PUT', '/servers/1234') + self.cs.servers.update(s, name='hi') + self.assert_called('PUT', '/servers/1234') def test_delete_server(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.delete() - cs.assert_called('DELETE', '/servers/1234') - cs.servers.delete(1234) - cs.assert_called('DELETE', '/servers/1234') - cs.servers.delete(s) - cs.assert_called('DELETE', '/servers/1234') + self.assert_called('DELETE', '/servers/1234') + self.cs.servers.delete(1234) + self.assert_called('DELETE', '/servers/1234') + self.cs.servers.delete(s) + self.assert_called('DELETE', '/servers/1234') def test_delete_server_meta(self): - cs.servers.delete_meta(1234, ['test_key']) - cs.assert_called('DELETE', '/servers/1234/metadata/test_key') + self.cs.servers.delete_meta(1234, ['test_key']) + self.assert_called('DELETE', '/servers/1234/metadata/test_key') def test_set_server_meta(self): - cs.servers.set_meta(1234, {'test_key': 'test_value'}) - cs.assert_called('POST', '/servers/1234/metadata', - {'metadata': {'test_key': 'test_value'}}) + self.cs.servers.set_meta(1234, {'test_key': 'test_value'}) + self.assert_called('POST', '/servers/1234/metadata', + {'metadata': {'test_key': 'test_value'}}) def test_set_server_meta_item(self): - cs.servers.set_meta_item(1234, 'test_key', 'test_value') - cs.assert_called('PUT', '/servers/1234/metadata/test_key', - {'meta': {'test_key': 'test_value'}}) + self.cs.servers.set_meta_item(1234, 'test_key', 'test_value') + self.assert_called('PUT', '/servers/1234/metadata/test_key', + {'meta': {'test_key': 'test_value'}}) def test_find(self): - server = cs.servers.find(name='sample-server') - cs.assert_called('GET', '/servers', pos=-2) - cs.assert_called('GET', '/servers/1234', pos=-1) + server = self.cs.servers.find(name='sample-server') + self.assert_called('GET', '/servers/1234') self.assertEqual(server.name, 'sample-server') - self.assertRaises(exceptions.NoUniqueMatch, cs.servers.find, + self.assertRaises(exceptions.NoUniqueMatch, self.cs.servers.find, flavor={"id": 1, "name": "256 MB Server"}) - sl = cs.servers.findall(flavor={"id": 1, "name": "256 MB Server"}) + sl = self.cs.servers.findall(flavor={"id": 1, "name": "256 MB Server"}) self.assertEqual([s.id for s in sl], [1234, 5678, 9012]) def test_reboot_server(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.reboot() - cs.assert_called('POST', '/servers/1234/action') - cs.servers.reboot(s, reboot_type='HARD') - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.reboot(s, reboot_type='HARD') + self.assert_called('POST', '/servers/1234/action') def test_rebuild_server(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.rebuild(image=1) - cs.assert_called('POST', '/servers/1234/action') - cs.servers.rebuild(s, image=1) - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.rebuild(s, image=1) + self.assert_called('POST', '/servers/1234/action') s.rebuild(image=1, password='5678') - cs.assert_called('POST', '/servers/1234/action') - cs.servers.rebuild(s, image=1, password='5678') - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.rebuild(s, image=1, password='5678') + self.assert_called('POST', '/servers/1234/action') def _rebuild_resize_disk_config(self, disk_config, operation="rebuild"): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) if operation == "rebuild": s.rebuild(image=1, disk_config=disk_config) elif operation == "resize": s.resize(flavor=1, disk_config=disk_config) - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') # verify disk config param was used in the request: - last_request = cs.client.callstack[-1] - body = last_request[-1] + body = httpretty.last_request().parsed_body d = body[operation] self.assertTrue('OS-DCF:diskConfig' in d) @@ -289,20 +293,20 @@ def test_rebuild_server_disk_config_manual(self): self._rebuild_resize_disk_config('MANUAL') def test_rebuild_server_preserve_ephemeral(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.rebuild(image=1, preserve_ephemeral=True) - cs.assert_called('POST', '/servers/1234/action') - body = cs.client.callstack[-1][-1] + self.assert_called('POST', '/servers/1234/action') + body = httpretty.last_request().parsed_body d = body['rebuild'] self.assertIn('preserve_ephemeral', d) self.assertEqual(d['preserve_ephemeral'], True) def test_resize_server(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.resize(flavor=1) - cs.assert_called('POST', '/servers/1234/action') - cs.servers.resize(s, flavor=1) - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.resize(s, flavor=1) + self.assert_called('POST', '/servers/1234/action') def test_resize_server_disk_config_auto(self): self._rebuild_resize_disk_config('AUTO', 'resize') @@ -311,162 +315,163 @@ def test_resize_server_disk_config_manual(self): self._rebuild_resize_disk_config('MANUAL', 'resize') def test_confirm_resized_server(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.confirm_resize() - cs.assert_called('POST', '/servers/1234/action') - cs.servers.confirm_resize(s) - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.confirm_resize(s) + self.assert_called('POST', '/servers/1234/action') def test_revert_resized_server(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.revert_resize() - cs.assert_called('POST', '/servers/1234/action') - cs.servers.revert_resize(s) - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.revert_resize(s) + self.assert_called('POST', '/servers/1234/action') def test_migrate_server(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.migrate() - cs.assert_called('POST', '/servers/1234/action') - cs.servers.migrate(s) - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.migrate(s) + self.assert_called('POST', '/servers/1234/action') def test_add_fixed_ip(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.add_fixed_ip(1) - cs.assert_called('POST', '/servers/1234/action') - cs.servers.add_fixed_ip(s, 1) - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.add_fixed_ip(s, 1) + self.assert_called('POST', '/servers/1234/action') def test_remove_fixed_ip(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.remove_fixed_ip('10.0.0.1') - cs.assert_called('POST', '/servers/1234/action') - cs.servers.remove_fixed_ip(s, '10.0.0.1') - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.remove_fixed_ip(s, '10.0.0.1') + self.assert_called('POST', '/servers/1234/action') def test_add_floating_ip(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.add_floating_ip('11.0.0.1') - cs.assert_called('POST', '/servers/1234/action') - cs.servers.add_floating_ip(s, '11.0.0.1') - cs.assert_called('POST', '/servers/1234/action') - f = cs.floating_ips.list()[0] - cs.servers.add_floating_ip(s, f) - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.add_floating_ip(s, '11.0.0.1') + self.assert_called('POST', '/servers/1234/action') + f = self.cs.floating_ips.list()[0] + self.cs.servers.add_floating_ip(s, f) + self.assert_called('POST', '/servers/1234/action') s.add_floating_ip(f) - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') def test_add_floating_ip_to_fixed(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.add_floating_ip('11.0.0.1', fixed_address='12.0.0.1') - cs.assert_called('POST', '/servers/1234/action') - cs.servers.add_floating_ip(s, '11.0.0.1', + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.add_floating_ip(s, '11.0.0.1', fixed_address='12.0.0.1') - cs.assert_called('POST', '/servers/1234/action') - f = cs.floating_ips.list()[0] - cs.servers.add_floating_ip(s, f) - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') + f = self.cs.floating_ips.list()[0] + self.cs.servers.add_floating_ip(s, f) + self.assert_called('POST', '/servers/1234/action') s.add_floating_ip(f) - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') def test_remove_floating_ip(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.remove_floating_ip('11.0.0.1') - cs.assert_called('POST', '/servers/1234/action') - cs.servers.remove_floating_ip(s, '11.0.0.1') - cs.assert_called('POST', '/servers/1234/action') - f = cs.floating_ips.list()[0] - cs.servers.remove_floating_ip(s, f) - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.remove_floating_ip(s, '11.0.0.1') + self.assert_called('POST', '/servers/1234/action') + f = self.cs.floating_ips.list()[0] + self.cs.servers.remove_floating_ip(s, f) + self.assert_called('POST', '/servers/1234/action') s.remove_floating_ip(f) - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') def test_stop(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.stop() - cs.assert_called('POST', '/servers/1234/action') - cs.servers.stop(s) - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.stop(s) + self.assert_called('POST', '/servers/1234/action') def test_force_delete(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.force_delete() - cs.assert_called('POST', '/servers/1234/action') - cs.servers.force_delete(s) - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.force_delete(s) + self.assert_called('POST', '/servers/1234/action') def test_restore(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.restore() - cs.assert_called('POST', '/servers/1234/action') - cs.servers.restore(s) - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.restore(s) + self.assert_called('POST', '/servers/1234/action') def test_start(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.start() - cs.assert_called('POST', '/servers/1234/action') - cs.servers.start(s) - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.start(s) + self.assert_called('POST', '/servers/1234/action') def test_rescue(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.rescue() - cs.assert_called('POST', '/servers/1234/action') - cs.servers.rescue(s) - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.rescue(s) + self.assert_called('POST', '/servers/1234/action') def test_unrescue(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.unrescue() - cs.assert_called('POST', '/servers/1234/action') - cs.servers.unrescue(s) - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.unrescue(s) + self.assert_called('POST', '/servers/1234/action') def test_lock(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.lock() - cs.assert_called('POST', '/servers/1234/action') - cs.servers.lock(s) - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.lock(s) + self.assert_called('POST', '/servers/1234/action') def test_unlock(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.unlock() - cs.assert_called('POST', '/servers/1234/action') - cs.servers.unlock(s) - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.unlock(s) + self.assert_called('POST', '/servers/1234/action') def test_backup(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.backup('back1', 'daily', 1) - cs.assert_called('POST', '/servers/1234/action') - cs.servers.backup(s, 'back1', 'daily', 2) - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.backup(s, 'back1', 'daily', 2) + self.assert_called('POST', '/servers/1234/action') def test_get_console_output_without_length(self): success = 'foo' - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.get_console_output() self.assertEqual(s.get_console_output(), success) - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') - cs.servers.get_console_output(s) - self.assertEqual(cs.servers.get_console_output(s), success) - cs.assert_called('POST', '/servers/1234/action') + self.cs.servers.get_console_output(s) + self.assertEqual(self.cs.servers.get_console_output(s), success) + self.assert_called('POST', '/servers/1234/action') def test_get_console_output_with_length(self): success = 'foo' - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.get_console_output(length=50) self.assertEqual(s.get_console_output(length=50), success) - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') - cs.servers.get_console_output(s, length=50) - self.assertEqual(cs.servers.get_console_output(s, length=50), success) - cs.assert_called('POST', '/servers/1234/action') + self.cs.servers.get_console_output(s, length=50) + self.assertEqual(self.cs.servers.get_console_output(s, length=50), + success) + self.assert_called('POST', '/servers/1234/action') # Testing password methods with the following password and key # @@ -483,13 +488,13 @@ def test_get_console_output_with_length(self): # Hi/fmZZNQQqj1Ijq0caOIw== def test_get_password(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) self.assertEqual(s.get_password('novaclient/tests/idfake.pem'), b'FooBar123') - cs.assert_called('GET', '/servers/1234/os-server-password') + self.assert_called('GET', '/servers/1234/os-server-password') def test_get_password_without_key(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) self.assertEqual(s.get_password(), 'OIuEuQttO8Rk93BcKlwHQsziDAnkAm/V6V8VPToA8ZeUaUBWwS0gwo2K6Y61Z96r' 'qG447iRz0uTEEYq3RAYJk1mh3mMIRVl27t8MtIecR5ggVVbz1S9AwXJQypDKl0ho' @@ -497,112 +502,113 @@ def test_get_password_without_key(self): '/y5a6Z3/AoJZYGG7IH5WN88UROU3B9JZGFB2qtPLQTOvDMZLUhoPRIJeHiVSlo1N' 'tI2/++UsXVg3ow6ItqCJGgdNuGG5JB+bslDHWPxROpesEIHdczk46HCpHQN8f1sk' 'Hi/fmZZNQQqj1Ijq0caOIw==') - cs.assert_called('GET', '/servers/1234/os-server-password') + self.assert_called('GET', '/servers/1234/os-server-password') def test_clear_password(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.clear_password() - cs.assert_called('DELETE', '/servers/1234/os-server-password') + self.assert_called('DELETE', '/servers/1234/os-server-password') def test_get_server_diagnostics(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) diagnostics = s.diagnostics() self.assertTrue(diagnostics is not None) - cs.assert_called('GET', '/servers/1234/diagnostics') + self.assert_called('GET', '/servers/1234/diagnostics') - diagnostics_from_manager = cs.servers.diagnostics(1234) + diagnostics_from_manager = self.cs.servers.diagnostics(1234) self.assertTrue(diagnostics_from_manager is not None) - cs.assert_called('GET', '/servers/1234/diagnostics') + self.assert_called('GET', '/servers/1234/diagnostics') - self.assertEqual(diagnostics, diagnostics_from_manager) + self.assertEqual(diagnostics[1], diagnostics_from_manager[1]) def test_get_vnc_console(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.get_vnc_console('fake') - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') - cs.servers.get_vnc_console(s, 'fake') - cs.assert_called('POST', '/servers/1234/action') + self.cs.servers.get_vnc_console(s, 'fake') + self.assert_called('POST', '/servers/1234/action') def test_get_spice_console(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.get_spice_console('fake') - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') - cs.servers.get_spice_console(s, 'fake') - cs.assert_called('POST', '/servers/1234/action') + self.cs.servers.get_spice_console(s, 'fake') + self.assert_called('POST', '/servers/1234/action') def test_get_rdp_console(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.get_rdp_console('fake') - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') - cs.servers.get_rdp_console(s, 'fake') - cs.assert_called('POST', '/servers/1234/action') + self.cs.servers.get_rdp_console(s, 'fake') + self.assert_called('POST', '/servers/1234/action') def test_create_image(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.create_image('123') - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') s.create_image('123', {}) - cs.assert_called('POST', '/servers/1234/action') - cs.servers.create_image(s, '123') - cs.assert_called('POST', '/servers/1234/action') - cs.servers.create_image(s, '123', {}) + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.create_image(s, '123') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.create_image(s, '123', {}) def test_live_migrate_server(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.live_migrate(host='hostname', block_migration=False, disk_over_commit=False) - cs.assert_called('POST', '/servers/1234/action') - cs.servers.live_migrate(s, host='hostname', block_migration=False, + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.live_migrate(s, host='hostname', block_migration=False, disk_over_commit=False) - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') def test_reset_state(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.reset_state('newstate') - cs.assert_called('POST', '/servers/1234/action') - cs.servers.reset_state(s, 'newstate') - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.reset_state(s, 'newstate') + self.assert_called('POST', '/servers/1234/action') def test_reset_network(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.reset_network() - cs.assert_called('POST', '/servers/1234/action') - cs.servers.reset_network(s) - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.reset_network(s) + self.assert_called('POST', '/servers/1234/action') def test_add_security_group(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.add_security_group('newsg') - cs.assert_called('POST', '/servers/1234/action') - cs.servers.add_security_group(s, 'newsg') - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.add_security_group(s, 'newsg') + self.assert_called('POST', '/servers/1234/action') def test_remove_security_group(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.remove_security_group('oldsg') - cs.assert_called('POST', '/servers/1234/action') - cs.servers.remove_security_group(s, 'oldsg') - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.remove_security_group(s, 'oldsg') + self.assert_called('POST', '/servers/1234/action') def test_list_security_group(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.list_security_group() - cs.assert_called('GET', '/servers/1234/os-security-groups') + self.assert_called('GET', '/servers/1234/os-security-groups') def test_evacuate(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.evacuate('fake_target_host', 'True') - cs.assert_called('POST', '/servers/1234/action') - cs.servers.evacuate(s, 'fake_target_host', 'False', 'NewAdminPassword') - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.evacuate(s, 'fake_target_host', + 'False', 'NewAdminPassword') + self.assert_called('POST', '/servers/1234/action') def test_interface_list(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.interface_list() - cs.assert_called('GET', '/servers/1234/os-interface') + self.assert_called('GET', '/servers/1234/os-interface') def test_interface_list_result_string_representable(self): """Test for bugs.launchpad.net/python-novaclient/+bug/1280453.""" @@ -631,11 +637,11 @@ def test_interface_list_result_string_representable(self): self.assertEqual('', '%r' % s) def test_interface_attach(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.interface_attach(None, None, None) - cs.assert_called('POST', '/servers/1234/os-interface') + self.assert_called('POST', '/servers/1234/os-interface') def test_interface_detach(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.interface_detach('port-id') - cs.assert_called('DELETE', '/servers/1234/os-interface/port-id') + self.assert_called('DELETE', '/servers/1234/os-interface/port-id') diff --git a/novaclient/tests/v3/test_servers.py b/novaclient/tests/v3/test_servers.py index e03fc5f40..98df862b1 100644 --- a/novaclient/tests/v3/test_servers.py +++ b/novaclient/tests/v3/test_servers.py @@ -17,50 +17,51 @@ import six from novaclient import exceptions +from novaclient.tests.fixture_data import client +from novaclient.tests.fixture_data import servers as data from novaclient.tests import utils -from novaclient.tests.v3 import fakes from novaclient.v3 import servers -cs = fakes.FakeClient() +class ServersTest(utils.FixturedTestCase): - -class ServersTest(utils.TestCase): + client_fixture_class = client.V3 + data_fixture_class = data.V3 def test_list_servers(self): - sl = cs.servers.list() - cs.assert_called('GET', '/servers/detail') + sl = self.cs.servers.list() + self.assert_called('GET', '/servers/detail') for s in sl: self.assertIsInstance(s, servers.Server) def test_list_servers_undetailed(self): - sl = cs.servers.list(detailed=False) - cs.assert_called('GET', '/servers') + sl = self.cs.servers.list(detailed=False) + self.assert_called('GET', '/servers') for s in sl: self.assertIsInstance(s, servers.Server) def test_list_servers_with_marker_limit(self): - sl = cs.servers.list(marker=1234, limit=2) - cs.assert_called('GET', '/servers/detail?limit=2&marker=1234') + sl = self.cs.servers.list(marker=1234, limit=2) + self.assert_called('GET', '/servers/detail?limit=2&marker=1234') for s in sl: self.assertIsInstance(s, servers.Server) def test_get_server_details(self): - s = cs.servers.get(1234) - cs.assert_called('GET', '/servers/1234') + s = self.cs.servers.get(1234) + self.assert_called('GET', '/servers/1234') self.assertIsInstance(s, servers.Server) self.assertEqual(s.id, 1234) self.assertEqual(s.status, 'BUILD') def test_get_server_promote_details(self): - s1 = cs.servers.list(detailed=False)[0] - s2 = cs.servers.list(detailed=True)[0] + s1 = self.cs.servers.list(detailed=False)[0] + s2 = self.cs.servers.list(detailed=True)[0] self.assertNotEqual(s1._info, s2._info) s1.get() self.assertEqual(s1._info, s2._info) def test_create_server(self): - s = cs.servers.create( + s = self.cs.servers.create( name="My server", image=1, flavor=1, @@ -72,11 +73,11 @@ def test_create_server(self): '/tmp/foo.txt': six.StringIO('data'), # a stream } ) - cs.assert_called('POST', '/servers') + self.assert_called('POST', '/servers') self.assertIsInstance(s, servers.Server) def test_create_server_boot_with_nics_ipv4(self): - old_boot = cs.servers._boot + old_boot = self.cs.servers._boot nics = [{'net-id': '11111111-1111-1111-1111-111111111111', 'v4-fixed-ip': '10.10.0.7'}] @@ -84,8 +85,8 @@ def wrapped_boot(url, key, *boot_args, **boot_kwargs): self.assertEqual(boot_kwargs['nics'], nics) return old_boot(url, key, *boot_args, **boot_kwargs) - with mock.patch.object(cs.servers, '_boot', wrapped_boot): - s = cs.servers.create( + with mock.patch.object(self.cs.servers, '_boot', wrapped_boot): + s = self.cs.servers.create( name="My server", image=1, flavor=1, @@ -94,11 +95,11 @@ def wrapped_boot(url, key, *boot_args, **boot_kwargs): key_name="fakekey", nics=nics ) - cs.assert_called('POST', '/servers') + self.assert_called('POST', '/servers') self.assertIsInstance(s, servers.Server) def test_create_server_boot_with_nics_ipv6(self): - old_boot = cs.servers._boot + old_boot = self.cs.servers._boot nics = [{'net-id': '11111111-1111-1111-1111-111111111111', 'v6-fixed-ip': '2001:db9:0:1::10'}] @@ -106,8 +107,8 @@ def wrapped_boot(url, key, *boot_args, **boot_kwargs): self.assertEqual(boot_kwargs['nics'], nics) return old_boot(url, key, *boot_args, **boot_kwargs) - with mock.patch.object(cs.servers, '_boot', wrapped_boot): - s = cs.servers.create( + with mock.patch.object(self.cs.servers, '_boot', wrapped_boot): + s = self.cs.servers.create( name="My server", image=1, flavor=1, @@ -116,11 +117,11 @@ def wrapped_boot(url, key, *boot_args, **boot_kwargs): key_name="fakekey", nics=nics ) - cs.assert_called('POST', '/servers') + self.assert_called('POST', '/servers') self.assertIsInstance(s, servers.Server) def test_create_server_userdata_file_object(self): - s = cs.servers.create( + s = self.cs.servers.create( name="My server", image=1, flavor=1, @@ -131,11 +132,11 @@ def test_create_server_userdata_file_object(self): '/tmp/foo.txt': six.StringIO('data'), # a stream }, ) - cs.assert_called('POST', '/servers') + self.assert_called('POST', '/servers') self.assertIsInstance(s, servers.Server) def test_create_server_userdata_unicode(self): - s = cs.servers.create( + s = self.cs.servers.create( name="My server", image=1, flavor=1, @@ -147,11 +148,11 @@ def test_create_server_userdata_unicode(self): '/tmp/foo.txt': six.StringIO('data'), # a stream }, ) - cs.assert_called('POST', '/servers') + self.assert_called('POST', '/servers') self.assertIsInstance(s, servers.Server) def test_create_server_userdata_utf8(self): - s = cs.servers.create( + s = self.cs.servers.create( name="My server", image=1, flavor=1, @@ -163,11 +164,11 @@ def test_create_server_userdata_utf8(self): '/tmp/foo.txt': six.StringIO('data'), # a stream }, ) - cs.assert_called('POST', '/servers') + self.assert_called('POST', '/servers') self.assertIsInstance(s, servers.Server) def test_create_server_return_reservation_id(self): - s = cs.servers.create( + s = self.cs.servers.create( name="My server", image=1, flavor=1, @@ -183,294 +184,295 @@ def test_create_server_return_reservation_id(self): 'os-multiple-create:return_reservation_id': True, } } - cs.assert_called('POST', '/servers', expected_body) + self.assert_called('POST', '/servers', expected_body) self.assertIsInstance(s, servers.Server) def test_update_server(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) # Update via instance s.update(name='hi') - cs.assert_called('PUT', '/servers/1234') + self.assert_called('PUT', '/servers/1234') s.update(name='hi') - cs.assert_called('PUT', '/servers/1234') + self.assert_called('PUT', '/servers/1234') # Silly, but not an error s.update() # Update via manager - cs.servers.update(s, name='hi') - cs.assert_called('PUT', '/servers/1234') + self.cs.servers.update(s, name='hi') + self.assert_called('PUT', '/servers/1234') def test_delete_server(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.delete() - cs.assert_called('DELETE', '/servers/1234') - cs.servers.delete(1234) - cs.assert_called('DELETE', '/servers/1234') - cs.servers.delete(s) - cs.assert_called('DELETE', '/servers/1234') + self.assert_called('DELETE', '/servers/1234') + self.cs.servers.delete(1234) + self.assert_called('DELETE', '/servers/1234') + self.cs.servers.delete(s) + self.assert_called('DELETE', '/servers/1234') def test_delete_server_meta(self): - cs.servers.delete_meta(1234, ['test_key']) - cs.assert_called('DELETE', '/servers/1234/metadata/test_key') + self.cs.servers.delete_meta(1234, ['test_key']) + self.assert_called('DELETE', '/servers/1234/metadata/test_key') def test_set_server_meta(self): - cs.servers.set_meta(1234, {'test_key': 'test_value'}) - cs.assert_called('POST', '/servers/1234/metadata', - {'metadata': {'test_key': 'test_value'}}) + self.cs.servers.set_meta(1234, {'test_key': 'test_value'}) + self.assert_called('POST', '/servers/1234/metadata', + {'metadata': {'test_key': 'test_value'}}) def test_find(self): - server = cs.servers.find(name='sample-server') - cs.assert_called('GET', '/servers', pos=-2) - cs.assert_called('GET', '/servers/1234', pos=-1) + server = self.cs.servers.find(name='sample-server') + self.assert_called('GET', '/servers/1234') self.assertEqual(server.name, 'sample-server') - self.assertRaises(exceptions.NoUniqueMatch, cs.servers.find, + self.assertRaises(exceptions.NoUniqueMatch, self.cs.servers.find, flavor={"id": 1, "name": "256 MB Server"}) - sl = cs.servers.findall(flavor={"id": 1, "name": "256 MB Server"}) + sl = self.cs.servers.findall(flavor={"id": 1, "name": "256 MB Server"}) self.assertEqual([s.id for s in sl], [1234, 5678, 9012]) def test_reboot_server(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.reboot() - cs.assert_called('POST', '/servers/1234/action') - cs.servers.reboot(s, reboot_type='HARD') - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.reboot(s, reboot_type='HARD') + self.assert_called('POST', '/servers/1234/action') def test_rebuild_server(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.rebuild(image=1) - cs.assert_called('POST', '/servers/1234/action') - cs.servers.rebuild(s, image=1) - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.rebuild(s, image=1) + self.assert_called('POST', '/servers/1234/action') s.rebuild(image=1, password='5678') - cs.assert_called('POST', '/servers/1234/action') - cs.servers.rebuild(s, image=1, password='5678') - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.rebuild(s, image=1, password='5678') + self.assert_called('POST', '/servers/1234/action') def test_resize_server(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.resize(flavor=1) - cs.assert_called('POST', '/servers/1234/action') - cs.servers.resize(s, flavor=1) - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.resize(s, flavor=1) + self.assert_called('POST', '/servers/1234/action') def test_confirm_resized_server(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.confirm_resize() - cs.assert_called('POST', '/servers/1234/action') - cs.servers.confirm_resize(s) - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.confirm_resize(s) + self.assert_called('POST', '/servers/1234/action') def test_revert_resized_server(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.revert_resize() - cs.assert_called('POST', '/servers/1234/action') - cs.servers.revert_resize(s) - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.revert_resize(s) + self.assert_called('POST', '/servers/1234/action') def test_migrate_server(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.migrate() - cs.assert_called('POST', '/servers/1234/action') - cs.servers.migrate(s) - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.migrate(s) + self.assert_called('POST', '/servers/1234/action') def test_add_fixed_ip(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.add_fixed_ip(1) - cs.assert_called('POST', '/servers/1234/action') - cs.servers.add_fixed_ip(s, 1) - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.add_fixed_ip(s, 1) + self.assert_called('POST', '/servers/1234/action') def test_remove_fixed_ip(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.remove_fixed_ip('10.0.0.1') - cs.assert_called('POST', '/servers/1234/action') - cs.servers.remove_fixed_ip(s, '10.0.0.1') - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.remove_fixed_ip(s, '10.0.0.1') + self.assert_called('POST', '/servers/1234/action') def test_stop(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.stop() - cs.assert_called('POST', '/servers/1234/action') - cs.servers.stop(s) - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.stop(s) + self.assert_called('POST', '/servers/1234/action') def test_force_delete(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.force_delete() - cs.assert_called('POST', '/servers/1234/action') - cs.servers.force_delete(s) - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.force_delete(s) + self.assert_called('POST', '/servers/1234/action') def test_restore(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.restore() - cs.assert_called('POST', '/servers/1234/action') - cs.servers.restore(s) - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.restore(s) + self.assert_called('POST', '/servers/1234/action') def test_start(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.start() - cs.assert_called('POST', '/servers/1234/action') - cs.servers.start(s) - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.start(s) + self.assert_called('POST', '/servers/1234/action') def test_rescue(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.rescue() - cs.assert_called('POST', '/servers/1234/action') - cs.servers.rescue(s) - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.rescue(s) + self.assert_called('POST', '/servers/1234/action') def test_unrescue(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.unrescue() - cs.assert_called('POST', '/servers/1234/action') - cs.servers.unrescue(s) - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.unrescue(s) + self.assert_called('POST', '/servers/1234/action') def test_lock(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.lock() - cs.assert_called('POST', '/servers/1234/action') - cs.servers.lock(s) - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.lock(s) + self.assert_called('POST', '/servers/1234/action') def test_unlock(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.unlock() - cs.assert_called('POST', '/servers/1234/action') - cs.servers.unlock(s) - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.unlock(s) + self.assert_called('POST', '/servers/1234/action') def test_backup(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.backup('back1', 'daily', 1) - cs.assert_called('POST', '/servers/1234/action') - cs.servers.backup(s, 'back1', 'daily', 2) - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.backup(s, 'back1', 'daily', 2) + self.assert_called('POST', '/servers/1234/action') def test_get_console_output_without_length(self): success = 'foo' - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.get_console_output() self.assertEqual(s.get_console_output(), success) - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') - cs.servers.get_console_output(s) - self.assertEqual(cs.servers.get_console_output(s), success) - cs.assert_called('POST', '/servers/1234/action', - {'get_console_output': {'length': -1}}) + self.cs.servers.get_console_output(s) + self.assertEqual(self.cs.servers.get_console_output(s), success) + self.assert_called('POST', '/servers/1234/action', + {'get_console_output': {'length': -1}}) def test_get_console_output_with_length(self): success = 'foo' - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.get_console_output(length=50) self.assertEqual(s.get_console_output(length=50), success) - cs.assert_called('POST', '/servers/1234/action', - {'get_console_output': {'length': 50}}) + self.assert_called('POST', '/servers/1234/action', + {'get_console_output': {'length': 50}}) - cs.servers.get_console_output(s, length=50) - self.assertEqual(cs.servers.get_console_output(s, length=50), success) - cs.assert_called('POST', '/servers/1234/action', - {'get_console_output': {'length': 50}}) + self.cs.servers.get_console_output(s, length=50) + self.assertEqual(self.cs.servers.get_console_output(s, length=50), + success) + self.assert_called('POST', '/servers/1234/action', + {'get_console_output': {'length': 50}}) def test_get_password(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) self.assertEqual(s.get_password('/foo/id_rsa'), '') - cs.assert_called('GET', '/servers/1234/os-server-password') + self.assert_called('GET', '/servers/1234/os-server-password') def test_clear_password(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.clear_password() - cs.assert_called('DELETE', '/servers/1234/os-server-password') + self.assert_called('DELETE', '/servers/1234/os-server-password') def test_get_server_diagnostics(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) diagnostics = s.diagnostics() self.assertTrue(diagnostics is not None) - cs.assert_called('GET', '/servers/1234/os-server-diagnostics') + self.assert_called('GET', '/servers/1234/os-server-diagnostics') - diagnostics_from_manager = cs.servers.diagnostics(1234) + diagnostics_from_manager = self.cs.servers.diagnostics(1234) self.assertTrue(diagnostics_from_manager is not None) - cs.assert_called('GET', '/servers/1234/os-server-diagnostics') + self.assert_called('GET', '/servers/1234/os-server-diagnostics') - self.assertEqual(diagnostics, diagnostics_from_manager) + self.assertEqual(diagnostics[1], diagnostics_from_manager[1]) def test_get_vnc_console(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.get_vnc_console('fake') - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') - cs.servers.get_vnc_console(s, 'fake') - cs.assert_called('POST', '/servers/1234/action') + self.cs.servers.get_vnc_console(s, 'fake') + self.assert_called('POST', '/servers/1234/action') def test_get_spice_console(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.get_spice_console('fake') - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') - cs.servers.get_spice_console(s, 'fake') - cs.assert_called('POST', '/servers/1234/action') + self.cs.servers.get_spice_console(s, 'fake') + self.assert_called('POST', '/servers/1234/action') def test_create_image(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.create_image('123') - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') s.create_image('123', {}) - cs.assert_called('POST', '/servers/1234/action') - cs.servers.create_image(s, '123') - cs.assert_called('POST', '/servers/1234/action') - cs.servers.create_image(s, '123', {}) + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.create_image(s, '123') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.create_image(s, '123', {}) def test_live_migrate_server(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.live_migrate(host='hostname', block_migration=False, disk_over_commit=False) - cs.assert_called('POST', '/servers/1234/action') - cs.servers.live_migrate(s, host='hostname', block_migration=False, - disk_over_commit=False) - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.live_migrate(s, host='hostname', block_migration=False, + disk_over_commit=False) + self.assert_called('POST', '/servers/1234/action') def test_reset_state(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.reset_state('newstate') - cs.assert_called('POST', '/servers/1234/action') - cs.servers.reset_state(s, 'newstate') - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.reset_state(s, 'newstate') + self.assert_called('POST', '/servers/1234/action') def test_reset_network(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.reset_network() - cs.assert_called('POST', '/servers/1234/action') - cs.servers.reset_network(s) - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.reset_network(s) + self.assert_called('POST', '/servers/1234/action') def test_evacuate(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.evacuate('fake_target_host', 'True') - cs.assert_called('POST', '/servers/1234/action') - cs.servers.evacuate(s, 'fake_target_host', 'False', 'NewAdminPassword') - cs.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.evacuate(s, 'fake_target_host', + 'False', 'NewAdminPassword') + self.assert_called('POST', '/servers/1234/action') def test_interface_list(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.interface_list() - cs.assert_called('GET', '/servers/1234/os-attach-interfaces') + self.assert_called('GET', '/servers/1234/os-attach-interfaces') def test_interface_attach(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.interface_attach(None, None, None) - cs.assert_called('POST', '/servers/1234/os-attach-interfaces') + self.assert_called('POST', '/servers/1234/os-attach-interfaces') def test_interface_detach(self): - s = cs.servers.get(1234) + s = self.cs.servers.get(1234) s.interface_detach('port-id') - cs.assert_called('DELETE', - '/servers/1234/os-attach-interfaces/port-id') + self.assert_called('DELETE', + '/servers/1234/os-attach-interfaces/port-id') From 52c5ad2eadc6a5baf4108a2283b494c27f0bfe37 Mon Sep 17 00:00:00 2001 From: liyingjun Date: Wed, 25 Jun 2014 07:53:20 +0800 Subject: [PATCH 0537/1705] Sync apiclient from oslo-incubator Need to sync the latest apiclient from oslo-incubator to make nova cli cache work. Command used: python update.py --base novaclient --dest-dir ../python-novaclient/ \ --modules apiclient syncd patches: apiclient: * 12341ef504 Merge "Restore UUID and human-ID bash completion" * 468afcb335 Merge "Don't slugify names that don't exist" Change-Id: Iec2ad851b47628c5dde4272f438cce4150e11912 Close-bug: 1337033 --- novaclient/exceptions.py | 5 + novaclient/openstack/common/apiclient/base.py | 18 +- .../openstack/common/apiclient/exceptions.py | 7 +- .../openstack/common/apiclient/fake_client.py | 2 +- novaclient/openstack/common/gettextutils.py | 192 +++++++++++------- novaclient/openstack/common/importutils.py | 4 +- 6 files changed, 147 insertions(+), 81 deletions(-) diff --git a/novaclient/exceptions.py b/novaclient/exceptions.py index 73f04dcef..29aa12cc2 100644 --- a/novaclient/exceptions.py +++ b/novaclient/exceptions.py @@ -113,3 +113,8 @@ def from_response(response, body, url, method=None): cls = _code_map.get(response.status_code, HttpError) return cls(**kwargs) + + +class ResourceNotFound(Exception): + """Error in getting the resource.""" + pass diff --git a/novaclient/openstack/common/apiclient/base.py b/novaclient/openstack/common/apiclient/base.py index 1f90b88e9..8380ba397 100644 --- a/novaclient/openstack/common/apiclient/base.py +++ b/novaclient/openstack/common/apiclient/base.py @@ -32,6 +32,7 @@ from novaclient.openstack.common.apiclient import exceptions from novaclient.openstack.common.gettextutils import _ from novaclient.openstack.common import strutils +from novaclient.openstack.common import uuidutils def getid(obj): @@ -436,6 +437,21 @@ def __init__(self, manager, info, loaded=False): self._info = info self._add_details(info) self._loaded = loaded + self._init_completion_cache() + + def _init_completion_cache(self): + cache_write = getattr(self.manager, 'write_to_completion_cache', None) + if not cache_write: + return + + # NOTE(sirp): ensure `id` is already present because if it isn't we'll + # enter an infinite loop of __getattr__ -> get -> __init__ -> + # __getattr__ -> ... + if 'id' in self.__dict__ and uuidutils.is_uuid_like(self.id): + cache_write('uuid', self.id) + + if self.human_id: + cache_write('human_id', self.human_id) def __repr__(self): reprkeys = sorted(k @@ -465,7 +481,7 @@ def _add_details(self, info): def __getattr__(self, k): if k not in self.__dict__: - #NOTE(bcwaldon): disallow lazy-loading if already loaded once + # NOTE(bcwaldon): disallow lazy-loading if already loaded once if not self.is_loaded(): self.get() return self.__getattr__(k) diff --git a/novaclient/openstack/common/apiclient/exceptions.py b/novaclient/openstack/common/apiclient/exceptions.py index 85b123ef8..8e0d2ad9b 100644 --- a/novaclient/openstack/common/apiclient/exceptions.py +++ b/novaclient/openstack/common/apiclient/exceptions.py @@ -57,11 +57,6 @@ class CommandError(ClientException): pass -class ResourceNotFound(ClientException): - """Error in getting the resource.""" - pass - - class AuthorizationFailure(ClientException): """Cannot authorize API client.""" pass @@ -432,7 +427,7 @@ def from_response(response, method, url): """ req_id = response.headers.get("x-openstack-request-id") - #NOTE(hdd) true for older versions of nova and cinder + # NOTE(hdd) true for older versions of nova and cinder if not req_id: req_id = response.headers.get("x-compute-request-id") kwargs = { diff --git a/novaclient/openstack/common/apiclient/fake_client.py b/novaclient/openstack/common/apiclient/fake_client.py index cdb3cc1c9..b6670a67b 100644 --- a/novaclient/openstack/common/apiclient/fake_client.py +++ b/novaclient/openstack/common/apiclient/fake_client.py @@ -79,7 +79,7 @@ class FakeHTTPClient(client.HTTPClient): def __init__(self, *args, **kwargs): self.callstack = [] self.fixtures = kwargs.pop("fixtures", None) or {} - if not args and not "auth_plugin" in kwargs: + if not args and "auth_plugin" not in kwargs: args = (None, ) super(FakeHTTPClient, self).__init__(*args, **kwargs) diff --git a/novaclient/openstack/common/gettextutils.py b/novaclient/openstack/common/gettextutils.py index e76b61a11..a48a72812 100644 --- a/novaclient/openstack/common/gettextutils.py +++ b/novaclient/openstack/common/gettextutils.py @@ -32,65 +32,131 @@ from babel import localedata import six -_localedir = os.environ.get('novaclient'.upper() + '_LOCALEDIR') -_t = gettext.translation('novaclient', localedir=_localedir, fallback=True) - -# We use separate translation catalogs for each log level, so set up a -# mapping between the log level name and the translator. The domain -# for the log level is project_name + "-log-" + log_level so messages -# for each level end up in their own catalog. -_t_log_levels = dict( - (level, gettext.translation('novaclient' + '-log-' + level, - localedir=_localedir, - fallback=True)) - for level in ['info', 'warning', 'error', 'critical'] -) - _AVAILABLE_LANGUAGES = {} -USE_LAZY = False +# FIXME(dhellmann): Remove this when moving to oslo.i18n. +USE_LAZY = False -def enable_lazy(): - """Convenience function for configuring _() to use lazy gettext - Call this at the start of execution to enable the gettextutils._ - function to use lazy gettext functionality. This is useful if - your project is importing _ directly instead of using the - gettextutils.install() way of importing the _ function. +class TranslatorFactory(object): + """Create translator functions """ - global USE_LAZY - USE_LAZY = True + def __init__(self, domain, lazy=False, localedir=None): + """Establish a set of translation functions for the domain. + + :param domain: Name of translation domain, + specifying a message catalog. + :type domain: str + :param lazy: Delays translation until a message is emitted. + Defaults to False. + :type lazy: Boolean + :param localedir: Directory with translation catalogs. + :type localedir: str + """ + self.domain = domain + self.lazy = lazy + if localedir is None: + localedir = os.environ.get(domain.upper() + '_LOCALEDIR') + self.localedir = localedir -def _(msg): - if USE_LAZY: - return Message(msg, domain='novaclient') - else: - if six.PY3: - return _t.gettext(msg) - return _t.ugettext(msg) + def _make_translation_func(self, domain=None): + """Return a new translation function ready for use. + Takes into account whether or not lazy translation is being + done. -def _log_translation(msg, level): - """Build a single translation of a log message - """ - if USE_LAZY: - return Message(msg, domain='novaclient' + '-log-' + level) - else: - translator = _t_log_levels[level] + The domain can be specified to override the default from the + factory, but the localedir from the factory is always used + because we assume the log-level translation catalogs are + installed in the same directory as the main application + catalog. + + """ + if domain is None: + domain = self.domain + if self.lazy: + return functools.partial(Message, domain=domain) + t = gettext.translation( + domain, + localedir=self.localedir, + fallback=True, + ) if six.PY3: - return translator.gettext(msg) - return translator.ugettext(msg) + return t.gettext + return t.ugettext + + @property + def primary(self): + "The default translation function." + return self._make_translation_func() + + def _make_log_translation_func(self, level): + return self._make_translation_func(self.domain + '-log-' + level) + + @property + def log_info(self): + "Translate info-level log messages." + return self._make_log_translation_func('info') + + @property + def log_warning(self): + "Translate warning-level log messages." + return self._make_log_translation_func('warning') + + @property + def log_error(self): + "Translate error-level log messages." + return self._make_log_translation_func('error') + + @property + def log_critical(self): + "Translate critical-level log messages." + return self._make_log_translation_func('critical') + + +# NOTE(dhellmann): When this module moves out of the incubator into +# oslo.i18n, these global variables can be moved to an integration +# module within each application. + +# Create the global translation functions. +_translators = TranslatorFactory('novaclient') + +# The primary translation function using the well-known name "_" +_ = _translators.primary # Translators for log levels. # # The abbreviated names are meant to reflect the usual use of a short # name like '_'. The "L" is for "log" and the other letter comes from # the level. -_LI = functools.partial(_log_translation, level='info') -_LW = functools.partial(_log_translation, level='warning') -_LE = functools.partial(_log_translation, level='error') -_LC = functools.partial(_log_translation, level='critical') +_LI = _translators.log_info +_LW = _translators.log_warning +_LE = _translators.log_error +_LC = _translators.log_critical + +# NOTE(dhellmann): End of globals that will move to the application's +# integration module. + + +def enable_lazy(): + """Convenience function for configuring _() to use lazy gettext + + Call this at the start of execution to enable the gettextutils._ + function to use lazy gettext functionality. This is useful if + your project is importing _ directly instead of using the + gettextutils.install() way of importing the _ function. + """ + # FIXME(dhellmann): This function will be removed in oslo.i18n, + # because the TranslatorFactory makes it superfluous. + global _, _LI, _LW, _LE, _LC, USE_LAZY + tf = TranslatorFactory('novaclient', lazy=True) + _ = tf.primary + _LI = tf.log_info + _LW = tf.log_warning + _LE = tf.log_error + _LC = tf.log_critical + USE_LAZY = True def install(domain, lazy=False): @@ -112,26 +178,9 @@ def install(domain, lazy=False): any available locale. """ if lazy: - # NOTE(mrodden): Lazy gettext functionality. - # - # The following introduces a deferred way to do translations on - # messages in OpenStack. We override the standard _() function - # and % (format string) operation to build Message objects that can - # later be translated when we have more information. - def _lazy_gettext(msg): - """Create and return a Message object. - - Lazy gettext function for a given domain, it is a factory method - for a project/module to get a lazy gettext function for its own - translation domain (i.e. nova, glance, cinder, etc.) - - Message encapsulates a string so that we can translate - it later when needed. - """ - return Message(msg, domain=domain) - from six import moves - moves.builtins.__dict__['_'] = _lazy_gettext + tf = TranslatorFactory(domain, lazy=True) + moves.builtins.__dict__['_'] = tf.primary else: localedir = '%s_LOCALEDIR' % domain.upper() if six.PY3: @@ -274,13 +323,14 @@ def __add__(self, other): def __radd__(self, other): return self.__add__(other) - def __str__(self): - # NOTE(luisg): Logging in python 2.6 tries to str() log records, - # and it expects specifically a UnicodeError in order to proceed. - msg = _('Message objects do not support str() because they may ' - 'contain non-ascii characters. ' - 'Please use unicode() or translate() instead.') - raise UnicodeError(msg) + if six.PY2: + def __str__(self): + # NOTE(luisg): Logging in python 2.6 tries to str() log records, + # and it expects specifically a UnicodeError in order to proceed. + msg = _('Message objects do not support str() because they may ' + 'contain non-ascii characters. ' + 'Please use unicode() or translate() instead.') + raise UnicodeError(msg) def get_available_languages(domain): @@ -323,8 +373,8 @@ def get_available_languages(domain): 'zh_Hant_HK': 'zh_HK', 'zh_Hant': 'zh_TW', 'fil': 'tl_PH'} - for (locale, alias) in six.iteritems(aliases): - if locale in language_list and alias not in language_list: + for (locale_, alias) in six.iteritems(aliases): + if locale_ in language_list and alias not in language_list: language_list.append(alias) _AVAILABLE_LANGUAGES[domain] = language_list diff --git a/novaclient/openstack/common/importutils.py b/novaclient/openstack/common/importutils.py index e35b088af..863255db3 100644 --- a/novaclient/openstack/common/importutils.py +++ b/novaclient/openstack/common/importutils.py @@ -24,10 +24,10 @@ def import_class(import_str): """Returns a class from a string including module and class.""" mod_str, _sep, class_str = import_str.rpartition('.') + __import__(mod_str) try: - __import__(mod_str) return getattr(sys.modules[mod_str], class_str) - except (ValueError, AttributeError): + except AttributeError: raise ImportError('Class %s cannot be found (%s)' % (class_str, traceback.format_exception(*sys.exc_info()))) From 58cdcabf1c6580fbf04ae54ca846692ec2127d78 Mon Sep 17 00:00:00 2001 From: liyingjun Date: Thu, 26 Jun 2014 01:55:30 +0800 Subject: [PATCH 0538/1705] Fix booting from volume when using api v3 The current code in novaclient/base.py, class BootingManagerWithFind do not take in account the differences required to boot an instance from a volume in API v3. V3 expects UUID as volume id and also the source type to be set. Change-Id: Id8cfb2d7811aead27cb26cf7ff615c7a9ed05d54 Close-bug: 1325303 --- novaclient/base.py | 4 ++++ novaclient/tests/v1_1/test_shell.py | 5 ++++- novaclient/tests/v3/test_shell.py | 12 ++++++++---- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/novaclient/base.py b/novaclient/base.py index 6b000f8fe..cdd439a98 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -204,11 +204,15 @@ def _parse_block_device_mapping(self, block_device_mapping): mapping_parts = mapping.split(':') source_id = mapping_parts[0] + bdm_dict['uuid'] = source_id + bdm_dict['boot_index'] = 0 if len(mapping_parts) == 1: bdm_dict['volume_id'] = source_id + bdm_dict['source_type'] = 'volume' elif len(mapping_parts) > 1: source_type = mapping_parts[1] + bdm_dict['source_type'] = source_type if source_type.startswith('snap'): bdm_dict['snapshot_id'] = source_id else: diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index 262fd6668..16b5878e5 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -266,7 +266,10 @@ def test_boot_no_image_bdms(self): { 'volume_id': 'blah', 'delete_on_termination': '0', - 'device_name': 'vda' + 'device_name': 'vda', + 'uuid': 'blah', + 'boot_index': 0, + 'source_type': '' } ], 'imageRef': '', diff --git a/novaclient/tests/v3/test_shell.py b/novaclient/tests/v3/test_shell.py index 3d9c2b598..b8a96165e 100644 --- a/novaclient/tests/v3/test_shell.py +++ b/novaclient/tests/v3/test_shell.py @@ -329,7 +329,10 @@ def test_boot_no_image_bdms(self): { 'volume_id': 'blah', 'delete_on_termination': '0', - 'device_name': 'vda' + 'device_name': 'vda', + 'boot_index': 0, + 'uuid': 'blah', + 'source_type': '' } ], 'image_ref': '', @@ -344,15 +347,16 @@ def test_boot_image_bdms(self): 'source=volume,dest=volume,device=vda,size=1,format=ext4,' 'type=disk,shutdown=preserve some-server' ) + id = ('fake-id,source=volume,dest=volume,device=vda,size=1,' + 'format=ext4,type=disk,shutdown=preserve') self.assert_called_anytime( 'POST', '/servers', {'server': { 'flavor_ref': '1', 'name': 'some-server', 'os-block-device-mapping:block_device_mapping': [ - {'device_name': 'id', 'volume_id': - 'fake-id,source=volume,dest=volume,device=vda,size=1,' - 'format=ext4,type=disk,shutdown=preserve'}], + {'device_name': 'id', 'volume_id': id, + 'source_type': 'volume', 'boot_index': 0, 'uuid': id}], 'image_ref': '1', 'os-multiple-create:min_count': 1, 'os-multiple-create:max_count': 1, From 65b0d36f3cd02d9068b0ded21898ca7ede0cc99c Mon Sep 17 00:00:00 2001 From: huangtianhua Date: Tue, 24 Jun 2014 17:50:27 +0800 Subject: [PATCH 0539/1705] Fix using a variable which is not defined There is a variable which is not defined but used, we should define it before using. Add a test for get_resource_manager_extra_kwargs_hook function, and change the exception to a more detail instead broad 'Exception'. Change-Id: I7a798dad55a65f06ebbe175925a00028940bb168 --- novaclient/tests/test_utils.py | 20 ++++++++++++++++++++ novaclient/utils.py | 11 ++++++----- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/novaclient/tests/test_utils.py b/novaclient/tests/test_utils.py index 87c8c208f..3892820ed 100644 --- a/novaclient/tests/test_utils.py +++ b/novaclient/tests/test_utils.py @@ -301,3 +301,23 @@ def test_validate_flavor_metadata_keys_with_invalid_keys(self): self.fail("Invalid key passed validation: %s" % key) except exceptions.CommandError as ce: self.assertTrue(key in str(ce)) + + +class ResourceManagerExtraKwargsHookTestCase(test_utils.TestCase): + def test_get_resource_manager_extra_kwargs_hook_test(self): + do_foo = mock.MagicMock() + + def hook1(args): + return {'kwarg1': 'v_hook1'} + + def hook2(args): + return {'kwarg1': 'v_hook2'} + do_foo.resource_manager_kwargs_hooks = [hook1, hook2] + args = {} + exc = self.assertRaises(exceptions.NoUniqueMatch, + utils.get_resource_manager_extra_kwargs, + do_foo, + args) + except_error = ("Hook 'hook2' is attempting to redefine " + "attributes") + self.assertIn(except_error, six.text_type(exc)) diff --git a/novaclient/utils.py b/novaclient/utils.py index def966992..aa3b5ab5d 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -57,13 +57,14 @@ def get_resource_manager_extra_kwargs(f, args, allow_conflicts=False): extra_kwargs = {} for hook in hooks: hook_kwargs = hook(args) - + hook_name = hook.__name__ conflicting_keys = set(hook_kwargs.keys()) & set(extra_kwargs.keys()) if conflicting_keys and not allow_conflicts: - raise Exception(_("Hook '%(hook_name)s' is attempting to redefine" - " attributes '%(conflicting_keys)s'") % - {'hook_name': hook_name, - 'conflicting_keys': conflicting_keys}) + msg = (_("Hook '%(hook_name)s' is attempting to redefine " + "attributes '%(conflicting_keys)s'") % + {'hook_name': hook_name, + 'conflicting_keys': conflicting_keys}) + raise exceptions.NoUniqueMatch(msg) extra_kwargs.update(hook_kwargs) From 9f1ee1249a0fa5704f6ec2f4e560867dc76011ea Mon Sep 17 00:00:00 2001 From: Mark McLoughlin Date: Tue, 1 Jul 2014 16:36:46 +0100 Subject: [PATCH 0540/1705] Mention keystoneclient.Session use in docs Jamie added some excellent "Using Sessions" docs to keystoneclient in I5e44c1029ce160cb2798cfb8a535aa9f3311799a. These will be published to http://docs.openstack.org/developer/python-keystoneclient/using-sessions.html once the version after 0.9.0 is released. Let's add a brief example on how to use this API and reference the keystoneclient docs. Change-Id: Icbcef45f13c1f962c90aa3db9dde4360520166ff --- doc/source/api.rst | 17 +++++++++++++++++ novaclient/v1_1/client.py | 13 +++++++++++++ novaclient/v3/client.py | 13 +++++++++++++ 3 files changed, 43 insertions(+) diff --git a/doc/source/api.rst b/doc/source/api.rst index 305ff8801..e53f2a14f 100644 --- a/doc/source/api.rst +++ b/doc/source/api.rst @@ -16,6 +16,23 @@ First create a client instance with your credentials:: Here ``VERSION`` can be: ``1.1``, ``2`` and ``3``. +Alternatively, you can create a client instance using the keystoneclient +session API:: + + >>> from keystoneclient.auth.identity import v2 + >>> from keystoneclient import session + >>> from novaclient.client import Client + >>> auth = v2.Password(auth_url=AUTH_URL, + username=USERNAME, + password=PASSWORD, + tenant_name=PROJECT_ID) + >>> sess = session.Session(auth=auth) + >>> nova = client.Client(VERSION, session=sess) + +For more information on this keystoneclient API, see `Using Sessions`_. + +.. _Using Sessions: http://docs.openstack.org/developer/python-keystoneclient/using-sessions.html + Then call methods on its managers:: >>> nova.servers.list() diff --git a/novaclient/v1_1/client.py b/novaclient/v1_1/client.py index 46034bb95..24ef79269 100644 --- a/novaclient/v1_1/client.py +++ b/novaclient/v1_1/client.py @@ -55,6 +55,19 @@ class Client(object): >>> client = Client(USERNAME, PASSWORD, PROJECT_ID, AUTH_URL) + Or, alternatively, you can create a client instance using the + keystoneclient.session API:: + + >>> from keystoneclient.auth.identity import v2 + >>> from keystoneclient import session + >>> from novaclient.client import Client + >>> auth = v2.Password(auth_url=AUTH_URL, + username=USERNAME, + password=PASSWORD, + tenant_name=PROJECT_ID) + >>> sess = session.Session(auth=auth) + >>> nova = client.Client(VERSION, session=sess) + Then call methods on its managers:: >>> client.servers.list() diff --git a/novaclient/v3/client.py b/novaclient/v3/client.py index 28ac908b7..c7928f603 100644 --- a/novaclient/v3/client.py +++ b/novaclient/v3/client.py @@ -40,6 +40,19 @@ class Client(object): >>> client = Client(USERNAME, PASSWORD, PROJECT_ID, AUTH_URL) + Or, alternatively, you can create a client instance using the + keystoneclient.session API:: + + >>> from keystoneclient.auth.identity import v2 + >>> from keystoneclient import session + >>> from novaclient.client import Client + >>> auth = v2.Password(auth_url=AUTH_URL, + username=USERNAME, + password=PASSWORD, + tenant_name=PROJECT_ID) + >>> sess = session.Session(auth=auth) + >>> nova = client.Client(VERSION, session=sess) + Then call methods on its managers:: >>> client.servers.list() From f10d8b60c836c05e8f14f54f55233995b1638f1b Mon Sep 17 00:00:00 2001 From: zhangtralon Date: Fri, 18 Jul 2014 10:55:30 +0800 Subject: [PATCH 0541/1705] Fixes typo in error message of do_network_create 'eith' is inappropriate Change-Id: I12b219bd5869d2194d7a8c3495e51e0ead4a64c9 --- novaclient/v1_1/shell.py | 2 +- novaclient/v3/shell.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index ed43277d8..ebae0766c 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -922,7 +922,7 @@ def do_network_create(cs, args): if not (args.cidr or args.cidr_v6): raise exceptions.CommandError( - _("Must specify eith fixed_range_v4 or fixed_range_v6")) + _("Must specify either fixed_range_v4 or fixed_range_v6")) kwargs = _filter_network_create_options(args) if args.multi_host is not None: kwargs['multi_host'] = bool(args.multi_host == 'T' or diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 9f4b13bde..6d8237c48 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -763,7 +763,7 @@ def do_network_create(cs, args): if not (args.cidr or args.cidr_v6): raise exceptions.CommandError( - "Must specify eith fixed_range_v4 or fixed_range_v6") + "Must specify either fixed_range_v4 or fixed_range_v6") kwargs = _filter_network_create_options(args) if args.multi_host is not None: kwargs['multi_host'] = bool(args.multi_host == 'T' or From 32d13a6ec797d8b9bf68ca4d4deea6960854e981 Mon Sep 17 00:00:00 2001 From: liyingjun Date: Thu, 26 Jun 2014 03:01:29 +0800 Subject: [PATCH 0542/1705] Add missing parameters for server rebuild Currently, nova rebuild only support reset: password, image, preserve-ephemeral. While nova api support other parameters like: name, metadata, personality. This patch adds these missing parameters for server rebuild. Change-Id: I4edd8146bbbb0b2e0c9e01907af5dab96f090029 Closes-bug: 1268435 --- novaclient/tests/v1_1/test_servers.py | 11 ++++++++ novaclient/tests/v1_1/test_shell.py | 13 ++++++++++ novaclient/v1_1/servers.py | 30 +++++++++++++++++++++- novaclient/v1_1/shell.py | 36 +++++++++++++++++++++++++++ 4 files changed, 89 insertions(+), 1 deletion(-) diff --git a/novaclient/tests/v1_1/test_servers.py b/novaclient/tests/v1_1/test_servers.py index e7c38ddd6..5ef06baaa 100644 --- a/novaclient/tests/v1_1/test_servers.py +++ b/novaclient/tests/v1_1/test_servers.py @@ -301,6 +301,17 @@ def test_rebuild_server_preserve_ephemeral(self): self.assertIn('preserve_ephemeral', d) self.assertEqual(d['preserve_ephemeral'], True) + def test_rebuild_server_name_meta_files(self): + files = {'/etc/passwd': 'some data'} + s = self.cs.servers.get(1234) + s.rebuild(image=1, name='new', meta={'foo': 'bar'}, files=files) + body = httpretty.last_request().parsed_body + d = body['rebuild'] + self.assertEqual('new', d['name']) + self.assertEqual({'foo': 'bar'}, d['metadata']) + self.assertEqual('/etc/passwd', + d['personality'][0]['path']) + def test_resize_server(self): s = self.cs.servers.get(1234) s.resize(flavor=1) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index da2fed520..888d8fe6d 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -886,6 +886,19 @@ def test_rebuild_preserve_ephemeral(self): self.assert_called('GET', '/flavors/1', pos=-2) self.assert_called('GET', '/images/2') + def test_rebuild_name_meta(self): + self.run_command('rebuild sample-server 1 --name asdf --meta ' + 'foo=bar') + self.assert_called('GET', '/servers', pos=-6) + self.assert_called('GET', '/servers/1234', pos=-5) + self.assert_called('GET', '/images/1', pos=-4) + self.assert_called('POST', '/servers/1234/action', + {'rebuild': {'imageRef': 1, + 'name': 'asdf', + 'metadata': {'foo': 'bar'}}}, pos=-3) + self.assert_called('GET', '/flavors/1', pos=-2) + self.assert_called('GET', '/images/2') + def test_start(self): self.run_command('start sample-server') self.assert_called('POST', '/servers/1234/action', {'os-start': None}) diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index 400999049..8a930e8f9 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -922,7 +922,8 @@ def reboot(self, server, reboot_type=REBOOT_SOFT): self._action('reboot', server, {'type': reboot_type}) def rebuild(self, server, image, password=None, disk_config=None, - preserve_ephemeral=False, **kwargs): + preserve_ephemeral=False, name=None, meta=None, files=None, + **kwargs): """ Rebuild -- shut down and then re-image -- a server. @@ -933,6 +934,15 @@ def rebuild(self, server, image, password=None, disk_config=None, Valid values are 'AUTO' or 'MANUAL' :param preserve_ephemeral: If True, request that any ephemeral device be preserved when rebuilding the instance. Defaults to False. + :param name: Something to name the server. + :param meta: A dict of arbitrary key/value metadata to store for this + server. A maximum of five entries is allowed, and both + keys and values must be 255 characters or less. + :param files: A dict of files to overwrite on the server upon boot. + Keys are file names (i.e. ``/etc/passwd``) and values + are the file contents (either as a string or as a + file-like object). A maximum of five entries is allowed, + and each file must be 10k or less. """ body = {'imageRef': base.getid(image)} if password is not None: @@ -941,6 +951,24 @@ def rebuild(self, server, image, password=None, disk_config=None, body['OS-DCF:diskConfig'] = disk_config if preserve_ephemeral is not False: body['preserve_ephemeral'] = True + if name is not None: + body['name'] = name + if meta: + body['metadata'] = meta + if files: + personality = body['personality'] = [] + for filepath, file_or_string in sorted(files.items(), + key=lambda x: x[0]): + if hasattr(file_or_string, 'read'): + data = file_or_string.read() + else: + data = file_or_string + + cont = base64.b64encode(data.encode('utf-8')).decode('utf-8') + personality.append({ + 'path': filepath, + 'contents': cont, + }) _resp, body = self._action('rebuild', server, body, **kwargs) return Server(self, body['server']) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index ddcbcc5ca..667892226 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1243,6 +1243,23 @@ def do_reboot(cs, args): action="store_true", default=False, help='Preserve the default ephemeral storage partition on rebuild.') +@utils.arg('--name', + metavar='', + default=None, + help=_('Name for the new server')) +@utils.arg('--meta', + metavar="", + action='append', + default=[], + help=_("Record arbitrary key/value metadata to /meta.js " + "on the new server. Can be specified multiple times.")) +@utils.arg('--file', + metavar="", + action='append', + dest='files', + default=[], + help=_("Store arbitrary files from locally to " + "on the new server. You may store up to 5 files.")) def do_rebuild(cs, args): """Shutdown, re-image, and re-boot a server.""" server = _find_server(cs, args.server) @@ -1255,6 +1272,25 @@ def do_rebuild(cs, args): kwargs = utils.get_resource_manager_extra_kwargs(do_rebuild, args) kwargs['preserve_ephemeral'] = args.preserve_ephemeral + kwargs['name'] = args.name + meta = dict(v.split('=', 1) for v in args.meta) + kwargs['meta'] = meta + + files = {} + for f in args.files: + try: + dst, src = f.split('=', 1) + with open(src, 'r') as s: + files[dst] = s.read() + except IOError as e: + raise exceptions.CommandError(_("Can't open '%(src)s': %(exc)s") % + {'src': src, 'exc': e}) + except ValueError as e: + raise exceptions.CommandError(_("Invalid file argument '%s'. " + "File arguments must be of the " + "form '--file " + "'") % f) + kwargs['files'] = files server = server.rebuild(image, _password, **kwargs) _print_server(cs, args, server) From 448b807ccc89f16e35aadd02167e8c17d7321d8f Mon Sep 17 00:00:00 2001 From: Jamie Lennox Date: Fri, 9 May 2014 17:45:03 +1000 Subject: [PATCH 0543/1705] Convert hosts tests to httpretty Change-Id: Ib1cdb508ef04a86350d3890c14e181b6ed1177f2 blueprint: httpretty-testing --- novaclient/tests/fixture_data/hosts.py | 165 +++++++++++++++++++++++++ novaclient/tests/v1_1/test_hosts.py | 45 +++---- novaclient/tests/v3/test_hosts.py | 45 +++---- 3 files changed, 211 insertions(+), 44 deletions(-) create mode 100644 novaclient/tests/fixture_data/hosts.py diff --git a/novaclient/tests/fixture_data/hosts.py b/novaclient/tests/fixture_data/hosts.py new file mode 100644 index 000000000..193a5bb67 --- /dev/null +++ b/novaclient/tests/fixture_data/hosts.py @@ -0,0 +1,165 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import httpretty +from six.moves.urllib import parse + +from novaclient.openstack.common import jsonutils +from novaclient.tests.fixture_data import base + + +class BaseFixture(base.Fixture): + + base_url = 'os-hosts' + + def setUp(self): + super(BaseFixture, self).setUp() + + get_os_hosts_host = { + 'host': [ + {'resource': {'project': '(total)', 'host': 'dummy', + 'cpu': 16, 'memory_mb': 32234, 'disk_gb': 128}}, + {'resource': {'project': '(used_now)', 'host': 'dummy', + 'cpu': 1, 'memory_mb': 2075, 'disk_gb': 45}}, + {'resource': {'project': '(used_max)', 'host': 'dummy', + 'cpu': 1, 'memory_mb': 2048, 'disk_gb': 30}}, + {'resource': {'project': 'admin', 'host': 'dummy', + 'cpu': 1, 'memory_mb': 2048, 'disk_gb': 30}} + ] + } + httpretty.register_uri(httpretty.GET, self.url('host'), + body=jsonutils.dumps(get_os_hosts_host), + content_type='application/json') + + def get_os_hosts(request, url, headers): + host, query = parse.splitquery(url) + zone = 'nova1' + + if query: + qs = parse.parse_qs(query) + try: + zone = qs['zone'][0] + except Exception: + pass + + data = { + 'hosts': [ + { + 'host': 'host1', + 'service': 'nova-compute', + 'zone': zone + }, + { + 'host': 'host1', + 'service': 'nova-cert', + 'zone': zone + } + ] + } + return 200, headers, jsonutils.dumps(data) + + httpretty.register_uri(httpretty.GET, self.url(), + body=get_os_hosts, + content_type='application/json') + + get_os_hosts_sample_host = { + 'host': [ + {'resource': {'host': 'sample_host'}} + ], + } + httpretty.register_uri(httpretty.GET, self.url('sample_host'), + body=jsonutils.dumps(get_os_hosts_sample_host), + content_type='application/json') + + httpretty.register_uri(httpretty.PUT, self.url('sample_host', 1), + body=jsonutils.dumps(self.put_host_1()), + content_type='application/json') + + httpretty.register_uri(httpretty.PUT, self.url('sample_host', 2), + body=jsonutils.dumps(self.put_host_2()), + content_type='application/json') + + httpretty.register_uri(httpretty.PUT, self.url('sample_host', 3), + body=jsonutils.dumps(self.put_host_3()), + content_type='application/json') + + url = self.url('sample_host', 'reboot') + httpretty.register_uri(httpretty.GET, url, + body=jsonutils.dumps(self.get_host_reboot()), + content_type='application/json') + + url = self.url('sample_host', 'startup') + httpretty.register_uri(httpretty.GET, url, + body=jsonutils.dumps(self.get_host_startup()), + content_type='application/json') + + url = self.url('sample_host', 'shutdown') + httpretty.register_uri(httpretty.GET, url, + body=jsonutils.dumps(self.get_host_shutdown()), + content_type='application/json') + + def put_os_hosts_sample_host(request, url, headers): + result = {'host': 'dummy'} + result.update(jsonutils.loads(request.body.decode('utf-8'))) + return 200, headers, jsonutils.dumps(result) + + httpretty.register_uri(httpretty.PUT, self.url('sample_host'), + body=put_os_hosts_sample_host, + content_type='application/json') + + +class V1(BaseFixture): + + def put_host_1(self): + return {'host': 'sample-host_1', + 'status': 'enabled'} + + def put_host_2(self): + return {'host': 'sample-host_2', + 'maintenance_mode': 'on_maintenance'} + + def put_host_3(self): + return {'host': 'sample-host_3', + 'status': 'enabled', + 'maintenance_mode': 'on_maintenance'} + + def get_host_reboot(self): + return {'host': 'sample_host', + 'power_action': 'reboot'} + + def get_host_startup(self): + return {'host': 'sample_host', + 'power_action': 'startup'} + + def get_host_shutdown(self): + return {'host': 'sample_host', + 'power_action': 'shutdown'} + + +class V3(V1): + def put_host_1(self): + return {'host': super(V3, self).put_host_1()} + + def put_host_2(self): + return {'host': super(V3, self).put_host_2()} + + def put_host_3(self): + return {'host': super(V3, self).put_host_3()} + + def get_host_reboot(self): + return {'host': super(V3, self).get_host_reboot()} + + def get_host_startup(self): + return {'host': super(V3, self).get_host_startup()} + + def get_host_shutdown(self): + return {'host': super(V3, self).get_host_shutdown()} diff --git a/novaclient/tests/v1_1/test_hosts.py b/novaclient/tests/v1_1/test_hosts.py index 1ab64d514..d8291c6ae 100644 --- a/novaclient/tests/v1_1/test_hosts.py +++ b/novaclient/tests/v1_1/test_hosts.py @@ -11,69 +11,70 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient.tests.fixture_data import client +from novaclient.tests.fixture_data import hosts as data from novaclient.tests import utils -from novaclient.tests.v1_1 import fakes from novaclient.v1_1 import hosts -cs = fakes.FakeClient() +class HostsTest(utils.FixturedTestCase): - -class HostsTest(utils.TestCase): + client_fixture_class = client.V1 + data_fixture_class = data.V1 def test_describe_resource(self): - hs = cs.hosts.get('host') - cs.assert_called('GET', '/os-hosts/host') + hs = self.cs.hosts.get('host') + self.assert_called('GET', '/os-hosts/host') [self.assertIsInstance(h, hosts.Host) for h in hs] def test_list_host(self): - hs = cs.hosts.list() - cs.assert_called('GET', '/os-hosts') + hs = self.cs.hosts.list() + self.assert_called('GET', '/os-hosts') [self.assertIsInstance(h, hosts.Host) for h in hs] [self.assertEqual(h.zone, 'nova1') for h in hs] def test_list_host_with_zone(self): - hs = cs.hosts.list('nova') - cs.assert_called('GET', '/os-hosts?zone=nova') + hs = self.cs.hosts.list('nova') + self.assert_called('GET', '/os-hosts?zone=nova') [self.assertIsInstance(h, hosts.Host) for h in hs] [self.assertEqual(h.zone, 'nova') for h in hs] def test_update_enable(self): - host = cs.hosts.get('sample_host')[0] + host = self.cs.hosts.get('sample_host')[0] values = {"status": "enabled"} result = host.update(values) - cs.assert_called('PUT', '/os-hosts/sample_host', values) + self.assert_called('PUT', '/os-hosts/sample_host', values) self.assertIsInstance(result, hosts.Host) def test_update_maintenance(self): - host = cs.hosts.get('sample_host')[0] + host = self.cs.hosts.get('sample_host')[0] values = {"maintenance_mode": "enable"} result = host.update(values) - cs.assert_called('PUT', '/os-hosts/sample_host', values) + self.assert_called('PUT', '/os-hosts/sample_host', values) self.assertIsInstance(result, hosts.Host) def test_update_both(self): - host = cs.hosts.get('sample_host')[0] + host = self.cs.hosts.get('sample_host')[0] values = {"status": "enabled", "maintenance_mode": "enable"} result = host.update(values) - cs.assert_called('PUT', '/os-hosts/sample_host', values) + self.assert_called('PUT', '/os-hosts/sample_host', values) self.assertIsInstance(result, hosts.Host) def test_host_startup(self): - host = cs.hosts.get('sample_host')[0] + host = self.cs.hosts.get('sample_host')[0] host.startup() - cs.assert_called( + self.assert_called( 'GET', '/os-hosts/sample_host/startup') def test_host_reboot(self): - host = cs.hosts.get('sample_host')[0] + host = self.cs.hosts.get('sample_host')[0] host.reboot() - cs.assert_called( + self.assert_called( 'GET', '/os-hosts/sample_host/reboot') def test_host_shutdown(self): - host = cs.hosts.get('sample_host')[0] + host = self.cs.hosts.get('sample_host')[0] host.shutdown() - cs.assert_called( + self.assert_called( 'GET', '/os-hosts/sample_host/shutdown') diff --git a/novaclient/tests/v3/test_hosts.py b/novaclient/tests/v3/test_hosts.py index 19a5060df..cbd47822b 100644 --- a/novaclient/tests/v3/test_hosts.py +++ b/novaclient/tests/v3/test_hosts.py @@ -12,72 +12,73 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient.tests.fixture_data import client +from novaclient.tests.fixture_data import hosts as data from novaclient.tests import utils -from novaclient.tests.v3 import fakes from novaclient.v3 import hosts -cs = fakes.FakeClient() +class HostsTest(utils.FixturedTestCase): - -class HostsTest(utils.TestCase): + client_fixture_class = client.V3 + data_fixture_class = data.V3 def test_describe_resource(self): - hs = cs.hosts.get('host') - cs.assert_called('GET', '/os-hosts/host') + hs = self.cs.hosts.get('host') + self.assert_called('GET', '/os-hosts/host') for h in hs: self.assertIsInstance(h, hosts.Host) def test_list_host(self): - hs = cs.hosts.list() - cs.assert_called('GET', '/os-hosts') + hs = self.cs.hosts.list() + self.assert_called('GET', '/os-hosts') for h in hs: self.assertIsInstance(h, hosts.Host) self.assertEqual(h.zone, 'nova1') def test_list_host_with_zone(self): - hs = cs.hosts.list('nova') - cs.assert_called('GET', '/os-hosts?zone=nova') + hs = self.cs.hosts.list('nova') + self.assert_called('GET', '/os-hosts?zone=nova') for h in hs: self.assertIsInstance(h, hosts.Host) self.assertEqual(h.zone, 'nova') def test_update_enable(self): - host = cs.hosts.get('sample_host')[0] + host = self.cs.hosts.get('sample_host')[0] values = {"status": "enabled"} result = host.update(values) - cs.assert_called('PUT', '/os-hosts/sample_host', {"host": values}) + self.assert_called('PUT', '/os-hosts/sample_host', {"host": values}) self.assertIsInstance(result, hosts.Host) def test_update_maintenance(self): - host = cs.hosts.get('sample_host')[0] + host = self.cs.hosts.get('sample_host')[0] values = {"maintenance_mode": "enable"} result = host.update(values) - cs.assert_called('PUT', '/os-hosts/sample_host', {"host": values}) + self.assert_called('PUT', '/os-hosts/sample_host', {"host": values}) self.assertIsInstance(result, hosts.Host) def test_update_both(self): - host = cs.hosts.get('sample_host')[0] + host = self.cs.hosts.get('sample_host')[0] values = {"status": "enabled", "maintenance_mode": "enable"} result = host.update(values) - cs.assert_called('PUT', '/os-hosts/sample_host', {"host": values}) + self.assert_called('PUT', '/os-hosts/sample_host', {"host": values}) self.assertIsInstance(result, hosts.Host) def test_host_startup(self): - host = cs.hosts.get('sample_host')[0] + host = self.cs.hosts.get('sample_host')[0] host.startup() - cs.assert_called( + self.assert_called( 'GET', '/os-hosts/sample_host/startup') def test_host_reboot(self): - host = cs.hosts.get('sample_host')[0] + host = self.cs.hosts.get('sample_host')[0] host.reboot() - cs.assert_called( + self.assert_called( 'GET', '/os-hosts/sample_host/reboot') def test_host_shutdown(self): - host = cs.hosts.get('sample_host')[0] + host = self.cs.hosts.get('sample_host')[0] host.shutdown() - cs.assert_called( + self.assert_called( 'GET', '/os-hosts/sample_host/shutdown') From 817ec0764c3e8078322be0f181bb0780f4fa9462 Mon Sep 17 00:00:00 2001 From: Jamie Lennox Date: Mon, 26 May 2014 10:47:38 +1000 Subject: [PATCH 0544/1705] Convert Hypervisor tests to httpretty Change-Id: I0dc0167af618e88f76ace9b893b2b26966903457 blueprint: httpretty-testing --- novaclient/tests/fixture_data/base.py | 10 +- novaclient/tests/fixture_data/hypervisors.py | 210 +++++++++++++++++++ novaclient/tests/v1_1/test_hypervisors.py | 26 ++- novaclient/tests/v3/test_hypervisors.py | 14 +- 4 files changed, 236 insertions(+), 24 deletions(-) create mode 100644 novaclient/tests/fixture_data/hypervisors.py diff --git a/novaclient/tests/fixture_data/base.py b/novaclient/tests/fixture_data/base.py index 3c96133b6..72f46d1dd 100644 --- a/novaclient/tests/fixture_data/base.py +++ b/novaclient/tests/fixture_data/base.py @@ -11,6 +11,7 @@ # under the License. import fixtures +from six.moves.urllib import parse COMPUTE_URL = 'http://compute.host' @@ -23,10 +24,15 @@ def __init__(self, compute_url=COMPUTE_URL): super(Fixture, self).__init__() self.compute_url = compute_url - def url(self, *args): + def url(self, *args, **kwargs): url_args = [self.compute_url] if self.base_url: url_args.append(self.base_url) - return '/'.join(str(a).strip('/') for a in tuple(url_args) + args) + url = '/'.join(str(a).strip('/') for a in tuple(url_args) + args) + + if kwargs: + url += '?%s' % parse.urlencode(kwargs, doseq=True) + + return url diff --git a/novaclient/tests/fixture_data/hypervisors.py b/novaclient/tests/fixture_data/hypervisors.py new file mode 100644 index 000000000..8133de442 --- /dev/null +++ b/novaclient/tests/fixture_data/hypervisors.py @@ -0,0 +1,210 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import httpretty + +from novaclient.openstack.common import jsonutils +from novaclient.tests.fixture_data import base + + +class V1(base.Fixture): + + base_url = 'os-hypervisors' + + def setUp(self): + super(V1, self).setUp() + + get_os_hypervisors = { + 'hypervisors': [ + {'id': 1234, 'hypervisor_hostname': 'hyper1'}, + {'id': 5678, 'hypervisor_hostname': 'hyper2'}, + ] + } + + httpretty.register_uri(httpretty.GET, self.url(), + body=jsonutils.dumps(get_os_hypervisors), + content_type='application/json') + + get_os_hypervisors_detail = { + 'hypervisors': [ + { + 'id': 1234, + 'service': {'id': 1, 'host': 'compute1'}, + 'vcpus': 4, + 'memory_mb': 10 * 1024, + 'local_gb': 250, + 'vcpus_used': 2, + 'memory_mb_used': 5 * 1024, + 'local_gb_used': 125, + 'hypervisor_type': 'xen', + 'hypervisor_version': 3, + 'hypervisor_hostname': 'hyper1', + 'free_ram_mb': 5 * 1024, + 'free_disk_gb': 125, + 'current_workload': 2, + 'running_vms': 2, + 'cpu_info': 'cpu_info', + 'disk_available_least': 100 + }, + { + 'id': 2, + 'service': {'id': 2, 'host': 'compute2'}, + 'vcpus': 4, + 'memory_mb': 10 * 1024, + 'local_gb': 250, + 'vcpus_used': 2, + 'memory_mb_used': 5 * 1024, + 'local_gb_used': 125, + 'hypervisor_type': 'xen', + 'hypervisor_version': 3, + 'hypervisor_hostname': 'hyper2', + 'free_ram_mb': 5 * 1024, + 'free_disk_gb': 125, + 'current_workload': 2, + 'running_vms': 2, + 'cpu_info': 'cpu_info', + 'disk_available_least': 100 + } + ] + } + + httpretty.register_uri(httpretty.GET, self.url('detail'), + body=jsonutils.dumps(get_os_hypervisors_detail), + content_type='application/json') + + get_os_hypervisors_stats = { + 'hypervisor_statistics': { + 'count': 2, + 'vcpus': 8, + 'memory_mb': 20 * 1024, + 'local_gb': 500, + 'vcpus_used': 4, + 'memory_mb_used': 10 * 1024, + 'local_gb_used': 250, + 'free_ram_mb': 10 * 1024, + 'free_disk_gb': 250, + 'current_workload': 4, + 'running_vms': 4, + 'disk_available_least': 200, + } + } + + httpretty.register_uri(httpretty.GET, self.url('statistics'), + body=jsonutils.dumps(get_os_hypervisors_stats), + content_type='application/json') + + get_os_hypervisors_search = { + 'hypervisors': [ + {'id': 1234, 'hypervisor_hostname': 'hyper1'}, + {'id': 5678, 'hypervisor_hostname': 'hyper2'} + ] + } + + httpretty.register_uri(httpretty.GET, self.url('hyper', 'search'), + body=jsonutils.dumps(get_os_hypervisors_search), + content_type='application/json') + + get_hyper_server = { + 'hypervisors': [ + { + 'id': 1234, + 'hypervisor_hostname': 'hyper1', + 'servers': [ + {'name': 'inst1', 'uuid': 'uuid1'}, + {'name': 'inst2', 'uuid': 'uuid2'} + ] + }, + { + 'id': 5678, + 'hypervisor_hostname': 'hyper2', + 'servers': [ + {'name': 'inst3', 'uuid': 'uuid3'}, + {'name': 'inst4', 'uuid': 'uuid4'} + ] + } + ] + } + + httpretty.register_uri(httpretty.GET, self.url('hyper', 'servers'), + body=jsonutils.dumps(get_hyper_server), + content_type='application/json') + + get_os_hypervisors_1234 = { + 'hypervisor': { + 'id': 1234, + 'service': {'id': 1, 'host': 'compute1'}, + 'vcpus': 4, + 'memory_mb': 10 * 1024, + 'local_gb': 250, + 'vcpus_used': 2, + 'memory_mb_used': 5 * 1024, + 'local_gb_used': 125, + 'hypervisor_type': 'xen', + 'hypervisor_version': 3, + 'hypervisor_hostname': 'hyper1', + 'free_ram_mb': 5 * 1024, + 'free_disk_gb': 125, + 'current_workload': 2, + 'running_vms': 2, + 'cpu_info': 'cpu_info', + 'disk_available_least': 100 + } + } + + httpretty.register_uri(httpretty.GET, self.url(1234), + body=jsonutils.dumps(get_os_hypervisors_1234), + content_type='application/json') + + get_os_hypervisors_uptime = { + 'hypervisor': { + 'id': 1234, + 'hypervisor_hostname': 'hyper1', + 'uptime': 'fake uptime' + } + } + + httpretty.register_uri(httpretty.GET, self.url(1234, 'uptime'), + body=jsonutils.dumps(get_os_hypervisors_uptime), + content_type='application/json') + + +class V3(V1): + + def setUp(self): + super(V3, self).setUp() + + get_os_hypervisors_search = { + 'hypervisors': [ + {'id': 1234, 'hypervisor_hostname': 'hyper1'}, + {'id': 5678, 'hypervisor_hostname': 'hyper2'} + ] + } + + httpretty.register_uri(httpretty.GET, + self.url('search', query='hyper'), + body=jsonutils.dumps(get_os_hypervisors_search), + content_type='application/json') + + get_1234_servers = { + 'hypervisor': { + 'id': 1234, + 'hypervisor_hostname': 'hyper1', + 'servers': [ + {'name': 'inst1', 'id': 'uuid1'}, + {'name': 'inst2', 'id': 'uuid2'} + ] + }, + } + + httpretty.register_uri(httpretty.GET, self.url(1234, 'servers'), + body=jsonutils.dumps(get_1234_servers), + content_type='application/json') diff --git a/novaclient/tests/v1_1/test_hypervisors.py b/novaclient/tests/v1_1/test_hypervisors.py index 3cf66c783..8376d61f5 100644 --- a/novaclient/tests/v1_1/test_hypervisors.py +++ b/novaclient/tests/v1_1/test_hypervisors.py @@ -13,17 +13,15 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient.tests.fixture_data import client +from novaclient.tests.fixture_data import hypervisors as data from novaclient.tests import utils -from novaclient.tests.v1_1 import fakes -class HypervisorsTest(utils.TestCase): - def setUp(self): - super(HypervisorsTest, self).setUp() - self.cs = self._get_fake_client() +class HypervisorsTest(utils.FixturedTestCase): - def _get_fake_client(self): - return fakes.FakeClient() + client_fixture_class = client.V1 + data_fixture_class = data.V1 def compare_to_expected(self, expected, hyper): for key, value in expected.items(): @@ -36,7 +34,7 @@ def test_hypervisor_index(self): ] result = self.cs.hypervisors.list(False) - self.cs.assert_called('GET', '/os-hypervisors') + self.assert_called('GET', '/os-hypervisors') for idx, hyper in enumerate(result): self.compare_to_expected(expected[idx], hyper) @@ -79,7 +77,7 @@ def test_hypervisor_detail(self): disk_available_least=100)] result = self.cs.hypervisors.list() - self.cs.assert_called('GET', '/os-hypervisors/detail') + self.assert_called('GET', '/os-hypervisors/detail') for idx, hyper in enumerate(result): self.compare_to_expected(expected[idx], hyper) @@ -91,7 +89,7 @@ def test_hypervisor_search(self): ] result = self.cs.hypervisors.search('hyper') - self.cs.assert_called('GET', '/os-hypervisors/hyper/search') + self.assert_called('GET', '/os-hypervisors/hyper/search') for idx, hyper in enumerate(result): self.compare_to_expected(expected[idx], hyper) @@ -111,7 +109,7 @@ def test_hypervisor_servers(self): ] result = self.cs.hypervisors.search('hyper', True) - self.cs.assert_called('GET', '/os-hypervisors/hyper/servers') + self.assert_called('GET', '/os-hypervisors/hyper/servers') for idx, hyper in enumerate(result): self.compare_to_expected(expected[idx], hyper) @@ -137,7 +135,7 @@ def test_hypervisor_get(self): disk_available_least=100) result = self.cs.hypervisors.get(1234) - self.cs.assert_called('GET', '/os-hypervisors/1234') + self.assert_called('GET', '/os-hypervisors/1234') self.compare_to_expected(expected, result) @@ -148,7 +146,7 @@ def test_hypervisor_uptime(self): uptime="fake uptime") result = self.cs.hypervisors.uptime(1234) - self.cs.assert_called('GET', '/os-hypervisors/1234/uptime') + self.assert_called('GET', '/os-hypervisors/1234/uptime') self.compare_to_expected(expected, result) @@ -169,6 +167,6 @@ def test_hypervisor_statistics(self): ) result = self.cs.hypervisors.statistics() - self.cs.assert_called('GET', '/os-hypervisors/statistics') + self.assert_called('GET', '/os-hypervisors/statistics') self.compare_to_expected(expected, result) diff --git a/novaclient/tests/v3/test_hypervisors.py b/novaclient/tests/v3/test_hypervisors.py index 97e69a8a1..ee7c83fcb 100644 --- a/novaclient/tests/v3/test_hypervisors.py +++ b/novaclient/tests/v3/test_hypervisors.py @@ -13,17 +13,15 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient.tests.fixture_data import client +from novaclient.tests.fixture_data import hypervisors as data from novaclient.tests.v1_1 import test_hypervisors -from novaclient.tests.v3 import fakes class HypervisorsTest(test_hypervisors.HypervisorsTest): - def setUp(self): - super(HypervisorsTest, self).setUp() - self.cs = self._get_fake_client() - def _get_fake_client(self): - return fakes.FakeClient() + client_fixture_class = client.V3 + data_fixture_class = data.V3 def test_hypervisor_search(self): expected = [ @@ -32,7 +30,7 @@ def test_hypervisor_search(self): ] result = self.cs.hypervisors.search('hyper') - self.cs.assert_called('GET', '/os-hypervisors/search?query=hyper') + self.assert_called('GET', '/os-hypervisors/search?query=hyper') for idx, hyper in enumerate(result): self.compare_to_expected(expected[idx], hyper) @@ -45,6 +43,6 @@ def test_hypervisor_servers(self): dict(name='inst2', id='uuid2')]) result = self.cs.hypervisors.servers('1234') - self.cs.assert_called('GET', '/os-hypervisors/1234/servers') + self.assert_called('GET', '/os-hypervisors/1234/servers') self.compare_to_expected(expected, result) From 935219a6eb80a81532c268f1ec6f59b456d46f48 Mon Sep 17 00:00:00 2001 From: Jamie Lennox Date: Mon, 26 May 2014 11:24:11 +1000 Subject: [PATCH 0545/1705] Convert image tests to httpretty Change-Id: I3abc51ba4dcc641b72e3ac5e09955e4b22718451 blueprint: httpretty-testing --- novaclient/tests/fixture_data/images.py | 119 ++++++++++++++++++++++++ novaclient/tests/v1_1/test_images.py | 44 ++++----- novaclient/tests/v3/test_images.py | 32 +++---- 3 files changed, 157 insertions(+), 38 deletions(-) create mode 100644 novaclient/tests/fixture_data/images.py diff --git a/novaclient/tests/fixture_data/images.py b/novaclient/tests/fixture_data/images.py new file mode 100644 index 000000000..09a134e5f --- /dev/null +++ b/novaclient/tests/fixture_data/images.py @@ -0,0 +1,119 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import httpretty + +from novaclient.openstack.common import jsonutils +from novaclient.tests import fakes +from novaclient.tests.fixture_data import base + + +class V1(base.Fixture): + + base_url = 'images' + + def setUp(self): + super(V1, self).setUp() + + get_images = { + 'images': [ + {'id': 1, 'name': 'CentOS 5.2'}, + {'id': 2, 'name': 'My Server Backup'} + ] + } + + httpretty.register_uri(httpretty.GET, self.url(), + body=jsonutils.dumps(get_images), + content_type='application/json') + + image_1 = { + 'id': 1, + 'name': 'CentOS 5.2', + "updated": "2010-10-10T12:00:00Z", + "created": "2010-08-10T12:00:00Z", + "status": "ACTIVE", + "metadata": { + "test_key": "test_value", + }, + "links": {}, + } + + image_2 = { + "id": 2, + "name": "My Server Backup", + "serverId": 1234, + "updated": "2010-10-10T12:00:00Z", + "created": "2010-08-10T12:00:00Z", + "status": "SAVING", + "progress": 80, + "links": {}, + } + + get_images_detail = {'images': [image_1, image_2]} + + httpretty.register_uri(httpretty.GET, self.url('detail'), + body=jsonutils.dumps(get_images_detail), + content_type='application/json') + + get_images_1 = {'image': image_1} + + httpretty.register_uri(httpretty.GET, self.url(1), + body=jsonutils.dumps(get_images_1), + content_type='application/json') + + get_images_2 = {'image': image_2} + + httpretty.register_uri(httpretty.GET, self.url(2), + body=jsonutils.dumps(get_images_2), + content_type='application/json') + + httpretty.register_uri(httpretty.GET, self.url(456), + body=jsonutils.dumps(get_images_2), + content_type='application/json') + + def post_images(request, url, headers): + body = jsonutils.loads(request.body.decode('utf-8')) + assert list(body) == ['image'] + fakes.assert_has_keys(body['image'], required=['serverId', 'name']) + return 202, headers, jsonutils.dumps(images_1) + + httpretty.register_uri(httpretty.POST, self.url(), + body=post_images, + content_type='application/json') + + def post_images_1_metadata(request, url, headers): + body = jsonutils.loads(request.body.decode('utf-8')) + assert list(body) == ['metadata'] + fakes.assert_has_keys(body['metadata'], required=['test_key']) + data = jsonutils.dumps({'metadata': image_1['metadata']}) + return 200, headers, data + + httpretty.register_uri(httpretty.POST, self.url(1, 'metadata'), + body=post_images_1_metadata, + content_type='application/json') + + for u in (1, 2, '1/metadata/test_key'): + httpretty.register_uri(httpretty.DELETE, self.url(u), + status=204) + + httpretty.register_uri(httpretty.HEAD, self.url(1), status=200, + x_image_meta_id=1, + x_image_meta_name='CentOS 5.2', + x_image_meta_updated='2010-10-10T12:00:00Z', + x_image_meta_created='2010-10-10T12:00:00Z', + x_image_meta_status='ACTIVE', + x_image_meta_property_test_key='test_value') + + +class V3(V1): + + base_url = 'v1/images' diff --git a/novaclient/tests/v1_1/test_images.py b/novaclient/tests/v1_1/test_images.py index 937bea1e6..6ff8fe9c7 100644 --- a/novaclient/tests/v1_1/test_images.py +++ b/novaclient/tests/v1_1/test_images.py @@ -11,56 +11,56 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient.tests.fixture_data import client +from novaclient.tests.fixture_data import images as data from novaclient.tests import utils -from novaclient.tests.v1_1 import fakes from novaclient.v1_1 import images -cs = fakes.FakeClient() +class ImagesTest(utils.FixturedTestCase): - -class ImagesTest(utils.TestCase): + client_fixture_class = client.V1 + data_fixture_class = data.V1 def test_list_images(self): - il = cs.images.list() - cs.assert_called('GET', '/images/detail') + il = self.cs.images.list() + self.assert_called('GET', '/images/detail') [self.assertIsInstance(i, images.Image) for i in il] def test_list_images_undetailed(self): - il = cs.images.list(detailed=False) - cs.assert_called('GET', '/images') + il = self.cs.images.list(detailed=False) + self.assert_called('GET', '/images') [self.assertIsInstance(i, images.Image) for i in il] def test_list_images_with_limit(self): - cs.images.list(limit=4) - cs.assert_called('GET', '/images/detail?limit=4') + self.cs.images.list(limit=4) + self.assert_called('GET', '/images/detail?limit=4') def test_get_image_details(self): - i = cs.images.get(1) - cs.assert_called('GET', '/images/1') + i = self.cs.images.get(1) + self.assert_called('GET', '/images/1') self.assertIsInstance(i, images.Image) self.assertEqual(i.id, 1) self.assertEqual(i.name, 'CentOS 5.2') def test_delete_image(self): - cs.images.delete(1) - cs.assert_called('DELETE', '/images/1') + self.cs.images.delete(1) + self.assert_called('DELETE', '/images/1') def test_delete_meta(self): - cs.images.delete_meta(1, {'test_key': 'test_value'}) - cs.assert_called('DELETE', '/images/1/metadata/test_key') + self.cs.images.delete_meta(1, {'test_key': 'test_value'}) + self.assert_called('DELETE', '/images/1/metadata/test_key') def test_set_meta(self): - cs.images.set_meta(1, {'test_key': 'test_value'}) - cs.assert_called('POST', '/images/1/metadata', + self.cs.images.set_meta(1, {'test_key': 'test_value'}) + self.assert_called('POST', '/images/1/metadata', {"metadata": {'test_key': 'test_value'}}) def test_find(self): - i = cs.images.find(name="CentOS 5.2") + i = self.cs.images.find(name="CentOS 5.2") self.assertEqual(i.id, 1) - cs.assert_called('GET', '/images', pos=-2) - cs.assert_called('GET', '/images/1', pos=-1) + self.assert_called('GET', '/images/1') - iml = cs.images.findall(status='SAVING') + iml = self.cs.images.findall(status='SAVING') self.assertEqual(len(iml), 1) self.assertEqual(iml[0].name, 'My Server Backup') diff --git a/novaclient/tests/v3/test_images.py b/novaclient/tests/v3/test_images.py index c3e8991a9..8b4694f00 100644 --- a/novaclient/tests/v3/test_images.py +++ b/novaclient/tests/v3/test_images.py @@ -13,45 +13,45 @@ # under the License. +from novaclient.tests.fixture_data import client +from novaclient.tests.fixture_data import images as data from novaclient.tests import utils -from novaclient.tests.v3 import fakes from novaclient.v3 import images -cs = fakes.FakeClient() +class ImagesTest(utils.FixturedTestCase): - -class ImagesTest(utils.TestCase): + client_fixture_class = client.V3 + data_fixture_class = data.V3 def test_list_images(self): - il = cs.images.list() - cs.assert_called('GET', '/v1/images/detail') + il = self.cs.images.list() + self.assert_called('GET', '/v1/images/detail') for i in il: self.assertIsInstance(i, images.Image) def test_list_images_undetailed(self): - il = cs.images.list(detailed=False) - cs.assert_called('GET', '/v1/images') + il = self.cs.images.list(detailed=False) + self.assert_called('GET', '/v1/images') for i in il: self.assertIsInstance(i, images.Image) def test_list_images_with_limit(self): - cs.images.list(limit=4) - cs.assert_called('GET', '/v1/images/detail?limit=4') + self.cs.images.list(limit=4) + self.assert_called('GET', '/v1/images/detail?limit=4') def test_get_image_details(self): - i = cs.images.get(1) - cs.assert_called('HEAD', '/v1/images/1') + i = self.cs.images.get(1) + self.assert_called('HEAD', '/v1/images/1') self.assertIsInstance(i, images.Image) self.assertEqual(i.id, '1') self.assertEqual(i.name, 'CentOS 5.2') def test_find(self): - i = cs.images.find(name="CentOS 5.2") + i = self.cs.images.find(name="CentOS 5.2") self.assertEqual(i.id, '1') - cs.assert_called('GET', '/v1/images', pos=-2) - cs.assert_called('HEAD', '/v1/images/1', pos=-1) + self.assert_called('HEAD', '/v1/images/1') - iml = cs.images.findall(status='SAVING') + iml = self.cs.images.findall(status='SAVING') self.assertEqual(len(iml), 1) self.assertEqual(iml[0].name, 'My Server Backup') From 9348128d35e7a030a3d29c9505d15c9a6d4bd029 Mon Sep 17 00:00:00 2001 From: Jamie Lennox Date: Mon, 26 May 2014 11:44:37 +1000 Subject: [PATCH 0546/1705] Convert keypair tests to httpretty Change-Id: I6876d97dd6600a0a34b89d9f693078f495085622 blueprint: httpretty-testing --- novaclient/tests/fixture_data/keypairs.py | 52 +++++++++++++++++++++++ novaclient/tests/v1_1/test_keypairs.py | 32 ++++++++------ novaclient/tests/v3/test_keypairs.py | 15 +++---- 3 files changed, 77 insertions(+), 22 deletions(-) create mode 100644 novaclient/tests/fixture_data/keypairs.py diff --git a/novaclient/tests/fixture_data/keypairs.py b/novaclient/tests/fixture_data/keypairs.py new file mode 100644 index 000000000..b3400a76c --- /dev/null +++ b/novaclient/tests/fixture_data/keypairs.py @@ -0,0 +1,52 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +import httpretty + +from novaclient.openstack.common import jsonutils +from novaclient.tests import fakes +from novaclient.tests.fixture_data import base + + +class V1(base.Fixture): + + base_url = 'os-keypairs' + + def setUp(self): + super(V1, self).setUp() + keypair = {'fingerprint': 'FAKE_KEYPAIR', 'name': 'test'} + + httpretty.register_uri(httpretty.GET, self.url(), + body=jsonutils.dumps({'keypairs': [keypair]}), + content_type='application/json') + + httpretty.register_uri(httpretty.GET, self.url('test'), + body=jsonutils.dumps({'keypair': keypair}), + content_type='application/json') + + httpretty.register_uri(httpretty.DELETE, self.url('test'), status=202) + + def post_os_keypairs(request, url, headers): + body = jsonutils.loads(request.body.decode('utf-8')) + assert list(body) == ['keypair'] + fakes.assert_has_keys(body['keypair'], required=['name']) + return 202, headers, jsonutils.dumps({'keypair': keypair}) + + httpretty.register_uri(httpretty.POST, self.url(), + body=post_os_keypairs, + content_type='application/json') + + +class V3(V1): + + base_url = 'keypairs' diff --git a/novaclient/tests/v1_1/test_keypairs.py b/novaclient/tests/v1_1/test_keypairs.py index 90a610924..7ebb23c2f 100644 --- a/novaclient/tests/v1_1/test_keypairs.py +++ b/novaclient/tests/v1_1/test_keypairs.py @@ -11,50 +11,54 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient.tests.fixture_data import client +from novaclient.tests.fixture_data import keypairs as data from novaclient.tests import utils -from novaclient.tests.v1_1 import fakes from novaclient.v1_1 import keypairs -class KeypairsTest(utils.TestCase): +class KeypairsTest(utils.FixturedTestCase): + + client_fixture_class = client.V1 + data_fixture_class = data.V1 + def setUp(self): super(KeypairsTest, self).setUp() - self.cs = self._get_fake_client() self.keypair_type = self._get_keypair_type() - self.keypair_prefix = keypairs.KeypairManager.keypair_prefix - - def _get_fake_client(self): - return fakes.FakeClient() + self.keypair_prefix = self._get_keypair_prefix() def _get_keypair_type(self): return keypairs.Keypair + def _get_keypair_prefix(self): + return keypairs.KeypairManager.keypair_prefix + def test_get_keypair(self): kp = self.cs.keypairs.get('test') - self.cs.assert_called('GET', '/%s/test' % self.keypair_prefix) + self.assert_called('GET', '/%s/test' % self.keypair_prefix) self.assertIsInstance(kp, keypairs.Keypair) self.assertEqual(kp.name, 'test') def test_list_keypairs(self): kps = self.cs.keypairs.list() - self.cs.assert_called('GET', '/%s' % self.keypair_prefix) + self.assert_called('GET', '/%s' % self.keypair_prefix) [self.assertIsInstance(kp, keypairs.Keypair) for kp in kps] def test_delete_keypair(self): kp = self.cs.keypairs.list()[0] kp.delete() - self.cs.assert_called('DELETE', '/%s/test' % self.keypair_prefix) + self.assert_called('DELETE', '/%s/test' % self.keypair_prefix) self.cs.keypairs.delete('test') - self.cs.assert_called('DELETE', '/%s/test' % self.keypair_prefix) + self.assert_called('DELETE', '/%s/test' % self.keypair_prefix) self.cs.keypairs.delete(kp) - self.cs.assert_called('DELETE', '/%s/test' % self.keypair_prefix) + self.assert_called('DELETE', '/%s/test' % self.keypair_prefix) def test_create_keypair(self): kp = self.cs.keypairs.create("foo") - self.cs.assert_called('POST', '/%s' % self.keypair_prefix) + self.assert_called('POST', '/%s' % self.keypair_prefix) self.assertIsInstance(kp, keypairs.Keypair) def test_import_keypair(self): kp = self.cs.keypairs.create("foo", "fake-public-key") - self.cs.assert_called('POST', '/%s' % self.keypair_prefix) + self.assert_called('POST', '/%s' % self.keypair_prefix) self.assertIsInstance(kp, keypairs.Keypair) diff --git a/novaclient/tests/v3/test_keypairs.py b/novaclient/tests/v3/test_keypairs.py index fd1e4bac4..157042fce 100644 --- a/novaclient/tests/v3/test_keypairs.py +++ b/novaclient/tests/v3/test_keypairs.py @@ -12,20 +12,19 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient.tests.fixture_data import client +from novaclient.tests.fixture_data import keypairs as data from novaclient.tests.v1_1 import test_keypairs -from novaclient.tests.v3 import fakes from novaclient.v3 import keypairs class KeypairsTest(test_keypairs.KeypairsTest): - def setUp(self): - super(KeypairsTest, self).setUp() - self.cs = self._get_fake_client() - self.keypair_type = self._get_keypair_type() - self.keypair_prefix = keypairs.KeypairManager.keypair_prefix - def _get_fake_client(self): - return fakes.FakeClient() + client_fixture_class = client.V3 + data_fixture_class = data.V3 def _get_keypair_type(self): return keypairs.Keypair + + def _get_keypair_prefix(self): + return keypairs.KeypairManager.keypair_prefix From c4fa6648c863ad003ed3e26d04b9bc2eedd07d1d Mon Sep 17 00:00:00 2001 From: Jamie Lennox Date: Mon, 26 May 2014 11:50:39 +1000 Subject: [PATCH 0547/1705] Convert limit tests to httpretty Change-Id: I10e0357e0f79c009a00759fd22e6148d10b5286d blueprint: httpretty-testing --- novaclient/tests/fixture_data/limits.py | 82 +++++++++++++++++++++++++ novaclient/tests/v1_1/test_limits.py | 25 ++++---- 2 files changed, 95 insertions(+), 12 deletions(-) create mode 100644 novaclient/tests/fixture_data/limits.py diff --git a/novaclient/tests/fixture_data/limits.py b/novaclient/tests/fixture_data/limits.py new file mode 100644 index 000000000..5eca22e4d --- /dev/null +++ b/novaclient/tests/fixture_data/limits.py @@ -0,0 +1,82 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import httpretty + +from novaclient.openstack.common import jsonutils +from novaclient.tests.fixture_data import base + + +class Fixture(base.Fixture): + + base_url = 'limits' + + def setUp(self): + super(Fixture, self).setUp() + + get_limits = { + "limits": { + "rate": [ + { + "uri": "*", + "regex": ".*", + "limit": [ + { + "value": 10, + "verb": "POST", + "remaining": 2, + "unit": "MINUTE", + "next-available": "2011-12-15T22:42:45Z" + }, + { + "value": 10, + "verb": "PUT", + "remaining": 2, + "unit": "MINUTE", + "next-available": "2011-12-15T22:42:45Z" + }, + { + "value": 100, + "verb": "DELETE", + "remaining": 100, + "unit": "MINUTE", + "next-available": "2011-12-15T22:42:45Z" + } + ] + }, + { + "uri": "*/servers", + "regex": "^/servers", + "limit": [ + { + "verb": "POST", + "value": 25, + "remaining": 24, + "unit": "DAY", + "next-available": "2011-12-15T22:42:45Z" + } + ] + } + ], + "absolute": { + "maxTotalRAMSize": 51200, + "maxServerMeta": 5, + "maxImageMeta": 5, + "maxPersonality": 5, + "maxPersonalitySize": 10240 + }, + }, + } + + httpretty.register_uri(httpretty.GET, self.url(), + body=jsonutils.dumps(get_limits), + content_type='application/json') diff --git a/novaclient/tests/v1_1/test_limits.py b/novaclient/tests/v1_1/test_limits.py index 7561aa4ff..212be4549 100644 --- a/novaclient/tests/v1_1/test_limits.py +++ b/novaclient/tests/v1_1/test_limits.py @@ -11,28 +11,29 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient.tests.fixture_data import client +from novaclient.tests.fixture_data import limits as data from novaclient.tests import utils -from novaclient.tests.v1_1 import fakes from novaclient.v1_1 import limits -cs = fakes.FakeClient() +class LimitsTest(utils.FixturedTestCase): - -class LimitsTest(utils.TestCase): + client_fixture_class = client.V1 + data_fixture_class = data.Fixture def test_get_limits(self): - obj = cs.limits.get() - cs.assert_called('GET', '/limits') + obj = self.cs.limits.get() + self.assert_called('GET', '/limits') self.assertIsInstance(obj, limits.Limits) def test_get_limits_for_a_tenant(self): - obj = cs.limits.get(tenant_id=1234) - cs.assert_called('GET', '/limits?tenant_id=1234') + obj = self.cs.limits.get(tenant_id=1234) + self.assert_called('GET', '/limits?tenant_id=1234') self.assertIsInstance(obj, limits.Limits) def test_absolute_limits(self): - obj = cs.limits.get() + obj = self.cs.limits.get() expected = ( limits.AbsoluteLimit("maxTotalRAMSize", 51200), @@ -49,7 +50,7 @@ def test_absolute_limits(self): self.assertTrue(limit in expected) def test_absolute_limits_reserved(self): - obj = cs.limits.get(reserved=True) + obj = self.cs.limits.get(reserved=True) expected = ( limits.AbsoluteLimit("maxTotalRAMSize", 51200), @@ -59,7 +60,7 @@ def test_absolute_limits_reserved(self): limits.AbsoluteLimit("maxPersonalitySize", 10240), ) - cs.assert_called('GET', '/limits?reserved=1') + self.assert_called('GET', '/limits?reserved=1') abs_limits = list(obj.absolute) self.assertEqual(len(abs_limits), len(expected)) @@ -67,7 +68,7 @@ def test_absolute_limits_reserved(self): self.assertTrue(limit in expected) def test_rate_limits(self): - obj = cs.limits.get() + obj = self.cs.limits.get() expected = ( limits.RateLimit('POST', '*', '.*', 10, 2, 'MINUTE', From 5d9537a02ed7f5ee3e6377ec85997ebe39ce71f1 Mon Sep 17 00:00:00 2001 From: Jamie Lennox Date: Mon, 26 May 2014 12:02:40 +1000 Subject: [PATCH 0548/1705] Convert network tests to httpretty Change-Id: I81a83c98b925a2bc7373dd17f2c2ecd78fa23a6d blueprint: httpretty-testing --- novaclient/tests/fixture_data/networks.py | 62 +++++++++++++++++++ novaclient/tests/v1_1/test_networks.py | 73 ++++++++++++----------- 2 files changed, 99 insertions(+), 36 deletions(-) create mode 100644 novaclient/tests/fixture_data/networks.py diff --git a/novaclient/tests/fixture_data/networks.py b/novaclient/tests/fixture_data/networks.py new file mode 100644 index 000000000..12bb4b1eb --- /dev/null +++ b/novaclient/tests/fixture_data/networks.py @@ -0,0 +1,62 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import httpretty + +from novaclient.openstack.common import jsonutils +from novaclient.tests.fixture_data import base + + +class Fixture(base.Fixture): + + base_url = 'os-networks' + + def setUp(self): + super(Fixture, self).setUp() + + get_os_networks = { + 'networks': [ + { + "label": "1", + "cidr": "10.0.0.0/24", + 'project_id': '4ffc664c198e435e9853f2538fbcd7a7', + 'id': '1' + } + ] + } + + httpretty.register_uri(httpretty.GET, self.url(), + body=jsonutils.dumps(get_os_networks), + content_type='application/json') + + def post_os_networks(request, url, headers): + body = jsonutils.loads(request.body.decode('utf-8')) + data = jsonutils.dumps({'network': body}) + return 202, headers, data + + httpretty.register_uri(httpretty.POST, self.url(), + body=post_os_networks, + content_type='application/json') + + get_os_networks_1 = {'network': {"label": "1", "cidr": "10.0.0.0/24"}} + + httpretty.register_uri(httpretty.GET, self.url(1), + body=jsonutils.dumps(get_os_networks_1), + content_type='application/json') + + httpretty.register_uri(httpretty.DELETE, + self.url('networkdelete'), + stauts=202) + + for u in ('add', 'networkdisassociate/action', 'networktest/action', + '1/action', '2/action'): + httpretty.register_uri(httpretty.POST, self.url(u), stauts=202) diff --git a/novaclient/tests/v1_1/test_networks.py b/novaclient/tests/v1_1/test_networks.py index 369772ea8..1fd6a774f 100644 --- a/novaclient/tests/v1_1/test_networks.py +++ b/novaclient/tests/v1_1/test_networks.py @@ -11,34 +11,35 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient.tests.fixture_data import client +from novaclient.tests.fixture_data import networks as data from novaclient.tests import utils -from novaclient.tests.v1_1 import fakes from novaclient.v1_1 import networks -cs = fakes.FakeClient() +class NetworksTest(utils.FixturedTestCase): - -class NetworksTest(utils.TestCase): + client_fixture_class = client.V1 + data_fixture_class = data.Fixture def test_list_networks(self): - fl = cs.networks.list() - cs.assert_called('GET', '/os-networks') + fl = self.cs.networks.list() + self.assert_called('GET', '/os-networks') [self.assertIsInstance(f, networks.Network) for f in fl] def test_get_network(self): - f = cs.networks.get(1) - cs.assert_called('GET', '/os-networks/1') + f = self.cs.networks.get(1) + self.assert_called('GET', '/os-networks/1') self.assertIsInstance(f, networks.Network) def test_delete(self): - cs.networks.delete('networkdelete') - cs.assert_called('DELETE', '/os-networks/networkdelete') + self.cs.networks.delete('networkdelete') + self.assert_called('DELETE', '/os-networks/networkdelete') def test_create(self): - f = cs.networks.create(label='foo') - cs.assert_called('POST', '/os-networks', - {'network': {'label': 'foo'}}) + f = self.cs.networks.create(label='foo') + self.assert_called('POST', '/os-networks', + {'network': {'label': 'foo'}}) self.assertIsInstance(f, networks.Network) def test_create_allparams(self): @@ -60,39 +61,39 @@ def test_create_allparams(self): 'vpn_start': 1 } - f = cs.networks.create(**params) - cs.assert_called('POST', '/os-networks', {'network': params}) + f = self.cs.networks.create(**params) + self.assert_called('POST', '/os-networks', {'network': params}) self.assertIsInstance(f, networks.Network) def test_associate_project(self): - cs.networks.associate_project('networktest') - cs.assert_called('POST', '/os-networks/add', - {'id': 'networktest'}) + self.cs.networks.associate_project('networktest') + self.assert_called('POST', '/os-networks/add', + {'id': 'networktest'}) def test_associate_host(self): - cs.networks.associate_host('networktest', 'testHost') - cs.assert_called('POST', '/os-networks/networktest/action', - {'associate_host': 'testHost'}) + self.cs.networks.associate_host('networktest', 'testHost') + self.assert_called('POST', '/os-networks/networktest/action', + {'associate_host': 'testHost'}) def test_disassociate(self): - cs.networks.disassociate('networkdisassociate') - cs.assert_called('POST', - '/os-networks/networkdisassociate/action', - {'disassociate': None}) + self.cs.networks.disassociate('networkdisassociate') + self.assert_called('POST', + '/os-networks/networkdisassociate/action', + {'disassociate': None}) def test_disassociate_host_only(self): - cs.networks.disassociate('networkdisassociate', True, False) - cs.assert_called('POST', - '/os-networks/networkdisassociate/action', - {'disassociate_host': None}) + self.cs.networks.disassociate('networkdisassociate', True, False) + self.assert_called('POST', + '/os-networks/networkdisassociate/action', + {'disassociate_host': None}) def test_disassociate_project(self): - cs.networks.disassociate('networkdisassociate', False, True) - cs.assert_called('POST', - '/os-networks/networkdisassociate/action', - {'disassociate_project': None}) + self.cs.networks.disassociate('networkdisassociate', False, True) + self.assert_called('POST', + '/os-networks/networkdisassociate/action', + {'disassociate_project': None}) def test_add(self): - cs.networks.add('networkadd') - cs.assert_called('POST', '/os-networks/add', - {'id': 'networkadd'}) + self.cs.networks.add('networkadd') + self.assert_called('POST', '/os-networks/add', + {'id': 'networkadd'}) From 6ee0b28e64b55a5d2a83a2cfe6be72e2c072f1c0 Mon Sep 17 00:00:00 2001 From: Laurens Van Houtven <_@lvh.cc> Date: Mon, 9 Jun 2014 11:12:15 +0200 Subject: [PATCH 0549/1705] Filter endpoints by region whenever possible Previously, the endpoints command just returned the first endpoint. This was not very useful; people expected to see the endpoints in the region they specified. This patch does the following: - If there's only one endpoint, use that. - If there's a specific one for your region, use that. - If there isn't, warn, and show the other ones. Closes-Bug: #1256009 Change-Id: I70b82bdffb9b2e3da415a3bb941bcff287e39789 --- novaclient/tests/v1_1/test_shell.py | 41 +++++++++++++++++++++++++++++ novaclient/tests/v3/test_shell.py | 41 +++++++++++++++++++++++++++++ novaclient/v1_1/shell.py | 36 +++++++++++++++++++++++-- novaclient/v3/shell.py | 37 ++++++++++++++++++++++++-- 4 files changed, 151 insertions(+), 4 deletions(-) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index 262fd6668..7c5711c50 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -2130,3 +2130,44 @@ def test_with_non_unique_name(self): novaclient.v1_1.shell._get_secgroup, cs, 'group_one') + + +class GetFirstEndpointTest(utils.TestCase): + def test_only_one_endpoint(self): + """If there is only one endpoint, it is returned.""" + endpoint = {"url": "test"} + result = novaclient.v1_1.shell._get_first_endpoint([endpoint], "XYZ") + self.assertEqual(endpoint, result) + + def test_multiple_endpoints(self): + """If there are multiple endpoints, the first one of the appropriate + region is returned. + + """ + endpoints = [ + {"region": "XYZ"}, + {"region": "ORD", "number": 1}, + {"region": "ORD", "number": 2} + ] + result = novaclient.v1_1.shell._get_first_endpoint(endpoints, "ORD") + self.assertEqual(endpoints[1], result) + + def test_multiple_endpoints_but_none_suitable(self): + """If there are multiple endpoints but none of them are suitable, an + exception is raised. + + """ + endpoints = [ + {"region": "XYZ"}, + {"region": "PQR"}, + {"region": "STU"} + ] + self.assertRaises(LookupError, + novaclient.v1_1.shell._get_first_endpoint, + endpoints, "ORD") + + def test_no_endpoints(self): + """If there are no endpoints available, an exception is raised.""" + self.assertRaises(LookupError, + novaclient.v1_1.shell._get_first_endpoint, + [], "ORD") diff --git a/novaclient/tests/v3/test_shell.py b/novaclient/tests/v3/test_shell.py index 3d9c2b598..fb133b637 100644 --- a/novaclient/tests/v3/test_shell.py +++ b/novaclient/tests/v3/test_shell.py @@ -594,3 +594,44 @@ def test_flavor_show_by_name_priv(self): self.assert_called('GET', '/flavors?is_public=None', pos=2) self.assert_called('GET', '/flavors/2', pos=3) self.assert_called('GET', '/flavors/2/flavor-extra-specs', pos=4) + + +class GetFirstEndpointTest(utils.TestCase): + def test_only_one_endpoint(self): + """If there is only one endpoint, it is returned.""" + endpoint = {"url": "test"} + result = novaclient.v3.shell._get_first_endpoint([endpoint], "XYZ") + self.assertEqual(endpoint, result) + + def test_multiple_endpoints(self): + """If there are multiple endpoints, the first one of the appropriate + region is returned. + + """ + endpoints = [ + {"region": "XYZ"}, + {"region": "ORD", "number": 1}, + {"region": "ORD", "number": 2} + ] + result = novaclient.v3.shell._get_first_endpoint(endpoints, "ORD") + self.assertEqual(endpoints[1], result) + + def test_multiple_endpoints_but_none_suitable(self): + """If there are multiple endpoints but none of them are suitable, an + exception is raised. + + """ + endpoints = [ + {"region": "XYZ"}, + {"region": "PQR"}, + {"region": "STU"} + ] + self.assertRaises(LookupError, + novaclient.v3.shell._get_first_endpoint, + endpoints, "ORD") + + def test_no_endpoints(self): + """If there are no endpoints available, an exception is raised.""" + self.assertRaises(LookupError, + novaclient.v3.shell._get_first_endpoint, + [], "ORD") diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 92025182d..78adea656 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -3072,9 +3072,41 @@ def ensure_service_catalog_present(cs): def do_endpoints(cs, _args): """Discover endpoints that get returned from the authenticate services.""" ensure_service_catalog_present(cs) + catalog = cs.client.service_catalog.catalog - for e in catalog['access']['serviceCatalog']: - utils.print_dict(e['endpoints'][0], e['name']) + region = cs.client.region_name + + for service in catalog['access']['serviceCatalog']: + name, endpoints = service["name"], service["endpoints"] + + try: + endpoint = _get_first_endpoint(endpoints, region) + utils.print_dict(endpoint, name) + except LookupError: + print(_("WARNING: %(service)s has no endpoint in %(region)s! " + "Available endpoints for this service:") % + {'service': name, 'region': region}) + for other_endpoint in endpoints: + utils.print_dict(other_endpoint, name) + + +def _get_first_endpoint(endpoints, region): + """Find the first suitable endpoint in endpoints. + + If there is only one endpoint, return it. If there is more than + one endpoint, return the first one with the given region. If there + are no endpoints, or there is more than one endpoint but none of + them match the given region, raise KeyError. + + """ + if len(endpoints) == 1: + return endpoints[0] + else: + for candidate_endpoint in endpoints: + if candidate_endpoint["region"] == region: + return candidate_endpoint + + raise LookupError("No suitable endpoint found") @utils.arg('--wrap', dest='wrap', metavar='', default=64, diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 1c4d09080..6f61585e6 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -30,6 +30,7 @@ import six from novaclient import exceptions +from novaclient.openstack.common.gettextutils import _ from novaclient.openstack.common import strutils from novaclient.openstack.common import timeutils from novaclient.openstack.common import uuidutils @@ -2610,9 +2611,41 @@ def ensure_service_catalog_present(cs): def do_endpoints(cs, _args): """Discover endpoints that get returned from the authenticate services.""" ensure_service_catalog_present(cs) + catalog = cs.client.service_catalog.catalog - for e in catalog['access']['serviceCatalog']: - utils.print_dict(e['endpoints'][0], e['name']) + region = cs.client.region_name + + for service in catalog['access']['serviceCatalog']: + name, endpoints = service["name"], service["endpoints"] + + try: + endpoint = _get_first_endpoint(endpoints, region) + utils.print_dict(endpoint, name) + except LookupError: + print(_("WARNING: %(service)s has no endpoint in %(region)s! " + "Available endpoints for this service:") % + {'service': name, 'region': region}) + for other_endpoint in endpoints: + utils.print_dict(other_endpoint, name) + + +def _get_first_endpoint(endpoints, region): + """Find the first suitable endpoint in endpoints. + + If there is only one endpoint, return it. If there is more than + one endpoint, return the first one with the given region. If there + are no endpoints, or there is more than one endpoint but none of + them match the given region, raise KeyError. + + """ + if len(endpoints) == 1: + return endpoints[0] + else: + for candidate_endpoint in endpoints: + if candidate_endpoint["region"] == region: + return candidate_endpoint + + raise LookupError("No suitable endpoint found") @utils.arg('--wrap', dest='wrap', metavar='', default=64, From 6aa419b82ed6e0b06599eeaf69581e995116669d Mon Sep 17 00:00:00 2001 From: Chris Yeoh Date: Mon, 17 Mar 2014 15:12:01 +1030 Subject: [PATCH 0550/1705] Adds clarification note for project_id vs tenant_id The client __init__ method takes both a project_id and tenant_id which is rather confusing as in the Nova API these terms are used interchangeably. The comment clarifies the difference between a project_id and tenant_id when using novaclient. For backwards compatibility reasons we can't really change the names (though for V3 perhaps we should in the future). Change-Id: I569fe9318335c8d686153b0936205cb190e01ef1 --- novaclient/v1_1/client.py | 7 +++++++ novaclient/v3/client.py | 6 ++++++ 2 files changed, 13 insertions(+) diff --git a/novaclient/v1_1/client.py b/novaclient/v1_1/client.py index 2cf62e947..9b7e01e41 100644 --- a/novaclient/v1_1/client.py +++ b/novaclient/v1_1/client.py @@ -92,6 +92,13 @@ def __init__(self, username, api_key, project_id, auth_url=None, connection_pool=False, completion_cache=None): # FIXME(comstud): Rename the api_key argument above when we # know it's not being used as keyword argument + + # NOTE(cyeoh): In the novaclient context (unlike Nova) the + # project_id is not the same as the tenant_id. Here project_id + # is a name (what the Nova API often refers to as a project or + # tenant name) and tenant_id is a UUID (what the Nova API + # often refers to as a project_id or tenant_id). + password = api_key self.projectid = project_id self.tenant_id = tenant_id diff --git a/novaclient/v3/client.py b/novaclient/v3/client.py index 5274c7aee..36a51902e 100644 --- a/novaclient/v3/client.py +++ b/novaclient/v3/client.py @@ -75,6 +75,12 @@ def __init__(self, username, password, project_id, auth_url=None, auth_plugin=None, auth_token=None, cacert=None, tenant_id=None, user_id=None, connection_pool=False, completion_cache=None): + # NOTE(cyeoh): In the novaclient context (unlike Nova) the + # project_id is not the same as the tenant_id. Here project_id + # is a name (what the Nova API often refers to as a project or + # tenant name) and tenant_id is a UUID (what the Nova API + # often refers to as a project_id or tenant_id). + self.projectid = project_id self.tenant_id = tenant_id self.user_id = user_id From ec4aef644e7f340c1d251fc0b07f1bb1e7271374 Mon Sep 17 00:00:00 2001 From: liuqing Date: Tue, 1 Jul 2014 15:08:18 +0800 Subject: [PATCH 0551/1705] Fix the section name in CONTRIBUTING.rst The name of the section "If you're developer, start here" has changed in the wiki, now is "If you're a developer". This commit updates it to correctly refer to the proper section. Change-Id: I16fc339292ac5766530f003770bee133170bed67 --- CONTRIBUTING.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 6b38289cb..a0a98b1f2 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -1,8 +1,7 @@ If you would like to contribute to the development of OpenStack, -you must follow the steps in the "If you're a developer, start here" -section of this page: +you must follow the steps documented at: - http://wiki.openstack.org/HowToContribute + http://wiki.openstack.org/HowToContribute#If_you.27re_a_developer Once those steps have been completed, changes to OpenStack should be submitted for review via the Gerrit tool, following From cc7364067f995bd952149cd172cebf083fe4dc99 Mon Sep 17 00:00:00 2001 From: Jamie Lennox Date: Tue, 8 Apr 2014 11:40:41 +1000 Subject: [PATCH 0552/1705] Allow us to use keystoneclient's session The session object is a cross-client means of standardizing the transport layer. Novaclient's HTTPClient object has diverged significantly from other clients. It is easier to simply replace it if a session is provided. If a session is provided then users of the library need to be aware that functions such as authenticate() will no longer have any effect/are in error because this is no longer managed by nova. Change-Id: I8f146b878908239d9b6c1c7d6cdc01c7e124f4e5 --- novaclient/client.py | 128 ++++++++++++++++++ novaclient/exceptions.py | 10 ++ novaclient/tests/fixture_data/client.py | 18 +++ novaclient/tests/utils.py | 3 +- novaclient/tests/v1_1/test_agents.py | 4 +- novaclient/tests/v1_1/test_aggregates.py | 4 +- .../tests/v1_1/test_availability_zone.py | 4 +- novaclient/tests/v1_1/test_certs.py | 4 +- novaclient/tests/v1_1/test_cloudpipe.py | 4 +- novaclient/tests/v1_1/test_fixed_ips.py | 4 +- novaclient/tests/v3/test_agents.py | 3 +- novaclient/tests/v3/test_aggregates.py | 4 +- novaclient/tests/v3/test_availability_zone.py | 4 +- novaclient/tests/v3/test_certs.py | 3 +- novaclient/v1_1/client.py | 80 ++++++----- novaclient/v3/client.py | 80 ++++++----- test-requirements.txt | 2 + 17 files changed, 276 insertions(+), 83 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index 8d2d3f678..de4a64597 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -21,6 +21,7 @@ """ import errno +import functools import glob import hashlib import logging @@ -132,6 +133,86 @@ def write_object(self, obj): self._write_attribute(resource, attribute, value) +class SessionClient(object): + + def __init__(self, session, auth, interface, service_type, region_name): + self.session = session + self.auth = auth + + self.interface = interface + self.service_type = service_type + self.region_name = region_name + + def request(self, url, method, **kwargs): + kwargs.setdefault('user_agent', 'python-novaclient') + kwargs.setdefault('auth', self.auth) + kwargs.setdefault('authenticated', False) + + try: + kwargs['json'] = kwargs.pop('body') + except KeyError: + pass + + headers = kwargs.setdefault('headers', {}) + headers.setdefault('Accept', 'application/json') + + endpoint_filter = kwargs.setdefault('endpoint_filter', {}) + endpoint_filter.setdefault('interface', self.interface) + endpoint_filter.setdefault('service_type', self.service_type) + endpoint_filter.setdefault('region_name', self.region_name) + + resp = self.session.request(url, method, raise_exc=False, **kwargs) + + body = None + if resp.text: + try: + body = resp.json() + except ValueError: + pass + + if resp.status_code >= 400: + raise exceptions.from_response(resp, body, url, method) + + return resp, body + + def _cs_request(self, url, method, **kwargs): + # this function is mostly redundant but makes compatibility easier + kwargs.setdefault('authenticated', True) + return self.request(url, method, **kwargs) + + def get(self, url, **kwargs): + return self._cs_request(url, 'GET', **kwargs) + + def post(self, url, **kwargs): + return self._cs_request(url, 'POST', **kwargs) + + def put(self, url, **kwargs): + return self._cs_request(url, 'PUT', **kwargs) + + def delete(self, url, **kwargs): + return self._cs_request(url, 'DELETE', **kwargs) + + +def _original_only(f): + """Indicates and enforces that this function can only be used if we are + using the original HTTPClient object. + + We use this to specify that if you use the newer Session HTTP client then + you are aware that the way you use your client has been updated and certain + functions are no longer allowed to be used. + """ + @functools.wraps(f) + def wrapper(self, *args, **kwargs): + if isinstance(self.client, SessionClient): + msg = ('This call is no longer available. The operation should ' + 'be performed on the session object instead.') + raise exceptions.InvalidUsage(msg) + + return f(self, *args, **kwargs) + + return wrapper + + class HTTPClient(object): USER_AGENT = 'python-novaclient' @@ -606,6 +687,53 @@ def _authenticate(self, url, body, **kwargs): return self._extract_service_catalog(url, resp, respbody) +def _construct_http_client(username=None, password=None, project_id=None, + auth_url=None, insecure=False, timeout=None, + proxy_tenant_id=None, proxy_token=None, + region_name=None, endpoint_type='publicURL', + extensions=None, service_type='compute', + service_name=None, volume_service_name=None, + timings=False, bypass_url=None, os_cache=False, + no_cache=True, http_log_debug=False, + auth_system='keystone', auth_plugin=None, + auth_token=None, cacert=None, tenant_id=None, + user_id=None, connection_pool=False, session=None, + auth=None): + if session: + return SessionClient(session=session, + auth=auth, + interface=endpoint_type, + service_type=service_type, + region_name=region_name) + else: + # FIXME(jamielennox): username and password are now optional. Need + # to test that they were provided in this mode. + return HTTPClient(username, + password, + user_id=user_id, + projectid=project_id, + tenant_id=tenant_id, + auth_url=auth_url, + auth_token=auth_token, + insecure=insecure, + timeout=timeout, + auth_system=auth_system, + auth_plugin=auth_plugin, + proxy_token=proxy_token, + proxy_tenant_id=proxy_tenant_id, + region_name=region_name, + endpoint_type=endpoint_type, + service_type=service_type, + service_name=service_name, + volume_service_name=volume_service_name, + timings=timings, + bypass_url=bypass_url, + os_cache=os_cache, + http_log_debug=http_log_debug, + cacert=cacert, + connection_pool=connection_pool) + + def get_client_class(version): version_map = { '1.1': 'novaclient.v1_1.client.Client', diff --git a/novaclient/exceptions.py b/novaclient/exceptions.py index c2a6bf6ed..73f04dcef 100644 --- a/novaclient/exceptions.py +++ b/novaclient/exceptions.py @@ -64,6 +64,16 @@ class RateLimit(RequestEntityTooLarge): ) +class InvalidUsage(RuntimeError): + """This function call is invalid in the way you are using this client. + + Due to the transition to using keystoneclient some function calls are no + longer available. You should make a similar call to the session object + instead. + """ + pass + + def from_response(response, body, url, method=None): """ Return an instance of an HttpError or subclass diff --git a/novaclient/tests/fixture_data/client.py b/novaclient/tests/fixture_data/client.py index a221b9bd1..c4ca3ec38 100644 --- a/novaclient/tests/fixture_data/client.py +++ b/novaclient/tests/fixture_data/client.py @@ -12,6 +12,8 @@ import fixtures import httpretty +from keystoneclient.auth.identity import v2 +from keystoneclient import session from novaclient.openstack.common import jsonutils from novaclient.v1_1 import client as v1_1client @@ -107,3 +109,19 @@ def new_client(self): password='xx', project_id='xx', auth_url=self.identity_url) + + +class SessionV1(V1): + + def new_client(self): + self.session = session.Session() + self.session.auth = v2.Password(self.identity_url, 'xx', 'xx') + return v1_1client.Client(session=self.session) + + +class SessionV3(V1): + + def new_client(self): + self.session = session.Session() + self.session.auth = v2.Password(self.identity_url, 'xx', 'xx') + return v3client.Client(session=self.session) diff --git a/novaclient/tests/utils.py b/novaclient/tests/utils.py index e544d7510..be251998c 100644 --- a/novaclient/tests/utils.py +++ b/novaclient/tests/utils.py @@ -17,6 +17,7 @@ import httpretty import requests import six +import testscenarios import testtools from novaclient.openstack.common import jsonutils @@ -43,7 +44,7 @@ def setUp(self): self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr)) -class FixturedTestCase(TestCase): +class FixturedTestCase(testscenarios.TestWithScenarios, TestCase): client_fixture_class = None data_fixture_class = None diff --git a/novaclient/tests/v1_1/test_agents.py b/novaclient/tests/v1_1/test_agents.py index 910400467..46efe839a 100644 --- a/novaclient/tests/v1_1/test_agents.py +++ b/novaclient/tests/v1_1/test_agents.py @@ -24,9 +24,11 @@ class AgentsTest(utils.FixturedTestCase): - client_fixture_class = client.V1 data_fixture_class = data.Fixture + scenarios = [('original', {'client_fixture_class': client.V1}), + ('session', {'client_fixture_class': client.SessionV1})] + def stub_hypervisors(self, hypervisor='kvm'): get_os_agents = {'agents': [ diff --git a/novaclient/tests/v1_1/test_aggregates.py b/novaclient/tests/v1_1/test_aggregates.py index 8d41de30d..6e7bd44e9 100644 --- a/novaclient/tests/v1_1/test_aggregates.py +++ b/novaclient/tests/v1_1/test_aggregates.py @@ -21,9 +21,11 @@ class AggregatesTest(utils.FixturedTestCase): - client_fixture_class = client.V1 data_fixture_class = data.Fixture + scenarios = [('original', {'client_fixture_class': client.V1}), + ('session', {'client_fixture_class': client.SessionV1})] + def test_list_aggregates(self): result = self.cs.aggregates.list() self.assert_called('GET', '/os-aggregates') diff --git a/novaclient/tests/v1_1/test_availability_zone.py b/novaclient/tests/v1_1/test_availability_zone.py index 6cd5fafa1..2144d8c19 100644 --- a/novaclient/tests/v1_1/test_availability_zone.py +++ b/novaclient/tests/v1_1/test_availability_zone.py @@ -27,9 +27,11 @@ class AvailabilityZoneTest(utils.FixturedTestCase): # this class can inherit off the v3 version of shell from novaclient.v1_1 import shell # noqa - client_fixture_class = client.V1 data_fixture_class = data.V1 + scenarios = [('original', {'client_fixture_class': client.V1}), + ('session', {'client_fixture_class': client.SessionV1})] + def setUp(self): super(AvailabilityZoneTest, self).setUp() self.availability_zone_type = self._get_availability_zone_type() diff --git a/novaclient/tests/v1_1/test_certs.py b/novaclient/tests/v1_1/test_certs.py index 0519254c0..0a568d585 100644 --- a/novaclient/tests/v1_1/test_certs.py +++ b/novaclient/tests/v1_1/test_certs.py @@ -19,10 +19,12 @@ class CertsTest(utils.FixturedTestCase): - client_fixture_class = client.V1 data_fixture_class = data.Fixture cert_type = certs.Certificate + scenarios = [('original', {'client_fixture_class': client.V1}), + ('session', {'client_fixture_class': client.SessionV1})] + def test_create_cert(self): cert = self.cs.certs.create() self.assert_called('POST', '/os-certificates') diff --git a/novaclient/tests/v1_1/test_cloudpipe.py b/novaclient/tests/v1_1/test_cloudpipe.py index d2183c47e..0a319085b 100644 --- a/novaclient/tests/v1_1/test_cloudpipe.py +++ b/novaclient/tests/v1_1/test_cloudpipe.py @@ -21,9 +21,11 @@ class CloudpipeTest(utils.FixturedTestCase): - client_fixture_class = client.V1 data_fixture_class = data.Fixture + scenarios = [('original', {'client_fixture_class': client.V1}), + ('session', {'client_fixture_class': client.SessionV1})] + def test_list_cloudpipes(self): cp = self.cs.cloudpipe.list() self.assert_called('GET', '/os-cloudpipe') diff --git a/novaclient/tests/v1_1/test_fixed_ips.py b/novaclient/tests/v1_1/test_fixed_ips.py index 38fa31c13..a081a8ecc 100644 --- a/novaclient/tests/v1_1/test_fixed_ips.py +++ b/novaclient/tests/v1_1/test_fixed_ips.py @@ -20,9 +20,11 @@ class FixedIpsTest(utils.FixturedTestCase): - client_fixture_class = client.V1 data_fixture_class = data.Fixture + scenarios = [('original', {'client_fixture_class': client.V1}), + ('session', {'client_fixture_class': client.SessionV1})] + def test_get_fixed_ip(self): info = self.cs.fixed_ips.get(fixed_ip='192.168.1.1') self.assert_called('GET', '/os-fixed-ips/192.168.1.1') diff --git a/novaclient/tests/v3/test_agents.py b/novaclient/tests/v3/test_agents.py index 6aaee08c5..bdd101bdc 100644 --- a/novaclient/tests/v3/test_agents.py +++ b/novaclient/tests/v3/test_agents.py @@ -19,7 +19,8 @@ class AgentsTest(test_agents.AgentsTest): - client_fixture_class = client.V3 + scenarios = [('original', {'client_fixture_class': client.V3}), + ('session', {'client_fixture_class': client.SessionV3})] def _build_example_update_body(self): return {"agent": { diff --git a/novaclient/tests/v3/test_aggregates.py b/novaclient/tests/v3/test_aggregates.py index 7f64fb68f..17e27d162 100644 --- a/novaclient/tests/v3/test_aggregates.py +++ b/novaclient/tests/v3/test_aggregates.py @@ -17,4 +17,6 @@ class AggregatesTest(test_aggregates.AggregatesTest): - client_fixture = client.V3 + + scenarios = [('original', {'client_fixture_class': client.V3}), + ('session', {'client_fixture_class': client.SessionV3})] diff --git a/novaclient/tests/v3/test_availability_zone.py b/novaclient/tests/v3/test_availability_zone.py index ad7f3a715..67a4415bf 100644 --- a/novaclient/tests/v3/test_availability_zone.py +++ b/novaclient/tests/v3/test_availability_zone.py @@ -23,9 +23,11 @@ class AvailabilityZoneTest(test_availability_zone.AvailabilityZoneTest): from novaclient.v3 import shell # noqa - client_fixture_class = client.V3 data_fixture_class = data.V3 + scenarios = [('original', {'client_fixture_class': client.V3}), + ('session', {'client_fixture_class': client.SessionV3})] + def _assertZone(self, zone, name, status): self.assertEqual(zone.zone_name, name) self.assertEqual(zone.zone_state, status) diff --git a/novaclient/tests/v3/test_certs.py b/novaclient/tests/v3/test_certs.py index cf09e58db..30597fc10 100644 --- a/novaclient/tests/v3/test_certs.py +++ b/novaclient/tests/v3/test_certs.py @@ -17,4 +17,5 @@ class CertsTest(test_certs.CertsTest): - client_fixture_data = client.V3 + scenarios = [('original', {'client_fixture_class': client.V3}), + ('session', {'client_fixture_class': client.SessionV3})] diff --git a/novaclient/v1_1/client.py b/novaclient/v1_1/client.py index 9b7e01e41..46034bb95 100644 --- a/novaclient/v1_1/client.py +++ b/novaclient/v1_1/client.py @@ -78,18 +78,17 @@ class Client(object): ... AUTH_URL, connection_pool=True) """ - # FIXME(jesse): project_id isn't required to authenticate - def __init__(self, username, api_key, project_id, auth_url=None, - insecure=False, timeout=None, proxy_tenant_id=None, - proxy_token=None, region_name=None, - endpoint_type='publicURL', extensions=None, - service_type='compute', service_name=None, - volume_service_name=None, timings=False, - bypass_url=None, os_cache=False, no_cache=True, - http_log_debug=False, auth_system='keystone', - auth_plugin=None, auth_token=None, - cacert=None, tenant_id=None, user_id=None, - connection_pool=False, completion_cache=None): + def __init__(self, username=None, api_key=None, project_id=None, + auth_url=None, insecure=False, timeout=None, + proxy_tenant_id=None, proxy_token=None, region_name=None, + endpoint_type='publicURL', extensions=None, + service_type='compute', service_name=None, + volume_service_name=None, timings=False, bypass_url=None, + os_cache=False, no_cache=True, http_log_debug=False, + auth_system='keystone', auth_plugin=None, auth_token=None, + cacert=None, tenant_id=None, user_id=None, + connection_pool=False, session=None, auth=None, + completion_cache=None): # FIXME(comstud): Rename the api_key argument above when we # know it's not being used as keyword argument @@ -149,30 +148,33 @@ def __init__(self, username, api_key, project_id, auth_url=None, setattr(self, extension.name, extension.manager_class(self)) - self.client = client.HTTPClient(username, - password, - user_id=user_id, - projectid=project_id, - tenant_id=tenant_id, - auth_url=auth_url, - auth_token=auth_token, - insecure=insecure, - timeout=timeout, - auth_system=auth_system, - auth_plugin=auth_plugin, - proxy_token=proxy_token, - proxy_tenant_id=proxy_tenant_id, - region_name=region_name, - endpoint_type=endpoint_type, - service_type=service_type, - service_name=service_name, - volume_service_name=volume_service_name, - timings=timings, - bypass_url=bypass_url, - os_cache=self.os_cache, - http_log_debug=http_log_debug, - cacert=cacert, - connection_pool=connection_pool) + self.client = client._construct_http_client( + username=username, + password=password, + user_id=user_id, + project_id=project_id, + tenant_id=tenant_id, + auth_url=auth_url, + auth_token=auth_token, + insecure=insecure, + timeout=timeout, + auth_system=auth_system, + auth_plugin=auth_plugin, + proxy_token=proxy_token, + proxy_tenant_id=proxy_tenant_id, + region_name=region_name, + endpoint_type=endpoint_type, + service_type=service_type, + service_name=service_name, + volume_service_name=volume_service_name, + timings=timings, + bypass_url=bypass_url, + os_cache=self.os_cache, + http_log_debug=http_log_debug, + cacert=cacert, + connection_pool=connection_pool, + session=session, + auth=auth) self.completion_cache = completion_cache @@ -184,22 +186,28 @@ def clear_completion_cache_for_class(self, obj_class): if self.completion_cache: self.completion_cache.clear_class(obj_class) + @client._original_only def __enter__(self): self.client.open_session() return self + @client._original_only def __exit__(self, t, v, tb): self.client.close_session() + @client._original_only def set_management_url(self, url): self.client.set_management_url(url) + @client._original_only def get_timings(self): return self.client.get_timings() + @client._original_only def reset_timings(self): self.client.reset_timings() + @client._original_only def authenticate(self): """ Authenticate against the server. diff --git a/novaclient/v3/client.py b/novaclient/v3/client.py index 36a51902e..28ac908b7 100644 --- a/novaclient/v3/client.py +++ b/novaclient/v3/client.py @@ -63,18 +63,17 @@ class Client(object): ... AUTH_URL, connection_pool=True) """ - # FIXME(jesse): project_id isn't required to authenticate - def __init__(self, username, password, project_id, auth_url=None, - insecure=False, timeout=None, proxy_tenant_id=None, - proxy_token=None, region_name=None, - endpoint_type='publicURL', extensions=None, - service_type='computev3', service_name=None, - volume_service_name=None, timings=False, - bypass_url=None, os_cache=False, no_cache=True, - http_log_debug=False, auth_system='keystone', - auth_plugin=None, auth_token=None, - cacert=None, tenant_id=None, user_id=None, - connection_pool=False, completion_cache=None): + def __init__(self, username=None, password=None, project_id=None, + auth_url=None, insecure=False, timeout=None, + proxy_tenant_id=None, proxy_token=None, region_name=None, + endpoint_type='publicURL', extensions=None, + service_type='computev3', service_name=None, + volume_service_name=None, timings=False, bypass_url=None, + os_cache=False, no_cache=True, http_log_debug=False, + auth_system='keystone', auth_plugin=None, auth_token=None, + cacert=None, tenant_id=None, user_id=None, + connection_pool=False, session=None, auth=None, + completion_cache=None): # NOTE(cyeoh): In the novaclient context (unlike Nova) the # project_id is not the same as the tenant_id. Here project_id # is a name (what the Nova API often refers to as a project or @@ -111,30 +110,33 @@ def __init__(self, username, password, project_id, auth_url=None, setattr(self, extension.name, extension.manager_class(self)) - self.client = client.HTTPClient(username, - password, - user_id=user_id, - projectid=project_id, - tenant_id=tenant_id, - auth_url=auth_url, - insecure=insecure, - timeout=timeout, - auth_system=auth_system, - auth_plugin=auth_plugin, - auth_token=auth_token, - proxy_token=proxy_token, - proxy_tenant_id=proxy_tenant_id, - region_name=region_name, - endpoint_type=endpoint_type, - service_type=service_type, - service_name=service_name, - volume_service_name=volume_service_name, - timings=timings, - bypass_url=bypass_url, - os_cache=os_cache, - http_log_debug=http_log_debug, - cacert=cacert, - connection_pool=connection_pool) + self.client = client._construct_http_client( + username=username, + password=password, + user_id=user_id, + project_id=project_id, + tenant_id=tenant_id, + auth_url=auth_url, + auth_token=auth_token, + insecure=insecure, + timeout=timeout, + auth_system=auth_system, + auth_plugin=auth_plugin, + proxy_token=proxy_token, + proxy_tenant_id=proxy_tenant_id, + region_name=region_name, + endpoint_type=endpoint_type, + service_type=service_type, + service_name=service_name, + volume_service_name=volume_service_name, + timings=timings, + bypass_url=bypass_url, + os_cache=self.os_cache, + http_log_debug=http_log_debug, + cacert=cacert, + connection_pool=connection_pool, + session=session, + auth=auth) self.completion_cache = completion_cache @@ -146,22 +148,28 @@ def clear_completion_cache_for_class(self, obj_class): if self.completion_cache: self.completion_cache.clear_class(obj_class) + @client._original_only def __enter__(self): self.client.open_session() return self + @client._original_only def __exit__(self, t, v, tb): self.client.close_session() + @client._original_only def set_management_url(self, url): self.client.set_management_url(url) + @client._original_only def get_timings(self): return self.client.get_timings() + @client._original_only def reset_timings(self): self.client.reset_timings() + @client._original_only def authenticate(self): """ Authenticate against the server. diff --git a/test-requirements.txt b/test-requirements.txt index 5f294a3c9..42d186390 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -7,6 +7,8 @@ httpretty>=0.8.0,!=0.8.1,!=0.8.2 keyring>=2.1 mock>=1.0 sphinx>=1.1.2,!=1.2.0,<1.3 +python-keystoneclient>=0.9.0 oslosphinx testrepository>=0.0.18 +testscenarios>=0.4 testtools>=0.9.34 From 465d3834719b1e26734af490451ab497519e642f Mon Sep 17 00:00:00 2001 From: henriquetruta Date: Wed, 18 Jun 2014 15:30:24 -0300 Subject: [PATCH 0553/1705] Fixing flavor access __repr__ method The Flavor Access __repr__ method was crashing because it was using a non-existing attribute called "name". On this fix, this method will now return the following: Closes-bug: #1331602 Change-Id: Ia2c24312e59e6c0aba1009766a15d390dc2721b1 --- novaclient/tests/v1_1/test_flavor_access.py | 12 ++++++++++++ novaclient/v1_1/flavor_access.py | 3 ++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/novaclient/tests/v1_1/test_flavor_access.py b/novaclient/tests/v1_1/test_flavor_access.py index ec937d01c..b85f838ee 100644 --- a/novaclient/tests/v1_1/test_flavor_access.py +++ b/novaclient/tests/v1_1/test_flavor_access.py @@ -56,3 +56,15 @@ def test_remove_tenant_access(self): cs.assert_called('POST', '/flavors/2/action', body) [self.assertIsInstance(a, flavor_access.FlavorAccess) for a in r] + + def test_repr_flavor_access(self): + flavor = cs.flavors.get(2) + tenant = 'proj3' + r = cs.flavor_access.add_tenant_access(flavor, tenant) + + def get_expected(flavor_access): + return ("" % + (flavor_access.flavor_id, flavor_access.tenant_id)) + + for a in r: + self.assertEqual(get_expected(a), repr(a)) diff --git a/novaclient/v1_1/flavor_access.py b/novaclient/v1_1/flavor_access.py index 29c1f84f5..3b4ce43e6 100644 --- a/novaclient/v1_1/flavor_access.py +++ b/novaclient/v1_1/flavor_access.py @@ -21,7 +21,8 @@ class FlavorAccess(base.Resource): def __repr__(self): - return "" % self.name + return ("" % + (self.flavor_id, self.tenant_id)) class FlavorAccessManager(base.ManagerWithFind): From 94beacd7e95242bdda8a48b126e68105f2abc71c Mon Sep 17 00:00:00 2001 From: Yaguang Tang Date: Fri, 13 Jun 2014 11:50:11 +0800 Subject: [PATCH 0554/1705] Fix the wrong dest of 'vlan' option and add new 'vlan_start' option Allow to specify vlan tag when create nova network, the dest of vlan option should be 'vlan' instead of 'vlan_start'. Provide new option 'vlan_start' when create nova network. Change-Id: Idfaf58d4eee4d9bbf6bba5e0b6c9dbf82236694d Closes-bug: #1329599 --- novaclient/tests/v1_1/test_networks.py | 1 + novaclient/tests/v1_1/test_shell.py | 9 ++++++++- novaclient/v1_1/networks.py | 1 + novaclient/v1_1/shell.py | 11 ++++++++--- 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/novaclient/tests/v1_1/test_networks.py b/novaclient/tests/v1_1/test_networks.py index 369772ea8..c3b2a7805 100644 --- a/novaclient/tests/v1_1/test_networks.py +++ b/novaclient/tests/v1_1/test_networks.py @@ -56,6 +56,7 @@ def test_create_allparams(self): 'multi_host': 'T', 'priority': '1', 'project_id': '1', + 'vlan': 5, 'vlan_start': 1, 'vpn_start': 1 } diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index 262fd6668..43dda1631 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -1743,7 +1743,14 @@ def test_network_create_vlan(self): self.run_command('network-create --fixed-range-v4 192.168.0.0/24' ' --vlan=200 new_network') body = {'network': {'cidr': '192.168.0.0/24', 'label': 'new_network', - 'vlan_start': '200'}} + 'vlan': '200'}} + self.assert_called('POST', '/os-networks', body) + + def test_network_create_vlan_start(self): + self.run_command('network-create --fixed-range-v4 192.168.0.0/24' + ' --vlan-start=100 new_network') + body = {'network': {'cidr': '192.168.0.0/24', 'label': 'new_network', + 'vlan_start': '100'}} self.assert_called('POST', '/os-networks', body) def test_add_fixed_ip(self): diff --git a/novaclient/v1_1/networks.py b/novaclient/v1_1/networks.py index d29faf26d..d4dc78b8e 100644 --- a/novaclient/v1_1/networks.py +++ b/novaclient/v1_1/networks.py @@ -86,6 +86,7 @@ def create(self, **kwargs): :param multi_host: str :param priority: str :param project_id: str + :param vlan: int :param vlan_start: int :param vpn_start: int diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 92025182d..91c1c62eb 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -835,7 +835,7 @@ def _filter_network_create_options(args): valid_args = ['label', 'cidr', 'vlan_start', 'vpn_start', 'cidr_v6', 'gateway', 'gateway_v6', 'bridge', 'bridge_interface', 'multi_host', 'dns1', 'dns2', 'uuid', 'fixed_cidr', - 'project_id', 'priority'] + 'project_id', 'priority', 'vlan'] kwargs = {} for k, v in args.__dict__.items(): if k in valid_args and v is not None: @@ -855,9 +855,14 @@ def _filter_network_create_options(args): dest="cidr_v6", help=_('IPv6 subnet (ex: fe80::/64')) @utils.arg('--vlan', - dest='vlan_start', + dest='vlan', metavar='', - help=_("vlan id")) + help=_("vlan id to be assigned to project")) +@utils.arg('--vlan-start', + dest='vlan_start', + metavar='', + help=_('First vlan ID to be assigned to project. Subsequent vlan' + ' IDs will be assigned incrementally')) @utils.arg('--vpn', dest='vpn_start', metavar='', From 48ba2f5ab2b5da74dca855d53786326cb0cdc173 Mon Sep 17 00:00:00 2001 From: Jamie Lennox Date: Mon, 26 May 2014 12:30:18 +1000 Subject: [PATCH 0555/1705] Convert Quota tests to httpretty Change-Id: I0b7e7a342f9dbb8d9db2d45e4a90ccb23210001a blueprint: httpretty-testing --- novaclient/tests/fixture_data/quotas.py | 87 +++++++++++++++++++++++++ novaclient/tests/v1_1/test_quotas.py | 24 ++++--- novaclient/tests/v3/test_quotas.py | 16 ++--- 3 files changed, 105 insertions(+), 22 deletions(-) create mode 100644 novaclient/tests/fixture_data/quotas.py diff --git a/novaclient/tests/fixture_data/quotas.py b/novaclient/tests/fixture_data/quotas.py new file mode 100644 index 000000000..70db079dd --- /dev/null +++ b/novaclient/tests/fixture_data/quotas.py @@ -0,0 +1,87 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import httpretty + +from novaclient.openstack.common import jsonutils +from novaclient.tests.fixture_data import base + + +class V1(base.Fixture): + + base_url = 'os-quota-sets' + + def setUp(self): + super(V1, self).setUp() + + uuid = '97f4c221-bff4-4578-b030-0df4ef119353' + uuid2 = '97f4c221bff44578b0300df4ef119353' + test_json = jsonutils.dumps({'quota_set': self.test_quota('test')}) + + for u in ('test', 'tenant-id', 'tenant-id/defaults', + '%s/defaults' % uuid2): + httpretty.register_uri(httpretty.GET, self.url(u), + body=test_json, + content_type='application/json') + + quota_json = jsonutils.dumps({'quota_set': self.test_quota(uuid)}) + httpretty.register_uri(httpretty.PUT, self.url(uuid), + body=quota_json, + content_type='application/json') + httpretty.register_uri(httpretty.GET, self.url(uuid), + body=quota_json, + content_type='application/json') + + quota_json2 = jsonutils.dumps({'quota_set': self.test_quota(uuid2)}) + httpretty.register_uri(httpretty.PUT, self.url(uuid2), + body=quota_json2, + content_type='application/json') + httpretty.register_uri(httpretty.GET, self.url(uuid2), + body=quota_json2, + content_type='application/json') + + for u in ('test', uuid2): + httpretty.register_uri(httpretty.DELETE, self.url(u), status=202) + + def test_quota(self, tenant_id='test'): + return { + 'tenant_id': tenant_id, + 'metadata_items': [], + 'injected_file_content_bytes': 1, + 'injected_file_path_bytes': 1, + 'ram': 1, + 'floating_ips': 1, + 'instances': 1, + 'injected_files': 1, + 'cores': 1, + 'keypairs': 1, + 'security_groups': 1, + 'security_group_rules': 1 + } + + +class V3(V1): + + def setUp(self): + super(V3, self).setUp() + + get_detail = { + 'quota_set': { + 'cores': {'reserved': 0, 'in_use': 0, 'limit': 10}, + 'instances': {'reserved': 0, 'in_use': 4, 'limit': 50}, + 'ram': {'reserved': 0, 'in_use': 1024, 'limit': 51200} + } + } + + httpretty.register_uri(httpretty.GET, self.url('test', 'detail'), + body=jsonutils.dumps(get_detail), + content_type='application/json') diff --git a/novaclient/tests/v1_1/test_quotas.py b/novaclient/tests/v1_1/test_quotas.py index 06d7461d3..8ff0d5967 100644 --- a/novaclient/tests/v1_1/test_quotas.py +++ b/novaclient/tests/v1_1/test_quotas.py @@ -13,39 +13,37 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient.tests.fixture_data import client +from novaclient.tests.fixture_data import quotas as data from novaclient.tests import utils -from novaclient.tests.v1_1 import fakes -class QuotaSetsTest(utils.TestCase): - def setUp(self): - super(QuotaSetsTest, self).setUp() - self.cs = self._get_fake_client() +class QuotaSetsTest(utils.FixturedTestCase): - def _get_fake_client(self): - return fakes.FakeClient() + client_fixture_class = client.V1 + data_fixture_class = data.V1 def test_tenant_quotas_get(self): tenant_id = 'test' self.cs.quotas.get(tenant_id) - self.cs.assert_called('GET', '/os-quota-sets/%s' % tenant_id) + self.assert_called('GET', '/os-quota-sets/%s' % tenant_id) def test_user_quotas_get(self): tenant_id = 'test' user_id = 'fake_user' self.cs.quotas.get(tenant_id, user_id=user_id) url = '/os-quota-sets/%s?user_id=%s' % (tenant_id, user_id) - self.cs.assert_called('GET', url) + self.assert_called('GET', url) def test_tenant_quotas_defaults(self): tenant_id = '97f4c221bff44578b0300df4ef119353' self.cs.quotas.defaults(tenant_id) - self.cs.assert_called('GET', '/os-quota-sets/%s/defaults' % tenant_id) + self.assert_called('GET', '/os-quota-sets/%s/defaults' % tenant_id) def test_force_update_quota(self): q = self.cs.quotas.get('97f4c221bff44578b0300df4ef119353') q.update(cores=2, force=True) - self.cs.assert_called( + self.assert_called( 'PUT', '/os-quota-sets/97f4c221bff44578b0300df4ef119353', {'quota_set': {'force': True, 'cores': 2, @@ -54,11 +52,11 @@ def test_force_update_quota(self): def test_quotas_delete(self): tenant_id = 'test' self.cs.quotas.delete(tenant_id) - self.cs.assert_called('DELETE', '/os-quota-sets/%s' % tenant_id) + self.assert_called('DELETE', '/os-quota-sets/%s' % tenant_id) def test_user_quotas_delete(self): tenant_id = 'test' user_id = 'fake_user' self.cs.quotas.delete(tenant_id, user_id=user_id) url = '/os-quota-sets/%s?user_id=%s' % (tenant_id, user_id) - self.cs.assert_called('DELETE', url) + self.assert_called('DELETE', url) diff --git a/novaclient/tests/v3/test_quotas.py b/novaclient/tests/v3/test_quotas.py index 1e4eef19b..53ca3258e 100644 --- a/novaclient/tests/v3/test_quotas.py +++ b/novaclient/tests/v3/test_quotas.py @@ -12,22 +12,20 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient.tests.fixture_data import client +from novaclient.tests.fixture_data import quotas as data from novaclient.tests.v1_1 import test_quotas -from novaclient.tests.v3 import fakes class QuotaSetsTest(test_quotas.QuotaSetsTest): - def setUp(self): - super(QuotaSetsTest, self).setUp() - self.cs = self._get_fake_client() - def _get_fake_client(self): - return fakes.FakeClient() + client_fixture_class = client.V3 + data_fixture_class = data.V3 def test_force_update_quota(self): q = self.cs.quotas.get('97f4c221bff44578b0300df4ef119353') q.update(cores=2, force=True) - self.cs.assert_called( + self.assert_called( 'PUT', '/os-quota-sets/97f4c221bff44578b0300df4ef119353', {'quota_set': {'force': True, 'cores': 2}}) @@ -35,11 +33,11 @@ def test_force_update_quota(self): def test_tenant_quotas_get_detail(self): tenant_id = 'test' self.cs.quotas.get(tenant_id, detail=True) - self.cs.assert_called('GET', '/os-quota-sets/%s/detail' % tenant_id) + self.assert_called('GET', '/os-quota-sets/%s/detail' % tenant_id) def test_user_quotas_get_detail(self): tenant_id = 'test' user_id = 'fake_user' self.cs.quotas.get(tenant_id, user_id=user_id, detail=True) url = '/os-quota-sets/%s/detail?user_id=%s' % (tenant_id, user_id) - self.cs.assert_called('GET', url) + self.assert_called('GET', url) From d6f59a30165f31e6df86a2deced6f96cd1b89e18 Mon Sep 17 00:00:00 2001 From: Jamie Lennox Date: Mon, 26 May 2014 13:01:36 +1000 Subject: [PATCH 0556/1705] Convert security group rules tests to httpretty Change-Id: I7b68712b05764661d3e7c3dcac7fce041e88e293 blueprint: httpretty-testing --- .../fixture_data/security_group_rules.py | 57 +++++++++++++++++++ .../tests/v1_1/test_security_group_rules.py | 37 ++++++------ 2 files changed, 78 insertions(+), 16 deletions(-) create mode 100644 novaclient/tests/fixture_data/security_group_rules.py diff --git a/novaclient/tests/fixture_data/security_group_rules.py b/novaclient/tests/fixture_data/security_group_rules.py new file mode 100644 index 000000000..303cfee7c --- /dev/null +++ b/novaclient/tests/fixture_data/security_group_rules.py @@ -0,0 +1,57 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import httpretty + +from novaclient.openstack.common import jsonutils +from novaclient.tests import fakes +from novaclient.tests.fixture_data import base + + +class Fixture(base.Fixture): + + base_url = 'os-security-group-rules' + + def setUp(self): + super(Fixture, self).setUp() + + rule = { + 'id': 1, + 'parent_group_id': 1, + 'group_id': 2, + 'ip_protocol': 'TCP', + 'from_port': '22', + 'to_port': 22, + 'cidr': '10.0.0.0/8' + } + + get_rules = {'security_group_rules': [rule]} + httpretty.register_uri(httpretty.GET, self.url(), + body=jsonutils.dumps(get_rules), + content_type='application/json') + + for u in (1, 11, 12): + httpretty.register_uri(httpretty.DELETE, self.url(u), status=202) + + def post_rules(request, url, headers): + body = jsonutils.loads(request.body.decode('utf-8')) + assert list(body) == ['security_group_rule'] + fakes.assert_has_keys(body['security_group_rule'], + required=['parent_group_id'], + optional=['group_id', 'ip_protocol', + 'from_port', 'to_port', 'cidr']) + + return 202, headers, jsonutils.dumps({'security_group_rule': rule}) + + httpretty.register_uri(httpretty.POST, self.url(), + body=post_rules, + content_type='application/json') diff --git a/novaclient/tests/v1_1/test_security_group_rules.py b/novaclient/tests/v1_1/test_security_group_rules.py index a7259619d..bf69a86ed 100644 --- a/novaclient/tests/v1_1/test_security_group_rules.py +++ b/novaclient/tests/v1_1/test_security_group_rules.py @@ -12,21 +12,24 @@ # under the License. from novaclient import exceptions +from novaclient.tests.fixture_data import client +from novaclient.tests.fixture_data import security_group_rules as data from novaclient.tests import utils -from novaclient.tests.v1_1 import fakes from novaclient.v1_1 import security_group_rules -cs = fakes.FakeClient() +class SecurityGroupRulesTest(utils.FixturedTestCase): + client_fixture_class = client.V1 + data_fixture_class = data.Fixture -class SecurityGroupRulesTest(utils.TestCase): def test_delete_security_group_rule(self): - cs.security_group_rules.delete(1) - cs.assert_called('DELETE', '/os-security-group-rules/1') + self.cs.security_group_rules.delete(1) + self.assert_called('DELETE', '/os-security-group-rules/1') def test_create_security_group_rule(self): - sg = cs.security_group_rules.create(1, "tcp", 1, 65535, "10.0.0.0/16") + sg = self.cs.security_group_rules.create(1, "tcp", 1, 65535, + "10.0.0.0/16") body = { "security_group_rule": { @@ -39,12 +42,12 @@ def test_create_security_group_rule(self): } } - cs.assert_called('POST', '/os-security-group-rules', body) + self.assert_called('POST', '/os-security-group-rules', body) self.assertTrue(isinstance(sg, security_group_rules.SecurityGroupRule)) def test_create_security_group_group_rule(self): - sg = cs.security_group_rules.create(1, "tcp", 1, 65535, "10.0.0.0/16", - 101) + sg = self.cs.security_group_rules.create(1, "tcp", 1, 65535, + "10.0.0.0/16", 101) body = { "security_group_rule": { @@ -57,25 +60,27 @@ def test_create_security_group_group_rule(self): } } - cs.assert_called('POST', '/os-security-group-rules', body) + self.assert_called('POST', '/os-security-group-rules', body) self.assertTrue(isinstance(sg, security_group_rules.SecurityGroupRule)) def test_invalid_parameters_create(self): self.assertRaises(exceptions.CommandError, - cs.security_group_rules.create, + self.cs.security_group_rules.create, 1, "invalid_ip_protocol", 1, 65535, "10.0.0.0/16", 101) self.assertRaises(exceptions.CommandError, - cs.security_group_rules.create, + self.cs.security_group_rules.create, 1, "tcp", "invalid_from_port", 65535, "10.0.0.0/16", 101) self.assertRaises(exceptions.CommandError, - cs.security_group_rules.create, + self.cs.security_group_rules.create, 1, "tcp", 1, "invalid_to_port", "10.0.0.0/16", 101) def test_security_group_rule_str(self): - sg = cs.security_group_rules.create(1, "tcp", 1, 65535, "10.0.0.0/16") + sg = self.cs.security_group_rules.create(1, "tcp", 1, 65535, + "10.0.0.0/16") self.assertEqual('1', str(sg)) def test_security_group_rule_del(self): - sg = cs.security_group_rules.create(1, "tcp", 1, 65535, "10.0.0.0/16") + sg = self.cs.security_group_rules.create(1, "tcp", 1, 65535, + "10.0.0.0/16") sg.delete() - cs.assert_called('DELETE', '/os-security-group-rules/1') + self.assert_called('DELETE', '/os-security-group-rules/1') From 0170e78454e81e8ea7444168ce2e83e0f1d8f2c9 Mon Sep 17 00:00:00 2001 From: Jamie Lennox Date: Mon, 26 May 2014 13:17:41 +1000 Subject: [PATCH 0557/1705] Convert security group tests to httpretty Change-Id: Icdc2c2c6f66a014c29241e262d21d50e39128105 blueprint: httpretty-testing --- .../tests/fixture_data/security_groups.py | 99 +++++++++++++++++++ novaclient/tests/v1_1/test_security_groups.py | 42 ++++---- 2 files changed, 121 insertions(+), 20 deletions(-) create mode 100644 novaclient/tests/fixture_data/security_groups.py diff --git a/novaclient/tests/fixture_data/security_groups.py b/novaclient/tests/fixture_data/security_groups.py new file mode 100644 index 000000000..9f0c271ad --- /dev/null +++ b/novaclient/tests/fixture_data/security_groups.py @@ -0,0 +1,99 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +import httpretty + +from novaclient.openstack.common import jsonutils +from novaclient.tests import fakes +from novaclient.tests.fixture_data import base + + +class Fixture(base.Fixture): + + base_url = 'os-security-groups' + + def setUp(self): + super(Fixture, self).setUp() + + security_group_1 = { + "name": "test", + "description": "FAKE_SECURITY_GROUP", + "tenant_id": "4ffc664c198e435e9853f2538fbcd7a7", + "id": 1, + "rules": [ + { + "id": 11, + "group": {}, + "ip_protocol": "TCP", + "from_port": 22, + "to_port": 22, + "parent_group_id": 1, + "ip_range": {"cidr": "10.0.0.0/8"} + }, + { + "id": 12, + "group": { + "tenant_id": "272bee4c1e624cd4a72a6b0ea55b4582", + "name": "test2" + }, + "ip_protocol": "TCP", + "from_port": 222, + "to_port": 222, + "parent_group_id": 1, + "ip_range": {} + } + ] + } + + security_group_2 = { + "name": "test2", + "description": "FAKE_SECURITY_GROUP2", + "tenant_id": "272bee4c1e624cd4a72a6b0ea55b4582", + "id": 2, + "rules": [] + } + + get_groups = {'security_groups': [security_group_1, security_group_2]} + httpretty.register_uri(httpretty.GET, self.url(), + body=jsonutils.dumps(get_groups), + content_type='application/json') + + get_group_1 = {'security_group': security_group_1} + httpretty.register_uri(httpretty.GET, self.url(1), + body=jsonutils.dumps(get_group_1), + content_type='application/json') + + httpretty.register_uri(httpretty.DELETE, self.url(1), status=202) + + def post_os_security_groups(request, url, headers): + body = jsonutils.loads(request.body.decode('utf-8')) + assert list(body) == ['security_group'] + fakes.assert_has_keys(body['security_group'], + required=['name', 'description']) + r = jsonutils.dumps({'security_group': security_group_1}) + return 202, headers, r + + httpretty.register_uri(httpretty.POST, self.url(), + body=post_os_security_groups, + content_type='application/json') + + def put_os_security_groups_1(request, url, headers): + body = jsonutils.loads(request.body.decode('utf-8')) + assert list(body) == ['security_group'] + fakes.assert_has_keys(body['security_group'], + required=['name', 'description']) + return 205, headers, request.body + + httpretty.register_uri(httpretty.PUT, self.url(1), + body=put_os_security_groups_1, + content_type='application/json') diff --git a/novaclient/tests/v1_1/test_security_groups.py b/novaclient/tests/v1_1/test_security_groups.py index a46533d9a..f5b771af1 100644 --- a/novaclient/tests/v1_1/test_security_groups.py +++ b/novaclient/tests/v1_1/test_security_groups.py @@ -11,18 +11,20 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient.tests.fixture_data import client +from novaclient.tests.fixture_data import security_groups as data from novaclient.tests import utils -from novaclient.tests.v1_1 import fakes from novaclient.v1_1 import security_groups -cs = fakes.FakeClient() +class SecurityGroupsTest(utils.FixturedTestCase): + client_fixture_class = client.V1 + data_fixture_class = data.Fixture -class SecurityGroupsTest(utils.TestCase): def _do_test_list_security_groups(self, search_opts, path): - sgs = cs.security_groups.list(search_opts=search_opts) - cs.assert_called('GET', path) + sgs = self.cs.security_groups.list(search_opts=search_opts) + self.assert_called('GET', path) for sg in sgs: self.assertIsInstance(sg, security_groups.SecurityGroup) @@ -39,34 +41,34 @@ def test_list_security_groups_all_tenants_off(self): {'all_tenants': 0}, '/os-security-groups') def test_get_security_groups(self): - sg = cs.security_groups.get(1) - cs.assert_called('GET', '/os-security-groups/1') + sg = self.cs.security_groups.get(1) + self.assert_called('GET', '/os-security-groups/1') self.assertIsInstance(sg, security_groups.SecurityGroup) self.assertEqual('1', str(sg)) def test_delete_security_group(self): - sg = cs.security_groups.list()[0] + sg = self.cs.security_groups.list()[0] sg.delete() - cs.assert_called('DELETE', '/os-security-groups/1') - cs.security_groups.delete(1) - cs.assert_called('DELETE', '/os-security-groups/1') - cs.security_groups.delete(sg) - cs.assert_called('DELETE', '/os-security-groups/1') + self.assert_called('DELETE', '/os-security-groups/1') + self.cs.security_groups.delete(1) + self.assert_called('DELETE', '/os-security-groups/1') + self.cs.security_groups.delete(sg) + self.assert_called('DELETE', '/os-security-groups/1') def test_create_security_group(self): - sg = cs.security_groups.create("foo", "foo barr") - cs.assert_called('POST', '/os-security-groups') + sg = self.cs.security_groups.create("foo", "foo barr") + self.assert_called('POST', '/os-security-groups') self.assertIsInstance(sg, security_groups.SecurityGroup) def test_update_security_group(self): - sg = cs.security_groups.list()[0] - secgroup = cs.security_groups.update(sg, "update", "update") - cs.assert_called('PUT', '/os-security-groups/1') + sg = self.cs.security_groups.list()[0] + secgroup = self.cs.security_groups.update(sg, "update", "update") + self.assert_called('PUT', '/os-security-groups/1') self.assertIsInstance(secgroup, security_groups.SecurityGroup) def test_refresh_security_group(self): - sg = cs.security_groups.get(1) - sg2 = cs.security_groups.get(1) + sg = self.cs.security_groups.get(1) + sg2 = self.cs.security_groups.get(1) self.assertEqual(sg.name, sg2.name) sg2.name = "should be test" self.assertNotEqual(sg.name, sg2.name) From aec5b59e794f67282b405ab76345f842297bb46f Mon Sep 17 00:00:00 2001 From: Jamie Lennox Date: Mon, 26 May 2014 13:31:18 +1000 Subject: [PATCH 0558/1705] Convert Server Group Tests to httpretty Change-Id: Ia67fe5f27fa9b7490e72e0e0c6e44b9c83d611c8 blueprint: httpretty-testing --- .../tests/fixture_data/server_groups.py | 75 +++++++++++++++++++ novaclient/tests/v1_1/test_server_groups.py | 25 ++++--- 2 files changed, 88 insertions(+), 12 deletions(-) create mode 100644 novaclient/tests/fixture_data/server_groups.py diff --git a/novaclient/tests/fixture_data/server_groups.py b/novaclient/tests/fixture_data/server_groups.py new file mode 100644 index 000000000..65a0c78d2 --- /dev/null +++ b/novaclient/tests/fixture_data/server_groups.py @@ -0,0 +1,75 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +import httpretty + +from novaclient.openstack.common import jsonutils +from novaclient.tests.fixture_data import base + + +class Fixture(base.Fixture): + + base_url = 'os-server-groups' + + def setUp(self): + super(Fixture, self).setUp() + + server_groups = [ + { + "members": [], + "metadata": {}, + "id": "2cbd51f4-fafe-4cdb-801b-cf913a6f288b", + "policies": [], + "name": "ig1" + }, + { + "members": [], + "metadata": {}, + "id": "4473bb03-4370-4bfb-80d3-dc8cffc47d94", + "policies": ["anti-affinity"], + "name": "ig2" + }, + { + "members": [], + "metadata": {"key": "value"}, + "id": "31ab9bdb-55e1-4ac3-b094-97eeb1b65cc4", + "policies": [], "name": "ig3" + }, + { + "members": ["2dccb4a1-02b9-482a-aa23-5799490d6f5d"], + "metadata": {}, + "id": "4890bb03-7070-45fb-8453-d34556c87d94", + "policies": ["anti-affinity"], + "name": "ig2" + } + ] + + get_server_groups = {'server_groups': server_groups} + httpretty.register_uri(httpretty.GET, self.url(), + body=jsonutils.dumps(get_server_groups), + content_type='application/json') + + server = server_groups[0] + server_json = jsonutils.dumps({'server_group': server}) + + def _register(method, *args): + httpretty.register_uri(method, self.url(*args), body=server_json) + + _register(httpretty.POST) + _register(httpretty.POST, server['id']) + _register(httpretty.GET, server['id']) + _register(httpretty.PUT, server['id']) + _register(httpretty.POST, server['id'], '/action') + + httpretty.register_uri(httpretty.DELETE, self.url(server['id']), + status=202) diff --git a/novaclient/tests/v1_1/test_server_groups.py b/novaclient/tests/v1_1/test_server_groups.py index fde5def2e..e341dc0c3 100644 --- a/novaclient/tests/v1_1/test_server_groups.py +++ b/novaclient/tests/v1_1/test_server_groups.py @@ -13,19 +13,20 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient.tests.fixture_data import client +from novaclient.tests.fixture_data import server_groups as data from novaclient.tests import utils -from novaclient.tests.v1_1 import fakes from novaclient.v1_1 import server_groups -cs = fakes.FakeClient() +class ServerGroupsTest(utils.FixturedTestCase): - -class ServerGroupsTest(utils.TestCase): + client_fixture_class = client.V1 + data_fixture_class = data.Fixture def test_list_server_groups(self): - result = cs.server_groups.list() - cs.assert_called('GET', '/os-server-groups') + result = self.cs.server_groups.list() + self.assert_called('GET', '/os-server-groups') for server_group in result: self.assertTrue(isinstance(server_group, server_groups.ServerGroup)) @@ -33,20 +34,20 @@ def test_list_server_groups(self): def test_create_server_group(self): kwargs = {'name': 'ig1', 'policies': ['anti-affinity']} - server_group = cs.server_groups.create(**kwargs) + server_group = self.cs.server_groups.create(**kwargs) body = {'server_group': kwargs} - cs.assert_called('POST', '/os-server-groups', body) + self.assert_called('POST', '/os-server-groups', body) self.assertTrue(isinstance(server_group, server_groups.ServerGroup)) def test_get_server_group(self): id = '2cbd51f4-fafe-4cdb-801b-cf913a6f288b' - server_group = cs.server_groups.get(id) - cs.assert_called('GET', '/os-server-groups/%s' % id) + server_group = self.cs.server_groups.get(id) + self.assert_called('GET', '/os-server-groups/%s' % id) self.assertTrue(isinstance(server_group, server_groups.ServerGroup)) def test_delete_server_group(self): id = '2cbd51f4-fafe-4cdb-801b-cf913a6f288b' - cs.server_groups.delete(id) - cs.assert_called('DELETE', '/os-server-groups/%s' % id) + self.cs.server_groups.delete(id) + self.assert_called('DELETE', '/os-server-groups/%s' % id) From a06a6abca7a3db6642c5963fe0cfec0f2d3e95b4 Mon Sep 17 00:00:00 2001 From: Pawel Koniszewski Date: Thu, 10 Jul 2014 15:15:40 +0200 Subject: [PATCH 0559/1705] Update nova boot help Actual help string for --block-device flag is not clear. User doesn't know which parameters are required and which are not. Change improves description of each parameter, so that user can understand how the --block-device works. Change-Id: If23cb2d64adb51dfb45e48511ca25988bc43b739 Implements: nova boot --block-device usage unclear Partial-Bug: #1323775 --- novaclient/v1_1/shell.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 67d60ae0d..de2b34ed6 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -394,16 +394,25 @@ def _boot(cs, args): action='append', default=[], help=_("Block device mapping with the keys: " - "id=image_id, snapshot_id or volume_id, " + "id=UUID (image_id, snapshot_id or volume_id only if using source " + "image, snapshot or volume) " "source=source type (image, snapshot, volume or blank), " "dest=destination type of the block device (volume or local), " - "bus=device's bus, " - "device=name of the device (e.g. vda, xda, ...), " - "size=size of the block device in GB, " - "format=device will be formatted (e.g. swap, ext3, ntfs, ...), " - "bootindex=integer used for ordering the boot disks, " - "type=device type (e.g. disk, cdrom, ...) and " - "shutdown=shutdown behaviour (either preserve or remove).")) + "bus=device's bus (e.g. uml, lxc, virtio, ...; if omitted, " + "hypervisor driver chooses a suitable default, " + "honoured only if device type is supplied) " + "type=device type (e.g. disk, cdrom, ...; defaults to 'disk') " + "device=name of the device (e.g. vda, xda, ...; " + "if omitted, hypervisor driver chooses suitable device " + "depending on selected bus), " + "size=size of the block device in GB (if omitted, " + "hypervisor driver calculates size), " + "format=device will be formatted (e.g. swap, ntfs, ...; optional), " + "bootindex=integer used for ordering the boot disks " + "(for image backed instances it is equal to 0, " + "for others need to be specified) and " + "shutdown=shutdown behaviour (either preserve or remove, " + "for local destination set to remove).")) @utils.arg('--swap', metavar="", default=None, From 3b8f0097003de1d526092b19ffffc419e70fcfc8 Mon Sep 17 00:00:00 2001 From: ozg Date: Fri, 4 Jul 2014 07:35:16 -0400 Subject: [PATCH 0560/1705] Adding multiple server support to nova reset-state Adding support specifying multiple servers to nova reset-state in a single invocation. Change-Id: I7add0c26b0e59b31751de7a60ebdde3f568b3354 Closes-Bug: #1314217 --- novaclient/tests/v1_1/fakes.py | 3 +++ novaclient/tests/v1_1/test_shell.py | 14 ++++++++++++++ novaclient/v1_1/shell.py | 17 +++++++++++++++-- novaclient/v3/shell.py | 17 +++++++++++++++-- 4 files changed, 47 insertions(+), 4 deletions(-) diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index d387e17c4..f61a02e92 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -641,6 +641,9 @@ def post_servers_1234_action(self, body, **kw): raise AssertionError("Unexpected server action: %s" % action) return (resp, _headers, _body) + def post_servers_5678_action(self, body, **kw): + return self.post_servers_1234_action(body, **kw) + # # Cloudpipe # diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index 262fd6668..1c8d12988 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -1366,6 +1366,20 @@ def test_reset_state(self): self.assert_called('POST', '/servers/1234/action', {'os-resetState': {'state': 'active'}}) + def test_reset_state_multiple(self): + self.run_command('reset-state sample-server sample-server2') + self.assert_called('POST', '/servers/1234/action', + {'os-resetState': {'state': 'error'}}, pos=-4) + self.assert_called('POST', '/servers/5678/action', + {'os-resetState': {'state': 'error'}}, pos=-1) + + def test_reset_state_active_multiple(self): + self.run_command('reset-state --active sample-server sample-server2') + self.assert_called('POST', '/servers/1234/action', + {'os-resetState': {'state': 'active'}}, pos=-4) + self.assert_called('POST', '/servers/5678/action', + {'os-resetState': {'state': 'active'}}, pos=-1) + def test_reset_network(self): self.run_command('reset-network sample-server') self.assert_called('POST', '/servers/1234/action', diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 67d60ae0d..f2233135f 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2856,14 +2856,27 @@ def do_live_migration(cs, args): args.disk_over_commit) -@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('server', metavar='', nargs='+', + help=_('Name or ID of server(s).')) @utils.arg('--active', action='store_const', dest='state', default='error', const='active', help=_('Request the server be reset to "active" state instead ' 'of "error" state (the default).')) def do_reset_state(cs, args): """Reset the state of a server.""" - _find_server(cs, args.server).reset_state(args.state) + failure_flag = False + + for server in args.server: + try: + _find_server(cs, server).reset_state(args.state) + except Exception as e: + failure_flag = True + msg = "Reset state for server %s failed: %s" % (server, e) + print(msg) + + if failure_flag: + msg = "Unable to reset the state for the specified server(s)." + raise exceptions.CommandError(msg) @utils.arg('server', metavar='', help=_('Name or ID of server.')) diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index f24e2917c..9f4b13bde 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -2382,14 +2382,27 @@ def do_live_migration(cs, args): args.disk_over_commit) -@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('server', metavar='', nargs='+', + help='Name or ID of server(s).') @utils.arg('--active', action='store_const', dest='state', default='error', const='active', help='Request the server be reset to "active" state instead ' 'of "error" state (the default).') def do_reset_state(cs, args): """Reset the state of a server.""" - _find_server(cs, args.server).reset_state(args.state) + failure_flag = False + + for server in args.server: + try: + _find_server(cs, server).reset_state(args.state) + except Exception as e: + failure_flag = True + msg = "Reset state for server %s failed: %s" % (server, e) + print(msg) + + if failure_flag: + msg = "Unable to reset the state for the specified server(s)." + raise exceptions.CommandError(msg) @utils.arg('server', metavar='', help='Name or ID of server.') From e14f9741a6fedc2ed4cddd304b2f86378b707575 Mon Sep 17 00:00:00 2001 From: Russell Bryant Date: Fri, 11 Jul 2014 13:29:34 -0400 Subject: [PATCH 0561/1705] Revert "Set default http-based exception as `HttpError`" This reverts commit 4e1ee661083bd672a884fd13fdb1ffaadea5252a. This change is a part of a series that is not backwards compatible, and caused breakage in applications that use the client library. Change-Id: If614826fb0832602110c1ac8be2c0e6324a5a248 Partial-Bug: #1340596 --- novaclient/base.py | 2 +- novaclient/exceptions.py | 14 ++------------ 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/novaclient/base.py b/novaclient/base.py index 6b000f8fe..a677e3229 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -139,7 +139,7 @@ def find(self, **kwargs): num_matches = len(matches) if num_matches == 0: msg = "No %s matching %s." % (self.resource_class.__name__, kwargs) - raise exceptions.NotFound(msg) + raise exceptions.NotFound(404, msg) elif num_matches > 1: raise exceptions.NoUniqueMatch else: diff --git a/novaclient/exceptions.py b/novaclient/exceptions.py index 73f04dcef..6c1c2deba 100644 --- a/novaclient/exceptions.py +++ b/novaclient/exceptions.py @@ -28,15 +28,6 @@ OverLimit = RequestEntityTooLarge -def _deprecate_code_attribute(slf): - import warnings - warnings.warn("'code' attribute is deprecated since v.2.17.0. Use " - "'http_status' instead of this one.", UserWarning) - return slf.http_status - -HttpError.code = property(_deprecate_code_attribute) - - class NoTokenLookupException(ClientException): """This form of authentication does not support looking up endpoints from an existing token. @@ -76,7 +67,7 @@ class InvalidUsage(RuntimeError): def from_response(response, body, url, method=None): """ - Return an instance of an HttpError or subclass + Return an instance of an ClientException or subclass based on an requests response. Usage:: @@ -110,6 +101,5 @@ def from_response(response, body, url, method=None): kwargs['message'] = message kwargs['details'] = details - cls = _code_map.get(response.status_code, HttpError) - + cls = _code_map.get(response.status_code, ClientException) return cls(**kwargs) From 2ebed395c9aa6d91d674c6cf1de8a29640dbfef8 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 11 Jul 2014 21:55:36 +0000 Subject: [PATCH 0562/1705] Updated from global requirements Change-Id: Ice5da31fbd5b5689ced6326f195a2241c6220083 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 42d186390..ea080c2ce 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -4,7 +4,7 @@ coverage>=3.6 discover fixtures>=0.3.14 httpretty>=0.8.0,!=0.8.1,!=0.8.2 -keyring>=2.1 +keyring>=2.1,!=3.3 mock>=1.0 sphinx>=1.1.2,!=1.2.0,<1.3 python-keystoneclient>=0.9.0 From d678dd2955f4976e953d727c6f5a6bcfae59c2dc Mon Sep 17 00:00:00 2001 From: Russell Bryant Date: Fri, 11 Jul 2014 13:31:14 -0400 Subject: [PATCH 0563/1705] Revert "Reuse exceptions from Oslo" This reverts commit 3bde9c3f427110dae3480644752f49b79ac66ec5. This change is a part of a series that is not backwards compatible. It broke applications using the client library. Change-Id: Id01a0a0feb601539d3855f445ebaf6a3cc000610 Partial-Bug: #1340596 --- novaclient/exceptions.py | 174 +++++++++++++++++++++++++--- novaclient/tests/test_http.py | 6 +- novaclient/tests/test_utils.py | 6 +- novaclient/tests/v1_1/test_shell.py | 2 +- novaclient/utils.py | 2 +- 5 files changed, 167 insertions(+), 23 deletions(-) diff --git a/novaclient/exceptions.py b/novaclient/exceptions.py index 6c1c2deba..cc3133bdc 100644 --- a/novaclient/exceptions.py +++ b/novaclient/exceptions.py @@ -16,31 +16,161 @@ Exception definitions. """ -import inspect -import sys -import six +class UnsupportedVersion(Exception): + """Indicates that the user is trying to use an unsupported + version of the API. + """ + pass + + +class CommandError(Exception): + pass -from novaclient.openstack.common.apiclient.exceptions import * # noqa -# NOTE(akurilin): Since v.2.17.0 this alias should be left here to support -# backwards compatibility. -OverLimit = RequestEntityTooLarge +class AuthorizationFailure(Exception): + pass + + +class NoUniqueMatch(Exception): + pass -class NoTokenLookupException(ClientException): +class AuthSystemNotFound(Exception): + """When the user specify a AuthSystem but not installed.""" + def __init__(self, auth_system): + self.auth_system = auth_system + + def __str__(self): + return "AuthSystemNotFound: %s" % repr(self.auth_system) + + +class NoTokenLookupException(Exception): """This form of authentication does not support looking up endpoints from an existing token. """ pass +class EndpointNotFound(Exception): + """Could not find Service or Region in Service Catalog.""" + pass + + +class AmbiguousEndpoints(Exception): + """Found more than one matching endpoint in Service Catalog.""" + def __init__(self, endpoints=None): + self.endpoints = endpoints + + def __str__(self): + return "AmbiguousEndpoints: %s" % repr(self.endpoints) + + +class ConnectionRefused(Exception): + """ + Connection refused: the server refused the connection. + """ + def __init__(self, response=None): + self.response = response + + def __str__(self): + return "ConnectionRefused: %s" % repr(self.response) + + class InstanceInErrorState(Exception): """Instance is in the error state.""" pass -class RateLimit(RequestEntityTooLarge): +class ClientException(Exception): + """ + The base exception class for all exceptions this library raises. + """ + message = 'Unknown Error' + + def __init__(self, code, message=None, details=None, request_id=None, + url=None, method=None): + self.code = code + self.message = message or self.__class__.message + self.details = details + self.request_id = request_id + self.url = url + self.method = method + + def __str__(self): + formatted_string = "%s (HTTP %s)" % (self.message, self.code) + if self.request_id: + formatted_string += " (Request-ID: %s)" % self.request_id + + return formatted_string + + +class BadRequest(ClientException): + """ + HTTP 400 - Bad request: you sent some malformed data. + """ + http_status = 400 + message = "Bad request" + + +class Unauthorized(ClientException): + """ + HTTP 401 - Unauthorized: bad credentials. + """ + http_status = 401 + message = "Unauthorized" + + +class Forbidden(ClientException): + """ + HTTP 403 - Forbidden: your credentials don't give you access to this + resource. + """ + http_status = 403 + message = "Forbidden" + + +class NotFound(ClientException): + """ + HTTP 404 - Not found + """ + http_status = 404 + message = "Not found" + + +class MethodNotAllowed(ClientException): + """ + HTTP 405 - Method Not Allowed + """ + http_status = 405 + message = "Method Not Allowed" + + +class Conflict(ClientException): + """ + HTTP 409 - Conflict + """ + http_status = 409 + message = "Conflict" + + +class OverLimit(ClientException): + """ + HTTP 413 - Over limit: you're over the API limits for this time period. + """ + http_status = 413 + message = "Over limit" + + def __init__(self, *args, **kwargs): + try: + self.retry_after = int(kwargs.pop('retry_after')) + except (KeyError, ValueError): + self.retry_after = 0 + + super(OverLimit, self).__init__(*args, **kwargs) + + +class RateLimit(OverLimit): """ HTTP 429 - Rate limit: you've sent too many requests for this time period. """ @@ -48,11 +178,25 @@ class RateLimit(RequestEntityTooLarge): message = "Rate limit" -_code_map = dict( - (getattr(obj, 'http_status', None), obj) - for name, obj in six.iteritems(vars(sys.modules[__name__])) - if inspect.isclass(obj) and getattr(obj, 'http_status', False) -) +# NotImplemented is a python keyword. +class HTTPNotImplemented(ClientException): + """ + HTTP 501 - Not Implemented: the server does not support this operation. + """ + http_status = 501 + message = "Not Implemented" + + +# In Python 2.4 Exception is old-style and thus doesn't have a __subclasses__() +# so we can do this: +# _code_map = dict((c.http_status, c) +# for c in ClientException.__subclasses__()) +# +# Instead, we have to hardcode it: +_error_classes = [BadRequest, Unauthorized, Forbidden, NotFound, + MethodNotAllowed, Conflict, OverLimit, RateLimit, + HTTPNotImplemented] +_code_map = dict((c.http_status, c) for c in _error_classes) class InvalidUsage(RuntimeError): @@ -77,7 +221,7 @@ def from_response(response, body, url, method=None): raise exception_from_response(resp, rest.text) """ kwargs = { - 'http_status': response.status_code, + 'code': response.status_code, 'method': method, 'url': url, 'request_id': None, diff --git a/novaclient/tests/test_http.py b/novaclient/tests/test_http.py index 824bf4b5f..2797a438f 100644 --- a/novaclient/tests/test_http.py +++ b/novaclient/tests/test_http.py @@ -150,7 +150,7 @@ def test_unknown_server_error(self): # Python 2.7 and testtools doesn't match that implementation yet try: cl.get('/hi') - except exceptions.ServiceUnavailable as exc: - self.assertIn('Service Unavailable (HTTP 503)', six.text_type(exc)) + except exceptions.ClientException as exc: + self.assertIn('Unknown Error', six.text_type(exc)) else: - self.fail('Expected exceptions.ServiceUnavailable') + self.fail('Expected exceptions.ClientException') diff --git a/novaclient/tests/test_utils.py b/novaclient/tests/test_utils.py index 3892820ed..31678ca2a 100644 --- a/novaclient/tests/test_utils.py +++ b/novaclient/tests/test_utils.py @@ -89,15 +89,15 @@ def setUp(self): def test_find_none(self): """Test a few non-valid inputs.""" - self.assertRaises(exceptions.ResourceNotFound, + self.assertRaises(exceptions.CommandError, utils.find_resource, self.manager, 'asdf') - self.assertRaises(exceptions.ResourceNotFound, + self.assertRaises(exceptions.CommandError, utils.find_resource, self.manager, None) - self.assertRaises(exceptions.ResourceNotFound, + self.assertRaises(exceptions.CommandError, utils.find_resource, self.manager, {}) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index e6718b188..da2fed520 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -990,7 +990,7 @@ def test_show_no_image(self): self.assert_called('GET', '/flavors/1', pos=-1) def test_show_bad_id(self): - self.assertRaises(exceptions.ResourceNotFound, + self.assertRaises(exceptions.CommandError, self.run_command, 'show xxx') @mock.patch('novaclient.v1_1.shell.utils.print_dict') diff --git a/novaclient/utils.py b/novaclient/utils.py index aa3b5ab5d..bedd74347 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -242,7 +242,7 @@ def find_resource(manager, name_or_id, **find_args): msg = _("No %(class)s with a name or ID of '%(name)s' exists.") % \ {'class': manager.resource_class.__name__.lower(), 'name': name_or_id} - raise exceptions.ResourceNotFound(msg) + raise exceptions.CommandError(msg) except exceptions.NoUniqueMatch: msg = (_("Multiple %(class)s matches found for '%(name)s', use an ID " "to be more specific.") % From 0ff4afc8e90910db8f8056cd0e49cd593dfb11ff Mon Sep 17 00:00:00 2001 From: Sanjay Kumar Singh Date: Thu, 26 Jun 2014 13:57:48 +0530 Subject: [PATCH 0564/1705] Description is mandatory parameter when creating Security Group As per the bug, description should not be a mandatory parameter as it creates inconsistency with other opestack items which has description as an optional parameter. So in the fix, the description argument has been made optional. The old command was: nova secgroup-create Now new command will be: nova secgroup-create [] Patch updated based on the comments received Updated commit message for gerrit autolink Closes-Bug: #1332133 Change-Id: I2ded915be4a57f3c46e6cf5d1bf00e8c917cd0b9 --- novaclient/tests/v1_1/test_shell.py | 7 +++++++ novaclient/v1_1/shell.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index e6718b188..94e82a2d0 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -1843,6 +1843,13 @@ def test_security_group_create(self): {'name': 'test', 'description': 'FAKE_SECURITY_GROUP'}}) + def test_security_group_create_without_description(self): + self.run_command('secgroup-create test') + self.assert_called('POST', '/os-security-groups', + {'security_group': + {'name': 'test', + 'description': None}}) + def test_security_group_update(self): self.run_command('secgroup-update test te FAKE_SECURITY_GROUP') self.assert_called('PUT', '/os-security-groups/1', diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index ed43277d8..58e8416eb 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2300,7 +2300,7 @@ def do_secgroup_delete_rule(cs, args): @utils.arg('name', metavar='', help=_('Name of security group.')) -@utils.arg('description', metavar='', +@utils.arg('description', metavar='', nargs='?', help=_('Description of security group.')) def do_secgroup_create(cs, args): """Create a security group.""" From 67585ab36e2b0d5a9ca2f7e8984e0ea02b295ae2 Mon Sep 17 00:00:00 2001 From: Ken'ichi Ohmichi Date: Tue, 8 Jul 2014 10:56:36 +0900 Subject: [PATCH 0565/1705] Add "version-list" for listing REST API versions Nova provides REST API versions. This patch adds a subcommand "version-list" for outputting the versions. Change-Id: I1b40ad57912aa740c0b1d9221018aba9b13a5436 --- novaclient/client.py | 16 +++++++++++---- novaclient/tests/test_client.py | 31 +++++++++++++++++++++++++++++ novaclient/v1_1/client.py | 2 ++ novaclient/v1_1/shell.py | 7 +++++++ novaclient/v1_1/versions.py | 35 +++++++++++++++++++++++++++++++++ 5 files changed, 87 insertions(+), 4 deletions(-) create mode 100644 novaclient/v1_1/versions.py diff --git a/novaclient/client.py b/novaclient/client.py index de4a64597..e87d06efe 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -26,6 +26,7 @@ import hashlib import logging import os +import re import time import requests @@ -446,6 +447,15 @@ def _time_request(self, url, method, **kwargs): def _cs_request(self, url, method, **kwargs): if not self.management_url: self.authenticate() + if url is None: + # To get API version information, it is necessary to GET + # a nova endpoint directly without "v2/". + magic_tuple = parse.urlsplit(self.management_url) + scheme, netloc, path, query, frag = magic_tuple + path = re.sub(r'v[1-9]/[a-z0-9]+$', '', path) + url = parse.urlunsplit((scheme, netloc, path, None, None)) + else: + url = self.management_url + url # Perform the request once. If we get a 401 back then it # might be because the auth token expired, so try to @@ -455,8 +465,7 @@ def _cs_request(self, url, method, **kwargs): if self.projectid: kwargs['headers']['X-Auth-Project-Id'] = self.projectid - resp, body = self._time_request(self.management_url + url, method, - **kwargs) + resp, body = self._time_request(url, method, **kwargs) return resp, body except exceptions.Unauthorized as e: try: @@ -467,8 +476,7 @@ def _cs_request(self, url, method, **kwargs): self.keyring_saved = False self.authenticate() kwargs['headers']['X-Auth-Token'] = self.auth_token - resp, body = self._time_request(self.management_url + url, - method, **kwargs) + resp, body = self._time_request(url, method, **kwargs) return resp, body except exceptions.Unauthorized: raise e diff --git a/novaclient/tests/test_client.py b/novaclient/tests/test_client.py index bf868f821..73aede9bd 100644 --- a/novaclient/tests/test_client.py +++ b/novaclient/tests/test_client.py @@ -109,6 +109,37 @@ def test_client_reauth(self): verify=mock.ANY)] self.assertEqual(mock_request.call_args_list, expected) + @mock.patch.object(novaclient.client.HTTPClient, 'request', + return_value=(200, "{'versions':[]}")) + def _check_version_url(self, management_url, version_url, mock_request): + projectid = '25e469aa1848471b875e68cde6531bc5' + instance = novaclient.client.HTTPClient(user='user', + password='password', + projectid=projectid, + auth_url="http://www.blah.com") + instance.auth_token = 'foobar' + instance.management_url = management_url % projectid + instance.version = 'v2.0' + + # If passing None as the part of url, a client accesses the url which + # doesn't include "v2/" for getting API version info. + instance.get(None) + mock_request.assert_called_once_with(version_url, 'GET', + headers=mock.ANY) + mock_request.reset_mock() + + # Otherwise, a client accesses the url which includes "v2/". + instance.get('servers') + url = instance.management_url + 'servers' + mock_request.assert_called_once_with(url, 'GET', headers=mock.ANY) + + def test_client_version_url(self): + self._check_version_url('http://foo.com/v2/%s', 'http://foo.com/') + + def test_client_version_url_with_project_name(self): + self._check_version_url('http://foo.com/nova/v2/%s', + 'http://foo.com/nova/') + def test_get_client_class_v3(self): output = novaclient.client.get_client_class('3') self.assertEqual(output, novaclient.v3.client.Client) diff --git a/novaclient/v1_1/client.py b/novaclient/v1_1/client.py index 46034bb95..c578b2022 100644 --- a/novaclient/v1_1/client.py +++ b/novaclient/v1_1/client.py @@ -41,6 +41,7 @@ from novaclient.v1_1 import servers from novaclient.v1_1 import services from novaclient.v1_1 import usage +from novaclient.v1_1 import versions from novaclient.v1_1 import virtual_interfaces from novaclient.v1_1 import volume_snapshots from novaclient.v1_1 import volume_types @@ -107,6 +108,7 @@ def __init__(self, username=None, api_key=None, project_id=None, self.images = images.ImageManager(self) self.limits = limits.LimitsManager(self) self.servers = servers.ServerManager(self) + self.versions = versions.VersionManager(self) # extensions self.agents = agents.AgentsManager(self) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index ed43277d8..595b92d60 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -3654,3 +3654,10 @@ def do_server_group_get(cs, args): """Get a specific server group.""" server_group = cs.server_groups.get(args.id) _print_server_group_details([server_group]) + + +def do_version_list(cs, args): + """List all API versions.""" + result = cs.versions.list() + columns = ["Id", "Status", "Updated"] + utils.print_list(result, columns) diff --git a/novaclient/v1_1/versions.py b/novaclient/v1_1/versions.py new file mode 100644 index 000000000..5ab09a103 --- /dev/null +++ b/novaclient/v1_1/versions.py @@ -0,0 +1,35 @@ +# Copyright 2014 NEC Corporation. All rights reserved. +# +# 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. + +""" +version interface +""" + +from novaclient import base + + +class Version(base.Resource): + """ + Compute REST API information + """ + def __repr__(self): + return "" + + +class VersionManager(base.ManagerWithFind): + resource_class = Version + + def list(self): + """List all versions.""" + return self._list(None, "versions") From c59a0c8748ccc5f6a0cf80910c09b9328b4253ac Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Fri, 9 May 2014 13:07:40 -0700 Subject: [PATCH 0566/1705] Add support for new fields in network create Also adds missing network-delete shell command. Additionally changes the type of vlan and vlan_start to int to match the api samples. Partially-implements blueprint better-support-for-multiple-networks Change-Id: I233614431db4737284cceaf8e6b947f50dd84602 --- novaclient/tests/v1_1/fakes.py | 6 +++- novaclient/tests/v1_1/test_networks.py | 8 ++++- novaclient/tests/v1_1/test_shell.py | 20 ++++++++++-- novaclient/v1_1/networks.py | 12 ++++++-- novaclient/v1_1/shell.py | 42 +++++++++++++++++++++++++- 5 files changed, 80 insertions(+), 8 deletions(-) diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index f61a02e92..d9c1f380e 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -1710,11 +1710,15 @@ def get_os_networks(self, **kw): 'project_id': '4ffc664c198e435e9853f2538fbcd7a7', 'id': '1'}]}) + def delete_os_networks_1(self, **kw): + return (202, {}, None) + def post_os_networks(self, **kw): return (202, {}, {'network': kw}) def get_os_networks_1(self, **kw): - return (200, {}, {'network': {"label": "1", "cidr": "10.0.0.0/24"}}) + return (200, {}, {'network': {"label": "1", "cidr": "10.0.0.0/24", + "id": "1"}}) def delete_os_networks_networkdelete(self, **kw): return (202, {}, None) diff --git a/novaclient/tests/v1_1/test_networks.py b/novaclient/tests/v1_1/test_networks.py index 8a63c443a..1fbd112ce 100644 --- a/novaclient/tests/v1_1/test_networks.py +++ b/novaclient/tests/v1_1/test_networks.py @@ -59,7 +59,13 @@ def test_create_allparams(self): 'project_id': '1', 'vlan': 5, 'vlan_start': 1, - 'vpn_start': 1 + 'vpn_start': 1, + 'mtu': 1500, + 'enable_dhcp': 'T', + 'dhcp_server': '1920.2.2', + 'share_address': 'T', + 'allowed_start': '192.0.2.10', + 'allowed_end': '192.0.2.20', } f = self.cs.networks.create(**params) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index da2fed520..0ccecf751 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -1757,16 +1757,32 @@ def test_network_create_vlan(self): self.run_command('network-create --fixed-range-v4 192.168.0.0/24' ' --vlan=200 new_network') body = {'network': {'cidr': '192.168.0.0/24', 'label': 'new_network', - 'vlan': '200'}} + 'vlan': 200}} self.assert_called('POST', '/os-networks', body) def test_network_create_vlan_start(self): self.run_command('network-create --fixed-range-v4 192.168.0.0/24' ' --vlan-start=100 new_network') body = {'network': {'cidr': '192.168.0.0/24', 'label': 'new_network', - 'vlan_start': '100'}} + 'vlan_start': 100}} self.assert_called('POST', '/os-networks', body) + def test_network_create_extra_args(self): + self.run_command('network-create --fixed-range-v4 192.168.0.0/24' + ' --enable-dhcp F --dhcp-server 192.168.0.2' + ' --share-address T --allowed-start 192.168.0.10' + ' --allowed-end 192.168.0.20 --mtu 9000 new_network') + body = {'network': {'cidr': '192.168.0.0/24', 'label': 'new_network', + 'enable_dhcp': False, 'dhcp_server': '192.168.0.2', + 'share_address': True, 'mtu': 9000, + 'allowed_start': '192.168.0.10', + 'allowed_end': '192.168.0.20'}} + self.assert_called('POST', '/os-networks', body) + + def test_network_delete(self): + self.run_command('network-delete 1') + self.assert_called('DELETE', '/os-networks/1') + def test_add_fixed_ip(self): self.run_command('add-fixed-ip sample-server 1') self.assert_called('POST', '/servers/1234/action', diff --git a/novaclient/v1_1/networks.py b/novaclient/v1_1/networks.py index d4dc78b8e..a27502868 100644 --- a/novaclient/v1_1/networks.py +++ b/novaclient/v1_1/networks.py @@ -26,7 +26,7 @@ class Network(base.Resource): """ A network. """ - HUMAN_ID = False + HUMAN_ID = True NAME_ATTR = "label" def __repr__(self): @@ -89,8 +89,14 @@ def create(self, **kwargs): :param vlan: int :param vlan_start: int :param vpn_start: int - - :rtype: list of :class:`Network` + :param mtu: int + :param enable_dhcp: int + :param dhcp_server: str + :param share_address: int + :param allowed_start: str + :param allowed_end: str + + :rtype: object of :class:`Network` """ body = {"network": kwargs} return self._create('/os-networks', body, 'network') diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index ed43277d8..7309ee7be 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -794,6 +794,15 @@ def do_network_show(cs, args): utils.print_dict(network._info) +@utils.arg('network', + metavar='', + help=_("uuid or label of network")) +def do_network_delete(cs, args): + """Delete network by label or id.""" + network = utils.find_resource(cs.networks, args.network) + network.delete() + + @utils.arg('--host-only', dest='host_only', metavar='<0|1>', @@ -844,7 +853,8 @@ def _filter_network_create_options(args): valid_args = ['label', 'cidr', 'vlan_start', 'vpn_start', 'cidr_v6', 'gateway', 'gateway_v6', 'bridge', 'bridge_interface', 'multi_host', 'dns1', 'dns2', 'uuid', 'fixed_cidr', - 'project_id', 'priority', 'vlan'] + 'project_id', 'priority', 'vlan', 'mtu', 'dhcp_server', + 'allowed_start', 'allowed_end'] kwargs = {} for k, v in args.__dict__.items(): if k in valid_args and v is not None: @@ -865,15 +875,18 @@ def _filter_network_create_options(args): help=_('IPv6 subnet (ex: fe80::/64')) @utils.arg('--vlan', dest='vlan', + type=int, metavar='', help=_("vlan id to be assigned to project")) @utils.arg('--vlan-start', dest='vlan_start', + type=int, metavar='', help=_('First vlan ID to be assigned to project. Subsequent vlan' ' IDs will be assigned incrementally')) @utils.arg('--vpn', dest='vpn_start', + type=int, metavar='', help=_("vpn start")) @utils.arg('--gateway', @@ -917,6 +930,27 @@ def _filter_network_create_options(args): dest="priority", metavar="", help=_('Network interface priority')) +@utils.arg('--mtu', + dest="mtu", + type=int, + help=_('MTU for network')) +@utils.arg('--enable-dhcp', + dest="enable_dhcp", + metavar="<'T'|'F'>", + help=_('Enable dhcp')) +@utils.arg('--dhcp-server', + dest="dhcp_server", + help=_('Dhcp-server (defaults to gateway address)')) +@utils.arg('--share-address', + dest="share_address", + metavar="<'T'|'F'>", + help=_('Share address')) +@utils.arg('--allowed-start', + dest="allowed_start", + help=_('Start of allowed addresses for instances')) +@utils.arg('--allowed-end', + dest="allowed_end", + help=_('End of allowed addresses for instances')) def do_network_create(cs, args): """Create a network.""" @@ -927,6 +961,12 @@ def do_network_create(cs, args): if args.multi_host is not None: kwargs['multi_host'] = bool(args.multi_host == 'T' or strutils.bool_from_string(args.multi_host)) + if args.enable_dhcp is not None: + kwargs['enable_dhcp'] = bool(args.enable_dhcp == 'T' or + strutils.bool_from_string(args.enable_dhcp)) + if args.share_address is not None: + kwargs['share_address'] = bool(args.share_address == 'T' or + strutils.bool_from_string(args.share_address)) cs.networks.create(**kwargs) From 8ec2a29d3081c4ba7058948e3187a8399b16672b Mon Sep 17 00:00:00 2001 From: Andreas Jaeger Date: Wed, 16 Jul 2014 11:55:19 +0200 Subject: [PATCH 0567/1705] Edits on help strings Improve help strings for consistency. Fix wording, typos, capitalization and add "." as appropriate. Change-Id: I376f6d0269aee8dd0e67ffab6386eac2139844ab --- novaclient/v1_1/shell.py | 20 ++++++++++---------- novaclient/v3/shell.py | 14 +++++++------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index ed43277d8..1e931ecc5 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -866,12 +866,12 @@ def _filter_network_create_options(args): @utils.arg('--vlan', dest='vlan', metavar='', - help=_("vlan id to be assigned to project")) + help=_("The vlan ID to be assigned to the project.")) @utils.arg('--vlan-start', dest='vlan_start', metavar='', - help=_('First vlan ID to be assigned to project. Subsequent vlan' - ' IDs will be assigned incrementally')) + help=_('First vlan ID to be assigned to the project. Subsequent vlan ' + 'IDs will be assigned incrementally.')) @utils.arg('--vpn', dest='vpn_start', metavar='', @@ -881,15 +881,15 @@ def _filter_network_create_options(args): help=_('gateway')) @utils.arg('--gateway-v6', dest="gateway_v6", - help=_('ipv6 gateway')) + help=_('IPv6 gateway')) @utils.arg('--bridge', dest="bridge", metavar='', - help=_('VIFs on this network are connected to this bridge')) + help=_('VIFs on this network are connected to this bridge.')) @utils.arg('--bridge-interface', dest="bridge_interface", metavar='', - help=_('the bridge is connected to this interface')) + help=_('The bridge is connected to this interface.')) @utils.arg('--multi-host', dest="multi_host", metavar="<'T'|'F'>", @@ -908,11 +908,11 @@ def _filter_network_create_options(args): @utils.arg('--fixed-cidr', dest="fixed_cidr", metavar='', - help=_('IPv4 subnet for fixed IPS (ex: 10.20.0.0/16)')) + help=_('IPv4 subnet for fixed IPs (ex: 10.20.0.0/16)')) @utils.arg('--project-id', dest="project_id", metavar="", - help=_('Project id')) + help=_('Project ID')) @utils.arg('--priority', dest="priority", metavar="", @@ -922,7 +922,7 @@ def do_network_create(cs, args): if not (args.cidr or args.cidr_v6): raise exceptions.CommandError( - _("Must specify eith fixed_range_v4 or fixed_range_v6")) + _("Must specify either fixed_range_v4 or fixed_range_v6")) kwargs = _filter_network_create_options(args) if args.multi_host is not None: kwargs['multi_host'] = bool(args.multi_host == 'T' or @@ -934,7 +934,7 @@ def do_network_create(cs, args): @utils.arg('--limit', dest="limit", metavar="", - help=_('number of images to return per request')) + help=_('Number of images to return per request.')) def do_image_list(cs, _args): """Print a list of available images to boot from.""" limit = _args.limit diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 9f4b13bde..02cb7f811 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -722,15 +722,15 @@ def _filter_network_create_options(args): help='gateway') @utils.arg('--gateway-v6', dest="gateway_v6", - help='ipv6 gateway') + help='IPv6 gateway') @utils.arg('--bridge', dest="bridge", metavar='', - help='VIFs on this network are connected to this bridge') + help='VIFs on this network are connected to this bridge.') @utils.arg('--bridge-interface', dest="bridge_interface", metavar='', - help='the bridge is connected to this interface') + help='The bridge is connected to this interface.') @utils.arg('--multi-host', dest="multi_host", metavar="<'T'|'F'>", @@ -749,11 +749,11 @@ def _filter_network_create_options(args): @utils.arg('--fixed-cidr', dest="fixed_cidr", metavar='', - help='IPv4 subnet for fixed IPS (ex: 10.20.0.0/16)') + help='IPv4 subnet for fixed IPs (ex: 10.20.0.0/16)') @utils.arg('--project-id', dest="project_id", metavar="", - help='Project id') + help='Project ID') @utils.arg('--priority', dest="priority", metavar="", @@ -763,7 +763,7 @@ def do_network_create(cs, args): if not (args.cidr or args.cidr_v6): raise exceptions.CommandError( - "Must specify eith fixed_range_v4 or fixed_range_v6") + "Must specify either fixed_range_v4 or fixed_range_v6") kwargs = _filter_network_create_options(args) if args.multi_host is not None: kwargs['multi_host'] = bool(args.multi_host == 'T' or @@ -775,7 +775,7 @@ def do_network_create(cs, args): @utils.arg('--limit', dest="limit", metavar="", - help='number of images to return per request') + help='Number of images to return per request.') @utils.service_type('image') def do_image_list(cs, _args): """Print a list of available images to boot from.""" From d51b546774e9fb452c842134e5decca6b74d4653 Mon Sep 17 00:00:00 2001 From: Christian Berendt Date: Thu, 17 Jul 2014 15:30:34 +0200 Subject: [PATCH 0568/1705] Enabled hacking checks H305 and H307 * H305 imports not grouped correctly * H307 like imports should be grouped together Change-Id: I23fdad285508707a2c1d7c832ff8b1a6a10e8f88 --- novaclient/tests/test_auth_plugins.py | 1 + novaclient/tests/test_client.py | 7 +++---- novaclient/tests/test_shell.py | 7 +++---- novaclient/tests/v1_1/contrib/test_baremetal.py | 3 +-- novaclient/tests/v1_1/contrib/test_instance_actions.py | 4 ++-- novaclient/tests/v1_1/contrib/test_list_extensions.py | 3 +-- novaclient/tests/v1_1/contrib/test_tenant_networks.py | 3 +-- novaclient/utils.py | 2 +- tox.ini | 4 ++-- 9 files changed, 15 insertions(+), 19 deletions(-) diff --git a/novaclient/tests/test_auth_plugins.py b/novaclient/tests/test_auth_plugins.py index 34e83baca..9052141e1 100644 --- a/novaclient/tests/test_auth_plugins.py +++ b/novaclient/tests/test_auth_plugins.py @@ -14,6 +14,7 @@ # under the License. import argparse + import mock import pkg_resources import requests diff --git a/novaclient/tests/test_client.py b/novaclient/tests/test_client.py index bf868f821..17cf7c2ee 100644 --- a/novaclient/tests/test_client.py +++ b/novaclient/tests/test_client.py @@ -14,11 +14,12 @@ # under the License. +import json import logging -import mock -import requests import fixtures +import mock +import requests import novaclient.client import novaclient.extension @@ -27,8 +28,6 @@ import novaclient.v1_1.client import novaclient.v3.client -import json - class ClientConnectionPoolTest(utils.TestCase): diff --git a/novaclient/tests/test_shell.py b/novaclient/tests/test_shell.py index fdcc78e68..daee60abe 100644 --- a/novaclient/tests/test_shell.py +++ b/novaclient/tests/test_shell.py @@ -11,15 +11,14 @@ # License for the specific language governing permissions and limitations # under the License. -import prettytable +from distutils.version import StrictVersion import re -import six import sys -from distutils.version import StrictVersion - import fixtures import mock +import prettytable +import six from testtools import matchers import novaclient.client diff --git a/novaclient/tests/v1_1/contrib/test_baremetal.py b/novaclient/tests/v1_1/contrib/test_baremetal.py index 6e2399f24..6a88d580c 100644 --- a/novaclient/tests/v1_1/contrib/test_baremetal.py +++ b/novaclient/tests/v1_1/contrib/test_baremetal.py @@ -15,10 +15,9 @@ from novaclient import extension -from novaclient.v1_1.contrib import baremetal - from novaclient.tests import utils from novaclient.tests.v1_1.contrib import fakes +from novaclient.v1_1.contrib import baremetal extensions = [ diff --git a/novaclient/tests/v1_1/contrib/test_instance_actions.py b/novaclient/tests/v1_1/contrib/test_instance_actions.py index cdfa14985..baaa1d040 100644 --- a/novaclient/tests/v1_1/contrib/test_instance_actions.py +++ b/novaclient/tests/v1_1/contrib/test_instance_actions.py @@ -12,11 +12,11 @@ # 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 novaclient import extension -from novaclient.v1_1.contrib import instance_action +from novaclient import extension from novaclient.tests import utils from novaclient.tests.v1_1.contrib import fakes +from novaclient.v1_1.contrib import instance_action extensions = [ diff --git a/novaclient/tests/v1_1/contrib/test_list_extensions.py b/novaclient/tests/v1_1/contrib/test_list_extensions.py index 2d4426fc0..77b6a3420 100644 --- a/novaclient/tests/v1_1/contrib/test_list_extensions.py +++ b/novaclient/tests/v1_1/contrib/test_list_extensions.py @@ -12,10 +12,9 @@ # under the License. from novaclient import extension -from novaclient.v1_1.contrib import list_extensions - from novaclient.tests import utils from novaclient.tests.v1_1 import fakes +from novaclient.v1_1.contrib import list_extensions extensions = [ diff --git a/novaclient/tests/v1_1/contrib/test_tenant_networks.py b/novaclient/tests/v1_1/contrib/test_tenant_networks.py index f2a077005..1289928b8 100644 --- a/novaclient/tests/v1_1/contrib/test_tenant_networks.py +++ b/novaclient/tests/v1_1/contrib/test_tenant_networks.py @@ -14,10 +14,9 @@ # under the License. from novaclient import extension -from novaclient.v1_1.contrib import tenant_networks - from novaclient.tests import utils from novaclient.tests.v1_1.contrib import fakes +from novaclient.v1_1.contrib import tenant_networks extensions = [ diff --git a/novaclient/utils.py b/novaclient/utils.py index bedd74347..8600cab05 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -12,12 +12,12 @@ # under the License. import json -import pkg_resources import re import sys import textwrap import uuid +import pkg_resources import prettytable import six diff --git a/tox.ini b/tox.ini index b9f8c9f08..d3a51ea9e 100644 --- a/tox.ini +++ b/tox.ini @@ -27,7 +27,7 @@ downloadcache = ~/cache/pip [flake8] # TODO fix following rules from hacking 0.9 -# E131,E265,H233,H305,H307,H402,H405,H904 -ignore = E12,E131,E265,F811,F821,H233,H302,H305,H307,H402,H404,H405,H904 +# E131,E265,H233,H402,H405,H904 +ignore = E12,E131,E265,F811,F821,H233,H302,H402,H404,H405,H904 show-source = True exclude=.venv,.git,.tox,dist,*openstack/common*,*lib/python*,*egg,build From 60d1283968643064351f182483b0df0ac93f6640 Mon Sep 17 00:00:00 2001 From: Qin Zhao Date: Tue, 15 Jul 2014 18:25:57 +0800 Subject: [PATCH 0569/1705] Don't log sensitive auth data This code change redacts the password in keystone request, and also redact the token text in keystone response. The code still makes REST call by itelf, instead of calling keystone client. Closes-Bug: 1327019 Change-Id: Ib9c0610c1ef351a127364478721cf961c2a30125 --- novaclient/client.py | 71 +++++++++++++++++++++++++-------- novaclient/tests/test_client.py | 32 +++++++++++++++ 2 files changed, 87 insertions(+), 16 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index de4a64597..559c07070 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -20,6 +20,7 @@ OpenStack Client interface. Handles the REST calls and responses. """ +import copy import errno import functools import glob @@ -44,8 +45,6 @@ from novaclient import service_catalog from novaclient import utils -SENSITIVE_HEADERS = ('X-Auth-Token',) - class _ClientConnectionPool(object): @@ -318,15 +317,41 @@ def get_timings(self): def reset_timings(self): self.times = [] - def safe_header(self, name, value): - if name in SENSITIVE_HEADERS: - # because in python3 byte string handling is ... ug - v = value.encode('utf-8') - h = hashlib.sha1(v) - d = h.hexdigest() - return name, "{SHA1}%s" % d - else: - return name, value + def _redact(self, target, path, text=None): + """Replace the value of a key in `target`. + + The key can be at the top level by specifying a list with a single + key as the path. Nested dictionaries are also supported by passing a + list of keys to be navigated to find the one that should be replaced. + In this case the last one is the one that will be replaced. + + :param dict target: the dictionary that may have a key to be redacted; + modified in place + :param list path: a list representing the nested structure in `target` + that should be redacted; modified in place + :param string text: optional text to use as a replacement for the + redacted key. if text is not specified, the + default text will be sha1 hash of the value being + redacted + """ + + key = path.pop() + + # move to the most nested dict + for p in path: + try: + target = target[p] + except KeyError: + return + + if key in target: + if text: + target[key] = text + else: + # because in python3 byte string handling is ... ug + value = target[key].encode('utf-8') + sha1sum = hashlib.sha1(value) + target[key] = "{SHA1}%s" % sha1sum.hexdigest() def http_log_req(self, method, url, kwargs): if not self.http_log_debug: @@ -340,24 +365,38 @@ def http_log_req(self, method, url, kwargs): string_parts.append(" '%s'" % url) string_parts.append(' -X %s' % method) + headers = copy.deepcopy(kwargs['headers']) + self._redact(headers, ['X-Auth-Token']) # because dict ordering changes from 2 to 3 - keys = sorted(kwargs['headers'].keys()) + keys = sorted(headers.keys()) for name in keys: - value = kwargs['headers'][name] - header = ' -H "%s: %s"' % self.safe_header(name, value) + value = headers[name] + header = ' -H "%s: %s"' % (name, value) string_parts.append(header) if 'data' in kwargs: - string_parts.append(" -d '%s'" % (kwargs['data'])) + data = json.loads(kwargs['data']) + self._redact(data, ['auth', 'passwordCredentials', 'password']) + string_parts.append(" -d '%s'" % json.dumps(data)) self._logger.debug("REQ: %s" % "".join(string_parts)) def http_log_resp(self, resp): if not self.http_log_debug: return + + if resp.text and resp.status_code != 400: + try: + body = json.loads(resp.text) + self._redact(body, ['access', 'token', 'id']) + except ValueError: + body = None + else: + body = None + self._logger.debug("RESP: [%(status)s] %(headers)s\nRESP BODY: " "%(text)s\n", {'status': resp.status_code, 'headers': resp.headers, - 'text': resp.text}) + 'text': json.dumps(body)}) def open_session(self): if not self._connection_pool: diff --git a/novaclient/tests/test_client.py b/novaclient/tests/test_client.py index bf868f821..e661673a0 100644 --- a/novaclient/tests/test_client.py +++ b/novaclient/tests/test_client.py @@ -343,6 +343,9 @@ def test_log_req(self): {'X-Foo': 'bar', 'X-Auth-Token': 'totally_bogus'} }) + cs.http_log_req('GET', '/foo', {'headers': {}, + 'data': '{"auth": {"passwordCredentials": ' + '{"password": "zhaoqin"}}}'}) output = self.logger.output.split('\n') @@ -356,3 +359,32 @@ def test_log_req(self): '"X-Auth-Token: {SHA1}b42162b6ffdbd7c3c37b7c95b7ba9f51dda0236d"' ' -H "X-Foo: bar"', output) + self.assertIn( + "REQ: curl -i '/foo' -X GET -d " + '\'{"auth": {"passwordCredentials": {"password":' + ' "{SHA1}4fc49c6a671ce889078ff6b250f7066cf6d2ada2"}}}\'', + output) + + def test_log_resp(self): + self.logger = self.useFixture( + fixtures.FakeLogger( + format="%(message)s", + level=logging.DEBUG, + nuke_handlers=True + ) + ) + + cs = novaclient.client.HTTPClient("user", None, "", + connection_pool=True) + cs.http_log_debug = True + text = ('{"access": {"token": {"id": "zhaoqin"}}}') + resp = utils.TestResponse({'status_code': 200, 'headers': {}, + 'text': text}) + + cs.http_log_resp(resp) + output = self.logger.output.split('\n') + + self.assertIn('RESP: [200] {}', output) + self.assertIn('RESP BODY: {"access": {"token": {"id":' + ' "{SHA1}4fc49c6a671ce889078ff6b250f7066cf6d2ada2"}}}', + output) From 4d2586716ada61b8087d2f95dfe2595f9c93cdc6 Mon Sep 17 00:00:00 2001 From: Joe Gordon Date: Mon, 16 Jun 2014 00:03:06 -0700 Subject: [PATCH 0570/1705] Enable H233 H233 (and H402) were temporarily disabled as part of the migration to hacking 0.9. This patch fixes all H233 issues and re-enables gating on them. H402 has been removed in hacking (I9426644fa708e9d5563abe04dc4ad656dbeb3656) so leave this one off as its going away anyway. Change-Id: Iae8f132c66c5e345dc2e9c2464f67609887c1475 --- novaclient/v1_1/shell.py | 2 +- novaclient/v3/shell.py | 2 +- tox.ini | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index a08f92aad..ad1a23e16 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -507,7 +507,7 @@ def print_progress(progress): sys.stdout.flush() if not silent: - print + print() while True: obj = poll_fn(obj_id) diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index aa57ad8c9..44665a928 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -353,7 +353,7 @@ def print_progress(progress): sys.stdout.flush() if not silent: - print + print() while True: obj = poll_fn(obj_id) diff --git a/tox.ini b/tox.ini index d3a51ea9e..8b2408de1 100644 --- a/tox.ini +++ b/tox.ini @@ -27,7 +27,7 @@ downloadcache = ~/cache/pip [flake8] # TODO fix following rules from hacking 0.9 -# E131,E265,H233,H402,H405,H904 -ignore = E12,E131,E265,F811,F821,H233,H302,H402,H404,H405,H904 +# E131,E265,H402,H405,H904 +ignore = E12,E131,E265,F811,F821,H302,H402,H404,H405,H904 show-source = True exclude=.venv,.git,.tox,dist,*openstack/common*,*lib/python*,*egg,build From 9ce03a98eb78652fd3480cb0d8323520fd78064c Mon Sep 17 00:00:00 2001 From: melanie witt Date: Mon, 28 Jul 2014 18:54:01 +0000 Subject: [PATCH 0571/1705] fix host resource repr to use 'host' attribute This change fixes an AttributeError raised when attempting to represent a host resource as a string. Change-Id: I33369b20ed7f5219c3b507107b8f3a37b46eacee Closes-Bug: #1327005 --- novaclient/tests/v1_1/test_hosts.py | 4 ++++ novaclient/v1_1/hosts.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/novaclient/tests/v1_1/test_hosts.py b/novaclient/tests/v1_1/test_hosts.py index d8291c6ae..8fcdf5a76 100644 --- a/novaclient/tests/v1_1/test_hosts.py +++ b/novaclient/tests/v1_1/test_hosts.py @@ -78,3 +78,7 @@ def test_host_shutdown(self): host.shutdown() self.assert_called( 'GET', '/os-hosts/sample_host/shutdown') + + def test_hosts_repr(self): + hs = self.cs.hosts.get('host') + self.assertEqual('', repr(hs[0])) diff --git a/novaclient/v1_1/hosts.py b/novaclient/v1_1/hosts.py index 0b641d44c..af1756ad2 100644 --- a/novaclient/v1_1/hosts.py +++ b/novaclient/v1_1/hosts.py @@ -21,7 +21,7 @@ class Host(base.Resource): def __repr__(self): - return "" % self.host_name + return "" % self.host def _add_details(self, info): dico = 'resource' in info and info['resource'] or info From ff4af92b6d138580fb8d4008869a666df2e7b933 Mon Sep 17 00:00:00 2001 From: Jaroslav Henner Date: Mon, 7 Jul 2014 11:45:40 +0200 Subject: [PATCH 0572/1705] Allow selecting the network for doing the ssh with Previously, nova ssh was searching for network types: public and private, which seems to be incorrect (fixed and floating seems to be correct), causing that this command has probably never worked. This commit fixes the above and adds an option for selecting the network to use, which is helpful when there are more networks for the VM. Change-Id: I01ea6cee725c0feaacab60975c3792b0ac1305e9 Closes-Bug: #1227694 Closes-Bug: #1343991 --- novaclient/tests/v1_1/test_shell.py | 40 +++++++++++-- novaclient/v1_1/shell.py | 89 ++++++++++++++++++++--------- novaclient/v3/shell.py | 80 ++++++++++++++++++-------- 3 files changed, 154 insertions(+), 55 deletions(-) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index d6f790df7..083d5f5fb 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -2070,10 +2070,15 @@ def test_migration_list_with_filters(self): def test_ssh(self, mock_system, mock_find_server): class FakeResources(object): addresses = { - "private": [{'version': 4, 'addr': "1.1.1.1"}, - {'version': 6, 'addr': "2607:f0d0:1002::4"}], - "public": [{'version': 4, 'addr': "2.2.2.2"}, - {'version': 6, 'addr': "7612:a1b2:2004::6"}] + "skynet": [ + {'version': 4, 'addr': "1.1.1.1", + "OS-EXT-IPS:type": 'fixed'}, + {'version': 4, 'addr': "2.2.2.2", + "OS-EXT-IPS:type": 'floating'}, + {'version': 6, 'addr': "2607:f0d0:1002::4", + "OS-EXT-IPS:type": 'fixed'}, + {'version': 6, 'addr': "7612:a1b2:2004::6"} + ] } mock_find_server.return_value = FakeResources() @@ -2108,6 +2113,33 @@ class FakeResources(object): mock_system.assert_called_with("ssh -6 -p22 " "root@2607:f0d0:1002::4 -1") + @mock.patch('novaclient.v1_1.shell._find_server') + @mock.patch('os.system') + def test_ssh_multinet(self, mock_system, mock_find_server): + class FakeResources(object): + addresses = { + "skynet": [ + {'version': 4, 'addr': "1.1.1.1", + "OS-EXT-IPS:type": 'fixed'}, + {'version': 4, 'addr': "2.2.2.2"}, + {'version': 6, 'addr': "2607:f0d0:1002::4", + "OS-EXT-IPS:type": 'fixed'} + ], + "other": [ + {'version': 4, 'addr': "2.3.4.5"}, + {'version': 6, 'addr': "7612:a1b2:2004::6"} + ] + } + mock_find_server.return_value = FakeResources() + + self.run_command("ssh --network other server") + mock_system.assert_called_with("ssh -4 -p22 root@2.3.4.5 ") + self.run_command("ssh --ipv6 --network other server") + mock_system.assert_called_with("ssh -6 -p22 root@7612:a1b2:2004::6 ") + self.assertRaises(exceptions.ResourceNotFound, + self.run_command, + "ssh --ipv6 --network nonexistent server") + def test_keypair_add(self): self.run_command('keypair-add test') self.assert_called('POST', '/os-keypairs', diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 93ff2b232..3ef23f7f5 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -23,6 +23,7 @@ import datetime import getpass import locale +import logging import os import sys import time @@ -40,6 +41,9 @@ from novaclient.v1_1 import servers +logger = logging.getLogger(__name__) + + CLIENT_BDM2_KEYS = { 'id': 'uuid', 'source': 'source_type', @@ -3207,9 +3211,16 @@ def do_credentials(cs, _args): dest='private', action='store_true', default=False, - help=_('Optional flag to indicate whether to only use private address ' - 'attached to an instance. (Default=False). If no public address is ' - 'found try private address')) + help=argparse.SUPPRESS) +@utils.arg('--address-type', + dest='address_type', + action='store', + type=str, + default='floating', + help=_('Optional flag to indicate which IP type to use. Possible values ' + 'includes fixed and floating (the Default).')) +@utils.arg('--network', metavar='', + help=_('Network to use for the ssh.'), default=None) @utils.arg('--ipv6', dest='ipv6', action='store_true', @@ -3234,35 +3245,57 @@ def do_ssh(cs, args): args.server = server addresses = _find_server(cs, args.server).addresses - address_type = "private" if args.private else "public" + address_type = "fixed" if args.private else args.address_type version = 6 if args.ipv6 else 4 - - if (address_type == "public" and address_type not in addresses and - "private" in addresses): - address_type = "private" - - if address_type not in addresses: - print(_("ERROR: No %(addr_type)s addresses found for '%(server)s'.") % - {'addr_type': address_type, 'server': args.server}) - return - - ip_address = None - for address in addresses[address_type]: - if address['version'] == version: - ip_address = address['addr'] - break + pretty_version = 'IPv%d' % version + + # Select the network to use. + if args.network: + network_addresses = addresses.get(args.network) + if not network_addresses: + msg = _("Server '%(server)s' is not attached to network " + "'%(network)s'") + raise exceptions.ResourceNotFound( + msg % {'server': args.server, 'network': args.network}) + else: + if len(addresses) > 1: + msg = _("Server '%(server)s' is attached to more than one network." + " Please pick the network to use.") + raise exceptions.CommandError(msg % {'server': args.server}) + elif not addresses: + msg = _("Server '%(server)s' is not attached to any network.") + raise exceptions.CommandError(msg % {'server': args.server}) + else: + network_addresses = list(six.itervalues(addresses))[0] + + # Select the address in the selected network. + # If the extension is not present, we assume the address to be floating. + match = lambda addr: all(( + addr.get('version') == version, + addr.get('OS-EXT-IPS:type', 'floating') == address_type)) + matching_addresses = [address.get('addr') for address in network_addresses + if match(address)] + if not any(matching_addresses): + msg = _("No address that would match network '%(network)s'" + " and type '%(address_type)s' of version %(pretty_version)s " + "has been found for server '%(server)s'.") + raise exceptions.ResourceNotFound(msg % { + 'network': args.network, 'address_type': address_type, + 'pretty_version': pretty_version, 'server': args.server}) + elif len(matching_addresses) > 1: + msg = _("More than one %(pretty_version)s %(address_type)s address" + "found.") + raise exceptions.CommandError(msg % {'pretty_version': pretty_version, + 'address_type': address_type}) + else: + ip_address = matching_addresses[0] identity = '-i %s' % args.identity if len(args.identity) else '' - if ip_address: - os.system("ssh -%d -p%d %s %s@%s %s" % (version, args.port, identity, - args.login, ip_address, - args.extra)) - else: - pretty_version = "IPv%d" % version - print(_("ERROR: No %(addr_type)s %(pretty_version)s address found.") % - {'addr_type': address_type, 'pretty_version': pretty_version}) - return + cmd = "ssh -%d -p%d %s %s@%s %s" % (version, args.port, identity, + args.login, ip_address, args.extra) + logger.debug("Executing cmd '%s'", cmd) + os.system(cmd) _quota_resources = ['instances', 'cores', 'ram', diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 4b6da7234..52e365052 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -2664,8 +2664,16 @@ def do_extension_list(cs, _args): dest='private', action='store_true', default=False, - help='Optional flag to indicate whether to use private address ' - 'attached to a server. (Default=False)') + help=argparse.SUPPRESS) +@utils.arg('--address-type', + dest='address_type', + action='store', + type=str, + default='floating', + help='Optional flag to indicate which IP type to use. Possible values ' + 'includes fixed and floating (the Default).') +@utils.arg('--network', metavar='', + help='Network to use for the ssh.', default=None) @utils.arg('--ipv6', dest='ipv6', action='store_true', @@ -2689,31 +2697,57 @@ def do_ssh(cs, args): args.server = server addresses = _find_server(cs, args.server).addresses - address_type = "private" if args.private else "public" + address_type = "fixed" if args.private else args.address_type version = 6 if args.ipv6 else 4 - - if address_type not in addresses: - print("ERROR: No %s addresses found for '%s'." % (address_type, - args.server)) - return - - ip_address = None - for address in addresses[address_type]: - if address['version'] == version: - ip_address = address['addr'] - break + pretty_version = 'IPv%d' % version + + # Select the network to use. + if args.network: + network_addresses = addresses.get(args.network) + if not network_addresses: + msg = _("Server '%(server)s' is not attached to network " + "'%(network)s'") + raise exceptions.ResourceNotFound( + msg % {'server': args.server, 'network': args.network}) + else: + if len(addresses) > 1: + msg = _("Server '%(server)s' is attached to more than one network." + " Please pick the network to use.") + raise exceptions.CommandError(msg % {'server': args.server}) + elif not addresses: + msg = _("Server '%(server)s' is not attached to any network.") + raise exceptions.CommandError(msg % {'server': args.server}) + else: + network_addresses = list(six.itervalues(addresses))[0] + + # Select the address in the selected network. + # If the extension is not present, we assume the address to be floating. + match = lambda addr: all(( + addr.get('version') == version, + addr.get('OS-EXT-IPS:type', 'floating') == address_type)) + matching_addresses = [address.get('addr') for address in network_addresses + if match(address)] + if not any(matching_addresses): + msg = _("No address that would match network '%(network)s'" + " and type '%(address_type)s' of version %(pretty_version)s " + "has been found for server '%(server)s'.") + raise exceptions.ResourceNotFound(msg % { + 'network': args.network, 'address_type': address_type, + 'pretty_version': pretty_version, 'server': args.server}) + elif len(matching_addresses) > 1: + msg = _("More than one %(pretty_version)s %(address_type)s address" + "found.") + raise exceptions.CommandError(msg % {'pretty_version': pretty_version, + 'address_type': address_type}) + else: + ip_address = matching_addresses[0] identity = '-i %s' % args.identity if len(args.identity) else '' - if ip_address: - os.system("ssh -%d -p%d %s %s@%s %s" % (version, args.port, identity, - args.login, ip_address, - args.extra)) - else: - pretty_version = "IPv%d" % version - print("ERROR: No %s %s address found." % (address_type, - pretty_version)) - return + cmd = "ssh -%d -p%d %s %s@%s %s" % (version, args.port, identity, + args.login, ip_address, args.extra) + logger.debug("Executing cmd '%s'", cmd) + os.system(cmd) _quota_resources = ['instances', 'cores', 'ram', From ad9a14ae2b1d1c860053db60442d4224dec3b30a Mon Sep 17 00:00:00 2001 From: Ken'ichi Ohmichi Date: Tue, 29 Jul 2014 20:15:08 +0000 Subject: [PATCH 0573/1705] Fix rxtx_factor name for creating a flavor On v3 API, rxtx_factor parameters has been renamed to 'os-flavor-rxtx:rxtx_factor' for consistent interfaces. This patch applies it to 'nova flavor-create' command. Closes-Bug: #1350022 Change-Id: I49e24f0220f87616f346f36973312b2f29333841 --- novaclient/tests/v3/test_flavors.py | 2 +- novaclient/v3/flavors.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/novaclient/tests/v3/test_flavors.py b/novaclient/tests/v3/test_flavors.py index cf3e86513..4dadc0ab5 100644 --- a/novaclient/tests/v3/test_flavors.py +++ b/novaclient/tests/v3/test_flavors.py @@ -38,7 +38,7 @@ def _create_body(self, name, ram, vcpus, disk, ephemeral, id, swap, "ephemeral": ephemeral, "id": id, "swap": swap, - "rxtx_factor": rxtx_factor, + "os-flavor-rxtx:rxtx_factor": rxtx_factor, "flavor-access:is_public": is_public, } } diff --git a/novaclient/v3/flavors.py b/novaclient/v3/flavors.py index 60553b2f5..e65f2f1f4 100644 --- a/novaclient/v3/flavors.py +++ b/novaclient/v3/flavors.py @@ -97,7 +97,7 @@ def _build_body(self, name, ram, vcpus, disk, id, swap, "id": id, "swap": swap, "ephemeral": ephemeral, - "rxtx_factor": rxtx_factor, + "os-flavor-rxtx:rxtx_factor": rxtx_factor, "flavor-access:is_public": is_public, } } From b6afd59e614941ccae6e52856aebf3dc9c27fe85 Mon Sep 17 00:00:00 2001 From: Matt Fischer Date: Thu, 5 Jun 2014 22:41:34 -0600 Subject: [PATCH 0574/1705] Add support for security-group-default-rules Add API & CLI support for security-group-default-rules. Security group default rules allows users to create, delete, list, and get rules that will be pre-populated into the Security Group "default" that is populated in all projects on creation. This feature was intoduced in Grizzly. This commit also includes a small typo fix in the security_group_rules fake. Change-Id: I6722b659934bde34bd206103f92dbd2cc42b1537 Closes-Bug: #1326941 --- novaclient/tests/v1_1/fakes.py | 30 ++++++- novaclient/v1_1/client.py | 3 + .../v1_1/security_group_default_rules.py | 81 +++++++++++++++++++ novaclient/v1_1/shell.py | 57 ++++++++++++- 4 files changed, 167 insertions(+), 4 deletions(-) create mode 100644 novaclient/v1_1/security_group_default_rules.py diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index d9c1f380e..cdac68009 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -1311,7 +1311,7 @@ def put_os_security_groups_1(self, body, **kw): def get_os_security_group_rules(self, **kw): return (200, {}, {"security_group_rules": [ {'id': 1, 'parent_group_id': 1, 'group_id': 2, - 'ip_protocol': 'TCP', 'from_port': '22', 'to_port': 22, + 'ip_protocol': 'TCP', 'from_port': 22, 'to_port': 22, 'cidr': '10.0.0.0/8'} ]}) @@ -1334,6 +1334,34 @@ def post_os_security_group_rules(self, body, **kw): self.get_os_security_group_rules()[2]['security_group_rules'][0]} return (202, {}, r) + # + # Security Group Default Rules + # + def get_os_security_group_default_rules(self, **kw): + return (200, {}, {"security_group_default_rules": [ + {'id': 1, 'ip_protocol': 'TCP', 'from_port': 22, + 'to_port': 22, 'cidr': '10.0.0.0/8'} + ]}) + + def delete_os_security_group_default_rules_1(self, **kw): + return (202, {}, None) + + def delete_os_security_group_default_rules_11(self, **kw): + return (202, {}, None) + + def delete_os_security_group_default_rules_12(self, **kw): + return (202, {}, None) + + def post_os_security_group_default_rules(self, body, **kw): + assert list(body) == ['security_group_default_rule'] + fakes.assert_has_keys(body['security_group_default_rule'], + optional=['ip_protocol', 'from_port', + 'to_port', 'cidr']) + rules = self.get_os_security_group_default_rules() + r = {'security_group_default_rule': + rules[2]['security_group_default_rules'][0]} + return (202, {}, r) + # # Tenant Usage # diff --git a/novaclient/v1_1/client.py b/novaclient/v1_1/client.py index 7723adcf8..2e79e5bb1 100644 --- a/novaclient/v1_1/client.py +++ b/novaclient/v1_1/client.py @@ -35,6 +35,7 @@ from novaclient.v1_1 import networks from novaclient.v1_1 import quota_classes from novaclient.v1_1 import quotas +from novaclient.v1_1 import security_group_default_rules from novaclient.v1_1 import security_group_rules from novaclient.v1_1 import security_groups from novaclient.v1_1 import server_groups @@ -142,6 +143,8 @@ def __init__(self, username=None, api_key=None, project_id=None, self.security_groups = security_groups.SecurityGroupManager(self) self.security_group_rules = \ security_group_rules.SecurityGroupRuleManager(self) + self.security_group_default_rules = \ + security_group_default_rules.SecurityGroupDefaultRuleManager(self) self.usage = usage.UsageManager(self) self.virtual_interfaces = \ virtual_interfaces.VirtualInterfaceManager(self) diff --git a/novaclient/v1_1/security_group_default_rules.py b/novaclient/v1_1/security_group_default_rules.py new file mode 100644 index 000000000..5208cb061 --- /dev/null +++ b/novaclient/v1_1/security_group_default_rules.py @@ -0,0 +1,81 @@ +# 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. + +""" +Security group default rules interface. +""" + +from novaclient import base +from novaclient import exceptions +from novaclient.openstack.common.gettextutils import _ + + +class SecurityGroupDefaultRule(base.Resource): + def __str__(self): + return str(self.id) + + def delete(self): + self.manager.delete(self) + + +class SecurityGroupDefaultRuleManager(base.Manager): + resource_class = SecurityGroupDefaultRule + + def create(self, ip_protocol=None, from_port=None, to_port=None, + cidr=None): + """ + Create a security group default rule + + :param ip_protocol: IP protocol, one of 'tcp', 'udp' or 'icmp' + :param from_port: Source port + :param to_port: Destination port + :param cidr: Destination IP address(es) in CIDR notation + """ + + try: + from_port = int(from_port) + except (TypeError, ValueError): + raise exceptions.CommandError(_("From port must be an integer.")) + try: + to_port = int(to_port) + except (TypeError, ValueError): + raise exceptions.CommandError(_("To port must be an integer.")) + if ip_protocol.upper() not in ['TCP', 'UDP', 'ICMP']: + raise exceptions.CommandError(_("Ip protocol must be 'tcp', 'udp'" + ", or 'icmp'.")) + + body = {"security_group_default_rule": { + "ip_protocol": ip_protocol, + "from_port": from_port, + "to_port": to_port, + "cidr": cidr}} + + return self._create('/os-security-group-default-rules', body, + 'security_group_default_rule') + + def delete(self, rule): + """ + Delete a security group default rule + + :param rule: The security group default rule to delete (ID or Class) + """ + self._delete('/os-security-group-default-rules/%s' % base.getid(rule)) + + def list(self): + """ + Get a list of all security group default rules + + :rtype: list of :class:`SecurityGroupDefaultRule` + """ + + return self._list('/os-security-group-default-rules', + 'security_group_default_rules') diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 93ff2b232..7001c039d 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2271,7 +2271,7 @@ def do_dns_create_public_domain(cs, args): args.project) -def _print_secgroup_rules(rules): +def _print_secgroup_rules(rules, show_source_group=True): class FormattedRule: def __init__(self, obj): items = (obj if isinstance(obj, dict) else obj._info).items() @@ -2287,8 +2287,10 @@ def __init__(self, obj): setattr(self, k, v) rules = [FormattedRule(rule) for rule in rules] - utils.print_list(rules, ['IP Protocol', 'From Port', 'To Port', - 'IP Range', 'Source Group']) + headers = ['IP Protocol', 'From Port', 'To Port', 'IP Range'] + if show_source_group: + headers.append('Source Group') + utils.print_list(rules, headers) def _print_secgroups(secgroups): @@ -3674,6 +3676,55 @@ def do_server_group_list(cs, args): _print_server_group_details(server_groups) +def do_secgroup_list_default_rules(cs, args): + """List rules for the default security group.""" + _print_secgroup_rules(cs.security_group_default_rules.list(), + show_source_group=False) + + +@utils.arg('ip_proto', + metavar='', + help=_('IP protocol (icmp, tcp, udp).')) +@utils.arg('from_port', + metavar='', + help=_('Port at start of range.')) +@utils.arg('to_port', + metavar='', + help=_('Port at end of range.')) +@utils.arg('cidr', metavar='', help=_('CIDR for address range.')) +def do_secgroup_add_default_rule(cs, args): + """Add a rule to the default security group.""" + rule = cs.security_group_default_rules.create(args.ip_proto, + args.from_port, + args.to_port, + args.cidr) + _print_secgroup_rules([rule], show_source_group=False) + + +@utils.arg('ip_proto', + metavar='', + help=_('IP protocol (icmp, tcp, udp).')) +@utils.arg('from_port', + metavar='', + help=_('Port at start of range.')) +@utils.arg('to_port', + metavar='', + help=_('Port at end of range.')) +@utils.arg('cidr', metavar='', help=_('CIDR for address range.')) +def do_secgroup_delete_default_rule(cs, args): + """Delete a rule from the default security group.""" + for rule in cs.security_group_default_rules.list(): + if (rule.ip_protocol and + rule.ip_protocol.upper() == args.ip_proto.upper() and + rule.from_port == int(args.from_port) and + rule.to_port == int(args.to_port) and + rule.ip_range['cidr'] == args.cidr): + _print_secgroup_rules([rule], show_source_group=False) + return cs.security_group_default_rules.delete(rule.id) + + raise exceptions.CommandError(_("Rule not found")) + + @utils.arg('name', metavar='', help='Server group name.') # NOTE(wingwj): The '--policy' way is still reserved here for preserving # the backwards compatibility of CLI, even if a user won't get this usage From 68f357d963423b7ea2bcc08214161f8a22362e8c Mon Sep 17 00:00:00 2001 From: Christian Berendt Date: Fri, 25 Jul 2014 19:27:19 +0200 Subject: [PATCH 0575/1705] Enable check for E131 * E131 continuation line unaligned for hanging indent Change-Id: I719d5bcd1b51896c947e637f6dfce2e1f1a6bd2b --- novaclient/tests/fixture_data/hypervisors.py | 40 +++++++++++--------- novaclient/tests/v1_1/fakes.py | 18 ++++----- novaclient/tests/v1_1/test_auth.py | 12 +++--- novaclient/tests/v3/fakes.py | 12 +++--- tox.ini | 4 +- 5 files changed, 46 insertions(+), 40 deletions(-) diff --git a/novaclient/tests/fixture_data/hypervisors.py b/novaclient/tests/fixture_data/hypervisors.py index 8133de442..068236a2d 100644 --- a/novaclient/tests/fixture_data/hypervisors.py +++ b/novaclient/tests/fixture_data/hypervisors.py @@ -38,7 +38,10 @@ def setUp(self): 'hypervisors': [ { 'id': 1234, - 'service': {'id': 1, 'host': 'compute1'}, + 'service': { + 'id': 1, + 'host': 'compute1', + }, 'vcpus': 4, 'memory_mb': 10 * 1024, 'local_gb': 250, @@ -57,22 +60,25 @@ def setUp(self): }, { 'id': 2, - 'service': {'id': 2, 'host': 'compute2'}, - 'vcpus': 4, - 'memory_mb': 10 * 1024, - 'local_gb': 250, - 'vcpus_used': 2, - 'memory_mb_used': 5 * 1024, - 'local_gb_used': 125, - 'hypervisor_type': 'xen', - 'hypervisor_version': 3, - 'hypervisor_hostname': 'hyper2', - 'free_ram_mb': 5 * 1024, - 'free_disk_gb': 125, - 'current_workload': 2, - 'running_vms': 2, - 'cpu_info': 'cpu_info', - 'disk_available_least': 100 + 'service': { + 'id': 2, + 'host': 'compute2', + }, + 'vcpus': 4, + 'memory_mb': 10 * 1024, + 'local_gb': 250, + 'vcpus_used': 2, + 'memory_mb_used': 5 * 1024, + 'local_gb_used': 125, + 'hypervisor_type': 'xen', + 'hypervisor_version': 3, + 'hypervisor_hostname': 'hyper2', + 'free_ram_mb': 5 * 1024, + 'free_disk_gb': 125, + 'current_workload': 2, + 'running_vms': 2, + 'cpu_info': 'cpu_info', + 'disk_available_least': 100 } ] } diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index d9c1f380e..c3fade4a8 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -710,14 +710,14 @@ def get_flavors_detail(self, **kw): if filter_is_public is not None: if filter_is_public: flavors['flavors'] = [ - v for v in flavors['flavors'] - if v['os-flavor-access:is_public'] - ] + v for v in flavors['flavors'] + if v['os-flavor-access:is_public'] + ] else: flavors['flavors'] = [ - v for v in flavors['flavors'] - if not v['os-flavor-access:is_public'] - ] + v for v in flavors['flavors'] + if not v['os-flavor-access:is_public'] + ] return (200, {}, flavors) @@ -1344,7 +1344,7 @@ def get_os_simple_tenant_usage(self, **kw): six.u('total_vcpus_usage'): 49.71047423333333, six.u('total_hours'): 49.71047423333333, six.u('tenant_id'): - six.u('7b0a1d73f8fb41718f3343c207597869'), + six.u('7b0a1d73f8fb41718f3343c207597869'), six.u('stop'): six.u('2012-01-22 19:48:41.750722'), six.u('server_usages'): [{ six.u('hours'): 49.71047423333333, @@ -1353,7 +1353,7 @@ def get_os_simple_tenant_usage(self, **kw): six.u('ended_at'): None, six.u('name'): six.u('f15image1'), six.u('tenant_id'): - six.u('7b0a1d73f8fb41718f3343c207597869'), + six.u('7b0a1d73f8fb41718f3343c207597869'), six.u('vcpus'): 1, six.u('memory_mb'): 512, six.u('state'): six.u('active'), @@ -1370,7 +1370,7 @@ def get_os_simple_tenant_usage_tenantfoo(self, **kw): six.u('total_vcpus_usage'): 49.71047423333333, six.u('total_hours'): 49.71047423333333, six.u('tenant_id'): - six.u('7b0a1d73f8fb41718f3343c207597869'), + six.u('7b0a1d73f8fb41718f3343c207597869'), six.u('stop'): six.u('2012-01-22 19:48:41.750722'), six.u('server_usages'): [{ six.u('hours'): 49.71047423333333, diff --git a/novaclient/tests/v1_1/test_auth.py b/novaclient/tests/v1_1/test_auth.py index c3df530b4..ea24d34f5 100644 --- a/novaclient/tests/v1_1/test_auth.py +++ b/novaclient/tests/v1_1/test_auth.py @@ -173,9 +173,9 @@ def test_auth_call(): 'passwordCredentials': { 'username': cs.client.user, 'password': cs.client.password, - }, - 'tenantName': cs.client.projectid, - }, + }, + 'tenantName': cs.client.projectid, + }, } token_url = cs.client.auth_url + "/tokens" @@ -261,9 +261,9 @@ def test_auth_call(): 'passwordCredentials': { 'username': cs.client.user, 'password': cs.client.password, - }, - 'tenantName': cs.client.projectid, - }, + }, + 'tenantName': cs.client.projectid, + }, } token_url = cs.client.auth_url + "/tokens" diff --git a/novaclient/tests/v3/fakes.py b/novaclient/tests/v3/fakes.py index 8bb9ffbd1..6d592f0cf 100644 --- a/novaclient/tests/v3/fakes.py +++ b/novaclient/tests/v3/fakes.py @@ -104,14 +104,14 @@ def get_flavors_detail(self, **kw): if filter_is_public is not None: if filter_is_public: flavors['flavors'] = [ - v for v in flavors['flavors'] - if v['flavor-access:is_public'] - ] + v for v in flavors['flavors'] + if v['flavor-access:is_public'] + ] else: flavors['flavors'] = [ - v for v in flavors['flavors'] - if not v['flavor-access:is_public'] - ] + v for v in flavors['flavors'] + if not v['flavor-access:is_public'] + ] return (200, {}, flavors) diff --git a/tox.ini b/tox.ini index 8b2408de1..a409dfda4 100644 --- a/tox.ini +++ b/tox.ini @@ -27,7 +27,7 @@ downloadcache = ~/cache/pip [flake8] # TODO fix following rules from hacking 0.9 -# E131,E265,H402,H405,H904 -ignore = E12,E131,E265,F811,F821,H302,H402,H404,H405,H904 +# E265,H402,H405,H904 +ignore = E12,E265,F811,F821,H302,H402,H404,H405,H904 show-source = True exclude=.venv,.git,.tox,dist,*openstack/common*,*lib/python*,*egg,build From 9561e620e2664265b0bbffadad733ce64969b3b6 Mon Sep 17 00:00:00 2001 From: liaonanhai Date: Mon, 4 Aug 2014 10:13:54 +0800 Subject: [PATCH 0576/1705] Fix order of arguments in assertEquals Part 10 Related to: bug #1277104 Change-Id: I582cfcf88def00c6c885ab78f26f2b4fa30c4457 --- novaclient/tests/test_auth_plugins.py | 4 +- novaclient/tests/test_base.py | 8 +-- novaclient/tests/test_client.py | 18 +++--- novaclient/tests/test_discover.py | 4 +- novaclient/tests/test_http.py | 6 +- novaclient/tests/test_service_catalog.py | 16 +++--- novaclient/tests/test_utils.py | 56 +++++++++---------- novaclient/tests/v1_1/test_agents.py | 4 +- .../tests/v1_1/test_availability_zone.py | 4 +- novaclient/tests/v1_1/test_fixed_ips.py | 8 +-- novaclient/tests/v1_1/test_flavors.py | 28 +++++----- novaclient/tests/v1_1/test_floating_ip_dns.py | 8 +-- novaclient/tests/v1_1/test_floating_ips.py | 2 +- novaclient/tests/v1_1/test_fping.py | 10 ++-- novaclient/tests/v1_1/test_images.py | 10 ++-- novaclient/tests/v1_1/test_keypairs.py | 2 +- novaclient/tests/v1_1/test_servers.py | 28 +++++----- novaclient/tests/v1_1/test_services.py | 22 ++++---- novaclient/tests/v1_1/test_shell.py | 6 +- novaclient/tests/v3/test_hosts.py | 4 +- novaclient/tests/v3/test_images.py | 10 ++-- novaclient/tests/v3/test_servers.py | 24 ++++---- novaclient/tests/v3/test_shell.py | 2 +- 23 files changed, 142 insertions(+), 142 deletions(-) diff --git a/novaclient/tests/test_auth_plugins.py b/novaclient/tests/test_auth_plugins.py index 9052141e1..8a15cce6d 100644 --- a/novaclient/tests/test_auth_plugins.py +++ b/novaclient/tests/test_auth_plugins.py @@ -172,7 +172,7 @@ def test_auth_call(): auth_system="fakewithauthurl", auth_plugin=plugin) cs.client.authenticate() - self.assertEqual(cs.client.auth_url, "http://faked/v2.0") + self.assertEqual("http://faked/v2.0", cs.client.auth_url) test_auth_call() @@ -302,7 +302,7 @@ def get_auth_url(self): cs = client.Client("username", "password", "project_id", auth_system="fakewithauthurl", auth_plugin=plugin) - self.assertEqual(cs.client.auth_url, "http://faked/v2.0") + self.assertEqual("http://faked/v2.0", cs.client.auth_url) @mock.patch.object(pkg_resources, "iter_entry_points") def test_exception_if_no_authenticate(self, mock_iter_entry_points): diff --git a/novaclient/tests/test_base.py b/novaclient/tests/test_base.py index 717dc6ea5..fca5b72d4 100644 --- a/novaclient/tests/test_base.py +++ b/novaclient/tests/test_base.py @@ -25,18 +25,18 @@ class BaseTest(utils.TestCase): def test_resource_repr(self): r = base.Resource(None, dict(foo="bar", baz="spam")) - self.assertEqual(repr(r), "") + self.assertEqual("", repr(r)) def test_getid(self): - self.assertEqual(base.getid(4), 4) + self.assertEqual(4, base.getid(4)) class TmpObject(object): id = 4 - self.assertEqual(base.getid(TmpObject), 4) + self.assertEqual(4, base.getid(TmpObject)) def test_resource_lazy_getattr(self): f = flavors.Flavor(cs.flavors, {'id': 1}) - self.assertEqual(f.name, '256 MB Server') + self.assertEqual('256 MB Server', f.name) cs.assert_called('GET', '/flavors/1') # Missing stuff still fails after a second get diff --git a/novaclient/tests/test_client.py b/novaclient/tests/test_client.py index ed349e1a6..f8bcaed80 100644 --- a/novaclient/tests/test_client.py +++ b/novaclient/tests/test_client.py @@ -47,7 +47,7 @@ def test_client_with_timeout(self): projectid='project', timeout=2, auth_url="http://www.blah.com") - self.assertEqual(instance.timeout, 2) + self.assertEqual(2, instance.timeout) mock_request = mock.Mock() mock_request.return_value = requests.Response() mock_request.return_value.status_code = 200 @@ -106,7 +106,7 @@ def test_client_reauth(self): allow_redirects=mock.ANY, data=json.dumps(data), verify=mock.ANY)] - self.assertEqual(mock_request.call_args_list, expected) + self.assertEqual(expected, mock_request.call_args_list) @mock.patch.object(novaclient.client.HTTPClient, 'request', return_value=(200, "{'versions':[]}")) @@ -263,7 +263,7 @@ def test_contextmanager_v3(self, mock_http_client): def test_get_password_simple(self): cs = novaclient.client.HTTPClient("user", "password", "", "") cs.password_func = mock.Mock() - self.assertEqual(cs._get_password(), "password") + self.assertEqual("password", cs._get_password()) self.assertFalse(cs.password_func.called) def test_get_password_none(self): @@ -273,26 +273,26 @@ def test_get_password_none(self): def test_get_password_func(self): cs = novaclient.client.HTTPClient("user", None, "", "") cs.password_func = mock.Mock(return_value="password") - self.assertEqual(cs._get_password(), "password") + self.assertEqual("password", cs._get_password()) cs.password_func.assert_called_once_with() cs.password_func = mock.Mock() - self.assertEqual(cs._get_password(), "password") + self.assertEqual("password", cs._get_password()) self.assertFalse(cs.password_func.called) def test_auth_url_rstrip_slash(self): cs = novaclient.client.HTTPClient("user", "password", "project_id", auth_url="foo/v2/") - self.assertEqual(cs.auth_url, "foo/v2") + self.assertEqual("foo/v2", cs.auth_url) def test_token_and_bypass_url(self): cs = novaclient.client.HTTPClient(None, None, None, auth_token="12345", bypass_url="compute/v100/") self.assertIsNone(cs.auth_url) - self.assertEqual(cs.auth_token, "12345") - self.assertEqual(cs.bypass_url, "compute/v100") - self.assertEqual(cs.management_url, "compute/v100") + self.assertEqual("12345", cs.auth_token) + self.assertEqual("compute/v100", cs.bypass_url) + self.assertEqual("compute/v100", cs.management_url) @mock.patch("novaclient.client.requests.Session") def test_session(self, mock_session): diff --git a/novaclient/tests/test_discover.py b/novaclient/tests/test_discover.py index e4acf0be5..a49882494 100644 --- a/novaclient/tests/test_discover.py +++ b/novaclient/tests/test_discover.py @@ -40,7 +40,7 @@ def mock_iter_entry_points(group): def test(): shell = novaclient.shell.OpenStackComputeShell() for name, module in shell._discover_via_entry_points(): - self.assertEqual(name, 'foo') + self.assertEqual('foo', name) self.assertTrue(inspect.ismodule(module)) test() @@ -68,7 +68,7 @@ def mock_discover_via_entry_points(self): def test(): shell = novaclient.shell.OpenStackComputeShell() extensions = shell._discover_extensions('1.1') - self.assertEqual(len(extensions), 3) + self.assertEqual(3, len(extensions)) names = sorted(['foo', 'bar', 'baz']) sorted_extensions = sorted(extensions, key=lambda ext: ext.name) for i in range(len(names)): diff --git a/novaclient/tests/test_http.py b/novaclient/tests/test_http.py index 2797a438f..7d9e3ecfa 100644 --- a/novaclient/tests/test_http.py +++ b/novaclient/tests/test_http.py @@ -79,7 +79,7 @@ def test_get_call(): headers=headers, **self.TEST_REQUEST_BASE) # Automatic JSON parsing - self.assertEqual(body, {"hi": "there"}) + self.assertEqual({"hi": "there"}, body) test_get_call() @@ -136,11 +136,11 @@ def test_refused_call(): def test_client_logger(self): cl1 = client.HTTPClient("username", "password", "project_id", "auth_test", http_log_debug=True) - self.assertEqual(len(cl1._logger.handlers), 1) + self.assertEqual(1, len(cl1._logger.handlers)) cl2 = client.HTTPClient("username", "password", "project_id", "auth_test", http_log_debug=True) - self.assertEqual(len(cl2._logger.handlers), 1) + self.assertEqual(1, len(cl2._logger.handlers)) @mock.patch.object(requests, 'request', unknown_error_mock_request) def test_unknown_server_error(self): diff --git a/novaclient/tests/test_service_catalog.py b/novaclient/tests/test_service_catalog.py index 9f224bb57..868cce9f6 100644 --- a/novaclient/tests/test_service_catalog.py +++ b/novaclient/tests/test_service_catalog.py @@ -129,10 +129,10 @@ def test_building_a_service_catalog(self): self.assertRaises(exceptions.AmbiguousEndpoints, sc.url_for, service_type='compute') - self.assertEqual(sc.url_for('tenantId', '1', service_type='compute'), - "https://compute1.host/v2/1") - self.assertEqual(sc.url_for('tenantId', '2', service_type='compute'), - "https://compute1.host/v1.1/2") + self.assertEqual("https://compute1.host/v2/1", + sc.url_for('tenantId', '1', service_type='compute')) + self.assertEqual("https://compute1.host/v1.1/2", + sc.url_for('tenantId', '2', service_type='compute')) self.assertRaises(exceptions.EndpointNotFound, sc.url_for, "region", "South", service_type='compute') @@ -148,10 +148,10 @@ def test_alternate_service_type(self): self.assertRaises(exceptions.AmbiguousEndpoints, sc.url_for, service_type='volume') - self.assertEqual(sc.url_for('tenantId', '1', service_type='volume'), - "https://volume1.host/v1/1") - self.assertEqual(sc.url_for('tenantId', '2', service_type='volume'), - "https://volume1.host/v1.1/2") + self.assertEqual("https://volume1.host/v1/1", + sc.url_for('tenantId', '1', service_type='volume')) + self.assertEqual("https://volume1.host/v1.1/2", + sc.url_for('tenantId', '2', service_type='volume')) self.assertRaises(exceptions.EndpointNotFound, sc.url_for, "region", "North", service_type='volume') diff --git a/novaclient/tests/test_utils.py b/novaclient/tests/test_utils.py index 31678ca2a..69e5d6188 100644 --- a/novaclient/tests/test_utils.py +++ b/novaclient/tests/test_utils.py @@ -140,26 +140,26 @@ class PrintResultTestCase(test_utils.TestCase): def test_print_dict(self): dict = {'key': 'value'} utils.print_dict(dict) - self.assertEqual(sys.stdout.getvalue(), - '+----------+-------+\n' + self.assertEqual('+----------+-------+\n' '| Property | Value |\n' '+----------+-------+\n' '| key | value |\n' - '+----------+-------+\n') + '+----------+-------+\n', + sys.stdout.getvalue()) @mock.patch('sys.stdout', six.StringIO()) def test_print_dict_wrap(self): dict = {'key1': 'not wrapped', 'key2': 'this will be wrapped'} utils.print_dict(dict, wrap=16) - self.assertEqual(sys.stdout.getvalue(), - '+----------+--------------+\n' + self.assertEqual('+----------+--------------+\n' '| Property | Value |\n' '+----------+--------------+\n' '| key1 | not wrapped |\n' '| key2 | this will be |\n' '| | wrapped |\n' - '+----------+--------------+\n') + '+----------+--------------+\n', + sys.stdout.getvalue()) @mock.patch('sys.stdout', six.StringIO()) def test_print_list_sort_by_str(self): @@ -169,14 +169,14 @@ def test_print_list_sort_by_str(self): utils.print_list(objs, ["Name", "Value"], sortby_index=0) - self.assertEqual(sys.stdout.getvalue(), - '+------+-------+\n' + self.assertEqual('+------+-------+\n' '| Name | Value |\n' '+------+-------+\n' '| k1 | 1 |\n' '| k2 | 3 |\n' '| k3 | 2 |\n' - '+------+-------+\n') + '+------+-------+\n', + sys.stdout.getvalue()) @mock.patch('sys.stdout', six.StringIO()) def test_print_list_sort_by_integer(self): @@ -186,14 +186,14 @@ def test_print_list_sort_by_integer(self): utils.print_list(objs, ["Name", "Value"], sortby_index=1) - self.assertEqual(sys.stdout.getvalue(), - '+------+-------+\n' + self.assertEqual('+------+-------+\n' '| Name | Value |\n' '+------+-------+\n' '| k1 | 1 |\n' '| k3 | 2 |\n' '| k2 | 3 |\n' - '+------+-------+\n') + '+------+-------+\n', + sys.stdout.getvalue()) # without sorting @mock.patch('sys.stdout', six.StringIO()) @@ -204,47 +204,47 @@ def test_print_list_sort_by_none(self): utils.print_list(objs, ["Name", "Value"], sortby_index=None) - self.assertEqual(sys.stdout.getvalue(), - '+------+-------+\n' + self.assertEqual('+------+-------+\n' '| Name | Value |\n' '+------+-------+\n' '| k1 | 1 |\n' '| k3 | 3 |\n' '| k2 | 2 |\n' - '+------+-------+\n') + '+------+-------+\n', + sys.stdout.getvalue()) @mock.patch('sys.stdout', six.StringIO()) def test_print_dict_dictionary(self): dict = {'k': {'foo': 'bar'}} utils.print_dict(dict) - self.assertEqual(sys.stdout.getvalue(), - '+----------+----------------+\n' + self.assertEqual('+----------+----------------+\n' '| Property | Value |\n' '+----------+----------------+\n' '| k | {"foo": "bar"} |\n' - '+----------+----------------+\n') + '+----------+----------------+\n', + sys.stdout.getvalue()) @mock.patch('sys.stdout', six.StringIO()) def test_print_dict_list_dictionary(self): dict = {'k': [{'foo': 'bar'}]} utils.print_dict(dict) - self.assertEqual(sys.stdout.getvalue(), - '+----------+------------------+\n' + self.assertEqual('+----------+------------------+\n' '| Property | Value |\n' '+----------+------------------+\n' '| k | [{"foo": "bar"}] |\n' - '+----------+------------------+\n') + '+----------+------------------+\n', + sys.stdout.getvalue()) @mock.patch('sys.stdout', six.StringIO()) def test_print_dict_list(self): dict = {'k': ['foo', 'bar']} utils.print_dict(dict) - self.assertEqual(sys.stdout.getvalue(), - '+----------+----------------+\n' + self.assertEqual('+----------+----------------+\n' '| Property | Value |\n' '+----------+----------------+\n' '| k | ["foo", "bar"] |\n' - '+----------+----------------+\n') + '+----------+----------------+\n', + sys.stdout.getvalue()) class FlattenTestCase(test_utils.TestCase): @@ -270,22 +270,22 @@ def test_flattening(self): def test_pretty_choice_list(self): l = [] r = utils.pretty_choice_list(l) - self.assertEqual(r, "") + self.assertEqual("", r) l = ["v1", "v2", "v3"] r = utils.pretty_choice_list(l) - self.assertEqual(r, "'v1', 'v2', 'v3'") + self.assertEqual("'v1', 'v2', 'v3'", r) def test_pretty_choice_dict(self): d = {} r = utils.pretty_choice_dict(d) - self.assertEqual(r, "") + self.assertEqual("", r) d = {"k1": "v1", "k2": "v2", "k3": "v3"} r = utils.pretty_choice_dict(d) - self.assertEqual(r, "'k1=v1', 'k2=v2', 'k3=v3'") + self.assertEqual("'k1=v1', 'k2=v2', 'k3=v3'", r) class ValidationsTestCase(test_utils.TestCase): diff --git a/novaclient/tests/v1_1/test_agents.py b/novaclient/tests/v1_1/test_agents.py index 46efe839a..c55cdaed8 100644 --- a/novaclient/tests/v1_1/test_agents.py +++ b/novaclient/tests/v1_1/test_agents.py @@ -63,7 +63,7 @@ def test_list_agents(self): self.assert_called('GET', '/os-agents') for a in ags: self.assertIsInstance(a, agents.Agent) - self.assertEqual(a.hypervisor, 'kvm') + self.assertEqual('kvm', a.hypervisor) def test_list_agents_with_hypervisor(self): self.stub_hypervisors('xen') @@ -71,7 +71,7 @@ def test_list_agents_with_hypervisor(self): self.assert_called('GET', '/os-agents?hypervisor=xen') for a in ags: self.assertIsInstance(a, agents.Agent) - self.assertEqual(a.hypervisor, 'xen') + self.assertEqual('xen', a.hypervisor) def test_agents_create(self): ag = self.cs.agents.create('win', 'x86', '7.0', diff --git a/novaclient/tests/v1_1/test_availability_zone.py b/novaclient/tests/v1_1/test_availability_zone.py index 2144d8c19..6851d9aea 100644 --- a/novaclient/tests/v1_1/test_availability_zone.py +++ b/novaclient/tests/v1_1/test_availability_zone.py @@ -58,7 +58,7 @@ def test_list_availability_zone(self): z0 = self.shell._treeizeAvailabilityZone(zones[0]) z1 = self.shell._treeizeAvailabilityZone(zones[1]) - self.assertEqual((len(z0), len(z1)), (1, 1)) + self.assertEqual((1, 1), (len(z0), len(z1))) self._assertZone(z0[0], l0[0], l0[1]) self._assertZone(z1[0], l1[0], l1[1]) @@ -89,7 +89,7 @@ def test_detail_availability_zone(self): z1 = self.shell._treeizeAvailabilityZone(zones[1]) z2 = self.shell._treeizeAvailabilityZone(zones[2]) - self.assertEqual((len(z0), len(z1), len(z2)), (3, 5, 1)) + self.assertEqual((3, 5, 1), (len(z0), len(z1), len(z2))) self._assertZone(z0[0], l0[0], l0[1]) self._assertZone(z0[1], l1[0], l1[1]) diff --git a/novaclient/tests/v1_1/test_fixed_ips.py b/novaclient/tests/v1_1/test_fixed_ips.py index a081a8ecc..5cfecf333 100644 --- a/novaclient/tests/v1_1/test_fixed_ips.py +++ b/novaclient/tests/v1_1/test_fixed_ips.py @@ -28,10 +28,10 @@ class FixedIpsTest(utils.FixturedTestCase): def test_get_fixed_ip(self): info = self.cs.fixed_ips.get(fixed_ip='192.168.1.1') self.assert_called('GET', '/os-fixed-ips/192.168.1.1') - self.assertEqual(info.cidr, '192.168.1.0/24') - self.assertEqual(info.address, '192.168.1.1') - self.assertEqual(info.hostname, 'foo') - self.assertEqual(info.host, 'bar') + self.assertEqual('192.168.1.0/24', info.cidr) + self.assertEqual('192.168.1.1', info.address) + self.assertEqual('foo', info.hostname) + self.assertEqual('bar', info.host) def test_reserve_fixed_ip(self): body = {"reserve": None} diff --git a/novaclient/tests/v1_1/test_flavors.py b/novaclient/tests/v1_1/test_flavors.py index 8028cf0ee..8f5aea007 100644 --- a/novaclient/tests/v1_1/test_flavors.py +++ b/novaclient/tests/v1_1/test_flavors.py @@ -67,36 +67,36 @@ def test_get_flavor_details(self): f = self.cs.flavors.get(1) self.cs.assert_called('GET', '/flavors/1') self.assertIsInstance(f, self.flavor_type) - self.assertEqual(f.ram, 256) - self.assertEqual(f.disk, 10) - self.assertEqual(f.ephemeral, 10) - self.assertEqual(f.is_public, True) + self.assertEqual(256, f.ram) + self.assertEqual(10, f.disk) + self.assertEqual(10, f.ephemeral) + self.assertEqual(True, f.is_public) def test_get_flavor_details_alphanum_id(self): f = self.cs.flavors.get('aa1') self.cs.assert_called('GET', '/flavors/aa1') self.assertIsInstance(f, self.flavor_type) - self.assertEqual(f.ram, 128) - self.assertEqual(f.disk, 0) - self.assertEqual(f.ephemeral, 0) - self.assertEqual(f.is_public, True) + self.assertEqual(128, f.ram) + self.assertEqual(0, f.disk) + self.assertEqual(0, f.ephemeral) + self.assertEqual(True, f.is_public) def test_get_flavor_details_diablo(self): f = self.cs.flavors.get(3) self.cs.assert_called('GET', '/flavors/3') self.assertIsInstance(f, self.flavor_type) - self.assertEqual(f.ram, 256) - self.assertEqual(f.disk, 10) - self.assertEqual(f.ephemeral, 'N/A') - self.assertEqual(f.is_public, 'N/A') + self.assertEqual(256, f.ram) + self.assertEqual(10, f.disk) + self.assertEqual('N/A', f.ephemeral) + self.assertEqual('N/A', f.is_public) def test_find(self): f = self.cs.flavors.find(ram=256) self.cs.assert_called('GET', '/flavors/detail') - self.assertEqual(f.name, '256 MB Server') + self.assertEqual('256 MB Server', f.name) f = self.cs.flavors.find(disk=0) - self.assertEqual(f.name, '128 MB Server') + self.assertEqual('128 MB Server', f.name) self.assertRaises(exceptions.NotFound, self.cs.flavors.find, disk=12345) diff --git a/novaclient/tests/v1_1/test_floating_ip_dns.py b/novaclient/tests/v1_1/test_floating_ip_dns.py index df310b269..2ded1725e 100644 --- a/novaclient/tests/v1_1/test_floating_ip_dns.py +++ b/novaclient/tests/v1_1/test_floating_ip_dns.py @@ -25,13 +25,13 @@ class FloatingIPDNSDomainTest(utils.FixturedTestCase): def test_dns_domains(self): domainlist = self.cs.dns_domains.domains() - self.assertEqual(len(domainlist), 2) + self.assertEqual(2, len(domainlist)) for entry in domainlist: self.assertIsInstance(entry, floating_ip_dns.FloatingIPDNSDomain) - self.assertEqual(domainlist[1].domain, 'example.com') + self.assertEqual('example.com', domainlist[1].domain) def test_create_private_domain(self): self.cs.dns_domains.create_private(self.testdomain, 'test_avzone') @@ -61,13 +61,13 @@ class FloatingIPDNSEntryTest(utils.FixturedTestCase): def test_get_dns_entries_by_ip(self): entries = self.cs.dns_entries.get_for_ip(self.testdomain, ip=self.testip) - self.assertEqual(len(entries), 2) + self.assertEqual(2, len(entries)) for entry in entries: self.assertIsInstance(entry, floating_ip_dns.FloatingIPDNSEntry) - self.assertEqual(entries[1].dns_entry['name'], 'host2') + self.assertEqual('host2', entries[1].dns_entry['name']) self.assertEqual(entries[1].dns_entry['ip'], self.testip) def test_get_dns_entry_by_name(self): diff --git a/novaclient/tests/v1_1/test_floating_ips.py b/novaclient/tests/v1_1/test_floating_ips.py index d14f8a710..93cc733ba 100644 --- a/novaclient/tests/v1_1/test_floating_ips.py +++ b/novaclient/tests/v1_1/test_floating_ips.py @@ -55,5 +55,5 @@ def test_create_floating_ip(self): def test_create_floating_ip_with_pool(self): fl = self.cs.floating_ips.create('nova') self.assert_called('POST', '/os-floating-ips') - self.assertEqual(fl.pool, 'nova') + self.assertEqual('nova', fl.pool) self.assertIsInstance(fl, floating_ips.FloatingIP) diff --git a/novaclient/tests/v1_1/test_fping.py b/novaclient/tests/v1_1/test_fping.py index 87f9af29e..5a3fb6401 100644 --- a/novaclient/tests/v1_1/test_fping.py +++ b/novaclient/tests/v1_1/test_fping.py @@ -26,15 +26,15 @@ class FpingTest(utils.FixturedTestCase): def test_fping_repr(self): r = self.cs.fping.get(1) - self.assertEqual(repr(r), "") + self.assertEqual("", repr(r)) def test_list_fpings(self): fl = self.cs.fping.list() self.assert_called('GET', '/os-fping') for f in fl: self.assertIsInstance(f, fping.Fping) - self.assertEqual(f.project_id, "fake-project") - self.assertEqual(f.alive, True) + self.assertEqual("fake-project", f.project_id) + self.assertEqual(True, f.alive) def test_list_fpings_all_tenants(self): fl = self.cs.fping.list(all_tenants=True) @@ -58,5 +58,5 @@ def test_get_fping(self): f = self.cs.fping.get(1) self.assert_called('GET', '/os-fping/1') self.assertIsInstance(f, fping.Fping) - self.assertEqual(f.project_id, "fake-project") - self.assertEqual(f.alive, True) + self.assertEqual("fake-project", f.project_id) + self.assertEqual(True, f.alive) diff --git a/novaclient/tests/v1_1/test_images.py b/novaclient/tests/v1_1/test_images.py index 6ff8fe9c7..e237bcd5c 100644 --- a/novaclient/tests/v1_1/test_images.py +++ b/novaclient/tests/v1_1/test_images.py @@ -40,8 +40,8 @@ def test_get_image_details(self): i = self.cs.images.get(1) self.assert_called('GET', '/images/1') self.assertIsInstance(i, images.Image) - self.assertEqual(i.id, 1) - self.assertEqual(i.name, 'CentOS 5.2') + self.assertEqual(1, i.id) + self.assertEqual('CentOS 5.2', i.name) def test_delete_image(self): self.cs.images.delete(1) @@ -58,9 +58,9 @@ def test_set_meta(self): def test_find(self): i = self.cs.images.find(name="CentOS 5.2") - self.assertEqual(i.id, 1) + self.assertEqual(1, i.id) self.assert_called('GET', '/images/1') iml = self.cs.images.findall(status='SAVING') - self.assertEqual(len(iml), 1) - self.assertEqual(iml[0].name, 'My Server Backup') + self.assertEqual(1, len(iml)) + self.assertEqual('My Server Backup', iml[0].name) diff --git a/novaclient/tests/v1_1/test_keypairs.py b/novaclient/tests/v1_1/test_keypairs.py index 7ebb23c2f..9e8e63fd5 100644 --- a/novaclient/tests/v1_1/test_keypairs.py +++ b/novaclient/tests/v1_1/test_keypairs.py @@ -37,7 +37,7 @@ def test_get_keypair(self): kp = self.cs.keypairs.get('test') self.assert_called('GET', '/%s/test' % self.keypair_prefix) self.assertIsInstance(kp, keypairs.Keypair) - self.assertEqual(kp.name, 'test') + self.assertEqual('test', kp.name) def test_list_keypairs(self): kps = self.cs.keypairs.list() diff --git a/novaclient/tests/v1_1/test_servers.py b/novaclient/tests/v1_1/test_servers.py index 5ef06baaa..496d77fb5 100644 --- a/novaclient/tests/v1_1/test_servers.py +++ b/novaclient/tests/v1_1/test_servers.py @@ -53,8 +53,8 @@ def test_get_server_details(self): s = self.cs.servers.get(1234) self.assert_called('GET', '/servers/1234') self.assertIsInstance(s, servers.Server) - self.assertEqual(s.id, 1234) - self.assertEqual(s.status, 'BUILD') + self.assertEqual(1234, s.id) + self.assertEqual('BUILD', s.status) def test_get_server_promote_details(self): s1 = self.cs.servers.list(detailed=False)[0] @@ -244,13 +244,13 @@ def test_set_server_meta_item(self): def test_find(self): server = self.cs.servers.find(name='sample-server') self.assert_called('GET', '/servers/1234') - self.assertEqual(server.name, 'sample-server') + self.assertEqual('sample-server', server.name) self.assertRaises(exceptions.NoUniqueMatch, self.cs.servers.find, flavor={"id": 1, "name": "256 MB Server"}) sl = self.cs.servers.findall(flavor={"id": 1, "name": "256 MB Server"}) - self.assertEqual([s.id for s in sl], [1234, 5678, 9012]) + self.assertEqual([1234, 5678, 9012], [s.id for s in sl]) def test_reboot_server(self): s = self.cs.servers.get(1234) @@ -299,7 +299,7 @@ def test_rebuild_server_preserve_ephemeral(self): body = httpretty.last_request().parsed_body d = body['rebuild'] self.assertIn('preserve_ephemeral', d) - self.assertEqual(d['preserve_ephemeral'], True) + self.assertEqual(True, d['preserve_ephemeral']) def test_rebuild_server_name_meta_files(self): files = {'/etc/passwd': 'some data'} @@ -464,11 +464,11 @@ def test_get_console_output_without_length(self): success = 'foo' s = self.cs.servers.get(1234) s.get_console_output() - self.assertEqual(s.get_console_output(), success) + self.assertEqual(success, s.get_console_output()) self.assert_called('POST', '/servers/1234/action') self.cs.servers.get_console_output(s) - self.assertEqual(self.cs.servers.get_console_output(s), success) + self.assertEqual(success, self.cs.servers.get_console_output(s)) self.assert_called('POST', '/servers/1234/action') def test_get_console_output_with_length(self): @@ -476,12 +476,12 @@ def test_get_console_output_with_length(self): s = self.cs.servers.get(1234) s.get_console_output(length=50) - self.assertEqual(s.get_console_output(length=50), success) + self.assertEqual(success, s.get_console_output(length=50)) self.assert_called('POST', '/servers/1234/action') self.cs.servers.get_console_output(s, length=50) - self.assertEqual(self.cs.servers.get_console_output(s, length=50), - success) + self.assertEqual(success, + self.cs.servers.get_console_output(s, length=50)) self.assert_called('POST', '/servers/1234/action') # Testing password methods with the following password and key @@ -500,19 +500,19 @@ def test_get_console_output_with_length(self): def test_get_password(self): s = self.cs.servers.get(1234) - self.assertEqual(s.get_password('novaclient/tests/idfake.pem'), - b'FooBar123') + self.assertEqual(b'FooBar123', + s.get_password('novaclient/tests/idfake.pem')) self.assert_called('GET', '/servers/1234/os-server-password') def test_get_password_without_key(self): s = self.cs.servers.get(1234) - self.assertEqual(s.get_password(), + self.assertEqual( 'OIuEuQttO8Rk93BcKlwHQsziDAnkAm/V6V8VPToA8ZeUaUBWwS0gwo2K6Y61Z96r' 'qG447iRz0uTEEYq3RAYJk1mh3mMIRVl27t8MtIecR5ggVVbz1S9AwXJQypDKl0ho' 'QFvhCBcMWPohyGewDJOhDbtuN1IoFI9G55ZvFwCm5y7m7B2aVcoLeIsJZE4PLsIw' '/y5a6Z3/AoJZYGG7IH5WN88UROU3B9JZGFB2qtPLQTOvDMZLUhoPRIJeHiVSlo1N' 'tI2/++UsXVg3ow6ItqCJGgdNuGG5JB+bslDHWPxROpesEIHdczk46HCpHQN8f1sk' - 'Hi/fmZZNQQqj1Ijq0caOIw==') + 'Hi/fmZZNQQqj1Ijq0caOIw==', s.get_password()) self.assert_called('GET', '/servers/1234/os-server-password') def test_clear_password(self): diff --git a/novaclient/tests/v1_1/test_services.py b/novaclient/tests/v1_1/test_services.py index d5242e52b..acd25152f 100644 --- a/novaclient/tests/v1_1/test_services.py +++ b/novaclient/tests/v1_1/test_services.py @@ -35,8 +35,8 @@ def test_list_services(self): self.cs.assert_called('GET', '/os-services') for s in svs: self.assertIsInstance(s, self._get_service_type()) - self.assertEqual(s.binary, 'nova-compute') - self.assertEqual(s.host, 'host1') + self.assertEqual('nova-compute', s.binary) + self.assertEqual('host1', s.host) self.assertTrue(str(s).startswith(' Date: Mon, 4 Aug 2014 03:28:14 +0000 Subject: [PATCH 0577/1705] Updated from global requirements Change-Id: Ia1c665bc9cf86003e72984564c6b954115ae13f0 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index ea080c2ce..910d74c47 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -7,7 +7,7 @@ httpretty>=0.8.0,!=0.8.1,!=0.8.2 keyring>=2.1,!=3.3 mock>=1.0 sphinx>=1.1.2,!=1.2.0,<1.3 -python-keystoneclient>=0.9.0 +python-keystoneclient>=0.10.0 oslosphinx testrepository>=0.0.18 testscenarios>=0.4 From 33058cbe8e19195ddace8ad5adad7bf7927c731e Mon Sep 17 00:00:00 2001 From: Christian Berendt Date: Tue, 5 Aug 2014 21:35:12 +0200 Subject: [PATCH 0578/1705] Enable several checks and do not check docs/source/conf.py * E265 block comment should start with '# ' * H302 import only modules Do not check docs/source/conf.py. The file is imported from the cookiecutter template. Documented why checks are ignored and if they should be enabled in the future. Change-Id: I367064ecaa6d1fd9d918f7ce003303e2db660647 --- novaclient/shell.py | 8 ++++---- novaclient/tests/test_shell.py | 5 +++-- novaclient/tests/v1_1/fakes.py | 22 ++++++++++++++++------ novaclient/tests/v3/fakes.py | 14 ++++++++++---- novaclient/v1_1/servers.py | 6 ++++-- novaclient/v1_1/shell.py | 4 ++-- novaclient/v3/client.py | 2 +- novaclient/v3/shell.py | 2 +- tox.ini | 23 +++++++++++++++++++---- 9 files changed, 60 insertions(+), 26 deletions(-) diff --git a/novaclient/shell.py b/novaclient/shell.py index 08f5e8c40..984460ed0 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -219,7 +219,7 @@ def error(self, message): exits. """ self.print_usage(sys.stderr) - #FIXME(lzyeval): if changes occur in argparse.ArgParser._check_value + # FIXME(lzyeval): if changes occur in argparse.ArgParser._check_value choose_from = ' (choose from' progparts = self.prog.partition(' ') self.exit(2, _("error: %(errmsg)s\nTry '%(mainp)s help %(subp)s'" @@ -360,8 +360,8 @@ def get_base_parser(self): # thinking usage-list --end is ambiguous; but it # works fine with only --endpoint-type present # Go figure. I'm leaving this here for doc purposes. - #parser.add_argument('--endpoint_type', - # help=argparse.SUPPRESS) + # parser.add_argument('--endpoint_type', + # help=argparse.SUPPRESS) parser.add_argument('--os-compute-api-version', metavar='', @@ -607,7 +607,7 @@ def main(self, argv): must_auth = not (cliutils.isunauthenticated(args.func) or (auth_token and management_url)) - #FIXME(usrleon): Here should be restrict for project id same as + # FIXME(usrleon): Here should be restrict for project id same as # for os_username or os_password but for compatibility it is not. if must_auth: if auth_plugin: diff --git a/novaclient/tests/test_shell.py b/novaclient/tests/test_shell.py index daee60abe..ee08179bb 100644 --- a/novaclient/tests/test_shell.py +++ b/novaclient/tests/test_shell.py @@ -11,7 +11,7 @@ # License for the specific language governing permissions and limitations # under the License. -from distutils.version import StrictVersion +import distutils.version as dist_version import re import sys @@ -200,7 +200,8 @@ def test_password(self, mock_getpass, mock_stdin): # default output of empty tables differs depending between prettytable # versions if (hasattr(prettytable, '__version__') and - StrictVersion(prettytable.__version__) < StrictVersion('0.7.2')): + dist_version.StrictVersion(prettytable.__version__) < + dist_version.StrictVersion('0.7.2')): ex = '\n' else: ex = ( diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index c3fade4a8..f9fc1fd91 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from datetime import datetime +import datetime import six from six.moves.urllib import parse @@ -1495,13 +1495,17 @@ def get_os_services(self, **kw): 'zone': 'nova', 'status': 'enabled', 'state': 'up', - 'updated_at': datetime(2012, 10, 29, 13, 42, 2)}, + 'updated_at': datetime.datetime( + 2012, 10, 29, 13, 42, 2 + )}, {'binary': binary, 'host': host, 'zone': 'nova', 'status': 'disabled', 'state': 'down', - 'updated_at': datetime(2012, 9, 18, 8, 3, 38)}, + 'updated_at': datetime.datetime( + 2012, 9, 18, 8, 3, 38 + )}, ]}) def put_os_services_enable(self, body, **kw): @@ -1785,7 +1789,9 @@ def get_os_availability_zone_detail(self, **kw): "nova-compute": {"active": True, "available": True, "updated_at": - datetime(2012, 12, 26, 14, 45, 25, 0)}}}}, + datetime.datetime( + 2012, 12, 26, 14, 45, 25, 0 + )}}}}, {"zoneName": "internal", "zoneState": {"available": True}, "hosts": { @@ -1794,13 +1800,17 @@ def get_os_availability_zone_detail(self, **kw): "active": True, "available": True, "updated_at": - datetime(2012, 12, 26, 14, 45, 25, 0)}}, + datetime.datetime( + 2012, 12, 26, 14, 45, 25, 0 + )}}, "fake_host-2": { "nova-network": { "active": True, "available": False, "updated_at": - datetime(2012, 12, 26, 14, 45, 24, 0)}}}}, + datetime.datetime( + 2012, 12, 26, 14, 45, 24, 0 + )}}}}, {"zoneName": "zone-2", "zoneState": {"available": False}, "hosts": None}]}) diff --git a/novaclient/tests/v3/fakes.py b/novaclient/tests/v3/fakes.py index 6d592f0cf..cdc65c8fc 100644 --- a/novaclient/tests/v3/fakes.py +++ b/novaclient/tests/v3/fakes.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from datetime import datetime +import datetime from novaclient.openstack.common import strutils from novaclient.tests import fakes @@ -282,7 +282,9 @@ def get_os_availability_zone_detail(self, **kw): "nova-compute": {"active": True, "available": True, "updated_at": - datetime(2012, 12, 26, 14, 45, 25, 0)}}}}, + datetime.datetime( + 2012, 12, 26, 14, 45, 25, 0 + )}}}}, {"zone_name": "internal", "zone_state": {"available": True}, "hosts": { @@ -291,13 +293,17 @@ def get_os_availability_zone_detail(self, **kw): "active": True, "available": True, "updated_at": - datetime(2012, 12, 26, 14, 45, 25, 0)}}, + datetime.datetime( + 2012, 12, 26, 14, 45, 25, 0 + )}}, "fake_host-2": { "nova-network": { "active": True, "available": False, "updated_at": - datetime(2012, 12, 26, 14, 45, 24, 0)}}}}, + datetime.datetime( + 2012, 12, 26, 14, 45, 24, 0 + )}}}}, {"zone_name": "zone-2", "zone_state": {"available": False}, "hosts": None}]}) diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index 8a930e8f9..68313a521 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -28,7 +28,8 @@ from novaclient import crypto from novaclient.openstack.common.gettextutils import _ from novaclient.openstack.common import strutils -from novaclient.v1_1.security_groups import SecurityGroup +from novaclient.v1_1 import security_groups + REBOOT_SOFT, REBOOT_HARD = 'SOFT', 'HARD' @@ -1147,7 +1148,8 @@ def list_security_group(self, server): """ return self._list('/servers/%s/os-security-groups' % - base.getid(server), 'security_groups', SecurityGroup) + base.getid(server), 'security_groups', + security_groups.SecurityGroup) def evacuate(self, server, host, on_shared_storage, password=None): """ diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 7373848c7..011989a96 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1151,7 +1151,7 @@ def do_image_delete(cs, args): const=1, help=argparse.SUPPRESS) @utils.arg('--tenant', - #nova db searches by project_id + # nova db searches by project_id dest='tenant', metavar='', nargs='?', @@ -2581,7 +2581,7 @@ def _find_keypair(cs, keypair): @utils.arg('--tenant', - #nova db searches by project_id + # nova db searches by project_id dest='tenant', metavar='', nargs='?', diff --git a/novaclient/v3/client.py b/novaclient/v3/client.py index c7928f603..a30f9191e 100644 --- a/novaclient/v3/client.py +++ b/novaclient/v3/client.py @@ -97,7 +97,7 @@ def __init__(self, username=None, password=None, project_id=None, self.tenant_id = tenant_id self.user_id = user_id self.os_cache = os_cache or not no_cache - #TODO(bnemec): Add back in v3 extensions + # TODO(bnemec): Add back in v3 extensions self.agents = agents.AgentsManager(self) self.aggregates = aggregates.AggregateManager(self) self.availability_zones = \ diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index ce856f119..bf756bb7f 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -950,7 +950,7 @@ def do_image_delete(cs, args): const=1, help=argparse.SUPPRESS) @utils.arg('--tenant', - #nova db searches by project_id + # nova db searches by project_id dest='tenant', metavar='', nargs='?', diff --git a/tox.ini b/tox.ini index a409dfda4..40978d654 100644 --- a/tox.ini +++ b/tox.ini @@ -26,8 +26,23 @@ commands = python setup.py testr --coverage --testr-args='{posargs}' downloadcache = ~/cache/pip [flake8] -# TODO fix following rules from hacking 0.9 -# E265,H402,H405,H904 -ignore = E12,E265,F811,F821,H302,H402,H404,H405,H904 +# Following checks should be enabled in the future. +# +# H404 multi line docstring should start without a leading new line +# H405 multi line docstring summary not separated with an empty line +# +# Following checks are ignored on purpose. +# +# H402 one line docstring needs punctuation +# reason: removed in hacking (https://review.openstack.org/#/c/101497/) +# +# H904 wrap long lines in parentheses instead of a backslash +# reason: removed in hacking (https://review.openstack.org/#/c/101701/) +# +# Additional checks are also ignored on purpose: E12, F811, F821 +ignore = E12,F811,F821,H402,H404,H405,H904 show-source = True -exclude=.venv,.git,.tox,dist,*openstack/common*,*lib/python*,*egg,build +exclude=.venv,.git,.tox,dist,*openstack/common*,*lib/python*,*egg,build,doc/source/conf.py + +[hacking] +import_exceptions = novaclient.openstack.common.gettextutils From caf9f799efeff2163cf0adc236986bfa4addfe9d Mon Sep 17 00:00:00 2001 From: Jamie Lennox Date: Thu, 10 Jul 2014 15:14:52 +1000 Subject: [PATCH 0579/1705] Convert to requests-mock We've had some trouble with httpretty in the past and so are moving to requests-mock. There should be no functionality change in this patch, simply a transition to a newer library. Examples: - Python 2/3 inconsistencies - Breaking compatibility between releases - Incorrect package dependency specifications - Problems with distro packaging around tests - *can* introduce a maintained state between tests. Change-Id: I666a5c7e6747f0c5c2dc96336774fd0fcd3f5907 --- novaclient/tests/fixture_data/agents.py | 21 +- novaclient/tests/fixture_data/aggregates.py | 30 +- .../tests/fixture_data/availability_zones.py | 16 +- novaclient/tests/fixture_data/base.py | 4 +- novaclient/tests/fixture_data/certs.py | 15 +- novaclient/tests/fixture_data/client.py | 15 +- novaclient/tests/fixture_data/cloudpipe.py | 25 +- novaclient/tests/fixture_data/fixedips.py | 20 +- novaclient/tests/fixture_data/floatingips.py | 142 +++++----- novaclient/tests/fixture_data/fping.py | 15 +- novaclient/tests/fixture_data/hosts.py | 94 +++---- novaclient/tests/fixture_data/hypervisors.py | 61 ++-- novaclient/tests/fixture_data/images.py | 85 +++--- novaclient/tests/fixture_data/keypairs.py | 31 +-- novaclient/tests/fixture_data/limits.py | 10 +- novaclient/tests/fixture_data/networks.py | 37 ++- novaclient/tests/fixture_data/quotas.py | 51 ++-- .../fixture_data/security_group_rules.py | 26 +- .../tests/fixture_data/security_groups.py | 46 +-- .../tests/fixture_data/server_groups.py | 30 +- novaclient/tests/fixture_data/servers.py | 261 +++++++++--------- novaclient/tests/utils.py | 16 +- novaclient/tests/v1_1/test_agents.py | 10 +- novaclient/tests/v1_1/test_servers.py | 12 +- test-requirements.txt | 2 +- 25 files changed, 517 insertions(+), 558 deletions(-) diff --git a/novaclient/tests/fixture_data/agents.py b/novaclient/tests/fixture_data/agents.py index a21f473de..f1c2aed01 100644 --- a/novaclient/tests/fixture_data/agents.py +++ b/novaclient/tests/fixture_data/agents.py @@ -10,9 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import httpretty - -from novaclient.openstack.common import jsonutils from novaclient.tests.fixture_data import base @@ -35,9 +32,9 @@ def setUp(self): } } - httpretty.register_uri(httpretty.POST, self.url(), - body=jsonutils.dumps(post_os_agents), - content_type='application/json') + self.requests.register_uri('POST', self.url(), + json=post_os_agents, + headers=self.json_headers) put_os_agents_1 = { "agent": { @@ -48,10 +45,10 @@ def setUp(self): } } - httpretty.register_uri(httpretty.PUT, self.url(1), - body=jsonutils.dumps(put_os_agents_1), - content_type='application/json') + self.requests.register_uri('PUT', self.url(1), + json=put_os_agents_1, + headers=self.json_headers) - httpretty.register_uri(httpretty.DELETE, self.url(1), - content_type='application/json', - status=202) + self.requests.register_uri('DELETE', self.url(1), + headers=self.json_headers, + status_code=202) diff --git a/novaclient/tests/fixture_data/aggregates.py b/novaclient/tests/fixture_data/aggregates.py index 33e30905f..26f00d7ac 100644 --- a/novaclient/tests/fixture_data/aggregates.py +++ b/novaclient/tests/fixture_data/aggregates.py @@ -10,9 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import httpretty - -from novaclient.openstack.common import jsonutils from novaclient.tests.fixture_data import base @@ -32,21 +29,24 @@ def setUp(self): 'availability_zone': 'nova1'}, ]} - httpretty.register_uri(httpretty.GET, self.url(), - body=jsonutils.dumps(get_os_aggregates), - content_type='application/json') + self.requests.register_uri('GET', self.url(), + json=get_os_aggregates, + headers=self.json_headers) - r = jsonutils.dumps({'aggregate': get_os_aggregates['aggregates'][0]}) + get_aggregates_1 = {'aggregate': get_os_aggregates['aggregates'][0]} - httpretty.register_uri(httpretty.POST, self.url(), body=r, - content_type='application/json') + self.requests.register_uri('POST', self.url(), + json=get_aggregates_1, + headers=self.json_headers) for agg_id in (1, 2): - for method in (httpretty.GET, httpretty.PUT): - httpretty.register_uri(method, self.url(agg_id), body=r, - content_type='application/json') + for method in ('GET', 'PUT'): + self.requests.register_uri(method, self.url(agg_id), + json=get_aggregates_1, + headers=self.json_headers) - httpretty.register_uri(httpretty.POST, self.url(agg_id, 'action'), - body=r, content_type='application/json') + self.requests.register_uri('POST', self.url(agg_id, 'action'), + json=get_aggregates_1, + headers=self.json_headers) - httpretty.register_uri(httpretty.DELETE, self.url(1), status=202) + self.requests.register_uri('DELETE', self.url(1), status_code=202) diff --git a/novaclient/tests/fixture_data/availability_zones.py b/novaclient/tests/fixture_data/availability_zones.py index c23788f51..451a64b2a 100644 --- a/novaclient/tests/fixture_data/availability_zones.py +++ b/novaclient/tests/fixture_data/availability_zones.py @@ -10,9 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import httpretty - -from novaclient.openstack.common import jsonutils from novaclient.tests.fixture_data import base @@ -41,9 +38,10 @@ def setUp(self): } ] } - httpretty.register_uri(httpretty.GET, self.url(), - body=jsonutils.dumps(get_os_availability_zone), - content_type='application/json') + + self.requests.register_uri('GET', self.url(), + json=get_os_availability_zone, + headers=self.json_headers) get_os_zone_detail = { self.zone_info_key: [ @@ -88,9 +86,9 @@ def setUp(self): ] } - httpretty.register_uri(httpretty.GET, self.url('detail'), - body=jsonutils.dumps(get_os_zone_detail), - content_type='application/json') + self.requests.register_uri('GET', self.url('detail'), + json=get_os_zone_detail, + headers=self.json_headers) class V3(V1): diff --git a/novaclient/tests/fixture_data/base.py b/novaclient/tests/fixture_data/base.py index 72f46d1dd..6a2e238b5 100644 --- a/novaclient/tests/fixture_data/base.py +++ b/novaclient/tests/fixture_data/base.py @@ -19,9 +19,11 @@ class Fixture(fixtures.Fixture): base_url = None + json_headers = {'Content-Type': 'application/json'} - def __init__(self, compute_url=COMPUTE_URL): + def __init__(self, requests, compute_url=COMPUTE_URL): super(Fixture, self).__init__() + self.requests = requests self.compute_url = compute_url def url(self, *args, **kwargs): diff --git a/novaclient/tests/fixture_data/certs.py b/novaclient/tests/fixture_data/certs.py index 3ce0f0f07..40421ca1b 100644 --- a/novaclient/tests/fixture_data/certs.py +++ b/novaclient/tests/fixture_data/certs.py @@ -10,9 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import httpretty - -from novaclient.openstack.common import jsonutils from novaclient.tests.fixture_data import base @@ -43,9 +40,9 @@ def setUp(self): 'data': 'foo' } } - httpretty.register_uri(httpretty.GET, self.url('root'), - body=jsonutils.dumps(get_os_certificate), - content_type='application/json') + self.requests.register_uri('GET', self.url('root'), + json=get_os_certificate, + headers=self.json_headers) post_os_certificates = { 'certificate': { @@ -53,6 +50,6 @@ def setUp(self): 'data': 'bar' } } - httpretty.register_uri(httpretty.POST, self.url(), - body=jsonutils.dumps(post_os_certificates), - content_type='application/json') + self.requests.register_uri('POST', self.url(), + json=post_os_certificates, + headers=self.json_headers) diff --git a/novaclient/tests/fixture_data/client.py b/novaclient/tests/fixture_data/client.py index c4ca3ec38..91aa47f21 100644 --- a/novaclient/tests/fixture_data/client.py +++ b/novaclient/tests/fixture_data/client.py @@ -11,11 +11,9 @@ # under the License. import fixtures -import httpretty from keystoneclient.auth.identity import v2 from keystoneclient import session -from novaclient.openstack.common import jsonutils from novaclient.v1_1 import client as v1_1client from novaclient.v3 import client as v3client @@ -25,11 +23,13 @@ class V1(fixtures.Fixture): - def __init__(self, compute_url=COMPUTE_URL, identity_url=IDENTITY_URL): + def __init__(self, requests, + compute_url=COMPUTE_URL, identity_url=IDENTITY_URL): super(V1, self).__init__() self.identity_url = identity_url self.compute_url = compute_url self.client = None + self.requests = requests self.token = { 'access': { @@ -86,13 +86,12 @@ def __init__(self, compute_url=COMPUTE_URL, identity_url=IDENTITY_URL): def setUp(self): super(V1, self).setUp() - httpretty.enable() - self.addCleanup(httpretty.disable) auth_url = '%s/tokens' % self.identity_url - httpretty.register_uri(httpretty.POST, auth_url, - body=jsonutils.dumps(self.token), - content_type='application/json') + headers = {'X-Content-Type': 'application/json'} + self.requests.register_uri('POST', auth_url, + json=self.token, + headers=headers) self.client = self.new_client() def new_client(self): diff --git a/novaclient/tests/fixture_data/cloudpipe.py b/novaclient/tests/fixture_data/cloudpipe.py index fffd2a1fe..32b5c210c 100644 --- a/novaclient/tests/fixture_data/cloudpipe.py +++ b/novaclient/tests/fixture_data/cloudpipe.py @@ -10,9 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import httpretty - -from novaclient.openstack.common import jsonutils from novaclient.tests.fixture_data import base @@ -24,17 +21,17 @@ def setUp(self): super(Fixture, self).setUp() get_os_cloudpipe = {'cloudpipes': [{'project_id': 1}]} - httpretty.register_uri(httpretty.GET, self.url(), - body=jsonutils.dumps(get_os_cloudpipe), - content_type='application/json') + self.requests.register_uri('GET', self.url(), + json=get_os_cloudpipe, + headers=self.json_headers) instance_id = '9d5824aa-20e6-4b9f-b967-76a699fc51fd' post_os_cloudpipe = {'instance_id': instance_id} - httpretty.register_uri(httpretty.POST, self.url(), - body=jsonutils.dumps(post_os_cloudpipe), - content_type='application/json', - status=202) - - httpretty.register_uri(httpretty.PUT, self.url('configure-project'), - content_type='application/json', - status=202) + self.requests.register_uri('POST', self.url(), + json=post_os_cloudpipe, + headers=self.json_headers, + status_code=202) + + self.requests.register_uri('PUT', self.url('configure-project'), + headers=self.json_headers, + status_code=202) diff --git a/novaclient/tests/fixture_data/fixedips.py b/novaclient/tests/fixture_data/fixedips.py index 64a36d213..71f18a4e6 100644 --- a/novaclient/tests/fixture_data/fixedips.py +++ b/novaclient/tests/fixture_data/fixedips.py @@ -10,9 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import httpretty - -from novaclient.openstack.common import jsonutils from novaclient.tests.fixture_data import base @@ -31,11 +28,12 @@ def setUp(self): 'host': 'bar' } } - httpretty.register_uri(httpretty.GET, self.url('192.168.1.1'), - body=jsonutils.dumps(get_os_fixed_ips), - content_type='application/json') - - httpretty.register_uri(httpretty.POST, - self.url('192.168.1.1', 'action'), - content_type='application/json', - status=202) + + self.requests.register_uri('GET', self.url('192.168.1.1'), + json=get_os_fixed_ips, + headers=self.json_headers) + + self.requests.register_uri('POST', + self.url('192.168.1.1', 'action'), + headers=self.json_headers, + status_code=202) diff --git a/novaclient/tests/fixture_data/floatingips.py b/novaclient/tests/fixture_data/floatingips.py index 8dd590427..d2c1b8016 100644 --- a/novaclient/tests/fixture_data/floatingips.py +++ b/novaclient/tests/fixture_data/floatingips.py @@ -10,8 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import httpretty - from novaclient.openstack.common import jsonutils from novaclient.tests import fakes from novaclient.tests.fixture_data import base @@ -28,29 +26,28 @@ def setUp(self): {'id': 2, 'fixed_ip': '10.0.0.2', 'ip': '11.0.0.2'}] get_os_floating_ips = {'floating_ips': floating_ips} - httpretty.register_uri(httpretty.GET, self.url(), - body=jsonutils.dumps(get_os_floating_ips), - content_type='application/json') + self.requests.register_uri('GET', self.url(), + json=get_os_floating_ips, + headers=self.json_headers) for ip in floating_ips: get_os_floating_ip = {'floating_ip': ip} - httpretty.register_uri(httpretty.GET, self.url(ip['id']), - body=jsonutils.dumps(get_os_floating_ip), - content_type='application/json') + self.requests.register_uri('GET', self.url(ip['id']), + json=get_os_floating_ip, + headers=self.json_headers) - httpretty.register_uri(httpretty.DELETE, self.url(ip['id']), - content_type='application/json', - status=204) + self.requests.register_uri('DELETE', self.url(ip['id']), + headers=self.json_headers, + status_code=204) - def post_os_floating_ips(request, url, headers): - body = jsonutils.loads(request.body.decode('utf-8')) + def post_os_floating_ips(request, context): + body = jsonutils.loads(request.body) ip = floating_ips[0].copy() ip['pool'] = body.get('pool') - ip = jsonutils.dumps({'floating_ip': ip}) - return 200, headers, ip - httpretty.register_uri(httpretty.POST, self.url(), - body=post_os_floating_ips, - content_type='application/json') + return {'floating_ip': ip} + self.requests.register_uri('POST', self.url(), + json=post_os_floating_ips, + headers=self.json_headers) class DNSFixture(base.Fixture): @@ -66,10 +63,10 @@ def setUp(self): {'domain': 'example.com'} ] } - httpretty.register_uri(httpretty.GET, self.url(), - body=jsonutils.dumps(get_os_floating_ip_dns), - content_type='application/json', - status=205) + self.requests.register_uri('GET', self.url(), + json=get_os_floating_ip_dns, + headers=self.json_headers, + status_code=205) get_dns_testdomain_entries_testname = { 'dns_entry': { @@ -80,31 +77,30 @@ def setUp(self): } } url = self.url('testdomain', 'entries', 'testname') - body = jsonutils.dumps(get_dns_testdomain_entries_testname) - httpretty.register_uri(httpretty.GET, url, - body=body, - content_type='application/json', - status=205) + self.requests.register_uri('GET', url, + json=get_dns_testdomain_entries_testname, + headers=self.json_headers, + status_code=205) - httpretty.register_uri(httpretty.DELETE, self.url('testdomain'), - status=200) + self.requests.register_uri('DELETE', self.url('testdomain')) url = self.url('testdomain', 'entries', 'testname') - httpretty.register_uri(httpretty.DELETE, url, status=200) + self.requests.register_uri('DELETE', url) - def put_dns_testdomain_entries_testname(request, url, headers): - body = jsonutils.loads(request.body.decode('utf-8')) + def put_dns_testdomain_entries_testname(request, context): + body = jsonutils.loads(request.body) fakes.assert_has_keys(body['dns_entry'], required=['ip', 'dns_type']) - return 205, headers, request.body - httpretty.register_uri(httpretty.PUT, url, - body=put_dns_testdomain_entries_testname, - content_type='application/json') + context.status_code = 205 + return request.body + self.requests.register_uri('PUT', url, + body=put_dns_testdomain_entries_testname, + headers=self.json_headers) url = self.url('testdomain', 'entries') - httpretty.register_uri(httpretty.GET, url, status=404) + self.requests.register_uri('GET', url, status_code=404) - get_os_floating_ip_dns_testdomain_entries = { + get_os_floating_ip_dns_testdomain = { 'dns_entries': [ { 'dns_entry': { @@ -124,14 +120,13 @@ def put_dns_testdomain_entries_testname(request, url, headers): }, ] } - body = jsonutils.dumps(get_os_floating_ip_dns_testdomain_entries) - httpretty.register_uri(httpretty.GET, url + '?ip=1.2.3.4', - body=body, - status=205, - content_type='application/json') - - def put_os_floating_ip_dns_testdomain(request, url, headers): - body = jsonutils.loads(request.body.decode('utf-8')) + self.requests.register_uri('GET', url + '?ip=1.2.3.4', + json=get_os_floating_ip_dns_testdomain, + status_code=205, + headers=self.json_headers) + + def put_os_floating_ip_dns_testdomain(request, context): + body = jsonutils.loads(request.body) if body['domain_entry']['scope'] == 'private': fakes.assert_has_keys(body['domain_entry'], required=['availability_zone', 'scope']) @@ -142,11 +137,12 @@ def put_os_floating_ip_dns_testdomain(request, url, headers): fakes.assert_has_keys(body['domain_entry'], required=['project', 'scope']) - headers['Content-Type'] = 'application/json' - return (205, headers, request.body) + return request.body - httpretty.register_uri(httpretty.PUT, self.url('testdomain'), - body=put_os_floating_ip_dns_testdomain) + self.requests.register_uri('PUT', self.url('testdomain'), + body=put_os_floating_ip_dns_testdomain, + status_code=205, + headers=self.json_headers) class BulkFixture(base.Fixture): @@ -162,40 +158,38 @@ def setUp(self): {'id': 2, 'fixed_ip': '10.0.0.2', 'ip': '11.0.0.2'}, ] } - httpretty.register_uri(httpretty.GET, self.url(), - body=jsonutils.dumps(get_os_floating_ips_bulk), - content_type='application/json') - httpretty.register_uri(httpretty.GET, self.url('testHost'), - body=jsonutils.dumps(get_os_floating_ips_bulk), - content_type='application/json') - - def put_os_floating_ips_bulk_delete(request, url, headers): - body = jsonutils.loads(request.body.decode('utf-8')) + self.requests.register_uri('GET', self.url(), + json=get_os_floating_ips_bulk, + headers=self.json_headers) + self.requests.register_uri('GET', self.url('testHost'), + json=get_os_floating_ips_bulk, + headers=self.json_headers) + + def put_os_floating_ips_bulk_delete(request, context): + body = jsonutils.loads(request.body) ip_range = body.get('ip_range') - data = {'floating_ips_bulk_delete': ip_range} - return 200, headers, jsonutils.dumps(data) + return {'floating_ips_bulk_delete': ip_range} - httpretty.register_uri(httpretty.PUT, self.url('delete'), - body=put_os_floating_ips_bulk_delete, - content_type='application/json') + self.requests.register_uri('PUT', self.url('delete'), + json=put_os_floating_ips_bulk_delete, + headers=self.json_headers) - def post_os_floating_ips_bulk(request, url, headers): - body = jsonutils.loads(request.body.decode('utf-8')) + def post_os_floating_ips_bulk(request, context): + body = jsonutils.loads(request.body) params = body.get('floating_ips_bulk_create') pool = params.get('pool', 'defaultPool') interface = params.get('interface', 'defaultInterface') - data = { + return { 'floating_ips_bulk_create': { 'ip_range': '192.168.1.0/30', 'pool': pool, 'interface': interface } } - return 200, headers, jsonutils.dumps(data) - httpretty.register_uri(httpretty.POST, self.url(), - body=post_os_floating_ips_bulk, - content_type='application/json') + self.requests.register_uri('POST', self.url(), + json=post_os_floating_ips_bulk, + headers=self.json_headers) class PoolsFixture(base.Fixture): @@ -211,6 +205,6 @@ def setUp(self): {'name': 'bar'} ] } - httpretty.register_uri(httpretty.GET, self.url(), - body=jsonutils.dumps(get_os_floating_ip_pools), - content_type='application/json') + self.requests.register_uri('GET', self.url(), + json=get_os_floating_ip_pools, + headers=self.json_headers) diff --git a/novaclient/tests/fixture_data/fping.py b/novaclient/tests/fixture_data/fping.py index 9542f83d4..837db6daa 100644 --- a/novaclient/tests/fixture_data/fping.py +++ b/novaclient/tests/fixture_data/fping.py @@ -10,9 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import httpretty - -from novaclient.openstack.common import jsonutils from novaclient.tests.fixture_data import base @@ -30,9 +27,9 @@ def setUp(self): "alive": True, } } - httpretty.register_uri(httpretty.GET, self.url(1), - body=jsonutils.dumps(get_os_fping_1), - content_type='application/json') + self.requests.register_uri('GET', self.url(1), + json=get_os_fping_1, + headers=self.json_headers) get_os_fping = { 'servers': [ @@ -44,6 +41,6 @@ def setUp(self): }, ] } - httpretty.register_uri(httpretty.GET, self.url(), - body=jsonutils.dumps(get_os_fping), - content_type='application/json') + self.requests.register_uri('GET', self.url(), + json=get_os_fping, + headers=self.json_headers) diff --git a/novaclient/tests/fixture_data/hosts.py b/novaclient/tests/fixture_data/hosts.py index 193a5bb67..6ac217cb0 100644 --- a/novaclient/tests/fixture_data/hosts.py +++ b/novaclient/tests/fixture_data/hosts.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import httpretty from six.moves.urllib import parse from novaclient.openstack.common import jsonutils @@ -36,12 +35,15 @@ def setUp(self): 'cpu': 1, 'memory_mb': 2048, 'disk_gb': 30}} ] } - httpretty.register_uri(httpretty.GET, self.url('host'), - body=jsonutils.dumps(get_os_hosts_host), - content_type='application/json') - def get_os_hosts(request, url, headers): - host, query = parse.splitquery(url) + headers = {'Content-Type': 'application/json'} + + self.requests.register_uri('GET', self.url('host'), + json=get_os_hosts_host, + headers=headers) + + def get_os_hosts(request, context): + host, query = parse.splitquery(request.url) zone = 'nova1' if query: @@ -51,7 +53,7 @@ def get_os_hosts(request, url, headers): except Exception: pass - data = { + return { 'hosts': [ { 'host': 'host1', @@ -65,56 +67,52 @@ def get_os_hosts(request, url, headers): } ] } - return 200, headers, jsonutils.dumps(data) - httpretty.register_uri(httpretty.GET, self.url(), - body=get_os_hosts, - content_type='application/json') + self.requests.register_uri('GET', self.url(), + json=get_os_hosts, + headers=headers) get_os_hosts_sample_host = { 'host': [ {'resource': {'host': 'sample_host'}} ], } - httpretty.register_uri(httpretty.GET, self.url('sample_host'), - body=jsonutils.dumps(get_os_hosts_sample_host), - content_type='application/json') - - httpretty.register_uri(httpretty.PUT, self.url('sample_host', 1), - body=jsonutils.dumps(self.put_host_1()), - content_type='application/json') - - httpretty.register_uri(httpretty.PUT, self.url('sample_host', 2), - body=jsonutils.dumps(self.put_host_2()), - content_type='application/json') - - httpretty.register_uri(httpretty.PUT, self.url('sample_host', 3), - body=jsonutils.dumps(self.put_host_3()), - content_type='application/json') - - url = self.url('sample_host', 'reboot') - httpretty.register_uri(httpretty.GET, url, - body=jsonutils.dumps(self.get_host_reboot()), - content_type='application/json') - - url = self.url('sample_host', 'startup') - httpretty.register_uri(httpretty.GET, url, - body=jsonutils.dumps(self.get_host_startup()), - content_type='application/json') - - url = self.url('sample_host', 'shutdown') - httpretty.register_uri(httpretty.GET, url, - body=jsonutils.dumps(self.get_host_shutdown()), - content_type='application/json') - - def put_os_hosts_sample_host(request, url, headers): + self.requests.register_uri('GET', self.url('sample_host'), + json=get_os_hosts_sample_host, + headers=headers) + + self.requests.register_uri('PUT', self.url('sample_host', 1), + json=self.put_host_1(), + headers=headers) + + self.requests.register_uri('PUT', self.url('sample_host', 2), + json=self.put_host_2(), + headers=headers) + + self.requests.register_uri('PUT', self.url('sample_host', 3), + json=self.put_host_3(), + headers=headers) + + self.requests.register_uri('GET', self.url('sample_host', 'reboot'), + json=self.get_host_reboot(), + headers=headers) + + self.requests.register_uri('GET', self.url('sample_host', 'startup'), + json=self.get_host_startup(), + headers=headers) + + self.requests.register_uri('GET', self.url('sample_host', 'shutdown'), + json=self.get_host_shutdown(), + headers=headers) + + def put_os_hosts_sample_host(request, context): result = {'host': 'dummy'} - result.update(jsonutils.loads(request.body.decode('utf-8'))) - return 200, headers, jsonutils.dumps(result) + result.update(jsonutils.loads(request.body)) + return result - httpretty.register_uri(httpretty.PUT, self.url('sample_host'), - body=put_os_hosts_sample_host, - content_type='application/json') + self.requests.register_uri('PUT', self.url('sample_host'), + json=put_os_hosts_sample_host, + headers=headers) class V1(BaseFixture): diff --git a/novaclient/tests/fixture_data/hypervisors.py b/novaclient/tests/fixture_data/hypervisors.py index 068236a2d..c3f6f7210 100644 --- a/novaclient/tests/fixture_data/hypervisors.py +++ b/novaclient/tests/fixture_data/hypervisors.py @@ -10,9 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import httpretty - -from novaclient.openstack.common import jsonutils from novaclient.tests.fixture_data import base @@ -30,9 +27,11 @@ def setUp(self): ] } - httpretty.register_uri(httpretty.GET, self.url(), - body=jsonutils.dumps(get_os_hypervisors), - content_type='application/json') + self.headers = {'Content-Type': 'application/json'} + + self.requests.register_uri('GET', self.url(), + json=get_os_hypervisors, + headers=self.headers) get_os_hypervisors_detail = { 'hypervisors': [ @@ -83,9 +82,9 @@ def setUp(self): ] } - httpretty.register_uri(httpretty.GET, self.url('detail'), - body=jsonutils.dumps(get_os_hypervisors_detail), - content_type='application/json') + self.requests.register_uri('GET', self.url('detail'), + json=get_os_hypervisors_detail, + headers=self.headers) get_os_hypervisors_stats = { 'hypervisor_statistics': { @@ -104,9 +103,9 @@ def setUp(self): } } - httpretty.register_uri(httpretty.GET, self.url('statistics'), - body=jsonutils.dumps(get_os_hypervisors_stats), - content_type='application/json') + self.requests.register_uri('GET', self.url('statistics'), + json=get_os_hypervisors_stats, + headers=self.headers) get_os_hypervisors_search = { 'hypervisors': [ @@ -115,9 +114,9 @@ def setUp(self): ] } - httpretty.register_uri(httpretty.GET, self.url('hyper', 'search'), - body=jsonutils.dumps(get_os_hypervisors_search), - content_type='application/json') + self.requests.register_uri('GET', self.url('hyper', 'search'), + json=get_os_hypervisors_search, + headers=self.headers) get_hyper_server = { 'hypervisors': [ @@ -140,9 +139,9 @@ def setUp(self): ] } - httpretty.register_uri(httpretty.GET, self.url('hyper', 'servers'), - body=jsonutils.dumps(get_hyper_server), - content_type='application/json') + self.requests.register_uri('GET', self.url('hyper', 'servers'), + json=get_hyper_server, + headers=self.headers) get_os_hypervisors_1234 = { 'hypervisor': { @@ -166,9 +165,9 @@ def setUp(self): } } - httpretty.register_uri(httpretty.GET, self.url(1234), - body=jsonutils.dumps(get_os_hypervisors_1234), - content_type='application/json') + self.requests.register_uri('GET', self.url(1234), + json=get_os_hypervisors_1234, + headers=self.headers) get_os_hypervisors_uptime = { 'hypervisor': { @@ -178,9 +177,9 @@ def setUp(self): } } - httpretty.register_uri(httpretty.GET, self.url(1234, 'uptime'), - body=jsonutils.dumps(get_os_hypervisors_uptime), - content_type='application/json') + self.requests.register_uri('GET', self.url(1234, 'uptime'), + json=get_os_hypervisors_uptime, + headers=self.headers) class V3(V1): @@ -195,10 +194,10 @@ def setUp(self): ] } - httpretty.register_uri(httpretty.GET, - self.url('search', query='hyper'), - body=jsonutils.dumps(get_os_hypervisors_search), - content_type='application/json') + self.requests.register_uri('GET', + self.url('search', query='hyper'), + json=get_os_hypervisors_search, + headers=self.headers) get_1234_servers = { 'hypervisor': { @@ -211,6 +210,6 @@ def setUp(self): }, } - httpretty.register_uri(httpretty.GET, self.url(1234, 'servers'), - body=jsonutils.dumps(get_1234_servers), - content_type='application/json') + self.requests.register_uri('GET', self.url(1234, 'servers'), + json=get_1234_servers, + headers=self.headers) diff --git a/novaclient/tests/fixture_data/images.py b/novaclient/tests/fixture_data/images.py index 09a134e5f..40cfd0c94 100644 --- a/novaclient/tests/fixture_data/images.py +++ b/novaclient/tests/fixture_data/images.py @@ -10,8 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import httpretty - from novaclient.openstack.common import jsonutils from novaclient.tests import fakes from novaclient.tests.fixture_data import base @@ -31,9 +29,11 @@ def setUp(self): ] } - httpretty.register_uri(httpretty.GET, self.url(), - body=jsonutils.dumps(get_images), - content_type='application/json') + headers = {'Content-Type': 'application/json'} + + self.requests.register_uri('GET', self.url(), + json=get_images, + headers=headers) image_1 = { 'id': 1, @@ -58,60 +58,53 @@ def setUp(self): "links": {}, } - get_images_detail = {'images': [image_1, image_2]} - - httpretty.register_uri(httpretty.GET, self.url('detail'), - body=jsonutils.dumps(get_images_detail), - content_type='application/json') - - get_images_1 = {'image': image_1} - - httpretty.register_uri(httpretty.GET, self.url(1), - body=jsonutils.dumps(get_images_1), - content_type='application/json') + self.requests.register_uri('GET', self.url('detail'), + json={'images': [image_1, image_2]}, + headers=headers) - get_images_2 = {'image': image_2} + self.requests.register_uri('GET', self.url(1), + json={'image': image_1}, + headers=headers) - httpretty.register_uri(httpretty.GET, self.url(2), - body=jsonutils.dumps(get_images_2), - content_type='application/json') + self.requests.register_uri('GET', self.url(2), + json={'image': image_2}, + headers=headers) - httpretty.register_uri(httpretty.GET, self.url(456), - body=jsonutils.dumps(get_images_2), - content_type='application/json') + self.requests.register_uri('GET', self.url(456), + json={'image': image_2}, + headers=headers) - def post_images(request, url, headers): - body = jsonutils.loads(request.body.decode('utf-8')) + def post_images(request, context): + body = jsonutils.loads(request.body) assert list(body) == ['image'] fakes.assert_has_keys(body['image'], required=['serverId', 'name']) - return 202, headers, jsonutils.dumps(images_1) + return images_1 - httpretty.register_uri(httpretty.POST, self.url(), - body=post_images, - content_type='application/json') + self.requests.register_uri('POST', self.url(), + json=post_images, + headers=headers, + status_code=202) - def post_images_1_metadata(request, url, headers): - body = jsonutils.loads(request.body.decode('utf-8')) + def post_images_1_metadata(request, context): + body = jsonutils.loads(request.body) assert list(body) == ['metadata'] fakes.assert_has_keys(body['metadata'], required=['test_key']) - data = jsonutils.dumps({'metadata': image_1['metadata']}) - return 200, headers, data + return {'metadata': image_1['metadata']} - httpretty.register_uri(httpretty.POST, self.url(1, 'metadata'), - body=post_images_1_metadata, - content_type='application/json') + self.requests.register_uri('POST', self.url(1, 'metadata'), + json=post_images_1_metadata, + headers=headers) for u in (1, 2, '1/metadata/test_key'): - httpretty.register_uri(httpretty.DELETE, self.url(u), - status=204) - - httpretty.register_uri(httpretty.HEAD, self.url(1), status=200, - x_image_meta_id=1, - x_image_meta_name='CentOS 5.2', - x_image_meta_updated='2010-10-10T12:00:00Z', - x_image_meta_created='2010-10-10T12:00:00Z', - x_image_meta_status='ACTIVE', - x_image_meta_property_test_key='test_value') + self.requests.register_uri('DELETE', self.url(u), status_code=204) + + image_headers = {'x-image-meta-id': '1', + 'x-image-meta-name': 'CentOS 5.2', + 'x-image-meta-updated': '2010-10-10T12:00:00Z', + 'x-image-meta-created': '2010-10-10T12:00:00Z', + 'x-image-meta-status': 'ACTIVE', + 'x-image-meta-property-test-key': 'test_value'} + self.requests.register_uri('HEAD', self.url(1), headers=image_headers) class V3(V1): diff --git a/novaclient/tests/fixture_data/keypairs.py b/novaclient/tests/fixture_data/keypairs.py index b3400a76c..cc8921beb 100644 --- a/novaclient/tests/fixture_data/keypairs.py +++ b/novaclient/tests/fixture_data/keypairs.py @@ -10,9 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. - -import httpretty - from novaclient.openstack.common import jsonutils from novaclient.tests import fakes from novaclient.tests.fixture_data import base @@ -26,25 +23,27 @@ def setUp(self): super(V1, self).setUp() keypair = {'fingerprint': 'FAKE_KEYPAIR', 'name': 'test'} - httpretty.register_uri(httpretty.GET, self.url(), - body=jsonutils.dumps({'keypairs': [keypair]}), - content_type='application/json') + headers = {'Content-Type': 'application/json'} + + self.requests.register_uri('GET', self.url(), + json={'keypairs': [keypair]}, + headers=headers) - httpretty.register_uri(httpretty.GET, self.url('test'), - body=jsonutils.dumps({'keypair': keypair}), - content_type='application/json') + self.requests.register_uri('GET', self.url('test'), + json={'keypair': keypair}, + headers=headers) - httpretty.register_uri(httpretty.DELETE, self.url('test'), status=202) + self.requests.register_uri('DELETE', self.url('test'), status_code=202) - def post_os_keypairs(request, url, headers): - body = jsonutils.loads(request.body.decode('utf-8')) + def post_os_keypairs(request, context): + body = jsonutils.loads(request.body) assert list(body) == ['keypair'] fakes.assert_has_keys(body['keypair'], required=['name']) - return 202, headers, jsonutils.dumps({'keypair': keypair}) + return {'keypair': keypair} - httpretty.register_uri(httpretty.POST, self.url(), - body=post_os_keypairs, - content_type='application/json') + self.requests.register_uri('POST', self.url(), + json=post_os_keypairs, + headers=headers) class V3(V1): diff --git a/novaclient/tests/fixture_data/limits.py b/novaclient/tests/fixture_data/limits.py index 5eca22e4d..0b9488d3f 100644 --- a/novaclient/tests/fixture_data/limits.py +++ b/novaclient/tests/fixture_data/limits.py @@ -10,9 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import httpretty - -from novaclient.openstack.common import jsonutils from novaclient.tests.fixture_data import base @@ -77,6 +74,7 @@ def setUp(self): }, } - httpretty.register_uri(httpretty.GET, self.url(), - body=jsonutils.dumps(get_limits), - content_type='application/json') + headers = {'Content-Type': 'application/json'} + self.requests.register_uri('GET', self.url(), + json=get_limits, + headers=headers) diff --git a/novaclient/tests/fixture_data/networks.py b/novaclient/tests/fixture_data/networks.py index 12bb4b1eb..df721b25d 100644 --- a/novaclient/tests/fixture_data/networks.py +++ b/novaclient/tests/fixture_data/networks.py @@ -10,8 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import httpretty - from novaclient.openstack.common import jsonutils from novaclient.tests.fixture_data import base @@ -34,29 +32,30 @@ def setUp(self): ] } - httpretty.register_uri(httpretty.GET, self.url(), - body=jsonutils.dumps(get_os_networks), - content_type='application/json') + headers = {'Content-Type': 'application/json'} + + self.requests.register_uri('GET', self.url(), + json=get_os_networks, + headers=headers) - def post_os_networks(request, url, headers): - body = jsonutils.loads(request.body.decode('utf-8')) - data = jsonutils.dumps({'network': body}) - return 202, headers, data + def post_os_networks(request, context): + body = jsonutils.loads(request.body) + return {'network': body} - httpretty.register_uri(httpretty.POST, self.url(), - body=post_os_networks, - content_type='application/json') + self.requests.register_uri("POST", self.url(), + json=post_os_networks, + headers=headers) get_os_networks_1 = {'network': {"label": "1", "cidr": "10.0.0.0/24"}} - httpretty.register_uri(httpretty.GET, self.url(1), - body=jsonutils.dumps(get_os_networks_1), - content_type='application/json') + self.requests.register_uri('GET', self.url(1), + json=get_os_networks_1, + headers=headers) - httpretty.register_uri(httpretty.DELETE, - self.url('networkdelete'), - stauts=202) + self.requests.register_uri('DELETE', + self.url('networkdelete'), + status_code=202) for u in ('add', 'networkdisassociate/action', 'networktest/action', '1/action', '2/action'): - httpretty.register_uri(httpretty.POST, self.url(u), stauts=202) + self.requests.register_uri('POST', self.url(u), status_code=202) diff --git a/novaclient/tests/fixture_data/quotas.py b/novaclient/tests/fixture_data/quotas.py index 70db079dd..3250f8843 100644 --- a/novaclient/tests/fixture_data/quotas.py +++ b/novaclient/tests/fixture_data/quotas.py @@ -10,9 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import httpretty - -from novaclient.openstack.common import jsonutils from novaclient.tests.fixture_data import base @@ -25,32 +22,32 @@ def setUp(self): uuid = '97f4c221-bff4-4578-b030-0df4ef119353' uuid2 = '97f4c221bff44578b0300df4ef119353' - test_json = jsonutils.dumps({'quota_set': self.test_quota('test')}) + test_json = {'quota_set': self.test_quota('test')} + self.headers = {'Content-Type': 'application/json'} for u in ('test', 'tenant-id', 'tenant-id/defaults', '%s/defaults' % uuid2): - httpretty.register_uri(httpretty.GET, self.url(u), - body=test_json, - content_type='application/json') - - quota_json = jsonutils.dumps({'quota_set': self.test_quota(uuid)}) - httpretty.register_uri(httpretty.PUT, self.url(uuid), - body=quota_json, - content_type='application/json') - httpretty.register_uri(httpretty.GET, self.url(uuid), - body=quota_json, - content_type='application/json') - - quota_json2 = jsonutils.dumps({'quota_set': self.test_quota(uuid2)}) - httpretty.register_uri(httpretty.PUT, self.url(uuid2), - body=quota_json2, - content_type='application/json') - httpretty.register_uri(httpretty.GET, self.url(uuid2), - body=quota_json2, - content_type='application/json') + self.requests.register_uri('GET', self.url(u), + json=test_json, + headers=self.headers) + + self.requests.register_uri('PUT', self.url(uuid), + json={'quota_set': self.test_quota(uuid)}, + headers=self.headers) + + self.requests.register_uri('GET', self.url(uuid), + json={'quota_set': self.test_quota(uuid)}, + headers=self.headers) + + self.requests.register_uri('PUT', self.url(uuid2), + json={'quota_set': self.test_quota(uuid2)}, + headers=self.headers) + self.requests.register_uri('GET', self.url(uuid2), + json={'quota_set': self.test_quota(uuid2)}, + headers=self.headers) for u in ('test', uuid2): - httpretty.register_uri(httpretty.DELETE, self.url(u), status=202) + self.requests.register_uri('DELETE', self.url(u), status_code=202) def test_quota(self, tenant_id='test'): return { @@ -82,6 +79,6 @@ def setUp(self): } } - httpretty.register_uri(httpretty.GET, self.url('test', 'detail'), - body=jsonutils.dumps(get_detail), - content_type='application/json') + self.requests.register_uri('GET', self.url('test', 'detail'), + json=get_detail, + headers=self.headers) diff --git a/novaclient/tests/fixture_data/security_group_rules.py b/novaclient/tests/fixture_data/security_group_rules.py index 303cfee7c..480c4fc6d 100644 --- a/novaclient/tests/fixture_data/security_group_rules.py +++ b/novaclient/tests/fixture_data/security_group_rules.py @@ -10,8 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import httpretty - from novaclient.openstack.common import jsonutils from novaclient.tests import fakes from novaclient.tests.fixture_data import base @@ -34,24 +32,26 @@ def setUp(self): 'cidr': '10.0.0.0/8' } - get_rules = {'security_group_rules': [rule]} - httpretty.register_uri(httpretty.GET, self.url(), - body=jsonutils.dumps(get_rules), - content_type='application/json') + headers = {'Content-Type': 'application/json'} + + self.requests.register_uri('GET', self.url(), + json={'security_group_rules': [rule]}, + headers=headers) for u in (1, 11, 12): - httpretty.register_uri(httpretty.DELETE, self.url(u), status=202) + self.requests.register_uri('DELETE', self.url(u), status_code=202) - def post_rules(request, url, headers): - body = jsonutils.loads(request.body.decode('utf-8')) + def post_rules(request, context): + body = jsonutils.loads(request.body) assert list(body) == ['security_group_rule'] fakes.assert_has_keys(body['security_group_rule'], required=['parent_group_id'], optional=['group_id', 'ip_protocol', 'from_port', 'to_port', 'cidr']) - return 202, headers, jsonutils.dumps({'security_group_rule': rule}) + return {'security_group_rule': rule} - httpretty.register_uri(httpretty.POST, self.url(), - body=post_rules, - content_type='application/json') + self.requests.register_uri('POST', self.url(), + json=post_rules, + headers=headers, + status_code=202) diff --git a/novaclient/tests/fixture_data/security_groups.py b/novaclient/tests/fixture_data/security_groups.py index 9f0c271ad..7d1fed380 100644 --- a/novaclient/tests/fixture_data/security_groups.py +++ b/novaclient/tests/fixture_data/security_groups.py @@ -10,9 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. - -import httpretty - from novaclient.openstack.common import jsonutils from novaclient.tests import fakes from novaclient.tests.fixture_data import base @@ -64,36 +61,39 @@ def setUp(self): } get_groups = {'security_groups': [security_group_1, security_group_2]} - httpretty.register_uri(httpretty.GET, self.url(), - body=jsonutils.dumps(get_groups), - content_type='application/json') + headers = {'Content-Type': 'application/json'} + + self.requests.register_uri('GET', self.url(), + json=get_groups, + headers=headers) get_group_1 = {'security_group': security_group_1} - httpretty.register_uri(httpretty.GET, self.url(1), - body=jsonutils.dumps(get_group_1), - content_type='application/json') + self.requests.register_uri('GET', self.url(1), + json=get_group_1, + headers=headers) - httpretty.register_uri(httpretty.DELETE, self.url(1), status=202) + self.requests.register_uri('DELETE', self.url(1), status_code=202) - def post_os_security_groups(request, url, headers): - body = jsonutils.loads(request.body.decode('utf-8')) + def post_os_security_groups(request, context): + body = jsonutils.loads(request.body) assert list(body) == ['security_group'] fakes.assert_has_keys(body['security_group'], required=['name', 'description']) - r = jsonutils.dumps({'security_group': security_group_1}) - return 202, headers, r + return {'security_group': security_group_1} - httpretty.register_uri(httpretty.POST, self.url(), - body=post_os_security_groups, - content_type='application/json') + self.requests.register_uri('POST', self.url(), + json=post_os_security_groups, + headers=headers, + status_code=202) - def put_os_security_groups_1(request, url, headers): - body = jsonutils.loads(request.body.decode('utf-8')) + def put_os_security_groups_1(request, context): + body = jsonutils.loads(request.body) assert list(body) == ['security_group'] fakes.assert_has_keys(body['security_group'], required=['name', 'description']) - return 205, headers, request.body + return body - httpretty.register_uri(httpretty.PUT, self.url(1), - body=put_os_security_groups_1, - content_type='application/json') + self.requests.register_uri('PUT', self.url(1), + json=put_os_security_groups_1, + headers=headers, + status_code=205) diff --git a/novaclient/tests/fixture_data/server_groups.py b/novaclient/tests/fixture_data/server_groups.py index 65a0c78d2..47e307153 100644 --- a/novaclient/tests/fixture_data/server_groups.py +++ b/novaclient/tests/fixture_data/server_groups.py @@ -10,9 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. - -import httpretty - from novaclient.openstack.common import jsonutils from novaclient.tests.fixture_data import base @@ -54,22 +51,23 @@ def setUp(self): } ] - get_server_groups = {'server_groups': server_groups} - httpretty.register_uri(httpretty.GET, self.url(), - body=jsonutils.dumps(get_server_groups), - content_type='application/json') + headers = {'Content-Type': 'application/json'} + + self.requests.register_uri('GET', self.url(), + json={'server_groups': server_groups}, + headers=headers) server = server_groups[0] - server_json = jsonutils.dumps({'server_group': server}) + server_j = jsonutils.dumps({'server_group': server}) def _register(method, *args): - httpretty.register_uri(method, self.url(*args), body=server_json) + self.requests.register_uri(method, self.url(*args), text=server_j) - _register(httpretty.POST) - _register(httpretty.POST, server['id']) - _register(httpretty.GET, server['id']) - _register(httpretty.PUT, server['id']) - _register(httpretty.POST, server['id'], '/action') + _register('POST') + _register('POST', server['id']) + _register('GET', server['id']) + _register('PUT', server['id']) + _register('POST', server['id'], '/action') - httpretty.register_uri(httpretty.DELETE, self.url(server['id']), - status=202) + self.requests.register_uri('DELETE', self.url(server['id']), + status_code=202) diff --git a/novaclient/tests/fixture_data/servers.py b/novaclient/tests/fixture_data/servers.py index e862b4f69..6f753fc85 100644 --- a/novaclient/tests/fixture_data/servers.py +++ b/novaclient/tests/fixture_data/servers.py @@ -10,8 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import httpretty - from novaclient.openstack.common import jsonutils from novaclient.tests import fakes from novaclient.tests.fixture_data import base @@ -31,9 +29,9 @@ def setUp(self): ] } - httpretty.register_uri(httpretty.GET, self.url(), - body=jsonutils.dumps(get_servers), - content_type='application/json') + self.requests.register_uri('GET', self.url(), + json=get_servers, + headers=self.json_headers) self.server_1234 = { "id": 1234, @@ -151,48 +149,47 @@ def setUp(self): servers = [self.server_1234, self.server_5678, self.server_9012] get_servers_detail = {"servers": servers} - httpretty.register_uri(httpretty.GET, self.url('detail'), - body=jsonutils.dumps(get_servers_detail), - content_type='application/json') + self.requests.register_uri('GET', self.url('detail'), + json=get_servers_detail, + headers=self.json_headers) self.server_1235 = self.server_1234.copy() self.server_1235['id'] = 1235 self.server_1235['status'] = 'error' self.server_1235['fault'] = {'message': 'something went wrong!'} - servers.append(self.server_1235) - for s in servers: - httpretty.register_uri(httpretty.GET, self.url(s['id']), - body=jsonutils.dumps({'server': s}), - content_type='application/json') + for s in servers + [self.server_1235]: + self.requests.register_uri('GET', self.url(s['id']), + json={'server': s}, + headers=self.json_headers) for s in (1234, 5678): - httpretty.register_uri(httpretty.DELETE, self.url(s), status=202) + self.requests.register_uri('DELETE', self.url(s), status_code=202) for k in ('test_key', 'key1', 'key2'): - httpretty.register_uri(httpretty.DELETE, - self.url(1234, 'metadata', k), - status=204) - - metadata1 = jsonutils.dumps({'metadata': {'test_key': 'test_value'}}) - httpretty.register_uri(httpretty.POST, self.url(1234, 'metadata'), - body=metadata1, status=200, - content_type='application/json') - httpretty.register_uri(httpretty.PUT, - self.url(1234, 'metadata', 'test_key'), - body=metadata1, status=200, - content_type='application/json') - - self.diagnostic = jsonutils.dumps({'data': 'Fake diagnostics'}) - - metadata2 = jsonutils.dumps({'metadata': {'key1': 'val1'}}) + self.requests.register_uri('DELETE', + self.url(1234, 'metadata', k), + status_code=204) + + metadata1 = {'metadata': {'test_key': 'test_value'}} + self.requests.register_uri('POST', self.url(1234, 'metadata'), + json=metadata1, + headers=self.json_headers) + self.requests.register_uri('PUT', + self.url(1234, 'metadata', 'test_key'), + json=metadata1, + headers=self.json_headers) + + self.diagnostic = {'data': 'Fake diagnostics'} + + metadata2 = {'metadata': {'key1': 'val1'}} for u in ('uuid1', 'uuid2', 'uuid3', 'uuid4'): - httpretty.register_uri(httpretty.POST, self.url(u, 'metadata'), - body=metadata2, status=204) - httpretty.register_uri(httpretty.DELETE, - self.url(u, 'metadata', 'key1'), - body=self.diagnostic, - content_type='application/json') + self.requests.register_uri('POST', self.url(u, 'metadata'), + json=metadata2, status_code=204) + self.requests.register_uri('DELETE', + self.url(u, 'metadata', 'key1'), + json=self.diagnostic, + headers=self.json_headers) get_security_groups = { "security_groups": [{ @@ -203,18 +200,17 @@ def setUp(self): 'rules': []}] } - httpretty.register_uri(httpretty.GET, - self.url('1234', 'os-security-groups'), - body=jsonutils.dumps(get_security_groups), - status=200) + self.requests.register_uri('GET', + self.url('1234', 'os-security-groups'), + json=get_security_groups) - httpretty.register_uri(httpretty.POST, self.url(), - body=self.post_servers, - content_type='application/json') + self.requests.register_uri('POST', self.url(), + json=self.post_servers, + headers=self.json_headers) - httpretty.register_uri(httpretty.POST, self.url('1234', 'action'), - body=self.post_servers_1234_action, - content_type='application/json') + self.requests.register_uri('POST', self.url('1234', 'action'), + json=self.post_servers_1234_action, + headers=self.json_headers) get_os_interface = { "interfaceAttachments": [ @@ -235,30 +231,31 @@ def setUp(self): ] } - httpretty.register_uri(httpretty.GET, - self.url('1234', 'os-interface'), - body=jsonutils.dumps(get_os_interface), - content_type='application/json') + self.requests.register_uri('GET', + self.url('1234', 'os-interface'), + json=get_os_interface, + headers=self.json_headers) interface_data = {'interfaceAttachment': {}} - httpretty.register_uri(httpretty.POST, - self.url('1234', 'os-interface'), - body=jsonutils.dumps(interface_data), - content_type='application/json') + self.requests.register_uri('POST', + self.url('1234', 'os-interface'), + json=interface_data, + headers=self.json_headers) - def put_servers_1234(request, url, headers): - body = jsonutils.loads(request.body.decode('utf-8')) + def put_servers_1234(request, context): + body = jsonutils.loads(request.body) assert list(body) == ['server'] fakes.assert_has_keys(body['server'], optional=['name', 'adminPass']) - return 204, headers, request.body + return request.body - httpretty.register_uri(httpretty.PUT, self.url(1234), - body=put_servers_1234, - content_type='application/json') + self.requests.register_uri('PUT', self.url(1234), + body=put_servers_1234, + status_code=204, + headers=self.json_headers) - def post_os_volumes_boot(request, url, headers): - body = jsonutils.loads(request.body.decode('utf-8')) + def post_os_volumes_boot(request, context): + body = jsonutils.loads(request.body) assert (set(body.keys()) <= set(['server', 'os:scheduler_hints'])) @@ -277,23 +274,24 @@ def post_os_volumes_boot(request, url, headers): msg = "found extra keys: 'block_device_mapping'" raise AssertionError(msg) - return 202, headers, jsonutils.dumps({'server': self.server_9012}) + return {'server': self.server_9012} # NOTE(jamielennox): hack to make os_volumes mock go to the right place base_url = self.base_url self.base_url = None - httpretty.register_uri(httpretty.POST, self.url('os-volumes_boot'), - body=post_os_volumes_boot, - content_type='application/json') + self.requests.register_uri('POST', self.url('os-volumes_boot'), + json=post_os_volumes_boot, + status_code=202, + headers=self.json_headers) self.base_url = base_url # # Server password # - httpretty.register_uri(httpretty.DELETE, - self.url(1234, 'os-server-password'), - status=202) + self.requests.register_uri('DELETE', + self.url(1234, 'os-server-password'), + status_code=202) class V1(Base): @@ -306,29 +304,28 @@ def setUp(self): # add = self.server_1234['addresses'] - httpretty.register_uri(httpretty.GET, self.url(1234, 'ips'), - jsonutils.dumps({'addresses': add}), - content_type='application/json') + self.requests.register_uri('GET', self.url(1234, 'ips'), + json={'addresses': add}, + headers=self.json_headers) - httpretty.register_uri(httpretty.GET, self.url(1234, 'ips', 'public'), - jsonutils.dumps({'public': add['public']}), - content_type='application/json') + self.requests.register_uri('GET', self.url(1234, 'ips', 'public'), + json={'public': add['public']}, + headers=self.json_headers) - httpretty.register_uri(httpretty.GET, self.url(1234, 'ips', 'private'), - jsonutils.dumps({'private': add['private']}), - content_type='application/json') + self.requests.register_uri('GET', self.url(1234, 'ips', 'private'), + json={'private': add['private']}, + headers=self.json_headers) - httpretty.register_uri(httpretty.DELETE, - self.url(1234, 'ips', 'public', '1.2.3.4'), - status=202) + self.requests.register_uri('DELETE', + self.url(1234, 'ips', 'public', '1.2.3.4'), + status_code=202) - httpretty.register_uri(httpretty.GET, - self.url('1234', 'diagnostics'), - body=self.diagnostic, - status=200) + self.requests.register_uri('GET', + self.url('1234', 'diagnostics'), + json=self.diagnostic) - httpretty.register_uri(httpretty.DELETE, - self.url('1234', 'os-interface', 'port-id')) + self.requests.register_uri('DELETE', + self.url('1234', 'os-interface', 'port-id')) # Testing with the following password and key # @@ -351,12 +348,13 @@ def setUp(self): '/y5a6Z3/AoJZYGG7IH5WN88UROU3B9JZGFB2qtPLQTOvDMZLUhoPRIJeHiVSlo1N' 'tI2/++UsXVg3ow6ItqCJGgdNuGG5JB+bslDHWPxROpesEIHdczk46HCpHQN8f1sk' 'Hi/fmZZNQQqj1Ijq0caOIw=='} - httpretty.register_uri(httpretty.GET, - self.url(1234, 'os-server-password'), - jsonutils.dumps(get_server_password)) + self.requests.register_uri('GET', + self.url(1234, 'os-server-password'), + json=get_server_password) - def post_servers(self, request, url, headers): - body = jsonutils.loads(request.body.decode('utf-8')) + def post_servers(self, request, context): + body = jsonutils.loads(request.body) + context.status_code = 202 assert (set(body.keys()) <= set(['server', 'os:scheduler_hints'])) fakes.assert_has_keys(body['server'], @@ -370,12 +368,12 @@ def post_servers(self, request, url, headers): else: body = self.server_1234 - return 202, headers, jsonutils.dumps({'server': body}) + return {'server': body} - def post_servers_1234_action(self, request, url, headers): + def post_servers_1234_action(self, request, context): _body = '' - body = jsonutils.loads(request.body.decode('utf-8')) - resp = 202 + body = jsonutils.loads(request.body) + context.status_code = 202 assert len(body.keys()) == 1 action = list(body)[0] if action == 'reboot': @@ -393,7 +391,8 @@ def post_servers_1234_action(self, request, url, headers): elif action == 'confirmResize': assert body[action] is None # This one method returns a different response code - return 204, headers, '' + context.status_code = 204 + return None elif action == 'revertResize': assert body[action] is None elif action == 'migrate': @@ -445,12 +444,13 @@ def post_servers_1234_action(self, request, url, headers): assert list(body[action]) == ['address'] elif action == 'createImage': assert set(body[action].keys()) == set(['name', 'metadata']) - headers['location'] = "http://blah/images/456" + context.headers['location'] = "http://blah/images/456" elif action == 'changePassword': assert list(body[action]) == ['adminPass'] elif action == 'os-getConsoleOutput': assert list(body[action]) == ['length'] - return 202, headers, jsonutils.dumps({'output': 'foo'}) + context.status_code = 202 + return {'output': 'foo'} elif action == 'os-getVNCConsole': assert list(body[action]) == ['type'] elif action == 'os-getSPICEConsole': @@ -480,7 +480,7 @@ def post_servers_1234_action(self, request, url, headers): assert set(keys) == set(['host', 'onSharedStorage']) else: raise AssertionError("Unexpected server action: %s" % action) - return resp, headers, jsonutils.dumps({'server': _body}) + return {'server': _body} class V3(Base): @@ -507,32 +507,30 @@ def setUp(self): ] } - httpretty.register_uri(httpretty.GET, - self.url('1234', 'os-attach-interfaces'), - body=jsonutils.dumps(get_interfaces), - content_type='application/json') + self.requests.register_uri('GET', + self.url('1234', 'os-attach-interfaces'), + json=get_interfaces, + headers=self.json_headers) attach_body = {'interface_attachment': {}} - httpretty.register_uri(httpretty.POST, - self.url('1234', 'os-attach-interfaces'), - body=jsonutils.dumps(attach_body), - content_type='application/json') - - httpretty.register_uri(httpretty.GET, - self.url('1234', 'os-server-diagnostics'), - body=self.diagnostic, - status=200) - - httpretty.register_uri(httpretty.DELETE, - self.url('1234', 'os-attach-interfaces', - 'port-id')) - - httpretty.register_uri(httpretty.GET, - self.url(1234, 'os-server-password'), - jsonutils.dumps({'password': ''})) - - def post_servers(self, request, url, headers): - body = jsonutils.loads(request.body.decode('utf-8')) + self.requests.register_uri('POST', + self.url('1234', 'os-attach-interfaces'), + json=attach_body, + headers=self.json_headers) + + self.requests.register_uri('GET', + self.url('1234', 'os-server-diagnostics'), + json=self.diagnostic) + + url = self.url('1234', 'os-attach-interfaces', 'port-id') + self.requests.register_uri('DELETE', url) + + self.requests.register_uri('GET', + self.url(1234, 'os-server-password'), + json={'password': ''}) + + def post_servers(self, request, context): + body = jsonutils.loads(request.body) assert set(body.keys()) <= set(['server']) fakes.assert_has_keys(body['server'], required=['name', 'image_ref', 'flavor_ref'], @@ -543,10 +541,11 @@ def post_servers(self, request, url, headers): else: body = self.server_1234 - return 202, headers, jsonutils.dumps({'server': body}) + context.status_code = 202 + return {'server': body} - def post_servers_1234_action(self, request, url, headers): - resp = 202 + def post_servers_1234_action(self, request, context): + context.status_code = 202 body_is_none_list = [ 'revert_resize', 'migrate', 'stop', 'start', 'force_delete', 'restore', 'pause', 'unpause', 'lock', 'unlock', 'unrescue', @@ -577,7 +576,7 @@ def post_servers_1234_action(self, request, url, headers): 'detach': ['volume_id'], 'swap_volume_attachment': ['old_volume_id', 'new_volume_id']} - body = jsonutils.loads(request.body.decode('utf-8')) + body = jsonutils.loads(request.body) assert len(body.keys()) == 1 action = list(body)[0] _body = body_return_map.get(action, '') @@ -598,13 +597,13 @@ def post_servers_1234_action(self, request, url, headers): assert body[action]['type'] in ['HARD', 'SOFT'] elif action == 'confirm_resize': # This one method returns a different response code - resp = 204 + context.status_code = 204 elif action == 'create_image': - headers['location'] = "http://blah/images/456" + context.headers['location'] = "http://blah/images/456" if action not in set.union(set(body_is_none_list), set(body_params_check_exact.keys()), set(body_param_check_exists.keys())): raise AssertionError("Unexpected server action: %s" % action) - return resp, headers, jsonutils.dumps(_body) + return _body diff --git a/novaclient/tests/utils.py b/novaclient/tests/utils.py index be251998c..036702ec5 100644 --- a/novaclient/tests/utils.py +++ b/novaclient/tests/utils.py @@ -14,8 +14,8 @@ import os import fixtures -import httpretty import requests +from requests_mock.contrib import fixture as requests_mock_fixture import six import testscenarios import testtools @@ -52,24 +52,26 @@ class FixturedTestCase(testscenarios.TestWithScenarios, TestCase): def setUp(self): super(FixturedTestCase, self).setUp() - httpretty.reset() + self.requests = self.useFixture(requests_mock_fixture.Fixture()) self.data_fixture = None self.client_fixture = None self.cs = None if self.client_fixture_class: - self.client_fixture = self.useFixture(self.client_fixture_class()) + fix = self.client_fixture_class(self.requests) + self.client_fixture = self.useFixture(fix) self.cs = self.client_fixture.client if self.data_fixture_class: - self.data_fixture = self.useFixture(self.data_fixture_class()) + fix = self.data_fixture_class(self.requests) + self.data_fixture = self.useFixture(fix) def assert_called(self, method, path, body=None): - self.assertEqual(httpretty.last_request().method, method) - self.assertEqual(httpretty.last_request().path, path) + self.assertEqual(self.requests.last_request.method, method) + self.assertEqual(self.requests.last_request.path_url, path) if body: - req_data = httpretty.last_request().body + req_data = self.requests.last_request.body if isinstance(req_data, six.binary_type): req_data = req_data.decode('utf-8') if not isinstance(body, six.string_types): diff --git a/novaclient/tests/v1_1/test_agents.py b/novaclient/tests/v1_1/test_agents.py index 46efe839a..3bc006fb5 100644 --- a/novaclient/tests/v1_1/test_agents.py +++ b/novaclient/tests/v1_1/test_agents.py @@ -13,9 +13,6 @@ # License for the specific language governing permissions and limitations # under the License. -import httpretty - -from novaclient.openstack.common import jsonutils from novaclient.tests.fixture_data import agents as data from novaclient.tests.fixture_data import client from novaclient.tests import utils @@ -53,9 +50,10 @@ def stub_hypervisors(self, hypervisor='kvm'): ] } - httpretty.register_uri(httpretty.GET, self.data_fixture.url(), - body=jsonutils.dumps(get_os_agents), - content_type='application/json') + headers = {'Content-Type': 'application/json'} + self.requests.register_uri('GET', self.data_fixture.url(), + json=get_os_agents, + headers=headers) def test_list_agents(self): self.stub_hypervisors() diff --git a/novaclient/tests/v1_1/test_servers.py b/novaclient/tests/v1_1/test_servers.py index 5ef06baaa..9df2edf4b 100644 --- a/novaclient/tests/v1_1/test_servers.py +++ b/novaclient/tests/v1_1/test_servers.py @@ -12,11 +12,11 @@ # License for the specific language governing permissions and limitations # under the License. -import httpretty import mock import six from novaclient import exceptions +from novaclient.openstack.common import jsonutils from novaclient.tests.fixture_data import client from novaclient.tests.fixture_data import floatingips from novaclient.tests.fixture_data import servers as data @@ -31,7 +31,7 @@ class ServersTest(utils.FixturedTestCase): def setUp(self): super(ServersTest, self).setUp() - self.useFixture(floatingips.FloatingFixture()) + self.useFixture(floatingips.FloatingFixture(self.requests)) def test_list_servers(self): sl = self.cs.servers.list() @@ -191,7 +191,7 @@ def _create_disk_config(self, disk_config): self.assertIsInstance(s, servers.Server) # verify disk config param was used in the request: - body = httpretty.last_request().parsed_body + body = jsonutils.loads(self.requests.last_request.body) server = body['server'] self.assertTrue('OS-DCF:diskConfig' in server) self.assertEqual(disk_config, server['OS-DCF:diskConfig']) @@ -280,7 +280,7 @@ def _rebuild_resize_disk_config(self, disk_config, operation="rebuild"): self.assert_called('POST', '/servers/1234/action') # verify disk config param was used in the request: - body = httpretty.last_request().parsed_body + body = jsonutils.loads(self.requests.last_request.body) d = body[operation] self.assertTrue('OS-DCF:diskConfig' in d) @@ -296,7 +296,7 @@ def test_rebuild_server_preserve_ephemeral(self): s = self.cs.servers.get(1234) s.rebuild(image=1, preserve_ephemeral=True) self.assert_called('POST', '/servers/1234/action') - body = httpretty.last_request().parsed_body + body = jsonutils.loads(self.requests.last_request.body) d = body['rebuild'] self.assertIn('preserve_ephemeral', d) self.assertEqual(d['preserve_ephemeral'], True) @@ -305,7 +305,7 @@ def test_rebuild_server_name_meta_files(self): files = {'/etc/passwd': 'some data'} s = self.cs.servers.get(1234) s.rebuild(image=1, name='new', meta={'foo': 'bar'}, files=files) - body = httpretty.last_request().parsed_body + body = jsonutils.loads(self.requests.last_request.body) d = body['rebuild'] self.assertEqual('new', d['name']) self.assertEqual({'foo': 'bar'}, d['metadata']) diff --git a/test-requirements.txt b/test-requirements.txt index 910d74c47..2240f7ad9 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -3,9 +3,9 @@ hacking>=0.9.2,<0.10 coverage>=3.6 discover fixtures>=0.3.14 -httpretty>=0.8.0,!=0.8.1,!=0.8.2 keyring>=2.1,!=3.3 mock>=1.0 +requests-mock>=0.4.0 sphinx>=1.1.2,!=1.2.0,<1.3 python-keystoneclient>=0.10.0 oslosphinx From c55383f734aaa336ea996c219cc52b1da02332ff Mon Sep 17 00:00:00 2001 From: liyingjun Date: Thu, 7 Aug 2014 04:18:14 +0800 Subject: [PATCH 0580/1705] Fix variable error for nova --service-type When running 'nova' with '--service-type' option, it raises an variable error. $ nova --service-type compute list ERROR: local variable 'os_compute_api_version' referenced before assignment When '--service-type' option is given, the default value of 'os_compute_api_version' does not apply. Change-Id: I19554756e8eb56e3a5984ed3078137af9687f986 Closes-bug: 1315368 --- novaclient/shell.py | 2 +- novaclient/tests/test_shell.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/novaclient/shell.py b/novaclient/shell.py index 08f5e8c40..0a4eee204 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -701,7 +701,7 @@ def main(self, argv): except exc.AuthorizationFailure: raise exc.CommandError(_("Unable to authorize user")) - if os_compute_api_version == "3" and service_type != 'image': + if options.os_compute_api_version == "3" and service_type != 'image': # NOTE(cyeoh): create an image based client because the # images api is no longer proxied by the V3 API and we # sometimes need to be able to look up images information diff --git a/novaclient/tests/test_shell.py b/novaclient/tests/test_shell.py index daee60abe..98af7c6b1 100644 --- a/novaclient/tests/test_shell.py +++ b/novaclient/tests/test_shell.py @@ -231,7 +231,8 @@ def _test_service_type(self, version, service_type, mock_client): if version is None: cmd = 'list' else: - cmd = '--os-compute-api-version %s list' % version + cmd = ('--service_type %s --os-compute-api-version %s list' % + (service_type, version)) self.make_env() self.shell(cmd) _, client_kwargs = mock_client.call_args_list[0] From 3955440ef2f57caf853a40e1cf28abe0a1d371ff Mon Sep 17 00:00:00 2001 From: Zhengguang Date: Fri, 11 Jul 2014 04:32:37 -0400 Subject: [PATCH 0581/1705] Fix the return code of the command "delete" Currently, the command delete return a zero if it failed for any of servers, the return code should be a nonzero. Change-Id: If7009fdeb5a60fe2e357bcc447313cbdb7b2ff39 Closes-Bug: #1339647 --- novaclient/tests/v1_1/test_shell.py | 6 +++-- novaclient/tests/v3/test_shell.py | 38 +++++++++++++++++++++++++++++ novaclient/v1_1/shell.py | 10 ++++---- novaclient/v3/shell.py | 10 ++++---- 4 files changed, 52 insertions(+), 12 deletions(-) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index b65ec81b4..c167498b6 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -1050,9 +1050,11 @@ def test_delete_two_with_two_existent(self): self.assert_called('DELETE', '/servers/5678', pos=-1) def test_delete_two_with_one_nonexistent(self): - self.run_command('delete 1234 123456789') + cmd = 'delete 1234 123456789' + self.assertRaises(exceptions.CommandError, self.run_command, cmd) self.assert_called_anytime('DELETE', '/servers/1234') - self.run_command('delete sample-server nonexistentserver') + cmd = 'delete sample-server nonexistentserver' + self.assertRaises(exceptions.CommandError, self.run_command, cmd) self.assert_called_anytime('DELETE', '/servers/1234') def test_delete_one_with_one_nonexistent(self): diff --git a/novaclient/tests/v3/test_shell.py b/novaclient/tests/v3/test_shell.py index 39ed62d7e..f011fb7ac 100644 --- a/novaclient/tests/v3/test_shell.py +++ b/novaclient/tests/v3/test_shell.py @@ -599,6 +599,44 @@ def test_flavor_show_by_name_priv(self): self.assert_called('GET', '/flavors/2', pos=3) self.assert_called('GET', '/flavors/2/flavor-extra-specs', pos=4) + def test_delete(self): + self.run_command('delete 1234') + self.assert_called('DELETE', '/servers/1234') + self.run_command('delete sample-server') + self.assert_called('DELETE', '/servers/1234') + + def test_delete_two_with_two_existent(self): + self.run_command('delete 1234 5678') + self.assert_called('DELETE', '/servers/1234', pos=-3) + self.assert_called('DELETE', '/servers/5678', pos=-1) + self.run_command('delete sample-server sample-server2') + self.assert_called('GET', '/servers', pos=-6) + self.assert_called('GET', '/servers/1234', pos=-5) + self.assert_called('DELETE', '/servers/1234', pos=-4) + self.assert_called('GET', '/servers', pos=-3) + self.assert_called('GET', '/servers/5678', pos=-2) + self.assert_called('DELETE', '/servers/5678', pos=-1) + + def test_delete_two_with_one_nonexistent(self): + cmd = 'delete 1234 123456789' + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + self.assert_called_anytime('DELETE', '/servers/1234') + cmd = 'delete sample-server nonexistentserver' + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + self.assert_called_anytime('DELETE', '/servers/1234') + + def test_delete_one_with_one_nonexistent(self): + cmd = 'delete 123456789' + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + cmd = 'delete nonexistent-server1' + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + def test_delete_two_with_two_nonexistent(self): + cmd = 'delete 123456789 987654321' + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + cmd = 'delete nonexistent-server1 nonexistent-server2' + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + class GetFirstEndpointTest(utils.TestCase): def test_only_one_endpoint(self): diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 7373848c7..a960a08dc 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1644,19 +1644,19 @@ def do_show(cs, args): help=_('Name or ID of server(s).')) def do_delete(cs, args): """Immediately shut down and delete specified server(s).""" - failure_count = 0 + failure_flag = False for server in args.server: try: _find_server(cs, server).delete() print(_("Request to delete server %s has been accepted.") % server) except Exception as e: - failure_count += 1 + failure_flag = True print(e) - if failure_count == len(args.server): - raise exceptions.CommandError(_("Unable to delete any of the " - "specified servers.")) + if failure_flag: + raise exceptions.CommandError(_("Unable to delete the " + "specified server(s).")) def _find_server(cs, server): diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index ce856f119..e88683a4d 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -1369,19 +1369,19 @@ def do_show(cs, args): help='Name or ID of server(s).') def do_delete(cs, args): """Immediately shut down and delete specified server(s).""" - failure_count = 0 + failure_flag = False for server in args.server: try: _find_server(cs, server).delete() print("Request to delete server %s has been accepted." % server) except Exception as e: - failure_count += 1 + failure_flag = True print(e) - if failure_count == len(args.server): - raise exceptions.CommandError("Unable to delete any of the specified " - "servers.") + if failure_flag: + raise exceptions.CommandError("Unable to delete the specified " + "server(s).") def _find_server(cs, server): From db6d6780d5d43d1c54f9635fa6135052694abd77 Mon Sep 17 00:00:00 2001 From: Jamie Lennox Date: Thu, 7 Aug 2014 20:18:58 +1000 Subject: [PATCH 0582/1705] Use adapter from keystoneclient SessionClient is mostly common code that wasn't yet released in keystoneclient. Now that 0.10 is released we should use the adapter from there as much as possible. Change-Id: Ief6cd7e752fd8c9e9157364f99e270da7faff074 --- novaclient/client.py | 72 +++++++++++--------------------------------- 1 file changed, 17 insertions(+), 55 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index ba90e0fc4..36baffa58 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -30,6 +30,7 @@ import re import time +from keystoneclient import adapter import requests from requests import adapters @@ -133,65 +134,22 @@ def write_object(self, obj): self._write_attribute(resource, attribute, value) -class SessionClient(object): - - def __init__(self, session, auth, interface, service_type, region_name): - self.session = session - self.auth = auth - - self.interface = interface - self.service_type = service_type - self.region_name = region_name +class SessionClient(adapter.LegacyJsonAdapter): def request(self, url, method, **kwargs): - kwargs.setdefault('user_agent', 'python-novaclient') - kwargs.setdefault('auth', self.auth) - kwargs.setdefault('authenticated', False) - - try: - kwargs['json'] = kwargs.pop('body') - except KeyError: - pass - - headers = kwargs.setdefault('headers', {}) - headers.setdefault('Accept', 'application/json') - - endpoint_filter = kwargs.setdefault('endpoint_filter', {}) - endpoint_filter.setdefault('interface', self.interface) - endpoint_filter.setdefault('service_type', self.service_type) - endpoint_filter.setdefault('region_name', self.region_name) - - resp = self.session.request(url, method, raise_exc=False, **kwargs) - - body = None - if resp.text: - try: - body = resp.json() - except ValueError: - pass - - if resp.status_code >= 400: + # NOTE(jamielennox): The standard call raises errors from + # keystoneclient, where we need to raise the novaclient errors. + raise_exc = kwargs.pop('raise_exc', True) + resp, body = super(SessionClient, self).request(url, + method, + raise_exc=False, + **kwargs) + + if raise_exc and resp.status_code >= 400: raise exceptions.from_response(resp, body, url, method) return resp, body - def _cs_request(self, url, method, **kwargs): - # this function is mostly redundant but makes compatibility easier - kwargs.setdefault('authenticated', True) - return self.request(url, method, **kwargs) - - def get(self, url, **kwargs): - return self._cs_request(url, 'GET', **kwargs) - - def post(self, url, **kwargs): - return self._cs_request(url, 'POST', **kwargs) - - def put(self, url, **kwargs): - return self._cs_request(url, 'PUT', **kwargs) - - def delete(self, url, **kwargs): - return self._cs_request(url, 'DELETE', **kwargs) - def _original_only(f): """Indicates and enforces that this function can only be used if we are @@ -745,13 +703,17 @@ def _construct_http_client(username=None, password=None, project_id=None, auth_system='keystone', auth_plugin=None, auth_token=None, cacert=None, tenant_id=None, user_id=None, connection_pool=False, session=None, - auth=None): + auth=None, user_agent='python-novaclient', + **kwargs): if session: return SessionClient(session=session, auth=auth, interface=endpoint_type, service_type=service_type, - region_name=region_name) + region_name=region_name, + service_name=service_name, + user_agent=user_agent, + **kwargs) else: # FIXME(jamielennox): username and password are now optional. Need # to test that they were provided in this mode. From 53be1f4e2a3ea9a12d25951cf6ab541e0beddcbf Mon Sep 17 00:00:00 2001 From: Takashi Sogabe Date: Sun, 10 Aug 2014 05:50:57 +0000 Subject: [PATCH 0583/1705] Fix listing of flavor-list (V1_1) to display swap value Current version of flavor-list(v1_1 API) does not show swap value because of selecting wrong field name. This change displays swap value in the same way to v3 API Closes-Bug: 1354546 Change-Id: I5c8fca8207032814b96b8410213bf58209d9b526 --- novaclient/v1_1/shell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index a960a08dc..0c4b6d265 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -587,7 +587,7 @@ def _print_flavor_list(flavors, show_extra_specs=False): 'Memory_MB', 'Disk', 'Ephemeral', - 'Swap_MB', + 'Swap', 'VCPUs', 'RXTX_Factor', 'Is_Public', From 9758ffc85dee10f1bfc4bfb1b5691f86d21347e5 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Wed, 13 Aug 2014 23:24:28 +0000 Subject: [PATCH 0584/1705] Updated from global requirements Change-Id: I1155f67196b5a8393ff53361ac9c63f3f7c23ad0 --- requirements.txt | 2 +- test-requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index b08504104..55b1a0523 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ pbr>=0.6,!=0.7,<1.0 argparse iso8601>=0.1.9 PrettyTable>=0.7,<0.8 -requests>=1.1 +requests>=1.2.1 simplejson>=2.0.9 six>=1.7.0 Babel>=1.3 diff --git a/test-requirements.txt b/test-requirements.txt index 2240f7ad9..e2a03bfd2 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -5,7 +5,7 @@ discover fixtures>=0.3.14 keyring>=2.1,!=3.3 mock>=1.0 -requests-mock>=0.4.0 +requests-mock>=0.4.0 # Apache-2.0 sphinx>=1.1.2,!=1.2.0,<1.3 python-keystoneclient>=0.10.0 oslosphinx From c204613ee48fd1e2e3a5fe5b32c23be769ca11b3 Mon Sep 17 00:00:00 2001 From: igormilovanovic Date: Mon, 18 Aug 2014 17:04:49 +0100 Subject: [PATCH 0585/1705] Quickstart (README) doc should refer to nova Closes Bug: #1358385 Change-Id: I0d9592d3221f3a4295bce7e5da8472ea73e0bcc7 --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 1fef0d4ba..2bbee7483 100644 --- a/README.rst +++ b/README.rst @@ -66,7 +66,7 @@ Python API There's also a complete Python API, but it has not yet been documented. -Quick-start using keystone:: +To use with nova, with keystone as the authentication system:: # use v2.0 auth with http://example.com:5000/v2.0/") >>> from novaclient.v1_1 import client From 3fa04e6d2d8bee36c4089620c16ce750dd115ece Mon Sep 17 00:00:00 2001 From: Michal Dulko Date: Tue, 19 Aug 2014 13:21:00 +0200 Subject: [PATCH 0586/1705] Add filtering by service to hosts list command This commit adds --service-name option to host-list command allowing user to filter hosts by service. DocImpact Closes-Bug: 1224763 Change-Id: Ic67deb3af7ddbe23c14fa38ae180d43ab484b78f --- novaclient/tests/fixture_data/hosts.py | 10 ++++++++-- novaclient/tests/v3/test_hosts.py | 15 +++++++++++++++ novaclient/v3/hosts.py | 16 ++++++++++++++++ novaclient/v3/shell.py | 5 ++++- 4 files changed, 43 insertions(+), 3 deletions(-) diff --git a/novaclient/tests/fixture_data/hosts.py b/novaclient/tests/fixture_data/hosts.py index 6ac217cb0..babfbec7c 100644 --- a/novaclient/tests/fixture_data/hosts.py +++ b/novaclient/tests/fixture_data/hosts.py @@ -45,6 +45,7 @@ def setUp(self): def get_os_hosts(request, context): host, query = parse.splitquery(request.url) zone = 'nova1' + service = None if query: qs = parse.parse_qs(query) @@ -53,16 +54,21 @@ def get_os_hosts(request, context): except Exception: pass + try: + service = qs['service'][0] + except Exception: + pass + return { 'hosts': [ { 'host': 'host1', - 'service': 'nova-compute', + 'service': service or 'nova-compute', 'zone': zone }, { 'host': 'host1', - 'service': 'nova-cert', + 'service': service or 'nova-cert', 'zone': zone } ] diff --git a/novaclient/tests/v3/test_hosts.py b/novaclient/tests/v3/test_hosts.py index cbd47822b..485633ffe 100644 --- a/novaclient/tests/v3/test_hosts.py +++ b/novaclient/tests/v3/test_hosts.py @@ -43,6 +43,21 @@ def test_list_host_with_zone(self): self.assertIsInstance(h, hosts.Host) self.assertEqual(h.zone, 'nova') + def test_list_host_with_service(self): + hs = self.cs.hosts.list(service='nova-compute') + self.assert_called('GET', '/os-hosts?service=nova-compute') + for h in hs: + self.assertIsInstance(h, hosts.Host) + self.assertEqual(h.service, 'nova-compute') + + def test_list_host_with_zone_and_service(self): + hs = self.cs.hosts.list(service='nova-compute', zone='nova') + self.assert_called('GET', '/os-hosts?zone=nova&service=nova-compute') + for h in hs: + self.assertIsInstance(h, hosts.Host) + self.assertEqual(h.zone, 'nova') + self.assertEqual(h.service, 'nova-compute') + def test_update_enable(self): host = self.cs.hosts.get('sample_host')[0] values = {"status": "enabled"} diff --git a/novaclient/v3/hosts.py b/novaclient/v3/hosts.py index 5327a2bf3..174b62334 100644 --- a/novaclient/v3/hosts.py +++ b/novaclient/v3/hosts.py @@ -33,3 +33,19 @@ def host_action(self, host, action): """Perform an action on a host.""" url = '/os-hosts/{0}/{1}'.format(host, action) return self._get(url, response_key='host') + + def list(self, zone=None, service=None): + """List cloud hosts.""" + + filters = [] + if zone: + filters.append('zone=%s' % zone) + if service: + filters.append('service=%s' % service) + + if filters: + url = '/os-hosts?%s' % '&'.join(filters) + else: + url = '/os-hosts' + + return self._list(url, "hosts") diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index e88683a4d..2e87a5530 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -2494,10 +2494,13 @@ def do_host_describe(cs, args): @utils.arg('--zone', metavar='', default=None, help='Filters the list, returning only those ' 'hosts in the availability zone .') +@utils.arg('--service-name', metavar='', default=None, + help='Filters the list, returning only those ' + 'hosts providing service .') def do_host_list(cs, args): """List all hosts by service.""" columns = ["host_name", "service", "zone"] - result = cs.hosts.list(args.zone) + result = cs.hosts.list(args.zone, args.service_name) utils.print_list(result, columns) From b3da3ebf97df260346fe80567492fa0055cc7bd7 Mon Sep 17 00:00:00 2001 From: "Leandro I. Costantino" Date: Thu, 27 Feb 2014 10:16:15 -0300 Subject: [PATCH 0587/1705] Adding Nova Client support for auto find host APIv3 Adding nova client functionality to use the auto find destination host for evacuate. This change makes the target host optional on evacuate command. Partial Implements: blueprint find-host-and-evacuate-instance Change-Id: I40619b8d1eea6ae44ed5e43fb50ce2c1862686d6 Co-Authored-By: Juan M. Olle --- novaclient/tests/v3/fakes.py | 7 ++----- novaclient/tests/v3/test_shell.py | 33 +++++++++++++++++++++++++++++++ novaclient/v3/servers.py | 12 +++++------ novaclient/v3/shell.py | 5 +++-- 4 files changed, 44 insertions(+), 13 deletions(-) diff --git a/novaclient/tests/v3/fakes.py b/novaclient/tests/v3/fakes.py index 8bb9ffbd1..61ed16f70 100644 --- a/novaclient/tests/v3/fakes.py +++ b/novaclient/tests/v3/fakes.py @@ -197,11 +197,11 @@ def post_servers_1234_action(self, body, **kw): } body_param_check_exists = { 'rebuild': 'image_ref', - 'resize': 'flavor_ref'} + 'resize': 'flavor_ref', + 'evacuate': 'on_shared_storage'} body_params_check_exact = { 'reboot': ['type'], 'add_fixed_ip': ['network_id'], - 'evacuate': ['host', 'on_shared_storage'], 'remove_fixed_ip': ['address'], 'change_password': ['admin_password'], 'get_console_output': ['length'], @@ -226,9 +226,6 @@ def post_servers_1234_action(self, body, **kw): if action in body_param_check_exists: assert body_param_check_exists[action] in body[action] - if action == 'evacuate': - body[action].pop('admin_password', None) - if action in body_params_check_exact: assert set(body[action]) == set(body_params_check_exact[action]) diff --git a/novaclient/tests/v3/test_shell.py b/novaclient/tests/v3/test_shell.py index b8a96165e..a8eb89c12 100644 --- a/novaclient/tests/v3/test_shell.py +++ b/novaclient/tests/v3/test_shell.py @@ -563,6 +563,39 @@ def test_boot_with_poll_to_check_VM_state_error(self): self.assertRaises(exceptions.InstanceInErrorState, self.run_command, 'boot --flavor 1 --image 1 some-bad-server --poll') + def test_evacuate(self): + self.run_command('evacuate sample-server new_host') + self.assert_called('POST', '/servers/1234/action', + {'evacuate': {'host': 'new_host', + 'on_shared_storage': False}}) + self.run_command('evacuate sample-server new_host ' + '--password NewAdminPass') + self.assert_called('POST', '/servers/1234/action', + {'evacuate': {'host': 'new_host', + 'on_shared_storage': False, + 'admin_password': 'NewAdminPass'}}) + self.run_command('evacuate sample-server new_host') + self.assert_called('POST', '/servers/1234/action', + {'evacuate': {'host': 'new_host', + 'on_shared_storage': False}}) + self.run_command('evacuate sample-server new_host ' + '--on-shared-storage') + self.assert_called('POST', '/servers/1234/action', + {'evacuate': {'host': 'new_host', + 'on_shared_storage': True}}) + + def test_evacuate_with_no_target_host(self): + self.run_command('evacuate sample-server') + self.assert_called('POST', '/servers/1234/action', + {'evacuate': {'on_shared_storage': False}}) + self.run_command('evacuate sample-server --password NewAdminPass') + self.assert_called('POST', '/servers/1234/action', + {'evacuate': {'on_shared_storage': False, + 'admin_password': 'NewAdminPass'}}) + self.run_command('evacuate sample-server --on-shared-storage') + self.assert_called('POST', '/servers/1234/action', + {'evacuate': {'on_shared_storage': True}}) + def test_boot_named_flavor(self): self.run_command(["boot", "--image", "1", "--flavor", "512 MB Server", diff --git a/novaclient/v3/servers.py b/novaclient/v3/servers.py index 0e028b5da..2d58650bc 100644 --- a/novaclient/v3/servers.py +++ b/novaclient/v3/servers.py @@ -320,7 +320,7 @@ def reset_network(self): """ self.manager.reset_network(self) - def evacuate(self, host, on_shared_storage, password=None): + def evacuate(self, host=None, on_shared_storage=True, password=None): """ Evacuate an instance from failed host to specified host. @@ -950,7 +950,8 @@ def reset_network(self, server): """ self._action('reset_network', server) - def evacuate(self, server, host, on_shared_storage, password=None): + def evacuate(self, server, host=None, + on_shared_storage=True, password=None): """ Evacuate a server instance. @@ -960,10 +961,9 @@ def evacuate(self, server, host, on_shared_storage, password=None): on shared storage :param password: string to set as password on the evacuated server. """ - body = { - 'host': host, - 'on_shared_storage': on_shared_storage, - } + body = {'on_shared_storage': on_shared_storage} + if host is not None: + body['host'] = host if password is not None: body['admin_password'] = password diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 4b6da7234..271de0a4b 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -2856,11 +2856,12 @@ def do_quota_delete(cs, args): @utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg('host', metavar='', help='Name or ID of target host.') +@utils.arg('host', metavar='', nargs='?', + help="Name or ID of the target host. " + "If no host is specified, the scheduler will choose one.") @utils.arg('--password', dest='password', metavar='', - default=None, help="Set the provided password on the evacuated server. Not applicable " "with on-shared-storage flag") @utils.arg('--on-shared-storage', From fe95fe48fcb7a90148e22bdfe654c2f66119c8bc Mon Sep 17 00:00:00 2001 From: Juan Manuel Olle Date: Thu, 13 Feb 2014 11:06:45 -0300 Subject: [PATCH 0588/1705] Adding Nova Client support for auto find host APIv2 Adding nova client functionality to use the auto find destination host for evacuate. This change makes the target host optional on evacuate command. Partial Implements: blueprint find-host-and-evacuate-instance Co-Authored-By: Leandro Costantino Change-Id: I4e6b52df9c55b223c8693058d73125962cff4d16 --- novaclient/tests/v1_1/fakes.py | 4 +++- novaclient/tests/v1_1/test_shell.py | 28 ++++++++++++++---------- novaclient/v1_1/contrib/host_evacuate.py | 10 +++++---- novaclient/v1_1/servers.py | 13 ++++++----- novaclient/v1_1/shell.py | 8 ++++--- 5 files changed, 37 insertions(+), 26 deletions(-) diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index d9c1f380e..d7bd80bf1 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -636,7 +636,9 @@ def post_servers_1234_action(self, body, **kw): keys = list(body[action]) if 'adminPass' in keys: keys.remove('adminPass') - assert set(keys) == set(['host', 'onSharedStorage']) + if 'host' in keys: + keys.remove('host') + assert set(keys) == set(['onSharedStorage']) else: raise AssertionError("Unexpected server action: %s" % action) return (resp, _headers, _body) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index d6f790df7..3c8e42575 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -1527,17 +1527,13 @@ def test_host_evacuate_with_no_target_host(self): self.run_command('host-evacuate --on-shared-storage hyper') self.assert_called('GET', '/os-hypervisors/hyper/servers', pos=0) self.assert_called('POST', '/servers/uuid1/action', - {'evacuate': {'host': None, - 'onSharedStorage': True}}, pos=1) + {'evacuate': {'onSharedStorage': True}}, pos=1) self.assert_called('POST', '/servers/uuid2/action', - {'evacuate': {'host': None, - 'onSharedStorage': True}}, pos=2) + {'evacuate': {'onSharedStorage': True}}, pos=2) self.assert_called('POST', '/servers/uuid3/action', - {'evacuate': {'host': None, - 'onSharedStorage': True}}, pos=3) + {'evacuate': {'onSharedStorage': True}}, pos=3) self.assert_called('POST', '/servers/uuid4/action', - {'evacuate': {'host': None, - 'onSharedStorage': True}}, pos=4) + {'evacuate': {'onSharedStorage': True}}, pos=4) def test_host_servers_migrate(self): self.run_command('host-servers-migrate hyper') @@ -1842,16 +1838,24 @@ def test_evacuate(self): {'evacuate': {'host': 'new_host', 'onSharedStorage': False, 'adminPass': 'NewAdminPass'}}) - self.run_command('evacuate sample-server new_host') - self.assert_called('POST', '/servers/1234/action', - {'evacuate': {'host': 'new_host', - 'onSharedStorage': False}}) self.run_command('evacuate sample-server new_host ' '--on-shared-storage') self.assert_called('POST', '/servers/1234/action', {'evacuate': {'host': 'new_host', 'onSharedStorage': True}}) + def test_evacuate_with_no_target_host(self): + self.run_command('evacuate sample-server') + self.assert_called('POST', '/servers/1234/action', + {'evacuate': {'onSharedStorage': False}}) + self.run_command('evacuate sample-server --password NewAdminPass') + self.assert_called('POST', '/servers/1234/action', + {'evacuate': {'onSharedStorage': False, + 'adminPass': 'NewAdminPass'}}) + self.run_command('evacuate sample-server --on-shared-storage') + self.assert_called('POST', '/servers/1234/action', + {'evacuate': {'onSharedStorage': True}}) + def test_get_password(self): self.run_command('get-password sample-server /foo/id_rsa') self.assert_called('GET', '/servers/1234/os-server-password') diff --git a/novaclient/v1_1/contrib/host_evacuate.py b/novaclient/v1_1/contrib/host_evacuate.py index dd1aef267..12ad602fa 100644 --- a/novaclient/v1_1/contrib/host_evacuate.py +++ b/novaclient/v1_1/contrib/host_evacuate.py @@ -26,8 +26,8 @@ def _server_evacuate(cs, server, args): success = True error_message = "" try: - cs.servers.evacuate(server['uuid'], args.target_host, - args.on_shared_storage) + cs.servers.evacuate(server=server['uuid'], host=args.target_host, + on_shared_storage=args.on_shared_storage) except Exception as e: success = False error_message = _("Error while evacuating instance: %s") % e @@ -41,7 +41,9 @@ def _server_evacuate(cs, server, args): @utils.arg('--target_host', metavar='', default=None, - help=_('Name of target host.')) + help=_('Name of target host. ' + 'If no host is specified the scheduler' + ' will select a target.')) @utils.arg('--on-shared-storage', dest='on_shared_storage', action="store_true", @@ -49,7 +51,7 @@ def _server_evacuate(cs, server, args): help=_('Specifies whether all instances files are on shared ' ' storage')) def do_host_evacuate(cs, args): - """Evacuate all instances from failed host to specified one.""" + """Evacuate all instances from failed host.""" hypervisors = cs.hypervisors.search(args.host, servers=True) response = [] for hyper in hypervisors: diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index 8a930e8f9..dc7ca4f86 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -364,7 +364,7 @@ def list_security_group(self): """ return self.manager.list_security_group(self) - def evacuate(self, host, on_shared_storage, password=None): + def evacuate(self, host=None, on_shared_storage=True, password=None): """ Evacuate an instance from failed host to specified host. @@ -1149,7 +1149,8 @@ def list_security_group(self, server): return self._list('/servers/%s/os-security-groups' % base.getid(server), 'security_groups', SecurityGroup) - def evacuate(self, server, host, on_shared_storage, password=None): + def evacuate(self, server, host=None, on_shared_storage=True, + password=None): """ Evacuate a server instance. @@ -1159,10 +1160,10 @@ def evacuate(self, server, host, on_shared_storage, password=None): on shared storage :param password: string to set as password on the evacuated server. """ - body = { - 'host': host, - 'onSharedStorage': on_shared_storage, - } + + body = {'onSharedStorage': on_shared_storage} + if host is not None: + body['host'] = host if password is not None: body['adminPass'] = password diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 93ff2b232..de5953f07 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -3528,11 +3528,12 @@ def do_quota_class_update(cs, args): @utils.arg('server', metavar='', help=_('Name or ID of server.')) -@utils.arg('host', metavar='', help=_('Name or ID of target host.')) +@utils.arg('host', metavar='', nargs='?', + help=_("Name or ID of the target host. " + "If no host is specified, the scheduler will choose one.")) @utils.arg('--password', dest='password', metavar='', - default=None, help=_("Set the provided password on the evacuated server. Not applicable " "with on-shared-storage flag")) @utils.arg('--on-shared-storage', @@ -3541,7 +3542,8 @@ def do_quota_class_update(cs, args): default=False, help=_('Specifies whether server files are located on shared storage')) def do_evacuate(cs, args): - """Evacuate server from failed host to specified one.""" + """Evacuate server from failed host.""" + server = _find_server(cs, args.server) res = server.evacuate(args.host, args.on_shared_storage, args.password)[1] From cc4f3dfc9c7d290a4c7140a925a793df8a3e09f6 Mon Sep 17 00:00:00 2001 From: Rafi Khardalian Date: Tue, 19 Aug 2014 15:33:24 -0700 Subject: [PATCH 0589/1705] Enhance network-list to allow --fields Add the ability to specify --fields, using any valid vields, when doing a network-list. This code was shamelessly borrowed from do_list(). This will allow crafting network-list queries which mirror the functionality of 'nova-manage network list', which this query was intended to replace. Change-Id: I7ec7066350cdb979a64d20d14362b12e3ec76496 --- novaclient/tests/v1_1/fakes.py | 2 +- novaclient/tests/v1_1/test_shell.py | 7 +++++++ novaclient/v1_1/shell.py | 17 ++++++++++++++++- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index c3fade4a8..dcf2231bc 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -1708,7 +1708,7 @@ def get_os_hypervisors_1234_uptime(self, **kw): def get_os_networks(self, **kw): return (200, {}, {'networks': [{"label": "1", "cidr": "10.0.0.0/24", 'project_id': '4ffc664c198e435e9853f2538fbcd7a7', - 'id': '1'}]}) + 'id': '1', 'vlan': '1234'}]}) def delete_os_networks_1(self, **kw): return (202, {}, None) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index c167498b6..54821262d 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -1690,6 +1690,13 @@ def test_network_list(self): self.run_command('network-list') self.assert_called('GET', '/os-networks') + def test_network_list_fields(self): + output = self.run_command('network-list --fields ' + 'vlan,project_id') + self.assert_called('GET', '/os-networks') + self.assertIn('1234', output) + self.assertIn('4ffc664c198e435e9853f2538fbcd7a7', output) + def test_network_show(self): self.run_command('network-show 1') self.assert_called('GET', '/os-networks/1') diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 0c4b6d265..61c540dd9 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -779,10 +779,25 @@ def do_scrub(cs, args): cs.security_groups.delete(group) -def do_network_list(cs, _args): +@utils.arg('--fields', + default=None, + metavar='', + help='Comma-separated list of fields to display. ' + 'Use the show command to see which fields are available.') +def do_network_list(cs, args): """Print a list of available networks.""" network_list = cs.networks.list() columns = ['ID', 'Label', 'Cidr'] + + formatters = {} + field_titles = [] + if args.fields: + for field in args.fields.split(','): + field_title, formatter = utils._make_field_formatter(field, {}) + field_titles.append(field_title) + formatters[field_title] = formatter + + columns = columns + field_titles utils.print_list(network_list, columns) From f0beb29156419dd76ea6b1161c912f2585baf99c Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 22 Aug 2014 12:34:19 +0000 Subject: [PATCH 0590/1705] Updated from global requirements Change-Id: I6ccf81a6c605b21946cfcebf817fc1b25f23cae6 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index e2a03bfd2..e97a62c20 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -8,7 +8,7 @@ mock>=1.0 requests-mock>=0.4.0 # Apache-2.0 sphinx>=1.1.2,!=1.2.0,<1.3 python-keystoneclient>=0.10.0 -oslosphinx +oslosphinx>=2.2.0.0a2 testrepository>=0.0.18 testscenarios>=0.4 testtools>=0.9.34 From bcc009aac95f9d907e0c03c71b972423bb39e2c4 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Tue, 26 Aug 2014 02:59:51 +0000 Subject: [PATCH 0591/1705] Updated from global requirements Change-Id: I6c2fb676276746719e982d42857e1d03a502d886 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 55b1a0523..5e7317e97 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,6 @@ argparse iso8601>=0.1.9 PrettyTable>=0.7,<0.8 requests>=1.2.1 -simplejson>=2.0.9 +simplejson>=2.2.0 six>=1.7.0 Babel>=1.3 From aa30c13fc5f8fd6288a6f1c39b0bbb98bd441456 Mon Sep 17 00:00:00 2001 From: Tom Cammann Date: Mon, 18 Aug 2014 17:45:13 +0100 Subject: [PATCH 0592/1705] Update requirements.txt to include keystoneclient Keystone client is required by nova client and needs to be in requirements.txt. It is currently in test-requirements.txt but also needs to be in install requirements. Change Ief6cd7e752fd8c9e9157364f99e270da7faff074 introduced the requirement for keystone client. Change-Id: I91f58810e0511d0d4acc5bba0c0e946edf247896 Closes-Bug: 1358675 --- requirements.txt | 1 + test-requirements.txt | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 55b1a0523..784d52aa5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,3 +6,4 @@ requests>=1.2.1 simplejson>=2.0.9 six>=1.7.0 Babel>=1.3 +python-keystoneclient>=0.10.0 diff --git a/test-requirements.txt b/test-requirements.txt index e97a62c20..8f38a985e 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -7,7 +7,6 @@ keyring>=2.1,!=3.3 mock>=1.0 requests-mock>=0.4.0 # Apache-2.0 sphinx>=1.1.2,!=1.2.0,<1.3 -python-keystoneclient>=0.10.0 oslosphinx>=2.2.0.0a2 testrepository>=0.0.18 testscenarios>=0.4 From e871bd2f7e10e0271888180b36b30e5c113ac223 Mon Sep 17 00:00:00 2001 From: Jamie Lennox Date: Wed, 27 Aug 2014 10:19:18 +1000 Subject: [PATCH 0593/1705] Use Token fixtures from keystoneclient Keystoneclient provides a way of generating correctly formed tokens. We should test using those rather than things we copy and paste ourselves. Change-Id: I163940c3cd92060f68e9b1149865feeef0480e6e --- novaclient/tests/fixture_data/client.py | 61 ++------- novaclient/tests/test_auth_plugins.py | 29 +---- novaclient/tests/test_service_catalog.py | 124 +++--------------- novaclient/tests/v1_1/test_auth.py | 154 ++++------------------- 4 files changed, 56 insertions(+), 312 deletions(-) diff --git a/novaclient/tests/fixture_data/client.py b/novaclient/tests/fixture_data/client.py index 91aa47f21..e52d4a233 100644 --- a/novaclient/tests/fixture_data/client.py +++ b/novaclient/tests/fixture_data/client.py @@ -12,6 +12,7 @@ import fixtures from keystoneclient.auth.identity import v2 +from keystoneclient import fixture from keystoneclient import session from novaclient.v1_1 import client as v1_1client @@ -31,58 +32,14 @@ def __init__(self, requests, self.client = None self.requests = requests - self.token = { - 'access': { - "token": { - "id": "ab48a9efdfedb23ty3494", - "expires": "2010-11-01T03:32:15-05:00", - "tenant": { - "id": "345", - "name": "My Project" - } - }, - "user": { - "id": "123", - "name": "jqsmith", - "roles": [ - { - "id": "234", - "name": "compute:admin", - }, - { - "id": "235", - "name": "object-store:admin", - "tenantId": "1", - } - ], - "roles_links": [], - }, - "serviceCatalog": [ - { - "name": "Cloud Servers", - "type": "compute", - "endpoints": [ - { - "publicURL": self.compute_url, - "internalURL": "https://compute1.host/v1/1", - }, - ], - "endpoints_links": [], - }, - { - "name": "Cloud Servers", - "type": "computev3", - "endpoints": [ - { - "publicURL": self.compute_url, - "internalURL": "https://compute1.host/v1/1", - }, - ], - "endpoints_links": [], - }, - ], - } - } + self.token = fixture.V2Token() + self.token.set_scope() + + s = self.token.add_service('compute') + s.add_endpoint(self.compute_url) + + s = self.token.add_service('computev3') + s.add_endpoint(self.compute_url) def setUp(self): super(V1, self).setUp() diff --git a/novaclient/tests/test_auth_plugins.py b/novaclient/tests/test_auth_plugins.py index 9052141e1..2319f5e3b 100644 --- a/novaclient/tests/test_auth_plugins.py +++ b/novaclient/tests/test_auth_plugins.py @@ -15,6 +15,7 @@ import argparse +from keystoneclient import fixture import mock import pkg_resources import requests @@ -33,30 +34,10 @@ def mock_http_request(resp=None): """Mock an HTTP Request.""" if not resp: - resp = { - "access": { - "token": { - "expires": "12345", - "id": "FAKE_ID", - "tenant": { - "id": "FAKE_TENANT_ID", - } - }, - "serviceCatalog": [ - { - "type": "compute", - "endpoints": [ - { - "region": "RegionOne", - "adminURL": "http://localhost:8774/v1.1", - "internalURL": "http://localhost:8774/v1.1", - "publicURL": "http://localhost:8774/v1.1/", - }, - ], - }, - ], - }, - } + resp = fixture.V2Token() + resp.set_scope() + s = resp.add_service('compute') + s.add_endpoint("http://localhost:8774/v1.1", region='RegionOne') auth_response = utils.TestResponse({ "status_code": 200, diff --git a/novaclient/tests/test_service_catalog.py b/novaclient/tests/test_service_catalog.py index 9f224bb57..c7179c9c4 100644 --- a/novaclient/tests/test_service_catalog.py +++ b/novaclient/tests/test_service_catalog.py @@ -11,116 +11,32 @@ # License for the specific language governing permissions and limitations # under the License. +from keystoneclient import fixture + from novaclient import exceptions from novaclient import service_catalog from novaclient.tests import utils -# Taken directly from keystone/content/common/samples/auth.json -# Do not edit this structure. Instead, grab the latest from there. +SERVICE_CATALOG = fixture.V2Token() +SERVICE_CATALOG.set_scope() + +_s = SERVICE_CATALOG.add_service('compute') +_e = _s.add_endpoint("https://compute1.host/v1/1") +_e["tenantId"] = "1" +_e["versionId"] = "1.0" +_e = _s.add_endpoint("https://compute1.host/v1.1/2", region="North") +_e["tenantId"] = "2" +_e["versionId"] = "1.1" +_e = _s.add_endpoint("https://compute1.host/v2/1", region="North") +_e["tenantId"] = "1" +_e["versionId"] = "2" -SERVICE_CATALOG = { - "access": { - "token": { - "id": "ab48a9efdfedb23ty3494", - "expires": "2010-11-01T03:32:15-05:00", - "tenant": { - "id": "345", - "name": "My Project" - } - }, - "user": { - "id": "123", - "name": "jqsmith", - "roles": [ - { - "id": "234", - "name": "compute:admin", - }, - { - "id": "235", - "name": "object-store:admin", - "tenantId": "1", - } - ], - "roles_links": [], - }, - "serviceCatalog": [ - { - "name": "Cloud Servers", - "type": "compute", - "endpoints": [ - { - # Tenant 1, no region, v1.0 - "tenantId": "1", - "publicURL": "https://compute1.host/v1/1", - "internalURL": "https://compute1.host/v1/1", - "versionId": "1.0", - "versionInfo": "https://compute1.host/v1.0/", - "versionList": "https://compute1.host/" - }, - { - # Tenant 2, with region, v1.1 - "tenantId": "2", - "publicURL": "https://compute1.host/v1.1/2", - "internalURL": "https://compute1.host/v1.1/2", - "region": "North", - "versionId": "1.1", - "versionInfo": "https://compute1.host/v1.1/", - "versionList": "https://compute1.host/" - }, - { - # Tenant 1, with region, v2.0 - "tenantId": "1", - "publicURL": "https://compute1.host/v2/1", - "internalURL": "https://compute1.host/v2/1", - "region": "North", - "versionId": "2", - "versionInfo": "https://compute1.host/v2/", - "versionList": "https://compute1.host/" - }, - ], - "endpoints_links": [], - }, - { - "name": "Nova Volumes", - "type": "volume", - "endpoints": [ - { - "tenantId": "1", - "publicURL": "https://volume1.host/v1/1", - "internalURL": "https://volume1.host/v1/1", - "region": "South", - "versionId": "1.0", - "versionInfo": "uri", - "versionList": "uri" - }, - { - "tenantId": "2", - "publicURL": "https://volume1.host/v1.1/2", - "internalURL": "https://volume1.host/v1.1/2", - "region": "South", - "versionId": "1.1", - "versionInfo": "https://volume1.host/v1.1/", - "versionList": "https://volume1.host/" - }, - ], - "endpoints_links": [ - { - "rel": "next", - "href": "https://identity1.host/v2.0/endpoints" - }, - ], - }, - ], - "serviceCatalog_links": [ - { - "rel": "next", - "href": "https://identity.host/v2.0/endpoints?session=2hfh8Ar", - }, - ], - }, -} +_s = SERVICE_CATALOG.add_service('volume') +_e = _s.add_endpoint("https://volume1.host/v1/1", region="South") +_e["tenantId"] = "1" +_e = _s.add_endpoint("https://volume1.host/v1.1/2", region="South") +_e["tenantId"] = "2" class ServiceCatalogTest(utils.TestCase): diff --git a/novaclient/tests/v1_1/test_auth.py b/novaclient/tests/v1_1/test_auth.py index ea24d34f5..6355b3245 100644 --- a/novaclient/tests/v1_1/test_auth.py +++ b/novaclient/tests/v1_1/test_auth.py @@ -14,6 +14,7 @@ import copy import json +from keystoneclient import fixture import mock import requests @@ -23,33 +24,21 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase): + + def get_token(self, **kwargs): + resp = fixture.V2Token(**kwargs) + resp.set_scope() + + s = resp.add_service('compute') + s.add_endpoint('http://localhost:8774/v1.1', region='RegionOne') + + return resp + def test_authenticate_success(self): cs = client.Client("username", "password", "project_id", utils.AUTH_URL_V2, service_type='compute') - resp = { - "access": { - "token": { - "expires": "12345", - "id": "FAKE_ID", - "tenant": { - "id": "FAKE_TENANT_ID", - } - }, - "serviceCatalog": [ - { - "type": "compute", - "endpoints": [ - { - "region": "RegionOne", - "adminURL": "http://localhost:8774/v1.1", - "internalURL": "http://localhost:8774/v1.1", - "publicURL": "http://localhost:8774/v1.1/", - }, - ], - }, - ], - }, - } + resp = self.get_token() + auth_response = utils.TestResponse({ "status_code": 200, "text": json.dumps(resp), @@ -112,30 +101,7 @@ def test_auth_call(): def test_v1_auth_redirect(self): cs = client.Client("username", "password", "project_id", utils.AUTH_URL_V1, service_type='compute') - dict_correct_response = { - "access": { - "token": { - "expires": "12345", - "id": "FAKE_ID", - "tenant": { - "id": "FAKE_TENANT_ID", - } - }, - "serviceCatalog": [ - { - "type": "compute", - "endpoints": [ - { - "adminURL": "http://localhost:8774/v1.1", - "region": "RegionOne", - "internalURL": "http://localhost:8774/v1.1", - "publicURL": "http://localhost:8774/v1.1/", - }, - ], - }, - ], - }, - } + dict_correct_response = self.get_token() correct_response = json.dumps(dict_correct_response) dict_responses = [ {"headers": {'location': 'http://127.0.0.1:5001'}, @@ -200,30 +166,7 @@ def test_auth_call(): def test_v2_auth_redirect(self): cs = client.Client("username", "password", "project_id", utils.AUTH_URL_V2, service_type='compute') - dict_correct_response = { - "access": { - "token": { - "expires": "12345", - "id": "FAKE_ID", - "tenant": { - "id": "FAKE_TENANT_ID", - } - }, - "serviceCatalog": [ - { - "type": "compute", - "endpoints": [ - { - "adminURL": "http://localhost:8774/v1.1", - "region": "RegionOne", - "internalURL": "http://localhost:8774/v1.1", - "publicURL": "http://localhost:8774/v1.1/", - }, - ], - }, - ], - }, - } + dict_correct_response = self.get_token() correct_response = json.dumps(dict_correct_response) dict_responses = [ {"headers": {'location': 'http://127.0.0.1:5001'}, @@ -288,42 +231,12 @@ def test_auth_call(): def test_ambiguous_endpoints(self): cs = client.Client("username", "password", "project_id", utils.AUTH_URL_V2, service_type='compute') - resp = { - "access": { - "token": { - "expires": "12345", - "id": "FAKE_ID", - "tenant": { - "id": "FAKE_TENANT_ID", - } - }, - "serviceCatalog": [ - { - "adminURL": "http://localhost:8774/v1.1", - "type": "compute", - "name": "Compute CLoud", - "endpoints": [ - { - "region": "RegionOne", - "internalURL": "http://localhost:8774/v1.1", - "publicURL": "http://localhost:8774/v1.1/", - }, - ], - }, - { - "adminURL": "http://localhost:8774/v1.1", - "type": "compute", - "name": "Hyper-compute Cloud", - "endpoints": [ - { - "internalURL": "http://localhost:8774/v1.1", - "publicURL": "http://localhost:8774/v1.1/", - }, - ], - }, - ], - }, - } + resp = self.get_token() + + # duplicate existing service + s = resp.add_service('compute') + s.add_endpoint('http://localhost:8774/v1.1', region='RegionOne') + auth_response = utils.TestResponse({ "status_code": 200, "text": json.dumps(resp), @@ -342,30 +255,7 @@ def test_authenticate_with_token_success(self): cs = client.Client("username", None, "project_id", utils.AUTH_URL_V2, service_type='compute') cs.client.auth_token = "FAKE_ID" - resp = { - "access": { - "token": { - "expires": "12345", - "id": "FAKE_ID", - "tenant": { - "id": "FAKE_TENANT_ID", - } - }, - "serviceCatalog": [ - { - "type": "compute", - "endpoints": [ - { - "region": "RegionOne", - "adminURL": "http://localhost:8774/v1.1", - "internalURL": "http://localhost:8774/v1.1", - "publicURL": "http://localhost:8774/v1.1/", - }, - ], - }, - ], - }, - } + resp = self.get_token(token_id="FAKE_ID") auth_response = utils.TestResponse({ "status_code": 200, "text": json.dumps(resp), From 392148c7ef6d05cb5fd05d8287af460a6651ee4d Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Wed, 27 Aug 2014 18:08:14 +0300 Subject: [PATCH 0594/1705] Use oslo.utils Modules `strutils`, `timeutils` and `network_utils` from common code are graduated in `oslo.utils`, so we can: 1. remove `novaclient.openstack.common.network_utils` and use `oslo.utils.netutils` instead. 2. use `oslo.utils.encodeutils` and `oslo.utils.strutils` instead of `novaclient.openstack.common.strutils`. 3. use `oslo.utils.timeutils` instead of `novaclient.openstack.common.timeutils`. Additional information: - modules `importutils`, `strutils` and `timeutils` from `novaclient.openstack.common` cannot be removed, because: - importutils is used by apiclient and jsonutils; - strutils is used by apiclient, cliutils and jsonutils; - timeutils is used by jsonutils - additional check for `safe_encode` in Py3 is required, since If91a866d864a22d28a352152beff4c7406a27b7b was merged. Change-Id: Ib8d79d9c85af4916e87a76a1a67a13488ddaa111 --- novaclient/client.py | 4 +- novaclient/openstack/common/network_utils.py | 108 ------------------- novaclient/shell.py | 9 +- novaclient/tests/v1_1/fakes.py | 2 +- novaclient/tests/v1_1/test_shell.py | 7 +- novaclient/tests/v3/fakes.py | 3 +- novaclient/utils.py | 20 +++- novaclient/v1_1/flavors.py | 2 +- novaclient/v1_1/servers.py | 4 +- novaclient/v1_1/shell.py | 7 +- novaclient/v3/images.py | 5 +- novaclient/v3/servers.py | 4 +- novaclient/v3/shell.py | 4 +- openstack-common.conf | 4 - requirements.txt | 1 + 15 files changed, 44 insertions(+), 140 deletions(-) delete mode 100644 novaclient/openstack/common/network_utils.py diff --git a/novaclient/client.py b/novaclient/client.py index 36baffa58..b0d9aea10 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -31,6 +31,7 @@ import time from keystoneclient import adapter +from oslo.utils import netutils import requests from requests import adapters @@ -43,7 +44,6 @@ from novaclient import exceptions from novaclient.openstack.common.gettextutils import _ -from novaclient.openstack.common import network_utils from novaclient import service_catalog from novaclient import utils @@ -558,7 +558,7 @@ def _fetch_endpoints_from_auth(self, url): extract_token=False) def authenticate(self): - magic_tuple = network_utils.urlsplit(self.auth_url) + magic_tuple = netutils.urlsplit(self.auth_url) scheme, netloc, path, query, frag = magic_tuple port = magic_tuple.port if port is None: diff --git a/novaclient/openstack/common/network_utils.py b/novaclient/openstack/common/network_utils.py deleted file mode 100644 index fa812b29f..000000000 --- a/novaclient/openstack/common/network_utils.py +++ /dev/null @@ -1,108 +0,0 @@ -# Copyright 2012 OpenStack Foundation. -# All Rights Reserved. -# -# 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. - -""" -Network-related utilities and helper functions. -""" - -# TODO(jd) Use six.moves once -# https://bitbucket.org/gutworth/six/pull-request/28 -# is merged -try: - import urllib.parse - SplitResult = urllib.parse.SplitResult -except ImportError: - import urlparse - SplitResult = urlparse.SplitResult - -from six.moves.urllib import parse - - -def parse_host_port(address, default_port=None): - """Interpret a string as a host:port pair. - - An IPv6 address MUST be escaped if accompanied by a port, - because otherwise ambiguity ensues: 2001:db8:85a3::8a2e:370:7334 - means both [2001:db8:85a3::8a2e:370:7334] and - [2001:db8:85a3::8a2e:370]:7334. - - >>> parse_host_port('server01:80') - ('server01', 80) - >>> parse_host_port('server01') - ('server01', None) - >>> parse_host_port('server01', default_port=1234) - ('server01', 1234) - >>> parse_host_port('[::1]:80') - ('::1', 80) - >>> parse_host_port('[::1]') - ('::1', None) - >>> parse_host_port('[::1]', default_port=1234) - ('::1', 1234) - >>> parse_host_port('2001:db8:85a3::8a2e:370:7334', default_port=1234) - ('2001:db8:85a3::8a2e:370:7334', 1234) - - """ - if address[0] == '[': - # Escaped ipv6 - _host, _port = address[1:].split(']') - host = _host - if ':' in _port: - port = _port.split(':')[1] - else: - port = default_port - else: - if address.count(':') == 1: - host, port = address.split(':') - else: - # 0 means ipv4, >1 means ipv6. - # We prohibit unescaped ipv6 addresses with port. - host = address - port = default_port - - return (host, None if port is None else int(port)) - - -class ModifiedSplitResult(SplitResult): - """Split results class for urlsplit.""" - - # NOTE(dims): The functions below are needed for Python 2.6.x. - # We can remove these when we drop support for 2.6.x. - @property - def hostname(self): - netloc = self.netloc.split('@', 1)[-1] - host, port = parse_host_port(netloc) - return host - - @property - def port(self): - netloc = self.netloc.split('@', 1)[-1] - host, port = parse_host_port(netloc) - return port - - -def urlsplit(url, scheme='', allow_fragments=True): - """Parse a URL using urlparse.urlsplit(), splitting query and fragments. - This function papers over Python issue9374 when needed. - - The parameters are the same as urlparse.urlsplit. - """ - scheme, netloc, path, query, fragment = parse.urlsplit( - url, scheme, allow_fragments) - if allow_fragments and '#' in path: - path, fragment = path.split('#', 1) - if '?' in path: - path, query = path.split('?', 1) - return ModifiedSplitResult(scheme, netloc, - path, query, fragment) diff --git a/novaclient/shell.py b/novaclient/shell.py index 7e9be0bf2..6623b7fbf 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -29,6 +29,8 @@ import pkgutil import sys +from oslo.utils import encodeutils +from oslo.utils import strutils import pkg_resources import six @@ -47,7 +49,6 @@ import novaclient.extension from novaclient.openstack.common import cliutils from novaclient.openstack.common.gettextutils import _ -from novaclient.openstack.common import strutils from novaclient import utils from novaclient.v1_1 import shell as shell_v1_1 from novaclient.v3 import shell as shell_v3 @@ -795,13 +796,13 @@ def start_section(self, heading): def main(): try: - argv = [strutils.safe_decode(a) for a in sys.argv[1:]] + argv = [encodeutils.safe_decode(a) for a in sys.argv[1:]] OpenStackComputeShell().main(argv) except Exception as e: logger.debug(e, exc_info=1) - details = {'name': strutils.safe_encode(e.__class__.__name__), - 'msg': strutils.safe_encode(six.text_type(e))} + details = {'name': encodeutils.safe_encode(e.__class__.__name__), + 'msg': encodeutils.safe_encode(six.text_type(e))} print("ERROR (%(name)s): %(msg)s" % details, file=sys.stderr) sys.exit(1) diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index afd043bae..c431464b4 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -16,12 +16,12 @@ import datetime +from oslo.utils import strutils import six from six.moves.urllib import parse from novaclient import client as base_client from novaclient import exceptions -from novaclient.openstack.common import strutils from novaclient.tests import fakes from novaclient.tests import utils from novaclient.v1_1 import client diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index ab9d0595b..4bcd7f7aa 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -22,6 +22,7 @@ import fixtures import mock +from oslo.utils import timeutils import six from six.moves import builtins @@ -1232,9 +1233,9 @@ def test_usage_list(self): 'end=2005-02-01T00:00:00&' + 'detailed=1') - @mock.patch('novaclient.openstack.common.timeutils.utcnow') - def test_usage_list_no_args(self, mock_utcnow): - mock_utcnow.return_value = datetime.datetime(2005, 2, 1, 0, 0) + def test_usage_list_no_args(self): + timeutils.set_time_override(datetime.datetime(2005, 2, 1, 0, 0)) + self.addCleanup(timeutils.clear_time_override) self.run_command('usage-list') self.assert_called('GET', '/os-simple-tenant-usage?' + diff --git a/novaclient/tests/v3/fakes.py b/novaclient/tests/v3/fakes.py index 854545d7e..23f1e7cbd 100644 --- a/novaclient/tests/v3/fakes.py +++ b/novaclient/tests/v3/fakes.py @@ -16,7 +16,8 @@ import datetime -from novaclient.openstack.common import strutils +from oslo.utils import strutils + from novaclient.tests import fakes from novaclient.tests.v1_1 import fakes as fakes_v1_1 from novaclient.v3 import client diff --git a/novaclient/utils.py b/novaclient/utils.py index 8600cab05..d6068ba0c 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -17,6 +17,7 @@ import textwrap import uuid +from oslo.utils import encodeutils import pkg_resources import prettytable import six @@ -25,7 +26,6 @@ from novaclient.openstack.common import cliutils from novaclient.openstack.common.gettextutils import _ from novaclient.openstack.common import jsonutils -from novaclient.openstack.common import strutils arg = cliutils.arg @@ -127,9 +127,12 @@ def print_list(objs, fields, formatters={}, sortby_index=None): pt.add_row(row) if sortby is not None: - result = strutils.safe_encode(pt.get_string(sortby=sortby)) + result = encodeutils.safe_encode(pt.get_string(sortby=sortby)) else: - result = strutils.safe_encode(pt.get_string()) + result = encodeutils.safe_encode(pt.get_string()) + + if six.PY3: + result = result.decode() print(result) @@ -197,7 +200,10 @@ def print_dict(d, dict_property="Property", dict_value="Value", wrap=0): v = '-' pt.add_row([k, v]) - result = strutils.safe_encode(pt.get_string()) + result = encodeutils.safe_encode(pt.get_string()) + + if six.PY3: + result = result.decode() print(result) @@ -219,7 +225,11 @@ def find_resource(manager, name_or_id, **find_args): # now try to get entity as uuid try: - tmp_id = strutils.safe_encode(name_or_id) + tmp_id = encodeutils.safe_encode(name_or_id) + + if six.PY3: + tmp_id = tmp_id.decode() + uuid.UUID(tmp_id) return manager.get(tmp_id) except (TypeError, ValueError, exceptions.NotFound): diff --git a/novaclient/v1_1/flavors.py b/novaclient/v1_1/flavors.py index 30536fc68..e66e0b9e2 100644 --- a/novaclient/v1_1/flavors.py +++ b/novaclient/v1_1/flavors.py @@ -16,12 +16,12 @@ Flavor interface. """ +from oslo.utils import strutils from six.moves.urllib import parse from novaclient import base from novaclient import exceptions from novaclient.openstack.common.gettextutils import _ -from novaclient.openstack.common import strutils from novaclient import utils diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index 2a28431af..d1ced5218 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -21,13 +21,13 @@ import base64 +from oslo.utils import encodeutils import six from six.moves.urllib import parse from novaclient import base from novaclient import crypto from novaclient.openstack.common.gettextutils import _ -from novaclient.openstack.common import strutils from novaclient.v1_1 import security_groups @@ -454,7 +454,7 @@ def _boot(self, resource_url, response_key, name, image, flavor, if six.PY3: userdata = userdata.encode("utf-8") else: - userdata = strutils.safe_encode(userdata) + userdata = encodeutils.safe_encode(userdata) userdata_b64 = base64.b64encode(userdata).decode('utf-8') body["server"]["user_data"] = userdata_b64 diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 4a11340bf..0dcf29615 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -28,12 +28,13 @@ import sys import time +from oslo.utils import encodeutils +from oslo.utils import strutils +from oslo.utils import timeutils import six from novaclient import exceptions from novaclient.openstack.common.gettextutils import _ -from novaclient.openstack.common import strutils -from novaclient.openstack.common import timeutils from novaclient.openstack.common import uuidutils from novaclient import utils from novaclient.v1_1 import availability_zones @@ -2301,7 +2302,7 @@ def _print_secgroups(secgroups): def _get_secgroup(cs, secgroup): # Check secgroup is an ID (nova-network) or UUID (neutron) - if (utils.is_integer_like(strutils.safe_encode(secgroup)) + if (utils.is_integer_like(encodeutils.safe_encode(secgroup)) or uuidutils.is_uuid_like(secgroup)): try: return cs.security_groups.get(secgroup) diff --git a/novaclient/v3/images.py b/novaclient/v3/images.py index b78d9af55..7bc3d584c 100644 --- a/novaclient/v3/images.py +++ b/novaclient/v3/images.py @@ -17,10 +17,11 @@ Image interface. """ +from oslo.utils import encodeutils +from oslo.utils import strutils from six.moves.urllib import parse from novaclient import base -from novaclient.openstack.common import strutils class Image(base.Resource): @@ -51,7 +52,7 @@ class ImageManager(base.ManagerWithFind): def _image_meta_from_headers(self, headers): meta = {'properties': {}} - safe_decode = strutils.safe_decode + safe_decode = encodeutils.safe_decode for key, value in headers.items(): value = safe_decode(value, incoming='utf-8') if key.startswith('x-image-meta-property-'): diff --git a/novaclient/v3/servers.py b/novaclient/v3/servers.py index 2d58650bc..226cf92a1 100644 --- a/novaclient/v3/servers.py +++ b/novaclient/v3/servers.py @@ -21,13 +21,13 @@ import base64 +from oslo.utils import encodeutils import six from six.moves.urllib import parse from novaclient import base from novaclient import crypto from novaclient.openstack.common.gettextutils import _ -from novaclient.openstack.common import strutils REBOOT_SOFT, REBOOT_HARD = 'SOFT', 'HARD' @@ -402,7 +402,7 @@ def _boot(self, resource_url, response_key, name, image, flavor, if six.PY3: userdata = userdata.encode("utf-8") else: - userdata = strutils.safe_encode(userdata) + userdata = encodeutils.safe_encode(userdata) data = base64.b64encode(userdata).decode('utf-8') body["server"]["os-user-data:user_data"] = data diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 18243412d..832b45d51 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -27,12 +27,12 @@ import sys import time +from oslo.utils import strutils +from oslo.utils import timeutils import six from novaclient import exceptions from novaclient.openstack.common.gettextutils import _ -from novaclient.openstack.common import strutils -from novaclient.openstack.common import timeutils from novaclient.openstack.common import uuidutils from novaclient import utils from novaclient.v3 import availability_zones diff --git a/openstack-common.conf b/openstack-common.conf index f21e5b551..3842ed9ba 100644 --- a/openstack-common.conf +++ b/openstack-common.conf @@ -5,11 +5,7 @@ module=apiclient module=cliutils module=gettextutils module=install_venv_common -module=importutils module=jsonutils -module=network_utils -module=strutils -module=timeutils module=uuidutils # The base module to hold the copy of openstack.common diff --git a/requirements.txt b/requirements.txt index 784d52aa5..1c7e8be4a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ pbr>=0.6,!=0.7,<1.0 argparse iso8601>=0.1.9 +oslo.utils>=0.2.0 # Apache-2.0 PrettyTable>=0.7,<0.8 requests>=1.2.1 simplejson>=2.0.9 From 319b61aa2f7aa8f4ef424e86add4b3dea87afb84 Mon Sep 17 00:00:00 2001 From: Jamie Lennox Date: Sun, 31 Aug 2014 21:56:24 +1000 Subject: [PATCH 0595/1705] Fix test mistake with requests-mock body= parameters are supposed to be io objects. This is obviously during the conversion from HTTPretty. Change-Id: I2f2077fbe219805c4aaf9db19bc53ad7e4dde4dd Closes-Bug: #1363632 --- novaclient/tests/fixture_data/floatingips.py | 4 ++-- novaclient/tests/fixture_data/servers.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/novaclient/tests/fixture_data/floatingips.py b/novaclient/tests/fixture_data/floatingips.py index d2c1b8016..5fc6848a7 100644 --- a/novaclient/tests/fixture_data/floatingips.py +++ b/novaclient/tests/fixture_data/floatingips.py @@ -94,7 +94,7 @@ def put_dns_testdomain_entries_testname(request, context): context.status_code = 205 return request.body self.requests.register_uri('PUT', url, - body=put_dns_testdomain_entries_testname, + text=put_dns_testdomain_entries_testname, headers=self.json_headers) url = self.url('testdomain', 'entries') @@ -140,7 +140,7 @@ def put_os_floating_ip_dns_testdomain(request, context): return request.body self.requests.register_uri('PUT', self.url('testdomain'), - body=put_os_floating_ip_dns_testdomain, + text=put_os_floating_ip_dns_testdomain, status_code=205, headers=self.json_headers) diff --git a/novaclient/tests/fixture_data/servers.py b/novaclient/tests/fixture_data/servers.py index 6f753fc85..85be06868 100644 --- a/novaclient/tests/fixture_data/servers.py +++ b/novaclient/tests/fixture_data/servers.py @@ -250,7 +250,7 @@ def put_servers_1234(request, context): return request.body self.requests.register_uri('PUT', self.url(1234), - body=put_servers_1234, + text=put_servers_1234, status_code=204, headers=self.json_headers) From 07260236ab2179579e0d0d2f9b7fb8027652dc32 Mon Sep 17 00:00:00 2001 From: Joe Gordon Date: Thu, 4 Sep 2014 03:45:13 +0000 Subject: [PATCH 0596/1705] Make findall support server side filtering Instead of listing all servers and doing clientside filtering, use the servers filtering on name.the server's list already supports filtering so just pass a search_opts dictionary into list(). This should speed up nova commands when a user has large numbers of servers. Change-Id: I6deea8523754ff213f43bd059fb00f34fc0e1a12 Closes-Bug: #1202179 --- novaclient/base.py | 20 ++++++++++++++------ novaclient/tests/v1_1/test_shell.py | 14 +++++++------- novaclient/tests/v3/test_shell.py | 4 ++-- 3 files changed, 23 insertions(+), 15 deletions(-) diff --git a/novaclient/base.py b/novaclient/base.py index ff8308bde..173eda085 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -131,9 +131,6 @@ def list(self): def find(self, **kwargs): """ Find a single item with attributes matching ``**kwargs``. - - This isn't very efficient: it loads the entire list then filters on - the Python side. """ matches = self.findall(**kwargs) num_matches = len(matches) @@ -148,9 +145,6 @@ def find(self, **kwargs): def findall(self, **kwargs): """ Find all items with attributes matching ``**kwargs``. - - This isn't very efficient: it loads the entire list then filters on - the Python side. """ found = [] searches = kwargs.items() @@ -173,6 +167,20 @@ def findall(self, **kwargs): del tmp_kwargs['is_public'] searches = tmp_kwargs.items() + if 'search_opts' in list_argspec.args: + # pass search_opts in to do server side based filtering. + # TODO(jogo) not all search_opts support regex, find way to + # identify when to use regex and when to use string matching. + # volumes does not support regex while servers does. So when + # doing findall on servers some client side filtering is still + # needed. + if "human_id" in kwargs: + list_kwargs['search_opts'] = {"name": kwargs["human_id"]} + elif "name" in kwargs: + list_kwargs['search_opts'] = {"name": kwargs["name"]} + elif "display_name" in kwargs: + list_kwargs['search_opts'] = {"name": kwargs["display_name"]} + listing = self.list(**list_kwargs) for obj in listing: diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index 9005759b4..fc5696045 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -857,7 +857,7 @@ def test_reboot(self): def test_rebuild(self): output = self.run_command('rebuild sample-server 1') - self.assert_called('GET', '/servers', pos=-6) + self.assert_called('GET', '/servers?name=sample-server', pos=-6) self.assert_called('GET', '/servers/1234', pos=-5) self.assert_called('GET', '/images/1', pos=-4) self.assert_called('POST', '/servers/1234/action', @@ -869,7 +869,7 @@ def test_rebuild(self): def test_rebuild_password(self): output = self.run_command('rebuild sample-server 1' ' --rebuild-password asdf') - self.assert_called('GET', '/servers', pos=-6) + self.assert_called('GET', '/servers?name=sample-server', pos=-6) self.assert_called('GET', '/servers/1234', pos=-5) self.assert_called('GET', '/images/1', pos=-4) self.assert_called('POST', '/servers/1234/action', @@ -881,7 +881,7 @@ def test_rebuild_password(self): def test_rebuild_preserve_ephemeral(self): self.run_command('rebuild sample-server 1 --preserve-ephemeral') - self.assert_called('GET', '/servers', pos=-6) + self.assert_called('GET', '/servers?name=sample-server', pos=-6) self.assert_called('GET', '/servers/1234', pos=-5) self.assert_called('GET', '/images/1', pos=-4) self.assert_called('POST', '/servers/1234/action', @@ -893,7 +893,7 @@ def test_rebuild_preserve_ephemeral(self): def test_rebuild_name_meta(self): self.run_command('rebuild sample-server 1 --name asdf --meta ' 'foo=bar') - self.assert_called('GET', '/servers', pos=-6) + self.assert_called('GET', '/servers?name=sample-server', pos=-6) self.assert_called('GET', '/servers/1234', pos=-5) self.assert_called('GET', '/images/1', pos=-4) self.assert_called('POST', '/servers/1234/action', @@ -1043,10 +1043,10 @@ def test_delete_two_with_two_existent(self): self.assert_called('DELETE', '/servers/1234', pos=-3) self.assert_called('DELETE', '/servers/5678', pos=-1) self.run_command('delete sample-server sample-server2') - self.assert_called('GET', '/servers', pos=-6) + self.assert_called('GET', '/servers?name=sample-server', pos=-6) self.assert_called('GET', '/servers/1234', pos=-5) self.assert_called('DELETE', '/servers/1234', pos=-4) - self.assert_called('GET', '/servers', pos=-3) + self.assert_called('GET', '/servers?name=sample-server2', pos=-3) self.assert_called('GET', '/servers/5678', pos=-2) self.assert_called('DELETE', '/servers/5678', pos=-1) @@ -1995,7 +1995,7 @@ def test_volume_list(self): def test_volume_show(self): self.run_command('volume-show Work') - self.assert_called('GET', '/volumes', pos=-2) + self.assert_called('GET', '/volumes?name=Work', pos=-2) self.assert_called( 'GET', '/volumes/15e59938-07d5-11e1-90e3-e3dffe0c5983', diff --git a/novaclient/tests/v3/test_shell.py b/novaclient/tests/v3/test_shell.py index 569ddabcb..0b6b747cf 100644 --- a/novaclient/tests/v3/test_shell.py +++ b/novaclient/tests/v3/test_shell.py @@ -655,10 +655,10 @@ def test_delete_two_with_two_existent(self): self.assert_called('DELETE', '/servers/1234', pos=-3) self.assert_called('DELETE', '/servers/5678', pos=-1) self.run_command('delete sample-server sample-server2') - self.assert_called('GET', '/servers', pos=-6) + self.assert_called('GET', '/servers?name=sample-server', pos=-6) self.assert_called('GET', '/servers/1234', pos=-5) self.assert_called('DELETE', '/servers/1234', pos=-4) - self.assert_called('GET', '/servers', pos=-3) + self.assert_called('GET', '/servers?name=sample-server2', pos=-3) self.assert_called('GET', '/servers/5678', pos=-2) self.assert_called('DELETE', '/servers/5678', pos=-1) From e7f880532d774cba352b6e3449b1baecc42ba560 Mon Sep 17 00:00:00 2001 From: Sahid Orentino Ferdjaoui Date: Tue, 9 Sep 2014 08:55:25 +0000 Subject: [PATCH 0597/1705] add new command get-serial-console Adds v1.1 command get-serial-console Change-Id: Id6b1e470088a72409f5609538be72c3cf2288a86 Parial-Implements: blueprint serial-ports --- novaclient/tests/fixture_data/servers.py | 3 +++ novaclient/tests/v1_1/test_servers.py | 8 ++++++++ novaclient/v1_1/servers.py | 19 +++++++++++++++++++ novaclient/v1_1/shell.py | 21 +++++++++++++++++++++ 4 files changed, 51 insertions(+) diff --git a/novaclient/tests/fixture_data/servers.py b/novaclient/tests/fixture_data/servers.py index 85be06868..c8f87bf60 100644 --- a/novaclient/tests/fixture_data/servers.py +++ b/novaclient/tests/fixture_data/servers.py @@ -457,6 +457,8 @@ def post_servers_1234_action(self, request, context): assert list(body[action]) == ['type'] elif action == 'os-getRDPConsole': assert list(body[action]) == ['type'] + elif action == 'os-getSerialConsole': + assert list(body[action]) == ['type'] elif action == 'os-migrateLive': assert set(body[action].keys()) == set(['host', 'block_migration', @@ -568,6 +570,7 @@ def post_servers_1234_action(self, request, context): 'get_console_output': ['length'], 'get_vnc_console': ['type'], 'get_spice_console': ['type'], + 'get_serial_console': ['type'], 'reset_state': ['state'], 'create_image': ['name', 'metadata'], 'migrate_live': ['host', 'block_migration', 'disk_over_commit'], diff --git a/novaclient/tests/v1_1/test_servers.py b/novaclient/tests/v1_1/test_servers.py index 9df2edf4b..8fbe1a0ed 100644 --- a/novaclient/tests/v1_1/test_servers.py +++ b/novaclient/tests/v1_1/test_servers.py @@ -548,6 +548,14 @@ def test_get_spice_console(self): self.cs.servers.get_spice_console(s, 'fake') self.assert_called('POST', '/servers/1234/action') + def test_get_serial_console(self): + s = self.cs.servers.get(1234) + s.get_serial_console('fake') + self.assert_called('POST', '/servers/1234/action') + + self.cs.servers.get_serial_console(s, 'fake') + self.assert_called('POST', '/servers/1234/action') + def test_get_rdp_console(self): s = self.cs.servers.get(1234) s.get_rdp_console('fake') diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index d1ced5218..43b45ebb7 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -86,6 +86,14 @@ def get_rdp_console(self, console_type): """ return self.manager.get_rdp_console(self, console_type) + def get_serial_console(self, console_type): + """ + Get serial console for a Server. + + :param console_type: Type of console ('serial') + """ + return self.manager.get_serial_console(self, console_type) + def get_password(self, private_key=None): """ Get password for a Server. @@ -675,6 +683,17 @@ def get_rdp_console(self, server, console_type): return self._action('os-getRDPConsole', server, {'type': console_type})[1] + def get_serial_console(self, server, console_type): + """ + Get a serial console for an instance + + :param server: The :class:`Server` (or its ID) to add an IP to. + :param console_type: Type of serial console to get ('serial') + """ + + return self._action('os-getSerialConsole', server, + {'type': console_type})[1] + def get_password(self, server, private_key=None): """ Get password for an instance diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index cb6b63167..8b2343417 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2022,6 +2022,27 @@ def __init__(self, console_dict): utils.print_list([RDPConsole(data['console'])], ['Type', 'Url']) +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('--console_type', default='serial', + help=_('Type of serial console, default="serial".')) +def do_get_serial_console(cs, args): + """Get a serial console to a server.""" + if args.console_type not in ('serial',): + raise exceptions.CommandError( + _("Invalid parameter value for 'console_type', " + "currently supported 'serial'.")) + + server = _find_server(cs, args.server) + data = server.get_serial_console(args.console_type) + + class SerialConsole: + def __init__(self, console_dict): + self.type = console_dict['type'] + self.url = console_dict['url'] + + utils.print_list([SerialConsole(data['console'])], ['Type', 'Url']) + + @utils.arg('server', metavar='', help='Name or ID of server.') @utils.arg('private_key', metavar='', From 2a1c07e790cc95b1e847974e4c757f826507834f Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Thu, 11 Sep 2014 00:31:16 +0000 Subject: [PATCH 0598/1705] Updated from global requirements Change-Id: I76f8003d3e387bd67a017435d21dc340c7ff0124 --- requirements.txt | 7 +++++-- test-requirements.txt | 3 +++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 9fe439178..9d3c1bdc0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,12 @@ +# 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. pbr>=0.6,!=0.7,<1.0 argparse iso8601>=0.1.9 -oslo.utils>=0.2.0 # Apache-2.0 +oslo.utils>=0.3.0 # Apache-2.0 PrettyTable>=0.7,<0.8 -requests>=1.2.1 +requests>=1.2.1,!=2.4.0 simplejson>=2.2.0 six>=1.7.0 Babel>=1.3 diff --git a/test-requirements.txt b/test-requirements.txt index 8f38a985e..02deabc2e 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,3 +1,6 @@ +# 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. hacking>=0.9.2,<0.10 coverage>=3.6 From 3d68063809547e37f862dba448dcf669b7a4445c Mon Sep 17 00:00:00 2001 From: Sean Dague Date: Thu, 11 Sep 2014 20:44:44 -0400 Subject: [PATCH 0599/1705] Don't display duplicated security groups As reported by users, we end up displaying duplicate security groups on multi nic vms using neutron. Let's not do that. Also add security group listing to the v3 client to make it consistent. Change-Id: I0a983aac08aaeacf3c2aef5aae49f64265fe78c5 Closes-Bug: #1331307 --- novaclient/v1_1/shell.py | 7 +++++-- novaclient/v3/shell.py | 7 +++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 7c3dba302..5378e2954 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1610,8 +1610,11 @@ def _print_server(cs, args, server=None): flavor_id) if 'security_groups' in info: - info['security_groups'] = \ - ', '.join(group['name'] for group in info['security_groups']) + # when we have multiple nics the info will include the + # security groups N times where N == number of nics. Be nice + # and only display it once. + info['security_groups'] = ', '.join( + sorted(set(group['name'] for group in info['security_groups']))) image = info.get('image', {}) if image: diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 30f881a67..63ae257b5 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -1334,6 +1334,13 @@ def _print_server(cs, args, server=None): info['flavor'] = '%s (%s)' % (_find_flavor(cs, flavor_id).name, flavor_id) + if 'security_groups' in info: + # when we have multiple nics the info will include the + # security groups N times where N == number of nics. Be nice + # and only display it once. + info['security_groups'] = ', '.join( + sorted(set(group['name'] for group in info['security_groups']))) + image = info.get('image', {}) if image: image_id = image.get('id', '') From 4bd0c389c0238200ec35e3ab4cba802ae37015f9 Mon Sep 17 00:00:00 2001 From: Chris Yeoh Date: Fri, 12 Sep 2014 14:36:10 +0930 Subject: [PATCH 0600/1705] quota delete tenant_id parameter should be required Makes the quota-delete tenant_id parameter required rather than optional. Currently if not supplied it will pass 'None' as the tenant id to the Nova API. It will be silently ignored (at least until https://review.openstack.org/120971 has merged after which it will fail) and no quota will be deleted. Change-Id: I3ad8f36e92ed9ac54bf892f329ce3feb56f01be5 Closes-Bug: 1367127 --- novaclient/v1_1/shell.py | 1 + novaclient/v3/shell.py | 1 + 2 files changed, 2 insertions(+) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 0aa81a918..662cb51be 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -3519,6 +3519,7 @@ def do_quota_update(cs, args): @utils.arg('--tenant', metavar='', + required=True, help=_('ID of tenant to delete quota for.')) @utils.arg('--user', metavar='', diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index f97331e0f..a50e1a5ae 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -2926,6 +2926,7 @@ def do_quota_update(cs, args): @utils.arg('--tenant', metavar='', + required=True, help='ID of tenant to delete quota for.') def do_quota_delete(cs, args): """Delete quota for a tenant so their quota will revert back to default.""" From 730fa4d0b2005fd50fafc528daf5a34a8ac9806d Mon Sep 17 00:00:00 2001 From: Cyril Roelandt Date: Wed, 23 Jul 2014 00:40:27 +0000 Subject: [PATCH 0601/1705] Add support for the server group quotas Implements: blueprint server-group-quotas Change-Id: I440d460d227f4447c2b960b6343b8fe1f6cbd3fb --- novaclient/tests/v1_1/test_shell.py | 3 ++- novaclient/tests/v3/fakes.py | 4 +++- novaclient/v1_1/shell.py | 23 ++++++++++++++++++++++- novaclient/v3/shell.py | 13 ++++++++++++- 4 files changed, 39 insertions(+), 4 deletions(-) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index eae395a83..3da03a20c 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -1683,7 +1683,8 @@ def test_quota_class_update(self): '--instances', '--cores', '--ram', '--floating-ips', '--fixed-ips', '--metadata-items', '--injected-files', '--injected-file-content-bytes', '--injected-file-path-bytes', - '--key-pairs', '--security-groups', '--security-group-rules' + '--key-pairs', '--security-groups', '--security-group-rules', + '--server-groups', '--server-group-members' ) for arg in args: self.run_command('quota-class-update ' diff --git a/novaclient/tests/v3/fakes.py b/novaclient/tests/v3/fakes.py index 23f1e7cbd..d8c6a1549 100644 --- a/novaclient/tests/v3/fakes.py +++ b/novaclient/tests/v3/fakes.py @@ -323,7 +323,9 @@ def put_os_quota_sets_97f4c221bff44578b0300df4ef119353(self, body, **kw): 'cores': 1, 'keypairs': 1, 'security_groups': 1, - 'security_group_rules': 1}}) + 'security_group_rules': 1, + 'server_groups': 1, + 'server_group_members': 1}}) def get_os_quota_sets_test_detail(self, **kw): return (200, {}, {'quota_set': { diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 0aa81a918..399efdeb0 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -3360,7 +3360,8 @@ def do_ssh(cs, args): 'floating_ips', 'fixed_ips', 'metadata_items', 'injected_files', 'injected_file_content_bytes', 'injected_file_path_bytes', 'key_pairs', - 'security_groups', 'security_group_rules'] + 'security_groups', 'security_group_rules', + 'server_groups', 'server_group_members'] def _quota_show(quotas): @@ -3505,6 +3506,16 @@ def do_quota_defaults(cs, args): type=int, default=None, help=_('New value for the "security-group-rules" quota.')) +@utils.arg('--server-groups', + metavar='', + type=int, + default=None, + help=_('New value for the "server-groups" quota.')) +@utils.arg('--server-group-members', + metavar='', + type=int, + default=None, + help=_('New value for the "server-group-members" quota.')) @utils.arg('--force', dest='force', action="store_true", @@ -3612,6 +3623,16 @@ def do_quota_class_show(cs, args): type=int, default=None, help=_('New value for the "security-group-rules" quota.')) +@utils.arg('--server-groups', + metavar='', + type=int, + default=None, + help=_('New value for the "server-groups" quota.')) +@utils.arg('--server-group-members', + metavar='', + type=int, + default=None, + help=_('New value for the "server-group-members" quota.')) def do_quota_class_update(cs, args): """Update the quotas for a quota class.""" diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index f97331e0f..9643ec5a8 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -2795,7 +2795,8 @@ def do_ssh(cs, args): _quota_resources = ['instances', 'cores', 'ram', - 'fixed_ips', 'metadata_items', 'key_pairs'] + 'fixed_ips', 'metadata_items', 'key_pairs', + 'server_groups', 'server_group_members'] def _quota_show(quotas): @@ -2912,6 +2913,16 @@ def do_quota_defaults(cs, args): type=int, default=None, help='New value for the "key-pairs" quota.') +@utils.arg('--server-groups', + metavar='', + type=int, + default=None, + help='New value for the "server-groups" quota.') +@utils.arg('--server-group-members', + metavar='', + type=int, + default=None, + help='New value for the "server-group-members" quota.') @utils.arg('--force', dest='force', action="store_true", From d96f13d2e2d7c0e8509e4fac8f151bc090a97930 Mon Sep 17 00:00:00 2001 From: Sean Dague Date: Fri, 12 Sep 2014 06:53:02 -0400 Subject: [PATCH 0602/1705] delete python bytecode before every test run Bring over the cleaning line from run_tests.sh for the pyc files to all the tox runs. This should eliminate the need to clean -x -i to kill pyc files in your local directory to get tests to pass. Related-Bug: #1368661 Change-Id: I00ee418eea2d82031bb510b09e63e2ec87fb1b09 --- tox.ini | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 40978d654..53fa8719f 100644 --- a/tox.ini +++ b/tox.ini @@ -11,7 +11,9 @@ setenv = VIRTUAL_ENV={envdir} deps = -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt -commands = python setup.py testr --testr-args='{posargs}' +commands = + find . -type f -name "*.pyc" -delete + python setup.py testr --testr-args='{posargs}' [testenv:pep8] commands = flake8 {posargs} From ca3621e21813da9a4258bfb5c18fce936ad2a816 Mon Sep 17 00:00:00 2001 From: Phil Day Date: Fri, 12 Sep 2014 23:02:34 +0000 Subject: [PATCH 0603/1705] novaclient: Convert v3 boot command with v2.1 spec (security-groups) The v3 client was written assuming that some arguments would be renamed for better consistency. V2.1 will follow the v2 syntax, so we need to convert the client back to that. This change converts os-security-groups:security-groups to security-groups in the server element Change-Id: I50a5b180b7edd645e610ff2d77b6d485924c82a1 --- novaclient/tests/v3/test_shell.py | 4 ++-- novaclient/v3/servers.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/novaclient/tests/v3/test_shell.py b/novaclient/tests/v3/test_shell.py index 569ddabcb..1227edc48 100644 --- a/novaclient/tests/v3/test_shell.py +++ b/novaclient/tests/v3/test_shell.py @@ -273,8 +273,8 @@ def test_boot_secgroup(self): self.assert_called_anytime( 'POST', '/servers', {'server': { - 'os-security-groups:security_groups': [{'name': 'secgroup1'}, - {'name': 'secgroup2'}], + 'security_groups': [{'name': 'secgroup1'}, + {'name': 'secgroup2'}], 'flavor_ref': '1', 'name': 'some-server', 'image_ref': '1', diff --git a/novaclient/v3/servers.py b/novaclient/v3/servers.py index 226cf92a1..4cba60c4c 100644 --- a/novaclient/v3/servers.py +++ b/novaclient/v3/servers.py @@ -428,7 +428,7 @@ def _boot(self, resource_url, response_key, name, image, flavor, body["server"]["os-multiple-create:max_count"] = max_count if security_groups: - body["server"]["os-security-groups:security_groups"] = \ + body["server"]["security_groups"] = \ [{'name': sg} for sg in security_groups] if availability_zone: From 688b1add606b84ec388f8e03a8e15393341f5131 Mon Sep 17 00:00:00 2001 From: Phil Day Date: Fri, 12 Sep 2014 23:09:48 +0000 Subject: [PATCH 0604/1705] novaclient: Convert v3 boot command with v2.1 spec (user-data) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The v3 client was written assuming that some arguments would be renamed for better consistency. V2.1 will follow the v2 syntax, so we need to convert the client back to that. This change strips the “os-user-data:” prefix from the user-data field in the server element Change-Id: I8f0e579b30547ce3fe778116745b4107d88c9be0 --- novaclient/tests/v3/test_shell.py | 2 +- novaclient/v3/servers.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/novaclient/tests/v3/test_shell.py b/novaclient/tests/v3/test_shell.py index 569ddabcb..1c8feed32 100644 --- a/novaclient/tests/v3/test_shell.py +++ b/novaclient/tests/v3/test_shell.py @@ -247,7 +247,7 @@ def test_boot_user_data(self): 'image_ref': '1', 'os-multiple-create:min_count': 1, 'os-multiple-create:max_count': 1, - 'os-user-data:user_data': user_data}}, + 'user_data': user_data}}, ) def test_boot_avzone(self): diff --git a/novaclient/v3/servers.py b/novaclient/v3/servers.py index 226cf92a1..6a783791e 100644 --- a/novaclient/v3/servers.py +++ b/novaclient/v3/servers.py @@ -405,7 +405,7 @@ def _boot(self, resource_url, response_key, name, image, flavor, userdata = encodeutils.safe_encode(userdata) data = base64.b64encode(userdata).decode('utf-8') - body["server"]["os-user-data:user_data"] = data + body["server"]["user_data"] = data if meta: body["server"]["metadata"] = meta if reservation_id: From cd566223006b3638f5e4fefa003631646ff6f530 Mon Sep 17 00:00:00 2001 From: Andreas Jaeger Date: Sat, 13 Sep 2014 09:44:39 +0200 Subject: [PATCH 0605/1705] Stop using intersphinx Remove intersphinx from the docs build as it triggers network calls that occasionally fail, and we don't really use intersphinx (links other sphinx documents out on the internet) This also removes the requirement for internet access during docs build. This can cause docs jobs to fail if the project errors out on warnings. Change-Id: I71e941e2a639641a662a163c682eb86d51de42fb Related-Bug: #1368910 --- doc/source/conf.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index e37b8b888..d33aea267 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -88,7 +88,7 @@ def gen_ref(ver, title, names): # Add any Sphinx extension module names here, as strings. They can be # extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'oslosphinx'] +extensions = ['sphinx.ext.autodoc', 'oslosphinx'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -267,7 +267,3 @@ def gen_ref(ver, title, names): # If false, no module index is generated. #latex_use_modindex = True - - -# Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'http://docs.python.org/': None} From 0763480048243295e3ebfcfda7868f42457e8c67 Mon Sep 17 00:00:00 2001 From: Phil Day Date: Fri, 12 Sep 2014 23:18:30 +0000 Subject: [PATCH 0606/1705] novaclient: Convert v3 boot command with v2.1 spec (bdm) The v3 client was written assuming that some arguments would be renamed for better consistency. V2.1 will follow the v2 syntax, so we need to convert the client back to that. This change fixes the block-device-mapping element. Change-Id: Id91ad05aa86b32755e0b06be5f0e50cc1e83cb15 --- novaclient/tests/v3/test_shell.py | 4 ++-- novaclient/v3/servers.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/novaclient/tests/v3/test_shell.py b/novaclient/tests/v3/test_shell.py index 569ddabcb..e104a8b01 100644 --- a/novaclient/tests/v3/test_shell.py +++ b/novaclient/tests/v3/test_shell.py @@ -337,7 +337,7 @@ def test_boot_no_image_bdms(self): {'server': { 'flavor_ref': '1', 'name': 'some-server', - 'os-block-device-mapping:block_device_mapping': [ + 'block_device_mapping': [ { 'volume_id': 'blah', 'delete_on_termination': '0', @@ -366,7 +366,7 @@ def test_boot_image_bdms(self): {'server': { 'flavor_ref': '1', 'name': 'some-server', - 'os-block-device-mapping:block_device_mapping': [ + 'block_device_mapping': [ {'device_name': 'id', 'volume_id': id, 'source_type': 'volume', 'boot_index': 0, 'uuid': id}], 'image_ref': '1', diff --git a/novaclient/v3/servers.py b/novaclient/v3/servers.py index 226cf92a1..b98d3e42b 100644 --- a/novaclient/v3/servers.py +++ b/novaclient/v3/servers.py @@ -437,11 +437,12 @@ def _boot(self, resource_url, response_key, name, image, flavor, # Block device mappings are passed as a list of dictionaries if block_device_mapping: - bdm_param = 'os-block-device-mapping:block_device_mapping' + bdm_param = 'block_device_mapping' body['server'][bdm_param] = \ self._parse_block_device_mapping(block_device_mapping) elif block_device_mapping_v2: # Append the image to the list only if we have new style BDMs + bdm_param = 'block_device_mapping_v2' if image: bdm_dict = {'uuid': image.id, 'source_type': 'image', 'destination_type': 'local', 'boot_index': 0, From a400b58d0344b50462cb25b84044f110a4a36d9a Mon Sep 17 00:00:00 2001 From: Ala Rezmerita Date: Thu, 24 Apr 2014 11:03:32 +0200 Subject: [PATCH 0607/1705] Live migrate each instance from one host to other hosts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Using the existing server live_migrate api, this new feature, adds the ability for admins to live-migrate all running instances from one host to other hosts. The patch implements the feature for Nova API V2 and API V3. Co-Authored-By: Cédric Soulas Change-Id: Ie8dd1b66fb8eaefa6ff38752b1e4f46bab145820 Implements: blueprint host-servers-live-migrate --- novaclient/tests/v1_1/test_shell.py | 45 +++++++++++++ novaclient/tests/v3/fakes.py | 4 ++ novaclient/tests/v3/test_shell.py | 41 ++++++++++++ novaclient/v1_1/contrib/host_evacuate_live.py | 64 +++++++++++++++++++ novaclient/v3/shell.py | 47 ++++++++++++++ 5 files changed, 201 insertions(+) create mode 100644 novaclient/v1_1/contrib/host_evacuate_live.py diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index eae395a83..ec0b2aa74 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -1389,6 +1389,51 @@ def test_live_migration(self): 'block_migration': True, 'disk_over_commit': True}}) + def test_host_evacuate_live_with_no_target_host(self): + self.run_command('host-evacuate-live hyper') + self.assert_called('GET', '/os-hypervisors/hyper/servers', pos=0) + body = {'os-migrateLive': {'host': None, + 'block_migration': False, + 'disk_over_commit': False}} + self.assert_called('POST', '/servers/uuid1/action', body, pos=1) + self.assert_called('POST', '/servers/uuid2/action', body, pos=2) + self.assert_called('POST', '/servers/uuid3/action', body, pos=3) + self.assert_called('POST', '/servers/uuid4/action', body, pos=4) + + def test_host_evacuate_live_with_target_host(self): + self.run_command('host-evacuate-live hyper ' + '--target-host hostname') + self.assert_called('GET', '/os-hypervisors/hyper/servers', pos=0) + body = {'os-migrateLive': {'host': 'hostname', + 'block_migration': False, + 'disk_over_commit': False}} + self.assert_called('POST', '/servers/uuid1/action', body, pos=1) + self.assert_called('POST', '/servers/uuid2/action', body, pos=2) + self.assert_called('POST', '/servers/uuid3/action', body, pos=3) + self.assert_called('POST', '/servers/uuid4/action', body, pos=4) + + def test_host_evacuate_live_with_block_migration(self): + self.run_command('host-evacuate-live --block-migrate hyper') + self.assert_called('GET', '/os-hypervisors/hyper/servers', pos=0) + body = {'os-migrateLive': {'host': None, + 'block_migration': True, + 'disk_over_commit': False}} + self.assert_called('POST', '/servers/uuid1/action', body, pos=1) + self.assert_called('POST', '/servers/uuid2/action', body, pos=2) + self.assert_called('POST', '/servers/uuid3/action', body, pos=3) + self.assert_called('POST', '/servers/uuid4/action', body, pos=4) + + def test_host_evacuate_live_with_disk_over_commit(self): + self.run_command('host-evacuate-live --disk-over-commit hyper') + self.assert_called('GET', '/os-hypervisors/hyper/servers', pos=0) + body = {'os-migrateLive': {'host': None, + 'block_migration': False, + 'disk_over_commit': True}} + self.assert_called('POST', '/servers/uuid1/action', body, pos=1) + self.assert_called('POST', '/servers/uuid2/action', body, pos=2) + self.assert_called('POST', '/servers/uuid3/action', body, pos=3) + self.assert_called('POST', '/servers/uuid4/action', body, pos=4) + def test_reset_state(self): self.run_command('reset-state sample-server') self.assert_called('POST', '/servers/1234/action', diff --git a/novaclient/tests/v3/fakes.py b/novaclient/tests/v3/fakes.py index 23f1e7cbd..8300f16f7 100644 --- a/novaclient/tests/v3/fakes.py +++ b/novaclient/tests/v3/fakes.py @@ -335,6 +335,10 @@ def get_os_quota_sets_test_detail(self, **kw): # Hypervisors # def get_os_hypervisors_search(self, **kw): + if kw['query'] == 'hyper1': + return (200, {}, {'hypervisors': [ + {'id': 1234, 'hypervisor_hostname': 'hyper1'}, + ]}) return (200, {}, {'hypervisors': [ {'id': 1234, 'hypervisor_hostname': 'hyper1'}, {'id': 5678, 'hypervisor_hostname': 'hyper2'} diff --git a/novaclient/tests/v3/test_shell.py b/novaclient/tests/v3/test_shell.py index 569ddabcb..306070cc2 100644 --- a/novaclient/tests/v3/test_shell.py +++ b/novaclient/tests/v3/test_shell.py @@ -644,6 +644,47 @@ def test_flavor_show_by_name_priv(self): self.assert_called('GET', '/flavors/2', pos=3) self.assert_called('GET', '/flavors/2/flavor-extra-specs', pos=4) + def test_host_evacuate_live_with_no_target_host(self): + self.run_command('host-evacuate-live hyper1') + self.assert_called('GET', '/os-hypervisors/search?query=hyper1', pos=0) + self.assert_called('GET', '/os-hypervisors/1234/servers', pos=1) + body = {'migrate_live': {'host': None, + 'block_migration': False, + 'disk_over_commit': False}} + self.assert_called('POST', '/servers/uuid1/action', body, pos=2) + self.assert_called('POST', '/servers/uuid2/action', body, pos=3) + + def test_host_evacuate_live_with_target_host(self): + self.run_command('host-evacuate-live hyper1 ' + '--target-host hostname') + self.assert_called('GET', '/os-hypervisors/search?query=hyper1', pos=0) + self.assert_called('GET', '/os-hypervisors/1234/servers', pos=1) + body = {'migrate_live': {'host': 'hostname', + 'block_migration': False, + 'disk_over_commit': False}} + self.assert_called('POST', '/servers/uuid1/action', body, pos=2) + self.assert_called('POST', '/servers/uuid2/action', body, pos=3) + + def test_host_evacuate_live_with_block_migration(self): + self.run_command('host-evacuate-live --block-migrate hyper1') + self.assert_called('GET', '/os-hypervisors/search?query=hyper1', pos=0) + self.assert_called('GET', '/os-hypervisors/1234/servers', pos=1) + body = {'migrate_live': {'host': None, + 'block_migration': True, + 'disk_over_commit': False}} + self.assert_called('POST', '/servers/uuid1/action', body, pos=2) + self.assert_called('POST', '/servers/uuid2/action', body, pos=3) + + def test_host_evacuate_live_with_disk_over_commit(self): + self.run_command('host-evacuate-live --disk-over-commit hyper1') + self.assert_called('GET', '/os-hypervisors/search?query=hyper1', pos=0) + self.assert_called('GET', '/os-hypervisors/1234/servers', pos=1) + body = {'migrate_live': {'host': None, + 'block_migration': False, + 'disk_over_commit': True}} + self.assert_called('POST', '/servers/uuid1/action', body, pos=2) + self.assert_called('POST', '/servers/uuid2/action', body, pos=3) + def test_delete(self): self.run_command('delete 1234') self.assert_called('DELETE', '/servers/1234') diff --git a/novaclient/v1_1/contrib/host_evacuate_live.py b/novaclient/v1_1/contrib/host_evacuate_live.py new file mode 100644 index 000000000..6ac4a018f --- /dev/null +++ b/novaclient/v1_1/contrib/host_evacuate_live.py @@ -0,0 +1,64 @@ +# Copyright 2014 OpenStack Foundation +# All Rights Reserved. +# +# 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 novaclient.openstack.common.gettextutils import _ +from novaclient import utils + + +def _server_live_migrate(cs, server, args): + class HostEvacuateLiveResponse(object): + def __init__(self, server_uuid, live_migration_accepted, + error_message): + self.server_uuid = server_uuid + self.live_migration_accepted = live_migration_accepted + self.error_message = error_message + success = True + error_message = "" + try: + cs.servers.live_migrate(server['uuid'], args.target_host, + args.block_migrate, args.disk_over_commit) + except Exception as e: + success = False + error_message = _("Error while live migrating instance: %s") % e + return HostEvacuateLiveResponse(server['uuid'], + success, + error_message) + + +@utils.arg('host', metavar='', help='Name of host.') +@utils.arg('--target-host', + metavar='', + default=None, + help=_('Name of target host.')) +@utils.arg('--block-migrate', + action='store_true', + default=False, + help=_('Enable block migration.')) +@utils.arg('--disk-over-commit', + action='store_true', + default=False, + help=_('Enable disk overcommit.')) +def do_host_evacuate_live(cs, args): + """Live migrate all instances of the specified host + to other available hosts. + """ + hypervisors = cs.hypervisors.search(args.host, servers=True) + response = [] + for hyper in hypervisors: + for server in getattr(hyper, 'servers', []): + response.append(_server_live_migrate(cs, server, args)) + + utils.print_list(response, ["Server UUID", "Live Migration Accepted", + "Error Message"]) diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index f97331e0f..9c4d09a65 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -2392,6 +2392,53 @@ def do_live_migration(cs, args): args.disk_over_commit) +def _server_live_migrate(cs, server, args): + class HostServersLiveMigrateResponse(object): + def __init__(self, server_uuid, live_migration_accepted, + error_message): + self.server_uuid = server_uuid + self.live_migration_accepted = live_migration_accepted + self.error_message = error_message + success = True + error_message = "" + try: + cs.servers.live_migrate(server['id'], args.target_host, + args.block_migrate, args.disk_over_commit) + except Exception as e: + success = False + error_message = "Error while live migrating instance: %s" % e + return HostServersLiveMigrateResponse(server['id'], + success, + error_message) + + +@utils.arg('host', metavar='', help='Name of host.') +@utils.arg('--target-host', + metavar='', + default=None, + help='Name of target host.') +@utils.arg('--block-migrate', + action='store_true', + default=False, + help='Enable block migration.') +@utils.arg('--disk-over-commit', + action='store_true', + default=False, + help='Enable disk overcommit.') +def do_host_evacuate_live(cs, args): + """Live Migrate all instances of the specified host + to other available hosts. + """ + hypervisors = cs.hypervisors.search(args.host) + response = [] + for hyper in hypervisors: + servers = getattr(cs.hypervisors.servers(hyper.id), 'servers', []) + for server in servers: + response.append(_server_live_migrate(cs, server, args)) + utils.print_list(response, ["Server UUID", "Live Migration Accepted", + "Error Message"]) + + @utils.arg('server', metavar='', nargs='+', help='Name or ID of server(s).') @utils.arg('--active', action='store_const', dest='state', From bd65fde64c153f3382db54ce39a8089ffd3d222d Mon Sep 17 00:00:00 2001 From: Chris Yeoh Date: Tue, 16 Sep 2014 11:25:18 +0930 Subject: [PATCH 0608/1705] secgroup-create description is not optional The description field to the Nova API for secgroup create is not optional. This enforces the supplying of the description field in novaclient as the Nova call will fail if it is not supplied. v3 version does not need to be updated as it was never changed. Change-Id: I6fc823990190b935a8e1214efc8c3ac3a0941abf Closes-Bug: 1367121 --- novaclient/tests/v1_1/test_shell.py | 7 ------- novaclient/v1_1/shell.py | 2 +- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index eae395a83..10afaef61 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -1901,13 +1901,6 @@ def test_security_group_create(self): {'name': 'test', 'description': 'FAKE_SECURITY_GROUP'}}) - def test_security_group_create_without_description(self): - self.run_command('secgroup-create test') - self.assert_called('POST', '/os-security-groups', - {'security_group': - {'name': 'test', - 'description': None}}) - def test_security_group_update(self): self.run_command('secgroup-update test te FAKE_SECURITY_GROUP') self.assert_called('PUT', '/os-security-groups/1', diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 662cb51be..043804bff 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2399,7 +2399,7 @@ def do_secgroup_delete_rule(cs, args): @utils.arg('name', metavar='', help=_('Name of security group.')) -@utils.arg('description', metavar='', nargs='?', +@utils.arg('description', metavar='', help=_('Description of security group.')) def do_secgroup_create(cs, args): """Create a security group.""" From 310b87ecbb8cd4c82a460781cf7b4e9e79c604d9 Mon Sep 17 00:00:00 2001 From: Michal Dulko Date: Thu, 18 Sep 2014 13:07:17 +0200 Subject: [PATCH 0609/1705] Add retry_after only to exceptions supporting it Having retry_after in HTTP response header for 403 status code caused client to fail with TypeError exception about unexpected keyword argument. This is related to bug in nova-api. To prevent this kind of client problems in the future patch adds check to from_response function to pass retry-after header only to classes that inherit from RetryAfterException and regression unit tests. Change-Id: I6bfc8b33eb591d30b3c647397b11100094718e13 Closes-Bug: 1365251 --- novaclient/exceptions.py | 31 +++++++++++-------- novaclient/tests/test_http.py | 56 +++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 12 deletions(-) diff --git a/novaclient/exceptions.py b/novaclient/exceptions.py index 7e9e0c8ae..d550b754b 100644 --- a/novaclient/exceptions.py +++ b/novaclient/exceptions.py @@ -105,6 +105,19 @@ def __str__(self): return formatted_string +class RetryAfterException(ClientException): + """ + The base exception class for ClientExceptions that use Retry-After header. + """ + def __init__(self, *args, **kwargs): + try: + self.retry_after = int(kwargs.pop('retry_after')) + except (KeyError, ValueError): + self.retry_after = 0 + + super(RetryAfterException, self).__init__(*args, **kwargs) + + class BadRequest(ClientException): """ HTTP 400 - Bad request: you sent some malformed data. @@ -154,23 +167,15 @@ class Conflict(ClientException): message = "Conflict" -class OverLimit(ClientException): +class OverLimit(RetryAfterException): """ HTTP 413 - Over limit: you're over the API limits for this time period. """ http_status = 413 message = "Over limit" - def __init__(self, *args, **kwargs): - try: - self.retry_after = int(kwargs.pop('retry_after')) - except (KeyError, ValueError): - self.retry_after = 0 - super(OverLimit, self).__init__(*args, **kwargs) - - -class RateLimit(OverLimit): +class RateLimit(RetryAfterException): """ HTTP 429 - Rate limit: you've sent too many requests for this time period. """ @@ -220,6 +225,8 @@ def from_response(response, body, url, method=None): if resp.status_code != 200: raise exception_from_response(resp, rest.text) """ + cls = _code_map.get(response.status_code, ClientException) + kwargs = { 'code': response.status_code, 'method': method, @@ -230,7 +237,8 @@ def from_response(response, body, url, method=None): if response.headers: kwargs['request_id'] = response.headers.get('x-compute-request-id') - if 'retry-after' in response.headers: + if (issubclass(cls, RetryAfterException) and + 'retry-after' in response.headers): kwargs['retry_after'] = response.headers.get('retry-after') if body: @@ -245,7 +253,6 @@ def from_response(response, body, url, method=None): kwargs['message'] = message kwargs['details'] = details - cls = _code_map.get(response.status_code, ClientException) return cls(**kwargs) diff --git a/novaclient/tests/test_http.py b/novaclient/tests/test_http.py index 2797a438f..a9be4ae66 100644 --- a/novaclient/tests/test_http.py +++ b/novaclient/tests/test_http.py @@ -44,6 +44,32 @@ }) unknown_error_mock_request = mock.Mock(return_value=unknown_error_response) +retry_after_response = utils.TestResponse({ + "status_code": 413, + "text": '', + "headers": { + "retry-after": "5" + }, +}) +retry_after_mock_request = mock.Mock(return_value=retry_after_response) + +retry_after_no_headers_response = utils.TestResponse({ + "status_code": 413, + "text": '', +}) +retry_after_no_headers_mock_request = mock.Mock( + return_value=retry_after_no_headers_response) + +retry_after_non_supporting_response = utils.TestResponse({ + "status_code": 403, + "text": '', + "headers": { + "retry-after": "5" + }, +}) +retry_after_non_supporting_mock_request = mock.Mock( + return_value=retry_after_non_supporting_response) + def get_client(): cl = client.HTTPClient("username", "password", @@ -154,3 +180,33 @@ def test_unknown_server_error(self): self.assertIn('Unknown Error', six.text_type(exc)) else: self.fail('Expected exceptions.ClientException') + + @mock.patch.object(requests, "request", retry_after_mock_request) + def test_retry_after_request(self): + cl = get_client() + + try: + cl.get("/hi") + except exceptions.OverLimit as exc: + self.assertEqual(5, exc.retry_after) + else: + self.fail('Expected exceptions.OverLimit') + + @mock.patch.object(requests, "request", + retry_after_no_headers_mock_request) + def test_retry_after_request_no_headers(self): + cl = get_client() + + try: + cl.get("/hi") + except exceptions.OverLimit as exc: + self.assertEqual(0, exc.retry_after) + else: + self.fail('Expected exceptions.OverLimit') + + @mock.patch.object(requests, "request", + retry_after_non_supporting_mock_request) + def test_retry_after_request_non_supporting_exc(self): + cl = get_client() + + self.assertRaises(exceptions.Forbidden, cl.get, "/hi") From 2e649865325c58f35f03e585fcc4f45850ca5534 Mon Sep 17 00:00:00 2001 From: Phil Day Date: Thu, 18 Sep 2014 17:38:30 +0000 Subject: [PATCH 0610/1705] Add list by user to shell Adds "--user" as a search option to the list command. Also since seach by tenant or user also needs all_tenants=1 add this to the seach options automatically instead of making users remember to add it Change-Id: Icc828080b400aecdb320b721c569992677848cc6 --- novaclient/tests/v1_1/test_shell.py | 10 ++++++++++ novaclient/v1_1/shell.py | 9 +++++++++ 2 files changed, 19 insertions(+) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index 9d44d65b1..78265f6be 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -838,6 +838,16 @@ def test_list_with_flavors(self): self.run_command('list --flavor 1') self.assert_called('GET', '/servers/detail?flavor=1') + def test_list_by_tenant(self): + self.run_command('list --tenant fake_tenant') + self.assert_called('GET', + '/servers/detail?all_tenants=1&tenant_id=fake_tenant') + + def test_list_by_user(self): + self.run_command('list --user fake_user') + self.assert_called('GET', + '/servers/detail?all_tenants=1&user_id=fake_user') + def test_list_fields(self): output = self.run_command('list --fields ' 'host,security_groups,OS-EXT-MOD:some_thing') diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 457ebc1a1..45db136ab 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1176,6 +1176,11 @@ def do_image_delete(cs, args): metavar='', nargs='?', help=_('Display information from single tenant (Admin only).')) +@utils.arg('--user', + dest='user', + metavar='', + nargs='?', + help=_('Display information from single user (Admin only).')) @utils.arg('--deleted', dest='deleted', action="store_true", @@ -1199,6 +1204,9 @@ def do_list(cs, args): imageid = _find_image(cs, args.image).id if args.flavor: flavorid = _find_flavor(cs, args.flavor).id + # search by tenant or user only works with all_tenants + if args.tenant or args.user: + args.all_tenants = 1 search_opts = { 'all_tenants': args.all_tenants, 'reservation_id': args.reservation_id, @@ -1209,6 +1217,7 @@ def do_list(cs, args): 'flavor': flavorid, 'status': args.status, 'tenant_id': args.tenant, + 'user_id': args.user, 'host': args.host, 'deleted': args.deleted, 'instance_name': args.instance_name} From 98acb7fb7d9f6179c0cddc59ecf050831a6a6464 Mon Sep 17 00:00:00 2001 From: Rafael Rivero Date: Tue, 16 Sep 2014 17:40:06 -0700 Subject: [PATCH 0611/1705] Corrects typos "coearse," "proejct," and "unecrypts" Misspelling of "coerce" found in the docstring body of method _boot from class ServerManager. Misspellings of "project" and "unencrypts" found in docstrings of client.py and crypto.py. Change-Id: I052c321f3ad5e13aa57a559e2f28ec5eec32a9b4 --- novaclient/crypto.py | 2 +- novaclient/v1_1/cloudpipe.py | 2 +- novaclient/v1_1/servers.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/novaclient/crypto.py b/novaclient/crypto.py index d817be5bb..21af60cdc 100644 --- a/novaclient/crypto.py +++ b/novaclient/crypto.py @@ -22,7 +22,7 @@ class DecryptionFailure(Exception): def decrypt_password(private_key, password): - """Base64 decodes password and unecrypts it with private key. + """Base64 decodes password and unencrypts it with private key. Requires openssl binary available in the path. """ diff --git a/novaclient/v1_1/cloudpipe.py b/novaclient/v1_1/cloudpipe.py index 74fed75c7..6e05f4a20 100644 --- a/novaclient/v1_1/cloudpipe.py +++ b/novaclient/v1_1/cloudpipe.py @@ -19,7 +19,7 @@ class Cloudpipe(base.Resource): - """A cloudpipe instance is a VPN attached to a proejct's VLAN.""" + """A cloudpipe instance is a VPN attached to a project's VLAN.""" def __repr__(self): return "" % self.project_id diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index 43b45ebb7..38bde8c62 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -428,7 +428,7 @@ def _boot(self, resource_url, response_key, name, image, flavor, file-like object). A maximum of five entries is allowed, and each file must be 10k or less. :param reservation_id: a UUID for the set of servers being requested. - :param return_raw: If True, don't try to coearse the result into + :param return_raw: If True, don't try to coerce the result into a Resource object. :param security_groups: list of security group names :param key_name: (optional extension) name of keypair to inject into From 5ecfdac6b34769e200ff5c4c7429c20518c5b24f Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 19 Sep 2014 08:51:36 +0000 Subject: [PATCH 0612/1705] Updated from global requirements Change-Id: Id91358b7d4efe79a4be5190dcdb4b0b7cb40f19b --- requirements.txt | 2 +- test-requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 9d3c1bdc0..08de4eb63 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ pbr>=0.6,!=0.7,<1.0 argparse iso8601>=0.1.9 -oslo.utils>=0.3.0 # Apache-2.0 +oslo.utils>=1.0.0 # Apache-2.0 PrettyTable>=0.7,<0.8 requests>=1.2.1,!=2.4.0 simplejson>=2.2.0 diff --git a/test-requirements.txt b/test-requirements.txt index 02deabc2e..8126ed634 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -10,7 +10,7 @@ keyring>=2.1,!=3.3 mock>=1.0 requests-mock>=0.4.0 # Apache-2.0 sphinx>=1.1.2,!=1.2.0,<1.3 -oslosphinx>=2.2.0.0a2 +oslosphinx>=2.2.0 # Apache-2.0 testrepository>=0.0.18 testscenarios>=0.4 testtools>=0.9.34 From d03014cdb0f52c1c1ea66b2c84c35b60e95ef729 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Fri, 19 Sep 2014 16:34:48 +0300 Subject: [PATCH 0613/1705] Use common code instead of novaclient.utils Module `novaclient.utils` contains a lot of functions which are presented in modules from common code. * service_type -> novaclient.openstack.common.cliutils.service_type * get_service_type -> novaclient.openstack.common.cliutils.get_service_type * pretty_choice_list -> novaclient.openstack.common.cliutils.pretty_choice_list * import_class -> oslo.utils.importutils.importclass * HookableMixin -> novaclient.openstack.common.apiclient.base.HookableMixin Change-Id: Ia6cac058da12c852d92f26875a66ae31cc4c63d4 --- novaclient/base.py | 3 +- novaclient/client.py | 3 +- novaclient/extension.py | 3 +- novaclient/shell.py | 2 +- novaclient/tests/test_utils.py | 9 ------ novaclient/utils.py | 54 ++-------------------------------- novaclient/v1_1/shell.py | 27 +++++++++-------- novaclient/v3/shell.py | 7 +++-- 8 files changed, 26 insertions(+), 82 deletions(-) diff --git a/novaclient/base.py b/novaclient/base.py index 173eda085..7f54d9363 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -26,7 +26,6 @@ from novaclient import exceptions from novaclient.openstack.common.apiclient import base -from novaclient import utils Resource = base.Resource @@ -42,7 +41,7 @@ def getid(obj): return obj -class Manager(utils.HookableMixin): +class Manager(base.HookableMixin): """ Managers interact with a particular type of API (servers, flavors, images, etc.) and provide CRUD operations for them. diff --git a/novaclient/client.py b/novaclient/client.py index b0d9aea10..5cd2d8f02 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -31,6 +31,7 @@ import time from keystoneclient import adapter +from oslo.utils import importutils from oslo.utils import netutils import requests from requests import adapters @@ -757,7 +758,7 @@ def get_client_class(version): 'keys': ', '.join(version_map.keys())} raise exceptions.UnsupportedVersion(msg) - return utils.import_class(client_path) + return importutils.import_class(client_path) def Client(version, *args, **kwargs): diff --git a/novaclient/extension.py b/novaclient/extension.py index ac105070a..9cfcc13d9 100644 --- a/novaclient/extension.py +++ b/novaclient/extension.py @@ -14,10 +14,11 @@ # under the License. from novaclient import base +from novaclient.openstack.common.apiclient import base as common_base from novaclient import utils -class Extension(utils.HookableMixin): +class Extension(common_base.HookableMixin): """Extension descriptor.""" SUPPORTED_HOOKS = ('__pre_parse_args__', '__post_parse_args__') diff --git a/novaclient/shell.py b/novaclient/shell.py index 6623b7fbf..d0d183b23 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -601,7 +601,7 @@ def main(self, argv): except KeyError: service_type = DEFAULT_NOVA_SERVICE_TYPE_MAP[ DEFAULT_OS_COMPUTE_API_VERSION] - service_type = utils.get_service_type(args.func) or service_type + service_type = cliutils.get_service_type(args.func) or service_type # If we have an auth token but no management_url, we must auth anyway. # Expired tokens are handled by client.py:_cs_request diff --git a/novaclient/tests/test_utils.py b/novaclient/tests/test_utils.py index 69e5d6188..b8960b57c 100644 --- a/novaclient/tests/test_utils.py +++ b/novaclient/tests/test_utils.py @@ -267,15 +267,6 @@ def test_flattening(self): 'a3': ('t',)}, squashed) - def test_pretty_choice_list(self): - l = [] - r = utils.pretty_choice_list(l) - self.assertEqual("", r) - - l = ["v1", "v2", "v3"] - r = utils.pretty_choice_list(l) - self.assertEqual("'v1', 'v2', 'v3'", r) - def test_pretty_choice_dict(self): d = {} r = utils.pretty_choice_dict(d) diff --git a/novaclient/utils.py b/novaclient/utils.py index d6068ba0c..3f4be4398 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -13,7 +13,6 @@ import json import re -import sys import textwrap import uuid @@ -71,34 +70,10 @@ def get_resource_manager_extra_kwargs(f, args, allow_conflicts=False): return extra_kwargs -def service_type(stype): - """ - Adds 'service_type' attribute to decorated function. - Usage: - @service_type('volume') - def mymethod(f): - ... - """ - def inner(f): - f.service_type = stype - return f - return inner - - -def get_service_type(f): - """ - Retrieves service type from function - """ - return getattr(f, 'service_type', None) - - -def pretty_choice_list(l): - return ', '.join("'%s'" % i for i in l) - - def pretty_choice_dict(d): """Returns a formatted dict as 'key=value'.""" - return pretty_choice_list(['%s=%s' % (k, d[k]) for k in sorted(d.keys())]) + return cliutils.pretty_choice_list( + ['%s=%s' % (k, d[k]) for k in sorted(d.keys())]) def print_list(objs, fields, formatters={}, sortby_index=None): @@ -313,24 +288,6 @@ def get_field(obj): return name, formatter -class HookableMixin(object): - """Mixin so classes can register and run hooks.""" - _hooks_map = {} - - @classmethod - def add_hook(cls, hook_type, hook_func): - if hook_type not in cls._hooks_map: - cls._hooks_map[hook_type] = [] - - cls._hooks_map[hook_type].append(hook_func) - - @classmethod - def run_hooks(cls, hook_type, *args, **kwargs): - hook_funcs = cls._hooks_map.get(hook_type) or [] - for hook_func in hook_funcs: - hook_func(*args, **kwargs) - - def safe_issubclass(*args): """Like issubclass, but will just return False if not a class.""" @@ -343,13 +300,6 @@ def safe_issubclass(*args): return False -def import_class(import_str): - """Returns a class from a string including module and class.""" - mod_str, _sep, class_str = import_str.rpartition('.') - __import__(mod_str) - return getattr(sys.modules[mod_str], class_str) - - def _load_entry_point(ep_name, name=None): """Try to load the entry point ep_name that matches name.""" for ep in pkg_resources.iter_entry_points(ep_name, name=name): diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 457ebc1a1..b7e8a74ed 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -34,6 +34,7 @@ import six from novaclient import exceptions +from novaclient.openstack.common import cliutils from novaclient.openstack.common.gettextutils import _ from novaclient.openstack.common import uuidutils from novaclient import utils @@ -1767,7 +1768,7 @@ def _translate_availability_zone_keys(collection): type=int, const=1, help=argparse.SUPPRESS) -@utils.service_type('volume') +@cliutils.service_type('volume') def do_volume_list(cs, args): """List all the volumes.""" search_opts = {'all_tenants': args.all_tenants} @@ -1783,7 +1784,7 @@ def do_volume_list(cs, args): @utils.arg('volume', metavar='', help=_('Name or ID of the volume.')) -@utils.service_type('volume') +@cliutils.service_type('volume') def do_volume_show(cs, args): """Show details about a volume.""" volume = _find_volume(cs, args.volume) @@ -1825,7 +1826,7 @@ def do_volume_show(cs, args): @utils.arg('--availability-zone', metavar='', help=_('Optional Availability Zone for volume. (Default=None)'), default=None) -@utils.service_type('volume') +@cliutils.service_type('volume') def do_volume_create(cs, args): """Add a new volume.""" volume = cs.volumes.create(args.size, @@ -1841,7 +1842,7 @@ def do_volume_create(cs, args): @utils.arg('volume', metavar='', nargs='+', help=_('Name or ID of the volume(s) to delete.')) -@utils.service_type('volume') +@cliutils.service_type('volume') def do_volume_delete(cs, args): """Remove volume(s).""" for volume in args.volume: @@ -1900,7 +1901,7 @@ def do_volume_detach(cs, args): args.attachment_id) -@utils.service_type('volume') +@cliutils.service_type('volume') def do_volume_snapshot_list(cs, _args): """List all the snapshots.""" snapshots = cs.volume_snapshots.list() @@ -1912,7 +1913,7 @@ def do_volume_snapshot_list(cs, _args): @utils.arg('snapshot', metavar='', help=_('Name or ID of the snapshot.')) -@utils.service_type('volume') +@cliutils.service_type('volume') def do_volume_snapshot_show(cs, args): """Show details about a snapshot.""" snapshot = _find_volume_snapshot(cs, args.snapshot) @@ -1939,7 +1940,7 @@ def do_volume_snapshot_show(cs, args): help=_('Optional snapshot description. (Default=None)')) @utils.arg('--display_description', help=argparse.SUPPRESS) -@utils.service_type('volume') +@cliutils.service_type('volume') def do_volume_snapshot_create(cs, args): """Add a new snapshot.""" snapshot = cs.volume_snapshots.create(args.volume_id, @@ -1952,7 +1953,7 @@ def do_volume_snapshot_create(cs, args): @utils.arg('snapshot', metavar='', help=_('Name or ID of the snapshot to delete.')) -@utils.service_type('volume') +@cliutils.service_type('volume') def do_volume_snapshot_delete(cs, args): """Remove a snapshot.""" snapshot = _find_volume_snapshot(cs, args.snapshot) @@ -1963,7 +1964,7 @@ def _print_volume_type_list(vtypes): utils.print_list(vtypes, ['ID', 'Name']) -@utils.service_type('volume') +@cliutils.service_type('volume') def do_volume_type_list(cs, args): """Print a list of available 'volume types'.""" vtypes = cs.volume_types.list() @@ -1973,7 +1974,7 @@ def do_volume_type_list(cs, args): @utils.arg('name', metavar='', help=_("Name of the new volume type")) -@utils.service_type('volume') +@cliutils.service_type('volume') def do_volume_type_create(cs, args): """Create a new volume type.""" vtype = cs.volume_types.create(args.name) @@ -1983,7 +1984,7 @@ def do_volume_type_create(cs, args): @utils.arg('id', metavar='', help=_("Unique ID of the volume type to delete")) -@utils.service_type('volume') +@cliutils.service_type('volume') def do_volume_type_delete(cs, args): """Delete a specific volume type.""" cs.volume_types.delete(args.id) @@ -2966,7 +2967,7 @@ def parser_metadata(fields): return utils.pretty_choice_dict(getattr(fields, 'metadata', {}) or {}) def parser_hosts(fields): - return utils.pretty_choice_list(getattr(fields, 'hosts', [])) + return cliutils.pretty_choice_list(getattr(fields, 'hosts', [])) formatters = { 'Metadata': parser_metadata, @@ -3783,7 +3784,7 @@ def _treeizeAvailabilityZone(zone): return result -@utils.service_type('compute') +@cliutils.service_type('compute') def do_availability_zone_list(cs, _args): """List all the availability zones.""" try: diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 3d17e2890..1a5eb9ef3 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -32,6 +32,7 @@ import six from novaclient import exceptions +from novaclient.openstack.common import cliutils from novaclient.openstack.common.gettextutils import _ from novaclient.openstack.common import uuidutils from novaclient import utils @@ -778,7 +779,7 @@ def do_network_create(cs, args): dest="limit", metavar="", help='Number of images to return per request.') -@utils.service_type('image') +@cliutils.service_type('image') def do_image_list(cs, _args): """Print a list of available images to boot from.""" limit = _args.limit @@ -867,7 +868,7 @@ def _print_flavor(flavor): @utils.arg('image', metavar='', help="Name or ID of image") -@utils.service_type('image') +@cliutils.service_type('image') def do_image_show(cs, args): """Show details about the given image.""" image = _find_image(cs, args.image) @@ -2363,7 +2364,7 @@ def parser_metadata(fields): return utils.pretty_choice_dict(getattr(fields, 'metadata', {}) or {}) def parser_hosts(fields): - return utils.pretty_choice_list(getattr(fields, 'hosts', [])) + return cliutils.pretty_choice_list(getattr(fields, 'hosts', [])) formatters = { 'Metadata': parser_metadata, From 8c0fd9a67420ac5c8138eb0ae6132559b40f1e9e Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Wed, 27 Aug 2014 16:45:35 +0300 Subject: [PATCH 0614/1705] Use oslo.serialization Since module `jsonutils` from common code is graduated in `oslo.serialization`, we should remove code from oslo-incubator and use this library. Change-Id: Ia8b5ef598ff415cdde19f523a36a552918f9f94b --- novaclient/openstack/common/jsonutils.py | 186 ---------------- novaclient/openstack/common/timeutils.py | 210 ------------------ novaclient/tests/fixture_data/floatingips.py | 3 +- novaclient/tests/fixture_data/hosts.py | 2 +- novaclient/tests/fixture_data/images.py | 3 +- novaclient/tests/fixture_data/keypairs.py | 3 +- novaclient/tests/fixture_data/networks.py | 3 +- .../fixture_data/security_group_rules.py | 3 +- .../tests/fixture_data/security_groups.py | 3 +- .../tests/fixture_data/server_groups.py | 3 +- novaclient/tests/fixture_data/servers.py | 3 +- novaclient/tests/utils.py | 3 +- novaclient/tests/v1_1/test_servers.py | 2 +- novaclient/utils.py | 2 +- openstack-common.conf | 1 - requirements.txt | 1 + 16 files changed, 21 insertions(+), 410 deletions(-) delete mode 100644 novaclient/openstack/common/jsonutils.py delete mode 100644 novaclient/openstack/common/timeutils.py diff --git a/novaclient/openstack/common/jsonutils.py b/novaclient/openstack/common/jsonutils.py deleted file mode 100644 index b08a679c5..000000000 --- a/novaclient/openstack/common/jsonutils.py +++ /dev/null @@ -1,186 +0,0 @@ -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# Copyright 2011 Justin Santa Barbara -# All Rights Reserved. -# -# 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. - -''' -JSON related utilities. - -This module provides a few things: - - 1) A handy function for getting an object down to something that can be - JSON serialized. See to_primitive(). - - 2) Wrappers around loads() and dumps(). The dumps() wrapper will - automatically use to_primitive() for you if needed. - - 3) This sets up anyjson to use the loads() and dumps() wrappers if anyjson - is available. -''' - - -import codecs -import datetime -import functools -import inspect -import itertools -import sys - -if sys.version_info < (2, 7): - # On Python <= 2.6, json module is not C boosted, so try to use - # simplejson module if available - try: - import simplejson as json - except ImportError: - import json -else: - import json - -import six -import six.moves.xmlrpc_client as xmlrpclib - -from novaclient.openstack.common import gettextutils -from novaclient.openstack.common import importutils -from novaclient.openstack.common import strutils -from novaclient.openstack.common import timeutils - -netaddr = importutils.try_import("netaddr") - -_nasty_type_tests = [inspect.ismodule, inspect.isclass, inspect.ismethod, - inspect.isfunction, inspect.isgeneratorfunction, - inspect.isgenerator, inspect.istraceback, inspect.isframe, - inspect.iscode, inspect.isbuiltin, inspect.isroutine, - inspect.isabstract] - -_simple_types = (six.string_types + six.integer_types - + (type(None), bool, float)) - - -def to_primitive(value, convert_instances=False, convert_datetime=True, - level=0, max_depth=3): - """Convert a complex object into primitives. - - Handy for JSON serialization. We can optionally handle instances, - but since this is a recursive function, we could have cyclical - data structures. - - To handle cyclical data structures we could track the actual objects - visited in a set, but not all objects are hashable. Instead we just - track the depth of the object inspections and don't go too deep. - - Therefore, convert_instances=True is lossy ... be aware. - - """ - # handle obvious types first - order of basic types determined by running - # full tests on nova project, resulting in the following counts: - # 572754 - # 460353 - # 379632 - # 274610 - # 199918 - # 114200 - # 51817 - # 26164 - # 6491 - # 283 - # 19 - if isinstance(value, _simple_types): - return value - - if isinstance(value, datetime.datetime): - if convert_datetime: - return timeutils.strtime(value) - else: - return value - - # value of itertools.count doesn't get caught by nasty_type_tests - # and results in infinite loop when list(value) is called. - if type(value) == itertools.count: - return six.text_type(value) - - # FIXME(vish): Workaround for LP bug 852095. Without this workaround, - # tests that raise an exception in a mocked method that - # has a @wrap_exception with a notifier will fail. If - # we up the dependency to 0.5.4 (when it is released) we - # can remove this workaround. - if getattr(value, '__module__', None) == 'mox': - return 'mock' - - if level > max_depth: - return '?' - - # The try block may not be necessary after the class check above, - # but just in case ... - try: - recursive = functools.partial(to_primitive, - convert_instances=convert_instances, - convert_datetime=convert_datetime, - level=level, - max_depth=max_depth) - if isinstance(value, dict): - return dict((k, recursive(v)) for k, v in six.iteritems(value)) - elif isinstance(value, (list, tuple)): - return [recursive(lv) for lv in value] - - # It's not clear why xmlrpclib created their own DateTime type, but - # for our purposes, make it a datetime type which is explicitly - # handled - if isinstance(value, xmlrpclib.DateTime): - value = datetime.datetime(*tuple(value.timetuple())[:6]) - - if convert_datetime and isinstance(value, datetime.datetime): - return timeutils.strtime(value) - elif isinstance(value, gettextutils.Message): - return value.data - elif hasattr(value, 'iteritems'): - return recursive(dict(value.iteritems()), level=level + 1) - elif hasattr(value, '__iter__'): - return recursive(list(value)) - elif convert_instances and hasattr(value, '__dict__'): - # Likely an instance of something. Watch for cycles. - # Ignore class member vars. - return recursive(value.__dict__, level=level + 1) - elif netaddr and isinstance(value, netaddr.IPAddress): - return six.text_type(value) - else: - if any(test(value) for test in _nasty_type_tests): - return six.text_type(value) - return value - except TypeError: - # Class objects are tricky since they may define something like - # __iter__ defined but it isn't callable as list(). - return six.text_type(value) - - -def dumps(value, default=to_primitive, **kwargs): - return json.dumps(value, default=default, **kwargs) - - -def loads(s, encoding='utf-8'): - return json.loads(strutils.safe_decode(s, encoding)) - - -def load(fp, encoding='utf-8'): - return json.load(codecs.getreader(encoding)(fp)) - - -try: - import anyjson -except ImportError: - pass -else: - anyjson._modules.append((__name__, 'dumps', TypeError, - 'loads', ValueError, 'load')) - anyjson.force_implementation(__name__) diff --git a/novaclient/openstack/common/timeutils.py b/novaclient/openstack/common/timeutils.py deleted file mode 100644 index 52688a026..000000000 --- a/novaclient/openstack/common/timeutils.py +++ /dev/null @@ -1,210 +0,0 @@ -# Copyright 2011 OpenStack Foundation. -# All Rights Reserved. -# -# 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. - -""" -Time related utilities and helper functions. -""" - -import calendar -import datetime -import time - -import iso8601 -import six - - -# ISO 8601 extended time format with microseconds -_ISO8601_TIME_FORMAT_SUBSECOND = '%Y-%m-%dT%H:%M:%S.%f' -_ISO8601_TIME_FORMAT = '%Y-%m-%dT%H:%M:%S' -PERFECT_TIME_FORMAT = _ISO8601_TIME_FORMAT_SUBSECOND - - -def isotime(at=None, subsecond=False): - """Stringify time in ISO 8601 format.""" - if not at: - at = utcnow() - st = at.strftime(_ISO8601_TIME_FORMAT - if not subsecond - else _ISO8601_TIME_FORMAT_SUBSECOND) - tz = at.tzinfo.tzname(None) if at.tzinfo else 'UTC' - st += ('Z' if tz == 'UTC' else tz) - return st - - -def parse_isotime(timestr): - """Parse time from ISO 8601 format.""" - try: - return iso8601.parse_date(timestr) - except iso8601.ParseError as e: - raise ValueError(six.text_type(e)) - except TypeError as e: - raise ValueError(six.text_type(e)) - - -def strtime(at=None, fmt=PERFECT_TIME_FORMAT): - """Returns formatted utcnow.""" - if not at: - at = utcnow() - return at.strftime(fmt) - - -def parse_strtime(timestr, fmt=PERFECT_TIME_FORMAT): - """Turn a formatted time back into a datetime.""" - return datetime.datetime.strptime(timestr, fmt) - - -def normalize_time(timestamp): - """Normalize time in arbitrary timezone to UTC naive object.""" - offset = timestamp.utcoffset() - if offset is None: - return timestamp - return timestamp.replace(tzinfo=None) - offset - - -def is_older_than(before, seconds): - """Return True if before is older than seconds.""" - if isinstance(before, six.string_types): - before = parse_strtime(before).replace(tzinfo=None) - else: - before = before.replace(tzinfo=None) - - return utcnow() - before > datetime.timedelta(seconds=seconds) - - -def is_newer_than(after, seconds): - """Return True if after is newer than seconds.""" - if isinstance(after, six.string_types): - after = parse_strtime(after).replace(tzinfo=None) - else: - after = after.replace(tzinfo=None) - - return after - utcnow() > datetime.timedelta(seconds=seconds) - - -def utcnow_ts(): - """Timestamp version of our utcnow function.""" - if utcnow.override_time is None: - # NOTE(kgriffs): This is several times faster - # than going through calendar.timegm(...) - return int(time.time()) - - return calendar.timegm(utcnow().timetuple()) - - -def utcnow(): - """Overridable version of utils.utcnow.""" - if utcnow.override_time: - try: - return utcnow.override_time.pop(0) - except AttributeError: - return utcnow.override_time - return datetime.datetime.utcnow() - - -def iso8601_from_timestamp(timestamp): - """Returns a iso8601 formatted date from timestamp.""" - return isotime(datetime.datetime.utcfromtimestamp(timestamp)) - - -utcnow.override_time = None - - -def set_time_override(override_time=None): - """Overrides utils.utcnow. - - Make it return a constant time or a list thereof, one at a time. - - :param override_time: datetime instance or list thereof. If not - given, defaults to the current UTC time. - """ - utcnow.override_time = override_time or datetime.datetime.utcnow() - - -def advance_time_delta(timedelta): - """Advance overridden time using a datetime.timedelta.""" - assert(not utcnow.override_time is None) - try: - for dt in utcnow.override_time: - dt += timedelta - except TypeError: - utcnow.override_time += timedelta - - -def advance_time_seconds(seconds): - """Advance overridden time by seconds.""" - advance_time_delta(datetime.timedelta(0, seconds)) - - -def clear_time_override(): - """Remove the overridden time.""" - utcnow.override_time = None - - -def marshall_now(now=None): - """Make an rpc-safe datetime with microseconds. - - Note: tzinfo is stripped, but not required for relative times. - """ - if not now: - now = utcnow() - return dict(day=now.day, month=now.month, year=now.year, hour=now.hour, - minute=now.minute, second=now.second, - microsecond=now.microsecond) - - -def unmarshall_time(tyme): - """Unmarshall a datetime dict.""" - return datetime.datetime(day=tyme['day'], - month=tyme['month'], - year=tyme['year'], - hour=tyme['hour'], - minute=tyme['minute'], - second=tyme['second'], - microsecond=tyme['microsecond']) - - -def delta_seconds(before, after): - """Return the difference between two timing objects. - - Compute the difference in seconds between two date, time, or - datetime objects (as a float, to microsecond resolution). - """ - delta = after - before - return total_seconds(delta) - - -def total_seconds(delta): - """Return the total seconds of datetime.timedelta object. - - Compute total seconds of datetime.timedelta, datetime.timedelta - doesn't have method total_seconds in Python2.6, calculate it manually. - """ - try: - return delta.total_seconds() - except AttributeError: - return ((delta.days * 24 * 3600) + delta.seconds + - float(delta.microseconds) / (10 ** 6)) - - -def is_soon(dt, window): - """Determines if time is going to happen in the next window seconds. - - :param dt: the time - :param window: minimum seconds to remain to consider the time not soon - - :return: True if expiration is within the given duration - """ - soon = (utcnow() + datetime.timedelta(seconds=window)) - return normalize_time(dt) <= soon diff --git a/novaclient/tests/fixture_data/floatingips.py b/novaclient/tests/fixture_data/floatingips.py index 5fc6848a7..d73546fb7 100644 --- a/novaclient/tests/fixture_data/floatingips.py +++ b/novaclient/tests/fixture_data/floatingips.py @@ -10,7 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.openstack.common import jsonutils +from oslo.serialization import jsonutils + from novaclient.tests import fakes from novaclient.tests.fixture_data import base diff --git a/novaclient/tests/fixture_data/hosts.py b/novaclient/tests/fixture_data/hosts.py index babfbec7c..0235117a8 100644 --- a/novaclient/tests/fixture_data/hosts.py +++ b/novaclient/tests/fixture_data/hosts.py @@ -10,9 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from oslo.serialization import jsonutils from six.moves.urllib import parse -from novaclient.openstack.common import jsonutils from novaclient.tests.fixture_data import base diff --git a/novaclient/tests/fixture_data/images.py b/novaclient/tests/fixture_data/images.py index 40cfd0c94..9c9ef10f2 100644 --- a/novaclient/tests/fixture_data/images.py +++ b/novaclient/tests/fixture_data/images.py @@ -10,7 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.openstack.common import jsonutils +from oslo.serialization import jsonutils + from novaclient.tests import fakes from novaclient.tests.fixture_data import base diff --git a/novaclient/tests/fixture_data/keypairs.py b/novaclient/tests/fixture_data/keypairs.py index cc8921beb..97cf24992 100644 --- a/novaclient/tests/fixture_data/keypairs.py +++ b/novaclient/tests/fixture_data/keypairs.py @@ -10,7 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.openstack.common import jsonutils +from oslo.serialization import jsonutils + from novaclient.tests import fakes from novaclient.tests.fixture_data import base diff --git a/novaclient/tests/fixture_data/networks.py b/novaclient/tests/fixture_data/networks.py index df721b25d..44d7d528a 100644 --- a/novaclient/tests/fixture_data/networks.py +++ b/novaclient/tests/fixture_data/networks.py @@ -10,7 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.openstack.common import jsonutils +from oslo.serialization import jsonutils + from novaclient.tests.fixture_data import base diff --git a/novaclient/tests/fixture_data/security_group_rules.py b/novaclient/tests/fixture_data/security_group_rules.py index 480c4fc6d..aab4c2456 100644 --- a/novaclient/tests/fixture_data/security_group_rules.py +++ b/novaclient/tests/fixture_data/security_group_rules.py @@ -10,7 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.openstack.common import jsonutils +from oslo.serialization import jsonutils + from novaclient.tests import fakes from novaclient.tests.fixture_data import base diff --git a/novaclient/tests/fixture_data/security_groups.py b/novaclient/tests/fixture_data/security_groups.py index 7d1fed380..9f2f96b7d 100644 --- a/novaclient/tests/fixture_data/security_groups.py +++ b/novaclient/tests/fixture_data/security_groups.py @@ -10,7 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.openstack.common import jsonutils +from oslo.serialization import jsonutils + from novaclient.tests import fakes from novaclient.tests.fixture_data import base diff --git a/novaclient/tests/fixture_data/server_groups.py b/novaclient/tests/fixture_data/server_groups.py index 47e307153..6defc895c 100644 --- a/novaclient/tests/fixture_data/server_groups.py +++ b/novaclient/tests/fixture_data/server_groups.py @@ -10,7 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.openstack.common import jsonutils +from oslo.serialization import jsonutils + from novaclient.tests.fixture_data import base diff --git a/novaclient/tests/fixture_data/servers.py b/novaclient/tests/fixture_data/servers.py index c8f87bf60..4cb3bddda 100644 --- a/novaclient/tests/fixture_data/servers.py +++ b/novaclient/tests/fixture_data/servers.py @@ -10,7 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.openstack.common import jsonutils +from oslo.serialization import jsonutils + from novaclient.tests import fakes from novaclient.tests.fixture_data import base diff --git a/novaclient/tests/utils.py b/novaclient/tests/utils.py index 036702ec5..a257ac1fa 100644 --- a/novaclient/tests/utils.py +++ b/novaclient/tests/utils.py @@ -14,14 +14,13 @@ import os import fixtures +from oslo.serialization import jsonutils import requests from requests_mock.contrib import fixture as requests_mock_fixture import six import testscenarios import testtools -from novaclient.openstack.common import jsonutils - AUTH_URL = "http://localhost:5002/auth_url" AUTH_URL_V1 = "http://localhost:5002/auth_url/v1.0" AUTH_URL_V2 = "http://localhost:5002/auth_url/v2.0" diff --git a/novaclient/tests/v1_1/test_servers.py b/novaclient/tests/v1_1/test_servers.py index a8a3aeabd..9bd7fddea 100644 --- a/novaclient/tests/v1_1/test_servers.py +++ b/novaclient/tests/v1_1/test_servers.py @@ -13,10 +13,10 @@ # under the License. import mock +from oslo.serialization import jsonutils import six from novaclient import exceptions -from novaclient.openstack.common import jsonutils from novaclient.tests.fixture_data import client from novaclient.tests.fixture_data import floatingips from novaclient.tests.fixture_data import servers as data diff --git a/novaclient/utils.py b/novaclient/utils.py index d6068ba0c..99d91abb2 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -17,6 +17,7 @@ import textwrap import uuid +from oslo.serialization import jsonutils from oslo.utils import encodeutils import pkg_resources import prettytable @@ -25,7 +26,6 @@ from novaclient import exceptions from novaclient.openstack.common import cliutils from novaclient.openstack.common.gettextutils import _ -from novaclient.openstack.common import jsonutils arg = cliutils.arg diff --git a/openstack-common.conf b/openstack-common.conf index 3842ed9ba..3af93417e 100644 --- a/openstack-common.conf +++ b/openstack-common.conf @@ -5,7 +5,6 @@ module=apiclient module=cliutils module=gettextutils module=install_venv_common -module=jsonutils module=uuidutils # The base module to hold the copy of openstack.common diff --git a/requirements.txt b/requirements.txt index 08de4eb63..266e01732 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,7 @@ pbr>=0.6,!=0.7,<1.0 argparse iso8601>=0.1.9 +oslo.serialization>=1.0.0 # Apache-2.0 oslo.utils>=1.0.0 # Apache-2.0 PrettyTable>=0.7,<0.8 requests>=1.2.1,!=2.4.0 From 7fc1588dfda6edcb68aee2ff40e7475f9a6e400b Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Wed, 24 Sep 2014 22:10:34 +0300 Subject: [PATCH 0615/1705] Enable check for E121 E121 continuation line under-indented for hanging indent Change-Id: I4cf246e3ec932ba0d2391eb8bcb793b28b005b4c --- novaclient/tests/fixture_data/certs.py | 2 +- novaclient/tests/fixture_data/floatingips.py | 2 +- novaclient/tests/test_shell.py | 13 +++---- novaclient/tests/v1_1/fakes.py | 38 ++++++++++---------- novaclient/tests/v1_1/test_hypervisors.py | 8 ++--- novaclient/v1_1/servers.py | 4 +-- novaclient/v3/servers.py | 4 +-- tox.ini | 5 +-- 8 files changed, 39 insertions(+), 37 deletions(-) diff --git a/novaclient/tests/fixture_data/certs.py b/novaclient/tests/fixture_data/certs.py index 40421ca1b..243bb0ea7 100644 --- a/novaclient/tests/fixture_data/certs.py +++ b/novaclient/tests/fixture_data/certs.py @@ -48,7 +48,7 @@ def setUp(self): 'certificate': { 'private_key': 'foo', 'data': 'bar' - } + } } self.requests.register_uri('POST', self.url(), json=post_os_certificates, diff --git a/novaclient/tests/fixture_data/floatingips.py b/novaclient/tests/fixture_data/floatingips.py index d73546fb7..3f2b25000 100644 --- a/novaclient/tests/fixture_data/floatingips.py +++ b/novaclient/tests/fixture_data/floatingips.py @@ -109,7 +109,7 @@ def put_dns_testdomain_entries_testname(request, context): 'name': "host1", 'type': "A", 'domain': 'testdomain' - } + } }, { 'dns_entry': { diff --git a/novaclient/tests/test_shell.py b/novaclient/tests/test_shell.py index 240965d79..8fae05657 100644 --- a/novaclient/tests/test_shell.py +++ b/novaclient/tests/test_shell.py @@ -204,12 +204,13 @@ def test_password(self, mock_getpass, mock_stdin): dist_version.StrictVersion('0.7.2')): ex = '\n' else: - ex = ( - '+----+------+--------+------------+-------------+----------+\n' - '| ID | Name | Status | Task State | Power State | Networks |\n' - '+----+------+--------+------------+-------------+----------+\n' - '+----+------+--------+------------+-------------+----------+\n' - ) + ex = '\n'.join([ + '+----+------+--------+------------+-------------+----------+', + '| ID | Name | Status | Task State | Power State | Networks |', + '+----+------+--------+------------+-------------+----------+', + '+----+------+--------+------------+-------------+----------+', + '' + ]) self.make_env(exclude='OS_PASSWORD') stdout, stderr = self.shell('list') self.assertEqual((stdout + stderr), ex) diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index 3e82eb053..4267df4e2 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -1989,8 +1989,8 @@ def get_servers_1234_os_volume_attachments(self, **kw): "attached": "2011-11-11T00:00:00Z", "size": 1024, "attachments": [ - {"id": "3333", - "links": ''}], + {"id": "3333", + "links": ''}], "metadata": {}}]}) def get_servers_1234_os_volume_attachments_Work(self, **kw): @@ -2003,8 +2003,8 @@ def get_servers_1234_os_volume_attachments_Work(self, **kw): "attached": "2011-11-11T00:00:00Z", "size": 1024, "attachments": [ - {"id": "3333", - "links": ''}], + {"id": "3333", + "links": ''}], "metadata": {}}}) def delete_servers_1234_os_volume_attachments_Work(self, **kw): @@ -2072,21 +2072,21 @@ def get_os_cells_child_cell_capacities(self, **kw): return self.get_os_cells_capacities() def get_os_migrations(self, **kw): - migrations = {'migrations': - [{ - "created_at": "2012-10-29T13:42:02.000000", - "dest_compute": "compute2", - "dest_host": "1.2.3.4", - "dest_node": "node2", - "id": 1234, - "instance_uuid": "instance_id_123", - "new_instance_type_id": 2, - "old_instance_type_id": 1, - "source_compute": "compute1", - "source_node": "node1", - "status": "Done", - "updated_at": "2012-10-29T13:42:02.000000" - }]} + migrations = {'migrations': [ + { + "created_at": "2012-10-29T13:42:02.000000", + "dest_compute": "compute2", + "dest_host": "1.2.3.4", + "dest_node": "node2", + "id": 1234, + "instance_uuid": "instance_id_123", + "new_instance_type_id": 2, + "old_instance_type_id": 1, + "source_compute": "compute1", + "source_node": "node1", + "status": "Done", + "updated_at": "2012-10-29T13:42:02.000000" + }]} return (200, {}, migrations) def post_os_server_external_events(self, **kw): diff --git a/novaclient/tests/v1_1/test_hypervisors.py b/novaclient/tests/v1_1/test_hypervisors.py index 8376d61f5..a1b404f0b 100644 --- a/novaclient/tests/v1_1/test_hypervisors.py +++ b/novaclient/tests/v1_1/test_hypervisors.py @@ -99,13 +99,13 @@ def test_hypervisor_servers(self): dict(id=1234, hypervisor_hostname='hyper1', servers=[ - dict(name='inst1', uuid='uuid1'), - dict(name='inst2', uuid='uuid2')]), + dict(name='inst1', uuid='uuid1'), + dict(name='inst2', uuid='uuid2')]), dict(id=5678, hypervisor_hostname='hyper2', servers=[ - dict(name='inst3', uuid='uuid3'), - dict(name='inst4', uuid='uuid4')]), + dict(name='inst3', uuid='uuid3'), + dict(name='inst4', uuid='uuid4')]), ] result = self.cs.hypervisors.search('hyper', True) diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index 38bde8c62..ec6682054 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -486,8 +486,8 @@ def _boot(self, resource_url, response_key, name, image, flavor, body["server"]["max_count"] = max_count if security_groups: - body["server"]["security_groups"] =\ - [{'name': sg} for sg in security_groups] + body["server"]["security_groups"] = [{'name': sg} + for sg in security_groups] # Files are a slight bit tricky. They're passed in a "personality" # list to the POST. Each item is a dict giving a file name and the diff --git a/novaclient/v3/servers.py b/novaclient/v3/servers.py index 70bc5a54b..c9324c477 100644 --- a/novaclient/v3/servers.py +++ b/novaclient/v3/servers.py @@ -428,8 +428,8 @@ def _boot(self, resource_url, response_key, name, image, flavor, body["server"]["os-multiple-create:max_count"] = max_count if security_groups: - body["server"]["security_groups"] = \ - [{'name': sg} for sg in security_groups] + body["server"]["security_groups"] = [{'name': sg} + for sg in security_groups] if availability_zone: body["server"][ diff --git a/tox.ini b/tox.ini index 53fa8719f..34f30deed 100644 --- a/tox.ini +++ b/tox.ini @@ -41,8 +41,9 @@ downloadcache = ~/cache/pip # H904 wrap long lines in parentheses instead of a backslash # reason: removed in hacking (https://review.openstack.org/#/c/101701/) # -# Additional checks are also ignored on purpose: E12, F811, F821 -ignore = E12,F811,F821,H402,H404,H405,H904 +# Additional checks are also ignored on purpose: E122, E123, E124, E126, +# E127, E128, E129, F811, F821 +ignore = E122,E123,E124,E126,E127,E128,E129,F811,F821,H402,H404,H405,H904 show-source = True exclude=.venv,.git,.tox,dist,*openstack/common*,*lib/python*,*egg,build,doc/source/conf.py From d05ae5dcc01b08c9ada2da9522b42ee97324696d Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Wed, 24 Sep 2014 22:19:17 +0300 Subject: [PATCH 0616/1705] Enable check for E122 E122 continuation line missing indentation or outdented Change-Id: Ic54714142b3c5aded42d544f296b0ef9a840c282 --- novaclient/tests/fixture_data/servers.py | 72 ++++++------ novaclient/tests/v1_1/fakes.py | 137 +++++++++++------------ novaclient/tests/v3/fakes.py | 63 +++++------ novaclient/v1_1/flavors.py | 3 +- novaclient/v3/flavors.py | 3 +- tox.ini | 4 +- 6 files changed, 140 insertions(+), 142 deletions(-) diff --git a/novaclient/tests/fixture_data/servers.py b/novaclient/tests/fixture_data/servers.py index 4cb3bddda..1594ad0a0 100644 --- a/novaclient/tests/fixture_data/servers.py +++ b/novaclient/tests/fixture_data/servers.py @@ -49,14 +49,15 @@ def setUp(self): "status": "BUILD", "progress": 60, "addresses": { - "public": [{ - "version": 4, - "addr": "1.2.3.4", - }, - { - "version": 4, - "addr": "5.6.7.8", - }], + "public": [ + { + "version": 4, + "addr": "1.2.3.4", + }, + { + "version": 4, + "addr": "5.6.7.8", + }], "private": [{ "version": 4, "addr": "10.11.12.13", @@ -89,14 +90,15 @@ def setUp(self): "hostId": "9e107d9d372bb6826bd81d3542a419d6", "status": "ACTIVE", "addresses": { - "public": [{ - "version": 4, - "addr": "4.5.6.7", - }, - { - "version": 4, - "addr": "5.6.9.8", - }], + "public": [ + { + "version": 4, + "addr": "4.5.6.7", + }, + { + "version": 4, + "addr": "5.6.9.8", + }], "private": [{ "version": 4, "addr": "10.13.12.13", @@ -106,16 +108,17 @@ def setUp(self): "Server Label": "DB 1" }, "OS-EXT-SRV-ATTR:host": "computenode2", - "security_groups": [{ - 'id': 1, 'name': 'securitygroup1', - 'description': 'FAKE_SECURITY_GROUP', - 'tenant_id': '4ffc664c198e435e9853f2538fbcd7a7' - }, - { - 'id': 2, 'name': 'securitygroup2', - 'description': 'ANOTHER_FAKE_SECURITY_GROUP', - 'tenant_id': '4ffc664c198e435e9853f2538fbcd7a7' - }], + "security_groups": [ + { + 'id': 1, 'name': 'securitygroup1', + 'description': 'FAKE_SECURITY_GROUP', + 'tenant_id': '4ffc664c198e435e9853f2538fbcd7a7' + }, + { + 'id': 2, 'name': 'securitygroup2', + 'description': 'ANOTHER_FAKE_SECURITY_GROUP', + 'tenant_id': '4ffc664c198e435e9853f2538fbcd7a7' + }], } self.server_9012 = { @@ -129,14 +132,15 @@ def setUp(self): "hostId": "9e107d9d372bb6826bd81d3542a419d6", "status": "ACTIVE", "addresses": { - "public": [{ - "version": 4, - "addr": "4.5.6.7", - }, - { - "version": 4, - "addr": "5.6.9.8", - }], + "public": [ + { + "version": 4, + "addr": "4.5.6.7", + }, + { + "version": 4, + "addr": "5.6.9.8", + }], "private": [{ "version": 4, "addr": "10.13.12.13", diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index 4267df4e2..921d72af1 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -234,8 +234,7 @@ def get_limits(self, **kw): "maxPersonality": 5, "maxPersonalitySize": 10240 }, - }, - }) + }}) # # Servers @@ -264,14 +263,15 @@ def get_servers_detail(self, **kw): "status": "BUILD", "progress": 60, "addresses": { - "public": [{ - "version": 4, - "addr": "1.2.3.4", - }, - { - "version": 4, - "addr": "5.6.7.8", - }], + "public": [ + { + "version": 4, + "addr": "1.2.3.4", + }, + { + "version": 4, + "addr": "5.6.7.8", + }], "private": [{ "version": 4, "addr": "10.11.12.13", @@ -303,14 +303,15 @@ def get_servers_detail(self, **kw): "hostId": "9e107d9d372bb6826bd81d3542a419d6", "status": "ACTIVE", "addresses": { - "public": [{ - "version": 4, - "addr": "4.5.6.7", - }, - { - "version": 4, - "addr": "5.6.9.8", - }], + "public": [ + { + "version": 4, + "addr": "4.5.6.7", + }, + { + "version": 4, + "addr": "5.6.9.8", + }], "private": [{ "version": 4, "addr": "10.13.12.13", @@ -320,16 +321,17 @@ def get_servers_detail(self, **kw): "Server Label": "DB 1" }, "OS-EXT-SRV-ATTR:host": "computenode2", - "security_groups": [{ - 'id': 1, 'name': 'securitygroup1', - 'description': 'FAKE_SECURITY_GROUP', - 'tenant_id': '4ffc664c198e435e9853f2538fbcd7a7' - }, - { - 'id': 2, 'name': 'securitygroup2', - 'description': 'ANOTHER_FAKE_SECURITY_GROUP', - 'tenant_id': '4ffc664c198e435e9853f2538fbcd7a7' - }], + "security_groups": [ + { + 'id': 1, 'name': 'securitygroup1', + 'description': 'FAKE_SECURITY_GROUP', + 'tenant_id': '4ffc664c198e435e9853f2538fbcd7a7' + }, + { + 'id': 2, 'name': 'securitygroup2', + 'description': 'ANOTHER_FAKE_SECURITY_GROUP', + 'tenant_id': '4ffc664c198e435e9853f2538fbcd7a7' + }], }, { "id": 9012, @@ -342,14 +344,15 @@ def get_servers_detail(self, **kw): "hostId": "9e107d9d372bb6826bd81d3542a419d6", "status": "ACTIVE", "addresses": { - "public": [{ - "version": 4, - "addr": "4.5.6.7", - }, - { - "version": 4, - "addr": "5.6.9.8", - }], + "public": [ + { + "version": 4, + "addr": "4.5.6.7", + }, + { + "version": 4, + "addr": "5.6.9.8", + }], "private": [{ "version": 4, "addr": "10.13.12.13", @@ -1827,39 +1830,35 @@ def get_os_availability_zone(self, **kw): "hosts": None}]}) def get_os_availability_zone_detail(self, **kw): - return (200, {}, {"availabilityZoneInfo": [ - {"zoneName": "zone-1", - "zoneState": {"available": True}, - "hosts": { - "fake_host-1": { - "nova-compute": {"active": True, - "available": True, - "updated_at": - datetime.datetime( - 2012, 12, 26, 14, 45, 25, 0 - )}}}}, - {"zoneName": "internal", - "zoneState": {"available": True}, - "hosts": { - "fake_host-1": { - "nova-sched": { - "active": True, - "available": True, - "updated_at": - datetime.datetime( - 2012, 12, 26, 14, 45, 25, 0 - )}}, - "fake_host-2": { - "nova-network": { - "active": True, - "available": False, - "updated_at": - datetime.datetime( - 2012, 12, 26, 14, 45, 24, 0 - )}}}}, - {"zoneName": "zone-2", - "zoneState": {"available": False}, - "hosts": None}]}) + return (200, {}, { + "availabilityZoneInfo": [ + {"zoneName": "zone-1", + "zoneState": {"available": True}, + "hosts": { + "fake_host-1": { + "nova-compute": { + "active": True, + "available": True, + "updated_at": datetime.datetime( + 2012, 12, 26, 14, 45, 25, 0)}}}}, + {"zoneName": "internal", + "zoneState": {"available": True}, + "hosts": { + "fake_host-1": { + "nova-sched": { + "active": True, + "available": True, + "updated_at": datetime.datetime( + 2012, 12, 26, 14, 45, 25, 0)}}, + "fake_host-2": { + "nova-network": { + "active": True, + "available": False, + "updated_at": datetime.datetime( + 2012, 12, 26, 14, 45, 24, 0)}}}}, + {"zoneName": "zone-2", + "zoneState": {"available": False}, + "hosts": None}]}) def get_servers_1234_os_interface(self, **kw): return (200, {}, {"interfaceAttachments": [ diff --git a/novaclient/tests/v3/fakes.py b/novaclient/tests/v3/fakes.py index 662e308e3..18989a253 100644 --- a/novaclient/tests/v3/fakes.py +++ b/novaclient/tests/v3/fakes.py @@ -272,39 +272,36 @@ def get_os_availability_zone(self, **kw): "hosts": None}]}) def get_os_availability_zone_detail(self, **kw): - return (200, {}, {"availability_zone_info": [ - {"zone_name": "zone-1", - "zone_state": {"available": True}, - "hosts": { - "fake_host-1": { - "nova-compute": {"active": True, - "available": True, - "updated_at": - datetime.datetime( - 2012, 12, 26, 14, 45, 25, 0 - )}}}}, - {"zone_name": "internal", - "zone_state": {"available": True}, - "hosts": { - "fake_host-1": { - "nova-sched": { - "active": True, - "available": True, - "updated_at": - datetime.datetime( - 2012, 12, 26, 14, 45, 25, 0 - )}}, - "fake_host-2": { - "nova-network": { - "active": True, - "available": False, - "updated_at": - datetime.datetime( - 2012, 12, 26, 14, 45, 24, 0 - )}}}}, - {"zone_name": "zone-2", - "zone_state": {"available": False}, - "hosts": None}]}) + return (200, {}, { + "availability_zone_info": [ + {"zone_name": "zone-1", + "zone_state": {"available": True}, + "hosts": { + "fake_host-1": { + "nova-compute": { + "active": True, + "available": True, + "updated_at": datetime.datetime( + 2012, 12, 26, 14, 45, 25, 0)}}}}, + {"zone_name": "internal", + "zone_state": { + "available": True}, + "hosts": { + "fake_host-1": { + "nova-sched": { + "active": True, + "available": True, + "updated_at": datetime.datetime( + 2012, 12, 26, 14, 45, 25, 0)}}, + "fake_host-2": { + "nova-network": { + "active": True, + "available": False, + "updated_at": datetime.datetime( + 2012, 12, 26, 14, 45, 24, 0)}}}}, + {"zone_name": "zone-2", + "zone_state": {"available": False}, + "hosts": None}]}) # # Quotas diff --git a/novaclient/v1_1/flavors.py b/novaclient/v1_1/flavors.py index e66e0b9e2..018bc1cf0 100644 --- a/novaclient/v1_1/flavors.py +++ b/novaclient/v1_1/flavors.py @@ -84,8 +84,7 @@ def unset_keys(self, keys): """ for k in keys: self.manager._delete( - "/flavors/%s/os-extra_specs/%s" % ( - base.getid(self), k)) + "/flavors/%s/os-extra_specs/%s" % (base.getid(self), k)) def delete(self): """ diff --git a/novaclient/v3/flavors.py b/novaclient/v3/flavors.py index e65f2f1f4..6dcb1c434 100644 --- a/novaclient/v3/flavors.py +++ b/novaclient/v3/flavors.py @@ -73,8 +73,7 @@ def unset_keys(self, keys): """ for k in keys: self.manager._delete( - "/flavors/%s/flavor-extra-specs/%s" % ( - base.getid(self), k)) + "/flavors/%s/flavor-extra-specs/%s" % (base.getid(self), k)) def delete(self): """ diff --git a/tox.ini b/tox.ini index 34f30deed..ca21ce589 100644 --- a/tox.ini +++ b/tox.ini @@ -41,9 +41,9 @@ downloadcache = ~/cache/pip # H904 wrap long lines in parentheses instead of a backslash # reason: removed in hacking (https://review.openstack.org/#/c/101701/) # -# Additional checks are also ignored on purpose: E122, E123, E124, E126, +# Additional checks are also ignored on purpose: E123, E124, E126, # E127, E128, E129, F811, F821 -ignore = E122,E123,E124,E126,E127,E128,E129,F811,F821,H402,H404,H405,H904 +ignore = E123,E124,E126,E127,E128,E129,F811,F821,H402,H404,H405,H904 show-source = True exclude=.venv,.git,.tox,dist,*openstack/common*,*lib/python*,*egg,build,doc/source/conf.py From 8b8aa499e1a2dd91d2b54bfe6b66f658649979ac Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Wed, 24 Sep 2014 22:30:16 +0300 Subject: [PATCH 0617/1705] Enable check for E123 E123 closing bracket does not match indentation of opening bracket's line Change-Id: I3f941032efb5ca2ef9a78f865effbf79c8674ebb --- novaclient/tests/fixture_data/servers.py | 2 +- novaclient/tests/test_client.py | 2 +- .../tests/v1_1/contrib/test_baremetal.py | 2 +- novaclient/tests/v1_1/fakes.py | 26 +++++++------------ novaclient/tests/v1_1/test_hypervisors.py | 10 +++---- novaclient/tests/v1_1/test_servers.py | 9 ++++--- novaclient/tests/v3/fakes.py | 12 +++------ novaclient/tests/v3/test_hypervisors.py | 3 +-- novaclient/v1_1/contrib/baremetal.py | 4 +-- tox.ini | 4 +-- 10 files changed, 31 insertions(+), 43 deletions(-) diff --git a/novaclient/tests/fixture_data/servers.py b/novaclient/tests/fixture_data/servers.py index 1594ad0a0..00b7cd9fb 100644 --- a/novaclient/tests/fixture_data/servers.py +++ b/novaclient/tests/fixture_data/servers.py @@ -562,7 +562,7 @@ def post_servers_1234_action(self, request, context): 'rescue': {'admin_password': 'RescuePassword'}, 'get_console_output': {'output': 'foo'}, 'rebuild': {'server': self.server_1234}, - } + } body_param_check_exists = { 'rebuild': 'image_ref', 'resize': 'flavor_ref'} diff --git a/novaclient/tests/test_client.py b/novaclient/tests/test_client.py index f8bcaed80..16d62a5b9 100644 --- a/novaclient/tests/test_client.py +++ b/novaclient/tests/test_client.py @@ -91,7 +91,7 @@ def test_client_reauth(self): "passwordCredentials": { "username": "user", "password": "password" - } + } } } diff --git a/novaclient/tests/v1_1/contrib/test_baremetal.py b/novaclient/tests/v1_1/contrib/test_baremetal.py index 6a88d580c..172d8b272 100644 --- a/novaclient/tests/v1_1/contrib/test_baremetal.py +++ b/novaclient/tests/v1_1/contrib/test_baremetal.py @@ -22,7 +22,7 @@ extensions = [ extension.Extension(baremetal.__name__.split(".")[-1], baremetal), - ] +] cs = fakes.FakeClient(extensions=extensions) diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index 921d72af1..929ad2082 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -1285,8 +1285,7 @@ def get_os_security_groups(self, **kw): "tenant_id": "272bee4c1e624cd4a72a6b0ea55b4582", "id": 2, "rules": []} - ]} - ) + ]}) def get_os_security_groups_1(self, **kw): return (200, {}, {"security_group": @@ -1649,8 +1648,7 @@ def put_os_hosts_sample_host(self, body, **kw): def get_os_hypervisors(self, **kw): return (200, {}, {"hypervisors": [ {'id': 1234, 'hypervisor_hostname': 'hyper1'}, - {'id': 5678, 'hypervisor_hostname': 'hyper2'}, - ]}) + {'id': 5678, 'hypervisor_hostname': 'hyper2'}]}) def get_os_hypervisors_detail(self, **kw): return (200, {}, {"hypervisors": [ @@ -1688,7 +1686,7 @@ def get_os_hypervisors_detail(self, **kw): 'running_vms': 2, 'cpu_info': 'cpu_info', 'disk_available_least': 100} - ]}) + ]}) def get_os_hypervisors_statistics(self, **kw): return (200, {}, {"hypervisor_statistics": { @@ -1704,13 +1702,12 @@ def get_os_hypervisors_statistics(self, **kw): 'current_workload': 4, 'running_vms': 4, 'disk_available_least': 200, - }}) + }}) def get_os_hypervisors_hyper_search(self, **kw): return (200, {}, {'hypervisors': [ {'id': 1234, 'hypervisor_hostname': 'hyper1'}, - {'id': 5678, 'hypervisor_hostname': 'hyper2'} - ]}) + {'id': 5678, 'hypervisor_hostname': 'hyper2'}]}) def get_os_hypervisors_hyper_servers(self, **kw): return (200, {}, {'hypervisors': [ @@ -1718,15 +1715,13 @@ def get_os_hypervisors_hyper_servers(self, **kw): 'hypervisor_hostname': 'hyper1', 'servers': [ {'name': 'inst1', 'uuid': 'uuid1'}, - {'name': 'inst2', 'uuid': 'uuid2'} - ]}, + {'name': 'inst2', 'uuid': 'uuid2'}]}, {'id': 5678, 'hypervisor_hostname': 'hyper2', 'servers': [ {'name': 'inst3', 'uuid': 'uuid3'}, - {'name': 'inst4', 'uuid': 'uuid4'} - ]} - ]}) + {'name': 'inst4', 'uuid': 'uuid4'}]} + ]}) def get_os_hypervisors_hyper_no_servers_servers(self, **kw): return (200, {}, {'hypervisors': @@ -2051,12 +2046,11 @@ def get_os_cells_child_cell(self, **kw): 'rpc_host': '10.0.1.10', 'type': 'child', 'name': 'cell1', - 'rpc_port': 5673 - }, + 'rpc_port': 5673}, 'type': 'child', 'rpc_port': 5673, 'loaded': True - }} + }} return (200, {}, cell) def get_os_cells_capacities(self, **kw): diff --git a/novaclient/tests/v1_1/test_hypervisors.py b/novaclient/tests/v1_1/test_hypervisors.py index a1b404f0b..94bbb0838 100644 --- a/novaclient/tests/v1_1/test_hypervisors.py +++ b/novaclient/tests/v1_1/test_hypervisors.py @@ -30,8 +30,7 @@ def compare_to_expected(self, expected, hyper): def test_hypervisor_index(self): expected = [ dict(id=1234, hypervisor_hostname='hyper1'), - dict(id=5678, hypervisor_hostname='hyper2'), - ] + dict(id=5678, hypervisor_hostname='hyper2')] result = self.cs.hypervisors.list(False) self.assert_called('GET', '/os-hypervisors') @@ -85,8 +84,7 @@ def test_hypervisor_detail(self): def test_hypervisor_search(self): expected = [ dict(id=1234, hypervisor_hostname='hyper1'), - dict(id=5678, hypervisor_hostname='hyper2'), - ] + dict(id=5678, hypervisor_hostname='hyper2')] result = self.cs.hypervisors.search('hyper') self.assert_called('GET', '/os-hypervisors/hyper/search') @@ -106,7 +104,7 @@ def test_hypervisor_servers(self): servers=[ dict(name='inst3', uuid='uuid3'), dict(name='inst4', uuid='uuid4')]), - ] + ] result = self.cs.hypervisors.search('hyper', True) self.assert_called('GET', '/os-hypervisors/hyper/servers') @@ -164,7 +162,7 @@ def test_hypervisor_statistics(self): current_workload=4, running_vms=4, disk_available_least=200, - ) + ) result = self.cs.hypervisors.statistics() self.assert_called('GET', '/os-hypervisors/statistics') diff --git a/novaclient/tests/v1_1/test_servers.py b/novaclient/tests/v1_1/test_servers.py index 9bd7fddea..0a81c5aae 100644 --- a/novaclient/tests/v1_1/test_servers.py +++ b/novaclient/tests/v1_1/test_servers.py @@ -640,11 +640,12 @@ def test_interface_list_result_string_representable(self): 'port_id': 'f35079da-36d5-4513-8ec1-0298d703f70e', 'mac_addr': 'fa:16:3e:4c:37:c8', 'port_state': 'ACTIVE', - 'fixed_ips': [{ - 'subnet_id': 'f1ad93ad-2967-46ba-b403-e8cbbe65f7fa', - 'ip_address': '10.2.0.96' + 'fixed_ips': [ + { + 'subnet_id': 'f1ad93ad-2967-46ba-b403-e8cbbe65f7fa', + 'ip_address': '10.2.0.96' }] - }] + }] # If server is not string representable, it will raise an exception, # because attribute named 'name' cannot be found. # Parameter 'loaded' must be True or it will try to get attribute diff --git a/novaclient/tests/v3/fakes.py b/novaclient/tests/v3/fakes.py index 18989a253..5e6f370dc 100644 --- a/novaclient/tests/v3/fakes.py +++ b/novaclient/tests/v3/fakes.py @@ -195,7 +195,7 @@ def post_servers_1234_action(self, body, **kw): 'rescue': {'admin_password': 'RescuePassword'}, 'get_console_output': {'output': 'foo'}, 'rebuild': self.get_servers_1234()[2], - } + } body_param_check_exists = { 'rebuild': 'image_ref', 'resize': 'flavor_ref', @@ -336,12 +336,10 @@ def get_os_quota_sets_test_detail(self, **kw): def get_os_hypervisors_search(self, **kw): if kw['query'] == 'hyper1': return (200, {}, {'hypervisors': [ - {'id': 1234, 'hypervisor_hostname': 'hyper1'}, - ]}) + {'id': 1234, 'hypervisor_hostname': 'hyper1'}]}) return (200, {}, {'hypervisors': [ {'id': 1234, 'hypervisor_hostname': 'hyper1'}, - {'id': 5678, 'hypervisor_hostname': 'hyper2'} - ]}) + {'id': 5678, 'hypervisor_hostname': 'hyper2'}]}) def get_os_hypervisors_1234_servers(self, **kw): return (200, {}, {'hypervisor': @@ -349,9 +347,7 @@ def get_os_hypervisors_1234_servers(self, **kw): 'hypervisor_hostname': 'hyper1', 'servers': [ {'name': 'inst1', 'id': 'uuid1'}, - {'name': 'inst2', 'id': 'uuid2'} - ]}, - }) + {'name': 'inst2', 'id': 'uuid2'}]}}) # # Keypairs diff --git a/novaclient/tests/v3/test_hypervisors.py b/novaclient/tests/v3/test_hypervisors.py index ee7c83fcb..94ea3fad4 100644 --- a/novaclient/tests/v3/test_hypervisors.py +++ b/novaclient/tests/v3/test_hypervisors.py @@ -26,8 +26,7 @@ class HypervisorsTest(test_hypervisors.HypervisorsTest): def test_hypervisor_search(self): expected = [ dict(id=1234, hypervisor_hostname='hyper1'), - dict(id=5678, hypervisor_hostname='hyper2'), - ] + dict(id=5678, hypervisor_hostname='hyper2')] result = self.cs.hypervisors.search('hyper') self.assert_called('GET', '/os-hypervisors/search?query=hyper') diff --git a/novaclient/v1_1/contrib/baremetal.py b/novaclient/v1_1/contrib/baremetal.py index d13018ce8..82803184a 100644 --- a/novaclient/v1_1/contrib/baremetal.py +++ b/novaclient/v1_1/contrib/baremetal.py @@ -243,7 +243,7 @@ def _parse_address(fields): 'PM Username', 'PM Password', 'Terminal Port', - ], formatters=formatters) + ], formatters=formatters) def do_baremetal_node_list(cs, _args): @@ -270,7 +270,7 @@ def _print_baremetal_node_interfaces(interfaces): 'Datapath_ID', 'Port_No', 'Address', - ]) + ]) @utils.arg('node', diff --git a/tox.ini b/tox.ini index ca21ce589..b38ae1c4a 100644 --- a/tox.ini +++ b/tox.ini @@ -41,9 +41,9 @@ downloadcache = ~/cache/pip # H904 wrap long lines in parentheses instead of a backslash # reason: removed in hacking (https://review.openstack.org/#/c/101701/) # -# Additional checks are also ignored on purpose: E123, E124, E126, +# Additional checks are also ignored on purpose: E124, E126, # E127, E128, E129, F811, F821 -ignore = E123,E124,E126,E127,E128,E129,F811,F821,H402,H404,H405,H904 +ignore = E124,E126,E127,E128,E129,F811,F821,H402,H404,H405,H904 show-source = True exclude=.venv,.git,.tox,dist,*openstack/common*,*lib/python*,*egg,build,doc/source/conf.py From 6bbedd1a13966c1d808521228196700746ce7323 Mon Sep 17 00:00:00 2001 From: Ian Wienand Date: Thu, 25 Sep 2014 10:15:30 +1000 Subject: [PATCH 0618/1705] Fix parameter description in create_server Make clear the parameter name is "metadata", not "meta" -- this is confusing at the moment as some other functions do use just "meta" as their metadata argument. Change-Id: I2b3ab478e5ad6e885e88bf0471502c9c971e46bd --- novaclient/v1_1/servers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index 38bde8c62..6520ab4b6 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -1043,7 +1043,7 @@ def create_image(self, server, image_name, metadata=None): :param server: The :class:`Server` (or its ID) to share onto. :param image_name: Name to give the snapshot image - :param meta: Metadata to give newly-created image entity + :param metadata: Metadata to give newly-created image entity """ body = {'name': image_name, 'metadata': metadata or {}} resp = self._action('createImage', server, body)[0] From 09f97e9ee43427d101c719ec26e8cd26dcd0e39d Mon Sep 17 00:00:00 2001 From: Rakesh H S Date: Thu, 25 Sep 2014 11:31:26 +0530 Subject: [PATCH 0619/1705] return 130 for keyboard interrupt when keyboard interrupt is received by novaclient, the return code as of now is 1. But since the client was terminated by an keyboard interrupt, the return code should be 130. (http://tldp.org/LDP/abs/html/exitcodes.html) It is useful when people are writing automation test cases and want to validate based on the return code. I have also changed the message that is printed on shell to match other clients. Change-Id: If544ade154c53b1f18518773f4d49cd335a394d0 Closes-Bug: #1373231 --- novaclient/shell.py | 4 ++-- novaclient/tests/test_shell.py | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/novaclient/shell.py b/novaclient/shell.py index d0d183b23..405ca7051 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -807,8 +807,8 @@ def main(): file=sys.stderr) sys.exit(1) except KeyboardInterrupt as e: - print("Shutting down novaclient", file=sys.stderr) - sys.exit(1) + print("... terminating nova client", file=sys.stderr) + sys.exit(130) if __name__ == "__main__": diff --git a/novaclient/tests/test_shell.py b/novaclient/tests/test_shell.py index 8fae05657..38e219f76 100644 --- a/novaclient/tests/test_shell.py +++ b/novaclient/tests/test_shell.py @@ -273,3 +273,12 @@ def test_main_noargs(self): # We expect the normal usage as a result self.assertIn('Command-line interface to the OpenStack Nova API', sys.stdout.getvalue()) + + @mock.patch.object(novaclient.shell.OpenStackComputeShell, 'main') + def test_main_keyboard_interrupt(self, mock_compute_shell): + # Ensure that exit code is 130 for KeyboardInterrupt + mock_compute_shell.side_effect = KeyboardInterrupt() + try: + novaclient.shell.main() + except SystemExit as ex: + self.assertEqual(ex.code, 130) From 9d5164aceea3a922a3df1bdac1310a48ed50f3e9 Mon Sep 17 00:00:00 2001 From: Haiwei Xu Date: Thu, 2 Oct 2014 19:24:37 +0900 Subject: [PATCH 0620/1705] Show 'state' and 'status' in hypervisor-list Currently hypervisor-list only shows 'ID' and 'Hypervisor hostname', but in fact nova server response also contains 'state' and 'status' attributes, it's better to show all the attributes. Change-Id: I97cd18a8451dc515c5fb19841f11d0e85bd0b484 Closes-Bug: #1376664 --- novaclient/v1_1/shell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index cca8b9b32..a33f85c19 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -3169,7 +3169,7 @@ def _find_hypervisor(cs, hypervisor): help=_('List hypervisors matching the given .')) def do_hypervisor_list(cs, args): """List hypervisors.""" - columns = ['ID', 'Hypervisor hostname'] + columns = ['ID', 'Hypervisor hostname', 'State', 'Status'] if args.matching: utils.print_list(cs.hypervisors.search(args.matching), columns) else: From d6889adb45e290afb0189fca97f8c99b6226aa98 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Tue, 7 Oct 2014 19:17:25 +0000 Subject: [PATCH 0621/1705] Updated from global requirements Change-Id: I41f431880d35b4f5841fc1ad602833c863ce5183 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 266e01732..fcd53ba0d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ iso8601>=0.1.9 oslo.serialization>=1.0.0 # Apache-2.0 oslo.utils>=1.0.0 # Apache-2.0 PrettyTable>=0.7,<0.8 -requests>=1.2.1,!=2.4.0 +requests>=2.2.0,!=2.4.0 simplejson>=2.2.0 six>=1.7.0 Babel>=1.3 From e82b46bb935389dcb84416315adb48799e315df5 Mon Sep 17 00:00:00 2001 From: Russell Bryant Date: Wed, 8 Oct 2014 15:21:28 -0400 Subject: [PATCH 0622/1705] Clarify "nova scrub" help text I came across the "nova scrub" command today. Someone was reporting that it did not delete everything they expected it to. It only deletes networks and security groups associated with the project, so update the help text to make that more clear. Change-Id: Id769018787cc73d52ab5fc93196e69bc0ec785ea --- novaclient/v1_1/shell.py | 2 +- novaclient/v3/shell.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index a33f85c19..eb5b39a12 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -771,7 +771,7 @@ def do_flavor_access_remove(cs, args): @utils.arg('project_id', metavar='', help=_('The ID of the project.')) def do_scrub(cs, args): - """Delete data associated with the project.""" + """Delete networks and security groups associated with a project.""" networks_list = cs.networks.list() networks_list = [network for network in networks_list if getattr(network, 'project_id', '') == args.project_id] diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 1041a45f4..957491543 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -613,7 +613,7 @@ def do_flavor_access_remove(cs, args): @utils.arg('project_id', metavar='', help='The ID of the project.') def do_scrub(cs, args): - """Delete data associated with the project.""" + """Delete networks and security groups associated with a project.""" networks_list = cs.networks.list() networks_list = [network for network in networks_list if getattr(network, 'project_id', '') == args.project_id] From 1eb1abe8700a137d017dbcd4a9dab3d4c0879c81 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Sat, 11 Oct 2014 22:37:57 +0000 Subject: [PATCH 0623/1705] Updated from global requirements Change-Id: Ia256b2210e8aecda0d9bbc10e563e06a68a2e44b --- requirements.txt | 2 +- test-requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index fcd53ba0d..2a1c48ae4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,4 +11,4 @@ requests>=2.2.0,!=2.4.0 simplejson>=2.2.0 six>=1.7.0 Babel>=1.3 -python-keystoneclient>=0.10.0 +python-keystoneclient>=0.11.1 diff --git a/test-requirements.txt b/test-requirements.txt index 8126ed634..76ed5b2a8 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -9,7 +9,7 @@ fixtures>=0.3.14 keyring>=2.1,!=3.3 mock>=1.0 requests-mock>=0.4.0 # Apache-2.0 -sphinx>=1.1.2,!=1.2.0,<1.3 +sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3 oslosphinx>=2.2.0 # Apache-2.0 testrepository>=0.0.18 testscenarios>=0.4 From f4709f02c24fdb3259d07057b9912e8e49229f1d Mon Sep 17 00:00:00 2001 From: Cedric Brandily Date: Mon, 13 Oct 2014 11:19:21 +0200 Subject: [PATCH 0624/1705] Avoid "ambiguous option" when only current/deprecated forms match argparse.ArgumentParser allows to specify partially options: nova boot ... --key-nam mykey # is equivalent to nova boot ... --key-name mykey an error is raised if the provided prefix matchs 0 or 2+ options: nova boot ... --os value # raises an "ambiguous option" error because --os could match # --os-username, --os_username ... even if the provided prefix matchs only the current/deprecated forms of the same attribute: nova boot ... --key mykey # raises an "ambiguous option" error because --key could match # --my-key, --my_key ... This change extends argparse.ArgumentParser to avoid raising an "ambiguous option" when the provided prefix matchs only the current and deprecated forms of the same attribute. Change-Id: I1089901de769df3312d4a15b6d6e5e60b1ed51e0 --- novaclient/shell.py | 17 +++++++++++++++++ novaclient/tests/test_shell.py | 25 +++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/novaclient/shell.py b/novaclient/shell.py index 405ca7051..2d7e72a5f 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -229,6 +229,23 @@ def error(self, message): 'mainp': progparts[0], 'subp': progparts[2]}) + def _get_option_tuples(self, option_string): + """returns (action, option, value) candidates for an option prefix + + Returns [first candidate] if all candidates refers to current and + deprecated forms of the same options: "nova boot ... --key KEY" + parsing succeed because --key could only match --key-name, + --key_name which are current/deprecated forms of the same option. + """ + option_tuples = (super(NovaClientArgumentParser, self) + ._get_option_tuples(option_string)) + if len(option_tuples) > 1: + normalizeds = [option.replace('_', '-') + for action, option, value in option_tuples] + if len(set(normalizeds)) == 1: + return option_tuples[:1] + return option_tuples + class OpenStackComputeShell(object): diff --git a/novaclient/tests/test_shell.py b/novaclient/tests/test_shell.py index 38e219f76..5dd54846d 100644 --- a/novaclient/tests/test_shell.py +++ b/novaclient/tests/test_shell.py @@ -37,6 +37,31 @@ 'OS_AUTH_URL': 'http://no.where'} +class ParserTest(utils.TestCase): + + def setUp(self): + super(ParserTest, self).setUp() + self.parser = novaclient.shell.NovaClientArgumentParser() + + def test_ambiguous_option(self): + self.parser.add_argument('--tic') + self.parser.add_argument('--tac') + + try: + self.parser.parse_args(['--t']) + except SystemExit as err: + self.assertEqual(2, err.code) + else: + self.fail('SystemExit not raised') + + def test_not_really_ambiguous_option(self): + # current/deprecated forms of the same option + self.parser.add_argument('--tic-tac', action="store_true") + self.parser.add_argument('--tic_tac', action="store_true") + args = self.parser.parse_args(['--tic']) + self.assertTrue(args.tic_tac) + + class ShellTest(utils.TestCase): def make_env(self, exclude=None, fake_env=FAKE_ENV): From dc09cf27e3e56baeb00462c0957d36f9e9232b90 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Thu, 16 Oct 2014 01:19:48 +0300 Subject: [PATCH 0625/1705] Port to use oslo.i18n Module gettextutils from oslo-incubator was graduated to separate library (oslo.i18n) and removed from repository, so we should use new library. Change-Id: I15d36ac5a2bb88b332ffa38d29488b75c55a20a9 --- novaclient/client.py | 2 +- novaclient/i18n.py | 35 +++++++++++++++++++ novaclient/shell.py | 2 +- novaclient/utils.py | 2 +- novaclient/v1_1/contrib/baremetal.py | 3 +- novaclient/v1_1/contrib/cells.py | 2 +- novaclient/v1_1/contrib/host_evacuate.py | 2 +- novaclient/v1_1/contrib/host_evacuate_live.py | 2 +- .../v1_1/contrib/host_servers_migrate.py | 2 +- novaclient/v1_1/contrib/instance_action.py | 2 +- .../v1_1/contrib/metadata_extensions.py | 2 +- novaclient/v1_1/contrib/migrations.py | 2 +- novaclient/v1_1/contrib/tenant_networks.py | 2 +- novaclient/v1_1/flavor_access.py | 2 +- novaclient/v1_1/flavors.py | 2 +- novaclient/v1_1/networks.py | 2 +- .../v1_1/security_group_default_rules.py | 2 +- novaclient/v1_1/security_group_rules.py | 2 +- novaclient/v1_1/servers.py | 2 +- novaclient/v1_1/shell.py | 2 +- novaclient/v3/servers.py | 3 +- novaclient/v3/shell.py | 2 +- openstack-common.conf | 1 - requirements.txt | 1 + tox.ini | 2 +- 25 files changed, 60 insertions(+), 23 deletions(-) create mode 100644 novaclient/i18n.py diff --git a/novaclient/client.py b/novaclient/client.py index 5cd2d8f02..4a7189386 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -44,7 +44,7 @@ from six.moves.urllib import parse from novaclient import exceptions -from novaclient.openstack.common.gettextutils import _ +from novaclient.i18n import _ from novaclient import service_catalog from novaclient import utils diff --git a/novaclient/i18n.py b/novaclient/i18n.py new file mode 100644 index 000000000..e9d39d4ab --- /dev/null +++ b/novaclient/i18n.py @@ -0,0 +1,35 @@ +# 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. + +"""oslo.i18n integration module for novaclient. + +See http://docs.openstack.org/developer/oslo.i18n/usage.html . + +""" + +from oslo import i18n + + +_translators = i18n.TranslatorFactory(domain='novaclient') + +# The primary translation function using the well-known name "_" +_ = _translators.primary + +# Translators for log levels. +# +# The abbreviated names are meant to reflect the usual use of a short +# name like '_'. The "L" is for "log" and the other letter comes from +# the level. +_LI = _translators.log_info +_LW = _translators.log_warning +_LE = _translators.log_error +_LC = _translators.log_critical diff --git a/novaclient/shell.py b/novaclient/shell.py index 405ca7051..fff534179 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -47,8 +47,8 @@ from novaclient import client from novaclient import exceptions as exc import novaclient.extension +from novaclient.i18n import _ from novaclient.openstack.common import cliutils -from novaclient.openstack.common.gettextutils import _ from novaclient import utils from novaclient.v1_1 import shell as shell_v1_1 from novaclient.v3 import shell as shell_v3 diff --git a/novaclient/utils.py b/novaclient/utils.py index 2fee83d6c..cf1bc7b21 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -23,8 +23,8 @@ import six from novaclient import exceptions +from novaclient.i18n import _ from novaclient.openstack.common import cliutils -from novaclient.openstack.common.gettextutils import _ arg = cliutils.arg diff --git a/novaclient/v1_1/contrib/baremetal.py b/novaclient/v1_1/contrib/baremetal.py index 82803184a..02e7ad76b 100644 --- a/novaclient/v1_1/contrib/baremetal.py +++ b/novaclient/v1_1/contrib/baremetal.py @@ -16,8 +16,9 @@ """ Baremetal interface (v2 extension). """ + from novaclient import base -from novaclient.openstack.common.gettextutils import _ +from novaclient.i18n import _ from novaclient import utils diff --git a/novaclient/v1_1/contrib/cells.py b/novaclient/v1_1/contrib/cells.py index 7c59400ef..f3b544ed7 100644 --- a/novaclient/v1_1/contrib/cells.py +++ b/novaclient/v1_1/contrib/cells.py @@ -14,7 +14,7 @@ # under the License. from novaclient import base -from novaclient.openstack.common.gettextutils import _ +from novaclient.i18n import _ from novaclient import utils diff --git a/novaclient/v1_1/contrib/host_evacuate.py b/novaclient/v1_1/contrib/host_evacuate.py index 12ad602fa..fc9416afc 100644 --- a/novaclient/v1_1/contrib/host_evacuate.py +++ b/novaclient/v1_1/contrib/host_evacuate.py @@ -14,7 +14,7 @@ # under the License. from novaclient import base -from novaclient.openstack.common.gettextutils import _ +from novaclient.i18n import _ from novaclient import utils diff --git a/novaclient/v1_1/contrib/host_evacuate_live.py b/novaclient/v1_1/contrib/host_evacuate_live.py index 6ac4a018f..0f6a59673 100644 --- a/novaclient/v1_1/contrib/host_evacuate_live.py +++ b/novaclient/v1_1/contrib/host_evacuate_live.py @@ -13,7 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.openstack.common.gettextutils import _ +from novaclient.i18n import _ from novaclient import utils diff --git a/novaclient/v1_1/contrib/host_servers_migrate.py b/novaclient/v1_1/contrib/host_servers_migrate.py index ac5238f42..95268186b 100644 --- a/novaclient/v1_1/contrib/host_servers_migrate.py +++ b/novaclient/v1_1/contrib/host_servers_migrate.py @@ -14,7 +14,7 @@ # under the License. from novaclient import base -from novaclient.openstack.common.gettextutils import _ +from novaclient.i18n import _ from novaclient import utils diff --git a/novaclient/v1_1/contrib/instance_action.py b/novaclient/v1_1/contrib/instance_action.py index cddc30012..1e4ed5829 100644 --- a/novaclient/v1_1/contrib/instance_action.py +++ b/novaclient/v1_1/contrib/instance_action.py @@ -16,7 +16,7 @@ import pprint from novaclient import base -from novaclient.openstack.common.gettextutils import _ +from novaclient.i18n import _ from novaclient import utils diff --git a/novaclient/v1_1/contrib/metadata_extensions.py b/novaclient/v1_1/contrib/metadata_extensions.py index 27b864f7a..54bf8fd04 100644 --- a/novaclient/v1_1/contrib/metadata_extensions.py +++ b/novaclient/v1_1/contrib/metadata_extensions.py @@ -13,7 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.openstack.common.gettextutils import _ +from novaclient.i18n import _ from novaclient import utils from novaclient.v1_1 import shell diff --git a/novaclient/v1_1/contrib/migrations.py b/novaclient/v1_1/contrib/migrations.py index 16be844ab..d43cb7a15 100644 --- a/novaclient/v1_1/contrib/migrations.py +++ b/novaclient/v1_1/contrib/migrations.py @@ -17,7 +17,7 @@ from six.moves.urllib import parse from novaclient import base -from novaclient.openstack.common.gettextutils import _ +from novaclient.i18n import _ from novaclient import utils diff --git a/novaclient/v1_1/contrib/tenant_networks.py b/novaclient/v1_1/contrib/tenant_networks.py index 8c9b22ebc..288d06cb4 100644 --- a/novaclient/v1_1/contrib/tenant_networks.py +++ b/novaclient/v1_1/contrib/tenant_networks.py @@ -13,7 +13,7 @@ # limitations under the License. from novaclient import base -from novaclient.openstack.common.gettextutils import _ +from novaclient.i18n import _ from novaclient import utils diff --git a/novaclient/v1_1/flavor_access.py b/novaclient/v1_1/flavor_access.py index 3b4ce43e6..fdc43cd7a 100644 --- a/novaclient/v1_1/flavor_access.py +++ b/novaclient/v1_1/flavor_access.py @@ -16,7 +16,7 @@ """Flavor access interface.""" from novaclient import base -from novaclient.openstack.common.gettextutils import _ +from novaclient.i18n import _ class FlavorAccess(base.Resource): diff --git a/novaclient/v1_1/flavors.py b/novaclient/v1_1/flavors.py index 018bc1cf0..86781847f 100644 --- a/novaclient/v1_1/flavors.py +++ b/novaclient/v1_1/flavors.py @@ -21,7 +21,7 @@ from novaclient import base from novaclient import exceptions -from novaclient.openstack.common.gettextutils import _ +from novaclient.i18n import _ from novaclient import utils diff --git a/novaclient/v1_1/networks.py b/novaclient/v1_1/networks.py index a27502868..06dee2704 100644 --- a/novaclient/v1_1/networks.py +++ b/novaclient/v1_1/networks.py @@ -19,7 +19,7 @@ from novaclient import base from novaclient import exceptions -from novaclient.openstack.common.gettextutils import _ +from novaclient.i18n import _ class Network(base.Resource): diff --git a/novaclient/v1_1/security_group_default_rules.py b/novaclient/v1_1/security_group_default_rules.py index 5208cb061..ec9f63aa4 100644 --- a/novaclient/v1_1/security_group_default_rules.py +++ b/novaclient/v1_1/security_group_default_rules.py @@ -16,7 +16,7 @@ from novaclient import base from novaclient import exceptions -from novaclient.openstack.common.gettextutils import _ +from novaclient.i18n import _ class SecurityGroupDefaultRule(base.Resource): diff --git a/novaclient/v1_1/security_group_rules.py b/novaclient/v1_1/security_group_rules.py index f1348920a..1f12a4c1c 100644 --- a/novaclient/v1_1/security_group_rules.py +++ b/novaclient/v1_1/security_group_rules.py @@ -19,7 +19,7 @@ from novaclient import base from novaclient import exceptions -from novaclient.openstack.common.gettextutils import _ +from novaclient.i18n import _ class SecurityGroupRule(base.Resource): diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index 9851012f7..29a4baf30 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -27,7 +27,7 @@ from novaclient import base from novaclient import crypto -from novaclient.openstack.common.gettextutils import _ +from novaclient.i18n import _ from novaclient.v1_1 import security_groups diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index eb5b39a12..1530bd8f9 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -34,8 +34,8 @@ import six from novaclient import exceptions +from novaclient.i18n import _ from novaclient.openstack.common import cliutils -from novaclient.openstack.common.gettextutils import _ from novaclient.openstack.common import uuidutils from novaclient import utils from novaclient.v1_1 import availability_zones diff --git a/novaclient/v3/servers.py b/novaclient/v3/servers.py index c9324c477..ce2c82058 100644 --- a/novaclient/v3/servers.py +++ b/novaclient/v3/servers.py @@ -27,7 +27,8 @@ from novaclient import base from novaclient import crypto -from novaclient.openstack.common.gettextutils import _ +from novaclient.i18n import _ + REBOOT_SOFT, REBOOT_HARD = 'SOFT', 'HARD' diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 957491543..315303b1d 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -32,8 +32,8 @@ import six from novaclient import exceptions +from novaclient.i18n import _ from novaclient.openstack.common import cliutils -from novaclient.openstack.common.gettextutils import _ from novaclient.openstack.common import uuidutils from novaclient import utils from novaclient.v3 import availability_zones diff --git a/openstack-common.conf b/openstack-common.conf index 3af93417e..d303f591a 100644 --- a/openstack-common.conf +++ b/openstack-common.conf @@ -3,7 +3,6 @@ # The list of modules to copy from oslo-incubator module=apiclient module=cliutils -module=gettextutils module=install_venv_common module=uuidutils diff --git a/requirements.txt b/requirements.txt index 2a1c48ae4..5b897b0fb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,7 @@ pbr>=0.6,!=0.7,<1.0 argparse iso8601>=0.1.9 +oslo.i18n>=1.0.0 # Apache-2.0 oslo.serialization>=1.0.0 # Apache-2.0 oslo.utils>=1.0.0 # Apache-2.0 PrettyTable>=0.7,<0.8 diff --git a/tox.ini b/tox.ini index b38ae1c4a..28c6542cf 100644 --- a/tox.ini +++ b/tox.ini @@ -48,4 +48,4 @@ show-source = True exclude=.venv,.git,.tox,dist,*openstack/common*,*lib/python*,*egg,build,doc/source/conf.py [hacking] -import_exceptions = novaclient.openstack.common.gettextutils +import_exceptions = novaclient.i18n From f9ac34d9e8cea7345f948507b17a44a740937395 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Mon, 13 Oct 2014 16:45:34 +0300 Subject: [PATCH 0626/1705] Sync latest code Modules from oslo-incubator already switched to use graduated libraries (oslo.i18n, oslo.serialization and oslo.utils), so we should sync latest code and remove outdated modules (gettextutils, importutils, strutils). NOTE: - module novaclient.openstack.common._i18n should be used only in common code - module novaclient.i18n designed for usage in novaclient Latest commit in oslo-incubator: 21985931a95981eabf1030ead60ae4d210bcd9c0 Synced changes for tools/install_venv_common.py: fe3389e Improve help strings Other synced changes: 55ca7c3 Split cliutils 5d40e14 Remove code that moved to oslo.i18n 3edbfb3 remove caching param from prettytable call 6ff6b4b Switch oslo-incubator to use oslo.utils and remove old modules 9eee5d0 Merge "Delete the token and endpoint on expiry of token of client" f76f44c Delete the token and endpoint on expiry of token of client ed0ffb8 Do not incur the cost of a second method call cf449e2 Fix response_key parameter usage in BaseManager d73f3b1 Remove unused/mutable default args e7d26a6 Merge "Centralize bash-completion in Novaclient" b4e098e Merge "Handle non-openstack errors gracefully" 5e00685 Centralize bash-completion in Novaclient 4ef0193 Handle non-openstack errors gracefully ac995be Fix E126 pep8 errors de4adbc pep8: fixed multiple violations Change-Id: I5f3015ebcbc665a2089ec88629e15ba336aee504 --- novaclient/openstack/common/__init__.py | 17 - novaclient/openstack/common/_i18n.py | 40 ++ novaclient/openstack/common/apiclient/base.py | 51 +- .../openstack/common/apiclient/client.py | 13 +- .../openstack/common/apiclient/exceptions.py | 14 +- .../openstack/common/apiclient/fake_client.py | 4 +- .../openstack/common/apiclient/utils.py | 87 +++ novaclient/openstack/common/cliutils.py | 111 ++-- novaclient/openstack/common/gettextutils.py | 498 ------------------ novaclient/openstack/common/importutils.py | 73 --- novaclient/openstack/common/strutils.py | 245 --------- tools/install_venv_common.py | 2 +- 12 files changed, 196 insertions(+), 959 deletions(-) create mode 100644 novaclient/openstack/common/_i18n.py create mode 100644 novaclient/openstack/common/apiclient/utils.py delete mode 100644 novaclient/openstack/common/gettextutils.py delete mode 100644 novaclient/openstack/common/importutils.py delete mode 100644 novaclient/openstack/common/strutils.py diff --git a/novaclient/openstack/common/__init__.py b/novaclient/openstack/common/__init__.py index d1223eaf7..e69de29bb 100644 --- a/novaclient/openstack/common/__init__.py +++ b/novaclient/openstack/common/__init__.py @@ -1,17 +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. - -import six - - -six.add_move(six.MovedModule('mox', 'mox', 'mox3.mox')) diff --git a/novaclient/openstack/common/_i18n.py b/novaclient/openstack/common/_i18n.py new file mode 100644 index 000000000..6ae2ff239 --- /dev/null +++ b/novaclient/openstack/common/_i18n.py @@ -0,0 +1,40 @@ +# 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. + +"""oslo.i18n integration module. + +See http://docs.openstack.org/developer/oslo.i18n/usage.html + +""" + +import oslo.i18n + + +# NOTE(dhellmann): This reference to o-s-l-o will be replaced by the +# application name when this module is synced into the separate +# repository. It is OK to have more than one translation function +# using the same domain, since there will still only be one message +# catalog. +_translators = oslo.i18n.TranslatorFactory(domain='novaclient') + +# The primary translation function using the well-known name "_" +_ = _translators.primary + +# Translators for log levels. +# +# The abbreviated names are meant to reflect the usual use of a short +# name like '_'. The "L" is for "log" and the other letter comes from +# the level. +_LI = _translators.log_info +_LW = _translators.log_warning +_LE = _translators.log_error +_LC = _translators.log_critical diff --git a/novaclient/openstack/common/apiclient/base.py b/novaclient/openstack/common/apiclient/base.py index 8380ba397..cc5c8b005 100644 --- a/novaclient/openstack/common/apiclient/base.py +++ b/novaclient/openstack/common/apiclient/base.py @@ -26,13 +26,12 @@ import abc import copy +from oslo.utils import strutils import six from six.moves.urllib import parse +from novaclient.openstack.common._i18n import _ from novaclient.openstack.common.apiclient import exceptions -from novaclient.openstack.common.gettextutils import _ -from novaclient.openstack.common import strutils -from novaclient.openstack.common import uuidutils def getid(obj): @@ -100,12 +99,13 @@ def __init__(self, client): super(BaseManager, self).__init__() self.client = client - def _list(self, url, response_key, obj_class=None, json=None): + def _list(self, url, response_key=None, obj_class=None, json=None): """List the collection. :param url: a partial URL, e.g., '/servers' :param response_key: the key to be looked up in response dictionary, - e.g., 'servers' + e.g., 'servers'. If response_key is None - all response body + will be used. :param obj_class: class for constructing the returned objects (self.resource_class will be used by default) :param json: data that will be encoded as JSON and passed in POST @@ -119,7 +119,7 @@ def _list(self, url, response_key, obj_class=None, json=None): if obj_class is None: obj_class = self.resource_class - data = body[response_key] + data = body[response_key] if response_key is not None else body # NOTE(ja): keystone returns values as list as {'values': [ ... ]} # unlike other services which just return the list... try: @@ -129,15 +129,17 @@ def _list(self, url, response_key, obj_class=None, json=None): return [obj_class(self, res, loaded=True) for res in data if res] - def _get(self, url, response_key): + def _get(self, url, response_key=None): """Get an object from collection. :param url: a partial URL, e.g., '/servers' :param response_key: the key to be looked up in response dictionary, - e.g., 'server' + e.g., 'server'. If response_key is None - all response body + will be used. """ body = self.client.get(url).json() - return self.resource_class(self, body[response_key], loaded=True) + data = body[response_key] if response_key is not None else body + return self.resource_class(self, data, loaded=True) def _head(self, url): """Retrieve request headers for an object. @@ -147,21 +149,23 @@ def _head(self, url): resp = self.client.head(url) return resp.status_code == 204 - def _post(self, url, json, response_key, return_raw=False): + def _post(self, url, json, response_key=None, return_raw=False): """Create an object. :param url: a partial URL, e.g., '/servers' :param json: data that will be encoded as JSON and passed in POST request (GET will be sent by default) :param response_key: the key to be looked up in response dictionary, - e.g., 'servers' + e.g., 'server'. If response_key is None - all response body + will be used. :param return_raw: flag to force returning raw JSON instead of Python object of self.resource_class """ body = self.client.post(url, json=json).json() + data = body[response_key] if response_key is not None else body if return_raw: - return body[response_key] - return self.resource_class(self, body[response_key]) + return data + return self.resource_class(self, data) def _put(self, url, json=None, response_key=None): """Update an object with PUT method. @@ -170,7 +174,8 @@ def _put(self, url, json=None, response_key=None): :param json: data that will be encoded as JSON and passed in POST request (GET will be sent by default) :param response_key: the key to be looked up in response dictionary, - e.g., 'servers' + e.g., 'servers'. If response_key is None - all response body + will be used. """ resp = self.client.put(url, json=json) # PUT requests may not return a body @@ -188,7 +193,8 @@ def _patch(self, url, json=None, response_key=None): :param json: data that will be encoded as JSON and passed in POST request (GET will be sent by default) :param response_key: the key to be looked up in response dictionary, - e.g., 'servers' + e.g., 'servers'. If response_key is None - all response body + will be used. """ body = self.client.patch(url, json=json).json() if response_key is not None: @@ -437,21 +443,6 @@ def __init__(self, manager, info, loaded=False): self._info = info self._add_details(info) self._loaded = loaded - self._init_completion_cache() - - def _init_completion_cache(self): - cache_write = getattr(self.manager, 'write_to_completion_cache', None) - if not cache_write: - return - - # NOTE(sirp): ensure `id` is already present because if it isn't we'll - # enter an infinite loop of __getattr__ -> get -> __init__ -> - # __getattr__ -> ... - if 'id' in self.__dict__ and uuidutils.is_uuid_like(self.id): - cache_write('uuid', self.id) - - if self.human_id: - cache_write('human_id', self.human_id) def __repr__(self): reprkeys = sorted(k diff --git a/novaclient/openstack/common/apiclient/client.py b/novaclient/openstack/common/apiclient/client.py index a3666daf0..7780cea91 100644 --- a/novaclient/openstack/common/apiclient/client.py +++ b/novaclient/openstack/common/apiclient/client.py @@ -33,11 +33,11 @@ except ImportError: import json +from oslo.utils import importutils import requests +from novaclient.openstack.common._i18n import _ from novaclient.openstack.common.apiclient import exceptions -from novaclient.openstack.common.gettextutils import _ -from novaclient.openstack.common import importutils _logger = logging.getLogger(__name__) @@ -156,7 +156,7 @@ def request(self, method, url, **kwargs): requests.Session.request (such as `headers`) or `json` that will be encoded as JSON and used as `data` argument """ - kwargs.setdefault("headers", kwargs.get("headers", {})) + kwargs.setdefault("headers", {}) kwargs["headers"]["User-Agent"] = self.user_agent if self.original_ip: kwargs["headers"]["Forwarded"] = "for=%s;by=%s" % ( @@ -247,6 +247,10 @@ def client_request(self, client, method, url, **kwargs): raise self.cached_token = None client.cached_endpoint = None + if self.auth_plugin.opts.get('token'): + self.auth_plugin.opts['token'] = None + if self.auth_plugin.opts.get('endpoint'): + self.auth_plugin.opts['endpoint'] = None self.authenticate() try: token, endpoint = self.auth_plugin.token_and_endpoint( @@ -357,8 +361,7 @@ def get_class(api_name, version, version_map): "Must be one of: %(version_map)s") % { 'api_name': api_name, 'version': version, - 'version_map': ', '.join(version_map.keys()) - } + 'version_map': ', '.join(version_map.keys())} raise exceptions.UnsupportedVersion(msg) return importutils.import_class(client_path) diff --git a/novaclient/openstack/common/apiclient/exceptions.py b/novaclient/openstack/common/apiclient/exceptions.py index 8e0d2ad9b..15e885b9b 100644 --- a/novaclient/openstack/common/apiclient/exceptions.py +++ b/novaclient/openstack/common/apiclient/exceptions.py @@ -25,7 +25,7 @@ import six -from novaclient.openstack.common.gettextutils import _ +from novaclient.openstack.common._i18n import _ class ClientException(Exception): @@ -34,14 +34,6 @@ class ClientException(Exception): pass -class MissingArgs(ClientException): - """Supplied arguments are not sufficient for calling a function.""" - def __init__(self, missing): - self.missing = missing - msg = _("Missing arguments: %s") % ", ".join(missing) - super(MissingArgs, self).__init__(msg) - - class ValidationError(ClientException): """Error in validation on API client side.""" pass @@ -447,8 +439,8 @@ def from_response(response, method, url): except ValueError: pass else: - if isinstance(body, dict): - error = list(body.values())[0] + if isinstance(body, dict) and isinstance(body.get("error"), dict): + error = body["error"] kwargs["message"] = error.get("message") kwargs["details"] = error.get("details") elif content_type.startswith("text/"): diff --git a/novaclient/openstack/common/apiclient/fake_client.py b/novaclient/openstack/common/apiclient/fake_client.py index b6670a67b..eeb9b810a 100644 --- a/novaclient/openstack/common/apiclient/fake_client.py +++ b/novaclient/openstack/common/apiclient/fake_client.py @@ -33,7 +33,9 @@ from novaclient.openstack.common.apiclient import client -def assert_has_keys(dct, required=[], optional=[]): +def assert_has_keys(dct, required=None, optional=None): + required = required or [] + optional = optional or [] for k in required: try: assert k in dct diff --git a/novaclient/openstack/common/apiclient/utils.py b/novaclient/openstack/common/apiclient/utils.py new file mode 100644 index 000000000..09ddae43b --- /dev/null +++ b/novaclient/openstack/common/apiclient/utils.py @@ -0,0 +1,87 @@ +# +# 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 oslo.utils import encodeutils +import six + +from novaclient.openstack.common._i18n import _ +from novaclient.openstack.common.apiclient import exceptions +from novaclient.openstack.common import uuidutils + + +def find_resource(manager, name_or_id, **find_args): + """Look for resource in a given manager. + + Used as a helper for the _find_* methods. + Example: + + .. code-block:: python + + def _find_hypervisor(cs, hypervisor): + #Get a hypervisor by name or ID. + return cliutils.find_resource(cs.hypervisors, hypervisor) + """ + # first try to get entity as integer id + try: + return manager.get(int(name_or_id)) + except (TypeError, ValueError, exceptions.NotFound): + pass + + # now try to get entity as uuid + try: + if six.PY2: + tmp_id = encodeutils.safe_encode(name_or_id) + else: + tmp_id = encodeutils.safe_decode(name_or_id) + + if uuidutils.is_uuid_like(tmp_id): + return manager.get(tmp_id) + except (TypeError, ValueError, exceptions.NotFound): + pass + + # for str id which is not uuid + if getattr(manager, 'is_alphanum_id_allowed', False): + try: + return manager.get(name_or_id) + except exceptions.NotFound: + pass + + try: + try: + return manager.find(human_id=name_or_id, **find_args) + except exceptions.NotFound: + pass + + # finally try to find entity by name + try: + resource = getattr(manager, 'resource_class', None) + name_attr = resource.NAME_ATTR if resource else 'name' + kwargs = {name_attr: name_or_id} + kwargs.update(find_args) + return manager.find(**kwargs) + except exceptions.NotFound: + msg = _("No %(name)s with a name or " + "ID of '%(name_or_id)s' exists.") % \ + { + "name": manager.resource_class.__name__.lower(), + "name_or_id": name_or_id + } + raise exceptions.CommandError(msg) + except exceptions.NoUniqueMatch: + msg = _("Multiple %(name)s matches found for " + "'%(name_or_id)s', use an ID to be more specific.") % \ + { + "name": manager.resource_class.__name__.lower(), + "name_or_id": name_or_id + } + raise exceptions.CommandError(msg) diff --git a/novaclient/openstack/common/cliutils.py b/novaclient/openstack/common/cliutils.py index 6a96da57b..ac2cf7573 100644 --- a/novaclient/openstack/common/cliutils.py +++ b/novaclient/openstack/common/cliutils.py @@ -24,14 +24,21 @@ import sys import textwrap +from oslo.utils import encodeutils +from oslo.utils import strutils import prettytable import six from six import moves -from novaclient.openstack.common.apiclient import exceptions -from novaclient.openstack.common.gettextutils import _ -from novaclient.openstack.common import strutils -from novaclient.openstack.common import uuidutils +from novaclient.openstack.common._i18n import _ + + +class MissingArgs(Exception): + """Supplied arguments are not sufficient for calling a function.""" + def __init__(self, missing): + self.missing = missing + msg = _("Missing arguments: %s") % ", ".join(missing) + super(MissingArgs, self).__init__(msg) def validate_args(fn, *args, **kwargs): @@ -56,7 +63,7 @@ def validate_args(fn, *args, **kwargs): required_args = argspec.args[:len(argspec.args) - num_defaults] def isbound(method): - return getattr(method, 'im_self', None) is not None + return getattr(method, '__self__', None) is not None if isbound(fn): required_args.pop(0) @@ -64,7 +71,7 @@ def isbound(method): missing = [arg for arg in required_args if arg not in kwargs] missing = missing[len(args):] if missing: - raise exceptions.MissingArgs(missing) + raise MissingArgs(missing) def arg(*args, **kwargs): @@ -132,7 +139,7 @@ def isunauthenticated(func): def print_list(objs, fields, formatters=None, sortby_index=0, - mixed_case_fields=None): + mixed_case_fields=None, field_labels=None): """Print a list or objects as a table, one row per object. :param objs: iterable of :class:`Resource` @@ -141,14 +148,22 @@ def print_list(objs, fields, formatters=None, sortby_index=0, :param sortby_index: index of the field for sorting table rows :param mixed_case_fields: fields corresponding to object attributes that have mixed case names (e.g., 'serverId') + :param field_labels: Labels to use in the heading of the table, default to + fields. """ formatters = formatters or {} mixed_case_fields = mixed_case_fields or [] + field_labels = field_labels or fields + if len(field_labels) != len(fields): + raise ValueError(_("Field labels list %(labels)s has different number " + "of elements than fields list %(fields)s"), + {'labels': field_labels, 'fields': fields}) + if sortby_index is None: kwargs = {} else: - kwargs = {'sortby': fields[sortby_index]} - pt = prettytable.PrettyTable(fields, caching=False) + kwargs = {'sortby': field_labels[sortby_index]} + pt = prettytable.PrettyTable(field_labels) pt.align = 'l' for o in objs: @@ -165,7 +180,7 @@ def print_list(objs, fields, formatters=None, sortby_index=0, row.append(data) pt.add_row(row) - print(strutils.safe_encode(pt.get_string(**kwargs))) + print(encodeutils.safe_encode(pt.get_string(**kwargs))) def print_dict(dct, dict_property="Property", wrap=0): @@ -175,7 +190,7 @@ def print_dict(dct, dict_property="Property", wrap=0): :param dict_property: name of the first column :param wrap: wrapping for the second column """ - pt = prettytable.PrettyTable([dict_property, 'Value'], caching=False) + pt = prettytable.PrettyTable([dict_property, 'Value']) pt.align = 'l' for k, v in six.iteritems(dct): # convert dict to str to check length @@ -193,7 +208,7 @@ def print_dict(dct, dict_property="Property", wrap=0): col1 = '' else: pt.add_row([k, v]) - print(strutils.safe_encode(pt.get_string())) + print(encodeutils.safe_encode(pt.get_string())) def get_password(max_password_prompts=3): @@ -217,76 +232,16 @@ def get_password(max_password_prompts=3): return pw -def find_resource(manager, name_or_id, **find_args): - """Look for resource in a given manager. - - Used as a helper for the _find_* methods. - Example: - - def _find_hypervisor(cs, hypervisor): - #Get a hypervisor by name or ID. - return cliutils.find_resource(cs.hypervisors, hypervisor) - """ - # first try to get entity as integer id - try: - return manager.get(int(name_or_id)) - except (TypeError, ValueError, exceptions.NotFound): - pass - - # now try to get entity as uuid - try: - tmp_id = strutils.safe_encode(name_or_id) - - if uuidutils.is_uuid_like(tmp_id): - return manager.get(tmp_id) - except (TypeError, ValueError, exceptions.NotFound): - pass - - # for str id which is not uuid - if getattr(manager, 'is_alphanum_id_allowed', False): - try: - return manager.get(name_or_id) - except exceptions.NotFound: - pass - - try: - try: - return manager.find(human_id=name_or_id, **find_args) - except exceptions.NotFound: - pass - - # finally try to find entity by name - try: - resource = getattr(manager, 'resource_class', None) - name_attr = resource.NAME_ATTR if resource else 'name' - kwargs = {name_attr: name_or_id} - kwargs.update(find_args) - return manager.find(**kwargs) - except exceptions.NotFound: - msg = _("No %(name)s with a name or " - "ID of '%(name_or_id)s' exists.") % \ - { - "name": manager.resource_class.__name__.lower(), - "name_or_id": name_or_id - } - raise exceptions.CommandError(msg) - except exceptions.NoUniqueMatch: - msg = _("Multiple %(name)s matches found for " - "'%(name_or_id)s', use an ID to be more specific.") % \ - { - "name": manager.resource_class.__name__.lower(), - "name_or_id": name_or_id - } - raise exceptions.CommandError(msg) - - def service_type(stype): """Adds 'service_type' attribute to decorated function. Usage: - @service_type('volume') - def mymethod(f): - ... + + .. code-block:: python + + @service_type('volume') + def mymethod(f): + ... """ def inner(f): f.service_type = stype diff --git a/novaclient/openstack/common/gettextutils.py b/novaclient/openstack/common/gettextutils.py deleted file mode 100644 index a48a72812..000000000 --- a/novaclient/openstack/common/gettextutils.py +++ /dev/null @@ -1,498 +0,0 @@ -# Copyright 2012 Red Hat, Inc. -# Copyright 2013 IBM Corp. -# All Rights Reserved. -# -# 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. - -""" -gettext for openstack-common modules. - -Usual usage in an openstack.common module: - - from novaclient.openstack.common.gettextutils import _ -""" - -import copy -import functools -import gettext -import locale -from logging import handlers -import os - -from babel import localedata -import six - -_AVAILABLE_LANGUAGES = {} - -# FIXME(dhellmann): Remove this when moving to oslo.i18n. -USE_LAZY = False - - -class TranslatorFactory(object): - """Create translator functions - """ - - def __init__(self, domain, lazy=False, localedir=None): - """Establish a set of translation functions for the domain. - - :param domain: Name of translation domain, - specifying a message catalog. - :type domain: str - :param lazy: Delays translation until a message is emitted. - Defaults to False. - :type lazy: Boolean - :param localedir: Directory with translation catalogs. - :type localedir: str - """ - self.domain = domain - self.lazy = lazy - if localedir is None: - localedir = os.environ.get(domain.upper() + '_LOCALEDIR') - self.localedir = localedir - - def _make_translation_func(self, domain=None): - """Return a new translation function ready for use. - - Takes into account whether or not lazy translation is being - done. - - The domain can be specified to override the default from the - factory, but the localedir from the factory is always used - because we assume the log-level translation catalogs are - installed in the same directory as the main application - catalog. - - """ - if domain is None: - domain = self.domain - if self.lazy: - return functools.partial(Message, domain=domain) - t = gettext.translation( - domain, - localedir=self.localedir, - fallback=True, - ) - if six.PY3: - return t.gettext - return t.ugettext - - @property - def primary(self): - "The default translation function." - return self._make_translation_func() - - def _make_log_translation_func(self, level): - return self._make_translation_func(self.domain + '-log-' + level) - - @property - def log_info(self): - "Translate info-level log messages." - return self._make_log_translation_func('info') - - @property - def log_warning(self): - "Translate warning-level log messages." - return self._make_log_translation_func('warning') - - @property - def log_error(self): - "Translate error-level log messages." - return self._make_log_translation_func('error') - - @property - def log_critical(self): - "Translate critical-level log messages." - return self._make_log_translation_func('critical') - - -# NOTE(dhellmann): When this module moves out of the incubator into -# oslo.i18n, these global variables can be moved to an integration -# module within each application. - -# Create the global translation functions. -_translators = TranslatorFactory('novaclient') - -# The primary translation function using the well-known name "_" -_ = _translators.primary - -# Translators for log levels. -# -# The abbreviated names are meant to reflect the usual use of a short -# name like '_'. The "L" is for "log" and the other letter comes from -# the level. -_LI = _translators.log_info -_LW = _translators.log_warning -_LE = _translators.log_error -_LC = _translators.log_critical - -# NOTE(dhellmann): End of globals that will move to the application's -# integration module. - - -def enable_lazy(): - """Convenience function for configuring _() to use lazy gettext - - Call this at the start of execution to enable the gettextutils._ - function to use lazy gettext functionality. This is useful if - your project is importing _ directly instead of using the - gettextutils.install() way of importing the _ function. - """ - # FIXME(dhellmann): This function will be removed in oslo.i18n, - # because the TranslatorFactory makes it superfluous. - global _, _LI, _LW, _LE, _LC, USE_LAZY - tf = TranslatorFactory('novaclient', lazy=True) - _ = tf.primary - _LI = tf.log_info - _LW = tf.log_warning - _LE = tf.log_error - _LC = tf.log_critical - USE_LAZY = True - - -def install(domain, lazy=False): - """Install a _() function using the given translation domain. - - Given a translation domain, install a _() function using gettext's - install() function. - - The main difference from gettext.install() is that we allow - overriding the default localedir (e.g. /usr/share/locale) using - a translation-domain-specific environment variable (e.g. - NOVA_LOCALEDIR). - - :param domain: the translation domain - :param lazy: indicates whether or not to install the lazy _() function. - The lazy _() introduces a way to do deferred translation - of messages by installing a _ that builds Message objects, - instead of strings, which can then be lazily translated into - any available locale. - """ - if lazy: - from six import moves - tf = TranslatorFactory(domain, lazy=True) - moves.builtins.__dict__['_'] = tf.primary - else: - localedir = '%s_LOCALEDIR' % domain.upper() - if six.PY3: - gettext.install(domain, - localedir=os.environ.get(localedir)) - else: - gettext.install(domain, - localedir=os.environ.get(localedir), - unicode=True) - - -class Message(six.text_type): - """A Message object is a unicode object that can be translated. - - Translation of Message is done explicitly using the translate() method. - For all non-translation intents and purposes, a Message is simply unicode, - and can be treated as such. - """ - - def __new__(cls, msgid, msgtext=None, params=None, - domain='novaclient', *args): - """Create a new Message object. - - In order for translation to work gettext requires a message ID, this - msgid will be used as the base unicode text. It is also possible - for the msgid and the base unicode text to be different by passing - the msgtext parameter. - """ - # If the base msgtext is not given, we use the default translation - # of the msgid (which is in English) just in case the system locale is - # not English, so that the base text will be in that locale by default. - if not msgtext: - msgtext = Message._translate_msgid(msgid, domain) - # We want to initialize the parent unicode with the actual object that - # would have been plain unicode if 'Message' was not enabled. - msg = super(Message, cls).__new__(cls, msgtext) - msg.msgid = msgid - msg.domain = domain - msg.params = params - return msg - - def translate(self, desired_locale=None): - """Translate this message to the desired locale. - - :param desired_locale: The desired locale to translate the message to, - if no locale is provided the message will be - translated to the system's default locale. - - :returns: the translated message in unicode - """ - - translated_message = Message._translate_msgid(self.msgid, - self.domain, - desired_locale) - if self.params is None: - # No need for more translation - return translated_message - - # This Message object may have been formatted with one or more - # Message objects as substitution arguments, given either as a single - # argument, part of a tuple, or as one or more values in a dictionary. - # When translating this Message we need to translate those Messages too - translated_params = _translate_args(self.params, desired_locale) - - translated_message = translated_message % translated_params - - return translated_message - - @staticmethod - def _translate_msgid(msgid, domain, desired_locale=None): - if not desired_locale: - system_locale = locale.getdefaultlocale() - # If the system locale is not available to the runtime use English - if not system_locale[0]: - desired_locale = 'en_US' - else: - desired_locale = system_locale[0] - - locale_dir = os.environ.get(domain.upper() + '_LOCALEDIR') - lang = gettext.translation(domain, - localedir=locale_dir, - languages=[desired_locale], - fallback=True) - if six.PY3: - translator = lang.gettext - else: - translator = lang.ugettext - - translated_message = translator(msgid) - return translated_message - - def __mod__(self, other): - # When we mod a Message we want the actual operation to be performed - # by the parent class (i.e. unicode()), the only thing we do here is - # save the original msgid and the parameters in case of a translation - params = self._sanitize_mod_params(other) - unicode_mod = super(Message, self).__mod__(params) - modded = Message(self.msgid, - msgtext=unicode_mod, - params=params, - domain=self.domain) - return modded - - def _sanitize_mod_params(self, other): - """Sanitize the object being modded with this Message. - - - Add support for modding 'None' so translation supports it - - Trim the modded object, which can be a large dictionary, to only - those keys that would actually be used in a translation - - Snapshot the object being modded, in case the message is - translated, it will be used as it was when the Message was created - """ - if other is None: - params = (other,) - elif isinstance(other, dict): - # Merge the dictionaries - # Copy each item in case one does not support deep copy. - params = {} - if isinstance(self.params, dict): - for key, val in self.params.items(): - params[key] = self._copy_param(val) - for key, val in other.items(): - params[key] = self._copy_param(val) - else: - params = self._copy_param(other) - return params - - def _copy_param(self, param): - try: - return copy.deepcopy(param) - except Exception: - # Fallback to casting to unicode this will handle the - # python code-like objects that can't be deep-copied - return six.text_type(param) - - def __add__(self, other): - msg = _('Message objects do not support addition.') - raise TypeError(msg) - - def __radd__(self, other): - return self.__add__(other) - - if six.PY2: - def __str__(self): - # NOTE(luisg): Logging in python 2.6 tries to str() log records, - # and it expects specifically a UnicodeError in order to proceed. - msg = _('Message objects do not support str() because they may ' - 'contain non-ascii characters. ' - 'Please use unicode() or translate() instead.') - raise UnicodeError(msg) - - -def get_available_languages(domain): - """Lists the available languages for the given translation domain. - - :param domain: the domain to get languages for - """ - if domain in _AVAILABLE_LANGUAGES: - return copy.copy(_AVAILABLE_LANGUAGES[domain]) - - localedir = '%s_LOCALEDIR' % domain.upper() - find = lambda x: gettext.find(domain, - localedir=os.environ.get(localedir), - languages=[x]) - - # NOTE(mrodden): en_US should always be available (and first in case - # order matters) since our in-line message strings are en_US - language_list = ['en_US'] - # NOTE(luisg): Babel <1.0 used a function called list(), which was - # renamed to locale_identifiers() in >=1.0, the requirements master list - # requires >=0.9.6, uncapped, so defensively work with both. We can remove - # this check when the master list updates to >=1.0, and update all projects - list_identifiers = (getattr(localedata, 'list', None) or - getattr(localedata, 'locale_identifiers')) - locale_identifiers = list_identifiers() - - for i in locale_identifiers: - if find(i) is not None: - language_list.append(i) - - # NOTE(luisg): Babel>=1.0,<1.3 has a bug where some OpenStack supported - # locales (e.g. 'zh_CN', and 'zh_TW') aren't supported even though they - # are perfectly legitimate locales: - # https://github.com/mitsuhiko/babel/issues/37 - # In Babel 1.3 they fixed the bug and they support these locales, but - # they are still not explicitly "listed" by locale_identifiers(). - # That is why we add the locales here explicitly if necessary so that - # they are listed as supported. - aliases = {'zh': 'zh_CN', - 'zh_Hant_HK': 'zh_HK', - 'zh_Hant': 'zh_TW', - 'fil': 'tl_PH'} - for (locale_, alias) in six.iteritems(aliases): - if locale_ in language_list and alias not in language_list: - language_list.append(alias) - - _AVAILABLE_LANGUAGES[domain] = language_list - return copy.copy(language_list) - - -def translate(obj, desired_locale=None): - """Gets the translated unicode representation of the given object. - - If the object is not translatable it is returned as-is. - If the locale is None the object is translated to the system locale. - - :param obj: the object to translate - :param desired_locale: the locale to translate the message to, if None the - default system locale will be used - :returns: the translated object in unicode, or the original object if - it could not be translated - """ - message = obj - if not isinstance(message, Message): - # If the object to translate is not already translatable, - # let's first get its unicode representation - message = six.text_type(obj) - if isinstance(message, Message): - # Even after unicoding() we still need to check if we are - # running with translatable unicode before translating - return message.translate(desired_locale) - return obj - - -def _translate_args(args, desired_locale=None): - """Translates all the translatable elements of the given arguments object. - - This method is used for translating the translatable values in method - arguments which include values of tuples or dictionaries. - If the object is not a tuple or a dictionary the object itself is - translated if it is translatable. - - If the locale is None the object is translated to the system locale. - - :param args: the args to translate - :param desired_locale: the locale to translate the args to, if None the - default system locale will be used - :returns: a new args object with the translated contents of the original - """ - if isinstance(args, tuple): - return tuple(translate(v, desired_locale) for v in args) - if isinstance(args, dict): - translated_dict = {} - for (k, v) in six.iteritems(args): - translated_v = translate(v, desired_locale) - translated_dict[k] = translated_v - return translated_dict - return translate(args, desired_locale) - - -class TranslationHandler(handlers.MemoryHandler): - """Handler that translates records before logging them. - - The TranslationHandler takes a locale and a target logging.Handler object - to forward LogRecord objects to after translating them. This handler - depends on Message objects being logged, instead of regular strings. - - The handler can be configured declaratively in the logging.conf as follows: - - [handlers] - keys = translatedlog, translator - - [handler_translatedlog] - class = handlers.WatchedFileHandler - args = ('/var/log/api-localized.log',) - formatter = context - - [handler_translator] - class = openstack.common.log.TranslationHandler - target = translatedlog - args = ('zh_CN',) - - If the specified locale is not available in the system, the handler will - log in the default locale. - """ - - def __init__(self, locale=None, target=None): - """Initialize a TranslationHandler - - :param locale: locale to use for translating messages - :param target: logging.Handler object to forward - LogRecord objects to after translation - """ - # NOTE(luisg): In order to allow this handler to be a wrapper for - # other handlers, such as a FileHandler, and still be able to - # configure it using logging.conf, this handler has to extend - # MemoryHandler because only the MemoryHandlers' logging.conf - # parsing is implemented such that it accepts a target handler. - handlers.MemoryHandler.__init__(self, capacity=0, target=target) - self.locale = locale - - def setFormatter(self, fmt): - self.target.setFormatter(fmt) - - def emit(self, record): - # We save the message from the original record to restore it - # after translation, so other handlers are not affected by this - original_msg = record.msg - original_args = record.args - - try: - self._translate_and_log_record(record) - finally: - record.msg = original_msg - record.args = original_args - - def _translate_and_log_record(self, record): - record.msg = translate(record.msg, self.locale) - - # In addition to translating the message, we also need to translate - # arguments that were passed to the log method that were not part - # of the main message e.g., log.info(_('Some message %s'), this_one)) - record.args = _translate_args(record.args, self.locale) - - self.target.emit(record) diff --git a/novaclient/openstack/common/importutils.py b/novaclient/openstack/common/importutils.py deleted file mode 100644 index 863255db3..000000000 --- a/novaclient/openstack/common/importutils.py +++ /dev/null @@ -1,73 +0,0 @@ -# Copyright 2011 OpenStack Foundation. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Import related utilities and helper functions. -""" - -import sys -import traceback - - -def import_class(import_str): - """Returns a class from a string including module and class.""" - mod_str, _sep, class_str = import_str.rpartition('.') - __import__(mod_str) - try: - return getattr(sys.modules[mod_str], class_str) - except AttributeError: - raise ImportError('Class %s cannot be found (%s)' % - (class_str, - traceback.format_exception(*sys.exc_info()))) - - -def import_object(import_str, *args, **kwargs): - """Import a class and return an instance of it.""" - return import_class(import_str)(*args, **kwargs) - - -def import_object_ns(name_space, import_str, *args, **kwargs): - """Tries to import object from default namespace. - - Imports a class and return an instance of it, first by trying - to find the class in a default namespace, then failing back to - a full path if not found in the default namespace. - """ - import_value = "%s.%s" % (name_space, import_str) - try: - return import_class(import_value)(*args, **kwargs) - except ImportError: - return import_class(import_str)(*args, **kwargs) - - -def import_module(import_str): - """Import a module.""" - __import__(import_str) - return sys.modules[import_str] - - -def import_versioned_module(version, submodule=None): - module = 'novaclient.v%s' % version - if submodule: - module = '.'.join((module, submodule)) - return import_module(module) - - -def try_import(import_str, default=None): - """Try to import a module and if it fails return default.""" - try: - return import_module(import_str) - except ImportError: - return default diff --git a/novaclient/openstack/common/strutils.py b/novaclient/openstack/common/strutils.py deleted file mode 100644 index c22063d90..000000000 --- a/novaclient/openstack/common/strutils.py +++ /dev/null @@ -1,245 +0,0 @@ -# Copyright 2011 OpenStack Foundation. -# All Rights Reserved. -# -# 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. - -""" -System-level utilities and helper functions. -""" - -import math -import re -import sys -import unicodedata - -import six - -from novaclient.openstack.common.gettextutils import _ - - -UNIT_PREFIX_EXPONENT = { - 'k': 1, - 'K': 1, - 'Ki': 1, - 'M': 2, - 'Mi': 2, - 'G': 3, - 'Gi': 3, - 'T': 4, - 'Ti': 4, -} -UNIT_SYSTEM_INFO = { - 'IEC': (1024, re.compile(r'(^[-+]?\d*\.?\d+)([KMGT]i?)?(b|bit|B)$')), - 'SI': (1000, re.compile(r'(^[-+]?\d*\.?\d+)([kMGT])?(b|bit|B)$')), -} - -TRUE_STRINGS = ('1', 't', 'true', 'on', 'y', 'yes') -FALSE_STRINGS = ('0', 'f', 'false', 'off', 'n', 'no') - -SLUGIFY_STRIP_RE = re.compile(r"[^\w\s-]") -SLUGIFY_HYPHENATE_RE = re.compile(r"[-\s]+") - - -def int_from_bool_as_string(subject): - """Interpret a string as a boolean and return either 1 or 0. - - Any string value in: - - ('True', 'true', 'On', 'on', '1') - - is interpreted as a boolean True. - - Useful for JSON-decoded stuff and config file parsing - """ - return bool_from_string(subject) and 1 or 0 - - -def bool_from_string(subject, strict=False, default=False): - """Interpret a string as a boolean. - - A case-insensitive match is performed such that strings matching 't', - 'true', 'on', 'y', 'yes', or '1' are considered True and, when - `strict=False`, anything else returns the value specified by 'default'. - - Useful for JSON-decoded stuff and config file parsing. - - If `strict=True`, unrecognized values, including None, will raise a - ValueError which is useful when parsing values passed in from an API call. - Strings yielding False are 'f', 'false', 'off', 'n', 'no', or '0'. - """ - if not isinstance(subject, six.string_types): - subject = str(subject) - - lowered = subject.strip().lower() - - if lowered in TRUE_STRINGS: - return True - elif lowered in FALSE_STRINGS: - return False - elif strict: - acceptable = ', '.join( - "'%s'" % s for s in sorted(TRUE_STRINGS + FALSE_STRINGS)) - msg = _("Unrecognized value '%(val)s', acceptable values are:" - " %(acceptable)s") % {'val': subject, - 'acceptable': acceptable} - raise ValueError(msg) - else: - return default - - -def safe_decode(text, incoming=None, errors='strict'): - """Decodes incoming text/bytes string using `incoming` if they're not - already unicode. - - :param incoming: Text's current encoding - :param errors: Errors handling policy. See here for valid - values http://docs.python.org/2/library/codecs.html - :returns: text or a unicode `incoming` encoded - representation of it. - :raises TypeError: If text is not an instance of str - """ - if not isinstance(text, (six.string_types, six.binary_type)): - raise TypeError("%s can't be decoded" % type(text)) - - if isinstance(text, six.text_type): - return text - - if not incoming: - incoming = (sys.stdin.encoding or - sys.getdefaultencoding()) - - try: - return text.decode(incoming, errors) - except UnicodeDecodeError: - # Note(flaper87) If we get here, it means that - # sys.stdin.encoding / sys.getdefaultencoding - # didn't return a suitable encoding to decode - # text. This happens mostly when global LANG - # var is not set correctly and there's no - # default encoding. In this case, most likely - # python will use ASCII or ANSI encoders as - # default encodings but they won't be capable - # of decoding non-ASCII characters. - # - # Also, UTF-8 is being used since it's an ASCII - # extension. - return text.decode('utf-8', errors) - - -def safe_encode(text, incoming=None, - encoding='utf-8', errors='strict'): - """Encodes incoming text/bytes string using `encoding`. - - If incoming is not specified, text is expected to be encoded with - current python's default encoding. (`sys.getdefaultencoding`) - - :param incoming: Text's current encoding - :param encoding: Expected encoding for text (Default UTF-8) - :param errors: Errors handling policy. See here for valid - values http://docs.python.org/2/library/codecs.html - :returns: text or a bytestring `encoding` encoded - representation of it. - :raises TypeError: If text is not an instance of str - """ - if not isinstance(text, (six.string_types, six.binary_type)): - raise TypeError("%s can't be encoded" % type(text)) - - if not incoming: - incoming = (sys.stdin.encoding or - sys.getdefaultencoding()) - - if isinstance(text, six.text_type): - if six.PY3: - return text.encode(encoding, errors).decode(incoming) - else: - return text.encode(encoding, errors) - elif text and encoding != incoming: - # Decode text before encoding it with `encoding` - text = safe_decode(text, incoming, errors) - if six.PY3: - return text.encode(encoding, errors).decode(incoming) - else: - return text.encode(encoding, errors) - - return text - - -def string_to_bytes(text, unit_system='IEC', return_int=False): - """Converts a string into an float representation of bytes. - - The units supported for IEC :: - - Kb(it), Kib(it), Mb(it), Mib(it), Gb(it), Gib(it), Tb(it), Tib(it) - KB, KiB, MB, MiB, GB, GiB, TB, TiB - - The units supported for SI :: - - kb(it), Mb(it), Gb(it), Tb(it) - kB, MB, GB, TB - - Note that the SI unit system does not support capital letter 'K' - - :param text: String input for bytes size conversion. - :param unit_system: Unit system for byte size conversion. - :param return_int: If True, returns integer representation of text - in bytes. (default: decimal) - :returns: Numerical representation of text in bytes. - :raises ValueError: If text has an invalid value. - - """ - try: - base, reg_ex = UNIT_SYSTEM_INFO[unit_system] - except KeyError: - msg = _('Invalid unit system: "%s"') % unit_system - raise ValueError(msg) - match = reg_ex.match(text) - if match: - magnitude = float(match.group(1)) - unit_prefix = match.group(2) - if match.group(3) in ['b', 'bit']: - magnitude /= 8 - else: - msg = _('Invalid string format: %s') % text - raise ValueError(msg) - if not unit_prefix: - res = magnitude - else: - res = magnitude * pow(base, UNIT_PREFIX_EXPONENT[unit_prefix]) - if return_int: - return int(math.ceil(res)) - return res - - -def to_slug(value, incoming=None, errors="strict"): - """Normalize string. - - Convert to lowercase, remove non-word characters, and convert spaces - to hyphens. - - Inspired by Django's `slugify` filter. - - :param value: Text to slugify - :param incoming: Text's current encoding - :param errors: Errors handling policy. See here for valid - values http://docs.python.org/2/library/codecs.html - :returns: slugified unicode representation of `value` - :raises TypeError: If text is not an instance of str - """ - value = safe_decode(value, incoming, errors) - # NOTE(aababilov): no need to use safe_(encode|decode) here: - # encodings are always "ascii", error handling is always "ignore" - # and types are always known (first: unicode; second: str) - value = unicodedata.normalize("NFKD", value).encode( - "ascii", "ignore").decode("ascii") - value = SLUGIFY_STRIP_RE.sub("", value).strip().lower() - return SLUGIFY_HYPHENATE_RE.sub("-", value) diff --git a/tools/install_venv_common.py b/tools/install_venv_common.py index 46822e329..e279159ab 100644 --- a/tools/install_venv_common.py +++ b/tools/install_venv_common.py @@ -125,7 +125,7 @@ def parse_args(self, argv): parser.add_option('-n', '--no-site-packages', action='store_true', help="Do not inherit packages from global Python " - "install") + "install.") return parser.parse_args(argv[1:])[0] From 2d6047ffb4b9ffc9fe843d3f273b6214e48e2970 Mon Sep 17 00:00:00 2001 From: Amandeep Date: Tue, 30 Sep 2014 17:45:50 +0530 Subject: [PATCH 0627/1705] no way to delete valid/invalid security rule Previously deleting valid/invalid security group rules was throwing the error of "AttributeError". With this fix, now both types of security group rules can be deleted. Change-Id: I253c2742efe69ee41976c88ca2775189bc5bde0d Closes-Bug: #1163469 --- novaclient/tests/v1_1/fakes.py | 19 +++++++++++++++++++ novaclient/tests/v1_1/test_shell.py | 12 ++++++++++-- novaclient/v1_1/shell.py | 4 ++-- 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index 929ad2082..6276965b1 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -1279,11 +1279,27 @@ def get_os_security_groups(self, **kw): "from_port": 222, "to_port": 222, "parent_group_id": 1, + "ip_range": {}}, + {"id": 14, + "group": { + "tenant_id": + "272bee4c1e624cd4a72a6b0ea55b4582", + "name": "test4"}, + + "ip_protocol": "TCP", + "from_port": -1, + "to_port": -1, + "parent_group_id": 1, "ip_range": {}}]}, {"name": "test2", "description": "FAKE_SECURITY_GROUP2", "tenant_id": "272bee4c1e624cd4a72a6b0ea55b4582", "id": 2, + "rules": []}, + {"name": "test4", + "description": "FAKE_SECURITY_GROUP4", + "tenant_id": "272bee4c1e624cd4a72a6b0ea55b4582", + "id": 4, "rules": []} ]}) @@ -1328,6 +1344,9 @@ def delete_os_security_group_rules_11(self, **kw): def delete_os_security_group_rules_12(self, **kw): return (202, {}, None) + def delete_os_security_group_rules_14(self, **kw): + return (202, {}, None) + def post_os_security_group_rules(self, body, **kw): assert list(body) == ['security_group_rule'] fakes.assert_has_keys(body['security_group_rule'], diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index 733c38931..de6f35bf5 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -1998,14 +1998,22 @@ def test_security_group_add_group_rule(self): 'cidr': None, 'group_id': 2}}) - def test_security_group_delete_group_rule(self): + def test_security_group_delete_valid_group_rule(self): self.run_command('secgroup-delete-group-rule test test2 TCP 222 222') self.assert_called('DELETE', '/os-security-group-rules/12') - def test_security_group_delete_group_rule_protocol_case(self): + def test_security_group_delete_valid_group_rule_protocol_case(self): self.run_command('secgroup-delete-group-rule test test2 tcp 222 222') self.assert_called('DELETE', '/os-security-group-rules/12') + def test_security_group_delete_invalid_group_rule(self): + self.run_command('secgroup-delete-group-rule test test4 TCP -1 -1') + self.assert_called('DELETE', '/os-security-group-rules/14') + + def test_security_group_delete_invalid_group_rule_protocol_case(self): + self.run_command('secgroup-delete-group-rule test test4 tcp -1 -1') + self.assert_called('DELETE', '/os-security-group-rules/14') + def test_security_group_list_rules(self): self.run_command('secgroup-list-rules test') self.assert_called('GET', '/os-security-groups') diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 1530bd8f9..a4ca62ad6 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2562,8 +2562,8 @@ def do_secgroup_delete_group_rule(cs, args): params['to_port'] = int(args.to_port) for rule in secgroup.rules: - if (rule.get('ip_protocol').upper() == params.get( - 'ip_protocol').upper() and + if (rule.get('ip_protocol') and rule['ip_protocol'].upper() == + params.get('ip_protocol').upper() and rule.get('from_port') == params.get('from_port') and rule.get('to_port') == params.get('to_port') and rule.get('group', {}).get('name') == From 1adc6d0251b290a08c5f2b89207c2a0a6480a06a Mon Sep 17 00:00:00 2001 From: Joe Gordon Date: Mon, 20 Oct 2014 10:21:20 -0700 Subject: [PATCH 0628/1705] Remove E12* from list of deliberately ignored flake8 rules I367064ecaa6d1fd9d918f7ce003303e2db660647 Added E12 to the ignored on purpose list, which also makes no sense E12 was never meant to be skipped all together (I was the one who added E12 to the flake8 ignore list (Ifc8924914b5a0d625bc8df6442ee85eb21459cde) Then I4cf246e3ec932ba0d2391eb8bcb793b28b005b4c came along and updated the list to break out the E12 ignored on purpose list. Drop all E12 rules from the ignored on purpose list, as we do not want to ignore these on purpose. Change-Id: Ic400047347a82aebbc0f6ce6f5061e7757a02d3d --- tox.ini | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 28c6542cf..4b317deb6 100644 --- a/tox.ini +++ b/tox.ini @@ -41,8 +41,7 @@ downloadcache = ~/cache/pip # H904 wrap long lines in parentheses instead of a backslash # reason: removed in hacking (https://review.openstack.org/#/c/101701/) # -# Additional checks are also ignored on purpose: E124, E126, -# E127, E128, E129, F811, F821 +# Additional checks are also ignored on purpose: F811, F821 ignore = E124,E126,E127,E128,E129,F811,F821,H402,H404,H405,H904 show-source = True exclude=.venv,.git,.tox,dist,*openstack/common*,*lib/python*,*egg,build,doc/source/conf.py From b6db84c9f83ff26ae4776244bdbf5dcf9b1f9da6 Mon Sep 17 00:00:00 2001 From: "M. David Bennett" Date: Sat, 18 Oct 2014 06:38:06 -0500 Subject: [PATCH 0629/1705] Add missing parameters for server rescue The server rescue call supports passing a password and an image, however novaclient does not have options to support this. This commit adds these options and updates the related unit tests to support the change. Change-Id: I14c878c0027f8206671ee001ba5195f94f842314 --- novaclient/tests/fixture_data/servers.py | 8 ++++++-- novaclient/tests/v1_1/fakes.py | 8 ++++++-- novaclient/tests/v1_1/test_servers.py | 18 ++++++++++++++++++ novaclient/tests/v1_1/test_shell.py | 10 ++++++++++ novaclient/v1_1/servers.py | 20 ++++++++++++++++---- novaclient/v1_1/shell.py | 18 ++++++++++++++++-- 6 files changed, 72 insertions(+), 10 deletions(-) diff --git a/novaclient/tests/fixture_data/servers.py b/novaclient/tests/fixture_data/servers.py index 00b7cd9fb..5e0edf148 100644 --- a/novaclient/tests/fixture_data/servers.py +++ b/novaclient/tests/fixture_data/servers.py @@ -419,8 +419,12 @@ def post_servers_1234_action(self, request, context): elif action == 'unlock': assert body[action] is None elif action == 'rescue': - assert body[action] is None - _body = {'Password': 'RescuePassword'} + if body[action]: + keys = set(body[action].keys()) + assert not (keys - set(['adminPass', 'rescue_image_ref'])) + else: + assert body[action] is None + _body = {'adminPass': 'RescuePassword'} elif action == 'unrescue': assert body[action] is None elif action == 'resume': diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index 929ad2082..6f17d85dc 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -577,8 +577,12 @@ def post_servers_1234_action(self, body, **kw): elif action == 'unlock': assert body[action] is None elif action == 'rescue': - assert body[action] is None - _body = {'Password': 'RescuePassword'} + if body[action]: + keys = set(body[action].keys()) + assert not (keys - set(['adminPass', 'rescue_image_ref'])) + else: + assert body[action] is None + _body = {'adminPass': 'RescuePassword'} elif action == 'unrescue': assert body[action] is None elif action == 'resume': diff --git a/novaclient/tests/v1_1/test_servers.py b/novaclient/tests/v1_1/test_servers.py index 0a81c5aae..9d2b495b6 100644 --- a/novaclient/tests/v1_1/test_servers.py +++ b/novaclient/tests/v1_1/test_servers.py @@ -432,6 +432,24 @@ def test_rescue(self): self.cs.servers.rescue(s) self.assert_called('POST', '/servers/1234/action') + def test_rescue_password(self): + s = self.cs.servers.get(1234) + s.rescue(password='asdf') + self.assert_called('POST', '/servers/1234/action', + {'rescue': {'adminPass': 'asdf'}}) + self.cs.servers.rescue(s, password='asdf') + self.assert_called('POST', '/servers/1234/action', + {'rescue': {'adminPass': 'asdf'}}) + + def test_rescue_image(self): + s = self.cs.servers.get(1234) + s.rescue(image=1) + self.assert_called('POST', '/servers/1234/action', + {'rescue': {'rescue_image_ref': 1}}) + self.cs.servers.rescue(s, image=1) + self.assert_called('POST', '/servers/1234/action', + {'rescue': {'rescue_image_ref': 1}}) + def test_unrescue(self): s = self.cs.servers.get(1234) s.unrescue() diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index 733c38931..5c264d3c5 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -949,6 +949,16 @@ def test_rescue(self): self.run_command('rescue sample-server') self.assert_called('POST', '/servers/1234/action', {'rescue': None}) + def test_rescue_password(self): + self.run_command('rescue sample-server --password asdf') + self.assert_called('POST', '/servers/1234/action', + {'rescue': {'adminPass': 'asdf'}}) + + def test_rescue_image(self): + self.run_command('rescue sample-server --image 1') + self.assert_called('POST', '/servers/1234/action', + {'rescue': {'rescue_image_ref': 1}}) + def test_unrescue(self): self.run_command('unrescue sample-server') self.assert_called('POST', '/servers/1234/action', {'unrescue': None}) diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index 29a4baf30..b75c95b9b 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -199,11 +199,14 @@ def resume(self): """ self.manager.resume(self) - def rescue(self): + def rescue(self, password=None, image=None): """ Rescue -- Rescue the problematic server. + + :param password: The admin password to be set in the rescue instance. + :param image: The :class:`Image` to rescue with. """ - return self.manager.rescue(self) + return self.manager.rescue(self, password, image) def unrescue(self): """ @@ -788,11 +791,20 @@ def resume(self, server): """ self._action('resume', server, None) - def rescue(self, server): + def rescue(self, server, password=None, image=None): """ Rescue the server. + + :param server: The :class:`Server` to rescue. + :param password: The admin password to be set in the rescue instance. + :param image: The :class:`Image` to rescue with. """ - return self._action('rescue', server, None) + info = {} + if password: + info['adminPass'] = password + if image: + info['rescue_image_ref'] = base.getid(image) + return self._action('rescue', server, info or None) def unrescue(self, server): """ diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 1530bd8f9..5fc9b5793 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1471,11 +1471,25 @@ def do_resume(cs, args): @utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('--password', + metavar='', + dest='password', + help=_('The admin password to be set in the rescue environment.')) +@utils.arg('--image', + metavar='', + dest='image', + help=_('The image to rescue with.')) def do_rescue(cs, args): """Reboots a server into rescue mode, which starts the machine - from the initial image, attaching the current boot disk as secondary. + from either the initial image or a specified image, attaching the current + boot disk as secondary. """ - utils.print_dict(_find_server(cs, args.server).rescue()[1]) + kwargs = {} + if args.image: + kwargs['image'] = _find_image(cs, args.image) + if args.password: + kwargs['password'] = args.password + utils.print_dict(_find_server(cs, args.server).rescue(**kwargs)[1]) @utils.arg('server', metavar='', help=_('Name or ID of server.')) From 333d2e7bd93b256340fa31bccbefd002063da93e Mon Sep 17 00:00:00 2001 From: Cedric Brandily Date: Wed, 22 Oct 2014 16:04:03 +0200 Subject: [PATCH 0630/1705] Define helper to run an action on many resources This change defines do_action_on_many function as a helper to run an action on many resources (like nova delete). Change-Id: I1c45d11a520703c57e3ac1923d94ec5f864f266b --- novaclient/tests/test_utils.py | 23 +++++++++++++++++++++++ novaclient/utils.py | 16 ++++++++++++++++ novaclient/v1_1/shell.py | 18 +++++------------- 3 files changed, 44 insertions(+), 13 deletions(-) diff --git a/novaclient/tests/test_utils.py b/novaclient/tests/test_utils.py index b8960b57c..b0079ef3f 100644 --- a/novaclient/tests/test_utils.py +++ b/novaclient/tests/test_utils.py @@ -312,3 +312,26 @@ def hook2(args): except_error = ("Hook 'hook2' is attempting to redefine " "attributes") self.assertIn(except_error, six.text_type(exc)) + + +class DoActionOnManyTestCase(test_utils.TestCase): + + def _test_do_action_on_many(self, side_effect, fail): + action = mock.Mock(side_effect=side_effect) + + if fail: + self.assertRaises(exceptions.CommandError, + utils.do_action_on_many, + action, [1, 2], 'success with %s', 'error') + else: + utils.do_action_on_many(action, [1, 2], 'success with %s', 'error') + action.assert_has_calls([mock.call(1), mock.call(2)]) + + def test_do_action_on_many_success(self): + self._test_do_action_on_many([None, None], fail=False) + + def test_do_action_on_many_first_fails(self): + self._test_do_action_on_many([Exception(), None], fail=True) + + def test_do_action_on_many_last_fails(self): + self._test_do_action_on_many([None, Exception()], fail=True) diff --git a/novaclient/utils.py b/novaclient/utils.py index cf1bc7b21..6033e8cac 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -300,6 +300,22 @@ def safe_issubclass(*args): return False +def do_action_on_many(action, resources, success_msg, error_msg): + """Helper to run an action on many resources.""" + failure_flag = False + + for resource in resources: + try: + action(resource) + print(success_msg % resource) + except Exception as e: + failure_flag = True + print(e) + + if failure_flag: + raise exceptions.CommandError(error_msg) + + def _load_entry_point(ep_name, name=None): """Try to load the entry point ep_name that matches name.""" for ep in pkg_resources.iter_entry_points(ep_name, name=name): diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 1530bd8f9..40cbbc50c 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1677,19 +1677,11 @@ def do_show(cs, args): help=_('Name or ID of server(s).')) def do_delete(cs, args): """Immediately shut down and delete specified server(s).""" - failure_flag = False - - for server in args.server: - try: - _find_server(cs, server).delete() - print(_("Request to delete server %s has been accepted.") % server) - except Exception as e: - failure_flag = True - print(e) - - if failure_flag: - raise exceptions.CommandError(_("Unable to delete the " - "specified server(s).")) + utils.do_action_on_many( + lambda s: _find_server(cs, s).delete(), + args.server, + _("Request to delete server %s has been accepted."), + _("Unable to delete the specified server(s).")) def _find_server(cs, server): From 70172026f144fe1eac1144b3d24384468d7b2b03 Mon Sep 17 00:00:00 2001 From: Cedric Brandily Date: Wed, 22 Oct 2014 16:22:53 +0200 Subject: [PATCH 0631/1705] Allow to start/stop multiple servers This change allows to pass multiple server names/ids to nova start/stop. Change-Id: Ide577a1016de3d09f31c1f6e7b61079380d99060 --- novaclient/v1_1/shell.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 40cbbc50c..d891edc1e 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1434,16 +1434,26 @@ def do_unpause(cs, args): _find_server(cs, args.server).unpause() -@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('server', metavar='', nargs='+', + help=_('Name or ID of server(s).')) def do_stop(cs, args): - """Stop a server.""" - _find_server(cs, args.server).stop() + """Stop the server(s).""" + utils.do_action_on_many( + lambda s: _find_server(cs, s).stop(), + args.server, + _("Request to stop server %s has been accepted."), + _("Unable to stop the specified server(s).")) -@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('server', metavar='', nargs='+', + help=_('Name or ID of server(s).')) def do_start(cs, args): - """Start a server.""" - _find_server(cs, args.server).start() + """Start the server(s).""" + utils.do_action_on_many( + lambda s: _find_server(cs, s).start(), + args.server, + _("Request to start server %s has been accepted."), + _("Unable to start the specified server(s).")) @utils.arg('server', metavar='', help=_('Name or ID of server.')) From c8a7e6a02db72241acb61d4f994b96ae351b61d5 Mon Sep 17 00:00:00 2001 From: Morgan Fainberg Date: Wed, 22 Oct 2014 16:01:32 -0700 Subject: [PATCH 0632/1705] Update novaclient shell to use shared arguments from Session The novaclient implemented it's own version of the CLI arguments that are now provided by keystoneclient's session object. This changeset converts nova over to utilizing the shared argument registration. Change-Id: Ifc9ddc08614e28358229db84cb9b54552ca36d51 Co-Authored-By: David Hu --- novaclient/shell.py | 31 ++++++++++--------------------- 1 file changed, 10 insertions(+), 21 deletions(-) diff --git a/novaclient/shell.py b/novaclient/shell.py index fff534179..301da99eb 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -29,6 +29,7 @@ import pkgutil import sys +from keystoneclient import session as ksession from oslo.utils import encodeutils from oslo.utils import strutils import pkg_resources @@ -232,6 +233,13 @@ def error(self, message): class OpenStackComputeShell(object): + def _append_global_identity_args(self, parser): + # Register the CLI arguments that have moved to the session object. + ksession.Session.register_cli_options(parser) + + parser.set_defaults(insecure=utils.env('NOVACLIENT_INSECURE', + default=False)) + def get_base_parser(self): parser = NovaClientArgumentParser( prog='nova', @@ -269,12 +277,6 @@ def get_base_parser(self): action='store_true', help=_("Print call timing info")) - parser.add_argument('--timeout', - default=600, - metavar='', - type=positive_non_zero_float, - help=_("Set HTTP call timeout (in seconds)")) - parser.add_argument('--os-auth-token', default=utils.env('OS_AUTH_TOKEN'), help='Defaults to env[OS_AUTH_TOKEN]') @@ -373,21 +375,6 @@ def get_base_parser(self): parser.add_argument('--os_compute_api_version', help=argparse.SUPPRESS) - parser.add_argument('--os-cacert', - metavar='', - default=utils.env('OS_CACERT', default=None), - help='Specify a CA bundle file to use in ' - 'verifying a TLS (https) server certificate. ' - 'Defaults to env[OS_CACERT]') - - parser.add_argument('--insecure', - default=utils.env('NOVACLIENT_INSECURE', default=False), - action='store_true', - help=_("Explicitly allow novaclient to perform \"insecure\" " - "SSL (https) requests. The server's certificate will " - "not be verified against any certificate authorities. " - "This option should be used with caution.")) - parser.add_argument('--bypass-url', metavar='', dest='bypass_url', @@ -400,6 +387,8 @@ def get_base_parser(self): # The auth-system-plugins might require some extra options novaclient.auth_plugin.load_auth_system_opts(parser) + self._append_global_identity_args(parser) + return parser def get_subcommand_parser(self, version): From 3f64b4577b402f1f733d264e3f1a8448a1a0b0f9 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Mon, 13 Oct 2014 17:26:40 +0300 Subject: [PATCH 0633/1705] Propose 'tox' as tests launcher Since ``tox`` is used as launcher in gates, it should be mentioned in docs instead of ``python setup.py test``. Change-Id: Ie5fcd9da755e1146b2b042fa5906aa28748980a6 --- doc/source/index.rst | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/doc/source/index.rst b/doc/source/index.rst index c17d19aac..a7d547d1e 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -1,5 +1,5 @@ Python bindings to the OpenStack Nova API -================================================== +========================================= This is a client for OpenStack Nova API. There's :doc:`a Python API ` (the :mod:`novaclient` module), and a :doc:`command-line script @@ -40,7 +40,14 @@ Code is hosted at `git.openstack.org`_. Submit bugs to the Nova project on .. _Launchpad: https://launchpad.net/nova .. _Gerrit: http://wiki.openstack.org/GerritWorkflow -Run tests with ``python setup.py test``. +Testing +------- + +The preferred way to run the unit tests is using ``tox``. + +See `Consistent Testing Interface`_ for more details. + +.. _Consistent Testing Interface: http://git.openstack.org/cgit/openstack/governance/tree/reference/project-testing-interface.rst Indices and tables ================== From 8de2237cc4e75c0257f1d2a25661b29465a8587d Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Wed, 24 Sep 2014 23:20:58 +0300 Subject: [PATCH 0634/1705] Enable check for E126 E126 continuation line over-indented for hanging indent Change-Id: Id64edce8278d96c1b2d4a6ca536e9fc8fbef478c --- novaclient/client.py | 2 +- novaclient/service_catalog.py | 10 +- novaclient/shell.py | 8 +- novaclient/tests/fixture_data/floatingips.py | 22 +- novaclient/tests/test_auth_plugins.py | 42 +- novaclient/tests/v1_1/contrib/fakes.py | 19 +- novaclient/tests/v1_1/fakes.py | 639 +++++++++--------- novaclient/tests/v1_1/test_agents.py | 12 +- novaclient/tests/v3/fakes.py | 90 +-- novaclient/utils.py | 6 +- novaclient/v1_1/agents.py | 20 +- novaclient/v1_1/certs.py | 6 +- .../v1_1/contrib/assisted_volume_snapshots.py | 2 +- novaclient/v1_1/flavors.py | 9 +- .../v1_1/security_group_default_rules.py | 8 +- novaclient/v1_1/security_group_rules.py | 12 +- novaclient/v1_1/servers.py | 2 +- novaclient/v1_1/shell.py | 66 +- novaclient/v1_1/usage.py | 6 +- novaclient/v3/agents.py | 6 +- novaclient/v3/flavors.py | 8 +- novaclient/v3/shell.py | 66 +- tox.ini | 2 +- 23 files changed, 539 insertions(+), 524 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index 4a7189386..de4fb3772 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -378,7 +378,7 @@ def _get_session(self, url): self._session.close() self._current_url = service_url self._logger.debug( - "New session created for: (%s)" % service_url) + "New session created for: (%s)" % service_url) self._session = requests.Session() self._session.mount(service_url, self._connection_pool.get(service_url)) diff --git a/novaclient/service_catalog.py b/novaclient/service_catalog.py index 26f4ccc5c..84effda7f 100644 --- a/novaclient/service_catalog.py +++ b/novaclient/service_catalog.py @@ -72,11 +72,11 @@ def url_for(self, attr=None, filter_value=None, endpoints = service['endpoints'] for endpoint in endpoints: # Ignore 1.0 compute endpoints - if service.get("type") == 'compute' and \ - endpoint.get('versionId', '2') not in ('1.1', '2'): + if (service.get("type") == 'compute' and + endpoint.get('versionId', '2') not in ('1.1', '2')): continue - if not filter_value or \ - endpoint.get(attr).lower() == filter_value.lower(): + if (not filter_value or + endpoint.get(attr).lower() == filter_value.lower()): endpoint["serviceName"] = service.get("name") matching_endpoints.append(endpoint) @@ -84,6 +84,6 @@ def url_for(self, attr=None, filter_value=None, raise novaclient.exceptions.EndpointNotFound() elif len(matching_endpoints) > 1: raise novaclient.exceptions.AmbiguousEndpoints( - endpoints=matching_endpoints) + endpoints=matching_endpoints) else: return matching_endpoints[0][endpoint_type] diff --git a/novaclient/shell.py b/novaclient/shell.py index fff534179..b92022a00 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -355,8 +355,8 @@ def get_base_parser(self): metavar='', default=utils.env('NOVA_ENDPOINT_TYPE', default=DEFAULT_NOVA_ENDPOINT_TYPE), - help=_('Defaults to env[NOVA_ENDPOINT_TYPE] or ') - + DEFAULT_NOVA_ENDPOINT_TYPE + '.') + help=(_('Defaults to env[NOVA_ENDPOINT_TYPE] or ') + + DEFAULT_NOVA_ENDPOINT_TYPE + '.')) # NOTE(dtroyer): We can't add --endpoint_type here due to argparse # thinking usage-list --end is ambiguous; but it # works fine with only --endpoint-type present @@ -527,7 +527,7 @@ def main(self, argv): # build available subcommands based on version self.extensions = self._discover_extensions( - options.os_compute_api_version) + options.os_compute_api_version) self._run_extension_hooks('__pre_parse_args__') # NOTE(dtroyer): Hackery to handle --endpoint_type due to argparse @@ -539,7 +539,7 @@ def main(self, argv): argv[spot] = '--endpoint-type' subcommand_parser = self.get_subcommand_parser( - options.os_compute_api_version) + options.os_compute_api_version) self.parser = subcommand_parser if options.help or not argv: diff --git a/novaclient/tests/fixture_data/floatingips.py b/novaclient/tests/fixture_data/floatingips.py index 3f2b25000..f493e10fb 100644 --- a/novaclient/tests/fixture_data/floatingips.py +++ b/novaclient/tests/fixture_data/floatingips.py @@ -105,18 +105,18 @@ def put_dns_testdomain_entries_testname(request, context): 'dns_entries': [ { 'dns_entry': { - 'ip': '1.2.3.4', - 'name': "host1", - 'type': "A", - 'domain': 'testdomain' + 'ip': '1.2.3.4', + 'name': "host1", + 'type': "A", + 'domain': 'testdomain' } }, { 'dns_entry': { - 'ip': '1.2.3.4', - 'name': "host2", - 'type': "A", - 'domain': 'testdomain' + 'ip': '1.2.3.4', + 'name': "host2", + 'type': "A", + 'domain': 'testdomain' } }, ] @@ -182,9 +182,9 @@ def post_os_floating_ips_bulk(request, context): interface = params.get('interface', 'defaultInterface') return { 'floating_ips_bulk_create': { - 'ip_range': '192.168.1.0/30', - 'pool': pool, - 'interface': interface + 'ip_range': '192.168.1.0/30', + 'pool': pool, + 'interface': interface } } diff --git a/novaclient/tests/test_auth_plugins.py b/novaclient/tests/test_auth_plugins.py index ffc591c7f..ce7e82a8c 100644 --- a/novaclient/tests/test_auth_plugins.py +++ b/novaclient/tests/test_auth_plugins.py @@ -167,9 +167,9 @@ def auth_url(self): return None mock_iter_entry_points.side_effect = lambda _t, name: [ - MockAuthUrlEntrypoint("fakewithauthurl", - "fakewithauthurl", - ["auth_url"])] + MockAuthUrlEntrypoint("fakewithauthurl", + "fakewithauthurl", + ["auth_url"])] plugin = auth_plugin.DeprecatedAuthPlugin("fakewithauthurl") self.assertRaises( @@ -192,7 +192,7 @@ def authenticate(self, cls, auth_url): cls._authenticate(auth_url, {"fake": "me"}) mock_iter_entry_points.side_effect = lambda _t: [ - MockEntrypoint("fake", "fake", ["FakePlugin"])] + MockEntrypoint("fake", "fake", ["FakePlugin"])] mock_request.side_effect = mock_http_request() @@ -207,12 +207,12 @@ def authenticate(self, cls, auth_url): token_url = cs.client.auth_url + "/tokens" mock_request.assert_called_with( - "POST", - token_url, - headers=headers, - data='{"fake": "me"}', - allow_redirects=True, - **self.TEST_REQUEST_BASE) + "POST", + token_url, + headers=headers, + data='{"fake": "me"}', + allow_redirects=True, + **self.TEST_REQUEST_BASE) @mock.patch.object(pkg_resources, "iter_entry_points") def test_discover_auth_system_options(self, mock_iter_entry_points): @@ -231,7 +231,7 @@ def load(self): return FakePlugin mock_iter_entry_points.side_effect = lambda _t: [ - MockEntrypoint("fake", "fake", ["FakePlugin"])] + MockEntrypoint("fake", "fake", ["FakePlugin"])] parser = argparse.ArgumentParser() auth_plugin.discover_auth_systems() @@ -255,7 +255,7 @@ def parse_opts(self, args): return self.opts mock_iter_entry_points.side_effect = lambda _t: [ - MockEntrypoint("fake", "fake", ["FakePlugin"])] + MockEntrypoint("fake", "fake", ["FakePlugin"])] auth_plugin.discover_auth_systems() plugin = auth_plugin.load_plugin("fake") @@ -275,7 +275,7 @@ def get_auth_url(self): return "http://faked/v2.0" mock_iter_entry_points.side_effect = lambda _t: [ - MockEntrypoint("fake", "fake", ["FakePlugin"])] + MockEntrypoint("fake", "fake", ["FakePlugin"])] auth_plugin.discover_auth_systems() plugin = auth_plugin.load_plugin("fake") @@ -296,15 +296,15 @@ class FakePlugin(auth_plugin.BaseAuthPlugin): pass mock_iter_entry_points.side_effect = lambda _t: [ - MockEntrypoint("fake", "fake", ["FakePlugin"])] + MockEntrypoint("fake", "fake", ["FakePlugin"])] auth_plugin.discover_auth_systems() plugin = auth_plugin.load_plugin("fake") self.assertRaises( - exceptions.EndpointNotFound, - client.Client, "username", "password", "project_id", - auth_system="fake", auth_plugin=plugin) + exceptions.EndpointNotFound, + client.Client, "username", "password", "project_id", + auth_system="fake", auth_plugin=plugin) @mock.patch.object(pkg_resources, "iter_entry_points") def test_exception_if_no_url(self, mock_iter_entry_points): @@ -317,12 +317,12 @@ class FakePlugin(auth_plugin.BaseAuthPlugin): pass mock_iter_entry_points.side_effect = lambda _t: [ - MockEntrypoint("fake", "fake", ["FakePlugin"])] + MockEntrypoint("fake", "fake", ["FakePlugin"])] auth_plugin.discover_auth_systems() plugin = auth_plugin.load_plugin("fake") self.assertRaises( - exceptions.EndpointNotFound, - client.Client, "username", "password", "project_id", - auth_system="fake", auth_plugin=plugin) + exceptions.EndpointNotFound, + client.Client, "username", "password", "project_id", + auth_system="fake", auth_plugin=plugin) diff --git a/novaclient/tests/v1_1/contrib/fakes.py b/novaclient/tests/v1_1/contrib/fakes.py index 3b859362e..0d4af537c 100644 --- a/novaclient/tests/v1_1/contrib/fakes.py +++ b/novaclient/tests/v1_1/contrib/fakes.py @@ -136,12 +136,13 @@ def delete_os_assisted_volume_snapshots_x(self, **kw): return (202, {}, {}) def post_os_server_external_events(self, **kw): - return (200, {}, {'events': [ - {'name': 'test-event', - 'status': 'completed', - 'tag': 'tag', - 'server_uuid': 'fake-uuid1'}, - {'name': 'test-event', - 'status': 'completed', - 'tag': 'tag', - 'server_uuid': 'fake-uuid2'}]}) + return (200, {}, { + 'events': [ + {'name': 'test-event', + 'status': 'completed', + 'tag': 'tag', + 'server_uuid': 'fake-uuid1'}, + {'name': 'test-event', + 'status': 'completed', + 'tag': 'tag', + 'server_uuid': 'fake-uuid2'}]}) diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index dcb0ee756..667f9edad 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -126,11 +126,11 @@ def delete_os_agents_1(self, **kw): return (202, {}, None) def put_os_agents_1(self, body, **kw): - return (200, {}, {"agent": { - "url": "/yyy/yyyy/yyyy", - "version": "8.0", - "md5hash": "add6bb58e139be103324d04d82d8f546", - 'id': 1}}) + return (200, {}, { + "agent": {"url": "/yyy/yyyy/yyyy", + "version": "8.0", + "md5hash": "add6bb58e139be103324d04d82d8f546", + 'id': 1}}) # # List all extensions @@ -1038,20 +1038,19 @@ def get_os_keypairs_test(self, *kw): self.get_os_keypairs()[2]['keypairs'][0]['keypair']}) def get_os_keypairs(self, *kw): - return (200, {}, {"keypairs": [ - {"keypair": { - "public_key": "FAKE_SSH_RSA", - "private_key": "FAKE_PRIVATE_KEY", - "user_id": - "81e373b596d6466e99c4896826abaa46", - "name": "test", - "deleted": False, - "created_at": "2014-04-19T02:16:44.000000", - "updated_at": "2014-04-19T10:12:3.000000", - "figerprint": "FAKE_KEYPAIR", - "deleted_at": None, - "id": 4} - }]}) + return (200, {}, { + "keypairs": [{"keypair": { + "public_key": "FAKE_SSH_RSA", + "private_key": "FAKE_PRIVATE_KEY", + "user_id": "81e373b596d6466e99c4896826abaa46", + "name": "test", + "deleted": False, + "created_at": "2014-04-19T02:16:44.000000", + "updated_at": "2014-04-19T10:12:3.000000", + "figerprint": "FAKE_KEYPAIR", + "deleted_at": None, + "id": 4}} + ]}) def delete_os_keypairs_test(self, **kw): return (202, {}, None) @@ -1076,127 +1075,135 @@ def get_servers_1234_os_virtual_interfaces(self, **kw): # def get_os_quota_sets_test(self, **kw): - return (200, {}, {'quota_set': { - 'tenant_id': 'test', - 'metadata_items': [], - 'injected_file_content_bytes': 1, - 'injected_file_path_bytes': 1, - 'ram': 1, - 'floating_ips': 1, - 'instances': 1, - 'injected_files': 1, - 'cores': 1, - 'keypairs': 1, - 'security_groups': 1, - 'security_group_rules': 1}}) + return (200, {}, { + 'quota_set': { + 'tenant_id': 'test', + 'metadata_items': [], + 'injected_file_content_bytes': 1, + 'injected_file_path_bytes': 1, + 'ram': 1, + 'floating_ips': 1, + 'instances': 1, + 'injected_files': 1, + 'cores': 1, + 'keypairs': 1, + 'security_groups': 1, + 'security_group_rules': 1}}) def get_os_quota_sets_tenant_id(self, **kw): - return (200, {}, {'quota_set': { - 'tenant_id': 'test', - 'metadata_items': [], - 'injected_file_content_bytes': 1, - 'injected_file_path_bytes': 1, - 'ram': 1, - 'floating_ips': 1, - 'instances': 1, - 'injected_files': 1, - 'cores': 1, - 'keypairs': 1, - 'security_groups': 1, - 'security_group_rules': 1}}) + return (200, {}, { + 'quota_set': { + 'tenant_id': 'test', + 'metadata_items': [], + 'injected_file_content_bytes': 1, + 'injected_file_path_bytes': 1, + 'ram': 1, + 'floating_ips': 1, + 'instances': 1, + 'injected_files': 1, + 'cores': 1, + 'keypairs': 1, + 'security_groups': 1, + 'security_group_rules': 1}}) def get_os_quota_sets_97f4c221bff44578b0300df4ef119353(self, **kw): - return (200, {}, {'quota_set': { - 'tenant_id': '97f4c221bff44578b0300df4ef119353', - 'metadata_items': [], - 'injected_file_content_bytes': 1, - 'injected_file_path_bytes': 1, - 'ram': 1, - 'floating_ips': 1, - 'instances': 1, - 'injected_files': 1, - 'cores': 1, - 'keypairs': 1, - 'security_groups': 1, - 'security_group_rules': 1}}) + return (200, {}, { + 'quota_set': { + 'tenant_id': '97f4c221bff44578b0300df4ef119353', + 'metadata_items': [], + 'injected_file_content_bytes': 1, + 'injected_file_path_bytes': 1, + 'ram': 1, + 'floating_ips': 1, + 'instances': 1, + 'injected_files': 1, + 'cores': 1, + 'keypairs': 1, + 'security_groups': 1, + 'security_group_rules': 1}}) def put_os_quota_sets_97f4c221_bff4_4578_b030_0df4ef119353(self, **kw): - return (200, {}, {'quota_set': { - 'tenant_id': '97f4c221-bff4-4578-b030-0df4ef119353', - 'metadata_items': [], - 'injected_file_content_bytes': 1, - 'injected_file_path_bytes': 1, - 'ram': 1, - 'floating_ips': 1, - 'instances': 1, - 'injected_files': 1, - 'cores': 1, - 'keypairs': 1, - 'security_groups': 1, - 'security_group_rules': 1}}) + return (200, {}, { + 'quota_set': { + 'tenant_id': '97f4c221-bff4-4578-b030-0df4ef119353', + 'metadata_items': [], + 'injected_file_content_bytes': 1, + 'injected_file_path_bytes': 1, + 'ram': 1, + 'floating_ips': 1, + 'instances': 1, + 'injected_files': 1, + 'cores': 1, + 'keypairs': 1, + 'security_groups': 1, + 'security_group_rules': 1}}) def get_os_quota_sets_97f4c221_bff4_4578_b030_0df4ef119353(self, **kw): - return (200, {}, {'quota_set': { - 'tenant_id': '97f4c221-bff4-4578-b030-0df4ef119353', - 'metadata_items': [], - 'injected_file_content_bytes': 1, - 'injected_file_path_bytes': 1, - 'ram': 1, - 'floating_ips': 1, - 'instances': 1, - 'injected_files': 1, - 'cores': 1, - 'keypairs': 1, - 'security_groups': 1, - 'security_group_rules': 1}}) + return (200, {}, { + 'quota_set': { + 'tenant_id': '97f4c221-bff4-4578-b030-0df4ef119353', + 'metadata_items': [], + 'injected_file_content_bytes': 1, + 'injected_file_path_bytes': 1, + 'ram': 1, + 'floating_ips': 1, + 'instances': 1, + 'injected_files': 1, + 'cores': 1, + 'keypairs': 1, + 'security_groups': 1, + 'security_group_rules': 1}}) def get_os_quota_sets_97f4c221bff44578b0300df4ef119353_defaults(self): - return (200, {}, {'quota_set': { - 'tenant_id': 'test', - 'metadata_items': [], - 'injected_file_content_bytes': 1, - 'injected_file_path_bytes': 1, - 'ram': 1, - 'floating_ips': 1, - 'instances': 1, - 'injected_files': 1, - 'cores': 1, - 'keypairs': 1, - 'security_groups': 1, - 'security_group_rules': 1}}) + return (200, {}, { + 'quota_set': { + 'tenant_id': 'test', + 'metadata_items': [], + 'injected_file_content_bytes': 1, + 'injected_file_path_bytes': 1, + 'ram': 1, + 'floating_ips': 1, + 'instances': 1, + 'injected_files': 1, + 'cores': 1, + 'keypairs': 1, + 'security_groups': 1, + 'security_group_rules': 1}}) def get_os_quota_sets_tenant_id_defaults(self): - return (200, {}, {'quota_set': { - 'tenant_id': 'test', - 'metadata_items': [], - 'injected_file_content_bytes': 1, - 'injected_file_path_bytes': 1, - 'ram': 1, - 'floating_ips': 1, - 'instances': 1, - 'injected_files': 1, - 'cores': 1, - 'keypairs': 1, - 'security_groups': 1, - 'security_group_rules': 1}}) + return (200, {}, { + 'quota_set': { + 'tenant_id': 'test', + 'metadata_items': [], + 'injected_file_content_bytes': 1, + 'injected_file_path_bytes': 1, + 'ram': 1, + 'floating_ips': 1, + 'instances': 1, + 'injected_files': 1, + 'cores': 1, + 'keypairs': 1, + 'security_groups': 1, + 'security_group_rules': 1}}) def put_os_quota_sets_97f4c221bff44578b0300df4ef119353(self, body, **kw): assert list(body) == ['quota_set'] fakes.assert_has_keys(body['quota_set'], required=['tenant_id']) - return (200, {}, {'quota_set': { - 'tenant_id': '97f4c221bff44578b0300df4ef119353', - 'metadata_items': [], - 'injected_file_content_bytes': 1, - 'injected_file_path_bytes': 1, - 'ram': 1, - 'floating_ips': 1, - 'instances': 1, - 'injected_files': 1, - 'cores': 1, - 'keypairs': 1, - 'security_groups': 1, - 'security_group_rules': 1}}) + return (200, {}, { + 'quota_set': { + 'tenant_id': '97f4c221bff44578b0300df4ef119353', + 'metadata_items': [], + 'injected_file_content_bytes': 1, + 'injected_file_path_bytes': 1, + 'ram': 1, + 'floating_ips': 1, + 'instances': 1, + 'injected_files': 1, + 'cores': 1, + 'keypairs': 1, + 'security_groups': 1, + 'security_group_rules': 1}}) def delete_os_quota_sets_test(self, **kw): return (202, {}, {}) @@ -1209,50 +1216,53 @@ def delete_os_quota_sets_97f4c221bff44578b0300df4ef119353(self, **kw): # def get_os_quota_class_sets_test(self, **kw): - return (200, {}, {'quota_class_set': { - 'id': 'test', - 'metadata_items': 1, - 'injected_file_content_bytes': 1, - 'injected_file_path_bytes': 1, - 'ram': 1, - 'floating_ips': 1, - 'instances': 1, - 'injected_files': 1, - 'cores': 1, - 'key_pairs': 1, - 'security_groups': 1, - 'security_group_rules': 1}}) + return (200, {}, { + 'quota_class_set': { + 'id': 'test', + 'metadata_items': 1, + 'injected_file_content_bytes': 1, + 'injected_file_path_bytes': 1, + 'ram': 1, + 'floating_ips': 1, + 'instances': 1, + 'injected_files': 1, + 'cores': 1, + 'key_pairs': 1, + 'security_groups': 1, + 'security_group_rules': 1}}) def put_os_quota_class_sets_test(self, body, **kw): assert list(body) == ['quota_class_set'] - return (200, {}, {'quota_class_set': { - 'metadata_items': 1, - 'injected_file_content_bytes': 1, - 'injected_file_path_bytes': 1, - 'ram': 1, - 'floating_ips': 1, - 'instances': 1, - 'injected_files': 1, - 'cores': 1, - 'key_pairs': 1, - 'security_groups': 1, - 'security_group_rules': 1}}) + return (200, {}, { + 'quota_class_set': { + 'metadata_items': 1, + 'injected_file_content_bytes': 1, + 'injected_file_path_bytes': 1, + 'ram': 1, + 'floating_ips': 1, + 'instances': 1, + 'injected_files': 1, + 'cores': 1, + 'key_pairs': 1, + 'security_groups': 1, + 'security_group_rules': 1}}) def put_os_quota_class_sets_97f4c221bff44578b0300df4ef119353(self, body, **kw): assert list(body) == ['quota_class_set'] - return (200, {}, {'quota_class_set': { - 'metadata_items': 1, - 'injected_file_content_bytes': 1, - 'injected_file_path_bytes': 1, - 'ram': 1, - 'floating_ips': 1, - 'instances': 1, - 'injected_files': 1, - 'cores': 1, - 'key_pairs': 1, - 'security_groups': 1, - 'security_group_rules': 1}}) + return (200, {}, { + 'quota_class_set': { + 'metadata_items': 1, + 'injected_file_content_bytes': 1, + 'injected_file_path_bytes': 1, + 'ram': 1, + 'floating_ips': 1, + 'instances': 1, + 'injected_files': 1, + 'cores': 1, + 'key_pairs': 1, + 'security_groups': 1, + 'security_group_rules': 1}}) # # Security Groups @@ -1669,110 +1679,116 @@ def put_os_hosts_sample_host(self, body, **kw): return (200, {}, result) def get_os_hypervisors(self, **kw): - return (200, {}, {"hypervisors": [ - {'id': 1234, 'hypervisor_hostname': 'hyper1'}, - {'id': 5678, 'hypervisor_hostname': 'hyper2'}]}) + return (200, {}, { + "hypervisors": [ + {'id': 1234, 'hypervisor_hostname': 'hyper1'}, + {'id': 5678, 'hypervisor_hostname': 'hyper2'}]}) def get_os_hypervisors_detail(self, **kw): - return (200, {}, {"hypervisors": [ - {'id': 1234, - 'service': {'id': 1, 'host': 'compute1'}, - 'vcpus': 4, - 'memory_mb': 10 * 1024, - 'local_gb': 250, - 'vcpus_used': 2, - 'memory_mb_used': 5 * 1024, - 'local_gb_used': 125, - 'hypervisor_type': "xen", - 'hypervisor_version': 3, - 'hypervisor_hostname': "hyper1", - 'free_ram_mb': 5 * 1024, - 'free_disk_gb': 125, - 'current_workload': 2, - 'running_vms': 2, - 'cpu_info': 'cpu_info', - 'disk_available_least': 100}, - {'id': 2, - 'service': {'id': 2, 'host': "compute2"}, - 'vcpus': 4, - 'memory_mb': 10 * 1024, - 'local_gb': 250, - 'vcpus_used': 2, - 'memory_mb_used': 5 * 1024, - 'local_gb_used': 125, - 'hypervisor_type': "xen", - 'hypervisor_version': 3, - 'hypervisor_hostname': "hyper2", - 'free_ram_mb': 5 * 1024, - 'free_disk_gb': 125, - 'current_workload': 2, - 'running_vms': 2, - 'cpu_info': 'cpu_info', - 'disk_available_least': 100} - ]}) + return (200, {}, { + "hypervisors": [ + {'id': 1234, + 'service': {'id': 1, 'host': 'compute1'}, + 'vcpus': 4, + 'memory_mb': 10 * 1024, + 'local_gb': 250, + 'vcpus_used': 2, + 'memory_mb_used': 5 * 1024, + 'local_gb_used': 125, + 'hypervisor_type': "xen", + 'hypervisor_version': 3, + 'hypervisor_hostname': "hyper1", + 'free_ram_mb': 5 * 1024, + 'free_disk_gb': 125, + 'current_workload': 2, + 'running_vms': 2, + 'cpu_info': 'cpu_info', + 'disk_available_least': 100}, + {'id': 2, + 'service': {'id': 2, 'host': "compute2"}, + 'vcpus': 4, + 'memory_mb': 10 * 1024, + 'local_gb': 250, + 'vcpus_used': 2, + 'memory_mb_used': 5 * 1024, + 'local_gb_used': 125, + 'hypervisor_type': "xen", + 'hypervisor_version': 3, + 'hypervisor_hostname': "hyper2", + 'free_ram_mb': 5 * 1024, + 'free_disk_gb': 125, + 'current_workload': 2, + 'running_vms': 2, + 'cpu_info': 'cpu_info', + 'disk_available_least': 100}] + }) def get_os_hypervisors_statistics(self, **kw): - return (200, {}, {"hypervisor_statistics": { - 'count': 2, - 'vcpus': 8, - 'memory_mb': 20 * 1024, - 'local_gb': 500, - 'vcpus_used': 4, - 'memory_mb_used': 10 * 1024, - 'local_gb_used': 250, - 'free_ram_mb': 10 * 1024, - 'free_disk_gb': 250, - 'current_workload': 4, - 'running_vms': 4, - 'disk_available_least': 200, - }}) + return (200, {}, { + "hypervisor_statistics": { + 'count': 2, + 'vcpus': 8, + 'memory_mb': 20 * 1024, + 'local_gb': 500, + 'vcpus_used': 4, + 'memory_mb_used': 10 * 1024, + 'local_gb_used': 250, + 'free_ram_mb': 10 * 1024, + 'free_disk_gb': 250, + 'current_workload': 4, + 'running_vms': 4, + 'disk_available_least': 200} + }) def get_os_hypervisors_hyper_search(self, **kw): - return (200, {}, {'hypervisors': [ - {'id': 1234, 'hypervisor_hostname': 'hyper1'}, - {'id': 5678, 'hypervisor_hostname': 'hyper2'}]}) + return (200, {}, { + 'hypervisors': [ + {'id': 1234, 'hypervisor_hostname': 'hyper1'}, + {'id': 5678, 'hypervisor_hostname': 'hyper2'}]}) def get_os_hypervisors_hyper_servers(self, **kw): - return (200, {}, {'hypervisors': [ - {'id': 1234, - 'hypervisor_hostname': 'hyper1', - 'servers': [ - {'name': 'inst1', 'uuid': 'uuid1'}, - {'name': 'inst2', 'uuid': 'uuid2'}]}, - {'id': 5678, - 'hypervisor_hostname': 'hyper2', - 'servers': [ - {'name': 'inst3', 'uuid': 'uuid3'}, - {'name': 'inst4', 'uuid': 'uuid4'}]} - ]}) + return (200, {}, { + 'hypervisors': [ + {'id': 1234, + 'hypervisor_hostname': 'hyper1', + 'servers': [ + {'name': 'inst1', 'uuid': 'uuid1'}, + {'name': 'inst2', 'uuid': 'uuid2'}]}, + {'id': 5678, + 'hypervisor_hostname': 'hyper2', + 'servers': [ + {'name': 'inst3', 'uuid': 'uuid3'}, + {'name': 'inst4', 'uuid': 'uuid4'}]}] + }) def get_os_hypervisors_hyper_no_servers_servers(self, **kw): return (200, {}, {'hypervisors': [{'id': 1234, 'hypervisor_hostname': 'hyper1'}]}) def get_os_hypervisors_1234(self, **kw): - return (200, {}, {'hypervisor': - {'id': 1234, - 'service': {'id': 1, 'host': 'compute1'}, - 'vcpus': 4, - 'memory_mb': 10 * 1024, - 'local_gb': 250, - 'vcpus_used': 2, - 'memory_mb_used': 5 * 1024, - 'local_gb_used': 125, - 'hypervisor_type': "xen", - 'hypervisor_version': 3, - 'hypervisor_hostname': "hyper1", - 'free_ram_mb': 5 * 1024, - 'free_disk_gb': 125, - 'current_workload': 2, - 'running_vms': 2, - 'cpu_info': 'cpu_info', - 'disk_available_least': 100}}) + return (200, {}, { + 'hypervisor': + {'id': 1234, + 'service': {'id': 1, 'host': 'compute1'}, + 'vcpus': 4, + 'memory_mb': 10 * 1024, + 'local_gb': 250, + 'vcpus_used': 2, + 'memory_mb_used': 5 * 1024, + 'local_gb_used': 125, + 'hypervisor_type': "xen", + 'hypervisor_version': 3, + 'hypervisor_hostname': "hyper1", + 'free_ram_mb': 5 * 1024, + 'free_disk_gb': 125, + 'current_workload': 2, + 'running_vms': 2, + 'cpu_info': 'cpu_info', + 'disk_available_least': 100}}) def get_os_hypervisors_1234_uptime(self, **kw): - return (200, {}, {'hypervisor': - {'id': 1234, + return (200, {}, { + 'hypervisor': {'id': 1234, 'hypervisor_hostname': "hyper1", 'uptime': "fake uptime"}}) @@ -1839,13 +1855,15 @@ def post_os_networks_2_action(self, **kw): return (202, {}, None) def get_os_availability_zone(self, **kw): - return (200, {}, {"availabilityZoneInfo": [ - {"zoneName": "zone-1", - "zoneState": {"available": True}, - "hosts": None}, - {"zoneName": "zone-2", - "zoneState": {"available": False}, - "hosts": None}]}) + return (200, {}, { + "availabilityZoneInfo": [ + {"zoneName": "zone-1", + "zoneState": {"available": True}, + "hosts": None}, + {"zoneName": "zone-2", + "zoneState": {"available": False}, + "hosts": None}] + }) def get_os_availability_zone_detail(self, **kw): return (200, {}, { @@ -1879,19 +1897,21 @@ def get_os_availability_zone_detail(self, **kw): "hosts": None}]}) def get_servers_1234_os_interface(self, **kw): - return (200, {}, {"interfaceAttachments": [ - {"port_state": "ACTIVE", - "net_id": "net-id-1", - "port_id": "port-id-1", - "mac_address": "aa:bb:cc:dd:ee:ff", - "fixed_ips": [{"ip_address": "1.2.3.4"}], - }, - {"port_state": "ACTIVE", - "net_id": "net-id-1", - "port_id": "port-id-1", - "mac_address": "aa:bb:cc:dd:ee:ff", - "fixed_ips": [{"ip_address": "1.2.3.4"}], - }]}) + return (200, {}, { + "interfaceAttachments": [ + {"port_state": "ACTIVE", + "net_id": "net-id-1", + "port_id": "port-id-1", + "mac_address": "aa:bb:cc:dd:ee:ff", + "fixed_ips": [{"ip_address": "1.2.3.4"}], + }, + {"port_state": "ACTIVE", + "net_id": "net-id-1", + "port_id": "port-id-1", + "mac_address": "aa:bb:cc:dd:ee:ff", + "fixed_ips": [{"ip_address": "1.2.3.4"}], + }] + }) def post_servers_1234_os_interface(self, **kw): return (200, {}, {'interfaceAttachment': {}}) @@ -1989,7 +2009,8 @@ def delete_volumes_15e59938_07d5_11e1_90e3_ee32ba30feaa(self, **kw): return (200, {}, {}) def post_servers_1234_os_volume_attachments(self, **kw): - return (200, {}, {"volumeAttachment": + return (200, {}, { + "volumeAttachment": {"device": "/dev/vdb", "volumeId": 2}}) @@ -1997,38 +2018,37 @@ def put_servers_1234_os_volume_attachments_Work(self, **kw): return (200, {}, {"volumeAttachment": {"volumeId": 2}}) def get_servers_1234_os_volume_attachments(self, **kw): - return (200, {}, {"volumeAttachments": [ - {"display_name": "Work", - "display_description": "volume for work", - "status": "ATTACHED", - "id": "15e59938-07d5-11e1-90e3-e3dffe0c5983", - "created_at": "2011-09-09T00:00:00Z", - "attached": "2011-11-11T00:00:00Z", - "size": 1024, - "attachments": [ - {"id": "3333", - "links": ''}], - "metadata": {}}]}) + return (200, {}, { + "volumeAttachments": [ + {"display_name": "Work", + "display_description": "volume for work", + "status": "ATTACHED", + "id": "15e59938-07d5-11e1-90e3-e3dffe0c5983", + "created_at": "2011-09-09T00:00:00Z", + "attached": "2011-11-11T00:00:00Z", + "size": 1024, + "attachments": [{"id": "3333", "links": ''}], + "metadata": {}}]}) def get_servers_1234_os_volume_attachments_Work(self, **kw): - return (200, {}, {"volumeAttachment": - {"display_name": "Work", - "display_description": "volume for work", - "status": "ATTACHED", - "id": "15e59938-07d5-11e1-90e3-e3dffe0c5983", - "created_at": "2011-09-09T00:00:00Z", - "attached": "2011-11-11T00:00:00Z", - "size": 1024, - "attachments": [ - {"id": "3333", - "links": ''}], - "metadata": {}}}) + return (200, {}, { + "volumeAttachment": + {"display_name": "Work", + "display_description": "volume for work", + "status": "ATTACHED", + "id": "15e59938-07d5-11e1-90e3-e3dffe0c5983", + "created_at": "2011-09-09T00:00:00Z", + "attached": "2011-11-11T00:00:00Z", + "size": 1024, + "attachments": [{"id": "3333", "links": ''}], + "metadata": {}}}) def delete_servers_1234_os_volume_attachments_Work(self, **kw): return (200, {}, {}) def get_servers_1234_os_instance_actions(self, **kw): - return (200, {}, {"instanceActions": + return (200, {}, { + "instanceActions": [{"instance_uuid": "1234", "user_id": "b968c25e04ab405f9fe4e6ca54cce9a5", "start_time": "2013-03-25T13:45:09.000000", @@ -2038,7 +2058,8 @@ def get_servers_1234_os_instance_actions(self, **kw): "project_id": "04019601fe3648c0abd4f4abfb9e6106"}]}) def get_servers_1234_os_instance_actions_req_abcde12345(self, **kw): - return (200, {}, {"instanceAction": + return (200, {}, { + "instanceAction": {"instance_uuid": "1234", "user_id": "b968c25e04ab405f9fe4e6ca54cce9a5", "start_time": "2013-03-25T13:45:09.000000", @@ -2061,18 +2082,18 @@ def post_servers_uuid4_action(self, **kw): def get_os_cells_child_cell(self, **kw): cell = {'cell': { - 'username': 'cell1_user', - 'name': 'cell1', - 'rpc_host': '10.0.1.10', - 'info': { - 'username': 'cell1_user', - 'rpc_host': '10.0.1.10', - 'type': 'child', - 'name': 'cell1', - 'rpc_port': 5673}, - 'type': 'child', - 'rpc_port': 5673, - 'loaded': True + 'username': 'cell1_user', + 'name': 'cell1', + 'rpc_host': '10.0.1.10', + 'info': { + 'username': 'cell1_user', + 'rpc_host': '10.0.1.10', + 'type': 'child', + 'name': 'cell1', + 'rpc_port': 5673}, + 'type': 'child', + 'rpc_port': 5673, + 'loaded': True }} return (200, {}, cell) @@ -2107,8 +2128,8 @@ def get_os_migrations(self, **kw): def post_os_server_external_events(self, **kw): return (200, {}, {'events': [ - {'name': 'network-changed', - 'server_uuid': '1234'}]}) + {'name': 'network-changed', + 'server_uuid': '1234'}]}) # # Server Groups diff --git a/novaclient/tests/v1_1/test_agents.py b/novaclient/tests/v1_1/test_agents.py index 1a580f5f1..424a52b79 100644 --- a/novaclient/tests/v1_1/test_agents.py +++ b/novaclient/tests/v1_1/test_agents.py @@ -77,12 +77,12 @@ def test_agents_create(self): 'add6bb58e139be103324d04d82d8f546', 'xen') body = {'agent': { - 'url': '/xxx/xxx/xxx', - 'hypervisor': 'xen', - 'md5hash': 'add6bb58e139be103324d04d82d8f546', - 'version': '7.0', - 'architecture': 'x86', - 'os': 'win'}} + 'url': '/xxx/xxx/xxx', + 'hypervisor': 'xen', + 'md5hash': 'add6bb58e139be103324d04d82d8f546', + 'version': '7.0', + 'architecture': 'x86', + 'os': 'win'}} self.assert_called('POST', '/os-agents', body) self.assertEqual(1, ag._info.copy()['id']) diff --git a/novaclient/tests/v3/fakes.py b/novaclient/tests/v3/fakes.py index 5e6f370dc..c125664ee 100644 --- a/novaclient/tests/v3/fakes.py +++ b/novaclient/tests/v3/fakes.py @@ -152,19 +152,20 @@ def head_v1_images_1(self, **kw): fakes_v1_1.FakeHTTPClient.delete_servers_1234_os_interface_port_id) def get_servers_1234_os_attach_interfaces(self, **kw): - return (200, {}, {"interface_attachments": [ - {"port_state": "ACTIVE", - "net_id": "net-id-1", - "port_id": "port-id-1", - "mac_address": "aa:bb:cc:dd:ee:ff", - "fixed_ips": [{"ip_address": "1.2.3.4"}], - }, - {"port_state": "ACTIVE", - "net_id": "net-id-1", - "port_id": "port-id-1", - "mac_address": "aa:bb:cc:dd:ee:ff", - "fixed_ips": [{"ip_address": "1.2.3.4"}], - }]}) + return (200, {}, { + "interface_attachments": [ + {"port_state": "ACTIVE", + "net_id": "net-id-1", + "port_id": "port-id-1", + "mac_address": "aa:bb:cc:dd:ee:ff", + "fixed_ips": [{"ip_address": "1.2.3.4"}], + }, + {"port_state": "ACTIVE", + "net_id": "net-id-1", + "port_id": "port-id-1", + "mac_address": "aa:bb:cc:dd:ee:ff", + "fixed_ips": [{"ip_address": "1.2.3.4"}]}] + }) def post_servers_1234_os_attach_interfaces(self, **kw): return (200, {}, {'interface_attachment': {}}) @@ -263,13 +264,12 @@ def delete_servers_1234_os_server_password(self, **kw): # Availability Zones # def get_os_availability_zone(self, **kw): - return (200, {}, {"availability_zone_info": [ - {"zone_name": "zone-1", - "zone_state": {"available": True}, - "hosts": None}, - {"zone_name": "zone-2", - "zone_state": {"available": False}, - "hosts": None}]}) + return (200, {}, { + "availability_zone_info": [ + {"zone_name": "zone-1", "zone_state": {"available": True}, + "hosts": None}, + {"zone_name": "zone-2", "zone_state": {"available": False}, + "hosts": None}]}) def get_os_availability_zone_detail(self, **kw): return (200, {}, { @@ -308,21 +308,22 @@ def get_os_availability_zone_detail(self, **kw): # def put_os_quota_sets_97f4c221bff44578b0300df4ef119353(self, body, **kw): assert list(body) == ['quota_set'] - return (200, {}, {'quota_set': { - 'tenant_id': '97f4c221bff44578b0300df4ef119353', - 'metadata_items': [], - 'injected_file_content_bytes': 1, - 'injected_file_path_bytes': 1, - 'ram': 1, - 'floating_ips': 1, - 'instances': 1, - 'injected_files': 1, - 'cores': 1, - 'keypairs': 1, - 'security_groups': 1, - 'security_group_rules': 1, - 'server_groups': 1, - 'server_group_members': 1}}) + return (200, {}, { + 'quota_set': { + 'tenant_id': '97f4c221bff44578b0300df4ef119353', + 'metadata_items': [], + 'injected_file_content_bytes': 1, + 'injected_file_path_bytes': 1, + 'ram': 1, + 'floating_ips': 1, + 'instances': 1, + 'injected_files': 1, + 'cores': 1, + 'keypairs': 1, + 'security_groups': 1, + 'security_group_rules': 1, + 'server_groups': 1, + 'server_group_members': 1}}) def get_os_quota_sets_test_detail(self, **kw): return (200, {}, {'quota_set': { @@ -337,17 +338,18 @@ def get_os_hypervisors_search(self, **kw): if kw['query'] == 'hyper1': return (200, {}, {'hypervisors': [ {'id': 1234, 'hypervisor_hostname': 'hyper1'}]}) - return (200, {}, {'hypervisors': [ - {'id': 1234, 'hypervisor_hostname': 'hyper1'}, - {'id': 5678, 'hypervisor_hostname': 'hyper2'}]}) + return (200, {}, { + 'hypervisors': [ + {'id': 1234, 'hypervisor_hostname': 'hyper1'}, + {'id': 5678, 'hypervisor_hostname': 'hyper2'}]}) def get_os_hypervisors_1234_servers(self, **kw): - return (200, {}, {'hypervisor': - {'id': 1234, - 'hypervisor_hostname': 'hyper1', - 'servers': [ - {'name': 'inst1', 'id': 'uuid1'}, - {'name': 'inst2', 'id': 'uuid2'}]}}) + return (200, {}, { + 'hypervisor': + {'id': 1234, 'hypervisor_hostname': 'hyper1', + 'servers': [ + {'name': 'inst1', 'id': 'uuid1'}, + {'name': 'inst2', 'id': 'uuid2'}]}}) # # Keypairs diff --git a/novaclient/utils.py b/novaclient/utils.py index cf1bc7b21..a9d56fdd6 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -224,9 +224,9 @@ def find_resource(manager, name_or_id, **find_args): kwargs.update(find_args) return manager.find(**kwargs) except exceptions.NotFound: - msg = _("No %(class)s with a name or ID of '%(name)s' exists.") % \ - {'class': manager.resource_class.__name__.lower(), - 'name': name_or_id} + msg = (_("No %(class)s with a name or ID of '%(name)s' exists.") % + {'class': manager.resource_class.__name__.lower(), + 'name': name_or_id}) raise exceptions.CommandError(msg) except exceptions.NoUniqueMatch: msg = (_("Multiple %(class)s matches found for '%(name)s', use an ID " diff --git a/novaclient/v1_1/agents.py b/novaclient/v1_1/agents.py index a58675ade..b5372cbd8 100644 --- a/novaclient/v1_1/agents.py +++ b/novaclient/v1_1/agents.py @@ -41,10 +41,9 @@ def list(self, hypervisor=None): return self._list(url, "agents") def _build_update_body(self, version, url, md5hash): - return {'para': { - 'version': version, - 'url': url, - 'md5hash': md5hash}} + return {'para': {'version': version, + 'url': url, + 'md5hash': md5hash}} def update(self, id, version, url, md5hash): @@ -55,13 +54,12 @@ def update(self, id, version, def create(self, os, architecture, version, url, md5hash, hypervisor): """Create a new agent build.""" - body = {'agent': { - 'hypervisor': hypervisor, - 'os': os, - 'architecture': architecture, - 'version': version, - 'url': url, - 'md5hash': md5hash}} + body = {'agent': {'hypervisor': hypervisor, + 'os': os, + 'architecture': architecture, + 'version': version, + 'url': url, + 'md5hash': md5hash}} return self._create('/os-agents', body, 'agent') def delete(self, id): diff --git a/novaclient/v1_1/certs.py b/novaclient/v1_1/certs.py index 232d7c1fd..2c3006c6f 100644 --- a/novaclient/v1_1/certs.py +++ b/novaclient/v1_1/certs.py @@ -24,9 +24,9 @@ class Certificate(base.Resource): def __repr__(self): - return "" % \ - (len(self.private_key) if self.private_key else 0, - len(self.data)) + return ("" % + (len(self.private_key) if self.private_key else 0, + len(self.data))) class CertificateManager(base.Manager): diff --git a/novaclient/v1_1/contrib/assisted_volume_snapshots.py b/novaclient/v1_1/contrib/assisted_volume_snapshots.py index 0f1773b49..ce1a0413c 100644 --- a/novaclient/v1_1/contrib/assisted_volume_snapshots.py +++ b/novaclient/v1_1/contrib/assisted_volume_snapshots.py @@ -42,7 +42,7 @@ def create(self, volume_id, create_info): def delete(self, snapshot, delete_info): self._delete("/os-assisted-volume-snapshots/%s?delete_info=%s" % ( - base.getid(snapshot), json.dumps(delete_info))) + base.getid(snapshot), json.dumps(delete_info))) manager_class = AssistedSnapshotManager name = 'assisted_volume_snapshots' diff --git a/novaclient/v1_1/flavors.py b/novaclient/v1_1/flavors.py index 86781847f..0545ad6df 100644 --- a/novaclient/v1_1/flavors.py +++ b/novaclient/v1_1/flavors.py @@ -55,8 +55,7 @@ def get_keys(self): :param flavor: The :class:`Flavor` to get extra specs from """ _resp, body = self.manager.api.client.get( - "/flavors/%s/os-extra_specs" % - base.getid(self)) + "/flavors/%s/os-extra_specs" % base.getid(self)) return body["extra_specs"] def set_keys(self, metadata): @@ -70,10 +69,8 @@ def set_keys(self, metadata): body = {'extra_specs': metadata} return self.manager._create( - "/flavors/%s/os-extra_specs" % base.getid(self), - body, - "extra_specs", - return_raw=True) + "/flavors/%s/os-extra_specs" % base.getid(self), body, + "extra_specs", return_raw=True) def unset_keys(self, keys): """ diff --git a/novaclient/v1_1/security_group_default_rules.py b/novaclient/v1_1/security_group_default_rules.py index ec9f63aa4..2627f9d61 100644 --- a/novaclient/v1_1/security_group_default_rules.py +++ b/novaclient/v1_1/security_group_default_rules.py @@ -54,10 +54,10 @@ def create(self, ip_protocol=None, from_port=None, to_port=None, ", or 'icmp'.")) body = {"security_group_default_rule": { - "ip_protocol": ip_protocol, - "from_port": from_port, - "to_port": to_port, - "cidr": cidr}} + "ip_protocol": ip_protocol, + "from_port": from_port, + "to_port": to_port, + "cidr": cidr}} return self._create('/os-security-group-default-rules', body, 'security_group_default_rule') diff --git a/novaclient/v1_1/security_group_rules.py b/novaclient/v1_1/security_group_rules.py index 1f12a4c1c..1f09a20d4 100644 --- a/novaclient/v1_1/security_group_rules.py +++ b/novaclient/v1_1/security_group_rules.py @@ -59,12 +59,12 @@ def create(self, parent_group_id, ip_protocol=None, from_port=None, ", or 'icmp'.")) body = {"security_group_rule": { - "ip_protocol": ip_protocol, - "from_port": from_port, - "to_port": to_port, - "cidr": cidr, - "group_id": group_id, - "parent_group_id": parent_group_id}} + "ip_protocol": ip_protocol, + "from_port": from_port, + "to_port": to_port, + "cidr": cidr, + "group_id": group_id, + "parent_group_id": parent_group_id}} return self._create('/os-security-group-rules', body, 'security_group_rule') diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index b75c95b9b..15171eec8 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -517,7 +517,7 @@ def _boot(self, resource_url, response_key, name, image, flavor, # Block device mappings are passed as a list of dictionaries if block_device_mapping: body['server']['block_device_mapping'] = \ - self._parse_block_device_mapping(block_device_mapping) + self._parse_block_device_mapping(block_device_mapping) elif block_device_mapping_v2: # Append the image to the list only if we have new style BDMs if image: diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 92e4541fc..e8cfffac6 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -294,19 +294,19 @@ def _boot(cs, args): config_drive = args.config_drive boot_kwargs = dict( - meta=meta, - files=files, - key_name=key_name, - min_count=min_count, - max_count=max_count, - userdata=userdata, - availability_zone=availability_zone, - security_groups=security_groups, - block_device_mapping=block_device_mapping, - block_device_mapping_v2=block_device_mapping_v2, - nics=nics, - scheduler_hints=hints, - config_drive=config_drive) + meta=meta, + files=files, + key_name=key_name, + min_count=min_count, + max_count=max_count, + userdata=userdata, + availability_zone=availability_zone, + security_groups=security_groups, + block_device_mapping=block_device_mapping, + block_device_mapping_v2=block_device_mapping_v2, + nics=nics, + scheduler_hints=hints, + config_drive=config_drive) return boot_args, boot_kwargs @@ -1209,19 +1209,19 @@ def do_list(cs, args): if args.tenant or args.user: args.all_tenants = 1 search_opts = { - 'all_tenants': args.all_tenants, - 'reservation_id': args.reservation_id, - 'ip': args.ip, - 'ip6': args.ip6, - 'name': args.name, - 'image': imageid, - 'flavor': flavorid, - 'status': args.status, - 'tenant_id': args.tenant, - 'user_id': args.user, - 'host': args.host, - 'deleted': args.deleted, - 'instance_name': args.instance_name} + 'all_tenants': args.all_tenants, + 'reservation_id': args.reservation_id, + 'ip': args.ip, + 'ip6': args.ip6, + 'name': args.name, + 'image': imageid, + 'flavor': flavorid, + 'status': args.status, + 'tenant_id': args.tenant, + 'user_id': args.user, + 'host': args.host, + 'deleted': args.deleted, + 'instance_name': args.instance_name} filters = {'flavor': lambda f: f['id'], 'security_groups': utils._format_security_groups} @@ -2281,7 +2281,7 @@ def do_dns_list(cs, args): """List current DNS entries for domain and ip or domain and name.""" if not (args.ip or args.name): raise exceptions.CommandError( - _("You must specify either --ip or --name")) + _("You must specify either --ip or --name")) if args.name: entry = cs.dns_entries.get(args.domain, args.name) _print_dns_list([entry]) @@ -2576,12 +2576,12 @@ def do_secgroup_delete_group_rule(cs, args): params['to_port'] = int(args.to_port) for rule in secgroup.rules: - if (rule.get('ip_protocol') and rule['ip_protocol'].upper() == - params.get('ip_protocol').upper() and - rule.get('from_port') == params.get('from_port') and - rule.get('to_port') == params.get('to_port') and - rule.get('group', {}).get('name') == - params.get('group_name')): + if (rule.get('ip_protocol') and + rule['ip_protocol'].upper() == params.get( + 'ip_protocol').upper() and + rule.get('from_port') == params.get('from_port') and + rule.get('to_port') == params.get('to_port') and + rule.get('group', {}).get('name') == params.get('group_name')): return cs.security_group_rules.delete(rule['id']) raise exceptions.CommandError(_("Rule not found")) diff --git a/novaclient/v1_1/usage.py b/novaclient/v1_1/usage.py index 05ce020a0..585ce1165 100644 --- a/novaclient/v1_1/usage.py +++ b/novaclient/v1_1/usage.py @@ -43,9 +43,9 @@ def list(self, start, end, detailed=False): :rtype: list of :class:`Usage`. """ return self._list( - "/os-simple-tenant-usage?start=%s&end=%s&detailed=%s" % - (start.isoformat(), end.isoformat(), int(bool(detailed))), - "tenant_usages") + "/os-simple-tenant-usage?start=%s&end=%s&detailed=%s" % + (start.isoformat(), end.isoformat(), int(bool(detailed))), + "tenant_usages") def get(self, tenant_id, start, end): """ diff --git a/novaclient/v3/agents.py b/novaclient/v3/agents.py index f26bba3fa..dacaf7671 100644 --- a/novaclient/v3/agents.py +++ b/novaclient/v3/agents.py @@ -29,6 +29,6 @@ class AgentsManager(agents.AgentsManager): def _build_update_body(self, version, url, md5hash): return {'agent': { - 'version': version, - 'url': url, - 'md5hash': md5hash}} + 'version': version, + 'url': url, + 'md5hash': md5hash}} diff --git a/novaclient/v3/flavors.py b/novaclient/v3/flavors.py index 6dcb1c434..19f4a3f28 100644 --- a/novaclient/v3/flavors.py +++ b/novaclient/v3/flavors.py @@ -45,8 +45,7 @@ def get_keys(self): :param flavor: The :class:`Flavor` to get extra specs from """ _resp, body = self.manager.api.client.get( - "/flavors/%s/flavor-extra-specs" % - base.getid(self)) + "/flavors/%s/flavor-extra-specs" % base.getid(self)) return body["extra_specs"] def set_keys(self, metadata): @@ -60,9 +59,8 @@ def set_keys(self, metadata): body = {'extra_specs': metadata} return self.manager._create( - "/flavors/%s/flavor-extra-specs" % - base.getid(self), body, "extra_specs", - return_raw=True) + "/flavors/%s/flavor-extra-specs" % base.getid(self), body, + "extra_specs", return_raw=True) def unset_keys(self, keys): """ diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 315303b1d..04ffcd87c 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -201,18 +201,18 @@ def _boot(cs, args): config_drive = args.config_drive boot_kwargs = dict( - meta=meta, - files=files, - key_name=key_name, - min_count=min_count, - max_count=max_count, - userdata=userdata, - availability_zone=availability_zone, - security_groups=security_groups, - block_device_mapping=block_device_mapping, - nics=nics, - scheduler_hints=hints, - config_drive=config_drive) + meta=meta, + files=files, + key_name=key_name, + min_count=min_count, + max_count=max_count, + userdata=userdata, + availability_zone=availability_zone, + security_groups=security_groups, + block_device_mapping=block_device_mapping, + nics=nics, + scheduler_hints=hints, + config_drive=config_drive) return boot_args, boot_kwargs @@ -980,18 +980,18 @@ def do_list(cs, args): if args.flavor: flavorid = _find_flavor(cs, args.flavor).id search_opts = { - 'all_tenants': args.all_tenants, - 'reservation_id': args.reservation_id, - 'ip': args.ip, - 'ip6': args.ip6, - 'name': args.name, - 'image': imageid, - 'flavor': flavorid, - 'status': args.status, - 'tenant_id': args.tenant, - 'host': args.host, - 'deleted': args.deleted, - 'instance_name': args.instance_name} + 'all_tenants': args.all_tenants, + 'reservation_id': args.reservation_id, + 'ip': args.ip, + 'ip6': args.ip6, + 'name': args.name, + 'image': imageid, + 'flavor': flavorid, + 'status': args.status, + 'tenant_id': args.tenant, + 'host': args.host, + 'deleted': args.deleted, + 'instance_name': args.instance_name} filters = {'flavor': lambda f: f['id'], 'security_groups': utils._format_security_groups} @@ -1696,8 +1696,7 @@ def do_dns_domains(cs, args): def do_dns_list(cs, args): """List current DNS entries for domain and ip or domain and name.""" if not (args.ip or args.name): - raise exceptions.CommandError( - "You must specify either --ip or --name") + raise exceptions.CommandError("You must specify either --ip or --name") if args.name: entry = cs.dns_entries.get(args.domain, args.name) _print_dns_list([entry]) @@ -1988,11 +1987,10 @@ def do_secgroup_delete_group_rule(cs, args): for rule in secgroup.rules: if (rule.get('ip_protocol').upper() == params.get( - 'ip_protocol').upper() and - rule.get('from_port') == params.get('from_port') and - rule.get('to_port') == params.get('to_port') and - rule.get('group', {}).get('name') == - params.get('group_name')): + 'ip_protocol').upper() and + rule.get('from_port') == params.get('from_port') and + rule.get('to_port') == params.get('to_port') and + rule.get('group', {}).get('name') == params.get('group_name')): return cs.security_group_rules.delete(rule['id']) raise exceptions.CommandError("Rule not found") @@ -3108,9 +3106,9 @@ def _treeizeAvailabilityZone(zone): copy.deepcopy(zone._info), zone._loaded) az.zone_name = '| |- %s' % svc az.zone_state = '%s %s %s' % ( - 'enabled' if state['active'] else 'disabled', - ':-)' if state['available'] else 'XXX', - state['updated_at']) + 'enabled' if state['active'] else 'disabled', + ':-)' if state['available'] else 'XXX', + state['updated_at']) az._info['zone_name'] = az.zone_name az._info['zone_state'] = az.zone_state result.append(az) diff --git a/tox.ini b/tox.ini index 4b317deb6..aa190f09c 100644 --- a/tox.ini +++ b/tox.ini @@ -42,7 +42,7 @@ downloadcache = ~/cache/pip # reason: removed in hacking (https://review.openstack.org/#/c/101701/) # # Additional checks are also ignored on purpose: F811, F821 -ignore = E124,E126,E127,E128,E129,F811,F821,H402,H404,H405,H904 +ignore = E124,E127,E128,E129,F811,F821,H402,H404,H405,H904 show-source = True exclude=.venv,.git,.tox,dist,*openstack/common*,*lib/python*,*egg,build,doc/source/conf.py From ae6c39397e9ae60a4e987d0f1af3086b70019571 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Wed, 24 Sep 2014 23:45:27 +0300 Subject: [PATCH 0635/1705] Enable check for E127 E127 continuation line over-indented for visual indent Change-Id: I9dfd489565ee2d1469b2048a930c3a8384dd245d --- novaclient/service_catalog.py | 4 +- novaclient/shell.py | 7 ++- novaclient/tests/fakes.py | 11 ++-- novaclient/tests/fixture_data/servers.py | 4 +- novaclient/tests/test_client.py | 25 ++++---- novaclient/tests/v1_1/fakes.py | 59 +++++++++---------- .../tests/v1_1/test_floating_ip_pools.py | 4 +- .../tests/v1_1/test_floating_ips_bulk.py | 6 +- novaclient/tests/v1_1/test_shell.py | 15 ++--- novaclient/tests/v3/fakes.py | 6 +- novaclient/v1_1/flavor_access.py | 2 +- novaclient/v1_1/floating_ip_dns.py | 39 ++++-------- novaclient/v1_1/images.py | 4 +- novaclient/v1_1/servers.py | 2 +- novaclient/v1_1/shell.py | 19 +++--- novaclient/v1_1/volume_snapshots.py | 4 +- novaclient/v1_1/volumes.py | 21 ++++--- novaclient/v3/servers.py | 6 +- novaclient/v3/shell.py | 24 ++++---- tox.ini | 2 +- 20 files changed, 120 insertions(+), 144 deletions(-) diff --git a/novaclient/service_catalog.py b/novaclient/service_catalog.py index 84effda7f..6883f537b 100644 --- a/novaclient/service_catalog.py +++ b/novaclient/service_catalog.py @@ -32,8 +32,8 @@ def get_tenant_id(self): return self.catalog['access']['token']['tenant']['id'] def url_for(self, attr=None, filter_value=None, - service_type=None, endpoint_type='publicURL', - service_name=None, volume_service_name=None): + service_type=None, endpoint_type='publicURL', + service_name=None, volume_service_name=None): """Fetch the public URL from the Compute service for a particular endpoint attribute. If none given, return the first. See tests for sample service catalog. diff --git a/novaclient/shell.py b/novaclient/shell.py index b92022a00..3c7950529 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -765,8 +765,11 @@ def do_bash_completion(self, _args): commands.remove('bash_completion') print(' '.join(commands | options)) - @utils.arg('command', metavar='', nargs='?', - help='Display help for ') + @utils.arg( + 'command', + metavar='', + nargs='?', + help='Display help for ') def do_help(self, args): """ Display help about this program or one of its subcommands. diff --git a/novaclient/tests/fakes.py b/novaclient/tests/fakes.py index c73f72f91..f3f5274bc 100644 --- a/novaclient/tests/fakes.py +++ b/novaclient/tests/fakes.py @@ -43,10 +43,10 @@ def assert_called(self, method, url, body=None, pos=-1): called = self.client.callstack[pos][0:2] assert self.client.callstack, \ - "Expected %s %s but no calls were made." % expected + "Expected %s %s but no calls were made." % expected - assert expected == called, 'Expected %s %s; got %s %s' % \ - (expected + called) + assert expected == called, \ + 'Expected %s %s; got %s %s' % (expected + called) if body is not None: if self.client.callstack[pos][2] != body: @@ -60,7 +60,7 @@ def assert_called_anytime(self, method, url, body=None): expected = (method, url) assert self.client.callstack, \ - "Expected %s %s but no calls were made." % expected + "Expected %s %s but no calls were made." % expected found = False for entry in self.client.callstack: @@ -68,8 +68,7 @@ def assert_called_anytime(self, method, url, body=None): found = True break - assert found, 'Expected %s; got %s' % \ - (expected, self.client.callstack) + assert found, 'Expected %s; got %s' % (expected, self.client.callstack) if body is not None: try: assert entry[2] == body diff --git a/novaclient/tests/fixture_data/servers.py b/novaclient/tests/fixture_data/servers.py index 5e0edf148..b226413c5 100644 --- a/novaclient/tests/fixture_data/servers.py +++ b/novaclient/tests/fixture_data/servers.py @@ -614,8 +614,8 @@ def post_servers_1234_action(self, request, context): context.headers['location'] = "http://blah/images/456" if action not in set.union(set(body_is_none_list), - set(body_params_check_exact.keys()), - set(body_param_check_exists.keys())): + set(body_params_check_exact.keys()), + set(body_param_check_exists.keys())): raise AssertionError("Unexpected server action: %s" % action) return _body diff --git a/novaclient/tests/test_client.py b/novaclient/tests/test_client.py index 16d62a5b9..90a6773ec 100644 --- a/novaclient/tests/test_client.py +++ b/novaclient/tests/test_client.py @@ -57,10 +57,9 @@ def test_client_with_timeout(self): } with mock.patch('requests.request', mock_request): instance.authenticate() - requests.request.assert_called_with(mock.ANY, mock.ANY, - timeout=2, - headers=mock.ANY, - verify=mock.ANY) + requests.request.assert_called_with( + mock.ANY, mock.ANY, timeout=2, headers=mock.ANY, + verify=mock.ANY) def test_client_reauth(self): instance = novaclient.client.HTTPClient(user='user', @@ -208,7 +207,7 @@ def test_client_set_management_url_v3(self): def test_client_get_reset_timings_v3(self): cs = novaclient.v3.client.Client("user", "password", "project_id", - auth_url="foo/v2") + auth_url="foo/v2") self.assertEqual(0, len(cs.get_timings())) cs.client.times.append("somevalue") self.assertEqual(["somevalue"], cs.get_timings()) @@ -220,15 +219,13 @@ def test_clent_extensions_v3(self): fake_attribute_name1 = "FakeAttribute1" fake_attribute_name2 = "FakeAttribute2" extensions = [ - novaclient.extension.Extension(fake_attribute_name1, - fakes), - novaclient.extension.Extension(fake_attribute_name2, - utils), + novaclient.extension.Extension(fake_attribute_name1, fakes), + novaclient.extension.Extension(fake_attribute_name2, utils), ] cs = novaclient.v3.client.Client("user", "password", "project_id", - auth_url="foo/v2", - extensions=extensions) + auth_url="foo/v2", + extensions=extensions) self.assertIsInstance(getattr(cs, fake_attribute_name1, None), fakes.FakeManager) self.assertFalse(hasattr(cs, fake_attribute_name2)) @@ -236,7 +233,7 @@ def test_clent_extensions_v3(self): @mock.patch.object(novaclient.client.HTTPClient, 'authenticate') def test_authenticate_call_v3(self, mock_authenticate): cs = novaclient.v3.client.Client("user", "password", "project_id", - auth_url="foo/v2") + auth_url="foo/v2") cs.authenticate() self.assertTrue(mock_authenticate.called) @@ -245,7 +242,7 @@ def test_contextmanager_v1_1(self, mock_http_client): fake_client = mock.Mock() mock_http_client.return_value = fake_client with novaclient.v1_1.client.Client("user", "password", "project_id", - auth_url="foo/v2"): + auth_url="foo/v2"): pass self.assertTrue(fake_client.open_session.called) self.assertTrue(fake_client.close_session.called) @@ -255,7 +252,7 @@ def test_contextmanager_v3(self, mock_http_client): fake_client = mock.Mock() mock_http_client.return_value = fake_client with novaclient.v3.client.Client("user", "password", "project_id", - auth_url="foo/v2"): + auth_url="foo/v2"): pass self.assertTrue(fake_client.open_session.called) self.assertTrue(fake_client.close_session.called) diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index 667f9edad..dfc7077ac 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -879,11 +879,11 @@ def post_os_floating_ips(self, body): if body.get('pool'): return (200, {}, {'floating_ip': {'id': 1, 'fixed_ip': '10.0.0.1', 'ip': '11.0.0.1', - 'pool': 'nova'}}) + 'pool': 'nova'}}) else: return (200, {}, {'floating_ip': {'id': 1, 'fixed_ip': '10.0.0.1', 'ip': '11.0.0.1', - 'pool': None}}) + 'pool': None}}) def delete_os_floating_ips_1(self, **kw): return (204, {}, None) @@ -895,43 +895,42 @@ def get_os_floating_ip_dns(self, **kw): def get_os_floating_ip_dns_testdomain_entries(self, **kw): if kw.get('ip'): - return (205, {}, {'dns_entries': - [{'dns_entry': - {'ip': kw.get('ip'), - 'name': "host1", - 'type': "A", - 'domain': 'testdomain'}}, - {'dns_entry': - {'ip': kw.get('ip'), - 'name': "host2", - 'type': "A", - 'domain': 'testdomain'}}]}) + return (205, {}, { + 'dns_entries': [ + {'dns_entry': {'ip': kw.get('ip'), + 'name': "host1", + 'type': "A", + 'domain': 'testdomain'}}, + {'dns_entry': {'ip': kw.get('ip'), + 'name': "host2", + 'type': "A", + 'domain': 'testdomain'}}]}) else: return (404, {}, None) def get_os_floating_ip_dns_testdomain_entries_testname(self, **kw): - return (205, {}, {'dns_entry': - {'ip': "10.10.10.10", - 'name': 'testname', - 'type': "A", - 'domain': 'testdomain'}}) + return (205, {}, { + 'dns_entry': {'ip': "10.10.10.10", + 'name': 'testname', + 'type': "A", + 'domain': 'testdomain'}}) def put_os_floating_ip_dns_testdomain(self, body, **kw): if body['domain_entry']['scope'] == 'private': fakes.assert_has_keys(body['domain_entry'], - required=['availability_zone', 'scope']) + required=['availability_zone', 'scope']) elif body['domain_entry']['scope'] == 'public': fakes.assert_has_keys(body['domain_entry'], - required=['project', 'scope']) + required=['project', 'scope']) else: fakes.assert_has_keys(body['domain_entry'], - required=['project', 'scope']) + required=['project', 'scope']) return (205, {}, body) def put_os_floating_ip_dns_testdomain_entries_testname(self, body, **kw): fakes.assert_has_keys(body['dns_entry'], - required=['ip', 'dns_type']) + required=['ip', 'dns_type']) return (205, {}, body) def delete_os_floating_ip_dns_testdomain(self, **kw): @@ -1271,8 +1270,7 @@ def get_os_security_groups(self, **kw): return (200, {}, {"security_groups": [ {"name": "test", "description": "FAKE_SECURITY_GROUP", - "tenant_id": - "4ffc664c198e435e9853f2538fbcd7a7", + "tenant_id": "4ffc664c198e435e9853f2538fbcd7a7", "id": 1, "rules": [ {"id": 11, @@ -1330,7 +1328,7 @@ def post_os_security_groups(self, body, **kw): fakes.assert_has_keys(body['security_group'], required=['name', 'description']) r = {'security_group': - self.get_os_security_groups()[2]['security_groups'][0]} + self.get_os_security_groups()[2]['security_groups'][0]} return (202, {}, r) def put_os_security_groups_1(self, body, **kw): @@ -1363,12 +1361,13 @@ def delete_os_security_group_rules_14(self, **kw): def post_os_security_group_rules(self, body, **kw): assert list(body) == ['security_group_rule'] - fakes.assert_has_keys(body['security_group_rule'], + fakes.assert_has_keys( + body['security_group_rule'], required=['parent_group_id'], optional=['group_id', 'ip_protocol', 'from_port', 'to_port', 'cidr']) r = {'security_group_rule': - self.get_os_security_group_rules()[2]['security_group_rules'][0]} + self.get_os_security_group_rules()[2]['security_group_rules'][0]} return (202, {}, r) # @@ -2160,12 +2159,10 @@ def _return_server_group(self): def post_os_server_groups(self, body, **kw): return self._return_server_group() - def get_os_server_groups_2cbd51f4_fafe_4cdb_801b_cf913a6f288b(self, - **kw): + def get_os_server_groups_2cbd51f4_fafe_4cdb_801b_cf913a6f288b(self, **kw): return self._return_server_group() - def put_os_server_groups_2cbd51f4_fafe_4cdb_801b_cf913a6f288b(self, - **kw): + def put_os_server_groups_2cbd51f4_fafe_4cdb_801b_cf913a6f288b(self, **kw): return self._return_server_group() def post_os_server_groups_2cbd51f4_fafe_4cdb_801b_cf913a6f288b_action( diff --git a/novaclient/tests/v1_1/test_floating_ip_pools.py b/novaclient/tests/v1_1/test_floating_ip_pools.py index efdfabf30..f2d0c8fdc 100644 --- a/novaclient/tests/v1_1/test_floating_ip_pools.py +++ b/novaclient/tests/v1_1/test_floating_ip_pools.py @@ -28,5 +28,5 @@ class TestFloatingIPPools(utils.FixturedTestCase): def test_list_floating_ips(self): fl = self.cs.floating_ip_pools.list() self.assert_called('GET', '/os-floating-ip-pools') - [self.assertIsInstance(f, floating_ip_pools.FloatingIPPool) - for f in fl] + for f in fl: + self.assertIsInstance(f, floating_ip_pools.FloatingIPPool) diff --git a/novaclient/tests/v1_1/test_floating_ips_bulk.py b/novaclient/tests/v1_1/test_floating_ips_bulk.py index dd4f823c7..ac21c1cbf 100644 --- a/novaclient/tests/v1_1/test_floating_ips_bulk.py +++ b/novaclient/tests/v1_1/test_floating_ips_bulk.py @@ -46,9 +46,9 @@ def test_create_floating_ips_bulk(self): def test_create_floating_ips_bulk_with_pool_and_host(self): fl = self.cs.floating_ips_bulk.create('192.168.1.0/30', 'poolTest', 'interfaceTest') - body = {'floating_ips_bulk_create': - {'ip_range': '192.168.1.0/30', 'pool': 'poolTest', - 'interface': 'interfaceTest'}} + body = {'floating_ips_bulk_create': { + 'ip_range': '192.168.1.0/30', 'pool': 'poolTest', + 'interface': 'interfaceTest'}} self.assert_called('POST', '/os-floating-ips-bulk', body) self.assertEqual(fl.ip_range, body['floating_ips_bulk_create']['ip_range']) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index cce55495d..3fef327c9 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -800,8 +800,8 @@ def test_create_image(self): ) def test_create_image_show(self): - output = self.run_command('image-create ' - 'sample-server mysnapshot --show') + output = self.run_command( + 'image-create sample-server mysnapshot --show') self.assert_called_anytime( 'POST', '/servers/1234/action', {'createImage': {'name': 'mysnapshot', 'metadata': {}}}, @@ -2243,10 +2243,8 @@ def test_keypair_add(self): mock.mock_open(read_data='FAKE_PUBLIC_KEY')) def test_keypair_import(self): self.run_command('keypair-add --pub-key test.pub test') - self.assert_called('POST', '/os-keypairs', - {'keypair': - {'public_key': 'FAKE_PUBLIC_KEY', - 'name': 'test'}}) + self.assert_called('POST', '/os-keypairs', { + 'keypair': {'public_key': 'FAKE_PUBLIC_KEY', 'name': 'test'}}) def test_keypair_list(self): self.run_command('keypair-list') @@ -2263,9 +2261,8 @@ def test_keypair_delete(self): def test_create_server_group(self): self.run_command('server-group-create wjsg affinity') self.assert_called('POST', '/os-server-groups', - {'server_group': - {'name': 'wjsg', - 'policies': ['affinity']}}) + {'server_group': {'name': 'wjsg', + 'policies': ['affinity']}}) def test_delete_multi_server_groups(self): self.run_command('server-group-delete 12345 56789') diff --git a/novaclient/tests/v3/fakes.py b/novaclient/tests/v3/fakes.py index c125664ee..4fbd1d317 100644 --- a/novaclient/tests/v3/fakes.py +++ b/novaclient/tests/v3/fakes.py @@ -243,9 +243,9 @@ def post_servers_1234_action(self, body, **kw): _headers = dict(location="http://blah/images/456") if action not in set.union(set(body_is_none_list), - set(body_params_check_exact.keys()), - set(body_param_check_exists.keys()), - set(body_params_check_superset.keys())): + set(body_params_check_exact.keys()), + set(body_param_check_exists.keys()), + set(body_params_check_superset.keys())): raise AssertionError("Unexpected server action: %s" % action) return (resp, _headers, _body) diff --git a/novaclient/v1_1/flavor_access.py b/novaclient/v1_1/flavor_access.py index fdc43cd7a..ca78bdff7 100644 --- a/novaclient/v1_1/flavor_access.py +++ b/novaclient/v1_1/flavor_access.py @@ -67,4 +67,4 @@ def _action(self, action, flavor, info, **kwargs): _resp, body = self.api.client.post(url, body=body) return [self.resource_class(self, res) - for res in body['flavor_access']] + for res in body['flavor_access']] diff --git a/novaclient/v1_1/floating_ip_dns.py b/novaclient/v1_1/floating_ip_dns.py index 449c7e11e..b980f7721 100644 --- a/novaclient/v1_1/floating_ip_dns.py +++ b/novaclient/v1_1/floating_ip_dns.py @@ -56,22 +56,18 @@ def domains(self): def create_private(self, fqdomain, availability_zone): """Add or modify a private DNS domain.""" - body = {'domain_entry': - {'scope': 'private', - 'availability_zone': availability_zone}} + body = {'domain_entry': {'scope': 'private', + 'availability_zone': availability_zone}} return self._update('/os-floating-ip-dns/%s' % _quote_domain(fqdomain), body, 'domain_entry') def create_public(self, fqdomain, project): """Add or modify a public DNS domain.""" - body = {'domain_entry': - {'scope': 'public', - 'project': project}} + body = {'domain_entry': {'scope': 'public', 'project': project}} return self._update('/os-floating-ip-dns/%s' % _quote_domain(fqdomain), - body, - 'domain_entry') + body, 'domain_entry') def delete(self, fqdomain): """Delete the specified domain.""" @@ -83,8 +79,7 @@ def delete(self): self.manager.delete(self.name, self.domain) def create(self): - self.manager.create(self.domain, self.name, - self.ip, self.dns_type) + self.manager.create(self.domain, self.name, self.ip, self.dns_type) def get(self): return self.manager.get(self.domain, self.name) @@ -96,8 +91,7 @@ class FloatingIPDNSEntryManager(base.Manager): def get(self, domain, name): """Return a list of entries for the given domain and ip or name.""" return self._get("/os-floating-ip-dns/%s/entries/%s" % - (_quote_domain(domain), name), - "dns_entry") + (_quote_domain(domain), name), "dns_entry") def get_for_ip(self, domain, ip): """Return a list of entries for the given domain and ip or name.""" @@ -105,32 +99,23 @@ def get_for_ip(self, domain, ip): params = "?%s" % parse.urlencode(qparams) return self._list("/os-floating-ip-dns/%s/entries%s" % - (_quote_domain(domain), params), - "dns_entries") + (_quote_domain(domain), params), "dns_entries") def create(self, domain, name, ip, dns_type): """Add a new DNS entry.""" - body = {'dns_entry': - {'ip': ip, - 'dns_type': dns_type}} + body = {'dns_entry': {'ip': ip, 'dns_type': dns_type}} return self._update("/os-floating-ip-dns/%s/entries/%s" % - (_quote_domain(domain), name), - body, - "dns_entry") + (_quote_domain(domain), name), body, "dns_entry") def modify_ip(self, domain, name, ip): """Add a new DNS entry.""" - body = {'dns_entry': - {'ip': ip, - 'dns_type': 'A'}} + body = {'dns_entry': {'ip': ip, 'dns_type': 'A'}} return self._update("/os-floating-ip-dns/%s/entries/%s" % - (_quote_domain(domain), name), - body, - "dns_entry") + (_quote_domain(domain), name), body, "dns_entry") def delete(self, domain, name): """Delete entry specified by name and domain.""" self._delete("/os-floating-ip-dns/%s/entries/%s" % - (_quote_domain(domain), name)) + (_quote_domain(domain), name)) diff --git a/novaclient/v1_1/images.py b/novaclient/v1_1/images.py index f355b894f..2d413a6bf 100644 --- a/novaclient/v1_1/images.py +++ b/novaclient/v1_1/images.py @@ -87,8 +87,8 @@ def set_meta(self, image, metadata): :param metadata: A dict of metadata to add to the image """ body = {'metadata': metadata} - return self._create("/images/%s/metadata" % base.getid(image), body, - "metadata") + return self._create("/images/%s/metadata" % base.getid(image), + body, "metadata") def delete_meta(self, image, keys): """ diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index 15171eec8..da0a74972 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -1086,7 +1086,7 @@ def set_meta(self, server, metadata): """ body = {'metadata': metadata} return self._create("/servers/%s/metadata" % base.getid(server), - body, "metadata") + body, "metadata") def set_meta_item(self, server, key, value): """ diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index e8cfffac6..9daeb61c9 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2367,8 +2367,8 @@ def _print_secgroups(secgroups): def _get_secgroup(cs, secgroup): # Check secgroup is an ID (nova-network) or UUID (neutron) - if (utils.is_integer_like(encodeutils.safe_encode(secgroup)) - or uuidutils.is_uuid_like(secgroup)): + if (utils.is_integer_like(encodeutils.safe_encode(secgroup)) or + uuidutils.is_uuid_like(secgroup)): try: return cs.security_groups.get(secgroup) except exceptions.NotFound: @@ -2377,9 +2377,8 @@ def _get_secgroup(cs, secgroup): # Check secgroup as a name match_found = False for s in cs.security_groups.list(): - encoding = (locale.getpreferredencoding() or - sys.stdin.encoding or - 'UTF-8') + encoding = ( + locale.getpreferredencoding() or sys.stdin.encoding or 'UTF-8') if not six.PY3: s.name = s.name.encode(encoding) if secgroup == s.name: @@ -3379,8 +3378,8 @@ def do_ssh(cs, args): match = lambda addr: all(( addr.get('version') == version, addr.get('OS-EXT-IPS:type', 'floating') == address_type)) - matching_addresses = [address.get('addr') for address in network_addresses - if match(address)] + matching_addresses = [address.get('addr') + for address in network_addresses if match(address)] if not any(matching_addresses): msg = _("No address that would match network '%(network)s'" " and type '%(address_type)s' of version %(pretty_version)s " @@ -3568,8 +3567,8 @@ def do_quota_defaults(cs, args): dest='force', action="store_true", default=None, - help=_('Whether force update the quota even if the already used' - ' and reserved exceeds the new quota')) + help=_('Whether force update the quota even if the already used and ' + 'reserved exceeds the new quota')) def do_quota_update(cs, args): """Update the quotas for a tenant/user.""" @@ -3902,7 +3901,7 @@ def do_secgroup_delete_default_rule(cs, args): default=argparse.SUPPRESS, nargs='*', help='Policies for the server groups ' - '("affinity" or "anti-affinity")') + '("affinity" or "anti-affinity")') @utils.arg('--policy', default=[], action='append', diff --git a/novaclient/v1_1/volume_snapshots.py b/novaclient/v1_1/volume_snapshots.py index b30a60a82..1cc72ee1b 100644 --- a/novaclient/v1_1/volume_snapshots.py +++ b/novaclient/v1_1/volume_snapshots.py @@ -42,8 +42,8 @@ class SnapshotManager(base.ManagerWithFind): """ resource_class = Snapshot - def create(self, volume_id, force=False, - display_name=None, display_description=None): + def create(self, volume_id, force=False, display_name=None, + display_description=None): """ Create a snapshot of the given volume. diff --git a/novaclient/v1_1/volumes.py b/novaclient/v1_1/volumes.py index 77746dd30..4ff92a093 100644 --- a/novaclient/v1_1/volumes.py +++ b/novaclient/v1_1/volumes.py @@ -45,10 +45,9 @@ class VolumeManager(base.ManagerWithFind): """ resource_class = Volume - def create(self, size, snapshot_id=None, - display_name=None, display_description=None, - volume_type=None, availability_zone=None, - imageRef=None): + def create(self, size, snapshot_id=None, display_name=None, + display_description=None, volume_type=None, + availability_zone=None, imageRef=None): """ Create a volume. @@ -62,12 +61,12 @@ def create(self, size, snapshot_id=None, :param imageRef: reference to an image stored in glance """ body = {'volume': {'size': size, - 'snapshot_id': snapshot_id, - 'display_name': display_name, - 'display_description': display_description, - 'volume_type': volume_type, - 'availability_zone': availability_zone, - 'imageRef': imageRef}} + 'snapshot_id': snapshot_id, + 'display_name': display_name, + 'display_description': display_description, + 'volume_type': volume_type, + 'availability_zone': availability_zone, + 'imageRef': imageRef}} return self._create('/volumes', body, 'volume') def get(self, volume_id): @@ -162,4 +161,4 @@ def delete_server_volume(self, server_id, attachment_id): :param attachment_id: The ID of the attachment """ self._delete("/servers/%s/os-volume_attachments/%s" % - (server_id, attachment_id,)) + (server_id, attachment_id,)) diff --git a/novaclient/v3/servers.py b/novaclient/v3/servers.py index ce2c82058..bd188804d 100644 --- a/novaclient/v3/servers.py +++ b/novaclient/v3/servers.py @@ -440,7 +440,7 @@ def _boot(self, resource_url, response_key, name, image, flavor, if block_device_mapping: bdm_param = 'block_device_mapping' body['server'][bdm_param] = \ - self._parse_block_device_mapping(block_device_mapping) + self._parse_block_device_mapping(block_device_mapping) elif block_device_mapping_v2: # Append the image to the list only if we have new style BDMs bdm_param = 'block_device_mapping_v2' @@ -893,8 +893,8 @@ def set_meta(self, server, metadata): :param metadata: A dict of metadata to add to the server """ body = {'metadata': metadata} - return self._create("/servers/%s/metadata" % base.getid(server), - body, "metadata") + return self._create( + "/servers/%s/metadata" % base.getid(server), body, "metadata") def get_console_output(self, server, length=None): """ diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 04ffcd87c..d06f8d09b 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -94,8 +94,8 @@ def _boot(cs, args): max_count = args.num_instances elif (args.num_instances is not None and (args.min_count is not None or args.max_count is not None)): - raise exceptions.CommandError("Don't mix num-instances and " - "max/min-count") + raise exceptions.CommandError( + "Don't mix num-instances and max/min-count") if args.min_count is not None: if args.min_count < 1: raise exceptions.CommandError("min_count should be >= 1") @@ -1945,8 +1945,8 @@ def do_secgroup_add_group_rule(cs, args): if args.ip_proto or args.from_port or args.to_port: if not (args.ip_proto and args.from_port and args.to_port): - raise exceptions.CommandError("ip_proto, from_port, and to_port" - " must be specified together") + raise exceptions.CommandError( + "ip_proto, from_port, and to_port must be specified together") params['ip_protocol'] = args.ip_proto.upper() params['from_port'] = args.from_port params['to_port'] = args.to_port @@ -1979,8 +1979,8 @@ def do_secgroup_delete_group_rule(cs, args): if args.ip_proto or args.from_port or args.to_port: if not (args.ip_proto and args.from_port and args.to_port): - raise exceptions.CommandError("ip_proto, from_port, and to_port" - " must be specified together") + raise exceptions.CommandError( + "ip_proto, from_port, and to_port must be specified together") params['ip_protocol'] = args.ip_proto.upper() params['from_port'] = int(args.from_port) params['to_port'] = int(args.to_port) @@ -2013,8 +2013,8 @@ def do_keypair_add(cs, args): with open(os.path.expanduser(pub_key)) as f: pub_key = f.read() except IOError as e: - raise exceptions.CommandError("Can't open or read '%s': %s" % - (pub_key, e)) + raise exceptions.CommandError( + "Can't open or read '%s': %s" % (pub_key, e)) keypair = cs.keypairs.create(name, pub_key) @@ -2822,8 +2822,8 @@ def do_ssh(cs, args): match = lambda addr: all(( addr.get('version') == version, addr.get('OS-EXT-IPS:type', 'floating') == address_type)) - matching_addresses = [address.get('addr') for address in network_addresses - if match(address)] + matching_addresses = [address.get('addr') + for address in network_addresses if match(address)] if not any(matching_addresses): msg = _("No address that would match network '%(network)s'" " and type '%(address_type)s' of version %(pretty_version)s " @@ -2980,8 +2980,8 @@ def do_quota_defaults(cs, args): dest='force', action="store_true", default=None, - help='Whether force update the quota even if the already used' - ' and reserved exceeds the new quota') + help='Whether force update the quota even if the already used and ' + 'reserved exceeds the new quota') def do_quota_update(cs, args): """Update the quotas for a tenant.""" diff --git a/tox.ini b/tox.ini index aa190f09c..3afa423f3 100644 --- a/tox.ini +++ b/tox.ini @@ -42,7 +42,7 @@ downloadcache = ~/cache/pip # reason: removed in hacking (https://review.openstack.org/#/c/101701/) # # Additional checks are also ignored on purpose: F811, F821 -ignore = E124,E127,E128,E129,F811,F821,H402,H404,H405,H904 +ignore = E124,E128,E129,F811,F821,H402,H404,H405,H904 show-source = True exclude=.venv,.git,.tox,dist,*openstack/common*,*lib/python*,*egg,build,doc/source/conf.py From 3065afd6c0512bce05a642f74d3d15867d586120 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Wed, 24 Sep 2014 23:55:54 +0300 Subject: [PATCH 0636/1705] Enable check for E129 E129 visually indented line with same indent as next logical line Change-Id: I438bf0d9de7f61ab65e753169c9a8007e84b215a --- novaclient/client.py | 2 +- novaclient/shell.py | 2 +- novaclient/tests/test_shell.py | 4 ++-- novaclient/v1_1/servers.py | 2 +- novaclient/v1_1/shell.py | 27 ++++++++++++++------------- novaclient/v3/servers.py | 2 +- novaclient/v3/shell.py | 19 ++++++++++--------- tox.ini | 2 +- 8 files changed, 31 insertions(+), 29 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index de4fb3772..eec9a8e57 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -421,7 +421,7 @@ def request(self, url, method, **kwargs): # or 'actively refused' in the body, so that's what we'll do. if resp.status_code == 400: if ('Connection refused' in resp.text or - 'actively refused' in resp.text): + 'actively refused' in resp.text): raise exceptions.ConnectionRefused(resp.text) try: body = json.loads(resp.text) diff --git a/novaclient/shell.py b/novaclient/shell.py index 3c7950529..a4e2dfcc6 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -135,7 +135,7 @@ def save(self, auth_token, management_url, tenant_id): if not HAS_KEYRING or not self.args.os_cache: return if (auth_token == self.auth_token and - management_url == self.management_url): + management_url == self.management_url): # Nothing changed.... return if not all([management_url, auth_token, tenant_id]): diff --git a/novaclient/tests/test_shell.py b/novaclient/tests/test_shell.py index 38e219f76..c2a484937 100644 --- a/novaclient/tests/test_shell.py +++ b/novaclient/tests/test_shell.py @@ -200,8 +200,8 @@ def test_password(self, mock_getpass, mock_stdin): # default output of empty tables differs depending between prettytable # versions if (hasattr(prettytable, '__version__') and - dist_version.StrictVersion(prettytable.__version__) < - dist_version.StrictVersion('0.7.2')): + dist_version.StrictVersion(prettytable.__version__) < + dist_version.StrictVersion('0.7.2')): ex = '\n' else: ex = '\n'.join([ diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index da0a74972..66db32deb 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -537,7 +537,7 @@ def _boot(self, resource_url, response_key, name, image, flavor, if nic_info.get('net-id'): net_data['uuid'] = nic_info['net-id'] if (nic_info.get('v4-fixed-ip') and - nic_info.get('v6-fixed-ip')): + nic_info.get('v6-fixed-ip')): raise base.exceptions.CommandError(_( "Only one of 'v4-fixed-ip' and 'v6-fixed-ip' may be" " provided.")) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 9daeb61c9..b36db0e44 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -156,7 +156,8 @@ def _boot(cs, args): max_count = 1 # Don't let user mix num_instances and max_count/min_count. if (args.num_instances is not None and - args.min_count is None and args.max_count is None): + args.min_count is None and + args.max_count is None): if args.num_instances < 1: raise exceptions.CommandError(_("num_instances should be >= 1")) max_count = args.num_instances @@ -173,10 +174,10 @@ def _boot(cs, args): if args.max_count < 1: raise exceptions.CommandError(_("max_count should be >= 1")) max_count = args.max_count - if (args.min_count is not None and args.max_count is not None and - args.min_count > args.max_count): - raise exceptions.CommandError(_( - "min_count should be <= max_count")) + if (args.min_count is not None and + args.max_count is not None and + args.min_count > args.max_count): + raise exceptions.CommandError(_("min_count should be <= max_count")) flavor = _find_flavor(cs, args.flavor) @@ -2435,10 +2436,10 @@ def do_secgroup_delete_rule(cs, args): secgroup = _get_secgroup(cs, args.secgroup) for rule in secgroup.rules: if (rule['ip_protocol'] and - rule['ip_protocol'].upper() == args.ip_proto.upper() and - rule['from_port'] == int(args.from_port) and - rule['to_port'] == int(args.to_port) and - rule['ip_range']['cidr'] == args.cidr): + rule['ip_protocol'].upper() == args.ip_proto.upper() and + rule['from_port'] == int(args.from_port) and + rule['to_port'] == int(args.to_port) and + rule['ip_range']['cidr'] == args.cidr): _print_secgroup_rules([rule]) return cs.security_group_rules.delete(rule['id']) @@ -3875,10 +3876,10 @@ def do_secgroup_delete_default_rule(cs, args): """Delete a rule from the default security group.""" for rule in cs.security_group_default_rules.list(): if (rule.ip_protocol and - rule.ip_protocol.upper() == args.ip_proto.upper() and - rule.from_port == int(args.from_port) and - rule.to_port == int(args.to_port) and - rule.ip_range['cidr'] == args.cidr): + rule.ip_protocol.upper() == args.ip_proto.upper() and + rule.from_port == int(args.from_port) and + rule.to_port == int(args.to_port) and + rule.ip_range['cidr'] == args.cidr): _print_secgroup_rules([rule], show_source_group=False) return cs.security_group_default_rules.delete(rule.id) diff --git a/novaclient/v3/servers.py b/novaclient/v3/servers.py index bd188804d..d25b6d84a 100644 --- a/novaclient/v3/servers.py +++ b/novaclient/v3/servers.py @@ -461,7 +461,7 @@ def _boot(self, resource_url, response_key, name, image, flavor, if nic_info.get('net-id'): net_data['uuid'] = nic_info['net-id'] if (nic_info.get('v4-fixed-ip') and - nic_info.get('v6-fixed-ip')): + nic_info.get('v6-fixed-ip')): raise base.exceptions.CommandError(_( "Only one of 'v4-fixed-ip' and 'v6-fixed-ip' may be" " provided.")) diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index d06f8d09b..3e67104fc 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -88,7 +88,8 @@ def _boot(cs, args): max_count = 1 # Don't let user mix num_instances and max_count/min_count. if (args.num_instances is not None and - args.min_count is None and args.max_count is None): + args.min_count is None and + args.max_count is None): if args.num_instances < 1: raise exceptions.CommandError("num_instances should be >= 1") max_count = args.num_instances @@ -105,10 +106,10 @@ def _boot(cs, args): if args.max_count < 1: raise exceptions.CommandError("max_count should be >= 1") max_count = args.max_count - if (args.min_count is not None and args.max_count is not None and - args.min_count > args.max_count): - raise exceptions.CommandError( - "min_count should be <= max_count") + if (args.min_count is not None and + args.max_count is not None and + args.min_count > args.max_count): + raise exceptions.CommandError("min_count should be <= max_count") flavor = _find_flavor(cs, args.flavor) @@ -1846,10 +1847,10 @@ def do_secgroup_delete_rule(cs, args): secgroup = _get_secgroup(cs, args.secgroup) for rule in secgroup.rules: if (rule['ip_protocol'] and - rule['ip_protocol'].upper() == args.ip_proto.upper() and - rule['from_port'] == int(args.from_port) and - rule['to_port'] == int(args.to_port) and - rule['ip_range']['cidr'] == args.cidr): + rule['ip_protocol'].upper() == args.ip_proto.upper() and + rule['from_port'] == int(args.from_port) and + rule['to_port'] == int(args.to_port) and + rule['ip_range']['cidr'] == args.cidr): _print_secgroup_rules([rule]) return cs.security_group_rules.delete(rule['id']) diff --git a/tox.ini b/tox.ini index 3afa423f3..47a36a4f3 100644 --- a/tox.ini +++ b/tox.ini @@ -42,7 +42,7 @@ downloadcache = ~/cache/pip # reason: removed in hacking (https://review.openstack.org/#/c/101701/) # # Additional checks are also ignored on purpose: F811, F821 -ignore = E124,E128,E129,F811,F821,H402,H404,H405,H904 +ignore = E124,E128,F811,F821,H402,H404,H405,H904 show-source = True exclude=.venv,.git,.tox,dist,*openstack/common*,*lib/python*,*egg,build,doc/source/conf.py From 571dc98a61c94ca7080fbd883434a3bfa72cc3ad Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Tue, 28 Oct 2014 11:52:34 +0000 Subject: [PATCH 0637/1705] Updated from global requirements Change-Id: I3701c9964d442191de3a6581bcbc0b6f5e14ab26 --- requirements.txt | 2 +- test-requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 5b897b0fb..0b1c7c0e4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ pbr>=0.6,!=0.7,<1.0 argparse iso8601>=0.1.9 -oslo.i18n>=1.0.0 # Apache-2.0 +oslo.i18n>=1.0.0 # Apache-2.0 oslo.serialization>=1.0.0 # Apache-2.0 oslo.utils>=1.0.0 # Apache-2.0 PrettyTable>=0.7,<0.8 diff --git a/test-requirements.txt b/test-requirements.txt index 76ed5b2a8..c4c297f6e 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -8,7 +8,7 @@ discover fixtures>=0.3.14 keyring>=2.1,!=3.3 mock>=1.0 -requests-mock>=0.4.0 # Apache-2.0 +requests-mock>=0.5.1 # Apache-2.0 sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3 oslosphinx>=2.2.0 # Apache-2.0 testrepository>=0.0.18 From 607ac48e6c70e2d3be9a896d5db791f376a8a640 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Fri, 17 Oct 2014 19:03:23 +0000 Subject: [PATCH 0638/1705] support OS_ENDPOINT_TYPE in nova client This change makes the handling of the endpoint_type setting in the nova client more consistent while keeping backwards compatibility. The NOVA_ENDPOINT_TYPE environment variable is checked first, and if it is not set then the more generic OS_ENDPOINT_TYPE variable is checked. A --os-endpoint-type command line option was added as well. Change-Id: I0d96f9f87106fc41a9853920f557ffad724859e7 Closes-Bug: 1382249 --- novaclient/shell.py | 14 ++++++++++---- novaclient/tests/test_shell.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/novaclient/shell.py b/novaclient/shell.py index 3c7950529..087197265 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -351,12 +351,18 @@ def get_base_parser(self): parser.add_argument('--volume_service_name', help=argparse.SUPPRESS) - parser.add_argument('--endpoint-type', + parser.add_argument('--os-endpoint-type', metavar='', + dest='endpoint_type', default=utils.env('NOVA_ENDPOINT_TYPE', - default=DEFAULT_NOVA_ENDPOINT_TYPE), - help=(_('Defaults to env[NOVA_ENDPOINT_TYPE] or ') - + DEFAULT_NOVA_ENDPOINT_TYPE + '.')) + default=utils.env('OS_ENDPOINT_TYPE', + default=DEFAULT_NOVA_ENDPOINT_TYPE)), + help=_('Defaults to env[NOVA_ENDPOINT_TYPE], ' + 'env[OS_ENDPOINT_TYPE] or ') + + DEFAULT_NOVA_ENDPOINT_TYPE + '.') + + parser.add_argument('--endpoint-type', + help=argparse.SUPPRESS) # NOTE(dtroyer): We can't add --endpoint_type here due to argparse # thinking usage-list --end is ambiguous; but it # works fine with only --endpoint-type present diff --git a/novaclient/tests/test_shell.py b/novaclient/tests/test_shell.py index 38e219f76..6c7cab2dd 100644 --- a/novaclient/tests/test_shell.py +++ b/novaclient/tests/test_shell.py @@ -36,6 +36,13 @@ 'OS_TENANT_ID': 'tenant_id', 'OS_AUTH_URL': 'http://no.where'} +FAKE_ENV3 = {'OS_USER_ID': 'user_id', + 'OS_PASSWORD': 'password', + 'OS_TENANT_ID': 'tenant_id', + 'OS_AUTH_URL': 'http://no.where', + 'NOVA_ENDPOINT_TYPE': 'novaURL', + 'OS_ENDPOINT_TYPE': 'osURL'} + class ShellTest(utils.TestCase): @@ -192,6 +199,27 @@ def test_no_auth_url(self): else: self.fail('CommandError not raised') + @mock.patch('novaclient.client.Client') + def test_nova_endpoint_type(self, mock_client): + self.make_env(fake_env=FAKE_ENV3) + self.shell('list') + client_kwargs = mock_client.call_args_list[0][1] + self.assertEqual(client_kwargs['endpoint_type'], 'novaURL') + + @mock.patch('novaclient.client.Client') + def test_os_endpoint_type(self, mock_client): + self.make_env(exclude='NOVA_ENDPOINT_TYPE', fake_env=FAKE_ENV3) + self.shell('list') + client_kwargs = mock_client.call_args_list[0][1] + self.assertEqual(client_kwargs['endpoint_type'], 'osURL') + + @mock.patch('novaclient.client.Client') + def test_default_endpoint_type(self, mock_client): + self.make_env() + self.shell('list') + client_kwargs = mock_client.call_args_list[0][1] + self.assertEqual(client_kwargs['endpoint_type'], 'publicURL') + @mock.patch('sys.stdin', side_effect=mock.MagicMock) @mock.patch('getpass.getpass', return_value='password') def test_password(self, mock_getpass, mock_stdin): From 3b2d0011f2eabfcffb6084745c16418467172f53 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Wed, 5 Nov 2014 08:35:42 +0000 Subject: [PATCH 0639/1705] Updated from global requirements Change-Id: I21538cf8831f5aaab282a4df341303836a3d8326 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index c4c297f6e..2202587e5 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -13,4 +13,4 @@ sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3 oslosphinx>=2.2.0 # Apache-2.0 testrepository>=0.0.18 testscenarios>=0.4 -testtools>=0.9.34 +testtools>=0.9.36 From 57ae6ce955afc31102da7e42b88d5eb4240324f5 Mon Sep 17 00:00:00 2001 From: Ken'ichi Ohmichi Date: Mon, 3 Nov 2014 17:27:22 +0900 Subject: [PATCH 0640/1705] Fix the help comment about metadata If passing metadata with "--meta" option, the metadata is stored into meta_data.json file on the metadata server like: $ curl http://169.254.169.254/openstack/latest/meta_data.json { "hostname": "vm01.novalocal", "meta": {"key01": "value01", "key02": "value02"}, "name": "vm01", [..] } This patch fixes the help comment. Change-Id: I7359d4c2b0aec6d5d07be7938d0000ca23c93ff0 --- novaclient/v1_1/shell.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index b36db0e44..4eef44f31 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -353,8 +353,8 @@ def _boot(cs, args): metavar="", action='append', default=[], - help=_("Record arbitrary key/value metadata to /meta.js " - "on the new server. Can be specified multiple times.")) + help=_("Record arbitrary key/value metadata to /meta_data.json " + "on the metadata server. Can be specified multiple times.")) @utils.arg('--file', metavar="", action='append', @@ -1322,8 +1322,8 @@ def do_reboot(cs, args): metavar="", action='append', default=[], - help=_("Record arbitrary key/value metadata to /meta.js " - "on the new server. Can be specified multiple times.")) + help=_("Record arbitrary key/value metadata to /meta_data.json " + "on the metadata server. Can be specified multiple times.")) @utils.arg('--file', metavar="", action='append', From ae8eadc45155f13b7a1e5c5e0137b7451a5bee4c Mon Sep 17 00:00:00 2001 From: Phil Day Date: Wed, 10 Sep 2014 17:45:47 +0000 Subject: [PATCH 0641/1705] Add limits to V3 and improve limits formatting in shell Adds support for the limits API to the v3 client. Also improve the formatting of absolute-limits from +-------------------------+-------+ | Name | Value | +-------------------------+-------+ | maxServerMeta | 128 | | maxPersonality | 5 | | maxImageMeta | 128 | | maxPersonalitySize | 10240 | | maxTotalRAMSize | 51200 | | maxSecurityGroupRules | 20 | | maxTotalKeypairs | 100 | | totalRAMUsed | 512 | | maxSecurityGroups | 10 | | totalFloatingIpsUsed | 0 | | totalInstancesUsed | 1 | | totalSecurityGroupsUsed | 1 | | maxTotalFloatingIps | 10 | | maxTotalInstances | 2 | | totalCoresUsed | 1 | | maxTotalCores | 20 | +-------------------------+-------+ to +--------------------+------+-------+ | Name | Used | Max | +--------------------+------+-------+ | Cores | 1 | 20 | | FloatingIps | 0 | 10 | | ImageMeta | - | 128 | | Instances | 1 | 2 | | Keypairs | - | 100 | | Personality | - | 5 | | PersonalitySize | - | 10240 | | RAM | 512 | 51200 | | SecurityGroupRules | - | 20 | | SecurityGroups | 1 | 10 | | ServerMeta | - | 128 | +--------------------+------+-------+ Change-Id: I93a456b402aeba8e39480567edb090cbb1898d16 --- novaclient/tests/v3/test_limits.py | 88 ++++++++++++++++++++++++++++++ novaclient/v1_1/shell.py | 61 ++++++++++++++++++++- novaclient/v3/client.py | 2 + novaclient/v3/limits.py | 50 +++++++++++++++++ novaclient/v3/shell.py | 82 ++++++++++++++++++++++++++++ 5 files changed, 281 insertions(+), 2 deletions(-) create mode 100644 novaclient/tests/v3/test_limits.py create mode 100644 novaclient/v3/limits.py diff --git a/novaclient/tests/v3/test_limits.py b/novaclient/tests/v3/test_limits.py new file mode 100644 index 000000000..51b575269 --- /dev/null +++ b/novaclient/tests/v3/test_limits.py @@ -0,0 +1,88 @@ +# +# 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 novaclient.tests.fixture_data import client +from novaclient.tests.fixture_data import limits as data +from novaclient.tests import utils +from novaclient.v3 import limits + + +class LimitsTest(utils.FixturedTestCase): + + client_fixture_class = client.V3 + data_fixture_class = data.Fixture + + def test_get_limits(self): + obj = self.cs.limits.get() + self.assert_called('GET', '/limits') + self.assertIsInstance(obj, limits.Limits) + + def test_get_limits_for_a_tenant(self): + obj = self.cs.limits.get(tenant_id=1234) + self.assert_called('GET', '/limits?tenant_id=1234') + self.assertIsInstance(obj, limits.Limits) + + def test_absolute_limits(self): + obj = self.cs.limits.get() + + expected = ( + limits.AbsoluteLimit("maxTotalRAMSize", 51200), + limits.AbsoluteLimit("maxServerMeta", 5), + limits.AbsoluteLimit("maxImageMeta", 5), + limits.AbsoluteLimit("maxPersonality", 5), + limits.AbsoluteLimit("maxPersonalitySize", 10240), + ) + + abs_limits = list(obj.absolute) + self.assertEqual(len(abs_limits), len(expected)) + + for limit in abs_limits: + self.assertTrue(limit in expected) + + def test_absolute_limits_reserved(self): + obj = self.cs.limits.get(reserved=True) + + expected = ( + limits.AbsoluteLimit("maxTotalRAMSize", 51200), + limits.AbsoluteLimit("maxServerMeta", 5), + limits.AbsoluteLimit("maxImageMeta", 5), + limits.AbsoluteLimit("maxPersonality", 5), + limits.AbsoluteLimit("maxPersonalitySize", 10240), + ) + + self.assert_called('GET', '/limits?reserved=1') + abs_limits = list(obj.absolute) + self.assertEqual(len(abs_limits), len(expected)) + + for limit in abs_limits: + self.assertTrue(limit in expected) + + def test_rate_limits(self): + obj = self.cs.limits.get() + + expected = ( + limits.RateLimit('POST', '*', '.*', 10, 2, 'MINUTE', + '2011-12-15T22:42:45Z'), + limits.RateLimit('PUT', '*', '.*', 10, 2, 'MINUTE', + '2011-12-15T22:42:45Z'), + limits.RateLimit('DELETE', '*', '.*', 100, 100, 'MINUTE', + '2011-12-15T22:42:45Z'), + limits.RateLimit('POST', '*/servers', '^/servers', 25, 24, 'DAY', + '2011-12-15T22:42:45Z'), + ) + + rate_limits = list(obj.rate) + self.assertEqual(len(rate_limits), len(expected)) + + for limit in rate_limits: + self.assertTrue(limit in expected) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 0aa81a918..809693e0a 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2616,8 +2616,65 @@ def _find_keypair(cs, keypair): def do_absolute_limits(cs, args): """Print a list of absolute limits for a user""" limits = cs.limits.get(args.reserved, args.tenant).absolute - columns = ['Name', 'Value'] - utils.print_list(limits, columns) + + class Limit(object): + def __init__(self, name, used, max, other): + self.name = name + self.used = used + self.max = max + self.other = other + + limit_map = { + 'maxServerMeta': {'name': 'Server Meta', 'type': 'max'}, + 'maxPersonality': {'name': 'Personality', 'type': 'max'}, + 'maxPersonalitySize': {'name': 'Personality Size', 'type': 'max'}, + 'maxImageMeta': {'name': 'ImageMeta', 'type': 'max'}, + 'maxTotalKeypairs': {'name': 'Keypairs', 'type': 'max'}, + 'totalCoresUsed': {'name': 'Cores', 'type': 'used'}, + 'maxTotalCores': {'name': 'Cores', 'type': 'max'}, + 'totalRAMUsed': {'name': 'RAM', 'type': 'used'}, + 'maxTotalRAMSize': {'name': 'RAM', 'type': 'max'}, + 'totalInstancesUsed': {'name': 'Instances', 'type': 'used'}, + 'maxTotalInstances': {'name': 'Instances', 'type': 'max'}, + 'totalFloatingIpsUsed': {'name': 'FloatingIps', 'type': 'used'}, + 'maxTotalFloatingIps': {'name': 'FloatingIps', 'type': 'max'}, + 'totalSecurityGroupsUsed': {'name': 'SecurityGroups', 'type': 'used'}, + 'maxSecurityGroups': {'name': 'SecurityGroups', 'type': 'max'}, + 'maxSecurityGroupRules': {'name': 'SecurityGroupRules', 'type': 'max'}, + 'maxServerGroups': {'name': 'ServerGroups', 'type': 'max'}, + 'totalServerGroupsUsed': {'name': 'ServerGroups', 'type': 'used'}, + 'maxServerGroupMembers': {'name': 'ServerGroupMembers', 'type': 'max'}, + } + + max = {} + used = {} + other = {} + limit_names = [] + columns = ['Name', 'Used', 'Max'] + for l in limits: + map = limit_map.get(l.name, {'name': l.name, 'type': 'other'}) + name = map['name'] + if map['type'] == 'max': + max[name] = l.value + elif map['type'] == 'used': + used[name] = l.value + else: + other[name] = l.value + columns.append('Other') + if name not in limit_names: + limit_names.append(name) + + limit_names.sort() + + limit_list = [] + for name in limit_names: + l = Limit(name, + used.get(name, "-"), + max.get(name, "-"), + other.get(name, "-")) + limit_list.append(l) + + utils.print_list(limit_list, columns) def do_rate_limits(cs, args): diff --git a/novaclient/v3/client.py b/novaclient/v3/client.py index a30f9191e..b9da6af0b 100644 --- a/novaclient/v3/client.py +++ b/novaclient/v3/client.py @@ -24,6 +24,7 @@ from novaclient.v3 import hypervisors from novaclient.v3 import images from novaclient.v3 import keypairs +from novaclient.v3 import limits from novaclient.v3 import list_extensions from novaclient.v3 import quotas from novaclient.v3 import servers @@ -110,6 +111,7 @@ def __init__(self, username=None, password=None, project_id=None, self.hypervisors = hypervisors.HypervisorManager(self) self.images = images.ImageManager(self) self.keypairs = keypairs.KeypairManager(self) + self.limits = limits.LimitsManager(self) self.quotas = quotas.QuotaSetManager(self) self.servers = servers.ServerManager(self) self.services = services.ServiceManager(self) diff --git a/novaclient/v3/limits.py b/novaclient/v3/limits.py new file mode 100644 index 000000000..ab8a4a279 --- /dev/null +++ b/novaclient/v3/limits.py @@ -0,0 +1,50 @@ +# Copyright 2011 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from six.moves.urllib import parse + +from novaclient.v1_1 import limits + + +class Limits(limits.Limits): + pass + + +class RateLimit(limits.RateLimit): + pass + + +class AbsoluteLimit(limits.AbsoluteLimit): + pass + + +class LimitsManager(limits.LimitsManager): + """Manager object used to interact with limits resource.""" + + resource_class = Limits + + def get(self, reserved=False, tenant_id=None): + """ + Get a specific extension. + + :rtype: :class:`Limits` + """ + opts = {} + if reserved: + opts['reserved'] = 1 + if tenant_id: + opts['tenant_id'] = tenant_id + query_string = "?%s" % parse.urlencode(opts) if opts else "" + + return self._get("/limits%s" % query_string, "limits") diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index f97331e0f..0523d4cee 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -3066,3 +3066,85 @@ def do_availability_zone_list(cs, _args): _translate_availability_zone_keys(result) utils.print_list(result, ['Name', 'Status'], sortby_index=None) + + +@utils.arg('--tenant', + # nova db searches by project_id + dest='tenant', + metavar='', + nargs='?', + help=_('Display information from single tenant (Admin only).')) +@utils.arg('--reserved', + dest='reserved', + action='store_true', + default=False, + help=_('Include reservations count.')) +def do_absolute_limits(cs, args): + """Print a list of absolute limits for a user""" + limits = cs.limits.get(args.reserved, args.tenant).absolute + + class Limit(object): + def __init__(self, name, used, max, other): + self.name = name + self.used = used + self.max = max + self.other = other + + limit_map = { + 'maxServerMeta': {'name': 'Server Meta', 'type': 'max'}, + 'maxPersonality': {'name': 'Personality', 'type': 'max'}, + 'maxPersonalitySize': {'name': 'Personality Size', 'type': 'max'}, + 'maxImageMeta': {'name': 'ImageMeta', 'type': 'max'}, + 'maxTotalKeypairs': {'name': 'Keypairs', 'type': 'max'}, + 'totalCoresUsed': {'name': 'Cores', 'type': 'used'}, + 'maxTotalCores': {'name': 'Cores', 'type': 'max'}, + 'totalRAMUsed': {'name': 'RAM', 'type': 'used'}, + 'maxTotalRAMSize': {'name': 'RAM', 'type': 'max'}, + 'totalInstancesUsed': {'name': 'Instances', 'type': 'used'}, + 'maxTotalInstances': {'name': 'Instances', 'type': 'max'}, + 'totalFloatingIpsUsed': {'name': 'FloatingIps', 'type': 'used'}, + 'maxTotalFloatingIps': {'name': 'FloatingIps', 'type': 'max'}, + 'totalSecurityGroupsUsed': {'name': 'SecurityGroups', 'type': 'used'}, + 'maxSecurityGroups': {'name': 'SecurityGroups', 'type': 'max'}, + 'maxSecurityGroupRules': {'name': 'SecurityGroupRules', 'type': 'max'}, + 'maxServerGroups': {'name': 'ServerGroups', 'type': 'max'}, + 'totalServerGroupsUsed': {'name': 'ServerGroups', 'type': 'used'}, + 'maxServerGroupMembers': {'name': 'ServerGroupMembers', 'type': 'max'}, + } + + max = {} + used = {} + other = {} + limit_names = [] + columns = ['Name', 'Used', 'Max'] + for l in limits: + map = limit_map.get(l.name, {'name': l.name, 'type': 'other'}) + name = map['name'] + if map['type'] == 'max': + max[name] = l.value + elif map['type'] == 'used': + used[name] = l.value + else: + other[name] = l.value + columns.append('Other') + if name not in limit_names: + limit_names.append(name) + + limit_names.sort() + + limit_list = [] + for name in limit_names: + l = Limit(name, + used.get(name, "-"), + max.get(name, "-"), + other.get(name, "-")) + limit_list.append(l) + + utils.print_list(limit_list, columns) + + +def do_rate_limits(cs, args): + """Print a list of rate limits for a user""" + limits = cs.limits.get().rate + columns = ['Verb', 'URI', 'Value', 'Remain', 'Unit', 'Next_Available'] + utils.print_list(limits, columns) From 8597a0c234ef643905d8e356a3986b79b52989c3 Mon Sep 17 00:00:00 2001 From: David Hu Date: Thu, 23 Oct 2014 01:50:49 -0700 Subject: [PATCH 0642/1705] Support using the Keystone V3 API from the Nova CLI This changeset enables support for Keystone V3 authentication on the Nova CLI. This provides consistency between using novaclient as the Nova CLI and using it as a library as the Keystone V3 support already exists for the libary usecase. The bulk of the change surrounds the use of the keystoneclient session object for authentication, retriving the service catalog, and HTTP connection/session management. Co-Authored-By: Morgan Fainberg Change-Id: Iece9f41320a8770176c7eeb5acd86be4d80cc58f --- novaclient/client.py | 15 ++++ novaclient/shell.py | 112 +++++++++++++++++++++------- novaclient/tests/test_shell.py | 60 +++++++++++---- novaclient/tests/v1_1/test_shell.py | 1 + novaclient/tests/v3/test_shell.py | 1 + novaclient/v1_1/client.py | 2 - novaclient/v1_1/shell.py | 63 +++++++++++----- novaclient/v3/client.py | 2 - novaclient/v3/shell.py | 64 +++++++++++----- 9 files changed, 234 insertions(+), 86 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index eec9a8e57..2c64ab1e1 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -137,20 +137,35 @@ def write_object(self, obj): class SessionClient(adapter.LegacyJsonAdapter): + def __init__(self, *args, **kwargs): + self.times = [] + super(SessionClient, self).__init__(*args, **kwargs) + def request(self, url, method, **kwargs): # NOTE(jamielennox): The standard call raises errors from # keystoneclient, where we need to raise the novaclient errors. raise_exc = kwargs.pop('raise_exc', True) + start_time = time.time() resp, body = super(SessionClient, self).request(url, method, raise_exc=False, **kwargs) + end_time = time.time() + self.times.append(('%s %s' % (method, url), + start_time, end_time)) + if raise_exc and resp.status_code >= 400: raise exceptions.from_response(resp, body, url, method) return resp, body + def get_timings(self): + return self.times + + def reset_timings(self): + self.times = [] + def _original_only(f): """Indicates and enforces that this function can only be used if we are diff --git a/novaclient/shell.py b/novaclient/shell.py index 7842c278a..027f1cc60 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -28,7 +28,11 @@ import os import pkgutil import sys +import time +from keystoneclient.auth.identity.generic import password +from keystoneclient.auth.identity.generic import token +from keystoneclient.auth.identity import v3 as identity from keystoneclient import session as ksession from oslo.utils import encodeutils from oslo.utils import strutils @@ -232,6 +236,7 @@ def error(self, message): class OpenStackComputeShell(object): + times = [] def _append_global_identity_args(self, parser): # Register the CLI arguments that have moved to the session object. @@ -240,6 +245,14 @@ def _append_global_identity_args(self, parser): parser.set_defaults(insecure=utils.env('NOVACLIENT_INSECURE', default=False)) + identity.Password.register_argparse_arguments(parser) + + parser.set_defaults(os_username=utils.env('OS_USERNAME', + 'NOVA_USERNAME')) + parser.set_defaults(os_password=utils.env('OS_PASSWORD', + 'NOVA_PASSWORD')) + parser.set_defaults(os_auth_url=utils.env('OS_AUTH_URL', 'NOVA_URL')) + def get_base_parser(self): parser = NovaClientArgumentParser( prog='nova', @@ -281,22 +294,9 @@ def get_base_parser(self): default=utils.env('OS_AUTH_TOKEN'), help='Defaults to env[OS_AUTH_TOKEN]') - parser.add_argument('--os-username', - metavar='', - default=utils.env('OS_USERNAME', 'NOVA_USERNAME'), - help=_('Defaults to env[OS_USERNAME].')) parser.add_argument('--os_username', help=argparse.SUPPRESS) - parser.add_argument('--os-user-id', - metavar='', - default=utils.env('OS_USER_ID'), - help=_('Defaults to env[OS_USER_ID].')) - - parser.add_argument('--os-password', - metavar='', - default=utils.env('OS_PASSWORD', 'NOVA_PASSWORD'), - help=_('Defaults to env[OS_PASSWORD].')) parser.add_argument('--os_password', help=argparse.SUPPRESS) @@ -312,10 +312,6 @@ def get_base_parser(self): default=utils.env('OS_TENANT_ID'), help=_('Defaults to env[OS_TENANT_ID].')) - parser.add_argument('--os-auth-url', - metavar='', - default=utils.env('OS_AUTH_URL', 'NOVA_URL'), - help=_('Defaults to env[OS_AUTH_URL].')) parser.add_argument('--os_auth_url', help=argparse.SUPPRESS) @@ -511,6 +507,19 @@ def setup_debugging(self, debug): logging.basicConfig(level=logging.DEBUG, format=streamformat) + def _get_keystone_auth(self, session, auth_url, **kwargs): + auth_token = kwargs.pop('auth_token', None) + if auth_token: + return token.Token(auth_url, auth_token, **kwargs) + else: + return password.Password(auth_url, + username=kwargs.pop('username'), + user_id=kwargs.pop('user_id'), + password=kwargs.pop('password'), + user_domain_id=kwargs.pop('user_domain_id'), + user_domain_name=kwargs.pop('user_domain_name'), + **kwargs) + def main(self, argv): # Parse args once to find version and debug settings parser = self.get_base_parser() @@ -570,6 +579,9 @@ def main(self, argv): cacert = args.os_cacert timeout = args.timeout + keystone_session = None + keystone_auth = None + # We may have either, both or none of these. # If we have both, we don't need USERNAME, PASSWORD etc. # Fill in the blanks from the SecretsHelper if possible. @@ -603,6 +615,13 @@ def main(self, argv): must_auth = not (cliutils.isunauthenticated(args.func) or (auth_token and management_url)) + # Do not use Keystone session for cases with no session support. The + # presence of auth_plugin means os_auth_system is present and is not + # keystone. + use_session = True + if auth_plugin or bypass_url or os_cache or volume_service_name: + use_session = False + # FIXME(usrleon): Here should be restrict for project id same as # for os_username or os_password but for compatibility it is not. if must_auth: @@ -615,11 +634,14 @@ def main(self, argv): "or user id via --os-username, --os-user-id, " "env[OS_USERNAME] or env[OS_USER_ID]")) - if not os_tenant_name and not os_tenant_id: - raise exc.CommandError(_("You must provide a tenant name " - "or tenant id via --os-tenant-name, " - "--os-tenant-id, env[OS_TENANT_NAME] " - "or env[OS_TENANT_ID]")) + if not any([args.os_tenant_name, args.os_tenant_id, + args.os_project_id, args.os_project_name]): + raise exc.CommandError(_("You must provide a project name or" + " project id via --os-project-name," + " --os-project-id, env[OS_PROJECT_ID]" + " or env[OS_PROJECT_NAME]. You may" + " use os-project and os-tenant" + " interchangeably.")) if not os_auth_url: if os_auth_system and os_auth_system != 'keystone': @@ -632,13 +654,39 @@ def main(self, argv): "default url with --os-auth-system " "or env[OS_AUTH_SYSTEM]")) + project_id = args.os_project_id or args.os_tenant_id + project_name = args.os_project_name or args.os_tenant_name + if use_session: + # Not using Nova auth plugin, so use keystone + start_time = time.time() + keystone_session = ksession.Session.load_from_cli_options(args) + keystone_auth = self._get_keystone_auth( + keystone_session, + args.os_auth_url, + username=args.os_username, + user_id=args.os_user_id, + user_domain_id=args.os_user_domain_id, + user_domain_name=args.os_user_domain_name, + password=args.os_password, + auth_token=args.os_auth_token, + project_id=project_id, + project_name=project_name, + project_domain_id=args.os_project_domain_id, + project_domain_name=args.os_project_domain_name) + end_time = time.time() + self.times.append(('%s %s' % ('auth_url', args.os_auth_url), + start_time, end_time)) + if (options.os_compute_api_version and options.os_compute_api_version != '1.0'): - if not os_tenant_name and not os_tenant_id: - raise exc.CommandError(_("You must provide a tenant name " - "or tenant id via --os-tenant-name, " - "--os-tenant-id, env[OS_TENANT_NAME] " - "or env[OS_TENANT_ID]")) + if not any([args.os_tenant_id, args.os_tenant_name, + args.os_project_id, args.os_project_name]): + raise exc.CommandError(_("You must provide a project name or" + " project id via --os-project-name," + " --os-project-id, env[OS_PROJECT_ID]" + " or env[OS_PROJECT_NAME]. You may" + " use os-project and os-tenant" + " interchangeably.")) if not os_auth_url: raise exc.CommandError(_("You must provide an auth url " @@ -658,6 +706,7 @@ def main(self, argv): timings=args.timings, bypass_url=bypass_url, os_cache=os_cache, http_log_debug=options.debug, cacert=cacert, timeout=timeout, + session=keystone_session, auth=keystone_auth, completion_cache=completion_cache) # Now check for the password/token of which pieces of the @@ -691,7 +740,11 @@ def main(self, argv): # This does a couple of bits which are useful even if we've # got the token + service URL already. It exits fast in that case. if not cliutils.isunauthenticated(args.func): - self.cs.authenticate() + if not use_session: + # Only call authenticate() if Nova auth plugin is used. + # If keystone is used, authentication is handled as part + # of session. + self.cs.authenticate() except exc.Unauthorized: raise exc.CommandError(_("Invalid OpenStack Nova credentials.")) except exc.AuthorizationFailure: @@ -720,12 +773,13 @@ def main(self, argv): volume_service_name=volume_service_name, timings=args.timings, bypass_url=bypass_url, os_cache=os_cache, http_log_debug=options.debug, + session=keystone_session, auth=keystone_auth, cacert=cacert, timeout=timeout) args.func(self.cs, args) if args.timings: - self._dump_timings(self.cs.get_timings()) + self._dump_timings(self.times + self.cs.get_timings()) def _dump_timings(self, timings): class Tyme(object): diff --git a/novaclient/tests/test_shell.py b/novaclient/tests/test_shell.py index 329f39ddc..b9028f094 100644 --- a/novaclient/tests/test_shell.py +++ b/novaclient/tests/test_shell.py @@ -16,8 +16,10 @@ import sys import fixtures +from keystoneclient import fixture import mock import prettytable +import requests_mock import six from testtools import matchers @@ -29,23 +31,33 @@ FAKE_ENV = {'OS_USERNAME': 'username', 'OS_PASSWORD': 'password', 'OS_TENANT_NAME': 'tenant_name', - 'OS_AUTH_URL': 'http://no.where'} + 'OS_AUTH_URL': 'http://no.where/v2.0'} FAKE_ENV2 = {'OS_USER_ID': 'user_id', 'OS_PASSWORD': 'password', 'OS_TENANT_ID': 'tenant_id', - 'OS_AUTH_URL': 'http://no.where'} + 'OS_AUTH_URL': 'http://no.where/v2.0'} FAKE_ENV3 = {'OS_USER_ID': 'user_id', 'OS_PASSWORD': 'password', 'OS_TENANT_ID': 'tenant_id', - 'OS_AUTH_URL': 'http://no.where', + 'OS_AUTH_URL': 'http://no.where/v2.0', 'NOVA_ENDPOINT_TYPE': 'novaURL', 'OS_ENDPOINT_TYPE': 'osURL'} +def _create_ver_list(versions): + return {'versions': {'values': versions}} + + class ShellTest(utils.TestCase): + _msg_no_tenant_project = ("You must provide a project name or project" + " id via --os-project-name, --os-project-id," + " env[OS_PROJECT_ID] or env[OS_PROJECT_NAME]." + " You may use os-project and os-tenant" + " interchangeably.") + def make_env(self, exclude=None, fake_env=FAKE_ENV): env = dict((k, v) for k, v in fake_env.items() if k != exclude) self.useFixture(fixtures.MonkeyPatch('os.environ', env)) @@ -79,6 +91,12 @@ def shell(self, argstr, exitcodes=(0,)): sys.stderr = orig_stderr return (stdout, stderr) + def register_keystone_discovery_fixture(self, mreq): + v2_url = "http://no.where/v2.0" + v2_version = fixture.V2Discovery(v2_url) + mreq.register_uri('GET', v2_url, json=_create_ver_list([v2_version]), + status_code=200) + def test_help_unknown_command(self): self.assertRaises(exceptions.CommandError, self.shell, 'help foofoo') @@ -163,9 +181,7 @@ def test_no_user_id(self): self.fail('CommandError not raised') def test_no_tenant_name(self): - required = ('You must provide a tenant name or tenant id' - ' via --os-tenant-name, --os-tenant-id,' - ' env[OS_TENANT_NAME] or env[OS_TENANT_ID]') + required = self._msg_no_tenant_project self.make_env(exclude='OS_TENANT_NAME') try: self.shell('list') @@ -175,14 +191,12 @@ def test_no_tenant_name(self): self.fail('CommandError not raised') def test_no_tenant_id(self): - required = ('You must provide a tenant name or tenant id' - ' via --os-tenant-name, --os-tenant-id,' - ' env[OS_TENANT_NAME] or env[OS_TENANT_ID]',) + required = self._msg_no_tenant_project self.make_env(exclude='OS_TENANT_ID', fake_env=FAKE_ENV2) try: self.shell('list') except exceptions.CommandError as message: - self.assertEqual(required, message.args) + self.assertEqual(required, message.args[0]) else: self.fail('CommandError not raised') @@ -200,15 +214,19 @@ def test_no_auth_url(self): self.fail('CommandError not raised') @mock.patch('novaclient.client.Client') - def test_nova_endpoint_type(self, mock_client): + @requests_mock.Mocker() + def test_nova_endpoint_type(self, mock_client, m_requests): self.make_env(fake_env=FAKE_ENV3) + self.register_keystone_discovery_fixture(m_requests) self.shell('list') client_kwargs = mock_client.call_args_list[0][1] self.assertEqual(client_kwargs['endpoint_type'], 'novaURL') @mock.patch('novaclient.client.Client') - def test_os_endpoint_type(self, mock_client): + @requests_mock.Mocker() + def test_os_endpoint_type(self, mock_client, m_requests): self.make_env(exclude='NOVA_ENDPOINT_TYPE', fake_env=FAKE_ENV3) + self.register_keystone_discovery_fixture(m_requests) self.shell('list') client_kwargs = mock_client.call_args_list[0][1] self.assertEqual(client_kwargs['endpoint_type'], 'osURL') @@ -222,7 +240,8 @@ def test_default_endpoint_type(self, mock_client): @mock.patch('sys.stdin', side_effect=mock.MagicMock) @mock.patch('getpass.getpass', return_value='password') - def test_password(self, mock_getpass, mock_stdin): + @requests_mock.Mocker() + def test_password(self, mock_getpass, mock_stdin, m_requests): mock_stdin.encoding = "utf-8" # default output of empty tables differs depending between prettytable @@ -240,6 +259,7 @@ def test_password(self, mock_getpass, mock_stdin): '' ]) self.make_env(exclude='OS_PASSWORD') + self.register_keystone_discovery_fixture(m_requests) stdout, stderr = self.shell('list') self.assertEqual((stdout + stderr), ex) @@ -310,3 +330,17 @@ def test_main_keyboard_interrupt(self, mock_compute_shell): novaclient.shell.main() except SystemExit as ex: self.assertEqual(ex.code, 130) + + +class ShellTestKeystoneV3(ShellTest): + def make_env(self, exclude=None, fake_env=FAKE_ENV): + if 'OS_AUTH_URL' in fake_env: + fake_env.update({'OS_AUTH_URL': 'http://no.where/v3'}) + env = dict((k, v) for k, v in fake_env.items() if k != exclude) + self.useFixture(fixtures.MonkeyPatch('os.environ', env)) + + def register_keystone_discovery_fixture(self, mreq): + v3_url = "http://no.where/v3" + v3_version = fixture.V3Discovery(v3_url) + mreq.register_uri('GET', v3_url, json=_create_ver_list([v3_version]), + status_code=200) diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index 3fef327c9..55b6137fd 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -56,6 +56,7 @@ class ShellTest(utils.TestCase): 'NOVA_PROJECT_ID': 'project_id', 'OS_COMPUTE_API_VERSION': '1.1', 'NOVA_URL': 'http://no.where', + 'OS_AUTH_URL': 'http://no.where/v2.0', } def setUp(self): diff --git a/novaclient/tests/v3/test_shell.py b/novaclient/tests/v3/test_shell.py index 658f07fba..2418aed7c 100644 --- a/novaclient/tests/v3/test_shell.py +++ b/novaclient/tests/v3/test_shell.py @@ -51,6 +51,7 @@ class ShellTest(utils.TestCase): 'NOVA_PROJECT_ID': 'project_id', 'OS_COMPUTE_API_VERSION': '3', 'NOVA_URL': 'http://no.where', + 'OS_AUTH_URL': 'http://no.where/v2.0', } def setUp(self): diff --git a/novaclient/v1_1/client.py b/novaclient/v1_1/client.py index 2e79e5bb1..70d5298dc 100644 --- a/novaclient/v1_1/client.py +++ b/novaclient/v1_1/client.py @@ -217,11 +217,9 @@ def __exit__(self, t, v, tb): def set_management_url(self, url): self.client.set_management_url(url) - @client._original_only def get_timings(self): return self.client.get_timings() - @client._original_only def reset_timings(self): self.client.reset_timings() diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index b36db0e44..5b5c87a32 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -33,6 +33,7 @@ from oslo.utils import timeutils import six +from novaclient import client from novaclient import exceptions from novaclient.i18n import _ from novaclient.openstack.common import cliutils @@ -2759,7 +2760,12 @@ def simplify_usage(u): if args.tenant: usage = cs.usage.get(args.tenant, start, end) else: - usage = cs.usage.get(cs.client.tenant_id, start, end) + if isinstance(cs.client, client.SessionClient): + auth = cs.client.auth + project_id = auth.get_auth_ref(cs.client.session).project_id + usage = cs.usage.get(project_id, start, end) + else: + usage = cs.usage.get(cs.client.tenant_id, start, end) print(_("Usage from %(start)s to %(end)s:") % {'start': start.strftime(dateformat), @@ -3256,23 +3262,32 @@ def ensure_service_catalog_present(cs): def do_endpoints(cs, _args): """Discover endpoints that get returned from the authenticate services.""" - ensure_service_catalog_present(cs) + if isinstance(cs.client, client.SessionClient): + auth = cs.client.auth + sc = auth.get_access(cs.client.session).service_catalog + for service in sc.get_data(): + _print_endpoints(service, cs.client.region_name) + else: + ensure_service_catalog_present(cs) - catalog = cs.client.service_catalog.catalog - region = cs.client.region_name + catalog = cs.client.service_catalog.catalog + region = cs.client.region_name + for service in catalog['access']['serviceCatalog']: + _print_endpoints(service, region) - for service in catalog['access']['serviceCatalog']: - name, endpoints = service["name"], service["endpoints"] - try: - endpoint = _get_first_endpoint(endpoints, region) - utils.print_dict(endpoint, name) - except LookupError: - print(_("WARNING: %(service)s has no endpoint in %(region)s! " - "Available endpoints for this service:") % - {'service': name, 'region': region}) - for other_endpoint in endpoints: - utils.print_dict(other_endpoint, name) +def _print_endpoints(service, region): + name, endpoints = service["name"], service["endpoints"] + + try: + endpoint = _get_first_endpoint(endpoints, region) + utils.print_dict(endpoint, name) + except LookupError: + print(_("WARNING: %(service)s has no endpoint in %(region)s! " + "Available endpoints for this service:") % + {'service': name, 'region': region}) + for other_endpoint in endpoints: + utils.print_dict(other_endpoint, name) def _get_first_endpoint(endpoints, region): @@ -3298,11 +3313,19 @@ def _get_first_endpoint(endpoints, region): help=_('wrap PKI tokens to a specified length, or 0 to disable')) def do_credentials(cs, _args): """Show user credentials returned from auth.""" - ensure_service_catalog_present(cs) - catalog = cs.client.service_catalog.catalog - utils.print_dict(catalog['access']['user'], "User Credentials", - wrap=int(_args.wrap)) - utils.print_dict(catalog['access']['token'], "Token", wrap=int(_args.wrap)) + if isinstance(cs.client, client.SessionClient): + auth = cs.client.auth + sc = auth.get_access(cs.client.session).service_catalog + utils.print_dict(sc.catalog['user'], 'User Credentials', + wrap=int(_args.wrap)) + utils.print_dict(sc.get_token(), 'Token', wrap=int(_args.wrap)) + else: + ensure_service_catalog_present(cs) + catalog = cs.client.service_catalog.catalog + utils.print_dict(catalog['access']['user'], "User Credentials", + wrap=int(_args.wrap)) + utils.print_dict(catalog['access']['token'], "Token", + wrap=int(_args.wrap)) @utils.arg('server', metavar='', help=_('Name or ID of server.')) diff --git a/novaclient/v3/client.py b/novaclient/v3/client.py index a30f9191e..6967ea44b 100644 --- a/novaclient/v3/client.py +++ b/novaclient/v3/client.py @@ -174,11 +174,9 @@ def __exit__(self, t, v, tb): def set_management_url(self, url): self.client.set_management_url(url) - @client._original_only def get_timings(self): return self.client.get_timings() - @client._original_only def reset_timings(self): self.client.reset_timings() diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 3e67104fc..f22ae0bc3 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -31,6 +31,7 @@ from oslo.utils import timeutils import six +from novaclient import client from novaclient import exceptions from novaclient.i18n import _ from novaclient.openstack.common import cliutils @@ -2140,7 +2141,12 @@ def simplify_usage(u): if args.tenant: usage = cs.usage.get(args.tenant, start, end) else: - usage = cs.usage.get(cs.client.tenant_id, start, end) + if isinstance(cs.client, client.SessionClient): + auth = cs.client.auth + project_id = auth.get_auth_ref(cs.client.session).project_id + usage = cs.usage.get(project_id, start, end) + else: + usage = cs.usage.get(cs.client.tenant_id, start, end) print("Usage from %s to %s:" % (start.strftime(dateformat), end.strftime(dateformat))) @@ -2692,23 +2698,33 @@ def ensure_service_catalog_present(cs): def do_endpoints(cs, _args): """Discover endpoints that get returned from the authenticate services.""" - ensure_service_catalog_present(cs) + if isinstance(cs.client, client.SessionClient): + auth = cs.client.auth + sc = auth.get_Access(cs.client.session).service_catalog + for service in sc.get_data(): + _print_endpoints(service, cs.client.region_name) + else: + ensure_service_catalog_present(cs) - catalog = cs.client.service_catalog.catalog - region = cs.client.region_name + catalog = cs.client.service_catalog.catalog + region = cs.client.region_name - for service in catalog['access']['serviceCatalog']: - name, endpoints = service["name"], service["endpoints"] + for service in catalog['access']['serviceCatalog']: + _print_endpoints(service, region) - try: - endpoint = _get_first_endpoint(endpoints, region) - utils.print_dict(endpoint, name) - except LookupError: - print(_("WARNING: %(service)s has no endpoint in %(region)s! " - "Available endpoints for this service:") % - {'service': name, 'region': region}) - for other_endpoint in endpoints: - utils.print_dict(other_endpoint, name) + +def _print_endpoints(service, region): + name, endpoints = service["name"], service["endpoints"] + + try: + endpoint = _get_first_endpoint(endpoints, region) + utils.print_dict(endpoint, name) + except LookupError: + print(_("WARNING: %(service)s has no endpoint in %(region)s! " + "Available endpoints for this service:") % + {'service': name, 'region': region}) + for other_endpoint in endpoints: + utils.print_dict(other_endpoint, name) def _get_first_endpoint(endpoints, region): @@ -2734,11 +2750,19 @@ def _get_first_endpoint(endpoints, region): help='wrap PKI tokens to a specified length, or 0 to disable') def do_credentials(cs, _args): """Show user credentials returned from auth.""" - ensure_service_catalog_present(cs) - catalog = cs.client.service_catalog.catalog - utils.print_dict(catalog['access']['user'], "User Credentials", - wrap=int(_args.wrap)) - utils.print_dict(catalog['access']['token'], "Token", wrap=int(_args.wrap)) + if isinstance(cs.client, client.SessionClient): + auth = cs.client.auth + sc = auth.get_access(cs.client.session).service_catalog + utils.print_dict(sc.catalog['user'], 'User Credentials', + wrap=int(_args.wrap)) + utils.print_dict(sc.get_token(), 'Token', wrap=int(_args.wrap)) + else: + ensure_service_catalog_present(cs) + catalog = cs.client.service_catalog.catalog + utils.print_dict(catalog['access']['user'], "User Credentials", + wrap=int(_args.wrap)) + utils.print_dict(catalog['access']['token'], "Token", + wrap=int(_args.wrap)) def do_extension_list(cs, _args): From e91d469704046db19356fbbe063ba7e541b24e65 Mon Sep 17 00:00:00 2001 From: sridhargaddam Date: Fri, 14 Nov 2014 14:26:53 +0000 Subject: [PATCH 0643/1705] Curl statements to include globoff for IPv6 URLs Novaclient displays curl statements for debugging/troubleshooting purposes. For IPv6 URLs, curl requires --globoff to be passed in the arguments. Since novaclient does not use curl directly, this patch displays the curl commands with globoff option which works for both IPv4 and IPv6 URLs. Closes-Bug: #1228744 Change-Id: Ib7099e8e3bbc15f29bbaa1db37ef21e78a74e7bc --- novaclient/client.py | 2 +- novaclient/openstack/common/apiclient/client.py | 2 +- novaclient/tests/test_client.py | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index eec9a8e57..d2c4357cb 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -317,7 +317,7 @@ def http_log_req(self, method, url, kwargs): if not self.http_log_debug: return - string_parts = ['curl -i'] + string_parts = ['curl -g -i'] if not kwargs.get('verify', True): string_parts.append(' --insecure') diff --git a/novaclient/openstack/common/apiclient/client.py b/novaclient/openstack/common/apiclient/client.py index 7780cea91..c591850e6 100644 --- a/novaclient/openstack/common/apiclient/client.py +++ b/novaclient/openstack/common/apiclient/client.py @@ -104,7 +104,7 @@ def _http_log_req(self, method, url, kwargs): return string_parts = [ - "curl -i", + "curl -g -i", "-X '%s'" % method, "'%s'" % url, ] diff --git a/novaclient/tests/test_client.py b/novaclient/tests/test_client.py index 90a6773ec..099b36636 100644 --- a/novaclient/tests/test_client.py +++ b/novaclient/tests/test_client.py @@ -376,18 +376,18 @@ def test_log_req(self): output = self.logger.output.split('\n') - self.assertIn("REQ: curl -i '/foo' -X GET", output) + self.assertIn("REQ: curl -g -i '/foo' -X GET", output) self.assertIn( - "REQ: curl -i '/foo' -X GET -H " + "REQ: curl -g -i '/foo' -X GET -H " '"X-Auth-Token: {SHA1}b42162b6ffdbd7c3c37b7c95b7ba9f51dda0236d"', output) self.assertIn( - "REQ: curl -i '/foo' -X GET -H " + "REQ: curl -g -i '/foo' -X GET -H " '"X-Auth-Token: {SHA1}b42162b6ffdbd7c3c37b7c95b7ba9f51dda0236d"' ' -H "X-Foo: bar"', output) self.assertIn( - "REQ: curl -i '/foo' -X GET -d " + "REQ: curl -g -i '/foo' -X GET -d " '\'{"auth": {"passwordCredentials": {"password":' ' "{SHA1}4fc49c6a671ce889078ff6b250f7066cf6d2ada2"}}}\'', output) From bb0d0b56558bd97b33cec37fd47a48c3ade5ab03 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Tue, 18 Nov 2014 11:37:01 +0000 Subject: [PATCH 0644/1705] Updated from global requirements Change-Id: I364ac4f5d90a2826523d629cc0bfe37ed1e8e17f --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 2202587e5..532705177 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -13,4 +13,4 @@ sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3 oslosphinx>=2.2.0 # Apache-2.0 testrepository>=0.0.18 testscenarios>=0.4 -testtools>=0.9.36 +testtools>=0.9.36,!=1.2.0,!=1.4.0 From d06a8dbb93b86aa44196a8b06b802c147b694e63 Mon Sep 17 00:00:00 2001 From: Eugeniya Kudryashova Date: Thu, 25 Sep 2014 17:04:14 +0300 Subject: [PATCH 0645/1705] Fix E128 failures in novaclient/v1_1/shell E128 continuation line under-indented for visual indent Change-Id: Ibc2e42e4d2070ae666ceef9598269fef576af46b --- novaclient/v1_1/shell.py | 1565 ++++++++++++++++++++++---------------- 1 file changed, 929 insertions(+), 636 deletions(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 0e8bae32c..b39df0244 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -246,15 +246,15 @@ def _boot(cs, args): if block_device_mapping and block_device_mapping_v2: raise exceptions.CommandError( _("you can't mix old block devices (--block-device-mapping) " - "with the new ones (--block-device, --boot-volume, --snapshot, " - "--ephemeral, --swap)")) + "with the new ones (--block-device, --boot-volume, --snapshot, " + "--ephemeral, --swap)")) nics = [] for nic_str in args.nics: err_msg = (_("Invalid nic argument '%s'. Nic arguments must be of " - "the form --nic , with at minimum " - "net-id or port-id (but not both) specified.") % nic_str) + "the form --nic , with at minimum " + "net-id or port-id (but not both) specified.") % nic_str) nic_info = {"net-id": "", "v4-fixed-ip": "", "v6-fixed-ip": "", "port-id": ""} @@ -313,139 +313,164 @@ def _boot(cs, args): return boot_args, boot_kwargs -@utils.arg('--flavor', - default=None, - metavar='', - help=_("Name or ID of flavor (see 'nova flavor-list').")) -@utils.arg('--image', - default=None, - metavar='', - help=_("Name or ID of image (see 'nova image-list'). ")) -@utils.arg('--image-with', - default=[], - type=_key_value_pairing, - action='append', - metavar='', - help=_("Image metadata property (see 'nova image-show'). ")) -@utils.arg('--boot-volume', +@utils.arg( + '--flavor', + default=None, + metavar='', + help=_("Name or ID of flavor (see 'nova flavor-list').")) +@utils.arg( + '--image', + default=None, + metavar='', + help=_("Name or ID of image (see 'nova image-list'). ")) +@utils.arg( + '--image-with', + default=[], + type=_key_value_pairing, + action='append', + metavar='', + help=_("Image metadata property (see 'nova image-show'). ")) +@utils.arg( + '--boot-volume', default=None, metavar="", help=_("Volume ID to boot from.")) -@utils.arg('--snapshot', +@utils.arg( + '--snapshot', default=None, metavar="", help=_("Snapshot ID to boot from (will create a volume).")) -@utils.arg('--num-instances', - default=None, - type=int, - metavar='', - help=argparse.SUPPRESS) -@utils.arg('--min-count', - default=None, - type=int, - metavar='', - help=_("Boot at least servers (limited by quota).")) -@utils.arg('--max-count', - default=None, - type=int, - metavar='', - help=_("Boot up to servers (limited by quota).")) -@utils.arg('--meta', - metavar="", - action='append', - default=[], - help=_("Record arbitrary key/value metadata to /meta_data.json " - "on the metadata server. Can be specified multiple times.")) -@utils.arg('--file', - metavar="", - action='append', - dest='files', - default=[], - help=_("Store arbitrary files from locally to " - "on the new server. You may store up to 5 files.")) -@utils.arg('--key-name', +@utils.arg( + '--num-instances', + default=None, + type=int, + metavar='', + help=argparse.SUPPRESS) +@utils.arg( + '--min-count', + default=None, + type=int, + metavar='', + help=_("Boot at least servers (limited by quota).")) +@utils.arg( + '--max-count', + default=None, + type=int, + metavar='', + help=_("Boot up to servers (limited by quota).")) +@utils.arg( + '--meta', + metavar="", + action='append', + default=[], + help=_("Record arbitrary key/value metadata to /meta_data.json " + "on the metadata server. Can be specified multiple times.")) +@utils.arg( + '--file', + metavar="", + action='append', + dest='files', + default=[], + help=_("Store arbitrary files from locally to " + "on the new server. You may store up to 5 files.")) +@utils.arg( + '--key-name', default=os.environ.get('NOVACLIENT_DEFAULT_KEY_NAME'), metavar='', help=_("Key name of keypair that should be created earlier with \ - the command keypair-add")) -@utils.arg('--key_name', + the command keypair-add")) +@utils.arg( + '--key_name', help=argparse.SUPPRESS) @utils.arg('name', metavar='', help=_('Name for the new server')) -@utils.arg('--user-data', +@utils.arg( + '--user-data', default=None, metavar='', help=_("user data file to pass to be exposed by the metadata server.")) -@utils.arg('--user_data', +@utils.arg( + '--user_data', help=argparse.SUPPRESS) -@utils.arg('--availability-zone', +@utils.arg( + '--availability-zone', default=None, metavar='', help=_("The availability zone for server placement.")) -@utils.arg('--availability_zone', +@utils.arg( + '--availability_zone', help=argparse.SUPPRESS) -@utils.arg('--security-groups', +@utils.arg( + '--security-groups', default=None, metavar='', help=_("Comma separated list of security group names.")) -@utils.arg('--security_groups', +@utils.arg( + '--security_groups', help=argparse.SUPPRESS) -@utils.arg('--block-device-mapping', +@utils.arg( + '--block-device-mapping', metavar="", action='append', default=[], help=_("Block device mapping in the format " - "=:::.")) -@utils.arg('--block_device_mapping', + "=:::.")) +@utils.arg( + '--block_device_mapping', action='append', help=argparse.SUPPRESS) -@utils.arg('--block-device', +@utils.arg( + '--block-device', metavar="key1=value1[,key2=value2...]", action='append', default=[], help=_("Block device mapping with the keys: " - "id=UUID (image_id, snapshot_id or volume_id only if using source " - "image, snapshot or volume) " - "source=source type (image, snapshot, volume or blank), " - "dest=destination type of the block device (volume or local), " - "bus=device's bus (e.g. uml, lxc, virtio, ...; if omitted, " - "hypervisor driver chooses a suitable default, " - "honoured only if device type is supplied) " - "type=device type (e.g. disk, cdrom, ...; defaults to 'disk') " - "device=name of the device (e.g. vda, xda, ...; " - "if omitted, hypervisor driver chooses suitable device " - "depending on selected bus), " - "size=size of the block device in GB (if omitted, " - "hypervisor driver calculates size), " - "format=device will be formatted (e.g. swap, ntfs, ...; optional), " - "bootindex=integer used for ordering the boot disks " - "(for image backed instances it is equal to 0, " - "for others need to be specified) and " - "shutdown=shutdown behaviour (either preserve or remove, " - "for local destination set to remove).")) -@utils.arg('--swap', + "id=UUID (image_id, snapshot_id or volume_id only if using source " + "image, snapshot or volume) " + "source=source type (image, snapshot, volume or blank), " + "dest=destination type of the block device (volume or local), " + "bus=device's bus (e.g. uml, lxc, virtio, ...; if omitted, " + "hypervisor driver chooses a suitable default, " + "honoured only if device type is supplied) " + "type=device type (e.g. disk, cdrom, ...; defaults to 'disk') " + "device=name of the device (e.g. vda, xda, ...; " + "if omitted, hypervisor driver chooses suitable device " + "depending on selected bus), " + "size=size of the block device in GB (if omitted, " + "hypervisor driver calculates size), " + "format=device will be formatted (e.g. swap, ntfs, ...; optional), " + "bootindex=integer used for ordering the boot disks " + "(for image backed instances it is equal to 0, " + "for others need to be specified) and " + "shutdown=shutdown behaviour (either preserve or remove, " + "for local destination set to remove).")) +@utils.arg( + '--swap', metavar="", default=None, help=_("Create and attach a local swap block device of MB.")) -@utils.arg('--ephemeral', +@utils.arg( + '--ephemeral', metavar="size=[,format=]", action='append', default=[], help=_("Create and attach a local ephemeral block device of GB " - "and format it to .")) -@utils.arg('--hint', - action='append', - dest='scheduler_hints', - default=[], - metavar='', - help=_("Send arbitrary key/value pairs to the scheduler for custom " - "use.")) -@utils.arg('--nic', - metavar="", - action='append', - dest='nics', - default=[], - help=_("Create a NIC on the server. " + "and format it to .")) +@utils.arg( + '--hint', + action='append', + dest='scheduler_hints', + default=[], + metavar='', + help=_("Send arbitrary key/value pairs to the scheduler for custom " + "use.")) +@utils.arg( + '--nic', + metavar="", + action='append', + dest='nics', + default=[], + help=_("Create a NIC on the server. " "Specify option multiple times to create multiple NICs. " "net-id: attach NIC to network with this UUID " "(either port-id or net-id must be provided), " @@ -453,12 +478,14 @@ def _boot(cs, args): "v6-fixed-ip: IPv6 fixed address for NIC (optional), " "port-id: attach NIC to port with this UUID " "(either port-id or net-id must be provided).")) -@utils.arg('--config-drive', - metavar="", - dest='config_drive', - default=False, - help=_("Enable config drive")) -@utils.arg('--poll', +@utils.arg( + '--config-drive', + metavar="", + dest='config_drive', + default=False, + help=_("Enable config drive")) +@utils.arg( + '--poll', dest='poll', action="store_true", default=False, @@ -565,8 +592,7 @@ def _translate_extended_states(collection): for item in collection: try: setattr(item, 'power_state', - power_states[getattr(item, 'power_state')] - ) + power_states[getattr(item, 'power_state')]) except AttributeError: setattr(item, 'power_state', "N/A") try: @@ -629,7 +655,8 @@ def do_flavor_list(cs, args): _print_flavor_list(flavors, args.extra_specs) -@utils.arg('flavor', +@utils.arg( + 'flavor', metavar='', help=_("Name or ID of the flavor to delete")) def do_flavor_delete(cs, args): @@ -639,48 +666,58 @@ def do_flavor_delete(cs, args): _print_flavor_list([flavorid]) -@utils.arg('flavor', - metavar='', - help=_("Name or ID of flavor")) +@utils.arg( + 'flavor', + metavar='', + help=_("Name or ID of flavor")) def do_flavor_show(cs, args): """Show details about the given flavor.""" flavor = _find_flavor(cs, args.flavor) _print_flavor(flavor) -@utils.arg('name', - metavar='', - help=_("Name of the new flavor")) -@utils.arg('id', - metavar='', - help=_("Unique ID (integer or UUID) for the new flavor." - " If specifying 'auto', a UUID will be generated as id")) -@utils.arg('ram', - metavar='', - help=_("Memory size in MB")) -@utils.arg('disk', - metavar='', - help=_("Disk size in GB")) -@utils.arg('--ephemeral', - metavar='', - help=_("Ephemeral space size in GB (default 0)"), - default=0) -@utils.arg('vcpus', - metavar='', - help=_("Number of vcpus")) -@utils.arg('--swap', - metavar='', - help=_("Swap space size in MB (default 0)"), - default=0) -@utils.arg('--rxtx-factor', - metavar='', - help=_("RX/TX factor (default 1)"), - default=1.0) -@utils.arg('--is-public', - metavar='', - help=_("Make flavor accessible to the public (default true)"), - type=lambda v: strutils.bool_from_string(v, True), - default=True) +@utils.arg( + 'name', + metavar='', + help=_("Name of the new flavor")) +@utils.arg( + 'id', + metavar='', + help=_("Unique ID (integer or UUID) for the new flavor." + " If specifying 'auto', a UUID will be generated as id")) +@utils.arg( + 'ram', + metavar='', + help=_("Memory size in MB")) +@utils.arg( + 'disk', + metavar='', + help=_("Disk size in GB")) +@utils.arg( + '--ephemeral', + metavar='', + help=_("Ephemeral space size in GB (default 0)"), + default=0) +@utils.arg( + 'vcpus', + metavar='', + help=_("Number of vcpus")) +@utils.arg( + '--swap', + metavar='', + help=_("Swap space size in MB (default 0)"), + default=0) +@utils.arg( + '--rxtx-factor', + metavar='', + help=_("RX/TX factor (default 1)"), + default=1.0) +@utils.arg( + '--is-public', + metavar='', + help=_("Make flavor accessible to the public (default true)"), + type=lambda v: strutils.bool_from_string(v, True), + default=True) def do_flavor_create(cs, args): """Create a new flavor""" f = cs.flavors.create(args.name, args.ram, args.vcpus, args.disk, args.id, @@ -689,14 +726,17 @@ def do_flavor_create(cs, args): _print_flavor_list([f]) -@utils.arg('flavor', +@utils.arg( + 'flavor', metavar='', help=_("Name or ID of flavor")) -@utils.arg('action', +@utils.arg( + 'action', metavar='', choices=['set', 'unset'], help=_("Actions: 'set' or 'unset'")) -@utils.arg('metadata', +@utils.arg( + 'metadata', metavar='', nargs='+', action='append', @@ -713,27 +753,29 @@ def do_flavor_key(cs, args): flavor.unset_keys(keypair.keys()) -@utils.arg('--flavor', - metavar='', - help=_("Filter results by flavor name or ID.")) -@utils.arg('--tenant', metavar='', - help=_('Filter results by tenant ID.')) +@utils.arg( + '--flavor', + metavar='', + help=_("Filter results by flavor name or ID.")) +@utils.arg( + '--tenant', metavar='', + help=_('Filter results by tenant ID.')) def do_flavor_access_list(cs, args): """Print access information about the given flavor.""" if args.flavor and args.tenant: raise exceptions.CommandError(_("Unable to filter results by " - "both --flavor and --tenant.")) + "both --flavor and --tenant.")) elif args.flavor: flavor = _find_flavor(cs, args.flavor) if flavor.is_public: raise exceptions.CommandError(_("Failed to get access list " - "for public flavor type.")) + "for public flavor type.")) kwargs = {'flavor': flavor} elif args.tenant: kwargs = {'tenant': args.tenant} else: raise exceptions.CommandError(_("Unable to get all access lists. " - "Specify --flavor or --tenant")) + "Specify --flavor or --tenant")) try: access_list = cs.flavor_access.list(**kwargs) @@ -744,11 +786,13 @@ def do_flavor_access_list(cs, args): utils.print_list(access_list, columns) -@utils.arg('flavor', - metavar='', - help=_("Flavor name or ID to add access for the given tenant.")) -@utils.arg('tenant', metavar='', - help=_('Tenant ID to add flavor access for.')) +@utils.arg( + 'flavor', + metavar='', + help=_("Flavor name or ID to add access for the given tenant.")) +@utils.arg( + 'tenant', metavar='', + help=_('Tenant ID to add flavor access for.')) def do_flavor_access_add(cs, args): """Add flavor access for the given tenant.""" flavor = _find_flavor(cs, args.flavor) @@ -757,11 +801,13 @@ def do_flavor_access_add(cs, args): utils.print_list(access_list, columns) -@utils.arg('flavor', - metavar='', - help=_("Flavor name or ID to remove access for the given tenant.")) -@utils.arg('tenant', metavar='', - help=_('Tenant ID to remove flavor access for.')) +@utils.arg( + 'flavor', + metavar='', + help=_("Flavor name or ID to remove access for the given tenant.")) +@utils.arg( + 'tenant', metavar='', + help=_('Tenant ID to remove flavor access for.')) def do_flavor_access_remove(cs, args): """Remove flavor access for the given tenant.""" flavor = _find_flavor(cs, args.flavor) @@ -770,13 +816,14 @@ def do_flavor_access_remove(cs, args): utils.print_list(access_list, columns) -@utils.arg('project_id', metavar='', - help=_('The ID of the project.')) +@utils.arg( + 'project_id', metavar='', + help=_('The ID of the project.')) def do_scrub(cs, args): """Delete networks and security groups associated with a project.""" networks_list = cs.networks.list() networks_list = [network for network in networks_list - if getattr(network, 'project_id', '') == args.project_id] + if getattr(network, 'project_id', '') == args.project_id] search_opts = {'all_tenants': 1} groups = cs.security_groups.list(search_opts) groups = [group for group in groups @@ -787,7 +834,8 @@ def do_scrub(cs, args): cs.security_groups.delete(group) -@utils.arg('--fields', +@utils.arg( + '--fields', default=None, metavar='', help='Comma-separated list of fields to display. ' @@ -809,41 +857,46 @@ def do_network_list(cs, args): utils.print_list(network_list, columns) -@utils.arg('network', - metavar='', - help=_("uuid or label of network")) +@utils.arg( + 'network', + metavar='', + help=_("uuid or label of network")) def do_network_show(cs, args): """Show details about the given network.""" network = utils.find_resource(cs.networks, args.network) utils.print_dict(network._info) -@utils.arg('network', - metavar='', - help=_("uuid or label of network")) +@utils.arg( + 'network', + metavar='', + help=_("uuid or label of network")) def do_network_delete(cs, args): """Delete network by label or id.""" network = utils.find_resource(cs.networks, args.network) network.delete() -@utils.arg('--host-only', - dest='host_only', - metavar='<0|1>', - nargs='?', - type=int, - const=1, - default=0) -@utils.arg('--project-only', - dest='project_only', - metavar='<0|1>', - nargs='?', - type=int, - const=1, - default=0) -@utils.arg('network', - metavar='', - help="uuid of network") +@utils.arg( + '--host-only', + dest='host_only', + metavar='<0|1>', + nargs='?', + type=int, + const=1, + default=0) +@utils.arg( + '--project-only', + dest='project_only', + metavar='<0|1>', + nargs='?', + type=int, + const=1, + default=0) +@utils.arg( + 'network', + metavar='', + help="uuid of network") def do_network_disassociate(cs, args): """Disassociate host and/or project from the given network.""" if args.host_only: @@ -854,20 +907,23 @@ def do_network_disassociate(cs, args): cs.networks.disassociate(args.network, True, True) -@utils.arg('network', - metavar='', - help="uuid of network") -@utils.arg('host', - metavar='', - help="Name of host") +@utils.arg( + 'network', + metavar='', + help="uuid of network") +@utils.arg( + 'host', + metavar='', + help="Name of host") def do_network_associate_host(cs, args): """Associate host with network.""" cs.networks.associate_host(args.network, args.host) -@utils.arg('network', - metavar='', - help="uuid of network") +@utils.arg( + 'network', + metavar='', + help="uuid of network") def do_network_associate_project(cs, args): """Associate project with network.""" cs.networks.associate_project(args.network) @@ -887,94 +943,117 @@ def _filter_network_create_options(args): return kwargs -@utils.arg('label', - metavar='', - help=_("Label for network")) -@utils.arg('--fixed-range-v4', - dest='cidr', - metavar='', - help=_("IPv4 subnet (ex: 10.0.0.0/8)")) -@utils.arg('--fixed-range-v6', - dest="cidr_v6", - help=_('IPv6 subnet (ex: fe80::/64')) -@utils.arg('--vlan', - dest='vlan', - type=int, - metavar='', - help=_("The vlan ID to be assigned to the project.")) -@utils.arg('--vlan-start', - dest='vlan_start', - type=int, - metavar='', - help=_('First vlan ID to be assigned to the project. Subsequent vlan ' - 'IDs will be assigned incrementally.')) -@utils.arg('--vpn', - dest='vpn_start', - type=int, - metavar='', - help=_("vpn start")) -@utils.arg('--gateway', - dest="gateway", - help=_('gateway')) -@utils.arg('--gateway-v6', - dest="gateway_v6", - help=_('IPv6 gateway')) -@utils.arg('--bridge', - dest="bridge", - metavar='', - help=_('VIFs on this network are connected to this bridge.')) -@utils.arg('--bridge-interface', - dest="bridge_interface", - metavar='', - help=_('The bridge is connected to this interface.')) -@utils.arg('--multi-host', - dest="multi_host", - metavar="<'T'|'F'>", - help=_('Multi host')) -@utils.arg('--dns1', - dest="dns1", - metavar="", help='First DNS') -@utils.arg('--dns2', - dest="dns2", - metavar="", - help=_('Second DNS')) -@utils.arg('--uuid', - dest="uuid", - metavar="", - help=_('Network UUID')) -@utils.arg('--fixed-cidr', - dest="fixed_cidr", - metavar='', - help=_('IPv4 subnet for fixed IPs (ex: 10.20.0.0/16)')) -@utils.arg('--project-id', - dest="project_id", - metavar="", - help=_('Project ID')) -@utils.arg('--priority', - dest="priority", - metavar="", - help=_('Network interface priority')) -@utils.arg('--mtu', - dest="mtu", - type=int, - help=_('MTU for network')) -@utils.arg('--enable-dhcp', - dest="enable_dhcp", - metavar="<'T'|'F'>", - help=_('Enable dhcp')) -@utils.arg('--dhcp-server', - dest="dhcp_server", - help=_('Dhcp-server (defaults to gateway address)')) -@utils.arg('--share-address', - dest="share_address", - metavar="<'T'|'F'>", - help=_('Share address')) -@utils.arg('--allowed-start', - dest="allowed_start", - help=_('Start of allowed addresses for instances')) -@utils.arg('--allowed-end', - dest="allowed_end", - help=_('End of allowed addresses for instances')) +@utils.arg( + 'label', + metavar='', + help=_("Label for network")) +@utils.arg( + '--fixed-range-v4', + dest='cidr', + metavar='', + help=_("IPv4 subnet (ex: 10.0.0.0/8)")) +@utils.arg( + '--fixed-range-v6', + dest="cidr_v6", + help=_('IPv6 subnet (ex: fe80::/64')) +@utils.arg( + '--vlan', + dest='vlan', + type=int, + metavar='', + help=_("The vlan ID to be assigned to the project.")) +@utils.arg( + '--vlan-start', + dest='vlan_start', + type=int, + metavar='', + help=_('First vlan ID to be assigned to the project. Subsequent vlan ' + 'IDs will be assigned incrementally.')) +@utils.arg( + '--vpn', + dest='vpn_start', + type=int, + metavar='', + help=_("vpn start")) +@utils.arg( + '--gateway', + dest="gateway", + help=_('gateway')) +@utils.arg( + '--gateway-v6', + dest="gateway_v6", + help=_('IPv6 gateway')) +@utils.arg( + '--bridge', + dest="bridge", + metavar='', + help=_('VIFs on this network are connected to this bridge.')) +@utils.arg( + '--bridge-interface', + dest="bridge_interface", + metavar='', + help=_('The bridge is connected to this interface.')) +@utils.arg( + '--multi-host', + dest="multi_host", + metavar="<'T'|'F'>", + help=_('Multi host')) +@utils.arg( + '--dns1', + dest="dns1", + metavar="", help='First DNS') +@utils.arg( + '--dns2', + dest="dns2", + metavar="", + help=_('Second DNS')) +@utils.arg( + '--uuid', + dest="uuid", + metavar="", + help=_('Network UUID')) +@utils.arg( + '--fixed-cidr', + dest="fixed_cidr", + metavar='', + help=_('IPv4 subnet for fixed IPs (ex: 10.20.0.0/16)')) +@utils.arg( + '--project-id', + dest="project_id", + metavar="", + help=_('Project ID')) +@utils.arg( + '--priority', + dest="priority", + metavar="", + help=_('Network interface priority')) +@utils.arg( + '--mtu', + dest="mtu", + type=int, + help=_('MTU for network')) +@utils.arg( + '--enable-dhcp', + dest="enable_dhcp", + metavar="<'T'|'F'>", + help=_('Enable dhcp')) +@utils.arg( + '--dhcp-server', + dest="dhcp_server", + help=_('Dhcp-server (defaults to gateway address)')) +@utils.arg( + '--share-address', + dest="share_address", + metavar="<'T'|'F'>", + help=_('Share address')) +@utils.arg( + '--allowed-start', + dest="allowed_start", + help=_('Start of allowed addresses for instances')) +@utils.arg( + '--allowed-end', + dest="allowed_end", + help=_('End of allowed addresses for instances')) def do_network_create(cs, args): """Create a network.""" @@ -986,19 +1065,22 @@ def do_network_create(cs, args): kwargs['multi_host'] = bool(args.multi_host == 'T' or strutils.bool_from_string(args.multi_host)) if args.enable_dhcp is not None: - kwargs['enable_dhcp'] = bool(args.enable_dhcp == 'T' or - strutils.bool_from_string(args.enable_dhcp)) + kwargs['enable_dhcp'] = bool( + args.enable_dhcp == 'T' or + strutils.bool_from_string(args.enable_dhcp)) if args.share_address is not None: - kwargs['share_address'] = bool(args.share_address == 'T' or - strutils.bool_from_string(args.share_address)) + kwargs['share_address'] = bool( + args.share_address == 'T' or + strutils.bool_from_string(args.share_address)) cs.networks.create(**kwargs) -@utils.arg('--limit', - dest="limit", - metavar="", - help=_('Number of images to return per request.')) +@utils.arg( + '--limit', + dest="limit", + metavar="", + help=_('Number of images to return per request.')) def do_image_list(cs, _args): """Print a list of available images to boot from.""" limit = _args.limit @@ -1015,20 +1097,23 @@ def parse_server_name(image): fmts, sortby_index=1) -@utils.arg('image', - metavar='', - help=_("Name or ID of image")) -@utils.arg('action', - metavar='', - choices=['set', 'delete'], - help=_("Actions: 'set' or 'delete'")) -@utils.arg('metadata', - metavar='', - nargs='+', - action='append', - default=[], - help=_('Metadata to add/update or delete (only key is necessary on ' - 'delete)')) +@utils.arg( + 'image', + metavar='', + help=_("Name or ID of image")) +@utils.arg( + 'action', + metavar='', + choices=['set', 'delete'], + help=_("Actions: 'set' or 'delete'")) +@utils.arg( + 'metadata', + metavar='', + nargs='+', + action='append', + default=[], + help=_('Metadata to add/update or delete (only key is necessary on ' + 'delete)')) def do_image_meta(cs, args): """Set or Delete metadata on an image.""" image = _find_image(cs, args.image) @@ -1088,17 +1173,19 @@ def _print_flavor(flavor): utils.print_dict(info) -@utils.arg('image', - metavar='', - help=_("Name or ID of image")) +@utils.arg( + 'image', + metavar='', + help=_("Name or ID of image")) def do_image_show(cs, args): """Show details about the given image.""" image = _find_image(cs, args.image) _print_image(image) -@utils.arg('image', metavar='', nargs='+', - help=_('Name or ID of image(s).')) +@utils.arg( + 'image', metavar='', nargs='+', + help=_('Name or ID of image(s).')) def do_image_delete(cs, args): """Delete specified image(s).""" for image in args.image: @@ -1109,57 +1196,69 @@ def do_image_delete(cs, args): {'image': image, 'e': e}) -@utils.arg('--reservation-id', +@utils.arg( + '--reservation-id', dest='reservation_id', metavar='', default=None, help=_('Only return servers that match reservation-id.')) -@utils.arg('--reservation_id', +@utils.arg( + '--reservation_id', help=argparse.SUPPRESS) -@utils.arg('--ip', +@utils.arg( + '--ip', dest='ip', metavar='', default=None, help=_('Search with regular expression match by IP address.')) -@utils.arg('--ip6', +@utils.arg( + '--ip6', dest='ip6', metavar='', default=None, help=_('Search with regular expression match by IPv6 address.')) -@utils.arg('--name', +@utils.arg( + '--name', dest='name', metavar='', default=None, help=_('Search with regular expression match by name')) -@utils.arg('--instance-name', +@utils.arg( + '--instance-name', dest='instance_name', metavar='', default=None, help=_('Search with regular expression match by server name.')) -@utils.arg('--instance_name', +@utils.arg( + '--instance_name', help=argparse.SUPPRESS) -@utils.arg('--status', +@utils.arg( + '--status', dest='status', metavar='', default=None, help=_('Search by server status')) -@utils.arg('--flavor', +@utils.arg( + '--flavor', dest='flavor', metavar='', default=None, help=_('Search by flavor name or ID')) -@utils.arg('--image', +@utils.arg( + '--image', dest='image', metavar='', default=None, help=_('Search by image name or ID')) -@utils.arg('--host', +@utils.arg( + '--host', dest='host', metavar='', default=None, help=_('Search servers by hostname to which they are assigned (Admin ' 'only).')) -@utils.arg('--all-tenants', +@utils.arg( + '--all-tenants', dest='all_tenants', metavar='<0|1>', nargs='?', @@ -1168,33 +1267,39 @@ def do_image_delete(cs, args): default=int(strutils.bool_from_string( os.environ.get("ALL_TENANTS", 'false'), True)), help=_('Display information from all tenants (Admin only).')) -@utils.arg('--all_tenants', +@utils.arg( + '--all_tenants', nargs='?', type=int, const=1, help=argparse.SUPPRESS) -@utils.arg('--tenant', +@utils.arg( + '--tenant', # nova db searches by project_id dest='tenant', metavar='', nargs='?', help=_('Display information from single tenant (Admin only).')) -@utils.arg('--user', +@utils.arg( + '--user', dest='user', metavar='', nargs='?', help=_('Display information from single user (Admin only).')) -@utils.arg('--deleted', +@utils.arg( + '--deleted', dest='deleted', action="store_true", default=False, help='Only display deleted servers (Admin only).') -@utils.arg('--fields', +@utils.arg( + '--fields', default=None, metavar='', help=_('Comma-separated list of fields to display. ' - 'Use the show command to see which fields are available.')) -@utils.arg('--minimal', + 'Use the show command to see which fields are available.')) +@utils.arg( + '--minimal', dest='minimal', action="store_true", default=False, @@ -1270,14 +1375,16 @@ def do_list(cs, args): formatters, sortby_index=1) -@utils.arg('--hard', +@utils.arg( + '--hard', dest='reboot_type', action='store_const', const=servers.REBOOT_HARD, default=servers.REBOOT_SOFT, help=_('Perform a hard reboot (instead of a soft one).')) @utils.arg('server', metavar='', help=_('Name or ID of server.')) -@utils.arg('--poll', +@utils.arg( + '--poll', dest='poll', action="store_true", default=False, @@ -1294,44 +1401,52 @@ def do_reboot(cs, args): @utils.arg('server', metavar='', help=_('Name or ID of server.')) @utils.arg('image', metavar='', help=_("Name or ID of new image.")) -@utils.arg('--rebuild-password', +@utils.arg( + '--rebuild-password', dest='rebuild_password', metavar='', default=False, help=_("Set the provided password on the rebuild server.")) -@utils.arg('--rebuild_password', +@utils.arg( + '--rebuild_password', help=argparse.SUPPRESS) -@utils.arg('--poll', +@utils.arg( + '--poll', dest='poll', action="store_true", default=False, help=_('Report the server rebuild progress until it completes.')) -@utils.arg('--minimal', +@utils.arg( + '--minimal', dest='minimal', action="store_true", default=False, help=_('Skips flavor/image lookups when showing servers')) -@utils.arg('--preserve-ephemeral', +@utils.arg( + '--preserve-ephemeral', action="store_true", default=False, help='Preserve the default ephemeral storage partition on rebuild.') -@utils.arg('--name', +@utils.arg( + '--name', metavar='', default=None, help=_('Name for the new server')) -@utils.arg('--meta', +@utils.arg( + '--meta', metavar="", action='append', default=[], help=_("Record arbitrary key/value metadata to /meta_data.json " - "on the metadata server. Can be specified multiple times.")) -@utils.arg('--file', + "on the metadata server. Can be specified multiple times.")) +@utils.arg( + '--file', metavar="", action='append', dest='files', default=[], help=_("Store arbitrary files from locally to " - "on the new server. You may store up to 5 files.")) + "on the new server. You may store up to 5 files.")) def do_rebuild(cs, args): """Shutdown, re-image, and re-boot a server.""" server = _find_server(cs, args.server) @@ -1370,8 +1485,9 @@ def do_rebuild(cs, args): _poll_for_status(cs.servers.get, server.id, 'rebuilding', ['active']) -@utils.arg('server', metavar='', - help=_('Name (old name) or ID of server.')) +@utils.arg( + 'server', metavar='', + help=_('Name (old name) or ID of server.')) @utils.arg('name', metavar='', help=_('New name for the server.')) def do_rename(cs, args): """Rename a server.""" @@ -1380,7 +1496,8 @@ def do_rename(cs, args): @utils.arg('server', metavar='', help=_('Name or ID of server.')) @utils.arg('flavor', metavar='', help=_("Name or ID of new flavor.")) -@utils.arg('--poll', +@utils.arg( + '--poll', dest='poll', action="store_true", default=False, @@ -1409,7 +1526,8 @@ def do_resize_revert(cs, args): @utils.arg('server', metavar='', help=_('Name or ID of server.')) -@utils.arg('--poll', +@utils.arg( + '--poll', dest='poll', action="store_true", default=False, @@ -1483,11 +1601,13 @@ def do_resume(cs, args): @utils.arg('server', metavar='', help=_('Name or ID of server.')) -@utils.arg('--password', +@utils.arg( + '--password', metavar='', dest='password', help=_('The admin password to be set in the rescue environment.')) -@utils.arg('--image', +@utils.arg( + '--image', metavar='', dest='image', help=_('The image to rescue with.')) @@ -1535,9 +1655,10 @@ def do_diagnostics(cs, args): utils.print_dict(cs.servers.diagnostics(server)[1], wrap=80) -@utils.arg('server', metavar='', - help=_('Name or ID of a server for which the network cache should ' - 'be refreshed from neutron (Admin only).')) +@utils.arg( + 'server', metavar='', + help=_('Name or ID of a server for which the network cache should ' + 'be refreshed from neutron (Admin only).')) def do_refresh_network(cs, args): """Refresh server network information.""" server = _find_server(cs, args.server) @@ -1560,12 +1681,14 @@ def do_root_password(cs, args): @utils.arg('server', metavar='', help=_('Name or ID of server.')) @utils.arg('name', metavar='', help=_('Name of snapshot.')) -@utils.arg('--show', +@utils.arg( + '--show', dest='show', action="store_true", default=False, help=_('Print image info.')) -@utils.arg('--poll', +@utils.arg( + '--poll', dest='poll', action="store_true", default=False, @@ -1601,11 +1724,13 @@ def do_image_create(cs, args): @utils.arg('server', metavar='', help=_('Name or ID of server.')) @utils.arg('name', metavar='', help=_('Name of the backup image.')) -@utils.arg('backup_type', metavar='', - help=_('The backup type, like "daily" or "weekly".')) -@utils.arg('rotation', metavar='', - help=_('Int parameter representing how many backups to keep ' - 'around.')) +@utils.arg( + 'backup_type', metavar='', + help=_('The backup type, like "daily" or "weekly".')) +@utils.arg( + 'rotation', metavar='', + help=_('Int parameter representing how many backups to keep ' + 'around.')) def do_backup(cs, args): """Backup a server by creating a 'backup' type snapshot.""" _find_server(cs, args.server).backup(args.name, @@ -1613,19 +1738,22 @@ def do_backup(cs, args): args.rotation) -@utils.arg('server', - metavar='', - help=_("Name or ID of server")) -@utils.arg('action', - metavar='', - choices=['set', 'delete'], - help=_("Actions: 'set' or 'delete'")) -@utils.arg('metadata', - metavar='', - nargs='+', - action='append', - default=[], - help=_('Metadata to set or delete (only key is necessary on delete)')) +@utils.arg( + 'server', + metavar='', + help=_("Name or ID of server")) +@utils.arg( + 'action', + metavar='', + choices=['set', 'delete'], + help=_("Actions: 'set' or 'delete'")) +@utils.arg( + 'metadata', + metavar='', + nargs='+', + action='append', + default=[], + help=_('Metadata to set or delete (only key is necessary on delete)')) def do_meta(cs, args): """Set or Delete metadata on a server.""" server = _find_server(cs, args.server) @@ -1688,7 +1816,8 @@ def _print_server(cs, args, server=None): utils.print_dict(info) -@utils.arg('--minimal', +@utils.arg( + '--minimal', dest='minimal', action="store_true", default=False, @@ -1699,8 +1828,9 @@ def do_show(cs, args): _print_server(cs, args) -@utils.arg('server', metavar='', nargs='+', - help=_('Name or ID of server(s).')) +@utils.arg( + 'server', metavar='', nargs='+', + help=_('Name or ID of server(s).')) def do_delete(cs, args): """Immediately shut down and delete specified server(s).""" utils.do_action_on_many( @@ -1729,7 +1859,8 @@ def _find_flavor(cs, flavor): @utils.arg('server', metavar='', help=_('Name or ID of server.')) -@utils.arg('network_id', +@utils.arg( + 'network_id', metavar='', help='Network ID.') def do_add_fixed_ip(cs, args): @@ -1781,7 +1912,8 @@ def _translate_availability_zone_keys(collection): [('zoneName', 'name'), ('zoneState', 'status')]) -@utils.arg('--all-tenants', +@utils.arg( + '--all-tenants', dest='all_tenants', metavar='<0|1>', nargs='?', @@ -1790,7 +1922,8 @@ def _translate_availability_zone_keys(collection): default=int(strutils.bool_from_string( os.environ.get("ALL_TENANTS", 'false'), True)), help=_('Display information from all tenants (Admin only).')) -@utils.arg('--all_tenants', +@utils.arg( + '--all_tenants', nargs='?', type=int, const=1, @@ -1807,7 +1940,7 @@ def do_volume_list(cs, args): servers = [s.get('server_id') for s in vol.attachments] setattr(vol, 'attached_to', ','.join(map(str, servers))) utils.print_list(volumes, ['ID', 'Status', 'Display Name', - 'Size', 'Volume Type', 'Attached to']) + 'Size', 'Volume Type', 'Attached to']) @utils.arg('volume', metavar='', help=_('Name or ID of the volume.')) @@ -1818,39 +1951,50 @@ def do_volume_show(cs, args): _print_volume(volume) -@utils.arg('size', +@utils.arg( + 'size', metavar='', type=int, help=_('Size of volume in GB')) -@utils.arg('--snapshot-id', +@utils.arg( + '--snapshot-id', metavar='', default=None, help=_('Optional snapshot id to create the volume from. (Default=None)')) -@utils.arg('--snapshot_id', +@utils.arg( + '--snapshot_id', help=argparse.SUPPRESS) -@utils.arg('--image-id', +@utils.arg( + '--image-id', metavar='', help=_('Optional image id to create the volume from. (Default=None)'), default=None) -@utils.arg('--display-name', +@utils.arg( + '--display-name', metavar='', default=None, help=_('Optional volume name. (Default=None)')) -@utils.arg('--display_name', +@utils.arg( + '--display_name', help=argparse.SUPPRESS) -@utils.arg('--display-description', +@utils.arg( + '--display-description', metavar='', default=None, help=_('Optional volume description. (Default=None)')) -@utils.arg('--display_description', +@utils.arg( + '--display_description', help=argparse.SUPPRESS) -@utils.arg('--volume-type', +@utils.arg( + '--volume-type', metavar='', default=None, help=_('Optional volume type. (Default=None)')) -@utils.arg('--volume_type', +@utils.arg( + '--volume_type', help=argparse.SUPPRESS) -@utils.arg('--availability-zone', metavar='', +@utils.arg( + '--availability-zone', metavar='', help=_('Optional Availability Zone for volume. (Default=None)'), default=None) @cliutils.service_type('volume') @@ -1866,7 +2010,8 @@ def do_volume_create(cs, args): _print_volume(volume) -@utils.arg('volume', +@utils.arg( + 'volume', metavar='', nargs='+', help=_('Name or ID of the volume(s) to delete.')) @cliutils.service_type('volume') @@ -1880,15 +2025,18 @@ def do_volume_delete(cs, args): {'volume': volume, 'e': e}) -@utils.arg('server', +@utils.arg( + 'server', metavar='', help=_('Name or ID of server.')) -@utils.arg('volume', +@utils.arg( + 'volume', metavar='', help=_('ID of the volume to attach.')) -@utils.arg('device', metavar='', default=None, nargs='?', +@utils.arg( + 'device', metavar='', default=None, nargs='?', help=_('Name of the device e.g. /dev/vdb. ' - 'Use "auto" for autoassign (if supported)')) + 'Use "auto" for autoassign (if supported)')) def do_volume_attach(cs, args): """Attach a volume to a server.""" if args.device == 'auto': @@ -1900,13 +2048,16 @@ def do_volume_attach(cs, args): _print_volume(volume) -@utils.arg('server', +@utils.arg( + 'server', metavar='', help=_('Name or ID of server.')) -@utils.arg('attachment_id', +@utils.arg( + 'attachment_id', metavar='', help=_('Attachment ID of the volume.')) -@utils.arg('new_volume', +@utils.arg( + 'new_volume', metavar='', help=_('ID of the volume to attach.')) def do_volume_update(cs, args): @@ -1916,10 +2067,12 @@ def do_volume_update(cs, args): args.new_volume) -@utils.arg('server', +@utils.arg( + 'server', metavar='', help=_('Name or ID of server.')) -@utils.arg('attachment_id', +@utils.arg( + 'attachment_id', metavar='', help=_('ID of the volume to detach.')) def do_volume_detach(cs, args): @@ -1934,10 +2087,11 @@ def do_volume_snapshot_list(cs, _args): snapshots = cs.volume_snapshots.list() _translate_volume_snapshot_keys(snapshots) utils.print_list(snapshots, ['ID', 'Volume ID', 'Status', 'Display Name', - 'Size']) + 'Size']) -@utils.arg('snapshot', +@utils.arg( + 'snapshot', metavar='', help=_('Name or ID of the snapshot.')) @cliutils.service_type('volume') @@ -1947,25 +2101,31 @@ def do_volume_snapshot_show(cs, args): _print_volume_snapshot(snapshot) -@utils.arg('volume_id', +@utils.arg( + 'volume_id', metavar='', help=_('ID of the volume to snapshot')) -@utils.arg('--force', +@utils.arg( + '--force', metavar='', help=_('Optional flag to indicate whether to snapshot a volume even if ' 'its attached to a server. (Default=False)'), default=False) -@utils.arg('--display-name', +@utils.arg( + '--display-name', metavar='', default=None, help=_('Optional snapshot name. (Default=None)')) -@utils.arg('--display_name', +@utils.arg( + '--display_name', help=argparse.SUPPRESS) -@utils.arg('--display-description', +@utils.arg( + '--display-description', metavar='', default=None, help=_('Optional snapshot description. (Default=None)')) -@utils.arg('--display_description', +@utils.arg( + '--display_description', help=argparse.SUPPRESS) @cliutils.service_type('volume') def do_volume_snapshot_create(cs, args): @@ -1977,7 +2137,8 @@ def do_volume_snapshot_create(cs, args): _print_volume_snapshot(snapshot) -@utils.arg('snapshot', +@utils.arg( + 'snapshot', metavar='', help=_('Name or ID of the snapshot to delete.')) @cliutils.service_type('volume') @@ -1998,9 +2159,10 @@ def do_volume_type_list(cs, args): _print_volume_type_list(vtypes) -@utils.arg('name', - metavar='', - help=_("Name of the new volume type")) +@utils.arg( + 'name', + metavar='', + help=_("Name of the new volume type")) @cliutils.service_type('volume') def do_volume_type_create(cs, args): """Create a new volume type.""" @@ -2008,9 +2170,10 @@ def do_volume_type_create(cs, args): _print_volume_type_list([vtype]) -@utils.arg('id', - metavar='', - help=_("Unique ID of the volume type to delete")) +@utils.arg( + 'id', + metavar='', + help=_("Unique ID of the volume type to delete")) @cliutils.service_type('volume') def do_volume_type_delete(cs, args): """Delete a specific volume type.""" @@ -2018,7 +2181,8 @@ def do_volume_type_delete(cs, args): @utils.arg('server', metavar='', help=_('Name or ID of server.')) -@utils.arg('console_type', +@utils.arg( + 'console_type', metavar='', help=_('Type of vnc console ("novnc" or "xvpvnc").')) def do_get_vnc_console(cs, args): @@ -2035,7 +2199,8 @@ def __init__(self, console_dict): @utils.arg('server', metavar='', help=_('Name or ID of server.')) -@utils.arg('console_type', +@utils.arg( + 'console_type', metavar='', help=_('Type of spice console ("spice-html5").')) def do_get_spice_console(cs, args): @@ -2052,7 +2217,8 @@ def __init__(self, console_dict): @utils.arg('server', metavar='', help=_('Name or ID of server.')) -@utils.arg('console_type', +@utils.arg( + 'console_type', metavar='', help='Type of rdp console ("rdp-html5").') def do_get_rdp_console(cs, args): @@ -2069,7 +2235,8 @@ def __init__(self, console_dict): @utils.arg('server', metavar='', help=_('Name or ID of server.')) -@utils.arg('--console_type', default='serial', +@utils.arg( + '--console_type', default='serial', help=_('Type of serial console, default="serial".')) def do_get_serial_console(cs, args): """Get a serial console to a server.""" @@ -2090,12 +2257,13 @@ def __init__(self, console_dict): @utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg('private_key', +@utils.arg( + 'private_key', metavar='', help=_('Private key (used locally to decrypt password) (Optional). ' - 'When specified, the command displays the clear (decrypted) VM ' - 'password. When not specified, the ciphered VM password is ' - 'displayed.'), + 'When specified, the command displays the clear (decrypted) VM ' + 'password. When not specified, the ciphered VM password is ' + 'displayed.'), nargs='?', default=None) def do_get_password(cs, args): @@ -2120,10 +2288,11 @@ def _print_floating_ip_list(floating_ips): @utils.arg('server', metavar='', help=_('Name or ID of server.')) -@utils.arg('--length', - metavar='', - default=None, - help=_('Length in lines to tail.')) +@utils.arg( + '--length', + metavar='', + default=None, + help=_('Length in lines to tail.')) def do_console_log(cs, args): """Get console log output of a server.""" server = _find_server(cs, args.server) @@ -2133,10 +2302,11 @@ def do_console_log(cs, args): @utils.arg('server', metavar='', help=_('Name or ID of server.')) @utils.arg('address', metavar='
', help=_('IP Address.')) -@utils.arg('--fixed-address', - metavar='', - default=None, - help=_('Fixed IP Address to associate with.')) +@utils.arg( + '--fixed-address', + metavar='', + default=None, + help=_('Fixed IP Address to associate with.')) def do_add_floating_ip(cs, args): """DEPRECATED, use floating-ip-associate instead.""" _associate_floating_ip(cs, args) @@ -2144,10 +2314,11 @@ def do_add_floating_ip(cs, args): @utils.arg('server', metavar='', help='Name or ID of server.') @utils.arg('address', metavar='
', help='IP Address.') -@utils.arg('--fixed-address', - metavar='', - default=None, - help='Fixed IP Address to associate with.') +@utils.arg( + '--fixed-address', + metavar='', + default=None, + help='Fixed IP Address to associate with.') def do_floating_ip_associate(cs, args): """Associate a floating IP address to a server.""" _associate_floating_ip(cs, args) @@ -2201,11 +2372,12 @@ def do_list_secgroup(cs, args): _print_secgroups(groups) -@utils.arg('pool', - metavar='', - help=_('Name of Floating IP Pool. (Optional)'), - nargs='?', - default=None) +@utils.arg( + 'pool', + metavar='', + help=_('Name of Floating IP Pool. (Optional)'), + nargs='?', + default=None) def do_floating_ip_create(cs, args): """Allocate a floating IP for the current tenant.""" _print_floating_ip_list([cs.floating_ips.create(pool=args.pool)]) @@ -2222,10 +2394,11 @@ def do_floating_ip_delete(cs, args): args.address) -@utils.arg('--all-tenants', - action='store_true', - default=False, - help=_('Display floatingips from all tenants (Admin only).')) +@utils.arg( + '--all-tenants', + action='store_true', + default=False, + help=_('Display floatingips from all tenants (Admin only).')) def do_floating_ip_list(cs, args): """List floating ips.""" _print_floating_ip_list(cs.floating_ips.list(args.all_tenants)) @@ -2236,8 +2409,9 @@ def do_floating_ip_pool_list(cs, _args): utils.print_list(cs.floating_ip_pools.list(), ['name']) -@utils.arg('--host', dest='host', metavar='', default=None, - help=_('Filter by host')) +@utils.arg( + '--host', dest='host', metavar='', default=None, + help=_('Filter by host')) def do_floating_ip_bulk_list(cs, args): """List all floating ips.""" utils.print_list(cs.floating_ips_bulk.list(args.host), ['project_id', @@ -2248,10 +2422,12 @@ def do_floating_ip_bulk_list(cs, args): @utils.arg('ip_range', metavar='', help=_('Address range to create')) -@utils.arg('--pool', dest='pool', metavar='', default=None, - help=_('Pool for new Floating IPs')) -@utils.arg('--interface', metavar='', default=None, - help=_('Interface for new Floating IPs')) +@utils.arg( + '--pool', dest='pool', metavar='', default=None, + help=_('Pool for new Floating IPs')) +@utils.arg( + '--interface', metavar='', default=None, + help=_('Interface for new Floating IPs')) def do_floating_ip_bulk_create(cs, args): """Bulk create floating ips by range.""" cs.floating_ips_bulk.create(args.ip_range, args.pool, args.interface) @@ -2269,7 +2445,7 @@ def _print_dns_list(dns_entries): def _print_domain_list(domain_entries): utils.print_list(domain_entries, ['domain', 'scope', - 'project', 'availability_zone']) + 'project', 'availability_zone']) def do_dns_domains(cs, args): @@ -2319,12 +2495,14 @@ def do_dns_delete_domain(cs, args): @utils.arg('domain', metavar='', help=_('DNS domain')) -@utils.arg('--availability-zone', +@utils.arg( + '--availability-zone', metavar='', default=None, help=_('Limit access to this domain to servers ' - 'in the specified availability zone.')) -@utils.arg('--availability_zone', + 'in the specified availability zone.')) +@utils.arg( + '--availability_zone', help=argparse.SUPPRESS) def do_dns_create_private_domain(cs, args): """Create the specified DNS domain.""" @@ -2333,10 +2511,11 @@ def do_dns_create_private_domain(cs, args): @utils.arg('domain', metavar='', help=_('DNS domain')) -@utils.arg('--project', metavar='', - help=_('Limit access to this domain to users ' - 'of the specified project.'), - default=None) +@utils.arg( + '--project', metavar='', + help=_('Limit access to this domain to users ' + 'of the specified project.'), + default=None) def do_dns_create_public_domain(cs, args): """Create the specified DNS domain.""" cs.dns_domains.create_public(args.domain, @@ -2397,16 +2576,20 @@ def _get_secgroup(cs, secgroup): return match_found -@utils.arg('secgroup', +@utils.arg( + 'secgroup', metavar='', help=_('ID or name of security group.')) -@utils.arg('ip_proto', +@utils.arg( + 'ip_proto', metavar='', help=_('IP protocol (icmp, tcp, udp).')) -@utils.arg('from_port', +@utils.arg( + 'from_port', metavar='', help=_('Port at start of range.')) -@utils.arg('to_port', +@utils.arg( + 'to_port', metavar='', help=_('Port at end of range.')) @utils.arg('cidr', metavar='', help=_('CIDR for address range.')) @@ -2421,16 +2604,20 @@ def do_secgroup_add_rule(cs, args): _print_secgroup_rules([rule]) -@utils.arg('secgroup', +@utils.arg( + 'secgroup', metavar='', help=_('ID or name of security group.')) -@utils.arg('ip_proto', +@utils.arg( + 'ip_proto', metavar='', help=_('IP protocol (icmp, tcp, udp).')) -@utils.arg('from_port', +@utils.arg( + 'from_port', metavar='', help=_('Port at start of range.')) -@utils.arg('to_port', +@utils.arg( + 'to_port', metavar='', help=_('Port at end of range.')) @utils.arg('cidr', metavar='', help=_('CIDR for address range.')) @@ -2450,20 +2637,23 @@ def do_secgroup_delete_rule(cs, args): @utils.arg('name', metavar='', help=_('Name of security group.')) -@utils.arg('description', metavar='', - help=_('Description of security group.')) +@utils.arg( + 'description', metavar='', + help=_('Description of security group.')) def do_secgroup_create(cs, args): """Create a security group.""" secgroup = cs.security_groups.create(args.name, args.description) _print_secgroups([secgroup]) -@utils.arg('secgroup', +@utils.arg( + 'secgroup', metavar='', help=_('ID or name of security group.')) @utils.arg('name', metavar='', help=_('Name of security group.')) -@utils.arg('description', metavar='', - help=_('Description of security group.')) +@utils.arg( + 'description', metavar='', + help=_('Description of security group.')) def do_secgroup_update(cs, args): """Update a security group.""" sg = _get_secgroup(cs, args.secgroup) @@ -2471,7 +2661,8 @@ def do_secgroup_update(cs, args): _print_secgroups([secgroup]) -@utils.arg('secgroup', +@utils.arg( + 'secgroup', metavar='', help=_('ID or name of security group.')) def do_secgroup_delete(cs, args): @@ -2481,7 +2672,8 @@ def do_secgroup_delete(cs, args): _print_secgroups([secgroup]) -@utils.arg('--all-tenants', +@utils.arg( + '--all-tenants', dest='all_tenants', metavar='<0|1>', nargs='?', @@ -2490,7 +2682,8 @@ def do_secgroup_delete(cs, args): default=int(strutils.bool_from_string( os.environ.get("ALL_TENANTS", 'false'), True)), help=_('Display information from all tenants (Admin only).')) -@utils.arg('--all_tenants', +@utils.arg( + '--all_tenants', nargs='?', type=int, const=1, @@ -2505,7 +2698,8 @@ def do_secgroup_list(cs, args): utils.print_list(groups, columns) -@utils.arg('secgroup', +@utils.arg( + 'secgroup', metavar='', help=_('ID or name of security group.')) def do_secgroup_list_rules(cs, args): @@ -2514,19 +2708,24 @@ def do_secgroup_list_rules(cs, args): _print_secgroup_rules(secgroup.rules) -@utils.arg('secgroup', +@utils.arg( + 'secgroup', metavar='', help=_('ID or name of security group.')) -@utils.arg('source_group', +@utils.arg( + 'source_group', metavar='', help=_('ID or name of source group.')) -@utils.arg('ip_proto', +@utils.arg( + 'ip_proto', metavar='', help=_('IP protocol (icmp, tcp, udp).')) -@utils.arg('from_port', +@utils.arg( + 'from_port', metavar='', help=_('Port at start of range.')) -@utils.arg('to_port', +@utils.arg( + 'to_port', metavar='', help=_('Port at end of range.')) def do_secgroup_add_group_rule(cs, args): @@ -2539,7 +2738,7 @@ def do_secgroup_add_group_rule(cs, args): if args.ip_proto or args.from_port or args.to_port: if not (args.ip_proto and args.from_port and args.to_port): raise exceptions.CommandError(_("ip_proto, from_port, and to_port" - " must be specified together")) + " must be specified together")) params['ip_protocol'] = args.ip_proto.upper() params['from_port'] = args.from_port params['to_port'] = args.to_port @@ -2548,19 +2747,24 @@ def do_secgroup_add_group_rule(cs, args): _print_secgroup_rules([rule]) -@utils.arg('secgroup', +@utils.arg( + 'secgroup', metavar='', help=_('ID or name of security group.')) -@utils.arg('source_group', +@utils.arg( + 'source_group', metavar='', help=_('ID or name of source group.')) -@utils.arg('ip_proto', +@utils.arg( + 'ip_proto', metavar='', help=_('IP protocol (icmp, tcp, udp).')) -@utils.arg('from_port', +@utils.arg( + 'from_port', metavar='', help=_('Port at start of range.')) -@utils.arg('to_port', +@utils.arg( + 'to_port', metavar='', help=_('Port at end of range.')) def do_secgroup_delete_group_rule(cs, args): @@ -2573,7 +2777,7 @@ def do_secgroup_delete_group_rule(cs, args): if args.ip_proto or args.from_port or args.to_port: if not (args.ip_proto and args.from_port and args.to_port): raise exceptions.CommandError(_("ip_proto, from_port, and to_port" - " must be specified together")) + " must be specified together")) params['ip_protocol'] = args.ip_proto.upper() params['from_port'] = int(args.from_port) params['to_port'] = int(args.to_port) @@ -2591,11 +2795,13 @@ def do_secgroup_delete_group_rule(cs, args): @utils.arg('name', metavar='', help=_('Name of key.')) -@utils.arg('--pub-key', +@utils.arg( + '--pub-key', metavar='', default=None, help=_('Path to a public ssh key.')) -@utils.arg('--pub_key', +@utils.arg( + '--pub_key', help=argparse.SUPPRESS) def do_keypair_add(cs, args): """Create a new key pair for use with servers.""" @@ -2639,7 +2845,8 @@ def _print_keypair(keypair): print(_("Public key: %s") % pk) -@utils.arg('keypair', +@utils.arg( + 'keypair', metavar='', help=_("Name or ID of keypair")) def do_keypair_show(cs, args): @@ -2837,12 +3044,14 @@ def simplify_usage(u): print(_('None')) -@utils.arg('pk_filename', +@utils.arg( + 'pk_filename', metavar='', nargs='?', default='pk.pem', help=_('Filename for the private key [Default: pk.pem]')) -@utils.arg('cert_filename', +@utils.arg( + 'cert_filename', metavar='', nargs='?', default='cert.pem', @@ -2872,11 +3081,12 @@ def do_x509_create_cert(cs, args): print(_("Wrote x509 certificate to %s") % args.cert_filename) -@utils.arg('filename', - metavar='', - nargs='?', - default='cacert.pem', - help=_('Filename to write the x509 root cert.')) +@utils.arg( + 'filename', + metavar='', + nargs='?', + default='cacert.pem', + help=_('Filename to write the x509 root cert.')) def do_x509_get_root_cert(cs, args): """Fetch the x509 root cert.""" if os.path.exists(args.filename): @@ -2945,7 +3155,8 @@ def do_aggregate_list(cs, args): @utils.arg('name', metavar='', help=_('Name of aggregate.')) -@utils.arg('availability_zone', +@utils.arg( + 'availability_zone', metavar='', default=None, nargs='?', @@ -2968,7 +3179,8 @@ def do_aggregate_delete(cs, args): @utils.arg('aggregate', metavar='', help=_('Name or ID of aggregate to update.')) @utils.arg('name', metavar='', help=_('Name of aggregate.')) -@utils.arg('availability_zone', +@utils.arg( + 'availability_zone', metavar='', nargs='?', default=None, @@ -2985,15 +3197,17 @@ def do_aggregate_update(cs, args): _print_aggregate_details(aggregate) -@utils.arg('aggregate', metavar='', - help=_('Name or ID of aggregate to update.')) -@utils.arg('metadata', - metavar='', - nargs='+', - action='append', - default=[], - help=_('Metadata to add/update to aggregate. ' - 'Specify only the key to delete a metadata item.')) +@utils.arg( + 'aggregate', metavar='', + help=_('Name or ID of aggregate to update.')) +@utils.arg( + 'metadata', + metavar='', + nargs='+', + action='append', + default=[], + help=_('Metadata to add/update to aggregate. ' + 'Specify only the key to delete a metadata item.')) def do_aggregate_set_metadata(cs, args): """Update the metadata associated with the aggregate.""" aggregate = _find_aggregate(cs, args.aggregate) @@ -3012,10 +3226,12 @@ def do_aggregate_set_metadata(cs, args): _print_aggregate_details(aggregate) -@utils.arg('aggregate', metavar='', - help=_('Name or ID of aggregate.')) -@utils.arg('host', metavar='', - help=_('The host to add to the aggregate.')) +@utils.arg( + 'aggregate', metavar='', + help=_('Name or ID of aggregate.')) +@utils.arg( + 'host', metavar='', + help=_('The host to add to the aggregate.')) def do_aggregate_add_host(cs, args): """Add the host to the specified aggregate.""" aggregate = _find_aggregate(cs, args.aggregate) @@ -3026,10 +3242,12 @@ def do_aggregate_add_host(cs, args): _print_aggregate_details(aggregate) -@utils.arg('aggregate', metavar='', - help=_('Name or ID of aggregate.')) -@utils.arg('host', metavar='', - help=_('The host to remove from the aggregate.')) +@utils.arg( + 'aggregate', metavar='', + help=_('Name or ID of aggregate.')) +@utils.arg( + 'host', metavar='', + help=_('The host to remove from the aggregate.')) def do_aggregate_remove_host(cs, args): """Remove the specified host from the specified aggregate.""" aggregate = _find_aggregate(cs, args.aggregate) @@ -3040,8 +3258,9 @@ def do_aggregate_remove_host(cs, args): _print_aggregate_details(aggregate) -@utils.arg('aggregate', metavar='', - help=_('Name or ID of aggregate.')) +@utils.arg( + 'aggregate', metavar='', + help=_('Name or ID of aggregate.')) def do_aggregate_details(cs, args): """Show details of the specified aggregate.""" aggregate = _find_aggregate(cs, args.aggregate) @@ -3065,22 +3284,27 @@ def parser_hosts(fields): @utils.arg('server', metavar='', help=_('Name or ID of server.')) -@utils.arg('host', metavar='', default=None, nargs='?', +@utils.arg( + 'host', metavar='', default=None, nargs='?', help=_('destination host name.')) -@utils.arg('--block-migrate', +@utils.arg( + '--block-migrate', action='store_true', dest='block_migrate', default=False, help=_('True in case of block_migration. (Default=False:live_migration)')) -@utils.arg('--block_migrate', +@utils.arg( + '--block_migrate', action='store_true', help=argparse.SUPPRESS) -@utils.arg('--disk-over-commit', +@utils.arg( + '--disk-over-commit', action='store_true', dest='disk_over_commit', default=False, help=_('Allow overcommit.(Default=False)')) -@utils.arg('--disk_over_commit', +@utils.arg( + '--disk_over_commit', action='store_true', help=argparse.SUPPRESS) def do_live_migration(cs, args): @@ -3090,11 +3314,13 @@ def do_live_migration(cs, args): args.disk_over_commit) -@utils.arg('server', metavar='', nargs='+', - help=_('Name or ID of server(s).')) -@utils.arg('--active', action='store_const', dest='state', - default='error', const='active', - help=_('Request the server be reset to "active" state instead ' +@utils.arg( + 'server', metavar='', nargs='+', + help=_('Name or ID of server(s).')) +@utils.arg( + '--active', action='store_const', dest='state', + default='error', const='active', + help=_('Request the server be reset to "active" state instead ' 'of "error" state (the default).')) def do_reset_state(cs, args): """Reset the state of a server.""" @@ -3199,7 +3425,7 @@ def do_host_describe(cs, args): @utils.arg('--zone', metavar='', default=None, help=_('Filters the list, returning only those ' - 'hosts in the availability zone .')) + 'hosts in the availability zone .')) def do_host_list(cs, args): """List all hosts by service.""" columns = ["host_name", "service", "zone"] @@ -3208,9 +3434,11 @@ def do_host_list(cs, args): @utils.arg('host', metavar='', help='Name of host.') -@utils.arg('--status', metavar='', default=None, dest='status', - help=_('Either enable or disable a host.')) -@utils.arg('--maintenance', +@utils.arg( + '--status', metavar='', default=None, dest='status', + help=_('Either enable or disable a host.')) +@utils.arg( + '--maintenance', metavar='', default=None, dest='maintenance', @@ -3230,9 +3458,10 @@ def do_host_update(cs, args): @utils.arg('host', metavar='', help='Name of host.') -@utils.arg('--action', metavar='', dest='action', - choices=['startup', 'shutdown', 'reboot'], - help=_('A power action: startup, reboot, or shutdown.')) +@utils.arg( + '--action', metavar='', dest='action', + choices=['startup', 'shutdown', 'reboot'], + help=_('A power action: startup, reboot, or shutdown.')) def do_host_action(cs, args): """Perform a power action on a host.""" result = cs.hosts.host_action(args.host, args.action) @@ -3274,17 +3503,18 @@ def __init__(self, **kwargs): hyper_id = hyper.id if hasattr(hyper, 'servers'): instances.extend([InstanceOnHyper(id=serv['uuid'], - name=serv['name'], - hypervisor_hostname=hyper_host, - hypervisor_id=hyper_id) - for serv in hyper.servers]) + name=serv['name'], + hypervisor_hostname=hyper_host, + hypervisor_id=hyper_id) + for serv in hyper.servers]) # Output the data utils.print_list(instances, ['ID', 'Name', 'Hypervisor ID', 'Hypervisor Hostname']) -@utils.arg('hypervisor', +@utils.arg( + 'hypervisor', metavar='', help=_('Name or ID of the hypervisor to show the details of.')) def do_hypervisor_show(cs, args): @@ -3293,7 +3523,8 @@ def do_hypervisor_show(cs, args): utils.print_dict(utils.flatten_dict(hyper._info)) -@utils.arg('hypervisor', +@utils.arg( + 'hypervisor', metavar='', help=_('Name or ID of the hypervisor to show the uptime of.')) def do_hypervisor_uptime(cs, args): @@ -3368,8 +3599,9 @@ def _get_first_endpoint(endpoints, region): raise LookupError("No suitable endpoint found") -@utils.arg('--wrap', dest='wrap', metavar='', default=64, - help=_('wrap PKI tokens to a specified length, or 0 to disable')) +@utils.arg( + '--wrap', dest='wrap', metavar='', default=64, + help=_('wrap PKI tokens to a specified length, or 0 to disable')) def do_credentials(cs, _args): """Show user credentials returned from auth.""" if isinstance(cs.client, client.SessionClient): @@ -3388,40 +3620,48 @@ def do_credentials(cs, _args): @utils.arg('server', metavar='', help=_('Name or ID of server.')) -@utils.arg('--port', +@utils.arg( + '--port', dest='port', action='store', type=int, default=22, help=_('Optional flag to indicate which port to use for ssh. ' - '(Default=22)')) -@utils.arg('--private', + '(Default=22)')) +@utils.arg( + '--private', dest='private', action='store_true', default=False, help=argparse.SUPPRESS) -@utils.arg('--address-type', +@utils.arg( + '--address-type', dest='address_type', action='store', type=str, default='floating', help=_('Optional flag to indicate which IP type to use. Possible values ' - 'includes fixed and floating (the Default).')) -@utils.arg('--network', metavar='', - help=_('Network to use for the ssh.'), default=None) -@utils.arg('--ipv6', + 'includes fixed and floating (the Default).')) +@utils.arg( + '--network', metavar='', + help=_('Network to use for the ssh.'), default=None) +@utils.arg( + '--ipv6', dest='ipv6', action='store_true', default=False, help=_('Optional flag to indicate whether to use an IPv6 address ' - 'attached to a server. (Defaults to IPv4 address)')) -@utils.arg('--login', metavar='', help=_('Login to use.'), - default="root") -@utils.arg('-i', '--identity', + 'attached to a server. (Defaults to IPv4 address)')) +@utils.arg( + '--login', metavar='', help=_('Login to use.'), + default="root") +@utils.arg( + '-i', '--identity', dest='identity', help=_('Private key file, same as the -i option to the ssh command.'), default='') -@utils.arg('--extra-opts', +@utils.arg( + '--extra-opts', dest='extra', help=_('Extra options to pass to ssh. see: man ssh'), default='') @@ -3530,11 +3770,13 @@ def _quota_update(manager, identifier, args): manager.update(identifier, **updates) -@utils.arg('--tenant', +@utils.arg( + '--tenant', metavar='', default=None, help=_('ID of tenant to list the quotas for.')) -@utils.arg('--user', +@utils.arg( + '--user', metavar='', default=None, help=_('ID of user to list the quotas for.')) @@ -3547,7 +3789,8 @@ def do_quota_show(cs, args): _quota_show(cs.quotas.get(args.tenant, user_id=args.user)) -@utils.arg('--tenant', +@utils.arg( + '--tenant', metavar='', default=None, help=_('ID of tenant to list the default quotas for.')) @@ -3560,93 +3803,114 @@ def do_quota_defaults(cs, args): _quota_show(cs.quotas.defaults(args.tenant)) -@utils.arg('tenant', +@utils.arg( + 'tenant', metavar='', help=_('ID of tenant to set the quotas for.')) -@utils.arg('--user', - metavar='', - default=None, - help=_('ID of user to set the quotas for.')) -@utils.arg('--instances', - metavar='', - type=int, default=None, - help=_('New value for the "instances" quota.')) -@utils.arg('--cores', - metavar='', - type=int, default=None, - help=_('New value for the "cores" quota.')) -@utils.arg('--ram', - metavar='', - type=int, default=None, - help=_('New value for the "ram" quota.')) -@utils.arg('--floating-ips', +@utils.arg( + '--user', + metavar='', + default=None, + help=_('ID of user to set the quotas for.')) +@utils.arg( + '--instances', + metavar='', + type=int, default=None, + help=_('New value for the "instances" quota.')) +@utils.arg( + '--cores', + metavar='', + type=int, default=None, + help=_('New value for the "cores" quota.')) +@utils.arg( + '--ram', + metavar='', + type=int, default=None, + help=_('New value for the "ram" quota.')) +@utils.arg( + '--floating-ips', metavar='', type=int, default=None, help=_('New value for the "floating-ips" quota.')) -@utils.arg('--floating_ips', +@utils.arg( + '--floating_ips', type=int, help=argparse.SUPPRESS) -@utils.arg('--fixed-ips', +@utils.arg( + '--fixed-ips', metavar='', type=int, default=None, help=_('New value for the "fixed-ips" quota.')) -@utils.arg('--metadata-items', +@utils.arg( + '--metadata-items', metavar='', type=int, default=None, help=_('New value for the "metadata-items" quota.')) -@utils.arg('--metadata_items', +@utils.arg( + '--metadata_items', type=int, help=argparse.SUPPRESS) -@utils.arg('--injected-files', +@utils.arg( + '--injected-files', metavar='', type=int, default=None, help=_('New value for the "injected-files" quota.')) -@utils.arg('--injected_files', +@utils.arg( + '--injected_files', type=int, help=argparse.SUPPRESS) -@utils.arg('--injected-file-content-bytes', +@utils.arg( + '--injected-file-content-bytes', metavar='', type=int, default=None, help=_('New value for the "injected-file-content-bytes" quota.')) -@utils.arg('--injected_file_content_bytes', +@utils.arg( + '--injected_file_content_bytes', type=int, help=argparse.SUPPRESS) -@utils.arg('--injected-file-path-bytes', +@utils.arg( + '--injected-file-path-bytes', metavar='', type=int, default=None, help=_('New value for the "injected-file-path-bytes" quota.')) -@utils.arg('--key-pairs', +@utils.arg( + '--key-pairs', metavar='', type=int, default=None, help=_('New value for the "key-pairs" quota.')) -@utils.arg('--security-groups', +@utils.arg( + '--security-groups', metavar='', type=int, default=None, help=_('New value for the "security-groups" quota.')) -@utils.arg('--security-group-rules', +@utils.arg( + '--security-group-rules', metavar='', type=int, default=None, help=_('New value for the "security-group-rules" quota.')) -@utils.arg('--server-groups', +@utils.arg( + '--server-groups', metavar='', type=int, default=None, help=_('New value for the "server-groups" quota.')) -@utils.arg('--server-group-members', +@utils.arg( + '--server-group-members', metavar='', type=int, default=None, help=_('New value for the "server-group-members" quota.')) -@utils.arg('--force', +@utils.arg( + '--force', dest='force', action="store_true", default=None, @@ -3673,7 +3937,8 @@ def do_quota_delete(cs, args): cs.quotas.delete(args.tenant, user_id=args.user) -@utils.arg('class_name', +@utils.arg( + 'class_name', metavar='', help=_('Name of quota class to list the quotas for.')) def do_quota_class_show(cs, args): @@ -3682,84 +3947,103 @@ def do_quota_class_show(cs, args): _quota_show(cs.quota_classes.get(args.class_name)) -@utils.arg('class_name', +@utils.arg( + 'class_name', metavar='', help=_('Name of quota class to set the quotas for.')) -@utils.arg('--instances', - metavar='', - type=int, default=None, - help=_('New value for the "instances" quota.')) -@utils.arg('--cores', - metavar='', - type=int, default=None, - help=_('New value for the "cores" quota.')) -@utils.arg('--ram', - metavar='', - type=int, default=None, - help=_('New value for the "ram" quota.')) -@utils.arg('--floating-ips', +@utils.arg( + '--instances', + metavar='', + type=int, default=None, + help=_('New value for the "instances" quota.')) +@utils.arg( + '--cores', + metavar='', + type=int, default=None, + help=_('New value for the "cores" quota.')) +@utils.arg( + '--ram', + metavar='', + type=int, default=None, + help=_('New value for the "ram" quota.')) +@utils.arg( + '--floating-ips', metavar='', type=int, default=None, help=_('New value for the "floating-ips" quota.')) -@utils.arg('--floating_ips', +@utils.arg( + '--floating_ips', type=int, help=argparse.SUPPRESS) -@utils.arg('--fixed-ips', +@utils.arg( + '--fixed-ips', metavar='', type=int, default=None, help=_('New value for the "fixed-ips" quota.')) -@utils.arg('--metadata-items', +@utils.arg( + '--metadata-items', metavar='', type=int, default=None, help=_('New value for the "metadata-items" quota.')) -@utils.arg('--metadata_items', +@utils.arg( + '--metadata_items', type=int, help=argparse.SUPPRESS) -@utils.arg('--injected-files', +@utils.arg( + '--injected-files', metavar='', type=int, default=None, help=_('New value for the "injected-files" quota.')) -@utils.arg('--injected_files', +@utils.arg( + '--injected_files', type=int, help=argparse.SUPPRESS) -@utils.arg('--injected-file-content-bytes', +@utils.arg( + '--injected-file-content-bytes', metavar='', type=int, default=None, help=_('New value for the "injected-file-content-bytes" quota.')) -@utils.arg('--injected_file_content_bytes', +@utils.arg( + '--injected_file_content_bytes', type=int, help=argparse.SUPPRESS) -@utils.arg('--injected-file-path-bytes', +@utils.arg( + '--injected-file-path-bytes', metavar='', type=int, default=None, help=_('New value for the "injected-file-path-bytes" quota.')) -@utils.arg('--key-pairs', +@utils.arg( + '--key-pairs', metavar='', type=int, default=None, help=_('New value for the "key-pairs" quota.')) -@utils.arg('--security-groups', +@utils.arg( + '--security-groups', metavar='', type=int, default=None, help=_('New value for the "security-groups" quota.')) -@utils.arg('--security-group-rules', +@utils.arg( + '--security-group-rules', metavar='', type=int, default=None, help=_('New value for the "security-group-rules" quota.')) -@utils.arg('--server-groups', +@utils.arg( + '--server-groups', metavar='', type=int, default=None, help=_('New value for the "server-groups" quota.')) -@utils.arg('--server-group-members', +@utils.arg( + '--server-group-members', metavar='', type=int, default=None, @@ -3771,15 +4055,18 @@ def do_quota_class_update(cs, args): @utils.arg('server', metavar='', help=_('Name or ID of server.')) -@utils.arg('host', metavar='', nargs='?', +@utils.arg( + 'host', metavar='', nargs='?', help=_("Name or ID of the target host. " "If no host is specified, the scheduler will choose one.")) -@utils.arg('--password', +@utils.arg( + '--password', dest='password', metavar='', help=_("Set the provided password on the evacuated server. Not applicable " "with on-shared-storage flag")) -@utils.arg('--on-shared-storage', +@utils.arg( + '--on-shared-storage', dest='on_shared_storage', action="store_true", default=False, @@ -3925,32 +4212,38 @@ def do_secgroup_list_default_rules(cs, args): show_source_group=False) -@utils.arg('ip_proto', +@utils.arg( + 'ip_proto', metavar='', help=_('IP protocol (icmp, tcp, udp).')) -@utils.arg('from_port', +@utils.arg( + 'from_port', metavar='', help=_('Port at start of range.')) -@utils.arg('to_port', +@utils.arg( + 'to_port', metavar='', help=_('Port at end of range.')) @utils.arg('cidr', metavar='', help=_('CIDR for address range.')) def do_secgroup_add_default_rule(cs, args): """Add a rule to the default security group.""" rule = cs.security_group_default_rules.create(args.ip_proto, - args.from_port, - args.to_port, - args.cidr) + args.from_port, + args.to_port, + args.cidr) _print_secgroup_rules([rule], show_source_group=False) -@utils.arg('ip_proto', +@utils.arg( + 'ip_proto', metavar='', help=_('IP protocol (icmp, tcp, udp).')) -@utils.arg('from_port', +@utils.arg( + 'from_port', metavar='', help=_('Port at start of range.')) -@utils.arg('to_port', +@utils.arg( + 'to_port', metavar='', help=_('Port at end of range.')) @utils.arg('cidr', metavar='', help=_('CIDR for address range.')) From 75727c2d37eb616fc2d0573200db44552e106e91 Mon Sep 17 00:00:00 2001 From: Eugeniya Kudryashova Date: Thu, 25 Sep 2014 18:13:22 +0300 Subject: [PATCH 0646/1705] Fix E128 failures in novaclient/v1_1/ E128 continuation line under-indented for visual indent Change-Id: I93660ef15df87993738c98f8bb1a88cc65f99870 --- novaclient/v1_1/cloudpipe.py | 2 +- novaclient/v1_1/contrib/baremetal.py | 77 +++++++++++++--------- novaclient/v1_1/contrib/cells.py | 16 +++-- novaclient/v1_1/contrib/host_evacuate.py | 4 +- novaclient/v1_1/contrib/instance_action.py | 28 ++++---- novaclient/v1_1/contrib/migrations.py | 4 +- novaclient/v1_1/contrib/tenant_networks.py | 7 +- novaclient/v1_1/servers.py | 7 +- novaclient/v1_1/volume_snapshots.py | 6 +- novaclient/v1_1/volumes.py | 11 ++-- 10 files changed, 94 insertions(+), 68 deletions(-) diff --git a/novaclient/v1_1/cloudpipe.py b/novaclient/v1_1/cloudpipe.py index 6e05f4a20..c0926ed40 100644 --- a/novaclient/v1_1/cloudpipe.py +++ b/novaclient/v1_1/cloudpipe.py @@ -39,7 +39,7 @@ def create(self, project): """ body = {'cloudpipe': {'project_id': project}} return self._create('/os-cloudpipe', body, 'instance_id', - return_raw=True) + return_raw=True) def list(self): """ diff --git a/novaclient/v1_1/contrib/baremetal.py b/novaclient/v1_1/contrib/baremetal.py index 02e7ad76b..325e27211 100644 --- a/novaclient/v1_1/contrib/baremetal.py +++ b/novaclient/v1_1/contrib/baremetal.py @@ -71,14 +71,14 @@ def create(self, :rtype: :class:`BareMetalNode` """ body = {'node': {'service_host': service_host, - 'cpus': cpus, - 'memory_mb': memory_mb, - 'local_gb': local_gb, - 'pm_address': pm_address, - 'pm_user': pm_user, - 'pm_password': pm_password, - 'prov_mac_address': prov_mac_address, - 'terminal_port': terminal_port}} + 'cpus': cpus, + 'memory_mb': memory_mb, + 'local_gb': local_gb, + 'pm_address': pm_address, + 'pm_user': pm_user, + 'pm_password': pm_password, + 'prov_mac_address': prov_mac_address, + 'terminal_port': terminal_port}} return self._create('/os-baremetal-nodes', body, 'node') @@ -151,49 +151,61 @@ def list_interfaces(self, node_id): return interfaces -@utils.arg('service_host', +@utils.arg( + 'service_host', metavar='', help=_('Name of nova compute host which will control this baremetal ' 'node')) -@utils.arg('cpus', +@utils.arg( + 'cpus', metavar='', type=int, help=_('Number of CPUs in the node')) -@utils.arg('memory_mb', +@utils.arg( + 'memory_mb', metavar='', type=int, help=_('Megabytes of RAM in the node')) -@utils.arg('local_gb', +@utils.arg( + 'local_gb', metavar='', type=int, help=_('Gigabytes of local storage in the node')) -@utils.arg('prov_mac_address', +@utils.arg( + 'prov_mac_address', metavar='', help=_('MAC address to provision the node')) -@utils.arg('--pm_address', default=None, +@utils.arg( + '--pm_address', default=None, metavar='', help=_('Power management IP for the node')) -@utils.arg('--pm_user', default=None, +@utils.arg( + '--pm_user', default=None, metavar='', help=_('Username for the node\'s power management')) -@utils.arg('--pm_password', default=None, +@utils.arg( + '--pm_password', default=None, metavar='', help=_('Password for the node\'s power management')) -@utils.arg('--terminal_port', default=None, +@utils.arg( + '--terminal_port', default=None, metavar='', type=int, help=_('ShellInABox port?')) def do_baremetal_node_create(cs, args): """Create a baremetal node.""" node = cs.baremetal.create(args.service_host, args.cpus, - args.memory_mb, args.local_gb, args.prov_mac_address, - pm_address=args.pm_address, pm_user=args.pm_user, - pm_password=args.pm_password, - terminal_port=args.terminal_port) + args.memory_mb, args.local_gb, + args.prov_mac_address, + pm_address=args.pm_address, + pm_user=args.pm_user, + pm_password=args.pm_password, + terminal_port=args.terminal_port) _print_baremetal_resource(node) -@utils.arg('node', +@utils.arg( + 'node', metavar='', help=_('ID of the node to delete.')) def do_baremetal_node_delete(cs, args): @@ -274,33 +286,38 @@ def _print_baremetal_node_interfaces(interfaces): ]) -@utils.arg('node', - metavar='', - help=_("ID of node")) +@utils.arg( + 'node', + metavar='', + help=_("ID of node")) def do_baremetal_node_show(cs, args): """Show information about a baremetal node.""" node = _find_baremetal_node(cs, args.node) _print_baremetal_resource(node) -@utils.arg('node', +@utils.arg( + 'node', metavar='', help=_("ID of node")) -@utils.arg('address', +@utils.arg( + 'address', metavar='
', help=_("MAC address of interface")) -@utils.arg('--datapath_id', +@utils.arg( + '--datapath_id', default=0, metavar='', help=_("OpenFlow Datapath ID of interface")) -@utils.arg('--port_no', +@utils.arg( + '--port_no', default=0, metavar='', help=_("OpenFlow port number of interface")) def do_baremetal_interface_add(cs, args): """Add a network interface to a baremetal node.""" bmif = cs.baremetal.add_interface(args.node, args.address, - args.datapath_id, args.port_no) + args.datapath_id, args.port_no) _print_baremetal_resource(bmif) diff --git a/novaclient/v1_1/contrib/cells.py b/novaclient/v1_1/contrib/cells.py index f3b544ed7..464ec8e15 100644 --- a/novaclient/v1_1/contrib/cells.py +++ b/novaclient/v1_1/contrib/cells.py @@ -46,19 +46,21 @@ def capacities(self, cell_name=None): return self._get("/os-cells/%s" % path, "cell") -@utils.arg('cell', - metavar='', - help=_('Name of the cell.')) +@utils.arg( + 'cell', + metavar='', + help=_('Name of the cell.')) def do_cell_show(cs, args): """Show details of a given cell.""" cell = cs.cells.get(args.cell) utils.print_dict(cell._info) -@utils.arg('--cell', - metavar='', - help=_("Name of the cell to get the capacities."), - default=None) +@utils.arg( + '--cell', + metavar='', + help=_("Name of the cell to get the capacities."), + default=None) def do_cell_capacities(cs, args): """Get cell capacities for all cells or a given cell.""" cell = cs.cells.capacities(args.cell) diff --git a/novaclient/v1_1/contrib/host_evacuate.py b/novaclient/v1_1/contrib/host_evacuate.py index fc9416afc..e935e23db 100644 --- a/novaclient/v1_1/contrib/host_evacuate.py +++ b/novaclient/v1_1/contrib/host_evacuate.py @@ -33,8 +33,8 @@ def _server_evacuate(cs, server, args): error_message = _("Error while evacuating instance: %s") % e return EvacuateHostResponse(base.Manager, {"server_uuid": server['uuid'], - "evacuate_accepted": success, - "error_message": error_message}) + "evacuate_accepted": success, + "error_message": error_message}) @utils.arg('host', metavar='', help='Name of host.') diff --git a/novaclient/v1_1/contrib/instance_action.py b/novaclient/v1_1/contrib/instance_action.py index 1e4ed5829..2f20e0611 100644 --- a/novaclient/v1_1/contrib/instance_action.py +++ b/novaclient/v1_1/contrib/instance_action.py @@ -30,22 +30,24 @@ def get(self, server, request_id): :param request_id: The request_id of the action to get. """ return self._get("/servers/%s/os-instance-actions/%s" % - (base.getid(server), request_id), 'instanceAction') + (base.getid(server), request_id), 'instanceAction') def list(self, server): """ Get a list of actions performed on an server. """ return self._list('/servers/%s/os-instance-actions' % - base.getid(server), 'instanceActions') + base.getid(server), 'instanceActions') -@utils.arg('server', - metavar='', - help=_('Name or UUID of the server to show an action for.')) -@utils.arg('request_id', - metavar='', - help=_('Request ID of the action to get.')) +@utils.arg( + 'server', + metavar='', + help=_('Name or UUID of the server to show an action for.')) +@utils.arg( + 'request_id', + metavar='', + help=_('Request ID of the action to get.')) def do_instance_action(cs, args): """Show an action.""" server = utils.find_resource(cs.servers, args.server) @@ -56,12 +58,14 @@ def do_instance_action(cs, args): utils.print_dict(action) -@utils.arg('server', - metavar='', - help=_('Name or UUID of the server to list actions for.')) +@utils.arg( + 'server', + metavar='', + help=_('Name or UUID of the server to list actions for.')) def do_instance_action_list(cs, args): """List actions on a server.""" server = utils.find_resource(cs.servers, args.server) actions = cs.instance_action.list(server) utils.print_list(actions, - ['Action', 'Request_ID', 'Message', 'Start_Time'], sortby_index=3) + ['Action', 'Request_ID', 'Message', 'Start_Time'], + sortby_index=3) diff --git a/novaclient/v1_1/contrib/migrations.py b/novaclient/v1_1/contrib/migrations.py index d43cb7a15..c10fb3519 100644 --- a/novaclient/v1_1/contrib/migrations.py +++ b/novaclient/v1_1/contrib/migrations.py @@ -73,8 +73,8 @@ def do_migration_list(cs, args): def _print_migrations(migrations): fields = ['Source Node', 'Dest Node', 'Source Compute', 'Dest Compute', - 'Dest Host', 'Status', 'Instance UUID', 'Old Flavor', - 'New Flavor', 'Created At', 'Updated At'] + 'Dest Host', 'Status', 'Instance UUID', 'Old Flavor', + 'New Flavor', 'Created At', 'Updated At'] def old_flavor(migration): return migration.old_instance_type_id diff --git a/novaclient/v1_1/contrib/tenant_networks.py b/novaclient/v1_1/contrib/tenant_networks.py index 288d06cb4..3bd2547d4 100644 --- a/novaclient/v1_1/contrib/tenant_networks.py +++ b/novaclient/v1_1/contrib/tenant_networks.py @@ -59,9 +59,10 @@ def do_net_list(cs, args): @utils.arg('label', metavar='', help=_('Network label (ex. my_new_network)')) -@utils.arg('cidr', metavar='', - help=_('IP block to allocate from (ex. 172.16.0.0/24 or ' - '2001:DB8::/64)')) +@utils.arg( + 'cidr', metavar='', + help=_('IP block to allocate from (ex. 172.16.0.0/24 or ' + '2001:DB8::/64)')) def do_net_create(cs, args): """ Create a network diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index 66db32deb..2fe29afab 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -266,7 +266,7 @@ def reboot(self, reboot_type=REBOOT_SOFT): self.manager.reboot(self, reboot_type) def rebuild(self, image, password=None, preserve_ephemeral=False, - **kwargs): + **kwargs): """ Rebuild -- shut down and then re-image -- this server. @@ -276,7 +276,8 @@ def rebuild(self, image, password=None, preserve_ephemeral=False, be preserved when rebuilding the instance. Defaults to False. """ return self.manager.rebuild(self, image, password=password, - preserve_ephemeral=preserve_ephemeral, **kwargs) + preserve_ephemeral=preserve_ephemeral, + **kwargs) def resize(self, flavor, **kwargs): """ @@ -911,7 +912,7 @@ def create(self, name, image, flavor, meta=None, files=None, response_key = "server" return self._boot(resource_url, response_key, *boot_args, - **boot_kwargs) + **boot_kwargs) def update(self, server, name=None): """ diff --git a/novaclient/v1_1/volume_snapshots.py b/novaclient/v1_1/volume_snapshots.py index 1cc72ee1b..d3bc91808 100644 --- a/novaclient/v1_1/volume_snapshots.py +++ b/novaclient/v1_1/volume_snapshots.py @@ -56,9 +56,9 @@ def create(self, volume_id, force=False, display_name=None, :rtype: :class:`Snapshot` """ body = {'snapshot': {'volume_id': volume_id, - 'force': force, - 'display_name': display_name, - 'display_description': display_description}} + 'force': force, + 'display_name': display_name, + 'display_description': display_description}} return self._create('/snapshots', body, 'snapshot') def get(self, snapshot_id): diff --git a/novaclient/v1_1/volumes.py b/novaclient/v1_1/volumes.py index 4ff92a093..a3ba327f7 100644 --- a/novaclient/v1_1/volumes.py +++ b/novaclient/v1_1/volumes.py @@ -113,9 +113,9 @@ def create_server_volume(self, server_id, volume_id, device): :rtype: :class:`Volume` """ body = {'volumeAttachment': {'volumeId': volume_id, - 'device': device}} + 'device': device}} return self._create("/servers/%s/os-volume_attachments" % server_id, - body, "volumeAttachment") + body, "volumeAttachment") def update_server_volume(self, server_id, attachment_id, new_volume_id): """ @@ -129,7 +129,8 @@ def update_server_volume(self, server_id, attachment_id, new_volume_id): """ body = {'volumeAttachment': {'volumeId': new_volume_id}} return self._update("/servers/%s/os-volume_attachments/%s" % - (server_id, attachment_id,), body, "volumeAttachment") + (server_id, attachment_id,), + body, "volumeAttachment") def get_server_volume(self, server_id, attachment_id): """ @@ -141,7 +142,7 @@ def get_server_volume(self, server_id, attachment_id): :rtype: :class:`Volume` """ return self._get("/servers/%s/os-volume_attachments/%s" % (server_id, - attachment_id,), "volumeAttachment") + attachment_id,), "volumeAttachment") def get_server_volumes(self, server_id): """ @@ -151,7 +152,7 @@ def get_server_volumes(self, server_id): :rtype: list of :class:`Volume` """ return self._list("/servers/%s/os-volume_attachments" % server_id, - "volumeAttachments") + "volumeAttachments") def delete_server_volume(self, server_id, attachment_id): """ From 545d60402fbd0d42bf99af6dfdd454e6aa769eb0 Mon Sep 17 00:00:00 2001 From: Eugeniya Kudryashova Date: Thu, 25 Sep 2014 18:55:43 +0300 Subject: [PATCH 0647/1705] Fix E128 failures in novaclient/v3 E128 continuation line under-indented for visual indent Change-Id: Id2b8bcc44e6835ac17d989928a4c54979637cd75 --- novaclient/v3/servers.py | 2 +- novaclient/v3/shell.py | 915 +++++++++++++++++++++++---------------- 2 files changed, 546 insertions(+), 371 deletions(-) diff --git a/novaclient/v3/servers.py b/novaclient/v3/servers.py index d25b6d84a..9ddc92fed 100644 --- a/novaclient/v3/servers.py +++ b/novaclient/v3/servers.py @@ -760,7 +760,7 @@ def create(self, name, image, flavor, meta=None, files=None, response_key = "server" return self._boot(resource_url, response_key, *boot_args, - **boot_kwargs) + **boot_kwargs) def update(self, server, name=None): """ diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py index 3b27911e7..1c35ed6d5 100644 --- a/novaclient/v3/shell.py +++ b/novaclient/v3/shell.py @@ -124,8 +124,10 @@ def _boot(cs, args): except IOError as e: raise exceptions.CommandError("Can't open '%s': %s" % (src, e)) except ValueError as e: - raise exceptions.CommandError("Invalid file argument '%s'. File " - "arguments must be of the form '--file '" % f) + raise exceptions.CommandError( + "Invalid file argument '%s'. File " + "arguments must be of the form '--file '" + % f) # use the os-keypair extension key_name = None @@ -219,109 +221,131 @@ def _boot(cs, args): return boot_args, boot_kwargs -@utils.arg('--flavor', - default=None, - metavar='', - help="Flavor ID (see 'nova flavor-list').") -@utils.arg('--image', - default=None, - metavar='', - help="Image ID (see 'nova image-list'). ") -@utils.arg('--image-with', - default=[], - type=_key_value_pairing, - action='append', - metavar='', - help="Image metadata property (see 'nova image-show'). ") -@utils.arg('--num-instances', - default=None, - type=int, - metavar='', - help=argparse.SUPPRESS) -@utils.arg('--min-count', - default=None, - type=int, - metavar='', - help="Boot at least servers (limited by quota).") -@utils.arg('--max-count', - default=None, - type=int, - metavar='', - help="Boot up to servers (limited by quota).") -@utils.arg('--meta', - metavar="", - action='append', - default=[], - help="Record arbitrary key/value metadata to /meta.js " - "on the new server. Can be specified multiple times.") -@utils.arg('--file', - metavar="", - action='append', - dest='files', - default=[], - help="Store arbitrary files from locally to " - "on the new server. You may store up to 5 files.") -@utils.arg('--key-name', +@utils.arg( + '--flavor', + default=None, + metavar='', + help="Flavor ID (see 'nova flavor-list').") +@utils.arg( + '--image', + default=None, + metavar='', + help="Image ID (see 'nova image-list'). ") +@utils.arg( + '--image-with', + default=[], + type=_key_value_pairing, + action='append', + metavar='', + help="Image metadata property (see 'nova image-show'). ") +@utils.arg( + '--num-instances', + default=None, + type=int, + metavar='', + help=argparse.SUPPRESS) +@utils.arg( + '--min-count', + default=None, + type=int, + metavar='', + help="Boot at least servers (limited by quota).") +@utils.arg( + '--max-count', + default=None, + type=int, + metavar='', + help="Boot up to servers (limited by quota).") +@utils.arg( + '--meta', + metavar="", + action='append', + default=[], + help="Record arbitrary key/value metadata to /meta.js " + "on the new server. Can be specified multiple times.") +@utils.arg( + '--file', + metavar="", + action='append', + dest='files', + default=[], + help="Store arbitrary files from locally to " + "on the new server. You may store up to 5 files.") +@utils.arg( + '--key-name', default=os.environ.get('NOVACLIENT_DEFAULT_KEY_NAME'), metavar='', help="Key name of keypair that should be created earlier with \ - the command keypair-add") -@utils.arg('--key_name', + the command keypair-add") +@utils.arg( + '--key_name', help=argparse.SUPPRESS) @utils.arg('name', metavar='', help='Name for the new server') -@utils.arg('--user-data', +@utils.arg( + '--user-data', default=None, metavar='', help="user data file to pass to be exposed by the metadata server.") -@utils.arg('--user_data', +@utils.arg( + '--user_data', help=argparse.SUPPRESS) -@utils.arg('--availability-zone', +@utils.arg( + '--availability-zone', default=None, metavar='', help="The availability zone for server placement.") -@utils.arg('--availability_zone', +@utils.arg( + '--availability_zone', help=argparse.SUPPRESS) -@utils.arg('--security-groups', +@utils.arg( + '--security-groups', default=None, metavar='', help="Comma separated list of security group names.") -@utils.arg('--security_groups', +@utils.arg( + '--security_groups', help=argparse.SUPPRESS) -@utils.arg('--block-device-mapping', +@utils.arg( + '--block-device-mapping', metavar="", action='append', default=[], help="Block device mapping in the format " - "=:::.") -@utils.arg('--block_device_mapping', + "=:::.") +@utils.arg( + '--block_device_mapping', action='append', help=argparse.SUPPRESS) -@utils.arg('--hint', - action='append', - dest='scheduler_hints', - default=[], - metavar='', - help="Send arbitrary key/value pairs to the scheduler for custom use.") -@utils.arg('--nic', - metavar="", - action='append', - dest='nics', - default=[], - help="Create a NIC on the server. " - "Specify option multiple times to create multiple NICs. " - "net-id: attach NIC to network with this UUID " - "(either port-id or net-id must be provided), " - "v4-fixed-ip: IPv4 fixed address for NIC (optional), " - "v6-fixed-ip: IPv6 fixed address for NIC (optional), " - "port-id: attach NIC to port with this UUID " - "(either port-id or net-id must be provided).") -@utils.arg('--config-drive', - metavar="", - dest='config_drive', - default=False, - help="Enable config drive") -@utils.arg('--poll', +@utils.arg( + '--hint', + action='append', + dest='scheduler_hints', + default=[], + metavar='', + help="Send arbitrary key/value pairs to the scheduler for custom use.") +@utils.arg( + '--nic', + metavar="", + action='append', + dest='nics', + default=[], + help="Create a NIC on the server. " + "Specify option multiple times to create multiple NICs. " + "net-id: attach NIC to network with this UUID " + "(either port-id or net-id must be provided), " + "v4-fixed-ip: IPv4 fixed address for NIC (optional), " + "v6-fixed-ip: IPv6 fixed address for NIC (optional), " + "port-id: attach NIC to port with this UUID " + "(either port-id or net-id must be provided).") +@utils.arg( + '--config-drive', + metavar="", + dest='config_drive', + default=False, + help="Enable config drive") +@utils.arg( + '--poll', dest='poll', action="store_true", default=False, @@ -407,8 +431,7 @@ def _translate_extended_states(collection): for item in collection: try: setattr(item, 'power_state', - power_states[getattr(item, 'power_state')] - ) + power_states[getattr(item, 'power_state')]) except AttributeError: setattr(item, 'power_state', "N/A") try: @@ -471,7 +494,8 @@ def do_flavor_list(cs, args): _print_flavor_list(flavors, args.extra_specs) -@utils.arg('flavor', +@utils.arg( + 'flavor', metavar='', help="Name or ID of the flavor to delete") def do_flavor_delete(cs, args): @@ -481,48 +505,58 @@ def do_flavor_delete(cs, args): _print_flavor_list([flavorid]) -@utils.arg('flavor', - metavar='', - help="Name or ID of flavor") +@utils.arg( + 'flavor', + metavar='', + help="Name or ID of flavor") def do_flavor_show(cs, args): """Show details about the given flavor.""" flavor = _find_flavor(cs, args.flavor) _print_flavor(flavor) -@utils.arg('name', - metavar='', - help="Name of the new flavor") -@utils.arg('id', - metavar='', - help="Unique ID (integer or UUID) for the new flavor." - " If specifying 'auto', a UUID will be generated as id") -@utils.arg('ram', - metavar='', - help="Memory size in MB") -@utils.arg('disk', - metavar='', - help="Disk size in GB") -@utils.arg('--ephemeral', - metavar='', - help="Ephemeral space size in GB (default 0)", - default=0) -@utils.arg('vcpus', - metavar='', - help="Number of vcpus") -@utils.arg('--swap', - metavar='', - help="Swap space size in MB (default 0)", - default=0) -@utils.arg('--rxtx-factor', - metavar='', - help="RX/TX factor (default 1)", - default=1.0) -@utils.arg('--is-public', - metavar='', - help="Make flavor accessible to the public (default true)", - type=lambda v: strutils.bool_from_string(v, True), - default=True) +@utils.arg( + 'name', + metavar='', + help="Name of the new flavor") +@utils.arg( + 'id', + metavar='', + help="Unique ID (integer or UUID) for the new flavor." + " If specifying 'auto', a UUID will be generated as id") +@utils.arg( + 'ram', + metavar='', + help="Memory size in MB") +@utils.arg( + 'disk', + metavar='', + help="Disk size in GB") +@utils.arg( + '--ephemeral', + metavar='', + help="Ephemeral space size in GB (default 0)", + default=0) +@utils.arg( + 'vcpus', + metavar='', + help="Number of vcpus") +@utils.arg( + '--swap', + metavar='', + help="Swap space size in MB (default 0)", + default=0) +@utils.arg( + '--rxtx-factor', + metavar='', + help="RX/TX factor (default 1)", + default=1.0) +@utils.arg( + '--is-public', + metavar='', + help="Make flavor accessible to the public (default true)", + type=lambda v: strutils.bool_from_string(v, True), + default=True) def do_flavor_create(cs, args): """Create a new flavor""" f = cs.flavors.create(args.name, args.ram, args.vcpus, args.disk, args.id, @@ -531,14 +565,17 @@ def do_flavor_create(cs, args): _print_flavor_list([f]) -@utils.arg('flavor', +@utils.arg( + 'flavor', metavar='', help="Name or ID of flavor") -@utils.arg('action', +@utils.arg( + 'action', metavar='', choices=['set', 'unset'], help="Actions: 'set' or 'unset'") -@utils.arg('metadata', +@utils.arg( + 'metadata', metavar='', nargs='+', action='append', @@ -555,9 +592,10 @@ def do_flavor_key(cs, args): flavor.unset_keys(keypair.keys()) -@utils.arg('--flavor', - metavar='', - help="Filter results by flavor name or ID.") +@utils.arg( + '--flavor', + metavar='', + help="Filter results by flavor name or ID.") @utils.arg('--tenant', metavar='', help='Filter results by tenant ID.') def do_flavor_access_list(cs, args): @@ -586,9 +624,10 @@ def do_flavor_access_list(cs, args): utils.print_list(access_list, columns) -@utils.arg('flavor', - metavar='', - help="Flavor name or ID to add access for the given tenant.") +@utils.arg( + 'flavor', + metavar='', + help="Flavor name or ID to add access for the given tenant.") @utils.arg('tenant', metavar='', help='Tenant ID to add flavor access for.') def do_flavor_access_add(cs, args): @@ -599,9 +638,10 @@ def do_flavor_access_add(cs, args): utils.print_list(access_list, columns) -@utils.arg('flavor', - metavar='', - help="Flavor name or ID to remove access for the given tenant.") +@utils.arg( + 'flavor', + metavar='', + help="Flavor name or ID to remove access for the given tenant.") @utils.arg('tenant', metavar='', help='Tenant ID to remove flavor access for.') def do_flavor_access_remove(cs, args): @@ -618,7 +658,7 @@ def do_scrub(cs, args): """Delete networks and security groups associated with a project.""" networks_list = cs.networks.list() networks_list = [network for network in networks_list - if getattr(network, 'project_id', '') == args.project_id] + if getattr(network, 'project_id', '') == args.project_id] search_opts = {'all_tenants': 1} groups = cs.security_groups.list(search_opts) groups = [group for group in groups @@ -636,9 +676,10 @@ def do_network_list(cs, _args): utils.print_list(network_list, columns) -@utils.arg('network', - metavar='', - help="uuid or label of network") +@utils.arg( + 'network', + metavar='', + help="uuid or label of network") def do_network_show(cs, args): """Show details about the given network.""" network = utils.find_resource(cs.networks, args.network) @@ -659,9 +700,10 @@ def do_network_show(cs, args): type=int, const=1, default=0) -@utils.arg('network', - metavar='', - help="uuid of network") +@utils.arg( + 'network', + metavar='', + help="uuid of network") def do_network_disassociate(cs, args): """Disassociate host and/or project from the given network.""" if args.host_only: @@ -672,20 +714,23 @@ def do_network_disassociate(cs, args): cs.networks.disassociate(args.network, True, True) -@utils.arg('network', - metavar='', - help="uuid of network") -@utils.arg('host', - metavar='', - help="Name of host") +@utils.arg( + 'network', + metavar='', + help="uuid of network") +@utils.arg( + 'host', + metavar='', + help="Name of host") def do_network_associate_host(cs, args): """Associate host with network.""" cs.networks.associate_host(args.network, args.host) -@utils.arg('network', - metavar='', - help="uuid of network") +@utils.arg( + 'network', + metavar='', + help="uuid of network") def do_network_associate_project(cs, args): """Associate project with network.""" cs.networks.associate_project(args.network) @@ -704,65 +749,81 @@ def _filter_network_create_options(args): return kwargs -@utils.arg('label', - metavar='', - help="Label for network") -@utils.arg('--fixed-range-v4', - dest='cidr', - metavar='', - help="IPv4 subnet (ex: 10.0.0.0/8)") -@utils.arg('--fixed-range-v6', - dest="cidr_v6", - help='IPv6 subnet (ex: fe80::/64') -@utils.arg('--vlan', - dest='vlan_start', - metavar='', - help="vlan id") -@utils.arg('--vpn', - dest='vpn_start', - metavar='', - help="vpn start") -@utils.arg('--gateway', - dest="gateway", - help='gateway') -@utils.arg('--gateway-v6', - dest="gateway_v6", - help='IPv6 gateway') -@utils.arg('--bridge', - dest="bridge", - metavar='', - help='VIFs on this network are connected to this bridge.') -@utils.arg('--bridge-interface', - dest="bridge_interface", - metavar='', - help='The bridge is connected to this interface.') -@utils.arg('--multi-host', - dest="multi_host", - metavar="<'T'|'F'>", - help='Multi host') -@utils.arg('--dns1', - dest="dns1", - metavar="", help='First DNS') -@utils.arg('--dns2', - dest="dns2", - metavar="", - help='Second DNS') -@utils.arg('--uuid', - dest="uuid", - metavar="", - help='Network UUID') -@utils.arg('--fixed-cidr', - dest="fixed_cidr", - metavar='', - help='IPv4 subnet for fixed IPs (ex: 10.20.0.0/16)') -@utils.arg('--project-id', - dest="project_id", - metavar="", - help='Project ID') -@utils.arg('--priority', - dest="priority", - metavar="", - help='Network interface priority') +@utils.arg( + 'label', + metavar='', + help="Label for network") +@utils.arg( + '--fixed-range-v4', + dest='cidr', + metavar='', + help="IPv4 subnet (ex: 10.0.0.0/8)") +@utils.arg( + '--fixed-range-v6', + dest="cidr_v6", + help='IPv6 subnet (ex: fe80::/64') +@utils.arg( + '--vlan', + dest='vlan_start', + metavar='', + help="vlan id") +@utils.arg( + '--vpn', + dest='vpn_start', + metavar='', + help="vpn start") +@utils.arg( + '--gateway', + dest="gateway", + help='gateway') +@utils.arg( + '--gateway-v6', + dest="gateway_v6", + help='IPv6 gateway') +@utils.arg( + '--bridge', + dest="bridge", + metavar='', + help='VIFs on this network are connected to this bridge.') +@utils.arg( + '--bridge-interface', + dest="bridge_interface", + metavar='', + help='The bridge is connected to this interface.') +@utils.arg( + '--multi-host', + dest="multi_host", + metavar="<'T'|'F'>", + help='Multi host') +@utils.arg( + '--dns1', + dest="dns1", + metavar="", help='First DNS') +@utils.arg( + '--dns2', + dest="dns2", + metavar="", + help='Second DNS') +@utils.arg( + '--uuid', + dest="uuid", + metavar="", + help='Network UUID') +@utils.arg( + '--fixed-cidr', + dest="fixed_cidr", + metavar='', + help='IPv4 subnet for fixed IPs (ex: 10.20.0.0/16)') +@utils.arg( + '--project-id', + dest="project_id", + metavar="", + help='Project ID') +@utils.arg( + '--priority', + dest="priority", + metavar="", + help='Network interface priority') def do_network_create(cs, args): """Create a network.""" @@ -777,10 +838,11 @@ def do_network_create(cs, args): cs.networks.create(**kwargs) -@utils.arg('--limit', - dest="limit", - metavar="", - help='Number of images to return per request.') +@utils.arg( + '--limit', + dest="limit", + metavar="", + help='Number of images to return per request.') @cliutils.service_type('image') def do_image_list(cs, _args): """Print a list of available images to boot from.""" @@ -798,19 +860,22 @@ def parse_server_name(image): fmts, sortby_index=1) -@utils.arg('image', - metavar='', - help="Name or ID of image") -@utils.arg('action', - metavar='', - choices=['set', 'delete'], - help="Actions: 'set' or 'delete'") -@utils.arg('metadata', - metavar='', - nargs='+', - action='append', - default=[], - help='Metadata to add/update or delete (only key is necessary on delete)') +@utils.arg( + 'image', + metavar='', + help="Name or ID of image") +@utils.arg( + 'action', + metavar='', + choices=['set', 'delete'], + help="Actions: 'set' or 'delete'") +@utils.arg( + 'metadata', + metavar='', + nargs='+', + action='append', + default=[], + help='Metadata to add/update or delete (only key is necessary on delete)') def do_image_meta(cs, args): """Set or Delete metadata on an image.""" image = _find_image(cs, args.image) @@ -867,9 +932,10 @@ def _print_flavor(flavor): utils.print_dict(info) -@utils.arg('image', - metavar='', - help="Name or ID of image") +@utils.arg( + 'image', + metavar='', + help="Name or ID of image") @cliutils.service_type('image') def do_image_show(cs, args): """Show details about the given image.""" @@ -888,57 +954,69 @@ def do_image_delete(cs, args): print("Delete for image %s failed: %s" % (image, e)) -@utils.arg('--reservation-id', +@utils.arg( + '--reservation-id', dest='reservation_id', metavar='', default=None, help='Only return servers that match reservation-id.') -@utils.arg('--reservation_id', +@utils.arg( + '--reservation_id', help=argparse.SUPPRESS) -@utils.arg('--ip', +@utils.arg( + '--ip', dest='ip', metavar='', default=None, help='Search with regular expression match by IP address.') -@utils.arg('--ip6', +@utils.arg( + '--ip6', dest='ip6', metavar='', default=None, help='Search with regular expression match by IPv6 address.') -@utils.arg('--name', +@utils.arg( + '--name', dest='name', metavar='', default=None, help='Search with regular expression match by name') -@utils.arg('--instance-name', +@utils.arg( + '--instance-name', dest='instance_name', metavar='', default=None, help='Search with regular expression match by server name.') -@utils.arg('--instance_name', +@utils.arg( + '--instance_name', help=argparse.SUPPRESS) -@utils.arg('--status', +@utils.arg( + '--status', dest='status', metavar='', default=None, help='Search by server status') -@utils.arg('--flavor', +@utils.arg( + '--flavor', dest='flavor', metavar='', default=None, help='Search by flavor name or ID') -@utils.arg('--image', +@utils.arg( + '--image', dest='image', metavar='', default=None, help='Search by image name or ID') -@utils.arg('--host', +@utils.arg( + '--host', dest='host', metavar='', default=None, help='Search servers by hostname to which they are assigned ' '(Admin only).') -@utils.arg('--all-tenants', +@utils.arg( + '--all-tenants', dest='all_tenants', metavar='<0|1>', nargs='?', @@ -947,28 +1025,33 @@ def do_image_delete(cs, args): default=int(strutils.bool_from_string( os.environ.get("ALL_TENANTS", 'false'), True)), help='Display information from all tenants (Admin only).') -@utils.arg('--all_tenants', +@utils.arg( + '--all_tenants', nargs='?', type=int, const=1, help=argparse.SUPPRESS) -@utils.arg('--tenant', +@utils.arg( + '--tenant', # nova db searches by project_id dest='tenant', metavar='', nargs='?', help='Display information from single tenant (Admin only).') -@utils.arg('--fields', +@utils.arg( + '--fields', default=None, metavar='', help='Comma-separated list of fields to display. ' 'Use the show command to see which fields are available.') -@utils.arg('--deleted', +@utils.arg( + '--deleted', dest='deleted', action="store_true", default=False, help='Only display deleted servers (Admin only).') -@utils.arg('--minimal', +@utils.arg( + '--minimal', dest='minimal', action="store_true", default=False, @@ -1040,14 +1123,16 @@ def do_list(cs, args): formatters, sortby_index=1) -@utils.arg('--hard', +@utils.arg( + '--hard', dest='reboot_type', action='store_const', const=servers.REBOOT_HARD, default=servers.REBOOT_SOFT, help='Perform a hard reboot (instead of a soft one).') @utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg('--poll', +@utils.arg( + '--poll', dest='poll', action="store_true", default=False, @@ -1064,19 +1149,23 @@ def do_reboot(cs, args): @utils.arg('server', metavar='', help='Name or ID of server.') @utils.arg('image', metavar='', help="Name or ID of new image.") -@utils.arg('--rebuild-password', +@utils.arg( + '--rebuild-password', dest='rebuild_password', metavar='', default=False, help="Set the provided password on the rebuild server.") -@utils.arg('--rebuild_password', +@utils.arg( + '--rebuild_password', help=argparse.SUPPRESS) -@utils.arg('--poll', +@utils.arg( + '--poll', dest='poll', action="store_true", default=False, help='Report the server rebuild progress until it completes.') -@utils.arg('--minimal', +@utils.arg( + '--minimal', dest='minimal', action="store_true", default=False, @@ -1109,7 +1198,8 @@ def do_rename(cs, args): @utils.arg('server', metavar='', help='Name or ID of server.') @utils.arg('flavor', metavar='', help="Name or ID of new flavor.") -@utils.arg('--poll', +@utils.arg( + '--poll', dest='poll', action="store_true", default=False, @@ -1138,7 +1228,8 @@ def do_resize_revert(cs, args): @utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg('--poll', +@utils.arg( + '--poll', dest='poll', action="store_true", default=False, @@ -1237,17 +1328,19 @@ def do_root_password(cs, args): @utils.arg('server', metavar='', help='Name or ID of server.') @utils.arg('name', metavar='', help='Name of snapshot.') -@utils.arg('--show', +@utils.arg( + '--show', dest='show', action="store_true", default=False, help='Print image info.') -@utils.arg('--poll', +@utils.arg( + '--poll', dest='poll', action="store_true", default=False, help='Report the snapshot progress and poll until image creation is ' - 'complete.') + 'complete.') def do_image_create(cs, args): """Create a new image by taking a snapshot of a running server.""" server = _find_server(cs, args.server) @@ -1289,19 +1382,22 @@ def do_backup(cs, args): args.rotation) -@utils.arg('server', - metavar='', - help="Name or ID of server") -@utils.arg('action', - metavar='', - choices=['set', 'delete'], - help="Actions: 'set' or 'delete'") -@utils.arg('metadata', - metavar='', - nargs='+', - action='append', - default=[], - help='Metadata to set or delete (only key is necessary on delete)') +@utils.arg( + 'server', + metavar='', + help="Name or ID of server") +@utils.arg( + 'action', + metavar='', + choices=['set', 'delete'], + help="Actions: 'set' or 'delete'") +@utils.arg( + 'metadata', + metavar='', + nargs='+', + action='append', + default=[], + help='Metadata to set or delete (only key is necessary on delete)') def do_meta(cs, args): """Set or Delete metadata on a server.""" server = _find_server(cs, args.server) @@ -1364,7 +1460,8 @@ def _print_server(cs, args, server=None): utils.print_dict(info) -@utils.arg('--minimal', +@utils.arg( + '--minimal', dest='minimal', action="store_true", default=False, @@ -1413,7 +1510,8 @@ def _find_flavor(cs, flavor): @utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg('network_id', +@utils.arg( + 'network_id', metavar='', help='Network ID.') def do_add_fixed_ip(cs, args): @@ -1435,21 +1533,26 @@ def _translate_availability_zone_keys(collection): [('zone_name', 'name'), ('zone_state', 'status')]) -@utils.arg('server', +@utils.arg( + 'server', metavar='', help='Name or ID of server.') -@utils.arg('volume', +@utils.arg( + 'volume', metavar='', help='ID of the volume to attach.') -@utils.arg('device', metavar='', default=None, nargs='?', +@utils.arg( + 'device', metavar='', default=None, nargs='?', help='Name of the device e.g. /dev/vdb. ' 'Use "auto" for autoassign (if supported)') -@utils.arg('disk_bus', +@utils.arg( + 'disk_bus', metavar='', default=None, nargs='?', help='The disk bus e.g. ide of the volume (optional).') -@utils.arg('device_type', +@utils.arg( + 'device_type', metavar='', default=None, nargs='?', @@ -1464,13 +1567,16 @@ def do_volume_attach(cs, args): args.device_type) -@utils.arg('server', +@utils.arg( + 'server', metavar='', help='Name or ID of server.') -@utils.arg('attachment_id', +@utils.arg( + 'attachment_id', metavar='', help='Attachment ID of the volume.') -@utils.arg('new_volume', +@utils.arg( + 'new_volume', metavar='', help='ID of the volume to attach.') def do_volume_update(cs, args): @@ -1479,10 +1585,12 @@ def do_volume_update(cs, args): args.attachment_id, args.new_volume) -@utils.arg('server', +@utils.arg( + 'server', metavar='', help='Name or ID of server.') -@utils.arg('attachment_id', +@utils.arg( + 'attachment_id', metavar='', help='ID of the volume to detach.') def do_volume_detach(cs, args): @@ -1492,7 +1600,8 @@ def do_volume_detach(cs, args): @utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg('console_type', +@utils.arg( + 'console_type', metavar='', help='Type of vnc console ("novnc" or "xvpvnc").') def do_get_vnc_console(cs, args): @@ -1509,7 +1618,8 @@ def __init__(self, console_dict): @utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg('console_type', +@utils.arg( + 'console_type', metavar='', help='Type of spice console ("spice-html5").') def do_get_spice_console(cs, args): @@ -1526,7 +1636,8 @@ def __init__(self, console_dict): @utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg('console_type', +@utils.arg( + 'console_type', metavar='', help='Type of rdp console ("rdp-html5").') def do_get_rdp_console(cs, args): @@ -1543,7 +1654,8 @@ def __init__(self, console_dict): @utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg('private_key', +@utils.arg( + 'private_key', metavar='', help='Private key (used locally to decrypt password) (Optional). ' 'When specified, the command displays the clear (decrypted) VM ' @@ -1683,7 +1795,7 @@ def _print_dns_list(dns_entries): def _print_domain_list(domain_entries): utils.print_list(domain_entries, ['domain', 'scope', - 'project', 'availability_zone']) + 'project', 'availability_zone']) def do_dns_domains(cs, args): @@ -1731,12 +1843,14 @@ def do_dns_delete_domain(cs, args): @utils.arg('domain', metavar='', help='DNS domain') -@utils.arg('--availability-zone', +@utils.arg( + '--availability-zone', metavar='', default=None, help='Limit access to this domain to servers ' - 'in the specified availability zone.') -@utils.arg('--availability_zone', + 'in the specified availability zone.') +@utils.arg( + '--availability_zone', help=argparse.SUPPRESS) def do_dns_create_private_domain(cs, args): """Create the specified DNS domain.""" @@ -1791,8 +1905,7 @@ def _get_secgroup(cs, secgroup): match_found = False for s in cs.security_groups.list(): encoding = (locale.getpreferredencoding() or - sys.stdin.encoding or - 'UTF-8') + sys.stdin.encoding or 'UTF-8') s.name = s.name.encode(encoding) if secgroup == s.name: if match_found is not False: @@ -1806,16 +1919,20 @@ def _get_secgroup(cs, secgroup): return match_found -@utils.arg('secgroup', +@utils.arg( + 'secgroup', metavar='', help='ID or name of security group.') -@utils.arg('ip_proto', +@utils.arg( + 'ip_proto', metavar='', help='IP protocol (icmp, tcp, udp).') -@utils.arg('from_port', +@utils.arg( + 'from_port', metavar='', help='Port at start of range.') -@utils.arg('to_port', +@utils.arg( + 'to_port', metavar='', help='Port at end of range.') @utils.arg('cidr', metavar='', help='CIDR for address range.') @@ -1830,16 +1947,20 @@ def do_secgroup_add_rule(cs, args): _print_secgroup_rules([rule]) -@utils.arg('secgroup', +@utils.arg( + 'secgroup', metavar='', help='ID or name of security group.') -@utils.arg('ip_proto', +@utils.arg( + 'ip_proto', metavar='', help='IP protocol (icmp, tcp, udp).') -@utils.arg('from_port', +@utils.arg( + 'from_port', metavar='', help='Port at start of range.') -@utils.arg('to_port', +@utils.arg( + 'to_port', metavar='', help='Port at end of range.') @utils.arg('cidr', metavar='', help='CIDR for address range.') @@ -1867,7 +1988,8 @@ def do_secgroup_create(cs, args): _print_secgroups([secgroup]) -@utils.arg('secgroup', +@utils.arg( + 'secgroup', metavar='', help='ID or name of security group.') @utils.arg('name', metavar='', help='Name of security group.') @@ -1880,7 +2002,8 @@ def do_secgroup_update(cs, args): _print_secgroups([secgroup]) -@utils.arg('secgroup', +@utils.arg( + 'secgroup', metavar='', help='ID or name of security group.') def do_secgroup_delete(cs, args): @@ -1890,7 +2013,8 @@ def do_secgroup_delete(cs, args): _print_secgroups([secgroup]) -@utils.arg('--all-tenants', +@utils.arg( + '--all-tenants', dest='all_tenants', metavar='<0|1>', nargs='?', @@ -1899,7 +2023,8 @@ def do_secgroup_delete(cs, args): default=int(strutils.bool_from_string( os.environ.get("ALL_TENANTS", 'false'), True)), help='Display information from all tenants (Admin only).') -@utils.arg('--all_tenants', +@utils.arg( + '--all_tenants', nargs='?', type=int, const=1, @@ -1914,7 +2039,8 @@ def do_secgroup_list(cs, args): utils.print_list(groups, columns) -@utils.arg('secgroup', +@utils.arg( + 'secgroup', metavar='', help='ID or name of security group.') def do_secgroup_list_rules(cs, args): @@ -1923,19 +2049,24 @@ def do_secgroup_list_rules(cs, args): _print_secgroup_rules(secgroup.rules) -@utils.arg('secgroup', +@utils.arg( + 'secgroup', metavar='', help='ID or name of security group.') -@utils.arg('source_group', +@utils.arg( + 'source_group', metavar='', help='ID or name of source group.') -@utils.arg('ip_proto', +@utils.arg( + 'ip_proto', metavar='', help='IP protocol (icmp, tcp, udp).') -@utils.arg('from_port', +@utils.arg( + 'from_port', metavar='', help='Port at start of range.') -@utils.arg('to_port', +@utils.arg( + 'to_port', metavar='', help='Port at end of range.') def do_secgroup_add_group_rule(cs, args): @@ -1957,19 +2088,24 @@ def do_secgroup_add_group_rule(cs, args): _print_secgroup_rules([rule]) -@utils.arg('secgroup', +@utils.arg( + 'secgroup', metavar='', help='ID or name of security group.') -@utils.arg('source_group', +@utils.arg( + 'source_group', metavar='', help='ID or name of source group.') -@utils.arg('ip_proto', +@utils.arg( + 'ip_proto', metavar='', help='IP protocol (icmp, tcp, udp).') -@utils.arg('from_port', +@utils.arg( + 'from_port', metavar='', help='Port at start of range.') -@utils.arg('to_port', +@utils.arg( + 'to_port', metavar='', help='Port at end of range.') def do_secgroup_delete_group_rule(cs, args): @@ -1999,11 +2135,13 @@ def do_secgroup_delete_group_rule(cs, args): @utils.arg('name', metavar='', help='Name of key.') -@utils.arg('--pub-key', +@utils.arg( + '--pub-key', metavar='', default=None, help='Path to a public ssh key.') -@utils.arg('--pub_key', +@utils.arg( + '--pub_key', help=argparse.SUPPRESS) def do_keypair_add(cs, args): """Create a new key pair for use with servers.""" @@ -2046,7 +2184,8 @@ def _print_keypair(keypair): print("Public key: %s" % pk) -@utils.arg('keypair', +@utils.arg( + 'keypair', metavar='', help="Name or ID of keypair") def do_keypair_show(cs, args): @@ -2158,12 +2297,14 @@ def simplify_usage(u): print('None') -@utils.arg('pk_filename', +@utils.arg( + 'pk_filename', metavar='', nargs='?', default='pk.pem', help='Filename for the private key [Default: pk.pem]') -@utils.arg('cert_filename', +@utils.arg( + 'cert_filename', metavar='', nargs='?', default='cert.pem', @@ -2173,10 +2314,10 @@ def do_x509_create_cert(cs, args): if os.path.exists(args.pk_filename): raise exceptions.CommandError("Unable to write privatekey - %s exists." - % args.pk_filename) + % args.pk_filename) if os.path.exists(args.cert_filename): raise exceptions.CommandError("Unable to write x509 cert - %s exists." - % args.cert_filename) + % args.cert_filename) certs = cs.certs.create() @@ -2266,7 +2407,8 @@ def do_aggregate_list(cs, args): @utils.arg('name', metavar='', help='Name of aggregate.') -@utils.arg('availability_zone', +@utils.arg( + 'availability_zone', metavar='', default=None, nargs='?', @@ -2289,7 +2431,8 @@ def do_aggregate_delete(cs, args): @utils.arg('aggregate', metavar='', help='Name or ID of aggregate to update.') @utils.arg('name', metavar='', help='Name of aggregate.') -@utils.arg('availability_zone', +@utils.arg( + 'availability_zone', metavar='', nargs='?', default=None, @@ -2345,7 +2488,7 @@ def do_aggregate_add_host(cs, args): @utils.arg('aggregate', metavar='', help='Name or ID of aggregate.') @utils.arg('host', metavar='', - help='The host to remove from the aggregate.') + help='The host to remove from the aggregate.') def do_aggregate_remove_host(cs, args): """Remove the specified host from the specified aggregate.""" aggregate = _find_aggregate(cs, args.aggregate) @@ -2379,23 +2522,28 @@ def parser_hosts(fields): @utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg('host', metavar='', default=None, nargs='?', +@utils.arg( + 'host', metavar='', default=None, nargs='?', help='destination host name.') -@utils.arg('--block-migrate', +@utils.arg( + '--block-migrate', action='store_true', dest='block_migrate', default=False, help='True in case of block_migration.\ (Default=False:live_migration)') -@utils.arg('--block_migrate', +@utils.arg( + '--block_migrate', action='store_true', help=argparse.SUPPRESS) -@utils.arg('--disk-over-commit', +@utils.arg( + '--disk-over-commit', action='store_true', dest='disk_over_commit', default=False, help='Allow overcommit.(Default=False)') -@utils.arg('--disk_over_commit', +@utils.arg( + '--disk_over_commit', action='store_true', help=argparse.SUPPRESS) def do_live_migration(cs, args): @@ -2426,18 +2574,21 @@ def __init__(self, server_uuid, live_migration_accepted, @utils.arg('host', metavar='', help='Name of host.') -@utils.arg('--target-host', - metavar='', - default=None, - help='Name of target host.') -@utils.arg('--block-migrate', - action='store_true', - default=False, - help='Enable block migration.') -@utils.arg('--disk-over-commit', - action='store_true', - default=False, - help='Enable disk overcommit.') +@utils.arg( + '--target-host', + metavar='', + default=None, + help='Name of target host.') +@utils.arg( + '--block-migrate', + action='store_true', + default=False, + help='Enable block migration.') +@utils.arg( + '--disk-over-commit', + action='store_true', + default=False, + help='Enable disk overcommit.') def do_host_evacuate_live(cs, args): """Live Migrate all instances of the specified host to other available hosts. @@ -2575,7 +2726,8 @@ def do_host_list(cs, args): @utils.arg('host', metavar='', help='Name of host.') @utils.arg('--status', metavar='', default=None, dest='status', help='Either enable or disable a host.') -@utils.arg('--maintenance', +@utils.arg( + '--maintenance', metavar='', default=None, dest='maintenance', @@ -2644,17 +2796,18 @@ def __init__(self, **kwargs): if hasattr(hyper_servers, 'servers'): print(hyper_servers.servers) servers.extend([InstanceOnHyper(id=serv['id'], - name=serv['name'], - hypervisor_hostname=hyper_host, - hypervisor_id=hyper_id) - for serv in hyper_servers.servers]) + name=serv['name'], + hypervisor_hostname=hyper_host, + hypervisor_id=hyper_id) + for serv in hyper_servers.servers]) # Output the data utils.print_list(servers, ['ID', 'Name', 'Hypervisor ID', 'Hypervisor Hostname']) -@utils.arg('hypervisor', +@utils.arg( + 'hypervisor', metavar='', help='Name or ID of the hypervisor to show the details of.') def do_hypervisor_show(cs, args): @@ -2670,7 +2823,8 @@ def do_hypervisor_show(cs, args): utils.print_dict(info) -@utils.arg('hypervisor', +@utils.arg( + 'hypervisor', metavar='', help='Name or ID of the hypervisor to show the uptime of.') def do_hypervisor_uptime(cs, args): @@ -2775,19 +2929,22 @@ def do_extension_list(cs, _args): @utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg('--port', +@utils.arg( + '--port', dest='port', action='store', type=int, default=22, help='Optional flag to indicate which port to use for ssh. ' '(Default=22)') -@utils.arg('--private', +@utils.arg( + '--private', dest='private', action='store_true', default=False, help=argparse.SUPPRESS) -@utils.arg('--address-type', +@utils.arg( + '--address-type', dest='address_type', action='store', type=str, @@ -2796,18 +2953,21 @@ def do_extension_list(cs, _args): 'includes fixed and floating (the Default).') @utils.arg('--network', metavar='', help='Network to use for the ssh.', default=None) -@utils.arg('--ipv6', +@utils.arg( + '--ipv6', dest='ipv6', action='store_true', default=False, help='Optional flag to indicate whether to use an IPv6 address ' 'attached to a server. (Defaults to IPv4 address)') @utils.arg('--login', metavar='', help='Login to use.', default="root") -@utils.arg('-i', '--identity', +@utils.arg( + '-i', '--identity', dest='identity', help='Private key file, same as the -i option to the ssh command.', default='') -@utils.arg('--extra-opts', +@utils.arg( + '--extra-opts', dest='extra', help='Extra options to pass to ssh. see: man ssh', default='') @@ -2917,7 +3077,8 @@ def _quota_update(manager, identifier, args): manager.update(identifier, **updates) -@utils.arg('--tenant', +@utils.arg( + '--tenant', metavar='', default=None, help='ID of tenant to list the quotas for.') @@ -2930,11 +3091,13 @@ def do_quota_show(cs, args): _quota_show(cs.quotas.get(args.tenant)) -@utils.arg('--tenant', +@utils.arg( + '--tenant', metavar='', default=None, help='ID of tenant to list the quotas for.') -@utils.arg('--user', +@utils.arg( + '--user', metavar='', default=None, help='ID of user to list the quotas for.') @@ -2945,7 +3108,8 @@ def do_quota_usage(cs, args): _quota_usage(cs.quotas.get(tenant, user_id=args.user, detail=True)) -@utils.arg('--tenant', +@utils.arg( + '--tenant', metavar='', default=None, help='ID of tenant to list the default quotas for.') @@ -2958,7 +3122,8 @@ def do_quota_defaults(cs, args): _quota_show(cs.quotas.defaults(args.tenant)) -@utils.arg('tenant', +@utils.arg( + 'tenant', metavar='', help='ID of tenant to set the quotas for.') @utils.arg('--instances', @@ -2973,35 +3138,42 @@ def do_quota_defaults(cs, args): metavar='', type=int, default=None, help='New value for the "ram" quota.') -@utils.arg('--fixed-ips', +@utils.arg( + '--fixed-ips', metavar='', type=int, default=None, help='New value for the "fixed-ips" quota.') -@utils.arg('--metadata-items', +@utils.arg( + '--metadata-items', metavar='', type=int, default=None, help='New value for the "metadata-items" quota.') -@utils.arg('--metadata_items', +@utils.arg( + '--metadata_items', type=int, help=argparse.SUPPRESS) -@utils.arg('--key-pairs', +@utils.arg( + '--key-pairs', metavar='', type=int, default=None, help='New value for the "key-pairs" quota.') -@utils.arg('--server-groups', +@utils.arg( + '--server-groups', metavar='', type=int, default=None, help='New value for the "server-groups" quota.') -@utils.arg('--server-group-members', +@utils.arg( + '--server-group-members', metavar='', type=int, default=None, help='New value for the "server-group-members" quota.') -@utils.arg('--force', +@utils.arg( + '--force', dest='force', action="store_true", default=None, @@ -3024,15 +3196,18 @@ def do_quota_delete(cs, args): @utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg('host', metavar='', nargs='?', +@utils.arg( + 'host', metavar='', nargs='?', help="Name or ID of the target host. " "If no host is specified, the scheduler will choose one.") -@utils.arg('--password', +@utils.arg( + '--password', dest='password', metavar='', help="Set the provided password on the evacuated server. Not applicable " "with on-shared-storage flag") -@utils.arg('--on-shared-storage', +@utils.arg( + '--on-shared-storage', dest='on_shared_storage', action="store_true", default=False, @@ -3108,7 +3283,7 @@ def _treeizeAvailabilityZone(zone): # Zone tree view item az.zone_name = zone.zone_name az.zone_state = ('available' - if zone.zone_state['available'] else 'not available') + if zone.zone_state['available'] else 'not available') az._info['zone_name'] = az.zone_name az._info['zone_state'] = az.zone_state result.append(az) From 2126b93e689760cd575e37a8ba9521b8c7602cfa Mon Sep 17 00:00:00 2001 From: Eugeniya Kudryashova Date: Thu, 25 Sep 2014 19:57:06 +0300 Subject: [PATCH 0648/1705] Fix E128 failures in novaclient/tests E128 continuation line under-indented for visual indent Change-Id: Ie751df84ad289e025bd8ccd4e294228bed803aa5 --- novaclient/tests/fakes.py | 2 +- novaclient/tests/fixture_data/client.py | 4 +- novaclient/tests/fixture_data/cloudpipe.py | 10 +- novaclient/tests/fixture_data/floatingips.py | 4 +- novaclient/tests/fixture_data/networks.py | 4 +- novaclient/tests/fixture_data/servers.py | 12 +- novaclient/tests/test_auth_plugins.py | 14 +- novaclient/tests/test_client.py | 5 +- novaclient/tests/v1_1/contrib/fakes.py | 21 +- .../contrib/test_assisted_volume_snapshots.py | 5 +- .../v1_1/contrib/test_instance_actions.py | 10 +- novaclient/tests/v1_1/fakes.py | 199 ++++++++++-------- novaclient/tests/v1_1/test_agents.py | 27 ++- .../tests/v1_1/test_availability_zone.py | 6 +- novaclient/tests/v1_1/test_flavors.py | 6 +- novaclient/tests/v1_1/test_images.py | 2 +- .../tests/v1_1/test_security_group_rules.py | 15 +- novaclient/tests/v1_1/test_servers.py | 4 +- novaclient/tests/v1_1/test_shell.py | 73 ++++--- novaclient/tests/v1_1/test_usage.py | 20 +- novaclient/tests/v3/fakes.py | 14 +- novaclient/tests/v3/test_flavors.py | 2 +- 22 files changed, 249 insertions(+), 210 deletions(-) diff --git a/novaclient/tests/fakes.py b/novaclient/tests/fakes.py index f3f5274bc..ef6b59b52 100644 --- a/novaclient/tests/fakes.py +++ b/novaclient/tests/fakes.py @@ -30,7 +30,7 @@ def assert_has_keys(dict, required=[], optional=[]): except AssertionError: extra_keys = set(keys).difference(set(required + optional)) raise AssertionError("found unexpected keys: %s" % - list(extra_keys)) + list(extra_keys)) class FakeClient(object): diff --git a/novaclient/tests/fixture_data/client.py b/novaclient/tests/fixture_data/client.py index e52d4a233..094bc969f 100644 --- a/novaclient/tests/fixture_data/client.py +++ b/novaclient/tests/fixture_data/client.py @@ -47,8 +47,8 @@ def setUp(self): auth_url = '%s/tokens' % self.identity_url headers = {'X-Content-Type': 'application/json'} self.requests.register_uri('POST', auth_url, - json=self.token, - headers=headers) + json=self.token, + headers=headers) self.client = self.new_client() def new_client(self): diff --git a/novaclient/tests/fixture_data/cloudpipe.py b/novaclient/tests/fixture_data/cloudpipe.py index 32b5c210c..9cb4efa89 100644 --- a/novaclient/tests/fixture_data/cloudpipe.py +++ b/novaclient/tests/fixture_data/cloudpipe.py @@ -22,15 +22,15 @@ def setUp(self): get_os_cloudpipe = {'cloudpipes': [{'project_id': 1}]} self.requests.register_uri('GET', self.url(), - json=get_os_cloudpipe, - headers=self.json_headers) + json=get_os_cloudpipe, + headers=self.json_headers) instance_id = '9d5824aa-20e6-4b9f-b967-76a699fc51fd' post_os_cloudpipe = {'instance_id': instance_id} self.requests.register_uri('POST', self.url(), - json=post_os_cloudpipe, - headers=self.json_headers, - status_code=202) + json=post_os_cloudpipe, + headers=self.json_headers, + status_code=202) self.requests.register_uri('PUT', self.url('configure-project'), headers=self.json_headers, diff --git a/novaclient/tests/fixture_data/floatingips.py b/novaclient/tests/fixture_data/floatingips.py index f493e10fb..4829a5a3c 100644 --- a/novaclient/tests/fixture_data/floatingips.py +++ b/novaclient/tests/fixture_data/floatingips.py @@ -47,8 +47,8 @@ def post_os_floating_ips(request, context): ip['pool'] = body.get('pool') return {'floating_ip': ip} self.requests.register_uri('POST', self.url(), - json=post_os_floating_ips, - headers=self.json_headers) + json=post_os_floating_ips, + headers=self.json_headers) class DNSFixture(base.Fixture): diff --git a/novaclient/tests/fixture_data/networks.py b/novaclient/tests/fixture_data/networks.py index 44d7d528a..023dce421 100644 --- a/novaclient/tests/fixture_data/networks.py +++ b/novaclient/tests/fixture_data/networks.py @@ -44,8 +44,8 @@ def post_os_networks(request, context): return {'network': body} self.requests.register_uri("POST", self.url(), - json=post_os_networks, - headers=headers) + json=post_os_networks, + headers=headers) get_os_networks_1 = {'network': {"label": "1", "cidr": "10.0.0.0/24"}} diff --git a/novaclient/tests/fixture_data/servers.py b/novaclient/tests/fixture_data/servers.py index b226413c5..d316d4527 100644 --- a/novaclient/tests/fixture_data/servers.py +++ b/novaclient/tests/fixture_data/servers.py @@ -346,7 +346,8 @@ def setUp(self): # tI2/++UsXVg3ow6ItqCJGgdNuGG5JB+bslDHWPxROpesEIHdczk46HCpHQN8f1sk # Hi/fmZZNQQqj1Ijq0caOIw== - get_server_password = {'password': + get_server_password = { + 'password': 'OIuEuQttO8Rk93BcKlwHQsziDAnkAm/V6V8VPToA8ZeUaUBWwS0gwo2K6Y61Z96r' 'qG447iRz0uTEEYq3RAYJk1mh3mMIRVl27t8MtIecR5ggVVbz1S9AwXJQypDKl0ho' 'QFvhCBcMWPohyGewDJOhDbtuN1IoFI9G55ZvFwCm5y7m7B2aVcoLeIsJZE4PLsIw' @@ -543,10 +544,11 @@ def setUp(self): def post_servers(self, request, context): body = jsonutils.loads(request.body) assert set(body.keys()) <= set(['server']) - fakes.assert_has_keys(body['server'], - required=['name', 'image_ref', 'flavor_ref'], - optional=['metadata', 'personality', - 'os-scheduler-hints:scheduler_hints']) + fakes.assert_has_keys( + body['server'], + required=['name', 'image_ref', 'flavor_ref'], + optional=['metadata', 'personality', + 'os-scheduler-hints:scheduler_hints']) if body['server']['name'] == 'some-bad-server': body = self.server_1235 else: diff --git a/novaclient/tests/test_auth_plugins.py b/novaclient/tests/test_auth_plugins.py index ce7e82a8c..33bf481b8 100644 --- a/novaclient/tests/test_auth_plugins.py +++ b/novaclient/tests/test_auth_plugins.py @@ -133,8 +133,8 @@ def authenticate(self, cls, auth_url): def mock_iter_entry_points(_type, name): if _type == 'openstack.client.auth_url': return [MockAuthUrlEntrypoint("fakewithauthurl", - "fakewithauthurl", - ["auth_url"])] + "fakewithauthurl", + ["auth_url"])] elif _type == 'openstack.client.authenticate': return [MockAuthenticateEntrypoint("fakewithauthurl", "fakewithauthurl", @@ -221,9 +221,9 @@ class FakePlugin(auth_plugin.BaseAuthPlugin): @staticmethod def add_opts(parser): parser.add_argument('--auth_system_opt', - default=False, - action='store_true', - help="Fake option") + default=False, + action='store_true', + help="Fake option") return parser class MockEntrypoint(pkg_resources.EntryPoint): @@ -281,8 +281,8 @@ def get_auth_url(self): plugin = auth_plugin.load_plugin("fake") cs = client.Client("username", "password", "project_id", - auth_system="fakewithauthurl", - auth_plugin=plugin) + auth_system="fakewithauthurl", + auth_plugin=plugin) self.assertEqual("http://faked/v2.0", cs.client.auth_url) @mock.patch.object(pkg_resources, "iter_entry_points") diff --git a/novaclient/tests/test_client.py b/novaclient/tests/test_client.py index 099b36636..7322b66ac 100644 --- a/novaclient/tests/test_client.py +++ b/novaclient/tests/test_client.py @@ -371,8 +371,9 @@ def test_log_req(self): 'X-Auth-Token': 'totally_bogus'} }) cs.http_log_req('GET', '/foo', {'headers': {}, - 'data': '{"auth": {"passwordCredentials": ' - '{"password": "zhaoqin"}}}'}) + 'data': + '{"auth": {"passwordCredentials": ' + '{"password": "zhaoqin"}}}'}) output = self.logger.output.split('\n') diff --git a/novaclient/tests/v1_1/contrib/fakes.py b/novaclient/tests/v1_1/contrib/fakes.py index 0d4af537c..43c6e6de3 100644 --- a/novaclient/tests/v1_1/contrib/fakes.py +++ b/novaclient/tests/v1_1/contrib/fakes.py @@ -26,19 +26,22 @@ def __init__(self, *args, **kwargs): class FakeHTTPClient(fakes.FakeHTTPClient): def get_os_tenant_networks(self): - return (200, {}, {'networks': [{"label": "1", "cidr": "10.0.0.0/24", - 'project_id': '4ffc664c198e435e9853f2538fbcd7a7', - 'id': '1'}]}) + return (200, {}, { + 'networks': [{"label": "1", "cidr": "10.0.0.0/24", + 'project_id': '4ffc664c198e435e9853f2538fbcd7a7', + 'id': '1'}]}) def get_os_tenant_networks_1(self, **kw): - return (200, {}, {'network': {"label": "1", "cidr": "10.0.0.0/24", - 'project_id': '4ffc664c198e435e9853f2538fbcd7a7', - 'id': '1'}}) + return (200, {}, { + 'network': {"label": "1", "cidr": "10.0.0.0/24", + 'project_id': '4ffc664c198e435e9853f2538fbcd7a7', + 'id': '1'}}) def post_os_tenant_networks(self, **kw): - return (201, {}, {'network': {"label": "1", "cidr": "10.0.0.0/24", - 'project_id': '4ffc664c198e435e9853f2538fbcd7a7', - 'id': '1'}}) + return (201, {}, { + 'network': {"label": "1", "cidr": "10.0.0.0/24", + 'project_id': '4ffc664c198e435e9853f2538fbcd7a7', + 'id': '1'}}) def delete_os_tenant_networks_1(self, **kw): return (204, {}, None) diff --git a/novaclient/tests/v1_1/contrib/test_assisted_volume_snapshots.py b/novaclient/tests/v1_1/contrib/test_assisted_volume_snapshots.py index c3e47b458..426d6debb 100644 --- a/novaclient/tests/v1_1/contrib/test_assisted_volume_snapshots.py +++ b/novaclient/tests/v1_1/contrib/test_assisted_volume_snapshots.py @@ -37,5 +37,6 @@ def test_create_snap(self): def test_delete_snap(self): cs.assisted_volume_snapshots.delete('x', {}) - cs.assert_called('DELETE', - '/os-assisted-volume-snapshots/x?delete_info={}') + cs.assert_called( + 'DELETE', + '/os-assisted-volume-snapshots/x?delete_info={}') diff --git a/novaclient/tests/v1_1/contrib/test_instance_actions.py b/novaclient/tests/v1_1/contrib/test_instance_actions.py index baaa1d040..364781975 100644 --- a/novaclient/tests/v1_1/contrib/test_instance_actions.py +++ b/novaclient/tests/v1_1/contrib/test_instance_actions.py @@ -30,12 +30,14 @@ class InstanceActionExtensionTests(utils.TestCase): def test_list_instance_actions(self): server_uuid = '1234' cs.instance_action.list(server_uuid) - cs.assert_called('GET', '/servers/%s/os-instance-actions' % - server_uuid) + cs.assert_called( + 'GET', '/servers/%s/os-instance-actions' % + server_uuid) def test_get_instance_action(self): server_uuid = '1234' request_id = 'req-abcde12345' cs.instance_action.get(server_uuid, request_id) - cs.assert_called('GET', '/servers/%s/os-instance-actions/%s' % - (server_uuid, request_id)) + cs.assert_called( + 'GET', '/servers/%s/os-instance-actions/%s' + % (server_uuid, request_id)) diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index dfc7077ac..53f9e7451 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -95,22 +95,22 @@ def _cs_request(self, url, method, **kwargs): def get_os_agents(self, **kw): hypervisor = kw.get('hypervisor', 'kvm') - return (200, {}, {'agents': - [{'hypervisor': hypervisor, - 'os': 'win', - 'architecture': 'x86', - 'version': '7.0', - 'url': 'xxx://xxxx/xxx/xxx', - 'md5hash': 'add6bb58e139be103324d04d82d8f545', - 'id': 1}, - {'hypervisor': hypervisor, - 'os': 'linux', - 'architecture': 'x86', - 'version': '16.0', - 'url': 'xxx://xxxx/xxx/xxx1', - 'md5hash': 'add6bb58e139be103324d04d82d8f546', - 'id': 2}, - ]}) + return (200, {}, { + 'agents': + [{'hypervisor': hypervisor, + 'os': 'win', + 'architecture': 'x86', + 'version': '7.0', + 'url': 'xxx://xxxx/xxx/xxx', + 'md5hash': 'add6bb58e139be103324d04d82d8f545', + 'id': 1}, + {'hypervisor': hypervisor, + 'os': 'linux', + 'architecture': 'x86', + 'version': '16.0', + 'url': 'xxx://xxxx/xxx/xxx1', + 'md5hash': 'add6bb58e139be103324d04d82d8f546', + 'id': 2}]}) def post_os_agents(self, body): return (200, {}, {'agent': { @@ -366,9 +366,10 @@ def get_servers_detail(self, **kw): def post_servers(self, body, **kw): assert set(body.keys()) <= set(['server', 'os:scheduler_hints']) - fakes.assert_has_keys(body['server'], - required=['name', 'imageRef', 'flavorRef'], - optional=['metadata', 'personality']) + fakes.assert_has_keys( + body['server'], + required=['name', 'imageRef', 'flavorRef'], + optional=['metadata', 'personality']) if 'personality' in body['server']: for pfile in body['server']['personality']: fakes.assert_has_keys(pfile, required=['path', 'contents']) @@ -379,9 +380,10 @@ def post_servers(self, body, **kw): def post_os_volumes_boot(self, body, **kw): assert set(body.keys()) <= set(['server', 'os:scheduler_hints']) - fakes.assert_has_keys(body['server'], - required=['name', 'flavorRef'], - optional=['imageRef']) + fakes.assert_has_keys( + body['server'], + required=['name', 'flavorRef'], + optional=['imageRef']) # Require one, and only one, of the keys for bdm if 'block_device_mapping' not in body['server']: @@ -487,16 +489,20 @@ def get_servers_1234_os_security_groups(self, **kw): # def get_servers_1234_ips(self, **kw): - return (200, {}, {'addresses': - self.get_servers_1234()[1]['server']['addresses']}) + return (200, {}, { + 'addresses': + self.get_servers_1234()[1]['server']['addresses']}) def get_servers_1234_ips_public(self, **kw): - return (200, {}, {'public': - self.get_servers_1234_ips()[1]['addresses']['public']}) + return (200, {}, { + 'public': + self.get_servers_1234_ips()[1]['addresses']['public']}) def get_servers_1234_ips_private(self, **kw): - return (200, {}, {'private': - self.get_servers_1234_ips()[1]['addresses']['private']}) + return ( + 200, {}, + {'private': + self.get_servers_1234_ips()[1]['addresses']['private']}) def delete_servers_1234_ips_public_1_2_3_4(self, **kw): return (202, {}, None) @@ -519,7 +525,8 @@ def delete_servers_1234_ips_public_1_2_3_4(self, **kw): # tI2/++UsXVg3ow6ItqCJGgdNuGG5JB+bslDHWPxROpesEIHdczk46HCpHQN8f1sk # Hi/fmZZNQQqj1Ijq0caOIw== def get_servers_1234_os_server_password(self, **kw): - return (200, {}, {'password': + return (200, {}, { + 'password': 'OIuEuQttO8Rk93BcKlwHQsziDAnkAm/V6V8VPToA8ZeUaUBWwS0gwo2K6Y61Z96r' 'qG447iRz0uTEEYq3RAYJk1mh3mMIRVl27t8MtIecR5ggVVbz1S9AwXJQypDKl0ho' 'QFvhCBcMWPohyGewDJOhDbtuN1IoFI9G55ZvFwCm5y7m7B2aVcoLeIsJZE4PLsIw' @@ -797,21 +804,25 @@ def post_flavors(self, body, **kw): ) def get_flavors_1_os_extra_specs(self, **kw): - return (200, + return ( + 200, {}, {'extra_specs': {"k1": "v1"}}) def get_flavors_2_os_extra_specs(self, **kw): - return (200, + return ( + 200, {}, {'extra_specs': {"k2": "v2"}}) def get_flavors_aa1_os_extra_specs(self, **kw): - return (200, {}, + return ( + 200, {}, {'extra_specs': {"k3": "v3"}}) def get_flavors_4_os_extra_specs(self, **kw): - return (200, + return ( + 200, {}, {'extra_specs': {"k4": "v4"}}) @@ -819,14 +830,16 @@ def post_flavors_1_os_extra_specs(self, body, **kw): assert list(body) == ['extra_specs'] fakes.assert_has_keys(body['extra_specs'], required=['k1']) - return (200, + return ( + 200, {}, {'extra_specs': {"k1": "v1"}}) def post_flavors_4_os_extra_specs(self, body, **kw): assert list(body) == ['extra_specs'] - return (200, + return ( + 200, {}, body) @@ -841,10 +854,10 @@ def get_flavors_1_os_flavor_access(self, **kw): return (404, {}, None) def get_flavors_2_os_flavor_access(self, **kw): - return (200, {}, {'flavor_access': [ - {'flavor_id': '2', 'tenant_id': 'proj1'}, - {'flavor_id': '2', 'tenant_id': 'proj2'} - ]}) + return ( + 200, {}, + {'flavor_access': [{'flavor_id': '2', 'tenant_id': 'proj1'}, + {'flavor_id': '2', 'tenant_id': 'proj2'}]}) def post_flavors_2_action(self, body, **kw): return (202, {}, self.get_flavors_2_os_flavor_access()[2]) @@ -871,19 +884,21 @@ def get_os_floating_ips(self, **kw): ) def get_os_floating_ips_1(self, **kw): - return (200, {}, {'floating_ip': - {'id': 1, 'fixed_ip': '10.0.0.1', 'ip': '11.0.0.1'} - }) + return ( + 200, {}, {'floating_ip': {'id': 1, 'fixed_ip': '10.0.0.1', + 'ip': '11.0.0.1'}}) def post_os_floating_ips(self, body): if body.get('pool'): - return (200, {}, {'floating_ip': - {'id': 1, 'fixed_ip': '10.0.0.1', 'ip': '11.0.0.1', - 'pool': 'nova'}}) + return ( + 200, {}, {'floating_ip': {'id': 1, 'fixed_ip': '10.0.0.1', + 'ip': '11.0.0.1', + 'pool': 'nova'}}) else: - return (200, {}, {'floating_ip': - {'id': 1, 'fixed_ip': '10.0.0.1', 'ip': '11.0.0.1', - 'pool': None}}) + return ( + 200, {}, {'floating_ip': {'id': 1, 'fixed_ip': '10.0.0.1', + 'ip': '11.0.0.1', + 'pool': None}}) def delete_os_floating_ips_1(self, **kw): return (204, {}, None) @@ -1016,7 +1031,8 @@ def post_images_1_metadata(self, body, **kw): assert list(body) == ['metadata'] fakes.assert_has_keys(body['metadata'], required=['test_key']) - return (200, + return ( + 200, {}, {'metadata': self.get_images_1()[2]['image']['metadata']}) @@ -1247,7 +1263,7 @@ def put_os_quota_class_sets_test(self, body, **kw): 'security_group_rules': 1}}) def put_os_quota_class_sets_97f4c221bff44578b0300df4ef119353(self, - body, **kw): + body, **kw): assert list(body) == ['quota_class_set'] return (200, {}, { 'quota_class_set': { @@ -1391,11 +1407,11 @@ def delete_os_security_group_default_rules_12(self, **kw): def post_os_security_group_default_rules(self, body, **kw): assert list(body) == ['security_group_default_rule'] fakes.assert_has_keys(body['security_group_default_rule'], - optional=['ip_protocol', 'from_port', - 'to_port', 'cidr']) + optional=['ip_protocol', 'from_port', + 'to_port', 'cidr']) rules = self.get_os_security_group_default_rules() r = {'security_group_default_rule': - rules[2]['security_group_default_rules'][0]} + rules[2]['security_group_default_rules'][0]} return (202, {}, r) # @@ -1569,24 +1585,21 @@ def delete_os_aggregates_1(self, **kw): def get_os_services(self, **kw): host = kw.get('host', 'host1') binary = kw.get('binary', 'nova-compute') - return (200, {}, {'services': - [{'binary': binary, - 'host': host, - 'zone': 'nova', - 'status': 'enabled', - 'state': 'up', - 'updated_at': datetime.datetime( - 2012, 10, 29, 13, 42, 2 - )}, - {'binary': binary, - 'host': host, - 'zone': 'nova', - 'status': 'disabled', - 'state': 'down', - 'updated_at': datetime.datetime( - 2012, 9, 18, 8, 3, 38 - )}, - ]}) + return (200, {}, {'services': [{'binary': binary, + 'host': host, + 'zone': 'nova', + 'status': 'enabled', + 'state': 'up', + 'updated_at': datetime.datetime( + 2012, 10, 29, 13, 42, 2)}, + {'binary': binary, + 'host': host, + 'zone': 'nova', + 'status': 'disabled', + 'state': 'down', + 'updated_at': datetime.datetime( + 2012, 9, 18, 8, 3, 38)}, + ]}) def put_os_services_enable(self, body, **kw): return (200, {}, {'service': {'host': body['host'], @@ -1599,10 +1612,11 @@ def put_os_services_disable(self, body, **kw): 'status': 'disabled'}}) def put_os_services_disable_log_reason(self, body, **kw): - return (200, {}, {'service': {'host': body['host'], - 'binary': body['binary'], - 'status': 'disabled', - 'disabled_reason': body['disabled_reason']}}) + return (200, {}, {'service': { + 'host': body['host'], + 'binary': body['binary'], + 'status': 'disabled', + 'disabled_reason': body['disabled_reason']}}) def delete_os_services_1(self, **kw): return (204, {}, None) @@ -1611,11 +1625,10 @@ def delete_os_services_1(self, **kw): # Fixed IPs # def get_os_fixed_ips_192_168_1_1(self, *kw): - return (200, {}, {"fixed_ip": - {'cidr': '192.168.1.0/24', - 'address': '192.168.1.1', - 'hostname': 'foo', - 'host': 'bar'}}) + return (200, {}, {"fixed_ip": {'cidr': '192.168.1.0/24', + 'address': '192.168.1.1', + 'hostname': 'foo', + 'host': 'bar'}}) def post_os_fixed_ips_192_168_1_1_action(self, body, **kw): return (202, {}, None) @@ -1636,29 +1649,28 @@ def get_os_hosts_host(self, *kw): def get_os_hosts(self, **kw): zone = kw.get('zone', 'nova1') - return (200, {}, {'hosts': - [{'host': 'host1', - 'service': 'nova-compute', - 'zone': zone}, - {'host': 'host1', - 'service': 'nova-cert', - 'zone': zone}]}) + return (200, {}, {'hosts': [{'host': 'host1', + 'service': 'nova-compute', + 'zone': zone}, + {'host': 'host1', + 'service': 'nova-cert', + 'zone': zone}]}) def get_os_hosts_sample_host(self, *kw): return (200, {}, {'host': [{'resource': {'host': 'sample_host'}}], }) def put_os_hosts_sample_host_1(self, body, **kw): return (200, {}, {'host': 'sample-host_1', - 'status': 'enabled'}) + 'status': 'enabled'}) def put_os_hosts_sample_host_2(self, body, **kw): return (200, {}, {'host': 'sample-host_2', - 'maintenance_mode': 'on_maintenance'}) + 'maintenance_mode': 'on_maintenance'}) def put_os_hosts_sample_host_3(self, body, **kw): return (200, {}, {'host': 'sample-host_3', - 'status': 'enabled', - 'maintenance_mode': 'on_maintenance'}) + 'status': 'enabled', + 'maintenance_mode': 'on_maintenance'}) def get_os_hosts_sample_host_reboot(self, **kw): return (200, {}, {'host': 'sample_host', @@ -1793,8 +1805,9 @@ def get_os_hypervisors_1234_uptime(self, **kw): def get_os_networks(self, **kw): return (200, {}, {'networks': [{"label": "1", "cidr": "10.0.0.0/24", - 'project_id': '4ffc664c198e435e9853f2538fbcd7a7', - 'id': '1', 'vlan': '1234'}]}) + 'project_id': + '4ffc664c198e435e9853f2538fbcd7a7', + 'id': '1', 'vlan': '1234'}]}) def delete_os_networks_1(self, **kw): return (202, {}, None) diff --git a/novaclient/tests/v1_1/test_agents.py b/novaclient/tests/v1_1/test_agents.py index 424a52b79..dbe7af49a 100644 --- a/novaclient/tests/v1_1/test_agents.py +++ b/novaclient/tests/v1_1/test_agents.py @@ -27,8 +27,8 @@ class AgentsTest(utils.FixturedTestCase): ('session', {'client_fixture_class': client.SessionV1})] def stub_hypervisors(self, hypervisor='kvm'): - get_os_agents = {'agents': - [ + get_os_agents = { + 'agents': [ { 'hypervisor': hypervisor, 'os': 'win', @@ -73,16 +73,15 @@ def test_list_agents_with_hypervisor(self): def test_agents_create(self): ag = self.cs.agents.create('win', 'x86', '7.0', - '/xxx/xxx/xxx', - 'add6bb58e139be103324d04d82d8f546', - 'xen') - body = {'agent': { - 'url': '/xxx/xxx/xxx', - 'hypervisor': 'xen', - 'md5hash': 'add6bb58e139be103324d04d82d8f546', - 'version': '7.0', - 'architecture': 'x86', - 'os': 'win'}} + '/xxx/xxx/xxx', + 'add6bb58e139be103324d04d82d8f546', + 'xen') + body = {'agent': {'url': '/xxx/xxx/xxx', + 'hypervisor': 'xen', + 'md5hash': 'add6bb58e139be103324d04d82d8f546', + 'version': '7.0', + 'architecture': 'x86', + 'os': 'win'}} self.assert_called('POST', '/os-agents', body) self.assertEqual(1, ag._info.copy()['id']) @@ -98,8 +97,8 @@ def _build_example_update_body(self): def test_agents_modify(self): ag = self.cs.agents.update('1', '8.0', - '/yyy/yyyy/yyyy', - 'add6bb58e139be103324d04d82d8f546') + '/yyy/yyyy/yyyy', + 'add6bb58e139be103324d04d82d8f546') body = self._build_example_update_body() self.assert_called('PUT', '/os-agents/1', body) self.assertEqual(1, ag.id) diff --git a/novaclient/tests/v1_1/test_availability_zone.py b/novaclient/tests/v1_1/test_availability_zone.py index 6851d9aea..be0623adf 100644 --- a/novaclient/tests/v1_1/test_availability_zone.py +++ b/novaclient/tests/v1_1/test_availability_zone.py @@ -75,14 +75,14 @@ def test_detail_availability_zone(self): l0 = [six.u('zone-1'), six.u('available')] l1 = [six.u('|- fake_host-1'), six.u('')] l2 = [six.u('| |- nova-compute'), - six.u('enabled :-) 2012-12-26 14:45:25')] + six.u('enabled :-) 2012-12-26 14:45:25')] l3 = [six.u('internal'), six.u('available')] l4 = [six.u('|- fake_host-1'), six.u('')] l5 = [six.u('| |- nova-sched'), - six.u('enabled :-) 2012-12-26 14:45:25')] + six.u('enabled :-) 2012-12-26 14:45:25')] l6 = [six.u('|- fake_host-2'), six.u('')] l7 = [six.u('| |- nova-network'), - six.u('enabled XXX 2012-12-26 14:45:24')] + six.u('enabled XXX 2012-12-26 14:45:24')] l8 = [six.u('zone-2'), six.u('not available')] z0 = self.shell._treeizeAvailabilityZone(zones[0]) diff --git a/novaclient/tests/v1_1/test_flavors.py b/novaclient/tests/v1_1/test_flavors.py index 8f5aea007..a69db858f 100644 --- a/novaclient/tests/v1_1/test_flavors.py +++ b/novaclient/tests/v1_1/test_flavors.py @@ -130,8 +130,8 @@ def test_create(self): def test_create_with_id_as_string(self): flavor_id = 'foobar' f = self.cs.flavors.create("flavorcreate", 512, - 1, 10, flavor_id, ephemeral=10, - is_public=False) + 1, 10, flavor_id, ephemeral=10, + is_public=False) body = self._create_body("flavorcreate", 512, 1, 10, 10, flavor_id, 0, 1.0, False) @@ -189,7 +189,7 @@ def test_set_keys(self): f = self.cs.flavors.get(1) f.set_keys({'k1': 'v1'}) self.cs.assert_called('POST', '/flavors/1/os-extra_specs', - {"extra_specs": {'k1': 'v1'}}) + {"extra_specs": {'k1': 'v1'}}) def test_set_with_valid_keys(self): valid_keys = ['key4', 'month.price', 'I-Am:AK-ey.44-', diff --git a/novaclient/tests/v1_1/test_images.py b/novaclient/tests/v1_1/test_images.py index e237bcd5c..e5fcfca9b 100644 --- a/novaclient/tests/v1_1/test_images.py +++ b/novaclient/tests/v1_1/test_images.py @@ -54,7 +54,7 @@ def test_delete_meta(self): def test_set_meta(self): self.cs.images.set_meta(1, {'test_key': 'test_value'}) self.assert_called('POST', '/images/1/metadata', - {"metadata": {'test_key': 'test_value'}}) + {"metadata": {'test_key': 'test_value'}}) def test_find(self): i = self.cs.images.find(name="CentOS 5.2") diff --git a/novaclient/tests/v1_1/test_security_group_rules.py b/novaclient/tests/v1_1/test_security_group_rules.py index bf69a86ed..7de882f96 100644 --- a/novaclient/tests/v1_1/test_security_group_rules.py +++ b/novaclient/tests/v1_1/test_security_group_rules.py @@ -65,14 +65,17 @@ def test_create_security_group_group_rule(self): def test_invalid_parameters_create(self): self.assertRaises(exceptions.CommandError, - self.cs.security_group_rules.create, - 1, "invalid_ip_protocol", 1, 65535, "10.0.0.0/16", 101) + self.cs.security_group_rules.create, + 1, "invalid_ip_protocol", 1, 65535, + "10.0.0.0/16", 101) self.assertRaises(exceptions.CommandError, - self.cs.security_group_rules.create, - 1, "tcp", "invalid_from_port", 65535, "10.0.0.0/16", 101) + self.cs.security_group_rules.create, + 1, "tcp", "invalid_from_port", 65535, + "10.0.0.0/16", 101) self.assertRaises(exceptions.CommandError, - self.cs.security_group_rules.create, - 1, "tcp", 1, "invalid_to_port", "10.0.0.0/16", 101) + self.cs.security_group_rules.create, + 1, "tcp", 1, "invalid_to_port", + "10.0.0.0/16", 101) def test_security_group_rule_str(self): sg = self.cs.security_group_rules.create(1, "tcp", 1, 65535, diff --git a/novaclient/tests/v1_1/test_servers.py b/novaclient/tests/v1_1/test_servers.py index 9d2b495b6..3797e5754 100644 --- a/novaclient/tests/v1_1/test_servers.py +++ b/novaclient/tests/v1_1/test_servers.py @@ -377,7 +377,7 @@ def test_add_floating_ip_to_fixed(self): s.add_floating_ip('11.0.0.1', fixed_address='12.0.0.1') self.assert_called('POST', '/servers/1234/action') self.cs.servers.add_floating_ip(s, '11.0.0.1', - fixed_address='12.0.0.1') + fixed_address='12.0.0.1') self.assert_called('POST', '/servers/1234/action') f = self.cs.floating_ips.list()[0] self.cs.servers.add_floating_ip(s, f) @@ -598,7 +598,7 @@ def test_live_migrate_server(self): disk_over_commit=False) self.assert_called('POST', '/servers/1234/action') self.cs.servers.live_migrate(s, host='hostname', block_migration=False, - disk_over_commit=False) + disk_over_commit=False) self.assert_called('POST', '/servers/1234/action') def test_reset_state(self): diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index 55b6137fd..622e082fc 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -841,17 +841,20 @@ def test_list_with_flavors(self): def test_list_by_tenant(self): self.run_command('list --tenant fake_tenant') - self.assert_called('GET', + self.assert_called( + 'GET', '/servers/detail?all_tenants=1&tenant_id=fake_tenant') def test_list_by_user(self): self.run_command('list --user fake_user') - self.assert_called('GET', + self.assert_called( + 'GET', '/servers/detail?all_tenants=1&user_id=fake_user') def test_list_fields(self): - output = self.run_command('list --fields ' - 'host,security_groups,OS-EXT-MOD:some_thing') + output = self.run_command( + 'list --fields ' + 'host,security_groups,OS-EXT-MOD:some_thing') self.assert_called('GET', '/servers/detail') self.assertIn('computenode1', output) self.assertIn('securitygroup1', output) @@ -1661,26 +1664,32 @@ def test_hypervisor_stats(self): self.assert_called('GET', '/os-hypervisors/statistics') def test_quota_show(self): - self.run_command('quota-show --tenant ' - '97f4c221bff44578b0300df4ef119353') - self.assert_called('GET', - '/os-quota-sets/97f4c221bff44578b0300df4ef119353') + self.run_command( + 'quota-show --tenant ' + '97f4c221bff44578b0300df4ef119353') + self.assert_called( + 'GET', + '/os-quota-sets/97f4c221bff44578b0300df4ef119353') def test_user_quota_show(self): - self.run_command('quota-show --tenant ' - '97f4c221bff44578b0300df4ef119353 --user u1') - self.assert_called('GET', - '/os-quota-sets/97f4c221bff44578b0300df4ef119353?user_id=u1') + self.run_command( + 'quota-show --tenant ' + '97f4c221bff44578b0300df4ef119353 --user u1') + self.assert_called( + 'GET', + '/os-quota-sets/97f4c221bff44578b0300df4ef119353?user_id=u1') def test_quota_show_no_tenant(self): self.run_command('quota-show') self.assert_called('GET', '/os-quota-sets/tenant_id') def test_quota_defaults(self): - self.run_command('quota-defaults --tenant ' - '97f4c221bff44578b0300df4ef119353') - self.assert_called('GET', - '/os-quota-sets/97f4c221bff44578b0300df4ef119353/defaults') + self.run_command( + 'quota-defaults --tenant ' + '97f4c221bff44578b0300df4ef119353') + self.assert_called( + 'GET', + '/os-quota-sets/97f4c221bff44578b0300df4ef119353/defaults') def test_quota_defaults_no_tenant(self): self.run_command('quota-defaults') @@ -1736,8 +1745,9 @@ def test_user_quota_delete(self): self.run_command('quota-delete --tenant ' '97f4c221bff44578b0300df4ef119353 ' '--user u1') - self.assert_called('DELETE', - '/os-quota-sets/97f4c221bff44578b0300df4ef119353?user_id=u1') + self.assert_called( + 'DELETE', + '/os-quota-sets/97f4c221bff44578b0300df4ef119353?user_id=u1') def test_quota_class_show(self): self.run_command('quota-class-show test') @@ -1767,8 +1777,9 @@ def test_network_list(self): self.assert_called('GET', '/os-networks') def test_network_list_fields(self): - output = self.run_command('network-list --fields ' - 'vlan,project_id') + output = self.run_command( + 'network-list --fields ' + 'vlan,project_id') self.assert_called('GET', '/os-networks') self.assertIn('1234', output) self.assertIn('4ffc664c198e435e9853f2538fbcd7a7', output) @@ -2133,8 +2144,9 @@ def test_instance_action_list(self): def test_instance_action_get(self): self.run_command('instance-action sample-server req-abcde12345') - self.assert_called('GET', - '/servers/1234/os-instance-actions/req-abcde12345') + self.assert_called( + 'GET', + '/servers/1234/os-instance-actions/req-abcde12345') def test_cell_show(self): self.run_command('cell-show child_cell') @@ -2166,11 +2178,11 @@ class FakeResources(object): addresses = { "skynet": [ {'version': 4, 'addr': "1.1.1.1", - "OS-EXT-IPS:type": 'fixed'}, + "OS-EXT-IPS:type": 'fixed'}, {'version': 4, 'addr': "2.2.2.2", - "OS-EXT-IPS:type": 'floating'}, + "OS-EXT-IPS:type": 'floating'}, {'version': 6, 'addr': "2607:f0d0:1002::4", - "OS-EXT-IPS:type": 'fixed'}, + "OS-EXT-IPS:type": 'fixed'}, {'version': 6, 'addr': "7612:a1b2:2004::6"} ] } @@ -2214,10 +2226,10 @@ class FakeResources(object): addresses = { "skynet": [ {'version': 4, 'addr': "1.1.1.1", - "OS-EXT-IPS:type": 'fixed'}, + "OS-EXT-IPS:type": 'fixed'}, {'version': 4, 'addr': "2.2.2.2"}, {'version': 6, 'addr': "2607:f0d0:1002::4", - "OS-EXT-IPS:type": 'fixed'} + "OS-EXT-IPS:type": 'fixed'} ], "other": [ {'version': 4, 'addr': "2.3.4.5"}, @@ -2241,11 +2253,12 @@ def test_keypair_add(self): {'name': 'test'}}) @mock.patch.object(builtins, 'open', - mock.mock_open(read_data='FAKE_PUBLIC_KEY')) + mock.mock_open(read_data='FAKE_PUBLIC_KEY')) def test_keypair_import(self): self.run_command('keypair-add --pub-key test.pub test') - self.assert_called('POST', '/os-keypairs', { - 'keypair': {'public_key': 'FAKE_PUBLIC_KEY', 'name': 'test'}}) + self.assert_called( + 'POST', '/os-keypairs', { + 'keypair': {'public_key': 'FAKE_PUBLIC_KEY', 'name': 'test'}}) def test_keypair_list(self): self.run_command('keypair-list') diff --git a/novaclient/tests/v1_1/test_usage.py b/novaclient/tests/v1_1/test_usage.py index 99d40c088..fe0f1e944 100644 --- a/novaclient/tests/v1_1/test_usage.py +++ b/novaclient/tests/v1_1/test_usage.py @@ -34,11 +34,12 @@ def test_usage_list(self, detailed=False): now = datetime.datetime.now() usages = self.cs.usage.list(now, now, detailed) - self.cs.assert_called('GET', - "/os-simple-tenant-usage?" + - ("start=%s&" % now.isoformat()) + - ("end=%s&" % now.isoformat()) + - ("detailed=%s" % int(bool(detailed)))) + self.cs.assert_called( + 'GET', + "/os-simple-tenant-usage?" + + ("start=%s&" % now.isoformat()) + + ("end=%s&" % now.isoformat()) + + ("detailed=%s" % int(bool(detailed)))) [self.assertIsInstance(u, usage.Usage) for u in usages] def test_usage_list_detailed(self): @@ -48,8 +49,9 @@ def test_usage_get(self): now = datetime.datetime.now() u = self.cs.usage.get("tenantfoo", now, now) - self.cs.assert_called('GET', - "/os-simple-tenant-usage/tenantfoo?" + - ("start=%s&" % now.isoformat()) + - ("end=%s" % now.isoformat())) + self.cs.assert_called( + 'GET', + "/os-simple-tenant-usage/tenantfoo?" + + ("start=%s&" % now.isoformat()) + + ("end=%s" % now.isoformat())) self.assertIsInstance(u, usage.Usage) diff --git a/novaclient/tests/v3/fakes.py b/novaclient/tests/v3/fakes.py index 4fbd1d317..f0c57f76b 100644 --- a/novaclient/tests/v3/fakes.py +++ b/novaclient/tests/v3/fakes.py @@ -38,16 +38,16 @@ class FakeHTTPClient(fakes_v1_1.FakeHTTPClient): # def put_os_hosts_sample_host_1(self, body, **kw): return (200, {}, {'host': {'host': 'sample-host_1', - 'status': 'enabled'}}) + 'status': 'enabled'}}) def put_os_hosts_sample_host_2(self, body, **kw): return (200, {}, {'host': {'host': 'sample-host_2', - 'maintenance_mode': 'on_maintenance'}}) + 'maintenance_mode': 'on_maintenance'}}) def put_os_hosts_sample_host_3(self, body, **kw): return (200, {}, {'host': {'host': 'sample-host_3', - 'status': 'enabled', - 'maintenance_mode': 'on_maintenance'}}) + 'status': 'enabled', + 'maintenance_mode': 'on_maintenance'}}) def get_os_hosts_sample_host_reboot(self, **kw): return (200, {}, {'host': {'host': 'sample_host', @@ -173,9 +173,9 @@ def post_servers_1234_os_attach_interfaces(self, **kw): def post_servers(self, body, **kw): assert set(body.keys()) <= set(['server']) fakes.assert_has_keys(body['server'], - required=['name', 'image_ref', 'flavor_ref'], - optional=['metadata', 'personality', - 'os-scheduler-hints:scheduler_hints']) + required=['name', 'image_ref', 'flavor_ref'], + optional=['metadata', 'personality', + 'os-scheduler-hints:scheduler_hints']) if body['server']['name'] == 'some-bad-server': return (202, {}, self.get_servers_1235()[2]) else: diff --git a/novaclient/tests/v3/test_flavors.py b/novaclient/tests/v3/test_flavors.py index 4dadc0ab5..03dee061d 100644 --- a/novaclient/tests/v3/test_flavors.py +++ b/novaclient/tests/v3/test_flavors.py @@ -47,7 +47,7 @@ def test_set_keys(self): f = self.cs.flavors.get(1) f.set_keys({'k1': 'v1'}) self.cs.assert_called('POST', '/flavors/1/flavor-extra-specs', - {"extra_specs": {'k1': 'v1'}}) + {"extra_specs": {'k1': 'v1'}}) def test_set_with_valid_keys(self): valid_keys = ['key4', 'month.price', 'I-Am:AK-ey.44-', From 1aa020989e801c2a7dcc87fcb7d74128daba3007 Mon Sep 17 00:00:00 2001 From: Eugeniya Kudryashova Date: Thu, 25 Sep 2014 20:05:18 +0300 Subject: [PATCH 0649/1705] Enable check for E128 rule Fix E128 failures in novaclient/client and novaclient/shell and enable check for E128 E128 continuation line under-indented for visual indent Change-Id: Id3ac46ccdbb8a9f97e54b4643240d93b06fac6ea --- novaclient/client.py | 4 +- novaclient/shell.py | 180 ++++++++++++++++++++------------- novaclient/tests/test_shell.py | 6 +- tox.ini | 2 +- 4 files changed, 115 insertions(+), 77 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index 9358790d0..9256dbfff 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -208,7 +208,7 @@ def __init__(self, user, password, projectid=None, auth_url=None, self.tenant_id = tenant_id self._connection_pool = (_ClientConnectionPool() - if connection_pool else None) + if connection_pool else None) # This will be called by #_get_password if self.password is None. # EG if a password can only be obtained by prompting the user, but a @@ -396,7 +396,7 @@ def _get_session(self, url): "New session created for: (%s)" % service_url) self._session = requests.Session() self._session.mount(service_url, - self._connection_pool.get(service_url)) + self._connection_pool.get(service_url)) return self._session elif self._session: return self._session diff --git a/novaclient/shell.py b/novaclient/shell.py index 027f1cc60..bc88283e0 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -229,10 +229,10 @@ def error(self, message): choose_from = ' (choose from' progparts = self.prog.partition(' ') self.exit(2, _("error: %(errmsg)s\nTry '%(mainp)s help %(subp)s'" - " for more information.\n") % - {'errmsg': message.split(choose_from)[0], - 'mainp': progparts[0], - 'subp': progparts[2]}) + " for more information.\n") % + {'errmsg': message.split(choose_from)[0], + 'mainp': progparts[0], + 'subp': progparts[2]}) class OpenStackComputeShell(object): @@ -264,7 +264,8 @@ def get_base_parser(self): ) # Global arguments - parser.add_argument('-h', '--help', + parser.add_argument( + '-h', '--help', action='store_true', help=argparse.SUPPRESS, ) @@ -273,93 +274,117 @@ def get_base_parser(self): action='version', version=novaclient.__version__) - parser.add_argument('--debug', + parser.add_argument( + '--debug', default=False, action='store_true', help=_("Print debugging output")) - parser.add_argument('--os-cache', + parser.add_argument( + '--os-cache', default=strutils.bool_from_string( utils.env('OS_CACHE', default=False), True), action='store_true', help=_("Use the auth token cache. Defaults to False if " "env[OS_CACHE] is not set.")) - parser.add_argument('--timings', + parser.add_argument( + '--timings', default=False, action='store_true', help=_("Print call timing info")) - parser.add_argument('--os-auth-token', - default=utils.env('OS_AUTH_TOKEN'), - help='Defaults to env[OS_AUTH_TOKEN]') + parser.add_argument( + '--os-auth-token', + default=utils.env('OS_AUTH_TOKEN'), + help='Defaults to env[OS_AUTH_TOKEN]') - parser.add_argument('--os_username', + parser.add_argument( + '--os_username', help=argparse.SUPPRESS) - parser.add_argument('--os_password', + parser.add_argument( + '--os_password', help=argparse.SUPPRESS) - parser.add_argument('--os-tenant-name', + parser.add_argument( + '--os-tenant-name', metavar='', default=utils.env('OS_TENANT_NAME', 'NOVA_PROJECT_ID'), help=_('Defaults to env[OS_TENANT_NAME].')) - parser.add_argument('--os_tenant_name', + parser.add_argument( + '--os_tenant_name', help=argparse.SUPPRESS) - parser.add_argument('--os-tenant-id', + parser.add_argument( + '--os-tenant-id', metavar='', default=utils.env('OS_TENANT_ID'), help=_('Defaults to env[OS_TENANT_ID].')) - parser.add_argument('--os_auth_url', + parser.add_argument( + '--os_auth_url', help=argparse.SUPPRESS) - parser.add_argument('--os-region-name', + parser.add_argument( + '--os-region-name', metavar='', default=utils.env('OS_REGION_NAME', 'NOVA_REGION_NAME'), help=_('Defaults to env[OS_REGION_NAME].')) - parser.add_argument('--os_region_name', + parser.add_argument( + '--os_region_name', help=argparse.SUPPRESS) - parser.add_argument('--os-auth-system', + parser.add_argument( + '--os-auth-system', metavar='', default=utils.env('OS_AUTH_SYSTEM'), help='Defaults to env[OS_AUTH_SYSTEM].') - parser.add_argument('--os_auth_system', + parser.add_argument( + '--os_auth_system', help=argparse.SUPPRESS) - parser.add_argument('--service-type', + parser.add_argument( + '--service-type', metavar='', help=_('Defaults to compute for most actions')) - parser.add_argument('--service_type', + parser.add_argument( + '--service_type', help=argparse.SUPPRESS) - parser.add_argument('--service-name', + parser.add_argument( + '--service-name', metavar='', default=utils.env('NOVA_SERVICE_NAME'), help=_('Defaults to env[NOVA_SERVICE_NAME]')) - parser.add_argument('--service_name', + parser.add_argument( + '--service_name', help=argparse.SUPPRESS) - parser.add_argument('--volume-service-name', + parser.add_argument( + '--volume-service-name', metavar='', default=utils.env('NOVA_VOLUME_SERVICE_NAME'), help=_('Defaults to env[NOVA_VOLUME_SERVICE_NAME]')) - parser.add_argument('--volume_service_name', + parser.add_argument( + '--volume_service_name', help=argparse.SUPPRESS) - parser.add_argument('--os-endpoint-type', + parser.add_argument( + '--os-endpoint-type', metavar='', dest='endpoint_type', - default=utils.env('NOVA_ENDPOINT_TYPE', - default=utils.env('OS_ENDPOINT_TYPE', + default=utils.env( + 'NOVA_ENDPOINT_TYPE', + default=utils.env( + 'OS_ENDPOINT_TYPE', default=DEFAULT_NOVA_ENDPOINT_TYPE)), help=_('Defaults to env[NOVA_ENDPOINT_TYPE], ' - 'env[OS_ENDPOINT_TYPE] or ') - + DEFAULT_NOVA_ENDPOINT_TYPE + '.') + 'env[OS_ENDPOINT_TYPE] or ') + + DEFAULT_NOVA_ENDPOINT_TYPE + '.') - parser.add_argument('--endpoint-type', + parser.add_argument( + '--endpoint-type', help=argparse.SUPPRESS) # NOTE(dtroyer): We can't add --endpoint_type here due to argparse # thinking usage-list --end is ambiguous; but it @@ -368,23 +393,26 @@ def get_base_parser(self): # parser.add_argument('--endpoint_type', # help=argparse.SUPPRESS) - parser.add_argument('--os-compute-api-version', + parser.add_argument( + '--os-compute-api-version', metavar='', default=utils.env('OS_COMPUTE_API_VERSION', - default=DEFAULT_OS_COMPUTE_API_VERSION), + default=DEFAULT_OS_COMPUTE_API_VERSION), help=_('Accepts 1.1 or 3, ' - 'defaults to env[OS_COMPUTE_API_VERSION].')) - parser.add_argument('--os_compute_api_version', + 'defaults to env[OS_COMPUTE_API_VERSION].')) + parser.add_argument( + '--os_compute_api_version', help=argparse.SUPPRESS) - parser.add_argument('--bypass-url', + parser.add_argument( + '--bypass-url', metavar='', dest='bypass_url', default=utils.env('NOVACLIENT_BYPASS_URL'), help="Use this API endpoint instead of the Service Catalog. " "Defaults to env[NOVACLIENT_BYPASS_URL]") parser.add_argument('--bypass_url', - help=argparse.SUPPRESS) + help=argparse.SUPPRESS) # The auth-system-plugins might require some extra options novaclient.auth_plugin.load_auth_system_opts(parser) @@ -466,7 +494,8 @@ def _discover_via_entry_points(self): yield name, module def _add_bash_completion_subparser(self, subparsers): - subparser = subparsers.add_parser('bash_completion', + subparser = subparsers.add_parser( + 'bash_completion', add_help=False, formatter_class=OpenStackHelpFormatter ) @@ -482,13 +511,14 @@ def _find_actions(self, subparsers, actions_module): action_help = desc.strip() arguments = getattr(callback, 'arguments', []) - subparser = subparsers.add_parser(command, + subparser = subparsers.add_parser( + command, help=action_help, description=desc, add_help=False, - formatter_class=OpenStackHelpFormatter - ) - subparser.add_argument('-h', '--help', + formatter_class=OpenStackHelpFormatter) + subparser.add_argument( + '-h', '--help', action='help', help=argparse.SUPPRESS, ) @@ -512,7 +542,8 @@ def _get_keystone_auth(self, session, auth_url, **kwargs): if auth_token: return token.Token(auth_url, auth_token, **kwargs) else: - return password.Password(auth_url, + return password.Password( + auth_url, username=kwargs.pop('username'), user_id=kwargs.pop('user_id'), password=kwargs.pop('password'), @@ -630,9 +661,10 @@ def main(self, argv): if not auth_plugin or not auth_plugin.opts: if not os_username and not os_user_id: - raise exc.CommandError(_("You must provide a username " - "or user id via --os-username, --os-user-id, " - "env[OS_USERNAME] or env[OS_USER_ID]")) + raise exc.CommandError( + _("You must provide a username " + "or user id via --os-username, --os-user-id, " + "env[OS_USERNAME] or env[OS_USER_ID]")) if not any([args.os_tenant_name, args.os_tenant_id, args.os_project_id, args.os_project_name]): @@ -648,11 +680,12 @@ def main(self, argv): os_auth_url = auth_plugin.get_auth_url() if not os_auth_url: - raise exc.CommandError(_("You must provide an auth url " - "via either --os-auth-url or env[OS_AUTH_URL] " - "or specify an auth_system which defines a " - "default url with --os-auth-system " - "or env[OS_AUTH_SYSTEM]")) + raise exc.CommandError( + _("You must provide an auth url " + "via either --os-auth-url or env[OS_AUTH_URL] " + "or specify an auth_system which defines a " + "default url with --os-auth-system " + "or env[OS_AUTH_SYSTEM]")) project_id = args.os_project_id or args.os_tenant_id project_name = args.os_project_name or args.os_tenant_name @@ -674,8 +707,9 @@ def main(self, argv): project_domain_id=args.os_project_domain_id, project_domain_name=args.os_project_domain_name) end_time = time.time() - self.times.append(('%s %s' % ('auth_url', args.os_auth_url), - start_time, end_time)) + self.times.append( + ('%s %s' % ('auth_url', args.os_auth_url), + start_time, end_time)) if (options.os_compute_api_version and options.os_compute_api_version != '1.0'): @@ -689,25 +723,27 @@ def main(self, argv): " interchangeably.")) if not os_auth_url: - raise exc.CommandError(_("You must provide an auth url " - "via either --os-auth-url or env[OS_AUTH_URL]")) + raise exc.CommandError( + _("You must provide an auth url " + "via either --os-auth-url or env[OS_AUTH_URL]")) completion_cache = client.CompletionCache(os_username, os_auth_url) - self.cs = client.Client(options.os_compute_api_version, - os_username, os_password, os_tenant_name, - tenant_id=os_tenant_id, user_id=os_user_id, - auth_url=os_auth_url, insecure=insecure, - region_name=os_region_name, endpoint_type=endpoint_type, - extensions=self.extensions, service_type=service_type, - service_name=service_name, auth_system=os_auth_system, - auth_plugin=auth_plugin, auth_token=auth_token, - volume_service_name=volume_service_name, - timings=args.timings, bypass_url=bypass_url, - os_cache=os_cache, http_log_debug=options.debug, - cacert=cacert, timeout=timeout, - session=keystone_session, auth=keystone_auth, - completion_cache=completion_cache) + self.cs = client.Client( + options.os_compute_api_version, + os_username, os_password, os_tenant_name, + tenant_id=os_tenant_id, user_id=os_user_id, + auth_url=os_auth_url, insecure=insecure, + region_name=os_region_name, endpoint_type=endpoint_type, + extensions=self.extensions, service_type=service_type, + service_name=service_name, auth_system=os_auth_system, + auth_plugin=auth_plugin, auth_token=auth_token, + volume_service_name=volume_service_name, + timings=args.timings, bypass_url=bypass_url, + os_cache=os_cache, http_log_debug=options.debug, + cacert=cacert, timeout=timeout, + session=keystone_session, auth=keystone_auth, + completion_cache=completion_cache) # Now check for the password/token of which pieces of the # identifying keyring key can come from the underlying client @@ -838,7 +874,7 @@ class OpenStackHelpFormatter(argparse.HelpFormatter): def __init__(self, prog, indent_increment=2, max_help_position=32, width=None): super(OpenStackHelpFormatter, self).__init__(prog, indent_increment, - max_help_position, width) + max_help_position, width) def start_section(self, heading): # Title-case the headings diff --git a/novaclient/tests/test_shell.py b/novaclient/tests/test_shell.py index b9028f094..610eeb2a8 100644 --- a/novaclient/tests/test_shell.py +++ b/novaclient/tests/test_shell.py @@ -94,7 +94,8 @@ def shell(self, argstr, exitcodes=(0,)): def register_keystone_discovery_fixture(self, mreq): v2_url = "http://no.where/v2.0" v2_version = fixture.V2Discovery(v2_url) - mreq.register_uri('GET', v2_url, json=_create_ver_list([v2_version]), + mreq.register_uri( + 'GET', v2_url, json=_create_ver_list([v2_version]), status_code=200) def test_help_unknown_command(self): @@ -342,5 +343,6 @@ def make_env(self, exclude=None, fake_env=FAKE_ENV): def register_keystone_discovery_fixture(self, mreq): v3_url = "http://no.where/v3" v3_version = fixture.V3Discovery(v3_url) - mreq.register_uri('GET', v3_url, json=_create_ver_list([v3_version]), + mreq.register_uri( + 'GET', v3_url, json=_create_ver_list([v3_version]), status_code=200) diff --git a/tox.ini b/tox.ini index 47a36a4f3..88ac1e26f 100644 --- a/tox.ini +++ b/tox.ini @@ -42,7 +42,7 @@ downloadcache = ~/cache/pip # reason: removed in hacking (https://review.openstack.org/#/c/101701/) # # Additional checks are also ignored on purpose: F811, F821 -ignore = E124,E128,F811,F821,H402,H404,H405,H904 +ignore = E124,F811,F821,H402,H404,H405,H904 show-source = True exclude=.venv,.git,.tox,dist,*openstack/common*,*lib/python*,*egg,build,doc/source/conf.py From cc62efef4175a7f46c8829f7a25430a033ac30c8 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Wed, 3 Dec 2014 14:11:39 +0200 Subject: [PATCH 0650/1705] Remove code related to V3 Since V3 API is not supported, code related to it should be removed. Discussion in mailing: http://lists.openstack.org/pipermail/openstack-dev/2014-December/052095.html Change-Id: Iac5c5e6d81479cbeb8bf10cfcda1cc5617680de8 --- novaclient/client.py | 2 +- novaclient/shell.py | 3 +- .../tests/fixture_data/availability_zones.py | 6 - novaclient/tests/fixture_data/client.py | 18 - novaclient/tests/fixture_data/hosts.py | 20 - novaclient/tests/fixture_data/hypervisors.py | 33 - novaclient/tests/fixture_data/keypairs.py | 5 - novaclient/tests/fixture_data/quotas.py | 18 - novaclient/tests/fixture_data/servers.py | 128 - novaclient/tests/test_client.py | 52 +- novaclient/tests/v3/__init__.py | 0 novaclient/tests/v3/fakes.py | 388 -- novaclient/tests/v3/test_agents.py | 29 - novaclient/tests/v3/test_aggregates.py | 22 - novaclient/tests/v3/test_availability_zone.py | 36 - novaclient/tests/v3/test_certs.py | 21 - novaclient/tests/v3/test_flavor_access.py | 63 - novaclient/tests/v3/test_flavors.py | 74 - novaclient/tests/v3/test_hosts.py | 99 - novaclient/tests/v3/test_hypervisors.py | 47 - novaclient/tests/v3/test_images.py | 57 - novaclient/tests/v3/test_keypairs.py | 30 - novaclient/tests/v3/test_limits.py | 88 - novaclient/tests/v3/test_list_extensions.py | 33 - novaclient/tests/v3/test_quotas.py | 43 - novaclient/tests/v3/test_servers.py | 478 --- novaclient/tests/v3/test_services.py | 38 - novaclient/tests/v3/test_shell.py | 766 ---- novaclient/tests/v3/test_usage.py | 30 - novaclient/tests/v3/test_volumes.py | 64 - novaclient/v3/__init__.py | 17 - novaclient/v3/agents.py | 34 - novaclient/v3/aggregates.py | 26 - novaclient/v3/availability_zones.py | 33 - novaclient/v3/certs.py | 30 - novaclient/v3/client.py | 196 - novaclient/v3/flavor_access.py | 45 - novaclient/v3/flavors.py | 100 - novaclient/v3/hosts.py | 51 - novaclient/v3/hypervisors.py | 49 - novaclient/v3/images.py | 107 - novaclient/v3/keypairs.py | 28 - novaclient/v3/limits.py | 50 - novaclient/v3/list_extensions.py | 26 - novaclient/v3/quotas.py | 42 - novaclient/v3/servers.py | 1022 ----- novaclient/v3/services.py | 34 - novaclient/v3/shell.py | 3415 ----------------- novaclient/v3/usage.py | 27 - novaclient/v3/volumes.py | 75 - 50 files changed, 3 insertions(+), 8095 deletions(-) delete mode 100644 novaclient/tests/v3/__init__.py delete mode 100644 novaclient/tests/v3/fakes.py delete mode 100644 novaclient/tests/v3/test_agents.py delete mode 100644 novaclient/tests/v3/test_aggregates.py delete mode 100644 novaclient/tests/v3/test_availability_zone.py delete mode 100644 novaclient/tests/v3/test_certs.py delete mode 100644 novaclient/tests/v3/test_flavor_access.py delete mode 100644 novaclient/tests/v3/test_flavors.py delete mode 100644 novaclient/tests/v3/test_hosts.py delete mode 100644 novaclient/tests/v3/test_hypervisors.py delete mode 100644 novaclient/tests/v3/test_images.py delete mode 100644 novaclient/tests/v3/test_keypairs.py delete mode 100644 novaclient/tests/v3/test_limits.py delete mode 100644 novaclient/tests/v3/test_list_extensions.py delete mode 100644 novaclient/tests/v3/test_quotas.py delete mode 100644 novaclient/tests/v3/test_servers.py delete mode 100644 novaclient/tests/v3/test_services.py delete mode 100644 novaclient/tests/v3/test_shell.py delete mode 100644 novaclient/tests/v3/test_usage.py delete mode 100644 novaclient/tests/v3/test_volumes.py delete mode 100644 novaclient/v3/__init__.py delete mode 100644 novaclient/v3/agents.py delete mode 100644 novaclient/v3/aggregates.py delete mode 100644 novaclient/v3/availability_zones.py delete mode 100644 novaclient/v3/certs.py delete mode 100644 novaclient/v3/client.py delete mode 100644 novaclient/v3/flavor_access.py delete mode 100644 novaclient/v3/flavors.py delete mode 100644 novaclient/v3/hosts.py delete mode 100644 novaclient/v3/hypervisors.py delete mode 100644 novaclient/v3/images.py delete mode 100644 novaclient/v3/keypairs.py delete mode 100644 novaclient/v3/limits.py delete mode 100644 novaclient/v3/list_extensions.py delete mode 100644 novaclient/v3/quotas.py delete mode 100644 novaclient/v3/servers.py delete mode 100644 novaclient/v3/services.py delete mode 100644 novaclient/v3/shell.py delete mode 100644 novaclient/v3/usage.py delete mode 100644 novaclient/v3/volumes.py diff --git a/novaclient/client.py b/novaclient/client.py index 9256dbfff..be31686a3 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -763,7 +763,7 @@ def get_client_class(version): version_map = { '1.1': 'novaclient.v1_1.client.Client', '2': 'novaclient.v1_1.client.Client', - '3': 'novaclient.v3.client.Client', + '3': 'novaclient.v1_1.client.Client', } try: client_path = version_map[str(version)] diff --git a/novaclient/shell.py b/novaclient/shell.py index bc88283e0..bb79d6379 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -56,7 +56,6 @@ from novaclient.openstack.common import cliutils from novaclient import utils from novaclient.v1_1 import shell as shell_v1_1 -from novaclient.v3 import shell as shell_v3 DEFAULT_OS_COMPUTE_API_VERSION = "1.1" DEFAULT_NOVA_ENDPOINT_TYPE = 'publicURL' @@ -431,7 +430,7 @@ def get_subcommand_parser(self, version): actions_module = { '1.1': shell_v1_1, '2': shell_v1_1, - '3': shell_v3, + '3': shell_v1_1, }[version] except KeyError: actions_module = shell_v1_1 diff --git a/novaclient/tests/fixture_data/availability_zones.py b/novaclient/tests/fixture_data/availability_zones.py index 451a64b2a..008cea2a9 100644 --- a/novaclient/tests/fixture_data/availability_zones.py +++ b/novaclient/tests/fixture_data/availability_zones.py @@ -89,9 +89,3 @@ def setUp(self): self.requests.register_uri('GET', self.url('detail'), json=get_os_zone_detail, headers=self.json_headers) - - -class V3(V1): - zone_info_key = 'availability_zone_info' - zone_name_key = 'zone_name' - zone_state_key = 'zone_state' diff --git a/novaclient/tests/fixture_data/client.py b/novaclient/tests/fixture_data/client.py index 094bc969f..c6ddad3b7 100644 --- a/novaclient/tests/fixture_data/client.py +++ b/novaclient/tests/fixture_data/client.py @@ -16,7 +16,6 @@ from keystoneclient import session from novaclient.v1_1 import client as v1_1client -from novaclient.v3 import client as v3client IDENTITY_URL = 'http://identityserver:5000/v2.0' COMPUTE_URL = 'http://compute.host' @@ -58,26 +57,9 @@ def new_client(self): auth_url=self.identity_url) -class V3(V1): - - def new_client(self): - return v3client.Client(username='xx', - password='xx', - project_id='xx', - auth_url=self.identity_url) - - class SessionV1(V1): def new_client(self): self.session = session.Session() self.session.auth = v2.Password(self.identity_url, 'xx', 'xx') return v1_1client.Client(session=self.session) - - -class SessionV3(V1): - - def new_client(self): - self.session = session.Session() - self.session.auth = v2.Password(self.identity_url, 'xx', 'xx') - return v3client.Client(session=self.session) diff --git a/novaclient/tests/fixture_data/hosts.py b/novaclient/tests/fixture_data/hosts.py index 0235117a8..e7684f606 100644 --- a/novaclient/tests/fixture_data/hosts.py +++ b/novaclient/tests/fixture_data/hosts.py @@ -147,23 +147,3 @@ def get_host_startup(self): def get_host_shutdown(self): return {'host': 'sample_host', 'power_action': 'shutdown'} - - -class V3(V1): - def put_host_1(self): - return {'host': super(V3, self).put_host_1()} - - def put_host_2(self): - return {'host': super(V3, self).put_host_2()} - - def put_host_3(self): - return {'host': super(V3, self).put_host_3()} - - def get_host_reboot(self): - return {'host': super(V3, self).get_host_reboot()} - - def get_host_startup(self): - return {'host': super(V3, self).get_host_startup()} - - def get_host_shutdown(self): - return {'host': super(V3, self).get_host_shutdown()} diff --git a/novaclient/tests/fixture_data/hypervisors.py b/novaclient/tests/fixture_data/hypervisors.py index c3f6f7210..83a37d58c 100644 --- a/novaclient/tests/fixture_data/hypervisors.py +++ b/novaclient/tests/fixture_data/hypervisors.py @@ -180,36 +180,3 @@ def setUp(self): self.requests.register_uri('GET', self.url(1234, 'uptime'), json=get_os_hypervisors_uptime, headers=self.headers) - - -class V3(V1): - - def setUp(self): - super(V3, self).setUp() - - get_os_hypervisors_search = { - 'hypervisors': [ - {'id': 1234, 'hypervisor_hostname': 'hyper1'}, - {'id': 5678, 'hypervisor_hostname': 'hyper2'} - ] - } - - self.requests.register_uri('GET', - self.url('search', query='hyper'), - json=get_os_hypervisors_search, - headers=self.headers) - - get_1234_servers = { - 'hypervisor': { - 'id': 1234, - 'hypervisor_hostname': 'hyper1', - 'servers': [ - {'name': 'inst1', 'id': 'uuid1'}, - {'name': 'inst2', 'id': 'uuid2'} - ] - }, - } - - self.requests.register_uri('GET', self.url(1234, 'servers'), - json=get_1234_servers, - headers=self.headers) diff --git a/novaclient/tests/fixture_data/keypairs.py b/novaclient/tests/fixture_data/keypairs.py index 97cf24992..9017d1cff 100644 --- a/novaclient/tests/fixture_data/keypairs.py +++ b/novaclient/tests/fixture_data/keypairs.py @@ -45,8 +45,3 @@ def post_os_keypairs(request, context): self.requests.register_uri('POST', self.url(), json=post_os_keypairs, headers=headers) - - -class V3(V1): - - base_url = 'keypairs' diff --git a/novaclient/tests/fixture_data/quotas.py b/novaclient/tests/fixture_data/quotas.py index 3250f8843..3972d20ff 100644 --- a/novaclient/tests/fixture_data/quotas.py +++ b/novaclient/tests/fixture_data/quotas.py @@ -64,21 +64,3 @@ def test_quota(self, tenant_id='test'): 'security_groups': 1, 'security_group_rules': 1 } - - -class V3(V1): - - def setUp(self): - super(V3, self).setUp() - - get_detail = { - 'quota_set': { - 'cores': {'reserved': 0, 'in_use': 0, 'limit': 10}, - 'instances': {'reserved': 0, 'in_use': 4, 'limit': 50}, - 'ram': {'reserved': 0, 'in_use': 1024, 'limit': 51200} - } - } - - self.requests.register_uri('GET', self.url('test', 'detail'), - json=get_detail, - headers=self.headers) diff --git a/novaclient/tests/fixture_data/servers.py b/novaclient/tests/fixture_data/servers.py index d316d4527..c728f2ff3 100644 --- a/novaclient/tests/fixture_data/servers.py +++ b/novaclient/tests/fixture_data/servers.py @@ -493,131 +493,3 @@ def post_servers_1234_action(self, request, context): else: raise AssertionError("Unexpected server action: %s" % action) return {'server': _body} - - -class V3(Base): - - def setUp(self): - super(V3, self).setUp() - - get_interfaces = { - "interface_attachments": [ - { - "port_state": "ACTIVE", - "net_id": "net-id-1", - "port_id": "port-id-1", - "mac_address": "aa:bb:cc:dd:ee:ff", - "fixed_ips": [{"ip_address": "1.2.3.4"}], - }, - { - "port_state": "ACTIVE", - "net_id": "net-id-1", - "port_id": "port-id-1", - "mac_address": "aa:bb:cc:dd:ee:ff", - "fixed_ips": [{"ip_address": "1.2.3.4"}], - } - ] - } - - self.requests.register_uri('GET', - self.url('1234', 'os-attach-interfaces'), - json=get_interfaces, - headers=self.json_headers) - - attach_body = {'interface_attachment': {}} - self.requests.register_uri('POST', - self.url('1234', 'os-attach-interfaces'), - json=attach_body, - headers=self.json_headers) - - self.requests.register_uri('GET', - self.url('1234', 'os-server-diagnostics'), - json=self.diagnostic) - - url = self.url('1234', 'os-attach-interfaces', 'port-id') - self.requests.register_uri('DELETE', url) - - self.requests.register_uri('GET', - self.url(1234, 'os-server-password'), - json={'password': ''}) - - def post_servers(self, request, context): - body = jsonutils.loads(request.body) - assert set(body.keys()) <= set(['server']) - fakes.assert_has_keys( - body['server'], - required=['name', 'image_ref', 'flavor_ref'], - optional=['metadata', 'personality', - 'os-scheduler-hints:scheduler_hints']) - if body['server']['name'] == 'some-bad-server': - body = self.server_1235 - else: - body = self.server_1234 - - context.status_code = 202 - return {'server': body} - - def post_servers_1234_action(self, request, context): - context.status_code = 202 - body_is_none_list = [ - 'revert_resize', 'migrate', 'stop', 'start', 'force_delete', - 'restore', 'pause', 'unpause', 'lock', 'unlock', 'unrescue', - 'resume', 'suspend', 'lock', 'unlock', 'shelve', 'shelve_offload', - 'unshelve', 'reset_network', 'rescue', 'confirm_resize'] - body_return_map = { - 'rescue': {'admin_password': 'RescuePassword'}, - 'get_console_output': {'output': 'foo'}, - 'rebuild': {'server': self.server_1234}, - } - body_param_check_exists = { - 'rebuild': 'image_ref', - 'resize': 'flavor_ref'} - body_params_check_exact = { - 'reboot': ['type'], - 'add_fixed_ip': ['network_id'], - 'evacuate': ['host', 'on_shared_storage'], - 'remove_fixed_ip': ['address'], - 'change_password': ['admin_password'], - 'get_console_output': ['length'], - 'get_vnc_console': ['type'], - 'get_spice_console': ['type'], - 'get_serial_console': ['type'], - 'reset_state': ['state'], - 'create_image': ['name', 'metadata'], - 'migrate_live': ['host', 'block_migration', 'disk_over_commit'], - 'create_backup': ['name', 'backup_type', 'rotation'], - 'attach': ['volume_id', 'device'], - 'detach': ['volume_id'], - 'swap_volume_attachment': ['old_volume_id', 'new_volume_id']} - - body = jsonutils.loads(request.body) - assert len(body.keys()) == 1 - action = list(body)[0] - _body = body_return_map.get(action, '') - - if action in body_is_none_list: - assert body[action] is None - - if action in body_param_check_exists: - assert body_param_check_exists[action] in body[action] - - if action == 'evacuate': - body[action].pop('admin_password', None) - - if action in body_params_check_exact: - assert set(body[action]) == set(body_params_check_exact[action]) - - if action == 'reboot': - assert body[action]['type'] in ['HARD', 'SOFT'] - elif action == 'confirm_resize': - # This one method returns a different response code - context.status_code = 204 - elif action == 'create_image': - context.headers['location'] = "http://blah/images/456" - - if action not in set.union(set(body_is_none_list), - set(body_params_check_exact.keys()), - set(body_param_check_exists.keys())): - raise AssertionError("Unexpected server action: %s" % action) - - return _body diff --git a/novaclient/tests/test_client.py b/novaclient/tests/test_client.py index 7322b66ac..e5a8db5f8 100644 --- a/novaclient/tests/test_client.py +++ b/novaclient/tests/test_client.py @@ -23,10 +23,8 @@ import novaclient.client import novaclient.extension -import novaclient.tests.fakes as fakes from novaclient.tests import utils import novaclient.v1_1.client -import novaclient.v3.client class ClientConnectionPoolTest(utils.TestCase): @@ -140,7 +138,7 @@ def test_client_version_url_with_project_name(self): def test_get_client_class_v3(self): output = novaclient.client.get_client_class('3') - self.assertEqual(output, novaclient.v3.client.Client) + self.assertEqual(output, novaclient.v1_1.client.Client) def test_get_client_class_v2(self): output = novaclient.client.get_client_class('2') @@ -199,44 +197,6 @@ def test_client_get_reset_timings_v1_1(self): cs.reset_timings() self.assertEqual(0, len(cs.get_timings())) - def test_client_set_management_url_v3(self): - cs = novaclient.v3.client.Client("user", "password", "project_id", - auth_url="foo/v2") - cs.set_management_url("blabla") - self.assertEqual("blabla", cs.client.management_url) - - def test_client_get_reset_timings_v3(self): - cs = novaclient.v3.client.Client("user", "password", "project_id", - auth_url="foo/v2") - self.assertEqual(0, len(cs.get_timings())) - cs.client.times.append("somevalue") - self.assertEqual(["somevalue"], cs.get_timings()) - - cs.reset_timings() - self.assertEqual(0, len(cs.get_timings())) - - def test_clent_extensions_v3(self): - fake_attribute_name1 = "FakeAttribute1" - fake_attribute_name2 = "FakeAttribute2" - extensions = [ - novaclient.extension.Extension(fake_attribute_name1, fakes), - novaclient.extension.Extension(fake_attribute_name2, utils), - ] - - cs = novaclient.v3.client.Client("user", "password", "project_id", - auth_url="foo/v2", - extensions=extensions) - self.assertIsInstance(getattr(cs, fake_attribute_name1, None), - fakes.FakeManager) - self.assertFalse(hasattr(cs, fake_attribute_name2)) - - @mock.patch.object(novaclient.client.HTTPClient, 'authenticate') - def test_authenticate_call_v3(self, mock_authenticate): - cs = novaclient.v3.client.Client("user", "password", "project_id", - auth_url="foo/v2") - cs.authenticate() - self.assertTrue(mock_authenticate.called) - @mock.patch('novaclient.client.HTTPClient') def test_contextmanager_v1_1(self, mock_http_client): fake_client = mock.Mock() @@ -247,16 +207,6 @@ def test_contextmanager_v1_1(self, mock_http_client): self.assertTrue(fake_client.open_session.called) self.assertTrue(fake_client.close_session.called) - @mock.patch('novaclient.client.HTTPClient') - def test_contextmanager_v3(self, mock_http_client): - fake_client = mock.Mock() - mock_http_client.return_value = fake_client - with novaclient.v3.client.Client("user", "password", "project_id", - auth_url="foo/v2"): - pass - self.assertTrue(fake_client.open_session.called) - self.assertTrue(fake_client.close_session.called) - def test_get_password_simple(self): cs = novaclient.client.HTTPClient("user", "password", "", "") cs.password_func = mock.Mock() diff --git a/novaclient/tests/v3/__init__.py b/novaclient/tests/v3/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/novaclient/tests/v3/fakes.py b/novaclient/tests/v3/fakes.py deleted file mode 100644 index f0c57f76b..000000000 --- a/novaclient/tests/v3/fakes.py +++ /dev/null @@ -1,388 +0,0 @@ -# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. -# Copyright 2011 OpenStack Foundation -# Copyright 2013 IBM Corp. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import datetime - -from oslo.utils import strutils - -from novaclient.tests import fakes -from novaclient.tests.v1_1 import fakes as fakes_v1_1 -from novaclient.v3 import client - - -class FakeClient(fakes.FakeClient, client.Client): - - def __init__(self, *args, **kwargs): - client.Client.__init__(self, 'username', 'password', - 'project_id', 'auth_url', - extensions=kwargs.get('extensions')) - self.client = FakeHTTPClient(**kwargs) - - -class FakeHTTPClient(fakes_v1_1.FakeHTTPClient): - # - # Hosts - # - def put_os_hosts_sample_host_1(self, body, **kw): - return (200, {}, {'host': {'host': 'sample-host_1', - 'status': 'enabled'}}) - - def put_os_hosts_sample_host_2(self, body, **kw): - return (200, {}, {'host': {'host': 'sample-host_2', - 'maintenance_mode': 'on_maintenance'}}) - - def put_os_hosts_sample_host_3(self, body, **kw): - return (200, {}, {'host': {'host': 'sample-host_3', - 'status': 'enabled', - 'maintenance_mode': 'on_maintenance'}}) - - def get_os_hosts_sample_host_reboot(self, **kw): - return (200, {}, {'host': {'host': 'sample_host', - 'power_action': 'reboot'}}) - - def get_os_hosts_sample_host_startup(self, **kw): - return (200, {}, {'host': {'host': 'sample_host', - 'power_action': 'startup'}}) - - def get_os_hosts_sample_host_shutdown(self, **kw): - return (200, {}, {'host': {'host': 'sample_host', - 'power_action': 'shutdown'}}) - - # - # Flavors - # - post_flavors_1_flavor_extra_specs = ( - fakes_v1_1.FakeHTTPClient.post_flavors_1_os_extra_specs) - - post_flavors_4_flavor_extra_specs = ( - fakes_v1_1.FakeHTTPClient.post_flavors_4_os_extra_specs) - - delete_flavors_1_flavor_extra_specs_k1 = ( - fakes_v1_1.FakeHTTPClient.delete_flavors_1_os_extra_specs_k1) - - def get_flavors_detail(self, **kw): - flavors = {'flavors': [ - {'id': 1, 'name': '256 MB Server', 'ram': 256, 'disk': 10, - 'ephemeral': 10, - 'flavor-access:is_public': True, - 'links': {}}, - {'id': 2, 'name': '512 MB Server', 'ram': 512, 'disk': 20, - 'ephemeral': 20, - 'flavor-access:is_public': False, - 'links': {}}, - {'id': 4, 'name': '1024 MB Server', 'ram': 1024, 'disk': 10, - 'ephemeral': 10, - 'flavor-access:is_public': True, - 'links': {}}, - {'id': 'aa1', 'name': '128 MB Server', 'ram': 128, 'disk': 0, - 'ephemeral': 0, - 'flavor-access:is_public': True, - 'links': {}} - ]} - - if 'is_public' not in kw: - filter_is_public = True - else: - if kw['is_public'].lower() == 'none': - filter_is_public = None - else: - filter_is_public = strutils.bool_from_string(kw['is_public'], - True) - - if filter_is_public is not None: - if filter_is_public: - flavors['flavors'] = [ - v for v in flavors['flavors'] - if v['flavor-access:is_public'] - ] - else: - flavors['flavors'] = [ - v for v in flavors['flavors'] - if not v['flavor-access:is_public'] - ] - - return (200, {}, flavors) - - # - # Flavor access - # - get_flavors_2_flavor_access = ( - fakes_v1_1.FakeHTTPClient.get_flavors_2_os_flavor_access) - get_flavors_2_flavor_extra_specs = ( - fakes_v1_1.FakeHTTPClient.get_flavors_2_os_extra_specs) - get_flavors_aa1_flavor_extra_specs = ( - fakes_v1_1.FakeHTTPClient.get_flavors_aa1_os_extra_specs) - - # - # Images - # - get_v1_images_detail = fakes_v1_1.FakeHTTPClient.get_images_detail - get_v1_images = fakes_v1_1.FakeHTTPClient.get_images - - def head_v1_images_1(self, **kw): - headers = { - 'x-image-meta-id': '1', - 'x-image-meta-name': 'CentOS 5.2', - 'x-image-meta-updated': '2010-10-10T12:00:00Z', - 'x-image-meta-created': '2010-10-10T12:00:00Z', - 'x-image-meta-status': 'ACTIVE', - 'x-image-meta-property-test_key': 'test_value'} - return 200, headers, '' - - # - # Servers - # - get_servers_1234_os_server_diagnostics = ( - fakes_v1_1.FakeHTTPClient.get_servers_1234_diagnostics) - - delete_servers_1234_os_attach_interfaces_port_id = ( - fakes_v1_1.FakeHTTPClient.delete_servers_1234_os_interface_port_id) - - def get_servers_1234_os_attach_interfaces(self, **kw): - return (200, {}, { - "interface_attachments": [ - {"port_state": "ACTIVE", - "net_id": "net-id-1", - "port_id": "port-id-1", - "mac_address": "aa:bb:cc:dd:ee:ff", - "fixed_ips": [{"ip_address": "1.2.3.4"}], - }, - {"port_state": "ACTIVE", - "net_id": "net-id-1", - "port_id": "port-id-1", - "mac_address": "aa:bb:cc:dd:ee:ff", - "fixed_ips": [{"ip_address": "1.2.3.4"}]}] - }) - - def post_servers_1234_os_attach_interfaces(self, **kw): - return (200, {}, {'interface_attachment': {}}) - - def post_servers(self, body, **kw): - assert set(body.keys()) <= set(['server']) - fakes.assert_has_keys(body['server'], - required=['name', 'image_ref', 'flavor_ref'], - optional=['metadata', 'personality', - 'os-scheduler-hints:scheduler_hints']) - if body['server']['name'] == 'some-bad-server': - return (202, {}, self.get_servers_1235()[2]) - else: - return (202, {}, self.get_servers_1234()[2]) - - # - # Server Actions - # - def post_servers_1234_action(self, body, **kw): - _headers = None - resp = 202 - body_is_none_list = [ - 'revert_resize', 'migrate', 'stop', 'start', 'force_delete', - 'restore', 'pause', 'unpause', 'lock', 'unlock', 'unrescue', - 'resume', 'suspend', 'lock', 'unlock', 'shelve', 'shelve_offload', - 'unshelve', 'reset_network', 'rescue', 'confirm_resize'] - body_return_map = { - 'rescue': {'admin_password': 'RescuePassword'}, - 'get_console_output': {'output': 'foo'}, - 'rebuild': self.get_servers_1234()[2], - } - body_param_check_exists = { - 'rebuild': 'image_ref', - 'resize': 'flavor_ref', - 'evacuate': 'on_shared_storage'} - body_params_check_exact = { - 'reboot': ['type'], - 'add_fixed_ip': ['network_id'], - 'remove_fixed_ip': ['address'], - 'change_password': ['admin_password'], - 'get_console_output': ['length'], - 'get_vnc_console': ['type'], - 'get_spice_console': ['type'], - 'reset_state': ['state'], - 'create_image': ['name', 'metadata'], - 'migrate_live': ['host', 'block_migration', 'disk_over_commit'], - 'create_backup': ['name', 'backup_type', 'rotation'], - 'detach': ['volume_id'], - 'swap_volume_attachment': ['old_volume_id', 'new_volume_id']} - body_params_check_superset = { - 'attach': ['volume_id', 'device']} - - assert len(body.keys()) == 1 - action = list(body)[0] - _body = body_return_map.get(action) - - if action in body_is_none_list: - assert body[action] is None - - if action in body_param_check_exists: - assert body_param_check_exists[action] in body[action] - - if action in body_params_check_exact: - assert set(body[action]) == set(body_params_check_exact[action]) - - if action in body_params_check_superset: - assert set(body[action]) >= set(body_params_check_superset[action]) - - if action == 'reboot': - assert body[action]['type'] in ['HARD', 'SOFT'] - elif action == 'confirm_resize': - # This one method returns a different response code - resp = 204 - elif action == 'create_image': - _headers = dict(location="http://blah/images/456") - - if action not in set.union(set(body_is_none_list), - set(body_params_check_exact.keys()), - set(body_param_check_exists.keys()), - set(body_params_check_superset.keys())): - raise AssertionError("Unexpected server action: %s" % action) - - return (resp, _headers, _body) - - # - # Server password - # - - def get_servers_1234_os_server_password(self, **kw): - return (200, {}, {'password': ''}) - - def delete_servers_1234_os_server_password(self, **kw): - return (202, {}, None) - - # - # Availability Zones - # - def get_os_availability_zone(self, **kw): - return (200, {}, { - "availability_zone_info": [ - {"zone_name": "zone-1", "zone_state": {"available": True}, - "hosts": None}, - {"zone_name": "zone-2", "zone_state": {"available": False}, - "hosts": None}]}) - - def get_os_availability_zone_detail(self, **kw): - return (200, {}, { - "availability_zone_info": [ - {"zone_name": "zone-1", - "zone_state": {"available": True}, - "hosts": { - "fake_host-1": { - "nova-compute": { - "active": True, - "available": True, - "updated_at": datetime.datetime( - 2012, 12, 26, 14, 45, 25, 0)}}}}, - {"zone_name": "internal", - "zone_state": { - "available": True}, - "hosts": { - "fake_host-1": { - "nova-sched": { - "active": True, - "available": True, - "updated_at": datetime.datetime( - 2012, 12, 26, 14, 45, 25, 0)}}, - "fake_host-2": { - "nova-network": { - "active": True, - "available": False, - "updated_at": datetime.datetime( - 2012, 12, 26, 14, 45, 24, 0)}}}}, - {"zone_name": "zone-2", - "zone_state": {"available": False}, - "hosts": None}]}) - - # - # Quotas - # - def put_os_quota_sets_97f4c221bff44578b0300df4ef119353(self, body, **kw): - assert list(body) == ['quota_set'] - return (200, {}, { - 'quota_set': { - 'tenant_id': '97f4c221bff44578b0300df4ef119353', - 'metadata_items': [], - 'injected_file_content_bytes': 1, - 'injected_file_path_bytes': 1, - 'ram': 1, - 'floating_ips': 1, - 'instances': 1, - 'injected_files': 1, - 'cores': 1, - 'keypairs': 1, - 'security_groups': 1, - 'security_group_rules': 1, - 'server_groups': 1, - 'server_group_members': 1}}) - - def get_os_quota_sets_test_detail(self, **kw): - return (200, {}, {'quota_set': { - 'cores': {'reserved': 0, 'in_use': 0, 'limit': 10}, - 'instances': {'reserved': 0, 'in_use': 4, 'limit': 50}, - 'ram': {'reserved': 0, 'in_use': 1024, 'limit': 51200}}}) - - # - # Hypervisors - # - def get_os_hypervisors_search(self, **kw): - if kw['query'] == 'hyper1': - return (200, {}, {'hypervisors': [ - {'id': 1234, 'hypervisor_hostname': 'hyper1'}]}) - return (200, {}, { - 'hypervisors': [ - {'id': 1234, 'hypervisor_hostname': 'hyper1'}, - {'id': 5678, 'hypervisor_hostname': 'hyper2'}]}) - - def get_os_hypervisors_1234_servers(self, **kw): - return (200, {}, { - 'hypervisor': - {'id': 1234, 'hypervisor_hostname': 'hyper1', - 'servers': [ - {'name': 'inst1', 'id': 'uuid1'}, - {'name': 'inst2', 'id': 'uuid2'}]}}) - - # - # Keypairs - # - get_keypairs_test = fakes_v1_1.FakeHTTPClient.get_os_keypairs_test - get_keypairs = fakes_v1_1.FakeHTTPClient.get_os_keypairs - delete_keypairs_test = fakes_v1_1.FakeHTTPClient.delete_os_keypairs_test - post_keypairs = fakes_v1_1.FakeHTTPClient.post_os_keypairs - - # - # List all extensions - # - def get_extensions(self, **kw): - exts = [ - { - "alias": "os-multinic", - "description": "Multiple network support", - "name": "Multinic", - "version": 1, - }, - { - "alias": "os-extended-server-attributes", - "description": "Extended Server Attributes support.", - "name": "ExtendedServerAttributes", - "version": 1, - }, - { - "alias": "os-extended-status", - "description": "Extended Status support", - "name": "ExtendedStatus", - "version": 1, - }, - ] - return (200, {}, { - "extensions": exts, - }) diff --git a/novaclient/tests/v3/test_agents.py b/novaclient/tests/v3/test_agents.py deleted file mode 100644 index bdd101bdc..000000000 --- a/novaclient/tests/v3/test_agents.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright 2012 IBM Corp. -# All Rights Reserved. -# -# 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 novaclient.tests.fixture_data import client -from novaclient.tests.v1_1 import test_agents - - -class AgentsTest(test_agents.AgentsTest): - - scenarios = [('original', {'client_fixture_class': client.V3}), - ('session', {'client_fixture_class': client.SessionV3})] - - def _build_example_update_body(self): - return {"agent": { - "url": "/yyy/yyyy/yyyy", - "version": "8.0", - "md5hash": "add6bb58e139be103324d04d82d8f546"}} diff --git a/novaclient/tests/v3/test_aggregates.py b/novaclient/tests/v3/test_aggregates.py deleted file mode 100644 index 17e27d162..000000000 --- a/novaclient/tests/v3/test_aggregates.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright 2013 IBM Corp. -# -# 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 novaclient.tests.fixture_data import client -from novaclient.tests.v1_1 import test_aggregates - - -class AggregatesTest(test_aggregates.AggregatesTest): - - scenarios = [('original', {'client_fixture_class': client.V3}), - ('session', {'client_fixture_class': client.SessionV3})] diff --git a/novaclient/tests/v3/test_availability_zone.py b/novaclient/tests/v3/test_availability_zone.py deleted file mode 100644 index 67a4415bf..000000000 --- a/novaclient/tests/v3/test_availability_zone.py +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright 2011 OpenStack Foundation -# Copyright 2013 IBM Corp. -# All Rights Reserved. -# -# 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 novaclient.tests.fixture_data import availability_zones as data -from novaclient.tests.fixture_data import client -from novaclient.tests.v1_1 import test_availability_zone -from novaclient.v3 import availability_zones - - -class AvailabilityZoneTest(test_availability_zone.AvailabilityZoneTest): - from novaclient.v3 import shell # noqa - - data_fixture_class = data.V3 - - scenarios = [('original', {'client_fixture_class': client.V3}), - ('session', {'client_fixture_class': client.SessionV3})] - - def _assertZone(self, zone, name, status): - self.assertEqual(zone.zone_name, name) - self.assertEqual(zone.zone_state, status) - - def _get_availability_zone_type(self): - return availability_zones.AvailabilityZone diff --git a/novaclient/tests/v3/test_certs.py b/novaclient/tests/v3/test_certs.py deleted file mode 100644 index 30597fc10..000000000 --- a/novaclient/tests/v3/test_certs.py +++ /dev/null @@ -1,21 +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 novaclient.tests.fixture_data import client -from novaclient.tests.v1_1 import test_certs - - -class CertsTest(test_certs.CertsTest): - - scenarios = [('original', {'client_fixture_class': client.V3}), - ('session', {'client_fixture_class': client.SessionV3})] diff --git a/novaclient/tests/v3/test_flavor_access.py b/novaclient/tests/v3/test_flavor_access.py deleted file mode 100644 index cd5914012..000000000 --- a/novaclient/tests/v3/test_flavor_access.py +++ /dev/null @@ -1,63 +0,0 @@ -# Copyright 2012 OpenStack Foundation -# Copyright 2013 IBM Corp. -# All Rights Reserved. -# -# 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 novaclient.tests import utils -from novaclient.tests.v3 import fakes -from novaclient.v3 import flavor_access - - -cs = fakes.FakeClient() - - -class FlavorAccessTest(utils.TestCase): - - def test_list_access_by_flavor_private(self): - kwargs = {'flavor': cs.flavors.get(2)} - r = cs.flavor_access.list(**kwargs) - cs.assert_called('GET', '/flavors/2/flavor-access') - for access_list in r: - self.assertIsInstance(access_list, - flavor_access.FlavorAccess) - - def test_add_tenant_access(self): - flavor = cs.flavors.get(2) - tenant = 'proj2' - r = cs.flavor_access.add_tenant_access(flavor, tenant) - - body = { - "add_tenant_access": { - "tenant_id": "proj2" - } - } - - cs.assert_called('POST', '/flavors/2/action', body) - for a in r: - self.assertIsInstance(a, flavor_access.FlavorAccess) - - def test_remove_tenant_access(self): - flavor = cs.flavors.get(2) - tenant = 'proj2' - r = cs.flavor_access.remove_tenant_access(flavor, tenant) - - body = { - "remove_tenant_access": { - "tenant_id": "proj2" - } - } - - cs.assert_called('POST', '/flavors/2/action', body) - for a in r: - self.assertIsInstance(a, flavor_access.FlavorAccess) diff --git a/novaclient/tests/v3/test_flavors.py b/novaclient/tests/v3/test_flavors.py deleted file mode 100644 index 03dee061d..000000000 --- a/novaclient/tests/v3/test_flavors.py +++ /dev/null @@ -1,74 +0,0 @@ -# Copyright (c) 2013, OpenStack -# Copyright 2013 IBM Corp. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import mock - -from novaclient.tests.v1_1 import test_flavors -from novaclient.tests.v3 import fakes -from novaclient.v3 import flavors - - -class FlavorsTest(test_flavors.FlavorsTest): - def _get_fake_client(self): - return fakes.FakeClient() - - def _get_flavor_type(self): - return flavors.Flavor - - def _create_body(self, name, ram, vcpus, disk, ephemeral, id, swap, - rxtx_factor, is_public): - return { - "flavor": { - "name": name, - "ram": ram, - "vcpus": vcpus, - "disk": disk, - "ephemeral": ephemeral, - "id": id, - "swap": swap, - "os-flavor-rxtx:rxtx_factor": rxtx_factor, - "flavor-access:is_public": is_public, - } - } - - def test_set_keys(self): - f = self.cs.flavors.get(1) - f.set_keys({'k1': 'v1'}) - self.cs.assert_called('POST', '/flavors/1/flavor-extra-specs', - {"extra_specs": {'k1': 'v1'}}) - - def test_set_with_valid_keys(self): - valid_keys = ['key4', 'month.price', 'I-Am:AK-ey.44-', - 'key with spaces and _'] - - f = self.cs.flavors.get(4) - for key in valid_keys: - f.set_keys({key: 'v4'}) - self.cs.assert_called('POST', '/flavors/4/flavor-extra-specs', - {"extra_specs": {key: 'v4'}}) - - @mock.patch.object(flavors.FlavorManager, '_delete') - def test_unset_keys(self, mock_delete): - f = self.cs.flavors.get(1) - keys = ['k1', 'k2'] - f.unset_keys(keys) - mock_delete.assert_has_calls([ - mock.call("/flavors/1/flavor-extra-specs/k1"), - mock.call("/flavors/1/flavor-extra-specs/k2") - ]) - - def test_get_flavor_details_diablo(self): - # Don't need for V3 API to work against diablo - pass diff --git a/novaclient/tests/v3/test_hosts.py b/novaclient/tests/v3/test_hosts.py deleted file mode 100644 index 56579330a..000000000 --- a/novaclient/tests/v3/test_hosts.py +++ /dev/null @@ -1,99 +0,0 @@ -# Copyright 2013 OpenStack Foundation -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from novaclient.tests.fixture_data import client -from novaclient.tests.fixture_data import hosts as data -from novaclient.tests import utils -from novaclient.v3 import hosts - - -class HostsTest(utils.FixturedTestCase): - - client_fixture_class = client.V3 - data_fixture_class = data.V3 - - def test_describe_resource(self): - hs = self.cs.hosts.get('host') - self.assert_called('GET', '/os-hosts/host') - for h in hs: - self.assertIsInstance(h, hosts.Host) - - def test_list_host(self): - hs = self.cs.hosts.list() - self.assert_called('GET', '/os-hosts') - for h in hs: - self.assertIsInstance(h, hosts.Host) - self.assertEqual('nova1', h.zone) - - def test_list_host_with_zone(self): - hs = self.cs.hosts.list('nova') - self.assert_called('GET', '/os-hosts?zone=nova') - for h in hs: - self.assertIsInstance(h, hosts.Host) - self.assertEqual('nova', h.zone) - - def test_list_host_with_service(self): - hs = self.cs.hosts.list(service='nova-compute') - self.assert_called('GET', '/os-hosts?service=nova-compute') - for h in hs: - self.assertIsInstance(h, hosts.Host) - self.assertEqual(h.service, 'nova-compute') - - def test_list_host_with_zone_and_service(self): - hs = self.cs.hosts.list(service='nova-compute', zone='nova') - self.assert_called('GET', '/os-hosts?zone=nova&service=nova-compute') - for h in hs: - self.assertIsInstance(h, hosts.Host) - self.assertEqual(h.zone, 'nova') - self.assertEqual(h.service, 'nova-compute') - - def test_update_enable(self): - host = self.cs.hosts.get('sample_host')[0] - values = {"status": "enabled"} - result = host.update(values) - self.assert_called('PUT', '/os-hosts/sample_host', {"host": values}) - self.assertIsInstance(result, hosts.Host) - - def test_update_maintenance(self): - host = self.cs.hosts.get('sample_host')[0] - values = {"maintenance_mode": "enable"} - result = host.update(values) - self.assert_called('PUT', '/os-hosts/sample_host', {"host": values}) - self.assertIsInstance(result, hosts.Host) - - def test_update_both(self): - host = self.cs.hosts.get('sample_host')[0] - values = {"status": "enabled", - "maintenance_mode": "enable"} - result = host.update(values) - self.assert_called('PUT', '/os-hosts/sample_host', {"host": values}) - self.assertIsInstance(result, hosts.Host) - - def test_host_startup(self): - host = self.cs.hosts.get('sample_host')[0] - host.startup() - self.assert_called( - 'GET', '/os-hosts/sample_host/startup') - - def test_host_reboot(self): - host = self.cs.hosts.get('sample_host')[0] - host.reboot() - self.assert_called( - 'GET', '/os-hosts/sample_host/reboot') - - def test_host_shutdown(self): - host = self.cs.hosts.get('sample_host')[0] - host.shutdown() - self.assert_called( - 'GET', '/os-hosts/sample_host/shutdown') diff --git a/novaclient/tests/v3/test_hypervisors.py b/novaclient/tests/v3/test_hypervisors.py deleted file mode 100644 index 94ea3fad4..000000000 --- a/novaclient/tests/v3/test_hypervisors.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright 2012 OpenStack Foundation -# All Rights Reserved. -# -# 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 novaclient.tests.fixture_data import client -from novaclient.tests.fixture_data import hypervisors as data -from novaclient.tests.v1_1 import test_hypervisors - - -class HypervisorsTest(test_hypervisors.HypervisorsTest): - - client_fixture_class = client.V3 - data_fixture_class = data.V3 - - def test_hypervisor_search(self): - expected = [ - dict(id=1234, hypervisor_hostname='hyper1'), - dict(id=5678, hypervisor_hostname='hyper2')] - - result = self.cs.hypervisors.search('hyper') - self.assert_called('GET', '/os-hypervisors/search?query=hyper') - - for idx, hyper in enumerate(result): - self.compare_to_expected(expected[idx], hyper) - - def test_hypervisor_servers(self): - expected = dict(id=1234, - hypervisor_hostname='hyper1', - servers=[ - dict(name='inst1', id='uuid1'), - dict(name='inst2', id='uuid2')]) - - result = self.cs.hypervisors.servers('1234') - self.assert_called('GET', '/os-hypervisors/1234/servers') - - self.compare_to_expected(expected, result) diff --git a/novaclient/tests/v3/test_images.py b/novaclient/tests/v3/test_images.py deleted file mode 100644 index be8260d8f..000000000 --- a/novaclient/tests/v3/test_images.py +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright 2013 IBM Corp. -# -# 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 novaclient.tests.fixture_data import client -from novaclient.tests.fixture_data import images as data -from novaclient.tests import utils -from novaclient.v3 import images - - -class ImagesTest(utils.FixturedTestCase): - - client_fixture_class = client.V3 - data_fixture_class = data.V3 - - def test_list_images(self): - il = self.cs.images.list() - self.assert_called('GET', '/v1/images/detail') - for i in il: - self.assertIsInstance(i, images.Image) - - def test_list_images_undetailed(self): - il = self.cs.images.list(detailed=False) - self.assert_called('GET', '/v1/images') - for i in il: - self.assertIsInstance(i, images.Image) - - def test_list_images_with_limit(self): - self.cs.images.list(limit=4) - self.assert_called('GET', '/v1/images/detail?limit=4') - - def test_get_image_details(self): - i = self.cs.images.get(1) - self.assert_called('HEAD', '/v1/images/1') - self.assertIsInstance(i, images.Image) - self.assertEqual('1', i.id) - self.assertEqual('CentOS 5.2', i.name) - - def test_find(self): - i = self.cs.images.find(name="CentOS 5.2") - self.assertEqual('1', i.id) - self.assert_called('HEAD', '/v1/images/1') - - iml = self.cs.images.findall(status='SAVING') - self.assertEqual(1, len(iml)) - self.assertEqual('My Server Backup', iml[0].name) diff --git a/novaclient/tests/v3/test_keypairs.py b/novaclient/tests/v3/test_keypairs.py deleted file mode 100644 index 157042fce..000000000 --- a/novaclient/tests/v3/test_keypairs.py +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright 2013 IBM Corp. -# -# 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 novaclient.tests.fixture_data import client -from novaclient.tests.fixture_data import keypairs as data -from novaclient.tests.v1_1 import test_keypairs -from novaclient.v3 import keypairs - - -class KeypairsTest(test_keypairs.KeypairsTest): - - client_fixture_class = client.V3 - data_fixture_class = data.V3 - - def _get_keypair_type(self): - return keypairs.Keypair - - def _get_keypair_prefix(self): - return keypairs.KeypairManager.keypair_prefix diff --git a/novaclient/tests/v3/test_limits.py b/novaclient/tests/v3/test_limits.py deleted file mode 100644 index 51b575269..000000000 --- a/novaclient/tests/v3/test_limits.py +++ /dev/null @@ -1,88 +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 novaclient.tests.fixture_data import client -from novaclient.tests.fixture_data import limits as data -from novaclient.tests import utils -from novaclient.v3 import limits - - -class LimitsTest(utils.FixturedTestCase): - - client_fixture_class = client.V3 - data_fixture_class = data.Fixture - - def test_get_limits(self): - obj = self.cs.limits.get() - self.assert_called('GET', '/limits') - self.assertIsInstance(obj, limits.Limits) - - def test_get_limits_for_a_tenant(self): - obj = self.cs.limits.get(tenant_id=1234) - self.assert_called('GET', '/limits?tenant_id=1234') - self.assertIsInstance(obj, limits.Limits) - - def test_absolute_limits(self): - obj = self.cs.limits.get() - - expected = ( - limits.AbsoluteLimit("maxTotalRAMSize", 51200), - limits.AbsoluteLimit("maxServerMeta", 5), - limits.AbsoluteLimit("maxImageMeta", 5), - limits.AbsoluteLimit("maxPersonality", 5), - limits.AbsoluteLimit("maxPersonalitySize", 10240), - ) - - abs_limits = list(obj.absolute) - self.assertEqual(len(abs_limits), len(expected)) - - for limit in abs_limits: - self.assertTrue(limit in expected) - - def test_absolute_limits_reserved(self): - obj = self.cs.limits.get(reserved=True) - - expected = ( - limits.AbsoluteLimit("maxTotalRAMSize", 51200), - limits.AbsoluteLimit("maxServerMeta", 5), - limits.AbsoluteLimit("maxImageMeta", 5), - limits.AbsoluteLimit("maxPersonality", 5), - limits.AbsoluteLimit("maxPersonalitySize", 10240), - ) - - self.assert_called('GET', '/limits?reserved=1') - abs_limits = list(obj.absolute) - self.assertEqual(len(abs_limits), len(expected)) - - for limit in abs_limits: - self.assertTrue(limit in expected) - - def test_rate_limits(self): - obj = self.cs.limits.get() - - expected = ( - limits.RateLimit('POST', '*', '.*', 10, 2, 'MINUTE', - '2011-12-15T22:42:45Z'), - limits.RateLimit('PUT', '*', '.*', 10, 2, 'MINUTE', - '2011-12-15T22:42:45Z'), - limits.RateLimit('DELETE', '*', '.*', 100, 100, 'MINUTE', - '2011-12-15T22:42:45Z'), - limits.RateLimit('POST', '*/servers', '^/servers', 25, 24, 'DAY', - '2011-12-15T22:42:45Z'), - ) - - rate_limits = list(obj.rate) - self.assertEqual(len(rate_limits), len(expected)) - - for limit in rate_limits: - self.assertTrue(limit in expected) diff --git a/novaclient/tests/v3/test_list_extensions.py b/novaclient/tests/v3/test_list_extensions.py deleted file mode 100644 index 61d387c3c..000000000 --- a/novaclient/tests/v3/test_list_extensions.py +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright 2014 NEC Corporation. All rights reserved. -# -# 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 novaclient import extension -from novaclient.tests import utils -from novaclient.tests.v3 import fakes -from novaclient.v3 import list_extensions - - -extensions = [ - extension.Extension("list_extensions", list_extensions), -] -cs = fakes.FakeClient(extensions=extensions) - - -class ListExtensionsTests(utils.TestCase): - def test_list_extensions(self): - all_exts = cs.list_extensions.show_all() - cs.assert_called('GET', '/extensions') - self.assertTrue(len(all_exts) > 0) - for r in all_exts: - self.assertTrue(len(r.summary) > 0) diff --git a/novaclient/tests/v3/test_quotas.py b/novaclient/tests/v3/test_quotas.py deleted file mode 100644 index 53ca3258e..000000000 --- a/novaclient/tests/v3/test_quotas.py +++ /dev/null @@ -1,43 +0,0 @@ -# Copyright IBM Corp. 2013 -# -# 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 novaclient.tests.fixture_data import client -from novaclient.tests.fixture_data import quotas as data -from novaclient.tests.v1_1 import test_quotas - - -class QuotaSetsTest(test_quotas.QuotaSetsTest): - - client_fixture_class = client.V3 - data_fixture_class = data.V3 - - def test_force_update_quota(self): - q = self.cs.quotas.get('97f4c221bff44578b0300df4ef119353') - q.update(cores=2, force=True) - self.assert_called( - 'PUT', '/os-quota-sets/97f4c221bff44578b0300df4ef119353', - {'quota_set': {'force': True, - 'cores': 2}}) - - def test_tenant_quotas_get_detail(self): - tenant_id = 'test' - self.cs.quotas.get(tenant_id, detail=True) - self.assert_called('GET', '/os-quota-sets/%s/detail' % tenant_id) - - def test_user_quotas_get_detail(self): - tenant_id = 'test' - user_id = 'fake_user' - self.cs.quotas.get(tenant_id, user_id=user_id, detail=True) - url = '/os-quota-sets/%s/detail?user_id=%s' % (tenant_id, user_id) - self.assert_called('GET', url) diff --git a/novaclient/tests/v3/test_servers.py b/novaclient/tests/v3/test_servers.py deleted file mode 100644 index c9357b565..000000000 --- a/novaclient/tests/v3/test_servers.py +++ /dev/null @@ -1,478 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2013 IBM Corp. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import mock -import six - -from novaclient import exceptions -from novaclient.tests.fixture_data import client -from novaclient.tests.fixture_data import servers as data -from novaclient.tests import utils -from novaclient.v3 import servers - - -class ServersTest(utils.FixturedTestCase): - - client_fixture_class = client.V3 - data_fixture_class = data.V3 - - def test_list_servers(self): - sl = self.cs.servers.list() - self.assert_called('GET', '/servers/detail') - for s in sl: - self.assertIsInstance(s, servers.Server) - - def test_list_servers_undetailed(self): - sl = self.cs.servers.list(detailed=False) - self.assert_called('GET', '/servers') - for s in sl: - self.assertIsInstance(s, servers.Server) - - def test_list_servers_with_marker_limit(self): - sl = self.cs.servers.list(marker=1234, limit=2) - self.assert_called('GET', '/servers/detail?limit=2&marker=1234') - for s in sl: - self.assertIsInstance(s, servers.Server) - - def test_get_server_details(self): - s = self.cs.servers.get(1234) - self.assert_called('GET', '/servers/1234') - self.assertIsInstance(s, servers.Server) - self.assertEqual(1234, s.id) - self.assertEqual('BUILD', s.status) - - def test_get_server_promote_details(self): - s1 = self.cs.servers.list(detailed=False)[0] - s2 = self.cs.servers.list(detailed=True)[0] - self.assertNotEqual(s1._info, s2._info) - s1.get() - self.assertEqual(s1._info, s2._info) - - def test_create_server(self): - s = self.cs.servers.create( - name="My server", - image=1, - flavor=1, - meta={'foo': 'bar'}, - userdata="hello moto", - key_name="fakekey", - files={ - '/etc/passwd': 'some data', # a file - '/tmp/foo.txt': six.StringIO('data'), # a stream - } - ) - self.assert_called('POST', '/servers') - self.assertIsInstance(s, servers.Server) - - def test_create_server_boot_with_nics_ipv4(self): - old_boot = self.cs.servers._boot - nics = [{'net-id': '11111111-1111-1111-1111-111111111111', - 'v4-fixed-ip': '10.10.0.7'}] - - def wrapped_boot(url, key, *boot_args, **boot_kwargs): - self.assertEqual(boot_kwargs['nics'], nics) - return old_boot(url, key, *boot_args, **boot_kwargs) - - with mock.patch.object(self.cs.servers, '_boot', wrapped_boot): - s = self.cs.servers.create( - name="My server", - image=1, - flavor=1, - meta={'foo': 'bar'}, - userdata="hello moto", - key_name="fakekey", - nics=nics - ) - self.assert_called('POST', '/servers') - self.assertIsInstance(s, servers.Server) - - def test_create_server_boot_with_nics_ipv6(self): - old_boot = self.cs.servers._boot - nics = [{'net-id': '11111111-1111-1111-1111-111111111111', - 'v6-fixed-ip': '2001:db9:0:1::10'}] - - def wrapped_boot(url, key, *boot_args, **boot_kwargs): - self.assertEqual(nics, boot_kwargs['nics']) - return old_boot(url, key, *boot_args, **boot_kwargs) - - with mock.patch.object(self.cs.servers, '_boot', wrapped_boot): - s = self.cs.servers.create( - name="My server", - image=1, - flavor=1, - meta={'foo': 'bar'}, - userdata="hello moto", - key_name="fakekey", - nics=nics - ) - self.assert_called('POST', '/servers') - self.assertIsInstance(s, servers.Server) - - def test_create_server_userdata_file_object(self): - s = self.cs.servers.create( - name="My server", - image=1, - flavor=1, - meta={'foo': 'bar'}, - userdata=six.StringIO('hello moto'), - files={ - '/etc/passwd': 'some data', # a file - '/tmp/foo.txt': six.StringIO('data'), # a stream - }, - ) - self.assert_called('POST', '/servers') - self.assertIsInstance(s, servers.Server) - - def test_create_server_userdata_unicode(self): - s = self.cs.servers.create( - name="My server", - image=1, - flavor=1, - meta={'foo': 'bar'}, - userdata=six.u('こんにちは'), - key_name="fakekey", - files={ - '/etc/passwd': 'some data', # a file - '/tmp/foo.txt': six.StringIO('data'), # a stream - }, - ) - self.assert_called('POST', '/servers') - self.assertIsInstance(s, servers.Server) - - def test_create_server_userdata_utf8(self): - s = self.cs.servers.create( - name="My server", - image=1, - flavor=1, - meta={'foo': 'bar'}, - userdata='こんにちは', - key_name="fakekey", - files={ - '/etc/passwd': 'some data', # a file - '/tmp/foo.txt': six.StringIO('data'), # a stream - }, - ) - self.assert_called('POST', '/servers') - self.assertIsInstance(s, servers.Server) - - def test_create_server_return_reservation_id(self): - s = self.cs.servers.create( - name="My server", - image=1, - flavor=1, - reservation_id=True - ) - expected_body = { - 'server': { - 'name': 'My server', - 'image_ref': '1', - 'flavor_ref': '1', - 'os-multiple-create:min_count': 1, - 'os-multiple-create:max_count': 1, - 'os-multiple-create:return_reservation_id': True, - } - } - self.assert_called('POST', '/servers', expected_body) - self.assertIsInstance(s, servers.Server) - - def test_update_server(self): - s = self.cs.servers.get(1234) - - # Update via instance - s.update(name='hi') - self.assert_called('PUT', '/servers/1234') - s.update(name='hi') - self.assert_called('PUT', '/servers/1234') - - # Silly, but not an error - s.update() - - # Update via manager - self.cs.servers.update(s, name='hi') - self.assert_called('PUT', '/servers/1234') - - def test_delete_server(self): - s = self.cs.servers.get(1234) - s.delete() - self.assert_called('DELETE', '/servers/1234') - self.cs.servers.delete(1234) - self.assert_called('DELETE', '/servers/1234') - self.cs.servers.delete(s) - self.assert_called('DELETE', '/servers/1234') - - def test_delete_server_meta(self): - self.cs.servers.delete_meta(1234, ['test_key']) - self.assert_called('DELETE', '/servers/1234/metadata/test_key') - - def test_set_server_meta(self): - self.cs.servers.set_meta(1234, {'test_key': 'test_value'}) - self.assert_called('POST', '/servers/1234/metadata', - {'metadata': {'test_key': 'test_value'}}) - - def test_find(self): - server = self.cs.servers.find(name='sample-server') - self.assert_called('GET', '/servers/1234') - self.assertEqual('sample-server', server.name) - - self.assertRaises(exceptions.NoUniqueMatch, self.cs.servers.find, - flavor={"id": 1, "name": "256 MB Server"}) - - sl = self.cs.servers.findall(flavor={"id": 1, "name": "256 MB Server"}) - self.assertEqual([1234, 5678, 9012], [s.id for s in sl]) - - def test_reboot_server(self): - s = self.cs.servers.get(1234) - s.reboot() - self.assert_called('POST', '/servers/1234/action') - self.cs.servers.reboot(s, reboot_type='HARD') - self.assert_called('POST', '/servers/1234/action') - - def test_rebuild_server(self): - s = self.cs.servers.get(1234) - s.rebuild(image=1) - self.assert_called('POST', '/servers/1234/action') - self.cs.servers.rebuild(s, image=1) - self.assert_called('POST', '/servers/1234/action') - s.rebuild(image=1, password='5678') - self.assert_called('POST', '/servers/1234/action') - self.cs.servers.rebuild(s, image=1, password='5678') - self.assert_called('POST', '/servers/1234/action') - - def test_resize_server(self): - s = self.cs.servers.get(1234) - s.resize(flavor=1) - self.assert_called('POST', '/servers/1234/action') - self.cs.servers.resize(s, flavor=1) - self.assert_called('POST', '/servers/1234/action') - - def test_confirm_resized_server(self): - s = self.cs.servers.get(1234) - s.confirm_resize() - self.assert_called('POST', '/servers/1234/action') - self.cs.servers.confirm_resize(s) - self.assert_called('POST', '/servers/1234/action') - - def test_revert_resized_server(self): - s = self.cs.servers.get(1234) - s.revert_resize() - self.assert_called('POST', '/servers/1234/action') - self.cs.servers.revert_resize(s) - self.assert_called('POST', '/servers/1234/action') - - def test_migrate_server(self): - s = self.cs.servers.get(1234) - s.migrate() - self.assert_called('POST', '/servers/1234/action') - self.cs.servers.migrate(s) - self.assert_called('POST', '/servers/1234/action') - - def test_add_fixed_ip(self): - s = self.cs.servers.get(1234) - s.add_fixed_ip(1) - self.assert_called('POST', '/servers/1234/action') - self.cs.servers.add_fixed_ip(s, 1) - self.assert_called('POST', '/servers/1234/action') - - def test_remove_fixed_ip(self): - s = self.cs.servers.get(1234) - s.remove_fixed_ip('10.0.0.1') - self.assert_called('POST', '/servers/1234/action') - self.cs.servers.remove_fixed_ip(s, '10.0.0.1') - self.assert_called('POST', '/servers/1234/action') - - def test_stop(self): - s = self.cs.servers.get(1234) - s.stop() - self.assert_called('POST', '/servers/1234/action') - self.cs.servers.stop(s) - self.assert_called('POST', '/servers/1234/action') - - def test_force_delete(self): - s = self.cs.servers.get(1234) - s.force_delete() - self.assert_called('POST', '/servers/1234/action') - self.cs.servers.force_delete(s) - self.assert_called('POST', '/servers/1234/action') - - def test_restore(self): - s = self.cs.servers.get(1234) - s.restore() - self.assert_called('POST', '/servers/1234/action') - self.cs.servers.restore(s) - self.assert_called('POST', '/servers/1234/action') - - def test_start(self): - s = self.cs.servers.get(1234) - s.start() - self.assert_called('POST', '/servers/1234/action') - self.cs.servers.start(s) - self.assert_called('POST', '/servers/1234/action') - - def test_rescue(self): - s = self.cs.servers.get(1234) - s.rescue() - self.assert_called('POST', '/servers/1234/action') - self.cs.servers.rescue(s) - self.assert_called('POST', '/servers/1234/action') - - def test_unrescue(self): - s = self.cs.servers.get(1234) - s.unrescue() - self.assert_called('POST', '/servers/1234/action') - self.cs.servers.unrescue(s) - self.assert_called('POST', '/servers/1234/action') - - def test_lock(self): - s = self.cs.servers.get(1234) - s.lock() - self.assert_called('POST', '/servers/1234/action') - self.cs.servers.lock(s) - self.assert_called('POST', '/servers/1234/action') - - def test_unlock(self): - s = self.cs.servers.get(1234) - s.unlock() - self.assert_called('POST', '/servers/1234/action') - self.cs.servers.unlock(s) - self.assert_called('POST', '/servers/1234/action') - - def test_backup(self): - s = self.cs.servers.get(1234) - s.backup('back1', 'daily', 1) - self.assert_called('POST', '/servers/1234/action') - self.cs.servers.backup(s, 'back1', 'daily', 2) - self.assert_called('POST', '/servers/1234/action') - - def test_get_console_output_without_length(self): - success = 'foo' - s = self.cs.servers.get(1234) - s.get_console_output() - self.assertEqual(success, s.get_console_output()) - self.assert_called('POST', '/servers/1234/action') - - self.cs.servers.get_console_output(s) - self.assertEqual(success, self.cs.servers.get_console_output(s)) - self.assert_called('POST', '/servers/1234/action', - {'get_console_output': {'length': -1}}) - - def test_get_console_output_with_length(self): - success = 'foo' - - s = self.cs.servers.get(1234) - s.get_console_output(length=50) - self.assertEqual(success, s.get_console_output(length=50)) - self.assert_called('POST', '/servers/1234/action', - {'get_console_output': {'length': 50}}) - - self.cs.servers.get_console_output(s, length=50) - self.assertEqual(success, - self.cs.servers.get_console_output(s, length=50)) - self.assert_called('POST', '/servers/1234/action', - {'get_console_output': {'length': 50}}) - - def test_get_password(self): - s = self.cs.servers.get(1234) - self.assertEqual('', s.get_password('/foo/id_rsa')) - self.assert_called('GET', '/servers/1234/os-server-password') - - def test_clear_password(self): - s = self.cs.servers.get(1234) - s.clear_password() - self.assert_called('DELETE', '/servers/1234/os-server-password') - - def test_get_server_diagnostics(self): - s = self.cs.servers.get(1234) - diagnostics = s.diagnostics() - self.assertTrue(diagnostics is not None) - self.assert_called('GET', '/servers/1234/os-server-diagnostics') - - diagnostics_from_manager = self.cs.servers.diagnostics(1234) - self.assertTrue(diagnostics_from_manager is not None) - self.assert_called('GET', '/servers/1234/os-server-diagnostics') - - self.assertEqual(diagnostics_from_manager[1], diagnostics[1]) - - def test_get_vnc_console(self): - s = self.cs.servers.get(1234) - s.get_vnc_console('fake') - self.assert_called('POST', '/servers/1234/action') - - self.cs.servers.get_vnc_console(s, 'fake') - self.assert_called('POST', '/servers/1234/action') - - def test_get_spice_console(self): - s = self.cs.servers.get(1234) - s.get_spice_console('fake') - self.assert_called('POST', '/servers/1234/action') - - self.cs.servers.get_spice_console(s, 'fake') - self.assert_called('POST', '/servers/1234/action') - - def test_create_image(self): - s = self.cs.servers.get(1234) - s.create_image('123') - self.assert_called('POST', '/servers/1234/action') - s.create_image('123', {}) - self.assert_called('POST', '/servers/1234/action') - self.cs.servers.create_image(s, '123') - self.assert_called('POST', '/servers/1234/action') - self.cs.servers.create_image(s, '123', {}) - - def test_live_migrate_server(self): - s = self.cs.servers.get(1234) - s.live_migrate(host='hostname', block_migration=False, - disk_over_commit=False) - self.assert_called('POST', '/servers/1234/action') - self.cs.servers.live_migrate(s, host='hostname', block_migration=False, - disk_over_commit=False) - self.assert_called('POST', '/servers/1234/action') - - def test_reset_state(self): - s = self.cs.servers.get(1234) - s.reset_state('newstate') - self.assert_called('POST', '/servers/1234/action') - self.cs.servers.reset_state(s, 'newstate') - self.assert_called('POST', '/servers/1234/action') - - def test_reset_network(self): - s = self.cs.servers.get(1234) - s.reset_network() - self.assert_called('POST', '/servers/1234/action') - self.cs.servers.reset_network(s) - self.assert_called('POST', '/servers/1234/action') - - def test_evacuate(self): - s = self.cs.servers.get(1234) - s.evacuate('fake_target_host', 'True') - self.assert_called('POST', '/servers/1234/action') - self.cs.servers.evacuate(s, 'fake_target_host', - 'False', 'NewAdminPassword') - self.assert_called('POST', '/servers/1234/action') - - def test_interface_list(self): - s = self.cs.servers.get(1234) - s.interface_list() - self.assert_called('GET', '/servers/1234/os-attach-interfaces') - - def test_interface_attach(self): - s = self.cs.servers.get(1234) - s.interface_attach(None, None, None) - self.assert_called('POST', '/servers/1234/os-attach-interfaces') - - def test_interface_detach(self): - s = self.cs.servers.get(1234) - s.interface_detach('port-id') - self.assert_called('DELETE', - '/servers/1234/os-attach-interfaces/port-id') diff --git a/novaclient/tests/v3/test_services.py b/novaclient/tests/v3/test_services.py deleted file mode 100644 index 0fd2fc0cf..000000000 --- a/novaclient/tests/v3/test_services.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright 2012 IBM Corp. -# All Rights Reserved. -# -# 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 novaclient.tests import utils -from novaclient.tests.v3 import fakes -from novaclient.v3 import services - - -class ServicesTest(utils.TestCase): - def setUp(self): - super(ServicesTest, self).setUp() - self.cs = self._get_fake_client() - self.service_type = self._get_service_type() - - def _get_fake_client(self): - return fakes.FakeClient() - - def _get_service_type(self): - return services.Service - - def _update_body(self, host, binary, disabled_reason=None): - body = {"host": host, - "binary": binary} - if disabled_reason is not None: - body["disabled_reason"] = disabled_reason - return body diff --git a/novaclient/tests/v3/test_shell.py b/novaclient/tests/v3/test_shell.py deleted file mode 100644 index 2418aed7c..000000000 --- a/novaclient/tests/v3/test_shell.py +++ /dev/null @@ -1,766 +0,0 @@ -# Copyright 2013 Cloudwatt -# Copyright 2010 Jacob Kaplan-Moss -# Copyright 2011 OpenStack Foundation -# Copyright 2012 IBM Corp. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import base64 -import os - -import fixtures -import mock -import six - -from novaclient import exceptions -import novaclient.shell -from novaclient.tests import utils -from novaclient.tests.v3 import fakes - - -class ShellFixture(fixtures.Fixture): - def setUp(self): - super(ShellFixture, self).setUp() - self.shell = novaclient.shell.OpenStackComputeShell() - - def tearDown(self): - # For some method like test_image_meta_bad_action we are - # testing a SystemExit to be thrown and object self.shell has - # no time to get instantatiated which is OK in this case, so - # we make sure the method is there before launching it. - if hasattr(self.shell, 'cs'): - self.shell.cs.clear_callstack() - super(ShellFixture, self).tearDown() - - -class ShellTest(utils.TestCase): - FAKE_ENV = { - 'NOVA_USERNAME': 'username', - 'NOVA_PASSWORD': 'password', - 'NOVA_PROJECT_ID': 'project_id', - 'OS_COMPUTE_API_VERSION': '3', - 'NOVA_URL': 'http://no.where', - 'OS_AUTH_URL': 'http://no.where/v2.0', - } - - def setUp(self): - """Run before each test.""" - super(ShellTest, self).setUp() - - for var in self.FAKE_ENV: - self.useFixture(fixtures.EnvironmentVariable(var, - self.FAKE_ENV[var])) - self.shell = self.useFixture(ShellFixture()).shell - - self.useFixture(fixtures.MonkeyPatch( - 'novaclient.client.get_client_class', - lambda *_: fakes.FakeClient)) - - @mock.patch('sys.stdout', new_callable=six.StringIO) - def run_command(self, cmd, mock_stdout): - if isinstance(cmd, list): - self.shell.main(cmd) - else: - self.shell.main(cmd.split()) - return mock_stdout.getvalue() - - def assert_called(self, method, url, body=None, **kwargs): - return self.shell.cs.assert_called(method, url, body, **kwargs) - - def assert_called_anytime(self, method, url, body=None): - return self.shell.cs.assert_called_anytime(method, url, body) - - def test_list_deleted(self): - self.run_command('list --deleted') - self.assert_called('GET', '/servers/detail?deleted=True') - - def test_aggregate_list(self): - self.run_command('aggregate-list') - self.assert_called('GET', '/os-aggregates') - - def test_aggregate_create(self): - self.run_command('aggregate-create test_name nova1') - body = {"aggregate": {"name": "test_name", - "availability_zone": "nova1"}} - self.assert_called('POST', '/os-aggregates', body, pos=-2) - self.assert_called('GET', '/os-aggregates/1', pos=-1) - - def test_aggregate_delete_by_id(self): - self.run_command('aggregate-delete 1') - self.assert_called('DELETE', '/os-aggregates/1') - - def test_aggregate_delete_by_name(self): - self.run_command('aggregate-delete test') - self.assert_called('DELETE', '/os-aggregates/1') - - def test_aggregate_update_by_id(self): - self.run_command('aggregate-update 1 new_name') - body = {"aggregate": {"name": "new_name"}} - self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) - self.assert_called('GET', '/os-aggregates/1', pos=-1) - - def test_aggregate_update_by_name(self): - self.run_command('aggregate-update test new_name') - body = {"aggregate": {"name": "new_name"}} - self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) - self.assert_called('GET', '/os-aggregates/1', pos=-1) - - def test_aggregate_update_with_availability_zone_by_id(self): - self.run_command('aggregate-update 1 foo new_zone') - body = {"aggregate": {"name": "foo", "availability_zone": "new_zone"}} - self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) - self.assert_called('GET', '/os-aggregates/1', pos=-1) - - def test_aggregate_update_with_availability_zone_by_name(self): - self.run_command('aggregate-update test foo new_zone') - body = {"aggregate": {"name": "foo", "availability_zone": "new_zone"}} - self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) - self.assert_called('GET', '/os-aggregates/1', pos=-1) - - def test_aggregate_set_metadata_add_by_id(self): - self.run_command('aggregate-set-metadata 3 foo=bar') - body = {"set_metadata": {"metadata": {"foo": "bar"}}} - self.assert_called('POST', '/os-aggregates/3/action', body, pos=-2) - self.assert_called('GET', '/os-aggregates/3', pos=-1) - - def test_aggregate_set_metadata_add_duplicate_by_id(self): - cmd = 'aggregate-set-metadata 3 test=dup' - self.assertRaises(exceptions.CommandError, self.run_command, cmd) - - def test_aggregate_set_metadata_delete_by_id(self): - self.run_command('aggregate-set-metadata 3 none_key') - body = {"set_metadata": {"metadata": {"none_key": None}}} - self.assert_called('POST', '/os-aggregates/3/action', body, pos=-2) - self.assert_called('GET', '/os-aggregates/3', pos=-1) - - def test_aggregate_set_metadata_delete_missing_by_id(self): - cmd = 'aggregate-set-metadata 3 delete_key2' - self.assertRaises(exceptions.CommandError, self.run_command, cmd) - - def test_aggregate_set_metadata_by_name(self): - self.run_command('aggregate-set-metadata test foo=bar') - body = {"set_metadata": {"metadata": {"foo": "bar"}}} - self.assert_called('POST', '/os-aggregates/1/action', body, pos=-2) - self.assert_called('GET', '/os-aggregates/1', pos=-1) - - def test_aggregate_add_host_by_id(self): - self.run_command('aggregate-add-host 1 host1') - body = {"add_host": {"host": "host1"}} - self.assert_called('POST', '/os-aggregates/1/action', body, pos=-2) - self.assert_called('GET', '/os-aggregates/1', pos=-1) - - def test_aggregate_add_host_by_name(self): - self.run_command('aggregate-add-host test host1') - body = {"add_host": {"host": "host1"}} - self.assert_called('POST', '/os-aggregates/1/action', body, pos=-2) - self.assert_called('GET', '/os-aggregates/1', pos=-1) - - def test_aggregate_remove_host_by_id(self): - self.run_command('aggregate-remove-host 1 host1') - body = {"remove_host": {"host": "host1"}} - self.assert_called('POST', '/os-aggregates/1/action', body, pos=-2) - self.assert_called('GET', '/os-aggregates/1', pos=-1) - - def test_aggregate_remove_host_by_name(self): - self.run_command('aggregate-remove-host test host1') - body = {"remove_host": {"host": "host1"}} - self.assert_called('POST', '/os-aggregates/1/action', body, pos=-2) - self.assert_called('GET', '/os-aggregates/1', pos=-1) - - def test_aggregate_details_by_id(self): - self.run_command('aggregate-details 1') - self.assert_called('GET', '/os-aggregates/1') - - def test_aggregate_details_by_name(self): - self.run_command('aggregate-details test') - self.assert_called('GET', '/os-aggregates') - - def test_boot(self): - self.run_command('boot --flavor 1 --image 1 some-server') - self.assert_called_anytime( - 'POST', '/servers', - {'server': { - 'flavor_ref': '1', - 'name': 'some-server', - 'image_ref': '1', - 'os-multiple-create:min_count': 1, - 'os-multiple-create:max_count': 1, - }}, - ) - - def test_boot_image_with(self): - self.run_command("boot --flavor 1" - " --image-with test_key=test_value some-server") - self.assert_called_anytime( - 'POST', '/servers', - {'server': { - 'flavor_ref': '1', - 'name': 'some-server', - 'image_ref': '1', - 'os-multiple-create:min_count': 1, - 'os-multiple-create:max_count': 1, - }}, - ) - - def test_boot_key(self): - self.run_command('boot --flavor 1 --image 1 --key_name 1 some-server') - self.assert_called_anytime( - 'POST', '/servers', - {'server': { - 'flavor_ref': '1', - 'name': 'some-server', - 'image_ref': '1', - 'key_name': '1', - 'os-multiple-create:min_count': 1, - 'os-multiple-create:max_count': 1, - }}, - ) - - def test_boot_user_data(self): - file_text = 'text' - - with mock.patch('novaclient.v3.shell.open', create=True) as mock_open: - mock_open.return_value = file_text - testfile = 'some_dir/some_file.txt' - - self.run_command('boot --flavor 1 --image 1 --user_data %s ' - 'some-server' % testfile) - - mock_open.assert_called_once_with(testfile) - - user_data = base64.b64encode(file_text.encode('utf-8')).decode('utf-8') - self.assert_called_anytime( - 'POST', '/servers', - {'server': { - 'flavor_ref': '1', - 'name': 'some-server', - 'image_ref': '1', - 'os-multiple-create:min_count': 1, - 'os-multiple-create:max_count': 1, - 'user_data': user_data}}, - ) - - def test_boot_avzone(self): - self.run_command( - 'boot --flavor 1 --image 1 --availability-zone avzone ' - 'some-server') - self.assert_called_anytime( - 'POST', '/servers', - {'server': { - 'flavor_ref': '1', - 'name': 'some-server', - 'image_ref': '1', - 'os-availability-zone:availability_zone': 'avzone', - 'os-multiple-create:min_count': 1, - 'os-multiple-create:max_count': 1 - }}, - ) - - def test_boot_secgroup(self): - self.run_command( - 'boot --flavor 1 --image 1 --security-groups secgroup1,' - 'secgroup2 some-server') - self.assert_called_anytime( - 'POST', '/servers', - {'server': { - 'security_groups': [{'name': 'secgroup1'}, - {'name': 'secgroup2'}], - 'flavor_ref': '1', - 'name': 'some-server', - 'image_ref': '1', - 'os-multiple-create:min_count': 1, - 'os-multiple-create:max_count': 1, - }}, - ) - - def test_boot_config_drive(self): - self.run_command( - 'boot --flavor 1 --image 1 --config-drive 1 some-server') - self.assert_called_anytime( - 'POST', '/servers', - {'server': { - 'flavor_ref': '1', - 'name': 'some-server', - 'image_ref': '1', - 'os-multiple-create:min_count': 1, - 'os-multiple-create:max_count': 1, - 'os-config-drive:config_drive': True - }}, - ) - - def test_boot_config_drive_custom(self): - self.run_command( - 'boot --flavor 1 --image 1 --config-drive /dev/hda some-server') - self.assert_called_anytime( - 'POST', '/servers', - {'server': { - 'flavor_ref': '1', - 'name': 'some-server', - 'image_ref': '1', - 'os-multiple-create:min_count': 1, - 'os-multiple-create:max_count': 1, - 'os-config-drive:config_drive': '/dev/hda' - }}, - ) - - def test_boot_invalid_user_data(self): - invalid_file = os.path.join(os.path.dirname(__file__), - 'no_such_file') - cmd = ('boot some-server --flavor 1 --image 1' - ' --user_data %s' % invalid_file) - self.assertRaises(exceptions.CommandError, self.run_command, cmd) - - def test_boot_no_image_no_bdms(self): - cmd = 'boot --flavor 1 some-server' - self.assertRaises(exceptions.CommandError, self.run_command, cmd) - - def test_boot_no_flavor(self): - cmd = 'boot --image 1 some-server' - self.assertRaises(exceptions.CommandError, self.run_command, cmd) - - def test_boot_no_image_bdms(self): - self.run_command( - 'boot --flavor 1 --block_device_mapping vda=blah:::0 some-server' - ) - self.assert_called_anytime( - 'POST', '/servers', - {'server': { - 'flavor_ref': '1', - 'name': 'some-server', - 'block_device_mapping': [ - { - 'volume_id': 'blah', - 'delete_on_termination': '0', - 'device_name': 'vda', - 'boot_index': 0, - 'uuid': 'blah', - 'source_type': '' - } - ], - 'image_ref': '', - 'os-multiple-create:min_count': 1, - 'os-multiple-create:max_count': 1, - }}, - ) - - def test_boot_image_bdms(self): - self.run_command( - 'boot --flavor 1 --image 1 --block-device id=fake-id,' - 'source=volume,dest=volume,device=vda,size=1,format=ext4,' - 'type=disk,shutdown=preserve some-server' - ) - id = ('fake-id,source=volume,dest=volume,device=vda,size=1,' - 'format=ext4,type=disk,shutdown=preserve') - self.assert_called_anytime( - 'POST', '/servers', - {'server': { - 'flavor_ref': '1', - 'name': 'some-server', - 'block_device_mapping': [ - {'device_name': 'id', 'volume_id': id, - 'source_type': 'volume', 'boot_index': 0, 'uuid': id}], - 'image_ref': '1', - 'os-multiple-create:min_count': 1, - 'os-multiple-create:max_count': 1, - }}, - ) - - def test_boot_metadata(self): - self.run_command('boot --image 1 --flavor 1 --meta foo=bar=pants' - ' --meta spam=eggs some-server ') - self.assert_called_anytime( - 'POST', '/servers', - {'server': { - 'flavor_ref': '1', - 'name': 'some-server', - 'image_ref': '1', - 'metadata': {'foo': 'bar=pants', 'spam': 'eggs'}, - 'os-multiple-create:min_count': 1, - 'os-multiple-create:max_count': 1, - }}, - ) - - def test_boot_hints(self): - self.run_command('boot --image 1 --flavor 1 ' - '--hint a=b1=c1 --hint a2=b2=c2 --hint a=b0=c0 ' - 'some-server') - self.assert_called_anytime( - 'POST', '/servers', - { - 'server': { - 'flavor_ref': '1', - 'name': 'some-server', - 'image_ref': '1', - 'os-multiple-create:min_count': 1, - 'os-multiple-create:max_count': 1, - 'os-scheduler-hints:scheduler_hints': { - 'a': ['b1=c1', 'b0=c0'], 'a2': 'b2=c2'}, - }, - }, - ) - - def test_boot_nics(self): - cmd = ('boot --image 1 --flavor 1 ' - '--nic net-id=a=c,v4-fixed-ip=10.0.0.1 some-server') - self.run_command(cmd) - self.assert_called_anytime( - 'POST', '/servers', - { - 'server': { - 'flavor_ref': '1', - 'name': 'some-server', - 'image_ref': '1', - 'os-multiple-create:min_count': 1, - 'os-multiple-create:max_count': 1, - 'networks': [ - {'uuid': 'a=c', 'fixed_ip': '10.0.0.1'}, - ], - }, - }, - ) - - def test_boot_nics_ipv6(self): - cmd = ('boot --image 1 --flavor 1 ' - '--nic net-id=a=c,v6-fixed-ip=2001:db9:0:1::10 some-server') - self.run_command(cmd) - self.assert_called_anytime( - 'POST', '/servers', - { - 'server': { - 'flavor_ref': '1', - 'name': 'some-server', - 'image_ref': '1', - 'os-multiple-create:min_count': 1, - 'os-multiple-create:max_count': 1, - 'networks': [ - {'uuid': 'a=c', 'fixed_ip': '2001:db9:0:1::10'}, - ], - }, - }, - ) - - def test_boot_nics_both_ipv4_and_ipv6(self): - cmd = ('boot --image 1 --flavor 1 ' - '--nic net-id=a=c,v4-fixed-ip=10.0.0.1,' - 'v6-fixed-ip=2001:db9:0:1::10 some-server') - self.assertRaises(exceptions.CommandError, self.run_command, cmd) - - def test_boot_nics_no_value(self): - cmd = ('boot --image 1 --flavor 1 ' - '--nic net-id some-server') - self.assertRaises(exceptions.CommandError, self.run_command, cmd) - - def test_boot_nics_random_key(self): - cmd = ('boot --image 1 --flavor 1 ' - '--nic net-id=a=c,v4-fixed-ip=10.0.0.1,foo=bar some-server') - self.assertRaises(exceptions.CommandError, self.run_command, cmd) - - def test_boot_nics_no_netid_or_portid(self): - cmd = ('boot --image 1 --flavor 1 ' - '--nic v4-fixed-ip=10.0.0.1 some-server') - self.assertRaises(exceptions.CommandError, self.run_command, cmd) - - def test_boot_nics_netid_and_portid(self): - cmd = ('boot --image 1 --flavor 1 ' - '--nic port-id=some=port,net-id=some=net some-server') - self.assertRaises(exceptions.CommandError, self.run_command, cmd) - - def test_boot_num_instances(self): - self.run_command('boot --image 1 --flavor 1 --num-instances 3 server') - self.assert_called_anytime( - 'POST', '/servers', - { - 'server': { - 'flavor_ref': '1', - 'name': 'server', - 'image_ref': '1', - 'os-multiple-create:min_count': 1, - 'os-multiple-create:max_count': 3, - } - }) - - def test_boot_invalid_num_instances(self): - cmd = 'boot --image 1 --flavor 1 --num-instances 0 server' - self.assertRaises(exceptions.CommandError, self.run_command, cmd) - - def test_boot_num_instances_and_count(self): - cmd = 'boot --image 1 --flavor 1 --num-instances 3 --min-count 3 serv' - self.assertRaises(exceptions.CommandError, self.run_command, cmd) - cmd = 'boot --image 1 --flavor 1 --num-instances 3 --max-count 3 serv' - self.assertRaises(exceptions.CommandError, self.run_command, cmd) - - def test_boot_min_max_count(self): - self.run_command('boot --image 1 --flavor 1 --max-count 3 server') - self.assert_called_anytime( - 'POST', '/servers', - { - 'server': { - 'flavor_ref': '1', - 'name': 'server', - 'image_ref': '1', - 'os-multiple-create:min_count': 1, - 'os-multiple-create:max_count': 3, - } - }) - self.run_command('boot --image 1 --flavor 1 --min-count 3 server') - self.assert_called_anytime( - 'POST', '/servers', - { - 'server': { - 'flavor_ref': '1', - 'name': 'server', - 'image_ref': '1', - 'os-multiple-create:min_count': 3, - 'os-multiple-create:max_count': 3, - } - }) - self.run_command('boot --image 1 --flavor 1 ' - '--min-count 3 --max-count 3 server') - self.assert_called_anytime( - 'POST', '/servers', - { - 'server': { - 'flavor_ref': '1', - 'name': 'server', - 'image_ref': '1', - 'os-multiple-create:min_count': 3, - 'os-multiple-create:max_count': 3, - } - }) - self.run_command('boot --image 1 --flavor 1 ' - '--min-count 3 --max-count 5 server') - self.assert_called_anytime( - 'POST', '/servers', - { - 'server': { - 'flavor_ref': '1', - 'name': 'server', - 'image_ref': '1', - 'os-multiple-create:min_count': 3, - 'os-multiple-create:max_count': 5, - } - }) - cmd = 'boot --image 1 --flavor 1 --min-count 3 --max-count 1 serv' - self.assertRaises(exceptions.CommandError, self.run_command, cmd) - - @mock.patch('novaclient.v3.shell._poll_for_status') - def test_boot_with_poll(self, poll_method): - self.run_command('boot --flavor 1 --image 1 some-server --poll') - self.assert_called_anytime( - 'POST', '/servers', - {'server': { - 'flavor_ref': '1', - 'name': 'some-server', - 'image_ref': '1', - 'os-multiple-create:min_count': 1, - 'os-multiple-create:max_count': 1, - }}, - ) - self.assertEqual(1, poll_method.call_count) - poll_method.assert_has_calls( - [mock.call(self.shell.cs.servers.get, 1234, 'building', - ['active'])]) - - def test_boot_with_poll_to_check_VM_state_error(self): - self.assertRaises(exceptions.InstanceInErrorState, self.run_command, - 'boot --flavor 1 --image 1 some-bad-server --poll') - - def test_evacuate(self): - self.run_command('evacuate sample-server new_host') - self.assert_called('POST', '/servers/1234/action', - {'evacuate': {'host': 'new_host', - 'on_shared_storage': False}}) - self.run_command('evacuate sample-server new_host ' - '--password NewAdminPass') - self.assert_called('POST', '/servers/1234/action', - {'evacuate': {'host': 'new_host', - 'on_shared_storage': False, - 'admin_password': 'NewAdminPass'}}) - self.run_command('evacuate sample-server new_host') - self.assert_called('POST', '/servers/1234/action', - {'evacuate': {'host': 'new_host', - 'on_shared_storage': False}}) - self.run_command('evacuate sample-server new_host ' - '--on-shared-storage') - self.assert_called('POST', '/servers/1234/action', - {'evacuate': {'host': 'new_host', - 'on_shared_storage': True}}) - - def test_evacuate_with_no_target_host(self): - self.run_command('evacuate sample-server') - self.assert_called('POST', '/servers/1234/action', - {'evacuate': {'on_shared_storage': False}}) - self.run_command('evacuate sample-server --password NewAdminPass') - self.assert_called('POST', '/servers/1234/action', - {'evacuate': {'on_shared_storage': False, - 'admin_password': 'NewAdminPass'}}) - self.run_command('evacuate sample-server --on-shared-storage') - self.assert_called('POST', '/servers/1234/action', - {'evacuate': {'on_shared_storage': True}}) - - def test_boot_named_flavor(self): - self.run_command(["boot", "--image", "1", - "--flavor", "512 MB Server", - "--max-count", "3", "server"]) - self.assert_called('GET', '/flavors/512 MB Server', pos=0) - self.assert_called('GET', '/flavors?is_public=None', pos=1) - self.assert_called('GET', '/flavors?is_public=None', pos=2) - self.assert_called('GET', '/flavors/2', pos=3) - self.assert_called( - 'POST', '/servers', - { - 'server': { - 'flavor_ref': '2', - 'name': 'server', - 'image_ref': '1', - 'os-multiple-create:min_count': 1, - 'os-multiple-create:max_count': 3, - } - }, pos=4) - - def test_flavor_show_by_name(self): - self.run_command(['flavor-show', '128 MB Server']) - self.assert_called('GET', '/flavors/128 MB Server', pos=0) - self.assert_called('GET', '/flavors?is_public=None', pos=1) - self.assert_called('GET', '/flavors?is_public=None', pos=2) - self.assert_called('GET', '/flavors/aa1', pos=3) - self.assert_called('GET', '/flavors/aa1/flavor-extra-specs', pos=4) - - def test_flavor_show_by_name_priv(self): - self.run_command(['flavor-show', '512 MB Server']) - self.assert_called('GET', '/flavors/512 MB Server', pos=0) - self.assert_called('GET', '/flavors?is_public=None', pos=1) - self.assert_called('GET', '/flavors?is_public=None', pos=2) - self.assert_called('GET', '/flavors/2', pos=3) - self.assert_called('GET', '/flavors/2/flavor-extra-specs', pos=4) - - def test_host_evacuate_live_with_no_target_host(self): - self.run_command('host-evacuate-live hyper1') - self.assert_called('GET', '/os-hypervisors/search?query=hyper1', pos=0) - self.assert_called('GET', '/os-hypervisors/1234/servers', pos=1) - body = {'migrate_live': {'host': None, - 'block_migration': False, - 'disk_over_commit': False}} - self.assert_called('POST', '/servers/uuid1/action', body, pos=2) - self.assert_called('POST', '/servers/uuid2/action', body, pos=3) - - def test_host_evacuate_live_with_target_host(self): - self.run_command('host-evacuate-live hyper1 ' - '--target-host hostname') - self.assert_called('GET', '/os-hypervisors/search?query=hyper1', pos=0) - self.assert_called('GET', '/os-hypervisors/1234/servers', pos=1) - body = {'migrate_live': {'host': 'hostname', - 'block_migration': False, - 'disk_over_commit': False}} - self.assert_called('POST', '/servers/uuid1/action', body, pos=2) - self.assert_called('POST', '/servers/uuid2/action', body, pos=3) - - def test_host_evacuate_live_with_block_migration(self): - self.run_command('host-evacuate-live --block-migrate hyper1') - self.assert_called('GET', '/os-hypervisors/search?query=hyper1', pos=0) - self.assert_called('GET', '/os-hypervisors/1234/servers', pos=1) - body = {'migrate_live': {'host': None, - 'block_migration': True, - 'disk_over_commit': False}} - self.assert_called('POST', '/servers/uuid1/action', body, pos=2) - self.assert_called('POST', '/servers/uuid2/action', body, pos=3) - - def test_host_evacuate_live_with_disk_over_commit(self): - self.run_command('host-evacuate-live --disk-over-commit hyper1') - self.assert_called('GET', '/os-hypervisors/search?query=hyper1', pos=0) - self.assert_called('GET', '/os-hypervisors/1234/servers', pos=1) - body = {'migrate_live': {'host': None, - 'block_migration': False, - 'disk_over_commit': True}} - self.assert_called('POST', '/servers/uuid1/action', body, pos=2) - self.assert_called('POST', '/servers/uuid2/action', body, pos=3) - - def test_delete(self): - self.run_command('delete 1234') - self.assert_called('DELETE', '/servers/1234') - self.run_command('delete sample-server') - self.assert_called('DELETE', '/servers/1234') - - def test_delete_two_with_two_existent(self): - self.run_command('delete 1234 5678') - self.assert_called('DELETE', '/servers/1234', pos=-3) - self.assert_called('DELETE', '/servers/5678', pos=-1) - self.run_command('delete sample-server sample-server2') - self.assert_called('GET', '/servers?name=sample-server', pos=-6) - self.assert_called('GET', '/servers/1234', pos=-5) - self.assert_called('DELETE', '/servers/1234', pos=-4) - self.assert_called('GET', '/servers?name=sample-server2', pos=-3) - self.assert_called('GET', '/servers/5678', pos=-2) - self.assert_called('DELETE', '/servers/5678', pos=-1) - - def test_delete_two_with_one_nonexistent(self): - cmd = 'delete 1234 123456789' - self.assertRaises(exceptions.CommandError, self.run_command, cmd) - self.assert_called_anytime('DELETE', '/servers/1234') - cmd = 'delete sample-server nonexistentserver' - self.assertRaises(exceptions.CommandError, self.run_command, cmd) - self.assert_called_anytime('DELETE', '/servers/1234') - - def test_delete_one_with_one_nonexistent(self): - cmd = 'delete 123456789' - self.assertRaises(exceptions.CommandError, self.run_command, cmd) - cmd = 'delete nonexistent-server1' - self.assertRaises(exceptions.CommandError, self.run_command, cmd) - - def test_delete_two_with_two_nonexistent(self): - cmd = 'delete 123456789 987654321' - self.assertRaises(exceptions.CommandError, self.run_command, cmd) - cmd = 'delete nonexistent-server1 nonexistent-server2' - self.assertRaises(exceptions.CommandError, self.run_command, cmd) - - -class GetFirstEndpointTest(utils.TestCase): - def test_only_one_endpoint(self): - """If there is only one endpoint, it is returned.""" - endpoint = {"url": "test"} - result = novaclient.v3.shell._get_first_endpoint([endpoint], "XYZ") - self.assertEqual(endpoint, result) - - def test_multiple_endpoints(self): - """If there are multiple endpoints, the first one of the appropriate - region is returned. - - """ - endpoints = [ - {"region": "XYZ"}, - {"region": "ORD", "number": 1}, - {"region": "ORD", "number": 2} - ] - result = novaclient.v3.shell._get_first_endpoint(endpoints, "ORD") - self.assertEqual(endpoints[1], result) - - def test_multiple_endpoints_but_none_suitable(self): - """If there are multiple endpoints but none of them are suitable, an - exception is raised. - - """ - endpoints = [ - {"region": "XYZ"}, - {"region": "PQR"}, - {"region": "STU"} - ] - self.assertRaises(LookupError, - novaclient.v3.shell._get_first_endpoint, - endpoints, "ORD") - - def test_no_endpoints(self): - """If there are no endpoints available, an exception is raised.""" - self.assertRaises(LookupError, - novaclient.v3.shell._get_first_endpoint, - [], "ORD") diff --git a/novaclient/tests/v3/test_usage.py b/novaclient/tests/v3/test_usage.py deleted file mode 100644 index b4ffab79e..000000000 --- a/novaclient/tests/v3/test_usage.py +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright 2013 IBM Corp. -# -# 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 novaclient.tests.v1_1 import fakes -from novaclient.tests.v1_1 import test_usage -from novaclient.v3 import usage - - -class UsageTest(test_usage.UsageTest): - def setUp(self): - super(UsageTest, self).setUp() - self.cs = self._get_fake_client() - self.usage_type = self._get_usage_type() - - def _get_fake_client(self): - return fakes.FakeClient() - - def _get_usage_type(self): - return usage.Usage diff --git a/novaclient/tests/v3/test_volumes.py b/novaclient/tests/v3/test_volumes.py deleted file mode 100644 index cfe6d9eb3..000000000 --- a/novaclient/tests/v3/test_volumes.py +++ /dev/null @@ -1,64 +0,0 @@ -# Copyright 2013 IBM Corp. -# All Rights Reserved. -# -# 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 novaclient.tests import utils -from novaclient.tests.v3 import fakes - - -class VolumesTest(utils.TestCase): - def setUp(self): - super(VolumesTest, self).setUp() - self.cs = self._get_fake_client() - - def _get_fake_client(self): - return fakes.FakeClient() - - def test_attach_server_volume(self): - self.cs.volumes.attach_server_volume( - server=1234, - volume_id='15e59938-07d5-11e1-90e3-e3dffe0c5983', - device='/dev/vdb' - ) - self.cs.assert_called('POST', '/servers/1234/action') - - def test_attach_server_volume_disk_bus_device_type(self): - volume_id = '15e59938-07d5-11e1-90e3-e3dffe0c5983' - device = '/dev/vdb' - disk_bus = 'ide' - device_type = 'cdrom' - self.cs.volumes.attach_server_volume(server=1234, - volume_id=volume_id, - device=device, - disk_bus=disk_bus, - device_type=device_type) - body_params = {'volume_id': volume_id, - 'device': device, - 'disk_bus': disk_bus, - 'device_type': device_type} - body = {'attach': body_params} - self.cs.assert_called('POST', '/servers/1234/action', body) - - def test_update_server_volume(self): - vol_id = '15e59938-07d5-11e1-90e3-e3dffe0c5983' - self.cs.volumes.update_server_volume( - server=1234, - old_volume_id='Work', - new_volume_id=vol_id - ) - self.cs.assert_called('POST', '/servers/1234/action') - - def test_delete_server_volume(self): - self.cs.volumes.delete_server_volume(1234, 'Work') - self.cs.assert_called('POST', '/servers/1234/action') diff --git a/novaclient/v3/__init__.py b/novaclient/v3/__init__.py deleted file mode 100644 index a442be109..000000000 --- a/novaclient/v3/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright (c) 2012 OpenStack Foundation -# -# All Rights Reserved. -# -# 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 novaclient.v3.client import Client # noqa diff --git a/novaclient/v3/agents.py b/novaclient/v3/agents.py deleted file mode 100644 index dacaf7671..000000000 --- a/novaclient/v3/agents.py +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright 2012 IBM Corp. -# All Rights Reserved. -# -# 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. - -""" -agent interface -""" - -from novaclient.v1_1 import agents - - -class Agent(agents.Agent): - pass - - -class AgentsManager(agents.AgentsManager): - resource_class = Agent - - def _build_update_body(self, version, url, md5hash): - return {'agent': { - 'version': version, - 'url': url, - 'md5hash': md5hash}} diff --git a/novaclient/v3/aggregates.py b/novaclient/v3/aggregates.py deleted file mode 100644 index b2728d6d1..000000000 --- a/novaclient/v3/aggregates.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright 2012 OpenStack Foundation -# All Rights Reserved. -# -# 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. - -"""Aggregate interface.""" - -from novaclient.v1_1 import aggregates - - -class Aggregate(aggregates.Aggregate): - pass - - -class AggregateManager(aggregates.AggregateManager): - resource_class = Aggregate diff --git a/novaclient/v3/availability_zones.py b/novaclient/v3/availability_zones.py deleted file mode 100644 index bd2a9d239..000000000 --- a/novaclient/v3/availability_zones.py +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright 2011 OpenStack Foundation -# Copyright 2013 IBM Corp. -# All Rights Reserved. -# -# 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. - -""" -Availability Zone interface. -""" - -from novaclient.v1_1 import availability_zones - - -class AvailabilityZone(availability_zones.AvailabilityZone): - pass - - -class AvailabilityZoneManager(availability_zones.AvailabilityZoneManager): - """ - Manage :class:`AvailabilityZone` resources. - """ - resource_class = AvailabilityZone - return_parameter_name = 'availability_zone_info' diff --git a/novaclient/v3/certs.py b/novaclient/v3/certs.py deleted file mode 100644 index 5e6785108..000000000 --- a/novaclient/v3/certs.py +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright 2010 Jacob Kaplan-Moss - -# Copyright 2011 OpenStack Foundation -# All Rights Reserved. -# -# 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. - -""" -Certificate interface. -""" - -from novaclient.v1_1 import certs - - -class Certificate(certs.Certificate): - pass - - -class CertificateManager(certs.CertificateManager): - pass diff --git a/novaclient/v3/client.py b/novaclient/v3/client.py deleted file mode 100644 index 21663ebc5..000000000 --- a/novaclient/v3/client.py +++ /dev/null @@ -1,196 +0,0 @@ -# Copyright 2012 OpenStack Foundation -# Copyright 2013 IBM Corp. -# -# 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 novaclient import client -from novaclient.v3 import agents -from novaclient.v3 import aggregates -from novaclient.v3 import availability_zones -from novaclient.v3 import certs -from novaclient.v3 import flavor_access -from novaclient.v3 import flavors -from novaclient.v3 import hosts -from novaclient.v3 import hypervisors -from novaclient.v3 import images -from novaclient.v3 import keypairs -from novaclient.v3 import limits -from novaclient.v3 import list_extensions -from novaclient.v3 import quotas -from novaclient.v3 import servers -from novaclient.v3 import services -from novaclient.v3 import usage -from novaclient.v3 import volumes - - -class Client(object): - """ - Top-level object to access the OpenStack Compute API. - - Create an instance with your creds:: - - >>> client = Client(USERNAME, PASSWORD, PROJECT_ID, AUTH_URL) - - Or, alternatively, you can create a client instance using the - keystoneclient.session API:: - - >>> from keystoneclient.auth.identity import v2 - >>> from keystoneclient import session - >>> from novaclient.client import Client - >>> auth = v2.Password(auth_url=AUTH_URL, - username=USERNAME, - password=PASSWORD, - tenant_name=PROJECT_ID) - >>> sess = session.Session(auth=auth) - >>> nova = client.Client(VERSION, session=sess) - - Then call methods on its managers:: - - >>> client.servers.list() - ... - >>> client.flavors.list() - ... - - It is also possible to use an instance as a context manager in which - case there will be a session kept alive for the duration of the with - statement:: - - >>> with Client(USERNAME, PASSWORD, PROJECT_ID, AUTH_URL) as client: - ... client.servers.list() - ... client.flavors.list() - ... - - It is also possible to have a permanent (process-long) connection pool, - by passing a connection_pool=True:: - - >>> client = Client(USERNAME, PASSWORD, PROJECT_ID, - ... AUTH_URL, connection_pool=True) - """ - - def __init__(self, username=None, password=None, project_id=None, - auth_url=None, insecure=False, timeout=None, - proxy_tenant_id=None, proxy_token=None, region_name=None, - endpoint_type='publicURL', extensions=None, - service_type='computev3', service_name=None, - volume_service_name=None, timings=False, bypass_url=None, - os_cache=False, no_cache=True, http_log_debug=False, - auth_system='keystone', auth_plugin=None, auth_token=None, - cacert=None, tenant_id=None, user_id=None, - connection_pool=False, session=None, auth=None, - completion_cache=None): - # NOTE(cyeoh): In the novaclient context (unlike Nova) the - # project_id is not the same as the tenant_id. Here project_id - # is a name (what the Nova API often refers to as a project or - # tenant name) and tenant_id is a UUID (what the Nova API - # often refers to as a project_id or tenant_id). - - self.projectid = project_id - self.tenant_id = tenant_id - self.user_id = user_id - self.os_cache = os_cache or not no_cache - # TODO(bnemec): Add back in v3 extensions - self.agents = agents.AgentsManager(self) - self.aggregates = aggregates.AggregateManager(self) - self.availability_zones = \ - availability_zones.AvailabilityZoneManager(self) - self.certs = certs.CertificateManager(self) - self.list_extensions = list_extensions.ListExtManager(self) - self.hosts = hosts.HostManager(self) - self.flavors = flavors.FlavorManager(self) - self.flavor_access = flavor_access.FlavorAccessManager(self) - self.hypervisors = hypervisors.HypervisorManager(self) - self.images = images.ImageManager(self) - self.keypairs = keypairs.KeypairManager(self) - self.limits = limits.LimitsManager(self) - self.quotas = quotas.QuotaSetManager(self) - self.servers = servers.ServerManager(self) - self.services = services.ServiceManager(self) - self.usage = usage.UsageManager(self) - self.volumes = volumes.VolumeManager(self) - - # Add in any extensions... - if extensions: - for extension in extensions: - if extension.manager_class: - setattr(self, extension.name, - extension.manager_class(self)) - - self.client = client._construct_http_client( - username=username, - password=password, - user_id=user_id, - project_id=project_id, - tenant_id=tenant_id, - auth_url=auth_url, - auth_token=auth_token, - insecure=insecure, - timeout=timeout, - auth_system=auth_system, - auth_plugin=auth_plugin, - proxy_token=proxy_token, - proxy_tenant_id=proxy_tenant_id, - region_name=region_name, - endpoint_type=endpoint_type, - service_type=service_type, - service_name=service_name, - volume_service_name=volume_service_name, - timings=timings, - bypass_url=bypass_url, - os_cache=self.os_cache, - http_log_debug=http_log_debug, - cacert=cacert, - connection_pool=connection_pool, - session=session, - auth=auth) - - self.completion_cache = completion_cache - - def write_object_to_completion_cache(self, obj): - if self.completion_cache: - self.completion_cache.write_object(obj) - - def clear_completion_cache_for_class(self, obj_class): - if self.completion_cache: - self.completion_cache.clear_class(obj_class) - - @client._original_only - def __enter__(self): - self.client.open_session() - return self - - @client._original_only - def __exit__(self, t, v, tb): - self.client.close_session() - - @client._original_only - def set_management_url(self, url): - self.client.set_management_url(url) - - def get_timings(self): - return self.client.get_timings() - - def reset_timings(self): - self.client.reset_timings() - - @client._original_only - def authenticate(self): - """ - Authenticate against the server. - - Normally this is called automatically when you first access the API, - but you can call this method to force authentication right now. - - Returns on success; raises :exc:`exceptions.Unauthorized` if the - credentials are wrong. - """ - self.client.authenticate() diff --git a/novaclient/v3/flavor_access.py b/novaclient/v3/flavor_access.py deleted file mode 100644 index 6896dac36..000000000 --- a/novaclient/v3/flavor_access.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright 2012 OpenStack Foundation -# Copyright 2013 IBM Corp. -# All Rights Reserved. -# -# 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. - -"""Flavor access interface.""" - -from novaclient import base -from novaclient.v1_1 import flavor_access - - -class FlavorAccess(flavor_access.FlavorAccess): - pass - - -class FlavorAccessManager(flavor_access.FlavorAccessManager): - """ - Manage :class:`FlavorAccess` resources. - """ - resource_class = FlavorAccess - - def _list_by_flavor(self, flavor): - return self._list('/flavors/%s/flavor-access' % base.getid(flavor), - 'flavor_access') - - def add_tenant_access(self, flavor, tenant): - """Add a tenant to the given flavor access list.""" - info = {'tenant_id': tenant} - return self._action('add_tenant_access', flavor, info) - - def remove_tenant_access(self, flavor, tenant): - """Remove a tenant from the given flavor access list.""" - info = {'tenant_id': tenant} - return self._action('remove_tenant_access', flavor, info) diff --git a/novaclient/v3/flavors.py b/novaclient/v3/flavors.py deleted file mode 100644 index 19f4a3f28..000000000 --- a/novaclient/v3/flavors.py +++ /dev/null @@ -1,100 +0,0 @@ -# Copyright 2010 Jacob Kaplan-Moss -# Copyright 2013 IBM Corp. -# -# 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. - -""" -Flavor interface. -""" - -from novaclient import base -from novaclient import utils -from novaclient.v1_1 import flavors - - -class Flavor(base.Resource): - """ - A flavor is an available hardware configuration for a server. - """ - HUMAN_ID = True - - def __repr__(self): - return "" % self.name - - @property - def is_public(self): - """ - Provide a user-friendly accessor to flavor-access:is_public - """ - return self._info.get("flavor-access:is_public", 'N/A') - - def get_keys(self): - """ - Get extra specs from a flavor. - - :param flavor: The :class:`Flavor` to get extra specs from - """ - _resp, body = self.manager.api.client.get( - "/flavors/%s/flavor-extra-specs" % base.getid(self)) - return body["extra_specs"] - - def set_keys(self, metadata): - """ - Set extra specs on a flavor. - - :param flavor: The :class:`Flavor` to set extra spec on - :param metadata: A dict of key/value pairs to be set - """ - utils.validate_flavor_metadata_keys(metadata.keys()) - - body = {'extra_specs': metadata} - return self.manager._create( - "/flavors/%s/flavor-extra-specs" % base.getid(self), body, - "extra_specs", return_raw=True) - - def unset_keys(self, keys): - """ - Unset extra specs on a flavor. - - :param flavor: The :class:`Flavor` to unset extra spec on - :param keys: A list of keys to be unset - """ - for k in keys: - self.manager._delete( - "/flavors/%s/flavor-extra-specs/%s" % (base.getid(self), k)) - - def delete(self): - """ - Delete this flavor. - """ - self.manager.delete(self) - - -class FlavorManager(flavors.FlavorManager): - resource_class = Flavor - - def _build_body(self, name, ram, vcpus, disk, id, swap, - ephemeral, rxtx_factor, is_public): - return { - "flavor": { - "name": name, - "ram": ram, - "vcpus": vcpus, - "disk": disk, - "id": id, - "swap": swap, - "ephemeral": ephemeral, - "os-flavor-rxtx:rxtx_factor": rxtx_factor, - "flavor-access:is_public": is_public, - } - } diff --git a/novaclient/v3/hosts.py b/novaclient/v3/hosts.py deleted file mode 100644 index 174b62334..000000000 --- a/novaclient/v3/hosts.py +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright 2013 OpenStack Foundation -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" V3 API versions of the Hosts interface. - -Inherits from the 1.1 code because a lot of the functionality is shared. -""" - -from novaclient.v1_1 import hosts - - -Host = hosts.Host - - -class HostManager(hosts.HostManager): - def update(self, host, values): - """Update status or maintenance mode for the host.""" - body = dict(host=values) - return self._update("/os-hosts/%s" % host, body, response_key='host') - - def host_action(self, host, action): - """Perform an action on a host.""" - url = '/os-hosts/{0}/{1}'.format(host, action) - return self._get(url, response_key='host') - - def list(self, zone=None, service=None): - """List cloud hosts.""" - - filters = [] - if zone: - filters.append('zone=%s' % zone) - if service: - filters.append('service=%s' % service) - - if filters: - url = '/os-hosts?%s' % '&'.join(filters) - else: - url = '/os-hosts' - - return self._list(url, "hosts") diff --git a/novaclient/v3/hypervisors.py b/novaclient/v3/hypervisors.py deleted file mode 100644 index b04a60080..000000000 --- a/novaclient/v3/hypervisors.py +++ /dev/null @@ -1,49 +0,0 @@ -# Copyright 2013 IBM Corp -# All Rights Reserved. -# -# 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. - -""" -Hypervisors interface -""" - -from six.moves.urllib import parse - -from novaclient.v1_1 import hypervisors - - -class Hypervisor(hypervisors.Hypervisor): - pass - - -class HypervisorManager(hypervisors.HypervisorManager): - resource_class = Hypervisor - - def search(self, hypervisor_match): - """ - Get a list of matching hypervisors. - - :param servers: If True, server information is also retrieved. - """ - url = ('/os-hypervisors/search?query=%s' % - parse.quote(hypervisor_match, safe='')) - return self._list(url, 'hypervisors') - - def servers(self, hypervisor): - """ - Get servers for a specific hypervisor - - :param hypervisor: ID of hypervisor to get list of servers for. - """ - return self._get('/os-hypervisors/%s/servers' % hypervisor, - 'hypervisor') diff --git a/novaclient/v3/images.py b/novaclient/v3/images.py deleted file mode 100644 index 7bc3d584c..000000000 --- a/novaclient/v3/images.py +++ /dev/null @@ -1,107 +0,0 @@ -# Copyright 2010 Jacob Kaplan-Moss -# Copyright 2013 IBM Corp. -# -# 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. - -""" -Image interface. -""" - -from oslo.utils import encodeutils -from oslo.utils import strutils -from six.moves.urllib import parse - -from novaclient import base - - -class Image(base.Resource): - """ - An image is a collection of files used to create or rebuild a server. - """ - HUMAN_ID = True - - def __repr__(self): - return "" % self.name - - def delete(self): - """ - Delete this image. - """ - self.manager.delete(self) - - -class ImageManager(base.ManagerWithFind): - """ - Manage :class:`Image` resources. - """ - resource_class = Image - # NOTE(cyeoh): Eventually we'll want novaclient to be smart - # enough to do version discovery, but for now we just request - # the v1 image API - image_api_prefix = '/v1' - - def _image_meta_from_headers(self, headers): - meta = {'properties': {}} - safe_decode = encodeutils.safe_decode - for key, value in headers.items(): - value = safe_decode(value, incoming='utf-8') - if key.startswith('x-image-meta-property-'): - _key = safe_decode(key[22:], incoming='utf-8') - meta['properties'][_key] = value - elif key.startswith('x-image-meta-'): - _key = safe_decode(key[13:], incoming='utf-8') - meta[_key] = value - - for key in ['is_public', 'protected', 'deleted']: - if key in meta: - meta[key] = strutils.bool_from_string(meta[key]) - - return self._format_image_meta_for_user(meta) - - @staticmethod - def _format_image_meta_for_user(meta): - for key in ['size', 'min_ram', 'min_disk']: - if key in meta: - try: - meta[key] = int(meta[key]) - except ValueError: - pass - return meta - - def get(self, image): - """ - Get an image. - - :param image: The ID of the image to get. - :rtype: :class:`Image` - """ - url = "%s/images/%s" % (self.image_api_prefix, base.getid(image)) - resp, _ = self.api.client._cs_request(url, 'HEAD') - foo = self._image_meta_from_headers(resp.headers) - return Image(self, foo) - - def list(self, detailed=True, limit=None): - """ - Get a list of all images. - - :rtype: list of :class:`Image` - :param limit: maximum number of images to return. - """ - params = {} - detail = '' - if detailed: - detail = '/detail' - if limit: - params['limit'] = int(limit) - query = '?%s' % parse.urlencode(params) if params else '' - return self._list('/v1/images%s%s' % (detail, query), 'images') diff --git a/novaclient/v3/keypairs.py b/novaclient/v3/keypairs.py deleted file mode 100644 index f1ef8ac58..000000000 --- a/novaclient/v3/keypairs.py +++ /dev/null @@ -1,28 +0,0 @@ -# Copyright 2013 IBM Corp. -# -# 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. - -""" -Keypair interface -""" - -from novaclient.v1_1 import keypairs - - -class Keypair(keypairs.Keypair): - pass - - -class KeypairManager(keypairs.KeypairManager): - resource_class = Keypair - keypair_prefix = "keypairs" diff --git a/novaclient/v3/limits.py b/novaclient/v3/limits.py deleted file mode 100644 index ab8a4a279..000000000 --- a/novaclient/v3/limits.py +++ /dev/null @@ -1,50 +0,0 @@ -# Copyright 2011 OpenStack Foundation -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from six.moves.urllib import parse - -from novaclient.v1_1 import limits - - -class Limits(limits.Limits): - pass - - -class RateLimit(limits.RateLimit): - pass - - -class AbsoluteLimit(limits.AbsoluteLimit): - pass - - -class LimitsManager(limits.LimitsManager): - """Manager object used to interact with limits resource.""" - - resource_class = Limits - - def get(self, reserved=False, tenant_id=None): - """ - Get a specific extension. - - :rtype: :class:`Limits` - """ - opts = {} - if reserved: - opts['reserved'] = 1 - if tenant_id: - opts['tenant_id'] = tenant_id - query_string = "?%s" % parse.urlencode(opts) if opts else "" - - return self._get("/limits%s" % query_string, "limits") diff --git a/novaclient/v3/list_extensions.py b/novaclient/v3/list_extensions.py deleted file mode 100644 index bcc187494..000000000 --- a/novaclient/v3/list_extensions.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright 2014 NEC Corporation. All rights reserved. -# -# 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. -""" -extension interface -""" - -from novaclient.v1_1.contrib import list_extensions - - -class ListExtResource(list_extensions.ListExtResource): - pass - - -class ListExtManager(list_extensions.ListExtManager): - pass diff --git a/novaclient/v3/quotas.py b/novaclient/v3/quotas.py deleted file mode 100644 index b55842daa..000000000 --- a/novaclient/v3/quotas.py +++ /dev/null @@ -1,42 +0,0 @@ -# Copyright 2011 OpenStack Foundation -# All Rights Reserved. -# -# 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 novaclient.v1_1 import quotas - - -class QuotaSet(quotas.QuotaSet): - pass - - -class QuotaSetManager(quotas.QuotaSetManager): - resource_class = QuotaSet - - def get(self, tenant_id, user_id=None, detail=False): - if detail: - detail_string = '/detail' - else: - detail_string = '' - - if hasattr(tenant_id, 'tenant_id'): - tenant_id = tenant_id.tenant_id - if user_id: - url = '/os-quota-sets/%s%s?user_id=%s' % (tenant_id, detail_string, - user_id) - else: - url = '/os-quota-sets/%s%s' % (tenant_id, detail_string) - return self._get(url, "quota_set") - - def _update_body(self, tenant_id, **kwargs): - return {'quota_set': kwargs} diff --git a/novaclient/v3/servers.py b/novaclient/v3/servers.py deleted file mode 100644 index 9ddc92fed..000000000 --- a/novaclient/v3/servers.py +++ /dev/null @@ -1,1022 +0,0 @@ -# Copyright 2010 Jacob Kaplan-Moss - -# Copyright 2011 OpenStack Foundation -# All Rights Reserved. -# -# 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. - -""" -Server interface. -""" - -import base64 - -from oslo.utils import encodeutils -import six -from six.moves.urllib import parse - -from novaclient import base -from novaclient import crypto -from novaclient.i18n import _ - - -REBOOT_SOFT, REBOOT_HARD = 'SOFT', 'HARD' - - -class Server(base.Resource): - HUMAN_ID = True - - def __repr__(self): - return "" % self.name - - def delete(self): - """ - Delete (i.e. shut down and delete the image) this server. - """ - self.manager.delete(self) - - def update(self, name=None): - """ - Update the name or the password for this server. - - :param name: Update the server's name. - :param password: Update the root password. - """ - self.manager.update(self, name=name) - - def get_console_output(self, length=None): - """ - Get text console log output from Server. - - :param length: The number of lines you would like to retrieve (as int) - """ - return self.manager.get_console_output(self, length) - - def get_vnc_console(self, console_type): - """ - Get vnc console for a Server. - - :param console_type: Type of console ('novnc' or 'xvpvnc') - """ - return self.manager.get_vnc_console(self, console_type) - - def get_spice_console(self, console_type): - """ - Get spice console for a Server. - - :param console_type: Type of console ('spice-html5') - """ - return self.manager.get_spice_console(self, console_type) - - def get_password(self, private_key): - """ - Get password for a Server. - - :param private_key: Path to private key file for decryption - """ - return self.manager.get_password(self, private_key) - - def clear_password(self): - """ - Get password for a Server. - - """ - return self.manager.clear_password(self) - - def add_fixed_ip(self, network_id): - """ - Add an IP address on a network. - - :param network_id: The ID of the network the IP should be on. - """ - self.manager.add_fixed_ip(self, network_id) - - def remove_floating_ip(self, address): - """ - Remove floating IP from an instance - - :param address: The ip address or FloatingIP to remove - """ - self.manager.remove_floating_ip(self, address) - - def stop(self): - """ - Stop -- Stop the running server. - """ - self.manager.stop(self) - - def force_delete(self): - """ - Force delete -- Force delete a server. - """ - self.manager.force_delete(self) - - def restore(self): - """ - Restore -- Restore a server in 'soft-deleted' state. - """ - self.manager.restore(self) - - def start(self): - """ - Start -- Start the paused server. - """ - self.manager.start(self) - - def pause(self): - """ - Pause -- Pause the running server. - """ - self.manager.pause(self) - - def unpause(self): - """ - Unpause -- Unpause the paused server. - """ - self.manager.unpause(self) - - def lock(self): - """ - Lock -- Lock the instance from certain operations. - """ - self.manager.lock(self) - - def unlock(self): - """ - Unlock -- Remove instance lock. - """ - self.manager.unlock(self) - - def suspend(self): - """ - Suspend -- Suspend the running server. - """ - self.manager.suspend(self) - - def resume(self): - """ - Resume -- Resume the suspended server. - """ - self.manager.resume(self) - - def rescue(self): - """ - Rescue -- Rescue the problematic server. - """ - return self.manager.rescue(self) - - def unrescue(self): - """ - Unrescue -- Unrescue the rescued server. - """ - self.manager.unrescue(self) - - def shelve(self): - """ - Shelve -- Shelve the server. - """ - self.manager.shelve(self) - - def shelve_offload(self): - """ - Shelve_offload -- Remove a shelved server from the compute node. - """ - self.manager.shelve_offload(self) - - def unshelve(self): - """ - Unshelve -- Unshelve the server. - """ - self.manager.unshelve(self) - - def diagnostics(self): - """Diagnostics -- Retrieve server diagnostics.""" - return self.manager.diagnostics(self) - - def migrate(self): - """ - Migrate a server to a new host. - """ - self.manager.migrate(self) - - def remove_fixed_ip(self, address): - """ - Remove an IP address. - - :param address: The IP address to remove. - """ - self.manager.remove_fixed_ip(self, address) - - def change_password(self, password): - """ - Update the password for a server. - """ - self.manager.change_password(self, password) - - def reboot(self, reboot_type=REBOOT_SOFT): - """ - Reboot the server. - - :param reboot_type: either :data:`REBOOT_SOFT` for a software-level - reboot, or `REBOOT_HARD` for a virtual power cycle hard reboot. - """ - self.manager.reboot(self, reboot_type) - - def rebuild(self, image, password=None, **kwargs): - """ - Rebuild -- shut down and then re-image -- this server. - - :param image: the :class:`Image` (or its ID) to re-image with. - :param password: string to set as password on the rebuilt server. - """ - return self.manager.rebuild(self, image, password=password, **kwargs) - - def resize(self, flavor, **kwargs): - """ - Resize the server's resources. - - :param flavor: the :class:`Flavor` (or its ID) to resize to. - - Until a resize event is confirmed with :meth:`confirm_resize`, the old - server will be kept around and you'll be able to roll back to the old - flavor quickly with :meth:`revert_resize`. All resizes are - automatically confirmed after 24 hours. - """ - self.manager.resize(self, flavor, **kwargs) - - def create_image(self, image_name, metadata=None): - """ - Create an image based on this server. - - :param image_name: The name to assign the newly create image. - :param metadata: Metadata to assign to the image. - """ - return self.manager.create_image(self, image_name, metadata) - - def backup(self, backup_name, backup_type, rotation): - """ - Backup a server instance. - - :param backup_name: Name of the backup image - :param backup_type: The backup type, like 'daily' or 'weekly' - :param rotation: Int parameter representing how many backups to - keep around. - """ - self.manager.backup(self, backup_name, backup_type, rotation) - - def confirm_resize(self): - """ - Confirm that the resize worked, thus removing the original server. - """ - self.manager.confirm_resize(self) - - def revert_resize(self): - """ - Revert a previous resize, switching back to the old server. - """ - self.manager.revert_resize(self) - - @property - def networks(self): - """ - Generate a simplified list of addresses - """ - networks = {} - try: - for network_label, address_list in self.addresses.items(): - networks[network_label] = [a['addr'] for a in address_list] - return networks - except Exception: - return {} - - def live_migrate(self, host=None, - block_migration=False, - disk_over_commit=False): - """ - Migrates a running instance to a new machine. - """ - self.manager.live_migrate(self, host, - block_migration, - disk_over_commit) - - def reset_state(self, state='error'): - """ - Reset the state of an instance to active or error. - """ - self.manager.reset_state(self, state) - - def reset_network(self): - """ - Reset network of an instance. - """ - self.manager.reset_network(self) - - def evacuate(self, host=None, on_shared_storage=True, password=None): - """ - Evacuate an instance from failed host to specified host. - - :param host: Name of the target host - :param on_shared_storage: Specifies whether instance files located - on shared storage - :param password: string to set as password on the evacuated server. - """ - return self.manager.evacuate(self, host, on_shared_storage, password) - - def interface_list(self): - """ - List interfaces attached to an instance. - """ - return self.manager.interface_list(self) - - def interface_attach(self, port_id, net_id, fixed_ip): - """ - Attach a network interface to an instance. - """ - return self.manager.interface_attach(self, port_id, net_id, fixed_ip) - - def interface_detach(self, port_id): - """ - Detach a network interface from an instance. - """ - return self.manager.interface_detach(self, port_id) - - -class ServerManager(base.BootingManagerWithFind): - resource_class = Server - - def _boot(self, resource_url, response_key, name, image, flavor, - meta=None, userdata=None, - reservation_id=None, return_raw=False, min_count=None, - max_count=None, security_groups=None, key_name=None, - availability_zone=None, block_device_mapping=None, - block_device_mapping_v2=None, nics=None, scheduler_hints=None, - config_drive=None, admin_pass=None, **kwargs): - """ - Create (boot) a new server. - - :param name: Something to name the server. - :param image: The :class:`Image` to boot with. - :param flavor: The :class:`Flavor` to boot onto. - :param meta: A dict of arbitrary key/value metadata to store for this - server. A maximum of five entries is allowed, and both - keys and values must be 255 characters or less. - :param reservation_id: a UUID for the set of servers being requested. - :param return_raw: If True, don't try to coearse the result into - a Resource object. - :param security_groups: list of security group names - :param key_name: (optional extension) name of keypair to inject into - the instance - :param availability_zone: Name of the availability zone for instance - placement. - :param block_device_mapping: A dict of block device mappings for this - server. - :param block_device_mapping_v2: A dict of block device mappings V2 for - this server. - :param nics: (optional extension) an ordered list of nics to be - added to this server, with information about - connected networks, fixed ips, etc. - :param scheduler_hints: (optional extension) arbitrary key-value pairs - specified by the client to help boot an instance. - :param config_drive: (optional extension) value for config drive - either boolean, or volume-id - :param admin_pass: admin password for the server. - """ - body = {"server": { - "name": name, - "image_ref": str(base.getid(image)) if image else '', - "flavor_ref": str(base.getid(flavor)), - }} - if userdata: - if hasattr(userdata, 'read'): - userdata = userdata.read() - - if six.PY3: - userdata = userdata.encode("utf-8") - else: - userdata = encodeutils.safe_encode(userdata) - - data = base64.b64encode(userdata).decode('utf-8') - body["server"]["user_data"] = data - if meta: - body["server"]["metadata"] = meta - if reservation_id: - body["server"][ - "os-multiple-create:return_reservation_id"] = reservation_id - if key_name: - body["server"]["key_name"] = key_name - if scheduler_hints: - body["server"][ - "os-scheduler-hints:scheduler_hints"] = scheduler_hints - if config_drive: - body["server"]["os-config-drive:config_drive"] = config_drive - if admin_pass: - body["server"]["admin_password"] = admin_pass - if not min_count: - min_count = 1 - if not max_count: - max_count = min_count - body["server"]["os-multiple-create:min_count"] = min_count - body["server"]["os-multiple-create:max_count"] = max_count - - if security_groups: - body["server"]["security_groups"] = [{'name': sg} - for sg in security_groups] - - if availability_zone: - body["server"][ - "os-availability-zone:availability_zone"] = availability_zone - - # Block device mappings are passed as a list of dictionaries - if block_device_mapping: - bdm_param = 'block_device_mapping' - body['server'][bdm_param] = \ - self._parse_block_device_mapping(block_device_mapping) - elif block_device_mapping_v2: - # Append the image to the list only if we have new style BDMs - bdm_param = 'block_device_mapping_v2' - if image: - bdm_dict = {'uuid': image.id, 'source_type': 'image', - 'destination_type': 'local', 'boot_index': 0, - 'delete_on_termination': True} - block_device_mapping_v2.insert(0, bdm_dict) - - body['server'][bdm_param] = block_device_mapping_v2 - - if nics is not None: - # NOTE(tr3buchet): nics can be an empty list - all_net_data = [] - for nic_info in nics: - net_data = {} - # if value is empty string, do not send value in body - if nic_info.get('net-id'): - net_data['uuid'] = nic_info['net-id'] - if (nic_info.get('v4-fixed-ip') and - nic_info.get('v6-fixed-ip')): - raise base.exceptions.CommandError(_( - "Only one of 'v4-fixed-ip' and 'v6-fixed-ip' may be" - " provided.")) - elif nic_info.get('v4-fixed-ip'): - net_data['fixed_ip'] = nic_info['v4-fixed-ip'] - elif nic_info.get('v6-fixed-ip'): - net_data['fixed_ip'] = nic_info['v6-fixed-ip'] - if nic_info.get('port-id'): - net_data['port'] = nic_info['port-id'] - all_net_data.append(net_data) - body['server']['networks'] = all_net_data - - return self._create(resource_url, body, response_key, - return_raw=return_raw, **kwargs) - - def get(self, server): - """ - Get a server. - - :param server: ID of the :class:`Server` to get. - :rtype: :class:`Server` - """ - return self._get("/servers/%s" % base.getid(server), "server") - - def list(self, detailed=True, search_opts=None, marker=None, limit=None): - """ - Get a list of servers. - - :param detailed: Whether to return detailed server info (optional). - :param search_opts: Search options to filter out servers (optional). - :param marker: Begin returning servers that appear later in the server - list than that represented by this server id (optional). - :param limit: Maximum number of servers to return (optional). - - :rtype: list of :class:`Server` - """ - if search_opts is None: - search_opts = {} - - qparams = {} - - for opt, val in six.iteritems(search_opts): - if val: - qparams[opt] = val - - if marker: - qparams['marker'] = marker - - if limit: - qparams['limit'] = limit - - # Transform the dict to a sequence of two-element tuples in fixed - # order, then the encoded string will be consistent in Python 2&3. - if qparams: - new_qparams = sorted(qparams.items(), key=lambda x: x[0]) - query_string = "?%s" % parse.urlencode(new_qparams) - else: - query_string = "" - - detail = "" - if detailed: - detail = "/detail" - return self._list("/servers%s%s" % (detail, query_string), "servers") - - def add_fixed_ip(self, server, network_id): - """ - Add an IP address on a network. - - :param server: The :class:`Server` (or its ID) to add an IP to. - :param network_id: The ID of the network the IP should be on. - """ - self._action('add_fixed_ip', server, {'network_id': network_id}) - - def remove_fixed_ip(self, server, address): - """ - Remove an IP address. - - :param server: The :class:`Server` (or its ID) to add an IP to. - :param address: The IP address to remove. - """ - self._action('remove_fixed_ip', server, {'address': address}) - - def get_vnc_console(self, server, console_type): - """ - Get a vnc console for an instance - - :param server: The :class:`Server` (or its ID) to add an IP to. - :param console_type: Type of vnc console to get ('novnc' or 'xvpvnc') - """ - - return self._action('get_vnc_console', server, - {'type': console_type})[1] - - def get_spice_console(self, server, console_type): - """ - Get a spice console for an instance - - :param server: The :class:`Server` (or its ID) to add an IP to. - :param console_type: Type of spice console to get ('spice-html5') - """ - - return self._action('get_spice_console', server, - {'type': console_type})[1] - - def get_password(self, server, private_key): - """ - Get password for an instance - - Requires that openssl is installed and in the path - - :param server: The :class:`Server` (or its ID) to add an IP to. - :param private_key: The private key to decrypt password - """ - - _resp, body = self.api.client.get("/servers/%s/os-server-password" - % base.getid(server)) - if body and body.get('password'): - try: - return crypto.decrypt_password(private_key, body['password']) - except Exception as exc: - return '%sFailed to decrypt:\n%s' % (exc, body['password']) - return '' - - def clear_password(self, server): - """ - Clear password for an instance - - :param server: The :class:`Server` (or its ID) to add an IP to. - """ - - return self._delete("/servers/%s/os-server-password" - % base.getid(server)) - - def stop(self, server): - """ - Stop the server. - """ - return self._action('stop', server, None) - - def force_delete(self, server): - """ - Force delete the server. - """ - return self._action('force_delete', server, None) - - def restore(self, server): - """ - Restore soft-deleted server. - """ - return self._action('restore', server, None) - - def start(self, server): - """ - Start the server. - """ - self._action('start', server, None) - - def pause(self, server): - """ - Pause the server. - """ - self._action('pause', server, None) - - def unpause(self, server): - """ - Unpause the server. - """ - self._action('unpause', server, None) - - def lock(self, server): - """ - Lock the server. - """ - self._action('lock', server, None) - - def unlock(self, server): - """ - Unlock the server. - """ - self._action('unlock', server, None) - - def suspend(self, server): - """ - Suspend the server. - """ - self._action('suspend', server, None) - - def resume(self, server): - """ - Resume the server. - """ - self._action('resume', server, None) - - def rescue(self, server): - """ - Rescue the server. - """ - return self._action('rescue', server, None) - - def unrescue(self, server): - """ - Unrescue the server. - """ - self._action('unrescue', server, None) - - def shelve(self, server): - """ - Shelve the server. - """ - self._action('shelve', server, None) - - def shelve_offload(self, server): - """ - Remove a shelved instance from the compute node. - """ - self._action('shelve_offload', server, None) - - def unshelve(self, server): - """ - Unshelve the server. - """ - self._action('unshelve', server, None) - - def diagnostics(self, server): - """Retrieve server diagnostics.""" - return self.api.client.get("/servers/%s/os-server-diagnostics" % - base.getid(server)) - - def create(self, name, image, flavor, meta=None, files=None, - reservation_id=None, min_count=None, - max_count=None, security_groups=None, userdata=None, - key_name=None, availability_zone=None, - block_device_mapping=None, block_device_mapping_v2=None, - nics=None, scheduler_hints=None, - config_drive=None, **kwargs): - # TODO(anthony): indicate in doc string if param is an extension - # and/or optional - """ - Create (boot) a new server. - - :param name: Something to name the server. - :param image: The :class:`Image` to boot with. - :param flavor: The :class:`Flavor` to boot onto. - :param meta: A dict of arbitrary key/value metadata to store for this - server. A maximum of five entries is allowed, and both - keys and values must be 255 characters or less. - :param files: A dict of files to overrwrite on the server upon boot. - Keys are file names (i.e. ``/etc/passwd``) and values - are the file contents (either as a string or as a - file-like object). A maximum of five entries is allowed, - and each file must be 10k or less. - :param userdata: user data to pass to be exposed by the metadata - server this can be a file type object as well or a - string. - :param reservation_id: a UUID for the set of servers being requested. - :param key_name: (optional extension) name of previously created - keypair to inject into the instance. - :param availability_zone: Name of the availability zone for instance - placement. - :param block_device_mapping: (optional extension) A dict of block - device mappings for this server. - :param block_device_mapping_v2: (optional extension) A dict of block - device mappings for this server. - :param nics: (optional extension) an ordered list of nics to be - added to this server, with information about - connected networks, fixed ips, port etc. - :param scheduler_hints: (optional extension) arbitrary key-value pairs - specified by the client to help boot an instance - :param config_drive: (optional extension) value for config drive - either boolean, or volume-id - """ - if not min_count: - min_count = 1 - if not max_count: - max_count = min_count - if min_count > max_count: - min_count = max_count - - boot_args = [name, image, flavor] - - boot_kwargs = dict( - meta=meta, files=files, userdata=userdata, - reservation_id=reservation_id, min_count=min_count, - max_count=max_count, security_groups=security_groups, - key_name=key_name, availability_zone=availability_zone, - scheduler_hints=scheduler_hints, config_drive=config_drive, - **kwargs) - - if block_device_mapping: - boot_kwargs['block_device_mapping'] = block_device_mapping - elif block_device_mapping_v2: - boot_kwargs['block_device_mapping_v2'] = block_device_mapping_v2 - resource_url = "/servers" - if nics: - boot_kwargs['nics'] = nics - - response_key = "server" - return self._boot(resource_url, response_key, *boot_args, - **boot_kwargs) - - def update(self, server, name=None): - """ - Update the name or the password for a server. - - :param server: The :class:`Server` (or its ID) to update. - :param name: Update the server's name. - """ - if name is None: - return - - body = { - "server": { - "name": name, - }, - } - - return self._update("/servers/%s" % base.getid(server), body, "server") - - def change_password(self, server, password): - """ - Update the password for a server. - """ - self._action("change_password", server, {"admin_password": password}) - - def delete(self, server): - """ - Delete (i.e. shut down and delete the image) this server. - """ - self._delete("/servers/%s" % base.getid(server)) - - def reboot(self, server, reboot_type=REBOOT_SOFT): - """ - Reboot a server. - - :param server: The :class:`Server` (or its ID) to share onto. - :param reboot_type: either :data:`REBOOT_SOFT` for a software-level - reboot, or `REBOOT_HARD` for a virtual power cycle hard reboot. - """ - self._action('reboot', server, {'type': reboot_type}) - - def rebuild(self, server, image, password=None, **kwargs): - """ - Rebuild -- shut down and then re-image -- a server. - - :param server: The :class:`Server` (or its ID) to share onto. - :param image: the :class:`Image` (or its ID) to re-image with. - :param password: string to set as password on the rebuilt server. - """ - body = {'image_ref': base.getid(image)} - if password is not None: - body['admin_password'] = password - - _resp, body = self._action('rebuild', server, body, **kwargs) - return Server(self, body['server']) - - def migrate(self, server): - """ - Migrate a server to a new host. - - :param server: The :class:`Server` (or its ID). - """ - self._action('migrate', server) - - def resize(self, server, flavor, **kwargs): - """ - Resize a server's resources. - - :param server: The :class:`Server` (or its ID) to share onto. - :param flavor: the :class:`Flavor` (or its ID) to resize to. - - Until a resize event is confirmed with :meth:`confirm_resize`, the old - server will be kept around and you'll be able to roll back to the old - flavor quickly with :meth:`revert_resize`. All resizes are - automatically confirmed after 24 hours. - """ - info = {'flavor_ref': base.getid(flavor)} - - self._action('resize', server, info=info, **kwargs) - - def confirm_resize(self, server): - """ - Confirm that the resize worked, thus removing the original server. - - :param server: The :class:`Server` (or its ID) to share onto. - """ - self._action('confirm_resize', server) - - def revert_resize(self, server): - """ - Revert a previous resize, switching back to the old server. - - :param server: The :class:`Server` (or its ID) to share onto. - """ - self._action('revert_resize', server) - - def create_image(self, server, image_name, metadata=None): - """ - Snapshot a server. - - :param server: The :class:`Server` (or its ID) to share onto. - :param image_name: Name to give the snapshot image - :param meta: Metadata to give newly-created image entity - """ - body = {'name': image_name, 'metadata': metadata or {}} - resp = self._action('create_image', server, body)[0] - location = resp.headers['location'] - image_uuid = location.split('/')[-1] - return image_uuid - - def backup(self, server, backup_name, backup_type, rotation): - """ - Backup a server instance. - - :param server: The :class:`Server` (or its ID) to share onto. - :param backup_name: Name of the backup image - :param backup_type: The backup type, like 'daily' or 'weekly' - :param rotation: Int parameter representing how many backups to - keep around. - """ - body = {'name': backup_name, - 'backup_type': backup_type, - 'rotation': rotation} - self._action('create_backup', server, body) - - def set_meta(self, server, metadata): - """ - Set a servers metadata - :param server: The :class:`Server` to add metadata to - :param metadata: A dict of metadata to add to the server - """ - body = {'metadata': metadata} - return self._create( - "/servers/%s/metadata" % base.getid(server), body, "metadata") - - def get_console_output(self, server, length=None): - """ - Get text console log output from Server. - - :param server: The :class:`Server` (or its ID) whose console output - you would like to retrieve. - :param length: The number of tail loglines you would like to retrieve. - """ - if length is None: - # NOTE: On v3 get_console_output API, -1 means an unlimited length. - # Here translates None, which means an unlimited in the internal - # implementation, to -1. - length = -1 - return self._action('get_console_output', - server, {'length': length})[1]['output'] - - def delete_meta(self, server, keys): - """ - Delete metadata from an server - :param server: The :class:`Server` to add metadata to - :param keys: A list of metadata keys to delete from the server - """ - for k in keys: - self._delete("/servers/%s/metadata/%s" % (base.getid(server), k)) - - def live_migrate(self, server, host, block_migration, disk_over_commit): - """ - Migrates a running instance to a new machine. - - :param server: instance id which comes from nova list. - :param host: destination host name. - :param block_migration: if True, do block_migration. - :param disk_over_commit: if True, Allow overcommit. - - """ - self._action('migrate_live', server, - {'host': host, - 'block_migration': block_migration, - 'disk_over_commit': disk_over_commit}) - - def reset_state(self, server, state='error'): - """ - Reset the state of an instance to active or error. - - :param server: ID of the instance to reset the state of. - :param state: Desired state; either 'active' or 'error'. - Defaults to 'error'. - """ - self._action('reset_state', server, dict(state=state)) - - def reset_network(self, server): - """ - Reset network of an instance. - """ - self._action('reset_network', server) - - def evacuate(self, server, host=None, - on_shared_storage=True, password=None): - """ - Evacuate a server instance. - - :param server: The :class:`Server` (or its ID) to share onto. - :param host: Name of the target host. - :param on_shared_storage: Specifies whether instance files located - on shared storage - :param password: string to set as password on the evacuated server. - """ - body = {'on_shared_storage': on_shared_storage} - if host is not None: - body['host'] = host - - if password is not None: - body['admin_password'] = password - - return self._action('evacuate', server, body) - - def interface_list(self, server): - """ - List attached network interfaces - - :param server: The :class:`Server` (or its ID) to query. - """ - return self._list('/servers/%s/os-attach-interfaces' - % base.getid(server), 'interface_attachments') - - def interface_attach(self, server, port_id, net_id, fixed_ip): - """ - Attach a network_interface to an instance. - - :param server: The :class:`Server` (or its ID) to attach to. - :param port_id: The port to attach. - """ - - body = {'interface_attachment': {}} - if port_id: - body['interface_attachment']['port_id'] = port_id - if net_id: - body['interface_attachment']['net_id'] = net_id - if fixed_ip: - body['interface_attachment']['fixed_ips'] = [ - {'ip_address': fixed_ip}] - - return self._create('/servers/%s/os-attach-interfaces' - % base.getid(server), - body, 'interface_attachment') - - def interface_detach(self, server, port_id): - """ - Detach a network_interface from an instance. - - :param server: The :class:`Server` (or its ID) to detach from. - :param port_id: The port to detach. - """ - self._delete('/servers/%s/os-attach-interfaces/%s' - % (base.getid(server), port_id)) - - def _action(self, action, server, info=None, **kwargs): - """ - Perform a server "action" -- reboot/rebuild/resize/etc. - """ - body = {action: info} - self.run_hooks('modify_body_for_action', body, **kwargs) - url = '/servers/%s/action' % base.getid(server) - return self.api.client.post(url, body=body) diff --git a/novaclient/v3/services.py b/novaclient/v3/services.py deleted file mode 100644 index fe0f3a6a4..000000000 --- a/novaclient/v3/services.py +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright 2013 IBM Corp. -# -# 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. - -""" -service interface -""" -from novaclient.v1_1 import services - - -class Service(services.Service): - pass - - -class ServiceManager(services.ServiceManager): - resource_class = Service - - def _update_body(self, host, binary, disabled_reason=None): - body = {"service": - {"host": host, - "binary": binary}} - if disabled_reason is not None: - body["service"]["disabled_reason"] = disabled_reason - return body diff --git a/novaclient/v3/shell.py b/novaclient/v3/shell.py deleted file mode 100644 index 1c35ed6d5..000000000 --- a/novaclient/v3/shell.py +++ /dev/null @@ -1,3415 +0,0 @@ -# Copyright 2010 Jacob Kaplan-Moss - -# Copyright 2011 OpenStack Foundation -# Copyright 2013 IBM Corp. -# All Rights Reserved. -# -# 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 __future__ import print_function - -import argparse -import copy -import datetime -import getpass -import locale -import os -import sys -import time - -from oslo.utils import strutils -from oslo.utils import timeutils -import six - -from novaclient import client -from novaclient import exceptions -from novaclient.i18n import _ -from novaclient.openstack.common import cliutils -from novaclient.openstack.common import uuidutils -from novaclient import utils -from novaclient.v3 import availability_zones -from novaclient.v3 import servers - - -def _key_value_pairing(text): - try: - (k, v) = text.split('=', 1) - return (k, v) - except ValueError: - msg = "%r is not in the format of key=value" % text - raise argparse.ArgumentTypeError(msg) - - -def _match_image(cs, wanted_properties): - image_list = cs.images.list() - images_matched = [] - match = set(wanted_properties) - for img in image_list: - try: - if match == match.intersection(set(img.metadata.items())): - images_matched.append(img) - except AttributeError: - pass - return images_matched - - -def _boot(cs, args): - """Boot a new server.""" - if args.image: - image = _find_image(cs.image_cs, args.image) - else: - image = None - - if not image and args.image_with: - images = _match_image(cs.image_cs, args.image_with) - if images: - # TODO(harlowja): log a warning that we - # are selecting the first of many? - image = images[0] - - if not image and not args.block_device_mapping: - raise exceptions.CommandError("you need to specify an Image ID " - "or a block device mapping " - "or provide a set of properties to match" - " against an image") - if not args.flavor: - raise exceptions.CommandError("you need to specify a Flavor ID ") - - min_count = 1 - max_count = 1 - # Don't let user mix num_instances and max_count/min_count. - if (args.num_instances is not None and - args.min_count is None and - args.max_count is None): - if args.num_instances < 1: - raise exceptions.CommandError("num_instances should be >= 1") - max_count = args.num_instances - elif (args.num_instances is not None and - (args.min_count is not None or args.max_count is not None)): - raise exceptions.CommandError( - "Don't mix num-instances and max/min-count") - if args.min_count is not None: - if args.min_count < 1: - raise exceptions.CommandError("min_count should be >= 1") - min_count = args.min_count - max_count = min_count - if args.max_count is not None: - if args.max_count < 1: - raise exceptions.CommandError("max_count should be >= 1") - max_count = args.max_count - if (args.min_count is not None and - args.max_count is not None and - args.min_count > args.max_count): - raise exceptions.CommandError("min_count should be <= max_count") - - flavor = _find_flavor(cs, args.flavor) - - meta = dict(v.split('=', 1) for v in args.meta) - - files = {} - for f in args.files: - try: - dst, src = f.split('=', 1) - files[dst] = open(src) - except IOError as e: - raise exceptions.CommandError("Can't open '%s': %s" % (src, e)) - except ValueError as e: - raise exceptions.CommandError( - "Invalid file argument '%s'. File " - "arguments must be of the form '--file '" - % f) - - # use the os-keypair extension - key_name = None - if args.key_name is not None: - key_name = args.key_name - - if args.user_data: - try: - userdata = open(args.user_data) - except IOError as e: - raise exceptions.CommandError("Can't open '%s': %s" % - (args.user_data, e)) - else: - userdata = None - - if args.availability_zone: - availability_zone = args.availability_zone - else: - availability_zone = None - - if args.security_groups: - security_groups = args.security_groups.split(',') - else: - security_groups = None - - block_device_mapping = {} - for bdm in args.block_device_mapping: - device_name, mapping = bdm.split('=', 1) - block_device_mapping[device_name] = mapping - - nics = [] - for nic_str in args.nics: - err_msg = ("Invalid nic argument '%s'. Nic arguments must be of the " - "form --nic , with at minimum " - "net-id or port-id (but not both) specified." % nic_str) - nic_info = {"net-id": "", "v4-fixed-ip": "", "v6-fixed-ip": "", - "port-id": ""} - - for kv_str in nic_str.split(","): - try: - k, v = kv_str.split("=", 1) - except ValueError as e: - raise exceptions.CommandError(err_msg) - - if k in nic_info: - nic_info[k] = v - else: - raise exceptions.CommandError(err_msg) - - if bool(nic_info['net-id']) == bool(nic_info['port-id']): - raise exceptions.CommandError(err_msg) - - nics.append(nic_info) - - hints = {} - if args.scheduler_hints: - for hint in args.scheduler_hints: - key, _sep, value = hint.partition('=') - # NOTE(vish): multiple copies of the same hint will - # result in a list of values - if key in hints: - if isinstance(hints[key], six.string_types): - hints[key] = [hints[key]] - hints[key] += [value] - else: - hints[key] = value - boot_args = [args.name, image, flavor] - - if str(args.config_drive).lower() in ("true", "1"): - config_drive = True - elif str(args.config_drive).lower() in ("false", "0", "", "none"): - config_drive = None - else: - config_drive = args.config_drive - - boot_kwargs = dict( - meta=meta, - files=files, - key_name=key_name, - min_count=min_count, - max_count=max_count, - userdata=userdata, - availability_zone=availability_zone, - security_groups=security_groups, - block_device_mapping=block_device_mapping, - nics=nics, - scheduler_hints=hints, - config_drive=config_drive) - - return boot_args, boot_kwargs - - -@utils.arg( - '--flavor', - default=None, - metavar='', - help="Flavor ID (see 'nova flavor-list').") -@utils.arg( - '--image', - default=None, - metavar='', - help="Image ID (see 'nova image-list'). ") -@utils.arg( - '--image-with', - default=[], - type=_key_value_pairing, - action='append', - metavar='', - help="Image metadata property (see 'nova image-show'). ") -@utils.arg( - '--num-instances', - default=None, - type=int, - metavar='', - help=argparse.SUPPRESS) -@utils.arg( - '--min-count', - default=None, - type=int, - metavar='', - help="Boot at least servers (limited by quota).") -@utils.arg( - '--max-count', - default=None, - type=int, - metavar='', - help="Boot up to servers (limited by quota).") -@utils.arg( - '--meta', - metavar="", - action='append', - default=[], - help="Record arbitrary key/value metadata to /meta.js " - "on the new server. Can be specified multiple times.") -@utils.arg( - '--file', - metavar="", - action='append', - dest='files', - default=[], - help="Store arbitrary files from locally to " - "on the new server. You may store up to 5 files.") -@utils.arg( - '--key-name', - default=os.environ.get('NOVACLIENT_DEFAULT_KEY_NAME'), - metavar='', - help="Key name of keypair that should be created earlier with \ - the command keypair-add") -@utils.arg( - '--key_name', - help=argparse.SUPPRESS) -@utils.arg('name', metavar='', help='Name for the new server') -@utils.arg( - '--user-data', - default=None, - metavar='', - help="user data file to pass to be exposed by the metadata server.") -@utils.arg( - '--user_data', - help=argparse.SUPPRESS) -@utils.arg( - '--availability-zone', - default=None, - metavar='', - help="The availability zone for server placement.") -@utils.arg( - '--availability_zone', - help=argparse.SUPPRESS) -@utils.arg( - '--security-groups', - default=None, - metavar='', - help="Comma separated list of security group names.") -@utils.arg( - '--security_groups', - help=argparse.SUPPRESS) -@utils.arg( - '--block-device-mapping', - metavar="", - action='append', - default=[], - help="Block device mapping in the format " - "=:::.") -@utils.arg( - '--block_device_mapping', - action='append', - help=argparse.SUPPRESS) -@utils.arg( - '--hint', - action='append', - dest='scheduler_hints', - default=[], - metavar='', - help="Send arbitrary key/value pairs to the scheduler for custom use.") -@utils.arg( - '--nic', - metavar="", - action='append', - dest='nics', - default=[], - help="Create a NIC on the server. " - "Specify option multiple times to create multiple NICs. " - "net-id: attach NIC to network with this UUID " - "(either port-id or net-id must be provided), " - "v4-fixed-ip: IPv4 fixed address for NIC (optional), " - "v6-fixed-ip: IPv6 fixed address for NIC (optional), " - "port-id: attach NIC to port with this UUID " - "(either port-id or net-id must be provided).") -@utils.arg( - '--config-drive', - metavar="", - dest='config_drive', - default=False, - help="Enable config drive") -@utils.arg( - '--poll', - dest='poll', - action="store_true", - default=False, - help='Report the new server boot progress until it completes.') -def do_boot(cs, args): - """Boot a new server.""" - boot_args, boot_kwargs = _boot(cs, args) - - extra_boot_kwargs = utils.get_resource_manager_extra_kwargs(do_boot, args) - boot_kwargs.update(extra_boot_kwargs) - - server = cs.servers.create(*boot_args, **boot_kwargs) - _print_server(cs, args, server) - - if args.poll: - _poll_for_status(cs.servers.get, server.id, 'building', ['active']) - - -def _poll_for_status(poll_fn, obj_id, action, final_ok_states, - poll_period=5, show_progress=True, - status_field="status", silent=False): - """Block while an action is being performed, periodically printing - progress. - """ - def print_progress(progress): - if show_progress: - msg = ('\rServer %(action)s... %(progress)s%% complete' - % dict(action=action, progress=progress)) - else: - msg = '\rServer %(action)s...' % dict(action=action) - - sys.stdout.write(msg) - sys.stdout.flush() - - if not silent: - print() - - while True: - obj = poll_fn(obj_id) - - status = getattr(obj, status_field) - - if status: - status = status.lower() - - progress = getattr(obj, 'progress', None) or 0 - if status in final_ok_states: - if not silent: - print_progress(100) - print("\nFinished") - break - elif status == "error": - if not silent: - print("\nError %s server" % action) - raise exceptions.InstanceInErrorState(obj.fault['message']) - - if not silent: - print_progress(progress) - - time.sleep(poll_period) - - -def _translate_keys(collection, convert): - for item in collection: - keys = item.__dict__.keys() - for from_key, to_key in convert: - if from_key in keys and to_key not in keys: - setattr(item, to_key, item._info[from_key]) - - -def _translate_extended_states(collection): - power_states = [ - 'NOSTATE', # 0x00 - 'Running', # 0x01 - '', # 0x02 - 'Paused', # 0x03 - 'Shutdown', # 0x04 - '', # 0x05 - 'Crashed', # 0x06 - 'Suspended' # 0x07 - ] - - for item in collection: - try: - setattr(item, 'power_state', - power_states[getattr(item, 'power_state')]) - except AttributeError: - setattr(item, 'power_state', "N/A") - try: - getattr(item, 'task_state') - except AttributeError: - setattr(item, 'task_state', "N/A") - - -def _translate_flavor_keys(collection): - _translate_keys(collection, [('ram', 'memory_mb')]) - - -def _print_flavor_extra_specs(flavor): - try: - return flavor.get_keys() - except exceptions.NotFound: - return "N/A" - - -def _print_flavor_list(flavors, show_extra_specs=False): - _translate_flavor_keys(flavors) - - headers = [ - 'ID', - 'Name', - 'Memory_MB', - 'Disk', - 'Ephemeral', - 'Swap', - 'VCPUs', - 'RXTX_Factor', - 'Is_Public', - ] - - if show_extra_specs: - formatters = {'extra_specs': _print_flavor_extra_specs} - headers.append('extra_specs') - else: - formatters = {} - - utils.print_list(flavors, headers, formatters) - - -@utils.arg('--extra-specs', - dest='extra_specs', - action='store_true', - default=False, - help='Get extra-specs of each flavor.') -@utils.arg('--all', - dest='all', - action='store_true', - default=False, - help='Display all flavors (Admin only).') -def do_flavor_list(cs, args): - """Print a list of available 'flavors' (sizes of servers).""" - if args.all: - flavors = cs.flavors.list(is_public=None) - else: - flavors = cs.flavors.list() - _print_flavor_list(flavors, args.extra_specs) - - -@utils.arg( - 'flavor', - metavar='', - help="Name or ID of the flavor to delete") -def do_flavor_delete(cs, args): - """Delete a specific flavor""" - flavorid = _find_flavor(cs, args.flavor) - cs.flavors.delete(flavorid) - _print_flavor_list([flavorid]) - - -@utils.arg( - 'flavor', - metavar='', - help="Name or ID of flavor") -def do_flavor_show(cs, args): - """Show details about the given flavor.""" - flavor = _find_flavor(cs, args.flavor) - _print_flavor(flavor) - - -@utils.arg( - 'name', - metavar='', - help="Name of the new flavor") -@utils.arg( - 'id', - metavar='', - help="Unique ID (integer or UUID) for the new flavor." - " If specifying 'auto', a UUID will be generated as id") -@utils.arg( - 'ram', - metavar='', - help="Memory size in MB") -@utils.arg( - 'disk', - metavar='', - help="Disk size in GB") -@utils.arg( - '--ephemeral', - metavar='', - help="Ephemeral space size in GB (default 0)", - default=0) -@utils.arg( - 'vcpus', - metavar='', - help="Number of vcpus") -@utils.arg( - '--swap', - metavar='', - help="Swap space size in MB (default 0)", - default=0) -@utils.arg( - '--rxtx-factor', - metavar='', - help="RX/TX factor (default 1)", - default=1.0) -@utils.arg( - '--is-public', - metavar='', - help="Make flavor accessible to the public (default true)", - type=lambda v: strutils.bool_from_string(v, True), - default=True) -def do_flavor_create(cs, args): - """Create a new flavor""" - f = cs.flavors.create(args.name, args.ram, args.vcpus, args.disk, args.id, - args.ephemeral, args.swap, args.rxtx_factor, - args.is_public) - _print_flavor_list([f]) - - -@utils.arg( - 'flavor', - metavar='', - help="Name or ID of flavor") -@utils.arg( - 'action', - metavar='', - choices=['set', 'unset'], - help="Actions: 'set' or 'unset'") -@utils.arg( - 'metadata', - metavar='', - nargs='+', - action='append', - default=[], - help='Extra_specs to set/unset (only key is necessary on unset)') -def do_flavor_key(cs, args): - """Set or unset extra_spec for a flavor.""" - flavor = _find_flavor(cs, args.flavor) - keypair = _extract_metadata(args) - - if args.action == 'set': - flavor.set_keys(keypair) - elif args.action == 'unset': - flavor.unset_keys(keypair.keys()) - - -@utils.arg( - '--flavor', - metavar='', - help="Filter results by flavor name or ID.") -@utils.arg('--tenant', metavar='', - help='Filter results by tenant ID.') -def do_flavor_access_list(cs, args): - """Print access information about the given flavor.""" - if args.flavor and args.tenant: - raise exceptions.CommandError("Unable to filter results by " - "both --flavor and --tenant.") - elif args.flavor: - flavor = _find_flavor(cs, args.flavor) - if flavor.is_public: - raise exceptions.CommandError("Failed to get access list " - "for public flavor type.") - kwargs = {'flavor': flavor} - elif args.tenant: - kwargs = {'tenant': args.tenant} - else: - raise exceptions.CommandError("Unable to get all access lists. " - "Specify --flavor or --tenant") - - try: - access_list = cs.flavor_access.list(**kwargs) - except NotImplementedError as e: - raise exceptions.CommandError("%s" % str(e)) - - columns = ['Flavor_ID', 'Tenant_ID'] - utils.print_list(access_list, columns) - - -@utils.arg( - 'flavor', - metavar='', - help="Flavor name or ID to add access for the given tenant.") -@utils.arg('tenant', metavar='', - help='Tenant ID to add flavor access for.') -def do_flavor_access_add(cs, args): - """Add flavor access for the given tenant.""" - flavor = _find_flavor(cs, args.flavor) - access_list = cs.flavor_access.add_tenant_access(flavor, args.tenant) - columns = ['Flavor_ID', 'Tenant_ID'] - utils.print_list(access_list, columns) - - -@utils.arg( - 'flavor', - metavar='', - help="Flavor name or ID to remove access for the given tenant.") -@utils.arg('tenant', metavar='', - help='Tenant ID to remove flavor access for.') -def do_flavor_access_remove(cs, args): - """Remove flavor access for the given tenant.""" - flavor = _find_flavor(cs, args.flavor) - access_list = cs.flavor_access.remove_tenant_access(flavor, args.tenant) - columns = ['Flavor_ID', 'Tenant_ID'] - utils.print_list(access_list, columns) - - -@utils.arg('project_id', metavar='', - help='The ID of the project.') -def do_scrub(cs, args): - """Delete networks and security groups associated with a project.""" - networks_list = cs.networks.list() - networks_list = [network for network in networks_list - if getattr(network, 'project_id', '') == args.project_id] - search_opts = {'all_tenants': 1} - groups = cs.security_groups.list(search_opts) - groups = [group for group in groups - if group.tenant_id == args.project_id] - for network in networks_list: - cs.networks.disassociate(network) - for group in groups: - cs.security_groups.delete(group) - - -def do_network_list(cs, _args): - """Print a list of available networks.""" - network_list = cs.networks.list() - columns = ['ID', 'Label', 'Cidr'] - utils.print_list(network_list, columns) - - -@utils.arg( - 'network', - metavar='', - help="uuid or label of network") -def do_network_show(cs, args): - """Show details about the given network.""" - network = utils.find_resource(cs.networks, args.network) - utils.print_dict(network._info) - - -@utils.arg('--host-only', - dest='host_only', - metavar='<0|1>', - nargs='?', - type=int, - const=1, - default=0) -@utils.arg('--project-only', - dest='project_only', - metavar='<0|1>', - nargs='?', - type=int, - const=1, - default=0) -@utils.arg( - 'network', - metavar='', - help="uuid of network") -def do_network_disassociate(cs, args): - """Disassociate host and/or project from the given network.""" - if args.host_only: - cs.networks.disassociate(args.network, True, False) - elif args.project_only: - cs.networks.disassociate(args.network, False, True) - else: - cs.networks.disassociate(args.network, True, True) - - -@utils.arg( - 'network', - metavar='', - help="uuid of network") -@utils.arg( - 'host', - metavar='', - help="Name of host") -def do_network_associate_host(cs, args): - """Associate host with network.""" - cs.networks.associate_host(args.network, args.host) - - -@utils.arg( - 'network', - metavar='', - help="uuid of network") -def do_network_associate_project(cs, args): - """Associate project with network.""" - cs.networks.associate_project(args.network) - - -def _filter_network_create_options(args): - valid_args = ['label', 'cidr', 'vlan_start', 'vpn_start', 'cidr_v6', - 'gateway', 'gateway_v6', 'bridge', 'bridge_interface', - 'multi_host', 'dns1', 'dns2', 'uuid', 'fixed_cidr', - 'project_id', 'priority'] - kwargs = {} - for k, v in args.__dict__.items(): - if k in valid_args and v is not None: - kwargs[k] = v - - return kwargs - - -@utils.arg( - 'label', - metavar='', - help="Label for network") -@utils.arg( - '--fixed-range-v4', - dest='cidr', - metavar='', - help="IPv4 subnet (ex: 10.0.0.0/8)") -@utils.arg( - '--fixed-range-v6', - dest="cidr_v6", - help='IPv6 subnet (ex: fe80::/64') -@utils.arg( - '--vlan', - dest='vlan_start', - metavar='', - help="vlan id") -@utils.arg( - '--vpn', - dest='vpn_start', - metavar='', - help="vpn start") -@utils.arg( - '--gateway', - dest="gateway", - help='gateway') -@utils.arg( - '--gateway-v6', - dest="gateway_v6", - help='IPv6 gateway') -@utils.arg( - '--bridge', - dest="bridge", - metavar='', - help='VIFs on this network are connected to this bridge.') -@utils.arg( - '--bridge-interface', - dest="bridge_interface", - metavar='', - help='The bridge is connected to this interface.') -@utils.arg( - '--multi-host', - dest="multi_host", - metavar="<'T'|'F'>", - help='Multi host') -@utils.arg( - '--dns1', - dest="dns1", - metavar="", help='First DNS') -@utils.arg( - '--dns2', - dest="dns2", - metavar="", - help='Second DNS') -@utils.arg( - '--uuid', - dest="uuid", - metavar="", - help='Network UUID') -@utils.arg( - '--fixed-cidr', - dest="fixed_cidr", - metavar='', - help='IPv4 subnet for fixed IPs (ex: 10.20.0.0/16)') -@utils.arg( - '--project-id', - dest="project_id", - metavar="", - help='Project ID') -@utils.arg( - '--priority', - dest="priority", - metavar="", - help='Network interface priority') -def do_network_create(cs, args): - """Create a network.""" - - if not (args.cidr or args.cidr_v6): - raise exceptions.CommandError( - "Must specify either fixed_range_v4 or fixed_range_v6") - kwargs = _filter_network_create_options(args) - if args.multi_host is not None: - kwargs['multi_host'] = bool(args.multi_host == 'T' or - strutils.bool_from_string(args.multi_host)) - - cs.networks.create(**kwargs) - - -@utils.arg( - '--limit', - dest="limit", - metavar="", - help='Number of images to return per request.') -@cliutils.service_type('image') -def do_image_list(cs, _args): - """Print a list of available images to boot from.""" - limit = _args.limit - image_list = cs.images.list(limit=limit) - - def parse_server_name(image): - try: - return image.server['id'] - except (AttributeError, KeyError): - return '' - - fmts = {'Server': parse_server_name} - utils.print_list(image_list, ['ID', 'Name', 'Status', 'Server'], - fmts, sortby_index=1) - - -@utils.arg( - 'image', - metavar='', - help="Name or ID of image") -@utils.arg( - 'action', - metavar='', - choices=['set', 'delete'], - help="Actions: 'set' or 'delete'") -@utils.arg( - 'metadata', - metavar='', - nargs='+', - action='append', - default=[], - help='Metadata to add/update or delete (only key is necessary on delete)') -def do_image_meta(cs, args): - """Set or Delete metadata on an image.""" - image = _find_image(cs, args.image) - metadata = _extract_metadata(args) - - if args.action == 'set': - cs.images.set_meta(image, metadata) - elif args.action == 'delete': - cs.images.delete_meta(image, metadata.keys()) - - -def _extract_metadata(args): - metadata = {} - for metadatum in args.metadata[0]: - # Can only pass the key in on 'delete' - # So this doesn't have to have '=' - if metadatum.find('=') > -1: - (key, value) = metadatum.split('=', 1) - else: - key = metadatum - value = None - - metadata[key] = value - return metadata - - -def _print_image(image): - info = image._info.copy() - - # try to replace a server entity to just an id - server = info.pop('server', None) - try: - info['server'] = server['id'] - except (KeyError, TypeError): - pass - - # break up metadata and display each on its own row - properties = info.pop('properties', {}) - try: - for key, value in properties.items(): - _key = 'Property %s' % key - info[_key] = value - except AttributeError: - pass - - utils.print_dict(info) - - -def _print_flavor(flavor): - info = flavor._info.copy() - # ignore links, we don't need to present those - info.pop('links') - info.update({"extra_specs": _print_flavor_extra_specs(flavor)}) - utils.print_dict(info) - - -@utils.arg( - 'image', - metavar='', - help="Name or ID of image") -@cliutils.service_type('image') -def do_image_show(cs, args): - """Show details about the given image.""" - image = _find_image(cs, args.image) - _print_image(image) - - -@utils.arg('image', metavar='', nargs='+', - help='Name or ID of image(s).') -def do_image_delete(cs, args): - """Delete specified image(s).""" - for image in args.image: - try: - _find_image(cs, image).delete() - except Exception as e: - print("Delete for image %s failed: %s" % (image, e)) - - -@utils.arg( - '--reservation-id', - dest='reservation_id', - metavar='', - default=None, - help='Only return servers that match reservation-id.') -@utils.arg( - '--reservation_id', - help=argparse.SUPPRESS) -@utils.arg( - '--ip', - dest='ip', - metavar='', - default=None, - help='Search with regular expression match by IP address.') -@utils.arg( - '--ip6', - dest='ip6', - metavar='', - default=None, - help='Search with regular expression match by IPv6 address.') -@utils.arg( - '--name', - dest='name', - metavar='', - default=None, - help='Search with regular expression match by name') -@utils.arg( - '--instance-name', - dest='instance_name', - metavar='', - default=None, - help='Search with regular expression match by server name.') -@utils.arg( - '--instance_name', - help=argparse.SUPPRESS) -@utils.arg( - '--status', - dest='status', - metavar='', - default=None, - help='Search by server status') -@utils.arg( - '--flavor', - dest='flavor', - metavar='', - default=None, - help='Search by flavor name or ID') -@utils.arg( - '--image', - dest='image', - metavar='', - default=None, - help='Search by image name or ID') -@utils.arg( - '--host', - dest='host', - metavar='', - default=None, - help='Search servers by hostname to which they are assigned ' - '(Admin only).') -@utils.arg( - '--all-tenants', - dest='all_tenants', - metavar='<0|1>', - nargs='?', - type=int, - const=1, - default=int(strutils.bool_from_string( - os.environ.get("ALL_TENANTS", 'false'), True)), - help='Display information from all tenants (Admin only).') -@utils.arg( - '--all_tenants', - nargs='?', - type=int, - const=1, - help=argparse.SUPPRESS) -@utils.arg( - '--tenant', - # nova db searches by project_id - dest='tenant', - metavar='', - nargs='?', - help='Display information from single tenant (Admin only).') -@utils.arg( - '--fields', - default=None, - metavar='', - help='Comma-separated list of fields to display. ' - 'Use the show command to see which fields are available.') -@utils.arg( - '--deleted', - dest='deleted', - action="store_true", - default=False, - help='Only display deleted servers (Admin only).') -@utils.arg( - '--minimal', - dest='minimal', - action="store_true", - default=False, - help='Get only uuid and name.') -def do_list(cs, args): - """List active servers.""" - imageid = None - flavorid = None - if args.image: - imageid = _find_image(cs, args.image).id - if args.flavor: - flavorid = _find_flavor(cs, args.flavor).id - search_opts = { - 'all_tenants': args.all_tenants, - 'reservation_id': args.reservation_id, - 'ip': args.ip, - 'ip6': args.ip6, - 'name': args.name, - 'image': imageid, - 'flavor': flavorid, - 'status': args.status, - 'tenant_id': args.tenant, - 'host': args.host, - 'deleted': args.deleted, - 'instance_name': args.instance_name} - - filters = {'flavor': lambda f: f['id'], - 'security_groups': utils._format_security_groups} - - formatters = {} - field_titles = [] - if args.fields: - for field in args.fields.split(','): - field_title, formatter = utils._make_field_formatter(field, - filters) - field_titles.append(field_title) - formatters[field_title] = formatter - - id_col = 'ID' - - detailed = not args.minimal - - servers = cs.servers.list(detailed=detailed, - search_opts=search_opts) - convert = [('os-extended-server-attributes:hypervisor_hostname', 'host'), - ('os-extended-status:task_state', 'task_state'), - ('os-extended-server-attributes:instance_name', - 'instance_name'), - ('os-extended-status:power_state', 'power_state')] - _translate_keys(servers, convert) - _translate_extended_states(servers) - if args.minimal: - columns = [ - id_col, - 'Name'] - elif field_titles: - columns = [id_col] + field_titles - else: - columns = [ - id_col, - 'Name', - 'Status', - 'Task State', - 'Power State', - 'Networks' - ] - formatters['Networks'] = utils._format_servers_list_networks - utils.print_list(servers, columns, - formatters, sortby_index=1) - - -@utils.arg( - '--hard', - dest='reboot_type', - action='store_const', - const=servers.REBOOT_HARD, - default=servers.REBOOT_SOFT, - help='Perform a hard reboot (instead of a soft one).') -@utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg( - '--poll', - dest='poll', - action="store_true", - default=False, - help='Poll until reboot is complete.') -def do_reboot(cs, args): - """Reboot a server.""" - server = _find_server(cs, args.server) - server.reboot(args.reboot_type) - - if args.poll: - _poll_for_status(cs.servers.get, server.id, 'rebooting', ['active'], - show_progress=False) - - -@utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg('image', metavar='', help="Name or ID of new image.") -@utils.arg( - '--rebuild-password', - dest='rebuild_password', - metavar='', - default=False, - help="Set the provided password on the rebuild server.") -@utils.arg( - '--rebuild_password', - help=argparse.SUPPRESS) -@utils.arg( - '--poll', - dest='poll', - action="store_true", - default=False, - help='Report the server rebuild progress until it completes.') -@utils.arg( - '--minimal', - dest='minimal', - action="store_true", - default=False, - help='Skips flavor/image lookups when showing servers') -def do_rebuild(cs, args): - """Shutdown, re-image, and re-boot a server.""" - server = _find_server(cs, args.server) - image = _find_image(cs, args.image) - - if args.rebuild_password is not False: - _password = args.rebuild_password - else: - _password = None - - kwargs = utils.get_resource_manager_extra_kwargs(do_rebuild, args) - server = server.rebuild(image, _password, **kwargs) - _print_server(cs, args, server) - - if args.poll: - _poll_for_status(cs.servers.get, server.id, 'rebuilding', ['active']) - - -@utils.arg('server', metavar='', - help='Name (old name) or ID of server.') -@utils.arg('name', metavar='', help='New name for the server.') -def do_rename(cs, args): - """Rename a server.""" - _find_server(cs, args.server).update(name=args.name) - - -@utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg('flavor', metavar='', help="Name or ID of new flavor.") -@utils.arg( - '--poll', - dest='poll', - action="store_true", - default=False, - help='Report the server resize progress until it completes.') -def do_resize(cs, args): - """Resize a server.""" - server = _find_server(cs, args.server) - flavor = _find_flavor(cs, args.flavor) - kwargs = utils.get_resource_manager_extra_kwargs(do_resize, args) - server.resize(flavor, **kwargs) - if args.poll: - _poll_for_status(cs.servers.get, server.id, 'resizing', - ['active', 'verify_resize']) - - -@utils.arg('server', metavar='', help='Name or ID of server.') -def do_resize_confirm(cs, args): - """Confirm a previous resize.""" - _find_server(cs, args.server).confirm_resize() - - -@utils.arg('server', metavar='', help='Name or ID of server.') -def do_resize_revert(cs, args): - """Revert a previous resize (and return to the previous VM).""" - _find_server(cs, args.server).revert_resize() - - -@utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg( - '--poll', - dest='poll', - action="store_true", - default=False, - help='Report the server migration progress until it completes.') -def do_migrate(cs, args): - """Migrate a server. The new host will be selected by the scheduler.""" - server = _find_server(cs, args.server) - server.migrate() - - if args.poll: - _poll_for_status(cs.servers.get, server.id, 'migrating', - ['active', 'verify_resize']) - - -@utils.arg('server', metavar='', help='Name or ID of server.') -def do_pause(cs, args): - """Pause a server.""" - _find_server(cs, args.server).pause() - - -@utils.arg('server', metavar='', help='Name or ID of server.') -def do_unpause(cs, args): - """Unpause a server.""" - _find_server(cs, args.server).unpause() - - -@utils.arg('server', metavar='', help='Name or ID of server.') -def do_stop(cs, args): - """Stop a server.""" - _find_server(cs, args.server).stop() - - -@utils.arg('server', metavar='', help='Name or ID of server.') -def do_start(cs, args): - """Start a server.""" - _find_server(cs, args.server).start() - - -@utils.arg('server', metavar='', help='Name or ID of server.') -def do_lock(cs, args): - """Lock a server.""" - _find_server(cs, args.server).lock() - - -@utils.arg('server', metavar='', help='Name or ID of server.') -def do_unlock(cs, args): - """Unlock a server.""" - _find_server(cs, args.server).unlock() - - -@utils.arg('server', metavar='', help='Name or ID of server.') -def do_suspend(cs, args): - """Suspend a server.""" - _find_server(cs, args.server).suspend() - - -@utils.arg('server', metavar='', help='Name or ID of server.') -def do_resume(cs, args): - """Resume a server.""" - _find_server(cs, args.server).resume() - - -@utils.arg('server', metavar='', help='Name or ID of server.') -def do_rescue(cs, args): - """Reboots a server into rescue mode, which starts the machine - from the initial image, attaching the current boot disk as secondary. - """ - utils.print_dict(_find_server(cs, args.server).rescue()[1]) - - -@utils.arg('server', metavar='', help='Name or ID of server.') -def do_unrescue(cs, args): - """Restart the server from normal boot disk again.""" - _find_server(cs, args.server).unrescue() - - -@utils.arg('server', metavar='', help='Name or ID of server.') -def do_diagnostics(cs, args): - """Retrieve server diagnostics.""" - server = _find_server(cs, args.server) - utils.print_dict(cs.servers.diagnostics(server)[1], wrap=80) - - -@utils.arg('server', metavar='', help='Name or ID of server.') -def do_root_password(cs, args): - """ - Change the root password for a server. - """ - server = _find_server(cs, args.server) - p1 = getpass.getpass('New password: ') - p2 = getpass.getpass('Again: ') - if p1 != p2: - raise exceptions.CommandError("Passwords do not match.") - server.change_password(p1) - - -@utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg('name', metavar='', help='Name of snapshot.') -@utils.arg( - '--show', - dest='show', - action="store_true", - default=False, - help='Print image info.') -@utils.arg( - '--poll', - dest='poll', - action="store_true", - default=False, - help='Report the snapshot progress and poll until image creation is ' - 'complete.') -def do_image_create(cs, args): - """Create a new image by taking a snapshot of a running server.""" - server = _find_server(cs, args.server) - image_uuid = cs.servers.create_image(server, args.name) - - if args.poll: - _poll_for_status(cs.images.get, image_uuid, 'snapshotting', - ['active']) - - # NOTE(sirp): A race-condition exists between when the image finishes - # uploading and when the servers's `task_state` is cleared. To account - # for this, we need to poll a second time to ensure the `task_state` is - # cleared before returning, ensuring that a snapshot taken immediately - # after this function returns will succeed. - # - # A better long-term solution will be to separate 'snapshotting' and - # 'image-uploading' in Nova and clear the task-state once the VM - # snapshot is complete but before the upload begins. - task_state_field = "OS-EXT-STS:task_state" - if hasattr(server, task_state_field): - _poll_for_status(cs.servers.get, server.id, 'image_snapshot', - [None], status_field=task_state_field, - show_progress=False, silent=True) - - if args.show: - _print_image(cs.images.get(image_uuid)) - - -@utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg('name', metavar='', help='Name of the backup image.') -@utils.arg('backup_type', metavar='', - help='The backup type, like "daily" or "weekly".') -@utils.arg('rotation', metavar='', - help='Int parameter representing how many backups to keep around.') -def do_backup(cs, args): - """Backup a server by creating a 'backup' type snapshot.""" - _find_server(cs, args.server).backup(args.name, - args.backup_type, - args.rotation) - - -@utils.arg( - 'server', - metavar='', - help="Name or ID of server") -@utils.arg( - 'action', - metavar='', - choices=['set', 'delete'], - help="Actions: 'set' or 'delete'") -@utils.arg( - 'metadata', - metavar='', - nargs='+', - action='append', - default=[], - help='Metadata to set or delete (only key is necessary on delete)') -def do_meta(cs, args): - """Set or Delete metadata on a server.""" - server = _find_server(cs, args.server) - metadata = _extract_metadata(args) - - if args.action == 'set': - cs.servers.set_meta(server, metadata) - elif args.action == 'delete': - cs.servers.delete_meta(server, metadata.keys()) - - -def _print_server(cs, args, server=None): - # By default when searching via name we will do a - # findall(name=blah) and due a REST /details which is not the same - # as a .get() and doesn't get the information about flavors and - # images. This fix it as we redo the call with the id which does a - # .get() to get all informations. - if not server: - server = _find_server(cs, args.server) - - minimal = getattr(args, "minimal", False) - - networks = server.networks - info = server._info.copy() - for network_label, address_list in networks.items(): - info['%s network' % network_label] = ', '.join(address_list) - - flavor = info.get('flavor', {}) - flavor_id = flavor.get('id', '') - if minimal: - info['flavor'] = flavor_id - else: - info['flavor'] = '%s (%s)' % (_find_flavor(cs, flavor_id).name, - flavor_id) - - if 'security_groups' in info: - # when we have multiple nics the info will include the - # security groups N times where N == number of nics. Be nice - # and only display it once. - info['security_groups'] = ', '.join( - sorted(set(group['name'] for group in info['security_groups']))) - - image = info.get('image', {}) - if image: - image_id = image.get('id', '') - if minimal: - info['image'] = image_id - else: - try: - info['image'] = '%s (%s)' % (_find_image(cs, image_id).name, - image_id) - except Exception: - info['image'] = '%s (%s)' % ("Image not found", image_id) - else: # Booted from volume - info['image'] = "Attempt to boot from volume - no image supplied" - - info.pop('links', None) - info.pop('addresses', None) - - utils.print_dict(info) - - -@utils.arg( - '--minimal', - dest='minimal', - action="store_true", - default=False, - help='Skips flavor/image lookups when showing servers') -@utils.arg('server', metavar='', help='Name or ID of server.') -def do_show(cs, args): - """Show details about the given server.""" - _print_server(cs, args) - - -@utils.arg('server', metavar='', nargs='+', - help='Name or ID of server(s).') -def do_delete(cs, args): - """Immediately shut down and delete specified server(s).""" - failure_flag = False - - for server in args.server: - try: - _find_server(cs, server).delete() - print("Request to delete server %s has been accepted." % server) - except Exception as e: - failure_flag = True - print(e) - - if failure_flag: - raise exceptions.CommandError("Unable to delete the specified " - "server(s).") - - -def _find_server(cs, server): - """Get a server by name or ID.""" - return utils.find_resource(cs.servers, server) - - -def _find_image(cs, image): - """Get an image by name or ID.""" - return utils.find_resource(cs.images, image) - - -def _find_flavor(cs, flavor): - """Get a flavor by name, ID, or RAM size.""" - try: - return utils.find_resource(cs.flavors, flavor, is_public=None) - except exceptions.NotFound: - return cs.flavors.find(ram=flavor) - - -@utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg( - 'network_id', - metavar='', - help='Network ID.') -def do_add_fixed_ip(cs, args): - """Add new IP address on a network to server.""" - server = _find_server(cs, args.server) - server.add_fixed_ip(args.network_id) - - -@utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg('address', metavar='
', help='IP Address.') -def do_remove_fixed_ip(cs, args): - """Remove an IP address from a server.""" - server = _find_server(cs, args.server) - server.remove_fixed_ip(args.address) - - -def _translate_availability_zone_keys(collection): - _translate_keys(collection, - [('zone_name', 'name'), ('zone_state', 'status')]) - - -@utils.arg( - 'server', - metavar='', - help='Name or ID of server.') -@utils.arg( - 'volume', - metavar='', - help='ID of the volume to attach.') -@utils.arg( - 'device', metavar='', default=None, nargs='?', - help='Name of the device e.g. /dev/vdb. ' - 'Use "auto" for autoassign (if supported)') -@utils.arg( - 'disk_bus', - metavar='', - default=None, - nargs='?', - help='The disk bus e.g. ide of the volume (optional).') -@utils.arg( - 'device_type', - metavar='', - default=None, - nargs='?', - help='The device type e.g. cdrom of the volume (optional).') -def do_volume_attach(cs, args): - """Attach a volume to a server.""" - if args.device == 'auto': - args.device = None - - cs.volumes.attach_server_volume(_find_server(cs, args.server).id, - args.volume, args.device, args.disk_bus, - args.device_type) - - -@utils.arg( - 'server', - metavar='', - help='Name or ID of server.') -@utils.arg( - 'attachment_id', - metavar='', - help='Attachment ID of the volume.') -@utils.arg( - 'new_volume', - metavar='', - help='ID of the volume to attach.') -def do_volume_update(cs, args): - """Update volume attachment.""" - cs.volumes.update_server_volume(_find_server(cs, args.server).id, - args.attachment_id, args.new_volume) - - -@utils.arg( - 'server', - metavar='', - help='Name or ID of server.') -@utils.arg( - 'attachment_id', - metavar='', - help='ID of the volume to detach.') -def do_volume_detach(cs, args): - """Detach a volume from a server.""" - cs.volumes.delete_server_volume(_find_server(cs, args.server).id, - args.attachment_id) - - -@utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg( - 'console_type', - metavar='', - help='Type of vnc console ("novnc" or "xvpvnc").') -def do_get_vnc_console(cs, args): - """Get a vnc console to a server.""" - server = _find_server(cs, args.server) - data = server.get_vnc_console(args.console_type) - - class VNCConsole: - def __init__(self, console_dict): - self.type = console_dict['type'] - self.url = console_dict['url'] - - utils.print_list([VNCConsole(data['console'])], ['Type', 'Url']) - - -@utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg( - 'console_type', - metavar='', - help='Type of spice console ("spice-html5").') -def do_get_spice_console(cs, args): - """Get a spice console to a server.""" - server = _find_server(cs, args.server) - data = server.get_spice_console(args.console_type) - - class SPICEConsole: - def __init__(self, console_dict): - self.type = console_dict['type'] - self.url = console_dict['url'] - - utils.print_list([SPICEConsole(data['console'])], ['Type', 'Url']) - - -@utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg( - 'console_type', - metavar='', - help='Type of rdp console ("rdp-html5").') -def do_get_rdp_console(cs, args): - """Get a rdp console to a server.""" - server = _find_server(cs, args.server) - data = server.get_rdp_console(args.console_type) - - class RDPConsole: - def __init__(self, console_dict): - self.type = console_dict['type'] - self.url = console_dict['url'] - - utils.print_list([RDPConsole(data['console'])], ['Type', 'Url']) - - -@utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg( - 'private_key', - metavar='', - help='Private key (used locally to decrypt password) (Optional). ' - 'When specified, the command displays the clear (decrypted) VM ' - 'password. When not specified, the ciphered VM password is ' - 'displayed.', - nargs='?', - default=None) -def do_get_password(cs, args): - """Get password for a server.""" - server = _find_server(cs, args.server) - data = server.get_password(args.private_key) - print(data) - - -@utils.arg('server', metavar='', help='Name or ID of server.') -def do_clear_password(cs, args): - """Clear password for a server.""" - server = _find_server(cs, args.server) - server.clear_password() - - -def _print_floating_ip_list(floating_ips): - convert = [('instance_id', 'server_id')] - _translate_keys(floating_ips, convert) - - utils.print_list(floating_ips, ['Ip', 'Server Id', 'Fixed Ip', 'Pool']) - - -@utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg('--length', - metavar='', - default=None, - help='Length in lines to tail.') -def do_console_log(cs, args): - """Get console log output of a server.""" - server = _find_server(cs, args.server) - data = server.get_console_output(length=args.length) - print(data) - - -@utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg('address', metavar='
', help='IP Address.') -@utils.arg('--fixed-address', - metavar='', - default=None, - help='Fixed IP Address to associate with.') -def do_add_floating_ip(cs, args): - """Add a floating IP address to a server.""" - server = _find_server(cs, args.server) - server.add_floating_ip(args.address, args.fixed_address) - - -@utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg('address', metavar='
', help='IP Address.') -def do_remove_floating_ip(cs, args): - """Remove a floating IP address from a server.""" - server = _find_server(cs, args.server) - server.remove_floating_ip(args.address) - - -@utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg('secgroup', metavar='', help='Name of Security Group.') -def do_add_secgroup(cs, args): - """Add a Security Group to a server.""" - server = _find_server(cs, args.server) - server.add_security_group(args.secgroup) - - -@utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg('secgroup', metavar='', help='Name of Security Group.') -def do_remove_secgroup(cs, args): - """Remove a Security Group from a server.""" - server = _find_server(cs, args.server) - server.remove_security_group(args.secgroup) - - -@utils.arg('pool', - metavar='', - help='Name of Floating IP Pool. (Optional)', - nargs='?', - default=None) -def do_floating_ip_create(cs, args): - """Allocate a floating IP for the current tenant.""" - _print_floating_ip_list([cs.floating_ips.create(pool=args.pool)]) - - -@utils.arg('address', metavar='
', help='IP of Floating Ip.') -def do_floating_ip_delete(cs, args): - """De-allocate a floating IP.""" - floating_ips = cs.floating_ips.list() - for floating_ip in floating_ips: - if floating_ip.ip == args.address: - return cs.floating_ips.delete(floating_ip.id) - raise exceptions.CommandError("Floating ip %s not found." % args.address) - - -def do_floating_ip_list(cs, _args): - """List floating ips for this tenant.""" - _print_floating_ip_list(cs.floating_ips.list()) - - -def do_floating_ip_pool_list(cs, _args): - """List all floating ip pools.""" - utils.print_list(cs.floating_ip_pools.list(), ['name']) - - -@utils.arg('--host', dest='host', metavar='', default=None, - help='Filter by host') -def do_floating_ip_bulk_list(cs, args): - """List all floating ips.""" - utils.print_list(cs.floating_ips_bulk.list(args.host), ['project_id', - 'address', - 'instance_uuid', - 'pool', - 'interface']) - - -@utils.arg('ip_range', metavar='', help='Address range to create') -@utils.arg('--pool', dest='pool', metavar='', default=None, - help='Pool for new Floating IPs') -@utils.arg('--interface', metavar='', default=None, - help='Interface for new Floating IPs') -def do_floating_ip_bulk_create(cs, args): - """Bulk create floating ips by range.""" - cs.floating_ips_bulk.create(args.ip_range, args.pool, args.interface) - - -@utils.arg('ip_range', metavar='', help='Address range to delete') -def do_floating_ip_bulk_delete(cs, args): - """Bulk delete floating ips by range.""" - cs.floating_ips_bulk.delete(args.ip_range) - - -def _print_dns_list(dns_entries): - utils.print_list(dns_entries, ['ip', 'name', 'domain']) - - -def _print_domain_list(domain_entries): - utils.print_list(domain_entries, ['domain', 'scope', - 'project', 'availability_zone']) - - -def do_dns_domains(cs, args): - """Print a list of available dns domains.""" - domains = cs.dns_domains.domains() - _print_domain_list(domains) - - -@utils.arg('domain', metavar='', help='DNS domain') -@utils.arg('--ip', metavar='', help='ip address', default=None) -@utils.arg('--name', metavar='', help='DNS name', default=None) -def do_dns_list(cs, args): - """List current DNS entries for domain and ip or domain and name.""" - if not (args.ip or args.name): - raise exceptions.CommandError("You must specify either --ip or --name") - if args.name: - entry = cs.dns_entries.get(args.domain, args.name) - _print_dns_list([entry]) - else: - entries = cs.dns_entries.get_for_ip(args.domain, - ip=args.ip) - _print_dns_list(entries) - - -@utils.arg('ip', metavar='', help='ip address') -@utils.arg('name', metavar='', help='DNS name') -@utils.arg('domain', metavar='', help='DNS domain') -@utils.arg('--type', metavar='', help='dns type (e.g. "A")', default='A') -def do_dns_create(cs, args): - """Create a DNS entry for domain, name and ip.""" - cs.dns_entries.create(args.domain, args.name, args.ip, args.type) - - -@utils.arg('domain', metavar='', help='DNS domain') -@utils.arg('name', metavar='', help='DNS name') -def do_dns_delete(cs, args): - """Delete the specified DNS entry.""" - cs.dns_entries.delete(args.domain, args.name) - - -@utils.arg('domain', metavar='', help='DNS domain') -def do_dns_delete_domain(cs, args): - """Delete the specified DNS domain.""" - cs.dns_domains.delete(args.domain) - - -@utils.arg('domain', metavar='', help='DNS domain') -@utils.arg( - '--availability-zone', - metavar='', - default=None, - help='Limit access to this domain to servers ' - 'in the specified availability zone.') -@utils.arg( - '--availability_zone', - help=argparse.SUPPRESS) -def do_dns_create_private_domain(cs, args): - """Create the specified DNS domain.""" - cs.dns_domains.create_private(args.domain, - args.availability_zone) - - -@utils.arg('domain', metavar='', help='DNS domain') -@utils.arg('--project', metavar='', - help='Limit access to this domain to users ' - 'of the specified project.', - default=None) -def do_dns_create_public_domain(cs, args): - """Create the specified DNS domain.""" - cs.dns_domains.create_public(args.domain, - args.project) - - -def _print_secgroup_rules(rules): - class FormattedRule: - def __init__(self, obj): - items = (obj if isinstance(obj, dict) else obj._info).items() - for k, v in items: - if k == 'ip_range': - v = v.get('cidr') - elif k == 'group': - k = 'source_group' - v = v.get('name') - if v is None: - v = '' - - setattr(self, k, v) - - rules = [FormattedRule(rule) for rule in rules] - utils.print_list(rules, ['IP Protocol', 'From Port', 'To Port', - 'IP Range', 'Source Group']) - - -def _print_secgroups(secgroups): - utils.print_list(secgroups, ['Id', 'Name', 'Description']) - - -def _get_secgroup(cs, secgroup): - # Check secgroup is an ID - if uuidutils.is_uuid_like(strutils.safe_encode(secgroup)): - try: - return cs.security_groups.get(secgroup) - except exceptions.NotFound: - pass - - # Check secgroup as a name - match_found = False - for s in cs.security_groups.list(): - encoding = (locale.getpreferredencoding() or - sys.stdin.encoding or 'UTF-8') - s.name = s.name.encode(encoding) - if secgroup == s.name: - if match_found is not False: - msg = ("Multiple security group matches found for name" - " '%s', use an ID to be more specific." % secgroup) - raise exceptions.NoUniqueMatch(msg) - match_found = s - if match_found is False: - raise exceptions.CommandError("Secgroup ID or name '%s' not found." - % secgroup) - return match_found - - -@utils.arg( - 'secgroup', - metavar='', - help='ID or name of security group.') -@utils.arg( - 'ip_proto', - metavar='', - help='IP protocol (icmp, tcp, udp).') -@utils.arg( - 'from_port', - metavar='', - help='Port at start of range.') -@utils.arg( - 'to_port', - metavar='', - help='Port at end of range.') -@utils.arg('cidr', metavar='', help='CIDR for address range.') -def do_secgroup_add_rule(cs, args): - """Add a rule to a security group.""" - secgroup = _get_secgroup(cs, args.secgroup) - rule = cs.security_group_rules.create(secgroup.id, - args.ip_proto, - args.from_port, - args.to_port, - args.cidr) - _print_secgroup_rules([rule]) - - -@utils.arg( - 'secgroup', - metavar='', - help='ID or name of security group.') -@utils.arg( - 'ip_proto', - metavar='', - help='IP protocol (icmp, tcp, udp).') -@utils.arg( - 'from_port', - metavar='', - help='Port at start of range.') -@utils.arg( - 'to_port', - metavar='', - help='Port at end of range.') -@utils.arg('cidr', metavar='', help='CIDR for address range.') -def do_secgroup_delete_rule(cs, args): - """Delete a rule from a security group.""" - secgroup = _get_secgroup(cs, args.secgroup) - for rule in secgroup.rules: - if (rule['ip_protocol'] and - rule['ip_protocol'].upper() == args.ip_proto.upper() and - rule['from_port'] == int(args.from_port) and - rule['to_port'] == int(args.to_port) and - rule['ip_range']['cidr'] == args.cidr): - _print_secgroup_rules([rule]) - return cs.security_group_rules.delete(rule['id']) - - raise exceptions.CommandError("Rule not found") - - -@utils.arg('name', metavar='', help='Name of security group.') -@utils.arg('description', metavar='', - help='Description of security group.') -def do_secgroup_create(cs, args): - """Create a security group.""" - secgroup = cs.security_groups.create(args.name, args.description) - _print_secgroups([secgroup]) - - -@utils.arg( - 'secgroup', - metavar='', - help='ID or name of security group.') -@utils.arg('name', metavar='', help='Name of security group.') -@utils.arg('description', metavar='', - help='Description of security group.') -def do_secgroup_update(cs, args): - """Update a security group.""" - sg = _get_secgroup(cs, args.secgroup) - secgroup = cs.security_groups.update(sg, args.name, args.description) - _print_secgroups([secgroup]) - - -@utils.arg( - 'secgroup', - metavar='', - help='ID or name of security group.') -def do_secgroup_delete(cs, args): - """Delete a security group.""" - secgroup = _get_secgroup(cs, args.secgroup) - cs.security_groups.delete(secgroup) - _print_secgroups([secgroup]) - - -@utils.arg( - '--all-tenants', - dest='all_tenants', - metavar='<0|1>', - nargs='?', - type=int, - const=1, - default=int(strutils.bool_from_string( - os.environ.get("ALL_TENANTS", 'false'), True)), - help='Display information from all tenants (Admin only).') -@utils.arg( - '--all_tenants', - nargs='?', - type=int, - const=1, - help=argparse.SUPPRESS) -def do_secgroup_list(cs, args): - """List security groups for the current tenant.""" - search_opts = {'all_tenants': args.all_tenants} - columns = ['Id', 'Name', 'Description'] - if args.all_tenants: - columns.append('Tenant_ID') - groups = cs.security_groups.list(search_opts=search_opts) - utils.print_list(groups, columns) - - -@utils.arg( - 'secgroup', - metavar='', - help='ID or name of security group.') -def do_secgroup_list_rules(cs, args): - """List rules for a security group.""" - secgroup = _get_secgroup(cs, args.secgroup) - _print_secgroup_rules(secgroup.rules) - - -@utils.arg( - 'secgroup', - metavar='', - help='ID or name of security group.') -@utils.arg( - 'source_group', - metavar='', - help='ID or name of source group.') -@utils.arg( - 'ip_proto', - metavar='', - help='IP protocol (icmp, tcp, udp).') -@utils.arg( - 'from_port', - metavar='', - help='Port at start of range.') -@utils.arg( - 'to_port', - metavar='', - help='Port at end of range.') -def do_secgroup_add_group_rule(cs, args): - """Add a source group rule to a security group.""" - secgroup = _get_secgroup(cs, args.secgroup) - source_group = _get_secgroup(cs, args.source_group) - params = {} - params['group_id'] = source_group.id - - if args.ip_proto or args.from_port or args.to_port: - if not (args.ip_proto and args.from_port and args.to_port): - raise exceptions.CommandError( - "ip_proto, from_port, and to_port must be specified together") - params['ip_protocol'] = args.ip_proto.upper() - params['from_port'] = args.from_port - params['to_port'] = args.to_port - - rule = cs.security_group_rules.create(secgroup.id, **params) - _print_secgroup_rules([rule]) - - -@utils.arg( - 'secgroup', - metavar='', - help='ID or name of security group.') -@utils.arg( - 'source_group', - metavar='', - help='ID or name of source group.') -@utils.arg( - 'ip_proto', - metavar='', - help='IP protocol (icmp, tcp, udp).') -@utils.arg( - 'from_port', - metavar='', - help='Port at start of range.') -@utils.arg( - 'to_port', - metavar='', - help='Port at end of range.') -def do_secgroup_delete_group_rule(cs, args): - """Delete a source group rule from a security group.""" - secgroup = _get_secgroup(cs, args.secgroup) - source_group = _get_secgroup(cs, args.source_group) - params = {} - params['group_name'] = source_group.name - - if args.ip_proto or args.from_port or args.to_port: - if not (args.ip_proto and args.from_port and args.to_port): - raise exceptions.CommandError( - "ip_proto, from_port, and to_port must be specified together") - params['ip_protocol'] = args.ip_proto.upper() - params['from_port'] = int(args.from_port) - params['to_port'] = int(args.to_port) - - for rule in secgroup.rules: - if (rule.get('ip_protocol').upper() == params.get( - 'ip_protocol').upper() and - rule.get('from_port') == params.get('from_port') and - rule.get('to_port') == params.get('to_port') and - rule.get('group', {}).get('name') == params.get('group_name')): - return cs.security_group_rules.delete(rule['id']) - - raise exceptions.CommandError("Rule not found") - - -@utils.arg('name', metavar='', help='Name of key.') -@utils.arg( - '--pub-key', - metavar='', - default=None, - help='Path to a public ssh key.') -@utils.arg( - '--pub_key', - help=argparse.SUPPRESS) -def do_keypair_add(cs, args): - """Create a new key pair for use with servers.""" - name = args.name - pub_key = args.pub_key - - if pub_key: - try: - with open(os.path.expanduser(pub_key)) as f: - pub_key = f.read() - except IOError as e: - raise exceptions.CommandError( - "Can't open or read '%s': %s" % (pub_key, e)) - - keypair = cs.keypairs.create(name, pub_key) - - if not pub_key: - private_key = keypair.private_key - print(private_key) - - -@utils.arg('name', metavar='', help='Keypair name to delete.') -def do_keypair_delete(cs, args): - """Delete keypair given by its name.""" - name = _find_keypair(cs, args.name) - cs.keypairs.delete(name) - - -def do_keypair_list(cs, args): - """Print a list of keypairs for a user""" - keypairs = cs.keypairs.list() - columns = ['Name', 'Fingerprint'] - utils.print_list(keypairs, columns) - - -def _print_keypair(keypair): - kp = keypair._info.copy() - pk = kp.pop('public_key') - utils.print_dict(kp) - print("Public key: %s" % pk) - - -@utils.arg( - 'keypair', - metavar='', - help="Name or ID of keypair") -def do_keypair_show(cs, args): - """Show details about the given keypair.""" - keypair = _find_keypair(cs, args.keypair) - _print_keypair(keypair) - - -def _find_keypair(cs, keypair): - """Get a keypair by name or ID.""" - return utils.find_resource(cs.keypairs, keypair) - - -@utils.arg('--start', metavar='', - help='Usage range start date ex 2012-01-20 (default: 4 weeks ago)', - default=None) -@utils.arg('--end', metavar='', - help='Usage range end date, ex 2012-01-20 (default: tomorrow) ', - default=None) -def do_usage_list(cs, args): - """List usage data for all tenants.""" - dateformat = "%Y-%m-%d" - rows = ["Tenant ID", "Servers", "RAM MB-Hours", "CPU Hours", - "Disk GB-Hours"] - - now = timeutils.utcnow() - - if args.start: - start = datetime.datetime.strptime(args.start, dateformat) - else: - start = now - datetime.timedelta(weeks=4) - - if args.end: - end = datetime.datetime.strptime(args.end, dateformat) - else: - end = now + datetime.timedelta(days=1) - - def simplify_usage(u): - simplerows = map(lambda x: x.lower().replace(" ", "_"), rows) - - setattr(u, simplerows[0], u.tenant_id) - setattr(u, simplerows[1], "%d" % len(u.server_usages)) - setattr(u, simplerows[2], "%.2f" % u.total_memory_mb_usage) - setattr(u, simplerows[3], "%.2f" % u.total_vcpus_usage) - setattr(u, simplerows[4], "%.2f" % u.total_local_gb_usage) - - usage_list = cs.usage.list(start, end, detailed=True) - - print("Usage from %s to %s:" % (start.strftime(dateformat), - end.strftime(dateformat))) - - for usage in usage_list: - simplify_usage(usage) - - utils.print_list(usage_list, rows) - - -@utils.arg('--start', metavar='', - help='Usage range start date ex 2012-01-20 (default: 4 weeks ago)', - default=None) -@utils.arg('--end', metavar='', - help='Usage range end date, ex 2012-01-20 (default: tomorrow) ', - default=None) -@utils.arg('--tenant', metavar='', - default=None, - help='UUID or name of tenant to get usage for.') -def do_usage(cs, args): - """Show usage data for a single tenant.""" - dateformat = "%Y-%m-%d" - rows = ["Servers", "RAM MB-Hours", "CPU Hours", "Disk GB-Hours"] - - now = timeutils.utcnow() - - if args.start: - start = datetime.datetime.strptime(args.start, dateformat) - else: - start = now - datetime.timedelta(weeks=4) - - if args.end: - end = datetime.datetime.strptime(args.end, dateformat) - else: - end = now + datetime.timedelta(days=1) - - def simplify_usage(u): - simplerows = map(lambda x: x.lower().replace(" ", "_"), rows) - - setattr(u, simplerows[0], "%d" % len(u.server_usages)) - setattr(u, simplerows[1], "%.2f" % u.total_memory_mb_usage) - setattr(u, simplerows[2], "%.2f" % u.total_vcpus_usage) - setattr(u, simplerows[3], "%.2f" % u.total_local_gb_usage) - - if args.tenant: - usage = cs.usage.get(args.tenant, start, end) - else: - if isinstance(cs.client, client.SessionClient): - auth = cs.client.auth - project_id = auth.get_auth_ref(cs.client.session).project_id - usage = cs.usage.get(project_id, start, end) - else: - usage = cs.usage.get(cs.client.tenant_id, start, end) - - print("Usage from %s to %s:" % (start.strftime(dateformat), - end.strftime(dateformat))) - - if getattr(usage, 'total_vcpus_usage', None): - simplify_usage(usage) - utils.print_list([usage], rows) - else: - print('None') - - -@utils.arg( - 'pk_filename', - metavar='', - nargs='?', - default='pk.pem', - help='Filename for the private key [Default: pk.pem]') -@utils.arg( - 'cert_filename', - metavar='', - nargs='?', - default='cert.pem', - help='Filename for the X.509 certificate [Default: cert.pem]') -def do_x509_create_cert(cs, args): - """Create x509 cert for a user in tenant.""" - - if os.path.exists(args.pk_filename): - raise exceptions.CommandError("Unable to write privatekey - %s exists." - % args.pk_filename) - if os.path.exists(args.cert_filename): - raise exceptions.CommandError("Unable to write x509 cert - %s exists." - % args.cert_filename) - - certs = cs.certs.create() - - try: - old_umask = os.umask(0o377) - with open(args.pk_filename, 'w') as private_key: - private_key.write(certs.private_key) - print("Wrote private key to %s" % args.pk_filename) - finally: - os.umask(old_umask) - - with open(args.cert_filename, 'w') as cert: - cert.write(certs.data) - print("Wrote x509 certificate to %s" % args.cert_filename) - - -@utils.arg('filename', - metavar='', - nargs='?', - default='cacert.pem', - help='Filename to write the x509 root cert.') -def do_x509_get_root_cert(cs, args): - """Fetch the x509 root cert.""" - if os.path.exists(args.filename): - raise exceptions.CommandError("Unable to write x509 root cert - \ - %s exists." % args.filename) - - with open(args.filename, 'w') as cert: - cacert = cs.certs.get() - cert.write(cacert.data) - print("Wrote x509 root cert to %s" % args.filename) - - -@utils.arg('--hypervisor', metavar='', default=None, - help='type of hypervisor.') -def do_agent_list(cs, args): - """List all builds.""" - result = cs.agents.list(args.hypervisor) - columns = ["Agent_id", "Hypervisor", "OS", "Architecture", "Version", - 'Md5hash', 'Url'] - utils.print_list(result, columns) - - -@utils.arg('os', metavar='', help='type of os.') -@utils.arg('architecture', metavar='', - help='type of architecture') -@utils.arg('version', metavar='', help='version') -@utils.arg('url', metavar='', help='url') -@utils.arg('md5hash', metavar='', help='md5 hash') -@utils.arg('hypervisor', metavar='', default='xen', - help='type of hypervisor.') -def do_agent_create(cs, args): - """Create new agent build.""" - result = cs.agents.create(args.os, args.architecture, - args.version, args.url, - args.md5hash, args.hypervisor) - utils.print_dict(result._info.copy()) - - -@utils.arg('id', metavar='', help='id of the agent-build') -def do_agent_delete(cs, args): - """Delete existing agent build.""" - cs.agents.delete(args.id) - - -@utils.arg('id', metavar='', help='id of the agent-build') -@utils.arg('version', metavar='', help='version') -@utils.arg('url', metavar='', help='url') -@utils.arg('md5hash', metavar='', help='md5hash') -def do_agent_modify(cs, args): - """Modify existing agent build.""" - result = cs.agents.update(args.id, args.version, - args.url, args.md5hash) - utils.print_dict(result._info) - - -def _find_aggregate(cs, aggregate): - """Get a aggregate by name or ID.""" - return utils.find_resource(cs.aggregates, aggregate) - - -def do_aggregate_list(cs, args): - """Print a list of all aggregates.""" - aggregates = cs.aggregates.list() - columns = ['Id', 'Name', 'Availability Zone'] - utils.print_list(aggregates, columns) - - -@utils.arg('name', metavar='', help='Name of aggregate.') -@utils.arg( - 'availability_zone', - metavar='', - default=None, - nargs='?', - help='The availability zone of the aggregate (optional).') -def do_aggregate_create(cs, args): - """Create a new aggregate with the specified details.""" - aggregate = cs.aggregates.create(args.name, args.availability_zone) - _print_aggregate_details(aggregate) - - -@utils.arg('aggregate', metavar='', - help='Name or ID of aggregate to delete.') -def do_aggregate_delete(cs, args): - """Delete the aggregate.""" - aggregate = _find_aggregate(cs, args.aggregate) - cs.aggregates.delete(aggregate) - print("Aggregate %s has been successfully deleted." % aggregate.id) - - -@utils.arg('aggregate', metavar='', - help='Name or ID of aggregate to update.') -@utils.arg('name', metavar='', help='Name of aggregate.') -@utils.arg( - 'availability_zone', - metavar='', - nargs='?', - default=None, - help='The availability zone of the aggregate.') -def do_aggregate_update(cs, args): - """Update the aggregate's name and optionally availability zone.""" - aggregate = _find_aggregate(cs, args.aggregate) - updates = {"name": args.name} - if args.availability_zone: - updates["availability_zone"] = args.availability_zone - - aggregate = cs.aggregates.update(aggregate.id, updates) - print("Aggregate %s has been successfully updated." % aggregate.id) - _print_aggregate_details(aggregate) - - -@utils.arg('aggregate', metavar='', - help='Name or ID of aggregate to update.') -@utils.arg('metadata', - metavar='', - nargs='+', - action='append', - default=[], - help='Metadata to add/update to aggregate') -def do_aggregate_set_metadata(cs, args): - """Update the metadata associated with the aggregate.""" - aggregate = _find_aggregate(cs, args.aggregate) - metadata = _extract_metadata(args) - currentmetadata = getattr(aggregate, 'metadata', {}) - if set(metadata.items()) & set(currentmetadata.items()): - raise exceptions.CommandError("metadata already exists") - for key, value in metadata.items(): - if value is None and key not in currentmetadata: - raise exceptions.CommandError("metadata key %s does not exist" - " hence can not be deleted" - % key) - aggregate = cs.aggregates.set_metadata(aggregate.id, metadata) - print("Metadata has been successfully updated for aggregate %s." % - aggregate.id) - _print_aggregate_details(aggregate) - - -@utils.arg('aggregate', metavar='', help='Name or ID of aggregate.') -@utils.arg('host', metavar='', help='The host to add to the aggregate.') -def do_aggregate_add_host(cs, args): - """Add the host to the specified aggregate.""" - aggregate = _find_aggregate(cs, args.aggregate) - aggregate = cs.aggregates.add_host(aggregate.id, args.host) - print("Host %s has been successfully added for aggregate %s " % - (args.host, aggregate.id)) - _print_aggregate_details(aggregate) - - -@utils.arg('aggregate', metavar='', help='Name or ID of aggregate.') -@utils.arg('host', metavar='', - help='The host to remove from the aggregate.') -def do_aggregate_remove_host(cs, args): - """Remove the specified host from the specified aggregate.""" - aggregate = _find_aggregate(cs, args.aggregate) - aggregate = cs.aggregates.remove_host(aggregate.id, args.host) - print("Host %s has been successfully removed from aggregate %s " % - (args.host, aggregate.id)) - _print_aggregate_details(aggregate) - - -@utils.arg('aggregate', metavar='', help='Name or ID of aggregate.') -def do_aggregate_details(cs, args): - """Show details of the specified aggregate.""" - aggregate = _find_aggregate(cs, args.aggregate) - _print_aggregate_details(aggregate) - - -def _print_aggregate_details(aggregate): - columns = ['Id', 'Name', 'Availability Zone', 'Hosts', 'Metadata'] - - def parser_metadata(fields): - return utils.pretty_choice_dict(getattr(fields, 'metadata', {}) or {}) - - def parser_hosts(fields): - return cliutils.pretty_choice_list(getattr(fields, 'hosts', [])) - - formatters = { - 'Metadata': parser_metadata, - 'Hosts': parser_hosts, - } - utils.print_list([aggregate], columns, formatters=formatters) - - -@utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg( - 'host', metavar='', default=None, nargs='?', - help='destination host name.') -@utils.arg( - '--block-migrate', - action='store_true', - dest='block_migrate', - default=False, - help='True in case of block_migration.\ - (Default=False:live_migration)') -@utils.arg( - '--block_migrate', - action='store_true', - help=argparse.SUPPRESS) -@utils.arg( - '--disk-over-commit', - action='store_true', - dest='disk_over_commit', - default=False, - help='Allow overcommit.(Default=False)') -@utils.arg( - '--disk_over_commit', - action='store_true', - help=argparse.SUPPRESS) -def do_live_migration(cs, args): - """Migrate running server to a new machine.""" - _find_server(cs, args.server).live_migrate(args.host, - args.block_migrate, - args.disk_over_commit) - - -def _server_live_migrate(cs, server, args): - class HostServersLiveMigrateResponse(object): - def __init__(self, server_uuid, live_migration_accepted, - error_message): - self.server_uuid = server_uuid - self.live_migration_accepted = live_migration_accepted - self.error_message = error_message - success = True - error_message = "" - try: - cs.servers.live_migrate(server['id'], args.target_host, - args.block_migrate, args.disk_over_commit) - except Exception as e: - success = False - error_message = "Error while live migrating instance: %s" % e - return HostServersLiveMigrateResponse(server['id'], - success, - error_message) - - -@utils.arg('host', metavar='', help='Name of host.') -@utils.arg( - '--target-host', - metavar='', - default=None, - help='Name of target host.') -@utils.arg( - '--block-migrate', - action='store_true', - default=False, - help='Enable block migration.') -@utils.arg( - '--disk-over-commit', - action='store_true', - default=False, - help='Enable disk overcommit.') -def do_host_evacuate_live(cs, args): - """Live Migrate all instances of the specified host - to other available hosts. - """ - hypervisors = cs.hypervisors.search(args.host) - response = [] - for hyper in hypervisors: - servers = getattr(cs.hypervisors.servers(hyper.id), 'servers', []) - for server in servers: - response.append(_server_live_migrate(cs, server, args)) - utils.print_list(response, ["Server UUID", "Live Migration Accepted", - "Error Message"]) - - -@utils.arg('server', metavar='', nargs='+', - help='Name or ID of server(s).') -@utils.arg('--active', action='store_const', dest='state', - default='error', const='active', - help='Request the server be reset to "active" state instead ' - 'of "error" state (the default).') -def do_reset_state(cs, args): - """Reset the state of a server.""" - failure_flag = False - - for server in args.server: - try: - _find_server(cs, server).reset_state(args.state) - except Exception as e: - failure_flag = True - msg = "Reset state for server %s failed: %s" % (server, e) - print(msg) - - if failure_flag: - msg = "Unable to reset the state for the specified server(s)." - raise exceptions.CommandError(msg) - - -@utils.arg('server', metavar='', help='Name or ID of server.') -def do_reset_network(cs, args): - """Reset network of a server.""" - _find_server(cs, args.server).reset_network() - - -@utils.arg('--host', metavar='', default=None, - help='Name of host.') -@utils.arg('--binary', metavar='', default=None, - help='Service binary.') -def do_service_list(cs, args): - """Show a list of all running services. Filter by host & binary.""" - result = cs.services.list(host=args.host, binary=args.binary) - columns = ["Binary", "Host", "Zone", "Status", "State", "Updated_at"] - # NOTE(sulo): we check if the response has disabled_reason - # so as not to add the column when the extended ext is not enabled. - if hasattr(result[0], 'disabled_reason'): - columns.append("Disabled Reason") - - # NOTE(gtt): After https://review.openstack.org/#/c/39998/ nova will - # show id in response. - if result and hasattr(result[0], 'id'): - columns.insert(0, "Id") - - utils.print_list(result, columns) - - -@utils.arg('host', metavar='', help='Name of host.') -@utils.arg('binary', metavar='', help='Service binary.') -def do_service_enable(cs, args): - """Enable the service.""" - result = cs.services.enable(args.host, args.binary) - utils.print_list([result], ['Host', 'Binary', 'Status']) - - -@utils.arg('host', metavar='', help='Name of host.') -@utils.arg('binary', metavar='', help='Service binary.') -@utils.arg('--reason', metavar='', - help='Reason for disabling service.') -def do_service_disable(cs, args): - """Disable the service.""" - if args.reason: - result = cs.services.disable_log_reason(args.host, args.binary, - args.reason) - utils.print_list([result], ['Host', 'Binary', 'Status', - 'Disabled Reason']) - else: - result = cs.services.disable(args.host, args.binary) - utils.print_list([result], ['Host', 'Binary', 'Status']) - - -@utils.arg('id', metavar='', help='Id of service.') -def do_service_delete(cs, args): - """Delete the service.""" - cs.services.delete(args.id) - - -@utils.arg('fixed_ip', metavar='', help='Fixed IP Address.') -def do_fixed_ip_get(cs, args): - """Retrieve info on a fixed ip.""" - result = cs.fixed_ips.get(args.fixed_ip) - utils.print_list([result], ['address', 'cidr', 'hostname', 'host']) - - -@utils.arg('fixed_ip', metavar='', help='Fixed IP Address.') -def do_fixed_ip_reserve(cs, args): - """Reserve a fixed IP.""" - cs.fixed_ips.reserve(args.fixed_ip) - - -@utils.arg('fixed_ip', metavar='', help='Fixed IP Address.') -def do_fixed_ip_unreserve(cs, args): - """Unreserve a fixed IP.""" - cs.fixed_ips.unreserve(args.fixed_ip) - - -@utils.arg('host', metavar='', help='Name of host.') -def do_host_describe(cs, args): - """Describe a specific host.""" - result = cs.hosts.get(args.host) - columns = ["HOST", "PROJECT", "cpu", "memory_mb", "disk_gb"] - utils.print_list(result, columns) - - -@utils.arg('--zone', metavar='', default=None, - help='Filters the list, returning only those ' - 'hosts in the availability zone .') -@utils.arg('--service-name', metavar='', default=None, - help='Filters the list, returning only those ' - 'hosts providing service .') -def do_host_list(cs, args): - """List all hosts by service.""" - columns = ["host_name", "service", "zone"] - result = cs.hosts.list(args.zone, args.service_name) - utils.print_list(result, columns) - - -@utils.arg('host', metavar='', help='Name of host.') -@utils.arg('--status', metavar='', default=None, dest='status', - help='Either enable or disable a host.') -@utils.arg( - '--maintenance', - metavar='', - default=None, - dest='maintenance', - help='Either put or resume host to/from maintenance.') -def do_host_update(cs, args): - """Update host settings.""" - updates = {} - columns = ["HOST"] - if args.status: - updates['status'] = args.status - columns.append("status") - if args.maintenance: - updates['maintenance_mode'] = args.maintenance - columns.append("maintenance_mode") - result = cs.hosts.update(args.host, updates) - utils.print_list([result], columns) - - -@utils.arg('host', metavar='', help='Name of host.') -@utils.arg('--action', metavar='', dest='action', - choices=['startup', 'shutdown', 'reboot'], - help='A power action: startup, reboot, or shutdown.') -def do_host_action(cs, args): - """Perform a power action on a host.""" - result = cs.hosts.host_action(args.host, args.action) - utils.print_list([result], ['HOST', 'power_action']) - - -def _find_hypervisor(cs, hypervisor): - """Get a hypervisor by name or ID.""" - return utils.find_resource(cs.hypervisors, hypervisor) - - -@utils.arg('--matching', metavar='', default=None, - help='List hypervisors matching the given .') -def do_hypervisor_list(cs, args): - """List hypervisors.""" - columns = ['ID', 'Hypervisor hostname'] - if args.matching: - utils.print_list(cs.hypervisors.search(args.matching), columns) - else: - # Since we're not outputting detail data, choose - # detailed=False for server-side efficiency - utils.print_list(cs.hypervisors.list(False), columns) - - -@utils.arg('hostname', metavar='', - help='The hypervisor hostname (or pattern) to search for.') -def do_hypervisor_servers(cs, args): - """List servers belonging to specific hypervisors.""" - # Get a list of hypervisors first - hypers = cs.hypervisors.search(args.hostname) - - class InstanceOnHyper(object): - def __init__(self, **kwargs): - self.__dict__.update(kwargs) - - # Massage the result into a list to be displayed - servers = [] - for hyper in hypers: - # Get a list of servers for each hypervisor - hyper_host = hyper.hypervisor_hostname - hyper_id = hyper.id - - hyper_servers = cs.hypervisors.servers(hyper_id) - if hasattr(hyper_servers, 'servers'): - print(hyper_servers.servers) - servers.extend([InstanceOnHyper(id=serv['id'], - name=serv['name'], - hypervisor_hostname=hyper_host, - hypervisor_id=hyper_id) - for serv in hyper_servers.servers]) - - # Output the data - utils.print_list(servers, ['ID', 'Name', 'Hypervisor ID', - 'Hypervisor Hostname']) - - -@utils.arg( - 'hypervisor', - metavar='', - help='Name or ID of the hypervisor to show the details of.') -def do_hypervisor_show(cs, args): - """Display the details of the specified hypervisor.""" - hyper = _find_hypervisor(cs, args.hypervisor) - - # Build up the dict - info = hyper._info.copy() - info['service_id'] = info['service']['id'] - info['service_host'] = info['service']['host'] - del info['service'] - - utils.print_dict(info) - - -@utils.arg( - 'hypervisor', - metavar='', - help='Name or ID of the hypervisor to show the uptime of.') -def do_hypervisor_uptime(cs, args): - """Display the uptime of the specified hypervisor.""" - hyper = _find_hypervisor(cs, args.hypervisor) - hyper = cs.hypervisors.uptime(hyper) - - # Output the uptime information - utils.print_dict(hyper._info.copy()) - - -def do_hypervisor_stats(cs, args): - """Get hypervisor statistics over all compute nodes.""" - stats = cs.hypervisors.statistics() - utils.print_dict(stats._info.copy()) - - -def ensure_service_catalog_present(cs): - if not hasattr(cs.client, 'service_catalog'): - # Turn off token caching and re-auth - cs.client.unauthenticate() - cs.client.use_token_cache(False) - cs.client.authenticate() - - -def do_endpoints(cs, _args): - """Discover endpoints that get returned from the authenticate services.""" - if isinstance(cs.client, client.SessionClient): - auth = cs.client.auth - sc = auth.get_Access(cs.client.session).service_catalog - for service in sc.get_data(): - _print_endpoints(service, cs.client.region_name) - else: - ensure_service_catalog_present(cs) - - catalog = cs.client.service_catalog.catalog - region = cs.client.region_name - - for service in catalog['access']['serviceCatalog']: - _print_endpoints(service, region) - - -def _print_endpoints(service, region): - name, endpoints = service["name"], service["endpoints"] - - try: - endpoint = _get_first_endpoint(endpoints, region) - utils.print_dict(endpoint, name) - except LookupError: - print(_("WARNING: %(service)s has no endpoint in %(region)s! " - "Available endpoints for this service:") % - {'service': name, 'region': region}) - for other_endpoint in endpoints: - utils.print_dict(other_endpoint, name) - - -def _get_first_endpoint(endpoints, region): - """Find the first suitable endpoint in endpoints. - - If there is only one endpoint, return it. If there is more than - one endpoint, return the first one with the given region. If there - are no endpoints, or there is more than one endpoint but none of - them match the given region, raise KeyError. - - """ - if len(endpoints) == 1: - return endpoints[0] - else: - for candidate_endpoint in endpoints: - if candidate_endpoint["region"] == region: - return candidate_endpoint - - raise LookupError("No suitable endpoint found") - - -@utils.arg('--wrap', dest='wrap', metavar='', default=64, - help='wrap PKI tokens to a specified length, or 0 to disable') -def do_credentials(cs, _args): - """Show user credentials returned from auth.""" - if isinstance(cs.client, client.SessionClient): - auth = cs.client.auth - sc = auth.get_access(cs.client.session).service_catalog - utils.print_dict(sc.catalog['user'], 'User Credentials', - wrap=int(_args.wrap)) - utils.print_dict(sc.get_token(), 'Token', wrap=int(_args.wrap)) - else: - ensure_service_catalog_present(cs) - catalog = cs.client.service_catalog.catalog - utils.print_dict(catalog['access']['user'], "User Credentials", - wrap=int(_args.wrap)) - utils.print_dict(catalog['access']['token'], "Token", - wrap=int(_args.wrap)) - - -def do_extension_list(cs, _args): - """ - List all the os-api extensions that are available. - """ - extensions = cs.list_extensions.show_all() - fields = ["Name", "Summary", "Alias", "Version"] - utils.print_list(extensions, fields) - - -@utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg( - '--port', - dest='port', - action='store', - type=int, - default=22, - help='Optional flag to indicate which port to use for ssh. ' - '(Default=22)') -@utils.arg( - '--private', - dest='private', - action='store_true', - default=False, - help=argparse.SUPPRESS) -@utils.arg( - '--address-type', - dest='address_type', - action='store', - type=str, - default='floating', - help='Optional flag to indicate which IP type to use. Possible values ' - 'includes fixed and floating (the Default).') -@utils.arg('--network', metavar='', - help='Network to use for the ssh.', default=None) -@utils.arg( - '--ipv6', - dest='ipv6', - action='store_true', - default=False, - help='Optional flag to indicate whether to use an IPv6 address ' - 'attached to a server. (Defaults to IPv4 address)') -@utils.arg('--login', metavar='', help='Login to use.', default="root") -@utils.arg( - '-i', '--identity', - dest='identity', - help='Private key file, same as the -i option to the ssh command.', - default='') -@utils.arg( - '--extra-opts', - dest='extra', - help='Extra options to pass to ssh. see: man ssh', - default='') -def do_ssh(cs, args): - """SSH into a server.""" - if '@' in args.server: - user, server = args.server.split('@', 1) - args.login = user - args.server = server - - addresses = _find_server(cs, args.server).addresses - address_type = "fixed" if args.private else args.address_type - version = 6 if args.ipv6 else 4 - pretty_version = 'IPv%d' % version - - # Select the network to use. - if args.network: - network_addresses = addresses.get(args.network) - if not network_addresses: - msg = _("Server '%(server)s' is not attached to network " - "'%(network)s'") - raise exceptions.ResourceNotFound( - msg % {'server': args.server, 'network': args.network}) - else: - if len(addresses) > 1: - msg = _("Server '%(server)s' is attached to more than one network." - " Please pick the network to use.") - raise exceptions.CommandError(msg % {'server': args.server}) - elif not addresses: - msg = _("Server '%(server)s' is not attached to any network.") - raise exceptions.CommandError(msg % {'server': args.server}) - else: - network_addresses = list(six.itervalues(addresses))[0] - - # Select the address in the selected network. - # If the extension is not present, we assume the address to be floating. - match = lambda addr: all(( - addr.get('version') == version, - addr.get('OS-EXT-IPS:type', 'floating') == address_type)) - matching_addresses = [address.get('addr') - for address in network_addresses if match(address)] - if not any(matching_addresses): - msg = _("No address that would match network '%(network)s'" - " and type '%(address_type)s' of version %(pretty_version)s " - "has been found for server '%(server)s'.") - raise exceptions.ResourceNotFound(msg % { - 'network': args.network, 'address_type': address_type, - 'pretty_version': pretty_version, 'server': args.server}) - elif len(matching_addresses) > 1: - msg = _("More than one %(pretty_version)s %(address_type)s address" - "found.") - raise exceptions.CommandError(msg % {'pretty_version': pretty_version, - 'address_type': address_type}) - else: - ip_address = matching_addresses[0] - - identity = '-i %s' % args.identity if len(args.identity) else '' - - cmd = "ssh -%d -p%d %s %s@%s %s" % (version, args.port, identity, - args.login, ip_address, args.extra) - logger.debug("Executing cmd '%s'", cmd) - os.system(cmd) - - -_quota_resources = ['instances', 'cores', 'ram', - 'fixed_ips', 'metadata_items', 'key_pairs', - 'server_groups', 'server_group_members'] - - -def _quota_show(quotas): - quota_dict = {} - for resource in _quota_resources: - try: - quota_dict[resource] = getattr(quotas, resource) - except AttributeError: - pass - utils.print_dict(quota_dict) - - -def _quota_usage(quotas): - class QuotaObj(object): - def __init__(self, resource, quota_dict): - setattr(self, 'resource', resource) - for (k, v) in six.iteritems(quota_dict): - setattr(self, k, v) - - quota_list = [] - for resource in _quota_resources: - try: - quota_list.append(QuotaObj(resource, getattr(quotas, resource))) - except AttributeError: - pass - utils.print_list(quota_list, ['resource', 'in use', 'limit'], - sortby_index=0) - - -def _quota_update(manager, identifier, args): - updates = {} - for resource in _quota_resources: - val = getattr(args, resource, None) - if val is not None: - updates[resource] = val - - if updates: - # default value of force is None to make sure this client - # will be compatibile with old nova server - manager.update(identifier, **updates) - - -@utils.arg( - '--tenant', - metavar='', - default=None, - help='ID of tenant to list the quotas for.') -def do_quota_show(cs, args): - """List the quotas for a tenant.""" - - if not args.tenant: - _quota_show(cs.quotas.get(cs.client.tenant_id)) - else: - _quota_show(cs.quotas.get(args.tenant)) - - -@utils.arg( - '--tenant', - metavar='', - default=None, - help='ID of tenant to list the quotas for.') -@utils.arg( - '--user', - metavar='', - default=None, - help='ID of user to list the quotas for.') -def do_quota_usage(cs, args): - """List the quotas for a tenant.""" - - tenant = args.tenant or cs.client.tenant_id - _quota_usage(cs.quotas.get(tenant, user_id=args.user, detail=True)) - - -@utils.arg( - '--tenant', - metavar='', - default=None, - help='ID of tenant to list the default quotas for.') -def do_quota_defaults(cs, args): - """List the default quotas for a tenant.""" - - if not args.tenant: - _quota_show(cs.quotas.defaults(cs.client.tenant_id)) - else: - _quota_show(cs.quotas.defaults(args.tenant)) - - -@utils.arg( - 'tenant', - metavar='', - help='ID of tenant to set the quotas for.') -@utils.arg('--instances', - metavar='', - type=int, default=None, - help='New value for the "instances" quota.') -@utils.arg('--cores', - metavar='', - type=int, default=None, - help='New value for the "cores" quota.') -@utils.arg('--ram', - metavar='', - type=int, default=None, - help='New value for the "ram" quota.') -@utils.arg( - '--fixed-ips', - metavar='', - type=int, - default=None, - help='New value for the "fixed-ips" quota.') -@utils.arg( - '--metadata-items', - metavar='', - type=int, - default=None, - help='New value for the "metadata-items" quota.') -@utils.arg( - '--metadata_items', - type=int, - help=argparse.SUPPRESS) -@utils.arg( - '--key-pairs', - metavar='', - type=int, - default=None, - help='New value for the "key-pairs" quota.') -@utils.arg( - '--server-groups', - metavar='', - type=int, - default=None, - help='New value for the "server-groups" quota.') -@utils.arg( - '--server-group-members', - metavar='', - type=int, - default=None, - help='New value for the "server-group-members" quota.') -@utils.arg( - '--force', - dest='force', - action="store_true", - default=None, - help='Whether force update the quota even if the already used and ' - 'reserved exceeds the new quota') -def do_quota_update(cs, args): - """Update the quotas for a tenant.""" - - _quota_update(cs.quotas, args.tenant, args) - - -@utils.arg('--tenant', - metavar='', - required=True, - help='ID of tenant to delete quota for.') -def do_quota_delete(cs, args): - """Delete quota for a tenant so their quota will revert back to default.""" - - cs.quotas.delete(args.tenant) - - -@utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg( - 'host', metavar='', nargs='?', - help="Name or ID of the target host. " - "If no host is specified, the scheduler will choose one.") -@utils.arg( - '--password', - dest='password', - metavar='', - help="Set the provided password on the evacuated server. Not applicable " - "with on-shared-storage flag") -@utils.arg( - '--on-shared-storage', - dest='on_shared_storage', - action="store_true", - default=False, - help='Specifies whether server files are located on shared storage') -def do_evacuate(cs, args): - """Evacuate server from failed host to specified one.""" - server = _find_server(cs, args.server) - - res = server.evacuate(args.host, args.on_shared_storage, args.password)[1] - if type(res) is dict: - utils.print_dict(res) - - -def _print_interfaces(interfaces): - columns = ['Port State', 'Port ID', 'Net ID', 'IP addresses', - 'MAC Addr'] - - class FormattedInterface(object): - def __init__(self, interface): - for col in columns: - key = col.lower().replace(" ", "_") - if hasattr(interface, key): - setattr(self, key, getattr(interface, key)) - self.ip_addresses = ",".join([fip['ip_address'] - for fip in interface.fixed_ips]) - utils.print_list([FormattedInterface(i) for i in interfaces], columns) - - -@utils.arg('server', metavar='', help='Name or ID of server.') -def do_interface_list(cs, args): - """List interfaces attached to a server.""" - server = _find_server(cs, args.server) - - res = server.interface_list() - if type(res) is list: - _print_interfaces(res) - - -@utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg('--port-id', metavar='', help='Port ID.', dest="port_id") -@utils.arg('--net-id', metavar='', help='Network ID', - default=None, dest="net_id") -@utils.arg('--fixed-ip', metavar='', help='Requested fixed IP.', - default=None, dest="fixed_ip") -def do_interface_attach(cs, args): - """Attach a network interface to a server.""" - server = _find_server(cs, args.server) - - res = server.interface_attach(args.port_id, args.net_id, args.fixed_ip) - if type(res) is dict: - utils.print_dict(res) - - -@utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg('port_id', metavar='', help='Port ID.') -def do_interface_detach(cs, args): - """Detach a network interface from a server.""" - server = _find_server(cs, args.server) - - res = server.interface_detach(args.port_id) - if type(res) is dict: - utils.print_dict(res) - - -def _treeizeAvailabilityZone(zone): - """Build a tree view for availability zones.""" - AvailabilityZone = availability_zones.AvailabilityZone - - az = AvailabilityZone(zone.manager, - copy.deepcopy(zone._info), zone._loaded) - result = [] - - # Zone tree view item - az.zone_name = zone.zone_name - az.zone_state = ('available' - if zone.zone_state['available'] else 'not available') - az._info['zone_name'] = az.zone_name - az._info['zone_state'] = az.zone_state - result.append(az) - - if zone.hosts is not None: - zone_hosts = sorted(zone.hosts.items(), key=lambda x: x[0]) - for (host, services) in zone_hosts: - # Host tree view item - az = AvailabilityZone(zone.manager, - copy.deepcopy(zone._info), zone._loaded) - az.zone_name = '|- %s' % host - az.zone_state = '' - az._info['zone_name'] = az.zone_name - az._info['zone_state'] = az.zone_state - result.append(az) - - for (svc, state) in services.items(): - # Service tree view item - az = AvailabilityZone(zone.manager, - copy.deepcopy(zone._info), zone._loaded) - az.zone_name = '| |- %s' % svc - az.zone_state = '%s %s %s' % ( - 'enabled' if state['active'] else 'disabled', - ':-)' if state['available'] else 'XXX', - state['updated_at']) - az._info['zone_name'] = az.zone_name - az._info['zone_state'] = az.zone_state - result.append(az) - return result - - -def do_availability_zone_list(cs, _args): - """List all the availability zones.""" - try: - availability_zones = cs.availability_zones.list() - except exceptions.Forbidden as e: # policy doesn't allow probably - try: - availability_zones = cs.availability_zones.list(detailed=False) - except Exception: - raise e - - result = [] - for zone in availability_zones: - result += _treeizeAvailabilityZone(zone) - _translate_availability_zone_keys(result) - utils.print_list(result, ['Name', 'Status'], - sortby_index=None) - - -@utils.arg('--tenant', - # nova db searches by project_id - dest='tenant', - metavar='', - nargs='?', - help=_('Display information from single tenant (Admin only).')) -@utils.arg('--reserved', - dest='reserved', - action='store_true', - default=False, - help=_('Include reservations count.')) -def do_absolute_limits(cs, args): - """Print a list of absolute limits for a user""" - limits = cs.limits.get(args.reserved, args.tenant).absolute - - class Limit(object): - def __init__(self, name, used, max, other): - self.name = name - self.used = used - self.max = max - self.other = other - - limit_map = { - 'maxServerMeta': {'name': 'Server Meta', 'type': 'max'}, - 'maxPersonality': {'name': 'Personality', 'type': 'max'}, - 'maxPersonalitySize': {'name': 'Personality Size', 'type': 'max'}, - 'maxImageMeta': {'name': 'ImageMeta', 'type': 'max'}, - 'maxTotalKeypairs': {'name': 'Keypairs', 'type': 'max'}, - 'totalCoresUsed': {'name': 'Cores', 'type': 'used'}, - 'maxTotalCores': {'name': 'Cores', 'type': 'max'}, - 'totalRAMUsed': {'name': 'RAM', 'type': 'used'}, - 'maxTotalRAMSize': {'name': 'RAM', 'type': 'max'}, - 'totalInstancesUsed': {'name': 'Instances', 'type': 'used'}, - 'maxTotalInstances': {'name': 'Instances', 'type': 'max'}, - 'totalFloatingIpsUsed': {'name': 'FloatingIps', 'type': 'used'}, - 'maxTotalFloatingIps': {'name': 'FloatingIps', 'type': 'max'}, - 'totalSecurityGroupsUsed': {'name': 'SecurityGroups', 'type': 'used'}, - 'maxSecurityGroups': {'name': 'SecurityGroups', 'type': 'max'}, - 'maxSecurityGroupRules': {'name': 'SecurityGroupRules', 'type': 'max'}, - 'maxServerGroups': {'name': 'ServerGroups', 'type': 'max'}, - 'totalServerGroupsUsed': {'name': 'ServerGroups', 'type': 'used'}, - 'maxServerGroupMembers': {'name': 'ServerGroupMembers', 'type': 'max'}, - } - - max = {} - used = {} - other = {} - limit_names = [] - columns = ['Name', 'Used', 'Max'] - for l in limits: - map = limit_map.get(l.name, {'name': l.name, 'type': 'other'}) - name = map['name'] - if map['type'] == 'max': - max[name] = l.value - elif map['type'] == 'used': - used[name] = l.value - else: - other[name] = l.value - columns.append('Other') - if name not in limit_names: - limit_names.append(name) - - limit_names.sort() - - limit_list = [] - for name in limit_names: - l = Limit(name, - used.get(name, "-"), - max.get(name, "-"), - other.get(name, "-")) - limit_list.append(l) - - utils.print_list(limit_list, columns) - - -def do_rate_limits(cs, args): - """Print a list of rate limits for a user""" - limits = cs.limits.get().rate - columns = ['Verb', 'URI', 'Value', 'Remain', 'Unit', 'Next_Available'] - utils.print_list(limits, columns) diff --git a/novaclient/v3/usage.py b/novaclient/v3/usage.py deleted file mode 100644 index dd16997c6..000000000 --- a/novaclient/v3/usage.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright 2013 IBM Corp. -# -# 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. - -""" -Usage interface. -""" - -from novaclient.v1_1 import usage - - -class Usage(usage.Usage): - pass - - -class UsageManager(usage.UsageManager): - pass diff --git a/novaclient/v3/volumes.py b/novaclient/v3/volumes.py deleted file mode 100644 index 53cc05f6c..000000000 --- a/novaclient/v3/volumes.py +++ /dev/null @@ -1,75 +0,0 @@ -# Copyright 2013 IBM Corp. -# -# 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. - -""" -Volume interface -""" - -from novaclient import base - - -class VolumeManager(base.Manager): - """ - Manage :class:`Volume` resources. - """ - - def attach_server_volume(self, server, volume_id, device, - disk_bus=None, device_type=None): - """ - Attach a volume identified by the volume ID to the given server ID - - :param server: The server (or it's ID) - :param volume_id: The ID of the volume to attach. - :param device: The device name - :param disk_bus: The disk bus of the volume - :param device_type: The device type of the volume - :rtype: :class:`Volume` - """ - body = {'volume_id': volume_id, 'device': device} - if disk_bus: - body['disk_bus'] = disk_bus - if device_type: - body['device_type'] = device_type - return self._action('attach', server, body) - - def update_server_volume(self, server, old_volume_id, new_volume_id): - """ - Update the volume identified by the attachment ID, that is attached to - the given server ID - - :param server_id: The server (or it's ID) - :param old_volume_id: The ID of the attachment - :param new_volume_id: The ID of the new volume to attach - :rtype: :class:`Volume` - """ - body = {'new_volume_id': new_volume_id, 'old_volume_id': old_volume_id} - return self._action('swap_volume_attachment', server, body) - - def delete_server_volume(self, server, volume_id): - """ - Detach a volume identified by the attachment ID from the given server - - :param server_id: The ID of the server - :param volume_id: The ID of the attachment - """ - return self._action('detach', server, {'volume_id': volume_id}) - - def _action(self, action, server, info=None, **kwargs): - """ - Perform a server "action" -- reboot/rebuild/resize/etc. - """ - body = {action: info} - self.run_hooks('modify_body_for_action', body, **kwargs) - url = '/servers/%s/action' % base.getid(server) - return self.api.client.post(url, body=body) From 821643e126e3c2a6297ec2b3a30ee2e51d5268cd Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Fri, 24 Oct 2014 14:11:02 +0300 Subject: [PATCH 0651/1705] Use `arg` and `env` from cliutils: v1_1/shell To remove aliases for `arg` and `env` functions in novaclient.utils, we should use `arg` and `env` directly from cliutils in novaclients modules. This is the first stage: change usage in novaclient.v1_1.shell Change-Id: I237f0473844de49db8a4b141a9a71603c9ac76a7 --- novaclient/v1_1/shell.py | 1067 ++++++++++++++++++++------------------ 1 file changed, 572 insertions(+), 495 deletions(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index b39df0244..ddf9de979 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -313,59 +313,59 @@ def _boot(cs, args): return boot_args, boot_kwargs -@utils.arg( +@cliutils.arg( '--flavor', default=None, metavar='', help=_("Name or ID of flavor (see 'nova flavor-list').")) -@utils.arg( +@cliutils.arg( '--image', default=None, metavar='', help=_("Name or ID of image (see 'nova image-list'). ")) -@utils.arg( +@cliutils.arg( '--image-with', default=[], type=_key_value_pairing, action='append', metavar='', help=_("Image metadata property (see 'nova image-show'). ")) -@utils.arg( +@cliutils.arg( '--boot-volume', default=None, metavar="", help=_("Volume ID to boot from.")) -@utils.arg( +@cliutils.arg( '--snapshot', default=None, metavar="", help=_("Snapshot ID to boot from (will create a volume).")) -@utils.arg( +@cliutils.arg( '--num-instances', default=None, type=int, metavar='', help=argparse.SUPPRESS) -@utils.arg( +@cliutils.arg( '--min-count', default=None, type=int, metavar='', help=_("Boot at least servers (limited by quota).")) -@utils.arg( +@cliutils.arg( '--max-count', default=None, type=int, metavar='', help=_("Boot up to servers (limited by quota).")) -@utils.arg( +@cliutils.arg( '--meta', metavar="", action='append', default=[], help=_("Record arbitrary key/value metadata to /meta_data.json " "on the metadata server. Can be specified multiple times.")) -@utils.arg( +@cliutils.arg( '--file', metavar="", action='append', @@ -373,52 +373,52 @@ def _boot(cs, args): default=[], help=_("Store arbitrary files from locally to " "on the new server. You may store up to 5 files.")) -@utils.arg( +@cliutils.arg( '--key-name', default=os.environ.get('NOVACLIENT_DEFAULT_KEY_NAME'), metavar='', help=_("Key name of keypair that should be created earlier with \ the command keypair-add")) -@utils.arg( +@cliutils.arg( '--key_name', help=argparse.SUPPRESS) -@utils.arg('name', metavar='', help=_('Name for the new server')) -@utils.arg( +@cliutils.arg('name', metavar='', help=_('Name for the new server')) +@cliutils.arg( '--user-data', default=None, metavar='', help=_("user data file to pass to be exposed by the metadata server.")) -@utils.arg( +@cliutils.arg( '--user_data', help=argparse.SUPPRESS) -@utils.arg( +@cliutils.arg( '--availability-zone', default=None, metavar='', help=_("The availability zone for server placement.")) -@utils.arg( +@cliutils.arg( '--availability_zone', help=argparse.SUPPRESS) -@utils.arg( +@cliutils.arg( '--security-groups', default=None, metavar='', help=_("Comma separated list of security group names.")) -@utils.arg( +@cliutils.arg( '--security_groups', help=argparse.SUPPRESS) -@utils.arg( +@cliutils.arg( '--block-device-mapping', metavar="", action='append', default=[], help=_("Block device mapping in the format " "=:::.")) -@utils.arg( +@cliutils.arg( '--block_device_mapping', action='append', help=argparse.SUPPRESS) -@utils.arg( +@cliutils.arg( '--block-device', metavar="key1=value1[,key2=value2...]", action='append', @@ -443,19 +443,19 @@ def _boot(cs, args): "for others need to be specified) and " "shutdown=shutdown behaviour (either preserve or remove, " "for local destination set to remove).")) -@utils.arg( +@cliutils.arg( '--swap', metavar="", default=None, help=_("Create and attach a local swap block device of MB.")) -@utils.arg( +@cliutils.arg( '--ephemeral', metavar="size=[,format=]", action='append', default=[], help=_("Create and attach a local ephemeral block device of GB " "and format it to .")) -@utils.arg( +@cliutils.arg( '--hint', action='append', dest='scheduler_hints', @@ -463,7 +463,7 @@ def _boot(cs, args): metavar='', help=_("Send arbitrary key/value pairs to the scheduler for custom " "use.")) -@utils.arg( +@cliutils.arg( '--nic', metavar="", @@ -478,13 +478,13 @@ def _boot(cs, args): "v6-fixed-ip: IPv6 fixed address for NIC (optional), " "port-id: attach NIC to port with this UUID " "(either port-id or net-id must be provided).")) -@utils.arg( +@cliutils.arg( '--config-drive', metavar="", dest='config_drive', default=False, help=_("Enable config drive")) -@utils.arg( +@cliutils.arg( '--poll', dest='poll', action="store_true", @@ -511,15 +511,17 @@ def do_cloudpipe_list(cs, _args): utils.print_list(cloudpipes, columns) -@utils.arg('project', metavar='', - help=_('UUID of the project to create the cloudpipe for.')) +@cliutils.arg( + 'project', + metavar='', + help=_('UUID of the project to create the cloudpipe for.')) def do_cloudpipe_create(cs, args): """Create a cloudpipe instance for the given project.""" cs.cloudpipe.create(args.project) -@utils.arg('address', metavar='', help=_('New IP Address.')) -@utils.arg('port', metavar='', help='New Port.') +@cliutils.arg('address', metavar='', help=_('New IP Address.')) +@cliutils.arg('port', metavar='', help='New Port.') def do_cloudpipe_configure(cs, args): """Update the VPN IP/port of a cloudpipe instance.""" cs.cloudpipe.update(args.address, args.port) @@ -636,16 +638,18 @@ def _print_flavor_list(flavors, show_extra_specs=False): utils.print_list(flavors, headers, formatters) -@utils.arg('--extra-specs', - dest='extra_specs', - action='store_true', - default=False, - help=_('Get extra-specs of each flavor.')) -@utils.arg('--all', - dest='all', - action='store_true', - default=False, - help=_('Display all flavors (Admin only).')) +@cliutils.arg( + '--extra-specs', + dest='extra_specs', + action='store_true', + default=False, + help=_('Get extra-specs of each flavor.')) +@cliutils.arg( + '--all', + dest='all', + action='store_true', + default=False, + help=_('Display all flavors (Admin only).')) def do_flavor_list(cs, args): """Print a list of available 'flavors' (sizes of servers).""" if args.all: @@ -655,7 +659,7 @@ def do_flavor_list(cs, args): _print_flavor_list(flavors, args.extra_specs) -@utils.arg( +@cliutils.arg( 'flavor', metavar='', help=_("Name or ID of the flavor to delete")) @@ -666,7 +670,7 @@ def do_flavor_delete(cs, args): _print_flavor_list([flavorid]) -@utils.arg( +@cliutils.arg( 'flavor', metavar='', help=_("Name or ID of flavor")) @@ -676,43 +680,43 @@ def do_flavor_show(cs, args): _print_flavor(flavor) -@utils.arg( +@cliutils.arg( 'name', metavar='', help=_("Name of the new flavor")) -@utils.arg( +@cliutils.arg( 'id', metavar='', help=_("Unique ID (integer or UUID) for the new flavor." " If specifying 'auto', a UUID will be generated as id")) -@utils.arg( +@cliutils.arg( 'ram', metavar='', help=_("Memory size in MB")) -@utils.arg( +@cliutils.arg( 'disk', metavar='', help=_("Disk size in GB")) -@utils.arg( +@cliutils.arg( '--ephemeral', metavar='', help=_("Ephemeral space size in GB (default 0)"), default=0) -@utils.arg( +@cliutils.arg( 'vcpus', metavar='', help=_("Number of vcpus")) -@utils.arg( +@cliutils.arg( '--swap', metavar='', help=_("Swap space size in MB (default 0)"), default=0) -@utils.arg( +@cliutils.arg( '--rxtx-factor', metavar='', help=_("RX/TX factor (default 1)"), default=1.0) -@utils.arg( +@cliutils.arg( '--is-public', metavar='', help=_("Make flavor accessible to the public (default true)"), @@ -726,16 +730,16 @@ def do_flavor_create(cs, args): _print_flavor_list([f]) -@utils.arg( +@cliutils.arg( 'flavor', metavar='', help=_("Name or ID of flavor")) -@utils.arg( +@cliutils.arg( 'action', metavar='', choices=['set', 'unset'], help=_("Actions: 'set' or 'unset'")) -@utils.arg( +@cliutils.arg( 'metadata', metavar='', nargs='+', @@ -753,11 +757,11 @@ def do_flavor_key(cs, args): flavor.unset_keys(keypair.keys()) -@utils.arg( +@cliutils.arg( '--flavor', metavar='', help=_("Filter results by flavor name or ID.")) -@utils.arg( +@cliutils.arg( '--tenant', metavar='', help=_('Filter results by tenant ID.')) def do_flavor_access_list(cs, args): @@ -786,11 +790,11 @@ def do_flavor_access_list(cs, args): utils.print_list(access_list, columns) -@utils.arg( +@cliutils.arg( 'flavor', metavar='', help=_("Flavor name or ID to add access for the given tenant.")) -@utils.arg( +@cliutils.arg( 'tenant', metavar='', help=_('Tenant ID to add flavor access for.')) def do_flavor_access_add(cs, args): @@ -801,11 +805,11 @@ def do_flavor_access_add(cs, args): utils.print_list(access_list, columns) -@utils.arg( +@cliutils.arg( 'flavor', metavar='', help=_("Flavor name or ID to remove access for the given tenant.")) -@utils.arg( +@cliutils.arg( 'tenant', metavar='', help=_('Tenant ID to remove flavor access for.')) def do_flavor_access_remove(cs, args): @@ -816,7 +820,7 @@ def do_flavor_access_remove(cs, args): utils.print_list(access_list, columns) -@utils.arg( +@cliutils.arg( 'project_id', metavar='', help=_('The ID of the project.')) def do_scrub(cs, args): @@ -834,7 +838,7 @@ def do_scrub(cs, args): cs.security_groups.delete(group) -@utils.arg( +@cliutils.arg( '--fields', default=None, metavar='', @@ -857,7 +861,7 @@ def do_network_list(cs, args): utils.print_list(network_list, columns) -@utils.arg( +@cliutils.arg( 'network', metavar='', help=_("uuid or label of network")) @@ -867,7 +871,7 @@ def do_network_show(cs, args): utils.print_dict(network._info) -@utils.arg( +@cliutils.arg( 'network', metavar='', help=_("uuid or label of network")) @@ -877,7 +881,7 @@ def do_network_delete(cs, args): network.delete() -@utils.arg( +@cliutils.arg( '--host-only', dest='host_only', metavar='<0|1>', @@ -885,7 +889,7 @@ def do_network_delete(cs, args): type=int, const=1, default=0) -@utils.arg( +@cliutils.arg( '--project-only', dest='project_only', metavar='<0|1>', @@ -893,7 +897,7 @@ def do_network_delete(cs, args): type=int, const=1, default=0) -@utils.arg( +@cliutils.arg( 'network', metavar='', help="uuid of network") @@ -907,11 +911,11 @@ def do_network_disassociate(cs, args): cs.networks.disassociate(args.network, True, True) -@utils.arg( +@cliutils.arg( 'network', metavar='', help="uuid of network") -@utils.arg( +@cliutils.arg( 'host', metavar='', help="Name of host") @@ -920,7 +924,7 @@ def do_network_associate_host(cs, args): cs.networks.associate_host(args.network, args.host) -@utils.arg( +@cliutils.arg( 'network', metavar='', help="uuid of network") @@ -943,114 +947,114 @@ def _filter_network_create_options(args): return kwargs -@utils.arg( +@cliutils.arg( 'label', metavar='', help=_("Label for network")) -@utils.arg( +@cliutils.arg( '--fixed-range-v4', dest='cidr', metavar='', help=_("IPv4 subnet (ex: 10.0.0.0/8)")) -@utils.arg( +@cliutils.arg( '--fixed-range-v6', dest="cidr_v6", help=_('IPv6 subnet (ex: fe80::/64')) -@utils.arg( +@cliutils.arg( '--vlan', dest='vlan', type=int, metavar='', help=_("The vlan ID to be assigned to the project.")) -@utils.arg( +@cliutils.arg( '--vlan-start', dest='vlan_start', type=int, metavar='', help=_('First vlan ID to be assigned to the project. Subsequent vlan ' 'IDs will be assigned incrementally.')) -@utils.arg( +@cliutils.arg( '--vpn', dest='vpn_start', type=int, metavar='', help=_("vpn start")) -@utils.arg( +@cliutils.arg( '--gateway', dest="gateway", help=_('gateway')) -@utils.arg( +@cliutils.arg( '--gateway-v6', dest="gateway_v6", help=_('IPv6 gateway')) -@utils.arg( +@cliutils.arg( '--bridge', dest="bridge", metavar='', help=_('VIFs on this network are connected to this bridge.')) -@utils.arg( +@cliutils.arg( '--bridge-interface', dest="bridge_interface", metavar='', help=_('The bridge is connected to this interface.')) -@utils.arg( +@cliutils.arg( '--multi-host', dest="multi_host", metavar="<'T'|'F'>", help=_('Multi host')) -@utils.arg( +@cliutils.arg( '--dns1', dest="dns1", metavar="", help='First DNS') -@utils.arg( +@cliutils.arg( '--dns2', dest="dns2", metavar="", help=_('Second DNS')) -@utils.arg( +@cliutils.arg( '--uuid', dest="uuid", metavar="", help=_('Network UUID')) -@utils.arg( +@cliutils.arg( '--fixed-cidr', dest="fixed_cidr", metavar='', help=_('IPv4 subnet for fixed IPs (ex: 10.20.0.0/16)')) -@utils.arg( +@cliutils.arg( '--project-id', dest="project_id", metavar="", help=_('Project ID')) -@utils.arg( +@cliutils.arg( '--priority', dest="priority", metavar="", help=_('Network interface priority')) -@utils.arg( +@cliutils.arg( '--mtu', dest="mtu", type=int, help=_('MTU for network')) -@utils.arg( +@cliutils.arg( '--enable-dhcp', dest="enable_dhcp", metavar="<'T'|'F'>", help=_('Enable dhcp')) -@utils.arg( +@cliutils.arg( '--dhcp-server', dest="dhcp_server", help=_('Dhcp-server (defaults to gateway address)')) -@utils.arg( +@cliutils.arg( '--share-address', dest="share_address", metavar="<'T'|'F'>", help=_('Share address')) -@utils.arg( +@cliutils.arg( '--allowed-start', dest="allowed_start", help=_('Start of allowed addresses for instances')) -@utils.arg( +@cliutils.arg( '--allowed-end', dest="allowed_end", help=_('End of allowed addresses for instances')) @@ -1076,7 +1080,7 @@ def do_network_create(cs, args): cs.networks.create(**kwargs) -@utils.arg( +@cliutils.arg( '--limit', dest="limit", metavar="", @@ -1097,16 +1101,16 @@ def parse_server_name(image): fmts, sortby_index=1) -@utils.arg( +@cliutils.arg( 'image', metavar='', help=_("Name or ID of image")) -@utils.arg( +@cliutils.arg( 'action', metavar='', choices=['set', 'delete'], help=_("Actions: 'set' or 'delete'")) -@utils.arg( +@cliutils.arg( 'metadata', metavar='', nargs='+', @@ -1173,7 +1177,7 @@ def _print_flavor(flavor): utils.print_dict(info) -@utils.arg( +@cliutils.arg( 'image', metavar='', help=_("Name or ID of image")) @@ -1183,7 +1187,7 @@ def do_image_show(cs, args): _print_image(image) -@utils.arg( +@cliutils.arg( 'image', metavar='', nargs='+', help=_('Name or ID of image(s).')) def do_image_delete(cs, args): @@ -1196,68 +1200,68 @@ def do_image_delete(cs, args): {'image': image, 'e': e}) -@utils.arg( +@cliutils.arg( '--reservation-id', dest='reservation_id', metavar='', default=None, help=_('Only return servers that match reservation-id.')) -@utils.arg( +@cliutils.arg( '--reservation_id', help=argparse.SUPPRESS) -@utils.arg( +@cliutils.arg( '--ip', dest='ip', metavar='', default=None, help=_('Search with regular expression match by IP address.')) -@utils.arg( +@cliutils.arg( '--ip6', dest='ip6', metavar='', default=None, help=_('Search with regular expression match by IPv6 address.')) -@utils.arg( +@cliutils.arg( '--name', dest='name', metavar='', default=None, help=_('Search with regular expression match by name')) -@utils.arg( +@cliutils.arg( '--instance-name', dest='instance_name', metavar='', default=None, help=_('Search with regular expression match by server name.')) -@utils.arg( +@cliutils.arg( '--instance_name', help=argparse.SUPPRESS) -@utils.arg( +@cliutils.arg( '--status', dest='status', metavar='', default=None, help=_('Search by server status')) -@utils.arg( +@cliutils.arg( '--flavor', dest='flavor', metavar='', default=None, help=_('Search by flavor name or ID')) -@utils.arg( +@cliutils.arg( '--image', dest='image', metavar='', default=None, help=_('Search by image name or ID')) -@utils.arg( +@cliutils.arg( '--host', dest='host', metavar='', default=None, help=_('Search servers by hostname to which they are assigned (Admin ' 'only).')) -@utils.arg( +@cliutils.arg( '--all-tenants', dest='all_tenants', metavar='<0|1>', @@ -1267,38 +1271,38 @@ def do_image_delete(cs, args): default=int(strutils.bool_from_string( os.environ.get("ALL_TENANTS", 'false'), True)), help=_('Display information from all tenants (Admin only).')) -@utils.arg( +@cliutils.arg( '--all_tenants', nargs='?', type=int, const=1, help=argparse.SUPPRESS) -@utils.arg( +@cliutils.arg( '--tenant', # nova db searches by project_id dest='tenant', metavar='', nargs='?', help=_('Display information from single tenant (Admin only).')) -@utils.arg( +@cliutils.arg( '--user', dest='user', metavar='', nargs='?', help=_('Display information from single user (Admin only).')) -@utils.arg( +@cliutils.arg( '--deleted', dest='deleted', action="store_true", default=False, help='Only display deleted servers (Admin only).') -@utils.arg( +@cliutils.arg( '--fields', default=None, metavar='', help=_('Comma-separated list of fields to display. ' 'Use the show command to see which fields are available.')) -@utils.arg( +@cliutils.arg( '--minimal', dest='minimal', action="store_true", @@ -1375,15 +1379,15 @@ def do_list(cs, args): formatters, sortby_index=1) -@utils.arg( +@cliutils.arg( '--hard', dest='reboot_type', action='store_const', const=servers.REBOOT_HARD, default=servers.REBOOT_SOFT, help=_('Perform a hard reboot (instead of a soft one).')) -@utils.arg('server', metavar='', help=_('Name or ID of server.')) -@utils.arg( +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg( '--poll', dest='poll', action="store_true", @@ -1399,47 +1403,47 @@ def do_reboot(cs, args): show_progress=False) -@utils.arg('server', metavar='', help=_('Name or ID of server.')) -@utils.arg('image', metavar='', help=_("Name or ID of new image.")) -@utils.arg( +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg('image', metavar='', help=_("Name or ID of new image.")) +@cliutils.arg( '--rebuild-password', dest='rebuild_password', metavar='', default=False, help=_("Set the provided password on the rebuild server.")) -@utils.arg( +@cliutils.arg( '--rebuild_password', help=argparse.SUPPRESS) -@utils.arg( +@cliutils.arg( '--poll', dest='poll', action="store_true", default=False, help=_('Report the server rebuild progress until it completes.')) -@utils.arg( +@cliutils.arg( '--minimal', dest='minimal', action="store_true", default=False, help=_('Skips flavor/image lookups when showing servers')) -@utils.arg( +@cliutils.arg( '--preserve-ephemeral', action="store_true", default=False, help='Preserve the default ephemeral storage partition on rebuild.') -@utils.arg( +@cliutils.arg( '--name', metavar='', default=None, help=_('Name for the new server')) -@utils.arg( +@cliutils.arg( '--meta', metavar="", action='append', default=[], help=_("Record arbitrary key/value metadata to /meta_data.json " "on the metadata server. Can be specified multiple times.")) -@utils.arg( +@cliutils.arg( '--file', metavar="", action='append', @@ -1485,18 +1489,21 @@ def do_rebuild(cs, args): _poll_for_status(cs.servers.get, server.id, 'rebuilding', ['active']) -@utils.arg( +@cliutils.arg( 'server', metavar='', help=_('Name (old name) or ID of server.')) -@utils.arg('name', metavar='', help=_('New name for the server.')) +@cliutils.arg('name', metavar='', help=_('New name for the server.')) def do_rename(cs, args): """Rename a server.""" _find_server(cs, args.server).update(name=args.name) -@utils.arg('server', metavar='', help=_('Name or ID of server.')) -@utils.arg('flavor', metavar='', help=_("Name or ID of new flavor.")) -@utils.arg( +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg( + 'flavor', + metavar='', + help=_("Name or ID of new flavor.")) +@cliutils.arg( '--poll', dest='poll', action="store_true", @@ -1513,20 +1520,20 @@ def do_resize(cs, args): ['active', 'verify_resize']) -@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) def do_resize_confirm(cs, args): """Confirm a previous resize.""" _find_server(cs, args.server).confirm_resize() -@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) def do_resize_revert(cs, args): """Revert a previous resize (and return to the previous VM).""" _find_server(cs, args.server).revert_resize() -@utils.arg('server', metavar='', help=_('Name or ID of server.')) -@utils.arg( +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg( '--poll', dest='poll', action="store_true", @@ -1542,20 +1549,22 @@ def do_migrate(cs, args): ['active', 'verify_resize']) -@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) def do_pause(cs, args): """Pause a server.""" _find_server(cs, args.server).pause() -@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) def do_unpause(cs, args): """Unpause a server.""" _find_server(cs, args.server).unpause() -@utils.arg('server', metavar='', nargs='+', - help=_('Name or ID of server(s).')) +@cliutils.arg( + 'server', + metavar='', nargs='+', + help=_('Name or ID of server(s).')) def do_stop(cs, args): """Stop the server(s).""" utils.do_action_on_many( @@ -1565,8 +1574,10 @@ def do_stop(cs, args): _("Unable to stop the specified server(s).")) -@utils.arg('server', metavar='', nargs='+', - help=_('Name or ID of server(s).')) +@cliutils.arg( + 'server', + metavar='', nargs='+', + help=_('Name or ID of server(s).')) def do_start(cs, args): """Start the server(s).""" utils.do_action_on_many( @@ -1576,37 +1587,37 @@ def do_start(cs, args): _("Unable to start the specified server(s).")) -@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) def do_lock(cs, args): """Lock a server.""" _find_server(cs, args.server).lock() -@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) def do_unlock(cs, args): """Unlock a server.""" _find_server(cs, args.server).unlock() -@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) def do_suspend(cs, args): """Suspend a server.""" _find_server(cs, args.server).suspend() -@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) def do_resume(cs, args): """Resume a server.""" _find_server(cs, args.server).resume() -@utils.arg('server', metavar='', help=_('Name or ID of server.')) -@utils.arg( +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg( '--password', metavar='', dest='password', help=_('The admin password to be set in the rescue environment.')) -@utils.arg( +@cliutils.arg( '--image', metavar='', dest='image', @@ -1624,38 +1635,38 @@ def do_rescue(cs, args): utils.print_dict(_find_server(cs, args.server).rescue(**kwargs)[1]) -@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) def do_unrescue(cs, args): """Restart the server from normal boot disk again.""" _find_server(cs, args.server).unrescue() -@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) def do_shelve(cs, args): """Shelve a server.""" _find_server(cs, args.server).shelve() -@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) def do_shelve_offload(cs, args): """Remove a shelved server from the compute node.""" _find_server(cs, args.server).shelve_offload() -@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) def do_unshelve(cs, args): """Unshelve a server.""" _find_server(cs, args.server).unshelve() -@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) def do_diagnostics(cs, args): """Retrieve server diagnostics.""" server = _find_server(cs, args.server) utils.print_dict(cs.servers.diagnostics(server)[1], wrap=80) -@utils.arg( +@cliutils.arg( 'server', metavar='', help=_('Name or ID of a server for which the network cache should ' 'be refreshed from neutron (Admin only).')) @@ -1666,7 +1677,7 @@ def do_refresh_network(cs, args): 'name': 'network-changed'}]) -@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) def do_root_password(cs, args): """ Change the root password for a server. @@ -1679,15 +1690,15 @@ def do_root_password(cs, args): server.change_password(p1) -@utils.arg('server', metavar='', help=_('Name or ID of server.')) -@utils.arg('name', metavar='', help=_('Name of snapshot.')) -@utils.arg( +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg('name', metavar='', help=_('Name of snapshot.')) +@cliutils.arg( '--show', dest='show', action="store_true", default=False, help=_('Print image info.')) -@utils.arg( +@cliutils.arg( '--poll', dest='poll', action="store_true", @@ -1722,12 +1733,12 @@ def do_image_create(cs, args): _print_image(cs.images.get(image_uuid)) -@utils.arg('server', metavar='', help=_('Name or ID of server.')) -@utils.arg('name', metavar='', help=_('Name of the backup image.')) -@utils.arg( +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg('name', metavar='', help=_('Name of the backup image.')) +@cliutils.arg( 'backup_type', metavar='', help=_('The backup type, like "daily" or "weekly".')) -@utils.arg( +@cliutils.arg( 'rotation', metavar='', help=_('Int parameter representing how many backups to keep ' 'around.')) @@ -1738,16 +1749,16 @@ def do_backup(cs, args): args.rotation) -@utils.arg( +@cliutils.arg( 'server', metavar='', help=_("Name or ID of server")) -@utils.arg( +@cliutils.arg( 'action', metavar='', choices=['set', 'delete'], help=_("Actions: 'set' or 'delete'")) -@utils.arg( +@cliutils.arg( 'metadata', metavar='', nargs='+', @@ -1816,19 +1827,19 @@ def _print_server(cs, args, server=None): utils.print_dict(info) -@utils.arg( +@cliutils.arg( '--minimal', dest='minimal', action="store_true", default=False, help=_('Skips flavor/image lookups when showing servers')) -@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) def do_show(cs, args): """Show details about the given server.""" _print_server(cs, args) -@utils.arg( +@cliutils.arg( 'server', metavar='', nargs='+', help=_('Name or ID of server(s).')) def do_delete(cs, args): @@ -1858,8 +1869,8 @@ def _find_flavor(cs, flavor): return cs.flavors.find(ram=flavor) -@utils.arg('server', metavar='', help=_('Name or ID of server.')) -@utils.arg( +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg( 'network_id', metavar='', help='Network ID.') @@ -1869,8 +1880,8 @@ def do_add_fixed_ip(cs, args): server.add_fixed_ip(args.network_id) -@utils.arg('server', metavar='', help=_('Name or ID of server.')) -@utils.arg('address', metavar='
', help=_('IP Address.')) +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg('address', metavar='
', help=_('IP Address.')) def do_remove_fixed_ip(cs, args): """Remove an IP address from a server.""" server = _find_server(cs, args.server) @@ -1912,7 +1923,7 @@ def _translate_availability_zone_keys(collection): [('zoneName', 'name'), ('zoneState', 'status')]) -@utils.arg( +@cliutils.arg( '--all-tenants', dest='all_tenants', metavar='<0|1>', @@ -1922,7 +1933,7 @@ def _translate_availability_zone_keys(collection): default=int(strutils.bool_from_string( os.environ.get("ALL_TENANTS", 'false'), True)), help=_('Display information from all tenants (Admin only).')) -@utils.arg( +@cliutils.arg( '--all_tenants', nargs='?', type=int, @@ -1943,7 +1954,10 @@ def do_volume_list(cs, args): 'Size', 'Volume Type', 'Attached to']) -@utils.arg('volume', metavar='', help=_('Name or ID of the volume.')) +@cliutils.arg( + 'volume', + metavar='', + help=_('Name or ID of the volume.')) @cliutils.service_type('volume') def do_volume_show(cs, args): """Show details about a volume.""" @@ -1951,49 +1965,49 @@ def do_volume_show(cs, args): _print_volume(volume) -@utils.arg( +@cliutils.arg( 'size', metavar='', type=int, help=_('Size of volume in GB')) -@utils.arg( +@cliutils.arg( '--snapshot-id', metavar='', default=None, help=_('Optional snapshot id to create the volume from. (Default=None)')) -@utils.arg( +@cliutils.arg( '--snapshot_id', help=argparse.SUPPRESS) -@utils.arg( +@cliutils.arg( '--image-id', metavar='', help=_('Optional image id to create the volume from. (Default=None)'), default=None) -@utils.arg( +@cliutils.arg( '--display-name', metavar='', default=None, help=_('Optional volume name. (Default=None)')) -@utils.arg( +@cliutils.arg( '--display_name', help=argparse.SUPPRESS) -@utils.arg( +@cliutils.arg( '--display-description', metavar='', default=None, help=_('Optional volume description. (Default=None)')) -@utils.arg( +@cliutils.arg( '--display_description', help=argparse.SUPPRESS) -@utils.arg( +@cliutils.arg( '--volume-type', metavar='', default=None, help=_('Optional volume type. (Default=None)')) -@utils.arg( +@cliutils.arg( '--volume_type', help=argparse.SUPPRESS) -@utils.arg( +@cliutils.arg( '--availability-zone', metavar='', help=_('Optional Availability Zone for volume. (Default=None)'), default=None) @@ -2010,7 +2024,7 @@ def do_volume_create(cs, args): _print_volume(volume) -@utils.arg( +@cliutils.arg( 'volume', metavar='', nargs='+', help=_('Name or ID of the volume(s) to delete.')) @@ -2025,15 +2039,15 @@ def do_volume_delete(cs, args): {'volume': volume, 'e': e}) -@utils.arg( +@cliutils.arg( 'server', metavar='', help=_('Name or ID of server.')) -@utils.arg( +@cliutils.arg( 'volume', metavar='', help=_('ID of the volume to attach.')) -@utils.arg( +@cliutils.arg( 'device', metavar='', default=None, nargs='?', help=_('Name of the device e.g. /dev/vdb. ' 'Use "auto" for autoassign (if supported)')) @@ -2048,15 +2062,15 @@ def do_volume_attach(cs, args): _print_volume(volume) -@utils.arg( +@cliutils.arg( 'server', metavar='', help=_('Name or ID of server.')) -@utils.arg( +@cliutils.arg( 'attachment_id', metavar='', help=_('Attachment ID of the volume.')) -@utils.arg( +@cliutils.arg( 'new_volume', metavar='', help=_('ID of the volume to attach.')) @@ -2067,11 +2081,11 @@ def do_volume_update(cs, args): args.new_volume) -@utils.arg( +@cliutils.arg( 'server', metavar='', help=_('Name or ID of server.')) -@utils.arg( +@cliutils.arg( 'attachment_id', metavar='', help=_('ID of the volume to detach.')) @@ -2090,7 +2104,7 @@ def do_volume_snapshot_list(cs, _args): 'Size']) -@utils.arg( +@cliutils.arg( 'snapshot', metavar='', help=_('Name or ID of the snapshot.')) @@ -2101,30 +2115,30 @@ def do_volume_snapshot_show(cs, args): _print_volume_snapshot(snapshot) -@utils.arg( +@cliutils.arg( 'volume_id', metavar='', help=_('ID of the volume to snapshot')) -@utils.arg( +@cliutils.arg( '--force', metavar='', help=_('Optional flag to indicate whether to snapshot a volume even if ' 'its attached to a server. (Default=False)'), default=False) -@utils.arg( +@cliutils.arg( '--display-name', metavar='', default=None, help=_('Optional snapshot name. (Default=None)')) -@utils.arg( +@cliutils.arg( '--display_name', help=argparse.SUPPRESS) -@utils.arg( +@cliutils.arg( '--display-description', metavar='', default=None, help=_('Optional snapshot description. (Default=None)')) -@utils.arg( +@cliutils.arg( '--display_description', help=argparse.SUPPRESS) @cliutils.service_type('volume') @@ -2137,7 +2151,7 @@ def do_volume_snapshot_create(cs, args): _print_volume_snapshot(snapshot) -@utils.arg( +@cliutils.arg( 'snapshot', metavar='', help=_('Name or ID of the snapshot to delete.')) @@ -2159,7 +2173,7 @@ def do_volume_type_list(cs, args): _print_volume_type_list(vtypes) -@utils.arg( +@cliutils.arg( 'name', metavar='', help=_("Name of the new volume type")) @@ -2170,7 +2184,7 @@ def do_volume_type_create(cs, args): _print_volume_type_list([vtype]) -@utils.arg( +@cliutils.arg( 'id', metavar='', help=_("Unique ID of the volume type to delete")) @@ -2180,8 +2194,8 @@ def do_volume_type_delete(cs, args): cs.volume_types.delete(args.id) -@utils.arg('server', metavar='', help=_('Name or ID of server.')) -@utils.arg( +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg( 'console_type', metavar='', help=_('Type of vnc console ("novnc" or "xvpvnc").')) @@ -2198,8 +2212,8 @@ def __init__(self, console_dict): utils.print_list([VNCConsole(data['console'])], ['Type', 'Url']) -@utils.arg('server', metavar='', help=_('Name or ID of server.')) -@utils.arg( +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg( 'console_type', metavar='', help=_('Type of spice console ("spice-html5").')) @@ -2216,8 +2230,8 @@ def __init__(self, console_dict): utils.print_list([SPICEConsole(data['console'])], ['Type', 'Url']) -@utils.arg('server', metavar='', help=_('Name or ID of server.')) -@utils.arg( +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg( 'console_type', metavar='', help='Type of rdp console ("rdp-html5").') @@ -2234,8 +2248,8 @@ def __init__(self, console_dict): utils.print_list([RDPConsole(data['console'])], ['Type', 'Url']) -@utils.arg('server', metavar='', help=_('Name or ID of server.')) -@utils.arg( +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg( '--console_type', default='serial', help=_('Type of serial console, default="serial".')) def do_get_serial_console(cs, args): @@ -2256,8 +2270,8 @@ def __init__(self, console_dict): utils.print_list([SerialConsole(data['console'])], ['Type', 'Url']) -@utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg( +@cliutils.arg('server', metavar='', help='Name or ID of server.') +@cliutils.arg( 'private_key', metavar='', help=_('Private key (used locally to decrypt password) (Optional). ' @@ -2273,7 +2287,7 @@ def do_get_password(cs, args): print(data) -@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) def do_clear_password(cs, args): """Clear password for a server.""" server = _find_server(cs, args.server) @@ -2287,8 +2301,8 @@ def _print_floating_ip_list(floating_ips): utils.print_list(floating_ips, ['Ip', 'Server Id', 'Fixed Ip', 'Pool']) -@utils.arg('server', metavar='', help=_('Name or ID of server.')) -@utils.arg( +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg( '--length', metavar='', default=None, @@ -2300,9 +2314,9 @@ def do_console_log(cs, args): print(data) -@utils.arg('server', metavar='', help=_('Name or ID of server.')) -@utils.arg('address', metavar='
', help=_('IP Address.')) -@utils.arg( +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg('address', metavar='
', help=_('IP Address.')) +@cliutils.arg( '--fixed-address', metavar='', default=None, @@ -2312,9 +2326,9 @@ def do_add_floating_ip(cs, args): _associate_floating_ip(cs, args) -@utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg('address', metavar='
', help='IP Address.') -@utils.arg( +@cliutils.arg('server', metavar='', help='Name or ID of server.') +@cliutils.arg('address', metavar='
', help='IP Address.') +@cliutils.arg( '--fixed-address', metavar='', default=None, @@ -2329,15 +2343,15 @@ def _associate_floating_ip(cs, args): server.add_floating_ip(args.address, args.fixed_address) -@utils.arg('server', metavar='', help=_('Name or ID of server.')) -@utils.arg('address', metavar='
', help=_('IP Address.')) +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg('address', metavar='
', help=_('IP Address.')) def do_remove_floating_ip(cs, args): """DEPRECATED, use floating-ip-disassociate instead.""" _disassociate_floating_ip(cs, args) -@utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg('address', metavar='
', help='IP Address.') +@cliutils.arg('server', metavar='', help='Name or ID of server.') +@cliutils.arg('address', metavar='
', help='IP Address.') def do_floating_ip_disassociate(cs, args): """Disassociate a floating IP address from a server.""" _disassociate_floating_ip(cs, args) @@ -2348,23 +2362,29 @@ def _disassociate_floating_ip(cs, args): server.remove_floating_ip(args.address) -@utils.arg('server', metavar='', help=_('Name or ID of server.')) -@utils.arg('secgroup', metavar='', help=_('Name of Security Group.')) +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg( + 'secgroup', + metavar='', + help=_('Name of Security Group.')) def do_add_secgroup(cs, args): """Add a Security Group to a server.""" server = _find_server(cs, args.server) server.add_security_group(args.secgroup) -@utils.arg('server', metavar='', help=_('Name or ID of server.')) -@utils.arg('secgroup', metavar='', help=_('Name of Security Group.')) +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg( + 'secgroup', + metavar='', + help=_('Name of Security Group.')) def do_remove_secgroup(cs, args): """Remove a Security Group from a server.""" server = _find_server(cs, args.server) server.remove_security_group(args.secgroup) -@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) def do_list_secgroup(cs, args): """List Security Group(s) of a server.""" server = _find_server(cs, args.server) @@ -2372,7 +2392,7 @@ def do_list_secgroup(cs, args): _print_secgroups(groups) -@utils.arg( +@cliutils.arg( 'pool', metavar='', help=_('Name of Floating IP Pool. (Optional)'), @@ -2383,7 +2403,7 @@ def do_floating_ip_create(cs, args): _print_floating_ip_list([cs.floating_ips.create(pool=args.pool)]) -@utils.arg('address', metavar='
', help=_('IP of Floating Ip.')) +@cliutils.arg('address', metavar='
', help=_('IP of Floating Ip.')) def do_floating_ip_delete(cs, args): """De-allocate a floating IP.""" floating_ips = cs.floating_ips.list() @@ -2394,7 +2414,7 @@ def do_floating_ip_delete(cs, args): args.address) -@utils.arg( +@cliutils.arg( '--all-tenants', action='store_true', default=False, @@ -2409,7 +2429,7 @@ def do_floating_ip_pool_list(cs, _args): utils.print_list(cs.floating_ip_pools.list(), ['name']) -@utils.arg( +@cliutils.arg( '--host', dest='host', metavar='', default=None, help=_('Filter by host')) def do_floating_ip_bulk_list(cs, args): @@ -2421,11 +2441,11 @@ def do_floating_ip_bulk_list(cs, args): 'interface']) -@utils.arg('ip_range', metavar='', help=_('Address range to create')) -@utils.arg( +@cliutils.arg('ip_range', metavar='', help=_('Address range to create')) +@cliutils.arg( '--pool', dest='pool', metavar='', default=None, help=_('Pool for new Floating IPs')) -@utils.arg( +@cliutils.arg( '--interface', metavar='', default=None, help=_('Interface for new Floating IPs')) def do_floating_ip_bulk_create(cs, args): @@ -2433,7 +2453,7 @@ def do_floating_ip_bulk_create(cs, args): cs.floating_ips_bulk.create(args.ip_range, args.pool, args.interface) -@utils.arg('ip_range', metavar='', help=_('Address range to delete')) +@cliutils.arg('ip_range', metavar='', help=_('Address range to delete')) def do_floating_ip_bulk_delete(cs, args): """Bulk delete floating ips by range.""" cs.floating_ips_bulk.delete(args.ip_range) @@ -2454,9 +2474,9 @@ def do_dns_domains(cs, args): _print_domain_list(domains) -@utils.arg('domain', metavar='', help=_('DNS domain')) -@utils.arg('--ip', metavar='', help=_('ip address'), default=None) -@utils.arg('--name', metavar='', help=_('DNS name'), default=None) +@cliutils.arg('domain', metavar='', help=_('DNS domain')) +@cliutils.arg('--ip', metavar='', help=_('ip address'), default=None) +@cliutils.arg('--name', metavar='', help=_('DNS name'), default=None) def do_dns_list(cs, args): """List current DNS entries for domain and ip or domain and name.""" if not (args.ip or args.name): @@ -2471,37 +2491,40 @@ def do_dns_list(cs, args): _print_dns_list(entries) -@utils.arg('ip', metavar='', help=_('ip address')) -@utils.arg('name', metavar='', help=_('DNS name')) -@utils.arg('domain', metavar='', help=_('DNS domain')) -@utils.arg('--type', metavar='', help=_('dns type (e.g. "A")'), - default='A') +@cliutils.arg('ip', metavar='', help=_('ip address')) +@cliutils.arg('name', metavar='', help=_('DNS name')) +@cliutils.arg('domain', metavar='', help=_('DNS domain')) +@cliutils.arg( + '--type', + metavar='', + help=_('dns type (e.g. "A")'), + default='A') def do_dns_create(cs, args): """Create a DNS entry for domain, name and ip.""" cs.dns_entries.create(args.domain, args.name, args.ip, args.type) -@utils.arg('domain', metavar='', help=_('DNS domain')) -@utils.arg('name', metavar='', help=_('DNS name')) +@cliutils.arg('domain', metavar='', help=_('DNS domain')) +@cliutils.arg('name', metavar='', help=_('DNS name')) def do_dns_delete(cs, args): """Delete the specified DNS entry.""" cs.dns_entries.delete(args.domain, args.name) -@utils.arg('domain', metavar='', help=_('DNS domain')) +@cliutils.arg('domain', metavar='', help=_('DNS domain')) def do_dns_delete_domain(cs, args): """Delete the specified DNS domain.""" cs.dns_domains.delete(args.domain) -@utils.arg('domain', metavar='', help=_('DNS domain')) -@utils.arg( +@cliutils.arg('domain', metavar='', help=_('DNS domain')) +@cliutils.arg( '--availability-zone', metavar='', default=None, help=_('Limit access to this domain to servers ' 'in the specified availability zone.')) -@utils.arg( +@cliutils.arg( '--availability_zone', help=argparse.SUPPRESS) def do_dns_create_private_domain(cs, args): @@ -2510,8 +2533,8 @@ def do_dns_create_private_domain(cs, args): args.availability_zone) -@utils.arg('domain', metavar='', help=_('DNS domain')) -@utils.arg( +@cliutils.arg('domain', metavar='', help=_('DNS domain')) +@cliutils.arg( '--project', metavar='', help=_('Limit access to this domain to users ' 'of the specified project.'), @@ -2576,23 +2599,23 @@ def _get_secgroup(cs, secgroup): return match_found -@utils.arg( +@cliutils.arg( 'secgroup', metavar='', help=_('ID or name of security group.')) -@utils.arg( +@cliutils.arg( 'ip_proto', metavar='', help=_('IP protocol (icmp, tcp, udp).')) -@utils.arg( +@cliutils.arg( 'from_port', metavar='', help=_('Port at start of range.')) -@utils.arg( +@cliutils.arg( 'to_port', metavar='', help=_('Port at end of range.')) -@utils.arg('cidr', metavar='', help=_('CIDR for address range.')) +@cliutils.arg('cidr', metavar='', help=_('CIDR for address range.')) def do_secgroup_add_rule(cs, args): """Add a rule to a security group.""" secgroup = _get_secgroup(cs, args.secgroup) @@ -2604,23 +2627,23 @@ def do_secgroup_add_rule(cs, args): _print_secgroup_rules([rule]) -@utils.arg( +@cliutils.arg( 'secgroup', metavar='', help=_('ID or name of security group.')) -@utils.arg( +@cliutils.arg( 'ip_proto', metavar='', help=_('IP protocol (icmp, tcp, udp).')) -@utils.arg( +@cliutils.arg( 'from_port', metavar='', help=_('Port at start of range.')) -@utils.arg( +@cliutils.arg( 'to_port', metavar='', help=_('Port at end of range.')) -@utils.arg('cidr', metavar='', help=_('CIDR for address range.')) +@cliutils.arg('cidr', metavar='', help=_('CIDR for address range.')) def do_secgroup_delete_rule(cs, args): """Delete a rule from a security group.""" secgroup = _get_secgroup(cs, args.secgroup) @@ -2636,8 +2659,8 @@ def do_secgroup_delete_rule(cs, args): raise exceptions.CommandError(_("Rule not found")) -@utils.arg('name', metavar='', help=_('Name of security group.')) -@utils.arg( +@cliutils.arg('name', metavar='', help=_('Name of security group.')) +@cliutils.arg( 'description', metavar='', help=_('Description of security group.')) def do_secgroup_create(cs, args): @@ -2646,12 +2669,12 @@ def do_secgroup_create(cs, args): _print_secgroups([secgroup]) -@utils.arg( +@cliutils.arg( 'secgroup', metavar='', help=_('ID or name of security group.')) -@utils.arg('name', metavar='', help=_('Name of security group.')) -@utils.arg( +@cliutils.arg('name', metavar='', help=_('Name of security group.')) +@cliutils.arg( 'description', metavar='', help=_('Description of security group.')) def do_secgroup_update(cs, args): @@ -2661,7 +2684,7 @@ def do_secgroup_update(cs, args): _print_secgroups([secgroup]) -@utils.arg( +@cliutils.arg( 'secgroup', metavar='', help=_('ID or name of security group.')) @@ -2672,7 +2695,7 @@ def do_secgroup_delete(cs, args): _print_secgroups([secgroup]) -@utils.arg( +@cliutils.arg( '--all-tenants', dest='all_tenants', metavar='<0|1>', @@ -2682,7 +2705,7 @@ def do_secgroup_delete(cs, args): default=int(strutils.bool_from_string( os.environ.get("ALL_TENANTS", 'false'), True)), help=_('Display information from all tenants (Admin only).')) -@utils.arg( +@cliutils.arg( '--all_tenants', nargs='?', type=int, @@ -2698,7 +2721,7 @@ def do_secgroup_list(cs, args): utils.print_list(groups, columns) -@utils.arg( +@cliutils.arg( 'secgroup', metavar='', help=_('ID or name of security group.')) @@ -2708,23 +2731,23 @@ def do_secgroup_list_rules(cs, args): _print_secgroup_rules(secgroup.rules) -@utils.arg( +@cliutils.arg( 'secgroup', metavar='', help=_('ID or name of security group.')) -@utils.arg( +@cliutils.arg( 'source_group', metavar='', help=_('ID or name of source group.')) -@utils.arg( +@cliutils.arg( 'ip_proto', metavar='', help=_('IP protocol (icmp, tcp, udp).')) -@utils.arg( +@cliutils.arg( 'from_port', metavar='', help=_('Port at start of range.')) -@utils.arg( +@cliutils.arg( 'to_port', metavar='', help=_('Port at end of range.')) @@ -2747,23 +2770,23 @@ def do_secgroup_add_group_rule(cs, args): _print_secgroup_rules([rule]) -@utils.arg( +@cliutils.arg( 'secgroup', metavar='', help=_('ID or name of security group.')) -@utils.arg( +@cliutils.arg( 'source_group', metavar='', help=_('ID or name of source group.')) -@utils.arg( +@cliutils.arg( 'ip_proto', metavar='', help=_('IP protocol (icmp, tcp, udp).')) -@utils.arg( +@cliutils.arg( 'from_port', metavar='', help=_('Port at start of range.')) -@utils.arg( +@cliutils.arg( 'to_port', metavar='', help=_('Port at end of range.')) @@ -2794,13 +2817,13 @@ def do_secgroup_delete_group_rule(cs, args): raise exceptions.CommandError(_("Rule not found")) -@utils.arg('name', metavar='', help=_('Name of key.')) -@utils.arg( +@cliutils.arg('name', metavar='', help=_('Name of key.')) +@cliutils.arg( '--pub-key', metavar='', default=None, help=_('Path to a public ssh key.')) -@utils.arg( +@cliutils.arg( '--pub_key', help=argparse.SUPPRESS) def do_keypair_add(cs, args): @@ -2824,7 +2847,7 @@ def do_keypair_add(cs, args): print(private_key) -@utils.arg('name', metavar='', help=_('Keypair name to delete.')) +@cliutils.arg('name', metavar='', help=_('Keypair name to delete.')) def do_keypair_delete(cs, args): """Delete keypair given by its name.""" name = _find_keypair(cs, args.name) @@ -2845,7 +2868,7 @@ def _print_keypair(keypair): print(_("Public key: %s") % pk) -@utils.arg( +@cliutils.arg( 'keypair', metavar='', help=_("Name or ID of keypair")) @@ -2860,17 +2883,19 @@ def _find_keypair(cs, keypair): return utils.find_resource(cs.keypairs, keypair) -@utils.arg('--tenant', - # nova db searches by project_id - dest='tenant', - metavar='', - nargs='?', - help=_('Display information from single tenant (Admin only).')) -@utils.arg('--reserved', - dest='reserved', - action='store_true', - default=False, - help=_('Include reservations count.')) +@cliutils.arg( + '--tenant', + # nova db searches by project_id + dest='tenant', + metavar='', + nargs='?', + help=_('Display information from single tenant (Admin only).')) +@cliutils.arg( + '--reserved', + dest='reserved', + action='store_true', + default=False, + help=_('Include reservations count.')) def do_absolute_limits(cs, args): """Print a list of absolute limits for a user""" limits = cs.limits.get(args.reserved, args.tenant).absolute @@ -2942,13 +2967,16 @@ def do_rate_limits(cs, args): utils.print_list(limits, columns) -@utils.arg('--start', metavar='', - help=_('Usage range start date ex 2012-01-20 (default: 4 weeks ' - 'ago)'), - default=None) -@utils.arg('--end', metavar='', - help=_('Usage range end date, ex 2012-01-20 (default: tomorrow)'), - default=None) +@cliutils.arg( + '--start', + metavar='', + help=_('Usage range start date ex 2012-01-20 (default: 4 weeks ago)'), + default=None) +@cliutils.arg( + '--end', + metavar='', + help=_('Usage range end date, ex 2012-01-20 (default: tomorrow)'), + default=None) def do_usage_list(cs, args): """List usage data for all tenants.""" dateformat = "%Y-%m-%d" @@ -2988,16 +3016,20 @@ def simplify_usage(u): utils.print_list(usage_list, rows) -@utils.arg('--start', metavar='', - help=_('Usage range start date ex 2012-01-20 (default: 4 weeks ' - 'ago)'), - default=None) -@utils.arg('--end', metavar='', - help=_('Usage range end date, ex 2012-01-20 (default: tomorrow)'), - default=None) -@utils.arg('--tenant', metavar='', - default=None, - help=_('UUID or name of tenant to get usage for.')) +@cliutils.arg( + '--start', + metavar='', + help=_('Usage range start date ex 2012-01-20 (default: 4 weeks ago)'), + default=None) +@cliutils.arg( + '--end', metavar='', + help=_('Usage range end date, ex 2012-01-20 (default: tomorrow)'), + default=None) +@cliutils.arg( + '--tenant', + metavar='', + default=None, + help=_('UUID or name of tenant to get usage for.')) def do_usage(cs, args): """Show usage data for a single tenant.""" dateformat = "%Y-%m-%d" @@ -3044,13 +3076,13 @@ def simplify_usage(u): print(_('None')) -@utils.arg( +@cliutils.arg( 'pk_filename', metavar='', nargs='?', default='pk.pem', help=_('Filename for the private key [Default: pk.pem]')) -@utils.arg( +@cliutils.arg( 'cert_filename', metavar='', nargs='?', @@ -3081,7 +3113,7 @@ def do_x509_create_cert(cs, args): print(_("Wrote x509 certificate to %s") % args.cert_filename) -@utils.arg( +@cliutils.arg( 'filename', metavar='', nargs='?', @@ -3099,8 +3131,11 @@ def do_x509_get_root_cert(cs, args): print(_("Wrote x509 root cert to %s") % args.filename) -@utils.arg('--hypervisor', metavar='', default=None, - help=_('type of hypervisor.')) +@cliutils.arg( + '--hypervisor', + metavar='', + default=None, + help=_('type of hypervisor.')) def do_agent_list(cs, args): """List all builds.""" result = cs.agents.list(args.hypervisor) @@ -3109,14 +3144,19 @@ def do_agent_list(cs, args): utils.print_list(result, columns) -@utils.arg('os', metavar='', help=_('type of os.')) -@utils.arg('architecture', metavar='', - help=_('type of architecture')) -@utils.arg('version', metavar='', help=_('version')) -@utils.arg('url', metavar='', help=_('url')) -@utils.arg('md5hash', metavar='', help=_('md5 hash')) -@utils.arg('hypervisor', metavar='', default='xen', - help=_('type of hypervisor.')) +@cliutils.arg('os', metavar='', help=_('type of os.')) +@cliutils.arg( + 'architecture', + metavar='', + help=_('type of architecture')) +@cliutils.arg('version', metavar='', help=_('version')) +@cliutils.arg('url', metavar='', help=_('url')) +@cliutils.arg('md5hash', metavar='', help=_('md5 hash')) +@cliutils.arg( + 'hypervisor', + metavar='', + default='xen', + help=_('type of hypervisor.')) def do_agent_create(cs, args): """Create new agent build.""" result = cs.agents.create(args.os, args.architecture, @@ -3125,16 +3165,16 @@ def do_agent_create(cs, args): utils.print_dict(result._info.copy()) -@utils.arg('id', metavar='', help=_('id of the agent-build')) +@cliutils.arg('id', metavar='', help=_('id of the agent-build')) def do_agent_delete(cs, args): """Delete existing agent build.""" cs.agents.delete(args.id) -@utils.arg('id', metavar='', help=_('id of the agent-build')) -@utils.arg('version', metavar='', help=_('version')) -@utils.arg('url', metavar='', help=_('url')) -@utils.arg('md5hash', metavar='', help=_('md5hash')) +@cliutils.arg('id', metavar='', help=_('id of the agent-build')) +@cliutils.arg('version', metavar='', help=_('version')) +@cliutils.arg('url', metavar='', help=_('url')) +@cliutils.arg('md5hash', metavar='', help=_('md5hash')) def do_agent_modify(cs, args): """Modify existing agent build.""" result = cs.agents.update(args.id, args.version, @@ -3154,8 +3194,8 @@ def do_aggregate_list(cs, args): utils.print_list(aggregates, columns) -@utils.arg('name', metavar='', help=_('Name of aggregate.')) -@utils.arg( +@cliutils.arg('name', metavar='', help=_('Name of aggregate.')) +@cliutils.arg( 'availability_zone', metavar='', default=None, @@ -3167,8 +3207,10 @@ def do_aggregate_create(cs, args): _print_aggregate_details(aggregate) -@utils.arg('aggregate', metavar='', - help=_('Name or ID of aggregate to delete.')) +@cliutils.arg( + 'aggregate', + metavar='', + help=_('Name or ID of aggregate to delete.')) def do_aggregate_delete(cs, args): """Delete the aggregate.""" aggregate = _find_aggregate(cs, args.aggregate) @@ -3176,10 +3218,12 @@ def do_aggregate_delete(cs, args): print(_("Aggregate %s has been successfully deleted.") % aggregate.id) -@utils.arg('aggregate', metavar='', - help=_('Name or ID of aggregate to update.')) -@utils.arg('name', metavar='', help=_('Name of aggregate.')) -@utils.arg( +@cliutils.arg( + 'aggregate', + metavar='', + help=_('Name or ID of aggregate to update.')) +@cliutils.arg('name', metavar='', help=_('Name of aggregate.')) +@cliutils.arg( 'availability_zone', metavar='', nargs='?', @@ -3197,10 +3241,10 @@ def do_aggregate_update(cs, args): _print_aggregate_details(aggregate) -@utils.arg( +@cliutils.arg( 'aggregate', metavar='', help=_('Name or ID of aggregate to update.')) -@utils.arg( +@cliutils.arg( 'metadata', metavar='', nargs='+', @@ -3226,10 +3270,10 @@ def do_aggregate_set_metadata(cs, args): _print_aggregate_details(aggregate) -@utils.arg( +@cliutils.arg( 'aggregate', metavar='', help=_('Name or ID of aggregate.')) -@utils.arg( +@cliutils.arg( 'host', metavar='', help=_('The host to add to the aggregate.')) def do_aggregate_add_host(cs, args): @@ -3242,10 +3286,10 @@ def do_aggregate_add_host(cs, args): _print_aggregate_details(aggregate) -@utils.arg( +@cliutils.arg( 'aggregate', metavar='', help=_('Name or ID of aggregate.')) -@utils.arg( +@cliutils.arg( 'host', metavar='', help=_('The host to remove from the aggregate.')) def do_aggregate_remove_host(cs, args): @@ -3258,7 +3302,7 @@ def do_aggregate_remove_host(cs, args): _print_aggregate_details(aggregate) -@utils.arg( +@cliutils.arg( 'aggregate', metavar='', help=_('Name or ID of aggregate.')) def do_aggregate_details(cs, args): @@ -3283,27 +3327,27 @@ def parser_hosts(fields): utils.print_list([aggregate], columns, formatters=formatters) -@utils.arg('server', metavar='', help=_('Name or ID of server.')) -@utils.arg( +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg( 'host', metavar='', default=None, nargs='?', help=_('destination host name.')) -@utils.arg( +@cliutils.arg( '--block-migrate', action='store_true', dest='block_migrate', default=False, help=_('True in case of block_migration. (Default=False:live_migration)')) -@utils.arg( +@cliutils.arg( '--block_migrate', action='store_true', help=argparse.SUPPRESS) -@utils.arg( +@cliutils.arg( '--disk-over-commit', action='store_true', dest='disk_over_commit', default=False, help=_('Allow overcommit.(Default=False)')) -@utils.arg( +@cliutils.arg( '--disk_over_commit', action='store_true', help=argparse.SUPPRESS) @@ -3314,10 +3358,10 @@ def do_live_migration(cs, args): args.disk_over_commit) -@utils.arg( +@cliutils.arg( 'server', metavar='', nargs='+', help=_('Name or ID of server(s).')) -@utils.arg( +@cliutils.arg( '--active', action='store_const', dest='state', default='error', const='active', help=_('Request the server be reset to "active" state instead ' @@ -3339,16 +3383,22 @@ def do_reset_state(cs, args): raise exceptions.CommandError(msg) -@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) def do_reset_network(cs, args): """Reset network of a server.""" _find_server(cs, args.server).reset_network() -@utils.arg('--host', metavar='', default=None, - help=_('Name of host.')) -@utils.arg('--binary', metavar='', default=None, - help=_('Service binary.')) +@cliutils.arg( + '--host', + metavar='', + default=None, + help=_('Name of host.')) +@cliutils.arg( + '--binary', + metavar='', + default=None, + help=_('Service binary.')) def do_service_list(cs, args): """Show a list of all running services. Filter by host & binary.""" result = cs.services.list(host=args.host, binary=args.binary) @@ -3366,18 +3416,20 @@ def do_service_list(cs, args): utils.print_list(result, columns) -@utils.arg('host', metavar='', help=_('Name of host.')) -@utils.arg('binary', metavar='', help=_('Service binary.')) +@cliutils.arg('host', metavar='', help=_('Name of host.')) +@cliutils.arg('binary', metavar='', help=_('Service binary.')) def do_service_enable(cs, args): """Enable the service.""" result = cs.services.enable(args.host, args.binary) utils.print_list([result], ['Host', 'Binary', 'Status']) -@utils.arg('host', metavar='', help=_('Name of host.')) -@utils.arg('binary', metavar='', help=_('Service binary.')) -@utils.arg('--reason', metavar='', - help=_('Reason for disabling service.')) +@cliutils.arg('host', metavar='', help=_('Name of host.')) +@cliutils.arg('binary', metavar='', help=_('Service binary.')) +@cliutils.arg( + '--reason', + metavar='', + help=_('Reason for disabling service.')) def do_service_disable(cs, args): """Disable the service.""" if args.reason: @@ -3390,32 +3442,32 @@ def do_service_disable(cs, args): utils.print_list([result], ['Host', 'Binary', 'Status']) -@utils.arg('id', metavar='', help=_('Id of service.')) +@cliutils.arg('id', metavar='', help=_('Id of service.')) def do_service_delete(cs, args): """Delete the service.""" cs.services.delete(args.id) -@utils.arg('fixed_ip', metavar='', help=_('Fixed IP Address.')) +@cliutils.arg('fixed_ip', metavar='', help=_('Fixed IP Address.')) def do_fixed_ip_get(cs, args): """Retrieve info on a fixed ip.""" result = cs.fixed_ips.get(args.fixed_ip) utils.print_list([result], ['address', 'cidr', 'hostname', 'host']) -@utils.arg('fixed_ip', metavar='', help=_('Fixed IP Address.')) +@cliutils.arg('fixed_ip', metavar='', help=_('Fixed IP Address.')) def do_fixed_ip_reserve(cs, args): """Reserve a fixed IP.""" cs.fixed_ips.reserve(args.fixed_ip) -@utils.arg('fixed_ip', metavar='', help=_('Fixed IP Address.')) +@cliutils.arg('fixed_ip', metavar='', help=_('Fixed IP Address.')) def do_fixed_ip_unreserve(cs, args): """Unreserve a fixed IP.""" cs.fixed_ips.unreserve(args.fixed_ip) -@utils.arg('host', metavar='', help=_('Name of host.')) +@cliutils.arg('host', metavar='', help=_('Name of host.')) def do_host_describe(cs, args): """Describe a specific host.""" result = cs.hosts.get(args.host) @@ -3423,9 +3475,12 @@ def do_host_describe(cs, args): utils.print_list(result, columns) -@utils.arg('--zone', metavar='', default=None, - help=_('Filters the list, returning only those ' - 'hosts in the availability zone .')) +@cliutils.arg( + '--zone', + metavar='', + default=None, + help=_('Filters the list, returning only those hosts in the availability ' + 'zone .')) def do_host_list(cs, args): """List all hosts by service.""" columns = ["host_name", "service", "zone"] @@ -3433,11 +3488,11 @@ def do_host_list(cs, args): utils.print_list(result, columns) -@utils.arg('host', metavar='', help='Name of host.') -@utils.arg( +@cliutils.arg('host', metavar='', help='Name of host.') +@cliutils.arg( '--status', metavar='', default=None, dest='status', help=_('Either enable or disable a host.')) -@utils.arg( +@cliutils.arg( '--maintenance', metavar='', default=None, @@ -3457,8 +3512,8 @@ def do_host_update(cs, args): utils.print_list([result], columns) -@utils.arg('host', metavar='', help='Name of host.') -@utils.arg( +@cliutils.arg('host', metavar='', help='Name of host.') +@cliutils.arg( '--action', metavar='', dest='action', choices=['startup', 'shutdown', 'reboot'], help=_('A power action: startup, reboot, or shutdown.')) @@ -3473,8 +3528,11 @@ def _find_hypervisor(cs, hypervisor): return utils.find_resource(cs.hypervisors, hypervisor) -@utils.arg('--matching', metavar='', default=None, - help=_('List hypervisors matching the given .')) +@cliutils.arg( + '--matching', + metavar='', + default=None, + help=_('List hypervisors matching the given .')) def do_hypervisor_list(cs, args): """List hypervisors.""" columns = ['ID', 'Hypervisor hostname', 'State', 'Status'] @@ -3486,8 +3544,10 @@ def do_hypervisor_list(cs, args): utils.print_list(cs.hypervisors.list(False), columns) -@utils.arg('hostname', metavar='', - help=_('The hypervisor hostname (or pattern) to search for.')) +@cliutils.arg( + 'hostname', + metavar='', + help=_('The hypervisor hostname (or pattern) to search for.')) def do_hypervisor_servers(cs, args): """List servers belonging to specific hypervisors.""" hypers = cs.hypervisors.search(args.hostname, servers=True) @@ -3513,7 +3573,7 @@ def __init__(self, **kwargs): 'Hypervisor Hostname']) -@utils.arg( +@cliutils.arg( 'hypervisor', metavar='', help=_('Name or ID of the hypervisor to show the details of.')) @@ -3523,7 +3583,7 @@ def do_hypervisor_show(cs, args): utils.print_dict(utils.flatten_dict(hyper._info)) -@utils.arg( +@cliutils.arg( 'hypervisor', metavar='', help=_('Name or ID of the hypervisor to show the uptime of.')) @@ -3599,7 +3659,7 @@ def _get_first_endpoint(endpoints, region): raise LookupError("No suitable endpoint found") -@utils.arg( +@cliutils.arg( '--wrap', dest='wrap', metavar='', default=64, help=_('wrap PKI tokens to a specified length, or 0 to disable')) def do_credentials(cs, _args): @@ -3619,8 +3679,8 @@ def do_credentials(cs, _args): wrap=int(_args.wrap)) -@utils.arg('server', metavar='', help=_('Name or ID of server.')) -@utils.arg( +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg( '--port', dest='port', action='store', @@ -3628,13 +3688,13 @@ def do_credentials(cs, _args): default=22, help=_('Optional flag to indicate which port to use for ssh. ' '(Default=22)')) -@utils.arg( +@cliutils.arg( '--private', dest='private', action='store_true', default=False, help=argparse.SUPPRESS) -@utils.arg( +@cliutils.arg( '--address-type', dest='address_type', action='store', @@ -3642,25 +3702,25 @@ def do_credentials(cs, _args): default='floating', help=_('Optional flag to indicate which IP type to use. Possible values ' 'includes fixed and floating (the Default).')) -@utils.arg( +@cliutils.arg( '--network', metavar='', help=_('Network to use for the ssh.'), default=None) -@utils.arg( +@cliutils.arg( '--ipv6', dest='ipv6', action='store_true', default=False, help=_('Optional flag to indicate whether to use an IPv6 address ' 'attached to a server. (Defaults to IPv4 address)')) -@utils.arg( +@cliutils.arg( '--login', metavar='', help=_('Login to use.'), default="root") -@utils.arg( +@cliutils.arg( '-i', '--identity', dest='identity', help=_('Private key file, same as the -i option to the ssh command.'), default='') -@utils.arg( +@cliutils.arg( '--extra-opts', dest='extra', help=_('Extra options to pass to ssh. see: man ssh'), @@ -3770,12 +3830,12 @@ def _quota_update(manager, identifier, args): manager.update(identifier, **updates) -@utils.arg( +@cliutils.arg( '--tenant', metavar='', default=None, help=_('ID of tenant to list the quotas for.')) -@utils.arg( +@cliutils.arg( '--user', metavar='', default=None, @@ -3789,7 +3849,7 @@ def do_quota_show(cs, args): _quota_show(cs.quotas.get(args.tenant, user_id=args.user)) -@utils.arg( +@cliutils.arg( '--tenant', metavar='', default=None, @@ -3803,113 +3863,113 @@ def do_quota_defaults(cs, args): _quota_show(cs.quotas.defaults(args.tenant)) -@utils.arg( +@cliutils.arg( 'tenant', metavar='', help=_('ID of tenant to set the quotas for.')) -@utils.arg( +@cliutils.arg( '--user', metavar='', default=None, help=_('ID of user to set the quotas for.')) -@utils.arg( +@cliutils.arg( '--instances', metavar='', type=int, default=None, help=_('New value for the "instances" quota.')) -@utils.arg( +@cliutils.arg( '--cores', metavar='', type=int, default=None, help=_('New value for the "cores" quota.')) -@utils.arg( +@cliutils.arg( '--ram', metavar='', type=int, default=None, help=_('New value for the "ram" quota.')) -@utils.arg( +@cliutils.arg( '--floating-ips', metavar='', type=int, default=None, help=_('New value for the "floating-ips" quota.')) -@utils.arg( +@cliutils.arg( '--floating_ips', type=int, help=argparse.SUPPRESS) -@utils.arg( +@cliutils.arg( '--fixed-ips', metavar='', type=int, default=None, help=_('New value for the "fixed-ips" quota.')) -@utils.arg( +@cliutils.arg( '--metadata-items', metavar='', type=int, default=None, help=_('New value for the "metadata-items" quota.')) -@utils.arg( +@cliutils.arg( '--metadata_items', type=int, help=argparse.SUPPRESS) -@utils.arg( +@cliutils.arg( '--injected-files', metavar='', type=int, default=None, help=_('New value for the "injected-files" quota.')) -@utils.arg( +@cliutils.arg( '--injected_files', type=int, help=argparse.SUPPRESS) -@utils.arg( +@cliutils.arg( '--injected-file-content-bytes', metavar='', type=int, default=None, help=_('New value for the "injected-file-content-bytes" quota.')) -@utils.arg( +@cliutils.arg( '--injected_file_content_bytes', type=int, help=argparse.SUPPRESS) -@utils.arg( +@cliutils.arg( '--injected-file-path-bytes', metavar='', type=int, default=None, help=_('New value for the "injected-file-path-bytes" quota.')) -@utils.arg( +@cliutils.arg( '--key-pairs', metavar='', type=int, default=None, help=_('New value for the "key-pairs" quota.')) -@utils.arg( +@cliutils.arg( '--security-groups', metavar='', type=int, default=None, help=_('New value for the "security-groups" quota.')) -@utils.arg( +@cliutils.arg( '--security-group-rules', metavar='', type=int, default=None, help=_('New value for the "security-group-rules" quota.')) -@utils.arg( +@cliutils.arg( '--server-groups', metavar='', type=int, default=None, help=_('New value for the "server-groups" quota.')) -@utils.arg( +@cliutils.arg( '--server-group-members', metavar='', type=int, default=None, help=_('New value for the "server-group-members" quota.')) -@utils.arg( +@cliutils.arg( '--force', dest='force', action="store_true", @@ -3922,13 +3982,15 @@ def do_quota_update(cs, args): _quota_update(cs.quotas, args.tenant, args) -@utils.arg('--tenant', - metavar='', - required=True, - help=_('ID of tenant to delete quota for.')) -@utils.arg('--user', - metavar='', - help=_('ID of user to delete quota for.')) +@cliutils.arg( + '--tenant', + metavar='', + required=True, + help=_('ID of tenant to delete quota for.')) +@cliutils.arg( + '--user', + metavar='', + help=_('ID of user to delete quota for.')) def do_quota_delete(cs, args): """Delete quota for a tenant/user so their quota will Revert back to default. @@ -3937,7 +3999,7 @@ def do_quota_delete(cs, args): cs.quotas.delete(args.tenant, user_id=args.user) -@utils.arg( +@cliutils.arg( 'class_name', metavar='', help=_('Name of quota class to list the quotas for.')) @@ -3947,102 +4009,102 @@ def do_quota_class_show(cs, args): _quota_show(cs.quota_classes.get(args.class_name)) -@utils.arg( +@cliutils.arg( 'class_name', metavar='', help=_('Name of quota class to set the quotas for.')) -@utils.arg( +@cliutils.arg( '--instances', metavar='', type=int, default=None, help=_('New value for the "instances" quota.')) -@utils.arg( +@cliutils.arg( '--cores', metavar='', type=int, default=None, help=_('New value for the "cores" quota.')) -@utils.arg( +@cliutils.arg( '--ram', metavar='', type=int, default=None, help=_('New value for the "ram" quota.')) -@utils.arg( +@cliutils.arg( '--floating-ips', metavar='', type=int, default=None, help=_('New value for the "floating-ips" quota.')) -@utils.arg( +@cliutils.arg( '--floating_ips', type=int, help=argparse.SUPPRESS) -@utils.arg( +@cliutils.arg( '--fixed-ips', metavar='', type=int, default=None, help=_('New value for the "fixed-ips" quota.')) -@utils.arg( +@cliutils.arg( '--metadata-items', metavar='', type=int, default=None, help=_('New value for the "metadata-items" quota.')) -@utils.arg( +@cliutils.arg( '--metadata_items', type=int, help=argparse.SUPPRESS) -@utils.arg( +@cliutils.arg( '--injected-files', metavar='', type=int, default=None, help=_('New value for the "injected-files" quota.')) -@utils.arg( +@cliutils.arg( '--injected_files', type=int, help=argparse.SUPPRESS) -@utils.arg( +@cliutils.arg( '--injected-file-content-bytes', metavar='', type=int, default=None, help=_('New value for the "injected-file-content-bytes" quota.')) -@utils.arg( +@cliutils.arg( '--injected_file_content_bytes', type=int, help=argparse.SUPPRESS) -@utils.arg( +@cliutils.arg( '--injected-file-path-bytes', metavar='', type=int, default=None, help=_('New value for the "injected-file-path-bytes" quota.')) -@utils.arg( +@cliutils.arg( '--key-pairs', metavar='', type=int, default=None, help=_('New value for the "key-pairs" quota.')) -@utils.arg( +@cliutils.arg( '--security-groups', metavar='', type=int, default=None, help=_('New value for the "security-groups" quota.')) -@utils.arg( +@cliutils.arg( '--security-group-rules', metavar='', type=int, default=None, help=_('New value for the "security-group-rules" quota.')) -@utils.arg( +@cliutils.arg( '--server-groups', metavar='', type=int, default=None, help=_('New value for the "server-groups" quota.')) -@utils.arg( +@cliutils.arg( '--server-group-members', metavar='', type=int, @@ -4054,18 +4116,18 @@ def do_quota_class_update(cs, args): _quota_update(cs.quota_classes, args.class_name, args) -@utils.arg('server', metavar='', help=_('Name or ID of server.')) -@utils.arg( +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg( 'host', metavar='', nargs='?', help=_("Name or ID of the target host. " "If no host is specified, the scheduler will choose one.")) -@utils.arg( +@cliutils.arg( '--password', dest='password', metavar='', help=_("Set the provided password on the evacuated server. Not applicable " "with on-shared-storage flag")) -@utils.arg( +@cliutils.arg( '--on-shared-storage', dest='on_shared_storage', action="store_true", @@ -4096,7 +4158,7 @@ def __init__(self, interface): utils.print_list([FormattedInterface(i) for i in interfaces], columns) -@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) def do_interface_list(cs, args): """List interfaces attached to a server.""" server = _find_server(cs, args.server) @@ -4106,13 +4168,22 @@ def do_interface_list(cs, args): _print_interfaces(res) -@utils.arg('server', metavar='', help=_('Name or ID of server.')) -@utils.arg('--port-id', metavar='', help=_('Port ID.'), - dest="port_id") -@utils.arg('--net-id', metavar='', help=_('Network ID'), - default=None, dest="net_id") -@utils.arg('--fixed-ip', metavar='', help=_('Requested fixed IP.'), - default=None, dest="fixed_ip") +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg( + '--port-id', + metavar='', + help=_('Port ID.'), + dest="port_id") +@cliutils.arg( + '--net-id', + metavar='', + help=_('Network ID'), + default=None, dest="net_id") +@cliutils.arg( + '--fixed-ip', + metavar='', + help=_('Requested fixed IP.'), + default=None, dest="fixed_ip") def do_interface_attach(cs, args): """Attach a network interface to a server.""" server = _find_server(cs, args.server) @@ -4122,8 +4193,8 @@ def do_interface_attach(cs, args): utils.print_dict(res) -@utils.arg('server', metavar='', help=_('Name or ID of server.')) -@utils.arg('port_id', metavar='', help=_('Port ID.')) +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg('port_id', metavar='', help=_('Port ID.')) def do_interface_detach(cs, args): """Detach a network interface from a server.""" server = _find_server(cs, args.server) @@ -4212,19 +4283,19 @@ def do_secgroup_list_default_rules(cs, args): show_source_group=False) -@utils.arg( +@cliutils.arg( 'ip_proto', metavar='', help=_('IP protocol (icmp, tcp, udp).')) -@utils.arg( +@cliutils.arg( 'from_port', metavar='', help=_('Port at start of range.')) -@utils.arg( +@cliutils.arg( 'to_port', metavar='', help=_('Port at end of range.')) -@utils.arg('cidr', metavar='', help=_('CIDR for address range.')) +@cliutils.arg('cidr', metavar='', help=_('CIDR for address range.')) def do_secgroup_add_default_rule(cs, args): """Add a rule to the default security group.""" rule = cs.security_group_default_rules.create(args.ip_proto, @@ -4234,19 +4305,19 @@ def do_secgroup_add_default_rule(cs, args): _print_secgroup_rules([rule], show_source_group=False) -@utils.arg( +@cliutils.arg( 'ip_proto', metavar='', help=_('IP protocol (icmp, tcp, udp).')) -@utils.arg( +@cliutils.arg( 'from_port', metavar='', help=_('Port at start of range.')) -@utils.arg( +@cliutils.arg( 'to_port', metavar='', help=_('Port at end of range.')) -@utils.arg('cidr', metavar='', help=_('CIDR for address range.')) +@cliutils.arg('cidr', metavar='', help=_('CIDR for address range.')) def do_secgroup_delete_default_rule(cs, args): """Delete a rule from the default security group.""" for rule in cs.security_group_default_rules.list(): @@ -4261,7 +4332,7 @@ def do_secgroup_delete_default_rule(cs, args): raise exceptions.CommandError(_("Rule not found")) -@utils.arg('name', metavar='', help='Server group name.') +@cliutils.arg('name', metavar='', help='Server group name.') # NOTE(wingwj): The '--policy' way is still reserved here for preserving # the backwards compatibility of CLI, even if a user won't get this usage # in '--help' description. It will be deprecated after an suitable deprecation @@ -4272,16 +4343,17 @@ def do_secgroup_delete_default_rule(cs, args): # the possibility that they might mix them here. That usage is unsupported. # The related discussion can be found in # https://review.openstack.org/#/c/96382/2/. -@utils.arg('policy', - metavar='', - default=argparse.SUPPRESS, - nargs='*', - help='Policies for the server groups ' - '("affinity" or "anti-affinity")') -@utils.arg('--policy', - default=[], - action='append', - help=argparse.SUPPRESS) +@cliutils.arg( + 'policy', + metavar='', + default=argparse.SUPPRESS, + nargs='*', + help='Policies for the server groups ("affinity" or "anti-affinity")') +@cliutils.arg( + '--policy', + default=[], + action='append', + help=argparse.SUPPRESS) def do_server_group_create(cs, args): """Create a new server group with the specified details.""" if not args.policy: @@ -4293,8 +4365,11 @@ def do_server_group_create(cs, args): _print_server_group_details([server_group]) -@utils.arg('id', metavar='', nargs='+', - help="Unique ID(s) of the server group to delete") +@cliutils.arg( + 'id', + metavar='', + nargs='+', + help="Unique ID(s) of the server group to delete") def do_server_group_delete(cs, args): """Delete specific server group(s).""" failure_count = 0 @@ -4312,8 +4387,10 @@ def do_server_group_delete(cs, args): "specified server groups.")) -@utils.arg('id', metavar='', - help="Unique ID of the server group to get") +@cliutils.arg( + 'id', + metavar='', + help="Unique ID of the server group to get") def do_server_group_get(cs, args): """Get a specific server group.""" server_group = cs.server_groups.get(args.id) From 96a124fae67c7fa9b63d7c524332dcbb947b0b18 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Fri, 24 Oct 2014 14:15:17 +0300 Subject: [PATCH 0652/1705] Remove aliases for `args` and `env` in utils To remove aliases for `arg` and `env` functions in novaclient.utils, we should use `arg` and `env` directly from cliutils in novaclients modules. This patch removes aliases in `novaclient.utils` and starts using `args` and `env` from novaclient.openstack.common.cliutils directly. Change-Id: I4585adae62bc66ad6bc9d19be10d4679bb3dc5a1 --- novaclient/client.py | 6 +-- novaclient/shell.py | 43 ++++++++++--------- novaclient/utils.py | 3 -- novaclient/v1_1/contrib/baremetal.py | 40 +++++++++-------- novaclient/v1_1/contrib/cells.py | 5 ++- novaclient/v1_1/contrib/deferred_delete.py | 5 ++- novaclient/v1_1/contrib/host_evacuate.py | 27 ++++++------ novaclient/v1_1/contrib/host_evacuate_live.py | 30 +++++++------ .../v1_1/contrib/host_servers_migrate.py | 3 +- novaclient/v1_1/contrib/instance_action.py | 7 +-- .../v1_1/contrib/metadata_extensions.py | 32 +++++++------- novaclient/v1_1/contrib/migrations.py | 28 ++++++------ novaclient/v1_1/contrib/tenant_networks.py | 19 ++++---- 13 files changed, 134 insertions(+), 114 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index be31686a3..a04a92e47 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -45,8 +45,8 @@ from novaclient import exceptions from novaclient.i18n import _ +from novaclient.openstack.common import cliutils from novaclient import service_catalog -from novaclient import utils class _ClientConnectionPool(object): @@ -90,8 +90,8 @@ def _make_directory_name(self, username, auth_url): """ uniqifier = hashlib.md5(username.encode('utf-8') + auth_url.encode('utf-8')).hexdigest() - base_dir = utils.env('NOVACLIENT_UUID_CACHE_DIR', - default="~/.novaclient") + base_dir = cliutils.env('NOVACLIENT_UUID_CACHE_DIR', + default="~/.novaclient") return os.path.expanduser(os.path.join(base_dir, uniqifier)) def _prepare_directory(self): diff --git a/novaclient/shell.py b/novaclient/shell.py index bb79d6379..b2a2203fd 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -159,7 +159,7 @@ def password(self): self._password = self.args.os_password else: verify_pass = strutils.bool_from_string( - utils.env("OS_VERIFY_PASSWORD", default=False), True) + cliutils.env("OS_VERIFY_PASSWORD", default=False), True) self._password = self._prompt_password(verify_pass) if not self._password: raise exc.CommandError( @@ -241,16 +241,17 @@ def _append_global_identity_args(self, parser): # Register the CLI arguments that have moved to the session object. ksession.Session.register_cli_options(parser) - parser.set_defaults(insecure=utils.env('NOVACLIENT_INSECURE', + parser.set_defaults(insecure=cliutils.env('NOVACLIENT_INSECURE', default=False)) identity.Password.register_argparse_arguments(parser) - parser.set_defaults(os_username=utils.env('OS_USERNAME', - 'NOVA_USERNAME')) - parser.set_defaults(os_password=utils.env('OS_PASSWORD', - 'NOVA_PASSWORD')) - parser.set_defaults(os_auth_url=utils.env('OS_AUTH_URL', 'NOVA_URL')) + parser.set_defaults(os_username=cliutils.env('OS_USERNAME', + 'NOVA_USERNAME')) + parser.set_defaults(os_password=cliutils.env('OS_PASSWORD', + 'NOVA_PASSWORD')) + parser.set_defaults(os_auth_url=cliutils.env('OS_AUTH_URL', + 'NOVA_URL')) def get_base_parser(self): parser = NovaClientArgumentParser( @@ -282,7 +283,7 @@ def get_base_parser(self): parser.add_argument( '--os-cache', default=strutils.bool_from_string( - utils.env('OS_CACHE', default=False), True), + cliutils.env('OS_CACHE', default=False), True), action='store_true', help=_("Use the auth token cache. Defaults to False if " "env[OS_CACHE] is not set.")) @@ -295,7 +296,7 @@ def get_base_parser(self): parser.add_argument( '--os-auth-token', - default=utils.env('OS_AUTH_TOKEN'), + default=cliutils.env('OS_AUTH_TOKEN'), help='Defaults to env[OS_AUTH_TOKEN]') parser.add_argument( @@ -309,7 +310,7 @@ def get_base_parser(self): parser.add_argument( '--os-tenant-name', metavar='', - default=utils.env('OS_TENANT_NAME', 'NOVA_PROJECT_ID'), + default=cliutils.env('OS_TENANT_NAME', 'NOVA_PROJECT_ID'), help=_('Defaults to env[OS_TENANT_NAME].')) parser.add_argument( '--os_tenant_name', @@ -318,7 +319,7 @@ def get_base_parser(self): parser.add_argument( '--os-tenant-id', metavar='', - default=utils.env('OS_TENANT_ID'), + default=cliutils.env('OS_TENANT_ID'), help=_('Defaults to env[OS_TENANT_ID].')) parser.add_argument( @@ -328,7 +329,7 @@ def get_base_parser(self): parser.add_argument( '--os-region-name', metavar='', - default=utils.env('OS_REGION_NAME', 'NOVA_REGION_NAME'), + default=cliutils.env('OS_REGION_NAME', 'NOVA_REGION_NAME'), help=_('Defaults to env[OS_REGION_NAME].')) parser.add_argument( '--os_region_name', @@ -337,7 +338,7 @@ def get_base_parser(self): parser.add_argument( '--os-auth-system', metavar='', - default=utils.env('OS_AUTH_SYSTEM'), + default=cliutils.env('OS_AUTH_SYSTEM'), help='Defaults to env[OS_AUTH_SYSTEM].') parser.add_argument( '--os_auth_system', @@ -354,7 +355,7 @@ def get_base_parser(self): parser.add_argument( '--service-name', metavar='', - default=utils.env('NOVA_SERVICE_NAME'), + default=cliutils.env('NOVA_SERVICE_NAME'), help=_('Defaults to env[NOVA_SERVICE_NAME]')) parser.add_argument( '--service_name', @@ -363,7 +364,7 @@ def get_base_parser(self): parser.add_argument( '--volume-service-name', metavar='', - default=utils.env('NOVA_VOLUME_SERVICE_NAME'), + default=cliutils.env('NOVA_VOLUME_SERVICE_NAME'), help=_('Defaults to env[NOVA_VOLUME_SERVICE_NAME]')) parser.add_argument( '--volume_service_name', @@ -373,9 +374,9 @@ def get_base_parser(self): '--os-endpoint-type', metavar='', dest='endpoint_type', - default=utils.env( + default=cliutils.env( 'NOVA_ENDPOINT_TYPE', - default=utils.env( + default=cliutils.env( 'OS_ENDPOINT_TYPE', default=DEFAULT_NOVA_ENDPOINT_TYPE)), help=_('Defaults to env[NOVA_ENDPOINT_TYPE], ' @@ -395,8 +396,8 @@ def get_base_parser(self): parser.add_argument( '--os-compute-api-version', metavar='', - default=utils.env('OS_COMPUTE_API_VERSION', - default=DEFAULT_OS_COMPUTE_API_VERSION), + default=cliutils.env('OS_COMPUTE_API_VERSION', + default=DEFAULT_OS_COMPUTE_API_VERSION), help=_('Accepts 1.1 or 3, ' 'defaults to env[OS_COMPUTE_API_VERSION].')) parser.add_argument( @@ -407,7 +408,7 @@ def get_base_parser(self): '--bypass-url', metavar='', dest='bypass_url', - default=utils.env('NOVACLIENT_BYPASS_URL'), + default=cliutils.env('NOVACLIENT_BYPASS_URL'), help="Use this API endpoint instead of the Service Catalog. " "Defaults to env[NOVACLIENT_BYPASS_URL]") parser.add_argument('--bypass_url', @@ -849,7 +850,7 @@ def do_bash_completion(self, _args): commands.remove('bash_completion') print(' '.join(commands | options)) - @utils.arg( + @cliutils.arg( 'command', metavar='', nargs='?', diff --git a/novaclient/utils.py b/novaclient/utils.py index e82a8ac5a..fbc5da604 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -27,9 +27,6 @@ from novaclient.openstack.common import cliutils -arg = cliutils.arg -env = cliutils.env - VALID_KEY_REGEX = re.compile(r"[\w\.\- :]+$", re.UNICODE) diff --git a/novaclient/v1_1/contrib/baremetal.py b/novaclient/v1_1/contrib/baremetal.py index 325e27211..d23a38e53 100644 --- a/novaclient/v1_1/contrib/baremetal.py +++ b/novaclient/v1_1/contrib/baremetal.py @@ -19,6 +19,7 @@ from novaclient import base from novaclient.i18n import _ +from novaclient.openstack.common import cliutils from novaclient import utils @@ -151,43 +152,43 @@ def list_interfaces(self, node_id): return interfaces -@utils.arg( +@cliutils.arg( 'service_host', metavar='', help=_('Name of nova compute host which will control this baremetal ' 'node')) -@utils.arg( +@cliutils.arg( 'cpus', metavar='', type=int, help=_('Number of CPUs in the node')) -@utils.arg( +@cliutils.arg( 'memory_mb', metavar='', type=int, help=_('Megabytes of RAM in the node')) -@utils.arg( +@cliutils.arg( 'local_gb', metavar='', type=int, help=_('Gigabytes of local storage in the node')) -@utils.arg( +@cliutils.arg( 'prov_mac_address', metavar='', help=_('MAC address to provision the node')) -@utils.arg( +@cliutils.arg( '--pm_address', default=None, metavar='', help=_('Power management IP for the node')) -@utils.arg( +@cliutils.arg( '--pm_user', default=None, metavar='', help=_('Username for the node\'s power management')) -@utils.arg( +@cliutils.arg( '--pm_password', default=None, metavar='', help=_('Password for the node\'s power management')) -@utils.arg( +@cliutils.arg( '--terminal_port', default=None, metavar='', type=int, @@ -204,7 +205,7 @@ def do_baremetal_node_create(cs, args): _print_baremetal_resource(node) -@utils.arg( +@cliutils.arg( 'node', metavar='', help=_('ID of the node to delete.')) @@ -286,7 +287,7 @@ def _print_baremetal_node_interfaces(interfaces): ]) -@utils.arg( +@cliutils.arg( 'node', metavar='', help=_("ID of node")) @@ -296,20 +297,20 @@ def do_baremetal_node_show(cs, args): _print_baremetal_resource(node) -@utils.arg( +@cliutils.arg( 'node', metavar='', help=_("ID of node")) -@utils.arg( +@cliutils.arg( 'address', metavar='
', help=_("MAC address of interface")) -@utils.arg( +@cliutils.arg( '--datapath_id', default=0, metavar='', help=_("OpenFlow Datapath ID of interface")) -@utils.arg( +@cliutils.arg( '--port_no', default=0, metavar='', @@ -321,14 +322,17 @@ def do_baremetal_interface_add(cs, args): _print_baremetal_resource(bmif) -@utils.arg('node', metavar='', help=_("ID of node")) -@utils.arg('address', metavar='
', help=_("MAC address of interface")) +@cliutils.arg('node', metavar='', help=_("ID of node")) +@cliutils.arg( + 'address', + metavar='
', + help=_("MAC address of interface")) def do_baremetal_interface_remove(cs, args): """Remove a network interface from a baremetal node.""" cs.baremetal.remove_interface(args.node, args.address) -@utils.arg('node', metavar='', help=_("ID of node")) +@cliutils.arg('node', metavar='', help=_("ID of node")) def do_baremetal_interface_list(cs, args): """List network interfaces associated with a baremetal node.""" interfaces = cs.baremetal.list_interfaces(args.node) diff --git a/novaclient/v1_1/contrib/cells.py b/novaclient/v1_1/contrib/cells.py index 464ec8e15..bfc6893d7 100644 --- a/novaclient/v1_1/contrib/cells.py +++ b/novaclient/v1_1/contrib/cells.py @@ -15,6 +15,7 @@ from novaclient import base from novaclient.i18n import _ +from novaclient.openstack.common import cliutils from novaclient import utils @@ -46,7 +47,7 @@ def capacities(self, cell_name=None): return self._get("/os-cells/%s" % path, "cell") -@utils.arg( +@cliutils.arg( 'cell', metavar='', help=_('Name of the cell.')) @@ -56,7 +57,7 @@ def do_cell_show(cs, args): utils.print_dict(cell._info) -@utils.arg( +@cliutils.arg( '--cell', metavar='', help=_("Name of the cell to get the capacities."), diff --git a/novaclient/v1_1/contrib/deferred_delete.py b/novaclient/v1_1/contrib/deferred_delete.py index 1412702d5..5d8d7d9e3 100644 --- a/novaclient/v1_1/contrib/deferred_delete.py +++ b/novaclient/v1_1/contrib/deferred_delete.py @@ -12,16 +12,17 @@ # See the License for the specific language governing permissions and # limitations under the License. +from novaclient.openstack.common import cliutils from novaclient import utils -@utils.arg('server', metavar='', help='Name or ID of server.') +@cliutils.arg('server', metavar='', help='Name or ID of server.') def do_force_delete(cs, args): """Force delete a server.""" utils.find_resource(cs.servers, args.server).force_delete() -@utils.arg('server', metavar='', help='Name or ID of server.') +@cliutils.arg('server', metavar='', help='Name or ID of server.') def do_restore(cs, args): """Restore a soft-deleted server.""" utils.find_resource(cs.servers, args.server).restore() diff --git a/novaclient/v1_1/contrib/host_evacuate.py b/novaclient/v1_1/contrib/host_evacuate.py index e935e23db..b08e28cd1 100644 --- a/novaclient/v1_1/contrib/host_evacuate.py +++ b/novaclient/v1_1/contrib/host_evacuate.py @@ -15,6 +15,7 @@ from novaclient import base from novaclient.i18n import _ +from novaclient.openstack.common import cliutils from novaclient import utils @@ -37,19 +38,19 @@ def _server_evacuate(cs, server, args): "error_message": error_message}) -@utils.arg('host', metavar='', help='Name of host.') -@utils.arg('--target_host', - metavar='', - default=None, - help=_('Name of target host. ' - 'If no host is specified the scheduler' - ' will select a target.')) -@utils.arg('--on-shared-storage', - dest='on_shared_storage', - action="store_true", - default=False, - help=_('Specifies whether all instances files are on shared ' - ' storage')) +@cliutils.arg('host', metavar='', help='Name of host.') +@cliutils.arg( + '--target_host', + metavar='', + default=None, + help=_('Name of target host. If no host is specified the scheduler will ' + 'select a target.')) +@cliutils.arg( + '--on-shared-storage', + dest='on_shared_storage', + action="store_true", + default=False, + help=_('Specifies whether all instances files are on shared storage')) def do_host_evacuate(cs, args): """Evacuate all instances from failed host.""" hypervisors = cs.hypervisors.search(args.host, servers=True) diff --git a/novaclient/v1_1/contrib/host_evacuate_live.py b/novaclient/v1_1/contrib/host_evacuate_live.py index 0f6a59673..276fb2f70 100644 --- a/novaclient/v1_1/contrib/host_evacuate_live.py +++ b/novaclient/v1_1/contrib/host_evacuate_live.py @@ -14,6 +14,7 @@ # under the License. from novaclient.i18n import _ +from novaclient.openstack.common import cliutils from novaclient import utils @@ -37,19 +38,22 @@ def __init__(self, server_uuid, live_migration_accepted, error_message) -@utils.arg('host', metavar='', help='Name of host.') -@utils.arg('--target-host', - metavar='', - default=None, - help=_('Name of target host.')) -@utils.arg('--block-migrate', - action='store_true', - default=False, - help=_('Enable block migration.')) -@utils.arg('--disk-over-commit', - action='store_true', - default=False, - help=_('Enable disk overcommit.')) +@cliutils.arg('host', metavar='', help='Name of host.') +@cliutils.arg( + '--target-host', + metavar='', + default=None, + help=_('Name of target host.')) +@cliutils.arg( + '--block-migrate', + action='store_true', + default=False, + help=_('Enable block migration.')) +@cliutils.arg( + '--disk-over-commit', + action='store_true', + default=False, + help=_('Enable disk overcommit.')) def do_host_evacuate_live(cs, args): """Live migrate all instances of the specified host to other available hosts. diff --git a/novaclient/v1_1/contrib/host_servers_migrate.py b/novaclient/v1_1/contrib/host_servers_migrate.py index 95268186b..470f161f1 100644 --- a/novaclient/v1_1/contrib/host_servers_migrate.py +++ b/novaclient/v1_1/contrib/host_servers_migrate.py @@ -15,6 +15,7 @@ from novaclient import base from novaclient.i18n import _ +from novaclient.openstack.common import cliutils from novaclient import utils @@ -36,7 +37,7 @@ def _server_migrate(cs, server): "error_message": error_message}) -@utils.arg('host', metavar='', help='Name of host.') +@cliutils.arg('host', metavar='', help='Name of host.') def do_host_servers_migrate(cs, args): """Migrate all instances of the specified host to other available hosts.""" hypervisors = cs.hypervisors.search(args.host, servers=True) diff --git a/novaclient/v1_1/contrib/instance_action.py b/novaclient/v1_1/contrib/instance_action.py index 2f20e0611..10e518ea2 100644 --- a/novaclient/v1_1/contrib/instance_action.py +++ b/novaclient/v1_1/contrib/instance_action.py @@ -17,6 +17,7 @@ from novaclient import base from novaclient.i18n import _ +from novaclient.openstack.common import cliutils from novaclient import utils @@ -40,11 +41,11 @@ def list(self, server): base.getid(server), 'instanceActions') -@utils.arg( +@cliutils.arg( 'server', metavar='', help=_('Name or UUID of the server to show an action for.')) -@utils.arg( +@cliutils.arg( 'request_id', metavar='', help=_('Request ID of the action to get.')) @@ -58,7 +59,7 @@ def do_instance_action(cs, args): utils.print_dict(action) -@utils.arg( +@cliutils.arg( 'server', metavar='', help=_('Name or UUID of the server to list actions for.')) diff --git a/novaclient/v1_1/contrib/metadata_extensions.py b/novaclient/v1_1/contrib/metadata_extensions.py index 54bf8fd04..36bef9abb 100644 --- a/novaclient/v1_1/contrib/metadata_extensions.py +++ b/novaclient/v1_1/contrib/metadata_extensions.py @@ -14,24 +14,26 @@ # under the License. from novaclient.i18n import _ -from novaclient import utils +from novaclient.openstack.common import cliutils from novaclient.v1_1 import shell -@utils.arg('host', - metavar='', - help=_('Name of host.')) -@utils.arg('action', - metavar='', - choices=['set', 'delete'], - help=_("Actions: 'set' or 'delete'")) -@utils.arg('metadata', - metavar='', - nargs='+', - action='append', - default=[], - help=_('Metadata to set or delete (only key is necessary on ' - 'delete)')) +@cliutils.arg( + 'host', + metavar='', + help=_('Name of host.')) +@cliutils.arg( + 'action', + metavar='', + choices=['set', 'delete'], + help=_("Actions: 'set' or 'delete'")) +@cliutils.arg( + 'metadata', + metavar='', + nargs='+', + action='append', + default=[], + help=_('Metadata to set or delete (only key is necessary on delete)')) def do_host_meta(cs, args): """Set or Delete metadata on all instances of a host.""" hypervisors = cs.hypervisors.search(args.host, servers=True) diff --git a/novaclient/v1_1/contrib/migrations.py b/novaclient/v1_1/contrib/migrations.py index c10fb3519..15959818a 100644 --- a/novaclient/v1_1/contrib/migrations.py +++ b/novaclient/v1_1/contrib/migrations.py @@ -18,6 +18,7 @@ from novaclient import base from novaclient.i18n import _ +from novaclient.openstack.common import cliutils from novaclient import utils @@ -53,18 +54,21 @@ def list(self, host=None, status=None, cell_name=None): return self._list("/os-migrations%s" % query_string, "migrations") -@utils.arg('--host', - dest='host', - metavar='', - help=_('Fetch migrations for the given host.')) -@utils.arg('--status', - dest='status', - metavar='', - help=_('Fetch migrations for the given status.')) -@utils.arg('--cell_name', - dest='cell_name', - metavar='', - help=_('Fetch migrations for the given cell_name.')) +@cliutils.arg( + '--host', + dest='host', + metavar='', + help=_('Fetch migrations for the given host.')) +@cliutils.arg( + '--status', + dest='status', + metavar='', + help=_('Fetch migrations for the given status.')) +@cliutils.arg( + '--cell_name', + dest='cell_name', + metavar='', + help=_('Fetch migrations for the given cell_name.')) def do_migration_list(cs, args): """Print a list of migrations.""" _print_migrations(cs.migrations.list(args.host, args.status, diff --git a/novaclient/v1_1/contrib/tenant_networks.py b/novaclient/v1_1/contrib/tenant_networks.py index 3bd2547d4..06edeb145 100644 --- a/novaclient/v1_1/contrib/tenant_networks.py +++ b/novaclient/v1_1/contrib/tenant_networks.py @@ -14,6 +14,7 @@ from novaclient import base from novaclient.i18n import _ +from novaclient.openstack.common import cliutils from novaclient import utils @@ -40,7 +41,7 @@ def create(self, label, cidr): return self._create('/os-tenant-networks', body, 'network') -@utils.arg('network_id', metavar='', help='ID of network') +@cliutils.arg('network_id', metavar='', help='ID of network') def do_net(cs, args): """ Show a network @@ -57,12 +58,14 @@ def do_net_list(cs, args): utils.print_list(networks, ['ID', 'Label', 'CIDR']) -@utils.arg('label', metavar='', - help=_('Network label (ex. my_new_network)')) -@utils.arg( - 'cidr', metavar='', - help=_('IP block to allocate from (ex. 172.16.0.0/24 or ' - '2001:DB8::/64)')) +@cliutils.arg( + 'label', + metavar='', + help=_('Network label (ex. my_new_network)')) +@cliutils.arg( + 'cidr', + metavar='', + help=_('IP block to allocate from (ex. 172.16.0.0/24 or 2001:DB8::/64)')) def do_net_create(cs, args): """ Create a network @@ -71,7 +74,7 @@ def do_net_create(cs, args): utils.print_dict(network._info) -@utils.arg('network_id', metavar='', help='ID of network') +@cliutils.arg('network_id', metavar='', help='ID of network') def do_net_delete(cs, args): """ Delete a network From 912506b3d57c7792e675d8659eb57f96e6c17a87 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Thu, 4 Dec 2014 21:34:17 +0000 Subject: [PATCH 0653/1705] Updated from global requirements Change-Id: I31700b01e5b56221059b64ca66ec2e7d374d5c84 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 532705177..22296a8e4 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -13,4 +13,4 @@ sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3 oslosphinx>=2.2.0 # Apache-2.0 testrepository>=0.0.18 testscenarios>=0.4 -testtools>=0.9.36,!=1.2.0,!=1.4.0 +testtools>=0.9.36,!=1.2.0 From c4233cfd2aaf864028571491fb1c9dd249dd3337 Mon Sep 17 00:00:00 2001 From: Jeremy Stanley Date: Fri, 5 Dec 2014 03:30:40 +0000 Subject: [PATCH 0654/1705] Workflow documentation is now in infra-manual Replace URLs for workflow documentation to appropriate parts of the OpenStack Project Infrastructure Manual. Change-Id: I75618aaf9e07324985a83ca74d35bcab59e186c1 --- CONTRIBUTING.rst | 4 ++-- README.rst | 2 +- doc/source/index.rst | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index a0a98b1f2..bdb4ad7eb 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -1,13 +1,13 @@ If you would like to contribute to the development of OpenStack, you must follow the steps documented at: - http://wiki.openstack.org/HowToContribute#If_you.27re_a_developer + http://docs.openstack.org/infra/manual/developers.html#development-workflow Once those steps have been completed, changes to OpenStack should be submitted for review via the Gerrit tool, following the workflow documented at: - http://wiki.openstack.org/GerritWorkflow + http://docs.openstack.org/infra/manual/developers.html#development-workflow Pull requests submitted through GitHub will be ignored. diff --git a/README.rst b/README.rst index 2bbee7483..4b8b8bb25 100644 --- a/README.rst +++ b/README.rst @@ -18,7 +18,7 @@ pull requests. .. _Github: https://github.com/openstack/python-novaclient .. _Launchpad: https://launchpad.net/python-novaclient -.. _Gerrit: http://wiki.openstack.org/GerritWorkflow +.. _Gerrit: http://docs.openstack.org/infra/manual/developers.html#development-workflow python-novaclient is licensed under the Apache License like the rest of OpenStack. diff --git a/doc/source/index.rst b/doc/source/index.rst index a7d547d1e..eb551e1ae 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -38,7 +38,7 @@ Code is hosted at `git.openstack.org`_. Submit bugs to the Nova project on .. _git.openstack.org: https://git.openstack.org/cgit/openstack/python-novaclient .. _Launchpad: https://launchpad.net/nova -.. _Gerrit: http://wiki.openstack.org/GerritWorkflow +.. _Gerrit: http://docs.openstack.org/infra/manual/developers.html#development-workflow Testing ------- From 85e15fe8856c1c689f77c488f0b9ff80263c6f5e Mon Sep 17 00:00:00 2001 From: melanie witt Date: Wed, 10 Dec 2014 20:41:16 +0000 Subject: [PATCH 0655/1705] pass id to ServerGroupsManager in ServerGroup.delete() ServerGroup object delete currently fails because the manager object requires the server group id and is instead passed the server group object itself (copy-paste error). This patch changes it to pass the id. Change-Id: Ic6a4a38157f52343fbb3e75a02d1f7c6bef28d99 Closes-Bug: #1400494 --- novaclient/tests/v1_1/test_server_groups.py | 6 ++++++ novaclient/v1_1/server_groups.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/novaclient/tests/v1_1/test_server_groups.py b/novaclient/tests/v1_1/test_server_groups.py index e341dc0c3..f7a5a2143 100644 --- a/novaclient/tests/v1_1/test_server_groups.py +++ b/novaclient/tests/v1_1/test_server_groups.py @@ -51,3 +51,9 @@ def test_delete_server_group(self): id = '2cbd51f4-fafe-4cdb-801b-cf913a6f288b' self.cs.server_groups.delete(id) self.assert_called('DELETE', '/os-server-groups/%s' % id) + + def test_delete_server_group_object(self): + id = '2cbd51f4-fafe-4cdb-801b-cf913a6f288b' + server_group = self.cs.server_groups.get(id) + server_group.delete() + self.assert_called('DELETE', '/os-server-groups/%s' % id) diff --git a/novaclient/v1_1/server_groups.py b/novaclient/v1_1/server_groups.py index be6ff8eef..a5205e772 100644 --- a/novaclient/v1_1/server_groups.py +++ b/novaclient/v1_1/server_groups.py @@ -30,7 +30,7 @@ def __repr__(self): return '' % self.id def delete(self): - self.manager.delete(self) + self.manager.delete(self.id) class ServerGroupsManager(base.ManagerWithFind): From 60d7baf2afdf5d14fecd0c24e54252876a1f7640 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Thu, 11 Dec 2014 07:20:11 +0000 Subject: [PATCH 0656/1705] Updated from global requirements Change-Id: I7ef8410576abe82bb1b5a0dda983ad103c688509 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 0b1c7c0e4..904d5c918 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ argparse iso8601>=0.1.9 oslo.i18n>=1.0.0 # Apache-2.0 oslo.serialization>=1.0.0 # Apache-2.0 -oslo.utils>=1.0.0 # Apache-2.0 +oslo.utils>=1.1.0 # Apache-2.0 PrettyTable>=0.7,<0.8 requests>=2.2.0,!=2.4.0 simplejson>=2.2.0 From ab2f1a742da217ff2de469267c4c4279ebddd4de Mon Sep 17 00:00:00 2001 From: Balazs Gibizer Date: Fri, 12 Dec 2014 15:09:12 +0100 Subject: [PATCH 0657/1705] Reject non existent mock assert calls assert_called and assert_not_called are not asserting the state of the mock object but considered as mocked calls so mock will never raise exception but always executed successfully This change patches the Mock class during unit test to raise an exception if a function called on a mock object that's name starts with 'assert' and does not one of the supported Mock assert calls. This change also fix the unit test to call only the supported assert function on mock object. Change-Id: I036587f355e42e362ac2b70fb6755cca81d30b75 --- novaclient/tests/utils.py | 24 ++++++++++++++++++++++++ novaclient/tests/v1_1/test_auth.py | 6 +++--- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/novaclient/tests/utils.py b/novaclient/tests/utils.py index a257ac1fa..ab1ddcd18 100644 --- a/novaclient/tests/utils.py +++ b/novaclient/tests/utils.py @@ -14,6 +14,7 @@ import os import fixtures +import mock from oslo.serialization import jsonutils import requests from requests_mock.contrib import fixture as requests_mock_fixture @@ -26,6 +27,29 @@ AUTH_URL_V2 = "http://localhost:5002/auth_url/v2.0" +def _patch_mock_to_raise_for_invalid_assert_calls(): + def raise_for_invalid_assert_calls(wrapped): + def wrapper(_self, name): + valid_asserts = [ + 'assert_called_with', + 'assert_called_once_with', + 'assert_has_calls', + 'assert_any_calls'] + + if name.startswith('assert') and name not in valid_asserts: + raise AttributeError('%s is not a valid mock assert method' + % name) + + return wrapped(_self, name) + return wrapper + mock.Mock.__getattr__ = raise_for_invalid_assert_calls( + mock.Mock.__getattr__) + +# NOTE(gibi): needs to be called only once at import time +# to patch the mock lib +_patch_mock_to_raise_for_invalid_assert_calls() + + class TestCase(testtools.TestCase): TEST_REQUEST_BASE = { 'verify': True, diff --git a/novaclient/tests/v1_1/test_auth.py b/novaclient/tests/v1_1/test_auth.py index 6355b3245..97e402798 100644 --- a/novaclient/tests/v1_1/test_auth.py +++ b/novaclient/tests/v1_1/test_auth.py @@ -369,8 +369,8 @@ def test_auth_automatic(self): @mock.patch.object(http_client, 'authenticate') def test_auth_call(m): http_client.get('/') - m.assert_called() - mock_request.assert_called() + self.assertTrue(m.called) + self.assertTrue(mock_request.called) test_auth_call() @@ -381,6 +381,6 @@ def test_auth_manual(self): @mock.patch.object(cs.client, 'authenticate') def test_auth_call(m): cs.authenticate() - m.assert_called() + self.assertTrue(m.called) test_auth_call() From 4b530bf9d629a93324af63da1fc4afe997c4e9b6 Mon Sep 17 00:00:00 2001 From: Steven Kaufer Date: Thu, 28 Aug 2014 02:58:58 +0000 Subject: [PATCH 0658/1705] novaclient sort parameters support Adds sorting support to the 'nova list' command. --sort [:] The --sort parameter is comma-separated and is used to specify one or more sort keys and directions. The direction defaults to 'desc' for each sort key and the user can supply 'asc' to override. Partially implements: blueprint nova-pagination Change-Id: I635e017c7f9ab61812333983bfecccd6fce8d394 --- novaclient/tests/v1_1/test_servers.py | 19 ++++++++++ novaclient/tests/v1_1/test_shell.py | 54 +++++++++++++++++++++++++++ novaclient/v1_1/servers.py | 17 +++++++-- novaclient/v1_1/shell.py | 29 +++++++++++++- 4 files changed, 114 insertions(+), 5 deletions(-) diff --git a/novaclient/tests/v1_1/test_servers.py b/novaclient/tests/v1_1/test_servers.py index 3797e5754..c4037e1b3 100644 --- a/novaclient/tests/v1_1/test_servers.py +++ b/novaclient/tests/v1_1/test_servers.py @@ -49,6 +49,25 @@ def test_list_servers_with_marker_limit(self): for s in sl: self.assertIsInstance(s, servers.Server) + def test_list_servers_sort_single(self): + sl = self.cs.servers.list(sort_keys=['display_name'], + sort_dirs=['asc']) + self.assert_called( + 'GET', + '/servers/detail?sort_dir=asc&sort_key=display_name') + for s in sl: + self.assertIsInstance(s, servers.Server) + + def test_list_servers_sort_multiple(self): + sl = self.cs.servers.list(sort_keys=['display_name', 'id'], + sort_dirs=['asc', 'desc']) + self.assert_called( + 'GET', + ('/servers/detail?sort_dir=asc&sort_dir=desc&' + 'sort_key=display_name&sort_key=id')) + for s in sl: + self.assertIsInstance(s, servers.Server) + def test_get_server_details(self): s = self.cs.servers.get(1234) self.assert_called('GET', '/servers/1234') diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index 622e082fc..c1b5e1af4 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -851,6 +851,60 @@ def test_list_by_user(self): 'GET', '/servers/detail?all_tenants=1&user_id=fake_user') + def test_list_with_single_sort_key_no_dir(self): + self.run_command('list --sort 1') + self.assert_called( + 'GET', ('/servers/detail?sort_dir=desc&sort_key=1')) + + def test_list_with_single_sort_key_and_dir(self): + self.run_command('list --sort 1:asc') + self.assert_called( + 'GET', ('/servers/detail?sort_dir=asc&sort_key=1')) + + def test_list_with_sort_keys_no_dir(self): + self.run_command('list --sort 1,2') + self.assert_called( + 'GET', ('/servers/detail?sort_dir=desc&sort_dir=desc&' + 'sort_key=1&sort_key=2')) + + def test_list_with_sort_keys_and_dirs(self): + self.run_command('list --sort 1:asc,2:desc') + self.assert_called( + 'GET', ('/servers/detail?sort_dir=asc&sort_dir=desc&' + 'sort_key=1&sort_key=2')) + + def test_list_with_sort_keys_and_some_dirs(self): + self.run_command('list --sort 1,2:asc') + self.assert_called( + 'GET', ('/servers/detail?sort_dir=desc&sort_dir=asc&' + 'sort_key=1&sort_key=2')) + + def test_list_with_invalid_sort_dir_one(self): + cmd = 'list --sort 1:foo' + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + def test_list_with_invalid_sort_dir_two(self): + cmd = 'list --sort 1:asc,2:foo' + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + def test_list_sortby_index_with_sort(self): + # sortby_index is None if there is sort information + for cmd in ['list --sort key', + 'list --sort key:desc', + 'list --sort key1,key2:asc']: + with mock.patch('novaclient.utils.print_list') as mock_print_list: + self.run_command(cmd) + mock_print_list.assert_called_once_with( + mock.ANY, mock.ANY, mock.ANY, sortby_index=None) + + def test_list_sortby_index_without_sort(self): + # sortby_index is 1 without sort information + for cmd in ['list', 'list --minimal', 'list --deleted']: + with mock.patch('novaclient.utils.print_list') as mock_print_list: + self.run_command(cmd) + mock_print_list.assert_called_once_with( + mock.ANY, mock.ANY, mock.ANY, sortby_index=1) + def test_list_fields(self): output = self.run_command( 'list --fields ' diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index 2fe29afab..f1b10a997 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -566,7 +566,8 @@ def get(self, server): """ return self._get("/servers/%s" % base.getid(server), "server") - def list(self, detailed=True, search_opts=None, marker=None, limit=None): + def list(self, detailed=True, search_opts=None, marker=None, limit=None, + sort_keys=None, sort_dirs=None): """ Get a list of servers. @@ -575,6 +576,8 @@ def list(self, detailed=True, search_opts=None, marker=None, limit=None): :param marker: Begin returning servers that appear later in the server list than that represented by this server id (optional). :param limit: Maximum number of servers to return (optional). + :param sort_keys: List of sort keys + :param sort_dirs: List of sort directions :rtype: list of :class:`Server` """ @@ -595,8 +598,16 @@ def list(self, detailed=True, search_opts=None, marker=None, limit=None): # Transform the dict to a sequence of two-element tuples in fixed # order, then the encoded string will be consistent in Python 2&3. - if qparams: - new_qparams = sorted(qparams.items(), key=lambda x: x[0]) + if qparams or sort_keys or sort_dirs: + # sort keys and directions are unique since the same parameter + # key is repeated for each associated value + # (ie, &sort_key=key1&sort_key=key2&sort_key=key3) + items = list(qparams.items()) + if sort_keys: + items.extend(('sort_key', sort_key) for sort_key in sort_keys) + if sort_dirs: + items.extend(('sort_dir', sort_dir) for sort_dir in sort_dirs) + new_qparams = sorted(items, key=lambda x: x[0]) query_string = "?%s" % parse.urlencode(new_qparams) else: query_string = "" diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index ddf9de979..3a438fdff 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1308,6 +1308,13 @@ def do_image_delete(cs, args): action="store_true", default=False, help=_('Get only uuid and name.')) +@cliutils.arg( + '--sort', + dest='sort', + metavar='[:]', + help=('Comma-separated list of sort keys and directions in the form' + ' of [:]. The direction defaults to descending if' + ' not specified.')) def do_list(cs, args): """List active servers.""" imageid = None @@ -1350,8 +1357,23 @@ def do_list(cs, args): detailed = not args.minimal + sort_keys = [] + sort_dirs = [] + if args.sort: + for sort in args.sort.split(','): + sort_key, _sep, sort_dir = sort.partition(':') + if not sort_dir: + sort_dir = 'desc' + elif sort_dir not in ('asc', 'desc'): + raise exceptions.CommandError(_( + 'Unknown sort direction: %s') % sort_dir) + sort_keys.append(sort_key) + sort_dirs.append(sort_dir) + servers = cs.servers.list(detailed=detailed, - search_opts=search_opts) + search_opts=search_opts, + sort_keys=sort_keys, + sort_dirs=sort_dirs) convert = [('OS-EXT-SRV-ATTR:host', 'host'), ('OS-EXT-STS:task_state', 'task_state'), ('OS-EXT-SRV-ATTR:instance_name', 'instance_name'), @@ -1375,8 +1397,11 @@ def do_list(cs, args): 'Networks' ] formatters['Networks'] = utils._format_servers_list_networks + sortby_index = 1 + if args.sort: + sortby_index = None utils.print_list(servers, columns, - formatters, sortby_index=1) + formatters, sortby_index=sortby_index) @cliutils.arg( From 5f154636c2037ee16e972c7dd1d78f60807dfacf Mon Sep 17 00:00:00 2001 From: yamini sardana Date: Thu, 18 Dec 2014 16:10:52 +0530 Subject: [PATCH 0659/1705] Display tenant id with nova list --all-tenants For each server, nova list --all-tenants will now display its corresponding tenant ID as well. Change-Id: I586d6c5bbb15ebb5df25dc4fa849ac4d5af149e9 Closes-bug: #1403431 --- novaclient/v1_1/shell.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 3a438fdff..a8acb5bf7 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1396,6 +1396,10 @@ def do_list(cs, args): 'Power State', 'Networks' ] + # If getting the data for all tenants, print + # Tenant ID as well + if search_opts['all_tenants']: + columns.insert(2, 'Tenant ID') formatters['Networks'] = utils._format_servers_list_networks sortby_index = 1 if args.sort: From 003f1ed0487faaee439f6d38f3de5a3e0c44a25b Mon Sep 17 00:00:00 2001 From: Jamie Lennox Date: Wed, 17 Dec 2014 15:47:15 +1000 Subject: [PATCH 0660/1705] Pass kwargs through to the adapter A major point of the adapter is that we should be able to add new arguments to the adapter that are supported automatically by the clients that support it. This means we have to pass all additional arguments through to adapter. Regardless of whether the arguments are passed to the adapter or to the traditional client the kwargs are explicitly provided so we won't end up with missing arguments. Closes-Bug: #1403329 Change-Id: I7303b184875dc296ca855ad89496173c42070879 --- novaclient/client.py | 4 +-- novaclient/tests/v1_1/test_client.py | 45 ++++++++++++++++++++++++++++ novaclient/v1_1/client.py | 5 ++-- 3 files changed, 50 insertions(+), 4 deletions(-) create mode 100644 novaclient/tests/v1_1/test_client.py diff --git a/novaclient/client.py b/novaclient/client.py index a04a92e47..790489c99 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -720,11 +720,11 @@ def _construct_http_client(username=None, password=None, project_id=None, auth_token=None, cacert=None, tenant_id=None, user_id=None, connection_pool=False, session=None, auth=None, user_agent='python-novaclient', - **kwargs): + interface=None, **kwargs): if session: return SessionClient(session=session, auth=auth, - interface=endpoint_type, + interface=interface or endpoint_type, service_type=service_type, region_name=region_name, service_name=service_name, diff --git a/novaclient/tests/v1_1/test_client.py b/novaclient/tests/v1_1/test_client.py new file mode 100644 index 000000000..fa0ed3af6 --- /dev/null +++ b/novaclient/tests/v1_1/test_client.py @@ -0,0 +1,45 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import uuid + +from keystoneclient import session + +from novaclient.tests import utils +from novaclient.v1_1 import client + + +class ClientTest(utils.TestCase): + + def test_adapter_properties(self): + # sample of properties, there are many more + user_agent = uuid.uuid4().hex + endpoint_override = uuid.uuid4().hex + + s = session.Session() + c = client.Client(session=s, + user_agent=user_agent, + endpoint_override=endpoint_override) + + self.assertEqual(user_agent, c.client.user_agent) + self.assertEqual(endpoint_override, c.client.endpoint_override) + + def test_passing_interface(self): + endpoint_type = uuid.uuid4().hex + interface = uuid.uuid4().hex + + s = session.Session() + c = client.Client(session=s, + interface=interface, + endpoint_type=endpoint_type) + + self.assertEqual(interface, c.client.interface) diff --git a/novaclient/v1_1/client.py b/novaclient/v1_1/client.py index 70d5298dc..5e26ddd64 100644 --- a/novaclient/v1_1/client.py +++ b/novaclient/v1_1/client.py @@ -103,7 +103,7 @@ def __init__(self, username=None, api_key=None, project_id=None, auth_system='keystone', auth_plugin=None, auth_token=None, cacert=None, tenant_id=None, user_id=None, connection_pool=False, session=None, auth=None, - completion_cache=None): + completion_cache=None, **kwargs): # FIXME(comstud): Rename the api_key argument above when we # know it's not being used as keyword argument @@ -192,7 +192,8 @@ def __init__(self, username=None, api_key=None, project_id=None, cacert=cacert, connection_pool=connection_pool, session=session, - auth=auth) + auth=auth, + **kwargs) self.completion_cache = completion_cache From c8ac6883c396606db21d1226882b8301b4c7b2fe Mon Sep 17 00:00:00 2001 From: Haiwei Xu Date: Fri, 19 Dec 2014 11:49:33 +0900 Subject: [PATCH 0661/1705] Add 'Id' column to floating-ip-list Currently nova floating-ip-list doesn't show the ip's id, though the id is return from the server. This patch adds the 'Id' column. Change-Id: I8680fb6344bf8f6862a476cdb95c936a5fbab965 Closes-bug: #1404091 --- novaclient/v1_1/shell.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 3a438fdff..21a335dce 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2323,7 +2323,8 @@ def _print_floating_ip_list(floating_ips): convert = [('instance_id', 'server_id')] _translate_keys(floating_ips, convert) - utils.print_list(floating_ips, ['Ip', 'Server Id', 'Fixed Ip', 'Pool']) + utils.print_list(floating_ips, + ['Id', 'Ip', 'Server Id', 'Fixed Ip', 'Pool']) @cliutils.arg('server', metavar='', help=_('Name or ID of server.')) From 7bced6673c19c69ab7ab190084162dede416a9fb Mon Sep 17 00:00:00 2001 From: David Kranz Date: Tue, 6 Jan 2015 09:33:36 -0500 Subject: [PATCH 0662/1705] Document unexpected need for --all-tenants when using --tenant This behavior was changed in v3 but that is now obsolete. Change-Id: I5e1ce2ab3ac60dc637c4416cbe1be088230c07b6 Related-Bug: #1185290 --- novaclient/v1_1/shell.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 2952c604f..1463f5fd4 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1283,7 +1283,8 @@ def do_image_delete(cs, args): dest='tenant', metavar='', nargs='?', - help=_('Display information from single tenant (Admin only).')) + help=_('Display information from single tenant (Admin only). ' + 'The --all-tenants option must also be provided.')) @cliutils.arg( '--user', dest='user', From f75ea86a2a4ee0bed076ee9e4ea5021eb0643e16 Mon Sep 17 00:00:00 2001 From: Liang Chen Date: Wed, 7 Jan 2015 13:23:10 -0500 Subject: [PATCH 0663/1705] Directly using base64 encoding for injected files Binary files cannot be treated as utf-8 byte streams and converted to plain. Change this back to just using base64 encoding as it was before commit 8b264fc61d21fe4d0c7405914fb084f898956888. Change-Id: I4ef6142676022b2e2f3178e7bfa24ed985fcae2c Closes-Bug: #1408088 --- novaclient/v1_1/servers.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index b75c95b9b..a0109094a 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -505,7 +505,9 @@ def _boot(self, resource_url, response_key, name, image, flavor, else: data = file_or_string - cont = base64.b64encode(data.encode('utf-8')).decode('utf-8') + if six.PY3 and isinstance(data, str): + data = data.encode('utf-8') + cont = base64.b64encode(data).decode('utf-8') personality.append({ 'path': filepath, 'contents': cont, From b5e36ced8fbae0b93cd713b1cf47c7cc877837b8 Mon Sep 17 00:00:00 2001 From: Matthew Gilliard Date: Thu, 18 Sep 2014 14:32:45 +0000 Subject: [PATCH 0664/1705] Adds separate class for Hypervisor Stats Hypervisor stats was being called from the Hypervisors class, which means that the statistics were being modeled as if they were a single Hypervisor. This mostly worked, except that the stats didn't have an id field so a call to __repr__() (implicitly called by print, or in the REPL) would throw an AttributeError. This patch creates a new class HypervisorStats which models a collection of statistics about hypervisors. So you can now call: nc.hypervisor_stats.statistics() The old call of nc.hypervisors.statistics() is left for backward compatibility but just calls into the new method. Change-Id: Ia31aacb95b1d517dab3ad38763d6448715bab68e Closes-bug: 1370415 --- novaclient/tests/v1_1/test_hypervisors.py | 8 ++++++++ novaclient/v1_1/client.py | 1 + novaclient/v1_1/hypervisors.py | 19 +++++++++++++++++++ novaclient/v1_1/shell.py | 2 +- 4 files changed, 29 insertions(+), 1 deletion(-) diff --git a/novaclient/tests/v1_1/test_hypervisors.py b/novaclient/tests/v1_1/test_hypervisors.py index 94bbb0838..1a6212015 100644 --- a/novaclient/tests/v1_1/test_hypervisors.py +++ b/novaclient/tests/v1_1/test_hypervisors.py @@ -168,3 +168,11 @@ def test_hypervisor_statistics(self): self.assert_called('GET', '/os-hypervisors/statistics') self.compare_to_expected(expected, result) + + def test_hypervisor_statistics_data_model(self): + result = self.cs.hypervisor_stats.statistics() + self.assert_called('GET', '/os-hypervisors/statistics') + + # Test for Bug #1370415, the line below used to raise AttributeError + self.assertEqual("", + result.__repr__()) diff --git a/novaclient/v1_1/client.py b/novaclient/v1_1/client.py index 5e26ddd64..83789aa6b 100644 --- a/novaclient/v1_1/client.py +++ b/novaclient/v1_1/client.py @@ -151,6 +151,7 @@ def __init__(self, username=None, api_key=None, project_id=None, self.aggregates = aggregates.AggregateManager(self) self.hosts = hosts.HostManager(self) self.hypervisors = hypervisors.HypervisorManager(self) + self.hypervisor_stats = hypervisors.HypervisorStatsManager(self) self.services = services.ServiceManager(self) self.fixed_ips = fixed_ips.FixedIPsManager(self) self.floating_ips_bulk = floating_ips_bulk.FloatingIPBulkManager(self) diff --git a/novaclient/v1_1/hypervisors.py b/novaclient/v1_1/hypervisors.py index 4a4f2b93a..b1bfcb4b9 100644 --- a/novaclient/v1_1/hypervisors.py +++ b/novaclient/v1_1/hypervisors.py @@ -66,6 +66,25 @@ def uptime(self, hypervisor): return self._get("/os-hypervisors/%s/uptime" % base.getid(hypervisor), "hypervisor") + def statistics(self): + """ + Get hypervisor statistics over all compute nodes. + + Kept for backwards compatibility, new code should call + hypervisor_stats.statistics() instead of hypervisors.statistics() + """ + return self.api.hypervisor_stats.statistics() + + +class HypervisorStats(base.Resource): + def __repr__(self): + return ("" % + (self.count, "s" if self.count != 1 else "")) + + +class HypervisorStatsManager(base.Manager): + resource_class = HypervisorStats + def statistics(self): """ Get hypervisor statistics over all compute nodes. diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 1463f5fd4..9eaca1177 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -3629,7 +3629,7 @@ def do_hypervisor_uptime(cs, args): def do_hypervisor_stats(cs, args): """Get hypervisor statistics over all compute nodes.""" - stats = cs.hypervisors.statistics() + stats = cs.hypervisor_stats.statistics() utils.print_dict(stats._info.copy()) From 71fe9cb82ededbf5e01205409e9eefd35bff0a22 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 9 Jan 2015 18:35:52 +0000 Subject: [PATCH 0665/1705] Updated from global requirements Change-Id: Icc68b5f77c03a693316ce9dd828733f81ac2f965 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 904d5c918..e34297b65 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ argparse iso8601>=0.1.9 oslo.i18n>=1.0.0 # Apache-2.0 oslo.serialization>=1.0.0 # Apache-2.0 -oslo.utils>=1.1.0 # Apache-2.0 +oslo.utils>=1.2.0 # Apache-2.0 PrettyTable>=0.7,<0.8 requests>=2.2.0,!=2.4.0 simplejson>=2.2.0 From 0eb2e72d38ff4aee9dac9ec909282ecb5c4fd0d0 Mon Sep 17 00:00:00 2001 From: Joe Gordon Date: Sun, 11 Jan 2015 01:30:19 +0000 Subject: [PATCH 0666/1705] Move to hacking 0.10 Release notes: http://lists.openstack.org/pipermail/openstack-dev/2015-January/054165.html Explicitly move everything to new style classes (H238). http://python3porting.com/preparing.html#use-new-style-classes Remove deleted hacking rules from tox.ini Change-Id: If65b0060e6f64a456b4869ef4129ad15aef107fb --- novaclient/v1_1/shell.py | 10 +++++----- test-requirements.txt | 2 +- tox.ini | 8 +------- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 1463f5fd4..16f40fe9f 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2234,7 +2234,7 @@ def do_get_vnc_console(cs, args): server = _find_server(cs, args.server) data = server.get_vnc_console(args.console_type) - class VNCConsole: + class VNCConsole(object): def __init__(self, console_dict): self.type = console_dict['type'] self.url = console_dict['url'] @@ -2252,7 +2252,7 @@ def do_get_spice_console(cs, args): server = _find_server(cs, args.server) data = server.get_spice_console(args.console_type) - class SPICEConsole: + class SPICEConsole(object): def __init__(self, console_dict): self.type = console_dict['type'] self.url = console_dict['url'] @@ -2270,7 +2270,7 @@ def do_get_rdp_console(cs, args): server = _find_server(cs, args.server) data = server.get_rdp_console(args.console_type) - class RDPConsole: + class RDPConsole(object): def __init__(self, console_dict): self.type = console_dict['type'] self.url = console_dict['url'] @@ -2292,7 +2292,7 @@ def do_get_serial_console(cs, args): server = _find_server(cs, args.server) data = server.get_serial_console(args.console_type) - class SerialConsole: + class SerialConsole(object): def __init__(self, console_dict): self.type = console_dict['type'] self.url = console_dict['url'] @@ -2577,7 +2577,7 @@ def do_dns_create_public_domain(cs, args): def _print_secgroup_rules(rules, show_source_group=True): - class FormattedRule: + class FormattedRule(object): def __init__(self, obj): items = (obj if isinstance(obj, dict) else obj._info).items() for k, v in items: diff --git a/test-requirements.txt b/test-requirements.txt index 22296a8e4..d6628ed82 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,7 +1,7 @@ # 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. -hacking>=0.9.2,<0.10 +hacking>=0.10.0,<0.11 coverage>=3.6 discover diff --git a/tox.ini b/tox.ini index 88ac1e26f..72fb43826 100644 --- a/tox.ini +++ b/tox.ini @@ -35,14 +35,8 @@ downloadcache = ~/cache/pip # # Following checks are ignored on purpose. # -# H402 one line docstring needs punctuation -# reason: removed in hacking (https://review.openstack.org/#/c/101497/) -# -# H904 wrap long lines in parentheses instead of a backslash -# reason: removed in hacking (https://review.openstack.org/#/c/101701/) -# # Additional checks are also ignored on purpose: F811, F821 -ignore = E124,F811,F821,H402,H404,H405,H904 +ignore = E124,F811,F821,H404,H405 show-source = True exclude=.venv,.git,.tox,dist,*openstack/common*,*lib/python*,*egg,build,doc/source/conf.py From 17367002609f011710014aef12a898e9f16db81c Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Tue, 13 Jan 2015 00:15:51 +0000 Subject: [PATCH 0667/1705] Updated from global requirements Change-Id: I1ae9906842bc1b4f91de0cb3b047f2c57d751fd7 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e34297b65..996bd48ca 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ pbr>=0.6,!=0.7,<1.0 argparse iso8601>=0.1.9 oslo.i18n>=1.0.0 # Apache-2.0 -oslo.serialization>=1.0.0 # Apache-2.0 +oslo.serialization>=1.2.0 # Apache-2.0 oslo.utils>=1.2.0 # Apache-2.0 PrettyTable>=0.7,<0.8 requests>=2.2.0,!=2.4.0 From 9ee6d696e2dd06a7fb4ebd21671d0c71008e7ae4 Mon Sep 17 00:00:00 2001 From: Pedro Navarro Date: Thu, 15 Jan 2015 15:54:57 +0100 Subject: [PATCH 0668/1705] Improving the help of the lock command The lock command allows you to block the actions on the server for the non-admin users. Improving the help so that api users understands better what locking a server implies. Change-Id: Ibccdd672587c4dc9570b86f0132a64204fe9b5c2 Closes-Bug: #1366279 --- novaclient/v1_1/shell.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 16f40fe9f..dc07004fc 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1619,7 +1619,9 @@ def do_start(cs, args): @cliutils.arg('server', metavar='', help=_('Name or ID of server.')) def do_lock(cs, args): - """Lock a server.""" + """Lock a server. A normal (non-admin) user will not be able to execute + actions on a locked server. + """ _find_server(cs, args.server).lock() From c58bc211f7f6377f24b0d84881c22e59c8a78fd6 Mon Sep 17 00:00:00 2001 From: Christian Berendt Date: Sat, 17 Jan 2015 08:51:36 +0100 Subject: [PATCH 0669/1705] In strings/comments change Ip/ip to IP The correct short form of "Internet Protocol" is IP. Change-Id: Ie5412fafbb093e0948970f030e443bed360c71ea --- novaclient/tests/v1_1/fakes.py | 2 +- novaclient/v1_1/floating_ip_dns.py | 4 +-- novaclient/v1_1/floating_ips.py | 12 ++++----- .../v1_1/security_group_default_rules.py | 2 +- novaclient/v1_1/security_group_rules.py | 2 +- novaclient/v1_1/servers.py | 10 +++---- novaclient/v1_1/shell.py | 26 +++++++++---------- 7 files changed, 29 insertions(+), 29 deletions(-) diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py index 53f9e7451..92c923898 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/v1_1/fakes.py @@ -863,7 +863,7 @@ def post_flavors_2_action(self, body, **kw): return (202, {}, self.get_flavors_2_os_flavor_access()[2]) # - # Floating ips + # Floating IPs # def get_os_floating_ip_pools(self): diff --git a/novaclient/v1_1/floating_ip_dns.py b/novaclient/v1_1/floating_ip_dns.py index b980f7721..41df0f628 100644 --- a/novaclient/v1_1/floating_ip_dns.py +++ b/novaclient/v1_1/floating_ip_dns.py @@ -89,12 +89,12 @@ class FloatingIPDNSEntryManager(base.Manager): resource_class = FloatingIPDNSEntry def get(self, domain, name): - """Return a list of entries for the given domain and ip or name.""" + """Return a list of entries for the given domain and IP or name.""" return self._get("/os-floating-ip-dns/%s/entries/%s" % (_quote_domain(domain), name), "dns_entry") def get_for_ip(self, domain, ip): - """Return a list of entries for the given domain and ip or name.""" + """Return a list of entries for the given domain and IP or name.""" qparams = {'ip': ip} params = "?%s" % parse.urlencode(qparams) diff --git a/novaclient/v1_1/floating_ips.py b/novaclient/v1_1/floating_ips.py index 54979371f..cce92da25 100644 --- a/novaclient/v1_1/floating_ips.py +++ b/novaclient/v1_1/floating_ips.py @@ -20,7 +20,7 @@ class FloatingIP(base.Resource): def delete(self): """ - Delete this floating ip + Delete this floating IP """ self.manager.delete(self) @@ -30,7 +30,7 @@ class FloatingIPManager(base.ManagerWithFind): def list(self, all_tenants=False): """ - List floating ips + List floating IPs """ url = '/os-floating-ips' if all_tenants: @@ -39,21 +39,21 @@ def list(self, all_tenants=False): def create(self, pool=None): """ - Create (allocate) a floating ip for a tenant + Create (allocate) a floating IP for a tenant """ return self._create("/os-floating-ips", {'pool': pool}, "floating_ip") def delete(self, floating_ip): """ - Delete (deallocate) a floating ip for a tenant + Delete (deallocate) a floating IP for a tenant - :param floating_ip: The floating ip address to delete. + :param floating_ip: The floating IP address to delete. """ self._delete("/os-floating-ips/%s" % base.getid(floating_ip)) def get(self, floating_ip): """ - Retrieve a floating ip + Retrieve a floating IP """ return self._get("/os-floating-ips/%s" % base.getid(floating_ip), "floating_ip") diff --git a/novaclient/v1_1/security_group_default_rules.py b/novaclient/v1_1/security_group_default_rules.py index 2627f9d61..d82a1d818 100644 --- a/novaclient/v1_1/security_group_default_rules.py +++ b/novaclient/v1_1/security_group_default_rules.py @@ -50,7 +50,7 @@ def create(self, ip_protocol=None, from_port=None, to_port=None, except (TypeError, ValueError): raise exceptions.CommandError(_("To port must be an integer.")) if ip_protocol.upper() not in ['TCP', 'UDP', 'ICMP']: - raise exceptions.CommandError(_("Ip protocol must be 'tcp', 'udp'" + raise exceptions.CommandError(_("IP protocol must be 'tcp', 'udp'" ", or 'icmp'.")) body = {"security_group_default_rule": { diff --git a/novaclient/v1_1/security_group_rules.py b/novaclient/v1_1/security_group_rules.py index 1f09a20d4..4cb51b01b 100644 --- a/novaclient/v1_1/security_group_rules.py +++ b/novaclient/v1_1/security_group_rules.py @@ -55,7 +55,7 @@ def create(self, parent_group_id, ip_protocol=None, from_port=None, except (TypeError, ValueError): raise exceptions.CommandError(_("To port must be an integer.")) if ip_protocol.upper() not in ['TCP', 'UDP', 'ICMP']: - raise exceptions.CommandError(_("Ip protocol must be 'tcp', 'udp'" + raise exceptions.CommandError(_("IP protocol must be 'tcp', 'udp'" ", or 'icmp'.")) body = {"security_group_rule": { diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index 66384a352..fb998ac27 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -125,7 +125,7 @@ def add_floating_ip(self, address, fixed_address=None): """ Add floating IP to an instance - :param address: The ip address or FloatingIP to add to the instance + :param address: The IP address or FloatingIP to add to the instance :param fixed_address: The fixedIP address the FloatingIP is to be associated with (optional) """ @@ -135,7 +135,7 @@ def remove_floating_ip(self, address): """ Remove floating IP from an instance - :param address: The ip address or FloatingIP to remove + :param address: The IP address or FloatingIP to remove """ self.manager.remove_floating_ip(self, address) @@ -445,7 +445,7 @@ def _boot(self, resource_url, response_key, name, image, flavor, this server. :param nics: (optional extension) an ordered list of nics to be added to this server, with information about - connected networks, fixed ips, etc. + connected networks, fixed IPs, etc. :param scheduler_hints: (optional extension) arbitrary key-value pairs specified by the client to help boot an instance. :param config_drive: (optional extension) If True, enable config drive @@ -639,7 +639,7 @@ def remove_fixed_ip(self, server, address): def add_floating_ip(self, server, address, fixed_address=None): """ - Add a floating ip to an instance + Add a floating IP to an instance :param server: The :class:`Server` (or its ID) to add an IP to. :param address: The FloatingIP or string floating address to add. @@ -886,7 +886,7 @@ def create(self, name, image, flavor, meta=None, files=None, device mappings for this server. :param nics: (optional extension) an ordered list of nics to be added to this server, with information about - connected networks, fixed ips, port etc. + connected networks, fixed IPs, port etc. :param scheduler_hints: (optional extension) arbitrary key-value pairs specified by the client to help boot an instance :param config_drive: (optional extension) value for config drive diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 16f40fe9f..9c8ce4805 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -2329,7 +2329,7 @@ def _print_floating_ip_list(floating_ips): _translate_keys(floating_ips, convert) utils.print_list(floating_ips, - ['Id', 'Ip', 'Server Id', 'Fixed Ip', 'Pool']) + ['Id', 'IP', 'Server Id', 'Fixed IP', 'Pool']) @cliutils.arg('server', metavar='', help=_('Name or ID of server.')) @@ -2434,14 +2434,14 @@ def do_floating_ip_create(cs, args): _print_floating_ip_list([cs.floating_ips.create(pool=args.pool)]) -@cliutils.arg('address', metavar='
', help=_('IP of Floating Ip.')) +@cliutils.arg('address', metavar='
', help=_('IP of Floating IP.')) def do_floating_ip_delete(cs, args): """De-allocate a floating IP.""" floating_ips = cs.floating_ips.list() for floating_ip in floating_ips: if floating_ip.ip == args.address: return cs.floating_ips.delete(floating_ip.id) - raise exceptions.CommandError(_("Floating ip %s not found.") % + raise exceptions.CommandError(_("Floating IP %s not found.") % args.address) @@ -2451,12 +2451,12 @@ def do_floating_ip_delete(cs, args): default=False, help=_('Display floatingips from all tenants (Admin only).')) def do_floating_ip_list(cs, args): - """List floating ips.""" + """List floating IPs.""" _print_floating_ip_list(cs.floating_ips.list(args.all_tenants)) def do_floating_ip_pool_list(cs, _args): - """List all floating ip pools.""" + """List all floating IP pools.""" utils.print_list(cs.floating_ip_pools.list(), ['name']) @@ -2464,7 +2464,7 @@ def do_floating_ip_pool_list(cs, _args): '--host', dest='host', metavar='', default=None, help=_('Filter by host')) def do_floating_ip_bulk_list(cs, args): - """List all floating ips.""" + """List all floating IPs.""" utils.print_list(cs.floating_ips_bulk.list(args.host), ['project_id', 'address', 'instance_uuid', @@ -2480,13 +2480,13 @@ def do_floating_ip_bulk_list(cs, args): '--interface', metavar='', default=None, help=_('Interface for new Floating IPs')) def do_floating_ip_bulk_create(cs, args): - """Bulk create floating ips by range.""" + """Bulk create floating IPs by range.""" cs.floating_ips_bulk.create(args.ip_range, args.pool, args.interface) @cliutils.arg('ip_range', metavar='', help=_('Address range to delete')) def do_floating_ip_bulk_delete(cs, args): - """Bulk delete floating ips by range.""" + """Bulk delete floating IPs by range.""" cs.floating_ips_bulk.delete(args.ip_range) @@ -2506,10 +2506,10 @@ def do_dns_domains(cs, args): @cliutils.arg('domain', metavar='', help=_('DNS domain')) -@cliutils.arg('--ip', metavar='', help=_('ip address'), default=None) +@cliutils.arg('--ip', metavar='', help=_('IP address'), default=None) @cliutils.arg('--name', metavar='', help=_('DNS name'), default=None) def do_dns_list(cs, args): - """List current DNS entries for domain and ip or domain and name.""" + """List current DNS entries for domain and IP or domain and name.""" if not (args.ip or args.name): raise exceptions.CommandError( _("You must specify either --ip or --name")) @@ -2522,7 +2522,7 @@ def do_dns_list(cs, args): _print_dns_list(entries) -@cliutils.arg('ip', metavar='', help=_('ip address')) +@cliutils.arg('ip', metavar='', help=_('IP address')) @cliutils.arg('name', metavar='', help=_('DNS name')) @cliutils.arg('domain', metavar='', help=_('DNS domain')) @cliutils.arg( @@ -2531,7 +2531,7 @@ def do_dns_list(cs, args): help=_('dns type (e.g. "A")'), default='A') def do_dns_create(cs, args): - """Create a DNS entry for domain, name and ip.""" + """Create a DNS entry for domain, name and IP.""" cs.dns_entries.create(args.domain, args.name, args.ip, args.type) @@ -3481,7 +3481,7 @@ def do_service_delete(cs, args): @cliutils.arg('fixed_ip', metavar='', help=_('Fixed IP Address.')) def do_fixed_ip_get(cs, args): - """Retrieve info on a fixed ip.""" + """Retrieve info on a fixed IP.""" result = cs.fixed_ips.get(args.fixed_ip) utils.print_list([result], ['address', 'cidr', 'hostname', 'host']) From fbad274c9db4a573d437b5e0852250b7b2e9dc9c Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 23 Jan 2015 04:38:21 +0000 Subject: [PATCH 0670/1705] Updated from global requirements Change-Id: I07688edd374ac6588fc6d120d612dea71cfa9b8c --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 996bd48ca..7e0284a4f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ pbr>=0.6,!=0.7,<1.0 argparse iso8601>=0.1.9 -oslo.i18n>=1.0.0 # Apache-2.0 +oslo.i18n>=1.3.0 # Apache-2.0 oslo.serialization>=1.2.0 # Apache-2.0 oslo.utils>=1.2.0 # Apache-2.0 PrettyTable>=0.7,<0.8 From 7713fa0b18932a00231818ebdb239493d3b9c714 Mon Sep 17 00:00:00 2001 From: Ian Cordasco Date: Wed, 10 Sep 2014 15:13:20 -0500 Subject: [PATCH 0671/1705] Use TCP Keep-Alive on the socket level There is not a way to pass the socket options to the HTTPAdapter upon creation so we have to sub-class it and override the init_poolmanager method. This also requires at least python-requests 2.4.0 but that has 2 severe bugs that were fixed in 2.4.1. If we try to fix this without a hard lower limit, we will not be able to properly set these options on the socket at creation time. Change-Id: I06e0d2c67d3197607e5f23f623c8fca69e1b23d7 Closes-bug: 1323862 --- novaclient/client.py | 14 +++++++++++++- novaclient/tests/test_client.py | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index b0d9aea10..061f870a7 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -28,6 +28,7 @@ import logging import os import re +import socket import time from keystoneclient import adapter @@ -48,6 +49,17 @@ from novaclient import utils +class TCPKeepAliveAdapter(adapters.HTTPAdapter): + """The custom adapter used to set TCP Keep-Alive on all connections.""" + def init_poolmanager(self, *args, **kwargs): + if requests.__version__ >= '2.4.1': + kwargs.setdefault('socket_options', [ + (socket.IPROTO_TCP, socket.TCP_NODELAY, 1), + (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1), + ]) + super(TCPKeepAliveAdapter, self).init_poolmanager(*args, **kwargs) + + class _ClientConnectionPool(object): def __init__(self): @@ -58,7 +70,7 @@ def get(self, url): Store and reuse HTTP adapters per Service URL. """ if url not in self._adapters: - self._adapters[url] = adapters.HTTPAdapter() + self._adapters[url] = TCPKeepAliveAdapter() return self._adapters[url] diff --git a/novaclient/tests/test_client.py b/novaclient/tests/test_client.py index ed349e1a6..d0388a910 100644 --- a/novaclient/tests/test_client.py +++ b/novaclient/tests/test_client.py @@ -31,7 +31,7 @@ class ClientConnectionPoolTest(utils.TestCase): - @mock.patch("novaclient.client.adapters.HTTPAdapter") + @mock.patch("novaclient.client.TCPKeepAliveAdapter") def test_get(self, mock_http_adapter): mock_http_adapter.side_effect = lambda: mock.Mock() pool = novaclient.client._ClientConnectionPool() From a3d09fe095e5727676bab1bd01e463db6b8b452b Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Mon, 26 Jan 2015 10:34:21 +0000 Subject: [PATCH 0672/1705] Updated from global requirements Change-Id: Ic1f8bde779fe269c0671c0d637bdf9a2d8866557 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7e0284a4f..b9b4b05df 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,4 +12,4 @@ requests>=2.2.0,!=2.4.0 simplejson>=2.2.0 six>=1.7.0 Babel>=1.3 -python-keystoneclient>=0.11.1 +python-keystoneclient>=1.0.0 From 7544dcb4ea92247c1e8fa97b8f0aba566e33ca48 Mon Sep 17 00:00:00 2001 From: Joe Gordon Date: Tue, 27 Jan 2015 12:35:40 -0800 Subject: [PATCH 0673/1705] whitelist find in testenv Stop logging a warning when running find inside of tox. Change-Id: I9a79ea62bc85d6a414ef3c6e45a059301a3ea8a4 --- tox.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tox.ini b/tox.ini index 72fb43826..a8e9a6f34 100644 --- a/tox.ini +++ b/tox.ini @@ -6,6 +6,8 @@ skipsdist = True [testenv] usedevelop = True +# tox is silly... these need to be separated by a newline.... +whitelist_externals = find install_command = pip install -U {opts} {packages} setenv = VIRTUAL_ENV={envdir} From 3561772f8b0cfee746af53fa228375b2ec7dfd9d Mon Sep 17 00:00:00 2001 From: Joe Gordon Date: Tue, 27 Jan 2015 12:59:15 -0800 Subject: [PATCH 0674/1705] Move unit tests into unit test directory In order to pave the way for functional testing, move existing unit tests into a directory labeled unit. A subsequent patch will add a directory for functional tests. Change-Id: I0adb8b9f14451acb382c725d31f5387b4b6d82bb --- novaclient/tests/{fixture_data => unit}/__init__.py | 0 novaclient/tests/{ => unit}/fakes.py | 0 .../tests/{v1_1 => unit/fixture_data}/__init__.py | 0 novaclient/tests/{ => unit}/fixture_data/agents.py | 2 +- .../tests/{ => unit}/fixture_data/aggregates.py | 2 +- .../{ => unit}/fixture_data/availability_zones.py | 2 +- novaclient/tests/{ => unit}/fixture_data/base.py | 0 novaclient/tests/{ => unit}/fixture_data/certs.py | 2 +- novaclient/tests/{ => unit}/fixture_data/client.py | 0 .../tests/{ => unit}/fixture_data/cloudpipe.py | 2 +- novaclient/tests/{ => unit}/fixture_data/fixedips.py | 2 +- .../tests/{ => unit}/fixture_data/floatingips.py | 4 ++-- novaclient/tests/{ => unit}/fixture_data/fping.py | 2 +- novaclient/tests/{ => unit}/fixture_data/hosts.py | 2 +- .../tests/{ => unit}/fixture_data/hypervisors.py | 2 +- novaclient/tests/{ => unit}/fixture_data/images.py | 4 ++-- novaclient/tests/{ => unit}/fixture_data/keypairs.py | 4 ++-- novaclient/tests/{ => unit}/fixture_data/limits.py | 2 +- novaclient/tests/{ => unit}/fixture_data/networks.py | 2 +- novaclient/tests/{ => unit}/fixture_data/quotas.py | 2 +- .../{ => unit}/fixture_data/security_group_rules.py | 4 ++-- .../tests/{ => unit}/fixture_data/security_groups.py | 4 ++-- .../tests/{ => unit}/fixture_data/server_groups.py | 2 +- novaclient/tests/{ => unit}/fixture_data/servers.py | 6 +++--- novaclient/tests/{ => unit}/idfake.pem | 0 novaclient/tests/{ => unit}/test_auth_plugins.py | 2 +- novaclient/tests/{ => unit}/test_base.py | 4 ++-- novaclient/tests/{ => unit}/test_client.py | 2 +- novaclient/tests/{ => unit}/test_discover.py | 2 +- novaclient/tests/{ => unit}/test_http.py | 2 +- novaclient/tests/{ => unit}/test_service_catalog.py | 2 +- novaclient/tests/{ => unit}/test_shell.py | 2 +- novaclient/tests/{ => unit}/test_utils.py | 2 +- novaclient/tests/{ => unit}/utils.py | 0 .../tests/{v1_1/contrib => unit/v1_1}/__init__.py | 0 novaclient/tests/unit/v1_1/contrib/__init__.py | 0 novaclient/tests/{ => unit}/v1_1/contrib/fakes.py | 2 +- .../v1_1/contrib/test_assisted_volume_snapshots.py | 4 ++-- .../tests/{ => unit}/v1_1/contrib/test_baremetal.py | 4 ++-- .../tests/{ => unit}/v1_1/contrib/test_cells.py | 4 ++-- .../{ => unit}/v1_1/contrib/test_instance_actions.py | 4 ++-- .../{ => unit}/v1_1/contrib/test_list_extensions.py | 4 ++-- .../tests/{ => unit}/v1_1/contrib/test_migrations.py | 4 ++-- .../v1_1/contrib/test_server_external_events.py | 4 ++-- .../{ => unit}/v1_1/contrib/test_tenant_networks.py | 4 ++-- novaclient/tests/{ => unit}/v1_1/fakes.py | 6 +++--- novaclient/tests/{ => unit}/v1_1/test_agents.py | 6 +++--- novaclient/tests/{ => unit}/v1_1/test_aggregates.py | 6 +++--- novaclient/tests/{ => unit}/v1_1/test_auth.py | 2 +- .../tests/{ => unit}/v1_1/test_availability_zone.py | 6 +++--- novaclient/tests/{ => unit}/v1_1/test_certs.py | 6 +++--- novaclient/tests/{ => unit}/v1_1/test_client.py | 2 +- novaclient/tests/{ => unit}/v1_1/test_cloudpipe.py | 6 +++--- novaclient/tests/{ => unit}/v1_1/test_fixed_ips.py | 6 +++--- .../tests/{ => unit}/v1_1/test_flavor_access.py | 4 ++-- novaclient/tests/{ => unit}/v1_1/test_flavors.py | 4 ++-- .../tests/{ => unit}/v1_1/test_floating_ip_dns.py | 6 +++--- .../tests/{ => unit}/v1_1/test_floating_ip_pools.py | 6 +++--- .../tests/{ => unit}/v1_1/test_floating_ips.py | 6 +++--- .../tests/{ => unit}/v1_1/test_floating_ips_bulk.py | 6 +++--- novaclient/tests/{ => unit}/v1_1/test_fping.py | 6 +++--- novaclient/tests/{ => unit}/v1_1/test_hosts.py | 6 +++--- novaclient/tests/{ => unit}/v1_1/test_hypervisors.py | 6 +++--- novaclient/tests/{ => unit}/v1_1/test_images.py | 6 +++--- novaclient/tests/{ => unit}/v1_1/test_keypairs.py | 6 +++--- novaclient/tests/{ => unit}/v1_1/test_limits.py | 6 +++--- novaclient/tests/{ => unit}/v1_1/test_networks.py | 6 +++--- .../tests/{ => unit}/v1_1/test_quota_classes.py | 4 ++-- novaclient/tests/{ => unit}/v1_1/test_quotas.py | 6 +++--- .../{ => unit}/v1_1/test_security_group_rules.py | 6 +++--- .../tests/{ => unit}/v1_1/test_security_groups.py | 6 +++--- .../tests/{ => unit}/v1_1/test_server_groups.py | 6 +++--- novaclient/tests/{ => unit}/v1_1/test_servers.py | 12 ++++++------ novaclient/tests/{ => unit}/v1_1/test_services.py | 4 ++-- novaclient/tests/{ => unit}/v1_1/test_shell.py | 4 ++-- novaclient/tests/{ => unit}/v1_1/test_usage.py | 4 ++-- novaclient/tests/{ => unit}/v1_1/test_volumes.py | 4 ++-- novaclient/tests/{ => unit}/v1_1/testfile.txt | 0 novaclient/tests/{ => unit}/v1_1/utils.py | 0 79 files changed, 140 insertions(+), 140 deletions(-) rename novaclient/tests/{fixture_data => unit}/__init__.py (100%) rename novaclient/tests/{ => unit}/fakes.py (100%) rename novaclient/tests/{v1_1 => unit/fixture_data}/__init__.py (100%) rename novaclient/tests/{ => unit}/fixture_data/agents.py (97%) rename novaclient/tests/{ => unit}/fixture_data/aggregates.py (97%) rename novaclient/tests/{ => unit}/fixture_data/availability_zones.py (98%) rename novaclient/tests/{ => unit}/fixture_data/base.py (100%) rename novaclient/tests/{ => unit}/fixture_data/certs.py (96%) rename novaclient/tests/{ => unit}/fixture_data/client.py (100%) rename novaclient/tests/{ => unit}/fixture_data/cloudpipe.py (96%) rename novaclient/tests/{ => unit}/fixture_data/fixedips.py (96%) rename novaclient/tests/{ => unit}/fixture_data/floatingips.py (98%) rename novaclient/tests/{ => unit}/fixture_data/fping.py (96%) rename novaclient/tests/{ => unit}/fixture_data/hosts.py (99%) rename novaclient/tests/{ => unit}/fixture_data/hypervisors.py (99%) rename novaclient/tests/{ => unit}/fixture_data/images.py (97%) rename novaclient/tests/{ => unit}/fixture_data/keypairs.py (94%) rename novaclient/tests/{ => unit}/fixture_data/limits.py (98%) rename novaclient/tests/{ => unit}/fixture_data/networks.py (97%) rename novaclient/tests/{ => unit}/fixture_data/quotas.py (97%) rename novaclient/tests/{ => unit}/fixture_data/security_group_rules.py (95%) rename novaclient/tests/{ => unit}/fixture_data/security_groups.py (97%) rename novaclient/tests/{ => unit}/fixture_data/server_groups.py (97%) rename novaclient/tests/{ => unit}/fixture_data/servers.py (99%) rename novaclient/tests/{ => unit}/idfake.pem (100%) rename novaclient/tests/{ => unit}/test_auth_plugins.py (99%) rename novaclient/tests/{ => unit}/test_base.py (96%) rename novaclient/tests/{ => unit}/test_client.py (99%) rename novaclient/tests/{ => unit}/test_discover.py (98%) rename novaclient/tests/{ => unit}/test_http.py (99%) rename novaclient/tests/{ => unit}/test_service_catalog.py (98%) rename novaclient/tests/{ => unit}/test_shell.py (99%) rename novaclient/tests/{ => unit}/test_utils.py (99%) rename novaclient/tests/{ => unit}/utils.py (100%) rename novaclient/tests/{v1_1/contrib => unit/v1_1}/__init__.py (100%) create mode 100644 novaclient/tests/unit/v1_1/contrib/__init__.py rename novaclient/tests/{ => unit}/v1_1/contrib/fakes.py (99%) rename novaclient/tests/{ => unit}/v1_1/contrib/test_assisted_volume_snapshots.py (93%) rename novaclient/tests/{ => unit}/v1_1/contrib/test_baremetal.py (96%) rename novaclient/tests/{ => unit}/v1_1/contrib/test_cells.py (93%) rename novaclient/tests/{ => unit}/v1_1/contrib/test_instance_actions.py (93%) rename novaclient/tests/{ => unit}/v1_1/contrib/test_list_extensions.py (92%) rename novaclient/tests/{ => unit}/v1_1/contrib/test_migrations.py (94%) rename novaclient/tests/{ => unit}/v1_1/contrib/test_server_external_events.py (94%) rename novaclient/tests/{ => unit}/v1_1/contrib/test_tenant_networks.py (94%) rename novaclient/tests/{ => unit}/v1_1/fakes.py (99%) rename novaclient/tests/{ => unit}/v1_1/test_agents.py (96%) rename novaclient/tests/{ => unit}/v1_1/test_aggregates.py (97%) rename novaclient/tests/{ => unit}/v1_1/test_auth.py (99%) rename novaclient/tests/{ => unit}/v1_1/test_availability_zone.py (95%) rename novaclient/tests/{ => unit}/v1_1/test_certs.py (88%) rename novaclient/tests/{ => unit}/v1_1/test_client.py (97%) rename novaclient/tests/{ => unit}/v1_1/test_cloudpipe.py (90%) rename novaclient/tests/{ => unit}/v1_1/test_fixed_ips.py (91%) rename novaclient/tests/{ => unit}/v1_1/test_flavor_access.py (96%) rename novaclient/tests/{ => unit}/v1_1/test_flavors.py (99%) rename novaclient/tests/{ => unit}/v1_1/test_floating_ip_dns.py (95%) rename novaclient/tests/{ => unit}/v1_1/test_floating_ip_pools.py (87%) rename novaclient/tests/{ => unit}/v1_1/test_floating_ips.py (93%) rename novaclient/tests/{ => unit}/v1_1/test_floating_ips_bulk.py (94%) rename novaclient/tests/{ => unit}/v1_1/test_fping.py (93%) rename novaclient/tests/{ => unit}/v1_1/test_hosts.py (95%) rename novaclient/tests/{ => unit}/v1_1/test_hypervisors.py (97%) rename novaclient/tests/{ => unit}/v1_1/test_images.py (93%) rename novaclient/tests/{ => unit}/v1_1/test_keypairs.py (93%) rename novaclient/tests/{ => unit}/v1_1/test_limits.py (95%) rename novaclient/tests/{ => unit}/v1_1/test_networks.py (96%) rename novaclient/tests/{ => unit}/v1_1/test_quota_classes.py (94%) rename novaclient/tests/{ => unit}/v1_1/test_quotas.py (93%) rename novaclient/tests/{ => unit}/v1_1/test_security_group_rules.py (95%) rename novaclient/tests/{ => unit}/v1_1/test_security_groups.py (94%) rename novaclient/tests/{ => unit}/v1_1/test_server_groups.py (93%) rename novaclient/tests/{ => unit}/v1_1/test_servers.py (98%) rename novaclient/tests/{ => unit}/v1_1/test_services.py (97%) rename novaclient/tests/{ => unit}/v1_1/test_shell.py (99%) rename novaclient/tests/{ => unit}/v1_1/test_usage.py (95%) rename novaclient/tests/{ => unit}/v1_1/test_volumes.py (97%) rename novaclient/tests/{ => unit}/v1_1/testfile.txt (100%) rename novaclient/tests/{ => unit}/v1_1/utils.py (100%) diff --git a/novaclient/tests/fixture_data/__init__.py b/novaclient/tests/unit/__init__.py similarity index 100% rename from novaclient/tests/fixture_data/__init__.py rename to novaclient/tests/unit/__init__.py diff --git a/novaclient/tests/fakes.py b/novaclient/tests/unit/fakes.py similarity index 100% rename from novaclient/tests/fakes.py rename to novaclient/tests/unit/fakes.py diff --git a/novaclient/tests/v1_1/__init__.py b/novaclient/tests/unit/fixture_data/__init__.py similarity index 100% rename from novaclient/tests/v1_1/__init__.py rename to novaclient/tests/unit/fixture_data/__init__.py diff --git a/novaclient/tests/fixture_data/agents.py b/novaclient/tests/unit/fixture_data/agents.py similarity index 97% rename from novaclient/tests/fixture_data/agents.py rename to novaclient/tests/unit/fixture_data/agents.py index f1c2aed01..46a0bb67b 100644 --- a/novaclient/tests/fixture_data/agents.py +++ b/novaclient/tests/unit/fixture_data/agents.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.tests.fixture_data import base +from novaclient.tests.unit.fixture_data import base class Fixture(base.Fixture): diff --git a/novaclient/tests/fixture_data/aggregates.py b/novaclient/tests/unit/fixture_data/aggregates.py similarity index 97% rename from novaclient/tests/fixture_data/aggregates.py rename to novaclient/tests/unit/fixture_data/aggregates.py index 26f00d7ac..ed743ed1f 100644 --- a/novaclient/tests/fixture_data/aggregates.py +++ b/novaclient/tests/unit/fixture_data/aggregates.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.tests.fixture_data import base +from novaclient.tests.unit.fixture_data import base class Fixture(base.Fixture): diff --git a/novaclient/tests/fixture_data/availability_zones.py b/novaclient/tests/unit/fixture_data/availability_zones.py similarity index 98% rename from novaclient/tests/fixture_data/availability_zones.py rename to novaclient/tests/unit/fixture_data/availability_zones.py index 008cea2a9..36659d70a 100644 --- a/novaclient/tests/fixture_data/availability_zones.py +++ b/novaclient/tests/unit/fixture_data/availability_zones.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.tests.fixture_data import base +from novaclient.tests.unit.fixture_data import base class V1(base.Fixture): diff --git a/novaclient/tests/fixture_data/base.py b/novaclient/tests/unit/fixture_data/base.py similarity index 100% rename from novaclient/tests/fixture_data/base.py rename to novaclient/tests/unit/fixture_data/base.py diff --git a/novaclient/tests/fixture_data/certs.py b/novaclient/tests/unit/fixture_data/certs.py similarity index 96% rename from novaclient/tests/fixture_data/certs.py rename to novaclient/tests/unit/fixture_data/certs.py index 243bb0ea7..1dedaa4a3 100644 --- a/novaclient/tests/fixture_data/certs.py +++ b/novaclient/tests/unit/fixture_data/certs.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.tests.fixture_data import base +from novaclient.tests.unit.fixture_data import base class Fixture(base.Fixture): diff --git a/novaclient/tests/fixture_data/client.py b/novaclient/tests/unit/fixture_data/client.py similarity index 100% rename from novaclient/tests/fixture_data/client.py rename to novaclient/tests/unit/fixture_data/client.py diff --git a/novaclient/tests/fixture_data/cloudpipe.py b/novaclient/tests/unit/fixture_data/cloudpipe.py similarity index 96% rename from novaclient/tests/fixture_data/cloudpipe.py rename to novaclient/tests/unit/fixture_data/cloudpipe.py index 9cb4efa89..abe590563 100644 --- a/novaclient/tests/fixture_data/cloudpipe.py +++ b/novaclient/tests/unit/fixture_data/cloudpipe.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.tests.fixture_data import base +from novaclient.tests.unit.fixture_data import base class Fixture(base.Fixture): diff --git a/novaclient/tests/fixture_data/fixedips.py b/novaclient/tests/unit/fixture_data/fixedips.py similarity index 96% rename from novaclient/tests/fixture_data/fixedips.py rename to novaclient/tests/unit/fixture_data/fixedips.py index 71f18a4e6..fb677e91c 100644 --- a/novaclient/tests/fixture_data/fixedips.py +++ b/novaclient/tests/unit/fixture_data/fixedips.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.tests.fixture_data import base +from novaclient.tests.unit.fixture_data import base class Fixture(base.Fixture): diff --git a/novaclient/tests/fixture_data/floatingips.py b/novaclient/tests/unit/fixture_data/floatingips.py similarity index 98% rename from novaclient/tests/fixture_data/floatingips.py rename to novaclient/tests/unit/fixture_data/floatingips.py index 4829a5a3c..10772a798 100644 --- a/novaclient/tests/fixture_data/floatingips.py +++ b/novaclient/tests/unit/fixture_data/floatingips.py @@ -12,8 +12,8 @@ from oslo.serialization import jsonutils -from novaclient.tests import fakes -from novaclient.tests.fixture_data import base +from novaclient.tests.unit import fakes +from novaclient.tests.unit.fixture_data import base class FloatingFixture(base.Fixture): diff --git a/novaclient/tests/fixture_data/fping.py b/novaclient/tests/unit/fixture_data/fping.py similarity index 96% rename from novaclient/tests/fixture_data/fping.py rename to novaclient/tests/unit/fixture_data/fping.py index 837db6daa..6c6cd4f89 100644 --- a/novaclient/tests/fixture_data/fping.py +++ b/novaclient/tests/unit/fixture_data/fping.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.tests.fixture_data import base +from novaclient.tests.unit.fixture_data import base class Fixture(base.Fixture): diff --git a/novaclient/tests/fixture_data/hosts.py b/novaclient/tests/unit/fixture_data/hosts.py similarity index 99% rename from novaclient/tests/fixture_data/hosts.py rename to novaclient/tests/unit/fixture_data/hosts.py index e7684f606..5c1ff60a3 100644 --- a/novaclient/tests/fixture_data/hosts.py +++ b/novaclient/tests/unit/fixture_data/hosts.py @@ -13,7 +13,7 @@ from oslo.serialization import jsonutils from six.moves.urllib import parse -from novaclient.tests.fixture_data import base +from novaclient.tests.unit.fixture_data import base class BaseFixture(base.Fixture): diff --git a/novaclient/tests/fixture_data/hypervisors.py b/novaclient/tests/unit/fixture_data/hypervisors.py similarity index 99% rename from novaclient/tests/fixture_data/hypervisors.py rename to novaclient/tests/unit/fixture_data/hypervisors.py index 83a37d58c..62952f081 100644 --- a/novaclient/tests/fixture_data/hypervisors.py +++ b/novaclient/tests/unit/fixture_data/hypervisors.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.tests.fixture_data import base +from novaclient.tests.unit.fixture_data import base class V1(base.Fixture): diff --git a/novaclient/tests/fixture_data/images.py b/novaclient/tests/unit/fixture_data/images.py similarity index 97% rename from novaclient/tests/fixture_data/images.py rename to novaclient/tests/unit/fixture_data/images.py index 9c9ef10f2..02f359668 100644 --- a/novaclient/tests/fixture_data/images.py +++ b/novaclient/tests/unit/fixture_data/images.py @@ -12,8 +12,8 @@ from oslo.serialization import jsonutils -from novaclient.tests import fakes -from novaclient.tests.fixture_data import base +from novaclient.tests.unit import fakes +from novaclient.tests.unit.fixture_data import base class V1(base.Fixture): diff --git a/novaclient/tests/fixture_data/keypairs.py b/novaclient/tests/unit/fixture_data/keypairs.py similarity index 94% rename from novaclient/tests/fixture_data/keypairs.py rename to novaclient/tests/unit/fixture_data/keypairs.py index 9017d1cff..9314c58a5 100644 --- a/novaclient/tests/fixture_data/keypairs.py +++ b/novaclient/tests/unit/fixture_data/keypairs.py @@ -12,8 +12,8 @@ from oslo.serialization import jsonutils -from novaclient.tests import fakes -from novaclient.tests.fixture_data import base +from novaclient.tests.unit import fakes +from novaclient.tests.unit.fixture_data import base class V1(base.Fixture): diff --git a/novaclient/tests/fixture_data/limits.py b/novaclient/tests/unit/fixture_data/limits.py similarity index 98% rename from novaclient/tests/fixture_data/limits.py rename to novaclient/tests/unit/fixture_data/limits.py index 0b9488d3f..55d324d95 100644 --- a/novaclient/tests/fixture_data/limits.py +++ b/novaclient/tests/unit/fixture_data/limits.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.tests.fixture_data import base +from novaclient.tests.unit.fixture_data import base class Fixture(base.Fixture): diff --git a/novaclient/tests/fixture_data/networks.py b/novaclient/tests/unit/fixture_data/networks.py similarity index 97% rename from novaclient/tests/fixture_data/networks.py rename to novaclient/tests/unit/fixture_data/networks.py index 023dce421..9ef692c3e 100644 --- a/novaclient/tests/fixture_data/networks.py +++ b/novaclient/tests/unit/fixture_data/networks.py @@ -12,7 +12,7 @@ from oslo.serialization import jsonutils -from novaclient.tests.fixture_data import base +from novaclient.tests.unit.fixture_data import base class Fixture(base.Fixture): diff --git a/novaclient/tests/fixture_data/quotas.py b/novaclient/tests/unit/fixture_data/quotas.py similarity index 97% rename from novaclient/tests/fixture_data/quotas.py rename to novaclient/tests/unit/fixture_data/quotas.py index 3972d20ff..83f448f49 100644 --- a/novaclient/tests/fixture_data/quotas.py +++ b/novaclient/tests/unit/fixture_data/quotas.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.tests.fixture_data import base +from novaclient.tests.unit.fixture_data import base class V1(base.Fixture): diff --git a/novaclient/tests/fixture_data/security_group_rules.py b/novaclient/tests/unit/fixture_data/security_group_rules.py similarity index 95% rename from novaclient/tests/fixture_data/security_group_rules.py rename to novaclient/tests/unit/fixture_data/security_group_rules.py index aab4c2456..5f1d2fad5 100644 --- a/novaclient/tests/fixture_data/security_group_rules.py +++ b/novaclient/tests/unit/fixture_data/security_group_rules.py @@ -12,8 +12,8 @@ from oslo.serialization import jsonutils -from novaclient.tests import fakes -from novaclient.tests.fixture_data import base +from novaclient.tests.unit import fakes +from novaclient.tests.unit.fixture_data import base class Fixture(base.Fixture): diff --git a/novaclient/tests/fixture_data/security_groups.py b/novaclient/tests/unit/fixture_data/security_groups.py similarity index 97% rename from novaclient/tests/fixture_data/security_groups.py rename to novaclient/tests/unit/fixture_data/security_groups.py index 9f2f96b7d..ac5d182c6 100644 --- a/novaclient/tests/fixture_data/security_groups.py +++ b/novaclient/tests/unit/fixture_data/security_groups.py @@ -12,8 +12,8 @@ from oslo.serialization import jsonutils -from novaclient.tests import fakes -from novaclient.tests.fixture_data import base +from novaclient.tests.unit import fakes +from novaclient.tests.unit.fixture_data import base class Fixture(base.Fixture): diff --git a/novaclient/tests/fixture_data/server_groups.py b/novaclient/tests/unit/fixture_data/server_groups.py similarity index 97% rename from novaclient/tests/fixture_data/server_groups.py rename to novaclient/tests/unit/fixture_data/server_groups.py index 6defc895c..1fcfdea55 100644 --- a/novaclient/tests/fixture_data/server_groups.py +++ b/novaclient/tests/unit/fixture_data/server_groups.py @@ -12,7 +12,7 @@ from oslo.serialization import jsonutils -from novaclient.tests.fixture_data import base +from novaclient.tests.unit.fixture_data import base class Fixture(base.Fixture): diff --git a/novaclient/tests/fixture_data/servers.py b/novaclient/tests/unit/fixture_data/servers.py similarity index 99% rename from novaclient/tests/fixture_data/servers.py rename to novaclient/tests/unit/fixture_data/servers.py index c728f2ff3..6e3dd46b7 100644 --- a/novaclient/tests/fixture_data/servers.py +++ b/novaclient/tests/unit/fixture_data/servers.py @@ -12,8 +12,8 @@ from oslo.serialization import jsonutils -from novaclient.tests import fakes -from novaclient.tests.fixture_data import base +from novaclient.tests.unit import fakes +from novaclient.tests.unit.fixture_data import base class Base(base.Fixture): @@ -336,7 +336,7 @@ def setUp(self): # # Clear password: FooBar123 # - # RSA Private Key: novaclient/tests/idfake.pem + # RSA Private Key: novaclient/tests/unit/idfake.pem # # Encrypted password # OIuEuQttO8Rk93BcKlwHQsziDAnkAm/V6V8VPToA8ZeUaUBWwS0gwo2K6Y61Z96r diff --git a/novaclient/tests/idfake.pem b/novaclient/tests/unit/idfake.pem similarity index 100% rename from novaclient/tests/idfake.pem rename to novaclient/tests/unit/idfake.pem diff --git a/novaclient/tests/test_auth_plugins.py b/novaclient/tests/unit/test_auth_plugins.py similarity index 99% rename from novaclient/tests/test_auth_plugins.py rename to novaclient/tests/unit/test_auth_plugins.py index 33bf481b8..992077d0a 100644 --- a/novaclient/tests/test_auth_plugins.py +++ b/novaclient/tests/unit/test_auth_plugins.py @@ -27,7 +27,7 @@ from novaclient import auth_plugin from novaclient import exceptions -from novaclient.tests import utils +from novaclient.tests.unit import utils from novaclient.v1_1 import client diff --git a/novaclient/tests/test_base.py b/novaclient/tests/unit/test_base.py similarity index 96% rename from novaclient/tests/test_base.py rename to novaclient/tests/unit/test_base.py index fca5b72d4..d3fc6c756 100644 --- a/novaclient/tests/test_base.py +++ b/novaclient/tests/unit/test_base.py @@ -13,8 +13,8 @@ from novaclient import base from novaclient import exceptions -from novaclient.tests import utils -from novaclient.tests.v1_1 import fakes +from novaclient.tests.unit import utils +from novaclient.tests.unit.v1_1 import fakes from novaclient.v1_1 import flavors diff --git a/novaclient/tests/test_client.py b/novaclient/tests/unit/test_client.py similarity index 99% rename from novaclient/tests/test_client.py rename to novaclient/tests/unit/test_client.py index 9b841c8a7..f8161156e 100644 --- a/novaclient/tests/test_client.py +++ b/novaclient/tests/unit/test_client.py @@ -23,7 +23,7 @@ import novaclient.client import novaclient.extension -from novaclient.tests import utils +from novaclient.tests.unit import utils import novaclient.v1_1.client diff --git a/novaclient/tests/test_discover.py b/novaclient/tests/unit/test_discover.py similarity index 98% rename from novaclient/tests/test_discover.py rename to novaclient/tests/unit/test_discover.py index a49882494..f8414c664 100644 --- a/novaclient/tests/test_discover.py +++ b/novaclient/tests/unit/test_discover.py @@ -20,7 +20,7 @@ import pkg_resources import novaclient.shell -from novaclient.tests import utils +from novaclient.tests.unit import utils class DiscoverTest(utils.TestCase): diff --git a/novaclient/tests/test_http.py b/novaclient/tests/unit/test_http.py similarity index 99% rename from novaclient/tests/test_http.py rename to novaclient/tests/unit/test_http.py index bd12ed827..671ab09da 100644 --- a/novaclient/tests/test_http.py +++ b/novaclient/tests/unit/test_http.py @@ -17,7 +17,7 @@ from novaclient import client from novaclient import exceptions -from novaclient.tests import utils +from novaclient.tests.unit import utils fake_response = utils.TestResponse({ diff --git a/novaclient/tests/test_service_catalog.py b/novaclient/tests/unit/test_service_catalog.py similarity index 98% rename from novaclient/tests/test_service_catalog.py rename to novaclient/tests/unit/test_service_catalog.py index 6f6ff4f46..33be77791 100644 --- a/novaclient/tests/test_service_catalog.py +++ b/novaclient/tests/unit/test_service_catalog.py @@ -15,7 +15,7 @@ from novaclient import exceptions from novaclient import service_catalog -from novaclient.tests import utils +from novaclient.tests.unit import utils SERVICE_CATALOG = fixture.V2Token() diff --git a/novaclient/tests/test_shell.py b/novaclient/tests/unit/test_shell.py similarity index 99% rename from novaclient/tests/test_shell.py rename to novaclient/tests/unit/test_shell.py index 4f4e464ea..794e88a3e 100644 --- a/novaclient/tests/test_shell.py +++ b/novaclient/tests/unit/test_shell.py @@ -26,7 +26,7 @@ import novaclient.client from novaclient import exceptions import novaclient.shell -from novaclient.tests import utils +from novaclient.tests.unit import utils FAKE_ENV = {'OS_USERNAME': 'username', 'OS_PASSWORD': 'password', diff --git a/novaclient/tests/test_utils.py b/novaclient/tests/unit/test_utils.py similarity index 99% rename from novaclient/tests/test_utils.py rename to novaclient/tests/unit/test_utils.py index b0079ef3f..fe0452636 100644 --- a/novaclient/tests/test_utils.py +++ b/novaclient/tests/unit/test_utils.py @@ -18,7 +18,7 @@ from novaclient import base from novaclient import exceptions -from novaclient.tests import utils as test_utils +from novaclient.tests.unit import utils as test_utils from novaclient import utils UUID = '8e8ec658-c7b0-4243-bdf8-6f7f2952c0d0' diff --git a/novaclient/tests/utils.py b/novaclient/tests/unit/utils.py similarity index 100% rename from novaclient/tests/utils.py rename to novaclient/tests/unit/utils.py diff --git a/novaclient/tests/v1_1/contrib/__init__.py b/novaclient/tests/unit/v1_1/__init__.py similarity index 100% rename from novaclient/tests/v1_1/contrib/__init__.py rename to novaclient/tests/unit/v1_1/__init__.py diff --git a/novaclient/tests/unit/v1_1/contrib/__init__.py b/novaclient/tests/unit/v1_1/contrib/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/novaclient/tests/v1_1/contrib/fakes.py b/novaclient/tests/unit/v1_1/contrib/fakes.py similarity index 99% rename from novaclient/tests/v1_1/contrib/fakes.py rename to novaclient/tests/unit/v1_1/contrib/fakes.py index 43c6e6de3..e961ae100 100644 --- a/novaclient/tests/v1_1/contrib/fakes.py +++ b/novaclient/tests/unit/v1_1/contrib/fakes.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from novaclient.tests.v1_1 import fakes +from novaclient.tests.unit.v1_1 import fakes from novaclient.v1_1 import client diff --git a/novaclient/tests/v1_1/contrib/test_assisted_volume_snapshots.py b/novaclient/tests/unit/v1_1/contrib/test_assisted_volume_snapshots.py similarity index 93% rename from novaclient/tests/v1_1/contrib/test_assisted_volume_snapshots.py rename to novaclient/tests/unit/v1_1/contrib/test_assisted_volume_snapshots.py index 426d6debb..37f584ce7 100644 --- a/novaclient/tests/v1_1/contrib/test_assisted_volume_snapshots.py +++ b/novaclient/tests/unit/v1_1/contrib/test_assisted_volume_snapshots.py @@ -17,8 +17,8 @@ """ from novaclient import extension -from novaclient.tests import utils -from novaclient.tests.v1_1.contrib import fakes +from novaclient.tests.unit import utils +from novaclient.tests.unit.v1_1.contrib import fakes from novaclient.v1_1.contrib import assisted_volume_snapshots as assisted_snaps diff --git a/novaclient/tests/v1_1/contrib/test_baremetal.py b/novaclient/tests/unit/v1_1/contrib/test_baremetal.py similarity index 96% rename from novaclient/tests/v1_1/contrib/test_baremetal.py rename to novaclient/tests/unit/v1_1/contrib/test_baremetal.py index 172d8b272..1866eea6e 100644 --- a/novaclient/tests/v1_1/contrib/test_baremetal.py +++ b/novaclient/tests/unit/v1_1/contrib/test_baremetal.py @@ -15,8 +15,8 @@ from novaclient import extension -from novaclient.tests import utils -from novaclient.tests.v1_1.contrib import fakes +from novaclient.tests.unit import utils +from novaclient.tests.unit.v1_1.contrib import fakes from novaclient.v1_1.contrib import baremetal diff --git a/novaclient/tests/v1_1/contrib/test_cells.py b/novaclient/tests/unit/v1_1/contrib/test_cells.py similarity index 93% rename from novaclient/tests/v1_1/contrib/test_cells.py rename to novaclient/tests/unit/v1_1/contrib/test_cells.py index 187fab847..ccdcd375d 100644 --- a/novaclient/tests/v1_1/contrib/test_cells.py +++ b/novaclient/tests/unit/v1_1/contrib/test_cells.py @@ -14,8 +14,8 @@ # under the License. from novaclient import extension -from novaclient.tests import utils -from novaclient.tests.v1_1.contrib import fakes +from novaclient.tests.unit import utils +from novaclient.tests.unit.v1_1.contrib import fakes from novaclient.v1_1.contrib import cells diff --git a/novaclient/tests/v1_1/contrib/test_instance_actions.py b/novaclient/tests/unit/v1_1/contrib/test_instance_actions.py similarity index 93% rename from novaclient/tests/v1_1/contrib/test_instance_actions.py rename to novaclient/tests/unit/v1_1/contrib/test_instance_actions.py index 364781975..66dbb418c 100644 --- a/novaclient/tests/v1_1/contrib/test_instance_actions.py +++ b/novaclient/tests/unit/v1_1/contrib/test_instance_actions.py @@ -14,8 +14,8 @@ # under the License. from novaclient import extension -from novaclient.tests import utils -from novaclient.tests.v1_1.contrib import fakes +from novaclient.tests.unit import utils +from novaclient.tests.unit.v1_1.contrib import fakes from novaclient.v1_1.contrib import instance_action diff --git a/novaclient/tests/v1_1/contrib/test_list_extensions.py b/novaclient/tests/unit/v1_1/contrib/test_list_extensions.py similarity index 92% rename from novaclient/tests/v1_1/contrib/test_list_extensions.py rename to novaclient/tests/unit/v1_1/contrib/test_list_extensions.py index 77b6a3420..dfb742ebd 100644 --- a/novaclient/tests/v1_1/contrib/test_list_extensions.py +++ b/novaclient/tests/unit/v1_1/contrib/test_list_extensions.py @@ -12,8 +12,8 @@ # under the License. from novaclient import extension -from novaclient.tests import utils -from novaclient.tests.v1_1 import fakes +from novaclient.tests.unit import utils +from novaclient.tests.unit.v1_1 import fakes from novaclient.v1_1.contrib import list_extensions diff --git a/novaclient/tests/v1_1/contrib/test_migrations.py b/novaclient/tests/unit/v1_1/contrib/test_migrations.py similarity index 94% rename from novaclient/tests/v1_1/contrib/test_migrations.py rename to novaclient/tests/unit/v1_1/contrib/test_migrations.py index 5337502a1..5fe5c8f07 100644 --- a/novaclient/tests/v1_1/contrib/test_migrations.py +++ b/novaclient/tests/unit/v1_1/contrib/test_migrations.py @@ -11,8 +11,8 @@ # under the License. from novaclient import extension -from novaclient.tests import utils -from novaclient.tests.v1_1 import fakes +from novaclient.tests.unit import utils +from novaclient.tests.unit.v1_1 import fakes from novaclient.v1_1.contrib import migrations extensions = [ diff --git a/novaclient/tests/v1_1/contrib/test_server_external_events.py b/novaclient/tests/unit/v1_1/contrib/test_server_external_events.py similarity index 94% rename from novaclient/tests/v1_1/contrib/test_server_external_events.py rename to novaclient/tests/unit/v1_1/contrib/test_server_external_events.py index c92ba34e0..a933ad89f 100644 --- a/novaclient/tests/v1_1/contrib/test_server_external_events.py +++ b/novaclient/tests/unit/v1_1/contrib/test_server_external_events.py @@ -17,8 +17,8 @@ """ from novaclient import extension -from novaclient.tests import utils -from novaclient.tests.v1_1.contrib import fakes +from novaclient.tests.unit import utils +from novaclient.tests.unit.v1_1.contrib import fakes from novaclient.v1_1.contrib import server_external_events as ext_events diff --git a/novaclient/tests/v1_1/contrib/test_tenant_networks.py b/novaclient/tests/unit/v1_1/contrib/test_tenant_networks.py similarity index 94% rename from novaclient/tests/v1_1/contrib/test_tenant_networks.py rename to novaclient/tests/unit/v1_1/contrib/test_tenant_networks.py index 1289928b8..a1a76cee3 100644 --- a/novaclient/tests/v1_1/contrib/test_tenant_networks.py +++ b/novaclient/tests/unit/v1_1/contrib/test_tenant_networks.py @@ -14,8 +14,8 @@ # under the License. from novaclient import extension -from novaclient.tests import utils -from novaclient.tests.v1_1.contrib import fakes +from novaclient.tests.unit import utils +from novaclient.tests.unit.v1_1.contrib import fakes from novaclient.v1_1.contrib import tenant_networks diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/unit/v1_1/fakes.py similarity index 99% rename from novaclient/tests/v1_1/fakes.py rename to novaclient/tests/unit/v1_1/fakes.py index 92c923898..6d97374fe 100644 --- a/novaclient/tests/v1_1/fakes.py +++ b/novaclient/tests/unit/v1_1/fakes.py @@ -22,8 +22,8 @@ from novaclient import client as base_client from novaclient import exceptions -from novaclient.tests import fakes -from novaclient.tests import utils +from novaclient.tests.unit import fakes +from novaclient.tests.unit import utils from novaclient.v1_1 import client @@ -515,7 +515,7 @@ def delete_servers_1234_ips_public_1_2_3_4(self, **kw): # # Clear password: FooBar123 # - # RSA Private Key: novaclient/tests/idfake.pem + # RSA Private Key: novaclient/tests/unit/idfake.pem # # Encrypted password # OIuEuQttO8Rk93BcKlwHQsziDAnkAm/V6V8VPToA8ZeUaUBWwS0gwo2K6Y61Z96r diff --git a/novaclient/tests/v1_1/test_agents.py b/novaclient/tests/unit/v1_1/test_agents.py similarity index 96% rename from novaclient/tests/v1_1/test_agents.py rename to novaclient/tests/unit/v1_1/test_agents.py index dbe7af49a..a269b322a 100644 --- a/novaclient/tests/v1_1/test_agents.py +++ b/novaclient/tests/unit/v1_1/test_agents.py @@ -13,9 +13,9 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.tests.fixture_data import agents as data -from novaclient.tests.fixture_data import client -from novaclient.tests import utils +from novaclient.tests.unit.fixture_data import agents as data +from novaclient.tests.unit.fixture_data import client +from novaclient.tests.unit import utils from novaclient.v1_1 import agents diff --git a/novaclient/tests/v1_1/test_aggregates.py b/novaclient/tests/unit/v1_1/test_aggregates.py similarity index 97% rename from novaclient/tests/v1_1/test_aggregates.py rename to novaclient/tests/unit/v1_1/test_aggregates.py index 6e7bd44e9..40cc7d922 100644 --- a/novaclient/tests/v1_1/test_aggregates.py +++ b/novaclient/tests/unit/v1_1/test_aggregates.py @@ -13,9 +13,9 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.tests.fixture_data import aggregates as data -from novaclient.tests.fixture_data import client -from novaclient.tests import utils +from novaclient.tests.unit.fixture_data import aggregates as data +from novaclient.tests.unit.fixture_data import client +from novaclient.tests.unit import utils from novaclient.v1_1 import aggregates diff --git a/novaclient/tests/v1_1/test_auth.py b/novaclient/tests/unit/v1_1/test_auth.py similarity index 99% rename from novaclient/tests/v1_1/test_auth.py rename to novaclient/tests/unit/v1_1/test_auth.py index 97e402798..65715e54d 100644 --- a/novaclient/tests/v1_1/test_auth.py +++ b/novaclient/tests/unit/v1_1/test_auth.py @@ -19,7 +19,7 @@ import requests from novaclient import exceptions -from novaclient.tests import utils +from novaclient.tests.unit import utils from novaclient.v1_1 import client diff --git a/novaclient/tests/v1_1/test_availability_zone.py b/novaclient/tests/unit/v1_1/test_availability_zone.py similarity index 95% rename from novaclient/tests/v1_1/test_availability_zone.py rename to novaclient/tests/unit/v1_1/test_availability_zone.py index be0623adf..fda4764a1 100644 --- a/novaclient/tests/v1_1/test_availability_zone.py +++ b/novaclient/tests/unit/v1_1/test_availability_zone.py @@ -16,9 +16,9 @@ import six -from novaclient.tests.fixture_data import availability_zones as data -from novaclient.tests.fixture_data import client -from novaclient.tests import utils +from novaclient.tests.unit.fixture_data import availability_zones as data +from novaclient.tests.unit.fixture_data import client +from novaclient.tests.unit import utils from novaclient.v1_1 import availability_zones diff --git a/novaclient/tests/v1_1/test_certs.py b/novaclient/tests/unit/v1_1/test_certs.py similarity index 88% rename from novaclient/tests/v1_1/test_certs.py rename to novaclient/tests/unit/v1_1/test_certs.py index 0a568d585..39eac06f7 100644 --- a/novaclient/tests/v1_1/test_certs.py +++ b/novaclient/tests/unit/v1_1/test_certs.py @@ -11,9 +11,9 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.tests.fixture_data import certs as data -from novaclient.tests.fixture_data import client -from novaclient.tests import utils +from novaclient.tests.unit.fixture_data import certs as data +from novaclient.tests.unit.fixture_data import client +from novaclient.tests.unit import utils from novaclient.v1_1 import certs diff --git a/novaclient/tests/v1_1/test_client.py b/novaclient/tests/unit/v1_1/test_client.py similarity index 97% rename from novaclient/tests/v1_1/test_client.py rename to novaclient/tests/unit/v1_1/test_client.py index fa0ed3af6..fbc98f7cb 100644 --- a/novaclient/tests/v1_1/test_client.py +++ b/novaclient/tests/unit/v1_1/test_client.py @@ -14,7 +14,7 @@ from keystoneclient import session -from novaclient.tests import utils +from novaclient.tests.unit import utils from novaclient.v1_1 import client diff --git a/novaclient/tests/v1_1/test_cloudpipe.py b/novaclient/tests/unit/v1_1/test_cloudpipe.py similarity index 90% rename from novaclient/tests/v1_1/test_cloudpipe.py rename to novaclient/tests/unit/v1_1/test_cloudpipe.py index 0a319085b..0a40db68d 100644 --- a/novaclient/tests/v1_1/test_cloudpipe.py +++ b/novaclient/tests/unit/v1_1/test_cloudpipe.py @@ -13,9 +13,9 @@ import six -from novaclient.tests.fixture_data import client -from novaclient.tests.fixture_data import cloudpipe as data -from novaclient.tests import utils +from novaclient.tests.unit.fixture_data import client +from novaclient.tests.unit.fixture_data import cloudpipe as data +from novaclient.tests.unit import utils from novaclient.v1_1 import cloudpipe diff --git a/novaclient/tests/v1_1/test_fixed_ips.py b/novaclient/tests/unit/v1_1/test_fixed_ips.py similarity index 91% rename from novaclient/tests/v1_1/test_fixed_ips.py rename to novaclient/tests/unit/v1_1/test_fixed_ips.py index 5cfecf333..02ba39b6d 100644 --- a/novaclient/tests/v1_1/test_fixed_ips.py +++ b/novaclient/tests/unit/v1_1/test_fixed_ips.py @@ -13,9 +13,9 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.tests.fixture_data import client -from novaclient.tests.fixture_data import fixedips as data -from novaclient.tests import utils +from novaclient.tests.unit.fixture_data import client +from novaclient.tests.unit.fixture_data import fixedips as data +from novaclient.tests.unit import utils class FixedIpsTest(utils.FixturedTestCase): diff --git a/novaclient/tests/v1_1/test_flavor_access.py b/novaclient/tests/unit/v1_1/test_flavor_access.py similarity index 96% rename from novaclient/tests/v1_1/test_flavor_access.py rename to novaclient/tests/unit/v1_1/test_flavor_access.py index b85f838ee..08677ad4e 100644 --- a/novaclient/tests/v1_1/test_flavor_access.py +++ b/novaclient/tests/unit/v1_1/test_flavor_access.py @@ -13,8 +13,8 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.tests import utils -from novaclient.tests.v1_1 import fakes +from novaclient.tests.unit import utils +from novaclient.tests.unit.v1_1 import fakes from novaclient.v1_1 import flavor_access diff --git a/novaclient/tests/v1_1/test_flavors.py b/novaclient/tests/unit/v1_1/test_flavors.py similarity index 99% rename from novaclient/tests/v1_1/test_flavors.py rename to novaclient/tests/unit/v1_1/test_flavors.py index a69db858f..90f7eb1d9 100644 --- a/novaclient/tests/v1_1/test_flavors.py +++ b/novaclient/tests/unit/v1_1/test_flavors.py @@ -16,8 +16,8 @@ import mock from novaclient import exceptions -from novaclient.tests import utils -from novaclient.tests.v1_1 import fakes +from novaclient.tests.unit import utils +from novaclient.tests.unit.v1_1 import fakes from novaclient.v1_1 import flavors diff --git a/novaclient/tests/v1_1/test_floating_ip_dns.py b/novaclient/tests/unit/v1_1/test_floating_ip_dns.py similarity index 95% rename from novaclient/tests/v1_1/test_floating_ip_dns.py rename to novaclient/tests/unit/v1_1/test_floating_ip_dns.py index 2ded1725e..36f900ced 100644 --- a/novaclient/tests/v1_1/test_floating_ip_dns.py +++ b/novaclient/tests/unit/v1_1/test_floating_ip_dns.py @@ -11,9 +11,9 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.tests.fixture_data import client -from novaclient.tests.fixture_data import floatingips as data -from novaclient.tests import utils +from novaclient.tests.unit.fixture_data import client +from novaclient.tests.unit.fixture_data import floatingips as data +from novaclient.tests.unit import utils from novaclient.v1_1 import floating_ip_dns diff --git a/novaclient/tests/v1_1/test_floating_ip_pools.py b/novaclient/tests/unit/v1_1/test_floating_ip_pools.py similarity index 87% rename from novaclient/tests/v1_1/test_floating_ip_pools.py rename to novaclient/tests/unit/v1_1/test_floating_ip_pools.py index f2d0c8fdc..55470daed 100644 --- a/novaclient/tests/v1_1/test_floating_ip_pools.py +++ b/novaclient/tests/unit/v1_1/test_floating_ip_pools.py @@ -14,9 +14,9 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.tests.fixture_data import client -from novaclient.tests.fixture_data import floatingips as data -from novaclient.tests import utils +from novaclient.tests.unit.fixture_data import client +from novaclient.tests.unit.fixture_data import floatingips as data +from novaclient.tests.unit import utils from novaclient.v1_1 import floating_ip_pools diff --git a/novaclient/tests/v1_1/test_floating_ips.py b/novaclient/tests/unit/v1_1/test_floating_ips.py similarity index 93% rename from novaclient/tests/v1_1/test_floating_ips.py rename to novaclient/tests/unit/v1_1/test_floating_ips.py index 93cc733ba..7deb2b65b 100644 --- a/novaclient/tests/v1_1/test_floating_ips.py +++ b/novaclient/tests/unit/v1_1/test_floating_ips.py @@ -14,9 +14,9 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.tests.fixture_data import client -from novaclient.tests.fixture_data import floatingips as data -from novaclient.tests import utils +from novaclient.tests.unit.fixture_data import client +from novaclient.tests.unit.fixture_data import floatingips as data +from novaclient.tests.unit import utils from novaclient.v1_1 import floating_ips diff --git a/novaclient/tests/v1_1/test_floating_ips_bulk.py b/novaclient/tests/unit/v1_1/test_floating_ips_bulk.py similarity index 94% rename from novaclient/tests/v1_1/test_floating_ips_bulk.py rename to novaclient/tests/unit/v1_1/test_floating_ips_bulk.py index ac21c1cbf..c53e453de 100644 --- a/novaclient/tests/v1_1/test_floating_ips_bulk.py +++ b/novaclient/tests/unit/v1_1/test_floating_ips_bulk.py @@ -13,9 +13,9 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.tests.fixture_data import client -from novaclient.tests.fixture_data import floatingips as data -from novaclient.tests import utils +from novaclient.tests.unit.fixture_data import client +from novaclient.tests.unit.fixture_data import floatingips as data +from novaclient.tests.unit import utils from novaclient.v1_1 import floating_ips_bulk diff --git a/novaclient/tests/v1_1/test_fping.py b/novaclient/tests/unit/v1_1/test_fping.py similarity index 93% rename from novaclient/tests/v1_1/test_fping.py rename to novaclient/tests/unit/v1_1/test_fping.py index 5a3fb6401..7fe54bc89 100644 --- a/novaclient/tests/v1_1/test_fping.py +++ b/novaclient/tests/unit/v1_1/test_fping.py @@ -13,9 +13,9 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.tests.fixture_data import client -from novaclient.tests.fixture_data import fping as data -from novaclient.tests import utils +from novaclient.tests.unit.fixture_data import client +from novaclient.tests.unit.fixture_data import fping as data +from novaclient.tests.unit import utils from novaclient.v1_1 import fping diff --git a/novaclient/tests/v1_1/test_hosts.py b/novaclient/tests/unit/v1_1/test_hosts.py similarity index 95% rename from novaclient/tests/v1_1/test_hosts.py rename to novaclient/tests/unit/v1_1/test_hosts.py index 8fcdf5a76..b51ab0718 100644 --- a/novaclient/tests/v1_1/test_hosts.py +++ b/novaclient/tests/unit/v1_1/test_hosts.py @@ -11,9 +11,9 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.tests.fixture_data import client -from novaclient.tests.fixture_data import hosts as data -from novaclient.tests import utils +from novaclient.tests.unit.fixture_data import client +from novaclient.tests.unit.fixture_data import hosts as data +from novaclient.tests.unit import utils from novaclient.v1_1 import hosts diff --git a/novaclient/tests/v1_1/test_hypervisors.py b/novaclient/tests/unit/v1_1/test_hypervisors.py similarity index 97% rename from novaclient/tests/v1_1/test_hypervisors.py rename to novaclient/tests/unit/v1_1/test_hypervisors.py index 1a6212015..b297c1247 100644 --- a/novaclient/tests/v1_1/test_hypervisors.py +++ b/novaclient/tests/unit/v1_1/test_hypervisors.py @@ -13,9 +13,9 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.tests.fixture_data import client -from novaclient.tests.fixture_data import hypervisors as data -from novaclient.tests import utils +from novaclient.tests.unit.fixture_data import client +from novaclient.tests.unit.fixture_data import hypervisors as data +from novaclient.tests.unit import utils class HypervisorsTest(utils.FixturedTestCase): diff --git a/novaclient/tests/v1_1/test_images.py b/novaclient/tests/unit/v1_1/test_images.py similarity index 93% rename from novaclient/tests/v1_1/test_images.py rename to novaclient/tests/unit/v1_1/test_images.py index e5fcfca9b..4f7d55372 100644 --- a/novaclient/tests/v1_1/test_images.py +++ b/novaclient/tests/unit/v1_1/test_images.py @@ -11,9 +11,9 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.tests.fixture_data import client -from novaclient.tests.fixture_data import images as data -from novaclient.tests import utils +from novaclient.tests.unit.fixture_data import client +from novaclient.tests.unit.fixture_data import images as data +from novaclient.tests.unit import utils from novaclient.v1_1 import images diff --git a/novaclient/tests/v1_1/test_keypairs.py b/novaclient/tests/unit/v1_1/test_keypairs.py similarity index 93% rename from novaclient/tests/v1_1/test_keypairs.py rename to novaclient/tests/unit/v1_1/test_keypairs.py index 9e8e63fd5..a5cf25913 100644 --- a/novaclient/tests/v1_1/test_keypairs.py +++ b/novaclient/tests/unit/v1_1/test_keypairs.py @@ -11,9 +11,9 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.tests.fixture_data import client -from novaclient.tests.fixture_data import keypairs as data -from novaclient.tests import utils +from novaclient.tests.unit.fixture_data import client +from novaclient.tests.unit.fixture_data import keypairs as data +from novaclient.tests.unit import utils from novaclient.v1_1 import keypairs diff --git a/novaclient/tests/v1_1/test_limits.py b/novaclient/tests/unit/v1_1/test_limits.py similarity index 95% rename from novaclient/tests/v1_1/test_limits.py rename to novaclient/tests/unit/v1_1/test_limits.py index 212be4549..598c38a0f 100644 --- a/novaclient/tests/v1_1/test_limits.py +++ b/novaclient/tests/unit/v1_1/test_limits.py @@ -11,9 +11,9 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.tests.fixture_data import client -from novaclient.tests.fixture_data import limits as data -from novaclient.tests import utils +from novaclient.tests.unit.fixture_data import client +from novaclient.tests.unit.fixture_data import limits as data +from novaclient.tests.unit import utils from novaclient.v1_1 import limits diff --git a/novaclient/tests/v1_1/test_networks.py b/novaclient/tests/unit/v1_1/test_networks.py similarity index 96% rename from novaclient/tests/v1_1/test_networks.py rename to novaclient/tests/unit/v1_1/test_networks.py index 1fbd112ce..4e57c2b6f 100644 --- a/novaclient/tests/v1_1/test_networks.py +++ b/novaclient/tests/unit/v1_1/test_networks.py @@ -11,9 +11,9 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.tests.fixture_data import client -from novaclient.tests.fixture_data import networks as data -from novaclient.tests import utils +from novaclient.tests.unit.fixture_data import client +from novaclient.tests.unit.fixture_data import networks as data +from novaclient.tests.unit import utils from novaclient.v1_1 import networks diff --git a/novaclient/tests/v1_1/test_quota_classes.py b/novaclient/tests/unit/v1_1/test_quota_classes.py similarity index 94% rename from novaclient/tests/v1_1/test_quota_classes.py rename to novaclient/tests/unit/v1_1/test_quota_classes.py index 338549bfa..8e651baea 100644 --- a/novaclient/tests/v1_1/test_quota_classes.py +++ b/novaclient/tests/unit/v1_1/test_quota_classes.py @@ -13,8 +13,8 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.tests import utils -from novaclient.tests.v1_1 import fakes +from novaclient.tests.unit import utils +from novaclient.tests.unit.v1_1 import fakes cs = fakes.FakeClient() diff --git a/novaclient/tests/v1_1/test_quotas.py b/novaclient/tests/unit/v1_1/test_quotas.py similarity index 93% rename from novaclient/tests/v1_1/test_quotas.py rename to novaclient/tests/unit/v1_1/test_quotas.py index 8ff0d5967..43f8ce1d8 100644 --- a/novaclient/tests/v1_1/test_quotas.py +++ b/novaclient/tests/unit/v1_1/test_quotas.py @@ -13,9 +13,9 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.tests.fixture_data import client -from novaclient.tests.fixture_data import quotas as data -from novaclient.tests import utils +from novaclient.tests.unit.fixture_data import client +from novaclient.tests.unit.fixture_data import quotas as data +from novaclient.tests.unit import utils class QuotaSetsTest(utils.FixturedTestCase): diff --git a/novaclient/tests/v1_1/test_security_group_rules.py b/novaclient/tests/unit/v1_1/test_security_group_rules.py similarity index 95% rename from novaclient/tests/v1_1/test_security_group_rules.py rename to novaclient/tests/unit/v1_1/test_security_group_rules.py index 7de882f96..af5b81d4c 100644 --- a/novaclient/tests/v1_1/test_security_group_rules.py +++ b/novaclient/tests/unit/v1_1/test_security_group_rules.py @@ -12,9 +12,9 @@ # under the License. from novaclient import exceptions -from novaclient.tests.fixture_data import client -from novaclient.tests.fixture_data import security_group_rules as data -from novaclient.tests import utils +from novaclient.tests.unit.fixture_data import client +from novaclient.tests.unit.fixture_data import security_group_rules as data +from novaclient.tests.unit import utils from novaclient.v1_1 import security_group_rules diff --git a/novaclient/tests/v1_1/test_security_groups.py b/novaclient/tests/unit/v1_1/test_security_groups.py similarity index 94% rename from novaclient/tests/v1_1/test_security_groups.py rename to novaclient/tests/unit/v1_1/test_security_groups.py index f5b771af1..91c9ef3f8 100644 --- a/novaclient/tests/v1_1/test_security_groups.py +++ b/novaclient/tests/unit/v1_1/test_security_groups.py @@ -11,9 +11,9 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.tests.fixture_data import client -from novaclient.tests.fixture_data import security_groups as data -from novaclient.tests import utils +from novaclient.tests.unit.fixture_data import client +from novaclient.tests.unit.fixture_data import security_groups as data +from novaclient.tests.unit import utils from novaclient.v1_1 import security_groups diff --git a/novaclient/tests/v1_1/test_server_groups.py b/novaclient/tests/unit/v1_1/test_server_groups.py similarity index 93% rename from novaclient/tests/v1_1/test_server_groups.py rename to novaclient/tests/unit/v1_1/test_server_groups.py index f7a5a2143..eecd71d41 100644 --- a/novaclient/tests/v1_1/test_server_groups.py +++ b/novaclient/tests/unit/v1_1/test_server_groups.py @@ -13,9 +13,9 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.tests.fixture_data import client -from novaclient.tests.fixture_data import server_groups as data -from novaclient.tests import utils +from novaclient.tests.unit.fixture_data import client +from novaclient.tests.unit.fixture_data import server_groups as data +from novaclient.tests.unit import utils from novaclient.v1_1 import server_groups diff --git a/novaclient/tests/v1_1/test_servers.py b/novaclient/tests/unit/v1_1/test_servers.py similarity index 98% rename from novaclient/tests/v1_1/test_servers.py rename to novaclient/tests/unit/v1_1/test_servers.py index c4037e1b3..214f394ee 100644 --- a/novaclient/tests/v1_1/test_servers.py +++ b/novaclient/tests/unit/v1_1/test_servers.py @@ -17,10 +17,10 @@ import six from novaclient import exceptions -from novaclient.tests.fixture_data import client -from novaclient.tests.fixture_data import floatingips -from novaclient.tests.fixture_data import servers as data -from novaclient.tests import utils +from novaclient.tests.unit.fixture_data import client +from novaclient.tests.unit.fixture_data import floatingips +from novaclient.tests.unit.fixture_data import servers as data +from novaclient.tests.unit import utils from novaclient.v1_1 import servers @@ -525,7 +525,7 @@ def test_get_console_output_with_length(self): # # Clear password: FooBar123 # - # RSA Private Key: novaclient/tests/idfake.pem + # RSA Private Key: novaclient/tests/unit/idfake.pem # # Encrypted password # OIuEuQttO8Rk93BcKlwHQsziDAnkAm/V6V8VPToA8ZeUaUBWwS0gwo2K6Y61Z96r @@ -538,7 +538,7 @@ def test_get_console_output_with_length(self): def test_get_password(self): s = self.cs.servers.get(1234) self.assertEqual(b'FooBar123', - s.get_password('novaclient/tests/idfake.pem')) + s.get_password('novaclient/tests/unit/idfake.pem')) self.assert_called('GET', '/servers/1234/os-server-password') def test_get_password_without_key(self): diff --git a/novaclient/tests/v1_1/test_services.py b/novaclient/tests/unit/v1_1/test_services.py similarity index 97% rename from novaclient/tests/v1_1/test_services.py rename to novaclient/tests/unit/v1_1/test_services.py index acd25152f..61cbd67f1 100644 --- a/novaclient/tests/v1_1/test_services.py +++ b/novaclient/tests/unit/v1_1/test_services.py @@ -13,8 +13,8 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.tests import utils -from novaclient.tests.v1_1 import fakes +from novaclient.tests.unit import utils +from novaclient.tests.unit.v1_1 import fakes from novaclient.v1_1 import services diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/unit/v1_1/test_shell.py similarity index 99% rename from novaclient/tests/v1_1/test_shell.py rename to novaclient/tests/unit/v1_1/test_shell.py index c1b5e1af4..ccece8012 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/unit/v1_1/test_shell.py @@ -29,8 +29,8 @@ import novaclient.client from novaclient import exceptions import novaclient.shell -from novaclient.tests import utils -from novaclient.tests.v1_1 import fakes +from novaclient.tests.unit import utils +from novaclient.tests.unit.v1_1 import fakes import novaclient.v1_1.shell diff --git a/novaclient/tests/v1_1/test_usage.py b/novaclient/tests/unit/v1_1/test_usage.py similarity index 95% rename from novaclient/tests/v1_1/test_usage.py rename to novaclient/tests/unit/v1_1/test_usage.py index fe0f1e944..2f52706dc 100644 --- a/novaclient/tests/v1_1/test_usage.py +++ b/novaclient/tests/unit/v1_1/test_usage.py @@ -13,8 +13,8 @@ import datetime -from novaclient.tests import utils -from novaclient.tests.v1_1 import fakes +from novaclient.tests.unit import utils +from novaclient.tests.unit.v1_1 import fakes from novaclient.v1_1 import usage diff --git a/novaclient/tests/v1_1/test_volumes.py b/novaclient/tests/unit/v1_1/test_volumes.py similarity index 97% rename from novaclient/tests/v1_1/test_volumes.py rename to novaclient/tests/unit/v1_1/test_volumes.py index 61a215341..432b80532 100644 --- a/novaclient/tests/v1_1/test_volumes.py +++ b/novaclient/tests/unit/v1_1/test_volumes.py @@ -13,8 +13,8 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.tests import utils -from novaclient.tests.v1_1 import fakes +from novaclient.tests.unit import utils +from novaclient.tests.unit.v1_1 import fakes from novaclient.v1_1 import volumes diff --git a/novaclient/tests/v1_1/testfile.txt b/novaclient/tests/unit/v1_1/testfile.txt similarity index 100% rename from novaclient/tests/v1_1/testfile.txt rename to novaclient/tests/unit/v1_1/testfile.txt diff --git a/novaclient/tests/v1_1/utils.py b/novaclient/tests/unit/v1_1/utils.py similarity index 100% rename from novaclient/tests/v1_1/utils.py rename to novaclient/tests/unit/v1_1/utils.py From f197c64e05596fc59c8318813d4f69a88ac832fc Mon Sep 17 00:00:00 2001 From: Joe Gordon Date: Tue, 27 Jan 2015 13:09:42 -0800 Subject: [PATCH 0675/1705] Add OS_TEST_PATH to testr Change default test path to unit tests, and support setting $OS_TEST_PATH to specify a different path (such as functional). Change-Id: I3c0d597b5e1c43a8cb04ac0fc936e9ad1cdcfbf8 --- .testr.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.testr.conf b/.testr.conf index 60477e871..0aae11096 100644 --- a/.testr.conf +++ b/.testr.conf @@ -1,4 +1,4 @@ [DEFAULT] -test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} ${PYTHON:-python} -m subunit.run discover -t ./ ./ $LISTOPT $IDOPTION +test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} ${PYTHON:-python} -m subunit.run discover -t ./ ${OS_TEST_PATH:-./novaclient/tests/unit} $LISTOPT $IDOPTION test_id_option=--load-list $IDFILE test_list_option=--list From b89da9be28172319a16bece42f068e2d7f359c67 Mon Sep 17 00:00:00 2001 From: Joe Gordon Date: Tue, 27 Jan 2015 13:11:09 -0800 Subject: [PATCH 0676/1705] First pass at tempest_lib based functional testing Begin moving tempest nova CLI tests out of tempest and into this repo using tempest-lib. This patch adds the framework to run the functional tests, later patches will port the existing tempest tests. Use standard OpenStack environment variables to get keystone auth Change-Id: Ie957bd450bfed97b63788cfb488f92988fbbc889 --- novaclient/tests/functional/__init__.py | 0 novaclient/tests/functional/base.py | 44 ++++++++++++++++ .../tests/functional/test_readonly_nova.py | 51 +++++++++++++++++++ test-requirements.txt | 1 + tox.ini | 4 ++ 5 files changed, 100 insertions(+) create mode 100644 novaclient/tests/functional/__init__.py create mode 100644 novaclient/tests/functional/base.py create mode 100644 novaclient/tests/functional/test_readonly_nova.py diff --git a/novaclient/tests/functional/__init__.py b/novaclient/tests/functional/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/novaclient/tests/functional/base.py b/novaclient/tests/functional/base.py new file mode 100644 index 000000000..27015bda8 --- /dev/null +++ b/novaclient/tests/functional/base.py @@ -0,0 +1,44 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import os + +from tempest_lib.cli import base + + +class ClientTestBase(base.ClientTestBase): + """ + This is a first pass at a simple read only python-novaclient test. This + only exercises client commands that are read only. + + This should test commands: + * as a regular user + * as a admin user + * with and without optional parameters + * initially just check return codes, and later test command outputs + + """ + def _get_clients(self): + cli_dir = os.environ.get( + 'OS_NOVACLIENT_EXEC_DIR', + os.path.join(os.path.abspath('.'), '.tox/functional/bin')) + + return base.CLIClient( + username=os.environ.get('OS_USERNAME'), + password=os.environ.get('OS_PASSWORD'), + tenant_name=os.environ.get('OS_TENANT_NAME'), + uri=os.environ.get('OS_AUTH_URL'), + cli_dir=cli_dir) + + def nova(self, *args, **kwargs): + return self.clients.nova(*args, + **kwargs) diff --git a/novaclient/tests/functional/test_readonly_nova.py b/novaclient/tests/functional/test_readonly_nova.py new file mode 100644 index 000000000..bc0589780 --- /dev/null +++ b/novaclient/tests/functional/test_readonly_nova.py @@ -0,0 +1,51 @@ +# 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 tempest_lib import exceptions + +from novaclient.tests.functional import base + + +class SimpleReadOnlyNovaClientTest(base.ClientTestBase): + + """ + This is a first pass at a simple read only python-novaclient test. This + only exercises client commands that are read only. + + This should test commands: + * as a regular user + * as a admin user + * with and without optional parameters + * initially just check return codes, and later test command outputs + + """ + + def test_admin_fake_action(self): + self.assertRaises(exceptions.CommandFailed, + self.nova, + 'this-does-nova-exist') + + # NOTE(jogo): Commands in order listed in 'nova help' + + # Optional arguments: + + def test_admin_version(self): + self.nova('', flags='--version') + + def test_admin_debug_list(self): + self.nova('list', flags='--debug') + + def test_admin_timeout(self): + self.nova('list', flags='--timeout %d' % 10) + + def test_admin_timing(self): + self.nova('list', flags='--timing') diff --git a/test-requirements.txt b/test-requirements.txt index d6628ed82..f15d3beac 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -14,3 +14,4 @@ oslosphinx>=2.2.0 # Apache-2.0 testrepository>=0.0.18 testscenarios>=0.4 testtools>=0.9.36,!=1.2.0 +tempest-lib>=0.1.0 diff --git a/tox.ini b/tox.ini index a8e9a6f34..0ea5da95b 100644 --- a/tox.ini +++ b/tox.ini @@ -23,6 +23,10 @@ commands = flake8 {posargs} [testenv:venv] commands = {posargs} +[testenv:functional] +setenv = + OS_TEST_PATH = ./novaclient/tests/functional + [testenv:cover] commands = python setup.py testr --coverage --testr-args='{posargs}' From 39dac3140cd141fbe4485ade8a43abcf2e2a36ca Mon Sep 17 00:00:00 2001 From: Katie McLaughlin Date: Fri, 30 Jan 2015 16:47:15 +1100 Subject: [PATCH 0677/1705] Update volumes.get() docstring to correctly reflect functionality The get() command gets a volume based on an id. It does not have anything to do with deleting a volume, and thus should not be documented as such. Change-Id: Id62fb07ddd9024ee02090201ce1b679d4968b168 --- novaclient/v1_1/volumes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/v1_1/volumes.py b/novaclient/v1_1/volumes.py index a3ba327f7..9b9cf1886 100644 --- a/novaclient/v1_1/volumes.py +++ b/novaclient/v1_1/volumes.py @@ -73,7 +73,7 @@ def get(self, volume_id): """ Get a volume. - :param volume_id: The ID of the volume to delete. + :param volume_id: The ID of the volume to get. :rtype: :class:`Volume` """ return self._get("/volumes/%s" % volume_id, "volume") From a5ea58fec9f655e0ddc1a341f3e27086bf0eaa4b Mon Sep 17 00:00:00 2001 From: James Penick Date: Wed, 28 Jan 2015 22:30:03 +0000 Subject: [PATCH 0678/1705] Refer to the admin password consistently The docstrings refer to instance passwords as the admin password, root password, or simply as "password". That makes documentation difficult to understand. I've cleaned things up where appropriate. I did not change the 'root-password' command in this patch, im planning to do that in a separate patch. Cleaning that up to have everything refer to the instance password as "admin password" for clarity. Change-Id: I421edcaf18bf8536d5e43f71db6e868868154be3 --- novaclient/tests/unit/test_shell.py | 6 +++--- novaclient/v1_1/servers.py | 26 +++++++++++++++++--------- novaclient/v1_1/shell.py | 12 ++++++------ 3 files changed, 26 insertions(+), 18 deletions(-) diff --git a/novaclient/tests/unit/test_shell.py b/novaclient/tests/unit/test_shell.py index 794e88a3e..9e9079637 100644 --- a/novaclient/tests/unit/test_shell.py +++ b/novaclient/tests/unit/test_shell.py @@ -139,7 +139,7 @@ def test_invalid_timeout(self): def test_help(self): required = [ '.*?^usage: ', - '.*?^\s+root-password\s+Change the root password', + '.*?^\s+root-password\s+Change the admin password', '.*?^See "nova help COMMAND" for help on a specific command', ] stdout, stderr = self.shell('help') @@ -150,7 +150,7 @@ def test_help(self): def test_help_on_subcommand(self): required = [ '.*?^usage: nova root-password', - '.*?^Change the root password', + '.*?^Change the admin password', '.*?^Positional arguments:', ] stdout, stderr = self.shell('help root-password') @@ -161,7 +161,7 @@ def test_help_on_subcommand(self): def test_help_no_options(self): required = [ '.*?^usage: ', - '.*?^\s+root-password\s+Change the root password', + '.*?^\s+root-password\s+Change the admin password', '.*?^See "nova help COMMAND" for help on a specific command', ] stdout, stderr = self.shell('') diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index fb998ac27..ef3974fba 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -252,7 +252,9 @@ def remove_fixed_ip(self, address): def change_password(self, password): """ - Update the password for a server. + Update the admin password for a server. + + :param password: string to set as the admin password on the server """ self.manager.change_password(self, password) @@ -271,7 +273,8 @@ def rebuild(self, image, password=None, preserve_ephemeral=False, Rebuild -- shut down and then re-image -- this server. :param image: the :class:`Image` (or its ID) to re-image with. - :param password: string to set as password on the rebuilt server. + :param password: string to set as the admin password on the rebuilt + server. :param preserve_ephemeral: If True, request that any ephemeral device be preserved when rebuilding the instance. Defaults to False. """ @@ -384,7 +387,8 @@ def evacuate(self, host=None, on_shared_storage=True, password=None): :param host: Name of the target host :param on_shared_storage: Specifies whether instance files located on shared storage - :param password: string to set as password on the evacuated server. + :param password: string to set as admin password on the evacuated + server. """ return self.manager.evacuate(self, host, on_shared_storage, password) @@ -713,14 +717,15 @@ def get_serial_console(self, server, console_type): def get_password(self, server, private_key=None): """ - Get password for an instance + Get admin password of an instance - Returns the clear password of an instance if private_key is - provided, returns the ciphered password otherwise. + Returns the admin password of an instance in the clear if private_key + is provided, returns the ciphered password otherwise. Requires that openssl is installed and in the path - :param server: The :class:`Server` (or its ID) to add an IP to. + :param server: The :class:`Server` (or its ID) for which the admin + password is to be returned :param private_key: The private key to decrypt password (optional) """ @@ -737,9 +742,12 @@ def get_password(self, server, private_key=None): def clear_password(self, server): """ - Clear password for an instance + Clear the admin password of an instance - :param server: The :class:`Server` (or its ID) to add an IP to. + Remove the admin password for an instance from the metadata server. + + :param server: The :class:`Server` (or its ID) for which the admin + password is to be cleared """ return self._delete("/servers/%s/os-server-password" diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 26f70249b..6bfbc5115 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1440,7 +1440,7 @@ def do_reboot(cs, args): dest='rebuild_password', metavar='', default=False, - help=_("Set the provided password on the rebuild server.")) + help=_("Set the provided admin password on the rebuilt server.")) @cliutils.arg( '--rebuild_password', help=argparse.SUPPRESS) @@ -1712,7 +1712,7 @@ def do_refresh_network(cs, args): @cliutils.arg('server', metavar='', help=_('Name or ID of server.')) def do_root_password(cs, args): """ - Change the root password for a server. + Change the admin password for a server. """ server = _find_server(cs, args.server) p1 = getpass.getpass('New password: ') @@ -2313,7 +2313,7 @@ def __init__(self, console_dict): nargs='?', default=None) def do_get_password(cs, args): - """Get password for a server.""" + """Get the admin password for a server.""" server = _find_server(cs, args.server) data = server.get_password(args.private_key) print(data) @@ -2321,7 +2321,7 @@ def do_get_password(cs, args): @cliutils.arg('server', metavar='', help=_('Name or ID of server.')) def do_clear_password(cs, args): - """Clear password for a server.""" + """Clear the admin password for a server.""" server = _find_server(cs, args.server) server.clear_password() @@ -4158,8 +4158,8 @@ def do_quota_class_update(cs, args): '--password', dest='password', metavar='', - help=_("Set the provided password on the evacuated server. Not applicable " - "with on-shared-storage flag")) + help=_("Set the provided admin password on the evacuated server. Not" + " applicable with on-shared-storage flag")) @cliutils.arg( '--on-shared-storage', dest='on_shared_storage', From 4e76f9590b49c925703cbbf42d1e232725465eff Mon Sep 17 00:00:00 2001 From: Rui Chen Date: Sun, 4 Jan 2015 16:35:12 +0800 Subject: [PATCH 0679/1705] Fix issue of quota-show and quota-defaults quota-show and quota-defaults not work because 'SessionClient' object has no attribute 'tenant_id'. Try to get project_id from auth_ref when cs.client is SessionClient instance. Change-Id: Ic125a99ba34e911485868454c3c7531a34eabdc9 Closes-Bug: #1407388 --- novaclient/tests/unit/v1_1/fakes.py | 24 ++++++++++++++++++++++++ novaclient/tests/unit/v1_1/test_shell.py | 10 ++++++++++ novaclient/v1_1/shell.py | 22 ++++++++++++++++------ 3 files changed, 50 insertions(+), 6 deletions(-) diff --git a/novaclient/tests/unit/v1_1/fakes.py b/novaclient/tests/unit/v1_1/fakes.py index 6d97374fe..7e659cb3d 100644 --- a/novaclient/tests/unit/v1_1/fakes.py +++ b/novaclient/tests/unit/v1_1/fakes.py @@ -16,6 +16,7 @@ import datetime +import mock from oslo.utils import strutils import six from six.moves.urllib import parse @@ -2185,3 +2186,26 @@ def post_os_server_groups_2cbd51f4_fafe_4cdb_801b_cf913a6f288b_action( def delete_os_server_groups_2cbd51f4_fafe_4cdb_801b_cf913a6f288b( self, **kw): return (202, {}, None) + + +class FakeSessionClient(fakes.FakeClient, client.Client): + + def __init__(self, *args, **kwargs): + client.Client.__init__(self, 'username', 'password', + 'project_id', 'auth_url', + extensions=kwargs.get('extensions')) + self.client = FakeSessionMockClient(**kwargs) + + +class FakeSessionMockClient(base_client.SessionClient, FakeHTTPClient): + + def __init__(self, *args, **kwargs): + + self.callstack = [] + self.auth = mock.Mock() + self.session = mock.Mock() + + self.auth.get_auth_ref.return_value.project_id = 'tenant_id' + + def request(self, url, method, **kwargs): + return self._cs_request(url, method, **kwargs) diff --git a/novaclient/tests/unit/v1_1/test_shell.py b/novaclient/tests/unit/v1_1/test_shell.py index ccece8012..cca5d6685 100644 --- a/novaclient/tests/unit/v1_1/test_shell.py +++ b/novaclient/tests/unit/v1_1/test_shell.py @@ -2338,6 +2338,16 @@ def test_delete_multi_server_groups(self): self.assert_called('DELETE', '/os-server-groups/12345', pos=-2) +class ShellWithSessionClientTest(ShellTest): + + def setUp(self): + """Run before each test.""" + super(ShellWithSessionClientTest, self).setUp() + self.useFixture(fixtures.MonkeyPatch( + 'novaclient.client.get_client_class', + lambda *_: fakes.FakeSessionClient)) + + class GetSecgroupTest(utils.TestCase): def test_with_integer(self): cs = mock.Mock(**{ diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 6bfbc5115..b6f78d85e 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -3876,10 +3876,15 @@ def _quota_update(manager, identifier, args): def do_quota_show(cs, args): """List the quotas for a tenant/user.""" - if not args.tenant: - _quota_show(cs.quotas.get(cs.client.tenant_id, user_id=args.user)) + if args.tenant: + project_id = args.tenant + elif isinstance(cs.client, client.SessionClient): + auth = cs.client.auth + project_id = auth.get_auth_ref(cs.client.session).project_id else: - _quota_show(cs.quotas.get(args.tenant, user_id=args.user)) + project_id = cs.client.tenant_id + + _quota_show(cs.quotas.get(project_id, user_id=args.user)) @cliutils.arg( @@ -3890,10 +3895,15 @@ def do_quota_show(cs, args): def do_quota_defaults(cs, args): """List the default quotas for a tenant.""" - if not args.tenant: - _quota_show(cs.quotas.defaults(cs.client.tenant_id)) + if args.tenant: + project_id = args.tenant + elif isinstance(cs.client, client.SessionClient): + auth = cs.client.auth + project_id = auth.get_auth_ref(cs.client.session).project_id else: - _quota_show(cs.quotas.defaults(args.tenant)) + project_id = cs.client.tenant_id + + _quota_show(cs.quotas.defaults(project_id)) @cliutils.arg( From 0a60aae852d2688861d0b4ba097a1a00529f0611 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Tue, 3 Feb 2015 02:32:58 +0200 Subject: [PATCH 0680/1705] Rename v1_1 to v2 Module novaclient.v1_1 is used as implementation of V1.1, V2 and V3. Since future development(microversioning) will be done across V2, implementation should be done in appropriate module(to prevent misleading). Despite the fact that implementation for all versions are equal, discover method for contrib path worked only for v1.1. This patch fixes this bug and modifies shell tests to check all versions. Change-Id: Ib6798f4dfe177586302141f522dc593560ce6a5b --- README.rst | 4 +- doc/source/api.rst | 3 +- doc/source/conf.py | 6 +- doc/source/index.rst | 3 +- doc/source/shell.rst | 2 +- novaclient/client.py | 6 +- novaclient/shell.py | 16 +++-- novaclient/tests/unit/fixture_data/client.py | 12 ++-- novaclient/tests/unit/test_auth_plugins.py | 2 +- novaclient/tests/unit/test_base.py | 4 +- novaclient/tests/unit/test_client.py | 38 +++++------ .../tests/unit/{v1_1 => v2}/__init__.py | 0 .../unit/{v1_1 => v2}/contrib/__init__.py | 0 .../tests/unit/{v1_1 => v2}/contrib/fakes.py | 4 +- .../contrib/test_assisted_volume_snapshots.py | 4 +- .../{v1_1 => v2}/contrib/test_baremetal.py | 4 +- .../unit/{v1_1 => v2}/contrib/test_cells.py | 4 +- .../contrib/test_instance_actions.py | 4 +- .../contrib/test_list_extensions.py | 4 +- .../{v1_1 => v2}/contrib/test_migrations.py | 4 +- .../contrib/test_server_external_events.py | 4 +- .../contrib/test_tenant_networks.py | 4 +- novaclient/tests/unit/{v1_1 => v2}/fakes.py | 2 +- .../tests/unit/{v1_1 => v2}/test_agents.py | 2 +- .../unit/{v1_1 => v2}/test_aggregates.py | 2 +- .../tests/unit/{v1_1 => v2}/test_auth.py | 2 +- .../{v1_1 => v2}/test_availability_zone.py | 4 +- .../tests/unit/{v1_1 => v2}/test_certs.py | 2 +- .../tests/unit/{v1_1 => v2}/test_client.py | 2 +- .../tests/unit/{v1_1 => v2}/test_cloudpipe.py | 2 +- .../tests/unit/{v1_1 => v2}/test_fixed_ips.py | 0 .../unit/{v1_1 => v2}/test_flavor_access.py | 4 +- .../tests/unit/{v1_1 => v2}/test_flavors.py | 4 +- .../unit/{v1_1 => v2}/test_floating_ip_dns.py | 2 +- .../{v1_1 => v2}/test_floating_ip_pools.py | 2 +- .../unit/{v1_1 => v2}/test_floating_ips.py | 2 +- .../{v1_1 => v2}/test_floating_ips_bulk.py | 2 +- .../tests/unit/{v1_1 => v2}/test_fping.py | 2 +- .../tests/unit/{v1_1 => v2}/test_hosts.py | 2 +- .../unit/{v1_1 => v2}/test_hypervisors.py | 0 .../tests/unit/{v1_1 => v2}/test_images.py | 2 +- .../tests/unit/{v1_1 => v2}/test_keypairs.py | 2 +- .../tests/unit/{v1_1 => v2}/test_limits.py | 2 +- .../tests/unit/{v1_1 => v2}/test_networks.py | 2 +- .../unit/{v1_1 => v2}/test_quota_classes.py | 2 +- .../tests/unit/{v1_1 => v2}/test_quotas.py | 0 .../{v1_1 => v2}/test_security_group_rules.py | 2 +- .../unit/{v1_1 => v2}/test_security_groups.py | 2 +- .../unit/{v1_1 => v2}/test_server_groups.py | 2 +- .../tests/unit/{v1_1 => v2}/test_servers.py | 2 +- .../tests/unit/{v1_1 => v2}/test_services.py | 4 +- .../tests/unit/{v1_1 => v2}/test_shell.py | 52 ++++++++++----- .../tests/unit/{v1_1 => v2}/test_usage.py | 4 +- .../tests/unit/{v1_1 => v2}/test_volumes.py | 4 +- .../tests/unit/{v1_1 => v2}/testfile.txt | 0 novaclient/tests/unit/{v1_1 => v2}/utils.py | 0 novaclient/v1_1/__init__.py | 30 ++++++++- novaclient/v2/__init__.py | 16 +++++ novaclient/{v1_1 => v2}/agents.py | 0 novaclient/{v1_1 => v2}/aggregates.py | 0 novaclient/{v1_1 => v2}/availability_zones.py | 0 novaclient/{v1_1 => v2}/certs.py | 0 novaclient/{v1_1 => v2}/client.py | 66 +++++++++---------- novaclient/{v1_1 => v2}/cloudpipe.py | 0 novaclient/{v1_1 => v2}/contrib/__init__.py | 0 .../contrib/assisted_volume_snapshots.py | 0 novaclient/{v1_1 => v2}/contrib/baremetal.py | 0 novaclient/{v1_1 => v2}/contrib/cells.py | 0 .../{v1_1 => v2}/contrib/deferred_delete.py | 0 .../{v1_1 => v2}/contrib/host_evacuate.py | 0 .../contrib/host_evacuate_live.py | 0 .../contrib/host_servers_migrate.py | 0 .../{v1_1 => v2}/contrib/instance_action.py | 0 .../{v1_1 => v2}/contrib/list_extensions.py | 0 .../contrib/metadata_extensions.py | 2 +- novaclient/{v1_1 => v2}/contrib/migrations.py | 0 .../contrib/server_external_events.py | 0 .../{v1_1 => v2}/contrib/tenant_networks.py | 0 novaclient/{v1_1 => v2}/fixed_ips.py | 0 novaclient/{v1_1 => v2}/flavor_access.py | 0 novaclient/{v1_1 => v2}/flavors.py | 0 novaclient/{v1_1 => v2}/floating_ip_dns.py | 0 novaclient/{v1_1 => v2}/floating_ip_pools.py | 0 novaclient/{v1_1 => v2}/floating_ips.py | 0 novaclient/{v1_1 => v2}/floating_ips_bulk.py | 0 novaclient/{v1_1 => v2}/fping.py | 0 novaclient/{v1_1 => v2}/hosts.py | 0 novaclient/{v1_1 => v2}/hypervisors.py | 0 novaclient/{v1_1 => v2}/images.py | 0 novaclient/{v1_1 => v2}/keypairs.py | 0 novaclient/{v1_1 => v2}/limits.py | 0 novaclient/{v1_1 => v2}/networks.py | 0 novaclient/{v1_1 => v2}/quota_classes.py | 0 novaclient/{v1_1 => v2}/quotas.py | 0 .../security_group_default_rules.py | 0 .../{v1_1 => v2}/security_group_rules.py | 0 novaclient/{v1_1 => v2}/security_groups.py | 0 novaclient/{v1_1 => v2}/server_groups.py | 0 novaclient/{v1_1 => v2}/servers.py | 2 +- novaclient/{v1_1 => v2}/services.py | 0 novaclient/{v1_1 => v2}/shell.py | 6 +- novaclient/{v1_1 => v2}/usage.py | 0 novaclient/{v1_1 => v2}/versions.py | 0 novaclient/{v1_1 => v2}/virtual_interfaces.py | 0 novaclient/{v1_1 => v2}/volume_snapshots.py | 0 novaclient/{v1_1 => v2}/volume_types.py | 0 novaclient/{v1_1 => v2}/volumes.py | 0 107 files changed, 218 insertions(+), 156 deletions(-) rename novaclient/tests/unit/{v1_1 => v2}/__init__.py (100%) rename novaclient/tests/unit/{v1_1 => v2}/contrib/__init__.py (100%) rename novaclient/tests/unit/{v1_1 => v2}/contrib/fakes.py (98%) rename novaclient/tests/unit/{v1_1 => v2}/contrib/test_assisted_volume_snapshots.py (90%) rename novaclient/tests/unit/{v1_1 => v2}/contrib/test_baremetal.py (95%) rename novaclient/tests/unit/{v1_1 => v2}/contrib/test_cells.py (93%) rename novaclient/tests/unit/{v1_1 => v2}/contrib/test_instance_actions.py (93%) rename novaclient/tests/unit/{v1_1 => v2}/contrib/test_list_extensions.py (91%) rename novaclient/tests/unit/{v1_1 => v2}/contrib/test_migrations.py (93%) rename novaclient/tests/unit/{v1_1 => v2}/contrib/test_server_external_events.py (92%) rename novaclient/tests/unit/{v1_1 => v2}/contrib/test_tenant_networks.py (93%) rename novaclient/tests/unit/{v1_1 => v2}/fakes.py (99%) rename novaclient/tests/unit/{v1_1 => v2}/test_agents.py (99%) rename novaclient/tests/unit/{v1_1 => v2}/test_aggregates.py (99%) rename novaclient/tests/unit/{v1_1 => v2}/test_auth.py (99%) rename novaclient/tests/unit/{v1_1 => v2}/test_availability_zone.py (97%) rename novaclient/tests/unit/{v1_1 => v2}/test_certs.py (97%) rename novaclient/tests/unit/{v1_1 => v2}/test_client.py (97%) rename novaclient/tests/unit/{v1_1 => v2}/test_cloudpipe.py (97%) rename novaclient/tests/unit/{v1_1 => v2}/test_fixed_ips.py (100%) rename novaclient/tests/unit/{v1_1 => v2}/test_flavor_access.py (96%) rename novaclient/tests/unit/{v1_1 => v2}/test_flavors.py (99%) rename novaclient/tests/unit/{v1_1 => v2}/test_floating_ip_dns.py (98%) rename novaclient/tests/unit/{v1_1 => v2}/test_floating_ip_pools.py (96%) rename novaclient/tests/unit/{v1_1 => v2}/test_floating_ips.py (98%) rename novaclient/tests/unit/{v1_1 => v2}/test_floating_ips_bulk.py (98%) rename novaclient/tests/unit/{v1_1 => v2}/test_fping.py (98%) rename novaclient/tests/unit/{v1_1 => v2}/test_hosts.py (98%) rename novaclient/tests/unit/{v1_1 => v2}/test_hypervisors.py (100%) rename novaclient/tests/unit/{v1_1 => v2}/test_images.py (98%) rename novaclient/tests/unit/{v1_1 => v2}/test_keypairs.py (98%) rename novaclient/tests/unit/{v1_1 => v2}/test_limits.py (98%) rename novaclient/tests/unit/{v1_1 => v2}/test_networks.py (99%) rename novaclient/tests/unit/{v1_1 => v2}/test_quota_classes.py (96%) rename novaclient/tests/unit/{v1_1 => v2}/test_quotas.py (100%) rename novaclient/tests/unit/{v1_1 => v2}/test_security_group_rules.py (98%) rename novaclient/tests/unit/{v1_1 => v2}/test_security_groups.py (98%) rename novaclient/tests/unit/{v1_1 => v2}/test_server_groups.py (98%) rename novaclient/tests/unit/{v1_1 => v2}/test_servers.py (99%) rename novaclient/tests/unit/{v1_1 => v2}/test_services.py (98%) rename novaclient/tests/unit/{v1_1 => v2}/test_shell.py (98%) rename novaclient/tests/unit/{v1_1 => v2}/test_usage.py (95%) rename novaclient/tests/unit/{v1_1 => v2}/test_volumes.py (97%) rename novaclient/tests/unit/{v1_1 => v2}/testfile.txt (100%) rename novaclient/tests/unit/{v1_1 => v2}/utils.py (100%) create mode 100644 novaclient/v2/__init__.py rename novaclient/{v1_1 => v2}/agents.py (100%) rename novaclient/{v1_1 => v2}/aggregates.py (100%) rename novaclient/{v1_1 => v2}/availability_zones.py (100%) rename novaclient/{v1_1 => v2}/certs.py (100%) rename novaclient/{v1_1 => v2}/client.py (86%) rename novaclient/{v1_1 => v2}/cloudpipe.py (100%) rename novaclient/{v1_1 => v2}/contrib/__init__.py (100%) rename novaclient/{v1_1 => v2}/contrib/assisted_volume_snapshots.py (100%) rename novaclient/{v1_1 => v2}/contrib/baremetal.py (100%) rename novaclient/{v1_1 => v2}/contrib/cells.py (100%) rename novaclient/{v1_1 => v2}/contrib/deferred_delete.py (100%) rename novaclient/{v1_1 => v2}/contrib/host_evacuate.py (100%) rename novaclient/{v1_1 => v2}/contrib/host_evacuate_live.py (100%) rename novaclient/{v1_1 => v2}/contrib/host_servers_migrate.py (100%) rename novaclient/{v1_1 => v2}/contrib/instance_action.py (100%) rename novaclient/{v1_1 => v2}/contrib/list_extensions.py (100%) rename novaclient/{v1_1 => v2}/contrib/metadata_extensions.py (97%) rename novaclient/{v1_1 => v2}/contrib/migrations.py (100%) rename novaclient/{v1_1 => v2}/contrib/server_external_events.py (100%) rename novaclient/{v1_1 => v2}/contrib/tenant_networks.py (100%) rename novaclient/{v1_1 => v2}/fixed_ips.py (100%) rename novaclient/{v1_1 => v2}/flavor_access.py (100%) rename novaclient/{v1_1 => v2}/flavors.py (100%) rename novaclient/{v1_1 => v2}/floating_ip_dns.py (100%) rename novaclient/{v1_1 => v2}/floating_ip_pools.py (100%) rename novaclient/{v1_1 => v2}/floating_ips.py (100%) rename novaclient/{v1_1 => v2}/floating_ips_bulk.py (100%) rename novaclient/{v1_1 => v2}/fping.py (100%) rename novaclient/{v1_1 => v2}/hosts.py (100%) rename novaclient/{v1_1 => v2}/hypervisors.py (100%) rename novaclient/{v1_1 => v2}/images.py (100%) rename novaclient/{v1_1 => v2}/keypairs.py (100%) rename novaclient/{v1_1 => v2}/limits.py (100%) rename novaclient/{v1_1 => v2}/networks.py (100%) rename novaclient/{v1_1 => v2}/quota_classes.py (100%) rename novaclient/{v1_1 => v2}/quotas.py (100%) rename novaclient/{v1_1 => v2}/security_group_default_rules.py (100%) rename novaclient/{v1_1 => v2}/security_group_rules.py (100%) rename novaclient/{v1_1 => v2}/security_groups.py (100%) rename novaclient/{v1_1 => v2}/server_groups.py (100%) rename novaclient/{v1_1 => v2}/servers.py (99%) rename novaclient/{v1_1 => v2}/services.py (100%) rename novaclient/{v1_1 => v2}/shell.py (99%) rename novaclient/{v1_1 => v2}/usage.py (100%) rename novaclient/{v1_1 => v2}/versions.py (100%) rename novaclient/{v1_1 => v2}/virtual_interfaces.py (100%) rename novaclient/{v1_1 => v2}/volume_snapshots.py (100%) rename novaclient/{v1_1 => v2}/volume_types.py (100%) rename novaclient/{v1_1 => v2}/volumes.py (100%) diff --git a/README.rst b/README.rst index 4b8b8bb25..a7edf8707 100644 --- a/README.rst +++ b/README.rst @@ -46,7 +46,7 @@ and the version of the API with ``--os-compute-api-version``. Or set them as an environment variables as well:: export OS_AUTH_URL=http://example.com:8774/v1.1/ - export OS_COMPUTE_API_VERSION=1.1 + export OS_COMPUTE_API_VERSION=2 If you are using Keystone, you need to set the OS_AUTH_URL to the keystone endpoint:: @@ -69,7 +69,7 @@ There's also a complete Python API, but it has not yet been documented. To use with nova, with keystone as the authentication system:: # use v2.0 auth with http://example.com:5000/v2.0/") - >>> from novaclient.v1_1 import client + >>> from novaclient.v2 import client >>> nt = client.Client(USER, PASS, TENANT, AUTH_URL, service_type="compute") >>> nt.flavors.list() [...] diff --git a/doc/source/api.rst b/doc/source/api.rst index e53f2a14f..9db817246 100644 --- a/doc/source/api.rst +++ b/doc/source/api.rst @@ -60,5 +60,4 @@ For more information, see the reference: :maxdepth: 2 ref/index - ref/v1_1/index - ref/v3/index + ref/v2/index diff --git a/doc/source/conf.py b/doc/source/conf.py index d33aea267..61437fd78 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -71,17 +71,13 @@ def gen_ref(ver, title, names): "pkg": pkg, "name": name}) gen_ref(None, "Exceptions", ["exceptions"]) -gen_ref("v1_1", "Version 1.1, Version 2 API Reference", +gen_ref("v2", "Version 1.1, Version 2 API Reference, Version 3 API Reference", ["flavors", "images", "servers", "hosts", "agents", "aggregates", "availability_zones", "certs", "fixed_ips", "floating_ip_pools", "floating_ips", "hypervisors", "keypairs", "limits", "networks", "quota_classes", "quotas", "security_group_rules", "security_groups", "services", "virtual_interfaces", "volume_snapshots", "volumes", "volume_types"]) -gen_ref("v3", "Version 3 API Reference", - ["flavors", "hosts", "agents", "aggregates", "availability_zones", - "certs", "hypervisors", "images", "keypairs", "quotas", - "quotas_classes", "servers", "services"]) # -- General configuration ---------------------------------------------------- diff --git a/doc/source/index.rst b/doc/source/index.rst index eb551e1ae..9bd302bcc 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -25,8 +25,7 @@ Contents: shell api ref/index - ref/v1_1/index - ref/v3/index + ref/v2/index releases Contributing diff --git a/doc/source/shell.rst b/doc/source/shell.rst index f02c1597e..52bcfb49d 100644 --- a/doc/source/shell.rst +++ b/doc/source/shell.rst @@ -41,7 +41,7 @@ For example, in Bash you'd use:: export OS_PASSWORD=yadayadayada export OS_TENANT_NAME=myproject export OS_AUTH_URL=http://... - export OS_COMPUTE_API_VERSION=1.1 + export OS_COMPUTE_API_VERSION=2 From there, all shell commands take the form:: diff --git a/novaclient/client.py b/novaclient/client.py index a44ade03c..200d75c73 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -773,9 +773,9 @@ def _construct_http_client(username=None, password=None, project_id=None, def get_client_class(version): version_map = { - '1.1': 'novaclient.v1_1.client.Client', - '2': 'novaclient.v1_1.client.Client', - '3': 'novaclient.v1_1.client.Client', + '1.1': 'novaclient.v2.client.Client', + '2': 'novaclient.v2.client.Client', + '3': 'novaclient.v2.client.Client', } try: client_path = version_map[str(version)] diff --git a/novaclient/shell.py b/novaclient/shell.py index 0cc6b43c7..940a01e08 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -55,9 +55,9 @@ from novaclient.i18n import _ from novaclient.openstack.common import cliutils from novaclient import utils -from novaclient.v1_1 import shell as shell_v1_1 +from novaclient.v2 import shell as shell_v2 -DEFAULT_OS_COMPUTE_API_VERSION = "1.1" +DEFAULT_OS_COMPUTE_API_VERSION = "2" DEFAULT_NOVA_ENDPOINT_TYPE = 'publicURL' # NOTE(cyeoh): Having the service type dependent on the API version # is pretty ugly, but we have to do this because traditionally the @@ -446,12 +446,12 @@ def get_subcommand_parser(self, version): try: actions_module = { - '1.1': shell_v1_1, - '2': shell_v1_1, - '3': shell_v1_1, + '1.1': shell_v2, + '2': shell_v2, + '3': shell_v2, }[version] except KeyError: - actions_module = shell_v1_1 + actions_module = shell_v2 self._find_actions(subparsers, actions_module) self._find_actions(subparsers, self) @@ -491,6 +491,10 @@ def _discover_via_python_path(self): def _discover_via_contrib_path(self, version): module_path = os.path.dirname(os.path.abspath(__file__)) version_str = "v%s" % version.replace('.', '_') + # NOTE(akurilin): v1.1, v2 and v3 have one implementation, so + # we should discover contrib modules in one place. + if version_str in ["v1_1", "v3"]: + version_str = "v2" ext_path = os.path.join(module_path, version_str, 'contrib') ext_glob = os.path.join(ext_path, "*.py") diff --git a/novaclient/tests/unit/fixture_data/client.py b/novaclient/tests/unit/fixture_data/client.py index c6ddad3b7..5f933b750 100644 --- a/novaclient/tests/unit/fixture_data/client.py +++ b/novaclient/tests/unit/fixture_data/client.py @@ -15,7 +15,7 @@ from keystoneclient import fixture from keystoneclient import session -from novaclient.v1_1 import client as v1_1client +from novaclient.v2 import client as v2client IDENTITY_URL = 'http://identityserver:5000/v2.0' COMPUTE_URL = 'http://compute.host' @@ -51,10 +51,10 @@ def setUp(self): self.client = self.new_client() def new_client(self): - return v1_1client.Client(username='xx', - api_key='xx', - project_id='xx', - auth_url=self.identity_url) + return v2client.Client(username='xx', + api_key='xx', + project_id='xx', + auth_url=self.identity_url) class SessionV1(V1): @@ -62,4 +62,4 @@ class SessionV1(V1): def new_client(self): self.session = session.Session() self.session.auth = v2.Password(self.identity_url, 'xx', 'xx') - return v1_1client.Client(session=self.session) + return v2client.Client(session=self.session) diff --git a/novaclient/tests/unit/test_auth_plugins.py b/novaclient/tests/unit/test_auth_plugins.py index 992077d0a..675102ca3 100644 --- a/novaclient/tests/unit/test_auth_plugins.py +++ b/novaclient/tests/unit/test_auth_plugins.py @@ -28,7 +28,7 @@ from novaclient import auth_plugin from novaclient import exceptions from novaclient.tests.unit import utils -from novaclient.v1_1 import client +from novaclient.v2 import client def mock_http_request(resp=None): diff --git a/novaclient/tests/unit/test_base.py b/novaclient/tests/unit/test_base.py index d3fc6c756..b7bceb7b8 100644 --- a/novaclient/tests/unit/test_base.py +++ b/novaclient/tests/unit/test_base.py @@ -14,8 +14,8 @@ from novaclient import base from novaclient import exceptions from novaclient.tests.unit import utils -from novaclient.tests.unit.v1_1 import fakes -from novaclient.v1_1 import flavors +from novaclient.tests.unit.v2 import fakes +from novaclient.v2 import flavors cs = fakes.FakeClient() diff --git a/novaclient/tests/unit/test_client.py b/novaclient/tests/unit/test_client.py index f8161156e..2c7f7eaf7 100644 --- a/novaclient/tests/unit/test_client.py +++ b/novaclient/tests/unit/test_client.py @@ -24,7 +24,7 @@ import novaclient.client import novaclient.extension from novaclient.tests.unit import utils -import novaclient.v1_1.client +import novaclient.v2.client class ClientConnectionPoolTest(utils.TestCase): @@ -138,57 +138,57 @@ def test_client_version_url_with_project_name(self): def test_get_client_class_v3(self): output = novaclient.client.get_client_class('3') - self.assertEqual(output, novaclient.v1_1.client.Client) + self.assertEqual(output, novaclient.v2.client.Client) def test_get_client_class_v2(self): output = novaclient.client.get_client_class('2') - self.assertEqual(output, novaclient.v1_1.client.Client) + self.assertEqual(output, novaclient.v2.client.Client) def test_get_client_class_v2_int(self): output = novaclient.client.get_client_class(2) - self.assertEqual(output, novaclient.v1_1.client.Client) + self.assertEqual(output, novaclient.v2.client.Client) def test_get_client_class_v1_1(self): output = novaclient.client.get_client_class('1.1') - self.assertEqual(output, novaclient.v1_1.client.Client) + self.assertEqual(output, novaclient.v2.client.Client) def test_get_client_class_unknown(self): self.assertRaises(novaclient.exceptions.UnsupportedVersion, novaclient.client.get_client_class, '0') def test_client_with_os_cache_enabled(self): - cs = novaclient.v1_1.client.Client("user", "password", "project_id", - auth_url="foo/v2", os_cache=True) + cs = novaclient.v2.client.Client("user", "password", "project_id", + auth_url="foo/v2", os_cache=True) self.assertEqual(True, cs.os_cache) self.assertEqual(True, cs.client.os_cache) def test_client_with_os_cache_disabled(self): - cs = novaclient.v1_1.client.Client("user", "password", "project_id", - auth_url="foo/v2", os_cache=False) + cs = novaclient.v2.client.Client("user", "password", "project_id", + auth_url="foo/v2", os_cache=False) self.assertEqual(False, cs.os_cache) self.assertEqual(False, cs.client.os_cache) def test_client_with_no_cache_enabled(self): - cs = novaclient.v1_1.client.Client("user", "password", "project_id", - auth_url="foo/v2", no_cache=True) + cs = novaclient.v2.client.Client("user", "password", "project_id", + auth_url="foo/v2", no_cache=True) self.assertEqual(False, cs.os_cache) self.assertEqual(False, cs.client.os_cache) def test_client_with_no_cache_disabled(self): - cs = novaclient.v1_1.client.Client("user", "password", "project_id", - auth_url="foo/v2", no_cache=False) + cs = novaclient.v2.client.Client("user", "password", "project_id", + auth_url="foo/v2", no_cache=False) self.assertEqual(True, cs.os_cache) self.assertEqual(True, cs.client.os_cache) def test_client_set_management_url_v1_1(self): - cs = novaclient.v1_1.client.Client("user", "password", "project_id", - auth_url="foo/v2") + cs = novaclient.v2.client.Client("user", "password", "project_id", + auth_url="foo/v2") cs.set_management_url("blabla") self.assertEqual("blabla", cs.client.management_url) def test_client_get_reset_timings_v1_1(self): - cs = novaclient.v1_1.client.Client("user", "password", "project_id", - auth_url="foo/v2") + cs = novaclient.v2.client.Client("user", "password", "project_id", + auth_url="foo/v2") self.assertEqual(0, len(cs.get_timings())) cs.client.times.append("somevalue") self.assertEqual(1, len(cs.get_timings())) @@ -201,8 +201,8 @@ def test_client_get_reset_timings_v1_1(self): def test_contextmanager_v1_1(self, mock_http_client): fake_client = mock.Mock() mock_http_client.return_value = fake_client - with novaclient.v1_1.client.Client("user", "password", "project_id", - auth_url="foo/v2"): + with novaclient.v2.client.Client("user", "password", "project_id", + auth_url="foo/v2"): pass self.assertTrue(fake_client.open_session.called) self.assertTrue(fake_client.close_session.called) diff --git a/novaclient/tests/unit/v1_1/__init__.py b/novaclient/tests/unit/v2/__init__.py similarity index 100% rename from novaclient/tests/unit/v1_1/__init__.py rename to novaclient/tests/unit/v2/__init__.py diff --git a/novaclient/tests/unit/v1_1/contrib/__init__.py b/novaclient/tests/unit/v2/contrib/__init__.py similarity index 100% rename from novaclient/tests/unit/v1_1/contrib/__init__.py rename to novaclient/tests/unit/v2/contrib/__init__.py diff --git a/novaclient/tests/unit/v1_1/contrib/fakes.py b/novaclient/tests/unit/v2/contrib/fakes.py similarity index 98% rename from novaclient/tests/unit/v1_1/contrib/fakes.py rename to novaclient/tests/unit/v2/contrib/fakes.py index e961ae100..870339584 100644 --- a/novaclient/tests/unit/v1_1/contrib/fakes.py +++ b/novaclient/tests/unit/v2/contrib/fakes.py @@ -12,8 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -from novaclient.tests.unit.v1_1 import fakes -from novaclient.v1_1 import client +from novaclient.tests.unit.v2 import fakes +from novaclient.v2 import client class FakeClient(fakes.FakeClient): diff --git a/novaclient/tests/unit/v1_1/contrib/test_assisted_volume_snapshots.py b/novaclient/tests/unit/v2/contrib/test_assisted_volume_snapshots.py similarity index 90% rename from novaclient/tests/unit/v1_1/contrib/test_assisted_volume_snapshots.py rename to novaclient/tests/unit/v2/contrib/test_assisted_volume_snapshots.py index 37f584ce7..8ab732aed 100644 --- a/novaclient/tests/unit/v1_1/contrib/test_assisted_volume_snapshots.py +++ b/novaclient/tests/unit/v2/contrib/test_assisted_volume_snapshots.py @@ -18,8 +18,8 @@ from novaclient import extension from novaclient.tests.unit import utils -from novaclient.tests.unit.v1_1.contrib import fakes -from novaclient.v1_1.contrib import assisted_volume_snapshots as assisted_snaps +from novaclient.tests.unit.v2.contrib import fakes +from novaclient.v2.contrib import assisted_volume_snapshots as assisted_snaps extensions = [ diff --git a/novaclient/tests/unit/v1_1/contrib/test_baremetal.py b/novaclient/tests/unit/v2/contrib/test_baremetal.py similarity index 95% rename from novaclient/tests/unit/v1_1/contrib/test_baremetal.py rename to novaclient/tests/unit/v2/contrib/test_baremetal.py index 1866eea6e..a5ee41467 100644 --- a/novaclient/tests/unit/v1_1/contrib/test_baremetal.py +++ b/novaclient/tests/unit/v2/contrib/test_baremetal.py @@ -16,8 +16,8 @@ from novaclient import extension from novaclient.tests.unit import utils -from novaclient.tests.unit.v1_1.contrib import fakes -from novaclient.v1_1.contrib import baremetal +from novaclient.tests.unit.v2.contrib import fakes +from novaclient.v2.contrib import baremetal extensions = [ diff --git a/novaclient/tests/unit/v1_1/contrib/test_cells.py b/novaclient/tests/unit/v2/contrib/test_cells.py similarity index 93% rename from novaclient/tests/unit/v1_1/contrib/test_cells.py rename to novaclient/tests/unit/v2/contrib/test_cells.py index ccdcd375d..e411502bc 100644 --- a/novaclient/tests/unit/v1_1/contrib/test_cells.py +++ b/novaclient/tests/unit/v2/contrib/test_cells.py @@ -15,8 +15,8 @@ from novaclient import extension from novaclient.tests.unit import utils -from novaclient.tests.unit.v1_1.contrib import fakes -from novaclient.v1_1.contrib import cells +from novaclient.tests.unit.v2.contrib import fakes +from novaclient.v2.contrib import cells extensions = [ diff --git a/novaclient/tests/unit/v1_1/contrib/test_instance_actions.py b/novaclient/tests/unit/v2/contrib/test_instance_actions.py similarity index 93% rename from novaclient/tests/unit/v1_1/contrib/test_instance_actions.py rename to novaclient/tests/unit/v2/contrib/test_instance_actions.py index 66dbb418c..0b0400a29 100644 --- a/novaclient/tests/unit/v1_1/contrib/test_instance_actions.py +++ b/novaclient/tests/unit/v2/contrib/test_instance_actions.py @@ -15,8 +15,8 @@ from novaclient import extension from novaclient.tests.unit import utils -from novaclient.tests.unit.v1_1.contrib import fakes -from novaclient.v1_1.contrib import instance_action +from novaclient.tests.unit.v2.contrib import fakes +from novaclient.v2.contrib import instance_action extensions = [ diff --git a/novaclient/tests/unit/v1_1/contrib/test_list_extensions.py b/novaclient/tests/unit/v2/contrib/test_list_extensions.py similarity index 91% rename from novaclient/tests/unit/v1_1/contrib/test_list_extensions.py rename to novaclient/tests/unit/v2/contrib/test_list_extensions.py index dfb742ebd..3fa5253ba 100644 --- a/novaclient/tests/unit/v1_1/contrib/test_list_extensions.py +++ b/novaclient/tests/unit/v2/contrib/test_list_extensions.py @@ -13,8 +13,8 @@ from novaclient import extension from novaclient.tests.unit import utils -from novaclient.tests.unit.v1_1 import fakes -from novaclient.v1_1.contrib import list_extensions +from novaclient.tests.unit.v2 import fakes +from novaclient.v2.contrib import list_extensions extensions = [ diff --git a/novaclient/tests/unit/v1_1/contrib/test_migrations.py b/novaclient/tests/unit/v2/contrib/test_migrations.py similarity index 93% rename from novaclient/tests/unit/v1_1/contrib/test_migrations.py rename to novaclient/tests/unit/v2/contrib/test_migrations.py index 5fe5c8f07..881fd1e50 100644 --- a/novaclient/tests/unit/v1_1/contrib/test_migrations.py +++ b/novaclient/tests/unit/v2/contrib/test_migrations.py @@ -12,8 +12,8 @@ from novaclient import extension from novaclient.tests.unit import utils -from novaclient.tests.unit.v1_1 import fakes -from novaclient.v1_1.contrib import migrations +from novaclient.tests.unit.v2 import fakes +from novaclient.v2.contrib import migrations extensions = [ extension.Extension(migrations.__name__.split(".")[-1], diff --git a/novaclient/tests/unit/v1_1/contrib/test_server_external_events.py b/novaclient/tests/unit/v2/contrib/test_server_external_events.py similarity index 92% rename from novaclient/tests/unit/v1_1/contrib/test_server_external_events.py rename to novaclient/tests/unit/v2/contrib/test_server_external_events.py index a933ad89f..1b941c9d6 100644 --- a/novaclient/tests/unit/v1_1/contrib/test_server_external_events.py +++ b/novaclient/tests/unit/v2/contrib/test_server_external_events.py @@ -18,8 +18,8 @@ from novaclient import extension from novaclient.tests.unit import utils -from novaclient.tests.unit.v1_1.contrib import fakes -from novaclient.v1_1.contrib import server_external_events as ext_events +from novaclient.tests.unit.v2.contrib import fakes +from novaclient.v2.contrib import server_external_events as ext_events extensions = [ diff --git a/novaclient/tests/unit/v1_1/contrib/test_tenant_networks.py b/novaclient/tests/unit/v2/contrib/test_tenant_networks.py similarity index 93% rename from novaclient/tests/unit/v1_1/contrib/test_tenant_networks.py rename to novaclient/tests/unit/v2/contrib/test_tenant_networks.py index a1a76cee3..13159ce5f 100644 --- a/novaclient/tests/unit/v1_1/contrib/test_tenant_networks.py +++ b/novaclient/tests/unit/v2/contrib/test_tenant_networks.py @@ -15,8 +15,8 @@ from novaclient import extension from novaclient.tests.unit import utils -from novaclient.tests.unit.v1_1.contrib import fakes -from novaclient.v1_1.contrib import tenant_networks +from novaclient.tests.unit.v2.contrib import fakes +from novaclient.v2.contrib import tenant_networks extensions = [ diff --git a/novaclient/tests/unit/v1_1/fakes.py b/novaclient/tests/unit/v2/fakes.py similarity index 99% rename from novaclient/tests/unit/v1_1/fakes.py rename to novaclient/tests/unit/v2/fakes.py index 7e659cb3d..68322a176 100644 --- a/novaclient/tests/unit/v1_1/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -25,7 +25,7 @@ from novaclient import exceptions from novaclient.tests.unit import fakes from novaclient.tests.unit import utils -from novaclient.v1_1 import client +from novaclient.v2 import client class FakeClient(fakes.FakeClient, client.Client): diff --git a/novaclient/tests/unit/v1_1/test_agents.py b/novaclient/tests/unit/v2/test_agents.py similarity index 99% rename from novaclient/tests/unit/v1_1/test_agents.py rename to novaclient/tests/unit/v2/test_agents.py index a269b322a..e7fa7d6ed 100644 --- a/novaclient/tests/unit/v1_1/test_agents.py +++ b/novaclient/tests/unit/v2/test_agents.py @@ -16,7 +16,7 @@ from novaclient.tests.unit.fixture_data import agents as data from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit import utils -from novaclient.v1_1 import agents +from novaclient.v2 import agents class AgentsTest(utils.FixturedTestCase): diff --git a/novaclient/tests/unit/v1_1/test_aggregates.py b/novaclient/tests/unit/v2/test_aggregates.py similarity index 99% rename from novaclient/tests/unit/v1_1/test_aggregates.py rename to novaclient/tests/unit/v2/test_aggregates.py index 40cc7d922..97a3ffc2b 100644 --- a/novaclient/tests/unit/v1_1/test_aggregates.py +++ b/novaclient/tests/unit/v2/test_aggregates.py @@ -16,7 +16,7 @@ from novaclient.tests.unit.fixture_data import aggregates as data from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit import utils -from novaclient.v1_1 import aggregates +from novaclient.v2 import aggregates class AggregatesTest(utils.FixturedTestCase): diff --git a/novaclient/tests/unit/v1_1/test_auth.py b/novaclient/tests/unit/v2/test_auth.py similarity index 99% rename from novaclient/tests/unit/v1_1/test_auth.py rename to novaclient/tests/unit/v2/test_auth.py index 65715e54d..95e96ded1 100644 --- a/novaclient/tests/unit/v1_1/test_auth.py +++ b/novaclient/tests/unit/v2/test_auth.py @@ -20,7 +20,7 @@ from novaclient import exceptions from novaclient.tests.unit import utils -from novaclient.v1_1 import client +from novaclient.v2 import client class AuthenticateAgainstKeystoneTests(utils.TestCase): diff --git a/novaclient/tests/unit/v1_1/test_availability_zone.py b/novaclient/tests/unit/v2/test_availability_zone.py similarity index 97% rename from novaclient/tests/unit/v1_1/test_availability_zone.py rename to novaclient/tests/unit/v2/test_availability_zone.py index fda4764a1..a7d6d82e2 100644 --- a/novaclient/tests/unit/v1_1/test_availability_zone.py +++ b/novaclient/tests/unit/v2/test_availability_zone.py @@ -19,13 +19,13 @@ from novaclient.tests.unit.fixture_data import availability_zones as data from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit import utils -from novaclient.v1_1 import availability_zones +from novaclient.v2 import availability_zones class AvailabilityZoneTest(utils.FixturedTestCase): # NOTE(cyeoh): import shell here so the V3 version of # this class can inherit off the v3 version of shell - from novaclient.v1_1 import shell # noqa + from novaclient.v2 import shell # noqa data_fixture_class = data.V1 diff --git a/novaclient/tests/unit/v1_1/test_certs.py b/novaclient/tests/unit/v2/test_certs.py similarity index 97% rename from novaclient/tests/unit/v1_1/test_certs.py rename to novaclient/tests/unit/v2/test_certs.py index 39eac06f7..6ec2258fc 100644 --- a/novaclient/tests/unit/v1_1/test_certs.py +++ b/novaclient/tests/unit/v2/test_certs.py @@ -14,7 +14,7 @@ from novaclient.tests.unit.fixture_data import certs as data from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit import utils -from novaclient.v1_1 import certs +from novaclient.v2 import certs class CertsTest(utils.FixturedTestCase): diff --git a/novaclient/tests/unit/v1_1/test_client.py b/novaclient/tests/unit/v2/test_client.py similarity index 97% rename from novaclient/tests/unit/v1_1/test_client.py rename to novaclient/tests/unit/v2/test_client.py index fbc98f7cb..b4c59ce38 100644 --- a/novaclient/tests/unit/v1_1/test_client.py +++ b/novaclient/tests/unit/v2/test_client.py @@ -15,7 +15,7 @@ from keystoneclient import session from novaclient.tests.unit import utils -from novaclient.v1_1 import client +from novaclient.v2 import client class ClientTest(utils.TestCase): diff --git a/novaclient/tests/unit/v1_1/test_cloudpipe.py b/novaclient/tests/unit/v2/test_cloudpipe.py similarity index 97% rename from novaclient/tests/unit/v1_1/test_cloudpipe.py rename to novaclient/tests/unit/v2/test_cloudpipe.py index 0a40db68d..3ef9533cd 100644 --- a/novaclient/tests/unit/v1_1/test_cloudpipe.py +++ b/novaclient/tests/unit/v2/test_cloudpipe.py @@ -16,7 +16,7 @@ from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit.fixture_data import cloudpipe as data from novaclient.tests.unit import utils -from novaclient.v1_1 import cloudpipe +from novaclient.v2 import cloudpipe class CloudpipeTest(utils.FixturedTestCase): diff --git a/novaclient/tests/unit/v1_1/test_fixed_ips.py b/novaclient/tests/unit/v2/test_fixed_ips.py similarity index 100% rename from novaclient/tests/unit/v1_1/test_fixed_ips.py rename to novaclient/tests/unit/v2/test_fixed_ips.py diff --git a/novaclient/tests/unit/v1_1/test_flavor_access.py b/novaclient/tests/unit/v2/test_flavor_access.py similarity index 96% rename from novaclient/tests/unit/v1_1/test_flavor_access.py rename to novaclient/tests/unit/v2/test_flavor_access.py index 08677ad4e..109f177e6 100644 --- a/novaclient/tests/unit/v1_1/test_flavor_access.py +++ b/novaclient/tests/unit/v2/test_flavor_access.py @@ -14,8 +14,8 @@ # under the License. from novaclient.tests.unit import utils -from novaclient.tests.unit.v1_1 import fakes -from novaclient.v1_1 import flavor_access +from novaclient.tests.unit.v2 import fakes +from novaclient.v2 import flavor_access cs = fakes.FakeClient() diff --git a/novaclient/tests/unit/v1_1/test_flavors.py b/novaclient/tests/unit/v2/test_flavors.py similarity index 99% rename from novaclient/tests/unit/v1_1/test_flavors.py rename to novaclient/tests/unit/v2/test_flavors.py index 90f7eb1d9..27b77c068 100644 --- a/novaclient/tests/unit/v1_1/test_flavors.py +++ b/novaclient/tests/unit/v2/test_flavors.py @@ -17,8 +17,8 @@ from novaclient import exceptions from novaclient.tests.unit import utils -from novaclient.tests.unit.v1_1 import fakes -from novaclient.v1_1 import flavors +from novaclient.tests.unit.v2 import fakes +from novaclient.v2 import flavors class FlavorsTest(utils.TestCase): diff --git a/novaclient/tests/unit/v1_1/test_floating_ip_dns.py b/novaclient/tests/unit/v2/test_floating_ip_dns.py similarity index 98% rename from novaclient/tests/unit/v1_1/test_floating_ip_dns.py rename to novaclient/tests/unit/v2/test_floating_ip_dns.py index 36f900ced..ec20e56c5 100644 --- a/novaclient/tests/unit/v1_1/test_floating_ip_dns.py +++ b/novaclient/tests/unit/v2/test_floating_ip_dns.py @@ -14,7 +14,7 @@ from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit.fixture_data import floatingips as data from novaclient.tests.unit import utils -from novaclient.v1_1 import floating_ip_dns +from novaclient.v2 import floating_ip_dns class FloatingIPDNSDomainTest(utils.FixturedTestCase): diff --git a/novaclient/tests/unit/v1_1/test_floating_ip_pools.py b/novaclient/tests/unit/v2/test_floating_ip_pools.py similarity index 96% rename from novaclient/tests/unit/v1_1/test_floating_ip_pools.py rename to novaclient/tests/unit/v2/test_floating_ip_pools.py index 55470daed..a138fc285 100644 --- a/novaclient/tests/unit/v1_1/test_floating_ip_pools.py +++ b/novaclient/tests/unit/v2/test_floating_ip_pools.py @@ -17,7 +17,7 @@ from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit.fixture_data import floatingips as data from novaclient.tests.unit import utils -from novaclient.v1_1 import floating_ip_pools +from novaclient.v2 import floating_ip_pools class TestFloatingIPPools(utils.FixturedTestCase): diff --git a/novaclient/tests/unit/v1_1/test_floating_ips.py b/novaclient/tests/unit/v2/test_floating_ips.py similarity index 98% rename from novaclient/tests/unit/v1_1/test_floating_ips.py rename to novaclient/tests/unit/v2/test_floating_ips.py index 7deb2b65b..2b091a516 100644 --- a/novaclient/tests/unit/v1_1/test_floating_ips.py +++ b/novaclient/tests/unit/v2/test_floating_ips.py @@ -17,7 +17,7 @@ from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit.fixture_data import floatingips as data from novaclient.tests.unit import utils -from novaclient.v1_1 import floating_ips +from novaclient.v2 import floating_ips class FloatingIPsTest(utils.FixturedTestCase): diff --git a/novaclient/tests/unit/v1_1/test_floating_ips_bulk.py b/novaclient/tests/unit/v2/test_floating_ips_bulk.py similarity index 98% rename from novaclient/tests/unit/v1_1/test_floating_ips_bulk.py rename to novaclient/tests/unit/v2/test_floating_ips_bulk.py index c53e453de..69b553f24 100644 --- a/novaclient/tests/unit/v1_1/test_floating_ips_bulk.py +++ b/novaclient/tests/unit/v2/test_floating_ips_bulk.py @@ -16,7 +16,7 @@ from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit.fixture_data import floatingips as data from novaclient.tests.unit import utils -from novaclient.v1_1 import floating_ips_bulk +from novaclient.v2 import floating_ips_bulk class FloatingIPsBulkTest(utils.FixturedTestCase): diff --git a/novaclient/tests/unit/v1_1/test_fping.py b/novaclient/tests/unit/v2/test_fping.py similarity index 98% rename from novaclient/tests/unit/v1_1/test_fping.py rename to novaclient/tests/unit/v2/test_fping.py index 7fe54bc89..b9ecc3991 100644 --- a/novaclient/tests/unit/v1_1/test_fping.py +++ b/novaclient/tests/unit/v2/test_fping.py @@ -16,7 +16,7 @@ from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit.fixture_data import fping as data from novaclient.tests.unit import utils -from novaclient.v1_1 import fping +from novaclient.v2 import fping class FpingTest(utils.FixturedTestCase): diff --git a/novaclient/tests/unit/v1_1/test_hosts.py b/novaclient/tests/unit/v2/test_hosts.py similarity index 98% rename from novaclient/tests/unit/v1_1/test_hosts.py rename to novaclient/tests/unit/v2/test_hosts.py index b51ab0718..029964b6b 100644 --- a/novaclient/tests/unit/v1_1/test_hosts.py +++ b/novaclient/tests/unit/v2/test_hosts.py @@ -14,7 +14,7 @@ from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit.fixture_data import hosts as data from novaclient.tests.unit import utils -from novaclient.v1_1 import hosts +from novaclient.v2 import hosts class HostsTest(utils.FixturedTestCase): diff --git a/novaclient/tests/unit/v1_1/test_hypervisors.py b/novaclient/tests/unit/v2/test_hypervisors.py similarity index 100% rename from novaclient/tests/unit/v1_1/test_hypervisors.py rename to novaclient/tests/unit/v2/test_hypervisors.py diff --git a/novaclient/tests/unit/v1_1/test_images.py b/novaclient/tests/unit/v2/test_images.py similarity index 98% rename from novaclient/tests/unit/v1_1/test_images.py rename to novaclient/tests/unit/v2/test_images.py index 4f7d55372..1f8104edb 100644 --- a/novaclient/tests/unit/v1_1/test_images.py +++ b/novaclient/tests/unit/v2/test_images.py @@ -14,7 +14,7 @@ from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit.fixture_data import images as data from novaclient.tests.unit import utils -from novaclient.v1_1 import images +from novaclient.v2 import images class ImagesTest(utils.FixturedTestCase): diff --git a/novaclient/tests/unit/v1_1/test_keypairs.py b/novaclient/tests/unit/v2/test_keypairs.py similarity index 98% rename from novaclient/tests/unit/v1_1/test_keypairs.py rename to novaclient/tests/unit/v2/test_keypairs.py index a5cf25913..91c249f82 100644 --- a/novaclient/tests/unit/v1_1/test_keypairs.py +++ b/novaclient/tests/unit/v2/test_keypairs.py @@ -14,7 +14,7 @@ from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit.fixture_data import keypairs as data from novaclient.tests.unit import utils -from novaclient.v1_1 import keypairs +from novaclient.v2 import keypairs class KeypairsTest(utils.FixturedTestCase): diff --git a/novaclient/tests/unit/v1_1/test_limits.py b/novaclient/tests/unit/v2/test_limits.py similarity index 98% rename from novaclient/tests/unit/v1_1/test_limits.py rename to novaclient/tests/unit/v2/test_limits.py index 598c38a0f..94a62c5c0 100644 --- a/novaclient/tests/unit/v1_1/test_limits.py +++ b/novaclient/tests/unit/v2/test_limits.py @@ -14,7 +14,7 @@ from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit.fixture_data import limits as data from novaclient.tests.unit import utils -from novaclient.v1_1 import limits +from novaclient.v2 import limits class LimitsTest(utils.FixturedTestCase): diff --git a/novaclient/tests/unit/v1_1/test_networks.py b/novaclient/tests/unit/v2/test_networks.py similarity index 99% rename from novaclient/tests/unit/v1_1/test_networks.py rename to novaclient/tests/unit/v2/test_networks.py index 4e57c2b6f..a45ba80dc 100644 --- a/novaclient/tests/unit/v1_1/test_networks.py +++ b/novaclient/tests/unit/v2/test_networks.py @@ -14,7 +14,7 @@ from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit.fixture_data import networks as data from novaclient.tests.unit import utils -from novaclient.v1_1 import networks +from novaclient.v2 import networks class NetworksTest(utils.FixturedTestCase): diff --git a/novaclient/tests/unit/v1_1/test_quota_classes.py b/novaclient/tests/unit/v2/test_quota_classes.py similarity index 96% rename from novaclient/tests/unit/v1_1/test_quota_classes.py rename to novaclient/tests/unit/v2/test_quota_classes.py index 8e651baea..467ff830d 100644 --- a/novaclient/tests/unit/v1_1/test_quota_classes.py +++ b/novaclient/tests/unit/v2/test_quota_classes.py @@ -14,7 +14,7 @@ # under the License. from novaclient.tests.unit import utils -from novaclient.tests.unit.v1_1 import fakes +from novaclient.tests.unit.v2 import fakes cs = fakes.FakeClient() diff --git a/novaclient/tests/unit/v1_1/test_quotas.py b/novaclient/tests/unit/v2/test_quotas.py similarity index 100% rename from novaclient/tests/unit/v1_1/test_quotas.py rename to novaclient/tests/unit/v2/test_quotas.py diff --git a/novaclient/tests/unit/v1_1/test_security_group_rules.py b/novaclient/tests/unit/v2/test_security_group_rules.py similarity index 98% rename from novaclient/tests/unit/v1_1/test_security_group_rules.py rename to novaclient/tests/unit/v2/test_security_group_rules.py index af5b81d4c..837519821 100644 --- a/novaclient/tests/unit/v1_1/test_security_group_rules.py +++ b/novaclient/tests/unit/v2/test_security_group_rules.py @@ -15,7 +15,7 @@ from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit.fixture_data import security_group_rules as data from novaclient.tests.unit import utils -from novaclient.v1_1 import security_group_rules +from novaclient.v2 import security_group_rules class SecurityGroupRulesTest(utils.FixturedTestCase): diff --git a/novaclient/tests/unit/v1_1/test_security_groups.py b/novaclient/tests/unit/v2/test_security_groups.py similarity index 98% rename from novaclient/tests/unit/v1_1/test_security_groups.py rename to novaclient/tests/unit/v2/test_security_groups.py index 91c9ef3f8..9aeb8dde8 100644 --- a/novaclient/tests/unit/v1_1/test_security_groups.py +++ b/novaclient/tests/unit/v2/test_security_groups.py @@ -14,7 +14,7 @@ from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit.fixture_data import security_groups as data from novaclient.tests.unit import utils -from novaclient.v1_1 import security_groups +from novaclient.v2 import security_groups class SecurityGroupsTest(utils.FixturedTestCase): diff --git a/novaclient/tests/unit/v1_1/test_server_groups.py b/novaclient/tests/unit/v2/test_server_groups.py similarity index 98% rename from novaclient/tests/unit/v1_1/test_server_groups.py rename to novaclient/tests/unit/v2/test_server_groups.py index eecd71d41..fcda2dec7 100644 --- a/novaclient/tests/unit/v1_1/test_server_groups.py +++ b/novaclient/tests/unit/v2/test_server_groups.py @@ -16,7 +16,7 @@ from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit.fixture_data import server_groups as data from novaclient.tests.unit import utils -from novaclient.v1_1 import server_groups +from novaclient.v2 import server_groups class ServerGroupsTest(utils.FixturedTestCase): diff --git a/novaclient/tests/unit/v1_1/test_servers.py b/novaclient/tests/unit/v2/test_servers.py similarity index 99% rename from novaclient/tests/unit/v1_1/test_servers.py rename to novaclient/tests/unit/v2/test_servers.py index 214f394ee..c9b15f30a 100644 --- a/novaclient/tests/unit/v1_1/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -21,7 +21,7 @@ from novaclient.tests.unit.fixture_data import floatingips from novaclient.tests.unit.fixture_data import servers as data from novaclient.tests.unit import utils -from novaclient.v1_1 import servers +from novaclient.v2 import servers class ServersTest(utils.FixturedTestCase): diff --git a/novaclient/tests/unit/v1_1/test_services.py b/novaclient/tests/unit/v2/test_services.py similarity index 98% rename from novaclient/tests/unit/v1_1/test_services.py rename to novaclient/tests/unit/v2/test_services.py index 61cbd67f1..2724342d4 100644 --- a/novaclient/tests/unit/v1_1/test_services.py +++ b/novaclient/tests/unit/v2/test_services.py @@ -14,8 +14,8 @@ # under the License. from novaclient.tests.unit import utils -from novaclient.tests.unit.v1_1 import fakes -from novaclient.v1_1 import services +from novaclient.tests.unit.v2 import fakes +from novaclient.v2 import services class ServicesTest(utils.TestCase): diff --git a/novaclient/tests/unit/v1_1/test_shell.py b/novaclient/tests/unit/v2/test_shell.py similarity index 98% rename from novaclient/tests/unit/v1_1/test_shell.py rename to novaclient/tests/unit/v2/test_shell.py index cca5d6685..49e1581aa 100644 --- a/novaclient/tests/unit/v1_1/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -30,8 +30,8 @@ from novaclient import exceptions import novaclient.shell from novaclient.tests.unit import utils -from novaclient.tests.unit.v1_1 import fakes -import novaclient.v1_1.shell +from novaclient.tests.unit.v2 import fakes +import novaclient.v2.shell class ShellFixture(fixtures.Fixture): @@ -54,7 +54,7 @@ class ShellTest(utils.TestCase): 'NOVA_USERNAME': 'username', 'NOVA_PASSWORD': 'password', 'NOVA_PROJECT_ID': 'project_id', - 'OS_COMPUTE_API_VERSION': '1.1', + 'OS_COMPUTE_API_VERSION': '2', 'NOVA_URL': 'http://no.where', 'OS_AUTH_URL': 'http://no.where/v2.0', } @@ -644,7 +644,7 @@ def test_boot_min_max_count(self): cmd = 'boot --image 1 --flavor 1 --min-count 3 --max-count 1 serv' self.assertRaises(exceptions.CommandError, self.run_command, cmd) - @mock.patch('novaclient.v1_1.shell._poll_for_status') + @mock.patch('novaclient.v2.shell._poll_for_status') def test_boot_with_poll(self, poll_method): self.run_command('boot --flavor 1 --image 1 some-server --poll') self.assert_called_anytime( @@ -1088,7 +1088,7 @@ def test_show_bad_id(self): self.assertRaises(exceptions.CommandError, self.run_command, 'show xxx') - @mock.patch('novaclient.v1_1.shell.utils.print_dict') + @mock.patch('novaclient.v2.shell.utils.print_dict') def test_print_server(self, mock_print_dict): self.run_command('show 5678') args, kwargs = mock_print_dict.call_args @@ -2225,7 +2225,7 @@ def test_migration_list_with_filters(self): '/os-migrations?cell_name=child1&host=host1' '&status=finished') - @mock.patch('novaclient.v1_1.shell._find_server') + @mock.patch('novaclient.v2.shell._find_server') @mock.patch('os.system') def test_ssh(self, mock_system, mock_find_server): class FakeResources(object): @@ -2273,7 +2273,7 @@ class FakeResources(object): mock_system.assert_called_with("ssh -6 -p22 " "root@2607:f0d0:1002::4 -1") - @mock.patch('novaclient.v1_1.shell._find_server') + @mock.patch('novaclient.v2.shell._find_server') @mock.patch('os.system') def test_ssh_multinet(self, mock_system, mock_find_server): class FakeResources(object): @@ -2338,6 +2338,28 @@ def test_delete_multi_server_groups(self): self.assert_called('DELETE', '/os-server-groups/12345', pos=-2) +class ShellTestV11(ShellTest): + FAKE_ENV = { + 'NOVA_USERNAME': 'username', + 'NOVA_PASSWORD': 'password', + 'NOVA_PROJECT_ID': 'project_id', + 'OS_COMPUTE_API_VERSION': '1.1', + 'NOVA_URL': 'http://no.where', + 'OS_AUTH_URL': 'http://no.where/v2.0', + } + + +class ShellTestV3(ShellTest): + FAKE_ENV = { + 'NOVA_USERNAME': 'username', + 'NOVA_PASSWORD': 'password', + 'NOVA_PROJECT_ID': 'project_id', + 'OS_COMPUTE_API_VERSION': '3', + 'NOVA_URL': 'http://no.where', + 'OS_AUTH_URL': 'http://no.where/v2.0', + } + + class ShellWithSessionClientTest(ShellTest): def setUp(self): @@ -2354,7 +2376,7 @@ def test_with_integer(self): 'security_groups.get.return_value': 'sec_group', 'security_groups.list.return_value': [], }) - result = novaclient.v1_1.shell._get_secgroup(cs, '1') + result = novaclient.v2.shell._get_secgroup(cs, '1') self.assertEqual('sec_group', result) cs.security_groups.get.assert_called_once_with('1') @@ -2363,7 +2385,7 @@ def test_with_uuid(self): 'security_groups.get.return_value': 'sec_group', 'security_groups.list.return_value': [], }) - result = novaclient.v1_1.shell._get_secgroup( + result = novaclient.v2.shell._get_secgroup( cs, 'c0c32459-dc5f-44dc-9a0a-473b28bac831') self.assertEqual('sec_group', result) cs.security_groups.get.assert_called_once_with( @@ -2375,7 +2397,7 @@ def test_with_an_nonexisting_name(self): 'security_groups.list.return_value': [], }) self.assertRaises(exceptions.CommandError, - novaclient.v1_1.shell._get_secgroup, + novaclient.v2.shell._get_secgroup, cs, 'abc') @@ -2387,7 +2409,7 @@ def test_with_non_unique_name(self): 'security_groups.list.return_value': [group_one, group_one], }) self.assertRaises(exceptions.NoUniqueMatch, - novaclient.v1_1.shell._get_secgroup, + novaclient.v2.shell._get_secgroup, cs, 'group_one') @@ -2396,7 +2418,7 @@ class GetFirstEndpointTest(utils.TestCase): def test_only_one_endpoint(self): """If there is only one endpoint, it is returned.""" endpoint = {"url": "test"} - result = novaclient.v1_1.shell._get_first_endpoint([endpoint], "XYZ") + result = novaclient.v2.shell._get_first_endpoint([endpoint], "XYZ") self.assertEqual(endpoint, result) def test_multiple_endpoints(self): @@ -2409,7 +2431,7 @@ def test_multiple_endpoints(self): {"region": "ORD", "number": 1}, {"region": "ORD", "number": 2} ] - result = novaclient.v1_1.shell._get_first_endpoint(endpoints, "ORD") + result = novaclient.v2.shell._get_first_endpoint(endpoints, "ORD") self.assertEqual(endpoints[1], result) def test_multiple_endpoints_but_none_suitable(self): @@ -2423,11 +2445,11 @@ def test_multiple_endpoints_but_none_suitable(self): {"region": "STU"} ] self.assertRaises(LookupError, - novaclient.v1_1.shell._get_first_endpoint, + novaclient.v2.shell._get_first_endpoint, endpoints, "ORD") def test_no_endpoints(self): """If there are no endpoints available, an exception is raised.""" self.assertRaises(LookupError, - novaclient.v1_1.shell._get_first_endpoint, + novaclient.v2.shell._get_first_endpoint, [], "ORD") diff --git a/novaclient/tests/unit/v1_1/test_usage.py b/novaclient/tests/unit/v2/test_usage.py similarity index 95% rename from novaclient/tests/unit/v1_1/test_usage.py rename to novaclient/tests/unit/v2/test_usage.py index 2f52706dc..a92be0e0d 100644 --- a/novaclient/tests/unit/v1_1/test_usage.py +++ b/novaclient/tests/unit/v2/test_usage.py @@ -14,8 +14,8 @@ import datetime from novaclient.tests.unit import utils -from novaclient.tests.unit.v1_1 import fakes -from novaclient.v1_1 import usage +from novaclient.tests.unit.v2 import fakes +from novaclient.v2 import usage class UsageTest(utils.TestCase): diff --git a/novaclient/tests/unit/v1_1/test_volumes.py b/novaclient/tests/unit/v2/test_volumes.py similarity index 97% rename from novaclient/tests/unit/v1_1/test_volumes.py rename to novaclient/tests/unit/v2/test_volumes.py index 432b80532..52b81ab6a 100644 --- a/novaclient/tests/unit/v1_1/test_volumes.py +++ b/novaclient/tests/unit/v2/test_volumes.py @@ -14,8 +14,8 @@ # under the License. from novaclient.tests.unit import utils -from novaclient.tests.unit.v1_1 import fakes -from novaclient.v1_1 import volumes +from novaclient.tests.unit.v2 import fakes +from novaclient.v2 import volumes cs = fakes.FakeClient() diff --git a/novaclient/tests/unit/v1_1/testfile.txt b/novaclient/tests/unit/v2/testfile.txt similarity index 100% rename from novaclient/tests/unit/v1_1/testfile.txt rename to novaclient/tests/unit/v2/testfile.txt diff --git a/novaclient/tests/unit/v1_1/utils.py b/novaclient/tests/unit/v2/utils.py similarity index 100% rename from novaclient/tests/unit/v1_1/utils.py rename to novaclient/tests/unit/v2/utils.py diff --git a/novaclient/v1_1/__init__.py b/novaclient/v1_1/__init__.py index 19712285f..9da768172 100644 --- a/novaclient/v1_1/__init__.py +++ b/novaclient/v1_1/__init__.py @@ -1,4 +1,3 @@ -# Copyright (c) 2012 OpenStack Foundation # # All Rights Reserved. # @@ -14,4 +13,31 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.v1_1.client import Client # noqa +# NOTE(akurilin): This module is left for backward compatibility. Feel free to +# remove it, when openstack project will use correct way to +# obtain novaclient object. +# Known problems: +# * python-openstackclient - +# https://bugs.launchpad.net/python-openstackclient/+bug/1418024 +# * neutron - https://bugs.launchpad.net/neutron/+bug/1418017 + + +import sys +import warnings + +from novaclient import v2 + +warnings.warn("Module novaclient.v1_1 is deprecated (taken as a basis for " + "novaclient.v2). " + "The preferable way to get client class or object you can find " + "in novaclient.client module.") + + +class MovedModule(object): + def __init__(self, new_module): + self.new_module = new_module + + def __getattr__(self, attr): + return getattr(self.new_module, attr) + +sys.modules["novaclient.v1_1"] = MovedModule(v2) diff --git a/novaclient/v2/__init__.py b/novaclient/v2/__init__.py new file mode 100644 index 000000000..21d7c4fcc --- /dev/null +++ b/novaclient/v2/__init__.py @@ -0,0 +1,16 @@ +# +# All Rights Reserved. +# +# 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 novaclient.v2.client import Client # noqa diff --git a/novaclient/v1_1/agents.py b/novaclient/v2/agents.py similarity index 100% rename from novaclient/v1_1/agents.py rename to novaclient/v2/agents.py diff --git a/novaclient/v1_1/aggregates.py b/novaclient/v2/aggregates.py similarity index 100% rename from novaclient/v1_1/aggregates.py rename to novaclient/v2/aggregates.py diff --git a/novaclient/v1_1/availability_zones.py b/novaclient/v2/availability_zones.py similarity index 100% rename from novaclient/v1_1/availability_zones.py rename to novaclient/v2/availability_zones.py diff --git a/novaclient/v1_1/certs.py b/novaclient/v2/certs.py similarity index 100% rename from novaclient/v1_1/certs.py rename to novaclient/v2/certs.py diff --git a/novaclient/v1_1/client.py b/novaclient/v2/client.py similarity index 86% rename from novaclient/v1_1/client.py rename to novaclient/v2/client.py index 83789aa6b..386c649f9 100644 --- a/novaclient/v1_1/client.py +++ b/novaclient/v2/client.py @@ -14,39 +14,39 @@ # under the License. from novaclient import client -from novaclient.v1_1 import agents -from novaclient.v1_1 import aggregates -from novaclient.v1_1 import availability_zones -from novaclient.v1_1 import certs -from novaclient.v1_1 import cloudpipe -from novaclient.v1_1 import fixed_ips -from novaclient.v1_1 import flavor_access -from novaclient.v1_1 import flavors -from novaclient.v1_1 import floating_ip_dns -from novaclient.v1_1 import floating_ip_pools -from novaclient.v1_1 import floating_ips -from novaclient.v1_1 import floating_ips_bulk -from novaclient.v1_1 import fping -from novaclient.v1_1 import hosts -from novaclient.v1_1 import hypervisors -from novaclient.v1_1 import images -from novaclient.v1_1 import keypairs -from novaclient.v1_1 import limits -from novaclient.v1_1 import networks -from novaclient.v1_1 import quota_classes -from novaclient.v1_1 import quotas -from novaclient.v1_1 import security_group_default_rules -from novaclient.v1_1 import security_group_rules -from novaclient.v1_1 import security_groups -from novaclient.v1_1 import server_groups -from novaclient.v1_1 import servers -from novaclient.v1_1 import services -from novaclient.v1_1 import usage -from novaclient.v1_1 import versions -from novaclient.v1_1 import virtual_interfaces -from novaclient.v1_1 import volume_snapshots -from novaclient.v1_1 import volume_types -from novaclient.v1_1 import volumes +from novaclient.v2 import agents +from novaclient.v2 import aggregates +from novaclient.v2 import availability_zones +from novaclient.v2 import certs +from novaclient.v2 import cloudpipe +from novaclient.v2 import fixed_ips +from novaclient.v2 import flavor_access +from novaclient.v2 import flavors +from novaclient.v2 import floating_ip_dns +from novaclient.v2 import floating_ip_pools +from novaclient.v2 import floating_ips +from novaclient.v2 import floating_ips_bulk +from novaclient.v2 import fping +from novaclient.v2 import hosts +from novaclient.v2 import hypervisors +from novaclient.v2 import images +from novaclient.v2 import keypairs +from novaclient.v2 import limits +from novaclient.v2 import networks +from novaclient.v2 import quota_classes +from novaclient.v2 import quotas +from novaclient.v2 import security_group_default_rules +from novaclient.v2 import security_group_rules +from novaclient.v2 import security_groups +from novaclient.v2 import server_groups +from novaclient.v2 import servers +from novaclient.v2 import services +from novaclient.v2 import usage +from novaclient.v2 import versions +from novaclient.v2 import virtual_interfaces +from novaclient.v2 import volume_snapshots +from novaclient.v2 import volume_types +from novaclient.v2 import volumes class Client(object): diff --git a/novaclient/v1_1/cloudpipe.py b/novaclient/v2/cloudpipe.py similarity index 100% rename from novaclient/v1_1/cloudpipe.py rename to novaclient/v2/cloudpipe.py diff --git a/novaclient/v1_1/contrib/__init__.py b/novaclient/v2/contrib/__init__.py similarity index 100% rename from novaclient/v1_1/contrib/__init__.py rename to novaclient/v2/contrib/__init__.py diff --git a/novaclient/v1_1/contrib/assisted_volume_snapshots.py b/novaclient/v2/contrib/assisted_volume_snapshots.py similarity index 100% rename from novaclient/v1_1/contrib/assisted_volume_snapshots.py rename to novaclient/v2/contrib/assisted_volume_snapshots.py diff --git a/novaclient/v1_1/contrib/baremetal.py b/novaclient/v2/contrib/baremetal.py similarity index 100% rename from novaclient/v1_1/contrib/baremetal.py rename to novaclient/v2/contrib/baremetal.py diff --git a/novaclient/v1_1/contrib/cells.py b/novaclient/v2/contrib/cells.py similarity index 100% rename from novaclient/v1_1/contrib/cells.py rename to novaclient/v2/contrib/cells.py diff --git a/novaclient/v1_1/contrib/deferred_delete.py b/novaclient/v2/contrib/deferred_delete.py similarity index 100% rename from novaclient/v1_1/contrib/deferred_delete.py rename to novaclient/v2/contrib/deferred_delete.py diff --git a/novaclient/v1_1/contrib/host_evacuate.py b/novaclient/v2/contrib/host_evacuate.py similarity index 100% rename from novaclient/v1_1/contrib/host_evacuate.py rename to novaclient/v2/contrib/host_evacuate.py diff --git a/novaclient/v1_1/contrib/host_evacuate_live.py b/novaclient/v2/contrib/host_evacuate_live.py similarity index 100% rename from novaclient/v1_1/contrib/host_evacuate_live.py rename to novaclient/v2/contrib/host_evacuate_live.py diff --git a/novaclient/v1_1/contrib/host_servers_migrate.py b/novaclient/v2/contrib/host_servers_migrate.py similarity index 100% rename from novaclient/v1_1/contrib/host_servers_migrate.py rename to novaclient/v2/contrib/host_servers_migrate.py diff --git a/novaclient/v1_1/contrib/instance_action.py b/novaclient/v2/contrib/instance_action.py similarity index 100% rename from novaclient/v1_1/contrib/instance_action.py rename to novaclient/v2/contrib/instance_action.py diff --git a/novaclient/v1_1/contrib/list_extensions.py b/novaclient/v2/contrib/list_extensions.py similarity index 100% rename from novaclient/v1_1/contrib/list_extensions.py rename to novaclient/v2/contrib/list_extensions.py diff --git a/novaclient/v1_1/contrib/metadata_extensions.py b/novaclient/v2/contrib/metadata_extensions.py similarity index 97% rename from novaclient/v1_1/contrib/metadata_extensions.py rename to novaclient/v2/contrib/metadata_extensions.py index 36bef9abb..ad843b746 100644 --- a/novaclient/v1_1/contrib/metadata_extensions.py +++ b/novaclient/v2/contrib/metadata_extensions.py @@ -15,7 +15,7 @@ from novaclient.i18n import _ from novaclient.openstack.common import cliutils -from novaclient.v1_1 import shell +from novaclient.v2 import shell @cliutils.arg( diff --git a/novaclient/v1_1/contrib/migrations.py b/novaclient/v2/contrib/migrations.py similarity index 100% rename from novaclient/v1_1/contrib/migrations.py rename to novaclient/v2/contrib/migrations.py diff --git a/novaclient/v1_1/contrib/server_external_events.py b/novaclient/v2/contrib/server_external_events.py similarity index 100% rename from novaclient/v1_1/contrib/server_external_events.py rename to novaclient/v2/contrib/server_external_events.py diff --git a/novaclient/v1_1/contrib/tenant_networks.py b/novaclient/v2/contrib/tenant_networks.py similarity index 100% rename from novaclient/v1_1/contrib/tenant_networks.py rename to novaclient/v2/contrib/tenant_networks.py diff --git a/novaclient/v1_1/fixed_ips.py b/novaclient/v2/fixed_ips.py similarity index 100% rename from novaclient/v1_1/fixed_ips.py rename to novaclient/v2/fixed_ips.py diff --git a/novaclient/v1_1/flavor_access.py b/novaclient/v2/flavor_access.py similarity index 100% rename from novaclient/v1_1/flavor_access.py rename to novaclient/v2/flavor_access.py diff --git a/novaclient/v1_1/flavors.py b/novaclient/v2/flavors.py similarity index 100% rename from novaclient/v1_1/flavors.py rename to novaclient/v2/flavors.py diff --git a/novaclient/v1_1/floating_ip_dns.py b/novaclient/v2/floating_ip_dns.py similarity index 100% rename from novaclient/v1_1/floating_ip_dns.py rename to novaclient/v2/floating_ip_dns.py diff --git a/novaclient/v1_1/floating_ip_pools.py b/novaclient/v2/floating_ip_pools.py similarity index 100% rename from novaclient/v1_1/floating_ip_pools.py rename to novaclient/v2/floating_ip_pools.py diff --git a/novaclient/v1_1/floating_ips.py b/novaclient/v2/floating_ips.py similarity index 100% rename from novaclient/v1_1/floating_ips.py rename to novaclient/v2/floating_ips.py diff --git a/novaclient/v1_1/floating_ips_bulk.py b/novaclient/v2/floating_ips_bulk.py similarity index 100% rename from novaclient/v1_1/floating_ips_bulk.py rename to novaclient/v2/floating_ips_bulk.py diff --git a/novaclient/v1_1/fping.py b/novaclient/v2/fping.py similarity index 100% rename from novaclient/v1_1/fping.py rename to novaclient/v2/fping.py diff --git a/novaclient/v1_1/hosts.py b/novaclient/v2/hosts.py similarity index 100% rename from novaclient/v1_1/hosts.py rename to novaclient/v2/hosts.py diff --git a/novaclient/v1_1/hypervisors.py b/novaclient/v2/hypervisors.py similarity index 100% rename from novaclient/v1_1/hypervisors.py rename to novaclient/v2/hypervisors.py diff --git a/novaclient/v1_1/images.py b/novaclient/v2/images.py similarity index 100% rename from novaclient/v1_1/images.py rename to novaclient/v2/images.py diff --git a/novaclient/v1_1/keypairs.py b/novaclient/v2/keypairs.py similarity index 100% rename from novaclient/v1_1/keypairs.py rename to novaclient/v2/keypairs.py diff --git a/novaclient/v1_1/limits.py b/novaclient/v2/limits.py similarity index 100% rename from novaclient/v1_1/limits.py rename to novaclient/v2/limits.py diff --git a/novaclient/v1_1/networks.py b/novaclient/v2/networks.py similarity index 100% rename from novaclient/v1_1/networks.py rename to novaclient/v2/networks.py diff --git a/novaclient/v1_1/quota_classes.py b/novaclient/v2/quota_classes.py similarity index 100% rename from novaclient/v1_1/quota_classes.py rename to novaclient/v2/quota_classes.py diff --git a/novaclient/v1_1/quotas.py b/novaclient/v2/quotas.py similarity index 100% rename from novaclient/v1_1/quotas.py rename to novaclient/v2/quotas.py diff --git a/novaclient/v1_1/security_group_default_rules.py b/novaclient/v2/security_group_default_rules.py similarity index 100% rename from novaclient/v1_1/security_group_default_rules.py rename to novaclient/v2/security_group_default_rules.py diff --git a/novaclient/v1_1/security_group_rules.py b/novaclient/v2/security_group_rules.py similarity index 100% rename from novaclient/v1_1/security_group_rules.py rename to novaclient/v2/security_group_rules.py diff --git a/novaclient/v1_1/security_groups.py b/novaclient/v2/security_groups.py similarity index 100% rename from novaclient/v1_1/security_groups.py rename to novaclient/v2/security_groups.py diff --git a/novaclient/v1_1/server_groups.py b/novaclient/v2/server_groups.py similarity index 100% rename from novaclient/v1_1/server_groups.py rename to novaclient/v2/server_groups.py diff --git a/novaclient/v1_1/servers.py b/novaclient/v2/servers.py similarity index 99% rename from novaclient/v1_1/servers.py rename to novaclient/v2/servers.py index ef3974fba..36de45b7f 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v2/servers.py @@ -28,7 +28,7 @@ from novaclient import base from novaclient import crypto from novaclient.i18n import _ -from novaclient.v1_1 import security_groups +from novaclient.v2 import security_groups REBOOT_SOFT, REBOOT_HARD = 'SOFT', 'HARD' diff --git a/novaclient/v1_1/services.py b/novaclient/v2/services.py similarity index 100% rename from novaclient/v1_1/services.py rename to novaclient/v2/services.py diff --git a/novaclient/v1_1/shell.py b/novaclient/v2/shell.py similarity index 99% rename from novaclient/v1_1/shell.py rename to novaclient/v2/shell.py index b6f78d85e..190d07fe3 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v2/shell.py @@ -39,9 +39,9 @@ from novaclient.openstack.common import cliutils from novaclient.openstack.common import uuidutils from novaclient import utils -from novaclient.v1_1 import availability_zones -from novaclient.v1_1 import quotas -from novaclient.v1_1 import servers +from novaclient.v2 import availability_zones +from novaclient.v2 import quotas +from novaclient.v2 import servers logger = logging.getLogger(__name__) diff --git a/novaclient/v1_1/usage.py b/novaclient/v2/usage.py similarity index 100% rename from novaclient/v1_1/usage.py rename to novaclient/v2/usage.py diff --git a/novaclient/v1_1/versions.py b/novaclient/v2/versions.py similarity index 100% rename from novaclient/v1_1/versions.py rename to novaclient/v2/versions.py diff --git a/novaclient/v1_1/virtual_interfaces.py b/novaclient/v2/virtual_interfaces.py similarity index 100% rename from novaclient/v1_1/virtual_interfaces.py rename to novaclient/v2/virtual_interfaces.py diff --git a/novaclient/v1_1/volume_snapshots.py b/novaclient/v2/volume_snapshots.py similarity index 100% rename from novaclient/v1_1/volume_snapshots.py rename to novaclient/v2/volume_snapshots.py diff --git a/novaclient/v1_1/volume_types.py b/novaclient/v2/volume_types.py similarity index 100% rename from novaclient/v1_1/volume_types.py rename to novaclient/v2/volume_types.py diff --git a/novaclient/v1_1/volumes.py b/novaclient/v2/volumes.py similarity index 100% rename from novaclient/v1_1/volumes.py rename to novaclient/v2/volumes.py From d11f960c58c523da7154b3311d6b37ec715392af Mon Sep 17 00:00:00 2001 From: Joe Gordon Date: Wed, 28 Jan 2015 15:13:17 -0800 Subject: [PATCH 0681/1705] Add first pass at post_test_hook for functional tests Add first pass at getting a post_test_hook working. Once the project config side of this lands this script can be self gating. Change-Id: I5b0f5508f8e0a2a13f71e4a6b7a29a0d1a59b951 --- .../tests/functional/hooks/post_test_hook.sh | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100755 novaclient/tests/functional/hooks/post_test_hook.sh diff --git a/novaclient/tests/functional/hooks/post_test_hook.sh b/novaclient/tests/functional/hooks/post_test_hook.sh new file mode 100755 index 000000000..e9e35b24e --- /dev/null +++ b/novaclient/tests/functional/hooks/post_test_hook.sh @@ -0,0 +1,50 @@ +#!/bin/bash -xe + +# 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. + +# This script is executed inside post_test_hook function in devstack gate. + +function generate_testr_results { + if [ -f .testrepository/0 ]; then + sudo .tox/functional/bin/testr last --subunit > $WORKSPACE/testrepository.subunit + sudo mv $WORKSPACE/testrepository.subunit $BASE/logs/testrepository.subunit + sudo .tox/functional/bin/python /usr/local/jenkins/slave_scripts/subunit2html.py $BASE/logs/testrepository.subunit $BASE/logs/testr_results.html + sudo gzip -9 $BASE/logs/testrepository.subunit + sudo gzip -9 $BASE/logs/testr_results.html + sudo chown jenkins:jenkins $BASE/logs/testrepository.subunit.gz $BASE/logs/testr_results.html.gz + sudo chmod a+r $BASE/logs/testrepository.subunit.gz $BASE/logs/testr_results.html.gz + fi +} + +export NOVACLIENT_DIR="$BASE/new/python-novaclient" + +# Get admin credentials +cd $BASE/new/devstack +source openrc admin admin + +# Go to the novaclient dir +cd $NOVACLIENT_DIR + +sudo chown -R jenkins:stack $NOVACLIENT_DIR + +# Run tests +echo "Running novaclient functional test suite" +set +e +# Preserve env for OS_ credentials +sudo -E -H -u jenkins tox -efunctional +EXIT_CODE=$? +set -e + +# Collect and parse result +generate_testr_results +exit $EXIT_CODE From 0fd8816aa51b69034929c56d5e053d87d93ce021 Mon Sep 17 00:00:00 2001 From: Tomofumi Hayashi Date: Thu, 5 Feb 2015 02:32:58 +0000 Subject: [PATCH 0682/1705] Change logic in find_resource() to identify upper-case/lower-case name. Currently find_resource() searches resource with human_id before does with name_attr. With this logic, find_resource() cannot identify upper/lower case. In find_resource(), human_id always matches and name_attr never used in find_resource() because human_id made from small letters of name_attr string (see bug/1318503). To identify upper/lower case, name_attr should be used before human_id. This fix moves name_attr to ahead of human_id with unit-test cases. Change-Id: I9b821d7111c11a97be38f19de06172daf410022d Closes-Bug: #1318503 --- novaclient/tests/unit/test_utils.py | 20 ++++++++++++++++++++ novaclient/tests/unit/v2/test_shell.py | 17 +++++++---------- novaclient/utils.py | 12 ++++++------ 3 files changed, 33 insertions(+), 16 deletions(-) diff --git a/novaclient/tests/unit/test_utils.py b/novaclient/tests/unit/test_utils.py index fe0452636..51b824f20 100644 --- a/novaclient/tests/unit/test_utils.py +++ b/novaclient/tests/unit/test_utils.py @@ -41,6 +41,10 @@ class FakeManager(base.ManagerWithFind): resources = [ FakeResource('1234', {'name': 'entity_one'}), + FakeResource('12345', {'name': 'UPPER'}), + FakeResource('123456', {'name': 'lower'}), + FakeResource('1234567', {'name': 'Mixed'}), + FakeResource('12345678', {'name': 'mixed'}), FakeResource(UUID, {'name': 'entity_two'}), FakeResource('5678', {'name': '9876'}), FakeResource('01234', {'name': 'entity_three'}) @@ -118,6 +122,22 @@ def test_find_by_str_name(self): output = utils.find_resource(self.manager, 'entity_one') self.assertEqual(output, self.manager.get('1234')) + def test_find_by_str_upper_name(self): + output = utils.find_resource(self.manager, 'UPPER') + self.assertEqual(output, self.manager.get('12345')) + + def test_find_by_str_name(self): + output = utils.find_resource(self.manager, 'lower') + self.assertEqual(output, self.manager.get('123456')) + + def test_find_by_str_mix_name(self): + output = utils.find_resource(self.manager, 'Mixed') + self.assertEqual(output, self.manager.get('1234567')) + + def test_find_by_str_lower_name(self): + output = utils.find_resource(self.manager, 'mixed') + self.assertEqual(output, self.manager.get('12345678')) + def test_find_by_str_displayname(self): display_manager = FakeDisplayManager(None) output = utils.find_resource(display_manager, 'entity_three') diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 49e1581aa..bdf958659 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -673,8 +673,7 @@ def test_boot_named_flavor(self): self.assert_called('GET', '/images/1', pos=0) self.assert_called('GET', '/flavors/512 MB Server', pos=1) self.assert_called('GET', '/flavors?is_public=None', pos=2) - self.assert_called('GET', '/flavors?is_public=None', pos=3) - self.assert_called('GET', '/flavors/2', pos=4) + self.assert_called('GET', '/flavors/2', pos=3) self.assert_called( 'POST', '/servers', { @@ -685,7 +684,7 @@ def test_boot_named_flavor(self): 'min_count': 1, 'max_count': 3, } - }, pos=5) + }, pos=4) def test_flavor_list(self): self.run_command('flavor-list') @@ -712,17 +711,15 @@ def test_flavor_show_by_name(self): self.run_command(['flavor-show', '128 MB Server']) self.assert_called('GET', '/flavors/128 MB Server', pos=0) self.assert_called('GET', '/flavors?is_public=None', pos=1) - self.assert_called('GET', '/flavors?is_public=None', pos=2) - self.assert_called('GET', '/flavors/aa1', pos=3) - self.assert_called('GET', '/flavors/aa1/os-extra_specs', pos=4) + self.assert_called('GET', '/flavors/aa1', pos=2) + self.assert_called('GET', '/flavors/aa1/os-extra_specs', pos=3) def test_flavor_show_by_name_priv(self): self.run_command(['flavor-show', '512 MB Server']) self.assert_called('GET', '/flavors/512 MB Server', pos=0) self.assert_called('GET', '/flavors?is_public=None', pos=1) - self.assert_called('GET', '/flavors?is_public=None', pos=2) - self.assert_called('GET', '/flavors/2', pos=3) - self.assert_called('GET', '/flavors/2/os-extra_specs', pos=4) + self.assert_called('GET', '/flavors/2', pos=2) + self.assert_called('GET', '/flavors/2/os-extra_specs', pos=3) def test_flavor_key_set(self): self.run_command('flavor-key 1 set k1=v1') @@ -2163,7 +2160,7 @@ def test_volume_delete_multiple(self): self.run_command('volume-delete Work Work2') self.assert_called('DELETE', '/volumes/15e59938-07d5-11e1-90e3-e3dffe0c5983', - pos=-5) + pos=-4) self.assert_called('DELETE', '/volumes/15e59938-07d5-11e1-90e3-ee32ba30feaa', pos=-1) diff --git a/novaclient/utils.py b/novaclient/utils.py index fbc5da604..289321c1b 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -208,18 +208,18 @@ def find_resource(manager, name_or_id, **find_args): pass try: - try: - return manager.find(human_id=name_or_id, **find_args) - except exceptions.NotFound: - pass - - # finally try to find entity by name try: resource = getattr(manager, 'resource_class', None) name_attr = resource.NAME_ATTR if resource else 'name' kwargs = {name_attr: name_or_id} kwargs.update(find_args) return manager.find(**kwargs) + except exceptions.NotFound: + pass + + # finally try to find entity by human_id + try: + return manager.find(human_id=name_or_id, **find_args) except exceptions.NotFound: msg = (_("No %(class)s with a name or ID of '%(name)s' exists.") % {'class': manager.resource_class.__name__.lower(), From aeb8c0c5e99157361d80750b0fae068e460c7a5f Mon Sep 17 00:00:00 2001 From: Sergey Nikitin Date: Tue, 27 Jan 2015 14:35:48 +0300 Subject: [PATCH 0683/1705] Moved set of asserts from post_servers_1234_action methods. Methods tests.v1_1.fakes.FakeHTTPClient.post_servers_1234_action and tests.fixture_data.servers.V1.post_servers_1234_action contain a set of same asserts. These asserts moved to method check_server_actions. This method is shared between post_servers_1234_action methods. Some almost equal asserts are grouped. Change-Id: I2643e77fe30400d462731bbc57351460fb28dc92 --- novaclient/tests/unit/fixture_data/servers.py | 84 ++---------- novaclient/tests/unit/v2/fakes.py | 127 +++++++----------- 2 files changed, 59 insertions(+), 152 deletions(-) diff --git a/novaclient/tests/unit/fixture_data/servers.py b/novaclient/tests/unit/fixture_data/servers.py index 6e3dd46b7..3766f4290 100644 --- a/novaclient/tests/unit/fixture_data/servers.py +++ b/novaclient/tests/unit/fixture_data/servers.py @@ -14,6 +14,7 @@ from novaclient.tests.unit import fakes from novaclient.tests.unit.fixture_data import base +from novaclient.tests.unit.v2 import fakes as v2_fakes class Base(base.Fixture): @@ -382,43 +383,24 @@ def post_servers_1234_action(self, request, context): context.status_code = 202 assert len(body.keys()) == 1 action = list(body)[0] - if action == 'reboot': - assert list(body[action]) == ['type'] - assert body[action]['type'] in ['HARD', 'SOFT'] + + if v2_fakes.FakeHTTPClient.check_server_actions(body): + # NOTE(snikitin): No need to do any operations here. This 'pass' + # is needed to avoid AssertionError in the last 'else' statement + # if we found 'action' in method check_server_actions and + # raise AssertionError if we didn't find 'action' at all. + pass elif action == 'rebuild': body = body[action] adminPass = body.get('adminPass', 'randompassword') assert 'imageRef' in body _body = self.server_1234.copy() _body['adminPass'] = adminPass - elif action == 'resize': - keys = body[action].keys() - assert 'flavorRef' in keys elif action == 'confirmResize': assert body[action] is None # This one method returns a different response code context.status_code = 204 return None - elif action == 'revertResize': - assert body[action] is None - elif action == 'migrate': - assert body[action] is None - elif action == 'os-stop': - assert body[action] is None - elif action == 'os-start': - assert body[action] is None - elif action == 'forceDelete': - assert body[action] is None - elif action == 'restore': - assert body[action] is None - elif action == 'pause': - assert body[action] is None - elif action == 'unpause': - assert body[action] is None - elif action == 'lock': - assert body[action] is None - elif action == 'unlock': - assert body[action] is None elif action == 'rescue': if body[action]: keys = set(body[action].keys()) @@ -426,65 +408,15 @@ def post_servers_1234_action(self, request, context): else: assert body[action] is None _body = {'adminPass': 'RescuePassword'} - elif action == 'unrescue': - assert body[action] is None - elif action == 'resume': - assert body[action] is None - elif action == 'suspend': - assert body[action] is None - elif action == 'lock': - assert body[action] is None - elif action == 'unlock': - assert body[action] is None - elif action == 'shelve': - assert body[action] is None - elif action == 'shelveOffload': - assert body[action] is None - elif action == 'unshelve': - assert body[action] is None - elif action == 'addFixedIp': - assert list(body[action]) == ['networkId'] - elif action == 'removeFixedIp': - assert list(body[action]) == ['address'] - elif action == 'addFloatingIp': - assert (list(body[action]) == ['address'] or - sorted(list(body[action])) == ['address', - 'fixed_address']) - elif action == 'removeFloatingIp': - assert list(body[action]) == ['address'] elif action == 'createImage': assert set(body[action].keys()) == set(['name', 'metadata']) context.headers['location'] = "http://blah/images/456" - elif action == 'changePassword': - assert list(body[action]) == ['adminPass'] elif action == 'os-getConsoleOutput': assert list(body[action]) == ['length'] context.status_code = 202 return {'output': 'foo'} - elif action == 'os-getVNCConsole': - assert list(body[action]) == ['type'] - elif action == 'os-getSPICEConsole': - assert list(body[action]) == ['type'] - elif action == 'os-getRDPConsole': - assert list(body[action]) == ['type'] elif action == 'os-getSerialConsole': assert list(body[action]) == ['type'] - elif action == 'os-migrateLive': - assert set(body[action].keys()) == set(['host', - 'block_migration', - 'disk_over_commit']) - elif action == 'os-resetState': - assert list(body[action]) == ['state'] - elif action == 'resetNetwork': - assert body[action] is None - elif action == 'addSecurityGroup': - assert list(body[action]) == ['name'] - elif action == 'removeSecurityGroup': - assert list(body[action]) == ['name'] - elif action == 'createBackup': - assert set(body[action]) == set(['name', - 'backup_type', - 'rotation']) elif action == 'evacuate': keys = list(body[action]) if 'adminPass' in keys: diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 68322a176..5230acd15 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -542,48 +542,73 @@ def delete_servers_1234_os_server_password(self, **kw): # Server actions # + none_actions = ['revertResize', 'migrate', 'os-stop', 'os-start', + 'forceDelete', 'restore', 'pause', 'unpause', 'unlock', + 'unrescue', 'resume', 'suspend', 'lock', 'shelve', + 'shelveOffload', 'unshelve', 'resetNetwork'] + type_actions = ['os-getVNCConsole', 'os-getSPICEConsole', + 'os-getRDPConsole'] + + @classmethod + def check_server_actions(cls, body): + action = list(body)[0] + if action == 'reboot': + assert list(body[action]) == ['type'] + assert body[action]['type'] in ['HARD', 'SOFT'] + elif action == 'resize': + assert 'flavorRef' in body[action] + elif action in cls.none_actions: + assert body[action] is None + elif action == 'addFixedIp': + assert list(body[action]) == ['networkId'] + elif action in ['removeFixedIp', 'removeFloatingIp']: + assert list(body[action]) == ['address'] + elif action == 'addFloatingIp': + assert (list(body[action]) == ['address'] or + sorted(list(body[action])) == ['address', 'fixed_address']) + elif action == 'changePassword': + assert list(body[action]) == ['adminPass'] + elif action in cls.type_actions: + assert list(body[action]) == ['type'] + elif action == 'os-migrateLive': + assert set(body[action].keys()) == set(['host', 'block_migration', + 'disk_over_commit']) + elif action == 'os-resetState': + assert list(body[action]) == ['state'] + elif action == 'resetNetwork': + assert body[action] is None + elif action in ['addSecurityGroup', 'removeSecurityGroup']: + assert list(body[action]) == ['name'] + elif action == 'createBackup': + assert set(body[action]) == set(['name', 'backup_type', + 'rotation']) + else: + return False + return True + def post_servers_1234_action(self, body, **kw): _headers = None _body = None resp = 202 assert len(body.keys()) == 1 action = list(body)[0] - if action == 'reboot': - assert list(body[action]) == ['type'] - assert body[action]['type'] in ['HARD', 'SOFT'] + + if self.check_server_actions(body): + # NOTE(snikitin): No need to do any operations here. This 'pass' + # is needed to avoid AssertionError in the last 'else' statement + # if we found 'action' in method check_server_actions and + # raise AssertionError if we didn't find 'action' at all. + pass elif action == 'rebuild': body = body[action] adminPass = body.get('adminPass', 'randompassword') assert 'imageRef' in body _body = self.get_servers_1234()[2] _body['server']['adminPass'] = adminPass - elif action == 'resize': - keys = body[action].keys() - assert 'flavorRef' in keys elif action == 'confirmResize': assert body[action] is None # This one method returns a different response code return (204, {}, None) - elif action == 'revertResize': - assert body[action] is None - elif action == 'migrate': - assert body[action] is None - elif action == 'os-stop': - assert body[action] is None - elif action == 'os-start': - assert body[action] is None - elif action == 'forceDelete': - assert body[action] is None - elif action == 'restore': - assert body[action] is None - elif action == 'pause': - assert body[action] is None - elif action == 'unpause': - assert body[action] is None - elif action == 'lock': - assert body[action] is None - elif action == 'unlock': - assert body[action] is None elif action == 'rescue': if body[action]: keys = set(body[action].keys()) @@ -591,62 +616,12 @@ def post_servers_1234_action(self, body, **kw): else: assert body[action] is None _body = {'adminPass': 'RescuePassword'} - elif action == 'unrescue': - assert body[action] is None - elif action == 'resume': - assert body[action] is None - elif action == 'suspend': - assert body[action] is None - elif action == 'lock': - assert body[action] is None - elif action == 'unlock': - assert body[action] is None - elif action == 'shelve': - assert body[action] is None - elif action == 'shelveOffload': - assert body[action] is None - elif action == 'unshelve': - assert body[action] is None - elif action == 'addFixedIp': - assert list(body[action]) == ['networkId'] - elif action == 'removeFixedIp': - assert list(body[action]) == ['address'] - elif action == 'addFloatingIp': - assert (list(body[action]) == ['address'] or - sorted(list(body[action])) == ['address', - 'fixed_address']) - elif action == 'removeFloatingIp': - assert list(body[action]) == ['address'] elif action == 'createImage': assert set(body[action].keys()) == set(['name', 'metadata']) _headers = dict(location="http://blah/images/456") - elif action == 'changePassword': - assert list(body[action]) == ['adminPass'] elif action == 'os-getConsoleOutput': assert list(body[action]) == ['length'] return (202, {}, {'output': 'foo'}) - elif action == 'os-getVNCConsole': - assert list(body[action]) == ['type'] - elif action == 'os-getSPICEConsole': - assert list(body[action]) == ['type'] - elif action == 'os-getRDPConsole': - assert list(body[action]) == ['type'] - elif action == 'os-migrateLive': - assert set(body[action].keys()) == set(['host', - 'block_migration', - 'disk_over_commit']) - elif action == 'os-resetState': - assert list(body[action]) == ['state'] - elif action == 'resetNetwork': - assert body[action] is None - elif action == 'addSecurityGroup': - assert list(body[action]) == ['name'] - elif action == 'removeSecurityGroup': - assert list(body[action]) == ['name'] - elif action == 'createBackup': - assert set(body[action]) == set(['name', - 'backup_type', - 'rotation']) elif action == 'evacuate': keys = list(body[action]) if 'adminPass' in keys: From 9a6a7664c2709023390e49447545799d5c7ca805 Mon Sep 17 00:00:00 2001 From: Ikuo Kumagai Date: Fri, 21 Nov 2014 13:47:30 +0000 Subject: [PATCH 0684/1705] Change the unsuitable item caching for completion This fix changes the item that cached for completion from "human_id" to "name". A string for completion shoud not be changed in any way, because changed keyword can not use as command line parameter. But the "human_id" means "human readable id" that is changed from "name" by method "to_slug". "to_slug" is meant to create a valid path name from a string. The tools/nova.bash_completion take the completion string from the file ".novaclient/*/*-cache". The file is created when client call "list" command. For example , "nova list","nova image-list", and the others. Currently, items that are written to the cache file is the "id" and "human_id". Closes-Bug: #1193049 Change-Id: I241ec8b7c8729274ee43db6e360141fd381b265e --- novaclient/client.py | 4 ++-- novaclient/tests/unit/test_client.py | 29 ++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index 200d75c73..b2a59474f 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -90,9 +90,9 @@ class CompletionCache(object): ~/.novaclient/ / -id-cache - -human-id-cache + -name-cache """ - def __init__(self, username, auth_url, attributes=('id', 'human_id')): + def __init__(self, username, auth_url, attributes=('id', 'name')): self.directory = self._make_directory_name(username, auth_url) self.attributes = attributes diff --git a/novaclient/tests/unit/test_client.py b/novaclient/tests/unit/test_client.py index 2c7f7eaf7..b7628b956 100644 --- a/novaclient/tests/unit/test_client.py +++ b/novaclient/tests/unit/test_client.py @@ -16,6 +16,7 @@ import json import logging +import os import fixtures import mock @@ -366,3 +367,31 @@ def test_log_resp(self): self.assertIn('RESP BODY: {"access": {"token": {"id":' ' "{SHA1}4fc49c6a671ce889078ff6b250f7066cf6d2ada2"}}}', output) + + @mock.patch('novaclient.client.HTTPClient') + def test_completion_cache(self, instance): + cp_cache = novaclient.client.CompletionCache('user', + "server/v2") + + client = novaclient.v2.client.Client("user", + "password", "project_id", + auth_url="server/v2", + completion_cache=cp_cache) + + instance.id = "v1c49c6a671ce889078ff6b250f7066cf6d2ada2" + instance.name = "foo.bar.baz v1_1" + client.write_object_to_completion_cache(instance) + self.assertTrue(os.path.isdir(cp_cache.directory)) + + for file_name in os.listdir(cp_cache.directory): + file_path = os.path.join(cp_cache.directory, file_name) + f = open(file_path, 'r') + for line in f: + line = line.rstrip() + if '-id-' in file_name: + self.assertEqual(instance.id, line) + elif '-name-' in file_name: + self.assertEqual(instance.name, line) + f.close() + os.remove(file_path) + os.rmdir(cp_cache.directory) From 5b598ab4e1f5779b9e632854b3cf4918f452b547 Mon Sep 17 00:00:00 2001 From: zhangjl Date: Thu, 5 Feb 2015 19:56:13 +0800 Subject: [PATCH 0685/1705] Remove image to local block device mapping In the method named from_api in nova/block_device.py, if source_type == 'image' and destination_type == 'local': raise exception.InvalidBDMFormat( details=_("Mapping image to local is not supported.")) While, in the method named _boot in novaclient/v2/servers.py if image: bdm_dict = {'uuid': image.id, 'source_type': 'image', 'destination_type': 'local', 'boot_index': 0, 'delete_on_termination': True} block_device_mapping_v2.insert(0, bdm_dict) Because of the above, if using --image and --block-device params at the same time to create vm, nova would raise InvalidBDMFormat exception To resolve this problem, remove the codes in novaclient which conflict with nova Change-Id: I488322ba0160100a6d641fde68fa824d0581956a Closes-Bug:#1418484 --- novaclient/tests/unit/v2/test_shell.py | 7 ------- novaclient/v2/servers.py | 7 ------- 2 files changed, 14 deletions(-) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 49e1581aa..94ee9ac4e 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -292,13 +292,6 @@ def test_boot_image_bdms_v2(self): 'flavorRef': '1', 'name': 'some-server', 'block_device_mapping_v2': [ - { - 'uuid': 1, - 'source_type': 'image', - 'destination_type': 'local', - 'boot_index': 0, - 'delete_on_termination': True, - }, { 'uuid': 'fake-id', 'source_type': 'volume', diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index 36de45b7f..8055f1509 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -526,13 +526,6 @@ def _boot(self, resource_url, response_key, name, image, flavor, body['server']['block_device_mapping'] = \ self._parse_block_device_mapping(block_device_mapping) elif block_device_mapping_v2: - # Append the image to the list only if we have new style BDMs - if image: - bdm_dict = {'uuid': image.id, 'source_type': 'image', - 'destination_type': 'local', 'boot_index': 0, - 'delete_on_termination': True} - block_device_mapping_v2.insert(0, bdm_dict) - body['server']['block_device_mapping_v2'] = block_device_mapping_v2 if nics is not None: From edb42ed4dc0cb5f5d012f95b760e3d48cd4598eb Mon Sep 17 00:00:00 2001 From: melanie witt Date: Fri, 6 Feb 2015 08:15:02 +0000 Subject: [PATCH 0686/1705] Compare dicts for POST data in test_client_reauth I observed a transient unit test failure [1] where occasionally the ordering of the keys in the string result from json.dumps() didn't match the ordering in the call_args_list, causing the test to fail. This change adds a dict comparison of the POST data payload instead of comparing the json.dumps(data) strings. [1] http://logs.openstack.org/03/153203/3/check/ gate-python-novaclient-python34/f8a12bc/ Change-Id: I6b8bd4169b92870f657bf4e7f7fca02722749017 --- novaclient/tests/unit/test_client.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/novaclient/tests/unit/test_client.py b/novaclient/tests/unit/test_client.py index 2c7f7eaf7..149fae7c7 100644 --- a/novaclient/tests/unit/test_client.py +++ b/novaclient/tests/unit/test_client.py @@ -101,9 +101,11 @@ def test_client_reauth(self): timeout=mock.ANY, headers=reauth_headers, allow_redirects=mock.ANY, - data=json.dumps(data), + data=mock.ANY, verify=mock.ANY)] self.assertEqual(expected, mock_request.call_args_list) + token_post_call = mock_request.call_args_list[1] + self.assertEqual(data, json.loads(token_post_call[1]['data'])) @mock.patch.object(novaclient.client.HTTPClient, 'request', return_value=(200, "{'versions':[]}")) From 45000677ec6c6c3e9d22aa2f1650a3458a67883a Mon Sep 17 00:00:00 2001 From: Natsuki Maruyama Date: Sun, 30 Nov 2014 20:21:03 +0900 Subject: [PATCH 0687/1705] Add support for keypair-add command reading public key from stdin If given filename is `-` for `--pub-key` option, reads public key from stdin. $ cat id_rsa.pub | nova keypair-add --pub-key - keyname Change-Id: Ib6dfe5a7b08d588868a923defb9ddd15fc28e01f Closes-Bug: #1333476 --- novaclient/tests/unit/v2/test_shell.py | 8 ++++++++ novaclient/v2/shell.py | 18 +++++++++++------- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index ffa3587d7..624f41d69 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2304,6 +2304,14 @@ def test_keypair_import(self): 'POST', '/os-keypairs', { 'keypair': {'public_key': 'FAKE_PUBLIC_KEY', 'name': 'test'}}) + def test_keypair_stdin(self): + with mock.patch('sys.stdin', six.StringIO('FAKE_PUBLIC_KEY')): + self.run_command('keypair-add --pub-key - test') + self.assert_called( + 'POST', '/os-keypairs', { + 'keypair': + {'public_key': 'FAKE_PUBLIC_KEY', 'name': 'test'}}) + def test_keypair_list(self): self.run_command('keypair-list') self.assert_called('GET', '/os-keypairs') diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 190d07fe3..736f1f36e 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -2865,13 +2865,17 @@ def do_keypair_add(cs, args): pub_key = args.pub_key if pub_key: - try: - with open(os.path.expanduser(pub_key)) as f: - pub_key = f.read() - except IOError as e: - raise exceptions.CommandError(_("Can't open or read '%(key)s': " - "%(exc)s") % {'key': pub_key, - 'exc': e}) + if pub_key == '-': + pub_key = sys.stdin.read() + else: + try: + with open(os.path.expanduser(pub_key)) as f: + pub_key = f.read() + except IOError as e: + raise exceptions.CommandError( + _("Can't open or read '%(key)s': %(exc)s") + % {'key': pub_key, 'exc': e} + ) keypair = cs.keypairs.create(name, pub_key) From 2504f39ce3e13dffdb70481e7c9f59f1432caad9 Mon Sep 17 00:00:00 2001 From: Abhishek Talwar Date: Mon, 9 Feb 2015 17:40:07 +0530 Subject: [PATCH 0688/1705] Wrong help for nova usage command nova help usage commands provides help that the command can be used with either tenant name or tenant ID but using tenant name gives wrong output. So, updated the help for the command so that it is shown that only tenant ID can be used. Change-Id: I2ae1edd28d75fc7988fcd0da5b0fdd8081455b16 Closes-Bug: #1419726 --- novaclient/v2/shell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 190d07fe3..4c7f3cb30 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -3062,7 +3062,7 @@ def simplify_usage(u): '--tenant', metavar='', default=None, - help=_('UUID or name of tenant to get usage for.')) + help=_('UUID of tenant to get usage for.')) def do_usage(cs, args): """Show usage data for a single tenant.""" dateformat = "%Y-%m-%d" From f84c6531fd726a883fa8403f14b2c43e0c47f5c2 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Mon, 9 Feb 2015 16:12:44 +0200 Subject: [PATCH 0689/1705] Check 'auth_url' is presented while authentication If 'auth_url' is missed in HTTPClient, authenticate method failed with non-friendly error while trying to parse it. Change-Id: Iaec95527293f3e1a34eb7f9ffa81097ba48107b3 --- novaclient/client.py | 4 ++++ novaclient/tests/unit/test_http.py | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/novaclient/client.py b/novaclient/client.py index b2a59474f..847115261 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -586,6 +586,10 @@ def _fetch_endpoints_from_auth(self, url): extract_token=False) def authenticate(self): + if not self.auth_url: + msg = _("Authentication requires 'auth_url', which should be " + "specified in '%s'") % self.__class__.__name__ + raise exceptions.AuthorizationFailure(msg) magic_tuple = netutils.urlsplit(self.auth_url) scheme, netloc, path, query, frag = magic_tuple port = magic_tuple.port diff --git a/novaclient/tests/unit/test_http.py b/novaclient/tests/unit/test_http.py index 671ab09da..1cebe1f07 100644 --- a/novaclient/tests/unit/test_http.py +++ b/novaclient/tests/unit/test_http.py @@ -141,6 +141,11 @@ def test_auth_call(): test_auth_call() + def test_auth_failure_due_to_miss_of_auth_url(self): + cl = client.HTTPClient("username", "password") + + self.assertRaises(exceptions.AuthorizationFailure, cl.authenticate) + def test_connection_refused(self): cl = get_client() From 0333c3f102126f595ded1928f8b54c2aa2d8d56a Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Mon, 9 Feb 2015 22:47:05 +0000 Subject: [PATCH 0690/1705] Updated from global requirements Change-Id: I857b36fc4253a2244e4099c89834b54e6c745c24 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index f15d3beac..051ced393 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -14,4 +14,4 @@ oslosphinx>=2.2.0 # Apache-2.0 testrepository>=0.0.18 testscenarios>=0.4 testtools>=0.9.36,!=1.2.0 -tempest-lib>=0.1.0 +tempest-lib>=0.2.0 From a0481e1c8008935d34e8f6e2cf896723b5e36f0a Mon Sep 17 00:00:00 2001 From: Ritesh Paiboina Date: Mon, 9 Feb 2015 07:32:36 +0100 Subject: [PATCH 0691/1705] Add all tenants search opt to del instnce by name Nova delete command deletes an instance by name or ID. Nova delete command is able to delete an instance within the same the tenant by name or ID. When admin credentials are sourced and try to delete a non admin tenant instances, nova delete command is able to delete an instance by ID only, it is not able to delete an instance by name. Nova delete command deletes an instances by id using following api call /v2/{tenant_id}/servers/{server_id} But to delete an instance by name, nova delete command first find the resources by name using following api call /servers?name={server_name} This api call is not able to retrive the list of other tenant instances. Adding all tenants parameter to this api call will retrive the list of other tenant instances. The following will be new api call /servers?all_tenants=1&name={server_name} Closes-Bug: #1247030 Change-Id: I03e578d58214c835d9a411752bd618d77ced37ff --- novaclient/base.py | 4 ++++ novaclient/tests/unit/v2/test_shell.py | 7 +++++-- novaclient/v2/shell.py | 7 ++++--- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/novaclient/base.py b/novaclient/base.py index 7f54d9363..731d220ee 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -179,6 +179,10 @@ def findall(self, **kwargs): list_kwargs['search_opts'] = {"name": kwargs["name"]} elif "display_name" in kwargs: list_kwargs['search_opts'] = {"name": kwargs["display_name"]} + if "all_tenants" in kwargs: + all_tenants = kwargs['all_tenants'] + list_kwargs['search_opts']['all_tenants'] = all_tenants + searches = [(k, v) for k, v in searches if k != 'all_tenants'] listing = self.list(**list_kwargs) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index ffa3587d7..fb8dac8f8 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -1111,10 +1111,13 @@ def test_delete_two_with_two_existent(self): self.assert_called('DELETE', '/servers/1234', pos=-3) self.assert_called('DELETE', '/servers/5678', pos=-1) self.run_command('delete sample-server sample-server2') - self.assert_called('GET', '/servers?name=sample-server', pos=-6) + self.assert_called('GET', + '/servers?all_tenants=1&name=sample-server', pos=-6) self.assert_called('GET', '/servers/1234', pos=-5) self.assert_called('DELETE', '/servers/1234', pos=-4) - self.assert_called('GET', '/servers?name=sample-server2', pos=-3) + self.assert_called('GET', + '/servers?all_tenants=1&name=sample-server2', + pos=-3) self.assert_called('GET', '/servers/5678', pos=-2) self.assert_called('DELETE', '/servers/5678', pos=-1) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 190d07fe3..29f266934 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -1876,16 +1876,17 @@ def do_show(cs, args): help=_('Name or ID of server(s).')) def do_delete(cs, args): """Immediately shut down and delete specified server(s).""" + find_args = {'all_tenants': '1'} utils.do_action_on_many( - lambda s: _find_server(cs, s).delete(), + lambda s: _find_server(cs, s, **find_args).delete(), args.server, _("Request to delete server %s has been accepted."), _("Unable to delete the specified server(s).")) -def _find_server(cs, server): +def _find_server(cs, server, **find_args): """Get a server by name or ID.""" - return utils.find_resource(cs.servers, server) + return utils.find_resource(cs.servers, server, **find_args) def _find_image(cs, image): From 27cd393028a103d8d52cf25f035e3a2985572ccb Mon Sep 17 00:00:00 2001 From: Joe Gordon Date: Wed, 28 Jan 2015 16:04:26 -0800 Subject: [PATCH 0692/1705] Copy functional tests from tempest cli read only Copy tests in from tempest/cli/simple_read_only/compute/test_nova.py in preparation for removing that file. Change-Id: I5a34cf1a39d4f73f94a27cce365434bd4a0eea0b --- .../tests/functional/test_readonly_nova.py | 136 ++++++++++++++++-- test-requirements.txt | 2 +- 2 files changed, 129 insertions(+), 9 deletions(-) diff --git a/novaclient/tests/functional/test_readonly_nova.py b/novaclient/tests/functional/test_readonly_nova.py index bc0589780..2ea4ec986 100644 --- a/novaclient/tests/functional/test_readonly_nova.py +++ b/novaclient/tests/functional/test_readonly_nova.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +from tempest_lib import decorators from tempest_lib import exceptions from novaclient.tests.functional import base @@ -18,15 +19,9 @@ class SimpleReadOnlyNovaClientTest(base.ClientTestBase): """ - This is a first pass at a simple read only python-novaclient test. This - only exercises client commands that are read only. - - This should test commands: - * as a regular user - * as a admin user - * with and without optional parameters - * initially just check return codes, and later test command outputs + read only functional python-novaclient tests. + This only exercises client commands that are read only. """ def test_admin_fake_action(self): @@ -36,6 +31,131 @@ def test_admin_fake_action(self): # NOTE(jogo): Commands in order listed in 'nova help' + def test_admin_absolute_limites(self): + self.nova('absolute-limits') + self.nova('absolute-limits', params='--reserved') + + def test_admin_aggregate_list(self): + self.nova('aggregate-list') + + def test_admin_availability_zone_list(self): + self.assertIn("internal", self.nova('availability-zone-list')) + + def test_admin_cloudpipe_list(self): + self.nova('cloudpipe-list') + + def test_admin_credentials(self): + self.nova('credentials') + + # "Neutron does not provide this feature" + def test_admin_dns_domains(self): + self.nova('dns-domains') + + @decorators.skip_because(bug="1157349") + def test_admin_dns_list(self): + self.nova('dns-list') + + def test_admin_endpoints(self): + self.nova('endpoints') + + def test_admin_flavor_acces_list(self): + self.assertRaises(exceptions.CommandFailed, + self.nova, + 'flavor-access-list') + # Failed to get access list for public flavor type + self.assertRaises(exceptions.CommandFailed, + self.nova, + 'flavor-access-list', + params='--flavor m1.tiny') + + def test_admin_flavor_list(self): + self.assertIn("Memory_MB", self.nova('flavor-list')) + + def test_admin_floating_ip_bulk_list(self): + self.nova('floating-ip-bulk-list') + + def test_admin_floating_ip_list(self): + self.nova('floating-ip-list') + + def test_admin_floating_ip_pool_list(self): + self.nova('floating-ip-pool-list') + + def test_admin_host_list(self): + self.nova('host-list') + + def test_admin_hypervisor_list(self): + self.nova('hypervisor-list') + + def test_admin_image_list(self): + self.nova('image-list') + + @decorators.skip_because(bug="1157349") + def test_admin_interface_list(self): + self.nova('interface-list') + + def test_admin_keypair_list(self): + self.nova('keypair-list') + + def test_admin_list(self): + self.nova('list') + self.nova('list', params='--all-tenants 1') + self.nova('list', params='--all-tenants 0') + self.assertRaises(exceptions.CommandFailed, + self.nova, + 'list', + params='--all-tenants bad') + + def test_admin_network_list(self): + self.nova('network-list') + + def test_admin_rate_limits(self): + self.nova('rate-limits') + + def test_admin_secgroup_list(self): + self.nova('secgroup-list') + + @decorators.skip_because(bug="1157349") + def test_admin_secgroup_list_rules(self): + self.nova('secgroup-list-rules') + + def test_admin_server_group_list(self): + self.nova('server-group-list') + + def test_admin_servce_list(self): + self.nova('service-list') + + def test_admin_usage(self): + self.nova('usage') + + def test_admin_usage_list(self): + self.nova('usage-list') + + def test_admin_volume_list(self): + self.nova('volume-list') + + def test_admin_volume_snapshot_list(self): + self.nova('volume-snapshot-list') + + def test_admin_volume_type_list(self): + self.nova('volume-type-list') + + def test_admin_help(self): + self.nova('help') + + def test_admin_list_extensions(self): + self.nova('list-extensions') + + def test_admin_net_list(self): + self.nova('net-list') + + def test_agent_list(self): + self.nova('agent-list') + self.nova('agent-list', flags='--debug') + + def test_migration_list(self): + self.nova('migration-list') + self.nova('migration-list', flags='--debug') + # Optional arguments: def test_admin_version(self): diff --git a/test-requirements.txt b/test-requirements.txt index f15d3beac..051ced393 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -14,4 +14,4 @@ oslosphinx>=2.2.0 # Apache-2.0 testrepository>=0.0.18 testscenarios>=0.4 testtools>=0.9.36,!=1.2.0 -tempest-lib>=0.1.0 +tempest-lib>=0.2.0 From 92a72a0e637f9a539a825f129033a95a94c1fa71 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 13 Feb 2015 02:00:26 +0000 Subject: [PATCH 0693/1705] Updated from global requirements Change-Id: Ica3a241ba6fea3bf2fbf1c6e7cd069d13dc7d84c --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b9b4b05df..6b3a459c4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,4 +12,4 @@ requests>=2.2.0,!=2.4.0 simplejson>=2.2.0 six>=1.7.0 Babel>=1.3 -python-keystoneclient>=1.0.0 +python-keystoneclient>=1.1.0 From dfc752d725fac9580a01452334aeb610e34198ad Mon Sep 17 00:00:00 2001 From: Haiwei Xu Date: Mon, 19 Jan 2015 10:45:35 +0900 Subject: [PATCH 0694/1705] Change commands name from net-* to tenant-network-* Currently the commands of os-tenant-network API use net-* which may confuse users sometime. This patch changes commands to tenant-network-*, and marks net-* commands as DEPRECATED. Closes-Bug: #1152862 Change-Id: I8c3a0be08763a6f626d7fc7cf84811ac61ccc526 --- novaclient/tests/unit/v2/fakes.py | 17 +++++++++ novaclient/tests/unit/v2/test_shell.py | 17 +++++++++ novaclient/v2/contrib/tenant_networks.py | 46 +++++++++++++++++++++--- 3 files changed, 76 insertions(+), 4 deletions(-) diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 68322a176..c36713eeb 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -1867,6 +1867,23 @@ def post_os_networks_networktest_action(self, **kw): def post_os_networks_2_action(self, **kw): return (202, {}, None) + def get_os_tenant_networks(self, **kw): + return (200, {}, {'networks': [{"label": "1", "cidr": "10.0.0.0/24", + 'project_id': + '4ffc664c198e435e9853f2538fbcd7a7', + 'id': '1', 'vlan': '1234'}]}) + + def get_os_tenant_networks_1(self, **kw): + return (200, {}, {'network': {"label": "1", "cidr": "10.0.0.0/24", + "id": "1"}}) + + def post_os_tenant_networks(self, **kw): + return (202, {}, {'network': {"label": "new_network1", + "cidr1": "10.0.1.0/24"}}) + + def delete_os_tenant_networks_1(self, **kw): + return (202, {}, None) + def get_os_availability_zone(self, **kw): return (200, {}, { "availabilityZoneInfo": [ diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 4ebea96fb..71eb85ee8 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -1942,6 +1942,23 @@ def test_network_delete(self): self.run_command('network-delete 1') self.assert_called('DELETE', '/os-networks/1') + def test_tenant_network_list(self): + self.run_command('tenant-network-list') + self.assert_called('GET', '/os-tenant-networks') + + def test_tenant_network_show(self): + self.run_command('tenant-network-show 1') + self.assert_called('GET', '/os-tenant-networks/1') + + def test_tenant_network_create(self): + self.run_command('tenant-network-create new_network 10.0.1.0/24') + body = {'network': {'cidr': '10.0.1.0/24', 'label': 'new_network'}} + self.assert_called('POST', '/os-tenant-networks', body) + + def test_tenant_network_delete(self): + self.run_command('tenant-network-delete 1') + self.assert_called('DELETE', '/os-tenant-networks/1') + def test_add_fixed_ip(self): self.run_command('add-fixed-ip sample-server 1') self.assert_called('POST', '/servers/1234/action', diff --git a/novaclient/v2/contrib/tenant_networks.py b/novaclient/v2/contrib/tenant_networks.py index 06edeb145..0b56ae8cc 100644 --- a/novaclient/v2/contrib/tenant_networks.py +++ b/novaclient/v2/contrib/tenant_networks.py @@ -44,7 +44,15 @@ def create(self, label, cidr): @cliutils.arg('network_id', metavar='', help='ID of network') def do_net(cs, args): """ - Show a network + DEPRECATED, Use tenant-network-show instead. + """ + do_tenant_network_show(cs, args) + + +@cliutils.arg('network_id', metavar='', help='ID of network') +def do_tenant_network_show(cs, args): + """ + Show a tenant network. """ network = cs.tenant_networks.get(args.network_id) utils.print_dict(network._info) @@ -52,7 +60,14 @@ def do_net(cs, args): def do_net_list(cs, args): """ - List networks + DEPRECATED, use tenant-network-list instead. + """ + do_tenant_network_list(cs, args) + + +def do_tenant_network_list(cs, args): + """ + List tenant networks. """ networks = cs.tenant_networks.list() utils.print_list(networks, ['ID', 'Label', 'CIDR']) @@ -68,7 +83,22 @@ def do_net_list(cs, args): help=_('IP block to allocate from (ex. 172.16.0.0/24 or 2001:DB8::/64)')) def do_net_create(cs, args): """ - Create a network + DEPRECATED, use tenant-network-create instead. + """ + do_tenant_network_create(cs, args) + + +@cliutils.arg( + 'label', + metavar='', + help=_('Network label (ex. my_new_network)')) +@cliutils.arg( + 'cidr', + metavar='', + help=_('IP block to allocate from (ex. 172.16.0.0/24 or 2001:DB8::/64)')) +def do_tenant_network_create(cs, args): + """ + Create a tenant network. """ network = cs.tenant_networks.create(args.label, args.cidr) utils.print_dict(network._info) @@ -77,6 +107,14 @@ def do_net_create(cs, args): @cliutils.arg('network_id', metavar='', help='ID of network') def do_net_delete(cs, args): """ - Delete a network + DEPRECATED, use tenant-network-delete instead. + """ + do_tenant_network_delete(cs, args) + + +@cliutils.arg('network_id', metavar='', help='ID of network') +def do_tenant_network_delete(cs, args): + """ + Delete a tenant network. """ cs.tenant_networks.delete(args.network_id) From ac6636a54d72ba76d0adca76e07d1d26d9ea35c3 Mon Sep 17 00:00:00 2001 From: Andrew Laski Date: Thu, 19 Feb 2015 18:24:51 -0500 Subject: [PATCH 0695/1705] Revert "Overhaul bash-completion to support non-UUID based IDs" This reverts commit 4c8cefb98a425382204df2f38f24e6b5b71520dd. The cache completion was causing a bug where 'nova volume-attach' would then try to query Nova for volume information using a URL that was not valid. This caused it to appear that the attach had failed. Additionally the idea of making extra API queries for a bash completion cache should be thought through more. Conflicts: novaclient/client.py novaclient/shell.py novaclient/v2/client.py novaclient/v3/client.py Closes-Bug: #1423695 Change-Id: I6676a11f9b5318a1cda2d03a5d4550bda549d5c2 --- novaclient/base.py | 97 +++++++++++++++++++++------- novaclient/client.py | 75 --------------------- novaclient/shell.py | 5 +- novaclient/tests/unit/test_client.py | 29 --------- novaclient/v2/client.py | 12 +--- 5 files changed, 77 insertions(+), 141 deletions(-) diff --git a/novaclient/base.py b/novaclient/base.py index 731d220ee..8cee35ae2 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -20,12 +20,17 @@ """ import abc +import contextlib +import hashlib import inspect +import os +import threading import six from novaclient import exceptions from novaclient.openstack.common.apiclient import base +from novaclient.openstack.common import cliutils Resource = base.Resource @@ -47,18 +52,11 @@ class Manager(base.HookableMixin): etc.) and provide CRUD operations for them. """ resource_class = None + cache_lock = threading.RLock() def __init__(self, api): self.api = api - def _write_object_to_completion_cache(self, obj): - if hasattr(self.api, 'write_object_to_completion_cache'): - self.api.write_object_to_completion_cache(obj) - - def _clear_completion_cache_for_class(self, obj_class): - if hasattr(self.api, 'clear_completion_cache_for_class'): - self.api.clear_completion_cache_for_class(obj_class) - def _list(self, url, response_key, obj_class=None, body=None): if body: _resp, body = self.api.client.post(url, body=body) @@ -77,22 +75,77 @@ def _list(self, url, response_key, obj_class=None, body=None): except KeyError: pass - self._clear_completion_cache_for_class(obj_class) + with self.completion_cache('human_id', obj_class, mode="w"): + with self.completion_cache('uuid', obj_class, mode="w"): + return [obj_class(self, res, loaded=True) + for res in data if res] + + @contextlib.contextmanager + def completion_cache(self, cache_type, obj_class, mode): + """ + The completion cache store items that can be used for bash + autocompletion, like UUIDs or human-friendly IDs. + + A resource listing will clear and repopulate the cache. - objs = [] - for res in data: - if res: - obj = obj_class(self, res, loaded=True) - self._write_object_to_completion_cache(obj) - objs.append(obj) + A resource create will append to the cache. - return objs + Delete is not handled because listings are assumed to be performed + often enough to keep the cache reasonably up-to-date. + """ + # NOTE(wryan): This lock protects read and write access to the + # completion caches + with self.cache_lock: + base_dir = cliutils.env('NOVACLIENT_UUID_CACHE_DIR', + default="~/.novaclient") + + # NOTE(sirp): Keep separate UUID caches for each username + + # endpoint pair + username = cliutils.env('OS_USERNAME', 'NOVA_USERNAME') + url = cliutils.env('OS_URL', 'NOVA_URL') + uniqifier = hashlib.md5(username.encode('utf-8') + + url.encode('utf-8')).hexdigest() + + cache_dir = os.path.expanduser(os.path.join(base_dir, uniqifier)) + + try: + os.makedirs(cache_dir, 0o755) + except OSError: + # NOTE(kiall): This is typically either permission denied while + # attempting to create the directory, or the + # directory already exists. Either way, don't + # fail. + pass + + resource = obj_class.__name__.lower() + filename = "%s-%s-cache" % (resource, cache_type.replace('_', '-')) + path = os.path.join(cache_dir, filename) + + cache_attr = "_%s_cache" % cache_type + + try: + setattr(self, cache_attr, open(path, mode)) + except IOError: + # NOTE(kiall): This is typically a permission denied while + # attempting to write the cache file. + pass + + try: + yield + finally: + cache = getattr(self, cache_attr, None) + if cache: + cache.close() + delattr(self, cache_attr) + + def write_to_completion_cache(self, cache_type, val): + cache = getattr(self, "_%s_cache" % cache_type, None) + if cache: + cache.write("%s\n" % val) def _get(self, url, response_key): _resp, body = self.api.client.get(url) - obj = self.resource_class(self, body[response_key], loaded=True) - self._write_object_to_completion_cache(obj) - return obj + return self.resource_class(self, body[response_key], loaded=True) def _create(self, url, body, response_key, return_raw=False, **kwargs): self.run_hooks('modify_body_for_create', body, **kwargs) @@ -100,9 +153,9 @@ def _create(self, url, body, response_key, return_raw=False, **kwargs): if return_raw: return body[response_key] - obj = self.resource_class(self, body[response_key]) - self._write_object_to_completion_cache(obj) - return obj + with self.completion_cache('human_id', self.resource_class, mode="a"): + with self.completion_cache('uuid', self.resource_class, mode="a"): + return self.resource_class(self, body[response_key]) def _delete(self, url): _resp, _body = self.api.client.delete(url) diff --git a/novaclient/client.py b/novaclient/client.py index 847115261..9a4fcde53 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -21,12 +21,9 @@ """ import copy -import errno import functools -import glob import hashlib import logging -import os import re import socket import time @@ -46,7 +43,6 @@ from novaclient import exceptions from novaclient.i18n import _ -from novaclient.openstack.common import cliutils from novaclient import service_catalog @@ -76,77 +72,6 @@ def get(self, url): return self._adapters[url] -class CompletionCache(object): - """The completion cache is how we support tab-completion with novaclient. - - The `Manager` writes object IDs and Human-IDs to the completion-cache on - object-show, object-list, and object-create calls. - - The `nova.bash_completion` script then uses these files to provide the - actual tab-completion. - - The cache directory layout is: - - ~/.novaclient/ - / - -id-cache - -name-cache - """ - def __init__(self, username, auth_url, attributes=('id', 'name')): - self.directory = self._make_directory_name(username, auth_url) - self.attributes = attributes - - def _make_directory_name(self, username, auth_url): - """Creates a unique directory name based on the auth_url and username - of the current user. - """ - uniqifier = hashlib.md5(username.encode('utf-8') + - auth_url.encode('utf-8')).hexdigest() - base_dir = cliutils.env('NOVACLIENT_UUID_CACHE_DIR', - default="~/.novaclient") - return os.path.expanduser(os.path.join(base_dir, uniqifier)) - - def _prepare_directory(self): - try: - os.makedirs(self.directory, 0o755) - except OSError: - # NOTE(kiall): This is typically either permission denied while - # attempting to create the directory, or the - # directory already exists. Either way, don't - # fail. - pass - - def clear_class(self, obj_class): - self._prepare_directory() - - resource = obj_class.__name__.lower() - resource_glob = os.path.join(self.directory, "%s-*-cache" % resource) - - for filename in glob.iglob(resource_glob): - try: - os.unlink(filename) - except OSError as e: - if e.errno != errno.ENOENT: - raise - - def _write_attribute(self, resource, attribute, value): - self._prepare_directory() - - filename = "%s-%s-cache" % (resource, attribute.replace('_', '-')) - path = os.path.join(self.directory, filename) - - with open(path, 'a') as f: - f.write("%s\n" % value) - - def write_object(self, obj): - resource = obj.__class__.__name__.lower() - - for attribute in self.attributes: - value = getattr(obj, attribute, None) - if value: - self._write_attribute(resource, attribute, value) - - class SessionClient(adapter.LegacyJsonAdapter): def __init__(self, *args, **kwargs): diff --git a/novaclient/shell.py b/novaclient/shell.py index 940a01e08..42376de55 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -748,8 +748,6 @@ def main(self, argv): _("You must provide an auth url " "via either --os-auth-url or env[OS_AUTH_URL]")) - completion_cache = client.CompletionCache(os_username, os_auth_url) - self.cs = client.Client( options.os_compute_api_version, os_username, os_password, os_tenant_name, @@ -763,8 +761,7 @@ def main(self, argv): timings=args.timings, bypass_url=bypass_url, os_cache=os_cache, http_log_debug=options.debug, cacert=cacert, timeout=timeout, - session=keystone_session, auth=keystone_auth, - completion_cache=completion_cache) + session=keystone_session, auth=keystone_auth) # Now check for the password/token of which pieces of the # identifying keyring key can come from the underlying client diff --git a/novaclient/tests/unit/test_client.py b/novaclient/tests/unit/test_client.py index 3ce5dfc00..149fae7c7 100644 --- a/novaclient/tests/unit/test_client.py +++ b/novaclient/tests/unit/test_client.py @@ -16,7 +16,6 @@ import json import logging -import os import fixtures import mock @@ -369,31 +368,3 @@ def test_log_resp(self): self.assertIn('RESP BODY: {"access": {"token": {"id":' ' "{SHA1}4fc49c6a671ce889078ff6b250f7066cf6d2ada2"}}}', output) - - @mock.patch('novaclient.client.HTTPClient') - def test_completion_cache(self, instance): - cp_cache = novaclient.client.CompletionCache('user', - "server/v2") - - client = novaclient.v2.client.Client("user", - "password", "project_id", - auth_url="server/v2", - completion_cache=cp_cache) - - instance.id = "v1c49c6a671ce889078ff6b250f7066cf6d2ada2" - instance.name = "foo.bar.baz v1_1" - client.write_object_to_completion_cache(instance) - self.assertTrue(os.path.isdir(cp_cache.directory)) - - for file_name in os.listdir(cp_cache.directory): - file_path = os.path.join(cp_cache.directory, file_name) - f = open(file_path, 'r') - for line in f: - line = line.rstrip() - if '-id-' in file_name: - self.assertEqual(instance.id, line) - elif '-name-' in file_name: - self.assertEqual(instance.name, line) - f.close() - os.remove(file_path) - os.rmdir(cp_cache.directory) diff --git a/novaclient/v2/client.py b/novaclient/v2/client.py index 386c649f9..e087ac68c 100644 --- a/novaclient/v2/client.py +++ b/novaclient/v2/client.py @@ -103,7 +103,7 @@ def __init__(self, username=None, api_key=None, project_id=None, auth_system='keystone', auth_plugin=None, auth_token=None, cacert=None, tenant_id=None, user_id=None, connection_pool=False, session=None, auth=None, - completion_cache=None, **kwargs): + **kwargs): # FIXME(comstud): Rename the api_key argument above when we # know it's not being used as keyword argument @@ -196,16 +196,6 @@ def __init__(self, username=None, api_key=None, project_id=None, auth=auth, **kwargs) - self.completion_cache = completion_cache - - def write_object_to_completion_cache(self, obj): - if self.completion_cache: - self.completion_cache.write_object(obj) - - def clear_completion_cache_for_class(self, obj_class): - if self.completion_cache: - self.completion_cache.clear_class(obj_class) - @client._original_only def __enter__(self): self.client.open_session() From be41ae238d05a2c3ade3822d869d2d199444c52a Mon Sep 17 00:00:00 2001 From: Sean Dague Date: Fri, 20 Feb 2015 16:23:24 -0500 Subject: [PATCH 0696/1705] add functional test for nova volume-attach bug This is a functional test that boots a server via the cli, creates a volume, and tries to attach it via the cli (which causes a failure due to completion cache code). Note: the failure actually happens *after* the attach command is dispatched, so the volume attach will still work, the user is presented an error though. Many TODOs remain for future patches. The test also tries to document what was learned about the CLI redirection to cinder API, which was introduced when Cinder was split out, but was tribal knowledge that was lost in the mists of time. Related-Bug: #1423695 Change-Id: Iaf474298be135843bff0114cf211bee19762f3ad --- novaclient/tests/functional/test_instances.py | 155 ++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 novaclient/tests/functional/test_instances.py diff --git a/novaclient/tests/functional/test_instances.py b/novaclient/tests/functional/test_instances.py new file mode 100644 index 000000000..c963a6b71 --- /dev/null +++ b/novaclient/tests/functional/test_instances.py @@ -0,0 +1,155 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import os +import time +import uuid + +import novaclient.client +from novaclient.tests.functional import base + + +# TODO(sdague): content that probably should be in utils, also throw +# Exceptions when they fail. +def pick_flavor(flavors): + """Given a flavor list pick a reasonable one.""" + for flavor in flavors: + if flavor.name == 'm1.tiny': + return flavor + + for flavor in flavors: + if flavor.name == 'm1.small': + return flavor + + +def pick_image(images): + for image in images: + if image.name.startswith('cirros') and image.name.endswith('-uec'): + return image + + +def volume_id_from_cli_create(output): + """Scrape the volume id out of the 'volume create' command + + The cli for Nova automatically routes requests to the volumes + service end point. However the nova api low level commands don't + redirect to the correct service endpoint, so for volumes commands + (even setup ones) we use the cli for magic routing. + + This function lets us get the id out of the prettytable that's + dumped on the cli during create. + + """ + for line in output.split("\n"): + fields = line.split() + if len(fields) > 4: + if fields[1] == "id": + return fields[3] + + +def volume_at_status(output, volume_id, status): + for line in output.split("\n"): + fields = line.split() + if len(fields) > 4: + if fields[1] == volume_id: + return fields[3] == status + raise Exception("Volume %s did not reach status '%s' in output: %s" + % (volume_id, status, output)) + + +class TestInstanceCLI(base.ClientTestBase): + def setUp(self): + super(TestInstanceCLI, self).setUp() + # TODO(sdague): while we collect this information in + # tempest-lib, we do it in a way that's not available for top + # level tests. Long term this probably needs to be in the base + # class. + user = os.environ['OS_USERNAME'] + passwd = os.environ['OS_PASSWORD'] + tenant = os.environ['OS_TENANT_NAME'] + auth_url = os.environ['OS_AUTH_URL'] + + # TODO(sdague): we made a lot of fun of the glanceclient team + # for version as int in first parameter. I guess we know where + # they copied it from. + self.client = novaclient.client.Client( + 2, user, passwd, tenant, + auth_url=auth_url) + + # pick some reasonable flavor / image combo + self.flavor = pick_flavor(self.client.flavors.list()) + self.image = pick_image(self.client.images.list()) + + def test_attach_volume(self): + """Test we can attach a volume via the cli. + + This test was added after bug 1423695. That bug exposed + inconsistencies in how to talk to API services from the CLI + vs. API level. The volumes api calls that were designed to + populate the completion cache were incorrectly routed to the + Nova endpoint. Novaclient volumes support actually talks to + Cinder endpoint directly. + + This would case volume-attach to return a bad error code, + however it does this *after* the attach command is correctly + dispatched. So the volume-attach still works, but the user is + presented a 404 error. + + This test ensures we can do a through path test of: boot, + create volume, attach volume, detach volume, delete volume, + destroy. + + """ + # TODO(sdague): better random name + name = str(uuid.uuid4()) + + # Boot via the cli, as we're primarily testing the cli in this test + self.nova('boot', params="--flavor %s --image %s %s --poll" % + (self.flavor.name, self.image.name, name)) + + # Be nice about cleaning up, however, use the API for this to avoid + # parsing text. + servers = self.client.servers.list(search_opts={"name": name}) + # the name is a random uuid, there better only be one + self.assertEqual(1, len(servers), servers) + server = servers[0] + self.addCleanup(server.delete) + + # create a volume for attachment. We use the CLI because it + # magic routes to cinder, however the low level API does not. + volume_id = volume_id_from_cli_create( + self.nova('volume-create', params="1")) + self.addCleanup(self.nova, 'volume-delete', params=volume_id) + + # allow volume to become available + for x in xrange(60): + volumes = self.nova('volume-list') + if volume_at_status(volumes, volume_id, 'available'): + break + time.sleep(1) + else: + self.fail("Volume %s not available after 60s" % volume_id) + + # attach the volume + self.nova('volume-attach', params="%s %s" % (name, volume_id)) + + # volume needs to transition to 'in-use' to be attached + for x in xrange(60): + volumes = self.nova('volume-list') + if volume_at_status(volumes, volume_id, 'in-use'): + break + time.sleep(1) + else: + self.fail("Volume %s not attached after 60s" % volume_id) + + # clean up on success + self.nova('volume-detach', params="%s %s" % (name, volume_id)) From 9a06348f4740a96270955e914c0450c263c932e2 Mon Sep 17 00:00:00 2001 From: Sean Dague Date: Mon, 23 Feb 2015 08:20:44 -0500 Subject: [PATCH 0697/1705] add pretty_tox to nova functional tests debugging tests is a lot easier when you can actually inject stderr directly through even on successes. Add the pretty tox facility from nova / tempest-lib into python-nova client as well for functional tests. Change-Id: I5c1f8244a5c743b590b74a8eb3eaf4a699555644 --- tools/pretty_tox.sh | 16 ++++++++++++++++ tox.ini | 5 ++++- 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100755 tools/pretty_tox.sh diff --git a/tools/pretty_tox.sh b/tools/pretty_tox.sh new file mode 100755 index 000000000..799ac1848 --- /dev/null +++ b/tools/pretty_tox.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +set -o pipefail + +TESTRARGS=$1 + +# --until-failure is not compatible with --subunit see: +# +# https://bugs.launchpad.net/testrepository/+bug/1411804 +# +# this work around exists until that is addressed +if [[ "$TESTARGS" =~ "until-failure" ]]; then + python setup.py testr --slowest --testr-args="$TESTRARGS" +else + python setup.py testr --slowest --testr-args="--subunit $TESTRARGS" | subunit-trace -f +fi diff --git a/tox.ini b/tox.ini index 0ea5da95b..11492c8ae 100644 --- a/tox.ini +++ b/tox.ini @@ -8,6 +8,7 @@ skipsdist = True usedevelop = True # tox is silly... these need to be separated by a newline.... whitelist_externals = find + bash install_command = pip install -U {opts} {packages} setenv = VIRTUAL_ENV={envdir} @@ -15,7 +16,9 @@ deps = -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt commands = find . -type f -name "*.pyc" -delete - python setup.py testr --testr-args='{posargs}' + bash tools/pretty_tox.sh '{posargs}' + # there is also secret magic in pretty_tox.sh which lets you run in a fail only + # mode. To do this define the TRACE_FAILONLY environmental variable. [testenv:pep8] commands = flake8 {posargs} From b00f6756c05446976038f40953ebfe07fd785513 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Tue, 24 Feb 2015 15:21:55 +0000 Subject: [PATCH 0698/1705] Updated from global requirements Change-Id: I94de0a33e1b5a2d9df134c58d45546612cf6ace9 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 6b3a459c4..bc729cf8c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,6 +10,6 @@ oslo.utils>=1.2.0 # Apache-2.0 PrettyTable>=0.7,<0.8 requests>=2.2.0,!=2.4.0 simplejson>=2.2.0 -six>=1.7.0 +six>=1.9.0 Babel>=1.3 python-keystoneclient>=1.1.0 From f2a581ee286832204b8eed938384e0430a62c4ab Mon Sep 17 00:00:00 2001 From: Pavel Kholkin Date: Tue, 24 Feb 2015 19:20:50 +0300 Subject: [PATCH 0699/1705] Fixed redeclared test_names Fixed redeclared test_names for two test functions that pass now: 1) 'test_list_security_groups_all_tenants_on' 2) 'test_find_by_str_name' Small spelling corrections TrivialFix Change-Id: Iacb0ce5697779f9342c22a22cb2f29a8e063b459 --- novaclient/tests/unit/test_utils.py | 8 ++++---- novaclient/tests/unit/v2/test_security_groups.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/novaclient/tests/unit/test_utils.py b/novaclient/tests/unit/test_utils.py index 51b824f20..2e72ef659 100644 --- a/novaclient/tests/unit/test_utils.py +++ b/novaclient/tests/unit/test_utils.py @@ -126,7 +126,7 @@ def test_find_by_str_upper_name(self): output = utils.find_resource(self.manager, 'UPPER') self.assertEqual(output, self.manager.get('12345')) - def test_find_by_str_name(self): + def test_find_by_str_lower_name(self): output = utils.find_resource(self.manager, 'lower') self.assertEqual(output, self.manager.get('123456')) @@ -134,16 +134,16 @@ def test_find_by_str_mix_name(self): output = utils.find_resource(self.manager, 'Mixed') self.assertEqual(output, self.manager.get('1234567')) - def test_find_by_str_lower_name(self): + def test_find_by_str_lower_name_mixed(self): output = utils.find_resource(self.manager, 'mixed') self.assertEqual(output, self.manager.get('12345678')) - def test_find_by_str_displayname(self): + def test_find_by_str_display_name(self): display_manager = FakeDisplayManager(None) output = utils.find_resource(display_manager, 'entity_three') self.assertEqual(output, display_manager.get('4242')) - def test_find_in_alphanum_allowd_manager_by_str_id_(self): + def test_find_in_alphanum_allowed_manager_by_str_id_(self): alphanum_manager = FakeManager(True) output = utils.find_resource(alphanum_manager, '01234') self.assertEqual(output, alphanum_manager.get('01234')) diff --git a/novaclient/tests/unit/v2/test_security_groups.py b/novaclient/tests/unit/v2/test_security_groups.py index 9aeb8dde8..d05a93aa1 100644 --- a/novaclient/tests/unit/v2/test_security_groups.py +++ b/novaclient/tests/unit/v2/test_security_groups.py @@ -32,7 +32,7 @@ def test_list_security_groups_all_tenants_on(self): self._do_test_list_security_groups( None, '/os-security-groups') - def test_list_security_groups_all_tenants_on(self): + def test_list_security_groups_all_tenants_on_with_search_opts(self): self._do_test_list_security_groups( {'all_tenants': 1}, '/os-security-groups?all_tenants=1') From e6883d24d0a6928678e560a1a20bb05f5046aa7f Mon Sep 17 00:00:00 2001 From: yatin karel Date: Tue, 24 Feb 2015 23:20:08 +0530 Subject: [PATCH 0700/1705] allow --endpoint-type internal|admin|public other openstack clients like glance, cinder use --endpoint-type (internal|admin|public). This allows users to use the same arguments with the nova cli. Partial-bug: #1367389 Change-Id: Ia55cad797ac0dca7fa60f55c1f2dfba0b64d0fd3 --- novaclient/shell.py | 5 +++++ novaclient/tests/unit/test_shell.py | 16 ++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/novaclient/shell.py b/novaclient/shell.py index 42376de55..2b979cb82 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -651,6 +651,11 @@ def main(self, argv): if not endpoint_type: endpoint_type = DEFAULT_NOVA_ENDPOINT_TYPE + # This allow users to use endpoint_type as (internal, public or admin) + # just like other openstack clients (glance, cinder etc) + if endpoint_type in ['internal', 'public', 'admin']: + endpoint_type += 'URL' + if not service_type: os_compute_api_version = (options.os_compute_api_version or DEFAULT_OS_COMPUTE_API_VERSION) diff --git a/novaclient/tests/unit/test_shell.py b/novaclient/tests/unit/test_shell.py index 9e9079637..e0995113b 100644 --- a/novaclient/tests/unit/test_shell.py +++ b/novaclient/tests/unit/test_shell.py @@ -45,6 +45,13 @@ 'NOVA_ENDPOINT_TYPE': 'novaURL', 'OS_ENDPOINT_TYPE': 'osURL'} +FAKE_ENV4 = {'OS_USER_ID': 'user_id', + 'OS_PASSWORD': 'password', + 'OS_TENANT_ID': 'tenant_id', + 'OS_AUTH_URL': 'http://no.where/v2.0', + 'NOVA_ENDPOINT_TYPE': 'internal', + 'OS_ENDPOINT_TYPE': 'osURL'} + def _create_ver_list(versions): return {'versions': {'values': versions}} @@ -248,6 +255,15 @@ def test_nova_endpoint_type(self, mock_client, m_requests): client_kwargs = mock_client.call_args_list[0][1] self.assertEqual(client_kwargs['endpoint_type'], 'novaURL') + @mock.patch('novaclient.client.Client') + @requests_mock.Mocker() + def test_endpoint_type_like_other_clients(self, mock_client, m_requests): + self.make_env(fake_env=FAKE_ENV4) + self.register_keystone_discovery_fixture(m_requests) + self.shell('list') + client_kwargs = mock_client.call_args_list[0][1] + self.assertEqual(client_kwargs['endpoint_type'], 'internalURL') + @mock.patch('novaclient.client.Client') @requests_mock.Mocker() def test_os_endpoint_type(self, mock_client, m_requests): From 4f9797a659793bb040d8cdf81d478da918eb20c0 Mon Sep 17 00:00:00 2001 From: Sergey Nikitin Date: Wed, 25 Feb 2015 14:05:09 +0300 Subject: [PATCH 0701/1705] Removed unused 'e' from 'try except' statements Change-Id: I57dee43ffcd42fe17914ae01078ae06baf3b4315 --- novaclient/shell.py | 2 +- novaclient/v2/shell.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/novaclient/shell.py b/novaclient/shell.py index 42376de55..b17c24a67 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -912,7 +912,7 @@ def main(): print("ERROR (%(name)s): %(msg)s" % details, file=sys.stderr) sys.exit(1) - except KeyboardInterrupt as e: + except KeyboardInterrupt: print("... terminating nova client", file=sys.stderr) sys.exit(130) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 38c933143..f030e768a 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -192,7 +192,7 @@ def _boot(cs, args): except IOError as e: raise exceptions.CommandError(_("Can't open '%(src)s': %(exc)s") % {'src': src, 'exc': e}) - except ValueError as e: + except ValueError: raise exceptions.CommandError(_("Invalid file argument '%s'. " "File arguments must be of the " "form '--file " @@ -261,7 +261,7 @@ def _boot(cs, args): for kv_str in nic_str.split(","): try: k, v = kv_str.split("=", 1) - except ValueError as e: + except ValueError: raise exceptions.CommandError(err_msg) if k in nic_info: @@ -1506,7 +1506,7 @@ def do_rebuild(cs, args): except IOError as e: raise exceptions.CommandError(_("Can't open '%(src)s': %(exc)s") % {'src': src, 'exc': e}) - except ValueError as e: + except ValueError: raise exceptions.CommandError(_("Invalid file argument '%s'. " "File arguments must be of the " "form '--file " From 53f0c5428fa4d5a4f9886d76d1974cbd86bfa224 Mon Sep 17 00:00:00 2001 From: Sergey Nikitin Date: Fri, 27 Feb 2015 16:29:30 +0300 Subject: [PATCH 0702/1705] Enable check for E124 rule Fix E124 failures and enable check for E124 E124 closing bracket does not match visual indentation Change-Id: Iec6af44362dcf613cfaccbccbe53de82aba51a6a --- novaclient/tests/unit/test_client.py | 6 ++---- novaclient/tests/unit/test_http.py | 3 +-- novaclient/tests/unit/v2/fakes.py | 10 +++++----- tox.ini | 2 +- 4 files changed, 9 insertions(+), 12 deletions(-) diff --git a/novaclient/tests/unit/test_client.py b/novaclient/tests/unit/test_client.py index 149fae7c7..8ed7cb9f3 100644 --- a/novaclient/tests/unit/test_client.py +++ b/novaclient/tests/unit/test_client.py @@ -316,12 +316,10 @@ def test_log_req(self): cs.http_log_debug = True cs.http_log_req('GET', '/foo', {'headers': {}}) cs.http_log_req('GET', '/foo', {'headers': - {'X-Auth-Token': 'totally_bogus'} - }) + {'X-Auth-Token': 'totally_bogus'}}) cs.http_log_req('GET', '/foo', {'headers': {'X-Foo': 'bar', - 'X-Auth-Token': 'totally_bogus'} - }) + 'X-Auth-Token': 'totally_bogus'}}) cs.http_log_req('GET', '/foo', {'headers': {}, 'data': '{"auth": {"passwordCredentials": ' diff --git a/novaclient/tests/unit/test_http.py b/novaclient/tests/unit/test_http.py index 1cebe1f07..5e676828a 100644 --- a/novaclient/tests/unit/test_http.py +++ b/novaclient/tests/unit/test_http.py @@ -97,8 +97,7 @@ def test_get_call(): headers = {"X-Auth-Token": "token", "X-Auth-Project-Id": "project_id", "User-Agent": cl.USER_AGENT, - 'Accept': 'application/json', - } + 'Accept': 'application/json'} mock_request.assert_called_with( "GET", "http://example.com/hi", diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index c36713eeb..55b5bad6f 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -1359,9 +1359,9 @@ def put_os_security_groups_1(self, body, **kw): # def get_os_security_group_rules(self, **kw): return (200, {}, {"security_group_rules": [ - {'id': 1, 'parent_group_id': 1, 'group_id': 2, - 'ip_protocol': 'TCP', 'from_port': 22, 'to_port': 22, - 'cidr': '10.0.0.0/8'} + {'id': 1, 'parent_group_id': 1, 'group_id': 2, + 'ip_protocol': 'TCP', 'from_port': 22, 'to_port': 22, + 'cidr': '10.0.0.0/8'} ]}) def delete_os_security_group_rules_1(self, **kw): @@ -1392,8 +1392,8 @@ def post_os_security_group_rules(self, body, **kw): # def get_os_security_group_default_rules(self, **kw): return (200, {}, {"security_group_default_rules": [ - {'id': 1, 'ip_protocol': 'TCP', 'from_port': 22, - 'to_port': 22, 'cidr': '10.0.0.0/8'} + {'id': 1, 'ip_protocol': 'TCP', 'from_port': 22, + 'to_port': 22, 'cidr': '10.0.0.0/8'} ]}) def delete_os_security_group_default_rules_1(self, **kw): diff --git a/tox.ini b/tox.ini index 11492c8ae..f3e88641d 100644 --- a/tox.ini +++ b/tox.ini @@ -45,7 +45,7 @@ downloadcache = ~/cache/pip # Following checks are ignored on purpose. # # Additional checks are also ignored on purpose: F811, F821 -ignore = E124,F811,F821,H404,H405 +ignore = F811,F821,H404,H405 show-source = True exclude=.venv,.git,.tox,dist,*openstack/common*,*lib/python*,*egg,build,doc/source/conf.py From 5f3f52e7db734d6789e98c099b5040da6ed03ef7 Mon Sep 17 00:00:00 2001 From: Pavel Kholkin Date: Fri, 27 Feb 2015 16:33:41 +0300 Subject: [PATCH 0703/1705] Fix description of parameters in nova-client functions Resolved issues in python-novaclient code like Function 'func_name' does not have a parameter 'param_name' TrivialFix Change-Id: I87cfd346ed8d7dd45bc4dc96bc89c576b5145711 --- novaclient/v2/contrib/cells.py | 4 ++-- novaclient/v2/flavors.py | 4 ---- novaclient/v2/fping.py | 2 +- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/novaclient/v2/contrib/cells.py b/novaclient/v2/contrib/cells.py index bfc6893d7..7099a5a00 100644 --- a/novaclient/v2/contrib/cells.py +++ b/novaclient/v2/contrib/cells.py @@ -31,7 +31,7 @@ def get(self, cell_name): """ Get a cell. - :param cell: Name of the :class:`Cell` to get. + :param cell_name: Name of the :class:`Cell` to get. :rtype: :class:`Cell` """ return self._get("/os-cells/%s" % cell_name, "cell") @@ -40,7 +40,7 @@ def capacities(self, cell_name=None): """ Get capacities for a cell. - :param cell: Name of the :class:`Cell` to get capacities for. + :param cell_name: Name of the :class:`Cell` to get capacities for. :rtype: :class:`Cell` """ path = ["%s/capacities" % cell_name, "capacities"][cell_name is None] diff --git a/novaclient/v2/flavors.py b/novaclient/v2/flavors.py index 0545ad6df..54b0f4974 100644 --- a/novaclient/v2/flavors.py +++ b/novaclient/v2/flavors.py @@ -51,8 +51,6 @@ def is_public(self): def get_keys(self): """ Get extra specs from a flavor. - - :param flavor: The :class:`Flavor` to get extra specs from """ _resp, body = self.manager.api.client.get( "/flavors/%s/os-extra_specs" % base.getid(self)) @@ -62,7 +60,6 @@ def set_keys(self, metadata): """ Set extra specs on a flavor. - :param flavor: The :class:`Flavor` to set extra spec on :param metadata: A dict of key/value pairs to be set """ utils.validate_flavor_metadata_keys(metadata.keys()) @@ -76,7 +73,6 @@ def unset_keys(self, keys): """ Unset extra specs on a flavor. - :param flavor: The :class:`Flavor` to unset extra spec on :param keys: A list of keys to be unset """ for k in keys: diff --git a/novaclient/v2/fping.py b/novaclient/v2/fping.py index ac958d4c6..5e8b74bb9 100644 --- a/novaclient/v2/fping.py +++ b/novaclient/v2/fping.py @@ -58,7 +58,7 @@ def get(self, server): """ Fping a specific server. - :param network: ID of the server to fping. + :param server: ID of the server to fping. :rtype: :class:`Fping` """ return self._get("/os-fping/%s" % base.getid(server), "server") From 9f4d64a1bfbf42dec0d872b1045d8adbc7f19623 Mon Sep 17 00:00:00 2001 From: Pavel Kholkin Date: Mon, 2 Mar 2015 19:00:53 +0300 Subject: [PATCH 0704/1705] Cleanup in test_images and image_fakes Unused code was removed in fixture_data/images.py for unknown image requests that relate to v2/images.py and test_images for them. Change-Id: I7e31a572116b1e5cf0df9f431f8e53470d27d066 --- novaclient/tests/unit/fixture_data/images.py | 29 +------------------- novaclient/tests/unit/v2/test_images.py | 1 + 2 files changed, 2 insertions(+), 28 deletions(-) diff --git a/novaclient/tests/unit/fixture_data/images.py b/novaclient/tests/unit/fixture_data/images.py index 02f359668..4ec472581 100644 --- a/novaclient/tests/unit/fixture_data/images.py +++ b/novaclient/tests/unit/fixture_data/images.py @@ -67,25 +67,6 @@ def setUp(self): json={'image': image_1}, headers=headers) - self.requests.register_uri('GET', self.url(2), - json={'image': image_2}, - headers=headers) - - self.requests.register_uri('GET', self.url(456), - json={'image': image_2}, - headers=headers) - - def post_images(request, context): - body = jsonutils.loads(request.body) - assert list(body) == ['image'] - fakes.assert_has_keys(body['image'], required=['serverId', 'name']) - return images_1 - - self.requests.register_uri('POST', self.url(), - json=post_images, - headers=headers, - status_code=202) - def post_images_1_metadata(request, context): body = jsonutils.loads(request.body) assert list(body) == ['metadata'] @@ -96,17 +77,9 @@ def post_images_1_metadata(request, context): json=post_images_1_metadata, headers=headers) - for u in (1, 2, '1/metadata/test_key'): + for u in (1, '1/metadata/test_key'): self.requests.register_uri('DELETE', self.url(u), status_code=204) - image_headers = {'x-image-meta-id': '1', - 'x-image-meta-name': 'CentOS 5.2', - 'x-image-meta-updated': '2010-10-10T12:00:00Z', - 'x-image-meta-created': '2010-10-10T12:00:00Z', - 'x-image-meta-status': 'ACTIVE', - 'x-image-meta-property-test-key': 'test_value'} - self.requests.register_uri('HEAD', self.url(1), headers=image_headers) - class V3(V1): diff --git a/novaclient/tests/unit/v2/test_images.py b/novaclient/tests/unit/v2/test_images.py index 1f8104edb..0cd110fb5 100644 --- a/novaclient/tests/unit/v2/test_images.py +++ b/novaclient/tests/unit/v2/test_images.py @@ -26,6 +26,7 @@ def test_list_images(self): il = self.cs.images.list() self.assert_called('GET', '/images/detail') [self.assertIsInstance(i, images.Image) for i in il] + self.assertEqual(2, len(il)) def test_list_images_undetailed(self): il = self.cs.images.list(detailed=False) From 574016a9ad01272876abb91efd3d287b97b09c7e Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Fri, 27 Feb 2015 15:27:22 +0200 Subject: [PATCH 0705/1705] Update version of novaclient in the docs docs config contains variables 'version' and 'release', which is a little bit outdated. Since these variables are not used, let's comment them(they can be helpful in future). Change-Id: I39f41002f69efc48841e7703b0c767bda0448c09 --- doc/source/conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index 61437fd78..cd45a8d13 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -107,9 +107,9 @@ def gen_ref(ver, title, names): # built documents. # # The short X.Y version. -version = '2.13' +#version = 'X.Y' # The full version, including alpha/beta/rc tags. -release = '2.13.0' +#release = 'X.Y.Z' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. From 560eeafb6d3b32aa1251c7349f9f90685bead566 Mon Sep 17 00:00:00 2001 From: Pavel Kholkin Date: Tue, 3 Mar 2015 15:34:01 +0300 Subject: [PATCH 0706/1705] Cleanup in asserts in python-novaclient The following replacements were done: 1) assertEqual(True, *) -> assertTrue(*) 2) assertEqual(False, *) -> assertFalse(*) 3) assertTrue(a in b) -> assertIn(a, b) 4) assertTrue(* is not None) -> assertIsNotNone(*) TrivialFix Change-Id: I8d8a2d7b5d3505e07728544b683b301d1a8850cf --- novaclient/tests/unit/test_client.py | 16 ++++++++-------- novaclient/tests/unit/test_utils.py | 2 +- novaclient/tests/unit/v2/test_flavors.py | 4 ++-- novaclient/tests/unit/v2/test_fping.py | 4 ++-- novaclient/tests/unit/v2/test_limits.py | 6 +++--- novaclient/tests/unit/v2/test_servers.py | 10 +++++----- 6 files changed, 21 insertions(+), 21 deletions(-) diff --git a/novaclient/tests/unit/test_client.py b/novaclient/tests/unit/test_client.py index 8ed7cb9f3..983fa2f10 100644 --- a/novaclient/tests/unit/test_client.py +++ b/novaclient/tests/unit/test_client.py @@ -161,26 +161,26 @@ def test_get_client_class_unknown(self): def test_client_with_os_cache_enabled(self): cs = novaclient.v2.client.Client("user", "password", "project_id", auth_url="foo/v2", os_cache=True) - self.assertEqual(True, cs.os_cache) - self.assertEqual(True, cs.client.os_cache) + self.assertTrue(cs.os_cache) + self.assertTrue(cs.client.os_cache) def test_client_with_os_cache_disabled(self): cs = novaclient.v2.client.Client("user", "password", "project_id", auth_url="foo/v2", os_cache=False) - self.assertEqual(False, cs.os_cache) - self.assertEqual(False, cs.client.os_cache) + self.assertFalse(cs.os_cache) + self.assertFalse(cs.client.os_cache) def test_client_with_no_cache_enabled(self): cs = novaclient.v2.client.Client("user", "password", "project_id", auth_url="foo/v2", no_cache=True) - self.assertEqual(False, cs.os_cache) - self.assertEqual(False, cs.client.os_cache) + self.assertFalse(cs.os_cache) + self.assertFalse(cs.client.os_cache) def test_client_with_no_cache_disabled(self): cs = novaclient.v2.client.Client("user", "password", "project_id", auth_url="foo/v2", no_cache=False) - self.assertEqual(True, cs.os_cache) - self.assertEqual(True, cs.client.os_cache) + self.assertTrue(cs.os_cache) + self.assertTrue(cs.client.os_cache) def test_client_set_management_url_v1_1(self): cs = novaclient.v2.client.Client("user", "password", "project_id", diff --git a/novaclient/tests/unit/test_utils.py b/novaclient/tests/unit/test_utils.py index 2e72ef659..da68ce792 100644 --- a/novaclient/tests/unit/test_utils.py +++ b/novaclient/tests/unit/test_utils.py @@ -311,7 +311,7 @@ def test_validate_flavor_metadata_keys_with_invalid_keys(self): utils.validate_flavor_metadata_keys([key]) self.fail("Invalid key passed validation: %s" % key) except exceptions.CommandError as ce: - self.assertTrue(key in str(ce)) + self.assertIn(key, str(ce)) class ResourceManagerExtraKwargsHookTestCase(test_utils.TestCase): diff --git a/novaclient/tests/unit/v2/test_flavors.py b/novaclient/tests/unit/v2/test_flavors.py index 27b77c068..4fe43c10a 100644 --- a/novaclient/tests/unit/v2/test_flavors.py +++ b/novaclient/tests/unit/v2/test_flavors.py @@ -70,7 +70,7 @@ def test_get_flavor_details(self): self.assertEqual(256, f.ram) self.assertEqual(10, f.disk) self.assertEqual(10, f.ephemeral) - self.assertEqual(True, f.is_public) + self.assertTrue(f.is_public) def test_get_flavor_details_alphanum_id(self): f = self.cs.flavors.get('aa1') @@ -79,7 +79,7 @@ def test_get_flavor_details_alphanum_id(self): self.assertEqual(128, f.ram) self.assertEqual(0, f.disk) self.assertEqual(0, f.ephemeral) - self.assertEqual(True, f.is_public) + self.assertTrue(f.is_public) def test_get_flavor_details_diablo(self): f = self.cs.flavors.get(3) diff --git a/novaclient/tests/unit/v2/test_fping.py b/novaclient/tests/unit/v2/test_fping.py index b9ecc3991..0dd7cd30d 100644 --- a/novaclient/tests/unit/v2/test_fping.py +++ b/novaclient/tests/unit/v2/test_fping.py @@ -34,7 +34,7 @@ def test_list_fpings(self): for f in fl: self.assertIsInstance(f, fping.Fping) self.assertEqual("fake-project", f.project_id) - self.assertEqual(True, f.alive) + self.assertTrue(f.alive) def test_list_fpings_all_tenants(self): fl = self.cs.fping.list(all_tenants=True) @@ -59,4 +59,4 @@ def test_get_fping(self): self.assert_called('GET', '/os-fping/1') self.assertIsInstance(f, fping.Fping) self.assertEqual("fake-project", f.project_id) - self.assertEqual(True, f.alive) + self.assertTrue(f.alive) diff --git a/novaclient/tests/unit/v2/test_limits.py b/novaclient/tests/unit/v2/test_limits.py index 94a62c5c0..9dc9dec43 100644 --- a/novaclient/tests/unit/v2/test_limits.py +++ b/novaclient/tests/unit/v2/test_limits.py @@ -47,7 +47,7 @@ def test_absolute_limits(self): self.assertEqual(len(abs_limits), len(expected)) for limit in abs_limits: - self.assertTrue(limit in expected) + self.assertIn(limit, expected) def test_absolute_limits_reserved(self): obj = self.cs.limits.get(reserved=True) @@ -65,7 +65,7 @@ def test_absolute_limits_reserved(self): self.assertEqual(len(abs_limits), len(expected)) for limit in abs_limits: - self.assertTrue(limit in expected) + self.assertIn(limit, expected) def test_rate_limits(self): obj = self.cs.limits.get() @@ -85,4 +85,4 @@ def test_rate_limits(self): self.assertEqual(len(rate_limits), len(expected)) for limit in rate_limits: - self.assertTrue(limit in expected) + self.assertIn(limit, expected) diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index c9b15f30a..c43aed422 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -212,7 +212,7 @@ def _create_disk_config(self, disk_config): # verify disk config param was used in the request: body = jsonutils.loads(self.requests.last_request.body) server = body['server'] - self.assertTrue('OS-DCF:diskConfig' in server) + self.assertIn('OS-DCF:diskConfig', server) self.assertEqual(disk_config, server['OS-DCF:diskConfig']) def test_create_server_disk_config_auto(self): @@ -302,7 +302,7 @@ def _rebuild_resize_disk_config(self, disk_config, operation="rebuild"): body = jsonutils.loads(self.requests.last_request.body) d = body[operation] - self.assertTrue('OS-DCF:diskConfig' in d) + self.assertIn('OS-DCF:diskConfig', d) self.assertEqual(disk_config, d['OS-DCF:diskConfig']) def test_rebuild_server_disk_config_auto(self): @@ -318,7 +318,7 @@ def test_rebuild_server_preserve_ephemeral(self): body = jsonutils.loads(self.requests.last_request.body) d = body['rebuild'] self.assertIn('preserve_ephemeral', d) - self.assertEqual(True, d['preserve_ephemeral']) + self.assertTrue(d['preserve_ephemeral']) def test_rebuild_server_name_meta_files(self): files = {'/etc/passwd': 'some data'} @@ -560,11 +560,11 @@ def test_clear_password(self): def test_get_server_diagnostics(self): s = self.cs.servers.get(1234) diagnostics = s.diagnostics() - self.assertTrue(diagnostics is not None) + self.assertIsNotNone(diagnostics) self.assert_called('GET', '/servers/1234/diagnostics') diagnostics_from_manager = self.cs.servers.diagnostics(1234) - self.assertTrue(diagnostics_from_manager is not None) + self.assertIsNotNone(diagnostics_from_manager) self.assert_called('GET', '/servers/1234/diagnostics') self.assertEqual(diagnostics[1], diagnostics_from_manager[1]) From 8f39d9c9bd74a8d3647e7d7a6d8b120c6379d560 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Sat, 7 Mar 2015 00:23:02 +0000 Subject: [PATCH 0707/1705] Updated from global requirements Change-Id: Ibd49980b4f03724a2b5208c348198c30766f7c9d --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 051ced393..acdbc2441 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -14,4 +14,4 @@ oslosphinx>=2.2.0 # Apache-2.0 testrepository>=0.0.18 testscenarios>=0.4 testtools>=0.9.36,!=1.2.0 -tempest-lib>=0.2.0 +tempest-lib>=0.3.0 From dc2d210f753b13ae59fabe9cd4748d96a2a8a912 Mon Sep 17 00:00:00 2001 From: Thomas Bechtold Date: Fri, 30 Jan 2015 19:28:38 +0100 Subject: [PATCH 0708/1705] Fix client usage in api example The used class to create an instance was not available. Use the correct class. Change-Id: Ie07504bff47e2c3e7120750cd2064cb97ad863a6 --- doc/source/api.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/source/api.rst b/doc/source/api.rst index e53f2a14f..6457d259b 100644 --- a/doc/source/api.rst +++ b/doc/source/api.rst @@ -11,8 +11,8 @@ Usage First create a client instance with your credentials:: - >>> from novaclient.client import Client - >>> nova = Client(VERSION, USERNAME, PASSWORD, PROJECT_ID, AUTH_URL) + >>> from novaclient import client + >>> nova = client.Client(VERSION, USERNAME, PASSWORD, PROJECT_ID, AUTH_URL) Here ``VERSION`` can be: ``1.1``, ``2`` and ``3``. @@ -21,7 +21,7 @@ session API:: >>> from keystoneclient.auth.identity import v2 >>> from keystoneclient import session - >>> from novaclient.client import Client + >>> from novaclient import client >>> auth = v2.Password(auth_url=AUTH_URL, username=USERNAME, password=PASSWORD, From cf03aeb436410ef558c8d577a0a82fadcf5d1269 Mon Sep 17 00:00:00 2001 From: Joe Gordon Date: Fri, 6 Mar 2015 15:04:28 -0800 Subject: [PATCH 0709/1705] Add functional testing README Outline the background and goal of the functional testing. Want to make sure there is consensus on how this should look. Next step is to reorganize the functional tests to follow the plan outlined here. Change-Id: I1d6889682f50f78b0681c99b77475adba95ef807 --- novaclient/tests/functional/README.rst | 50 ++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 novaclient/tests/functional/README.rst diff --git a/novaclient/tests/functional/README.rst b/novaclient/tests/functional/README.rst new file mode 100644 index 000000000..be4ebd079 --- /dev/null +++ b/novaclient/tests/functional/README.rst @@ -0,0 +1,50 @@ +===================================== +python-novaclient functional testing +===================================== + +Idea +------ + +Over time we have noticed two issues with novaclient unit tests. + +* Does not exercise the CLI +* We can get the expected server behavior wrong, and test the wrong thing. + +We are using functional tests, run against a running cloud +(primarily devstack), to address these two cases. + +Additionally these functional tests can be considered example uses +of python-novaclient. + +These tests started out in tempest as read only nova CLI tests, to make sure +the CLI didn't simply stacktrace when being used (which happened on +multiple occasions). + + +Testing Theory +---------------- + +We are treating python-novaclient as legacy code, so we do not want to spend a +lot of effort adding in missing features. In the future the CLI will move to +python-openstackclient, and the python API will be based on the OpenStack +SDK project. But until that happens we still need better functional testing, +to prevent regressions etc. + + +Since python-novaclient has two uses, CLI and python API, we should have two +sets of functional tests. CLI and python API. The python API tests should +never use the CLI. But the CLI tests can use the python API where adding +native support to the CLI for the required functionality would involve a +non trivial amount of work. + +Functional Test Guidelines +--------------------------- + +* Consume credentials via standard client environmental variables:: + + OS_USERNAME + OS_PASSWORD + OS_TENANT_NAME + OS_AUTH_URL + +* Try not to require an additional configuration file From b7972478ff656bc6fa1cc909b617b02b0dfb28b7 Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Tue, 10 Mar 2015 21:27:45 -0500 Subject: [PATCH 0710/1705] Update help messages for default security group commands The secgroup-{add,delete,list}-default-rules? family of commands has confusing help text that makes it sounds like they actually operate on the default security group (that is, the security group named 'default' which is the default for the current tenant). Instead, the operate on a sort of meta-security-group object which is only used to populate the 'default' security group in newly-created tenants. This is crazy confusing. These commands get fancy new help strings that should hopefully make it a little more clear. Closes-Bug: 1430354 Change-Id: I4d9569ba7ea9ad7a20e984df7a137c99240a9748 --- novaclient/v2/shell.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index f030e768a..01dd4ac8d 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -4326,7 +4326,9 @@ def do_server_group_list(cs, args): def do_secgroup_list_default_rules(cs, args): - """List rules for the default security group.""" + """List rules that will be added to the 'default' security group for + new tenants. + """ _print_secgroup_rules(cs.security_group_default_rules.list(), show_source_group=False) @@ -4345,7 +4347,9 @@ def do_secgroup_list_default_rules(cs, args): help=_('Port at end of range.')) @cliutils.arg('cidr', metavar='', help=_('CIDR for address range.')) def do_secgroup_add_default_rule(cs, args): - """Add a rule to the default security group.""" + """Add a rule to the set of rules that will be added to the 'default' + security group for new tenants. + """ rule = cs.security_group_default_rules.create(args.ip_proto, args.from_port, args.to_port, @@ -4367,7 +4371,9 @@ def do_secgroup_add_default_rule(cs, args): help=_('Port at end of range.')) @cliutils.arg('cidr', metavar='', help=_('CIDR for address range.')) def do_secgroup_delete_default_rule(cs, args): - """Delete a rule from the default security group.""" + """Delete a rule from the set of rules that will be added to the + 'default' security group for new tenants. + """ for rule in cs.security_group_default_rules.list(): if (rule.ip_protocol and rule.ip_protocol.upper() == args.ip_proto.upper() and From 4e79285b45ec1490c8e923f724cbaf4d42fe81c4 Mon Sep 17 00:00:00 2001 From: Abhishek Talwar Date: Mon, 23 Feb 2015 17:26:53 +0530 Subject: [PATCH 0711/1705] nova flavor-show command is inconsistent The nova flavor-show command accepts a flavor name as its parameter. The command show inconsistency as it works with the exact flavor name and flavor name in all lowercase but fails for any other case pattern. So, updated the code so that if we run the command with flavor-name case patern it runs fine. Closes-Bug: #1423885 Change-Id: I713bf2b5aca977df40dc745e29d4fe98a950c991 --- novaclient/tests/unit/test_base.py | 2 +- novaclient/tests/unit/v2/fakes.py | 20 ++++++++++---------- novaclient/tests/unit/v2/test_flavors.py | 4 ++-- novaclient/tests/unit/v2/test_shell.py | 16 ++++++++-------- novaclient/v2/shell.py | 11 +++++++++++ 5 files changed, 32 insertions(+), 21 deletions(-) diff --git a/novaclient/tests/unit/test_base.py b/novaclient/tests/unit/test_base.py index b7bceb7b8..1ba1d32b0 100644 --- a/novaclient/tests/unit/test_base.py +++ b/novaclient/tests/unit/test_base.py @@ -36,7 +36,7 @@ class TmpObject(object): def test_resource_lazy_getattr(self): f = flavors.Flavor(cs.flavors, {'id': 1}) - self.assertEqual('256 MB Server', f.name) + self.assertEqual('256 mb server', f.name) cs.assert_called('GET', '/flavors/1') # Missing stuff still fails after a second get diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index c36713eeb..ae278755f 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -258,7 +258,7 @@ def get_servers_detail(self, **kw): }, "flavor": { "id": 1, - "name": "256 MB Server", + "name": "256 mb server", }, "hostId": "e4d909c290d0fb1ca068ffaddf22cbd0", "status": "BUILD", @@ -299,7 +299,7 @@ def get_servers_detail(self, **kw): }, "flavor": { "id": 1, - "name": "256 MB Server", + "name": "256 mb server", }, "hostId": "9e107d9d372bb6826bd81d3542a419d6", "status": "ACTIVE", @@ -340,7 +340,7 @@ def get_servers_detail(self, **kw): "image": "", "flavor": { "id": 1, - "name": "256 MB Server", + "name": "256 mb server", }, "hostId": "9e107d9d372bb6826bd81d3542a419d6", "status": "ACTIVE", @@ -697,19 +697,19 @@ def get_flavors(self, **kw): def get_flavors_detail(self, **kw): flavors = {'flavors': [ - {'id': 1, 'name': '256 MB Server', 'ram': 256, 'disk': 10, + {'id': 1, 'name': '256 mb server', 'ram': 256, 'disk': 10, 'OS-FLV-EXT-DATA:ephemeral': 10, 'os-flavor-access:is_public': True, 'links': {}}, - {'id': 2, 'name': '512 MB Server', 'ram': 512, 'disk': 20, + {'id': 2, 'name': '512 mb server', 'ram': 512, 'disk': 20, 'OS-FLV-EXT-DATA:ephemeral': 20, 'os-flavor-access:is_public': False, 'links': {}}, - {'id': 4, 'name': '1024 MB Server', 'ram': 1024, 'disk': 10, + {'id': 4, 'name': '1024 mb server', 'ram': 1024, 'disk': 10, 'OS-FLV-EXT-DATA:ephemeral': 10, 'os-flavor-access:is_public': True, 'links': {}}, - {'id': 'aa1', 'name': '128 MB Server', 'ram': 128, 'disk': 0, + {'id': 'aa1', 'name': '128 mb server', 'ram': 128, 'disk': 0, 'OS-FLV-EXT-DATA:ephemeral': 0, 'os-flavor-access:is_public': True, 'links': {}} @@ -761,16 +761,16 @@ def get_flavors_3(self, **kw): {}, {'flavor': { 'id': 3, - 'name': '256 MB Server', + 'name': '256 mb server', 'ram': 256, 'disk': 10, }}, ) - def get_flavors_512_MB_Server(self, **kw): + def get_flavors_512_mb_server(self, **kw): raise exceptions.NotFound('404') - def get_flavors_128_MB_Server(self, **kw): + def get_flavors_128_mb_server(self, **kw): raise exceptions.NotFound('404') def get_flavors_aa1(self, **kw): diff --git a/novaclient/tests/unit/v2/test_flavors.py b/novaclient/tests/unit/v2/test_flavors.py index 27b77c068..c379b5840 100644 --- a/novaclient/tests/unit/v2/test_flavors.py +++ b/novaclient/tests/unit/v2/test_flavors.py @@ -93,10 +93,10 @@ def test_get_flavor_details_diablo(self): def test_find(self): f = self.cs.flavors.find(ram=256) self.cs.assert_called('GET', '/flavors/detail') - self.assertEqual('256 MB Server', f.name) + self.assertEqual('256 mb server', f.name) f = self.cs.flavors.find(disk=0) - self.assertEqual('128 MB Server', f.name) + self.assertEqual('128 mb server', f.name) self.assertRaises(exceptions.NotFound, self.cs.flavors.find, disk=12345) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 71eb85ee8..d90756816 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -661,10 +661,10 @@ def test_boot_with_poll_to_check_VM_state_error(self): def test_boot_named_flavor(self): self.run_command(["boot", "--image", "1", - "--flavor", "512 MB Server", + "--flavor", "512 mb server", "--max-count", "3", "server"]) self.assert_called('GET', '/images/1', pos=0) - self.assert_called('GET', '/flavors/512 MB Server', pos=1) + self.assert_called('GET', '/flavors/512 mb server', pos=1) self.assert_called('GET', '/flavors?is_public=None', pos=2) self.assert_called('GET', '/flavors/2', pos=3) self.assert_called( @@ -701,15 +701,15 @@ def test_flavor_show_with_alphanum_id(self): self.assert_called_anytime('GET', '/flavors/aa1') def test_flavor_show_by_name(self): - self.run_command(['flavor-show', '128 MB Server']) - self.assert_called('GET', '/flavors/128 MB Server', pos=0) + self.run_command(['flavor-show', '128 mb server']) + self.assert_called('GET', '/flavors/128 mb server', pos=0) self.assert_called('GET', '/flavors?is_public=None', pos=1) self.assert_called('GET', '/flavors/aa1', pos=2) self.assert_called('GET', '/flavors/aa1/os-extra_specs', pos=3) def test_flavor_show_by_name_priv(self): - self.run_command(['flavor-show', '512 MB Server']) - self.assert_called('GET', '/flavors/512 MB Server', pos=0) + self.run_command(['flavor-show', '512 mb server']) + self.assert_called('GET', '/flavors/512 mb server', pos=0) self.assert_called('GET', '/flavors?is_public=None', pos=1) self.assert_called('GET', '/flavors/2', pos=2) self.assert_called('GET', '/flavors/2/os-extra_specs', pos=3) @@ -746,7 +746,7 @@ def test_flavor_access_add_by_id(self): {'addTenantAccess': {'tenant': 'proj2'}}) def test_flavor_access_add_by_name(self): - self.run_command(['flavor-access-add', '512 MB Server', 'proj2']) + self.run_command(['flavor-access-add', '512 mb server', 'proj2']) self.assert_called('POST', '/flavors/2/action', {'addTenantAccess': {'tenant': 'proj2'}}) @@ -756,7 +756,7 @@ def test_flavor_access_remove_by_id(self): {'removeTenantAccess': {'tenant': 'proj2'}}) def test_flavor_access_remove_by_name(self): - self.run_command(['flavor-access-remove', '512 MB Server', 'proj2']) + self.run_command(['flavor-access-remove', '512 mb server', 'proj2']) self.assert_called('POST', '/flavors/2/action', {'removeTenantAccess': {'tenant': 'proj2'}}) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 38c933143..949d39742 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -1897,6 +1897,17 @@ def _find_image(cs, image): def _find_flavor(cs, flavor): """Get a flavor by name, ID, or RAM size.""" try: + # isinstance() is being used to check if flavor is an instance of + # integer. It will help us to check if the user has entered flavor + # name or flavorid. If flavor name has been entered it is being + # converted to lowercase using lower(). Incase it is an ID the user + # has passed it will not go through the "flavor = flavor.lower()" + # code.The reason for checking if it is a flavor name or flavorid is + # that int has no lower() so it will give an error. + if isinstance(flavor, six.integer_types): + pass + else: + flavor = flavor.lower() return utils.find_resource(cs.flavors, flavor, is_public=None) except exceptions.NotFound: return cs.flavors.find(ram=flavor) From 41ef74971eec9d80438435c16afe302eacad7852 Mon Sep 17 00:00:00 2001 From: Anand Shanmugam Date: Wed, 11 Mar 2015 20:48:46 +0530 Subject: [PATCH 0712/1705] nova client cinder query param changed to display_name nova client is not able to show the list of volumes when querying by name.This is because cinder api accepts only display_name as query parameter and when qeried with 'name' parameter returns a empty list Change-Id: Ie4ffc275d1754052d9e239d8457baf6f7fd53b83 Closes-Bug: #1430415 --- novaclient/tests/unit/v2/test_shell.py | 2 +- novaclient/v2/volumes.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 71eb85ee8..0b69f437a 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2145,7 +2145,7 @@ def test_volume_list(self): def test_volume_show(self): self.run_command('volume-show Work') - self.assert_called('GET', '/volumes?name=Work', pos=-2) + self.assert_called('GET', '/volumes?display_name=Work', pos=-2) self.assert_called( 'GET', '/volumes/15e59938-07d5-11e1-90e3-e3dffe0c5983', diff --git a/novaclient/v2/volumes.py b/novaclient/v2/volumes.py index 9b9cf1886..99e3d6fed 100644 --- a/novaclient/v2/volumes.py +++ b/novaclient/v2/volumes.py @@ -86,6 +86,9 @@ def list(self, detailed=True, search_opts=None): """ search_opts = search_opts or {} + if 'name' in search_opts.keys(): + search_opts['display_name'] = search_opts.pop('name') + qparams = dict((k, v) for (k, v) in six.iteritems(search_opts) if v) query_string = '?%s' % parse.urlencode(qparams) if qparams else '' From 689a884e18deef696a7734b4d77eca8f699e12d0 Mon Sep 17 00:00:00 2001 From: David Moreau Simard Date: Wed, 11 Mar 2015 13:49:20 -0400 Subject: [PATCH 0713/1705] Fix typo in socket attribute name The proper attribute name is 'IPPROTO_TCP', not 'IPROTO_TCP'. This would lead to an AttributeError since socket does not have an attribute named 'IPROTO_TCP'. Change-Id: Ibd3c1e8d48ae57994d023bf18dd53a298466f6cb Closes-Bug: 1430935 --- novaclient/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/client.py b/novaclient/client.py index 9a4fcde53..c40c21a18 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -51,7 +51,7 @@ class TCPKeepAliveAdapter(adapters.HTTPAdapter): def init_poolmanager(self, *args, **kwargs): if requests.__version__ >= '2.4.1': kwargs.setdefault('socket_options', [ - (socket.IPROTO_TCP, socket.TCP_NODELAY, 1), + (socket.IPPROTO_TCP, socket.TCP_NODELAY, 1), (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1), ]) super(TCPKeepAliveAdapter, self).init_poolmanager(*args, **kwargs) From 1b512da159645a54a0acee1120a0d1cf78ca3666 Mon Sep 17 00:00:00 2001 From: Joe Gordon Date: Wed, 11 Mar 2015 14:00:21 -0700 Subject: [PATCH 0714/1705] Update help message for nova boot --file Stop hard coding the default value for the injected_files quota and just point to the quota instead. Change-Id: Ia2abc7e4c3d0e5f954a5c09ff76f11a794e89695 --- novaclient/v2/shell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index f030e768a..524457db7 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -372,7 +372,7 @@ def _boot(cs, args): dest='files', default=[], help=_("Store arbitrary files from locally to " - "on the new server. You may store up to 5 files.")) + "on the new server. Limited by the injected_files quota value.")) @cliutils.arg( '--key-name', default=os.environ.get('NOVACLIENT_DEFAULT_KEY_NAME'), From 8c0baaea5781ec59975786d9439a5c4c46d4a043 Mon Sep 17 00:00:00 2001 From: melanie witt Date: Wed, 11 Mar 2015 18:22:28 +0000 Subject: [PATCH 0715/1705] Add a test for the TCPKeepAliveAdapter Related-Bug: #1430935 Change-Id: Idfea26c8eb6448a4c6adc0f3a916515bd4655c1a --- novaclient/tests/unit/test_client.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/novaclient/tests/unit/test_client.py b/novaclient/tests/unit/test_client.py index 983fa2f10..db4c87d09 100644 --- a/novaclient/tests/unit/test_client.py +++ b/novaclient/tests/unit/test_client.py @@ -16,6 +16,7 @@ import json import logging +import socket import fixtures import mock @@ -27,6 +28,24 @@ import novaclient.v2.client +class TCPKeepAliveAdapterTest(utils.TestCase): + + @mock.patch.object(requests.adapters.HTTPAdapter, 'init_poolmanager') + def test_init_poolmanager(self, mock_init_poolmgr): + adapter = novaclient.client.TCPKeepAliveAdapter() + kwargs = {} + adapter.init_poolmanager(**kwargs) + if requests.__version__ >= '2.4.1': + kwargs.setdefault('socket_options', [ + (socket.IPPROTO_TCP, socket.TCP_NODELAY, 1), + (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1), + ]) + # NOTE(melwitt): This is called twice because + # HTTPAdapter.__init__ calls it first. + self.assertEqual(2, mock_init_poolmgr.call_count) + mock_init_poolmgr.assert_called_with(**kwargs) + + class ClientConnectionPoolTest(utils.TestCase): @mock.patch("novaclient.client.TCPKeepAliveAdapter") From e9c70598f33af40791c2c14a9c84b415ac374c58 Mon Sep 17 00:00:00 2001 From: Ian Wienand Date: Wed, 11 Mar 2015 15:29:14 +1100 Subject: [PATCH 0716/1705] Add Client object to documentation After wanting to do a rather simple thing -- figure out of the "timeout" argument to nodepool was a int or a float, it lead me down the rabbit-hole to python-novaclient. It turns out the timeout does get passed through to requests as a float so that mystery was solved. But the "Client" class seems to be missing from the documentation as it's not included in the class list. So add that and also at least document the types of the arguments. However, then I noticd that this wasn't showing up; turns out sphinx requires "autoclass_content = both" if you want it to document __init__() functions. Several other classes had their init args documented but they weren't showing up because of this. Change-Id: I8f44e92f2a0f25a75926b1813a8b374e79b4f5db --- doc/source/conf.py | 14 ++++++++------ novaclient/v2/client.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index cd45a8d13..738574773 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -72,12 +72,12 @@ def gen_ref(ver, title, names): gen_ref(None, "Exceptions", ["exceptions"]) gen_ref("v2", "Version 1.1, Version 2 API Reference, Version 3 API Reference", - ["flavors", "images", "servers", "hosts", "agents", "aggregates", - "availability_zones", "certs", "fixed_ips", "floating_ip_pools", - "floating_ips", "hypervisors", "keypairs", "limits", "networks", - "quota_classes", "quotas", "security_group_rules", - "security_groups", "services", "virtual_interfaces", - "volume_snapshots", "volumes", "volume_types"]) + ["client", "flavors", "images", "servers", "hosts", "agents", + "aggregates", "availability_zones", "certs", "fixed_ips", + "floating_ip_pools", "floating_ips", "hypervisors", "keypairs", + "limits", "networks", "quota_classes", "quotas", + "security_group_rules", "security_groups", "services", + "virtual_interfaces", "volume_snapshots", "volumes", "volume_types"]) # -- General configuration ---------------------------------------------------- @@ -86,6 +86,8 @@ def gen_ref(ver, title, names): # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'oslosphinx'] +autoclass_content = 'both' + # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] diff --git a/novaclient/v2/client.py b/novaclient/v2/client.py index e087ac68c..bbb8949d9 100644 --- a/novaclient/v2/client.py +++ b/novaclient/v2/client.py @@ -104,6 +104,36 @@ def __init__(self, username=None, api_key=None, project_id=None, cacert=None, tenant_id=None, user_id=None, connection_pool=False, session=None, auth=None, **kwargs): + """ + :param str username: Username + :param str api_key: API Key + :param str project_id: Project ID + :param str auth_url: Auth URL + :param bool insecure: Allow insecure + :param float timeout: API timeout, None or 0 disables + :param str proxy_tenant_id: Tenant ID + :param str proxy_token: Proxy Token + :param str region_name: Region Name + :param str endpoint_type: Endpoint Type + :param str extensions: Exensions + :param str service_type: Service Type + :param str service_name: Service Name + :param str volume_service_name: Volume Service Name + :param bool timings: Timings + :param str bypass_url: Bypass URL + :param bool os_cache: OS cache + :param bool no_cache: No cache + :param bool http_log_debug: Enable debugging for HTTP connections + :param str auth_system: Auth system + :param str auth_plugin: Auth plugin + :param str auth_token: Auth token + :param str cacert: cacert + :param str tenant_id: Tenant ID + :param str user_id: User ID + :param bool connection_pool: Use a connection pool + :param str session: Session + :param str auth: Auth + """ # FIXME(comstud): Rename the api_key argument above when we # know it's not being used as keyword argument From 7b1734d44e96d3fc523a72f3bee8f4466d14ce2f Mon Sep 17 00:00:00 2001 From: melanie witt Date: Thu, 12 Mar 2015 18:16:58 +0000 Subject: [PATCH 0717/1705] Add missing servers.create parameter documentation The min_count, max_count, and security_groups parameters for servers.create are missing documentation. The security_groups parameter was documented in the servers._boot function but that private function isn't published to the docs. This change adds the missing documentation for the aforementioned parameters and removes the duplicate docs in the servers._boot function. Change-Id: Ib0dcc7e8f7f4d0b83b6caeddd1ace66add4e0003 --- novaclient/v2/servers.py | 41 ++++++---------------------------------- 1 file changed, 6 insertions(+), 35 deletions(-) diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index 8055f1509..397a12ac1 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -423,40 +423,6 @@ def _boot(self, resource_url, response_key, name, image, flavor, config_drive=None, admin_pass=None, disk_config=None, **kwargs): """ Create (boot) a new server. - - :param name: Something to name the server. - :param image: The :class:`Image` to boot with. - :param flavor: The :class:`Flavor` to boot onto. - :param meta: A dict of arbitrary key/value metadata to store for this - server. A maximum of five entries is allowed, and both - keys and values must be 255 characters or less. - :param files: A dict of files to overwrite on the server upon boot. - Keys are file names (i.e. ``/etc/passwd``) and values - are the file contents (either as a string or as a - file-like object). A maximum of five entries is allowed, - and each file must be 10k or less. - :param reservation_id: a UUID for the set of servers being requested. - :param return_raw: If True, don't try to coerce the result into - a Resource object. - :param security_groups: list of security group names - :param key_name: (optional extension) name of keypair to inject into - the instance - :param availability_zone: Name of the availability zone for instance - placement. - :param block_device_mapping: A dict of block device mappings for this - server. - :param block_device_mapping_v2: A dict of block device mappings V2 for - this server. - :param nics: (optional extension) an ordered list of nics to be - added to this server, with information about - connected networks, fixed IPs, etc. - :param scheduler_hints: (optional extension) arbitrary key-value pairs - specified by the client to help boot an instance. - :param config_drive: (optional extension) If True, enable config drive - on the server. - :param admin_pass: admin password for the server. - :param disk_config: (optional extension) control how the disk is - partitioned when the server is created. """ body = {"server": { "name": name, @@ -873,10 +839,15 @@ def create(self, name, image, flavor, meta=None, files=None, are the file contents (either as a string or as a file-like object). A maximum of five entries is allowed, and each file must be 10k or less. + :param reservation_id: a UUID for the set of servers being requested. + :param min_count: (optional extension) The minimum number of + servers to launch. + :param max_count: (optional extension) The maximum number of + servers to launch. + :param security_groups: A list of security group names :param userdata: user data to pass to be exposed by the metadata server this can be a file type object as well or a string. - :param reservation_id: a UUID for the set of servers being requested. :param key_name: (optional extension) name of previously created keypair to inject into the instance. :param availability_zone: Name of the availability zone for instance From e0f5072907a00d48a183dd8fc91a6cf6038ca279 Mon Sep 17 00:00:00 2001 From: melanie witt Date: Sat, 14 Mar 2015 23:39:05 +0000 Subject: [PATCH 0718/1705] Ensure the use of volume endpoint in volumes apis Currently, several of the volumes apis depend on callers to pass service_type='volume' as a parameter when creating a client object, to route correctly. The problem is, it makes it impossible for callers to work with both the compute and volume endpoints at the same time. They can either work with compute and have volumes.* calls return 404, or they can work with volume and have servers.* images.* flavors.* calls return 404. The CLI sets service_type='volume' for client objects for the following commands via a decorator: volume-list volume-show volume-create volume-delete volume-snapshot-list volume-snapshot-show volume-snapshot-create volume-snapshot-delete volume-type-list volume-type-create volume-type-delete With this change, the service_type 'volume' is set in the api, so the decorators on the shell commands are no longer needed. Closes-Bug: #1431154 Change-Id: I11b48ac14fa4c64d8aae528552ec5b363be384c5 --- novaclient/base.py | 9 ++ novaclient/client.py | 34 ++++-- .../tests/functional/test_volumes_api.py | 111 ++++++++++++++++++ novaclient/tests/unit/test_client.py | 3 + novaclient/tests/unit/test_http.py | 1 + novaclient/tests/unit/v2/fakes.py | 1 + novaclient/tests/unit/v2/test_auth.py | 1 + novaclient/v2/shell.py | 11 -- novaclient/v2/volume_snapshots.py | 26 ++-- novaclient/v2/volume_types.py | 23 ++-- novaclient/v2/volumes.py | 44 ++++--- 11 files changed, 204 insertions(+), 60 deletions(-) create mode 100644 novaclient/tests/functional/test_volumes_api.py diff --git a/novaclient/base.py b/novaclient/base.py index 8cee35ae2..01b4bac94 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -80,6 +80,15 @@ def _list(self, url, response_key, obj_class=None, body=None): return [obj_class(self, res, loaded=True) for res in data if res] + @contextlib.contextmanager + def alternate_service_type(self, service_type): + original_service_type = self.api.client.service_type + self.api.client.service_type = service_type + try: + yield + finally: + self.api.client.service_type = original_service_type + @contextlib.contextmanager def completion_cache(self, cache_type, obj_class, mode): """ diff --git a/novaclient/client.py b/novaclient/client.py index c40c21a18..32eb77505 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -212,6 +212,11 @@ def __init__(self, user, password, projectid=None, auth_url=None, # otherwise we will get all the requests logging messages rql.setLevel(logging.WARNING) + # NOTE(melwitt): Service catalog is only set if bypass_url isn't + # used. Otherwise, we can cache using services_url. + self.service_catalog = None + self.services_url = {} + def use_token_cache(self, use_it): self.os_cache = use_it @@ -405,7 +410,12 @@ def _cs_request(self, url, method, **kwargs): path = re.sub(r'v[1-9]/[a-z0-9]+$', '', path) url = parse.urlunsplit((scheme, netloc, path, None, None)) else: - url = self.management_url + url + if self.service_catalog: + url = self.get_service_url(self.service_type) + url + else: + # NOTE(melwitt): The service catalog is not available + # when bypass_url is used. + url = self.management_url + url # Perform the request once. If we get a 401 back then it # might be because the auth token expired, so try to @@ -448,6 +458,19 @@ def put(self, url, **kwargs): def delete(self, url, **kwargs): return self._cs_request(url, 'DELETE', **kwargs) + def get_service_url(self, service_type): + if service_type not in self.services_url: + url = self.service_catalog.url_for( + attr='region', + filter_value=self.region_name, + endpoint_type=self.endpoint_type, + service_type=service_type, + service_name=self.service_name, + volume_service_name=self.volume_service_name,) + url = url.rstrip('/') + self.services_url[service_type] = url + return self.services_url[service_type] + def _extract_service_catalog(self, url, resp, body, extract_token=True): """See what the auth service told us and process the response. We may get redirected to another site, fail or actually get @@ -464,14 +487,7 @@ def _extract_service_catalog(self, url, resp, body, extract_token=True): self.auth_token = self.service_catalog.get_token() self.tenant_id = self.service_catalog.get_tenant_id() - management_url = self.service_catalog.url_for( - attr='region', - filter_value=self.region_name, - endpoint_type=self.endpoint_type, - service_type=self.service_type, - service_name=self.service_name, - volume_service_name=self.volume_service_name,) - self.management_url = management_url.rstrip('/') + self.management_url = self.get_service_url(self.service_type) return None except exceptions.AmbiguousEndpoints: print(_("Found more than one valid endpoint. Use a more " diff --git a/novaclient/tests/functional/test_volumes_api.py b/novaclient/tests/functional/test_volumes_api.py new file mode 100644 index 000000000..e427c2e21 --- /dev/null +++ b/novaclient/tests/functional/test_volumes_api.py @@ -0,0 +1,111 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import os +import time +import uuid + +import six.moves + +from novaclient import client +from novaclient import exceptions +from novaclient.tests.functional import base + + +def wait_for_delete(test, name, thing, get_func): + thing.delete() + for x in six.moves.range(60): + try: + thing = get_func(thing.id) + except exceptions.NotFound: + break + time.sleep(1) + else: + test.fail('%s %s still not deleted after 60s' % (name, thing.id)) + + +class TestVolumesAPI(base.ClientTestBase): + + def setUp(self): + super(TestVolumesAPI, self).setUp() + user = os.environ['OS_USERNAME'] + passwd = os.environ['OS_PASSWORD'] + tenant = os.environ['OS_TENANT_NAME'] + auth_url = os.environ['OS_AUTH_URL'] + + self.client = client.Client(2, user, passwd, tenant, auth_url=auth_url) + + def test_volumes_snapshots_types_create_get_list_delete(self): + # Create a volume + volume = self.client.volumes.create(1) + + # Make sure we can still list servers after using the volume endpoint + self.client.servers.list() + + # This cleanup tests volume delete + self.addCleanup(volume.delete) + + # Wait for the volume to become available + for x in six.moves.range(60): + volume = self.client.volumes.get(volume.id) + if volume.status == 'available': + break + elif volume.status == 'error': + self.fail('Volume %s is in error state' % volume.id) + time.sleep(1) + else: + self.fail('Volume %s not available after 60s' % volume.id) + + # List all volumes + self.client.volumes.list() + + # Create a volume snapshot + snapshot = self.client.volume_snapshots.create(volume.id) + + # This cleanup tests volume snapshot delete. The volume + # can't be deleted until the dependent snapshot is gone + self.addCleanup(wait_for_delete, self, 'Snapshot', snapshot, + self.client.volume_snapshots.get) + + # Wait for the snapshot to become available + for x in six.moves.range(60): + snapshot = self.client.volume_snapshots.get(snapshot.id) + if snapshot.status == 'available': + break + elif snapshot.status == 'error': + self.fail('Snapshot %s is in error state' % snapshot.id) + time.sleep(1) + else: + self.fail('Snapshot %s not available after 60s' % snapshot.id) + + # List snapshots + self.client.volume_snapshots.list() + + # List servers again to make sure things are still good + self.client.servers.list() + + # Create a volume type + # TODO(melwitt): Use a better random name + name = str(uuid.uuid4()) + volume_type = self.client.volume_types.create(name) + + # This cleanup tests volume type delete + self.addCleanup(self.client.volume_types.delete, volume_type.id) + + # Get the volume type + volume_type = self.client.volume_types.get(volume_type.id) + + # List all volume types + self.client.volume_types.list() + + # One more servers list + self.client.servers.list() diff --git a/novaclient/tests/unit/test_client.py b/novaclient/tests/unit/test_client.py index db4c87d09..d240321f7 100644 --- a/novaclient/tests/unit/test_client.py +++ b/novaclient/tests/unit/test_client.py @@ -86,6 +86,7 @@ def test_client_reauth(self): auth_url="http://www.blah.com") instance.auth_token = 'foobar' instance.management_url = 'http://example.com' + instance.get_service_url = mock.Mock(return_value='http://example.com') instance.version = 'v2.0' mock_request = mock.Mock() mock_request.side_effect = novaclient.exceptions.Unauthorized(401) @@ -136,6 +137,8 @@ def _check_version_url(self, management_url, version_url, mock_request): auth_url="http://www.blah.com") instance.auth_token = 'foobar' instance.management_url = management_url % projectid + mock_get_service_url = mock.Mock(return_value=instance.management_url) + instance.get_service_url = mock_get_service_url instance.version = 'v2.0' # If passing None as the part of url, a client accesses the url which diff --git a/novaclient/tests/unit/test_http.py b/novaclient/tests/unit/test_http.py index 5e676828a..a80d94426 100644 --- a/novaclient/tests/unit/test_http.py +++ b/novaclient/tests/unit/test_http.py @@ -82,6 +82,7 @@ def get_authed_client(): cl = get_client() cl.management_url = "http://example.com" cl.auth_token = "token" + cl.get_service_url = mock.Mock(return_value="http://example.com") return cl diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 07db412b3..343c421e2 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -2196,6 +2196,7 @@ def __init__(self, *args, **kwargs): self.callstack = [] self.auth = mock.Mock() self.session = mock.Mock() + self.service_type = 'service_type' self.auth.get_auth_ref.return_value.project_id = 'tenant_id' diff --git a/novaclient/tests/unit/v2/test_auth.py b/novaclient/tests/unit/v2/test_auth.py index 95e96ded1..696428a4a 100644 --- a/novaclient/tests/unit/v2/test_auth.py +++ b/novaclient/tests/unit/v2/test_auth.py @@ -363,6 +363,7 @@ def test_auth_automatic(self): "project_id", utils.AUTH_URL) http_client = cs.client http_client.management_url = '' + http_client.get_service_url = mock.Mock(return_value='') mock_request = mock.Mock(return_value=(None, None)) @mock.patch.object(http_client, 'request', mock_request) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 6ce1681ab..7e3a53100 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -1983,7 +1983,6 @@ def _translate_availability_zone_keys(collection): type=int, const=1, help=argparse.SUPPRESS) -@cliutils.service_type('volume') def do_volume_list(cs, args): """List all the volumes.""" search_opts = {'all_tenants': args.all_tenants} @@ -2002,7 +2001,6 @@ def do_volume_list(cs, args): 'volume', metavar='', help=_('Name or ID of the volume.')) -@cliutils.service_type('volume') def do_volume_show(cs, args): """Show details about a volume.""" volume = _find_volume(cs, args.volume) @@ -2055,7 +2053,6 @@ def do_volume_show(cs, args): '--availability-zone', metavar='', help=_('Optional Availability Zone for volume. (Default=None)'), default=None) -@cliutils.service_type('volume') def do_volume_create(cs, args): """Add a new volume.""" volume = cs.volumes.create(args.size, @@ -2072,7 +2069,6 @@ def do_volume_create(cs, args): 'volume', metavar='', nargs='+', help=_('Name or ID of the volume(s) to delete.')) -@cliutils.service_type('volume') def do_volume_delete(cs, args): """Remove volume(s).""" for volume in args.volume: @@ -2139,7 +2135,6 @@ def do_volume_detach(cs, args): args.attachment_id) -@cliutils.service_type('volume') def do_volume_snapshot_list(cs, _args): """List all the snapshots.""" snapshots = cs.volume_snapshots.list() @@ -2152,7 +2147,6 @@ def do_volume_snapshot_list(cs, _args): 'snapshot', metavar='', help=_('Name or ID of the snapshot.')) -@cliutils.service_type('volume') def do_volume_snapshot_show(cs, args): """Show details about a snapshot.""" snapshot = _find_volume_snapshot(cs, args.snapshot) @@ -2185,7 +2179,6 @@ def do_volume_snapshot_show(cs, args): @cliutils.arg( '--display_description', help=argparse.SUPPRESS) -@cliutils.service_type('volume') def do_volume_snapshot_create(cs, args): """Add a new snapshot.""" snapshot = cs.volume_snapshots.create(args.volume_id, @@ -2199,7 +2192,6 @@ def do_volume_snapshot_create(cs, args): 'snapshot', metavar='', help=_('Name or ID of the snapshot to delete.')) -@cliutils.service_type('volume') def do_volume_snapshot_delete(cs, args): """Remove a snapshot.""" snapshot = _find_volume_snapshot(cs, args.snapshot) @@ -2210,7 +2202,6 @@ def _print_volume_type_list(vtypes): utils.print_list(vtypes, ['ID', 'Name']) -@cliutils.service_type('volume') def do_volume_type_list(cs, args): """Print a list of available 'volume types'.""" vtypes = cs.volume_types.list() @@ -2221,7 +2212,6 @@ def do_volume_type_list(cs, args): 'name', metavar='', help=_("Name of the new volume type")) -@cliutils.service_type('volume') def do_volume_type_create(cs, args): """Create a new volume type.""" vtype = cs.volume_types.create(args.name) @@ -2232,7 +2222,6 @@ def do_volume_type_create(cs, args): 'id', metavar='', help=_("Unique ID of the volume type to delete")) -@cliutils.service_type('volume') def do_volume_type_delete(cs, args): """Delete a specific volume type.""" cs.volume_types.delete(args.id) diff --git a/novaclient/v2/volume_snapshots.py b/novaclient/v2/volume_snapshots.py index d3bc91808..6ff6deb9e 100644 --- a/novaclient/v2/volume_snapshots.py +++ b/novaclient/v2/volume_snapshots.py @@ -55,11 +55,12 @@ def create(self, volume_id, force=False, display_name=None, :param display_description: Description of the snapshot :rtype: :class:`Snapshot` """ - body = {'snapshot': {'volume_id': volume_id, - 'force': force, - 'display_name': display_name, - 'display_description': display_description}} - return self._create('/snapshots', body, 'snapshot') + with self.alternate_service_type('volume'): + body = {'snapshot': {'volume_id': volume_id, + 'force': force, + 'display_name': display_name, + 'display_description': display_description}} + return self._create('/snapshots', body, 'snapshot') def get(self, snapshot_id): """ @@ -68,7 +69,8 @@ def get(self, snapshot_id): :param snapshot_id: The ID of the snapshot to get. :rtype: :class:`Snapshot` """ - return self._get("/snapshots/%s" % snapshot_id, "snapshot") + with self.alternate_service_type('volume'): + return self._get("/snapshots/%s" % snapshot_id, "snapshot") def list(self, detailed=True): """ @@ -76,10 +78,11 @@ def list(self, detailed=True): :rtype: list of :class:`Snapshot` """ - if detailed is True: - return self._list("/snapshots/detail", "snapshots") - else: - return self._list("/snapshots", "snapshots") + with self.alternate_service_type('volume'): + if detailed is True: + return self._list("/snapshots/detail", "snapshots") + else: + return self._list("/snapshots", "snapshots") def delete(self, snapshot): """ @@ -87,4 +90,5 @@ def delete(self, snapshot): :param snapshot: The :class:`Snapshot` to delete. """ - self._delete("/snapshots/%s" % base.getid(snapshot)) + with self.alternate_service_type('volume'): + self._delete("/snapshots/%s" % base.getid(snapshot)) diff --git a/novaclient/v2/volume_types.py b/novaclient/v2/volume_types.py index 3d1c7f531..e36225b45 100644 --- a/novaclient/v2/volume_types.py +++ b/novaclient/v2/volume_types.py @@ -41,7 +41,8 @@ def list(self): :rtype: list of :class:`VolumeType`. """ - return self._list("/types", "volume_types") + with self.alternate_service_type('volume'): + return self._list("/types", "volume_types") def get(self, volume_type): """ @@ -50,7 +51,9 @@ def get(self, volume_type): :param volume_type: The ID of the :class:`VolumeType` to get. :rtype: :class:`VolumeType` """ - return self._get("/types/%s" % base.getid(volume_type), "volume_type") + with self.alternate_service_type('volume'): + return self._get("/types/%s" % base.getid(volume_type), + "volume_type") def delete(self, volume_type): """ @@ -58,7 +61,8 @@ def delete(self, volume_type): :param volume_type: The ID of the :class:`VolumeType` to get. """ - self._delete("/types/%s" % base.getid(volume_type)) + with self.alternate_service_type('volume'): + self._delete("/types/%s" % base.getid(volume_type)) def create(self, name): """ @@ -67,11 +71,10 @@ def create(self, name): :param name: Descriptive name of the volume type :rtype: :class:`VolumeType` """ - - body = { - "volume_type": { - "name": name, + with self.alternate_service_type('volume'): + body = { + "volume_type": { + "name": name, + } } - } - - return self._create("/types", body, "volume_type") + return self._create("/types", body, "volume_type") diff --git a/novaclient/v2/volumes.py b/novaclient/v2/volumes.py index 99e3d6fed..182698ff5 100644 --- a/novaclient/v2/volumes.py +++ b/novaclient/v2/volumes.py @@ -60,14 +60,16 @@ def create(self, size, snapshot_id=None, display_name=None, :rtype: :class:`Volume` :param imageRef: reference to an image stored in glance """ - body = {'volume': {'size': size, - 'snapshot_id': snapshot_id, - 'display_name': display_name, - 'display_description': display_description, - 'volume_type': volume_type, - 'availability_zone': availability_zone, - 'imageRef': imageRef}} - return self._create('/volumes', body, 'volume') + # NOTE(melwitt): Ensure we use the volume endpoint for this call + with self.alternate_service_type('volume'): + body = {'volume': {'size': size, + 'snapshot_id': snapshot_id, + 'display_name': display_name, + 'display_description': display_description, + 'volume_type': volume_type, + 'availability_zone': availability_zone, + 'imageRef': imageRef}} + return self._create('/volumes', body, 'volume') def get(self, volume_id): """ @@ -76,7 +78,8 @@ def get(self, volume_id): :param volume_id: The ID of the volume to get. :rtype: :class:`Volume` """ - return self._get("/volumes/%s" % volume_id, "volume") + with self.alternate_service_type('volume'): + return self._get("/volumes/%s" % volume_id, "volume") def list(self, detailed=True, search_opts=None): """ @@ -84,19 +87,21 @@ def list(self, detailed=True, search_opts=None): :rtype: list of :class:`Volume` """ - search_opts = search_opts or {} + with self.alternate_service_type('volume'): + search_opts = search_opts or {} - if 'name' in search_opts.keys(): - search_opts['display_name'] = search_opts.pop('name') + if 'name' in search_opts.keys(): + search_opts['display_name'] = search_opts.pop('name') - qparams = dict((k, v) for (k, v) in six.iteritems(search_opts) if v) + qparams = dict((k, v) for (k, v) in + six.iteritems(search_opts) if v) - query_string = '?%s' % parse.urlencode(qparams) if qparams else '' + query_str = '?%s' % parse.urlencode(qparams) if qparams else '' - if detailed is True: - return self._list("/volumes/detail%s" % query_string, "volumes") - else: - return self._list("/volumes%s" % query_string, "volumes") + if detailed is True: + return self._list("/volumes/detail%s" % query_str, "volumes") + else: + return self._list("/volumes%s" % query_str, "volumes") def delete(self, volume): """ @@ -104,7 +109,8 @@ def delete(self, volume): :param volume: The :class:`Volume` to delete. """ - self._delete("/volumes/%s" % base.getid(volume)) + with self.alternate_service_type('volume'): + self._delete("/volumes/%s" % base.getid(volume)) def create_server_volume(self, server_id, volume_id, device): """ From 8679eedb8352630012202c12a9c9acf8757802a5 Mon Sep 17 00:00:00 2001 From: jichenjc Date: Thu, 19 Mar 2015 22:58:17 +0800 Subject: [PATCH 0719/1705] Revert 'Remove image to local block device mapping' https://review.openstack.org/#/c/153203/3 added check for novaclient, removed the logic for both --image and --block-device are added. But actually the following valid boot command failed due to the change here, so this patch reverted original one and add some descriptions to avoid further removal. nova boot test-vm --flavor m1.medium --image centos-vm-32 --nic net-id=c3f40e33-d535-4217-916b-1450b8cd3987 --block-device id=26b7b917-2794-452a-95e5-2efb2ca6e32d,bus=sata, source=volume,bootindex=1 Change-Id: Ia29e63c72b34d3038aa591c466425e65edf5755b Partial-Bug: #1433609 --- novaclient/tests/unit/v2/test_shell.py | 7 +++++++ novaclient/v2/servers.py | 8 ++++++++ 2 files changed, 15 insertions(+) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index b7e8b9e9d..b1b9b8085 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -292,6 +292,13 @@ def test_boot_image_bdms_v2(self): 'flavorRef': '1', 'name': 'some-server', 'block_device_mapping_v2': [ + { + 'uuid': 1, + 'source_type': 'image', + 'destination_type': 'local', + 'boot_index': 0, + 'delete_on_termination': True, + }, { 'uuid': 'fake-id', 'source_type': 'volume', diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index 397a12ac1..582c4d840 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -492,6 +492,14 @@ def _boot(self, resource_url, response_key, name, image, flavor, body['server']['block_device_mapping'] = \ self._parse_block_device_mapping(block_device_mapping) elif block_device_mapping_v2: + # Following logic can't be removed because it will leaves + # a valid boot with both --image and --block-device + # failed , see bug 1433609 for more info + if image: + bdm_dict = {'uuid': image.id, 'source_type': 'image', + 'destination_type': 'local', 'boot_index': 0, + 'delete_on_termination': True} + block_device_mapping_v2.insert(0, bdm_dict) body['server']['block_device_mapping_v2'] = block_device_mapping_v2 if nics is not None: From 025bcfb1e375f8ea419e6fa6368dffbb6681afa8 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Sat, 21 Mar 2015 00:17:56 +0000 Subject: [PATCH 0720/1705] Updated from global requirements Change-Id: I88ec1f9d3356c8d384b56abc06696755ee3fa2a5 --- requirements.txt | 6 +++--- test-requirements.txt | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/requirements.txt b/requirements.txt index bc729cf8c..77218d26d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,9 +4,9 @@ pbr>=0.6,!=0.7,<1.0 argparse iso8601>=0.1.9 -oslo.i18n>=1.3.0 # Apache-2.0 -oslo.serialization>=1.2.0 # Apache-2.0 -oslo.utils>=1.2.0 # Apache-2.0 +oslo.i18n>=1.5.0,<1.6.0 # Apache-2.0 +oslo.serialization>=1.4.0,<1.5.0 # Apache-2.0 +oslo.utils>=1.4.0,<1.5.0 # Apache-2.0 PrettyTable>=0.7,<0.8 requests>=2.2.0,!=2.4.0 simplejson>=2.2.0 diff --git a/test-requirements.txt b/test-requirements.txt index acdbc2441..7e17e032f 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -8,10 +8,10 @@ discover fixtures>=0.3.14 keyring>=2.1,!=3.3 mock>=1.0 -requests-mock>=0.5.1 # Apache-2.0 +requests-mock>=0.6.0 # Apache-2.0 sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3 -oslosphinx>=2.2.0 # Apache-2.0 +oslosphinx>=2.5.0,<2.6.0 # Apache-2.0 testrepository>=0.0.18 testscenarios>=0.4 testtools>=0.9.36,!=1.2.0 -tempest-lib>=0.3.0 +tempest-lib>=0.4.0 From 1c39f8fabf8ed44e0de563893d5813be1bf7b5e3 Mon Sep 17 00:00:00 2001 From: ZhiQiang Fan Date: Wed, 18 Mar 2015 17:37:54 +0800 Subject: [PATCH 0721/1705] Don't record time when self.timing is False The expected behavior is when timing is True, then we record each request's time, then we should call reset_timings() to release memory. However, the shell, client.{HTTPClient,SessionClient} will record each request's time no matter what timing is set, then after long running time in service like ceilometer-agent-compute, the memory keeps increasing. We'd better not record request's time when timing is set to False. Users are not responiable to call reset_timings() when they don't want timing functionality. Change-Id: I3e7d2fadf9a21be018781d528a1b6562228da6dd Closes-Bug: #1433491 --- novaclient/client.py | 25 ++++++++----------- novaclient/shell.py | 37 +++++++++++++--------------- novaclient/tests/unit/test_client.py | 32 ++++++++++++++++++++++++ novaclient/tests/unit/test_shell.py | 10 ++++++++ novaclient/tests/unit/test_utils.py | 19 ++++++++++++++ novaclient/utils.py | 22 +++++++++++++++++ 6 files changed, 110 insertions(+), 35 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index 32eb77505..007cf9140 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -26,7 +26,6 @@ import logging import re import socket -import time from keystoneclient import adapter from oslo.utils import importutils @@ -44,6 +43,7 @@ from novaclient import exceptions from novaclient.i18n import _ from novaclient import service_catalog +from novaclient import utils class TCPKeepAliveAdapter(adapters.HTTPAdapter): @@ -76,22 +76,18 @@ class SessionClient(adapter.LegacyJsonAdapter): def __init__(self, *args, **kwargs): self.times = [] + self.timings = kwargs.pop('timings', False) super(SessionClient, self).__init__(*args, **kwargs) def request(self, url, method, **kwargs): # NOTE(jamielennox): The standard call raises errors from # keystoneclient, where we need to raise the novaclient errors. raise_exc = kwargs.pop('raise_exc', True) - start_time = time.time() - resp, body = super(SessionClient, self).request(url, - method, - raise_exc=False, - **kwargs) - - end_time = time.time() - self.times.append(('%s %s' % (method, url), - start_time, end_time)) - + with utils.record_time(self.times, self.timings, method, url): + resp, body = super(SessionClient, self).request(url, + method, + raise_exc=False, + **kwargs) if raise_exc and resp.status_code >= 400: raise exceptions.from_response(resp, body, url, method) @@ -393,10 +389,8 @@ def request(self, url, method, **kwargs): return resp, body def _time_request(self, url, method, **kwargs): - start_time = time.time() - resp, body = self.request(url, method, **kwargs) - self.times.append(("%s %s" % (method, url), - start_time, time.time())) + with utils.record_time(self.times, self.timings, method, url): + resp, body = self.request(url, method, **kwargs) return resp, body def _cs_request(self, url, method, **kwargs): @@ -686,6 +680,7 @@ def _construct_http_client(username=None, password=None, project_id=None, region_name=region_name, service_name=service_name, user_agent=user_agent, + timings=timings, **kwargs) else: # FIXME(jamielennox): username and password are now optional. Need diff --git a/novaclient/shell.py b/novaclient/shell.py index 652713ba6..439a98d99 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -28,7 +28,6 @@ import os import pkgutil import sys -import time from keystoneclient.auth.identity.generic import password from keystoneclient.auth.identity.generic import token @@ -717,25 +716,23 @@ def main(self, argv): project_name = args.os_project_name or args.os_tenant_name if use_session: # Not using Nova auth plugin, so use keystone - start_time = time.time() - keystone_session = ksession.Session.load_from_cli_options(args) - keystone_auth = self._get_keystone_auth( - keystone_session, - args.os_auth_url, - username=args.os_username, - user_id=args.os_user_id, - user_domain_id=args.os_user_domain_id, - user_domain_name=args.os_user_domain_name, - password=args.os_password, - auth_token=args.os_auth_token, - project_id=project_id, - project_name=project_name, - project_domain_id=args.os_project_domain_id, - project_domain_name=args.os_project_domain_name) - end_time = time.time() - self.times.append( - ('%s %s' % ('auth_url', args.os_auth_url), - start_time, end_time)) + with utils.record_time(self.times, args.timings, + 'auth_url', args.os_auth_url): + keystone_session = (ksession.Session + .load_from_cli_options(args)) + keystone_auth = self._get_keystone_auth( + keystone_session, + args.os_auth_url, + username=args.os_username, + user_id=args.os_user_id, + user_domain_id=args.os_user_domain_id, + user_domain_name=args.os_user_domain_name, + password=args.os_password, + auth_token=args.os_auth_token, + project_id=project_id, + project_name=project_name, + project_domain_id=args.os_project_domain_id, + project_domain_name=args.os_project_domain_name) if (options.os_compute_api_version and options.os_compute_api_version != '1.0'): diff --git a/novaclient/tests/unit/test_client.py b/novaclient/tests/unit/test_client.py index d240321f7..805278cca 100644 --- a/novaclient/tests/unit/test_client.py +++ b/novaclient/tests/unit/test_client.py @@ -19,6 +19,7 @@ import socket import fixtures +from keystoneclient import adapter import mock import requests @@ -388,3 +389,34 @@ def test_log_resp(self): self.assertIn('RESP BODY: {"access": {"token": {"id":' ' "{SHA1}4fc49c6a671ce889078ff6b250f7066cf6d2ada2"}}}', output) + + @mock.patch.object(novaclient.client.HTTPClient, 'request') + def test_timings(self, m_request): + m_request.return_value = (None, None) + + client = novaclient.client.HTTPClient(user='zqfan', password='') + client._time_request("http://no.where", 'GET') + self.assertEqual(0, len(client.times)) + + client = novaclient.client.HTTPClient(user='zqfan', password='', + timings=True) + client._time_request("http://no.where", 'GET') + self.assertEqual(1, len(client.times)) + self.assertEqual('GET http://no.where', client.times[0][0]) + + +class SessionClientTest(utils.TestCase): + + @mock.patch.object(adapter.LegacyJsonAdapter, 'request') + def test_timings(self, m_request): + m_request.return_value = (mock.MagicMock(status_code=200), None) + + client = novaclient.client.SessionClient(session=mock.MagicMock()) + client.request("http://no.where", 'GET') + self.assertEqual(0, len(client.times)) + + client = novaclient.client.SessionClient(session=mock.MagicMock(), + timings=True) + client.request("http://no.where", 'GET') + self.assertEqual(1, len(client.times)) + self.assertEqual('GET http://no.where', client.times[0][0]) diff --git a/novaclient/tests/unit/test_shell.py b/novaclient/tests/unit/test_shell.py index e0995113b..f284eb88e 100644 --- a/novaclient/tests/unit/test_shell.py +++ b/novaclient/tests/unit/test_shell.py @@ -373,6 +373,16 @@ def test_main_keyboard_interrupt(self, mock_compute_shell): except SystemExit as ex: self.assertEqual(ex.code, 130) + @mock.patch.object(novaclient.shell.OpenStackComputeShell, 'times') + @requests_mock.Mocker() + def test_timing(self, m_times, m_requests): + m_times.append.side_effect = RuntimeError('Boom!') + self.make_env() + self.register_keystone_discovery_fixture(m_requests) + self.shell('list') + exc = self.assertRaises(RuntimeError, self.shell, '--timings list') + self.assertEqual('Boom!', str(exc)) + class ShellTestKeystoneV3(ShellTest): def make_env(self, exclude=None, fake_env=FAKE_ENV): diff --git a/novaclient/tests/unit/test_utils.py b/novaclient/tests/unit/test_utils.py index da68ce792..8bcb2bfcd 100644 --- a/novaclient/tests/unit/test_utils.py +++ b/novaclient/tests/unit/test_utils.py @@ -355,3 +355,22 @@ def test_do_action_on_many_first_fails(self): def test_do_action_on_many_last_fails(self): self._test_do_action_on_many([None, Exception()], fail=True) + + +class RecordTimeTestCase(test_utils.TestCase): + + def test_record_time(self): + times = [] + + with utils.record_time(times, True, 'a', 'b'): + pass + self.assertEqual(1, len(times)) + self.assertEqual(3, len(times[0])) + self.assertEqual('a b', times[0][0]) + self.assertIsInstance(times[0][1], float) + self.assertIsInstance(times[0][2], float) + + times = [] + with utils.record_time(times, False, 'x'): + pass + self.assertEqual(0, len(times)) diff --git a/novaclient/utils.py b/novaclient/utils.py index 289321c1b..2cf4fc619 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -11,9 +11,11 @@ # License for the specific language governing permissions and limitations # under the License. +import contextlib import json import re import textwrap +import time import uuid from oslo.serialization import jsonutils @@ -339,3 +341,23 @@ def validate_flavor_metadata_keys(keys): 'numbers, spaces, underscores, periods, colons and ' 'hyphens.') raise exceptions.CommandError(msg % key) + + +@contextlib.contextmanager +def record_time(times, enabled, *args): + """Record the time of a specific action. + + :param times: A list of tuples holds time data. + :type times: list + :param enabled: Whether timing is enabled. + :type enabled: bool + :param *args: Other data to be stored besides time data, these args + will be joined to a string. + """ + if not enabled: + yield + else: + start = time.time() + yield + end = time.time() + times.append((' '.join(args), start, end)) From 0343dff9735887ddcdf977c78f16fdb46a7ed09c Mon Sep 17 00:00:00 2001 From: Chris Yeoh Date: Wed, 25 Mar 2015 12:05:40 +1030 Subject: [PATCH 0722/1705] Removes reference to v3 nova api from novaclient docs The Nova V3 API no longer exists. Although a version of the framework exists in Nova as part of the new V2.1 API which is equivalent to the V2 API there is no need to make mention of V3 in the novaclient documentation. In future we will be distinguish between legacy Nova API support (one implementation of V2) and microversions Nova API support (the new one) to reduce confusion around version numbers which will change quite a bit without the framework changing. Co-Authored-By: Andrey Kurilin Change-Id: Id0ff51e1165cb267045d7a63aff13c0e41336738 --- doc/source/api.rst | 2 +- doc/source/conf.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/api.rst b/doc/source/api.rst index 84276c9da..8bcb219b3 100644 --- a/doc/source/api.rst +++ b/doc/source/api.rst @@ -14,7 +14,7 @@ First create a client instance with your credentials:: >>> from novaclient import client >>> nova = client.Client(VERSION, USERNAME, PASSWORD, PROJECT_ID, AUTH_URL) -Here ``VERSION`` can be: ``1.1``, ``2`` and ``3``. +Here ``VERSION`` can be: ``1.1``, ``2``. Alternatively, you can create a client instance using the keystoneclient session API:: diff --git a/doc/source/conf.py b/doc/source/conf.py index 738574773..1e11705bf 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -71,7 +71,7 @@ def gen_ref(ver, title, names): "pkg": pkg, "name": name}) gen_ref(None, "Exceptions", ["exceptions"]) -gen_ref("v2", "Version 1.1, Version 2 API Reference, Version 3 API Reference", +gen_ref("v2", "Version 1.1, Version 2 API", ["client", "flavors", "images", "servers", "hosts", "agents", "aggregates", "availability_zones", "certs", "fixed_ips", "floating_ip_pools", "floating_ips", "hypervisors", "keypairs", From 4cef067d41156977cc03572bf009924b56484141 Mon Sep 17 00:00:00 2001 From: yatinkarel Date: Thu, 26 Mar 2015 12:05:57 +0530 Subject: [PATCH 0723/1705] Corrected help for nova boot when attaching block device When adding block device with swap format, size is in MB and for other formats it is in GB, so help is modified to show correct description Change-Id: I0472f2a86ed2a2a651d7bbb08976e8813fb72041 Closes-Bug: #1407383 --- novaclient/v2/shell.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 7e3a53100..33653a121 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -435,8 +435,9 @@ def _boot(cs, args): "device=name of the device (e.g. vda, xda, ...; " "if omitted, hypervisor driver chooses suitable device " "depending on selected bus), " - "size=size of the block device in GB (if omitted, " - "hypervisor driver calculates size), " + "size=size of the block device in MB(for swap) and in " + "GB(for other formats) " + "(if omitted, hypervisor driver calculates size), " "format=device will be formatted (e.g. swap, ntfs, ...; optional), " "bootindex=integer used for ordering the boot disks " "(for image backed instances it is equal to 0, " From 038c0dc62493ae98eecd5d139931b4f0a7a2d5fb Mon Sep 17 00:00:00 2001 From: tengqm Date: Thu, 26 Mar 2015 16:36:12 +0800 Subject: [PATCH 0724/1705] Fix comments on metadata number limitation The comment says that at most five key/value pairs are allowed but it is not true, at least not true any more, based on some experiments. This patch fixes the comment to avoid confusion. Change-Id: I098ded910a6c7c2a7f967e6afe14e93d231d7ace --- novaclient/v2/servers.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index 582c4d840..ba576ffd2 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -840,8 +840,7 @@ def create(self, name, image, flavor, meta=None, files=None, :param image: The :class:`Image` to boot with. :param flavor: The :class:`Flavor` to boot onto. :param meta: A dict of arbitrary key/value metadata to store for this - server. A maximum of five entries is allowed, and both - keys and values must be 255 characters or less. + server. Both keys and values must be <=255 characters. :param files: A dict of files to overrwrite on the server upon boot. Keys are file names (i.e. ``/etc/passwd``) and values are the file contents (either as a string or as a @@ -962,8 +961,7 @@ def rebuild(self, server, image, password=None, disk_config=None, be preserved when rebuilding the instance. Defaults to False. :param name: Something to name the server. :param meta: A dict of arbitrary key/value metadata to store for this - server. A maximum of five entries is allowed, and both - keys and values must be 255 characters or less. + server. Both keys and values must be <=255 characters. :param files: A dict of files to overwrite on the server upon boot. Keys are file names (i.e. ``/etc/passwd``) and values are the file contents (either as a string or as a From d614dbcab9cc8c5732ce28c655738218e147aad7 Mon Sep 17 00:00:00 2001 From: Eugeniya Kudryashova Date: Fri, 27 Mar 2015 13:00:07 +0200 Subject: [PATCH 0725/1705] Fix repr of FloatingIPBulk While creating floting ip range returned by API object doesn't contains attribute 'address', but repr tries to get it. So change list to return objects of FloatingIP and add repr to FloatingIP, also changed repr of FloatingIpRange to return actual range of addresses Closes-bug: #1437244 Change-Id: Ia9e30a3f3d82fa8dde113b40d2da372bf1e7a086 --- novaclient/tests/unit/v2/test_floating_ips_bulk.py | 6 +++--- novaclient/v2/floating_ips.py | 3 +++ novaclient/v2/floating_ips_bulk.py | 14 +++++++++----- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/novaclient/tests/unit/v2/test_floating_ips_bulk.py b/novaclient/tests/unit/v2/test_floating_ips_bulk.py index 69b553f24..34c200f3b 100644 --- a/novaclient/tests/unit/v2/test_floating_ips_bulk.py +++ b/novaclient/tests/unit/v2/test_floating_ips_bulk.py @@ -16,7 +16,7 @@ from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit.fixture_data import floatingips as data from novaclient.tests.unit import utils -from novaclient.v2 import floating_ips_bulk +from novaclient.v2 import floating_ips class FloatingIPsBulkTest(utils.FixturedTestCase): @@ -27,13 +27,13 @@ class FloatingIPsBulkTest(utils.FixturedTestCase): def test_list_floating_ips_bulk(self): fl = self.cs.floating_ips_bulk.list() self.assert_called('GET', '/os-floating-ips-bulk') - [self.assertIsInstance(f, floating_ips_bulk.FloatingIP) + [self.assertIsInstance(f, floating_ips.FloatingIP) for f in fl] def test_list_floating_ips_bulk_host_filter(self): fl = self.cs.floating_ips_bulk.list('testHost') self.assert_called('GET', '/os-floating-ips-bulk/testHost') - [self.assertIsInstance(f, floating_ips_bulk.FloatingIP) + [self.assertIsInstance(f, floating_ips.FloatingIP) for f in fl] def test_create_floating_ips_bulk(self): diff --git a/novaclient/v2/floating_ips.py b/novaclient/v2/floating_ips.py index cce92da25..7c625e997 100644 --- a/novaclient/v2/floating_ips.py +++ b/novaclient/v2/floating_ips.py @@ -24,6 +24,9 @@ def delete(self): """ self.manager.delete(self) + def __repr__(self): + return "" % self.address + class FloatingIPManager(base.ManagerWithFind): resource_class = FloatingIP diff --git a/novaclient/v2/floating_ips_bulk.py b/novaclient/v2/floating_ips_bulk.py index fb59a19e7..b0fdebf62 100644 --- a/novaclient/v2/floating_ips_bulk.py +++ b/novaclient/v2/floating_ips_bulk.py @@ -17,25 +17,29 @@ Bulk Floating IPs interface """ from novaclient import base +from novaclient.v2 import floating_ips -class FloatingIP(base.Resource): +class FloatingIPRange(base.Resource): def __repr__(self): - return "" % self.address + return "" % self.ip_range class FloatingIPBulkManager(base.ManagerWithFind): - resource_class = FloatingIP + resource_class = FloatingIPRange def list(self, host=None): """ List all floating IPs """ if host is None: - return self._list('/os-floating-ips-bulk', 'floating_ip_info') + return self._list('/os-floating-ips-bulk', + 'floating_ip_info', + obj_class=floating_ips.FloatingIP) else: return self._list('/os-floating-ips-bulk/%s' % host, - 'floating_ip_info') + 'floating_ip_info', + obj_class=floating_ips.FloatingIP) def create(self, ip_range, pool=None, interface=None): """ From 88beed19db0aeb1d77f2d138c74838be8f66985b Mon Sep 17 00:00:00 2001 From: Rajiv Kumar Date: Thu, 26 Mar 2015 04:18:08 +0000 Subject: [PATCH 0726/1705] Combine test cases for checking nova limits response nova rate-limit returns absolute limits too, but novaclient does not show absolute limit,a bug (https://bugs.launchpad.net/python-novaclient/+bug/1172254) has been filed for this one. There is no test which assert on both absolute and rate limit. This patch assert on both the rate and absolute limit returned Change-Id: Ia635f32eefc46e22e3f4e45a7885f492bb967a99 Closes-Bug: #1436653 --- novaclient/tests/unit/v2/test_limits.py | 33 +++++++++++-------------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/novaclient/tests/unit/v2/test_limits.py b/novaclient/tests/unit/v2/test_limits.py index 9dc9dec43..a72d6d602 100644 --- a/novaclient/tests/unit/v2/test_limits.py +++ b/novaclient/tests/unit/v2/test_limits.py @@ -32,23 +32,6 @@ def test_get_limits_for_a_tenant(self): self.assert_called('GET', '/limits?tenant_id=1234') self.assertIsInstance(obj, limits.Limits) - def test_absolute_limits(self): - obj = self.cs.limits.get() - - expected = ( - limits.AbsoluteLimit("maxTotalRAMSize", 51200), - limits.AbsoluteLimit("maxServerMeta", 5), - limits.AbsoluteLimit("maxImageMeta", 5), - limits.AbsoluteLimit("maxPersonality", 5), - limits.AbsoluteLimit("maxPersonalitySize", 10240), - ) - - abs_limits = list(obj.absolute) - self.assertEqual(len(abs_limits), len(expected)) - - for limit in abs_limits: - self.assertIn(limit, expected) - def test_absolute_limits_reserved(self): obj = self.cs.limits.get(reserved=True) @@ -67,7 +50,7 @@ def test_absolute_limits_reserved(self): for limit in abs_limits: self.assertIn(limit, expected) - def test_rate_limits(self): + def test_rate_absolute_limits(self): obj = self.cs.limits.get() expected = ( @@ -86,3 +69,17 @@ def test_rate_limits(self): for limit in rate_limits: self.assertIn(limit, expected) + + expected = ( + limits.AbsoluteLimit("maxTotalRAMSize", 51200), + limits.AbsoluteLimit("maxServerMeta", 5), + limits.AbsoluteLimit("maxImageMeta", 5), + limits.AbsoluteLimit("maxPersonality", 5), + limits.AbsoluteLimit("maxPersonalitySize", 10240), + ) + + abs_limits = list(obj.absolute) + self.assertEqual(len(abs_limits), len(expected)) + + for limit in abs_limits: + self.assertIn(limit, expected) From 14cada7d0d8518bebbf0705be1a93514f9d7dad4 Mon Sep 17 00:00:00 2001 From: melanie witt Date: Thu, 2 Apr 2015 00:03:30 +0000 Subject: [PATCH 0727/1705] Add --all-tenants option to 'nova delete' Currently, the all_tenants=1 search option is being passed all the time for 'nova delete' commands in order to enable 'nova delete' by name to work across tenants, for those that have all_tenants access in the nova policy.json. This however breaks all 'nova delete' for non-admins when policy has been configured to allow only admin to list servers across all_tenants. This patch changes 'nova delete' to take an option --all-tenants to get the functionality to delete by name across tenants. This is similar to how 'nova list --all-tenants' works. Closes-Bug: #1439381 Change-Id: I204daaf5c0f4dab7c93ef0bd85ffab3529ca352a --- novaclient/tests/unit/v2/test_shell.py | 16 ++++++++++++++-- novaclient/v2/shell.py | 8 +++++++- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index b1b9b8085..82ee08688 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -1119,15 +1119,27 @@ def test_delete_two_with_two_existent(self): self.assert_called('DELETE', '/servers/5678', pos=-1) self.run_command('delete sample-server sample-server2') self.assert_called('GET', - '/servers?all_tenants=1&name=sample-server', pos=-6) + '/servers?name=sample-server', pos=-6) self.assert_called('GET', '/servers/1234', pos=-5) self.assert_called('DELETE', '/servers/1234', pos=-4) self.assert_called('GET', - '/servers?all_tenants=1&name=sample-server2', + '/servers?name=sample-server2', pos=-3) self.assert_called('GET', '/servers/5678', pos=-2) self.assert_called('DELETE', '/servers/5678', pos=-1) + def test_delete_two_with_two_existent_all_tenants(self): + self.run_command('delete sample-server sample-server2 --all-tenants') + self.assert_called('GET', + '/servers?all_tenants=1&name=sample-server', pos=0) + self.assert_called('GET', '/servers/1234', pos=1) + self.assert_called('DELETE', '/servers/1234', pos=2) + self.assert_called('GET', + '/servers?all_tenants=1&name=sample-server2', + pos=3) + self.assert_called('GET', '/servers/5678', pos=4) + self.assert_called('DELETE', '/servers/5678', pos=5) + def test_delete_two_with_one_nonexistent(self): cmd = 'delete 1234 123456789' self.assertRaises(exceptions.CommandError, self.run_command, cmd) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 33653a121..4fdb0f2f8 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -1872,12 +1872,18 @@ def do_show(cs, args): _print_server(cs, args) +@cliutils.arg( + '--all-tenants', + action='store_const', + const=1, + default=0, + help=_('Delete server(s) in another tenant by name (Admin only).')) @cliutils.arg( 'server', metavar='', nargs='+', help=_('Name or ID of server(s).')) def do_delete(cs, args): """Immediately shut down and delete specified server(s).""" - find_args = {'all_tenants': '1'} + find_args = {'all_tenants': args.all_tenants} utils.do_action_on_many( lambda s: _find_server(cs, s, **find_args).delete(), args.server, From 19d4d35a4e87598f2ec0115753b147552870ceb7 Mon Sep 17 00:00:00 2001 From: melanie witt Date: Fri, 3 Apr 2015 08:52:42 +0000 Subject: [PATCH 0728/1705] Handle binary userdata files such as gzip The current code in the servers api assumes only text files will be provided as userdata. That is, the data is utf-8 encoded before it is base64 encoded and sent. This breaks for binary userdata files, (example, gzip files) as they can't be utf-8 encoded. This change ignores specifically the exceptions that are raised when utf-8 encoding is attempted on non-text data: AttributeError and UnicodeDecodeError. These exceptions will be ignored to let binary files proceed to the base64 encoding step. Closes-Bug: #1419859 Change-Id: Ie96283f6ff892ae30485722cf7c3ec40f3f1b5df --- novaclient/tests/unit/v2/test_servers.py | 30 ++++++++++++++++++++++++ novaclient/v2/servers.py | 13 ++++++++-- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index c43aed422..2b140a0b9 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -12,6 +12,10 @@ # License for the specific language governing permissions and limitations # under the License. +import base64 +import os +import tempfile + import mock from oslo.serialization import jsonutils import six @@ -199,6 +203,32 @@ def test_create_server_userdata_utf8(self): self.assert_called('POST', '/servers') self.assertIsInstance(s, servers.Server) + def test_create_server_userdata_bin(self): + with tempfile.TemporaryFile(mode='wb+') as bin_file: + original_data = os.urandom(1024) + bin_file.write(original_data) + bin_file.flush() + bin_file.seek(0) + s = self.cs.servers.create( + name="My server", + image=1, + flavor=1, + meta={'foo': 'bar'}, + userdata=bin_file, + key_name="fakekey", + files={ + '/etc/passwd': 'some data', # a file + '/tmp/foo.txt': six.StringIO('data'), # a stream + }, + ) + self.assert_called('POST', '/servers') + self.assertIsInstance(s, servers.Server) + # verify userdata matches original + body = jsonutils.loads(self.requests.last_request.body) + transferred_data = body['server']['user_data'] + transferred_data = base64.b64decode(transferred_data) + self.assertEqual(original_data, transferred_data) + def _create_disk_config(self, disk_config): s = self.cs.servers.create( name="My server", diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index ba576ffd2..79f9f1870 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -433,10 +433,19 @@ def _boot(self, resource_url, response_key, name, image, flavor, if hasattr(userdata, 'read'): userdata = userdata.read() + # NOTE(melwitt): Text file data is converted to bytes prior to + # base64 encoding. The utf-8 encoding will fail for binary files. if six.PY3: - userdata = userdata.encode("utf-8") + try: + userdata = userdata.encode("utf-8") + except AttributeError: + # In python 3, 'bytes' object has no attribute 'encode' + pass else: - userdata = encodeutils.safe_encode(userdata) + try: + userdata = encodeutils.safe_encode(userdata) + except UnicodeDecodeError: + pass userdata_b64 = base64.b64encode(userdata).decode('utf-8') body["server"]["user_data"] = userdata_b64 From a63aa515f579c2ccce6c75eca4a3b2e0be6f331a Mon Sep 17 00:00:00 2001 From: Feodor Tersin Date: Mon, 6 Apr 2015 15:31:16 +0300 Subject: [PATCH 0729/1705] Fix displaying of an unavailable flavor of a showing instance Since then an instance was booted with a flavor, the flavor could be deleted or become unavailable (private flavors). As a result novaclient fails to show the instance with member role user. This patch set adds handling of unavailable flavors to showing of instances as it is done for unavailable images. Closes-Bug: #1366168 Change-Id: I810dcac814b7523313112bb9151754659b85df51 --- novaclient/tests/unit/v2/fakes.py | 24 +++++++++++++++++++++++- novaclient/tests/unit/v2/test_shell.py | 12 ++++++++++++ novaclient/v2/shell.py | 7 +++++-- 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 343c421e2..1c034593d 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -362,7 +362,19 @@ def get_servers_detail(self, **kw): "metadata": { "Server Label": "DB 1" } - } + }, + { + "id": 9013, + "name": "sample-server4", + "flavor": { + "id": '80645cf4-6ad3-410a-bbc8-6f3e1e291f51', + }, + "image": { + "id": '3e861307-73a6-4d1f-8d68-f68b03223032', + }, + "hostId": "9e107d9d372bb6826bd81d3542a419d6", + "status": "ACTIVE", + }, ]}) def post_servers(self, body, **kw): @@ -416,6 +428,10 @@ def get_servers_9012(self, **kw): r = {'server': self.get_servers_detail()[2]['servers'][2]} return (200, {}, r) + def get_servers_9013(self, **kw): + r = {'server': self.get_servers_detail()[2]['servers'][3]} + return (200, {}, r) + def put_servers_1234(self, body, **kw): assert list(body) == ['server'] fakes.assert_has_keys(body['server'], optional=['name', 'adminPass']) @@ -748,6 +764,9 @@ def get_flavors_512_mb_server(self, **kw): def get_flavors_128_mb_server(self, **kw): raise exceptions.NotFound('404') + def get_flavors_80645cf4_6ad3_410a_bbc8_6f3e1e291f51(self, **kw): + raise exceptions.NotFound('404') + def get_flavors_aa1(self, **kw): # Alphanumeric flavor id are allowed. return ( @@ -998,6 +1017,9 @@ def get_images_2(self, **kw): def get_images_456(self, **kw): return (200, {}, {'image': self.get_images_detail()[2]['images'][1]}) + def get_images_3e861307_73a6_4d1f_8d68_f68b03223032(self): + raise exceptions.NotFound('404') + def post_images(self, body, **kw): assert list(body) == ['image'] fakes.assert_has_keys(body['image'], required=['serverId', 'name']) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index b1b9b8085..a0e1594c3 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -1085,6 +1085,18 @@ def test_show_bad_id(self): self.assertRaises(exceptions.CommandError, self.run_command, 'show xxx') + def test_show_unavailable_image_and_flavor(self): + output = self.run_command('show 9013') + self.assert_called('GET', '/servers/9013', pos=-8) + self.assert_called('GET', + '/flavors/80645cf4-6ad3-410a-bbc8-6f3e1e291f51', + pos=-7) + self.assert_called('GET', + '/images/3e861307-73a6-4d1f-8d68-f68b03223032', + pos=-3) + self.assertIn('Image not found', output) + self.assertIn('Flavor not found', output) + @mock.patch('novaclient.v2.shell.utils.print_dict') def test_print_server(self, mock_print_dict): self.run_command('show 5678') diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 33653a121..36e74ed70 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -1830,8 +1830,11 @@ def _print_server(cs, args, server=None): if minimal: info['flavor'] = flavor_id else: - info['flavor'] = '%s (%s)' % (_find_flavor(cs, flavor_id).name, - flavor_id) + try: + info['flavor'] = '%s (%s)' % (_find_flavor(cs, flavor_id).name, + flavor_id) + except Exception: + info['flavor'] = '%s (%s)' % (_("Flavor not found"), flavor_id) if 'security_groups' in info: # when we have multiple nics the info will include the From 2761606fbf9c73860aa665c8af3a55bff48ce6b6 Mon Sep 17 00:00:00 2001 From: Ramaraja Ramachandran Date: Wed, 1 Apr 2015 11:31:46 +0530 Subject: [PATCH 0730/1705] Report better error message --ephemeral poor usage The eph_dict expects the format size=value.Catch valueError and return proper error text Change-Id: I99ef7efe7dc14aa346913009e244ad4eea0369a9 Closes-bug: #1433200 --- novaclient/tests/unit/v2/test_shell.py | 5 +++++ novaclient/v2/shell.py | 7 +++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index b1b9b8085..9b3728e0b 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -16,6 +16,7 @@ # License for the specific language governing permissions and limitations # under the License. +import argparse import base64 import datetime import os @@ -686,6 +687,10 @@ def test_boot_named_flavor(self): } }, pos=4) + def test_boot_invalid_ephemeral_data_format(self): + cmd = 'boot --flavor 1 --image 1 --ephemeral 1 some-server' + self.assertRaises(argparse.ArgumentTypeError, self.run_command, cmd) + def test_flavor_list(self): self.run_command('flavor-list') self.assert_called_anytime('GET', '/flavors/detail') diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 33653a121..d3108a941 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -118,8 +118,11 @@ def _parse_block_device_mapping_v2(args, image): for ephemeral_spec in args.ephemeral: bdm_dict = {'source_type': 'blank', 'destination_type': 'local', 'boot_index': -1, 'delete_on_termination': True} - - eph_dict = dict(v.split('=') for v in ephemeral_spec.split(',')) + try: + eph_dict = dict(v.split('=') for v in ephemeral_spec.split(',')) + except ValueError: + err_msg = (_("Invalid ephemeral argument '%s'.") % args.ephemeral) + raise argparse.ArgumentTypeError(err_msg) if 'size' in eph_dict: bdm_dict['volume_size'] = eph_dict['size'] if 'format' in eph_dict: From 97993537812c5979781a36b50d2adf8db57f7424 Mon Sep 17 00:00:00 2001 From: Doug Hellmann Date: Thu, 16 Apr 2015 18:13:32 +0000 Subject: [PATCH 0731/1705] Uncap library requirements for liberty Change-Id: Ia92f6e83fa2f5229606095ca41bc638cdab99fee Depends-On: Ib948b756b8e6ca47a4c9c44c48031e54b7386a06 --- requirements.txt | 6 +++--- test-requirements.txt | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index 77218d26d..9fe2b9542 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,9 +4,9 @@ pbr>=0.6,!=0.7,<1.0 argparse iso8601>=0.1.9 -oslo.i18n>=1.5.0,<1.6.0 # Apache-2.0 -oslo.serialization>=1.4.0,<1.5.0 # Apache-2.0 -oslo.utils>=1.4.0,<1.5.0 # Apache-2.0 +oslo.i18n>=1.5.0 # Apache-2.0 +oslo.serialization>=1.4.0 # Apache-2.0 +oslo.utils>=1.4.0 # Apache-2.0 PrettyTable>=0.7,<0.8 requests>=2.2.0,!=2.4.0 simplejson>=2.2.0 diff --git a/test-requirements.txt b/test-requirements.txt index 7e17e032f..6790cc68a 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -10,7 +10,7 @@ keyring>=2.1,!=3.3 mock>=1.0 requests-mock>=0.6.0 # Apache-2.0 sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3 -oslosphinx>=2.5.0,<2.6.0 # Apache-2.0 +oslosphinx>=2.5.0 # Apache-2.0 testrepository>=0.0.18 testscenarios>=0.4 testtools>=0.9.36,!=1.2.0 From c4d2f92116ca3a2b01254156cc04ff2e9eaeadc4 Mon Sep 17 00:00:00 2001 From: Ken'ichi Ohmichi Date: Fri, 17 Apr 2015 05:35:54 +0000 Subject: [PATCH 0732/1705] Fix typo on class Client sample Current Client sample doesn't work because the imported class is not used in the sample. This patch fixes it. Change-Id: I01d4ec277a8858a14f4e3a50b8de130aa5a4f0b5 --- novaclient/v2/client.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/novaclient/v2/client.py b/novaclient/v2/client.py index bbb8949d9..c759dcec6 100644 --- a/novaclient/v2/client.py +++ b/novaclient/v2/client.py @@ -62,7 +62,7 @@ class Client(object): >>> from keystoneclient.auth.identity import v2 >>> from keystoneclient import session - >>> from novaclient.client import Client + >>> from novaclient import client >>> auth = v2.Password(auth_url=AUTH_URL, username=USERNAME, password=PASSWORD, @@ -72,9 +72,9 @@ class Client(object): Then call methods on its managers:: - >>> client.servers.list() + >>> nova.servers.list() ... - >>> client.flavors.list() + >>> nova.flavors.list() ... It is also possible to use an instance as a context manager in which From ccff3d3c948c1b0b3ce28f6928e39915b16d80ee Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Mon, 26 Jan 2015 16:37:53 +0200 Subject: [PATCH 0733/1705] Remove all imports from oslo namespace oslo namespace is deprecated Change-Id: I345eb210222d8e973b99f27821099f52883ab27d --- novaclient/client.py | 4 ++-- novaclient/i18n.py | 6 +++--- novaclient/shell.py | 4 ++-- novaclient/tests/unit/fixture_data/floatingips.py | 2 +- novaclient/tests/unit/fixture_data/hosts.py | 2 +- novaclient/tests/unit/fixture_data/images.py | 2 +- novaclient/tests/unit/fixture_data/keypairs.py | 2 +- novaclient/tests/unit/fixture_data/networks.py | 2 +- novaclient/tests/unit/fixture_data/security_group_rules.py | 2 +- novaclient/tests/unit/fixture_data/security_groups.py | 2 +- novaclient/tests/unit/fixture_data/server_groups.py | 2 +- novaclient/tests/unit/fixture_data/servers.py | 2 +- novaclient/tests/unit/utils.py | 2 +- novaclient/tests/unit/v2/fakes.py | 2 +- novaclient/tests/unit/v2/test_servers.py | 2 +- novaclient/tests/unit/v2/test_shell.py | 2 +- novaclient/utils.py | 4 ++-- novaclient/v2/flavors.py | 2 +- novaclient/v2/servers.py | 2 +- novaclient/v2/shell.py | 6 +++--- 20 files changed, 27 insertions(+), 27 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index 007cf9140..85d618da1 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -28,8 +28,8 @@ import socket from keystoneclient import adapter -from oslo.utils import importutils -from oslo.utils import netutils +from oslo_utils import importutils +from oslo_utils import netutils import requests from requests import adapters diff --git a/novaclient/i18n.py b/novaclient/i18n.py index e9d39d4ab..6a17312d6 100644 --- a/novaclient/i18n.py +++ b/novaclient/i18n.py @@ -10,16 +10,16 @@ # License for the specific language governing permissions and limitations # under the License. -"""oslo.i18n integration module for novaclient. +"""oslo_i18n integration module for novaclient. See http://docs.openstack.org/developer/oslo.i18n/usage.html . """ -from oslo import i18n +import oslo_i18n -_translators = i18n.TranslatorFactory(domain='novaclient') +_translators = oslo_i18n.TranslatorFactory(domain='novaclient') # The primary translation function using the well-known name "_" _ = _translators.primary diff --git a/novaclient/shell.py b/novaclient/shell.py index 439a98d99..4e7b5c96c 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -33,8 +33,8 @@ from keystoneclient.auth.identity.generic import token from keystoneclient.auth.identity import v3 as identity from keystoneclient import session as ksession -from oslo.utils import encodeutils -from oslo.utils import strutils +from oslo_utils import encodeutils +from oslo_utils import strutils import pkg_resources import six diff --git a/novaclient/tests/unit/fixture_data/floatingips.py b/novaclient/tests/unit/fixture_data/floatingips.py index 10772a798..f5ad1c10f 100644 --- a/novaclient/tests/unit/fixture_data/floatingips.py +++ b/novaclient/tests/unit/fixture_data/floatingips.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from oslo.serialization import jsonutils +from oslo_serialization import jsonutils from novaclient.tests.unit import fakes from novaclient.tests.unit.fixture_data import base diff --git a/novaclient/tests/unit/fixture_data/hosts.py b/novaclient/tests/unit/fixture_data/hosts.py index 5c1ff60a3..28ff17ef1 100644 --- a/novaclient/tests/unit/fixture_data/hosts.py +++ b/novaclient/tests/unit/fixture_data/hosts.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from oslo.serialization import jsonutils +from oslo_serialization import jsonutils from six.moves.urllib import parse from novaclient.tests.unit.fixture_data import base diff --git a/novaclient/tests/unit/fixture_data/images.py b/novaclient/tests/unit/fixture_data/images.py index 4ec472581..0eb8a14f1 100644 --- a/novaclient/tests/unit/fixture_data/images.py +++ b/novaclient/tests/unit/fixture_data/images.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from oslo.serialization import jsonutils +from oslo_serialization import jsonutils from novaclient.tests.unit import fakes from novaclient.tests.unit.fixture_data import base diff --git a/novaclient/tests/unit/fixture_data/keypairs.py b/novaclient/tests/unit/fixture_data/keypairs.py index 9314c58a5..64d102ea5 100644 --- a/novaclient/tests/unit/fixture_data/keypairs.py +++ b/novaclient/tests/unit/fixture_data/keypairs.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from oslo.serialization import jsonutils +from oslo_serialization import jsonutils from novaclient.tests.unit import fakes from novaclient.tests.unit.fixture_data import base diff --git a/novaclient/tests/unit/fixture_data/networks.py b/novaclient/tests/unit/fixture_data/networks.py index 9ef692c3e..d2743fc60 100644 --- a/novaclient/tests/unit/fixture_data/networks.py +++ b/novaclient/tests/unit/fixture_data/networks.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from oslo.serialization import jsonutils +from oslo_serialization import jsonutils from novaclient.tests.unit.fixture_data import base diff --git a/novaclient/tests/unit/fixture_data/security_group_rules.py b/novaclient/tests/unit/fixture_data/security_group_rules.py index 5f1d2fad5..d0806c5ce 100644 --- a/novaclient/tests/unit/fixture_data/security_group_rules.py +++ b/novaclient/tests/unit/fixture_data/security_group_rules.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from oslo.serialization import jsonutils +from oslo_serialization import jsonutils from novaclient.tests.unit import fakes from novaclient.tests.unit.fixture_data import base diff --git a/novaclient/tests/unit/fixture_data/security_groups.py b/novaclient/tests/unit/fixture_data/security_groups.py index ac5d182c6..c1a3073cf 100644 --- a/novaclient/tests/unit/fixture_data/security_groups.py +++ b/novaclient/tests/unit/fixture_data/security_groups.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from oslo.serialization import jsonutils +from oslo_serialization import jsonutils from novaclient.tests.unit import fakes from novaclient.tests.unit.fixture_data import base diff --git a/novaclient/tests/unit/fixture_data/server_groups.py b/novaclient/tests/unit/fixture_data/server_groups.py index 1fcfdea55..bf4c82d0e 100644 --- a/novaclient/tests/unit/fixture_data/server_groups.py +++ b/novaclient/tests/unit/fixture_data/server_groups.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from oslo.serialization import jsonutils +from oslo_serialization import jsonutils from novaclient.tests.unit.fixture_data import base diff --git a/novaclient/tests/unit/fixture_data/servers.py b/novaclient/tests/unit/fixture_data/servers.py index 3766f4290..0bece9642 100644 --- a/novaclient/tests/unit/fixture_data/servers.py +++ b/novaclient/tests/unit/fixture_data/servers.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from oslo.serialization import jsonutils +from oslo_serialization import jsonutils from novaclient.tests.unit import fakes from novaclient.tests.unit.fixture_data import base diff --git a/novaclient/tests/unit/utils.py b/novaclient/tests/unit/utils.py index ab1ddcd18..6aaf0ed20 100644 --- a/novaclient/tests/unit/utils.py +++ b/novaclient/tests/unit/utils.py @@ -15,7 +15,7 @@ import fixtures import mock -from oslo.serialization import jsonutils +from oslo_serialization import jsonutils import requests from requests_mock.contrib import fixture as requests_mock_fixture import six diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 343c421e2..b36f5429d 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -17,7 +17,7 @@ import datetime import mock -from oslo.utils import strutils +from oslo_utils import strutils import six from six.moves.urllib import parse diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index c43aed422..c1a876e75 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -13,7 +13,7 @@ # under the License. import mock -from oslo.serialization import jsonutils +from oslo_serialization import jsonutils import six from novaclient import exceptions diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index b1b9b8085..139ea18e5 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -22,7 +22,7 @@ import fixtures import mock -from oslo.utils import timeutils +from oslo_utils import timeutils import six from six.moves import builtins diff --git a/novaclient/utils.py b/novaclient/utils.py index 2cf4fc619..50310bfdb 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -18,8 +18,8 @@ import time import uuid -from oslo.serialization import jsonutils -from oslo.utils import encodeutils +from oslo_serialization import jsonutils +from oslo_utils import encodeutils import pkg_resources import prettytable import six diff --git a/novaclient/v2/flavors.py b/novaclient/v2/flavors.py index 54b0f4974..1db42e24e 100644 --- a/novaclient/v2/flavors.py +++ b/novaclient/v2/flavors.py @@ -16,7 +16,7 @@ Flavor interface. """ -from oslo.utils import strutils +from oslo_utils import strutils from six.moves.urllib import parse from novaclient import base diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index ba576ffd2..624651b05 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -21,7 +21,7 @@ import base64 -from oslo.utils import encodeutils +from oslo_utils import encodeutils import six from six.moves.urllib import parse diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 33653a121..f609d671b 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -28,9 +28,9 @@ import sys import time -from oslo.utils import encodeutils -from oslo.utils import strutils -from oslo.utils import timeutils +from oslo_utils import encodeutils +from oslo_utils import strutils +from oslo_utils import timeutils import six from novaclient import client From 420dc2884ac369924e8e65eba5cf81935b6ffc9d Mon Sep 17 00:00:00 2001 From: Sean Dague Date: Tue, 21 Apr 2015 09:19:13 -0400 Subject: [PATCH 0734/1705] refactor functional test base class to no inherit from tempest_lib Test base classes inheriting outside the existing source tree to anything higher up the stack than testtools is really an anti pattern. It makes it *far* less clear what is going on. We really need to own and understand our base class setup for tests. This does that unwind, so that we only now call out to tempest_lib in specific ways (like building clients, using decorators). The timeout and log capture pieces are pulled inline. At the end of this we end up with a base test that defines: - self.client - a Nova API client - self.flavor - a workable flavor for booting guests - self.image - a workable image for booting guests - self.cli_clients - tempest_lib cli clients Change-Id: I716be51d7d1825a757934298f06b2f04d64cf0dd --- novaclient/tests/functional/base.py | 99 +++++++++++++++++-- novaclient/tests/functional/test_instances.py | 42 -------- .../tests/functional/test_volumes_api.py | 11 --- 3 files changed, 93 insertions(+), 59 deletions(-) diff --git a/novaclient/tests/functional/base.py b/novaclient/tests/functional/base.py index 27015bda8..2ccdbf448 100644 --- a/novaclient/tests/functional/base.py +++ b/novaclient/tests/functional/base.py @@ -12,10 +12,44 @@ import os -from tempest_lib.cli import base +import fixtures +import tempest_lib.cli.base +import testtools +import novaclient.client -class ClientTestBase(base.ClientTestBase): + +# The following are simple filter functions that filter our available +# image / flavor list so that they can be used in standard testing. +def pick_flavor(flavors): + """Given a flavor list pick a reasonable one.""" + for flavor in flavors: + if flavor.name == 'm1.tiny': + return flavor + for flavor in flavors: + if flavor.name == 'm1.small': + return flavor + raise NoFlavorException() + + +def pick_image(images): + for image in images: + if image.name.startswith('cirros') and image.name.endswith('-uec'): + return image + raise NoImageException() + + +class NoImageException(Exception): + """We couldn't find an acceptable image.""" + pass + + +class NoFlavorException(Exception): + """We couldn't find an acceptable flavor.""" + pass + + +class ClientTestBase(testtools.TestCase): """ This is a first pass at a simple read only python-novaclient test. This only exercises client commands that are read only. @@ -27,12 +61,65 @@ class ClientTestBase(base.ClientTestBase): * initially just check return codes, and later test command outputs """ - def _get_clients(self): + log_format = ('%(asctime)s %(process)d %(levelname)-8s ' + '[%(name)s] %(message)s') + + def setUp(self): + super(ClientTestBase, self).setUp() + + test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0) + try: + test_timeout = int(test_timeout) + except ValueError: + test_timeout = 0 + if test_timeout > 0: + self.useFixture(fixtures.Timeout(test_timeout, gentle=True)) + + if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or + os.environ.get('OS_STDOUT_CAPTURE') == '1'): + stdout = self.useFixture(fixtures.StringStream('stdout')).stream + self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout)) + if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or + os.environ.get('OS_STDERR_CAPTURE') == '1'): + stderr = self.useFixture(fixtures.StringStream('stderr')).stream + self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr)) + + if (os.environ.get('OS_LOG_CAPTURE') != 'False' and + os.environ.get('OS_LOG_CAPTURE') != '0'): + self.useFixture(fixtures.LoggerFixture(nuke_handlers=False, + format=self.log_format, + level=None)) + + # TODO(sdague): while we collect this information in + # tempest-lib, we do it in a way that's not available for top + # level tests. Long term this probably needs to be in the base + # class. + user = os.environ['OS_USERNAME'] + passwd = os.environ['OS_PASSWORD'] + tenant = os.environ['OS_TENANT_NAME'] + auth_url = os.environ['OS_AUTH_URL'] + + # TODO(sdague): we made a lot of fun of the glanceclient team + # for version as int in first parameter. I guess we know where + # they copied it from. + self.client = novaclient.client.Client( + 2, user, passwd, tenant, + auth_url=auth_url) + + # pick some reasonable flavor / image combo + self.flavor = pick_flavor(self.client.flavors.list()) + self.image = pick_image(self.client.images.list()) + + # create a CLI client in case we'd like to do CLI + # testing. tempest_lib does this realy weird thing where it + # builds a giant factory of all the CLIs that it knows + # about. Eventually that should really be unwound into + # something more sensible. cli_dir = os.environ.get( 'OS_NOVACLIENT_EXEC_DIR', os.path.join(os.path.abspath('.'), '.tox/functional/bin')) - return base.CLIClient( + self.cli_clients = tempest_lib.cli.base.CLIClient( username=os.environ.get('OS_USERNAME'), password=os.environ.get('OS_PASSWORD'), tenant_name=os.environ.get('OS_TENANT_NAME'), @@ -40,5 +127,5 @@ def _get_clients(self): cli_dir=cli_dir) def nova(self, *args, **kwargs): - return self.clients.nova(*args, - **kwargs) + return self.cli_clients.nova(*args, + **kwargs) diff --git a/novaclient/tests/functional/test_instances.py b/novaclient/tests/functional/test_instances.py index c963a6b71..8df28d136 100644 --- a/novaclient/tests/functional/test_instances.py +++ b/novaclient/tests/functional/test_instances.py @@ -10,33 +10,12 @@ # License for the specific language governing permissions and limitations # under the License. -import os import time import uuid -import novaclient.client from novaclient.tests.functional import base -# TODO(sdague): content that probably should be in utils, also throw -# Exceptions when they fail. -def pick_flavor(flavors): - """Given a flavor list pick a reasonable one.""" - for flavor in flavors: - if flavor.name == 'm1.tiny': - return flavor - - for flavor in flavors: - if flavor.name == 'm1.small': - return flavor - - -def pick_image(images): - for image in images: - if image.name.startswith('cirros') and image.name.endswith('-uec'): - return image - - def volume_id_from_cli_create(output): """Scrape the volume id out of the 'volume create' command @@ -67,27 +46,6 @@ def volume_at_status(output, volume_id, status): class TestInstanceCLI(base.ClientTestBase): - def setUp(self): - super(TestInstanceCLI, self).setUp() - # TODO(sdague): while we collect this information in - # tempest-lib, we do it in a way that's not available for top - # level tests. Long term this probably needs to be in the base - # class. - user = os.environ['OS_USERNAME'] - passwd = os.environ['OS_PASSWORD'] - tenant = os.environ['OS_TENANT_NAME'] - auth_url = os.environ['OS_AUTH_URL'] - - # TODO(sdague): we made a lot of fun of the glanceclient team - # for version as int in first parameter. I guess we know where - # they copied it from. - self.client = novaclient.client.Client( - 2, user, passwd, tenant, - auth_url=auth_url) - - # pick some reasonable flavor / image combo - self.flavor = pick_flavor(self.client.flavors.list()) - self.image = pick_image(self.client.images.list()) def test_attach_volume(self): """Test we can attach a volume via the cli. diff --git a/novaclient/tests/functional/test_volumes_api.py b/novaclient/tests/functional/test_volumes_api.py index e427c2e21..38c63bbf5 100644 --- a/novaclient/tests/functional/test_volumes_api.py +++ b/novaclient/tests/functional/test_volumes_api.py @@ -10,13 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. -import os import time import uuid import six.moves -from novaclient import client from novaclient import exceptions from novaclient.tests.functional import base @@ -35,15 +33,6 @@ def wait_for_delete(test, name, thing, get_func): class TestVolumesAPI(base.ClientTestBase): - def setUp(self): - super(TestVolumesAPI, self).setUp() - user = os.environ['OS_USERNAME'] - passwd = os.environ['OS_PASSWORD'] - tenant = os.environ['OS_TENANT_NAME'] - auth_url = os.environ['OS_AUTH_URL'] - - self.client = client.Client(2, user, passwd, tenant, auth_url=auth_url) - def test_volumes_snapshots_types_create_get_list_delete(self): # Create a volume volume = self.client.volumes.create(1) From af7c850bb87f5a14fa31b83e15b59b634157ebf5 Mon Sep 17 00:00:00 2001 From: Doug Hellmann Date: Tue, 21 Apr 2015 15:44:09 +0000 Subject: [PATCH 0735/1705] Update README to work with release tools The README file needs to have links to the project documentation and bug tracker in a parsable format in order for some of the release tools scripts to work (particularly the one that prints the release note email). Change-Id: I37e0acc5ed8e1af565359290fa622456901c735e --- README.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.rst b/README.rst index a7edf8707..84bf9c172 100644 --- a/README.rst +++ b/README.rst @@ -77,3 +77,9 @@ To use with nova, with keystone as the authentication system:: [...] >>> nt.keypairs.list() [...] + + +* License: Apache License, Version 2.0 +* Documentation: http://docs.openstack.org/developer/python-novaclient +* Source: http://git.openstack.org/cgit/openstack/python-novaclient +* Bugs: http://bugs.launchpad.net/python-novaclient From de4e40a7549f39dc341e8853b2bce456e258fc62 Mon Sep 17 00:00:00 2001 From: Sean Dague Date: Mon, 20 Apr 2015 19:00:27 -0400 Subject: [PATCH 0736/1705] add ips to novaclient server manager This exposes the ips Resource from the nova server manager which the infra team believes will help them optimize some of their API load on various clouds. Change-Id: I00730dc809ae4e86d728e2ec76bfb38024d7634e --- novaclient/tests/functional/api/__init__.py | 0 .../tests/functional/api/test_servers.py | 35 +++++++++++++++++++ novaclient/v2/servers.py | 10 ++++++ 3 files changed, 45 insertions(+) create mode 100644 novaclient/tests/functional/api/__init__.py create mode 100644 novaclient/tests/functional/api/test_servers.py diff --git a/novaclient/tests/functional/api/__init__.py b/novaclient/tests/functional/api/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/novaclient/tests/functional/api/test_servers.py b/novaclient/tests/functional/api/test_servers.py new file mode 100644 index 000000000..5615eb849 --- /dev/null +++ b/novaclient/tests/functional/api/test_servers.py @@ -0,0 +1,35 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import time + +from novaclient.tests.functional import base + + +class TestServersAPI(base.ClientTestBase): + def test_server_ips(self): + server_name = "test_server" + initial_server = self.client.servers.create( + server_name, self.image, self.flavor) + self.addCleanup(initial_server.delete) + + for x in xrange(60): + server = self.client.servers.get(initial_server) + if server.status == "ACTIVE": + break + else: + time.sleep(1) + else: + self.fail("Server %s did not go ACTIVE after 60s" % server) + + ips = self.client.servers.ips(server) + self.assertIn('private', ips) diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index 624651b05..11f1a56eb 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -819,6 +819,16 @@ def unshelve(self, server): """ self._action('unshelve', server, None) + def ips(self, server): + """ + Return IP Addresses associated with the server. + + Often a cheaper call then getting all the details for a server. + """ + _resp, body = self.api.client.get("/servers/%s/ips" % + base.getid(server)) + return body['addresses'] + def diagnostics(self, server): """Retrieve server diagnostics.""" return self.api.client.get("/servers/%s/diagnostics" % From 098116d6a574b8dede101b257db2ff22d269c6c7 Mon Sep 17 00:00:00 2001 From: melanie witt Date: Tue, 21 Apr 2015 21:32:33 +0000 Subject: [PATCH 0737/1705] Revert "nova flavor-show command is inconsistent" This reverts commit 4e79285b45ec1490c8e923f724cbaf4d42fe81c4. The aforementioned commit broke flavor-show for mixed case flavorids. The reason is a bit complex. On the nova api side, there is caching of db items for flavors, keyed off the flavorid retrieved from the db, case sensitive, unlike the db query itself. Attempts to flavor-show a flavor with flavorid composed of letters will fail with a 400 if the capitalization doesn't match. For the flavor names, they work in lowercase because the find_resource function falls back on a search by "human_id" after failing every other attempt to find the flavor, because "human_id" is a oslo-slugified string (all lowercase, non-word characters removed, spaces converted to hyphens). Closes-Bug: #1446850 Change-Id: I73247b50f5a6918167c071ccc13cd676aa2c7fec --- novaclient/tests/unit/test_base.py | 2 +- novaclient/tests/unit/v2/fakes.py | 20 ++++++++++---------- novaclient/tests/unit/v2/test_flavors.py | 4 ++-- novaclient/tests/unit/v2/test_shell.py | 16 ++++++++-------- novaclient/v2/shell.py | 11 ----------- 5 files changed, 21 insertions(+), 32 deletions(-) diff --git a/novaclient/tests/unit/test_base.py b/novaclient/tests/unit/test_base.py index 1ba1d32b0..b7bceb7b8 100644 --- a/novaclient/tests/unit/test_base.py +++ b/novaclient/tests/unit/test_base.py @@ -36,7 +36,7 @@ class TmpObject(object): def test_resource_lazy_getattr(self): f = flavors.Flavor(cs.flavors, {'id': 1}) - self.assertEqual('256 mb server', f.name) + self.assertEqual('256 MB Server', f.name) cs.assert_called('GET', '/flavors/1') # Missing stuff still fails after a second get diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 343c421e2..40cd47174 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -258,7 +258,7 @@ def get_servers_detail(self, **kw): }, "flavor": { "id": 1, - "name": "256 mb server", + "name": "256 MB Server", }, "hostId": "e4d909c290d0fb1ca068ffaddf22cbd0", "status": "BUILD", @@ -299,7 +299,7 @@ def get_servers_detail(self, **kw): }, "flavor": { "id": 1, - "name": "256 mb server", + "name": "256 MB Server", }, "hostId": "9e107d9d372bb6826bd81d3542a419d6", "status": "ACTIVE", @@ -340,7 +340,7 @@ def get_servers_detail(self, **kw): "image": "", "flavor": { "id": 1, - "name": "256 mb server", + "name": "256 MB Server", }, "hostId": "9e107d9d372bb6826bd81d3542a419d6", "status": "ACTIVE", @@ -672,19 +672,19 @@ def get_flavors(self, **kw): def get_flavors_detail(self, **kw): flavors = {'flavors': [ - {'id': 1, 'name': '256 mb server', 'ram': 256, 'disk': 10, + {'id': 1, 'name': '256 MB Server', 'ram': 256, 'disk': 10, 'OS-FLV-EXT-DATA:ephemeral': 10, 'os-flavor-access:is_public': True, 'links': {}}, - {'id': 2, 'name': '512 mb server', 'ram': 512, 'disk': 20, + {'id': 2, 'name': '512 MB Server', 'ram': 512, 'disk': 20, 'OS-FLV-EXT-DATA:ephemeral': 20, 'os-flavor-access:is_public': False, 'links': {}}, - {'id': 4, 'name': '1024 mb server', 'ram': 1024, 'disk': 10, + {'id': 4, 'name': '1024 MB Server', 'ram': 1024, 'disk': 10, 'OS-FLV-EXT-DATA:ephemeral': 10, 'os-flavor-access:is_public': True, 'links': {}}, - {'id': 'aa1', 'name': '128 mb server', 'ram': 128, 'disk': 0, + {'id': 'aa1', 'name': '128 MB Server', 'ram': 128, 'disk': 0, 'OS-FLV-EXT-DATA:ephemeral': 0, 'os-flavor-access:is_public': True, 'links': {}} @@ -736,16 +736,16 @@ def get_flavors_3(self, **kw): {}, {'flavor': { 'id': 3, - 'name': '256 mb server', + 'name': '256 MB Server', 'ram': 256, 'disk': 10, }}, ) - def get_flavors_512_mb_server(self, **kw): + def get_flavors_512_MB_Server(self, **kw): raise exceptions.NotFound('404') - def get_flavors_128_mb_server(self, **kw): + def get_flavors_128_MB_Server(self, **kw): raise exceptions.NotFound('404') def get_flavors_aa1(self, **kw): diff --git a/novaclient/tests/unit/v2/test_flavors.py b/novaclient/tests/unit/v2/test_flavors.py index 504590277..4fe43c10a 100644 --- a/novaclient/tests/unit/v2/test_flavors.py +++ b/novaclient/tests/unit/v2/test_flavors.py @@ -93,10 +93,10 @@ def test_get_flavor_details_diablo(self): def test_find(self): f = self.cs.flavors.find(ram=256) self.cs.assert_called('GET', '/flavors/detail') - self.assertEqual('256 mb server', f.name) + self.assertEqual('256 MB Server', f.name) f = self.cs.flavors.find(disk=0) - self.assertEqual('128 mb server', f.name) + self.assertEqual('128 MB Server', f.name) self.assertRaises(exceptions.NotFound, self.cs.flavors.find, disk=12345) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index b1b9b8085..3e7462c23 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -668,10 +668,10 @@ def test_boot_with_poll_to_check_VM_state_error(self): def test_boot_named_flavor(self): self.run_command(["boot", "--image", "1", - "--flavor", "512 mb server", + "--flavor", "512 MB Server", "--max-count", "3", "server"]) self.assert_called('GET', '/images/1', pos=0) - self.assert_called('GET', '/flavors/512 mb server', pos=1) + self.assert_called('GET', '/flavors/512 MB Server', pos=1) self.assert_called('GET', '/flavors?is_public=None', pos=2) self.assert_called('GET', '/flavors/2', pos=3) self.assert_called( @@ -708,15 +708,15 @@ def test_flavor_show_with_alphanum_id(self): self.assert_called_anytime('GET', '/flavors/aa1') def test_flavor_show_by_name(self): - self.run_command(['flavor-show', '128 mb server']) - self.assert_called('GET', '/flavors/128 mb server', pos=0) + self.run_command(['flavor-show', '128 MB Server']) + self.assert_called('GET', '/flavors/128 MB Server', pos=0) self.assert_called('GET', '/flavors?is_public=None', pos=1) self.assert_called('GET', '/flavors/aa1', pos=2) self.assert_called('GET', '/flavors/aa1/os-extra_specs', pos=3) def test_flavor_show_by_name_priv(self): - self.run_command(['flavor-show', '512 mb server']) - self.assert_called('GET', '/flavors/512 mb server', pos=0) + self.run_command(['flavor-show', '512 MB Server']) + self.assert_called('GET', '/flavors/512 MB Server', pos=0) self.assert_called('GET', '/flavors?is_public=None', pos=1) self.assert_called('GET', '/flavors/2', pos=2) self.assert_called('GET', '/flavors/2/os-extra_specs', pos=3) @@ -753,7 +753,7 @@ def test_flavor_access_add_by_id(self): {'addTenantAccess': {'tenant': 'proj2'}}) def test_flavor_access_add_by_name(self): - self.run_command(['flavor-access-add', '512 mb server', 'proj2']) + self.run_command(['flavor-access-add', '512 MB Server', 'proj2']) self.assert_called('POST', '/flavors/2/action', {'addTenantAccess': {'tenant': 'proj2'}}) @@ -763,7 +763,7 @@ def test_flavor_access_remove_by_id(self): {'removeTenantAccess': {'tenant': 'proj2'}}) def test_flavor_access_remove_by_name(self): - self.run_command(['flavor-access-remove', '512 mb server', 'proj2']) + self.run_command(['flavor-access-remove', '512 MB Server', 'proj2']) self.assert_called('POST', '/flavors/2/action', {'removeTenantAccess': {'tenant': 'proj2'}}) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 33653a121..f8b1a10a6 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -1898,17 +1898,6 @@ def _find_image(cs, image): def _find_flavor(cs, flavor): """Get a flavor by name, ID, or RAM size.""" try: - # isinstance() is being used to check if flavor is an instance of - # integer. It will help us to check if the user has entered flavor - # name or flavorid. If flavor name has been entered it is being - # converted to lowercase using lower(). Incase it is an ID the user - # has passed it will not go through the "flavor = flavor.lower()" - # code.The reason for checking if it is a flavor name or flavorid is - # that int has no lower() so it will give an error. - if isinstance(flavor, six.integer_types): - pass - else: - flavor = flavor.lower() return utils.find_resource(cs.flavors, flavor, is_public=None) except exceptions.NotFound: return cs.flavors.find(ram=flavor) From 4f9e65c438c484bffff7a42e9cdad28300d51bdc Mon Sep 17 00:00:00 2001 From: melanie witt Date: Mon, 20 Apr 2015 22:00:27 +0000 Subject: [PATCH 0738/1705] Don't lookup service url when bypass_url is given Change https://review.openstack.org/#/c/164321 broke the bypass_url option. It made the assumption that self.service_catalog won't be set in novaclient/client.py when bypass_url is specified, which isn't necessarily true. This change adds a check for self.bypass_url and looks up service url only if bypass_url hasn't been specified. Closes-Bug: #1445086 Change-Id: I72d2b3e3199aeae7fb2f2c68be259774e07a4501 --- novaclient/client.py | 6 +-- .../tests/functional/test_readonly_nova.py | 6 +++ novaclient/tests/unit/test_client.py | 39 +++++++++++++++++++ 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index 007cf9140..7684ce98a 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -208,8 +208,6 @@ def __init__(self, user, password, projectid=None, auth_url=None, # otherwise we will get all the requests logging messages rql.setLevel(logging.WARNING) - # NOTE(melwitt): Service catalog is only set if bypass_url isn't - # used. Otherwise, we can cache using services_url. self.service_catalog = None self.services_url = {} @@ -404,11 +402,9 @@ def _cs_request(self, url, method, **kwargs): path = re.sub(r'v[1-9]/[a-z0-9]+$', '', path) url = parse.urlunsplit((scheme, netloc, path, None, None)) else: - if self.service_catalog: + if self.service_catalog and not self.bypass_url: url = self.get_service_url(self.service_type) + url else: - # NOTE(melwitt): The service catalog is not available - # when bypass_url is used. url = self.management_url + url # Perform the request once. If we get a 401 back then it diff --git a/novaclient/tests/functional/test_readonly_nova.py b/novaclient/tests/functional/test_readonly_nova.py index 2ea4ec986..aa4a57c2f 100644 --- a/novaclient/tests/functional/test_readonly_nova.py +++ b/novaclient/tests/functional/test_readonly_nova.py @@ -169,3 +169,9 @@ def test_admin_timeout(self): def test_admin_timing(self): self.nova('list', flags='--timing') + + def test_admin_invalid_bypass_url(self): + self.assertRaises(exceptions.CommandFailed, + self.nova, + 'list', + flags='--bypass-url badurl') diff --git a/novaclient/tests/unit/test_client.py b/novaclient/tests/unit/test_client.py index 805278cca..f5807a6bc 100644 --- a/novaclient/tests/unit/test_client.py +++ b/novaclient/tests/unit/test_client.py @@ -266,6 +266,45 @@ def test_token_and_bypass_url(self): self.assertEqual("compute/v100", cs.bypass_url) self.assertEqual("compute/v100", cs.management_url) + def test_service_url_lookup(self): + service_type = 'compute' + cs = novaclient.client.HTTPClient(None, None, None, + auth_url='foo/v2', + service_type=service_type) + + @mock.patch.object(cs, 'get_service_url', return_value='compute/v5') + @mock.patch.object(cs, 'request', return_value=(200, '{}')) + @mock.patch.object(cs, 'authenticate') + def do_test(mock_auth, mock_request, mock_get): + + def set_service_catalog(): + cs.service_catalog = 'catalog' + + mock_auth.side_effect = set_service_catalog + cs.get('/servers') + mock_get.assert_called_once_with(service_type) + mock_request.assert_called_once_with('compute/v5/servers', + 'GET', headers=mock.ANY) + mock_auth.assert_called_once_with() + + do_test() + + def test_bypass_url_no_service_url_lookup(self): + bypass_url = 'compute/v100' + cs = novaclient.client.HTTPClient(None, None, None, + auth_url='foo/v2', + bypass_url=bypass_url) + + @mock.patch.object(cs, 'get_service_url') + @mock.patch.object(cs, 'request', return_value=(200, '{}')) + def do_test(mock_request, mock_get): + cs.get('/servers') + self.assertFalse(mock_get.called) + mock_request.assert_called_once_with(bypass_url + '/servers', + 'GET', headers=mock.ANY) + + do_test() + @mock.patch("novaclient.client.requests.Session") def test_session(self, mock_session): fake_session = mock.Mock() From 31f97a011b6a563e1cec16c16422a288e4a6ea80 Mon Sep 17 00:00:00 2001 From: Sean Dague Date: Wed, 22 Apr 2015 06:55:31 -0400 Subject: [PATCH 0739/1705] fix FloatingIP repr The FloatingIP repr added in d614dbcab9cc8c5732ce28c655738218e147aad7 is incorrect, and can cause fails when you attempt to do bulk creation activities with logging turned on. This removes the __repr__ for FloatingIP so that it fails back to the Resource defaults which are safe. Partial Revert of d614dbcab9cc8c5732ce28c655738218e147aad7 Closes-Bug: #1437244 Change-Id: I8cedf418917157ce632606bef640049140134372 --- novaclient/tests/unit/v2/test_floating_ips.py | 6 ++++++ novaclient/tests/unit/v2/test_floating_ips_bulk.py | 5 +++++ novaclient/v2/floating_ips.py | 3 --- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/novaclient/tests/unit/v2/test_floating_ips.py b/novaclient/tests/unit/v2/test_floating_ips.py index 2b091a516..3a4775b1c 100644 --- a/novaclient/tests/unit/v2/test_floating_ips.py +++ b/novaclient/tests/unit/v2/test_floating_ips.py @@ -57,3 +57,9 @@ def test_create_floating_ip_with_pool(self): self.assert_called('POST', '/os-floating-ips') self.assertEqual('nova', fl.pool) self.assertIsInstance(fl, floating_ips.FloatingIP) + + def test_repr(self): + fl = self.cs.floating_ips.list()[0] + string = "%s" % fl + self.assertEqual("", + string) diff --git a/novaclient/tests/unit/v2/test_floating_ips_bulk.py b/novaclient/tests/unit/v2/test_floating_ips_bulk.py index 34c200f3b..f59150758 100644 --- a/novaclient/tests/unit/v2/test_floating_ips_bulk.py +++ b/novaclient/tests/unit/v2/test_floating_ips_bulk.py @@ -62,3 +62,8 @@ def test_delete_floating_ips_bulk(self): body = {'ip_range': '192.168.1.0/30'} self.assert_called('PUT', '/os-floating-ips-bulk/delete', body) self.assertEqual(fl.floating_ips_bulk_delete, body['ip_range']) + + def test_repr(self): + fl = self.cs.floating_ips_bulk.create('192.168.1.0/30', 'poolTest', + 'interfaceTest') + self.assertEqual('', "%s" % fl) diff --git a/novaclient/v2/floating_ips.py b/novaclient/v2/floating_ips.py index 7c625e997..cce92da25 100644 --- a/novaclient/v2/floating_ips.py +++ b/novaclient/v2/floating_ips.py @@ -24,9 +24,6 @@ def delete(self): """ self.manager.delete(self) - def __repr__(self): - return "" % self.address - class FloatingIPManager(base.ManagerWithFind): resource_class = FloatingIP From 61ef35fe79e2a3a76987a92f9ee2db0bf1f6e651 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Tue, 31 Mar 2015 18:09:36 +0300 Subject: [PATCH 0740/1705] Deprecate v1.1 and remove v3 Module novaclient.v1_1 is already deprecated, so it's time to stop using it inside novaclient. Since support of v3 nova is undocumented feature, which uses v2 implementation, we can remove code related to it. Also, this patch removes redundant check for compute api version != 1.0. Change-Id: I06b349f704d5ae2c592d8d286da268870f2a69e9 --- novaclient/client.py | 37 ++++++++++++----- novaclient/shell.py | 56 ++++---------------------- novaclient/tests/unit/test_client.py | 4 -- novaclient/tests/unit/test_shell.py | 4 -- novaclient/tests/unit/v2/test_shell.py | 11 ----- 5 files changed, 34 insertions(+), 78 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index 85d618da1..d7656dab7 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -24,8 +24,10 @@ import functools import hashlib import logging +import pkgutil import re import socket +import warnings from keystoneclient import adapter from oslo_utils import importutils @@ -41,11 +43,15 @@ from six.moves.urllib import parse from novaclient import exceptions -from novaclient.i18n import _ +from novaclient.i18n import _, _LW from novaclient import service_catalog from novaclient import utils +# key is a deprecated version and value is an alternative version. +DEPRECATED_VERSIONS = {"1.1": "2"} + + class TCPKeepAliveAdapter(adapters.HTTPAdapter): """The custom adapter used to set TCP Keep-Alive on all connections.""" def init_poolmanager(self, *args, **kwargs): @@ -712,21 +718,30 @@ def _construct_http_client(username=None, password=None, project_id=None, def get_client_class(version): - version_map = { - '1.1': 'novaclient.v2.client.Client', - '2': 'novaclient.v2.client.Client', - '3': 'novaclient.v2.client.Client', - } + version = str(version) + if version in DEPRECATED_VERSIONS: + warnings.warn(_LW( + "Version %(deprecated_version)s is deprecated, using " + "alternative version %(alternative)s instead.") % + {"deprecated_version": version, + "alternative": DEPRECATED_VERSIONS[version]}) + version = DEPRECATED_VERSIONS[version] try: - client_path = version_map[str(version)] - except (KeyError, ValueError): + return importutils.import_class( + "novaclient.v%s.client.Client" % version) + except ImportError: + # NOTE(andreykurilin): available clients version should not be + # hardcoded, so let's discover them. + matcher = re.compile(r"v[0-9_]*$") + submodules = pkgutil.iter_modules(['novaclient']) + available_versions = [ + name[1:].replace("_", ".") for loader, name, ispkg in submodules + if matcher.search(name)] msg = _("Invalid client version '%(version)s'. must be one of: " "%(keys)s") % {'version': version, - 'keys': ', '.join(version_map.keys())} + 'keys': ', '.join(available_versions)} raise exceptions.UnsupportedVersion(msg) - return importutils.import_class(client_path) - def Client(version, *args, **kwargs): client_class = get_client_class(version) diff --git a/novaclient/shell.py b/novaclient/shell.py index 4e7b5c96c..515163533 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -58,13 +58,7 @@ DEFAULT_OS_COMPUTE_API_VERSION = "2" DEFAULT_NOVA_ENDPOINT_TYPE = 'publicURL' -# NOTE(cyeoh): Having the service type dependent on the API version -# is pretty ugly, but we have to do this because traditionally the -# catalog entry for compute points directly to the V2 API rather than -# the root, and then doing version discovery. -DEFAULT_NOVA_SERVICE_TYPE_MAP = {'1.1': 'compute', - '2': 'compute', - '3': 'computev3'} +DEFAULT_NOVA_SERVICE_TYPE = "compute" logger = logging.getLogger(__name__) @@ -414,7 +408,7 @@ def get_base_parser(self): metavar='', default=cliutils.env('OS_COMPUTE_API_VERSION', default=DEFAULT_OS_COMPUTE_API_VERSION), - help=_('Accepts 1.1 or 3, ' + help=_('Accepts number of API version, ' 'defaults to env[OS_COMPUTE_API_VERSION].')) parser.add_argument( '--os_compute_api_version', @@ -490,9 +484,9 @@ def _discover_via_python_path(self): def _discover_via_contrib_path(self, version): module_path = os.path.dirname(os.path.abspath(__file__)) version_str = "v%s" % version.replace('.', '_') - # NOTE(akurilin): v1.1, v2 and v3 have one implementation, so - # we should discover contrib modules in one place. - if version_str in ["v1_1", "v3"]: + # NOTE(andreykurilin): v1.1 uses implementation of v2, so we should + # discover contrib modules in novaclient.v2 dir. + if version_str == "v1_1": version_str = "v2" ext_path = os.path.join(module_path, version_str, 'contrib') ext_glob = os.path.join(ext_path, "*.py") @@ -656,15 +650,8 @@ def main(self, argv): endpoint_type += 'URL' if not service_type: - os_compute_api_version = (options.os_compute_api_version or - DEFAULT_OS_COMPUTE_API_VERSION) - try: - service_type = DEFAULT_NOVA_SERVICE_TYPE_MAP[ - os_compute_api_version] - except KeyError: - service_type = DEFAULT_NOVA_SERVICE_TYPE_MAP[ - DEFAULT_OS_COMPUTE_API_VERSION] - service_type = cliutils.get_service_type(args.func) or service_type + service_type = (cliutils.get_service_type(args.func) or + DEFAULT_NOVA_SERVICE_TYPE) # If we have an auth token but no management_url, we must auth anyway. # Expired tokens are handled by client.py:_cs_request @@ -734,8 +721,7 @@ def main(self, argv): project_domain_id=args.os_project_domain_id, project_domain_name=args.os_project_domain_name) - if (options.os_compute_api_version and - options.os_compute_api_version != '1.0'): + if options.os_compute_api_version: if not any([args.os_tenant_id, args.os_tenant_name, args.os_project_id, args.os_project_name]): raise exc.CommandError(_("You must provide a project name or" @@ -806,32 +792,6 @@ def main(self, argv): except exc.AuthorizationFailure: raise exc.CommandError(_("Unable to authorize user")) - if options.os_compute_api_version == "3" and service_type != 'image': - # NOTE(cyeoh): create an image based client because the - # images api is no longer proxied by the V3 API and we - # sometimes need to be able to look up images information - # via glance when connected to the nova api. - image_service_type = 'image' - # NOTE(hdd): the password is needed again because creating a new - # Client without specifying bypass_url will force authentication. - # We can't reuse self.cs's bypass_url, because that's the URL for - # the nova service; we need to get glance's URL for this Client - if not os_password: - os_password = helper.password - self.cs.image_cs = client.Client( - options.os_compute_api_version, os_username, - os_password, os_tenant_name, tenant_id=os_tenant_id, - auth_url=os_auth_url, insecure=insecure, - region_name=os_region_name, endpoint_type=endpoint_type, - extensions=self.extensions, service_type=image_service_type, - service_name=service_name, auth_system=os_auth_system, - auth_plugin=auth_plugin, - volume_service_name=volume_service_name, - timings=args.timings, bypass_url=bypass_url, - os_cache=os_cache, http_log_debug=options.debug, - session=keystone_session, auth=keystone_auth, - cacert=cacert, timeout=timeout) - args.func(self.cs, args) if args.timings: diff --git a/novaclient/tests/unit/test_client.py b/novaclient/tests/unit/test_client.py index 805278cca..a828503e1 100644 --- a/novaclient/tests/unit/test_client.py +++ b/novaclient/tests/unit/test_client.py @@ -161,10 +161,6 @@ def test_client_version_url_with_project_name(self): self._check_version_url('http://foo.com/nova/v2/%s', 'http://foo.com/nova/') - def test_get_client_class_v3(self): - output = novaclient.client.get_client_class('3') - self.assertEqual(output, novaclient.v2.client.Client) - def test_get_client_class_v2(self): output = novaclient.client.get_client_class('2') self.assertEqual(output, novaclient.v2.client.Client) diff --git a/novaclient/tests/unit/test_shell.py b/novaclient/tests/unit/test_shell.py index f284eb88e..a8826deb5 100644 --- a/novaclient/tests/unit/test_shell.py +++ b/novaclient/tests/unit/test_shell.py @@ -342,10 +342,6 @@ def test_v1_1_service_type(self, mock_client): def test_v2_service_type(self, mock_client): self._test_service_type('2', 'compute', mock_client) - @mock.patch('novaclient.client.Client') - def test_v3_service_type(self, mock_client): - self._test_service_type('3', 'computev3', mock_client) - @mock.patch('novaclient.client.Client') def test_v_unknown_service_type(self, mock_client): self._test_service_type('unknown', 'compute', mock_client) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 139ea18e5..0abc345d1 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2374,17 +2374,6 @@ class ShellTestV11(ShellTest): } -class ShellTestV3(ShellTest): - FAKE_ENV = { - 'NOVA_USERNAME': 'username', - 'NOVA_PASSWORD': 'password', - 'NOVA_PROJECT_ID': 'project_id', - 'OS_COMPUTE_API_VERSION': '3', - 'NOVA_URL': 'http://no.where', - 'OS_AUTH_URL': 'http://no.where/v2.0', - } - - class ShellWithSessionClientTest(ShellTest): def setUp(self): From 86ec0c6bac591061f8b1a2140560dc03423b9153 Mon Sep 17 00:00:00 2001 From: Ken'ichi Ohmichi Date: Sun, 26 Apr 2015 15:43:17 +0000 Subject: [PATCH 0741/1705] Add min/max microversions to version-list cmd Since Id464a07d624d0e228fe0aa66a04c8e51f292ba0c, Nova returns min/max microversions on a "list versions" API response. For using microversions, API users need to know available microversions. This patch adds them to version-list command. Change-Id: I81f667ca4f0403183cac918e416a730c16b6eeff --- novaclient/v2/shell.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 55b9fb712..82d77988e 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -4456,5 +4456,8 @@ def do_server_group_get(cs, args): def do_version_list(cs, args): """List all API versions.""" result = cs.versions.list() - columns = ["Id", "Status", "Updated"] + if 'min_version' in dir(result[0]): + columns = ["Id", "Status", "Updated", "Min Version", "Version"] + else: + columns = ["Id", "Status", "Updated"] utils.print_list(result, columns) From 95421a37029e6555e5ef25fa1944e4ff229356ea Mon Sep 17 00:00:00 2001 From: Ken'ichi Ohmichi Date: Wed, 15 Apr 2015 03:53:00 +0000 Subject: [PATCH 0742/1705] Don't use SessionClient for version-list API The endpoint of version API doesn't contain "/v2/{project-id}" and it is just "/". SessionClient is based on the endpoint "/v2/{project-id}/...", so this patch makes a client use a raw url for version-list API. Change-Id: I53f2afacce2dda14ac9761e824b48887f7c192f9 Closes-Bug: #1444235 --- .../tests/functional/test_readonly_nova.py | 3 ++ novaclient/tests/unit/v2/fakes.py | 44 +++++++++++++++---- novaclient/tests/unit/v2/test_versions.py | 38 ++++++++++++++++ novaclient/v2/versions.py | 18 +++++++- 4 files changed, 93 insertions(+), 10 deletions(-) create mode 100644 novaclient/tests/unit/v2/test_versions.py diff --git a/novaclient/tests/functional/test_readonly_nova.py b/novaclient/tests/functional/test_readonly_nova.py index aa4a57c2f..c67c3a378 100644 --- a/novaclient/tests/functional/test_readonly_nova.py +++ b/novaclient/tests/functional/test_readonly_nova.py @@ -156,6 +156,9 @@ def test_migration_list(self): self.nova('migration-list') self.nova('migration-list', flags='--debug') + def test_version_list(self): + self.nova('version-list') + # Optional arguments: def test_admin_version(self): diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 110891cf3..1c3e93bd2 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -64,15 +64,21 @@ def _cs_request(self, url, method, **kwargs): elif method == 'PUT': assert 'body' in kwargs - # Call the method - args = parse.parse_qsl(parse.urlparse(url)[4]) - kwargs.update(args) - munged_url = url.rsplit('?', 1)[0] - munged_url = munged_url.strip('/').replace('/', '_').replace('.', '_') - munged_url = munged_url.replace('-', '_') - munged_url = munged_url.replace(' ', '_') - - callback = "%s_%s" % (method.lower(), munged_url) + if url is not None: + # Call the method + args = parse.parse_qsl(parse.urlparse(url)[4]) + kwargs.update(args) + munged_url = url.rsplit('?', 1)[0] + munged_url = munged_url.strip('/').replace('/', '_') + munged_url = munged_url.replace('.', '_') + munged_url = munged_url.replace('-', '_') + munged_url = munged_url.replace(' ', '_') + callback = "%s_%s" % (method.lower(), munged_url) + + if url is None or callback == "get_http:__nova_api:8774": + # To get API version information, it is necessary to GET + # a nova endpoint directly without "v2/". + callback = "get_versions" if not hasattr(self, callback): raise AssertionError('Called unknown API method: %s %s, ' @@ -90,6 +96,26 @@ def _cs_request(self, url, method, **kwargs): }) return r, body + def get_endpoint(self): + return "http://nova-api:8774/v2/190a755eef2e4aac9f06aa6be9786385" + + def get_versions(self): + return (200, {}, { + "versions": [ + {"status": "SUPPORTED", "updated": "2011-01-21T11:33:21Z", + "links": [{"href": "http://nova-api:8774/v2/", + "rel": "self"}], + "min_version": "", + "version": "", + "id": "v2.0"}, + {"status": "CURRENT", "updated": "2013-07-23T11:33:21Z", + "links": [{"href": "http://nova-api:8774/v2.1/", + "rel": "self"}], + "min_version": "2.1", + "version": "2.3", + "id": "v2.1"} + ]}) + # # agents # diff --git a/novaclient/tests/unit/v2/test_versions.py b/novaclient/tests/unit/v2/test_versions.py new file mode 100644 index 000000000..91139844f --- /dev/null +++ b/novaclient/tests/unit/v2/test_versions.py @@ -0,0 +1,38 @@ +# Copyright 2015 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import mock + +from novaclient.tests.unit import utils +from novaclient.tests.unit.v2 import fakes +from novaclient.v2 import versions + + +class VersionsTest(utils.TestCase): + def setUp(self): + super(VersionsTest, self).setUp() + self.cs = fakes.FakeClient() + self.service_type = versions.Version + + @mock.patch.object(versions.VersionManager, '_is_session_client', + return_value=False) + def test_list_services_with_http_client(self, mock_is_session_client): + self.cs.versions.list() + self.cs.assert_called('GET', None) + + @mock.patch.object(versions.VersionManager, '_is_session_client', + return_value=True) + def test_list_services_with_session_client(self, mock_is_session_client): + self.cs.versions.list() + self.cs.assert_called('GET', 'http://nova-api:8774/') diff --git a/novaclient/v2/versions.py b/novaclient/v2/versions.py index 5ab09a103..85d62d706 100644 --- a/novaclient/v2/versions.py +++ b/novaclient/v2/versions.py @@ -16,7 +16,10 @@ version interface """ +from six.moves import urllib + from novaclient import base +from novaclient import client class Version(base.Resource): @@ -30,6 +33,19 @@ def __repr__(self): class VersionManager(base.ManagerWithFind): resource_class = Version + def _is_session_client(self): + return isinstance(self.api.client, client.SessionClient) + def list(self): """List all versions.""" - return self._list(None, "versions") + + version_url = None + if self._is_session_client(): + # NOTE: "list versions" API needs to be accessed without base + # URI (like "v2/{project-id}"), so here should be a scheme("http", + # etc.) and a hostname. + endpoint = self.api.client.get_endpoint() + url = urllib.parse.urlparse(endpoint) + version_url = '%s://%s/' % (url.scheme, url.netloc) + + return self._list(version_url, "versions") From bf6fbdb8d76a3930f04ed0891e0ba9e04bbb5f73 Mon Sep 17 00:00:00 2001 From: "rajiv.kumar" Date: Tue, 24 Mar 2015 03:49:41 +0000 Subject: [PATCH 0743/1705] nova client now support limits subcommand Added new subcommand "limits" which displays absolute limits and rate limits both. do_limits() functions calls get() only once and then passes limits.rate and limits.absolute to _print_rate_limits and _print_absolute_limits respectively; these functions then format the output. Change-Id: I1344da1a3925a3f3a757e340f11381b69a668bf7 Closes-Bug: #1172254 --- novaclient/tests/unit/v2/test_shell.py | 14 +++++++++++ novaclient/v2/shell.py | 33 ++++++++++++++++++++++++-- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index b1b9b8085..5d1d20d9b 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -1998,6 +1998,20 @@ def test_absolute_limits(self): self.run_command('absolute-limits --tenant 1234') self.assert_called('GET', '/limits?tenant_id=1234') + def test_limits(self): + self.run_command('limits') + self.assert_called('GET', '/limits') + + self.run_command('limits --reserved') + self.assert_called('GET', '/limits?reserved=1') + + self.run_command('limits --tenant 1234') + self.assert_called('GET', '/limits?tenant_id=1234') + + stdout = self.run_command('limits --tenant 1234') + self.assertIn('Verb', stdout) + self.assertIn('Name', stdout) + def test_evacuate(self): self.run_command('evacuate sample-server new_host') self.assert_called('POST', '/servers/1234/action', diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 7e3a53100..a6c7228a2 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -2935,9 +2935,13 @@ def _find_keypair(cs, keypair): default=False, help=_('Include reservations count.')) def do_absolute_limits(cs, args): - """Print a list of absolute limits for a user""" + """DEPRECATED, use limits instead.""" limits = cs.limits.get(args.reserved, args.tenant).absolute + _print_absolute_limits(limits) + +def _print_absolute_limits(limits): + """Prints absolute limits.""" class Limit(object): def __init__(self, name, used, max, other): self.name = name @@ -2999,12 +3003,37 @@ def __init__(self, name, used, max, other): def do_rate_limits(cs, args): - """Print a list of rate limits for a user""" + """DEPRECATED, use limits instead.""" limits = cs.limits.get().rate + _print_rate_limits(limits) + + +def _print_rate_limits(limits): + """print rate limits.""" columns = ['Verb', 'URI', 'Value', 'Remain', 'Unit', 'Next_Available'] utils.print_list(limits, columns) +@cliutils.arg( + '--tenant', + # nova db searches by project_id + dest='tenant', + metavar='', + nargs='?', + help=_('Display information from single tenant (Admin only).')) +@cliutils.arg( + '--reserved', + dest='reserved', + action='store_true', + default=False, + help=_('Include reservations count.')) +def do_limits(cs, args): + """Print rate and absolute limits.""" + limits = cs.limits.get(args.reserved, args.tenant) + _print_rate_limits(limits.rate) + _print_absolute_limits(limits.absolute) + + @cliutils.arg( '--start', metavar='', From 99fcc6939e37b796f60097030fe07511a4aa2058 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Mon, 4 May 2015 20:09:13 +0000 Subject: [PATCH 0744/1705] Updated from global requirements Change-Id: I897e515d571c81c318206758c2cd419b4bbed55f --- requirements.txt | 4 ++-- test-requirements.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 9fe2b9542..127cac203 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,8 +8,8 @@ oslo.i18n>=1.5.0 # Apache-2.0 oslo.serialization>=1.4.0 # Apache-2.0 oslo.utils>=1.4.0 # Apache-2.0 PrettyTable>=0.7,<0.8 -requests>=2.2.0,!=2.4.0 +requests>=2.5.2 simplejson>=2.2.0 six>=1.9.0 Babel>=1.3 -python-keystoneclient>=1.1.0 +python-keystoneclient>=1.3.0 diff --git a/test-requirements.txt b/test-requirements.txt index 6790cc68a..225265245 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -14,4 +14,4 @@ oslosphinx>=2.5.0 # Apache-2.0 testrepository>=0.0.18 testscenarios>=0.4 testtools>=0.9.36,!=1.2.0 -tempest-lib>=0.4.0 +tempest-lib>=0.5.0 From 02c04c5658dcd968b91f7256b749755abb9d2074 Mon Sep 17 00:00:00 2001 From: kylin7-sg Date: Tue, 7 Apr 2015 15:23:14 +0800 Subject: [PATCH 0745/1705] Make _discover_extensions public Heat uses `novaclient.shell.OpenStackComputeShell._discover_extensions` function, which is private currently. It's better to change this method to public for public use. Change-Id: I15879d56db2ec88a9bef99b6af8924ebd4ad169b Closes-bug: #1440779 --- novaclient/client.py | 61 ++++++++++++++++++++++++++ novaclient/shell.py | 58 ++---------------------- novaclient/tests/unit/test_discover.py | 20 ++++----- 3 files changed, 73 insertions(+), 66 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index ef3b0e307..d2b5462c5 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -22,8 +22,12 @@ import copy import functools +import glob import hashlib +import imp +import itertools import logging +import os import pkgutil import re import socket @@ -32,6 +36,7 @@ from keystoneclient import adapter from oslo_utils import importutils from oslo_utils import netutils +import pkg_resources import requests from requests import adapters @@ -43,6 +48,7 @@ from six.moves.urllib import parse from novaclient import exceptions +from novaclient import extension as ext from novaclient.i18n import _, _LW from novaclient import service_catalog from novaclient import utils @@ -713,6 +719,61 @@ def _construct_http_client(username=None, password=None, project_id=None, connection_pool=connection_pool) +def discover_extensions(version): + extensions = [] + for name, module in itertools.chain( + _discover_via_python_path(), + _discover_via_contrib_path(version), + _discover_via_entry_points()): + + extension = ext.Extension(name, module) + extensions.append(extension) + + return extensions + + +def _discover_via_python_path(): + for (module_loader, name, _ispkg) in pkgutil.iter_modules(): + if name.endswith('_python_novaclient_ext'): + if not hasattr(module_loader, 'load_module'): + # Python 2.6 compat: actually get an ImpImporter obj + module_loader = module_loader.find_module(name) + + module = module_loader.load_module(name) + if hasattr(module, 'extension_name'): + name = module.extension_name + + yield name, module + + +def _discover_via_contrib_path(version): + module_path = os.path.dirname(os.path.abspath(__file__)) + version_str = "v%s" % version.replace('.', '_') + # NOTE(andreykurilin): v1.1 uses implementation of v2, so we should + # discover contrib modules in novaclient.v2 dir. + if version_str == "v1_1": + version_str = "v2" + ext_path = os.path.join(module_path, version_str, 'contrib') + ext_glob = os.path.join(ext_path, "*.py") + + for ext_path in glob.iglob(ext_glob): + name = os.path.basename(ext_path)[:-3] + + if name == "__init__": + continue + + module = imp.load_source(name, ext_path) + yield name, module + + +def _discover_via_entry_points(): + for ep in pkg_resources.iter_entry_points('novaclient.extension'): + name = ep.name + module = ep.load() + + yield name, module + + def get_client_class(version): version = str(version) if version in DEPRECATED_VERSIONS: diff --git a/novaclient/shell.py b/novaclient/shell.py index 515163533..1dbc08584 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -21,12 +21,7 @@ from __future__ import print_function import argparse import getpass -import glob -import imp -import itertools import logging -import os -import pkgutil import sys from keystoneclient.auth.identity.generic import password @@ -35,7 +30,6 @@ from keystoneclient import session as ksession from oslo_utils import encodeutils from oslo_utils import strutils -import pkg_resources import six HAS_KEYRING = False @@ -456,56 +450,10 @@ def get_subcommand_parser(self, version): return parser + # TODO(lyj): Delete this method after heat patched to use + # client.discover_extensions def _discover_extensions(self, version): - extensions = [] - for name, module in itertools.chain( - self._discover_via_python_path(), - self._discover_via_contrib_path(version), - self._discover_via_entry_points()): - - extension = novaclient.extension.Extension(name, module) - extensions.append(extension) - - return extensions - - def _discover_via_python_path(self): - for (module_loader, name, _ispkg) in pkgutil.iter_modules(): - if name.endswith('_python_novaclient_ext'): - if not hasattr(module_loader, 'load_module'): - # Python 2.6 compat: actually get an ImpImporter obj - module_loader = module_loader.find_module(name) - - module = module_loader.load_module(name) - if hasattr(module, 'extension_name'): - name = module.extension_name - - yield name, module - - def _discover_via_contrib_path(self, version): - module_path = os.path.dirname(os.path.abspath(__file__)) - version_str = "v%s" % version.replace('.', '_') - # NOTE(andreykurilin): v1.1 uses implementation of v2, so we should - # discover contrib modules in novaclient.v2 dir. - if version_str == "v1_1": - version_str = "v2" - ext_path = os.path.join(module_path, version_str, 'contrib') - ext_glob = os.path.join(ext_path, "*.py") - - for ext_path in glob.iglob(ext_glob): - name = os.path.basename(ext_path)[:-3] - - if name == "__init__": - continue - - module = imp.load_source(name, ext_path) - yield name, module - - def _discover_via_entry_points(self): - for ep in pkg_resources.iter_entry_points('novaclient.extension'): - name = ep.name - module = ep.load() - - yield name, module + return client.discover_extensions(version) def _add_bash_completion_subparser(self, subparsers): subparser = subparsers.add_parser( diff --git a/novaclient/tests/unit/test_discover.py b/novaclient/tests/unit/test_discover.py index f8414c664..44f3a49ef 100644 --- a/novaclient/tests/unit/test_discover.py +++ b/novaclient/tests/unit/test_discover.py @@ -19,7 +19,7 @@ import mock import pkg_resources -import novaclient.shell +from novaclient import client from novaclient.tests.unit import utils @@ -38,8 +38,7 @@ def mock_iter_entry_points(group): @mock.patch.object(pkg_resources, 'iter_entry_points', mock_iter_entry_points) def test(): - shell = novaclient.shell.OpenStackComputeShell() - for name, module in shell._discover_via_entry_points(): + for name, module in client._discover_via_entry_points(): self.assertEqual('foo', name) self.assertTrue(inspect.ismodule(module)) @@ -47,27 +46,26 @@ def test(): def test_discover_extensions(self): - def mock_discover_via_python_path(self): + def mock_discover_via_python_path(): yield 'foo', imp.new_module('foo') - def mock_discover_via_contrib_path(self, version): + def mock_discover_via_contrib_path(version): yield 'bar', imp.new_module('bar') - def mock_discover_via_entry_points(self): + def mock_discover_via_entry_points(): yield 'baz', imp.new_module('baz') - @mock.patch.object(novaclient.shell.OpenStackComputeShell, + @mock.patch.object(client, '_discover_via_python_path', mock_discover_via_python_path) - @mock.patch.object(novaclient.shell.OpenStackComputeShell, + @mock.patch.object(client, '_discover_via_contrib_path', mock_discover_via_contrib_path) - @mock.patch.object(novaclient.shell.OpenStackComputeShell, + @mock.patch.object(client, '_discover_via_entry_points', mock_discover_via_entry_points) def test(): - shell = novaclient.shell.OpenStackComputeShell() - extensions = shell._discover_extensions('1.1') + extensions = client.discover_extensions('1.1') self.assertEqual(3, len(extensions)) names = sorted(['foo', 'bar', 'baz']) sorted_extensions = sorted(extensions, key=lambda ext: ext.name) From d03a85a205ea6a325dee25a75cd622b40570a238 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Thu, 7 May 2015 23:37:26 +0000 Subject: [PATCH 0746/1705] Updated from global requirements Change-Id: I9b195f06731e390087f8850fbba59afacc89239f --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 127cac203..76a752f20 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ # 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. -pbr>=0.6,!=0.7,<1.0 +pbr>=0.11,<2.0 argparse iso8601>=0.1.9 oslo.i18n>=1.5.0 # Apache-2.0 From 4a7cf9676694968fedace5100d72f880d3bad339 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Fri, 8 May 2015 12:27:59 +0300 Subject: [PATCH 0747/1705] Sync latest code from oslo-incubator Hash of latest commit in oslo: f5646edc61b9653d7ff71ed0177ed77811bbdcd0 Change-Id: I2f3bf41434591fcfc811ae6dec40ccabd42db741 --- novaclient/openstack/common/_i18n.py | 49 ++++++++++--------- novaclient/openstack/common/apiclient/auth.py | 13 +++++ .../openstack/common/apiclient/client.py | 27 ++++++++-- .../openstack/common/apiclient/exceptions.py | 37 +++++++++++--- .../openstack/common/apiclient/fake_client.py | 15 ++++++ .../openstack/common/apiclient/utils.py | 17 ++++++- novaclient/openstack/common/cliutils.py | 15 ++++-- 7 files changed, 134 insertions(+), 39 deletions(-) diff --git a/novaclient/openstack/common/_i18n.py b/novaclient/openstack/common/_i18n.py index 6ae2ff239..c07496727 100644 --- a/novaclient/openstack/common/_i18n.py +++ b/novaclient/openstack/common/_i18n.py @@ -16,25 +16,30 @@ """ -import oslo.i18n - - -# NOTE(dhellmann): This reference to o-s-l-o will be replaced by the -# application name when this module is synced into the separate -# repository. It is OK to have more than one translation function -# using the same domain, since there will still only be one message -# catalog. -_translators = oslo.i18n.TranslatorFactory(domain='novaclient') - -# The primary translation function using the well-known name "_" -_ = _translators.primary - -# Translators for log levels. -# -# The abbreviated names are meant to reflect the usual use of a short -# name like '_'. The "L" is for "log" and the other letter comes from -# the level. -_LI = _translators.log_info -_LW = _translators.log_warning -_LE = _translators.log_error -_LC = _translators.log_critical +try: + import oslo_i18n + + # NOTE(dhellmann): This reference to o-s-l-o will be replaced by the + # application name when this module is synced into the separate + # repository. It is OK to have more than one translation function + # using the same domain, since there will still only be one message + # catalog. + _translators = oslo_i18n.TranslatorFactory(domain='novaclient') + + # The primary translation function using the well-known name "_" + _ = _translators.primary + + # Translators for log levels. + # + # The abbreviated names are meant to reflect the usual use of a short + # name like '_'. The "L" is for "log" and the other letter comes from + # the level. + _LI = _translators.log_info + _LW = _translators.log_warning + _LE = _translators.log_error + _LC = _translators.log_critical +except ImportError: + # NOTE(dims): Support for cases where a project wants to use + # code from oslo-incubator, but is not ready to be internationalized + # (like tempest) + _ = _LI = _LW = _LE = _LC = lambda x: x diff --git a/novaclient/openstack/common/apiclient/auth.py b/novaclient/openstack/common/apiclient/auth.py index 67a1bf7dd..68ed09fb5 100644 --- a/novaclient/openstack/common/apiclient/auth.py +++ b/novaclient/openstack/common/apiclient/auth.py @@ -17,6 +17,19 @@ # E0202: An attribute inherited from %s hide this method # pylint: disable=E0202 +######################################################################## +# +# THIS MODULE IS DEPRECATED +# +# Please refer to +# https://etherpad.openstack.org/p/kilo-novaclient-library-proposals for +# the discussion leading to this deprecation. +# +# We recommend checking out the python-openstacksdk project +# (https://launchpad.net/python-openstacksdk) instead. +# +######################################################################## + import abc import argparse import os diff --git a/novaclient/openstack/common/apiclient/client.py b/novaclient/openstack/common/apiclient/client.py index c591850e6..fa317c2da 100644 --- a/novaclient/openstack/common/apiclient/client.py +++ b/novaclient/openstack/common/apiclient/client.py @@ -25,6 +25,7 @@ # E0202: An attribute inherited from %s hide this method # pylint: disable=E0202 +import hashlib import logging import time @@ -33,14 +34,15 @@ except ImportError: import json -from oslo.utils import importutils +from oslo_utils import encodeutils +from oslo_utils import importutils import requests from novaclient.openstack.common._i18n import _ from novaclient.openstack.common.apiclient import exceptions - _logger = logging.getLogger(__name__) +SENSITIVE_HEADERS = ('X-Auth-Token', 'X-Subject-Token',) class HTTPClient(object): @@ -98,6 +100,18 @@ def __init__(self, self.http = http or requests.Session() self.cached_token = None + self.last_request_id = None + + def _safe_header(self, name, value): + if name in SENSITIVE_HEADERS: + # because in python3 byte string handling is ... ug + v = value.encode('utf-8') + h = hashlib.sha1(v) + d = h.hexdigest() + return encodeutils.safe_decode(name), "{SHA1}%s" % d + else: + return (encodeutils.safe_decode(name), + encodeutils.safe_decode(value)) def _http_log_req(self, method, url, kwargs): if not self.debug: @@ -110,7 +124,8 @@ def _http_log_req(self, method, url, kwargs): ] for element in kwargs['headers']: - header = "-H '%s: %s'" % (element, kwargs['headers'][element]) + header = ("-H '%s: %s'" % + self._safe_header(element, kwargs['headers'][element])) string_parts.append(header) _logger.debug("REQ: %s" % " ".join(string_parts)) @@ -177,6 +192,8 @@ def request(self, method, url, **kwargs): start_time, time.time())) self._http_log_resp(resp) + self.last_request_id = resp.headers.get('x-openstack-request-id') + if resp.status_code >= 400: _logger.debug( "Request returned failure status: %s", @@ -327,6 +344,10 @@ def client_request(self, method, url, **kwargs): return self.http_client.client_request( self, method, url, **kwargs) + @property + def last_request_id(self): + return self.http_client.last_request_id + def head(self, url, **kwargs): return self.client_request("HEAD", url, **kwargs) diff --git a/novaclient/openstack/common/apiclient/exceptions.py b/novaclient/openstack/common/apiclient/exceptions.py index 15e885b9b..540eefd34 100644 --- a/novaclient/openstack/common/apiclient/exceptions.py +++ b/novaclient/openstack/common/apiclient/exceptions.py @@ -20,6 +20,19 @@ Exception definitions. """ +######################################################################## +# +# THIS MODULE IS DEPRECATED +# +# Please refer to +# https://etherpad.openstack.org/p/kilo-novaclient-library-proposals for +# the discussion leading to this deprecation. +# +# We recommend checking out the python-openstacksdk project +# (https://launchpad.net/python-openstacksdk) instead. +# +######################################################################## + import inspect import sys @@ -54,11 +67,16 @@ class AuthorizationFailure(ClientException): pass -class ConnectionRefused(ClientException): +class ConnectionError(ClientException): """Cannot connect to API service.""" pass +class ConnectionRefused(ConnectionError): + """Connection refused while trying to connect to API service.""" + pass + + class AuthPluginOptionsMissing(AuthorizationFailure): """Auth plugin misses some options.""" def __init__(self, opt_names): @@ -72,7 +90,7 @@ class AuthSystemNotFound(AuthorizationFailure): """User has specified an AuthSystem that is not installed.""" def __init__(self, auth_system): super(AuthSystemNotFound, self).__init__( - _("AuthSystemNotFound: %s") % repr(auth_system)) + _("AuthSystemNotFound: %r") % auth_system) self.auth_system = auth_system @@ -95,7 +113,7 @@ class AmbiguousEndpoints(EndpointException): """Found more than one matching endpoint in Service Catalog.""" def __init__(self, endpoints=None): super(AmbiguousEndpoints, self).__init__( - _("AmbiguousEndpoints: %s") % repr(endpoints)) + _("AmbiguousEndpoints: %r") % endpoints) self.endpoints = endpoints @@ -439,12 +457,15 @@ def from_response(response, method, url): except ValueError: pass else: - if isinstance(body, dict) and isinstance(body.get("error"), dict): - error = body["error"] - kwargs["message"] = error.get("message") - kwargs["details"] = error.get("details") + if isinstance(body, dict): + error = body.get(list(body)[0]) + if isinstance(error, dict): + kwargs["message"] = (error.get("message") or + error.get("faultstring")) + kwargs["details"] = (error.get("details") or + six.text_type(body)) elif content_type.startswith("text/"): - kwargs["details"] = response.text + kwargs["details"] = getattr(response, 'text', '') try: cls = _code_map[response.status_code] diff --git a/novaclient/openstack/common/apiclient/fake_client.py b/novaclient/openstack/common/apiclient/fake_client.py index eeb9b810a..1a0324c8a 100644 --- a/novaclient/openstack/common/apiclient/fake_client.py +++ b/novaclient/openstack/common/apiclient/fake_client.py @@ -21,6 +21,19 @@ places where actual behavior differs from the spec. """ +######################################################################## +# +# THIS MODULE IS DEPRECATED +# +# Please refer to +# https://etherpad.openstack.org/p/kilo-novaclient-library-proposals for +# the discussion leading to this deprecation. +# +# We recommend checking out the python-openstacksdk project +# (https://launchpad.net/python-openstacksdk) instead. +# +######################################################################## + # W0102: Dangerous default value %s as argument # pylint: disable=W0102 @@ -168,6 +181,8 @@ def client_request(self, client, method, url, **kwargs): else: status, body = resp headers = {} + self.last_request_id = headers.get('x-openstack-request-id', + 'req-test') return TestResponse({ "status_code": status, "text": body, diff --git a/novaclient/openstack/common/apiclient/utils.py b/novaclient/openstack/common/apiclient/utils.py index 09ddae43b..c8938ef54 100644 --- a/novaclient/openstack/common/apiclient/utils.py +++ b/novaclient/openstack/common/apiclient/utils.py @@ -11,12 +11,25 @@ # License for the specific language governing permissions and limitations # under the License. -from oslo.utils import encodeutils +######################################################################## +# +# THIS MODULE IS DEPRECATED +# +# Please refer to +# https://etherpad.openstack.org/p/kilo-novaclient-library-proposals for +# the discussion leading to this deprecation. +# +# We recommend checking out the python-openstacksdk project +# (https://launchpad.net/python-openstacksdk) instead. +# +######################################################################## + +from oslo_utils import encodeutils +from oslo_utils import uuidutils import six from novaclient.openstack.common._i18n import _ from novaclient.openstack.common.apiclient import exceptions -from novaclient.openstack.common import uuidutils def find_resource(manager, name_or_id, **find_args): diff --git a/novaclient/openstack/common/cliutils.py b/novaclient/openstack/common/cliutils.py index ac2cf7573..080dd2d44 100644 --- a/novaclient/openstack/common/cliutils.py +++ b/novaclient/openstack/common/cliutils.py @@ -24,8 +24,8 @@ import sys import textwrap -from oslo.utils import encodeutils -from oslo.utils import strutils +from oslo_utils import encodeutils +from oslo_utils import strutils import prettytable import six from six import moves @@ -180,7 +180,10 @@ def print_list(objs, fields, formatters=None, sortby_index=0, row.append(data) pt.add_row(row) - print(encodeutils.safe_encode(pt.get_string(**kwargs))) + if six.PY3: + print(encodeutils.safe_encode(pt.get_string(**kwargs)).decode()) + else: + print(encodeutils.safe_encode(pt.get_string(**kwargs))) def print_dict(dct, dict_property="Property", wrap=0): @@ -208,7 +211,11 @@ def print_dict(dct, dict_property="Property", wrap=0): col1 = '' else: pt.add_row([k, v]) - print(encodeutils.safe_encode(pt.get_string())) + + if six.PY3: + print(encodeutils.safe_encode(pt.get_string()).decode()) + else: + print(encodeutils.safe_encode(pt.get_string())) def get_password(max_password_prompts=3): From 667f1af9728558882171a5f349a4a02291918693 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Fri, 8 May 2015 12:36:41 +0300 Subject: [PATCH 0748/1705] Reuse uuidutils frim oslo_utils Change-Id: I0c5ec41215e97dafd7377179979b01a67704cb05 --- novaclient/openstack/common/uuidutils.py | 37 ------------------------ novaclient/v2/shell.py | 2 +- openstack-common.conf | 1 - 3 files changed, 1 insertion(+), 39 deletions(-) delete mode 100644 novaclient/openstack/common/uuidutils.py diff --git a/novaclient/openstack/common/uuidutils.py b/novaclient/openstack/common/uuidutils.py deleted file mode 100644 index 234b880c9..000000000 --- a/novaclient/openstack/common/uuidutils.py +++ /dev/null @@ -1,37 +0,0 @@ -# Copyright (c) 2012 Intel Corporation. -# All Rights Reserved. -# -# 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. - -""" -UUID related utilities and helper functions. -""" - -import uuid - - -def generate_uuid(): - return str(uuid.uuid4()) - - -def is_uuid_like(val): - """Returns validation of a value as a UUID. - - For our purposes, a UUID is a canonical form string: - aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa - - """ - try: - return str(uuid.UUID(val)) == val - except (TypeError, ValueError, AttributeError): - return False diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 9dfa78099..39278f812 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -31,13 +31,13 @@ from oslo_utils import encodeutils from oslo_utils import strutils from oslo_utils import timeutils +from oslo_utils import uuidutils import six from novaclient import client from novaclient import exceptions from novaclient.i18n import _ from novaclient.openstack.common import cliutils -from novaclient.openstack.common import uuidutils from novaclient import utils from novaclient.v2 import availability_zones from novaclient.v2 import quotas diff --git a/openstack-common.conf b/openstack-common.conf index d303f591a..01962c84a 100644 --- a/openstack-common.conf +++ b/openstack-common.conf @@ -4,7 +4,6 @@ module=apiclient module=cliutils module=install_venv_common -module=uuidutils # The base module to hold the copy of openstack.common base=novaclient From 0e35f2a2fabe5beae065cc92c44f653ff76e6558 Mon Sep 17 00:00:00 2001 From: Doug Hellmann Date: Wed, 6 May 2015 19:41:18 +0000 Subject: [PATCH 0749/1705] Drop use of 'oslo' namespace package The Oslo libraries have moved all of their code out of the 'oslo' namespace package into per-library packages. The namespace package was retained during kilo for backwards compatibility, but will be removed by the liberty-2 milestone. This change removes the use of the namespace package, replacing it with the new package names. The patches in the libraries will be put on hold until application patches have landed, or L2, whichever comes first. At that point, new versions of the libraries without namespace packages will be released as a major version update. Please merge this patch, or an equivalent, before L2 to avoid problems with those library releases. Blueprint: remove-namespace-packages https://blueprints.launchpad.net/oslo-incubator/+spec/remove-namespace-packages Change-Id: If5e8257085b5fd0a26a34705505fc0077adbabb2 --- novaclient/openstack/common/apiclient/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/openstack/common/apiclient/base.py b/novaclient/openstack/common/apiclient/base.py index cc5c8b005..ae067c6f1 100644 --- a/novaclient/openstack/common/apiclient/base.py +++ b/novaclient/openstack/common/apiclient/base.py @@ -26,7 +26,7 @@ import abc import copy -from oslo.utils import strutils +from oslo_utils import strutils import six from six.moves.urllib import parse From 9cfecf922a27703f05f220c1cacd6e5d71733453 Mon Sep 17 00:00:00 2001 From: Rui Chen Date: Wed, 13 May 2015 16:06:21 +0800 Subject: [PATCH 0750/1705] server-group-list support 'all_projects' parameter Supporting 'all_projects' in server-group-list so that admin users can list other tenant's server groups. Change-Id: Ie50288464ebc6585590a1a74efa8804584771ebc Closes-Bug: #1454523 --- novaclient/tests/unit/v2/test_server_groups.py | 7 +++++++ novaclient/tests/unit/v2/test_shell.py | 8 ++++++++ novaclient/v2/server_groups.py | 5 +++-- novaclient/v2/shell.py | 8 +++++++- 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/novaclient/tests/unit/v2/test_server_groups.py b/novaclient/tests/unit/v2/test_server_groups.py index fcda2dec7..20b3f4f43 100644 --- a/novaclient/tests/unit/v2/test_server_groups.py +++ b/novaclient/tests/unit/v2/test_server_groups.py @@ -31,6 +31,13 @@ def test_list_server_groups(self): self.assertTrue(isinstance(server_group, server_groups.ServerGroup)) + def test_list_server_groups_with_all_projects(self): + result = self.cs.server_groups.list(all_projects=True) + self.assert_called('GET', '/os-server-groups?all_projects') + for server_group in result: + self.assertTrue(isinstance(server_group, + server_groups.ServerGroup)) + def test_create_server_group(self): kwargs = {'name': 'ig1', 'policies': ['anti-affinity']} diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 14bb14959..91f0e27e4 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2405,6 +2405,14 @@ def test_delete_multi_server_groups(self): self.assert_called('DELETE', '/os-server-groups/56789') self.assert_called('DELETE', '/os-server-groups/12345', pos=-2) + def test_list_server_group(self): + self.run_command('server-group-list') + self.assert_called('GET', '/os-server-groups') + + def test_list_server_group_with_all_projects(self): + self.run_command('server-group-list --all-projects') + self.assert_called('GET', '/os-server-groups?all_projects') + class ShellTestV11(ShellTest): FAKE_ENV = { diff --git a/novaclient/v2/server_groups.py b/novaclient/v2/server_groups.py index a5205e772..c699c65a7 100644 --- a/novaclient/v2/server_groups.py +++ b/novaclient/v2/server_groups.py @@ -39,12 +39,13 @@ class ServerGroupsManager(base.ManagerWithFind): """ resource_class = ServerGroup - def list(self): + def list(self, all_projects=False): """Get a list of all server groups. :rtype: list of :class:`ServerGroup`. """ - return self._list('/os-server-groups', 'server_groups') + all = '?all_projects' if all_projects else '' + return self._list('/os-server-groups%s' % all, 'server_groups') def get(self, id): """Get a specific server group. diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 39278f812..f304988be 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -4350,9 +4350,15 @@ def _print_server_group_details(server_group): utils.print_list(server_group, columns) +@cliutils.arg( + '--all-projects', + dest='all_projects', + action='store_true', + default=False, + help=_('Display server groups from all projects (Admin only).')) def do_server_group_list(cs, args): """Print a list of all server groups.""" - server_groups = cs.server_groups.list() + server_groups = cs.server_groups.list(args.all_projects) _print_server_group_details(server_groups) From 6379287480bf6bc0a8f80bc93f01005a31882aec Mon Sep 17 00:00:00 2001 From: Sean Dague Date: Thu, 14 May 2015 07:06:05 -0400 Subject: [PATCH 0751/1705] pass credentials via config file instead of magic Passing credentials via an assumed environment inheritance is bad form, and breaks under new tox. Honestly, we only really need 2 vars passed in, so we might as well make it a config file that we'll parse. Add a functional_creds.conf.sample for people to know what one should look like. Update README to explain it. Related-Bug: #1455102 Change-Id: Ifdab38a03c94f51d30449149c0dbd9c6265460a5 --- README.rst | 13 +++++++ functional_creds.conf.sample | 8 +++++ novaclient/tests/functional/base.py | 35 ++++++++++++++----- .../tests/functional/hooks/post_test_hook.sh | 17 +++++++-- 4 files changed, 63 insertions(+), 10 deletions(-) create mode 100644 functional_creds.conf.sample diff --git a/README.rst b/README.rst index 84bf9c172..edf26ec60 100644 --- a/README.rst +++ b/README.rst @@ -83,3 +83,16 @@ To use with nova, with keystone as the authentication system:: * Documentation: http://docs.openstack.org/developer/python-novaclient * Source: http://git.openstack.org/cgit/openstack/python-novaclient * Bugs: http://bugs.launchpad.net/python-novaclient + +Testing +------- + +There are multiple test targets that can be run to validate the code. + +* tox -e pep8 - style guidelines enforcement +* tox -e py27 - traditional unit testing +* tox -e functional - live functional testing against an existing + openstack + +Functional testing assumes the existance of a functional_creds.conf in +the root directory. See the .sample for example format. diff --git a/functional_creds.conf.sample b/functional_creds.conf.sample new file mode 100644 index 000000000..081a73681 --- /dev/null +++ b/functional_creds.conf.sample @@ -0,0 +1,8 @@ +# Credentials for functional testing +[auth] +uri = http://10.42.0.50:5000/v2.0 + +[admin] +user = admin +tenant = admin +pass = secrete diff --git a/novaclient/tests/functional/base.py b/novaclient/tests/functional/base.py index 2ccdbf448..ac7953259 100644 --- a/novaclient/tests/functional/base.py +++ b/novaclient/tests/functional/base.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import ConfigParser import os import fixtures @@ -90,14 +91,32 @@ def setUp(self): format=self.log_format, level=None)) + # Collecting of credentials: + # + # Support the existence of a functional_creds.conf for + # testing. This makes it possible to use a config file. + # + # Those variables can be overridden by environmental variables + # as well to support existing users running these the old + # way. We should deprecate that. + # TODO(sdague): while we collect this information in # tempest-lib, we do it in a way that's not available for top # level tests. Long term this probably needs to be in the base # class. - user = os.environ['OS_USERNAME'] - passwd = os.environ['OS_PASSWORD'] - tenant = os.environ['OS_TENANT_NAME'] - auth_url = os.environ['OS_AUTH_URL'] + user = os.environ.get('OS_USERNAME') + passwd = os.environ.get('OS_PASSWORD') + tenant = os.environ.get('OS_TENANT_NAME') + auth_url = os.environ.get('OS_AUTH_URL') + + config = ConfigParser.RawConfigParser() + if config.read('functional_creds.conf'): + # the OR pattern means the environment is preferred for + # override + user = user or config.get('admin', 'user') + passwd = passwd or config.get('admin', 'pass') + tenant = tenant or config.get('admin', 'tenant') + auth_url = auth_url or config.get('auth', 'uri') # TODO(sdague): we made a lot of fun of the glanceclient team # for version as int in first parameter. I guess we know where @@ -120,10 +139,10 @@ def setUp(self): os.path.join(os.path.abspath('.'), '.tox/functional/bin')) self.cli_clients = tempest_lib.cli.base.CLIClient( - username=os.environ.get('OS_USERNAME'), - password=os.environ.get('OS_PASSWORD'), - tenant_name=os.environ.get('OS_TENANT_NAME'), - uri=os.environ.get('OS_AUTH_URL'), + username=user, + password=passwd, + tenant_name=tenant, + uri=auth_url, cli_dir=cli_dir) def nova(self, *args, **kwargs): diff --git a/novaclient/tests/functional/hooks/post_test_hook.sh b/novaclient/tests/functional/hooks/post_test_hook.sh index e9e35b24e..5878ace9f 100755 --- a/novaclient/tests/functional/hooks/post_test_hook.sh +++ b/novaclient/tests/functional/hooks/post_test_hook.sh @@ -28,15 +28,28 @@ function generate_testr_results { export NOVACLIENT_DIR="$BASE/new/python-novaclient" +sudo chown -R jenkins:stack $NOVACLIENT_DIR + # Get admin credentials cd $BASE/new/devstack source openrc admin admin +# pass the appropriate variables via a config file +CREDS_FILE=$NOVACLIENT_DIR/functional_creds.conf +cat < $CREDS_FILE +# Credentials for functional testing +[auth] +uri = $OS_AUTH_URL + +[admin] +user = $OS_USERNAME +tenant = $OS_TENANT_NAME +pass = $OS_PASSWORD + +EOF # Go to the novaclient dir cd $NOVACLIENT_DIR -sudo chown -R jenkins:stack $NOVACLIENT_DIR - # Run tests echo "Running novaclient functional test suite" set +e From 22569f218e4ca461da4e50dfc686adae6a44d2ba Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 14 May 2015 12:18:02 -0400 Subject: [PATCH 0752/1705] Use clouds.yaml for functional test credentials devstack emits a clouds.yaml file now, so it can be used for finding the credentials needed to connect to the cloud for functional testing. Depends-On: I1150b943f52f10d19f8434b27e8dde73a14d7843 Change-Id: If278565ef07de1a8fef3ff96fc3608e41f3ceea3 --- README.rst | 7 ++- functional_creds.conf.sample | 8 --- novaclient/tests/functional/base.py | 62 +++++++++++++------ .../tests/functional/hooks/post_test_hook.sh | 20 ++---- test-requirements.txt | 1 + 5 files changed, 52 insertions(+), 46 deletions(-) delete mode 100644 functional_creds.conf.sample diff --git a/README.rst b/README.rst index edf26ec60..a98c198c6 100644 --- a/README.rst +++ b/README.rst @@ -94,5 +94,8 @@ There are multiple test targets that can be run to validate the code. * tox -e functional - live functional testing against an existing openstack -Functional testing assumes the existance of a functional_creds.conf in -the root directory. See the .sample for example format. +Functional testing assumes the existance of a `clouds.yaml` file as supported +by `os-client-config` (http://docs.openstack.org/developer/os-client-config) +It assumes the existence of a cloud named `devstack` that behaves like a normal +devstack installation with a demo and an admin user/tenant - or clouds named +`functional_admin` and `functional_nonadmin`. diff --git a/functional_creds.conf.sample b/functional_creds.conf.sample deleted file mode 100644 index 081a73681..000000000 --- a/functional_creds.conf.sample +++ /dev/null @@ -1,8 +0,0 @@ -# Credentials for functional testing -[auth] -uri = http://10.42.0.50:5000/v2.0 - -[admin] -user = admin -tenant = admin -pass = secrete diff --git a/novaclient/tests/functional/base.py b/novaclient/tests/functional/base.py index ac7953259..540bb22ff 100644 --- a/novaclient/tests/functional/base.py +++ b/novaclient/tests/functional/base.py @@ -10,10 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. -import ConfigParser import os import fixtures +import os_client_config import tempest_lib.cli.base import testtools @@ -50,6 +50,11 @@ class NoFlavorException(Exception): pass +class NoCloudConfigException(Exception): + """We couldn't find a cloud configuration.""" + pass + + class ClientTestBase(testtools.TestCase): """ This is a first pass at a simple read only python-novaclient test. This @@ -93,30 +98,47 @@ def setUp(self): # Collecting of credentials: # - # Support the existence of a functional_creds.conf for - # testing. This makes it possible to use a config file. + # Grab the cloud config from a user's clouds.yaml file. + # First look for a functional_admin cloud, as this is a cloud + # that the user may have defined for functional testing that has + # admin credentials. + # If that is not found, get the devstack config and override the + # username and project_name to be admin so that admin credentials + # will be used. + # + # Finally, fall back to looking for environment variables to support + # existing users running these the old way. We should deprecate that + # as tox 2.0 blanks out environment. # - # Those variables can be overridden by environmental variables - # as well to support existing users running these the old - # way. We should deprecate that. - # TODO(sdague): while we collect this information in # tempest-lib, we do it in a way that's not available for top # level tests. Long term this probably needs to be in the base # class. - user = os.environ.get('OS_USERNAME') - passwd = os.environ.get('OS_PASSWORD') - tenant = os.environ.get('OS_TENANT_NAME') - auth_url = os.environ.get('OS_AUTH_URL') - - config = ConfigParser.RawConfigParser() - if config.read('functional_creds.conf'): - # the OR pattern means the environment is preferred for - # override - user = user or config.get('admin', 'user') - passwd = passwd or config.get('admin', 'pass') - tenant = tenant or config.get('admin', 'tenant') - auth_url = auth_url or config.get('auth', 'uri') + openstack_config = os_client_config.config.OpenStackConfig() + try: + cloud_config = openstack_config.get_one_cloud('functional_admin') + except os_client_config.exceptions.OpenStackConfigException: + try: + cloud_config = openstack_config.get_one_cloud( + 'devstack', auth=dict( + username='admin', project_name='admin')) + except os_client_config.exceptions.OpenStackConfigException: + try: + cloud_config = openstack_config.get_one_cloud('envvars') + except os_client_config.exceptions.OpenStackConfigException: + cloud_config = None + + if cloud_config is None: + raise NoCloudConfigException( + "Cloud not find a cloud named functional_admin or a cloud" + " named devstack. Please check your clouds.yaml file and" + " try again.") + auth_info = cloud_config.config['auth'] + + user = auth_info['username'] + passwd = auth_info['password'] + tenant = auth_info['project_name'] + auth_url = auth_info['auth_url'] # TODO(sdague): we made a lot of fun of the glanceclient team # for version as int in first parameter. I guess we know where diff --git a/novaclient/tests/functional/hooks/post_test_hook.sh b/novaclient/tests/functional/hooks/post_test_hook.sh index 5878ace9f..8eff1fd28 100755 --- a/novaclient/tests/functional/hooks/post_test_hook.sh +++ b/novaclient/tests/functional/hooks/post_test_hook.sh @@ -30,22 +30,10 @@ export NOVACLIENT_DIR="$BASE/new/python-novaclient" sudo chown -R jenkins:stack $NOVACLIENT_DIR -# Get admin credentials -cd $BASE/new/devstack -source openrc admin admin -# pass the appropriate variables via a config file -CREDS_FILE=$NOVACLIENT_DIR/functional_creds.conf -cat < $CREDS_FILE -# Credentials for functional testing -[auth] -uri = $OS_AUTH_URL - -[admin] -user = $OS_USERNAME -tenant = $OS_TENANT_NAME -pass = $OS_PASSWORD - -EOF +# ensure clouds.yaml exists +mkdir -p ~/.config/openstack +sudo cp -a ~stack/.config/openstack/clouds.yaml ~/.config/openstack +sudo chown -R jenkins:stack ~/.config/openstack # Go to the novaclient dir cd $NOVACLIENT_DIR diff --git a/test-requirements.txt b/test-requirements.txt index 225265245..ca767e764 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -10,6 +10,7 @@ keyring>=2.1,!=3.3 mock>=1.0 requests-mock>=0.6.0 # Apache-2.0 sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3 +os-client-config oslosphinx>=2.5.0 # Apache-2.0 testrepository>=0.0.18 testscenarios>=0.4 From aa4c947519ccd6258ae3e228969ab61af06b94e8 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Fri, 15 May 2015 18:05:14 +0300 Subject: [PATCH 0753/1705] Remove redundant check for version of `requests` Novaclient requirements list contains next entry: requests>=2.5.2 so we don't need to check that installed version of requests is greater than '2.4.1' Change-Id: I7d270e6d75c76bc56e21f42c683fde9284a8dfd7 --- novaclient/client.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index d2b5462c5..8650875f3 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -61,11 +61,10 @@ class TCPKeepAliveAdapter(adapters.HTTPAdapter): """The custom adapter used to set TCP Keep-Alive on all connections.""" def init_poolmanager(self, *args, **kwargs): - if requests.__version__ >= '2.4.1': - kwargs.setdefault('socket_options', [ - (socket.IPPROTO_TCP, socket.TCP_NODELAY, 1), - (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1), - ]) + kwargs.setdefault('socket_options', [ + (socket.IPPROTO_TCP, socket.TCP_NODELAY, 1), + (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1), + ]) super(TCPKeepAliveAdapter, self).init_poolmanager(*args, **kwargs) From 0a327ce3752736dbc91535355b05fc09a43c7110 Mon Sep 17 00:00:00 2001 From: "Kevin L. Mitchell" Date: Thu, 21 May 2015 19:12:35 -0500 Subject: [PATCH 0754/1705] Eliminate test comprehensions The novaclient tests are rife with examples of using a list comprehension to perform a series of asserts, when a simple for loop would be more appropriate and more idiomatic; part of the problem is the propensity for people to cut and paste. Eliminating the existing examples should remove the temptation for developers to use this non-idiom to perform tests. Change-Id: I2cc8979e720740eae81692a60e30a3d95dce2a2c --- novaclient/tests/unit/v2/test_cloudpipe.py | 3 ++- novaclient/tests/unit/v2/test_flavor_access.py | 9 ++++++--- novaclient/tests/unit/v2/test_floating_ips_bulk.py | 8 ++++---- novaclient/tests/unit/v2/test_hosts.py | 13 ++++++++----- novaclient/tests/unit/v2/test_images.py | 6 ++++-- novaclient/tests/unit/v2/test_keypairs.py | 3 ++- novaclient/tests/unit/v2/test_networks.py | 3 ++- novaclient/tests/unit/v2/test_servers.py | 6 ++++-- novaclient/tests/unit/v2/test_usage.py | 3 ++- novaclient/tests/unit/v2/test_volumes.py | 9 ++++++--- 10 files changed, 40 insertions(+), 23 deletions(-) diff --git a/novaclient/tests/unit/v2/test_cloudpipe.py b/novaclient/tests/unit/v2/test_cloudpipe.py index 3ef9533cd..59b18c362 100644 --- a/novaclient/tests/unit/v2/test_cloudpipe.py +++ b/novaclient/tests/unit/v2/test_cloudpipe.py @@ -29,7 +29,8 @@ class CloudpipeTest(utils.FixturedTestCase): def test_list_cloudpipes(self): cp = self.cs.cloudpipe.list() self.assert_called('GET', '/os-cloudpipe') - [self.assertIsInstance(c, cloudpipe.Cloudpipe) for c in cp] + for c in cp: + self.assertIsInstance(c, cloudpipe.Cloudpipe) def test_create(self): project = "test" diff --git a/novaclient/tests/unit/v2/test_flavor_access.py b/novaclient/tests/unit/v2/test_flavor_access.py index 109f177e6..7d1bca18c 100644 --- a/novaclient/tests/unit/v2/test_flavor_access.py +++ b/novaclient/tests/unit/v2/test_flavor_access.py @@ -27,7 +27,8 @@ def test_list_access_by_flavor_private(self): kwargs = {'flavor': cs.flavors.get(2)} r = cs.flavor_access.list(**kwargs) cs.assert_called('GET', '/flavors/2/os-flavor-access') - [self.assertIsInstance(a, flavor_access.FlavorAccess) for a in r] + for a in r: + self.assertIsInstance(a, flavor_access.FlavorAccess) def test_add_tenant_access(self): flavor = cs.flavors.get(2) @@ -41,7 +42,8 @@ def test_add_tenant_access(self): } cs.assert_called('POST', '/flavors/2/action', body) - [self.assertIsInstance(a, flavor_access.FlavorAccess) for a in r] + for a in r: + self.assertIsInstance(a, flavor_access.FlavorAccess) def test_remove_tenant_access(self): flavor = cs.flavors.get(2) @@ -55,7 +57,8 @@ def test_remove_tenant_access(self): } cs.assert_called('POST', '/flavors/2/action', body) - [self.assertIsInstance(a, flavor_access.FlavorAccess) for a in r] + for a in r: + self.assertIsInstance(a, flavor_access.FlavorAccess) def test_repr_flavor_access(self): flavor = cs.flavors.get(2) diff --git a/novaclient/tests/unit/v2/test_floating_ips_bulk.py b/novaclient/tests/unit/v2/test_floating_ips_bulk.py index f59150758..e5cefb6ea 100644 --- a/novaclient/tests/unit/v2/test_floating_ips_bulk.py +++ b/novaclient/tests/unit/v2/test_floating_ips_bulk.py @@ -27,14 +27,14 @@ class FloatingIPsBulkTest(utils.FixturedTestCase): def test_list_floating_ips_bulk(self): fl = self.cs.floating_ips_bulk.list() self.assert_called('GET', '/os-floating-ips-bulk') - [self.assertIsInstance(f, floating_ips.FloatingIP) - for f in fl] + for f in fl: + self.assertIsInstance(f, floating_ips.FloatingIP) def test_list_floating_ips_bulk_host_filter(self): fl = self.cs.floating_ips_bulk.list('testHost') self.assert_called('GET', '/os-floating-ips-bulk/testHost') - [self.assertIsInstance(f, floating_ips.FloatingIP) - for f in fl] + for f in fl: + self.assertIsInstance(f, floating_ips.FloatingIP) def test_create_floating_ips_bulk(self): fl = self.cs.floating_ips_bulk.create('192.168.1.0/30') diff --git a/novaclient/tests/unit/v2/test_hosts.py b/novaclient/tests/unit/v2/test_hosts.py index 029964b6b..d99627b09 100644 --- a/novaclient/tests/unit/v2/test_hosts.py +++ b/novaclient/tests/unit/v2/test_hosts.py @@ -25,19 +25,22 @@ class HostsTest(utils.FixturedTestCase): def test_describe_resource(self): hs = self.cs.hosts.get('host') self.assert_called('GET', '/os-hosts/host') - [self.assertIsInstance(h, hosts.Host) for h in hs] + for h in hs: + self.assertIsInstance(h, hosts.Host) def test_list_host(self): hs = self.cs.hosts.list() self.assert_called('GET', '/os-hosts') - [self.assertIsInstance(h, hosts.Host) for h in hs] - [self.assertEqual(h.zone, 'nova1') for h in hs] + for h in hs: + self.assertIsInstance(h, hosts.Host) + self.assertEqual(h.zone, 'nova1') def test_list_host_with_zone(self): hs = self.cs.hosts.list('nova') self.assert_called('GET', '/os-hosts?zone=nova') - [self.assertIsInstance(h, hosts.Host) for h in hs] - [self.assertEqual(h.zone, 'nova') for h in hs] + for h in hs: + self.assertIsInstance(h, hosts.Host) + self.assertEqual(h.zone, 'nova') def test_update_enable(self): host = self.cs.hosts.get('sample_host')[0] diff --git a/novaclient/tests/unit/v2/test_images.py b/novaclient/tests/unit/v2/test_images.py index 0cd110fb5..4c25e8af8 100644 --- a/novaclient/tests/unit/v2/test_images.py +++ b/novaclient/tests/unit/v2/test_images.py @@ -25,13 +25,15 @@ class ImagesTest(utils.FixturedTestCase): def test_list_images(self): il = self.cs.images.list() self.assert_called('GET', '/images/detail') - [self.assertIsInstance(i, images.Image) for i in il] + for i in il: + self.assertIsInstance(i, images.Image) self.assertEqual(2, len(il)) def test_list_images_undetailed(self): il = self.cs.images.list(detailed=False) self.assert_called('GET', '/images') - [self.assertIsInstance(i, images.Image) for i in il] + for i in il: + self.assertIsInstance(i, images.Image) def test_list_images_with_limit(self): self.cs.images.list(limit=4) diff --git a/novaclient/tests/unit/v2/test_keypairs.py b/novaclient/tests/unit/v2/test_keypairs.py index 91c249f82..ac3602f69 100644 --- a/novaclient/tests/unit/v2/test_keypairs.py +++ b/novaclient/tests/unit/v2/test_keypairs.py @@ -42,7 +42,8 @@ def test_get_keypair(self): def test_list_keypairs(self): kps = self.cs.keypairs.list() self.assert_called('GET', '/%s' % self.keypair_prefix) - [self.assertIsInstance(kp, keypairs.Keypair) for kp in kps] + for kp in kps: + self.assertIsInstance(kp, keypairs.Keypair) def test_delete_keypair(self): kp = self.cs.keypairs.list()[0] diff --git a/novaclient/tests/unit/v2/test_networks.py b/novaclient/tests/unit/v2/test_networks.py index a45ba80dc..8de3fff20 100644 --- a/novaclient/tests/unit/v2/test_networks.py +++ b/novaclient/tests/unit/v2/test_networks.py @@ -25,7 +25,8 @@ class NetworksTest(utils.FixturedTestCase): def test_list_networks(self): fl = self.cs.networks.list() self.assert_called('GET', '/os-networks') - [self.assertIsInstance(f, networks.Network) for f in fl] + for f in fl: + self.assertIsInstance(f, networks.Network) def test_get_network(self): f = self.cs.networks.get(1) diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index 603b3a9e0..c195da50d 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -40,12 +40,14 @@ def setUp(self): def test_list_servers(self): sl = self.cs.servers.list() self.assert_called('GET', '/servers/detail') - [self.assertIsInstance(s, servers.Server) for s in sl] + for s in sl: + self.assertIsInstance(s, servers.Server) def test_list_servers_undetailed(self): sl = self.cs.servers.list(detailed=False) self.assert_called('GET', '/servers') - [self.assertIsInstance(s, servers.Server) for s in sl] + for s in sl: + self.assertIsInstance(s, servers.Server) def test_list_servers_with_marker_limit(self): sl = self.cs.servers.list(marker=1234, limit=2) diff --git a/novaclient/tests/unit/v2/test_usage.py b/novaclient/tests/unit/v2/test_usage.py index a92be0e0d..768d98ce4 100644 --- a/novaclient/tests/unit/v2/test_usage.py +++ b/novaclient/tests/unit/v2/test_usage.py @@ -40,7 +40,8 @@ def test_usage_list(self, detailed=False): ("start=%s&" % now.isoformat()) + ("end=%s&" % now.isoformat()) + ("detailed=%s" % int(bool(detailed)))) - [self.assertIsInstance(u, usage.Usage) for u in usages] + for u in usages: + self.assertIsInstance(u, usage.Usage) def test_usage_list_detailed(self): self.test_usage_list(True) diff --git a/novaclient/tests/unit/v2/test_volumes.py b/novaclient/tests/unit/v2/test_volumes.py index 52b81ab6a..f3da71d33 100644 --- a/novaclient/tests/unit/v2/test_volumes.py +++ b/novaclient/tests/unit/v2/test_volumes.py @@ -26,12 +26,14 @@ class VolumesTest(utils.TestCase): def test_list_servers(self): vl = cs.volumes.list() cs.assert_called('GET', '/volumes/detail') - [self.assertIsInstance(v, volumes.Volume) for v in vl] + for v in vl: + self.assertIsInstance(v, volumes.Volume) def test_list_volumes_undetailed(self): vl = cs.volumes.list(detailed=False) cs.assert_called('GET', '/volumes') - [self.assertIsInstance(v, volumes.Volume) for v in vl] + for v in vl: + self.assertIsInstance(v, volumes.Volume) def test_get_volume_details(self): vol_id = '15e59938-07d5-11e1-90e3-e3dffe0c5983' @@ -86,7 +88,8 @@ def test_get_server_volume(self): def test_list_server_volumes(self): vl = cs.volumes.get_server_volumes(1234) cs.assert_called('GET', '/servers/1234/os-volume_attachments') - [self.assertIsInstance(v, volumes.Volume) for v in vl] + for v in vl: + self.assertIsInstance(v, volumes.Volume) def test_delete_server_volume(self): cs.volumes.delete_server_volume(1234, 'Work') From e649cea843b8453d268da8ea74ea180f513d5ea3 Mon Sep 17 00:00:00 2001 From: Doug Hellmann Date: Wed, 27 May 2015 18:08:02 +0000 Subject: [PATCH 0755/1705] Do not check requirements when loading entry points Update the calls to pkg_resources to avoid forcing a requirements check when the plugins are being loaded. There are 2 versions of the entry point API in different releases of setuptools. In one version, the require keyword argument can be passed to load(). In the other, separate methods resolve() and require() need to be used. This change updates the mock and fake objects to support either, since the fakes are subclasses of the EntryPoint class in pkg_resources. It would be better to replace the calls to pkg_resources with stevedore, which provides a more stable API, abstracts away this difference, and provides an API for creating test managers directly. That change would have required more extensive updates to the test suite, though, and since I'm not as familiar with this code base as others will be, I will leave those changes for someone else. Change-Id: I2a9aeb53ccad04c7fa687f25340306b84218f9ff Partial-bug: #1457100 --- novaclient/auth_plugin.py | 9 +++- novaclient/tests/unit/test_auth_plugins.py | 62 ++++++++++++++++------ novaclient/utils.py | 9 +++- 3 files changed, 62 insertions(+), 18 deletions(-) diff --git a/novaclient/auth_plugin.py b/novaclient/auth_plugin.py index da2c07b26..d729c4d5a 100644 --- a/novaclient/auth_plugin.py +++ b/novaclient/auth_plugin.py @@ -37,7 +37,14 @@ def discover_auth_systems(): ep_name = 'openstack.client.auth_plugin' for ep in pkg_resources.iter_entry_points(ep_name): try: - auth_plugin = ep.load() + # FIXME(dhellmann): It would be better to use stevedore + # here, since it abstracts this difference in behavior + # between versions of setuptools, but this seemed like a + # more expedient fix. + if hasattr(ep, 'resolve') and hasattr(ep, 'require'): + auth_plugin = ep.resolve() + else: + auth_plugin = ep.load(require=False) except (ImportError, pkg_resources.UnknownExtra, AttributeError) as e: logger.debug("ERROR: Cannot load auth plugin %s" % ep.name) logger.debug(e, exc_info=1) diff --git a/novaclient/tests/unit/test_auth_plugins.py b/novaclient/tests/unit/test_auth_plugins.py index 675102ca3..9dc02b02a 100644 --- a/novaclient/tests/unit/test_auth_plugins.py +++ b/novaclient/tests/unit/test_auth_plugins.py @@ -58,7 +58,10 @@ def requested_headers(cs): class DeprecatedAuthPluginTest(utils.TestCase): def test_auth_system_success(self): class MockEntrypoint(pkg_resources.EntryPoint): - def load(self): + def load(self, require=False): + return self.authenticate + + def resolve(self): return self.authenticate def authenticate(self, cls, auth_url): @@ -117,14 +120,20 @@ def test_auth_call(): def test_auth_system_defining_auth_url(self): class MockAuthUrlEntrypoint(pkg_resources.EntryPoint): - def load(self): + def load(self, require=False): + return self.auth_url + + def resolve(self): return self.auth_url def auth_url(self): return "http://faked/v2.0" class MockAuthenticateEntrypoint(pkg_resources.EntryPoint): - def load(self): + def load(self, require=False): + return self.authenticate + + def resolve(self): return self.authenticate def authenticate(self, cls, auth_url): @@ -160,7 +169,10 @@ def test_auth_call(): @mock.patch.object(pkg_resources, "iter_entry_points") def test_client_raises_exc_without_auth_url(self, mock_iter_entry_points): class MockAuthUrlEntrypoint(pkg_resources.EntryPoint): - def load(self): + def load(self, require=False): + return self.auth_url + + def resolve(self): return self.auth_url def auth_url(self): @@ -184,14 +196,17 @@ class AuthPluginTest(utils.TestCase): def test_auth_system_success(self, mock_iter_entry_points, mock_request): """Test that we can authenticate using the auth system.""" class MockEntrypoint(pkg_resources.EntryPoint): - def load(self): + def load(self, require=False): + return FakePlugin + + def resolve(self): return FakePlugin class FakePlugin(auth_plugin.BaseAuthPlugin): def authenticate(self, cls, auth_url): cls._authenticate(auth_url, {"fake": "me"}) - mock_iter_entry_points.side_effect = lambda _t: [ + mock_iter_entry_points.side_effect = lambda _t, name=None: [ MockEntrypoint("fake", "fake", ["FakePlugin"])] mock_request.side_effect = mock_http_request() @@ -227,10 +242,13 @@ def add_opts(parser): return parser class MockEntrypoint(pkg_resources.EntryPoint): - def load(self): + def load(self, require=False): return FakePlugin - mock_iter_entry_points.side_effect = lambda _t: [ + def resolve(self): + return FakePlugin + + mock_iter_entry_points.side_effect = lambda _t, name=None: [ MockEntrypoint("fake", "fake", ["FakePlugin"])] parser = argparse.ArgumentParser() @@ -244,7 +262,10 @@ def load(self): def test_parse_auth_system_options(self, mock_iter_entry_points): """Test that we can parse the auth system options.""" class MockEntrypoint(pkg_resources.EntryPoint): - def load(self): + def load(self, require=False): + return FakePlugin + + def resolve(self): return FakePlugin class FakePlugin(auth_plugin.BaseAuthPlugin): @@ -254,7 +275,7 @@ def __init__(self): def parse_opts(self, args): return self.opts - mock_iter_entry_points.side_effect = lambda _t: [ + mock_iter_entry_points.side_effect = lambda _t, name=None: [ MockEntrypoint("fake", "fake", ["FakePlugin"])] auth_plugin.discover_auth_systems() @@ -267,14 +288,17 @@ def parse_opts(self, args): def test_auth_system_defining_url(self, mock_iter_entry_points): """Test the auth_system defining an url.""" class MockEntrypoint(pkg_resources.EntryPoint): - def load(self): + def load(self, require=False): + return FakePlugin + + def resolve(self): return FakePlugin class FakePlugin(auth_plugin.BaseAuthPlugin): def get_auth_url(self): return "http://faked/v2.0" - mock_iter_entry_points.side_effect = lambda _t: [ + mock_iter_entry_points.side_effect = lambda _t, name=None: [ MockEntrypoint("fake", "fake", ["FakePlugin"])] auth_plugin.discover_auth_systems() @@ -289,13 +313,16 @@ def get_auth_url(self): def test_exception_if_no_authenticate(self, mock_iter_entry_points): """Test that no authenticate raises a proper exception.""" class MockEntrypoint(pkg_resources.EntryPoint): - def load(self): + def load(self, require=False): + return FakePlugin + + def resolve(self): return FakePlugin class FakePlugin(auth_plugin.BaseAuthPlugin): pass - mock_iter_entry_points.side_effect = lambda _t: [ + mock_iter_entry_points.side_effect = lambda _t, name=None: [ MockEntrypoint("fake", "fake", ["FakePlugin"])] auth_plugin.discover_auth_systems() @@ -310,13 +337,16 @@ class FakePlugin(auth_plugin.BaseAuthPlugin): def test_exception_if_no_url(self, mock_iter_entry_points): """Test that no auth_url at all raises exception.""" class MockEntrypoint(pkg_resources.EntryPoint): - def load(self): + def load(self, require=False): + return FakePlugin + + def resolve(self): return FakePlugin class FakePlugin(auth_plugin.BaseAuthPlugin): pass - mock_iter_entry_points.side_effect = lambda _t: [ + mock_iter_entry_points.side_effect = lambda _t, name=None: [ MockEntrypoint("fake", "fake", ["FakePlugin"])] auth_plugin.discover_auth_systems() diff --git a/novaclient/utils.py b/novaclient/utils.py index 50310bfdb..1f4df7a57 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -319,7 +319,14 @@ def _load_entry_point(ep_name, name=None): """Try to load the entry point ep_name that matches name.""" for ep in pkg_resources.iter_entry_points(ep_name, name=name): try: - return ep.load() + # FIXME(dhellmann): It would be better to use stevedore + # here, since it abstracts this difference in behavior + # between versions of setuptools, but this seemed like a + # more expedient fix. + if hasattr(ep, 'resolve') and hasattr(ep, 'require'): + return ep.resolve() + else: + return ep.load(require=False) except (ImportError, pkg_resources.UnknownExtra, AttributeError): continue From 23f13437dd64496fcbc138bbaa9b0ac615a3cf23 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Fri, 22 May 2015 13:34:51 -0700 Subject: [PATCH 0756/1705] Deprecate volume/volume-type/volume-snapshot CRUD CLIs/APIs This deprecates all of the volume CLIs/APIs that go directly to the cinder API via the volume service type. This will emit a warning each time a deprecated CLI/API is used and also updates the help docs for the deprecated CLIs and docstrings for APIs. The plan is to do a release once this is merged so people start seeing it and then we'll actually remove the deprecated CLIs/APIs in the first python-novaclient release after the Nova server 2016.1 'M' release. DocImpact: The volume, volume-type and volume-snapshot CRUD CLIs/APIs are deprecated and will be removed after the Nova 2016.1 release. Use python-cinderclient or openstackclient for CLIs instead. Use python-cinderclient or python-openstacksdk for APIs instead. Related-Bug: #1454369 Change-Id: I4e92f924739de9b664e08117984bfb60359f2212 --- .../tests/functional/test_readonly_nova.py | 6 ++- novaclient/tests/unit/v2/test_shell.py | 33 ++++++++------- novaclient/tests/unit/v2/test_volumes.py | 24 ++++++++--- novaclient/v2/shell.py | 41 ++++++++++++++----- novaclient/v2/volume_snapshots.py | 34 +++++++++++---- novaclient/v2/volume_types.py | 32 +++++++++++---- novaclient/v2/volumes.py | 33 +++++++++++---- 7 files changed, 149 insertions(+), 54 deletions(-) diff --git a/novaclient/tests/functional/test_readonly_nova.py b/novaclient/tests/functional/test_readonly_nova.py index c67c3a378..67454e3cd 100644 --- a/novaclient/tests/functional/test_readonly_nova.py +++ b/novaclient/tests/functional/test_readonly_nova.py @@ -134,10 +134,12 @@ def test_admin_volume_list(self): self.nova('volume-list') def test_admin_volume_snapshot_list(self): - self.nova('volume-snapshot-list') + out = self.nova('volume-snapshot-list', merge_stderr=True) + self.assertIn('Command volume-snapshot-list is deprecated', out) def test_admin_volume_type_list(self): - self.nova('volume-type-list') + out = self.nova('volume-type-list', merge_stderr=True) + self.assertIn('Command volume-type-list is deprecated', out) def test_admin_help(self): self.nova('help') diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 91f0e27e4..d6ddc2e14 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -74,12 +74,13 @@ def setUp(self): lambda *_: fakes.FakeClient)) @mock.patch('sys.stdout', new_callable=six.StringIO) - def run_command(self, cmd, mock_stdout): + @mock.patch('sys.stderr', new_callable=six.StringIO) + def run_command(self, cmd, mock_stderr, mock_stdout): if isinstance(cmd, list): self.shell.main(cmd) else: self.shell.main(cmd.split()) - return mock_stdout.getvalue() + return mock_stdout.getvalue(), mock_stderr.getvalue() def assert_called(self, method, url, body=None, **kwargs): return self.shell.cs.assert_called(method, url, body, **kwargs) @@ -803,7 +804,7 @@ def test_create_image(self): ) def test_create_image_show(self): - output = self.run_command( + output, _ = self.run_command( 'image-create sample-server mysnapshot --show') self.assert_called_anytime( 'POST', '/servers/1234/action', @@ -908,7 +909,7 @@ def test_list_sortby_index_without_sort(self): mock.ANY, mock.ANY, mock.ANY, sortby_index=1) def test_list_fields(self): - output = self.run_command( + output, _ = self.run_command( 'list --fields ' 'host,security_groups,OS-EXT-MOD:some_thing') self.assert_called('GET', '/servers/detail') @@ -926,7 +927,7 @@ def test_reboot(self): {'reboot': {'type': 'HARD'}}) def test_rebuild(self): - output = self.run_command('rebuild sample-server 1') + output, _ = self.run_command('rebuild sample-server 1') self.assert_called('GET', '/servers?name=sample-server', pos=-6) self.assert_called('GET', '/servers/1234', pos=-5) self.assert_called('GET', '/images/1', pos=-4) @@ -937,8 +938,8 @@ def test_rebuild(self): self.assertIn('adminPass', output) def test_rebuild_password(self): - output = self.run_command('rebuild sample-server 1' - ' --rebuild-password asdf') + output, _ = self.run_command('rebuild sample-server 1' + ' --rebuild-password asdf') self.assert_called('GET', '/servers?name=sample-server', pos=-6) self.assert_called('GET', '/servers/1234', pos=-5) self.assert_called('GET', '/images/1', pos=-4) @@ -1091,7 +1092,7 @@ def test_show_bad_id(self): self.run_command, 'show xxx') def test_show_unavailable_image_and_flavor(self): - output = self.run_command('show 9013') + output, _ = self.run_command('show 9013') self.assert_called('GET', '/servers/9013', pos=-8) self.assert_called('GET', '/flavors/80645cf4-6ad3-410a-bbc8-6f3e1e291f51', @@ -1860,7 +1861,7 @@ def test_network_list(self): self.assert_called('GET', '/os-networks') def test_network_list_fields(self): - output = self.run_command( + output, _ = self.run_command( 'network-list --fields ' 'vlan,project_id') self.assert_called('GET', '/os-networks') @@ -2037,7 +2038,7 @@ def test_limits(self): self.run_command('limits --tenant 1234') self.assert_called('GET', '/limits?tenant_id=1234') - stdout = self.run_command('limits --tenant 1234') + stdout, _ = self.run_command('limits --tenant 1234') self.assertIn('Verb', stdout) self.assertIn('Name', stdout) @@ -2190,11 +2191,13 @@ def test_interface_detach(self): self.assert_called('DELETE', '/servers/1234/os-interface/port_id') def test_volume_list(self): - self.run_command('volume-list') + _, err = self.run_command('volume-list') + self.assertIn('Command volume-list is deprecated', err) self.assert_called('GET', '/volumes/detail') def test_volume_show(self): - self.run_command('volume-show Work') + _, err = self.run_command('volume-show Work') + self.assertIn('Command volume-show is deprecated', err) self.assert_called('GET', '/volumes?display_name=Work', pos=-2) self.assert_called( 'GET', @@ -2203,7 +2206,8 @@ def test_volume_show(self): ) def test_volume_create(self): - self.run_command('volume-create 2 --display-name Work') + _, err = self.run_command('volume-create 2 --display-name Work') + self.assertIn('Command volume-create is deprecated', err) self.assert_called('POST', '/volumes', {'volume': {'display_name': 'Work', @@ -2215,7 +2219,8 @@ def test_volume_create(self): 'size': 2}}) def test_volume_delete(self): - self.run_command('volume-delete Work') + _, err = self.run_command('volume-delete Work') + self.assertIn('Command volume-delete is deprecated', err) self.assert_called('DELETE', '/volumes/15e59938-07d5-11e1-90e3-e3dffe0c5983') diff --git a/novaclient/tests/unit/v2/test_volumes.py b/novaclient/tests/unit/v2/test_volumes.py index f3da71d33..2e5f3d47a 100644 --- a/novaclient/tests/unit/v2/test_volumes.py +++ b/novaclient/tests/unit/v2/test_volumes.py @@ -13,6 +13,10 @@ # License for the specific language governing permissions and limitations # under the License. +import warnings + +import mock + from novaclient.tests.unit import utils from novaclient.tests.unit.v2 import fakes from novaclient.v2 import volumes @@ -23,26 +27,33 @@ class VolumesTest(utils.TestCase): - def test_list_servers(self): + @mock.patch.object(warnings, 'warn') + def test_list_volumes(self, mock_warn): vl = cs.volumes.list() cs.assert_called('GET', '/volumes/detail') for v in vl: self.assertIsInstance(v, volumes.Volume) + self.assertEqual(1, mock_warn.call_count) - def test_list_volumes_undetailed(self): + @mock.patch.object(warnings, 'warn') + def test_list_volumes_undetailed(self, mock_warn): vl = cs.volumes.list(detailed=False) cs.assert_called('GET', '/volumes') for v in vl: self.assertIsInstance(v, volumes.Volume) + self.assertEqual(1, mock_warn.call_count) - def test_get_volume_details(self): + @mock.patch.object(warnings, 'warn') + def test_get_volume_details(self, mock_warn): vol_id = '15e59938-07d5-11e1-90e3-e3dffe0c5983' v = cs.volumes.get(vol_id) cs.assert_called('GET', '/volumes/%s' % vol_id) self.assertIsInstance(v, volumes.Volume) self.assertEqual(v.id, vol_id) + self.assertEqual(1, mock_warn.call_count) - def test_create_volume(self): + @mock.patch.object(warnings, 'warn') + def test_create_volume(self, mock_warn): v = cs.volumes.create( size=2, display_name="My volume", @@ -50,8 +61,10 @@ def test_create_volume(self): ) cs.assert_called('POST', '/volumes') self.assertIsInstance(v, volumes.Volume) + self.assertEqual(1, mock_warn.call_count) - def test_delete_volume(self): + @mock.patch.object(warnings, 'warn') + def test_delete_volume(self, mock_warn): vol_id = '15e59938-07d5-11e1-90e3-e3dffe0c5983' v = cs.volumes.get(vol_id) v.delete() @@ -60,6 +73,7 @@ def test_delete_volume(self): cs.assert_called('DELETE', '/volumes/%s' % vol_id) cs.volumes.delete(v) cs.assert_called('DELETE', '/volumes/%s' % vol_id) + self.assertEqual(4, mock_warn.call_count) def test_create_server_volume(self): v = cs.volumes.create_server_volume( diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index f304988be..d7112b751 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -61,6 +61,14 @@ } +# NOTE(mriedem): Remove this along with the deprecated commands in the first +# python-novaclient release AFTER the nova server 2016.1 'M' release. +def emit_volume_deprecation_warning(command_name): + print('WARNING: Command %s is deprecated and will be removed after Nova ' + '2016.1 is released. Use python-cinderclient or openstackclient ' + 'instead.' % command_name, file=sys.stderr) + + def _key_value_pairing(text): try: (k, v) = text.split('=', 1) @@ -1986,7 +1994,8 @@ def _translate_availability_zone_keys(collection): const=1, help=argparse.SUPPRESS) def do_volume_list(cs, args): - """List all the volumes.""" + """DEPRECATED: List all the volumes.""" + emit_volume_deprecation_warning('volume-list') search_opts = {'all_tenants': args.all_tenants} volumes = cs.volumes.list(search_opts=search_opts) _translate_volume_keys(volumes) @@ -2004,7 +2013,8 @@ def do_volume_list(cs, args): metavar='', help=_('Name or ID of the volume.')) def do_volume_show(cs, args): - """Show details about a volume.""" + """DEPRECATED: Show details about a volume.""" + emit_volume_deprecation_warning('volume-show') volume = _find_volume(cs, args.volume) _print_volume(volume) @@ -2056,7 +2066,8 @@ def do_volume_show(cs, args): help=_('Optional Availability Zone for volume. (Default=None)'), default=None) def do_volume_create(cs, args): - """Add a new volume.""" + """DEPRECATED: Add a new volume.""" + emit_volume_deprecation_warning('volume-create') volume = cs.volumes.create(args.size, args.snapshot_id, args.display_name, @@ -2072,7 +2083,8 @@ def do_volume_create(cs, args): metavar='', nargs='+', help=_('Name or ID of the volume(s) to delete.')) def do_volume_delete(cs, args): - """Remove volume(s).""" + """DEPRECATED: Remove volume(s).""" + emit_volume_deprecation_warning('volume-delete') for volume in args.volume: try: _find_volume(cs, volume).delete() @@ -2138,7 +2150,8 @@ def do_volume_detach(cs, args): def do_volume_snapshot_list(cs, _args): - """List all the snapshots.""" + """DEPRECATED: List all the snapshots.""" + emit_volume_deprecation_warning('volume-snapshot-list') snapshots = cs.volume_snapshots.list() _translate_volume_snapshot_keys(snapshots) utils.print_list(snapshots, ['ID', 'Volume ID', 'Status', 'Display Name', @@ -2150,7 +2163,8 @@ def do_volume_snapshot_list(cs, _args): metavar='', help=_('Name or ID of the snapshot.')) def do_volume_snapshot_show(cs, args): - """Show details about a snapshot.""" + """DEPRECATED: Show details about a snapshot.""" + emit_volume_deprecation_warning('volume-snapshot-show') snapshot = _find_volume_snapshot(cs, args.snapshot) _print_volume_snapshot(snapshot) @@ -2182,7 +2196,8 @@ def do_volume_snapshot_show(cs, args): '--display_description', help=argparse.SUPPRESS) def do_volume_snapshot_create(cs, args): - """Add a new snapshot.""" + """DEPRECATED: Add a new snapshot.""" + emit_volume_deprecation_warning('volume-snapshot-create') snapshot = cs.volume_snapshots.create(args.volume_id, args.force, args.display_name, @@ -2195,7 +2210,8 @@ def do_volume_snapshot_create(cs, args): metavar='', help=_('Name or ID of the snapshot to delete.')) def do_volume_snapshot_delete(cs, args): - """Remove a snapshot.""" + """DEPRECATED: Remove a snapshot.""" + emit_volume_deprecation_warning('volume-snapshot-delete') snapshot = _find_volume_snapshot(cs, args.snapshot) snapshot.delete() @@ -2205,7 +2221,8 @@ def _print_volume_type_list(vtypes): def do_volume_type_list(cs, args): - """Print a list of available 'volume types'.""" + """DEPRECATED: Print a list of available 'volume types'.""" + emit_volume_deprecation_warning('volume-type-list') vtypes = cs.volume_types.list() _print_volume_type_list(vtypes) @@ -2215,7 +2232,8 @@ def do_volume_type_list(cs, args): metavar='', help=_("Name of the new volume type")) def do_volume_type_create(cs, args): - """Create a new volume type.""" + """DEPRECATED: Create a new volume type.""" + emit_volume_deprecation_warning('volume-type-create') vtype = cs.volume_types.create(args.name) _print_volume_type_list([vtype]) @@ -2225,7 +2243,8 @@ def do_volume_type_create(cs, args): metavar='', help=_("Unique ID of the volume type to delete")) def do_volume_type_delete(cs, args): - """Delete a specific volume type.""" + """DEPRECATED: Delete a specific volume type.""" + emit_volume_deprecation_warning('volume-type-delete') cs.volume_types.delete(args.id) diff --git a/novaclient/v2/volume_snapshots.py b/novaclient/v2/volume_snapshots.py index 6ff6deb9e..1bc154d32 100644 --- a/novaclient/v2/volume_snapshots.py +++ b/novaclient/v2/volume_snapshots.py @@ -14,15 +14,17 @@ # under the License. """ -Volume snapshot interface (1.1 extension). +DEPRECATED: Volume snapshot interface (1.1 extension). """ +import warnings + from novaclient import base class Snapshot(base.Resource): """ - A Snapshot is a point-in-time snapshot of an openstack volume. + DEPRECATED: A Snapshot is a point-in-time snapshot of an openstack volume. """ NAME_ATTR = 'display_name' @@ -31,14 +33,14 @@ def __repr__(self): def delete(self): """ - Delete this snapshot. + DEPRECATED: Delete this snapshot. """ self.manager.delete(self) class SnapshotManager(base.ManagerWithFind): """ - Manage :class:`Snapshot` resources. + DEPRECATED: Manage :class:`Snapshot` resources. """ resource_class = Snapshot @@ -46,7 +48,7 @@ def create(self, volume_id, force=False, display_name=None, display_description=None): """ - Create a snapshot of the given volume. + DEPRECATED: Create a snapshot of the given volume. :param volume_id: The ID of the volume to snapshot. :param force: If force is True, create a snapshot even if the volume is @@ -55,6 +57,10 @@ def create(self, volume_id, force=False, display_name=None, :param display_description: Description of the snapshot :rtype: :class:`Snapshot` """ + warnings.warn('The novaclient.v2.volume_snapshots module is ' + 'deprecated and will be removed after Nova 2016.1 is ' + 'released. Use python-cinderclient or ' + 'python-openstacksdk instead.', DeprecationWarning) with self.alternate_service_type('volume'): body = {'snapshot': {'volume_id': volume_id, 'force': force, @@ -64,20 +70,28 @@ def create(self, volume_id, force=False, display_name=None, def get(self, snapshot_id): """ - Get a snapshot. + DEPRECATED: Get a snapshot. :param snapshot_id: The ID of the snapshot to get. :rtype: :class:`Snapshot` """ + warnings.warn('The novaclient.v2.volume_snapshots module is ' + 'deprecated and will be removed after Nova 2016.1 is ' + 'released. Use python-cinderclient or ' + 'python-openstacksdk instead.', DeprecationWarning) with self.alternate_service_type('volume'): return self._get("/snapshots/%s" % snapshot_id, "snapshot") def list(self, detailed=True): """ - Get a list of all snapshots. + DEPRECATED: Get a list of all snapshots. :rtype: list of :class:`Snapshot` """ + warnings.warn('The novaclient.v2.volume_snapshots module is ' + 'deprecated and will be removed after Nova 2016.1 is ' + 'released. Use python-cinderclient or ' + 'python-openstacksdk instead.', DeprecationWarning) with self.alternate_service_type('volume'): if detailed is True: return self._list("/snapshots/detail", "snapshots") @@ -86,9 +100,13 @@ def list(self, detailed=True): def delete(self, snapshot): """ - Delete a snapshot. + DEPRECATED: Delete a snapshot. :param snapshot: The :class:`Snapshot` to delete. """ + warnings.warn('The novaclient.v2.volume_snapshots module is ' + 'deprecated and will be removed after Nova 2016.1 is ' + 'released. Use python-cinderclient or ' + 'python-openstacksdk instead.', DeprecationWarning) with self.alternate_service_type('volume'): self._delete("/snapshots/%s" % base.getid(snapshot)) diff --git a/novaclient/v2/volume_types.py b/novaclient/v2/volume_types.py index e36225b45..fd9268775 100644 --- a/novaclient/v2/volume_types.py +++ b/novaclient/v2/volume_types.py @@ -15,15 +15,17 @@ """ -Volume Type interface. +DEPRECATED: Volume Type interface. """ +import warnings + from novaclient import base class VolumeType(base.Resource): """ - A Volume Type is the type of volume to be created + DEPRECATED: A Volume Type is the type of volume to be created """ def __repr__(self): return "" % self.name @@ -31,46 +33,62 @@ def __repr__(self): class VolumeTypeManager(base.ManagerWithFind): """ - Manage :class:`VolumeType` resources. + DEPRECATED: Manage :class:`VolumeType` resources. """ resource_class = VolumeType def list(self): """ - Get a list of all volume types. + DEPRECATED: Get a list of all volume types. :rtype: list of :class:`VolumeType`. """ + warnings.warn('The novaclient.v2.volume_types module is deprecated ' + 'and will be removed after Nova 2016.1 is released. Use ' + 'python-cinderclient or python-openstacksdk instead.', + DeprecationWarning) with self.alternate_service_type('volume'): return self._list("/types", "volume_types") def get(self, volume_type): """ - Get a specific volume type. + DEPRECATED: Get a specific volume type. :param volume_type: The ID of the :class:`VolumeType` to get. :rtype: :class:`VolumeType` """ + warnings.warn('The novaclient.v2.volume_types module is deprecated ' + 'and will be removed after Nova 2016.1 is released. Use ' + 'python-cinderclient or python-openstacksdk instead.', + DeprecationWarning) with self.alternate_service_type('volume'): return self._get("/types/%s" % base.getid(volume_type), "volume_type") def delete(self, volume_type): """ - Delete a specific volume_type. + DEPRECATED: Delete a specific volume_type. :param volume_type: The ID of the :class:`VolumeType` to get. """ + warnings.warn('The novaclient.v2.volume_types module is deprecated ' + 'and will be removed after Nova 2016.1 is released. Use ' + 'python-cinderclient or python-openstacksdk instead.', + DeprecationWarning) with self.alternate_service_type('volume'): self._delete("/types/%s" % base.getid(volume_type)) def create(self, name): """ - Create a volume type. + DEPRECATED: Create a volume type. :param name: Descriptive name of the volume type :rtype: :class:`VolumeType` """ + warnings.warn('The novaclient.v2.volume_types module is deprecated ' + 'and will be removed after Nova 2016.1 is released. Use ' + 'python-cinderclient or python-openstacksdk instead.', + DeprecationWarning) with self.alternate_service_type('volume'): body = { "volume_type": { diff --git a/novaclient/v2/volumes.py b/novaclient/v2/volumes.py index 182698ff5..b5f2ae687 100644 --- a/novaclient/v2/volumes.py +++ b/novaclient/v2/volumes.py @@ -17,6 +17,8 @@ Volume interface (1.1 extension). """ +import warnings + import six from six.moves.urllib import parse @@ -25,7 +27,8 @@ class Volume(base.Resource): """ - A volume is an extra block level storage to the OpenStack instances. + DEPRECATED: A volume is an extra block level storage to the OpenStack + instances. """ NAME_ATTR = 'display_name' @@ -34,14 +37,14 @@ def __repr__(self): def delete(self): """ - Delete this volume. + DEPRECATED: Delete this volume. """ self.manager.delete(self) class VolumeManager(base.ManagerWithFind): """ - Manage :class:`Volume` resources. + DEPRECATED: Manage :class:`Volume` resources. """ resource_class = Volume @@ -49,7 +52,7 @@ def create(self, size, snapshot_id=None, display_name=None, display_description=None, volume_type=None, availability_zone=None, imageRef=None): """ - Create a volume. + DEPRECATED: Create a volume. :param size: Size of volume in GB :param snapshot_id: ID of the snapshot @@ -60,6 +63,10 @@ def create(self, size, snapshot_id=None, display_name=None, :rtype: :class:`Volume` :param imageRef: reference to an image stored in glance """ + warnings.warn('The novaclient.v2.volumes.VolumeManager.create() ' + 'method is deprecated and will be removed after Nova ' + '2016.1 is released. Use python-cinderclient or ' + 'python-openstacksdk instead.', DeprecationWarning) # NOTE(melwitt): Ensure we use the volume endpoint for this call with self.alternate_service_type('volume'): body = {'volume': {'size': size, @@ -73,20 +80,28 @@ def create(self, size, snapshot_id=None, display_name=None, def get(self, volume_id): """ - Get a volume. + DEPRECATED: Get a volume. :param volume_id: The ID of the volume to get. :rtype: :class:`Volume` """ + warnings.warn('The novaclient.v2.volumes.VolumeManager.get() ' + 'method is deprecated and will be removed after Nova ' + '2016.1 is released. Use python-cinderclient or ' + 'python-openstacksdk instead.', DeprecationWarning) with self.alternate_service_type('volume'): return self._get("/volumes/%s" % volume_id, "volume") def list(self, detailed=True, search_opts=None): """ - Get a list of all volumes. + DEPRECATED: Get a list of all volumes. :rtype: list of :class:`Volume` """ + warnings.warn('The novaclient.v2.volumes.VolumeManager.list() ' + 'method is deprecated and will be removed after Nova ' + '2016.1 is released. Use python-cinderclient or ' + 'python-openstacksdk instead.', DeprecationWarning) with self.alternate_service_type('volume'): search_opts = search_opts or {} @@ -105,10 +120,14 @@ def list(self, detailed=True, search_opts=None): def delete(self, volume): """ - Delete a volume. + DEPRECATED: Delete a volume. :param volume: The :class:`Volume` to delete. """ + warnings.warn('The novaclient.v2.volumes.VolumeManager.delete() ' + 'method is deprecated and will be removed after Nova ' + '2016.1 is released. Use python-cinderclient or ' + 'python-openstacksdk instead.', DeprecationWarning) with self.alternate_service_type('volume'): self._delete("/volumes/%s" % base.getid(volume)) From 3502c8aee268bd10d13565dc77cf2c1cd006b2ab Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Sat, 23 May 2015 19:22:04 -0700 Subject: [PATCH 0757/1705] Add documentation on command deprecation process Since we haven't removed deprecated commands before, add some documentation to the devref on the process for this type of change. Change-Id: Iff6f5df372bec524757b84747c7a95bbf6412c35 --- doc/source/index.rst | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/doc/source/index.rst b/doc/source/index.rst index 9bd302bcc..126805a42 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -48,6 +48,37 @@ See `Consistent Testing Interface`_ for more details. .. _Consistent Testing Interface: http://git.openstack.org/cgit/openstack/governance/tree/reference/project-testing-interface.rst +Deprecating commands +==================== + +There are times when commands need to be deprecated due to rename or removal. +The process for command deprecation is: + + 1. Push up a change for review which deprecates the command(s). + + - The change should print a deprecation warning to stderr each time a + deprecated command is used. + - That warning message should include a rough timeline for when the command + will be removed and what should be used instead, if anything. + - The commit message on the change should include a DocImpact tag so it + gets in the release notes. + - The deprecation cycle is typically the first client release *after* the + next *full* Nova server release so that there is at least six months of + deprecation. + + 2. Once the change is approved, have a member of the `nova-release`_ team + release a new version of python-novaclient. + + .. _nova-release: https://review.openstack.org/#/admin/groups/147,members + + 3. Example: ``_ + + This change was made while the Nova 2015.2 Liberty release was in + development. The current version of python-novaclient at the time was + 2.25.0. Once the change was merged, python-novaclient 2.26.0 was released. + Since there was less than six months before 2015.2 would be released, the + deprecation cycle ran through the 2016.1 Nova server release. + Indices and tables ================== From acf6d1fe65b285b2cc94ad756f4f61bdbb6c0227 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Sat, 23 May 2015 19:34:04 -0700 Subject: [PATCH 0758/1705] Remove unused novaclient.tests.unit.v2.utils module This is an old carry over from pre-testtools days and has a nose import. Nothing is using it so remove it. Closes-Bug: #1458272 Change-Id: I01b5d35d2e2aec161bd3dbaecb4670ddf3ee8794 --- novaclient/tests/unit/v2/utils.py | 42 ------------------------------- 1 file changed, 42 deletions(-) delete mode 100644 novaclient/tests/unit/v2/utils.py diff --git a/novaclient/tests/unit/v2/utils.py b/novaclient/tests/unit/v2/utils.py deleted file mode 100644 index d28392f95..000000000 --- a/novaclient/tests/unit/v2/utils.py +++ /dev/null @@ -1,42 +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 nose.tools import ok_ - - -def fail(msg): - raise AssertionError(msg) - - -def assert_in(thing, seq, msg=None): - msg = msg or "'%s' not found in %s" % (thing, seq) - ok_(thing in seq, msg) - - -def assert_not_in(thing, seq, msg=None): - msg = msg or "unexpected '%s' found in %s" % (thing, seq) - ok_(thing not in seq, msg) - - -def assert_has_keys(dict, required=[], optional=[]): - keys = dict.keys() - for k in required: - assert_in(k, keys, "required key %s missing from %s" % (k, dict)) - allowed_keys = set(required) | set(optional) - extra_keys = set(keys).difference(allowed_keys) - if extra_keys: - fail("found unexpected keys: %s" % list(extra_keys)) - - -def assert_isinstance(thing, kls): - ok_(isinstance(thing, kls), "%s is not an instance of %s" % (thing, kls)) From d37c19a13fb32424f9cf021cbef416cd2cfde53b Mon Sep 17 00:00:00 2001 From: Joe Gordon Date: Thu, 4 Jun 2015 16:56:11 +0900 Subject: [PATCH 0759/1705] Add docs tox env Just like in nova, lets add a 'docs' env to here so you can simply run 'tox -edocs' and build the docs which get published to http://docs.openstack.org/developer/python-novaclient/ Change-Id: Ie381a78477b60b4b9981576d75c911cb0deb5696 --- tox.ini | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index f3e88641d..9f17a0e33 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ # noted to use py34 you need virtualenv >= 1.11.4 [tox] -envlist = py26,py27,py33,py34,pypy,pep8 +envlist = py26,py27,py33,py34,pypy,pep8,docs minversion = 1.6 skipsdist = True @@ -26,6 +26,12 @@ commands = flake8 {posargs} [testenv:venv] commands = {posargs} +[testenv:docs] +commands = + python setup.py build_sphinx + + + [testenv:functional] setenv = OS_TEST_PATH = ./novaclient/tests/functional From 4e9a2b4c69c7dde8cf6c1f9803d6f5d7889e15f9 Mon Sep 17 00:00:00 2001 From: Joe Gordon Date: Thu, 4 Jun 2015 17:00:12 +0900 Subject: [PATCH 0760/1705] Fix all doc warnings and gate on warnings * Remove releases (File removed in e334096aa3a4badb2930c766cb4b42bbcc9ac107) * Add link to man page * Fix some docstring formatting Change-Id: Iec67a6d32f0365e514394c1e3eb7d8e4ae6aff65 --- doc/source/index.rst | 12 +++++++++++- novaclient/v2/volume_snapshots.py | 2 +- setup.cfg | 3 +++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/doc/source/index.rst b/doc/source/index.rst index 126805a42..e80274936 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -26,7 +26,6 @@ Contents: api ref/index ref/v2/index - releases Contributing ============ @@ -79,6 +78,17 @@ The process for command deprecation is: Since there was less than six months before 2015.2 would be released, the deprecation cycle ran through the 2016.1 Nova server release. + +Man Page +======== + +.. toctree:: + :maxdepth: 1 + + man/nova + + + Indices and tables ================== diff --git a/novaclient/v2/volume_snapshots.py b/novaclient/v2/volume_snapshots.py index 1bc154d32..8ca1971ac 100644 --- a/novaclient/v2/volume_snapshots.py +++ b/novaclient/v2/volume_snapshots.py @@ -52,7 +52,7 @@ def create(self, volume_id, force=False, display_name=None, :param volume_id: The ID of the volume to snapshot. :param force: If force is True, create a snapshot even if the volume is - attached to an instance. Default is False. + attached to an instance. Default is False. :param display_name: Name of the snapshot :param display_description: Description of the snapshot :rtype: :class:`Snapshot` diff --git a/setup.cfg b/setup.cfg index 6df42821b..07cb81b0f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -38,3 +38,6 @@ upload-dir = doc/build/html [wheel] universal = 1 + +[pbr] +warnerrors = true From 5ebe89f0a1ae19feb2f0fb3b187baadac1d83b08 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Thu, 4 Jun 2015 20:14:15 +0000 Subject: [PATCH 0761/1705] Updated from global requirements Change-Id: I9ae60f20f6e3050d1cc82ae78c25df1dfab876af --- requirements.txt | 2 +- test-requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 76a752f20..f98033120 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,4 +12,4 @@ requests>=2.5.2 simplejson>=2.2.0 six>=1.9.0 Babel>=1.3 -python-keystoneclient>=1.3.0 +python-keystoneclient>=1.6.0 diff --git a/test-requirements.txt b/test-requirements.txt index ca767e764..150df4be1 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -10,7 +10,7 @@ keyring>=2.1,!=3.3 mock>=1.0 requests-mock>=0.6.0 # Apache-2.0 sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3 -os-client-config +os-client-config>=1.2.0 oslosphinx>=2.5.0 # Apache-2.0 testrepository>=0.0.18 testscenarios>=0.4 From 09571ba7dc12241e26ac36e1b16ed9abef5994a8 Mon Sep 17 00:00:00 2001 From: raiesmh08 Date: Mon, 8 Jun 2015 03:25:06 -0700 Subject: [PATCH 0762/1705] Adding missing nova read only CLI test 1. nova quota-defaults 2. nova bash-completion Previously these were proposed at - https://review.openstack.org/#/c/98735/ Change-Id: I3c2e9aa652cb712b41b9347cb144044012e2338e --- novaclient/tests/functional/test_readonly_nova.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/novaclient/tests/functional/test_readonly_nova.py b/novaclient/tests/functional/test_readonly_nova.py index 67454e3cd..17cfc3097 100644 --- a/novaclient/tests/functional/test_readonly_nova.py +++ b/novaclient/tests/functional/test_readonly_nova.py @@ -161,6 +161,13 @@ def test_migration_list(self): def test_version_list(self): self.nova('version-list') + def test_quota_defaults(self): + self.nova('quota-defaults') + self.nova('quota-defaults', flags='--debug') + + def test_bash_completion(self): + self.nova('bash-completion') + # Optional arguments: def test_admin_version(self): From 953a12e986449a3bfa8b80409ecf88d93b8becd0 Mon Sep 17 00:00:00 2001 From: Masaki Matsushita Date: Sun, 17 May 2015 14:05:25 -0700 Subject: [PATCH 0763/1705] Adds support to set admin password from the cli You can set admin password as nova boot --image --flavor --admin-pass vm-name Change-Id: I119aba428c15d736e64d990be47cc5c13bb83738 Closes-Bug: #1248517 --- novaclient/tests/unit/v2/test_servers.py | 15 +++++++++++++++ novaclient/v2/servers.py | 6 ++++-- novaclient/v2/shell.py | 9 ++++++++- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index c195da50d..d16714c7c 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -205,6 +205,21 @@ def test_create_server_userdata_utf8(self): self.assert_called('POST', '/servers') self.assertIsInstance(s, servers.Server) + def test_create_server_admin_pass(self): + test_password = "test-pass" + test_key = "fakekey" + s = self.cs.servers.create( + name="My server", + image=1, + flavor=1, + admin_pass=test_password, + key_name=test_key + ) + self.assert_called('POST', '/servers') + self.assertIsInstance(s, servers.Server) + body = jsonutils.loads(self.requests.last_request.body) + self.assertEqual(test_password, body['server']['adminPass']) + def test_create_server_userdata_bin(self): with tempfile.TemporaryFile(mode='wb+') as bin_file: original_data = os.urandom(1024) diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index f10d85aa1..a8848154f 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -849,7 +849,7 @@ def create(self, name, image, flavor, meta=None, files=None, key_name=None, availability_zone=None, block_device_mapping=None, block_device_mapping_v2=None, nics=None, scheduler_hints=None, - config_drive=None, disk_config=None, **kwargs): + config_drive=None, disk_config=None, admin_pass=None, **kwargs): # TODO(anthony): indicate in doc string if param is an extension # and/or optional """ @@ -892,6 +892,8 @@ def create(self, name, image, flavor, meta=None, files=None, :param disk_config: (optional extension) control how the disk is partitioned when the server is created. possible values are 'AUTO' or 'MANUAL'. + :param admin_pass: (optional extension) add a user supplied admin + password. """ if not min_count: min_count = 1 @@ -908,7 +910,7 @@ def create(self, name, image, flavor, meta=None, files=None, max_count=max_count, security_groups=security_groups, key_name=key_name, availability_zone=availability_zone, scheduler_hints=scheduler_hints, config_drive=config_drive, - disk_config=disk_config, **kwargs) + disk_config=disk_config, admin_pass=admin_pass, **kwargs) if block_device_mapping: resource_url = "/os-volumes_boot" diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index d7112b751..a9234cb70 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -319,7 +319,8 @@ def _boot(cs, args): block_device_mapping_v2=block_device_mapping_v2, nics=nics, scheduler_hints=hints, - config_drive=config_drive) + config_drive=config_drive, + admin_pass=args.admin_pass) return boot_args, boot_kwargs @@ -502,6 +503,12 @@ def _boot(cs, args): action="store_true", default=False, help=_('Report the new server boot progress until it completes.')) +@cliutils.arg( + '--admin-pass', + dest='admin_pass', + metavar='', + default=None, + help='Admin password for the instance') def do_boot(cs, args): """Boot a new server.""" boot_args, boot_kwargs = _boot(cs, args) From bffd79d64ccd1a57b20d22187a3e05304e183518 Mon Sep 17 00:00:00 2001 From: Tony Breeds Date: Tue, 9 Jun 2015 15:25:02 +1000 Subject: [PATCH 0764/1705] Update weblinks The targets for _OpenStack CLI Guide and _OpenStack API documentation no longer point at helpful information. Update them to something more helpful. Change-Id: I1ecf796650c15864e9f848f16e96ee58db97ddcc --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index a98c198c6..0faa248e4 100644 --- a/README.rst +++ b/README.rst @@ -9,8 +9,8 @@ See the `OpenStack CLI guide`_ for information on how to use the ``nova`` command-line tool. You may also want to look at the `OpenStack API documentation`_. -.. _OpenStack CLI Guide: http://docs.openstack.org/cli/quick-start/content/ -.. _OpenStack API documentation: http://docs.openstack.org/api/ +.. _OpenStack CLI Guide: http://docs.openstack.org/cli-reference/content/novaclient_commands.html +.. _OpenStack API documentation: http://docs.openstack.org/api/quick-start/content/ The project is hosted on `Launchpad`_, where bugs can be filed. The code is hosted on `Github`_. Patches must be submitted using `Gerrit`_, *not* Github From efe988d29ab13e645277e69d26779777f952de15 Mon Sep 17 00:00:00 2001 From: melanie witt Date: Tue, 9 Jun 2015 06:31:54 +0000 Subject: [PATCH 0765/1705] Cleanup various inaccuracies in the README.rst There was a mention of v1.1, a version-specific import, omission of the required version argument creating a Client object, an irrelevant comment, use of service_type implying the ability to use novaclient with services other than nova (we have deprecated use of the volume endpoint, for example), and a misspelling. Change-Id: I91b2aa4001295e152883134326f40f887a55cc50 --- README.rst | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/README.rst b/README.rst index a98c198c6..cc09c0b6e 100644 --- a/README.rst +++ b/README.rst @@ -45,7 +45,7 @@ You will also need to define the authentication url with ``--os-auth-url`` and the version of the API with ``--os-compute-api-version``. Or set them as an environment variables as well:: - export OS_AUTH_URL=http://example.com:8774/v1.1/ + export OS_AUTH_URL=http://example.com:8774/v2/ export OS_COMPUTE_API_VERSION=2 If you are using Keystone, you need to set the OS_AUTH_URL to the keystone @@ -63,14 +63,13 @@ You'll find complete documentation on the shell by running Python API ---------- -There's also a complete Python API, but it has not yet been documented. +There's also a complete Python API, with documentation linked below. -To use with nova, with keystone as the authentication system:: +To use with keystone as the authentication system:: - # use v2.0 auth with http://example.com:5000/v2.0/") - >>> from novaclient.v2 import client - >>> nt = client.Client(USER, PASS, TENANT, AUTH_URL, service_type="compute") + >>> from novaclient import client + >>> nt = client.Client(VERSION, USER, PASSWORD, TENANT, AUTH_URL) >>> nt.flavors.list() [...] >>> nt.servers.list() @@ -94,7 +93,7 @@ There are multiple test targets that can be run to validate the code. * tox -e functional - live functional testing against an existing openstack -Functional testing assumes the existance of a `clouds.yaml` file as supported +Functional testing assumes the existence of a `clouds.yaml` file as supported by `os-client-config` (http://docs.openstack.org/developer/os-client-config) It assumes the existence of a cloud named `devstack` that behaves like a normal devstack installation with a demo and an admin user/tenant - or clouds named From 89a4ca828bda834252d5f682e753cae81fd57df7 Mon Sep 17 00:00:00 2001 From: Yusuke Ide Date: Mon, 8 Jun 2015 20:03:32 +0900 Subject: [PATCH 0766/1705] Add help message for secgroup-add/del-default-rule Add help message about only work with the nova-network backend, not neutron for 'secgroup-add-default-rule' and 'secgroup-delete-default-rule'. Change-Id: Ia88b022d86505c2d7baa920fafbbfbaee04e4e58 Closes-Bug: #1419739 --- novaclient/v2/shell.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index d7112b751..cdf6d8644 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -4404,7 +4404,7 @@ def do_secgroup_list_default_rules(cs, args): @cliutils.arg('cidr', metavar='', help=_('CIDR for address range.')) def do_secgroup_add_default_rule(cs, args): """Add a rule to the set of rules that will be added to the 'default' - security group for new tenants. + security group for new tenants (nova-network only). """ rule = cs.security_group_default_rules.create(args.ip_proto, args.from_port, @@ -4428,7 +4428,7 @@ def do_secgroup_add_default_rule(cs, args): @cliutils.arg('cidr', metavar='', help=_('CIDR for address range.')) def do_secgroup_delete_default_rule(cs, args): """Delete a rule from the set of rules that will be added to the - 'default' security group for new tenants. + 'default' security group for new tenants (nova-network only). """ for rule in cs.security_group_default_rules.list(): if (rule.ip_protocol and From 784a8b4a9113e919b6514cdb57da6b0b27ee2c63 Mon Sep 17 00:00:00 2001 From: Jimmy McCrory Date: Sun, 7 Jun 2015 11:16:11 -0500 Subject: [PATCH 0767/1705] Cache a new token when the existing token expires The client.keyring_saver attribute was not being reliably set so the client._save_keys function was not storing any new tokens in the keyring. Add a unit test to ensure keyring_saver is being property set. Closes-Bug: #1397732 Change-Id: If0df24c819d71b4df302309d049079a867a11c76 --- novaclient/shell.py | 2 +- novaclient/tests/unit/test_shell.py | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/novaclient/shell.py b/novaclient/shell.py index 1dbc08584..ec415bb6e 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -703,6 +703,7 @@ def main(self, argv): # identifying keyring key can come from the underlying client if must_auth: helper = SecretsHelper(args, self.cs.client) + self.cs.client.keyring_saver = helper if (auth_plugin and auth_plugin.opts and "os_password" not in auth_plugin.opts): use_pw = False @@ -724,7 +725,6 @@ def main(self, argv): # We're missing something, so auth with user/pass and save # the result in our helper. self.cs.client.password = helper.password - self.cs.client.keyring_saver = helper try: # This does a couple of bits which are useful even if we've diff --git a/novaclient/tests/unit/test_shell.py b/novaclient/tests/unit/test_shell.py index a8826deb5..4958023d7 100644 --- a/novaclient/tests/unit/test_shell.py +++ b/novaclient/tests/unit/test_shell.py @@ -379,6 +379,26 @@ def test_timing(self, m_times, m_requests): exc = self.assertRaises(RuntimeError, self.shell, '--timings list') self.assertEqual('Boom!', str(exc)) + @mock.patch('novaclient.shell.SecretsHelper.tenant_id', + return_value=True) + @mock.patch('novaclient.shell.SecretsHelper.auth_token', + return_value=True) + @mock.patch('novaclient.shell.SecretsHelper.management_url', + return_value=True) + @mock.patch('novaclient.client.Client') + @requests_mock.Mocker() + def test_keyring_saver_helper(self, mock_client, + sh_management_url_function, + sh_auth_token_function, + sh_tenant_id_function, + m_requests): + self.make_env(fake_env=FAKE_ENV) + self.register_keystone_discovery_fixture(m_requests) + self.shell('list') + mock_client_instance = mock_client.return_value + keyring_saver = mock_client_instance.client.keyring_saver + self.assertIsInstance(keyring_saver, novaclient.shell.SecretsHelper) + class ShellTestKeystoneV3(ShellTest): def make_env(self, exclude=None, fake_env=FAKE_ENV): From 84aec86319c6fc456db472e340188a5782e13741 Mon Sep 17 00:00:00 2001 From: Matt Thompson Date: Wed, 10 Jun 2015 13:05:18 +0100 Subject: [PATCH 0768/1705] Pass full path to pkgutil.iter_modules() On my production system, when using client version 3, an exception is raised with an incomplete message as the installed novaclient is failing to find the bundled client versions. This commit creates a new _get_available_client_versions() method and moves the client version searching from get_client_class() into it. We also update __get_available_client_versions() to pass pkgutil.iter_modules the full path to novaclient rather than 'novaclient', which is what actually resolves the issue here. Lastly, we add a single test to ensure that __get_available_client_versions() does not return an empty list. Change-Id: I07ce414863e833c72e50ad8cf1824054cfb41457 Closes-Bug: #1463809 --- novaclient/client.py | 20 +++++++++++++------- novaclient/tests/unit/test_client.py | 4 ++++ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index 8650875f3..abb1c29c6 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -773,6 +773,18 @@ def _discover_via_entry_points(): yield name, module +def _get_available_client_versions(): + # NOTE(andreykurilin): available clients version should not be + # hardcoded, so let's discover them. + matcher = re.compile(r"v[0-9_]*$") + submodules = pkgutil.iter_modules([os.path.dirname(__file__)]) + available_versions = [ + name[1:].replace("_", ".") for loader, name, ispkg in submodules + if matcher.search(name)] + + return available_versions + + def get_client_class(version): version = str(version) if version in DEPRECATED_VERSIONS: @@ -786,13 +798,7 @@ def get_client_class(version): return importutils.import_class( "novaclient.v%s.client.Client" % version) except ImportError: - # NOTE(andreykurilin): available clients version should not be - # hardcoded, so let's discover them. - matcher = re.compile(r"v[0-9_]*$") - submodules = pkgutil.iter_modules(['novaclient']) - available_versions = [ - name[1:].replace("_", ".") for loader, name, ispkg in submodules - if matcher.search(name)] + available_versions = _get_available_client_versions() msg = _("Invalid client version '%(version)s'. must be one of: " "%(keys)s") % {'version': version, 'keys': ', '.join(available_versions)} diff --git a/novaclient/tests/unit/test_client.py b/novaclient/tests/unit/test_client.py index f94827f30..ace5751a1 100644 --- a/novaclient/tests/unit/test_client.py +++ b/novaclient/tests/unit/test_client.py @@ -161,6 +161,10 @@ def test_client_version_url_with_project_name(self): self._check_version_url('http://foo.com/nova/v2/%s', 'http://foo.com/nova/') + def test_get_available_client_versions(self): + output = novaclient.client._get_available_client_versions() + self.assertNotEqual([], output) + def test_get_client_class_v2(self): output = novaclient.client.get_client_class('2') self.assertEqual(output, novaclient.v2.client.Client) From 9231f939f937ad1f591b189997821362f585abeb Mon Sep 17 00:00:00 2001 From: melanie witt Date: Mon, 15 Jun 2015 17:51:48 +0000 Subject: [PATCH 0769/1705] Add a sample clouds.yaml Since commit 22569f218e4ca461da4e50dfc686adae6a44d2ba functional testing credentials are managed by os-cloud-config. This change adds a sample config to help developers run functional tests locally. Change-Id: I16d1cb9a8bcaf18dda26af596652564b190cbe93 --- novaclient/tests/functional/clouds.yaml.sample | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 novaclient/tests/functional/clouds.yaml.sample diff --git a/novaclient/tests/functional/clouds.yaml.sample b/novaclient/tests/functional/clouds.yaml.sample new file mode 100644 index 000000000..4d65bd9bc --- /dev/null +++ b/novaclient/tests/functional/clouds.yaml.sample @@ -0,0 +1,7 @@ +clouds: + devstack: + auth: + username: admin + password: a + project_name: admin + auth_url: http://localhost:5000/v2.0 From 3026ea92960c42ce00d08fc56dfa4584dc0a9d5b Mon Sep 17 00:00:00 2001 From: Artom Lifshitz Date: Mon, 15 Jun 2015 15:25:37 -0400 Subject: [PATCH 0770/1705] Revert "Allow admin user to get all tenant's floating IPs" This reverts commit 02328d33373479b33f4fee8112d47c1ae29e8bd4. As per the comments on the support all-tenants in floatingips-list spec [1], this patch reverts the --all-tenants flag from python-novaclient since support has been reverted out of Nova more than a year ago. [1] https://review.openstack.org/#/c/171389/ Change-Id: I2d6566ecf59d307e3ea117b3b20dc918c758b63e --- novaclient/tests/unit/v2/test_floating_ips.py | 6 ------ novaclient/tests/unit/v2/test_shell.py | 4 ---- novaclient/v2/floating_ips.py | 7 ++----- novaclient/v2/shell.py | 9 ++------- 4 files changed, 4 insertions(+), 22 deletions(-) diff --git a/novaclient/tests/unit/v2/test_floating_ips.py b/novaclient/tests/unit/v2/test_floating_ips.py index 3a4775b1c..a91113945 100644 --- a/novaclient/tests/unit/v2/test_floating_ips.py +++ b/novaclient/tests/unit/v2/test_floating_ips.py @@ -31,12 +31,6 @@ def test_list_floating_ips(self): for fip in fips: self.assertIsInstance(fip, floating_ips.FloatingIP) - def test_list_floating_ips_all_tenants(self): - fips = self.cs.floating_ips.list(all_tenants=True) - self.assert_called('GET', '/os-floating-ips?all_tenants=1') - for fip in fips: - self.assertIsInstance(fip, floating_ips.FloatingIP) - def test_delete_floating_ip(self): fl = self.cs.floating_ips.list()[0] fl.delete() diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index d6ddc2e14..5297697da 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -1277,10 +1277,6 @@ def test_floating_ip_list(self): self.run_command('floating-ip-list') self.assert_called('GET', '/os-floating-ips') - def test_floating_ip_list_all_tenants(self): - self.run_command('floating-ip-list --all-tenants') - self.assert_called('GET', '/os-floating-ips?all_tenants=1') - def test_floating_ip_create(self): self.run_command('floating-ip-create') self.assert_called('GET', '/os-floating-ips/1') diff --git a/novaclient/v2/floating_ips.py b/novaclient/v2/floating_ips.py index cce92da25..0eb75b79b 100644 --- a/novaclient/v2/floating_ips.py +++ b/novaclient/v2/floating_ips.py @@ -28,14 +28,11 @@ def delete(self): class FloatingIPManager(base.ManagerWithFind): resource_class = FloatingIP - def list(self, all_tenants=False): + def list(self): """ List floating IPs """ - url = '/os-floating-ips' - if all_tenants: - url += '?all_tenants=1' - return self._list(url, "floating_ips") + return self._list("/os-floating-ips", "floating_ips") def create(self, pool=None): """ diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 641941abf..7af3fa8ab 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -2476,14 +2476,9 @@ def do_floating_ip_delete(cs, args): args.address) -@cliutils.arg( - '--all-tenants', - action='store_true', - default=False, - help=_('Display floatingips from all tenants (Admin only).')) -def do_floating_ip_list(cs, args): +def do_floating_ip_list(cs, _args): """List floating IPs.""" - _print_floating_ip_list(cs.floating_ips.list(args.all_tenants)) + _print_floating_ip_list(cs.floating_ips.list()) def do_floating_ip_pool_list(cs, _args): From 39020950d651b9601a136ca0b90e6b5085f373ca Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Tue, 16 Jun 2015 19:56:44 +0000 Subject: [PATCH 0771/1705] Updated from global requirements Change-Id: Ib1d40740abb395532443ea55e2125bf7156f039c --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f98033120..009e59256 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ argparse iso8601>=0.1.9 oslo.i18n>=1.5.0 # Apache-2.0 oslo.serialization>=1.4.0 # Apache-2.0 -oslo.utils>=1.4.0 # Apache-2.0 +oslo.utils>=1.6.0 # Apache-2.0 PrettyTable>=0.7,<0.8 requests>=2.5.2 simplejson>=2.2.0 From a5f30d173f59cf30d47ae413c134dc6682d676a1 Mon Sep 17 00:00:00 2001 From: Davanum Srinivas Date: Sun, 7 Jun 2015 10:11:13 -0400 Subject: [PATCH 0772/1705] cleanup openstack-common.conf and sync updated files Changes include: e782594 Add last_request_id only if it is not none 18bf5ca Fix usage of NotFound exception in apiclient.base 3bc8231 deprecate apiclient package Depends-On: Ia83ef6136da1c551ea947679dc546a0d7ad2f876 Change-Id: I0b5917c657d6adc3d34229b14833a5224c168e07 --- novaclient/base.py | 4 ++++ novaclient/client.py | 3 +++ novaclient/openstack/common/apiclient/base.py | 19 ++++++++++++++++++- .../openstack/common/apiclient/fake_client.py | 3 +-- novaclient/tests/unit/v2/fakes.py | 1 + openstack-common.conf | 1 - 6 files changed, 27 insertions(+), 4 deletions(-) diff --git a/novaclient/base.py b/novaclient/base.py index 01b4bac94..e541c8e30 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -57,6 +57,10 @@ class Manager(base.HookableMixin): def __init__(self, api): self.api = api + @property + def client(self): + return self.api.client + def _list(self, url, response_key, obj_class=None, body=None): if body: _resp, body = self.api.client.post(url, body=body) diff --git a/novaclient/client.py b/novaclient/client.py index abb1c29c6..f8858b003 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -221,6 +221,7 @@ def __init__(self, user, password, projectid=None, auth_url=None, self.service_catalog = None self.services_url = {} + self.last_request_id = None def use_token_cache(self, use_it): self.os_cache = use_it @@ -392,6 +393,8 @@ def request(self, url, method, **kwargs): else: body = None + self.last_request_id = (resp.headers.get('x-openstack-request-id') + if resp.headers else None) if resp.status_code >= 400: raise exceptions.from_response(resp, body, url, method) diff --git a/novaclient/openstack/common/apiclient/base.py b/novaclient/openstack/common/apiclient/base.py index ae067c6f1..89688151f 100644 --- a/novaclient/openstack/common/apiclient/base.py +++ b/novaclient/openstack/common/apiclient/base.py @@ -20,6 +20,20 @@ Base utilities to build API operation managers and objects on top of. """ +######################################################################## +# +# THIS MODULE IS DEPRECATED +# +# Please refer to +# https://etherpad.openstack.org/p/kilo-novaclient-library-proposals for +# the discussion leading to this deprecation. +# +# We recommend checking out the python-openstacksdk project +# (https://launchpad.net/python-openstacksdk) instead. +# +######################################################################## + + # E1102: %s is not callable # pylint: disable=E1102 @@ -388,7 +402,7 @@ def find(self, base_url=None, **kwargs): 'name': self.resource_class.__name__, 'args': kwargs } - raise exceptions.NotFound(404, msg) + raise exceptions.NotFound(msg) elif num > 1: raise exceptions.NoUniqueMatch else: @@ -495,6 +509,9 @@ def get(self): new = self.manager.get(self.id) if new: self._add_details(new._info) + if self.manager.client.last_request_id: + self._add_details( + {'x_request_id': self.manager.client.last_request_id}) def __eq__(self, other): if not isinstance(other, Resource): diff --git a/novaclient/openstack/common/apiclient/fake_client.py b/novaclient/openstack/common/apiclient/fake_client.py index 1a0324c8a..2d2a728b1 100644 --- a/novaclient/openstack/common/apiclient/fake_client.py +++ b/novaclient/openstack/common/apiclient/fake_client.py @@ -181,8 +181,7 @@ def client_request(self, client, method, url, **kwargs): else: status, body = resp headers = {} - self.last_request_id = headers.get('x-openstack-request-id', - 'req-test') + self.last_request_id = headers.get('x-openstack-request-id') return TestResponse({ "status_code": status, "text": body, diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 1c3e93bd2..f3052252e 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -56,6 +56,7 @@ def __init__(self, **kwargs): self.bypass_url = 'bypass_url' self.os_cache = 'os_cache' self.http_log_debug = 'http_log_debug' + self.last_request_id = None def _cs_request(self, url, method, **kwargs): # Check that certain things are called correctly diff --git a/openstack-common.conf b/openstack-common.conf index 01962c84a..a987faae9 100644 --- a/openstack-common.conf +++ b/openstack-common.conf @@ -3,7 +3,6 @@ # The list of modules to copy from oslo-incubator module=apiclient module=cliutils -module=install_venv_common # The base module to hold the copy of openstack.common base=novaclient From f3fef4ab2f53ae7b05df1e49ed7023dcca275cd1 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Wed, 17 Jun 2015 14:19:19 -0700 Subject: [PATCH 0773/1705] Change future nova version number references based on new values The nova server liberty version is changing from 2015.2 to 12.0.0 and the 2016.1 'M' release will be 13.0.0 so update the deprecation warnings in the docs and code using those versions. Depends on nova change 192406. Change-Id: I6a35fa0dda798fad93b804d00a46af80f08d475c --- doc/source/index.rst | 6 +++--- novaclient/v2/shell.py | 4 ++-- novaclient/v2/volume_snapshots.py | 8 ++++---- novaclient/v2/volume_types.py | 8 ++++---- novaclient/v2/volumes.py | 8 ++++---- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/doc/source/index.rst b/doc/source/index.rst index e80274936..bdbce90c4 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -72,11 +72,11 @@ The process for command deprecation is: 3. Example: ``_ - This change was made while the Nova 2015.2 Liberty release was in + This change was made while the Nova 12.0.0 Liberty release was in development. The current version of python-novaclient at the time was 2.25.0. Once the change was merged, python-novaclient 2.26.0 was released. - Since there was less than six months before 2015.2 would be released, the - deprecation cycle ran through the 2016.1 Nova server release. + Since there was less than six months before 12.0.0 would be released, the + deprecation cycle ran through the 13.0.0 Nova server release. Man Page diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 7af3fa8ab..32ba61164 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -62,10 +62,10 @@ # NOTE(mriedem): Remove this along with the deprecated commands in the first -# python-novaclient release AFTER the nova server 2016.1 'M' release. +# python-novaclient release AFTER the nova server 13.0.0 'M' release. def emit_volume_deprecation_warning(command_name): print('WARNING: Command %s is deprecated and will be removed after Nova ' - '2016.1 is released. Use python-cinderclient or openstackclient ' + '13.0.0 is released. Use python-cinderclient or openstackclient ' 'instead.' % command_name, file=sys.stderr) diff --git a/novaclient/v2/volume_snapshots.py b/novaclient/v2/volume_snapshots.py index 8ca1971ac..ffdb812c0 100644 --- a/novaclient/v2/volume_snapshots.py +++ b/novaclient/v2/volume_snapshots.py @@ -58,7 +58,7 @@ def create(self, volume_id, force=False, display_name=None, :rtype: :class:`Snapshot` """ warnings.warn('The novaclient.v2.volume_snapshots module is ' - 'deprecated and will be removed after Nova 2016.1 is ' + 'deprecated and will be removed after Nova 13.0.0 is ' 'released. Use python-cinderclient or ' 'python-openstacksdk instead.', DeprecationWarning) with self.alternate_service_type('volume'): @@ -76,7 +76,7 @@ def get(self, snapshot_id): :rtype: :class:`Snapshot` """ warnings.warn('The novaclient.v2.volume_snapshots module is ' - 'deprecated and will be removed after Nova 2016.1 is ' + 'deprecated and will be removed after Nova 13.0.0 is ' 'released. Use python-cinderclient or ' 'python-openstacksdk instead.', DeprecationWarning) with self.alternate_service_type('volume'): @@ -89,7 +89,7 @@ def list(self, detailed=True): :rtype: list of :class:`Snapshot` """ warnings.warn('The novaclient.v2.volume_snapshots module is ' - 'deprecated and will be removed after Nova 2016.1 is ' + 'deprecated and will be removed after Nova 13.0.0 is ' 'released. Use python-cinderclient or ' 'python-openstacksdk instead.', DeprecationWarning) with self.alternate_service_type('volume'): @@ -105,7 +105,7 @@ def delete(self, snapshot): :param snapshot: The :class:`Snapshot` to delete. """ warnings.warn('The novaclient.v2.volume_snapshots module is ' - 'deprecated and will be removed after Nova 2016.1 is ' + 'deprecated and will be removed after Nova 13.0.0 is ' 'released. Use python-cinderclient or ' 'python-openstacksdk instead.', DeprecationWarning) with self.alternate_service_type('volume'): diff --git a/novaclient/v2/volume_types.py b/novaclient/v2/volume_types.py index fd9268775..861c4a357 100644 --- a/novaclient/v2/volume_types.py +++ b/novaclient/v2/volume_types.py @@ -44,7 +44,7 @@ def list(self): :rtype: list of :class:`VolumeType`. """ warnings.warn('The novaclient.v2.volume_types module is deprecated ' - 'and will be removed after Nova 2016.1 is released. Use ' + 'and will be removed after Nova 13.0.0 is released. Use ' 'python-cinderclient or python-openstacksdk instead.', DeprecationWarning) with self.alternate_service_type('volume'): @@ -58,7 +58,7 @@ def get(self, volume_type): :rtype: :class:`VolumeType` """ warnings.warn('The novaclient.v2.volume_types module is deprecated ' - 'and will be removed after Nova 2016.1 is released. Use ' + 'and will be removed after Nova 13.0.0 is released. Use ' 'python-cinderclient or python-openstacksdk instead.', DeprecationWarning) with self.alternate_service_type('volume'): @@ -72,7 +72,7 @@ def delete(self, volume_type): :param volume_type: The ID of the :class:`VolumeType` to get. """ warnings.warn('The novaclient.v2.volume_types module is deprecated ' - 'and will be removed after Nova 2016.1 is released. Use ' + 'and will be removed after Nova 13.0.0 is released. Use ' 'python-cinderclient or python-openstacksdk instead.', DeprecationWarning) with self.alternate_service_type('volume'): @@ -86,7 +86,7 @@ def create(self, name): :rtype: :class:`VolumeType` """ warnings.warn('The novaclient.v2.volume_types module is deprecated ' - 'and will be removed after Nova 2016.1 is released. Use ' + 'and will be removed after Nova 13.0.0 is released. Use ' 'python-cinderclient or python-openstacksdk instead.', DeprecationWarning) with self.alternate_service_type('volume'): diff --git a/novaclient/v2/volumes.py b/novaclient/v2/volumes.py index b5f2ae687..f0403af63 100644 --- a/novaclient/v2/volumes.py +++ b/novaclient/v2/volumes.py @@ -65,7 +65,7 @@ def create(self, size, snapshot_id=None, display_name=None, """ warnings.warn('The novaclient.v2.volumes.VolumeManager.create() ' 'method is deprecated and will be removed after Nova ' - '2016.1 is released. Use python-cinderclient or ' + '13.0.0 is released. Use python-cinderclient or ' 'python-openstacksdk instead.', DeprecationWarning) # NOTE(melwitt): Ensure we use the volume endpoint for this call with self.alternate_service_type('volume'): @@ -87,7 +87,7 @@ def get(self, volume_id): """ warnings.warn('The novaclient.v2.volumes.VolumeManager.get() ' 'method is deprecated and will be removed after Nova ' - '2016.1 is released. Use python-cinderclient or ' + '13.0.0 is released. Use python-cinderclient or ' 'python-openstacksdk instead.', DeprecationWarning) with self.alternate_service_type('volume'): return self._get("/volumes/%s" % volume_id, "volume") @@ -100,7 +100,7 @@ def list(self, detailed=True, search_opts=None): """ warnings.warn('The novaclient.v2.volumes.VolumeManager.list() ' 'method is deprecated and will be removed after Nova ' - '2016.1 is released. Use python-cinderclient or ' + '13.0.0 is released. Use python-cinderclient or ' 'python-openstacksdk instead.', DeprecationWarning) with self.alternate_service_type('volume'): search_opts = search_opts or {} @@ -126,7 +126,7 @@ def delete(self, volume): """ warnings.warn('The novaclient.v2.volumes.VolumeManager.delete() ' 'method is deprecated and will be removed after Nova ' - '2016.1 is released. Use python-cinderclient or ' + '13.0.0 is released. Use python-cinderclient or ' 'python-openstacksdk instead.', DeprecationWarning) with self.alternate_service_type('volume'): self._delete("/volumes/%s" % base.getid(volume)) From 3fe8196b3ca158c8c9eac8235a0e9f8843c30642 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Mon, 22 Jun 2015 08:28:03 +0000 Subject: [PATCH 0774/1705] Updated from global requirements Change-Id: I85bdd78c229fae83a5900453197b05be52cf061b --- requirements.txt | 10 +++++----- setup.py | 1 - test-requirements.txt | 12 ++++++------ 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/requirements.txt b/requirements.txt index 009e59256..87e56ae61 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1,13 @@ # 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. -pbr>=0.11,<2.0 +pbr<2.0,>=0.11 argparse iso8601>=0.1.9 -oslo.i18n>=1.5.0 # Apache-2.0 -oslo.serialization>=1.4.0 # Apache-2.0 -oslo.utils>=1.6.0 # Apache-2.0 -PrettyTable>=0.7,<0.8 +oslo.i18n>=1.5.0 # Apache-2.0 +oslo.serialization>=1.4.0 # Apache-2.0 +oslo.utils>=1.6.0 # Apache-2.0 +PrettyTable<0.8,>=0.7 requests>=2.5.2 simplejson>=2.2.0 six>=1.9.0 diff --git a/setup.py b/setup.py index 736375744..056c16c2b 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # Copyright (c) 2013 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/test-requirements.txt b/test-requirements.txt index 150df4be1..7eaf3174c 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,18 +1,18 @@ # 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. -hacking>=0.10.0,<0.11 +hacking<0.11,>=0.10.0 coverage>=3.6 discover fixtures>=0.3.14 -keyring>=2.1,!=3.3 +keyring!=3.3,>=2.1 mock>=1.0 -requests-mock>=0.6.0 # Apache-2.0 -sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3 +requests-mock>=0.6.0 # Apache-2.0 +sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2 os-client-config>=1.2.0 -oslosphinx>=2.5.0 # Apache-2.0 +oslosphinx>=2.5.0 # Apache-2.0 testrepository>=0.0.18 testscenarios>=0.4 -testtools>=0.9.36,!=1.2.0 +testtools>=1.4.0 tempest-lib>=0.5.0 From 916993a1f654948a65ee0916ceae2df253a78a59 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Wed, 24 Jun 2015 00:21:38 +0000 Subject: [PATCH 0775/1705] Updated from global requirements Change-Id: I8948bdd08ea55200ce7899ee8704651eb08a359a --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 7eaf3174c..701bf80d2 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -10,7 +10,7 @@ keyring!=3.3,>=2.1 mock>=1.0 requests-mock>=0.6.0 # Apache-2.0 sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2 -os-client-config>=1.2.0 +os-client-config>=1.4.0 oslosphinx>=2.5.0 # Apache-2.0 testrepository>=0.0.18 testscenarios>=0.4 From bde241b94747223e8f67e2e5765e9c55996a90dc Mon Sep 17 00:00:00 2001 From: Pete Savage Date: Fri, 26 Jun 2015 11:05:10 +0100 Subject: [PATCH 0776/1705] Added marker functionality to flavours and images Markers to support pagination was missing from flavours and from images. Change-Id: I27de83f18a4850b63020ac60e010839a13e916fb --- novaclient/tests/unit/v2/test_flavors.py | 4 ++++ novaclient/tests/unit/v2/test_images.py | 6 +++--- novaclient/v2/flavors.py | 10 +++++++++- novaclient/v2/images.py | 7 ++++++- 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/novaclient/tests/unit/v2/test_flavors.py b/novaclient/tests/unit/v2/test_flavors.py index 4fe43c10a..b78d6cde9 100644 --- a/novaclient/tests/unit/v2/test_flavors.py +++ b/novaclient/tests/unit/v2/test_flavors.py @@ -45,6 +45,10 @@ def test_list_flavors_undetailed(self): for flavor in fl: self.assertIsInstance(flavor, self.flavor_type) + def test_list_flavors_with_marker_limit(self): + self.cs.flavors.list(marker=1234, limit=4) + self.cs.assert_called('GET', '/flavors/detail?limit=4&marker=1234') + def test_list_flavors_is_public_none(self): fl = self.cs.flavors.list(is_public=None) self.cs.assert_called('GET', '/flavors/detail?is_public=None') diff --git a/novaclient/tests/unit/v2/test_images.py b/novaclient/tests/unit/v2/test_images.py index 4c25e8af8..f4a6ef86b 100644 --- a/novaclient/tests/unit/v2/test_images.py +++ b/novaclient/tests/unit/v2/test_images.py @@ -35,9 +35,9 @@ def test_list_images_undetailed(self): for i in il: self.assertIsInstance(i, images.Image) - def test_list_images_with_limit(self): - self.cs.images.list(limit=4) - self.assert_called('GET', '/images/detail?limit=4') + def test_list_images_with_marker_limit(self): + self.cs.images.list(marker=1234, limit=4) + self.assert_called('GET', '/images/detail?limit=4&marker=1234') def test_get_image_details(self): i = self.cs.images.get(1) diff --git a/novaclient/v2/flavors.py b/novaclient/v2/flavors.py index 1db42e24e..cf8591d74 100644 --- a/novaclient/v2/flavors.py +++ b/novaclient/v2/flavors.py @@ -93,18 +93,26 @@ class FlavorManager(base.ManagerWithFind): resource_class = Flavor is_alphanum_id_allowed = True - def list(self, detailed=True, is_public=True): + def list(self, detailed=True, is_public=True, marker=None, limit=None): """ Get a list of all flavors. :rtype: list of :class:`Flavor`. + :param limit: maximum number of flavors to return (optional). + :param marker: Begin returning flavors that appear later in the flavor + list than that represented by this flavor id (optional). """ qparams = {} # is_public is ternary - None means give all flavors. # By default Nova assumes True and gives admins public flavors # and flavors from their own projects only. + if marker: + qparams['marker'] = str(marker) + if limit: + qparams['limit'] = int(limit) if not is_public: qparams['is_public'] = is_public + qparams = sorted(qparams.items(), key=lambda x: x[0]) query_string = "?%s" % parse.urlencode(qparams) if qparams else "" detail = "" diff --git a/novaclient/v2/images.py b/novaclient/v2/images.py index 2d413a6bf..32001b465 100644 --- a/novaclient/v2/images.py +++ b/novaclient/v2/images.py @@ -52,12 +52,14 @@ def get(self, image): """ return self._get("/images/%s" % base.getid(image), "image") - def list(self, detailed=True, limit=None): + def list(self, detailed=True, limit=None, marker=None): """ Get a list of all images. :rtype: list of :class:`Image` :param limit: maximum number of images to return. + :param marker: Begin returning images that appear later in the image + list than that represented by this image id (optional). """ params = {} detail = '' @@ -65,6 +67,9 @@ def list(self, detailed=True, limit=None): detail = '/detail' if limit: params['limit'] = int(limit) + if marker: + params['marker'] = str(marker) + params = sorted(params.items(), key=lambda x: x[0]) query = '?%s' % parse.urlencode(params) if params else '' return self._list('/images%s%s' % (detail, query), 'images') From 0a8fffffbaa48083ba2e79abf67096efa59fa18b Mon Sep 17 00:00:00 2001 From: Andrey Pavlov Date: Fri, 26 Jun 2015 17:36:39 +0300 Subject: [PATCH 0777/1705] Fix resolving image.id in servers.boot Most places resolve id of input parameter as 'base.getid' and in the beginning of _boot function it resolves image id with such method. But in fixed place it tries to get id directly from object. But code that uses novaclient directly(without shell) falls in this place because it passes string with id to image parameter. Change-Id: Ib90e4ffa3b7835f6648a62eddf7f7b87b5ab0f7e --- novaclient/tests/unit/v2/test_servers.py | 25 ++++++++++++++++++++++++ novaclient/v2/servers.py | 2 +- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index d16714c7c..faf7dd67f 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -136,6 +136,31 @@ def test_create_server_from_volume(): test_create_server_from_volume() + def test_create_server_boot_from_volume_bdm_v2(self): + old_boot = self.cs.servers._boot + + bdm = [{"volume_size": "1", + "volume_id": "11111111-1111-1111-1111-111111111111", + "delete_on_termination": "0", + "device_name": "vda"}] + + def wrapped_boot(url, key, *boot_args, **boot_kwargs): + self.assertEqual(boot_kwargs['block_device_mapping_v2'], bdm) + return old_boot(url, key, *boot_args, **boot_kwargs) + + with mock.patch.object(self.cs.servers, '_boot', wrapped_boot): + s = self.cs.servers.create( + name="My server", + image=1, + flavor=1, + meta={'foo': 'bar'}, + userdata="hello moto", + key_name="fakekey", + block_device_mapping_v2=bdm + ) + self.assert_called('POST', '/os-volumes_boot') + self.assertIsInstance(s, servers.Server) + def test_create_server_boot_with_nics_ipv6(self): old_boot = self.cs.servers._boot nics = [{'net-id': '11111111-1111-1111-1111-111111111111', diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index a8848154f..d4aed3be8 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -505,7 +505,7 @@ def _boot(self, resource_url, response_key, name, image, flavor, # a valid boot with both --image and --block-device # failed , see bug 1433609 for more info if image: - bdm_dict = {'uuid': image.id, 'source_type': 'image', + bdm_dict = {'uuid': base.getid(image), 'source_type': 'image', 'destination_type': 'local', 'boot_index': 0, 'delete_on_termination': True} block_device_mapping_v2.insert(0, bdm_dict) From 8dd63328fbd6a4e632ead2edad1e9138d3af5e42 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Tue, 30 Jun 2015 22:45:43 +0000 Subject: [PATCH 0778/1705] Updated from global requirements Change-Id: Id28831c58c9cd2c8df4a155b2727dc3291966401 --- test-requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test-requirements.txt b/test-requirements.txt index 701bf80d2..b429932ce 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -5,7 +5,7 @@ hacking<0.11,>=0.10.0 coverage>=3.6 discover -fixtures>=0.3.14 +fixtures>=1.3.1 keyring!=3.3,>=2.1 mock>=1.0 requests-mock>=0.6.0 # Apache-2.0 @@ -15,4 +15,4 @@ oslosphinx>=2.5.0 # Apache-2.0 testrepository>=0.0.18 testscenarios>=0.4 testtools>=1.4.0 -tempest-lib>=0.5.0 +tempest-lib>=0.6.1 From a061470a8edf282ab24911c20e6e033c97541cc5 Mon Sep 17 00:00:00 2001 From: rsritesh Date: Thu, 18 Jun 2015 13:08:34 +0200 Subject: [PATCH 0779/1705] Improve hypervisor-show print list Current hypervisor-show command does not properly list when there is a long list of cpu info feature. The long list of cpu info feature has comma-separated values. Because of this user is not able to read the print out properly. print_dict() has been changed to show a list with comma-separated values properly. Change-Id: Icc53439cecd3b5eee2340267a0447ce209d7b653 Closes-Bug: #1466435 --- novaclient/tests/unit/test_utils.py | 14 ++++++++++++++ novaclient/v2/shell.py | 6 +++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/novaclient/tests/unit/test_utils.py b/novaclient/tests/unit/test_utils.py index 8bcb2bfcd..d4eff35c2 100644 --- a/novaclient/tests/unit/test_utils.py +++ b/novaclient/tests/unit/test_utils.py @@ -266,6 +266,20 @@ def test_print_dict_list(self): '+----------+----------------+\n', sys.stdout.getvalue()) + @mock.patch('sys.stdout', six.StringIO()) + def test_print_large_dict_list(self): + dict = {'k': ['foo1', 'bar1', 'foo2', 'bar2', + 'foo3', 'bar3', 'foo4', 'bar4']} + utils.print_dict(dict, wrap=40) + self.assertEqual( + '+----------+------------------------------------------+\n' + '| Property | Value |\n' + '+----------+------------------------------------------+\n' + '| k | ["foo1", "bar1", "foo2", "bar2", "foo3", |\n' + '| | "bar3", "foo4", "bar4"] |\n' + '+----------+------------------------------------------+\n', + sys.stdout.getvalue()) + class FlattenTestCase(test_utils.TestCase): def test_flattening(self): diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 7af3fa8ab..c38d6081c 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -3667,10 +3667,14 @@ def __init__(self, **kwargs): 'hypervisor', metavar='', help=_('Name or ID of the hypervisor to show the details of.')) +@cliutils.arg( + '--wrap', dest='wrap', metavar='', default=40, + help=_('Wrap the output to a specified length. ' + 'Default is 40 or 0 to disable')) def do_hypervisor_show(cs, args): """Display the details of the specified hypervisor.""" hyper = _find_hypervisor(cs, args.hypervisor) - utils.print_dict(utils.flatten_dict(hyper._info)) + utils.print_dict(utils.flatten_dict(hyper._info), wrap=int(args.wrap)) @cliutils.arg( From d64e288de793b3abbebfc5beacd426a09d59efa6 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Sun, 12 Jul 2015 15:22:23 +0000 Subject: [PATCH 0780/1705] Updated from global requirements Issue found with mock 1.1.3: Because of the test inheritance test_keypair_import is getting called 3 times. It appears that the mock for open builtins is getting torn down after it's first use, and not correctly built again on subsequent calls. In the gate, this is a race, because we have 3 tests and 8 workers, and this will only fail if 2 or more of the tests happen to be allocated to the same worker. This can be reproduced locally with: tox -e py27 -- --concurrency=1 Converting this to the context manager usage of mock appears to resolve the issue. Co-Authored-By: Sean Dague Related-Bug: https://github.com/testing-cabal/mock/issues/280 Change-Id: I9a87375d2eb6c7cf7b9124b2095a5a4bcc8e7bf3 --- novaclient/tests/unit/v2/test_shell.py | 13 +++++++------ test-requirements.txt | 3 ++- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 5297697da..e5d8914a5 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2367,13 +2367,14 @@ def test_keypair_add(self): {'keypair': {'name': 'test'}}) - @mock.patch.object(builtins, 'open', - mock.mock_open(read_data='FAKE_PUBLIC_KEY')) def test_keypair_import(self): - self.run_command('keypair-add --pub-key test.pub test') - self.assert_called( - 'POST', '/os-keypairs', { - 'keypair': {'public_key': 'FAKE_PUBLIC_KEY', 'name': 'test'}}) + with mock.patch.object(builtins, 'open', + mock.mock_open(read_data='FAKE_PUBLIC_KEY')): + self.run_command('keypair-add --pub-key test.pub test') + self.assert_called( + 'POST', '/os-keypairs', { + 'keypair': {'public_key': 'FAKE_PUBLIC_KEY', + 'name': 'test'}}) def test_keypair_stdin(self): with mock.patch('sys.stdin', six.StringIO('FAKE_PUBLIC_KEY')): diff --git a/test-requirements.txt b/test-requirements.txt index b429932ce..962eb60bf 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -7,7 +7,8 @@ coverage>=3.6 discover fixtures>=1.3.1 keyring!=3.3,>=2.1 -mock>=1.0 +mock>=1.1;python_version!='2.6' +mock==1.0.1;python_version=='2.6' requests-mock>=0.6.0 # Apache-2.0 sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2 os-client-config>=1.4.0 From d3afbd65f6a79f22a1ba230d53981ba22830c748 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Wed, 15 Jul 2015 01:37:41 +0000 Subject: [PATCH 0781/1705] Updated from global requirements Change-Id: If7920f7069ba5af958757de37fcce7532033ef1f --- requirements.txt | 4 ++-- setup.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 87e56ae61..5e1e30bb5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,12 +1,12 @@ # 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. -pbr<2.0,>=0.11 +pbr<2.0,>=1.3 argparse iso8601>=0.1.9 oslo.i18n>=1.5.0 # Apache-2.0 oslo.serialization>=1.4.0 # Apache-2.0 -oslo.utils>=1.6.0 # Apache-2.0 +oslo.utils>=1.9.0 # Apache-2.0 PrettyTable<0.8,>=0.7 requests>=2.5.2 simplejson>=2.2.0 diff --git a/setup.py b/setup.py index 056c16c2b..d8080d05c 100644 --- a/setup.py +++ b/setup.py @@ -25,5 +25,5 @@ pass setuptools.setup( - setup_requires=['pbr'], + setup_requires=['pbr>=1.3'], pbr=True) From ea0b3bd60853dccd189b86c9198274a9cd00faf7 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Thu, 2 Apr 2015 16:37:59 +0300 Subject: [PATCH 0782/1705] Implements 'microversions' api type - Part 1 Compute API version will be transmitted to API side via X-OpenStack-Nova-API-Version header, if minor part of version is presented. New module "novaclient.api_versions" was added as storage for all api versions related functions, classes, variables and etc. `novaclient.api_versions.APIVersion` class is similar to `nova.api.openstack.api_version_request.APIVersionRequest`. The main difference relates to compare methods(method `cmp` is missed from Py3) and processing "latest" version. Related to bp api-microversion-support Change-Id: I0e6574ddaec11fdd053a49adb6b9de9056d0fbac --- novaclient/api_versions.py | 208 +++++++++++++++++++++ novaclient/client.py | 76 ++++---- novaclient/exceptions.py | 12 +- novaclient/shell.py | 52 +++--- novaclient/tests/unit/test_api_versions.py | 170 +++++++++++++++++ novaclient/tests/unit/test_client.py | 10 +- novaclient/tests/unit/test_shell.py | 8 +- novaclient/tests/unit/v2/fakes.py | 3 +- novaclient/tests/unit/v2/test_shell.py | 8 +- novaclient/v2/client.py | 6 +- 10 files changed, 466 insertions(+), 87 deletions(-) create mode 100644 novaclient/api_versions.py create mode 100644 novaclient/tests/unit/test_api_versions.py diff --git a/novaclient/api_versions.py b/novaclient/api_versions.py new file mode 100644 index 000000000..6d1c0672b --- /dev/null +++ b/novaclient/api_versions.py @@ -0,0 +1,208 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging +import os +import pkgutil +import re + +from oslo_utils import strutils + +from novaclient import exceptions +from novaclient.i18n import _, _LW + +LOG = logging.getLogger(__name__) +if not LOG.handlers: + LOG.addHandler(logging.StreamHandler()) + + +# key is a deprecated version and value is an alternative version. +DEPRECATED_VERSIONS = {"1.1": "2"} + + +_type_error_msg = _("'%(other)s' should be an instance of '%(cls)s'") + + +class APIVersion(object): + """This class represents an API Version with convenience + methods for manipulation and comparison of version + numbers that we need to do to implement microversions. + """ + + def __init__(self, version_str=None): + """Create an API version object.""" + self.ver_major = 0 + self.ver_minor = 0 + + if version_str is not None: + match = re.match(r"^([1-9]\d*)\.([1-9]\d*|0|latest)$", version_str) + if match: + self.ver_major = int(match.group(1)) + if match.group(2) == "latest": + # NOTE(andreykurilin): Infinity allows to easily determine + # latest version and doesn't require any additional checks + # in comparison methods. + self.ver_minor = float("inf") + else: + self.ver_minor = int(match.group(2)) + else: + msg = _("Invalid format of client version '%s'. " + "Expected format 'X.Y', where X is a major part and Y " + "is a minor part of version.") % version_str + raise exceptions.UnsupportedVersion(msg) + + def __str__(self): + """Debug/Logging representation of object.""" + if self.is_latest(): + return "Latest API Version Major: %s" % self.ver_major + return ("API Version Major: %s, Minor: %s" + % (self.ver_major, self.ver_minor)) + + def __repr__(self): + if self.is_null(): + return "" + else: + return "" % self.get_string() + + def is_null(self): + return self.ver_major == 0 and self.ver_minor == 0 + + def is_latest(self): + return self.ver_minor == float("inf") + + def __lt__(self, other): + if not isinstance(other, APIVersion): + raise TypeError(_type_error_msg % {"other": other, + "cls": self.__class__}) + + return ((self.ver_major, self.ver_minor) < + (other.ver_major, other.ver_minor)) + + def __eq__(self, other): + if not isinstance(other, APIVersion): + raise TypeError(_type_error_msg % {"other": other, + "cls": self.__class__}) + + return ((self.ver_major, self.ver_minor) == + (other.ver_major, other.ver_minor)) + + def __gt__(self, other): + if not isinstance(other, APIVersion): + raise TypeError(_type_error_msg % {"other": other, + "cls": self.__class__}) + + return ((self.ver_major, self.ver_minor) > + (other.ver_major, other.ver_minor)) + + def __le__(self, other): + return self < other or self == other + + def __ne__(self, other): + return not self.__eq__(other) + + def __ge__(self, other): + return self > other or self == other + + def matches(self, min_version, max_version): + """Returns whether the version object represents a version + greater than or equal to the minimum version and less than + or equal to the maximum version. + + :param min_version: Minimum acceptable version. + :param max_version: Maximum acceptable version. + :returns: boolean + + If min_version is null then there is no minimum limit. + If max_version is null then there is no maximum limit. + If self is null then raise ValueError + """ + + if self.is_null(): + raise ValueError(_("Null APIVersion doesn't support 'matches'.")) + if max_version.is_null() and min_version.is_null(): + return True + elif max_version.is_null(): + return min_version <= self + elif min_version.is_null(): + return self <= max_version + else: + return min_version <= self <= max_version + + def get_string(self): + """Converts object to string representation which if used to create + an APIVersion object results in the same version. + """ + if self.is_null(): + raise ValueError( + _("Null APIVersion cannot be converted to string.")) + elif self.is_latest(): + return "%s.%s" % (self.ver_major, "latest") + return "%s.%s" % (self.ver_major, self.ver_minor) + + +def get_available_major_versions(): + # NOTE(andreykurilin): available clients version should not be + # hardcoded, so let's discover them. + matcher = re.compile(r"v[0-9]*$") + submodules = pkgutil.iter_modules([os.path.dirname(__file__)]) + available_versions = [name[1:] for loader, name, ispkg in submodules + if matcher.search(name)] + + return available_versions + + +def check_major_version(api_version): + """Checks major part of ``APIVersion`` obj is supported. + + :raises novaclient.exceptions.UnsupportedVersion: if major part is not + supported + """ + available_versions = get_available_major_versions() + if (not api_version.is_null() and + str(api_version.ver_major) not in available_versions): + if len(available_versions) == 1: + msg = _("Invalid client version '%(version)s'. " + "Major part should be '%(major)s'") % { + "version": api_version.get_string(), + "major": available_versions[0]} + else: + msg = _("Invalid client version '%(version)s'. " + "Major part must be one of: '%(major)s'") % { + "version": api_version.get_string(), + "major": ", ".join(available_versions)} + raise exceptions.UnsupportedVersion(msg) + + +def get_api_version(version_string): + """Returns checked APIVersion object""" + version_string = str(version_string) + if version_string in DEPRECATED_VERSIONS: + LOG.warning( + _LW("Version %(deprecated_version)s is deprecated, using " + "alternative version %(alternative)s instead.") % + {"deprecated_version": version_string, + "alternative": DEPRECATED_VERSIONS[version_string]}) + version_string = DEPRECATED_VERSIONS[version_string] + if strutils.is_int_like(version_string): + version_string = "%s.0" % version_string + + api_version = APIVersion(version_string) + check_major_version(api_version) + return api_version + + +def update_headers(headers, api_version): + """Set 'X-OpenStack-Nova-API-Version' header if api_version is not null""" + + if not api_version.is_null() and api_version.ver_minor != 0: + headers["X-OpenStack-Nova-API-Version"] = api_version.get_string() diff --git a/novaclient/client.py b/novaclient/client.py index f8858b003..5ee6867dc 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -31,7 +31,6 @@ import pkgutil import re import socket -import warnings from keystoneclient import adapter from oslo_utils import importutils @@ -47,17 +46,14 @@ from six.moves.urllib import parse +from novaclient import api_versions from novaclient import exceptions from novaclient import extension as ext -from novaclient.i18n import _, _LW +from novaclient.i18n import _ from novaclient import service_catalog from novaclient import utils -# key is a deprecated version and value is an alternative version. -DEPRECATED_VERSIONS = {"1.1": "2"} - - class TCPKeepAliveAdapter(adapters.HTTPAdapter): """The custom adapter used to set TCP Keep-Alive on all connections.""" def init_poolmanager(self, *args, **kwargs): @@ -88,9 +84,13 @@ class SessionClient(adapter.LegacyJsonAdapter): def __init__(self, *args, **kwargs): self.times = [] self.timings = kwargs.pop('timings', False) + self.api_version = kwargs.pop('api_version', None) + self.api_version = self.api_version or api_versions.APIVersion() super(SessionClient, self).__init__(*args, **kwargs) def request(self, url, method, **kwargs): + kwargs.setdefault('headers', kwargs.get('headers', {})) + api_versions.update_headers(kwargs["headers"], self.api_version) # NOTE(jamielennox): The standard call raises errors from # keystoneclient, where we need to raise the novaclient errors. raise_exc = kwargs.pop('raise_exc', True) @@ -144,12 +144,13 @@ def __init__(self, user, password, projectid=None, auth_url=None, http_log_debug=False, auth_system='keystone', auth_plugin=None, auth_token=None, cacert=None, tenant_id=None, user_id=None, - connection_pool=False): + connection_pool=False, api_version=None): self.user = user self.user_id = user_id self.password = password self.projectid = projectid self.tenant_id = tenant_id + self.api_version = api_version or api_versions.APIVersion() self._connection_pool = (_ClientConnectionPool() if connection_pool else None) @@ -357,6 +358,7 @@ def request(self, url, method, **kwargs): kwargs['headers']['Content-Type'] = 'application/json' kwargs['data'] = json.dumps(kwargs['body']) del kwargs['body'] + api_versions.update_headers(kwargs["headers"], self.api_version) if self.timeout is not None: kwargs.setdefault('timeout', self.timeout) kwargs['verify'] = self.verify_cert @@ -681,7 +683,7 @@ def _construct_http_client(username=None, password=None, project_id=None, auth_token=None, cacert=None, tenant_id=None, user_id=None, connection_pool=False, session=None, auth=None, user_agent='python-novaclient', - interface=None, **kwargs): + interface=None, api_version=None, **kwargs): if session: return SessionClient(session=session, auth=auth, @@ -691,6 +693,7 @@ def _construct_http_client(username=None, password=None, project_id=None, service_name=service_name, user_agent=user_agent, timings=timings, + api_version=api_version, **kwargs) else: # FIXME(jamielennox): username and password are now optional. Need @@ -718,10 +721,13 @@ def _construct_http_client(username=None, password=None, project_id=None, os_cache=os_cache, http_log_debug=http_log_debug, cacert=cacert, - connection_pool=connection_pool) + connection_pool=connection_pool, + api_version=api_version) def discover_extensions(version): + if not isinstance(version, api_versions.APIVersion): + version = api_versions.get_api_version(version) extensions = [] for name, module in itertools.chain( _discover_via_python_path(), @@ -750,12 +756,7 @@ def _discover_via_python_path(): def _discover_via_contrib_path(version): module_path = os.path.dirname(os.path.abspath(__file__)) - version_str = "v%s" % version.replace('.', '_') - # NOTE(andreykurilin): v1.1 uses implementation of v2, so we should - # discover contrib modules in novaclient.v2 dir. - if version_str == "v1_1": - version_str = "v2" - ext_path = os.path.join(module_path, version_str, 'contrib') + ext_path = os.path.join(module_path, "v%s" % version.ver_major, 'contrib') ext_glob = os.path.join(ext_path, "*.py") for ext_path in glob.iglob(ext_glob): @@ -776,38 +777,25 @@ def _discover_via_entry_points(): yield name, module -def _get_available_client_versions(): - # NOTE(andreykurilin): available clients version should not be - # hardcoded, so let's discover them. - matcher = re.compile(r"v[0-9_]*$") - submodules = pkgutil.iter_modules([os.path.dirname(__file__)]) - available_versions = [ - name[1:].replace("_", ".") for loader, name, ispkg in submodules - if matcher.search(name)] - - return available_versions +def _get_client_class_and_version(version): + if not isinstance(version, api_versions.APIVersion): + version = api_versions.get_api_version(version) + else: + api_versions.check_major_version(version) + if version.is_latest(): + raise exceptions.UnsupportedVersion( + _("The version should be explicit, not latest.")) + return version, importutils.import_class( + "novaclient.v%s.client.Client" % version.ver_major) def get_client_class(version): - version = str(version) - if version in DEPRECATED_VERSIONS: - warnings.warn(_LW( - "Version %(deprecated_version)s is deprecated, using " - "alternative version %(alternative)s instead.") % - {"deprecated_version": version, - "alternative": DEPRECATED_VERSIONS[version]}) - version = DEPRECATED_VERSIONS[version] - try: - return importutils.import_class( - "novaclient.v%s.client.Client" % version) - except ImportError: - available_versions = _get_available_client_versions() - msg = _("Invalid client version '%(version)s'. must be one of: " - "%(keys)s") % {'version': version, - 'keys': ', '.join(available_versions)} - raise exceptions.UnsupportedVersion(msg) + """Returns Client class based on given version.""" + _api_version, client_class = _get_client_class_and_version(version) + return client_class def Client(version, *args, **kwargs): - client_class = get_client_class(version) - return client_class(*args, **kwargs) + """Initialize client object based on given version.""" + api_version, client_class = _get_client_class_and_version(version) + return client_class(api_version=api_version, *args, **kwargs) diff --git a/novaclient/exceptions.py b/novaclient/exceptions.py index d550b754b..c95925416 100644 --- a/novaclient/exceptions.py +++ b/novaclient/exceptions.py @@ -159,6 +159,14 @@ class MethodNotAllowed(ClientException): message = "Method Not Allowed" +class NotAcceptable(ClientException): + """ + HTTP 406 - Not Acceptable + """ + http_status = 406 + message = "Not Acceptable" + + class Conflict(ClientException): """ HTTP 409 - Conflict @@ -199,8 +207,8 @@ class HTTPNotImplemented(ClientException): # # Instead, we have to hardcode it: _error_classes = [BadRequest, Unauthorized, Forbidden, NotFound, - MethodNotAllowed, Conflict, OverLimit, RateLimit, - HTTPNotImplemented] + MethodNotAllowed, NotAcceptable, Conflict, OverLimit, + RateLimit, HTTPNotImplemented] _code_map = dict((c.http_status, c) for c in _error_classes) diff --git a/novaclient/shell.py b/novaclient/shell.py index ec415bb6e..c7713d3d5 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -29,6 +29,7 @@ from keystoneclient.auth.identity import v3 as identity from keystoneclient import session as ksession from oslo_utils import encodeutils +from oslo_utils import importutils from oslo_utils import strutils import six @@ -41,6 +42,7 @@ pass import novaclient +from novaclient import api_versions import novaclient.auth_plugin from novaclient import client from novaclient import exceptions as exc @@ -48,7 +50,6 @@ from novaclient.i18n import _ from novaclient.openstack.common import cliutils from novaclient import utils -from novaclient.v2 import shell as shell_v2 DEFAULT_OS_COMPUTE_API_VERSION = "2" DEFAULT_NOVA_ENDPOINT_TYPE = 'publicURL' @@ -402,7 +403,7 @@ def get_base_parser(self): metavar='', default=cliutils.env('OS_COMPUTE_API_VERSION', default=DEFAULT_OS_COMPUTE_API_VERSION), - help=_('Accepts number of API version, ' + help=_('Accepts X, X.Y (where X is major and Y is minor part), ' 'defaults to env[OS_COMPUTE_API_VERSION].')) parser.add_argument( '--os_compute_api_version', @@ -431,15 +432,10 @@ def get_subcommand_parser(self, version): self.subcommands = {} subparsers = parser.add_subparsers(metavar='') - try: - actions_module = { - '1.1': shell_v2, - '2': shell_v2, - '3': shell_v2, - }[version] - except KeyError: - actions_module = shell_v2 + actions_module = importutils.import_module( + "novaclient.v%s.shell" % version.ver_major) + # TODO(andreykurilin): discover actions based on microversions self._find_actions(subparsers, actions_module) self._find_actions(subparsers, self) @@ -522,9 +518,11 @@ def main(self, argv): # Discover available auth plugins novaclient.auth_plugin.discover_auth_systems() - # build available subcommands based on version - self.extensions = self._discover_extensions( + api_version = api_versions.get_api_version( options.os_compute_api_version) + + # build available subcommands based on version + self.extensions = self._discover_extensions(api_version) self._run_extension_hooks('__pre_parse_args__') # NOTE(dtroyer): Hackery to handle --endpoint_type due to argparse @@ -535,8 +533,7 @@ def main(self, argv): spot = argv.index('--endpoint_type') argv[spot] = '--endpoint-type' - subcommand_parser = self.get_subcommand_parser( - options.os_compute_api_version) + subcommand_parser = self.get_subcommand_parser(api_version) self.parser = subcommand_parser if options.help or not argv: @@ -669,23 +666,22 @@ def main(self, argv): project_domain_id=args.os_project_domain_id, project_domain_name=args.os_project_domain_name) - if options.os_compute_api_version: - if not any([args.os_tenant_id, args.os_tenant_name, - args.os_project_id, args.os_project_name]): - raise exc.CommandError(_("You must provide a project name or" - " project id via --os-project-name," - " --os-project-id, env[OS_PROJECT_ID]" - " or env[OS_PROJECT_NAME]. You may" - " use os-project and os-tenant" - " interchangeably.")) + if not any([args.os_tenant_id, args.os_tenant_name, + args.os_project_id, args.os_project_name]): + raise exc.CommandError(_("You must provide a project name or" + " project id via --os-project-name," + " --os-project-id, env[OS_PROJECT_ID]" + " or env[OS_PROJECT_NAME]. You may" + " use os-project and os-tenant" + " interchangeably.")) - if not os_auth_url: - raise exc.CommandError( - _("You must provide an auth url " - "via either --os-auth-url or env[OS_AUTH_URL]")) + if not os_auth_url: + raise exc.CommandError( + _("You must provide an auth url " + "via either --os-auth-url or env[OS_AUTH_URL]")) self.cs = client.Client( - options.os_compute_api_version, + api_version, os_username, os_password, os_tenant_name, tenant_id=os_tenant_id, user_id=os_user_id, auth_url=os_auth_url, insecure=insecure, diff --git a/novaclient/tests/unit/test_api_versions.py b/novaclient/tests/unit/test_api_versions.py new file mode 100644 index 000000000..13f557cfd --- /dev/null +++ b/novaclient/tests/unit/test_api_versions.py @@ -0,0 +1,170 @@ +# Copyright 2015 Mirantis +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import mock + +from novaclient import api_versions +from novaclient import exceptions +from novaclient.tests.unit import utils + + +class APIVersionTestCase(utils.TestCase): + def test_valid_version_strings(self): + def _test_string(version, exp_major, exp_minor): + v = api_versions.APIVersion(version) + self.assertEqual(v.ver_major, exp_major) + self.assertEqual(v.ver_minor, exp_minor) + + _test_string("1.1", 1, 1) + _test_string("2.10", 2, 10) + _test_string("5.234", 5, 234) + _test_string("12.5", 12, 5) + _test_string("2.0", 2, 0) + _test_string("2.200", 2, 200) + + def test_null_version(self): + v = api_versions.APIVersion() + self.assertTrue(v.is_null()) + + def test_invalid_version_strings(self): + self.assertRaises(exceptions.UnsupportedVersion, + api_versions.APIVersion, "2") + + self.assertRaises(exceptions.UnsupportedVersion, + api_versions.APIVersion, "200") + + self.assertRaises(exceptions.UnsupportedVersion, + api_versions.APIVersion, "2.1.4") + + self.assertRaises(exceptions.UnsupportedVersion, + api_versions.APIVersion, "200.23.66.3") + + self.assertRaises(exceptions.UnsupportedVersion, + api_versions.APIVersion, "5 .3") + + self.assertRaises(exceptions.UnsupportedVersion, + api_versions.APIVersion, "5. 3") + + self.assertRaises(exceptions.UnsupportedVersion, + api_versions.APIVersion, "5.03") + + self.assertRaises(exceptions.UnsupportedVersion, + api_versions.APIVersion, "02.1") + + self.assertRaises(exceptions.UnsupportedVersion, + api_versions.APIVersion, "2.001") + + self.assertRaises(exceptions.UnsupportedVersion, + api_versions.APIVersion, "") + + self.assertRaises(exceptions.UnsupportedVersion, + api_versions.APIVersion, " 2.1") + + self.assertRaises(exceptions.UnsupportedVersion, + api_versions.APIVersion, "2.1 ") + + def test_version_comparisons(self): + v1 = api_versions.APIVersion("2.0") + v2 = api_versions.APIVersion("2.5") + v3 = api_versions.APIVersion("5.23") + v4 = api_versions.APIVersion("2.0") + v_null = api_versions.APIVersion() + + self.assertTrue(v1 < v2) + self.assertTrue(v3 > v2) + self.assertTrue(v1 != v2) + self.assertTrue(v1 == v4) + self.assertTrue(v1 != v_null) + self.assertTrue(v_null == v_null) + self.assertRaises(TypeError, v1.__le__, "2.1") + + def test_version_matches(self): + v1 = api_versions.APIVersion("2.0") + v2 = api_versions.APIVersion("2.5") + v3 = api_versions.APIVersion("2.45") + v4 = api_versions.APIVersion("3.3") + v5 = api_versions.APIVersion("3.23") + v6 = api_versions.APIVersion("2.0") + v7 = api_versions.APIVersion("3.3") + v8 = api_versions.APIVersion("4.0") + v_null = api_versions.APIVersion() + + self.assertTrue(v2.matches(v1, v3)) + self.assertTrue(v2.matches(v1, v_null)) + self.assertTrue(v1.matches(v6, v2)) + self.assertTrue(v4.matches(v2, v7)) + self.assertTrue(v4.matches(v_null, v7)) + self.assertTrue(v4.matches(v_null, v8)) + self.assertFalse(v1.matches(v2, v3)) + self.assertFalse(v5.matches(v2, v4)) + self.assertFalse(v2.matches(v3, v1)) + + self.assertRaises(ValueError, v_null.matches, v1, v3) + + def test_get_string(self): + v1_string = "3.23" + v1 = api_versions.APIVersion(v1_string) + self.assertEqual(v1_string, v1.get_string()) + + self.assertRaises(ValueError, + api_versions.APIVersion().get_string) + + +class UpdateHeadersTestCase(utils.TestCase): + def test_api_version_is_null(self): + headers = {} + api_versions.update_headers(headers, api_versions.APIVersion()) + self.assertEqual({}, headers) + + def test_api_version_is_major(self): + headers = {} + api_versions.update_headers(headers, api_versions.APIVersion("7.0")) + self.assertEqual({}, headers) + + def test_api_version_is_not_null(self): + api_version = api_versions.APIVersion("2.3") + headers = {} + api_versions.update_headers(headers, api_version) + self.assertEqual( + {"X-OpenStack-Nova-API-Version": api_version.get_string()}, + headers) + + +class GetAPIVersionTestCase(utils.TestCase): + def test_get_available_client_versions(self): + output = api_versions.get_available_major_versions() + self.assertNotEqual([], output) + + def test_wrong_format(self): + self.assertRaises(exceptions.UnsupportedVersion, + api_versions.get_api_version, "something_wrong") + + def test_wrong_major_version(self): + self.assertRaises(exceptions.UnsupportedVersion, + api_versions.get_api_version, "1") + + @mock.patch("novaclient.api_versions.APIVersion") + def test_only_major_part_is_presented(self, mock_apiversion): + version = 7 + self.assertEqual(mock_apiversion.return_value, + api_versions.get_api_version(version)) + mock_apiversion.assert_called_once_with("%s.0" % str(version)) + + @mock.patch("novaclient.api_versions.APIVersion") + def test_major_and_minor_parts_is_presented(self, mock_apiversion): + version = "2.7" + self.assertEqual(mock_apiversion.return_value, + api_versions.get_api_version(version)) + mock_apiversion.assert_called_once_with(version) diff --git a/novaclient/tests/unit/test_client.py b/novaclient/tests/unit/test_client.py index ace5751a1..1cade390d 100644 --- a/novaclient/tests/unit/test_client.py +++ b/novaclient/tests/unit/test_client.py @@ -161,10 +161,6 @@ def test_client_version_url_with_project_name(self): self._check_version_url('http://foo.com/nova/v2/%s', 'http://foo.com/nova/') - def test_get_available_client_versions(self): - output = novaclient.client._get_available_client_versions() - self.assertNotEqual([], output) - def test_get_client_class_v2(self): output = novaclient.client.get_client_class('2') self.assertEqual(output, novaclient.v2.client.Client) @@ -181,6 +177,12 @@ def test_get_client_class_unknown(self): self.assertRaises(novaclient.exceptions.UnsupportedVersion, novaclient.client.get_client_class, '0') + def test_get_client_class_latest(self): + self.assertRaises(novaclient.exceptions.UnsupportedVersion, + novaclient.client.get_client_class, 'latest') + self.assertRaises(novaclient.exceptions.UnsupportedVersion, + novaclient.client.get_client_class, '2.latest') + def test_client_with_os_cache_enabled(self): cs = novaclient.v2.client.Client("user", "password", "project_id", auth_url="foo/v2", os_cache=True) diff --git a/novaclient/tests/unit/test_shell.py b/novaclient/tests/unit/test_shell.py index 4958023d7..3d6fc63c0 100644 --- a/novaclient/tests/unit/test_shell.py +++ b/novaclient/tests/unit/test_shell.py @@ -97,8 +97,8 @@ def make_env(self, exclude=None, fake_env=FAKE_ENV): def setUp(self): super(ShellTest, self).setUp() self.useFixture(fixtures.MonkeyPatch( - 'novaclient.client.get_client_class', - mock.MagicMock)) + 'novaclient.client.Client', + mock.MagicMock())) self.nc_util = mock.patch( 'novaclient.openstack.common.cliutils.isunauthenticated').start() self.nc_util.return_value = False @@ -344,7 +344,9 @@ def test_v2_service_type(self, mock_client): @mock.patch('novaclient.client.Client') def test_v_unknown_service_type(self, mock_client): - self._test_service_type('unknown', 'compute', mock_client) + self.assertRaises(exceptions.UnsupportedVersion, + self._test_service_type, + 'unknown', 'compute', mock_client) @mock.patch('sys.argv', ['nova']) @mock.patch('sys.stdout', six.StringIO()) diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index f3052252e..5b5971e4d 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -30,10 +30,11 @@ class FakeClient(fakes.FakeClient, client.Client): - def __init__(self, *args, **kwargs): + def __init__(self, api_version=None, *args, **kwargs): client.Client.__init__(self, 'username', 'password', 'project_id', 'auth_url', extensions=kwargs.get('extensions')) + self.api_version = api_version self.client = FakeHTTPClient(**kwargs) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index e5d8914a5..1a540ee22 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -70,8 +70,8 @@ def setUp(self): self.shell = self.useFixture(ShellFixture()).shell self.useFixture(fixtures.MonkeyPatch( - 'novaclient.client.get_client_class', - lambda *_: fakes.FakeClient)) + 'novaclient.client.Client', + lambda *args, **kwargs: fakes.FakeClient(*args, **kwargs))) @mock.patch('sys.stdout', new_callable=six.StringIO) @mock.patch('sys.stderr', new_callable=six.StringIO) @@ -2433,8 +2433,8 @@ def setUp(self): """Run before each test.""" super(ShellWithSessionClientTest, self).setUp() self.useFixture(fixtures.MonkeyPatch( - 'novaclient.client.get_client_class', - lambda *_: fakes.FakeSessionClient)) + 'novaclient.client.Client', + lambda *args, **kwargs: fakes.FakeSessionClient(*args, **kwargs))) class GetSecgroupTest(utils.TestCase): diff --git a/novaclient/v2/client.py b/novaclient/v2/client.py index c759dcec6..1c195d5a1 100644 --- a/novaclient/v2/client.py +++ b/novaclient/v2/client.py @@ -103,7 +103,7 @@ def __init__(self, username=None, api_key=None, project_id=None, auth_system='keystone', auth_plugin=None, auth_token=None, cacert=None, tenant_id=None, user_id=None, connection_pool=False, session=None, auth=None, - **kwargs): + api_version=None, **kwargs): """ :param str username: Username :param str api_key: API Key @@ -133,6 +133,8 @@ def __init__(self, username=None, api_key=None, project_id=None, :param bool connection_pool: Use a connection pool :param str session: Session :param str auth: Auth + :param api_version: Compute API version + :type api_version: novaclient.api_versions.APIVersion """ # FIXME(comstud): Rename the api_key argument above when we # know it's not being used as keyword argument @@ -153,6 +155,7 @@ def __init__(self, username=None, api_key=None, project_id=None, self.limits = limits.LimitsManager(self) self.servers = servers.ServerManager(self) self.versions = versions.VersionManager(self) + self.api_version = api_version # extensions self.agents = agents.AgentsManager(self) @@ -224,6 +227,7 @@ def __init__(self, username=None, api_key=None, project_id=None, connection_pool=connection_pool, session=session, auth=auth, + api_version=api_version, **kwargs) @client._original_only From 7c96ed73a05c9d7944a82d8068f9602ebb464af1 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 17 Jul 2015 16:18:11 +0000 Subject: [PATCH 0783/1705] Updated from global requirements Change-Id: Idd68dae30f17b33351c024315f6e0c2a532fc332 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 962eb60bf..408743f7f 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -7,7 +7,7 @@ coverage>=3.6 discover fixtures>=1.3.1 keyring!=3.3,>=2.1 -mock>=1.1;python_version!='2.6' +mock!=1.1.4,>=1.1;python_version!='2.6' mock==1.0.1;python_version=='2.6' requests-mock>=0.6.0 # Apache-2.0 sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2 From a80ac8523a086ff469cc085bd253a7d9e83b31dc Mon Sep 17 00:00:00 2001 From: Jimmy McCrory Date: Sun, 19 Jul 2015 11:01:47 -0700 Subject: [PATCH 0784/1705] Set iso8601 log level to WARNING When debug logging is enabled, set the log level of iso8601 to WARNING to effectively silence its logging. Change-Id: I17d7e6d5776b2369a2f8c42fd88f6c6790ad1b80 Closes-bug: #1441639 --- novaclient/shell.py | 1 + 1 file changed, 1 insertion(+) diff --git a/novaclient/shell.py b/novaclient/shell.py index ec415bb6e..41eb54774 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -498,6 +498,7 @@ def setup_debugging(self, debug): # print debug messages logging.basicConfig(level=logging.DEBUG, format=streamformat) + logging.getLogger('iso8601').setLevel(logging.WARNING) def _get_keystone_auth(self, session, auth_url, **kwargs): auth_token = kwargs.pop('auth_token', None) From c3a0c035dc6bd4676805d0618dfe334ed2d81228 Mon Sep 17 00:00:00 2001 From: "Chung Chih, Hung" Date: Sun, 19 Jul 2015 18:31:45 +0800 Subject: [PATCH 0785/1705] hypervisor command can't use cell format id to show hypervisor This bug was occurred in cell mode. In cell mode, compute node's id was not identified by integer type. It was formatted with "path!to!cell@ID". Therefore we can't using the id of hypervisor-list output. For example, > nova hypervisor-list +----------------+--------------------------+-------+---------+ | ID | Hypervisor hostname | State | Status | +----------------+--------------------------+-------+---------+ | region!child@1 | vagrant-ubuntu-trusty-64 | up | enabled | +----------------+--------------------------+-------+---------+ Change-Id: Iba0cc1993f67351b11d034f372d7a5b98dc017f0 Closes-Bug: 1475973 --- novaclient/tests/unit/v2/fakes.py | 50 ++++++++++++++++++++++++++ novaclient/tests/unit/v2/test_shell.py | 10 +++++- novaclient/utils.py | 3 +- novaclient/v2/hypervisors.py | 1 + 4 files changed, 62 insertions(+), 2 deletions(-) diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index f3052252e..77cc93b50 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -74,6 +74,8 @@ def _cs_request(self, url, method, **kwargs): munged_url = munged_url.replace('.', '_') munged_url = munged_url.replace('-', '_') munged_url = munged_url.replace(' ', '_') + munged_url = munged_url.replace('!', '_') + munged_url = munged_url.replace('@', '_') callback = "%s_%s" % (method.lower(), munged_url) if url is None or callback == "get_http:__nova_api:8774": @@ -1776,6 +1778,48 @@ def get_os_hypervisors_statistics(self, **kw): 'disk_available_least': 200} }) + def get_os_hypervisors_hyper1(self, **kw): + return (200, {}, { + 'hypervisor': + {'id': 1234, + 'service': {'id': 1, 'host': 'compute1'}, + 'vcpus': 4, + 'memory_mb': 10 * 1024, + 'local_gb': 250, + 'vcpus_used': 2, + 'memory_mb_used': 5 * 1024, + 'local_gb_used': 125, + 'hypervisor_type': "xen", + 'hypervisor_version': 3, + 'hypervisor_hostname': "hyper1", + 'free_ram_mb': 5 * 1024, + 'free_disk_gb': 125, + 'current_workload': 2, + 'running_vms': 2, + 'cpu_info': 'cpu_info', + 'disk_available_least': 100}}) + + def get_os_hypervisors_region_child_1(self, **kw): + return (200, {}, { + 'hypervisor': + {'id': 'region!child@1', + 'service': {'id': 1, 'host': 'compute1'}, + 'vcpus': 4, + 'memory_mb': 10 * 1024, + 'local_gb': 250, + 'vcpus_used': 2, + 'memory_mb_used': 5 * 1024, + 'local_gb_used': 125, + 'hypervisor_type': "xen", + 'hypervisor_version': 3, + 'hypervisor_hostname': "hyper1", + 'free_ram_mb': 5 * 1024, + 'free_disk_gb': 125, + 'current_workload': 2, + 'running_vms': 2, + 'cpu_info': 'cpu_info', + 'disk_available_least': 100}}) + def get_os_hypervisors_hyper_search(self, **kw): return (200, {}, { 'hypervisors': [ @@ -1828,6 +1872,12 @@ def get_os_hypervisors_1234_uptime(self, **kw): 'hypervisor_hostname': "hyper1", 'uptime': "fake uptime"}}) + def get_os_hypervisors_region_child_1_uptime(self, **kw): + return (200, {}, { + 'hypervisor': {'id': 'region!child@1', + 'hypervisor_hostname': "hyper1", + 'uptime': "fake uptime"}}) + def get_os_networks(self, **kw): return (200, {}, {'networks': [{"label": "1", "cidr": "10.0.0.0/24", 'project_id': diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index e5d8914a5..cc3ef7478 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -1727,14 +1727,22 @@ def test_hypervisor_show_by_id(self): self.run_command('hypervisor-show 1234') self.assert_called('GET', '/os-hypervisors/1234') + def test_hypervisor_list_show_by_cell_id(self): + self.run_command('hypervisor-show region!child@1') + self.assert_called('GET', '/os-hypervisors/region!child@1') + def test_hypervisor_show_by_name(self): self.run_command('hypervisor-show hyper1') - self.assert_called('GET', '/os-hypervisors/detail') + self.assert_called('GET', '/os-hypervisors/hyper1') def test_hypervisor_uptime_by_id(self): self.run_command('hypervisor-uptime 1234') self.assert_called('GET', '/os-hypervisors/1234/uptime') + def test_hypervisor_uptime_by_cell_id(self): + self.run_command('hypervisor-uptime region!child@1') + self.assert_called('GET', '/os-hypervisors/region!child@1/uptime') + def test_hypervisor_uptime_by_name(self): self.run_command('hypervisor-uptime hyper1') self.assert_called('GET', '/os-hypervisors/1234/uptime') diff --git a/novaclient/utils.py b/novaclient/utils.py index 1f4df7a57..5abb122e7 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -184,7 +184,8 @@ def print_dict(d, dict_property="Property", dict_value="Value", wrap=0): def find_resource(manager, name_or_id, **find_args): """Helper for the _find_* methods.""" - # for str id which is not uuid (for Flavor and Keypair search currently) + # for str id which is not uuid (for Flavor, Keypair and hypervsior in cells + # environments search currently) if getattr(manager, 'is_alphanum_id_allowed', False): try: return manager.get(name_or_id) diff --git a/novaclient/v2/hypervisors.py b/novaclient/v2/hypervisors.py index b1bfcb4b9..684a7fb34 100644 --- a/novaclient/v2/hypervisors.py +++ b/novaclient/v2/hypervisors.py @@ -31,6 +31,7 @@ def __repr__(self): class HypervisorManager(base.ManagerWithFind): resource_class = Hypervisor + is_alphanum_id_allowed = True def list(self, detailed=True): """ From d41e97e3ac164b0b65d91120b9c7f7e72f745c51 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Wed, 22 Jul 2015 04:59:47 +0000 Subject: [PATCH 0786/1705] Updated from global requirements Change-Id: I41e48745263ed9b5019a36275094db5988f2981d --- test-requirements.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test-requirements.txt b/test-requirements.txt index 408743f7f..11659711d 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -7,8 +7,7 @@ coverage>=3.6 discover fixtures>=1.3.1 keyring!=3.3,>=2.1 -mock!=1.1.4,>=1.1;python_version!='2.6' -mock==1.0.1;python_version=='2.6' +mock>=1.2 requests-mock>=0.6.0 # Apache-2.0 sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2 os-client-config>=1.4.0 From 3bf6f8fe99a988ef508edbac429b122f84bf1768 Mon Sep 17 00:00:00 2001 From: James Penick Date: Wed, 22 Jul 2015 16:23:31 +0000 Subject: [PATCH 0787/1705] rename root-password to set-password all references to `root password` have been changed to `admin password` but the `root-password` command remained. This changes it to `set-password` which is less ambiguous and consistent with existing commands `clear-password` and `get-password` Change-Id: I8683e4ecd4d9294989eb3d8d27d1fb75c16b261b --- novaclient/tests/unit/test_shell.py | 8 ++++---- novaclient/tests/unit/v2/test_shell.py | 8 ++++++++ novaclient/v2/shell.py | 6 ++++++ 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/novaclient/tests/unit/test_shell.py b/novaclient/tests/unit/test_shell.py index 4958023d7..d1f80c5a2 100644 --- a/novaclient/tests/unit/test_shell.py +++ b/novaclient/tests/unit/test_shell.py @@ -146,7 +146,7 @@ def test_invalid_timeout(self): def test_help(self): required = [ '.*?^usage: ', - '.*?^\s+root-password\s+Change the admin password', + '.*?^\s+set-password\s+Change the admin password', '.*?^See "nova help COMMAND" for help on a specific command', ] stdout, stderr = self.shell('help') @@ -156,11 +156,11 @@ def test_help(self): def test_help_on_subcommand(self): required = [ - '.*?^usage: nova root-password', + '.*?^usage: nova set-password', '.*?^Change the admin password', '.*?^Positional arguments:', ] - stdout, stderr = self.shell('help root-password') + stdout, stderr = self.shell('help set-password') for r in required: self.assertThat((stdout + stderr), matchers.MatchesRegex(r, re.DOTALL | re.MULTILINE)) @@ -168,7 +168,7 @@ def test_help_on_subcommand(self): def test_help_no_options(self): required = [ '.*?^usage: ', - '.*?^\s+root-password\s+Change the admin password', + '.*?^\s+set-password\s+Change the admin password', '.*?^See "nova help COMMAND" for help on a specific command', ] stdout, stderr = self.shell('') diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index e5d8914a5..287e87873 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -1061,6 +1061,14 @@ def test_resize_revert(self): self.assert_called('POST', '/servers/1234/action', {'revertResize': None}) + @mock.patch('getpass.getpass', mock.Mock(return_value='p')) + def test_set_password(self): + self.run_command('set-password sample-server') + self.assert_called('POST', '/servers/1234/action', + {'changePassword': {'adminPass': 'p'}}) + + # root-password is deprecated, keeping this arond until it's removed + # entirely - penick @mock.patch('getpass.getpass', mock.Mock(return_value='p')) def test_root_password(self): self.run_command('root-password sample-server') diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 32ba61164..36b0ce0c0 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -1730,6 +1730,12 @@ def do_refresh_network(cs, args): @cliutils.arg('server', metavar='', help=_('Name or ID of server.')) def do_root_password(cs, args): + """DEPRECATED, use set-password instead.""" + do_set_password(cs, args) + + +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +def do_set_password(cs, args): """ Change the admin password for a server. """ From 1d39033c8e6fae42de0f5bff9b00790c6d4c4052 Mon Sep 17 00:00:00 2001 From: liyingjun Date: Wed, 22 Jul 2015 10:01:44 +0800 Subject: [PATCH 0788/1705] Fixes table when there are multiline in result data The table doesn't display right when there are multiple line in result data. Fixes this by replace "\r" with "". Change-Id: Ia3ca4146f17c3ae097a2aad0092c25f6807fcbab Closes-bug: #1476462 --- novaclient/utils.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/novaclient/utils.py b/novaclient/utils.py index 1f4df7a57..ae4894db5 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -97,6 +97,8 @@ def print_list(objs, fields, formatters={}, sortby_index=None): data = getattr(o, field_name, '') if data is None: data = '-' + # '\r' would break the table, so remove it. + data = str(data).replace("\r", "") row.append(data) pt.add_row(row) @@ -163,7 +165,10 @@ def print_dict(d, dict_property="Property", dict_value="Value", wrap=0): v = textwrap.fill(str(v), wrap) # if value has a newline, add in multiple rows # e.g. fault with stacktrace - if v and isinstance(v, six.string_types) and r'\n' in v: + if v and isinstance(v, six.string_types) and (r'\n' in v or '\r' in v): + # '\r' would break the table, so remove it. + if '\r' in v: + v = v.replace('\r', '') lines = v.strip().split(r'\n') col1 = k for line in lines: From 936cf572dff92036db8966204e01a59fe67ffb83 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Thu, 2 Apr 2015 19:28:02 +0300 Subject: [PATCH 0789/1705] Implements 'microversions' api type - Part 2 New decorator "novaclient.api_versions.wraps" replaces original method with substitution. This substitution searches for methods which desire specified api version. Also, this patch updates novaclient shell to discover versioned methods and arguments. Related to bp api-microversion-support Co-Authored-By: Alex Xu Change-Id: I1939c19664e58e2def684380d64c465dc1cfc132 --- novaclient/api_versions.py | 69 ++++++++++++ novaclient/base.py | 4 + novaclient/exceptions.py | 11 ++ novaclient/shell.py | 51 +++++++-- novaclient/tests/unit/fake_actions_module.py | 39 +++++++ novaclient/tests/unit/test_api_versions.py | 79 ++++++++++++++ novaclient/tests/unit/test_shell.py | 109 +++++++++++++++++++ novaclient/tests/unit/v2/fakes.py | 5 +- novaclient/utils.py | 10 ++ 9 files changed, 367 insertions(+), 10 deletions(-) create mode 100644 novaclient/tests/unit/fake_actions_module.py diff --git a/novaclient/api_versions.py b/novaclient/api_versions.py index 6d1c0672b..e50edea2a 100644 --- a/novaclient/api_versions.py +++ b/novaclient/api_versions.py @@ -11,6 +11,7 @@ # License for the specific language governing permissions and limitations # under the License. +import functools import logging import os import pkgutil @@ -20,6 +21,7 @@ from novaclient import exceptions from novaclient.i18n import _, _LW +from novaclient import utils LOG = logging.getLogger(__name__) if not LOG.handlers: @@ -29,6 +31,7 @@ # key is a deprecated version and value is an alternative version. DEPRECATED_VERSIONS = {"1.1": "2"} +_SUBSTITUTIONS = {} _type_error_msg = _("'%(other)s' should be an instance of '%(cls)s'") @@ -150,6 +153,31 @@ def get_string(self): return "%s.%s" % (self.ver_major, self.ver_minor) +class VersionedMethod(object): + + def __init__(self, name, start_version, end_version, func): + """Versioning information for a single method + + :param name: Name of the method + :param start_version: Minimum acceptable version + :param end_version: Maximum acceptable_version + :param func: Method to call + + Minimum and maximums are inclusive + """ + self.name = name + self.start_version = start_version + self.end_version = end_version + self.func = func + + def __str__(self): + return ("Version Method %s: min: %s, max: %s" + % (self.name, self.start_version, self.end_version)) + + def __repr__(self): + return "" % self.name + + def get_available_major_versions(): # NOTE(andreykurilin): available clients version should not be # hardcoded, so let's discover them. @@ -206,3 +234,44 @@ def update_headers(headers, api_version): if not api_version.is_null() and api_version.ver_minor != 0: headers["X-OpenStack-Nova-API-Version"] = api_version.get_string() + + +def add_substitution(versioned_method): + _SUBSTITUTIONS.setdefault(versioned_method.name, []) + _SUBSTITUTIONS[versioned_method.name].append(versioned_method) + + +def get_substitutions(func_name, api_version=None): + substitutions = _SUBSTITUTIONS.get(func_name, []) + if api_version and not api_version.is_null(): + return [m for m in substitutions + if api_version.matches(m.start_version, m.end_version)] + return substitutions + + +def wraps(start_version, end_version=None): + start_version = APIVersion(start_version) + if end_version: + end_version = APIVersion(end_version) + else: + end_version = APIVersion("%s.latest" % start_version.ver_major) + + def decor(func): + func.versioned = True + name = utils.get_function_name(func) + versioned_method = VersionedMethod(name, start_version, + end_version, func) + add_substitution(versioned_method) + + @functools.wraps(func) + def substitution(obj, *args, **kwargs): + methods = get_substitutions(name, obj.api_version) + + if not methods: + raise exceptions.VersionNotFoundForAPIMethod( + obj.api_version.get_string(), name) + else: + return max(methods, key=lambda f: f.start_version).func( + obj, *args, **kwargs) + return substitution + return decor diff --git a/novaclient/base.py b/novaclient/base.py index e541c8e30..74c7e2fe2 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -61,6 +61,10 @@ def __init__(self, api): def client(self): return self.api.client + @property + def api_version(self): + return self.api.api_version + def _list(self, url, response_key, obj_class=None, body=None): if body: _resp, body = self.api.client.post(url, body=body) diff --git a/novaclient/exceptions.py b/novaclient/exceptions.py index c95925416..371197581 100644 --- a/novaclient/exceptions.py +++ b/novaclient/exceptions.py @@ -82,6 +82,17 @@ class InstanceInErrorState(Exception): pass +class VersionNotFoundForAPIMethod(Exception): + msg_fmt = "API version '%(vers)s' is not supported on '%(method)s' method." + + def __init__(self, version, method): + self.version = version + self.method = method + + def __str__(self): + return self.msg_fmt % {"vers": self.version, "method": self.method} + + class ClientException(Exception): """ The base exception class for all exceptions this library raises. diff --git a/novaclient/shell.py b/novaclient/shell.py index f91d1fa80..acda8bf04 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -426,7 +426,7 @@ def get_base_parser(self): return parser - def get_subcommand_parser(self, version): + def get_subcommand_parser(self, version, do_help=False): parser = self.get_base_parser() self.subcommands = {} @@ -435,12 +435,11 @@ def get_subcommand_parser(self, version): actions_module = importutils.import_module( "novaclient.v%s.shell" % version.ver_major) - # TODO(andreykurilin): discover actions based on microversions - self._find_actions(subparsers, actions_module) - self._find_actions(subparsers, self) + self._find_actions(subparsers, actions_module, version, do_help) + self._find_actions(subparsers, self, version, do_help) for extension in self.extensions: - self._find_actions(subparsers, extension.module) + self._find_actions(subparsers, extension.module, version, do_help) self._add_bash_completion_subparser(subparsers) @@ -460,12 +459,28 @@ def _add_bash_completion_subparser(self, subparsers): self.subcommands['bash_completion'] = subparser subparser.set_defaults(func=self.do_bash_completion) - def _find_actions(self, subparsers, actions_module): + def _find_actions(self, subparsers, actions_module, version, do_help): + msg = _(" (Supported by API versions '%(start)s' - '%(end)s')") for attr in (a for a in dir(actions_module) if a.startswith('do_')): # I prefer to be hyphen-separated instead of underscores. command = attr[3:].replace('_', '-') callback = getattr(actions_module, attr) desc = callback.__doc__ or '' + if hasattr(callback, "versioned"): + subs = api_versions.get_substitutions( + utils.get_function_name(callback)) + if do_help: + desc += msg % {'start': subs[0].start_version.get_string(), + 'end': subs[-1].end_version.get_string()} + else: + for versioned_method in subs: + if version.matches(versioned_method.start_version, + versioned_method.end_version): + callback = versioned_method.func + break + else: + continue + action_help = desc.strip() arguments = getattr(callback, 'arguments', []) @@ -482,7 +497,26 @@ def _find_actions(self, subparsers, actions_module): ) self.subcommands[command] = subparser for (args, kwargs) in arguments: - subparser.add_argument(*args, **kwargs) + start_version = kwargs.get("start_version", None) + if start_version: + start_version = api_versions.APIVersion(start_version) + end_version = kwargs.get("end_version", None) + if end_version: + end_version = api_versions.APIVersion(end_version) + else: + end_version = api_versions.APIVersion( + "%s.latest" % start_version.ver_major) + if do_help: + kwargs["help"] = kwargs.get("help", "") + (msg % { + "start": start_version.get_string(), + "end": end_version.get_string()}) + else: + if not version.matches(start_version, end_version): + continue + kw = kwargs.copy() + kw.pop("start_version", None) + kw.pop("end_version", None) + subparser.add_argument(*args, **kw) subparser.set_defaults(func=callback) def setup_debugging(self, debug): @@ -534,7 +568,8 @@ def main(self, argv): spot = argv.index('--endpoint_type') argv[spot] = '--endpoint-type' - subcommand_parser = self.get_subcommand_parser(api_version) + subcommand_parser = self.get_subcommand_parser( + api_version, do_help=("help" in args)) self.parser = subcommand_parser if options.help or not argv: diff --git a/novaclient/tests/unit/fake_actions_module.py b/novaclient/tests/unit/fake_actions_module.py new file mode 100644 index 000000000..2bd0e2f04 --- /dev/null +++ b/novaclient/tests/unit/fake_actions_module.py @@ -0,0 +1,39 @@ +# Copyright 2011 OpenStack Foundation +# All Rights Reserved. +# +# 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 novaclient import api_versions +from novaclient.openstack.common import cliutils + + +@api_versions.wraps("2.10", "2.20") +def do_fake_action(): + return 1 + + +@api_versions.wraps("2.21", "2.30") +def do_fake_action(): + return 2 + + +@cliutils.arg( + '--foo', + start_version='2.1', + end_version='2.2') +@cliutils.arg( + '--bar', + start_version='2.3', + end_version='2.4') +def do_fake_action2(): + return 3 diff --git a/novaclient/tests/unit/test_api_versions.py b/novaclient/tests/unit/test_api_versions.py index 13f557cfd..2db3a8859 100644 --- a/novaclient/tests/unit/test_api_versions.py +++ b/novaclient/tests/unit/test_api_versions.py @@ -168,3 +168,82 @@ def test_major_and_minor_parts_is_presented(self, mock_apiversion): self.assertEqual(mock_apiversion.return_value, api_versions.get_api_version(version)) mock_apiversion.assert_called_once_with(version) + + +class WrapsTestCase(utils.TestCase): + + def _get_obj_with_vers(self, vers): + return mock.MagicMock(api_version=api_versions.APIVersion(vers)) + + def _side_effect_of_vers_method(self, *args, **kwargs): + m = mock.MagicMock(start_version=args[1], end_version=args[2]) + m.name = args[0] + return m + + @mock.patch("novaclient.utils.get_function_name") + @mock.patch("novaclient.api_versions.VersionedMethod") + def test_end_version_is_none(self, mock_versioned_method, mock_name): + func_name = "foo" + mock_name.return_value = func_name + mock_versioned_method.side_effect = self._side_effect_of_vers_method + + @api_versions.wraps("2.2") + def foo(*args, **kwargs): + pass + + foo(self._get_obj_with_vers("2.4")) + + mock_versioned_method.assert_called_once_with( + func_name, api_versions.APIVersion("2.2"), + api_versions.APIVersion("2.latest"), mock.ANY) + + @mock.patch("novaclient.utils.get_function_name") + @mock.patch("novaclient.api_versions.VersionedMethod") + def test_start_and_end_version_are_presented(self, mock_versioned_method, + mock_name): + func_name = "foo" + mock_name.return_value = func_name + mock_versioned_method.side_effect = self._side_effect_of_vers_method + + @api_versions.wraps("2.2", "2.6") + def foo(*args, **kwargs): + pass + + foo(self._get_obj_with_vers("2.4")) + + mock_versioned_method.assert_called_once_with( + func_name, api_versions.APIVersion("2.2"), + api_versions.APIVersion("2.6"), mock.ANY) + + @mock.patch("novaclient.utils.get_function_name") + @mock.patch("novaclient.api_versions.VersionedMethod") + def test_api_version_doesnt_match(self, mock_versioned_method, mock_name): + func_name = "foo" + mock_name.return_value = func_name + mock_versioned_method.side_effect = self._side_effect_of_vers_method + + @api_versions.wraps("2.2", "2.6") + def foo(*args, **kwargs): + pass + + self.assertRaises(exceptions.VersionNotFoundForAPIMethod, + foo, self._get_obj_with_vers("2.1")) + + mock_versioned_method.assert_called_once_with( + func_name, api_versions.APIVersion("2.2"), + api_versions.APIVersion("2.6"), mock.ANY) + + def test_define_method_is_actually_called(self): + checker = mock.MagicMock() + + @api_versions.wraps("2.2", "2.6") + def some_func(*args, **kwargs): + checker(*args, **kwargs) + + obj = self._get_obj_with_vers("2.4") + some_args = ("arg_1", "arg_2") + some_kwargs = {"key1": "value1", "key2": "value2"} + + some_func(obj, *some_args, **some_kwargs) + + checker.assert_called_once_with(*((obj,) + some_args), **some_kwargs) diff --git a/novaclient/tests/unit/test_shell.py b/novaclient/tests/unit/test_shell.py index 3d6fc63c0..1caa56cbd 100644 --- a/novaclient/tests/unit/test_shell.py +++ b/novaclient/tests/unit/test_shell.py @@ -23,9 +23,11 @@ import six from testtools import matchers +from novaclient import api_versions import novaclient.client from novaclient import exceptions import novaclient.shell +from novaclient.tests.unit import fake_actions_module from novaclient.tests.unit import utils FAKE_ENV = {'OS_USERNAME': 'username', @@ -402,6 +404,113 @@ def test_keyring_saver_helper(self, mock_client, self.assertIsInstance(keyring_saver, novaclient.shell.SecretsHelper) +class TestLoadVersionedActions(utils.TestCase): + + def test_load_versioned_actions(self): + parser = novaclient.shell.NovaClientArgumentParser() + subparsers = parser.add_subparsers(metavar='') + shell = novaclient.shell.OpenStackComputeShell() + shell.subcommands = {} + shell._find_actions(subparsers, fake_actions_module, + api_versions.APIVersion("2.15"), False) + self.assertIn('fake-action', shell.subcommands.keys()) + self.assertEqual( + 1, shell.subcommands['fake-action'].get_default('func')()) + + shell.subcommands = {} + shell._find_actions(subparsers, fake_actions_module, + api_versions.APIVersion("2.25"), False) + self.assertIn('fake-action', shell.subcommands.keys()) + self.assertEqual( + 2, shell.subcommands['fake-action'].get_default('func')()) + + self.assertIn('fake-action2', shell.subcommands.keys()) + self.assertEqual( + 3, shell.subcommands['fake-action2'].get_default('func')()) + + def test_load_versioned_actions_not_in_version_range(self): + parser = novaclient.shell.NovaClientArgumentParser() + subparsers = parser.add_subparsers(metavar='') + shell = novaclient.shell.OpenStackComputeShell() + shell.subcommands = {} + shell._find_actions(subparsers, fake_actions_module, + api_versions.APIVersion("2.10000"), False) + self.assertNotIn('fake-action', shell.subcommands.keys()) + self.assertIn('fake-action2', shell.subcommands.keys()) + + def test_load_versioned_actions_with_help(self): + parser = novaclient.shell.NovaClientArgumentParser() + subparsers = parser.add_subparsers(metavar='') + shell = novaclient.shell.OpenStackComputeShell() + shell.subcommands = {} + shell._find_actions(subparsers, fake_actions_module, + api_versions.APIVersion("2.10000"), True) + self.assertIn('fake-action', shell.subcommands.keys()) + expected_desc = ("(Supported by API versions '%(start)s' - " + "'%(end)s')") % {'start': '2.10', 'end': '2.30'} + self.assertIn(expected_desc, + shell.subcommands['fake-action'].description) + + @mock.patch.object(novaclient.shell.NovaClientArgumentParser, + 'add_argument') + def test_load_versioned_actions_with_args(self, mock_add_arg): + parser = novaclient.shell.NovaClientArgumentParser(add_help=False) + subparsers = parser.add_subparsers(metavar='') + shell = novaclient.shell.OpenStackComputeShell() + shell.subcommands = {} + shell._find_actions(subparsers, fake_actions_module, + api_versions.APIVersion("2.1"), False) + self.assertIn('fake-action2', shell.subcommands.keys()) + mock_add_arg.assert_has_calls([ + mock.call('-h', '--help', action='help', help='==SUPPRESS=='), + mock.call('--foo')]) + + @mock.patch.object(novaclient.shell.NovaClientArgumentParser, + 'add_argument') + def test_load_versioned_actions_with_args2(self, mock_add_arg): + parser = novaclient.shell.NovaClientArgumentParser(add_help=False) + subparsers = parser.add_subparsers(metavar='') + shell = novaclient.shell.OpenStackComputeShell() + shell.subcommands = {} + shell._find_actions(subparsers, fake_actions_module, + api_versions.APIVersion("2.4"), False) + self.assertIn('fake-action2', shell.subcommands.keys()) + mock_add_arg.assert_has_calls([ + mock.call('-h', '--help', action='help', help='==SUPPRESS=='), + mock.call('--bar')]) + + @mock.patch.object(novaclient.shell.NovaClientArgumentParser, + 'add_argument') + def test_load_versioned_actions_with_args_not_in_version_range( + self, mock_add_arg): + parser = novaclient.shell.NovaClientArgumentParser(add_help=False) + subparsers = parser.add_subparsers(metavar='') + shell = novaclient.shell.OpenStackComputeShell() + shell.subcommands = {} + shell._find_actions(subparsers, fake_actions_module, + api_versions.APIVersion("2.10000"), False) + self.assertIn('fake-action2', shell.subcommands.keys()) + mock_add_arg.assert_has_calls([ + mock.call('-h', '--help', action='help', help='==SUPPRESS==')]) + + @mock.patch.object(novaclient.shell.NovaClientArgumentParser, + 'add_argument') + def test_load_versioned_actions_with_args_and_help(self, mock_add_arg): + parser = novaclient.shell.NovaClientArgumentParser(add_help=False) + subparsers = parser.add_subparsers(metavar='') + shell = novaclient.shell.OpenStackComputeShell() + shell.subcommands = {} + shell._find_actions(subparsers, fake_actions_module, + api_versions.APIVersion("2.4"), True) + mock_add_arg.assert_has_calls([ + mock.call('-h', '--help', action='help', help='==SUPPRESS=='), + mock.call('-h', '--help', action='help', help='==SUPPRESS=='), + mock.call('--foo', + help=" (Supported by API versions '2.1' - '2.2')"), + mock.call('--bar', + help=" (Supported by API versions '2.3' - '2.4')")]) + + class ShellTestKeystoneV3(ShellTest): def make_env(self, exclude=None, fake_env=FAKE_ENV): if 'OS_AUTH_URL' in fake_env: diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 5b5971e4d..5819e2707 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -2232,10 +2232,11 @@ def delete_os_server_groups_2cbd51f4_fafe_4cdb_801b_cf913a6f288b( class FakeSessionClient(fakes.FakeClient, client.Client): - def __init__(self, *args, **kwargs): + def __init__(self, api_version, *args, **kwargs): client.Client.__init__(self, 'username', 'password', 'project_id', 'auth_url', - extensions=kwargs.get('extensions')) + extensions=kwargs.get('extensions'), + api_version=api_version) self.client = FakeSessionMockClient(**kwargs) diff --git a/novaclient/utils.py b/novaclient/utils.py index 1f4df7a57..be513bc78 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -368,3 +368,13 @@ def record_time(times, enabled, *args): yield end = time.time() times.append((' '.join(args), start, end)) + + +def get_function_name(func): + if six.PY2: + if hasattr(func, "im_class"): + return "%s.%s" % (func.im_class, func.__name__) + else: + return "%s.%s" % (func.__module__, func.__name__) + else: + return "%s.%s" % (func.__module__, func.__qualname__) From eddb4d61c2412d809b684ec2b0947c37a540171a Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Wed, 15 Jul 2015 18:57:45 +0300 Subject: [PATCH 0790/1705] Add "get_current" method to VersionManager get_current returns the current endpoint's API version info. Related to bp api-microversion-support Co-Authored-By: Alex Xu Change-Id: I36e84680c578a88d5c274f8ca24ecd18d1f3a179 --- novaclient/tests/unit/v2/fakes.py | 15 +++++++++++- novaclient/tests/unit/v2/test_versions.py | 28 +++++++++++++++++++++++ novaclient/v2/versions.py | 15 ++++++++++++ 3 files changed, 57 insertions(+), 1 deletion(-) diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 5819e2707..f468e3e48 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -58,6 +58,7 @@ def __init__(self, **kwargs): self.os_cache = 'os_cache' self.http_log_debug = 'http_log_debug' self.last_request_id = None + self.management_url = self.get_endpoint() def _cs_request(self, url, method, **kwargs): # Check that certain things are called correctly @@ -81,6 +82,8 @@ def _cs_request(self, url, method, **kwargs): # To get API version information, it is necessary to GET # a nova endpoint directly without "v2/". callback = "get_versions" + elif callback == "get_http:__nova_api:8774_v2_1": + callback = "get_current_version" if not hasattr(self, callback): raise AssertionError('Called unknown API method: %s %s, ' @@ -99,7 +102,7 @@ def _cs_request(self, url, method, **kwargs): return r, body def get_endpoint(self): - return "http://nova-api:8774/v2/190a755eef2e4aac9f06aa6be9786385" + return "http://nova-api:8774/v2.1/190a755eef2e4aac9f06aa6be9786385" def get_versions(self): return (200, {}, { @@ -118,6 +121,16 @@ def get_versions(self): "id": "v2.1"} ]}) + def get_current_version(self): + return (200, {}, { + "version": {"status": "CURRENT", + "updated": "2013-07-23T11:33:21Z", + "links": [{"href": "http://nova-api:8774/v2.1/", + "rel": "self"}], + "min_version": "2.1", + "version": "2.3", + "id": "v2.1"}}) + # # agents # diff --git a/novaclient/tests/unit/v2/test_versions.py b/novaclient/tests/unit/v2/test_versions.py index 91139844f..eeba89a28 100644 --- a/novaclient/tests/unit/v2/test_versions.py +++ b/novaclient/tests/unit/v2/test_versions.py @@ -36,3 +36,31 @@ def test_list_services_with_http_client(self, mock_is_session_client): def test_list_services_with_session_client(self, mock_is_session_client): self.cs.versions.list() self.cs.assert_called('GET', 'http://nova-api:8774/') + + @mock.patch.object(versions.VersionManager, '_is_session_client', + return_value=False) + @mock.patch.object(versions.VersionManager, 'list') + def test_get_current_with_http_client(self, mock_list, + mock_is_session_client): + current_version = versions.Version( + None, {"links": [{"href": "http://nova-api:8774/v2.1"}]}, + loaded=True) + + mock_list.return_value = [ + versions.Version( + None, {"links": [{"href": "http://url/v1"}]}, loaded=True), + versions.Version( + None, {"links": [{"href": "http://url/v2"}]}, loaded=True), + versions.Version( + None, {"links": [{"href": "http://url/v3"}]}, loaded=True), + current_version, + versions.Version( + None, {"links": [{"href": "http://url/v21"}]}, loaded=True)] + self.assertEqual(current_version, self.cs.versions.get_current()) + + @mock.patch.object(versions.VersionManager, '_is_session_client', + return_value=True) + def test_get_current_with_session_client(self, mock_is_session_client): + self.cs.callback = [] + self.cs.versions.get_current() + self.cs.assert_called('GET', 'http://nova-api:8774/v2.1') diff --git a/novaclient/v2/versions.py b/novaclient/v2/versions.py index 85d62d706..b43fb4dee 100644 --- a/novaclient/v2/versions.py +++ b/novaclient/v2/versions.py @@ -36,6 +36,21 @@ class VersionManager(base.ManagerWithFind): def _is_session_client(self): return isinstance(self.api.client, client.SessionClient) + def get_current(self): + """Returns info about current version.""" + if self._is_session_client(): + url = self.api.client.get_endpoint().rsplit("/", 1)[0] + return self._get(url, "version") + else: + # NOTE(andreykurilin): HTTPClient doesn't have ability to send get + # request without token in the url, so `self._get` doesn't work. + all_versions = self.list() + url = self.client.management_url.rsplit("/", 1)[0] + for version in all_versions: + for link in version.links: + if link["href"].rstrip('/') == url: + return version + def list(self): """List all versions.""" From ba79073d7b6a9f0b8af88dad4a13b80181be0ba6 Mon Sep 17 00:00:00 2001 From: wangxiyuan Date: Wed, 1 Jul 2015 15:26:10 +0800 Subject: [PATCH 0791/1705] Add 'deleted' status check in _poll_for_status When use command '--poll' to show progress, if the object's staus change into 'deleted' for some reason, it will lead a endless loop.Becasue there is no check about 'deleted' status. For example, when use 'image-create --poll' to create a vm's snapshoot, if glance's quota is small than the snapshoot, the image will be deleted. Change-Id: I028e3064b3371f87873d74494c39255775a2c818 Closes-bug:#1470047 --- novaclient/exceptions.py | 5 +++++ novaclient/tests/unit/v2/fakes.py | 15 +++++++++++++++ novaclient/tests/unit/v2/test_shell.py | 18 ++++++++++++++++++ novaclient/v2/shell.py | 4 ++++ 4 files changed, 42 insertions(+) diff --git a/novaclient/exceptions.py b/novaclient/exceptions.py index 371197581..ede19bd8c 100644 --- a/novaclient/exceptions.py +++ b/novaclient/exceptions.py @@ -93,6 +93,11 @@ def __str__(self): return self.msg_fmt % {"vers": self.version, "method": self.method} +class InstanceInDeletedState(Exception): + """Instance is in the deleted state.""" + pass + + class ClientException(Exception): """ The base exception class for all exceptions this library raises. diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index b6656a2f4..add037a9c 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -665,6 +665,8 @@ def post_servers_1234_action(self, body, **kw): elif action == 'createImage': assert set(body[action].keys()) == set(['name', 'metadata']) _headers = dict(location="http://blah/images/456") + if body[action]['name'] == 'mysnapshot_deleted': + _headers = dict(location="http://blah/images/457") elif action == 'os-getConsoleOutput': assert list(body[action]) == ['length'] return (202, {}, {'output': 'foo'}) @@ -1035,6 +1037,16 @@ def get_images_detail(self, **kw): "status": "SAVING", "progress": 80, "links": {}, + }, + { + "id": 3, + "name": "My Server Backup Deleted", + "serverId": 1234, + "updated": "2010-10-10T12:00:00Z", + "created": "2010-08-10T12:00:00Z", + "status": "DELETED", + "fault": {'message': 'Image has been deleted.'}, + "links": {}, } ]}) @@ -1047,6 +1059,9 @@ def get_images_2(self, **kw): def get_images_456(self, **kw): return (200, {}, {'image': self.get_images_detail()[2]['images'][1]}) + def get_images_457(self, **kw): + return (200, {}, {'image': self.get_images_detail()[2]['images'][2]}) + def get_images_3e861307_73a6_4d1f_8d68_f68b03223032(self): raise exceptions.NotFound('404') diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 8abfd1b81..3e1c4d700 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -813,6 +813,24 @@ def test_create_image_show(self): self.assertIn('My Server Backup', output) self.assertIn('SAVING', output) + @mock.patch('novaclient.v2.shell._poll_for_status') + def test_create_image_with_poll(self, poll_method): + self.run_command( + 'image-create sample-server mysnapshot --poll') + self.assert_called_anytime( + 'POST', '/servers/1234/action', + {'createImage': {'name': 'mysnapshot', 'metadata': {}}}, + ) + self.assertEqual(1, poll_method.call_count) + poll_method.assert_has_calls( + [mock.call(self.shell.cs.images.get, '456', 'snapshotting', + ['active'])]) + + def test_create_image_with_poll_to_check_image_state_deleted(self): + self.assertRaises( + exceptions.InstanceInDeletedState, self.run_command, + 'image-create sample-server mysnapshot_deleted --poll') + def test_image_delete(self): self.run_command('image-delete 1') self.assert_called('DELETE', '/images/1') diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index dbab11c09..1e74ff83b 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -583,6 +583,10 @@ def print_progress(progress): if not silent: print(_("\nError %s server") % action) raise exceptions.InstanceInErrorState(obj.fault['message']) + elif status == "deleted": + if not silent: + print(_("\nDeleted %s server") % action) + raise exceptions.InstanceInDeletedState(obj.fault['message']) if not silent: print_progress(progress) From c69c38c58f6789cdaa89d51b696efc0c7b5f825d Mon Sep 17 00:00:00 2001 From: Ian Cordasco Date: Wed, 22 Jul 2015 15:13:01 -0500 Subject: [PATCH 0792/1705] Use keystoneclient's TCPKeepAliveAdapter When novaclient isn't using a session from keystoneclient, it needs to set reasonable TCP Keep-Alive values otherwise the operating system defaults may cause the client to hang for hours before a connection will time out. Using keystoneclient's adpater (which sets good defaults) will allow us to not have to maintain this adapter here and to benefit from their defaults. Closes-bug: 1477275 Related-bug: 1323862 Depends-On: Ibd53ae2d4d2455db0ebc9951e5c764befc57850f Change-Id: I1924bd96eb1a4bac5d57a5cc5d5461acb3f7f5ac --- novaclient/client.py | 15 ++------------- novaclient/tests/unit/test_client.py | 21 +-------------------- 2 files changed, 3 insertions(+), 33 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index 5ee6867dc..98dab06c8 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -30,14 +30,13 @@ import os import pkgutil import re -import socket from keystoneclient import adapter +from keystoneclient import session from oslo_utils import importutils from oslo_utils import netutils import pkg_resources import requests -from requests import adapters try: import json @@ -54,16 +53,6 @@ from novaclient import utils -class TCPKeepAliveAdapter(adapters.HTTPAdapter): - """The custom adapter used to set TCP Keep-Alive on all connections.""" - def init_poolmanager(self, *args, **kwargs): - kwargs.setdefault('socket_options', [ - (socket.IPPROTO_TCP, socket.TCP_NODELAY, 1), - (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1), - ]) - super(TCPKeepAliveAdapter, self).init_poolmanager(*args, **kwargs) - - class _ClientConnectionPool(object): def __init__(self): @@ -74,7 +63,7 @@ def get(self, url): Store and reuse HTTP adapters per Service URL. """ if url not in self._adapters: - self._adapters[url] = TCPKeepAliveAdapter() + self._adapters[url] = session.TCPKeepAliveAdapter() return self._adapters[url] diff --git a/novaclient/tests/unit/test_client.py b/novaclient/tests/unit/test_client.py index 1cade390d..a97ecbe86 100644 --- a/novaclient/tests/unit/test_client.py +++ b/novaclient/tests/unit/test_client.py @@ -16,7 +16,6 @@ import json import logging -import socket import fixtures from keystoneclient import adapter @@ -29,27 +28,9 @@ import novaclient.v2.client -class TCPKeepAliveAdapterTest(utils.TestCase): - - @mock.patch.object(requests.adapters.HTTPAdapter, 'init_poolmanager') - def test_init_poolmanager(self, mock_init_poolmgr): - adapter = novaclient.client.TCPKeepAliveAdapter() - kwargs = {} - adapter.init_poolmanager(**kwargs) - if requests.__version__ >= '2.4.1': - kwargs.setdefault('socket_options', [ - (socket.IPPROTO_TCP, socket.TCP_NODELAY, 1), - (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1), - ]) - # NOTE(melwitt): This is called twice because - # HTTPAdapter.__init__ calls it first. - self.assertEqual(2, mock_init_poolmgr.call_count) - mock_init_poolmgr.assert_called_with(**kwargs) - - class ClientConnectionPoolTest(utils.TestCase): - @mock.patch("novaclient.client.TCPKeepAliveAdapter") + @mock.patch("keystoneclient.session.TCPKeepAliveAdapter") def test_get(self, mock_http_adapter): mock_http_adapter.side_effect = lambda: mock.Mock() pool = novaclient.client._ClientConnectionPool() From 089318d8317416ae567b9f66bec8e9ad8d5b2f5e Mon Sep 17 00:00:00 2001 From: Kyrylo Romanenko Date: Thu, 30 Jul 2015 12:13:54 +0300 Subject: [PATCH 0793/1705] Refactor and update test_instances and test_volumes_api Add method wait_for_volume_status to base.py to deduplicate code in test_instances.py and test_volumes_api.py. Removed volume_id_from_cli_create function as obsolete. Change-Id: I44e35e181515923a677b336ed48bc705cf673134 --- novaclient/tests/functional/base.py | 20 +++++++ novaclient/tests/functional/test_instances.py | 57 +++---------------- .../tests/functional/test_volumes_api.py | 10 +--- 3 files changed, 29 insertions(+), 58 deletions(-) diff --git a/novaclient/tests/functional/base.py b/novaclient/tests/functional/base.py index 540bb22ff..30d6cd21b 100644 --- a/novaclient/tests/functional/base.py +++ b/novaclient/tests/functional/base.py @@ -11,6 +11,7 @@ # under the License. import os +import time import fixtures import os_client_config @@ -170,3 +171,22 @@ def setUp(self): def nova(self, *args, **kwargs): return self.cli_clients.nova(*args, **kwargs) + + def wait_for_volume_status(self, volume, status, timeout=60, + poll_interval=1): + """Wait until volume reaches given status. + + :param volume_id: uuid4 id of given volume + :param status: expected status of volume + :param timeout: timeout in seconds + :param poll_interval: poll interval in seconds + """ + start_time = time.time() + while time.time() - start_time < timeout: + volume = self.client.volumes.get(volume.id) + if volume.status == status: + break + time.sleep(poll_interval) + else: + self.fail("Volume %s did not reach status %s after %d s" + % (volume.id, status, timeout)) diff --git a/novaclient/tests/functional/test_instances.py b/novaclient/tests/functional/test_instances.py index 8df28d136..b569cb3cb 100644 --- a/novaclient/tests/functional/test_instances.py +++ b/novaclient/tests/functional/test_instances.py @@ -10,41 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. -import time import uuid from novaclient.tests.functional import base -def volume_id_from_cli_create(output): - """Scrape the volume id out of the 'volume create' command - - The cli for Nova automatically routes requests to the volumes - service end point. However the nova api low level commands don't - redirect to the correct service endpoint, so for volumes commands - (even setup ones) we use the cli for magic routing. - - This function lets us get the id out of the prettytable that's - dumped on the cli during create. - - """ - for line in output.split("\n"): - fields = line.split() - if len(fields) > 4: - if fields[1] == "id": - return fields[3] - - -def volume_at_status(output, volume_id, status): - for line in output.split("\n"): - fields = line.split() - if len(fields) > 4: - if fields[1] == volume_id: - return fields[3] == status - raise Exception("Volume %s did not reach status '%s' in output: %s" - % (volume_id, status, output)) - - class TestInstanceCLI(base.ClientTestBase): def test_attach_volume(self): @@ -84,30 +54,19 @@ def test_attach_volume(self): # create a volume for attachment. We use the CLI because it # magic routes to cinder, however the low level API does not. - volume_id = volume_id_from_cli_create( - self.nova('volume-create', params="1")) - self.addCleanup(self.nova, 'volume-delete', params=volume_id) + volume = self.client.volumes.create(1) + self.addCleanup(self.nova, 'volume-delete', params=volume.id) # allow volume to become available - for x in xrange(60): - volumes = self.nova('volume-list') - if volume_at_status(volumes, volume_id, 'available'): - break - time.sleep(1) - else: - self.fail("Volume %s not available after 60s" % volume_id) + self.wait_for_volume_status(volume, 'available') # attach the volume - self.nova('volume-attach', params="%s %s" % (name, volume_id)) + self.nova('volume-attach', params="%s %s" % (name, volume.id)) # volume needs to transition to 'in-use' to be attached - for x in xrange(60): - volumes = self.nova('volume-list') - if volume_at_status(volumes, volume_id, 'in-use'): - break - time.sleep(1) - else: - self.fail("Volume %s not attached after 60s" % volume_id) + self.wait_for_volume_status(volume, 'in-use') # clean up on success - self.nova('volume-detach', params="%s %s" % (name, volume_id)) + self.nova('volume-detach', params="%s %s" % (name, volume.id)) + self.wait_for_volume_status(volume, 'available') + self.nova('volume-delete', params=volume.id) diff --git a/novaclient/tests/functional/test_volumes_api.py b/novaclient/tests/functional/test_volumes_api.py index 38c63bbf5..7c9214268 100644 --- a/novaclient/tests/functional/test_volumes_api.py +++ b/novaclient/tests/functional/test_volumes_api.py @@ -44,15 +44,7 @@ def test_volumes_snapshots_types_create_get_list_delete(self): self.addCleanup(volume.delete) # Wait for the volume to become available - for x in six.moves.range(60): - volume = self.client.volumes.get(volume.id) - if volume.status == 'available': - break - elif volume.status == 'error': - self.fail('Volume %s is in error state' % volume.id) - time.sleep(1) - else: - self.fail('Volume %s not available after 60s' % volume.id) + self.wait_for_volume_status(volume, 'available') # List all volumes self.client.volumes.list() From c0c17099a54daabdcf3e6727f941d06ee9d50ba3 Mon Sep 17 00:00:00 2001 From: Kyrylo Romanenko Date: Thu, 30 Jul 2015 19:10:50 +0300 Subject: [PATCH 0794/1705] Specify NIC option for nova boot Choose first network from networks list and specify it for nova boot. Otherwise nova can not boot instance when more then one network available. Change-Id: I4813f6fff3050e2141e3a5e8eb4d7d8919721703 --- novaclient/tests/functional/test_instances.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/novaclient/tests/functional/test_instances.py b/novaclient/tests/functional/test_instances.py index 8df28d136..8350b4f00 100644 --- a/novaclient/tests/functional/test_instances.py +++ b/novaclient/tests/functional/test_instances.py @@ -71,8 +71,10 @@ def test_attach_volume(self): name = str(uuid.uuid4()) # Boot via the cli, as we're primarily testing the cli in this test - self.nova('boot', params="--flavor %s --image %s %s --poll" % - (self.flavor.name, self.image.name, name)) + network = self.client.networks.list()[0] + self.nova('boot', + params="--flavor %s --image %s %s --nic net-id=%s --poll" % + (self.flavor.name, self.image.name, name, network.id)) # Be nice about cleaning up, however, use the API for this to avoid # parsing text. From 9ed9ab68f7d2fb98c505afc3a1079a72f15c959c Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Wed, 15 Jul 2015 23:51:14 +0300 Subject: [PATCH 0795/1705] Add version discover and check in CLI This patch adds version discovery when user request with latest version. The latest version means the most recently version between server and client. Related to bp api-microversion-support Co-Authored-By: Alex Xu Change-Id: I0b7a95e77371aef6e349bf54b5dd6ce6c6d2732c --- novaclient/__init__.py | 5 + novaclient/api_versions.py | 70 ++++++++++++ novaclient/shell.py | 123 ++++++++++++++------- novaclient/tests/unit/test_api_versions.py | 89 ++++++++++++++- novaclient/tests/unit/test_shell.py | 88 +++++++++++++++ novaclient/tests/unit/v2/fakes.py | 7 ++ 6 files changed, 343 insertions(+), 39 deletions(-) diff --git a/novaclient/__init__.py b/novaclient/__init__.py index bfa75532f..43e5f2567 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -14,5 +14,10 @@ import pbr.version +from novaclient import api_versions + __version__ = pbr.version.VersionInfo('python-novaclient').version_string() + +API_MIN_VERSION = api_versions.APIVersion("2.1") +API_MAX_VERSION = api_versions.APIVersion("2.1") diff --git a/novaclient/api_versions.py b/novaclient/api_versions.py index e50edea2a..ba4a56db8 100644 --- a/novaclient/api_versions.py +++ b/novaclient/api_versions.py @@ -19,6 +19,7 @@ from oslo_utils import strutils +import novaclient from novaclient import exceptions from novaclient.i18n import _, _LW from novaclient import utils @@ -229,6 +230,75 @@ def get_api_version(version_string): return api_version +def _get_server_version_range(client): + version = client.versions.get_current() + + if not hasattr(version, 'version') or not version.version: + return APIVersion(), APIVersion() + + return APIVersion(version.min_version), APIVersion(version.version) + + +def discover_version(client, requested_version): + """Returns latest version supported by both API and client. + + :param client: client object + :returns: APIVersion + """ + + server_start_version, server_end_version = _get_server_version_range( + client) + + if (not requested_version.is_latest() and + requested_version != APIVersion('2.0')): + if server_start_version.is_null() and server_end_version.is_null(): + raise exceptions.UnsupportedVersion( + _("Server doesn't support microversions")) + if not requested_version.matches(server_start_version, + server_end_version): + raise exceptions.UnsupportedVersion( + _("The specified version isn't supported by server. The valid " + "version range is '%(min)s' to '%(max)s'") % { + "min": server_start_version.get_string(), + "max": server_end_version.get_string()}) + return requested_version + + if requested_version == APIVersion('2.0'): + if (server_start_version == APIVersion('2.1') or + (server_start_version.is_null() and + server_end_version.is_null())): + return APIVersion('2.0') + else: + raise exceptions.UnsupportedVersion( + _("The server isn't backward compatible with Nova V2 REST " + "API")) + + if server_start_version.is_null() and server_end_version.is_null(): + return APIVersion('2.0') + elif novaclient.API_MIN_VERSION > server_end_version: + raise exceptions.UnsupportedVersion( + _("Server version is too old. The client valid version range is " + "'%(client_min)s' to '%(client_max)s'. The server valid version " + "range is '%(server_min)s' to '%(server_max)s'.") % { + 'client_min': novaclient.API_MIN_VERSION.get_string(), + 'client_max': novaclient.API_MAX_VERSION.get_string(), + 'server_min': server_start_version.get_string(), + 'server_max': server_end_version.get_string()}) + elif novaclient.API_MAX_VERSION < server_start_version: + raise exceptions.UnsupportedVersion( + _("Server version is too new. The client valid version range is " + "'%(client_min)s' to '%(client_max)s'. The server valid version " + "range is '%(server_min)s' to '%(server_max)s'.") % { + 'client_min': novaclient.API_MIN_VERSION.get_string(), + 'client_max': novaclient.API_MAX_VERSION.get_string(), + 'server_min': server_start_version.get_string(), + 'server_max': server_end_version.get_string()}) + elif novaclient.API_MAX_VERSION <= server_end_version: + return novaclient.API_MAX_VERSION + elif server_end_version < novaclient.API_MAX_VERSION: + return server_end_version + + def update_headers(headers, api_version): """Set 'X-OpenStack-Nova-API-Version' header if api_version is not null""" diff --git a/novaclient/shell.py b/novaclient/shell.py index acda8bf04..42fcbdcda 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -403,8 +403,8 @@ def get_base_parser(self): metavar='', default=cliutils.env('OS_COMPUTE_API_VERSION', default=DEFAULT_OS_COMPUTE_API_VERSION), - help=_('Accepts X, X.Y (where X is major and Y is minor part), ' - 'defaults to env[OS_COMPUTE_API_VERSION].')) + help=_('Accepts X, X.Y (where X is major and Y is minor part) or ' + '"X.latest", defaults to env[OS_COMPUTE_API_VERSION].')) parser.add_argument( '--os_compute_api_version', help=argparse.SUPPRESS) @@ -547,18 +547,6 @@ def _get_keystone_auth(self, session, auth_url, **kwargs): def main(self, argv): # Parse args once to find version and debug settings parser = self.get_base_parser() - (options, args) = parser.parse_known_args(argv) - self.setup_debugging(options.debug) - - # Discover available auth plugins - novaclient.auth_plugin.discover_auth_systems() - - api_version = api_versions.get_api_version( - options.os_compute_api_version) - - # build available subcommands based on version - self.extensions = self._discover_extensions(api_version) - self._run_extension_hooks('__pre_parse_args__') # NOTE(dtroyer): Hackery to handle --endpoint_type due to argparse # thinking usage-list --end is ambiguous; but it @@ -568,24 +556,17 @@ def main(self, argv): spot = argv.index('--endpoint_type') argv[spot] = '--endpoint-type' - subcommand_parser = self.get_subcommand_parser( - api_version, do_help=("help" in args)) - self.parser = subcommand_parser + (args, args_list) = parser.parse_known_args(argv) - if options.help or not argv: - subcommand_parser.print_help() - return 0 + self.setup_debugging(args.debug) + self.extensions = [] + do_help = ('help' in argv) or not argv - args = subcommand_parser.parse_args(argv) - self._run_extension_hooks('__post_parse_args__', args) + # Discover available auth plugins + novaclient.auth_plugin.discover_auth_systems() - # Short-circuit and deal with help right away. - if args.func == self.do_help: - self.do_help(args) - return 0 - elif args.func == self.do_bash_completion: - self.do_bash_completion(args) - return 0 + api_version = api_versions.get_api_version( + args.os_compute_api_version) os_username = args.os_username os_user_id = args.os_user_id @@ -631,13 +612,13 @@ def main(self, argv): endpoint_type += 'URL' if not service_type: - service_type = (cliutils.get_service_type(args.func) or - DEFAULT_NOVA_SERVICE_TYPE) + # Note(alex_xu): We need discover version first, so if there isn't + # service type specified, we use default nova service type. + service_type = DEFAULT_NOVA_SERVICE_TYPE # If we have an auth token but no management_url, we must auth anyway. # Expired tokens are handled by client.py:_cs_request - must_auth = not (cliutils.isunauthenticated(args.func) - or (auth_token and management_url)) + must_auth = not (auth_token and management_url) # Do not use Keystone session for cases with no session support. The # presence of auth_plugin means os_auth_system is present and is not @@ -648,7 +629,7 @@ def main(self, argv): # FIXME(usrleon): Here should be restrict for project id same as # for os_username or os_password but for compatibility it is not. - if must_auth: + if must_auth and not do_help: if auth_plugin: auth_plugin.parse_opts(args) @@ -702,8 +683,8 @@ def main(self, argv): project_domain_id=args.os_project_domain_id, project_domain_name=args.os_project_domain_name) - if not any([args.os_tenant_id, args.os_tenant_name, - args.os_project_id, args.os_project_name]): + if not do_help and not any([args.os_tenant_id, args.os_tenant_name, + args.os_project_id, args.os_project_name]): raise exc.CommandError(_("You must provide a project name or" " project id via --os-project-name," " --os-project-id, env[OS_PROJECT_ID]" @@ -711,11 +692,77 @@ def main(self, argv): " use os-project and os-tenant" " interchangeably.")) - if not os_auth_url: + if not os_auth_url and not do_help: raise exc.CommandError( _("You must provide an auth url " "via either --os-auth-url or env[OS_AUTH_URL]")) + # This client is just used to discover api version. Version API needn't + # microversion, so we just pass version 2 at here. + self.cs = client.Client( + api_versions.APIVersion("2.0"), + os_username, os_password, os_tenant_name, + tenant_id=os_tenant_id, user_id=os_user_id, + auth_url=os_auth_url, insecure=insecure, + region_name=os_region_name, endpoint_type=endpoint_type, + extensions=self.extensions, service_type=service_type, + service_name=service_name, auth_system=os_auth_system, + auth_plugin=auth_plugin, auth_token=auth_token, + volume_service_name=volume_service_name, + timings=args.timings, bypass_url=bypass_url, + os_cache=os_cache, http_log_debug=args.debug, + cacert=cacert, timeout=timeout, + session=keystone_session, auth=keystone_auth) + + if not do_help: + if not api_version.is_latest(): + if api_version > api_versions.APIVersion("2.0"): + if not api_version.matches(novaclient.API_MIN_VERSION, + novaclient.API_MAX_VERSION): + raise exc.CommandError( + _("The specified version isn't supported by " + "client. The valid version range is '%(min)s' " + "to '%(max)s'") % { + "min": novaclient.API_MIN_VERSION.get_string(), + "max": novaclient.API_MAX_VERSION.get_string()} + ) + api_version = api_versions.discover_version(self.cs, api_version) + + # build available subcommands based on version + self.extensions = self._discover_extensions(api_version) + self._run_extension_hooks('__pre_parse_args__') + + subcommand_parser = self.get_subcommand_parser( + api_version, do_help=do_help) + self.parser = subcommand_parser + + if args.help or not argv: + subcommand_parser.print_help() + return 0 + + args = subcommand_parser.parse_args(argv) + self._run_extension_hooks('__post_parse_args__', args) + + # Short-circuit and deal with help right away. + if args.func == self.do_help: + self.do_help(args) + return 0 + elif args.func == self.do_bash_completion: + self.do_bash_completion(args) + return 0 + + if not args.service_type: + service_type = (cliutils.get_service_type(args.func) or + DEFAULT_NOVA_SERVICE_TYPE) + + if cliutils.isunauthenticated(args.func): + # NOTE(alex_xu): We need authentication for discover microversion. + # But the subcommands may needn't it. If the subcommand needn't, + # we clear the session arguements. + keystone_session = None + keystone_auth = None + + # Recreate client object with discovered version. self.cs = client.Client( api_version, os_username, os_password, os_tenant_name, @@ -727,7 +774,7 @@ def main(self, argv): auth_plugin=auth_plugin, auth_token=auth_token, volume_service_name=volume_service_name, timings=args.timings, bypass_url=bypass_url, - os_cache=os_cache, http_log_debug=options.debug, + os_cache=os_cache, http_log_debug=args.debug, cacert=cacert, timeout=timeout, session=keystone_session, auth=keystone_auth) diff --git a/novaclient/tests/unit/test_api_versions.py b/novaclient/tests/unit/test_api_versions.py index 2db3a8859..e3c204de4 100644 --- a/novaclient/tests/unit/test_api_versions.py +++ b/novaclient/tests/unit/test_api_versions.py @@ -1,4 +1,4 @@ -# Copyright 2015 Mirantis +# Copyright 2016 Mirantis # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -15,9 +15,11 @@ import mock +import novaclient from novaclient import api_versions from novaclient import exceptions from novaclient.tests.unit import utils +from novaclient.v2 import versions class APIVersionTestCase(utils.TestCase): @@ -247,3 +249,88 @@ def some_func(*args, **kwargs): some_func(obj, *some_args, **some_kwargs) checker.assert_called_once_with(*((obj,) + some_args), **some_kwargs) + + +class DiscoverVersionTestCase(utils.TestCase): + def setUp(self): + super(DiscoverVersionTestCase, self).setUp() + self.orig_max = novaclient.API_MAX_VERSION + self.orig_min = novaclient.API_MIN_VERSION + self.addCleanup(self._clear_fake_version) + + def _clear_fake_version(self): + novaclient.API_MAX_VERSION = self.orig_max + novaclient.API_MIN_VERSION = self.orig_min + + def test_server_is_too_new(self): + fake_client = mock.MagicMock() + fake_client.versions.get_current.return_value = mock.MagicMock( + version="2.7", min_version="2.4") + novaclient.API_MAX_VERSION = api_versions.APIVersion("2.3") + novaclient.API_MIN_VERSION = api_versions.APIVersion("2.1") + self.assertRaises(exceptions.UnsupportedVersion, + api_versions.discover_version, fake_client, + api_versions.APIVersion('2.latest')) + + def test_server_is_too_old(self): + fake_client = mock.MagicMock() + fake_client.versions.get_current.return_value = mock.MagicMock( + version="2.7", min_version="2.4") + novaclient.API_MAX_VERSION = api_versions.APIVersion("2.10") + novaclient.API_MIN_VERSION = api_versions.APIVersion("2.9") + + self.assertRaises(exceptions.UnsupportedVersion, + api_versions.discover_version, fake_client, + api_versions.APIVersion('2.latest')) + + def test_server_end_version_is_the_latest_one(self): + fake_client = mock.MagicMock() + fake_client.versions.get_current.return_value = mock.MagicMock( + version="2.7", min_version="2.4") + novaclient.API_MAX_VERSION = api_versions.APIVersion("2.11") + novaclient.API_MIN_VERSION = api_versions.APIVersion("2.1") + + self.assertEqual( + "2.7", + api_versions.discover_version( + fake_client, + api_versions.APIVersion('2.latest')).get_string()) + + def test_client_end_version_is_the_latest_one(self): + fake_client = mock.MagicMock() + fake_client.versions.get_current.return_value = mock.MagicMock( + version="2.16", min_version="2.4") + novaclient.API_MAX_VERSION = api_versions.APIVersion("2.11") + novaclient.API_MIN_VERSION = api_versions.APIVersion("2.1") + + self.assertEqual( + "2.11", + api_versions.discover_version( + fake_client, + api_versions.APIVersion('2.latest')).get_string()) + + def test_server_without_microversion(self): + fake_client = mock.MagicMock() + fake_client.versions.get_current.return_value = mock.MagicMock( + version='', min_version='') + novaclient.API_MAX_VERSION = api_versions.APIVersion("2.11") + novaclient.API_MIN_VERSION = api_versions.APIVersion("2.1") + + self.assertEqual( + "2.0", + api_versions.discover_version( + fake_client, + api_versions.APIVersion('2.latest')).get_string()) + + def test_server_without_microversion_and_no_version_field(self): + fake_client = mock.MagicMock() + fake_client.versions.get_current.return_value = versions.Version( + None, {}) + novaclient.API_MAX_VERSION = api_versions.APIVersion("2.11") + novaclient.API_MIN_VERSION = api_versions.APIVersion("2.1") + + self.assertEqual( + "2.0", + api_versions.discover_version( + fake_client, + api_versions.APIVersion('2.latest')).get_string()) diff --git a/novaclient/tests/unit/test_shell.py b/novaclient/tests/unit/test_shell.py index 1caa56cbd..2209f6996 100644 --- a/novaclient/tests/unit/test_shell.py +++ b/novaclient/tests/unit/test_shell.py @@ -104,6 +104,19 @@ def setUp(self): self.nc_util = mock.patch( 'novaclient.openstack.common.cliutils.isunauthenticated').start() self.nc_util.return_value = False + self.mock_server_version_range = mock.patch( + 'novaclient.api_versions._get_server_version_range').start() + self.mock_server_version_range.return_value = ( + novaclient.API_MIN_VERSION, + novaclient.API_MIN_VERSION) + self.orig_max_ver = novaclient.API_MAX_VERSION + self.orig_min_ver = novaclient.API_MIN_VERSION + self.addCleanup(self._clear_fake_version) + self.addCleanup(mock.patch.stopall) + + def _clear_fake_version(self): + novaclient.API_MAX_VERSION = self.orig_max_ver + novaclient.API_MIN_VERSION = self.orig_min_ver def shell(self, argstr, exitcodes=(0,)): orig = sys.stdout @@ -168,6 +181,7 @@ def test_help_on_subcommand(self): matchers.MatchesRegex(r, re.DOTALL | re.MULTILINE)) def test_help_no_options(self): + self.make_env() required = [ '.*?^usage: ', '.*?^\s+root-password\s+Change the admin password', @@ -179,6 +193,7 @@ def test_help_no_options(self): matchers.MatchesRegex(r, re.DOTALL | re.MULTILINE)) def test_bash_completion(self): + self.make_env() stdout, stderr = self.shell('bash-completion') # just check we have some output required = [ @@ -403,6 +418,79 @@ def test_keyring_saver_helper(self, mock_client, keyring_saver = mock_client_instance.client.keyring_saver self.assertIsInstance(keyring_saver, novaclient.shell.SecretsHelper) + @mock.patch('novaclient.client.Client') + def test_microversion_with_latest(self, mock_client): + self.make_env() + novaclient.API_MAX_VERSION = api_versions.APIVersion('2.3') + self.mock_server_version_range.return_value = ( + api_versions.APIVersion("2.1"), api_versions.APIVersion("2.3")) + self.shell('--os-compute-api-version 2.latest list') + client_args = mock_client.call_args_list[1][0] + self.assertEqual(api_versions.APIVersion("2.3"), client_args[0]) + + @mock.patch('novaclient.client.Client') + def test_microversion_with_specified_version(self, mock_client): + self.make_env() + self.mock_server_version_range.return_value = ( + api_versions.APIVersion("2.10"), api_versions.APIVersion("2.100")) + novaclient.API_MAX_VERSION = api_versions.APIVersion("2.100") + novaclient.API_MIN_VERSION = api_versions.APIVersion("2.90") + self.shell('--os-compute-api-version 2.99 list') + client_args = mock_client.call_args_list[1][0] + self.assertEqual(api_versions.APIVersion("2.99"), client_args[0]) + + @mock.patch('novaclient.client.Client') + def test_microversion_with_specified_version_out_of_range(self, + mock_client): + novaclient.API_MAX_VERSION = api_versions.APIVersion("2.100") + novaclient.API_MIN_VERSION = api_versions.APIVersion("2.90") + self.assertRaises(exceptions.CommandError, + self.shell, '--os-compute-api-version 2.199 list') + + @mock.patch('novaclient.client.Client') + def test_microversion_with_v2_and_v2_1_server(self, mock_client): + self.make_env() + self.mock_server_version_range.return_value = ( + api_versions.APIVersion('2.1'), api_versions.APIVersion('2.3')) + novaclient.API_MAX_VERSION = api_versions.APIVersion("2.100") + novaclient.API_MIN_VERSION = api_versions.APIVersion("2.1") + self.shell('--os-compute-api-version 2 list') + client_args = mock_client.call_args_list[1][0] + self.assertEqual(api_versions.APIVersion("2.0"), client_args[0]) + + @mock.patch('novaclient.client.Client') + def test_microversion_with_v2_and_v2_server(self, mock_client): + self.make_env() + self.mock_server_version_range.return_value = ( + api_versions.APIVersion(), api_versions.APIVersion()) + novaclient.API_MAX_VERSION = api_versions.APIVersion("2.100") + novaclient.API_MIN_VERSION = api_versions.APIVersion("2.1") + self.shell('--os-compute-api-version 2 list') + client_args = mock_client.call_args_list[1][0] + self.assertEqual(api_versions.APIVersion("2.0"), client_args[0]) + + @mock.patch('novaclient.client.Client') + def test_microversion_with_v2_without_server_compatible(self, mock_client): + self.make_env() + self.mock_server_version_range.return_value = ( + api_versions.APIVersion('2.2'), api_versions.APIVersion('2.3')) + novaclient.API_MAX_VERSION = api_versions.APIVersion("2.100") + novaclient.API_MIN_VERSION = api_versions.APIVersion("2.1") + self.assertRaises( + exceptions.UnsupportedVersion, + self.shell, '--os-compute-api-version 2 list') + + def test_microversion_with_specific_version_without_microversions(self): + self.make_env() + self.mock_server_version_range.return_value = ( + api_versions.APIVersion(), api_versions.APIVersion()) + novaclient.API_MAX_VERSION = api_versions.APIVersion("2.100") + novaclient.API_MIN_VERSION = api_versions.APIVersion("2.1") + self.assertRaises( + exceptions.UnsupportedVersion, + self.shell, + '--os-compute-api-version 2.3 list') + class TestLoadVersionedActions(utils.TestCase): diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index f468e3e48..9433f6799 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -2260,7 +2260,14 @@ def __init__(self, *args, **kwargs): self.callstack = [] self.auth = mock.Mock() self.session = mock.Mock() + self.session.get_endpoint.return_value = FakeHTTPClient.get_endpoint( + self) self.service_type = 'service_type' + self.service_name = None + self.endpoint_override = None + self.interface = None + self.region_name = None + self.version = None self.auth.get_auth_ref.return_value.project_id = 'tenant_id' From 02889515a5db5ea43a7aa2bc76ffa70248861ce9 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Thu, 16 Jul 2015 00:00:17 +0300 Subject: [PATCH 0796/1705] Set "latest" as default compute api version It would be nice to use "latest" version of api by default. Related to bp api-microversion-support Change-Id: Ife981c87d32011f01fcbdf871071a9d8273ebb92 --- novaclient/shell.py | 2 +- novaclient/tests/unit/test_shell.py | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/novaclient/shell.py b/novaclient/shell.py index 42fcbdcda..c3e8c17c4 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -51,7 +51,7 @@ from novaclient.openstack.common import cliutils from novaclient import utils -DEFAULT_OS_COMPUTE_API_VERSION = "2" +DEFAULT_OS_COMPUTE_API_VERSION = "2.latest" DEFAULT_NOVA_ENDPOINT_TYPE = 'publicURL' DEFAULT_NOVA_SERVICE_TYPE = "compute" diff --git a/novaclient/tests/unit/test_shell.py b/novaclient/tests/unit/test_shell.py index 2209f6996..29cb110b7 100644 --- a/novaclient/tests/unit/test_shell.py +++ b/novaclient/tests/unit/test_shell.py @@ -33,26 +33,30 @@ FAKE_ENV = {'OS_USERNAME': 'username', 'OS_PASSWORD': 'password', 'OS_TENANT_NAME': 'tenant_name', - 'OS_AUTH_URL': 'http://no.where/v2.0'} + 'OS_AUTH_URL': 'http://no.where/v2.0', + 'OS_COMPUTE_API_VERSION': '2'} FAKE_ENV2 = {'OS_USER_ID': 'user_id', 'OS_PASSWORD': 'password', 'OS_TENANT_ID': 'tenant_id', - 'OS_AUTH_URL': 'http://no.where/v2.0'} + 'OS_AUTH_URL': 'http://no.where/v2.0', + 'OS_COMPUTE_API_VERSION': '2'} FAKE_ENV3 = {'OS_USER_ID': 'user_id', 'OS_PASSWORD': 'password', 'OS_TENANT_ID': 'tenant_id', 'OS_AUTH_URL': 'http://no.where/v2.0', 'NOVA_ENDPOINT_TYPE': 'novaURL', - 'OS_ENDPOINT_TYPE': 'osURL'} + 'OS_ENDPOINT_TYPE': 'osURL', + 'OS_COMPUTE_API_VERSION': '2'} FAKE_ENV4 = {'OS_USER_ID': 'user_id', 'OS_PASSWORD': 'password', 'OS_TENANT_ID': 'tenant_id', 'OS_AUTH_URL': 'http://no.where/v2.0', 'NOVA_ENDPOINT_TYPE': 'internal', - 'OS_ENDPOINT_TYPE': 'osURL'} + 'OS_ENDPOINT_TYPE': 'osURL', + 'OS_COMPUTE_API_VERSION': '2'} def _create_ver_list(versions): From 4a812d953b27d3481fbe9d6af8723008f2060f94 Mon Sep 17 00:00:00 2001 From: Claudiu Belu Date: Thu, 26 Jun 2014 09:30:10 -0400 Subject: [PATCH 0797/1705] Adds support for x509 certificates as keypairs Adds new parameter --key-type for novaclient for creating x509 certificates as keypairs. If no --key-type is specified, a ssh keypair is created, for backwards compatibility. Adds 'Type' column for keypair-list, displaying the keypair type. This commit will have to merge after: https://review.openstack.org/#/c/140313 Depends-On: I215662f2f92a01921a866c3218031787a9eaf915 Implements: blueprint keypair-x509-certificates Co-Authored-By: Andrey Kurilin Co-Authored-By: Alex Xu Change-Id: I12bb13e24b660ffb6da0e5be275acbba7453d011 --- novaclient/__init__.py | 2 +- novaclient/tests/functional/fake_crypto.py | 49 ++++++++ novaclient/tests/functional/test_keypairs.py | 125 +++++++++++++++++++ novaclient/tests/unit/v2/test_keypairs.py | 46 ++++++- novaclient/tests/unit/v2/test_shell.py | 67 +++++++--- novaclient/v2/keypairs.py | 17 +++ novaclient/v2/shell.py | 32 ++++- 7 files changed, 315 insertions(+), 23 deletions(-) create mode 100644 novaclient/tests/functional/fake_crypto.py create mode 100644 novaclient/tests/functional/test_keypairs.py diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 43e5f2567..a52cedc8a 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -20,4 +20,4 @@ __version__ = pbr.version.VersionInfo('python-novaclient').version_string() API_MIN_VERSION = api_versions.APIVersion("2.1") -API_MAX_VERSION = api_versions.APIVersion("2.1") +API_MAX_VERSION = api_versions.APIVersion("2.2") diff --git a/novaclient/tests/functional/fake_crypto.py b/novaclient/tests/functional/fake_crypto.py new file mode 100644 index 000000000..56df5151a --- /dev/null +++ b/novaclient/tests/functional/fake_crypto.py @@ -0,0 +1,49 @@ +# Copyright 2015 Cloudbase Solutions +# All Rights Reserved. +# +# 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. + + +def get_x509_cert_and_fingerprint(): + fingerprint = "a1:6f:6d:ea:a6:36:d0:3a:c6:eb:b6:ee:07:94:3e:2a:90:98:2b:c9" + certif = ( + "-----BEGIN CERTIFICATE-----\n" + "MIIDIjCCAgqgAwIBAgIJAIE8EtWfZhhFMA0GCSqGSIb3DQEBCwUAMCQxIjAgBgNV\n" + "BAMTGWNsb3VkYmFzZS1pbml0LXVzZXItMTM1NTkwHhcNMTUwMTI5MTgyMzE4WhcN\n" + "MjUwMTI2MTgyMzE4WjAkMSIwIAYDVQQDExljbG91ZGJhc2UtaW5pdC11c2VyLTEz\n" + "NTU5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv4lv95ofkXLIbALU\n" + "UEb1f949TYNMUvMGNnLyLgGOY+D61TNG7RZn85cRg9GVJ7KDjSLN3e3LwH5rgv5q\n" + "pU+nM/idSMhG0CQ1lZeExTsMEJVT3bG7LoU5uJ2fJSf5+hA0oih2M7/Kap5ggHgF\n" + "h+h8MWvDC9Ih8x1aadkk/OEmJsTrziYm0C/V/FXPHEuXfZn8uDNKZ/tbyfI6hwEj\n" + "nLz5Zjgg29n6tIPYMrnLNDHScCwtNZOcnixmWzsxCt1bxsAEA/y9gXUT7xWUf52t\n" + "2+DGQbLYxo0PHjnPf3YnFXNavfTt+4c7ZdHhOQ6ZA8FGQ2LJHDHM1r2/8lK4ld2V\n" + "qgNTcQIDAQABo1cwVTATBgNVHSUEDDAKBggrBgEFBQcDAjA+BgNVHREENzA1oDMG\n" + "CisGAQQBgjcUAgOgJQwjY2xvdWRiYXNlLWluaXQtdXNlci0xMzU1OUBsb2NhbGhv\n" + "c3QwDQYJKoZIhvcNAQELBQADggEBAHHX/ZUOMR0ZggQnfXuXLIHWlffVxxLOV/bE\n" + "7JC/dtedHqi9iw6sRT5R6G1pJo0xKWr2yJVDH6nC7pfxCFkby0WgVuTjiu6iNRg2\n" + "4zNJd8TGrTU+Mst+PPJFgsxrAY6vjwiaUtvZ/k8PsphHXu4ON+oLurtVDVgog7Vm\n" + "fQCShx434OeJj1u8pb7o2WyYS5nDVrHBhlCAqVf2JPKu9zY+i9gOG2kimJwH7fJD\n" + "xXpMIwAQ+flwlHR7OrE0L8TNcWwKPRAY4EPcXrT+cWo1k6aTqZDSK54ygW2iWtni\n" + "ZBcstxwcB4GIwnp1DrPW9L2gw5eLe1Sl6wdz443TW8K/KPV9rWQ=\n" + "-----END CERTIFICATE-----\n") + return certif, fingerprint + + +def get_ssh_pub_key_and_fingerprint(): + fingerprint = "1e:2c:9b:56:79:4b:45:77:f9:ca:7a:98:2c:b0:d5:3c" + public_key = ("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDx8nkQv/zgGg" + "B4rMYmIf+6A4l6Rr+o/6lHBQdW5aYd44bd8JttDCE/F/pNRr0l" + "RE+PiqSPO8nDPHw0010JeMH9gYgnnFlyY3/OcJ02RhIPyyxYpv" + "9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYc" + "pSxsIbECHw== Generated-by-Nova") + return public_key, fingerprint diff --git a/novaclient/tests/functional/test_keypairs.py b/novaclient/tests/functional/test_keypairs.py new file mode 100644 index 000000000..02bf3ecd2 --- /dev/null +++ b/novaclient/tests/functional/test_keypairs.py @@ -0,0 +1,125 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import tempfile +import uuid + +from tempest_lib import exceptions + +from novaclient.tests.functional import base +from novaclient.tests.functional import fake_crypto + + +class TestKeypairsNovaClient(base.ClientTestBase): + """Keypairs functional tests. + """ + + def _serialize_kwargs(self, kwargs): + kwargs_pairs = ['--%(key)s %(val)s' % {'key': key.replace('_', '-'), + 'val': val} + for key, val in kwargs.items()] + return " ".join(kwargs_pairs) + + def _create_keypair(self, **kwargs): + key_name = self._raw_create_keypair(**kwargs) + self.addCleanup(self.nova, 'keypair-delete %s' % key_name) + return key_name + + def _raw_create_keypair(self, **kwargs): + key_name = 'keypair-' + str(uuid.uuid4()) + kwargs_str = self._serialize_kwargs(kwargs) + self.nova('keypair-add %s %s' % (kwargs_str, key_name)) + return key_name + + def _show_keypair(self, key_name): + return self.nova('keypair-show %s' % key_name) + + def _list_keypairs(self): + return self.nova('keypair-list') + + def _delete_keypair(self, key_name): + self.nova('keypair-delete %s' % key_name) + + def _create_public_key_file(self, public_key): + pubfile = tempfile.mkstemp()[1] + with open(pubfile, 'w') as f: + f.write(public_key) + return pubfile + + def test_create_keypair(self): + key_name = self._create_keypair() + keypair = self._show_keypair(key_name) + self.assertIn(key_name, keypair) + + return keypair + + def _test_import_keypair(self, fingerprint, **create_kwargs): + key_name = self._create_keypair(**create_kwargs) + keypair = self._show_keypair(key_name) + self.assertIn(key_name, keypair) + self.assertIn(fingerprint, keypair) + + return keypair + + def test_import_keypair(self): + pub_key, fingerprint = fake_crypto.get_ssh_pub_key_and_fingerprint() + pub_key_file = self._create_public_key_file(pub_key) + self._test_import_keypair(fingerprint, pub_key=pub_key_file) + + def test_list_keypair(self): + key_name = self._create_keypair() + keypairs = self._list_keypairs() + self.assertIn(key_name, keypairs) + + def test_delete_keypair(self): + key_name = self._raw_create_keypair() + keypair = self._show_keypair(key_name) + self.assertIsNotNone(keypair) + + self._delete_keypair(key_name) + + # keypair-show should fail if no keypair with given name is found. + self.assertRaises(exceptions.CommandFailed, + self._show_keypair, key_name) + + +class TestKeypairsNovaClientV22(TestKeypairsNovaClient): + """Keypairs functional tests for v2.2 nova-api microversion. + """ + + def nova(self, *args, **kwargs): + return self.cli_clients.nova(flags='--os-compute-api-version 2.2 ' + '--service-type computev21', + *args, **kwargs) + + def test_create_keypair(self): + keypair = super(TestKeypairsNovaClientV22, self).test_create_keypair() + self.assertIn('ssh', keypair) + + def test_create_keypair_x509(self): + key_name = self._create_keypair(key_type='x509') + keypair = self._show_keypair(key_name) + self.assertIn(key_name, keypair) + self.assertIn('x509', keypair) + + def test_import_keypair(self): + pub_key, fingerprint = fake_crypto.get_ssh_pub_key_and_fingerprint() + pub_key_file = self._create_public_key_file(pub_key) + keypair = self._test_import_keypair(fingerprint, pub_key=pub_key_file) + self.assertIn('ssh', keypair) + + def test_import_keypair_x509(self): + certif, fingerprint = fake_crypto.get_x509_cert_and_fingerprint() + pub_key_file = self._create_public_key_file(certif) + keypair = self._test_import_keypair(fingerprint, key_type='x509', + pub_key=pub_key_file) + self.assertIn('x509', keypair) diff --git a/novaclient/tests/unit/v2/test_keypairs.py b/novaclient/tests/unit/v2/test_keypairs.py index ac3602f69..109d350a3 100644 --- a/novaclient/tests/unit/v2/test_keypairs.py +++ b/novaclient/tests/unit/v2/test_keypairs.py @@ -11,6 +11,7 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient import api_versions from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit.fixture_data import keypairs as data from novaclient.tests.unit import utils @@ -54,12 +55,49 @@ def test_delete_keypair(self): self.cs.keypairs.delete(kp) self.assert_called('DELETE', '/%s/test' % self.keypair_prefix) + +class KeypairsV2TestCase(KeypairsTest): + def setUp(self): + super(KeypairsV2TestCase, self).setUp() + self.cs.api_version = api_versions.APIVersion("2.0") + + def test_create_keypair(self): + name = "foo" + kp = self.cs.keypairs.create(name) + self.assert_called('POST', '/%s' % self.keypair_prefix, + body={'keypair': {'name': name}}) + self.assertIsInstance(kp, keypairs.Keypair) + + def test_import_keypair(self): + name = "foo" + pub_key = "fake-public-key" + kp = self.cs.keypairs.create(name, pub_key) + self.assert_called('POST', '/%s' % self.keypair_prefix, + body={'keypair': {'name': name, + 'public_key': pub_key}}) + self.assertIsInstance(kp, keypairs.Keypair) + + +class KeypairsV22TestCase(KeypairsTest): + def setUp(self): + super(KeypairsV22TestCase, self).setUp() + self.cs.api_version = api_versions.APIVersion("2.2") + def test_create_keypair(self): - kp = self.cs.keypairs.create("foo") - self.assert_called('POST', '/%s' % self.keypair_prefix) + name = "foo" + key_type = "some_type" + kp = self.cs.keypairs.create(name, key_type=key_type) + self.assert_called('POST', '/%s' % self.keypair_prefix, + body={'keypair': {'name': name, + 'type': key_type}}) self.assertIsInstance(kp, keypairs.Keypair) def test_import_keypair(self): - kp = self.cs.keypairs.create("foo", "fake-public-key") - self.assert_called('POST', '/%s' % self.keypair_prefix) + name = "foo" + pub_key = "fake-public-key" + kp = self.cs.keypairs.create(name, pub_key) + self.assert_called('POST', '/%s' % self.keypair_prefix, + body={'keypair': {'name': name, + 'public_key': pub_key, + 'type': 'ssh'}}) self.assertIsInstance(kp, keypairs.Keypair) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 1a540ee22..4d83464e0 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -75,11 +75,15 @@ def setUp(self): @mock.patch('sys.stdout', new_callable=six.StringIO) @mock.patch('sys.stderr', new_callable=six.StringIO) - def run_command(self, cmd, mock_stderr, mock_stdout): + def run_command(self, cmd, mock_stderr, mock_stdout, api_version=None): + version_options = [] + if api_version: + version_options.extend(["--os-compute-api-version", api_version, + "--service-type", "computev21"]) if isinstance(cmd, list): - self.shell.main(cmd) + self.shell.main(version_options + cmd) else: - self.shell.main(cmd.split()) + self.shell.main(version_options + cmd.split()) return mock_stdout.getvalue(), mock_stderr.getvalue() def assert_called(self, method, url, body=None, **kwargs): @@ -2361,28 +2365,61 @@ class FakeResources(object): self.run_command, "ssh --ipv6 --network nonexistent server") - def test_keypair_add(self): - self.run_command('keypair-add test') - self.assert_called('POST', '/os-keypairs', - {'keypair': - {'name': 'test'}}) + def _check_keypair_add(self, expected_key_type=None, extra_args='', + api_version=None): + self.run_command("keypair-add %s test" % extra_args, + api_version=api_version) + expected_body = {"keypair": {"name": "test"}} + if expected_key_type: + expected_body["keypair"]["type"] = expected_key_type + self.assert_called("POST", "/os-keypairs", expected_body) - def test_keypair_import(self): + def test_keypair_add_v20(self): + self._check_keypair_add(api_version="2.0") + + def test_keypair_add_v22(self): + self._check_keypair_add('ssh', api_version="2.2") + + def test_keypair_add_ssh(self): + self._check_keypair_add('ssh', '--key-type ssh', api_version="2.2") + + def test_keypair_add_ssh_x509(self): + self._check_keypair_add('x509', '--key-type x509', api_version="2.2") + + def _check_keypair_import(self, expected_key_type=None, extra_args='', + api_version=None): with mock.patch.object(builtins, 'open', mock.mock_open(read_data='FAKE_PUBLIC_KEY')): - self.run_command('keypair-add --pub-key test.pub test') + self.run_command('keypair-add --pub-key test.pub %s test' % + extra_args, api_version=api_version) + expected_body = {"keypair": {'public_key': 'FAKE_PUBLIC_KEY', + 'name': 'test'}} + if expected_key_type: + expected_body["keypair"]["type"] = expected_key_type self.assert_called( - 'POST', '/os-keypairs', { - 'keypair': {'public_key': 'FAKE_PUBLIC_KEY', - 'name': 'test'}}) + 'POST', '/os-keypairs', expected_body) + + def test_keypair_import_v20(self): + self._check_keypair_import(api_version="2.0") + + def test_keypair_import_v22(self): + self._check_keypair_import('ssh', api_version="2.2") + + def test_keypair_import_ssh(self): + self._check_keypair_import('ssh', '--key-type ssh', api_version="2.2") + + def test_keypair_import_x509(self): + self._check_keypair_import('x509', '--key-type x509', + api_version="2.2") def test_keypair_stdin(self): with mock.patch('sys.stdin', six.StringIO('FAKE_PUBLIC_KEY')): - self.run_command('keypair-add --pub-key - test') + self.run_command('keypair-add --pub-key - test', api_version="2.2") self.assert_called( 'POST', '/os-keypairs', { 'keypair': - {'public_key': 'FAKE_PUBLIC_KEY', 'name': 'test'}}) + {'public_key': 'FAKE_PUBLIC_KEY', 'name': 'test', + 'type': 'ssh'}}) def test_keypair_list(self): self.run_command('keypair-list') diff --git a/novaclient/v2/keypairs.py b/novaclient/v2/keypairs.py index 96caff618..021960517 100644 --- a/novaclient/v2/keypairs.py +++ b/novaclient/v2/keypairs.py @@ -17,6 +17,7 @@ Keypair interface (1.1 extension). """ +from novaclient import api_versions from novaclient import base @@ -65,6 +66,7 @@ def get(self, keypair): return self._get("/%s/%s" % (self.keypair_prefix, base.getid(keypair)), "keypair") + @api_versions.wraps("2.0", "2.1") def create(self, name, public_key=None): """ Create a keypair @@ -77,6 +79,21 @@ def create(self, name, public_key=None): body['keypair']['public_key'] = public_key return self._create('/%s' % self.keypair_prefix, body, 'keypair') + @api_versions.wraps("2.2") + def create(self, name, public_key=None, key_type="ssh"): + """ + Create a keypair + + :param name: name for the keypair to create + :param public_key: existing public key to import + :param key_type: keypair type to create + """ + body = {'keypair': {'name': name, + 'type': key_type}} + if public_key: + body['keypair']['public_key'] = public_key + return self._create('/%s' % self.keypair_prefix, body, 'keypair') + def delete(self, key): """ Delete a keypair diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index c8ef86bad..a2552655a 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -34,6 +34,7 @@ from oslo_utils import uuidutils import six +from novaclient import api_versions from novaclient import client from novaclient import exceptions from novaclient.i18n import _ @@ -2874,6 +2875,16 @@ def do_secgroup_delete_group_rule(cs, args): raise exceptions.CommandError(_("Rule not found")) +@api_versions.wraps("2.0", "2.1") +def _keypair_create(cs, args, name, pub_key): + return cs.keypairs.create(name, pub_key) + + +@api_versions.wraps("2.2") +def _keypair_create(cs, args, name, pub_key): + return cs.keypairs.create(name, pub_key, key_type=args.key_type) + + @cliutils.arg('name', metavar='', help=_('Name of key.')) @cliutils.arg( '--pub-key', @@ -2883,11 +2894,16 @@ def do_secgroup_delete_group_rule(cs, args): @cliutils.arg( '--pub_key', help=argparse.SUPPRESS) +@cliutils.arg( + '--key-type', + metavar='', + default='ssh', + help=_('Keypair type. Can be ssh or x509.'), + start_version="2.2") def do_keypair_add(cs, args): """Create a new key pair for use with servers.""" name = args.name pub_key = args.pub_key - if pub_key: if pub_key == '-': pub_key = sys.stdin.read() @@ -2901,7 +2917,7 @@ def do_keypair_add(cs, args): % {'key': pub_key, 'exc': e} ) - keypair = cs.keypairs.create(name, pub_key) + keypair = _keypair_create(cs, args, name, pub_key) if not pub_key: private_key = keypair.private_key @@ -2915,10 +2931,20 @@ def do_keypair_delete(cs, args): cs.keypairs.delete(name) +@api_versions.wraps("2.0", "2.1") +def _get_keypairs_list_columns(cs, args): + return ['Name', 'Fingerprint'] + + +@api_versions.wraps("2.2") +def _get_keypairs_list_columns(cs, args): + return ['Name', 'Type', 'Fingerprint'] + + def do_keypair_list(cs, args): """Print a list of keypairs for a user""" keypairs = cs.keypairs.list() - columns = ['Name', 'Fingerprint'] + columns = _get_keypairs_list_columns(cs, args) utils.print_list(keypairs, columns) From 39739158b0cf10a775fd3899e35df7f53b4c9336 Mon Sep 17 00:00:00 2001 From: Carlos Goncalves Date: Wed, 27 May 2015 08:32:31 +0200 Subject: [PATCH 0798/1705] Support forcing service down Extending Nova CLI to support forcing service to be set/unset as down, as specified in blueprint mark-host-down. Depends-On: I612582ba7b70bb6d167aa68bdfc47faa3b7b85ed Depends-On: I39f1a84c100726f87a4dc464dd9922d66efdb53f Implements: blueprint support-force-down-service Change-Id: I2b80ac32a95fe80363b4ad95d8d89fff097935a3 --- novaclient/__init__.py | 2 +- novaclient/tests/unit/v2/fakes.py | 6 ++++++ novaclient/tests/unit/v2/test_services.py | 26 +++++++++++++++++++++++ novaclient/v2/services.py | 19 +++++++++++++++++ novaclient/v2/shell.py | 15 +++++++++++++ 5 files changed, 67 insertions(+), 1 deletion(-) diff --git a/novaclient/__init__.py b/novaclient/__init__.py index a52cedc8a..5e6ccb0bb 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -20,4 +20,4 @@ __version__ = pbr.version.VersionInfo('python-novaclient').version_string() API_MIN_VERSION = api_versions.APIVersion("2.1") -API_MAX_VERSION = api_versions.APIVersion("2.2") +API_MAX_VERSION = api_versions.APIVersion("2.11") diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 4a2cf8886..6814461d1 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -1677,6 +1677,12 @@ def put_os_services_disable_log_reason(self, body, **kw): def delete_os_services_1(self, **kw): return (204, {}, None) + def put_os_services_force_down(self, body, **kw): + return (200, {}, {'service': { + 'host': body['host'], + 'binary': body['binary'], + 'forced_down': False}}) + # # Fixed IPs # diff --git a/novaclient/tests/unit/v2/test_services.py b/novaclient/tests/unit/v2/test_services.py index 2724342d4..f7ab553a0 100644 --- a/novaclient/tests/unit/v2/test_services.py +++ b/novaclient/tests/unit/v2/test_services.py @@ -13,6 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient import api_versions from novaclient.tests.unit import utils from novaclient.tests.unit.v2 import fakes from novaclient.v2 import services @@ -97,3 +98,28 @@ def test_services_disable_log_reason(self): self.cs.assert_called('PUT', '/os-services/disable-log-reason', values) self.assertIsInstance(service, self._get_service_type()) self.assertEqual('disabled', service.status) + + +class ServicesV211TestCase(ServicesTest): + def setUp(self): + super(ServicesV211TestCase, self).setUp() + self.cs.api_version = api_versions.APIVersion("2.11") + + def _update_body(self, host, binary, disabled_reason=None, + force_down=None): + body = {"host": host, + "binary": binary} + if disabled_reason is not None: + body["disabled_reason"] = disabled_reason + if force_down is not None: + body["forced_down"] = force_down + return body + + def test_services_force_down(self): + service = self.cs.services.force_down( + 'compute1', 'nova-compute', False) + values = self._update_body("compute1", "nova-compute", + force_down=False) + self.cs.assert_called('PUT', '/os-services/force-down', values) + self.assertIsInstance(service, self._get_service_type()) + self.assertEqual(False, service.forced_down) diff --git a/novaclient/v2/services.py b/novaclient/v2/services.py index d51fa3ebf..fcf800938 100644 --- a/novaclient/v2/services.py +++ b/novaclient/v2/services.py @@ -16,6 +16,7 @@ """ service interface """ +from novaclient import api_versions from novaclient import base @@ -48,6 +49,7 @@ def list(self, host=None, binary=None): url = "%s?%s" % (url, "&".join(filters)) return self._list(url, "services") + @api_versions.wraps("2.0", "2.10") def _update_body(self, host, binary, disabled_reason=None): body = {"host": host, "binary": binary} @@ -55,6 +57,17 @@ def _update_body(self, host, binary, disabled_reason=None): body["disabled_reason"] = disabled_reason return body + @api_versions.wraps("2.11") + def _update_body(self, host, binary, disabled_reason=None, + force_down=None): + body = {"host": host, + "binary": binary} + if disabled_reason is not None: + body["disabled_reason"] = disabled_reason + if force_down is not None: + body["forced_down"] = force_down + return body + def enable(self, host, binary): """Enable the service specified by hostname and binary.""" body = self._update_body(host, binary) @@ -73,3 +86,9 @@ def disable_log_reason(self, host, binary, reason): def delete(self, service_id): """Delete a service.""" return self._delete("/os-services/%s" % service_id) + + @api_versions.wraps("2.11") + def force_down(self, host, binary, force_down=None): + """Force service state to down specified by hostname and binary.""" + body = self._update_body(host, binary, force_down=force_down) + return self._update("/os-services/force-down", body, "service") diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 5ad0014d1..9e840a70d 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -3568,6 +3568,21 @@ def do_service_disable(cs, args): utils.print_list([result], ['Host', 'Binary', 'Status']) +@api_versions.wraps("2.11") +@cliutils.arg('host', metavar='', help=_('Name of host.')) +@cliutils.arg('binary', metavar='', help=_('Service binary.')) +@cliutils.arg( + '--unset', + dest='force_down', + help=_("Unset the force state down of service"), + action='store_false', + default=True) +def do_service_force_down(cs, args): + """Force service to down.""" + result = cs.services.force_down(args.host, args.binary, args.force_down) + utils.print_list([result], ['Host', 'Binary', 'Forced down']) + + @cliutils.arg('id', metavar='', help=_('Id of service.')) def do_service_delete(cs, args): """Delete the service.""" From d0af5f5e981ee60bf3d10afc40a971a020611b0e Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Mon, 10 Aug 2015 01:10:23 +0000 Subject: [PATCH 0799/1705] Updated from global requirements Change-Id: Ib3778c8a9bea62b30185d6fec6d9b0c5b999ec12 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 5e1e30bb5..fb672b570 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ # 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. -pbr<2.0,>=1.3 +pbr<2.0,>=1.4 argparse iso8601>=0.1.9 oslo.i18n>=1.5.0 # Apache-2.0 From cb7be1281a6487692d9b7301bd19b9f4de4bda31 Mon Sep 17 00:00:00 2001 From: kylin7-sg Date: Fri, 3 Jul 2015 16:06:29 +0800 Subject: [PATCH 0800/1705] Remove _discover_extensions Since heat already patched to use discover_extensions [1], _discover_extensions is no longer used by any other project, it could be safely removed. [1]: https://review.openstack.org/#/c/180812/ Change-Id: If527e15dcb9fb19cf9440084f50a2c6551cbd3b3 --- novaclient/shell.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/novaclient/shell.py b/novaclient/shell.py index c3e8c17c4..8926f70d2 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -445,11 +445,6 @@ def get_subcommand_parser(self, version, do_help=False): return parser - # TODO(lyj): Delete this method after heat patched to use - # client.discover_extensions - def _discover_extensions(self, version): - return client.discover_extensions(version) - def _add_bash_completion_subparser(self, subparsers): subparser = subparsers.add_parser( 'bash_completion', @@ -729,7 +724,7 @@ def main(self, argv): api_version = api_versions.discover_version(self.cs, api_version) # build available subcommands based on version - self.extensions = self._discover_extensions(api_version) + self.extensions = client.discover_extensions(api_version) self._run_extension_hooks('__pre_parse_args__') subcommand_parser = self.get_subcommand_parser( From 01c2e60eb3f3718884dd77cb05b4cde76052fb9d Mon Sep 17 00:00:00 2001 From: Kevin_Zheng Date: Tue, 4 Aug 2015 19:43:47 +0800 Subject: [PATCH 0801/1705] Allow display project-id for server groups Currently, the display of server groups doesn't contain informations about project-id. It is very difficult for admin to tell which group belongs to which project when using command: "nova server-group-list --all-projects". This patch adds project-id to the display. Change-Id: I010bf02f696396c404bc7a51ce93252c8c7f68a6 Partial-bug:#1481210 --- novaclient/tests/unit/v2/test_shell.py | 3 ++- novaclient/v2/shell.py | 8 +++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 8b148c04c..828170bd3 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2471,7 +2471,8 @@ def test_create_server_group(self): self.run_command('server-group-create wjsg affinity') self.assert_called('POST', '/os-server-groups', {'server_group': {'name': 'wjsg', - 'policies': ['affinity']}}) + 'policies': ['affinity']}}, + pos=0) def test_delete_multi_server_groups(self): self.run_command('server-group-delete 12345 56789') diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 9e840a70d..5be248d55 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -4422,7 +4422,13 @@ def do_availability_zone_list(cs, _args): def _print_server_group_details(server_group): - columns = ['Id', 'Name', 'Policies', 'Members', 'Metadata'] + # This is for compatible with Nova v2 API, remove after v2 + # is dropped. + if hasattr(server_group, 'project_id'): + columns = ['Id', 'Name', 'Project_id', 'Policies', + 'Members', 'Metadata'] + else: + columns = ['Id', 'Name', 'Policies', 'Members', 'Metadata'] utils.print_list(server_group, columns) From 9f19f9a2852d5a4adaf0c70ea91a5028eb34f766 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Mon, 10 Aug 2015 17:07:18 +0300 Subject: [PATCH 0802/1705] Change docstring of api_versions.discover_version Change-Id: I814bedf45980fb90002b2d253c8b38ff0d289f82 --- novaclient/api_versions.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/novaclient/api_versions.py b/novaclient/api_versions.py index ba4a56db8..02729f690 100644 --- a/novaclient/api_versions.py +++ b/novaclient/api_versions.py @@ -240,9 +240,11 @@ def _get_server_version_range(client): def discover_version(client, requested_version): - """Returns latest version supported by both API and client. + """Checks ``requested_version`` and returns the most recent version + supported by both the API and the client. :param client: client object + :param requested_version: requested version represented by APIVersion obj :returns: APIVersion """ From a570b60d674d6bbdf90f26440e68771f22b704db Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Fri, 7 Aug 2015 21:03:39 +0300 Subject: [PATCH 0803/1705] Copy cli arguments in api_versions.wraps decorator novaclient.api_versions.wraps decorator can be used in shell. Most of shell function has cli arguments which are added via another decorator. Currently, to keep this cli arguments, novaclient.api_versions.wraps decorator should be used in the top of decorators. Something like: @api_versions.wraps("123.456") @cliutils.arg("name", help="Name of the something") @cliutils.arg("action", help="Some action") def do_something(cs, args): pass This patch adds ability to put api_versions.wraps decorator everywhere: @api_versions.wraps("123.456") @cliutils.arg("name", help="Name of the something") @cliutils.arg("action", help="Some action") def do_something_1(cs, args): pass @cliutils.arg("name", help="Name of the something") @cliutils.arg("action", help="Some action") @api_versions.wraps("123.456") def do_something_2(cs, args): pass Related to bp api-microversion-support Change-Id: I8d599b712b17dcfc0be940a61c537d2dfe1b715b --- novaclient/api_versions.py | 13 +++++++++--- novaclient/tests/unit/test_api_versions.py | 23 ++++++++++++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/novaclient/api_versions.py b/novaclient/api_versions.py index ba4a56db8..b31801411 100644 --- a/novaclient/api_versions.py +++ b/novaclient/api_versions.py @@ -22,6 +22,7 @@ import novaclient from novaclient import exceptions from novaclient.i18n import _, _LW +from novaclient.openstack.common import cliutils from novaclient import utils LOG = logging.getLogger(__name__) @@ -340,8 +341,14 @@ def substitution(obj, *args, **kwargs): if not methods: raise exceptions.VersionNotFoundForAPIMethod( obj.api_version.get_string(), name) - else: - return max(methods, key=lambda f: f.start_version).func( - obj, *args, **kwargs) + + method = max(methods, key=lambda f: f.start_version) + + return method.func(obj, *args, **kwargs) + + if hasattr(func, 'arguments'): + for cli_args, cli_kwargs in func.arguments: + cliutils.add_arg(substitution, *cli_args, **cli_kwargs) return substitution + return decor diff --git a/novaclient/tests/unit/test_api_versions.py b/novaclient/tests/unit/test_api_versions.py index e3c204de4..02df69a41 100644 --- a/novaclient/tests/unit/test_api_versions.py +++ b/novaclient/tests/unit/test_api_versions.py @@ -18,6 +18,7 @@ import novaclient from novaclient import api_versions from novaclient import exceptions +from novaclient.openstack.common import cliutils from novaclient.tests.unit import utils from novaclient.v2 import versions @@ -250,6 +251,28 @@ def some_func(*args, **kwargs): checker.assert_called_once_with(*((obj,) + some_args), **some_kwargs) + def test_cli_args_are_copied(self): + + @api_versions.wraps("2.2", "2.6") + @cliutils.arg("name_1", help="Name of the something") + @cliutils.arg("action_1", help="Some action") + def some_func_1(cs, args): + pass + + @cliutils.arg("name_2", help="Name of the something") + @cliutils.arg("action_2", help="Some action") + @api_versions.wraps("2.2", "2.6") + def some_func_2(cs, args): + pass + + args_1 = [(('name_1',), {'help': 'Name of the something'}), + (('action_1',), {'help': 'Some action'})] + self.assertEqual(args_1, some_func_1.arguments) + + args_2 = [(('name_2',), {'help': 'Name of the something'}), + (('action_2',), {'help': 'Some action'})] + self.assertEqual(args_2, some_func_2.arguments) + class DiscoverVersionTestCase(utils.TestCase): def setUp(self): From e28f2a0d205d0bd50e13ee6085f7f6774276fc7b Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Tue, 11 Aug 2015 18:52:50 +0300 Subject: [PATCH 0804/1705] Add ability to use default major version If user specify os-compute-api-version=None, default major version (currently 2.0) will be used. Change-Id: Ie3fc31f4537cb17ed3c0cb97c91c04a9518fbb72 --- novaclient/shell.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/novaclient/shell.py b/novaclient/shell.py index c3e8c17c4..69ad67350 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -51,6 +51,7 @@ from novaclient.openstack.common import cliutils from novaclient import utils +DEFAULT_MAJOR_OS_COMPUTE_API_VERSION = "2.0" DEFAULT_OS_COMPUTE_API_VERSION = "2.latest" DEFAULT_NOVA_ENDPOINT_TYPE = 'publicURL' DEFAULT_NOVA_SERVICE_TYPE = "compute" @@ -565,8 +566,12 @@ def main(self, argv): # Discover available auth plugins novaclient.auth_plugin.discover_auth_systems() - api_version = api_versions.get_api_version( - args.os_compute_api_version) + if not args.os_compute_api_version: + api_version = api_versions.get_api_version( + DEFAULT_MAJOR_OS_COMPUTE_API_VERSION) + else: + api_version = api_versions.get_api_version( + args.os_compute_api_version) os_username = args.os_username os_user_id = args.os_user_id From 879d5e2568548f5d13d7e2007fb7550c20b6605f Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Thu, 13 Aug 2015 09:31:08 +0000 Subject: [PATCH 0805/1705] Updated from global requirements Change-Id: I50a7995f7b954cd059d2c22c8c17ad65cdd8477c --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 11659711d..eb8bb3a65 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -10,7 +10,7 @@ keyring!=3.3,>=2.1 mock>=1.2 requests-mock>=0.6.0 # Apache-2.0 sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2 -os-client-config>=1.4.0 +os-client-config!=1.6.2,>=1.4.0 oslosphinx>=2.5.0 # Apache-2.0 testrepository>=0.0.18 testscenarios>=0.4 From 1552d3e4d329c2e9bb695db3d48bb7a7b4f91ec4 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Thu, 13 Aug 2015 20:21:54 +0000 Subject: [PATCH 0806/1705] Updated from global requirements Change-Id: I6d1a1292616fda06d560fcf2fd602853fa299017 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index fb672b570..83dcdcdff 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ argparse iso8601>=0.1.9 oslo.i18n>=1.5.0 # Apache-2.0 oslo.serialization>=1.4.0 # Apache-2.0 -oslo.utils>=1.9.0 # Apache-2.0 +oslo.utils>=2.0.0 # Apache-2.0 PrettyTable<0.8,>=0.7 requests>=2.5.2 simplejson>=2.2.0 From 8a734a29f3022d2200eca8c3ec006993c6f1aedd Mon Sep 17 00:00:00 2001 From: ZhuChunzhan Date: Fri, 7 Aug 2015 07:49:11 +0000 Subject: [PATCH 0807/1705] Correct the files's description "overrwriter" The parameter of method create files's description contains incorrect word. Modify "overrwrite" to "overwrite". Closes-Bug: #1482081 Change-Id: I1ca15e521d8853e690d9d7bad12d6c63bca7d19d --- novaclient/v2/servers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index d4aed3be8..69338e8f3 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -860,7 +860,7 @@ def create(self, name, image, flavor, meta=None, files=None, :param flavor: The :class:`Flavor` to boot onto. :param meta: A dict of arbitrary key/value metadata to store for this server. Both keys and values must be <=255 characters. - :param files: A dict of files to overrwrite on the server upon boot. + :param files: A dict of files to overwrite on the server upon boot. Keys are file names (i.e. ``/etc/passwd``) and values are the file contents (either as a string or as a file-like object). A maximum of five entries is allowed, From a2ddda139929ba336d1633c111d36ea28b163d14 Mon Sep 17 00:00:00 2001 From: Zhenzan Zhou Date: Wed, 19 Aug 2015 15:32:42 +0800 Subject: [PATCH 0808/1705] Add help message for floating ip bulk operation Those floating ip bulk operations are nova-network only. This patch makes it more clear for users. Change-Id: Iaa7a6d6441749080fc7dde03562845ef4d5c5411 Closes-Bug: #1484001 --- novaclient/v2/shell.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 9e840a70d..257e2100a 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -2501,7 +2501,7 @@ def do_floating_ip_pool_list(cs, _args): '--host', dest='host', metavar='', default=None, help=_('Filter by host')) def do_floating_ip_bulk_list(cs, args): - """List all floating IPs.""" + """List all floating IPs (nova-network only).""" utils.print_list(cs.floating_ips_bulk.list(args.host), ['project_id', 'address', 'instance_uuid', @@ -2517,13 +2517,13 @@ def do_floating_ip_bulk_list(cs, args): '--interface', metavar='', default=None, help=_('Interface for new Floating IPs')) def do_floating_ip_bulk_create(cs, args): - """Bulk create floating IPs by range.""" + """Bulk create floating IPs by range (nova-network only).""" cs.floating_ips_bulk.create(args.ip_range, args.pool, args.interface) @cliutils.arg('ip_range', metavar='', help=_('Address range to delete')) def do_floating_ip_bulk_delete(cs, args): - """Bulk delete floating IPs by range.""" + """Bulk delete floating IPs by range (nova-network only).""" cs.floating_ips_bulk.delete(args.ip_range) From bf91805f358a48f9a8463c652d395791d7a334be Mon Sep 17 00:00:00 2001 From: Feodor Tersin Date: Fri, 21 Aug 2015 21:58:38 +0300 Subject: [PATCH 0809/1705] Fix versions.list for v2.1 Nova API novaclient transforms its management_url to get an url for a request for server versions list. Currently this transformation does not consider that version part of an url can be complex and contain major and minor parts. As a result versions.list does request on a wrong url and fails with NotFound error. This patch adds support of version formats like 'vN.N', 'vN.NN', etc. Partial-Bug: #1483030 Change-Id: Ie105bec37ee41c6a77ffc5f1cf680bbf88846159 --- novaclient/client.py | 2 +- novaclient/tests/unit/test_client.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/novaclient/client.py b/novaclient/client.py index 98dab06c8..5f221680b 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -404,7 +404,7 @@ def _cs_request(self, url, method, **kwargs): # a nova endpoint directly without "v2/". magic_tuple = parse.urlsplit(self.management_url) scheme, netloc, path, query, frag = magic_tuple - path = re.sub(r'v[1-9]/[a-z0-9]+$', '', path) + path = re.sub(r'v[1-9](\.[1-9][0-9]*)?/[a-z0-9]+$', '', path) url = parse.urlunsplit((scheme, netloc, path, None, None)) else: if self.service_catalog and not self.bypass_url: diff --git a/novaclient/tests/unit/test_client.py b/novaclient/tests/unit/test_client.py index a97ecbe86..b0f3a300f 100644 --- a/novaclient/tests/unit/test_client.py +++ b/novaclient/tests/unit/test_client.py @@ -137,10 +137,16 @@ def _check_version_url(self, management_url, version_url, mock_request): def test_client_version_url(self): self._check_version_url('http://foo.com/v2/%s', 'http://foo.com/') + self._check_version_url('http://foo.com/v2.1/%s', 'http://foo.com/') + self._check_version_url('http://foo.com/v3.785/%s', 'http://foo.com/') def test_client_version_url_with_project_name(self): self._check_version_url('http://foo.com/nova/v2/%s', 'http://foo.com/nova/') + self._check_version_url('http://foo.com/nova/v2.1/%s', + 'http://foo.com/nova/') + self._check_version_url('http://foo.com/nova/v3.785/%s', + 'http://foo.com/nova/') def test_get_client_class_v2(self): output = novaclient.client.get_client_class('2') From 67f673f2c9e655cf22b9194cfe4c581b1714b09e Mon Sep 17 00:00:00 2001 From: Satheesh Ulaganathan Date: Fri, 31 Jul 2015 15:10:49 -0700 Subject: [PATCH 0810/1705] Add response message when the state of a server is reset When a request is made for reset the server state it will not print any success message if it got reset successfully. The error message is already getting printed when the reset action fails. This fix will print response message if the reset action is success. Request>> nova reset-state --active Output Message will print as: Reset state for server succeeded; new state is active Closes-Bug: #1480428 Change-Id: Id069bc55fd88ce7e62f82f20e47be5373bb6ce8f --- novaclient/v2/shell.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index e1afd4b2e..6b532c29d 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -3499,6 +3499,8 @@ def do_reset_state(cs, args): for server in args.server: try: _find_server(cs, server).reset_state(args.state) + msg = "Reset state for server %s succeeded; new state is %s" + print(msg % (server, args.state)) except Exception as e: failure_flag = True msg = "Reset state for server %s failed: %s" % (server, e) From b6130e2e18b714cebe6b6318bd736fcb4e69e507 Mon Sep 17 00:00:00 2001 From: Feodor Tersin Date: Mon, 24 Aug 2015 19:45:53 +0300 Subject: [PATCH 0811/1705] Fix a fault of request logging with no credentials If a user doesn't specify for novaclient's Client neither credentials nor auth token, but specifies http_log_debug=True, and requests Nova version list, novaclient fails with AttributeError exception. The reason is that obfuscation of auth token doesn't expect None value of the token. This patch bypass obfuscation of None token. Closes-Bug: #1488169 Change-Id: I3712d633719bc77310ce863da33b2eec59a7ce91 --- novaclient/client.py | 2 +- novaclient/tests/unit/test_client.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/novaclient/client.py b/novaclient/client.py index 98dab06c8..52ed3cb23 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -260,7 +260,7 @@ def _redact(self, target, path, text=None): if key in target: if text: target[key] = text - else: + elif target[key] is not None: # because in python3 byte string handling is ... ug value = target[key].encode('utf-8') sha1sum = hashlib.sha1(value) diff --git a/novaclient/tests/unit/test_client.py b/novaclient/tests/unit/test_client.py index a97ecbe86..abde0c0b6 100644 --- a/novaclient/tests/unit/test_client.py +++ b/novaclient/tests/unit/test_client.py @@ -360,6 +360,8 @@ def test_log_req(self): connection_pool=True) cs.http_log_debug = True cs.http_log_req('GET', '/foo', {'headers': {}}) + cs.http_log_req('GET', '/foo', {'headers': + {'X-Auth-Token': None}}) cs.http_log_req('GET', '/foo', {'headers': {'X-Auth-Token': 'totally_bogus'}}) cs.http_log_req('GET', '/foo', {'headers': @@ -373,6 +375,10 @@ def test_log_req(self): output = self.logger.output.split('\n') self.assertIn("REQ: curl -g -i '/foo' -X GET", output) + self.assertIn( + "REQ: curl -g -i '/foo' -X GET -H " + '"X-Auth-Token: None"', + output) self.assertIn( "REQ: curl -g -i '/foo' -X GET -H " '"X-Auth-Token: {SHA1}b42162b6ffdbd7c3c37b7c95b7ba9f51dda0236d"', From a708190499568a19fca1584ce92f7b9054407517 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Tue, 25 Aug 2015 20:09:03 +0300 Subject: [PATCH 0812/1705] Add 'marker' argument to server list cli Nova API side supports returns limited number of resources, option "marker" is already supported by "novaclient as a lib" and allowes to list servers after "marker". Change-Id: I04c42858a19b2afed37f967c14234291203e6c1e --- novaclient/tests/unit/v2/test_shell.py | 4 ++++ novaclient/v2/shell.py | 10 +++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 828170bd3..b5bfec17a 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -940,6 +940,10 @@ def test_list_fields(self): self.assertIn('OS-EXT-MOD: Some Thing', output) self.assertIn('mod_some_thing_value', output) + def test_list_with_marker(self): + self.run_command('list --marker some-uuid') + self.assert_called('GET', '/servers/detail?marker=some-uuid') + def test_reboot(self): self.run_command('reboot sample-server') self.assert_called('POST', '/servers/1234/action', diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index e1afd4b2e..b3c712b55 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -1340,6 +1340,13 @@ def do_image_delete(cs, args): help=('Comma-separated list of sort keys and directions in the form' ' of [:]. The direction defaults to descending if' ' not specified.')) +@cliutils.arg( + '--marker', + dest='marker', + metavar='', + default=None, + help=('The last server uuid of the previous page; displays list of servers' + ' after "marker".')) def do_list(cs, args): """List active servers.""" imageid = None @@ -1398,7 +1405,8 @@ def do_list(cs, args): servers = cs.servers.list(detailed=detailed, search_opts=search_opts, sort_keys=sort_keys, - sort_dirs=sort_dirs) + sort_dirs=sort_dirs, + marker=args.marker) convert = [('OS-EXT-SRV-ATTR:host', 'host'), ('OS-EXT-STS:task_state', 'task_state'), ('OS-EXT-SRV-ATTR:instance_name', 'instance_name'), From d106dd485bc061461b1951413916c2179feebacb Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Tue, 25 Aug 2015 20:25:28 +0300 Subject: [PATCH 0813/1705] Add "limit" option to servers list cli This option is supported by both API and "novaclient-as-a-lib" sides, but was missed from CLI. Change-Id: Ie85949a7d7040f73b97b0676c5d574ede5a3034e --- novaclient/tests/unit/v2/test_shell.py | 4 ++++ novaclient/v2/shell.py | 12 +++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index b5bfec17a..3f190a80d 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -944,6 +944,10 @@ def test_list_with_marker(self): self.run_command('list --marker some-uuid') self.assert_called('GET', '/servers/detail?marker=some-uuid') + def test_list_with_limit(self): + self.run_command('list --limit 3') + self.assert_called('GET', '/servers/detail?limit=3') + def test_reboot(self): self.run_command('reboot sample-server') self.assert_called('POST', '/servers/1234/action', diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index b3c712b55..8478e7419 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -1347,6 +1347,15 @@ def do_image_delete(cs, args): default=None, help=('The last server uuid of the previous page; displays list of servers' ' after "marker".')) +@cliutils.arg( + '--limit', + dest='limit', + metavar='', + type=int, + default=None, + help=("Maximum number of servers to display. (If limit is bigger than " + "'osapi_max_limit' option of Nova API, limit 'osapi_max_limit' will " + "be used instead).")) def do_list(cs, args): """List active servers.""" imageid = None @@ -1406,7 +1415,8 @@ def do_list(cs, args): search_opts=search_opts, sort_keys=sort_keys, sort_dirs=sort_dirs, - marker=args.marker) + marker=args.marker, + limit=args.limit) convert = [('OS-EXT-SRV-ATTR:host', 'host'), ('OS-EXT-STS:task_state', 'task_state'), ('OS-EXT-SRV-ATTR:instance_name', 'instance_name'), From e682929e2570b84cad3802d25949f92f69b9717f Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Wed, 26 Aug 2015 14:11:56 +0000 Subject: [PATCH 0814/1705] Updated from global requirements Change-Id: I1703d2b09ab046e5ab2f34dfb168f8d05a521ced --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 83dcdcdff..5270cf4d8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ # 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. -pbr<2.0,>=1.4 +pbr<2.0,>=1.6 argparse iso8601>=0.1.9 oslo.i18n>=1.5.0 # Apache-2.0 From 43520009dea3c4c60c45cf15aac120a9693a3b67 Mon Sep 17 00:00:00 2001 From: rahulram Date: Wed, 26 Aug 2015 14:06:08 -0700 Subject: [PATCH 0815/1705] Fixed typo Errors in comments Change-Id: Id821aefe825226e9a96e90c6e89d4f715280acdc --- novaclient/tests/functional/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/tests/functional/base.py b/novaclient/tests/functional/base.py index 30d6cd21b..7fc638b3c 100644 --- a/novaclient/tests/functional/base.py +++ b/novaclient/tests/functional/base.py @@ -153,7 +153,7 @@ def setUp(self): self.image = pick_image(self.client.images.list()) # create a CLI client in case we'd like to do CLI - # testing. tempest_lib does this realy weird thing where it + # testing. tempest_lib does this really weird thing where it # builds a giant factory of all the CLIs that it knows # about. Eventually that should really be unwound into # something more sensible. From b80ac974e3e4b20e21d173176de2dcd8ef5b028b Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Wed, 26 Aug 2015 14:29:42 +0300 Subject: [PATCH 0816/1705] Add mechanism to vm list to return all resources This patch adds a loop to servers list, which makes calls to API side until all resources will be listed. Also, there are several functional tests are added with checks of markers and limit. Change-Id: Ic030a68867781cca25e723ab51986d87ecd3ab2e --- novaclient/tests/functional/test_servers.py | 52 +++++++++++++++++ novaclient/tests/unit/fixture_data/servers.py | 10 ++++ novaclient/tests/unit/v2/test_servers.py | 14 +++++ novaclient/v2/servers.py | 58 +++++++++++-------- novaclient/v2/shell.py | 5 +- 5 files changed, 114 insertions(+), 25 deletions(-) create mode 100644 novaclient/tests/functional/test_servers.py diff --git a/novaclient/tests/functional/test_servers.py b/novaclient/tests/functional/test_servers.py new file mode 100644 index 000000000..9de2cb2c5 --- /dev/null +++ b/novaclient/tests/functional/test_servers.py @@ -0,0 +1,52 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import uuid + +from novaclient.tests.functional import base +from novaclient.v2 import shell + + +class TestServersListNovaClient(base.ClientTestBase): + """Servers list functional tests. + """ + + def _create_servers(self, name, number): + network = self.client.networks.list()[0] + servers = [] + for i in range(number): + servers.append(self.client.servers.create( + name, self.image, self.flavor, nics=[{"net-id": network.id}])) + shell._poll_for_status( + self.client.servers.get, servers[-1].id, + 'building', ['active']) + + self.addCleanup(servers[-1].delete) + return servers + + def test_list_with_limit(self): + name = str(uuid.uuid4()) + self._create_servers(name, 2) + output = self.nova("list", params="--limit 1 --name %s" % name) + # Cut header and footer of the table + servers = output.split("\n")[3:-2] + self.assertEqual(1, len(servers), output) + + def test_list_all_servers(self): + name = str(uuid.uuid4()) + precreated_servers = self._create_servers(name, 3) + # there are no possibility to exceed the limit on API side, so just + # check that "-1" limit processes by novaclient side + output = self.nova("list", params="--limit -1 --name %s" % name) + # Cut header and footer of the table + for server in precreated_servers: + self.assertIn(server.id, output) diff --git a/novaclient/tests/unit/fixture_data/servers.py b/novaclient/tests/unit/fixture_data/servers.py index 0bece9642..5f45b528c 100644 --- a/novaclient/tests/unit/fixture_data/servers.py +++ b/novaclient/tests/unit/fixture_data/servers.py @@ -159,6 +159,16 @@ def setUp(self): json=get_servers_detail, headers=self.json_headers) + self.requests.register_uri( + 'GET', self.url('detail', marker=self.server_1234["id"]), + json={"servers": [self.server_1234, self.server_5678]}, + headers=self.json_headers, complete_qs=True) + + self.requests.register_uri( + 'GET', self.url('detail', marker=self.server_5678["id"]), + json={"servers": []}, + headers=self.json_headers, complete_qs=True) + self.server_1235 = self.server_1234.copy() self.server_1235['id'] = 1235 self.server_1235['status'] = 'error' diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index faf7dd67f..741cab55f 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -43,6 +43,20 @@ def test_list_servers(self): for s in sl: self.assertIsInstance(s, servers.Server) + def test_list_all_servers(self): + # use marker just to identify this call in fixtures + sl = self.cs.servers.list(limit=-1, marker=1234) + + self.assertEqual(2, len(sl)) + + self.assertEqual(self.requests.request_history[-2].method, 'GET') + self.assertEqual(self.requests.request_history[-2].path_url, + '/servers/detail?marker=1234') + self.assert_called('GET', '/servers/detail?marker=5678') + + for s in sl: + self.assertIsInstance(s, servers.Server) + def test_list_servers_undetailed(self): sl = self.cs.servers.list(detailed=False) self.assert_called('GET', '/servers') diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index 69338e8f3..3c34f95a4 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -572,32 +572,44 @@ def list(self, detailed=True, search_opts=None, marker=None, limit=None, if val: qparams[opt] = val - if marker: - qparams['marker'] = marker - - if limit: - qparams['limit'] = limit - - # Transform the dict to a sequence of two-element tuples in fixed - # order, then the encoded string will be consistent in Python 2&3. - if qparams or sort_keys or sort_dirs: - # sort keys and directions are unique since the same parameter - # key is repeated for each associated value - # (ie, &sort_key=key1&sort_key=key2&sort_key=key3) - items = list(qparams.items()) - if sort_keys: - items.extend(('sort_key', sort_key) for sort_key in sort_keys) - if sort_dirs: - items.extend(('sort_dir', sort_dir) for sort_dir in sort_dirs) - new_qparams = sorted(items, key=lambda x: x[0]) - query_string = "?%s" % parse.urlencode(new_qparams) - else: - query_string = "" - detail = "" if detailed: detail = "/detail" - return self._list("/servers%s%s" % (detail, query_string), "servers") + + result = [] + while True: + if marker: + qparams['marker'] = marker + + if limit and limit != -1: + qparams['limit'] = limit + + # Transform the dict to a sequence of two-element tuples in fixed + # order, then the encoded string will be consistent in Python 2&3. + if qparams or sort_keys or sort_dirs: + # sort keys and directions are unique since the same parameter + # key is repeated for each associated value + # (ie, &sort_key=key1&sort_key=key2&sort_key=key3) + items = list(qparams.items()) + if sort_keys: + items.extend(('sort_key', sort_key) + for sort_key in sort_keys) + if sort_dirs: + items.extend(('sort_dir', sort_dir) + for sort_dir in sort_dirs) + new_qparams = sorted(items, key=lambda x: x[0]) + query_string = "?%s" % parse.urlencode(new_qparams) + else: + query_string = "" + + servers = self._list("/servers%s%s" % (detail, query_string), + "servers") + result.extend(servers) + + if not servers or limit != -1: + break + marker = result[-1].id + return result def add_fixed_ip(self, server, network_id): """ diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 8478e7419..e0f711627 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -1353,9 +1353,10 @@ def do_image_delete(cs, args): metavar='', type=int, default=None, - help=("Maximum number of servers to display. (If limit is bigger than " + help=("Maximum number of servers to display. If limit == -1, all servers " + "will be displayed. If limit is bigger than " "'osapi_max_limit' option of Nova API, limit 'osapi_max_limit' will " - "be used instead).")) + "be used instead.")) def do_list(cs, args): """List active servers.""" imageid = None From 6f4f3f40cc9f4969c31937b156e4229b1b51344a Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Thu, 27 Aug 2015 18:34:37 +0300 Subject: [PATCH 0817/1705] Launch functional tests serially Currently, functional tests are not isolated and each test has access to resources from parallel launched tests. This patch is a hack to isolate tests and give ability to create as many resources as tenant quotas allow. Change-Id: Idc0a60d350e408bdbd67a7a527698be9f4d6b27b --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 9f17a0e33..3ac09aef5 100644 --- a/tox.ini +++ b/tox.ini @@ -35,6 +35,7 @@ commands = [testenv:functional] setenv = OS_TEST_PATH = ./novaclient/tests/functional +commands = python setup.py testr --testr-args='--concurrency=1 {posargs}' [testenv:cover] commands = python setup.py testr --coverage --testr-args='{posargs}' From 3f105a5989416db5b758caf17390784cfb9bfa94 Mon Sep 17 00:00:00 2001 From: Kyrylo Romanenko Date: Tue, 4 Aug 2015 13:20:57 +0300 Subject: [PATCH 0818/1705] Add method for better random name New method generates randomized name for entities. This method allows to set prefix that explicitly shows what kind of entity we have. This should be more useful than UUID-only names. Change-Id: Iafec7686f1b3b00cb21302ee0e5db40d74e77c81 --- novaclient/tests/functional/base.py | 10 ++++++++++ novaclient/tests/functional/test_instances.py | 5 +---- novaclient/tests/functional/test_volumes_api.py | 4 +--- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/novaclient/tests/functional/base.py b/novaclient/tests/functional/base.py index 30d6cd21b..7f773c5b6 100644 --- a/novaclient/tests/functional/base.py +++ b/novaclient/tests/functional/base.py @@ -12,9 +12,11 @@ import os import time +import uuid import fixtures import os_client_config +import six import tempest_lib.cli.base import testtools @@ -190,3 +192,11 @@ def wait_for_volume_status(self, volume, status, timeout=60, else: self.fail("Volume %s did not reach status %s after %d s" % (volume.id, status, timeout)) + + def name_generate(self, prefix='Entity'): + """Generate randomized name for some entity. + + :param prefix: string prefix + """ + name = "%s-%s" % (prefix, six.text_type(uuid.uuid4())) + return name diff --git a/novaclient/tests/functional/test_instances.py b/novaclient/tests/functional/test_instances.py index b569cb3cb..5ddaa9bf8 100644 --- a/novaclient/tests/functional/test_instances.py +++ b/novaclient/tests/functional/test_instances.py @@ -10,8 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import uuid - from novaclient.tests.functional import base @@ -37,8 +35,7 @@ def test_attach_volume(self): destroy. """ - # TODO(sdague): better random name - name = str(uuid.uuid4()) + name = self.name_generate('Instance') # Boot via the cli, as we're primarily testing the cli in this test self.nova('boot', params="--flavor %s --image %s %s --poll" % diff --git a/novaclient/tests/functional/test_volumes_api.py b/novaclient/tests/functional/test_volumes_api.py index 7c9214268..fcfd9f122 100644 --- a/novaclient/tests/functional/test_volumes_api.py +++ b/novaclient/tests/functional/test_volumes_api.py @@ -11,7 +11,6 @@ # under the License. import time -import uuid import six.moves @@ -75,8 +74,7 @@ def test_volumes_snapshots_types_create_get_list_delete(self): self.client.servers.list() # Create a volume type - # TODO(melwitt): Use a better random name - name = str(uuid.uuid4()) + name = self.name_generate('VolumeType') volume_type = self.client.volume_types.create(name) # This cleanup tests volume type delete From 9b7dd38d8fb80a0b8b1152205c6a08cd854c33de Mon Sep 17 00:00:00 2001 From: Cedric Brandily Date: Fri, 28 Aug 2015 14:02:22 +0200 Subject: [PATCH 0819/1705] Allow to reboot multiple servers This change allows to pass multiple server names/ids to nova reboot. Change-Id: I7ad891beafba019c262f656aa3e78686336e072b --- novaclient/tests/unit/v2/test_shell.py | 7 +++++++ novaclient/v2/shell.py | 21 ++++++++++++++++----- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 3f190a80d..cfd81fccb 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -956,6 +956,13 @@ def test_reboot(self): self.assert_called('POST', '/servers/1234/action', {'reboot': {'type': 'HARD'}}) + def test_reboot_many(self): + self.run_command('reboot sample-server sample-server2') + self.assert_called('POST', '/servers/1234/action', + {'reboot': {'type': 'SOFT'}}, pos=-2) + self.assert_called('POST', '/servers/5678/action', + {'reboot': {'type': 'SOFT'}}, pos=-1) + def test_rebuild(self): output, _ = self.run_command('rebuild sample-server 1') self.assert_called('GET', '/servers?name=sample-server', pos=-6) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index e0f711627..ab15d692a 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -1459,7 +1459,10 @@ def do_list(cs, args): const=servers.REBOOT_HARD, default=servers.REBOOT_SOFT, help=_('Perform a hard reboot (instead of a soft one).')) -@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg( + 'server', + metavar='', nargs='+', + help=_('Name or ID of server(s).')) @cliutils.arg( '--poll', dest='poll', @@ -1468,12 +1471,20 @@ def do_list(cs, args): help=_('Poll until reboot is complete.')) def do_reboot(cs, args): """Reboot a server.""" - server = _find_server(cs, args.server) - server.reboot(args.reboot_type) + servers = [_find_server(cs, s) for s in args.server] + utils.do_action_on_many( + lambda s: s.reboot(args.reboot_type), + servers, + _("Request to reboot server %s has been accepted."), + _("Unable to reboot the specified server(s).")) if args.poll: - _poll_for_status(cs.servers.get, server.id, 'rebooting', ['active'], - show_progress=False) + utils.do_action_on_many( + lambda s: _poll_for_status(cs.servers.get, s.id, 'rebooting', + ['active'], show_progress=False), + servers, + _("Wait for server %s reboot."), + _("Wait for specified server(s) failed.")) @cliutils.arg('server', metavar='', help=_('Name or ID of server.')) From 3d9c01b2d37e0ba5e4ea21a0697fdd83ace3d1ab Mon Sep 17 00:00:00 2001 From: Artom Lifshitz Date: Wed, 2 Sep 2015 16:47:44 +0000 Subject: [PATCH 0820/1705] Option to specify max servers for live evacuate The current behaviour of live evacuate is to naively request that all servers on the hypervisor be evacuated. The more VMs are migrated simultaneously, the more bandwidth is required. Once a certain number of migrating VMs is reached, there is not enough bandwidth to cope with the rate at which they dirty their memory. The migrations will thus never complete. The correct solution to this would be a whole lot of work on the Nova side. As a stopgap measure, this patch introduces a --max-servers option to host-evacuate-live. With this option, the user can control the number of VMs that are live-migrated at the same time, thus allowing the user to avoid the dirty memory scenario described above. Change-Id: I17bad5f3253d6657fc1e6610159fc8e3921e6ea4 --- novaclient/tests/unit/v2/test_shell.py | 8 ++++++++ novaclient/v2/contrib/host_evacuate_live.py | 10 ++++++++++ 2 files changed, 18 insertions(+) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index cfd81fccb..06224ed31 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -1583,6 +1583,14 @@ def test_host_evacuate_live_with_disk_over_commit(self): self.assert_called('POST', '/servers/uuid3/action', body, pos=3) self.assert_called('POST', '/servers/uuid4/action', body, pos=4) + def test_host_evacuate_list_with_max_servers(self): + self.run_command('host-evacuate-live --max-servers 1 hyper') + self.assert_called('GET', '/os-hypervisors/hyper/servers', pos=0) + body = {'os-migrateLive': {'host': None, + 'block_migration': False, + 'disk_over_commit': False}} + self.assert_called('POST', '/servers/uuid1/action', body, pos=1) + def test_reset_state(self): self.run_command('reset-state sample-server') self.assert_called('POST', '/servers/1234/action', diff --git a/novaclient/v2/contrib/host_evacuate_live.py b/novaclient/v2/contrib/host_evacuate_live.py index 276fb2f70..f91599bbd 100644 --- a/novaclient/v2/contrib/host_evacuate_live.py +++ b/novaclient/v2/contrib/host_evacuate_live.py @@ -54,15 +54,25 @@ def __init__(self, server_uuid, live_migration_accepted, action='store_true', default=False, help=_('Enable disk overcommit.')) +@cliutils.arg( + '--max-servers', + type=int, + dest='max_servers', + metavar='', + help='Maximum number of servers to live migrate simultaneously') def do_host_evacuate_live(cs, args): """Live migrate all instances of the specified host to other available hosts. """ hypervisors = cs.hypervisors.search(args.host, servers=True) response = [] + migrating = 0 for hyper in hypervisors: for server in getattr(hyper, 'servers', []): response.append(_server_live_migrate(cs, server, args)) + migrating = migrating + 1 + if args.max_servers is not None and migrating >= args.max_servers: + break utils.print_list(response, ["Server UUID", "Live Migration Accepted", "Error Message"]) From eaf1e56b210c94e7d26ca99b5931f3688cef984c Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Wed, 2 Sep 2015 17:05:46 -0700 Subject: [PATCH 0821/1705] Update path to subunit2html in post_test_hook Per: http://lists.openstack.org/pipermail/openstack-dev/2015-August/072982.html The location of subunit2html changed on the images in the gate so update the path used in the post_test_hook. Long-term we should just use what's in devstack-gate. Change-Id: I5e50e7d7ad845aba26403df1df412c0a139a6dc7 Closes-Bug: #1491646 -------------- squashed with: -------------- Don't pass null device when attaching a volume The v2.1 API schema rejects null device values in an os-volume_attachments request, so only include the device in the request if one is specified on the command line. Closes-Bug: #1491325 Change-Id: I4fa4019f19f9af6ff350db2fb6e524fa8570a6f3 --- novaclient/tests/functional/hooks/post_test_hook.sh | 2 +- novaclient/tests/unit/v2/test_shell.py | 3 +-- novaclient/v2/volumes.py | 9 +++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/novaclient/tests/functional/hooks/post_test_hook.sh b/novaclient/tests/functional/hooks/post_test_hook.sh index 8eff1fd28..1fa2618ba 100755 --- a/novaclient/tests/functional/hooks/post_test_hook.sh +++ b/novaclient/tests/functional/hooks/post_test_hook.sh @@ -18,7 +18,7 @@ function generate_testr_results { if [ -f .testrepository/0 ]; then sudo .tox/functional/bin/testr last --subunit > $WORKSPACE/testrepository.subunit sudo mv $WORKSPACE/testrepository.subunit $BASE/logs/testrepository.subunit - sudo .tox/functional/bin/python /usr/local/jenkins/slave_scripts/subunit2html.py $BASE/logs/testrepository.subunit $BASE/logs/testr_results.html + sudo /usr/os-testr-env/bin/subunit2html $BASE/logs/testrepository.subunit $BASE/logs/testr_results.html sudo gzip -9 $BASE/logs/testrepository.subunit sudo gzip -9 $BASE/logs/testr_results.html sudo chown jenkins:jenkins $BASE/logs/testrepository.subunit.gz $BASE/logs/testr_results.html.gz diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index cfd81fccb..603e59ec1 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2293,8 +2293,7 @@ def test_volume_attach_without_device(self): self.run_command('volume-attach sample-server Work') self.assert_called('POST', '/servers/1234/os-volume_attachments', {'volumeAttachment': - {'device': None, - 'volumeId': 'Work'}}) + {'volumeId': 'Work'}}) def test_volume_update(self): self.run_command('volume-update sample-server Work Work') diff --git a/novaclient/v2/volumes.py b/novaclient/v2/volumes.py index f0403af63..d0446c5fd 100644 --- a/novaclient/v2/volumes.py +++ b/novaclient/v2/volumes.py @@ -131,17 +131,18 @@ def delete(self, volume): with self.alternate_service_type('volume'): self._delete("/volumes/%s" % base.getid(volume)) - def create_server_volume(self, server_id, volume_id, device): + def create_server_volume(self, server_id, volume_id, device=None): """ Attach a volume identified by the volume ID to the given server ID :param server_id: The ID of the server :param volume_id: The ID of the volume to attach. - :param device: The device name + :param device: The device name (optional) :rtype: :class:`Volume` """ - body = {'volumeAttachment': {'volumeId': volume_id, - 'device': device}} + body = {'volumeAttachment': {'volumeId': volume_id}} + if device is not None: + body['volumeAttachment']['device'] = device return self._create("/servers/%s/os-volume_attachments" % server_id, body, "volumeAttachment") From c915757ed5e7ce3714eb9f0682d24530112f735e Mon Sep 17 00:00:00 2001 From: Sean Dague Date: Thu, 3 Sep 2015 08:19:10 -0400 Subject: [PATCH 0822/1705] Don't assume oscomputeversions is correctly deployed The paste pipeline that's "above" the /v2/ and /v2.1/ urls is the oscomputeversions dispatcher, which should provide 300 redirects to versions in question. Previously the code that was trying to discover information about the current version did so via a GET of something that looked like /v2. On an upstream compatible OpenStack this actually triggers a 300 and redirect to /v2/ for the real answer. On deployed public clouds, oscomputeversions is sometimes not deployed at all, or front end blocking by the load balancer does weirdness. Sometimes with a 401, sometimes a hang. This fixes the way that first request is made that should avoid the 300 redirect entirely. The code probably should have done this originally, but it was tested with upstream configuration that worked fine in this case. This fix is tested against HP Cloud (the only pub cloud I have creds with). Before this fix "nova list" hangs trying to get the supported versions, after the fix it works as expected. Change-Id: I1692380fe8d340e5c044f46dd0b103c7550d2c7d Closes-Bug: #1491579 --- novaclient/tests/unit/v2/test_versions.py | 2 +- novaclient/v2/versions.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/novaclient/tests/unit/v2/test_versions.py b/novaclient/tests/unit/v2/test_versions.py index eeba89a28..2d46472da 100644 --- a/novaclient/tests/unit/v2/test_versions.py +++ b/novaclient/tests/unit/v2/test_versions.py @@ -63,4 +63,4 @@ def test_get_current_with_http_client(self, mock_list, def test_get_current_with_session_client(self, mock_is_session_client): self.cs.callback = [] self.cs.versions.get_current() - self.cs.assert_called('GET', 'http://nova-api:8774/v2.1') + self.cs.assert_called('GET', 'http://nova-api:8774/v2.1/') diff --git a/novaclient/v2/versions.py b/novaclient/v2/versions.py index b43fb4dee..09fd25f0f 100644 --- a/novaclient/v2/versions.py +++ b/novaclient/v2/versions.py @@ -40,6 +40,11 @@ def get_current(self): """Returns info about current version.""" if self._is_session_client(): url = self.api.client.get_endpoint().rsplit("/", 1)[0] + # NOTE(sdague): many service providers don't really + # implement GET / in the expected way, if we do a GET /v2 + # that's actually a 300 redirect to /v2/... because of how + # paste works. So adding the end slash is really important. + url = "%s/" % url return self._get(url, "version") else: # NOTE(andreykurilin): HTTPClient doesn't have ability to send get From 5869abc53162e2d6e855f617c5ede6d024604649 Mon Sep 17 00:00:00 2001 From: Sean Dague Date: Thu, 3 Sep 2015 13:17:30 -0400 Subject: [PATCH 0823/1705] fix novaclient functional tests for new devstack config The novaclient functional tests assumed computev21 as a type in the service catalog. We're defaulting compute in devstack to v21 now, so this no longer works. This is probably far too coupled to devstack atm, however until we get to unversioned service catalog, this is what we need to do. Change-Id: Ifb0056924b20f62d75d13a7b4aae829df61a5396 --- novaclient/tests/functional/test_keypairs.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/novaclient/tests/functional/test_keypairs.py b/novaclient/tests/functional/test_keypairs.py index 02bf3ecd2..1d5b09e64 100644 --- a/novaclient/tests/functional/test_keypairs.py +++ b/novaclient/tests/functional/test_keypairs.py @@ -97,8 +97,7 @@ class TestKeypairsNovaClientV22(TestKeypairsNovaClient): """ def nova(self, *args, **kwargs): - return self.cli_clients.nova(flags='--os-compute-api-version 2.2 ' - '--service-type computev21', + return self.cli_clients.nova(flags='--os-compute-api-version 2.2 ', *args, **kwargs) def test_create_keypair(self): From e4b0d46c4b5b99973a7f65c294a9b73c8adfefb7 Mon Sep 17 00:00:00 2001 From: Sean Dague Date: Thu, 3 Sep 2015 11:13:21 -0400 Subject: [PATCH 0824/1705] workaround for RAX repose configuration The RAX Repose environment is blocking access to the API version information by local policy, returning a 401 before we even get to Nova itself. While this in clearly incorrect behavior, it is behavior in the field, and we should not break all our users on that cloud. This catches the 401 and translates that so that it's the equivalent of only supporting v2.0. We will be taking these API calls to defcore, and intend to remove this work around once that is done. Change-Id: I2072095c24b41efcfd58d6f25205bcc94f1174da Related-Bug: #1491579 --- novaclient/tests/unit/test_api_versions.py | 12 ++++++++++++ novaclient/tests/unit/v2/test_versions.py | 9 +++++++++ novaclient/v2/versions.py | 11 ++++++++++- 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/novaclient/tests/unit/test_api_versions.py b/novaclient/tests/unit/test_api_versions.py index 02df69a41..0159d198b 100644 --- a/novaclient/tests/unit/test_api_versions.py +++ b/novaclient/tests/unit/test_api_versions.py @@ -357,3 +357,15 @@ def test_server_without_microversion_and_no_version_field(self): api_versions.discover_version( fake_client, api_versions.APIVersion('2.latest')).get_string()) + + def test_server_without_microversion_rax_workaround(self): + fake_client = mock.MagicMock() + fake_client.versions.get_current.return_value = None + novaclient.API_MAX_VERSION = api_versions.APIVersion("2.11") + novaclient.API_MIN_VERSION = api_versions.APIVersion("2.1") + + self.assertEqual( + "2.0", + api_versions.discover_version( + fake_client, + api_versions.APIVersion('2.latest')).get_string()) diff --git a/novaclient/tests/unit/v2/test_versions.py b/novaclient/tests/unit/v2/test_versions.py index 2d46472da..da348751f 100644 --- a/novaclient/tests/unit/v2/test_versions.py +++ b/novaclient/tests/unit/v2/test_versions.py @@ -14,6 +14,7 @@ import mock +from novaclient import exceptions as exc from novaclient.tests.unit import utils from novaclient.tests.unit.v2 import fakes from novaclient.v2 import versions @@ -64,3 +65,11 @@ def test_get_current_with_session_client(self, mock_is_session_client): self.cs.callback = [] self.cs.versions.get_current() self.cs.assert_called('GET', 'http://nova-api:8774/v2.1/') + + @mock.patch.object(versions.VersionManager, '_is_session_client', + return_value=True) + @mock.patch.object(versions.VersionManager, '_get', + side_effect=exc.Unauthorized("401 RAX")) + def test_get_current_with_rax_workaround(self, session, get): + self.cs.callback = [] + self.assertIsNone(self.cs.versions.get_current()) diff --git a/novaclient/v2/versions.py b/novaclient/v2/versions.py index 09fd25f0f..17a20df99 100644 --- a/novaclient/v2/versions.py +++ b/novaclient/v2/versions.py @@ -20,6 +20,7 @@ from novaclient import base from novaclient import client +from novaclient import exceptions as exc class Version(base.Resource): @@ -45,7 +46,15 @@ def get_current(self): # that's actually a 300 redirect to /v2/... because of how # paste works. So adding the end slash is really important. url = "%s/" % url - return self._get(url, "version") + try: + return self._get(url, "version") + except exc.Unauthorized: + # NOTE(sdague): RAX's repose configuration blocks + # access to the versioned endpoint, which is + # definitely non-compliant behavior. However, there is + # no defcore test for this yet. Remove this code block + # once we land things in defcore. + return None else: # NOTE(andreykurilin): HTTPClient doesn't have ability to send get # request without token in the url, so `self._get` doesn't work. From d970de480d5ad440e36c570a47893a17beb71351 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20G=C3=B3rski?= Date: Wed, 2 Sep 2015 18:08:26 +0200 Subject: [PATCH 0825/1705] Adds missing internationalization for help message Change-Id: Iecb3eaa791969da422ea062148b58645457f8aab Closes-Bug: #1491492 --- novaclient/v2/shell.py | 50 +++++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index ab15d692a..e969cf5e7 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -509,7 +509,7 @@ def _boot(cs, args): dest='admin_pass', metavar='', default=None, - help='Admin password for the instance') + help=_('Admin password for the instance')) def do_boot(cs, args): """Boot a new server.""" boot_args, boot_kwargs = _boot(cs, args) @@ -541,7 +541,7 @@ def do_cloudpipe_create(cs, args): @cliutils.arg('address', metavar='', help=_('New IP Address.')) -@cliutils.arg('port', metavar='', help='New Port.') +@cliutils.arg('port', metavar='', help=_('New Port.')) def do_cloudpipe_configure(cs, args): """Update the VPN IP/port of a cloudpipe instance.""" cs.cloudpipe.update(args.address, args.port) @@ -866,8 +866,8 @@ def do_scrub(cs, args): '--fields', default=None, metavar='', - help='Comma-separated list of fields to display. ' - 'Use the show command to see which fields are available.') + help=_('Comma-separated list of fields to display. ' + 'Use the show command to see which fields are available.')) def do_network_list(cs, args): """Print a list of available networks.""" network_list = cs.networks.list() @@ -924,7 +924,7 @@ def do_network_delete(cs, args): @cliutils.arg( 'network', metavar='', - help="uuid of network") + help=_("uuid of network")) def do_network_disassociate(cs, args): """Disassociate host and/or project from the given network.""" if args.host_only: @@ -938,11 +938,11 @@ def do_network_disassociate(cs, args): @cliutils.arg( 'network', metavar='', - help="uuid of network") + help=_("uuid of network")) @cliutils.arg( 'host', metavar='', - help="Name of host") + help=_("Name of host")) def do_network_associate_host(cs, args): """Associate host with network.""" cs.networks.associate_host(args.network, args.host) @@ -951,7 +951,7 @@ def do_network_associate_host(cs, args): @cliutils.arg( 'network', metavar='', - help="uuid of network") + help=_("uuid of network")) def do_network_associate_project(cs, args): """Associate project with network.""" cs.networks.associate_project(args.network) @@ -1029,7 +1029,7 @@ def _filter_network_create_options(args): @cliutils.arg( '--dns1', dest="dns1", - metavar="", help='First DNS') + metavar="", help=_('First DNS')) @cliutils.arg( '--dns2', dest="dns2", @@ -1320,7 +1320,7 @@ def do_image_delete(cs, args): dest='deleted', action="store_true", default=False, - help='Only display deleted servers (Admin only).') + help=_('Only display deleted servers (Admin only).')) @cliutils.arg( '--fields', default=None, @@ -1514,7 +1514,7 @@ def do_reboot(cs, args): '--preserve-ephemeral', action="store_true", default=False, - help='Preserve the default ephemeral storage partition on rebuild.') + help=_('Preserve the default ephemeral storage partition on rebuild.')) @cliutils.arg( '--name', metavar='', @@ -1975,7 +1975,7 @@ def _find_flavor(cs, flavor): @cliutils.arg( 'network_id', metavar='', - help='Network ID.') + help=_('Network ID.')) def do_add_fixed_ip(cs, args): """Add new IP address on a network to server.""" server = _find_server(cs, args.server) @@ -2336,7 +2336,7 @@ def __init__(self, console_dict): @cliutils.arg( 'console_type', metavar='', - help='Type of rdp console ("rdp-html5").') + help=_('Type of rdp console ("rdp-html5").')) def do_get_rdp_console(cs, args): """Get a rdp console to a server.""" server = _find_server(cs, args.server) @@ -2372,7 +2372,7 @@ def __init__(self, console_dict): utils.print_list([SerialConsole(data['console'])], ['Type', 'Url']) -@cliutils.arg('server', metavar='', help='Name or ID of server.') +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) @cliutils.arg( 'private_key', metavar='', @@ -2429,13 +2429,13 @@ def do_add_floating_ip(cs, args): _associate_floating_ip(cs, args) -@cliutils.arg('server', metavar='', help='Name or ID of server.') -@cliutils.arg('address', metavar='
', help='IP Address.') +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg('address', metavar='
', help=_('IP Address.')) @cliutils.arg( '--fixed-address', metavar='', default=None, - help='Fixed IP Address to associate with.') + help=_('Fixed IP Address to associate with.')) def do_floating_ip_associate(cs, args): """Associate a floating IP address to a server.""" _associate_floating_ip(cs, args) @@ -2453,8 +2453,8 @@ def do_remove_floating_ip(cs, args): _disassociate_floating_ip(cs, args) -@cliutils.arg('server', metavar='', help='Name or ID of server.') -@cliutils.arg('address', metavar='
', help='IP Address.') +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg('address', metavar='
', help=_('IP Address.')) def do_floating_ip_disassociate(cs, args): """Disassociate a floating IP address from a server.""" _disassociate_floating_ip(cs, args) @@ -3659,7 +3659,7 @@ def do_host_list(cs, args): utils.print_list(result, columns) -@cliutils.arg('host', metavar='', help='Name of host.') +@cliutils.arg('host', metavar='', help=_('Name of host.')) @cliutils.arg( '--status', metavar='', default=None, dest='status', help=_('Either enable or disable a host.')) @@ -3683,7 +3683,7 @@ def do_host_update(cs, args): utils.print_list([result], columns) -@cliutils.arg('host', metavar='', help='Name of host.') +@cliutils.arg('host', metavar='', help=_('Name of host.')) @cliutils.arg( '--action', metavar='', dest='action', choices=['startup', 'shutdown', 'reboot'], @@ -4535,7 +4535,7 @@ def do_secgroup_delete_default_rule(cs, args): raise exceptions.CommandError(_("Rule not found")) -@cliutils.arg('name', metavar='', help='Server group name.') +@cliutils.arg('name', metavar='', help=_('Server group name.')) # NOTE(wingwj): The '--policy' way is still reserved here for preserving # the backwards compatibility of CLI, even if a user won't get this usage # in '--help' description. It will be deprecated after an suitable deprecation @@ -4551,7 +4551,7 @@ def do_secgroup_delete_default_rule(cs, args): metavar='', default=argparse.SUPPRESS, nargs='*', - help='Policies for the server groups ("affinity" or "anti-affinity")') + help=_('Policies for the server groups ("affinity" or "anti-affinity")')) @cliutils.arg( '--policy', default=[], @@ -4572,7 +4572,7 @@ def do_server_group_create(cs, args): 'id', metavar='', nargs='+', - help="Unique ID(s) of the server group to delete") + help=_("Unique ID(s) of the server group to delete")) def do_server_group_delete(cs, args): """Delete specific server group(s).""" failure_count = 0 @@ -4593,7 +4593,7 @@ def do_server_group_delete(cs, args): @cliutils.arg( 'id', metavar='', - help="Unique ID of the server group to get") + help=_("Unique ID of the server group to get")) def do_server_group_get(cs, args): """Get a specific server group.""" server_group = cs.server_groups.get(args.id) From 6fc3a943e6e4b4cb9404e4c868d9027f23c1763a Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Fri, 4 Sep 2015 16:02:56 +0300 Subject: [PATCH 0826/1705] [Bug-Fix] Update requests body for quota-update Update quotas API call doesn't accept parameter "tenant-id". Change-Id: I1cbc6088c5deeb32e4e98ef1ced5f83ce71fe3ab Closes-Bug: #1492242 --- novaclient/tests/functional/test_quotas.py | 59 ++++++++++++++++++++++ novaclient/tests/unit/v2/fakes.py | 3 +- novaclient/tests/unit/v2/test_quotas.py | 4 +- novaclient/tests/unit/v2/test_shell.py | 12 ++--- novaclient/v2/quotas.py | 6 +-- 5 files changed, 66 insertions(+), 18 deletions(-) create mode 100644 novaclient/tests/functional/test_quotas.py diff --git a/novaclient/tests/functional/test_quotas.py b/novaclient/tests/functional/test_quotas.py new file mode 100644 index 000000000..3b75ed5f6 --- /dev/null +++ b/novaclient/tests/functional/test_quotas.py @@ -0,0 +1,59 @@ +# 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 novaclient.tests.functional import base + + +class TestQuotasNovaClient(base.ClientTestBase): + """Nova quotas functional tests. + """ + + _quota_resources = ['instances', 'cores', 'ram', + 'floating_ips', 'fixed_ips', 'metadata_items', + 'injected_files', 'injected_file_content_bytes', + 'injected_file_path_bytes', 'key_pairs', + 'security_groups', 'security_group_rules', + 'server_groups', 'server_group_members'] + + def test_quotas_update(self): + # `nova quota-update` requires tenant-id. EXAMPLE of keystone output: + # +-------------+----------------------------------+ + # | Property | Value | + # +-------------+----------------------------------+ + # | description | | + # | enabled | True | + # | id | 582df899eabc47018c96713c2f7196ba | + # | name | admin | + # +-------------+----------------------------------+ + tenant_info = self.cli_clients.keystone( + "tenant-get", params=self.cli_clients.tenant_name).split("\n") + tenant_id = [l.rsplit("|", 2)[-2].strip() + for l in tenant_info if "id" in l][0] + + self.addCleanup(self.client.quotas.delete, tenant_id) + + original_quotas = self.client.quotas.get(tenant_id) + + difference = 10 + params = [tenant_id] + for quota_name in self._quota_resources: + params.append("--%(name)s %(value)s" % { + "name": quota_name.replace("_", "-"), + "value": getattr(original_quotas, quota_name) + difference}) + + self.nova("quota-update", params=" ".join(params)) + + updated_quotas = self.client.quotas.get(tenant_id) + + for quota_name in self._quota_resources: + self.assertEqual(getattr(original_quotas, quota_name), + getattr(updated_quotas, quota_name) - difference) diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 6814461d1..e50cddd0f 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -1259,8 +1259,7 @@ def get_os_quota_sets_tenant_id_defaults(self): def put_os_quota_sets_97f4c221bff44578b0300df4ef119353(self, body, **kw): assert list(body) == ['quota_set'] - fakes.assert_has_keys(body['quota_set'], - required=['tenant_id']) + fakes.assert_has_keys(body['quota_set']) return (200, {}, { 'quota_set': { 'tenant_id': '97f4c221bff44578b0300df4ef119353', diff --git a/novaclient/tests/unit/v2/test_quotas.py b/novaclient/tests/unit/v2/test_quotas.py index 43f8ce1d8..76bfcfe8e 100644 --- a/novaclient/tests/unit/v2/test_quotas.py +++ b/novaclient/tests/unit/v2/test_quotas.py @@ -45,9 +45,7 @@ def test_force_update_quota(self): q.update(cores=2, force=True) self.assert_called( 'PUT', '/os-quota-sets/97f4c221bff44578b0300df4ef119353', - {'quota_set': {'force': True, - 'cores': 2, - 'tenant_id': '97f4c221bff44578b0300df4ef119353'}}) + {'quota_set': {'force': True, 'cores': 2}}) def test_quotas_delete(self): tenant_id = 'test' diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 603e59ec1..9a2ae0c70 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -1835,8 +1835,7 @@ def test_quota_update(self): self.assert_called( 'PUT', '/os-quota-sets/97f4c221bff44578b0300df4ef119353', - {'quota_set': {'instances': 5, - 'tenant_id': '97f4c221bff44578b0300df4ef119353'}}) + {'quota_set': {'instances': 5}}) def test_user_quota_update(self): self.run_command( @@ -1846,8 +1845,7 @@ def test_user_quota_update(self): self.assert_called( 'PUT', '/os-quota-sets/97f4c221bff44578b0300df4ef119353?user_id=u1', - {'quota_set': {'instances': 5, - 'tenant_id': '97f4c221bff44578b0300df4ef119353'}}) + {'quota_set': {'instances': 5}}) def test_quota_force_update(self): self.run_command( @@ -1856,8 +1854,7 @@ def test_quota_force_update(self): self.assert_called( 'PUT', '/os-quota-sets/97f4c221bff44578b0300df4ef119353', {'quota_set': {'force': True, - 'instances': 5, - 'tenant_id': '97f4c221bff44578b0300df4ef119353'}}) + 'instances': 5}}) def test_quota_update_fixed_ip(self): self.run_command( @@ -1865,8 +1862,7 @@ def test_quota_update_fixed_ip(self): ' --fixed-ips=5') self.assert_called( 'PUT', '/os-quota-sets/97f4c221bff44578b0300df4ef119353', - {'quota_set': {'fixed_ips': 5, - 'tenant_id': '97f4c221bff44578b0300df4ef119353'}}) + {'quota_set': {'fixed_ips': 5}}) def test_quota_delete(self): self.run_command('quota-delete --tenant ' diff --git a/novaclient/v2/quotas.py b/novaclient/v2/quotas.py index cd16042b4..a77e16e27 100644 --- a/novaclient/v2/quotas.py +++ b/novaclient/v2/quotas.py @@ -41,14 +41,10 @@ def get(self, tenant_id, user_id=None): url = '/os-quota-sets/%s' % tenant_id return self._get(url, "quota_set") - def _update_body(self, tenant_id, **kwargs): - kwargs['tenant_id'] = tenant_id - return {'quota_set': kwargs} - def update(self, tenant_id, **kwargs): user_id = kwargs.pop('user_id', None) - body = self._update_body(tenant_id, **kwargs) + body = {'quota_set': kwargs} for key in list(body['quota_set']): if body['quota_set'][key] is None: From ca031f991565a3764f8c36bdd28c942ab383b4d3 Mon Sep 17 00:00:00 2001 From: Ramaraja Date: Mon, 7 Sep 2015 14:33:16 +0530 Subject: [PATCH 0827/1705] Fix nova --help needs authentication Added --help,-h in do_help flag so that authentication will not be prompted. Change-Id: Iafb766304eeda58eff0c92b87a127018820832fd Closes-Bug: #1491677 --- novaclient/shell.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/novaclient/shell.py b/novaclient/shell.py index 6957f8313..0f1dd98e1 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -556,7 +556,8 @@ def main(self, argv): self.setup_debugging(args.debug) self.extensions = [] - do_help = ('help' in argv) or not argv + do_help = ('help' in argv) or ( + '--help' in argv) or ('-h' in argv) or not argv # Discover available auth plugins novaclient.auth_plugin.discover_auth_systems() From f7a26601b28805939a797fd1f4d1a844ec5d3847 Mon Sep 17 00:00:00 2001 From: Mark Doffman Date: Tue, 8 Sep 2015 09:33:18 -0700 Subject: [PATCH 0828/1705] Enable i18n with Babel. Add Babel commands to setup.cfg. Create the first .pot file with current translatable strings using `python setup.py extract_messages`. Co-Authored-By: Matt Riedemann Change-Id: I57804496d1ec67b566b350941e71206903dd8d1d Closes-Bug: #1492444 --- babel.cfg | 1 + .../locale/python-novaclient.pot | 2171 +++++++++++++++++ setup.cfg | 14 + 3 files changed, 2186 insertions(+) create mode 100644 babel.cfg create mode 100644 python-novaclient/locale/python-novaclient.pot diff --git a/babel.cfg b/babel.cfg new file mode 100644 index 000000000..efceab818 --- /dev/null +++ b/babel.cfg @@ -0,0 +1 @@ +[python: **.py] diff --git a/python-novaclient/locale/python-novaclient.pot b/python-novaclient/locale/python-novaclient.pot new file mode 100644 index 000000000..9d226bda9 --- /dev/null +++ b/python-novaclient/locale/python-novaclient.pot @@ -0,0 +1,2171 @@ +# Translations template for python-novaclient. +# Copyright (C) 2015 ORGANIZATION +# This file is distributed under the same license as the python-novaclient +# project. +# FIRST AUTHOR , 2015. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: python-novaclient 2.28.2.dev1\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2015-09-08 10:09-0700\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 1.3\n" + +#: novaclient/api_versions.py:38 +#, python-format +msgid "'%(other)s' should be an instance of '%(cls)s'" +msgstr "" + +#: novaclient/api_versions.py:64 +#, python-format +msgid "" +"Invalid format of client version '%s'. Expected format 'X.Y', where X is " +"a major part and Y is a minor part of version." +msgstr "" + +#: novaclient/api_versions.py:136 +msgid "Null APIVersion doesn't support 'matches'." +msgstr "" + +#: novaclient/api_versions.py:152 +msgid "Null APIVersion cannot be converted to string." +msgstr "" + +#: novaclient/api_versions.py:204 +#, python-format +msgid "Invalid client version '%(version)s'. Major part should be '%(major)s'" +msgstr "" + +#: novaclient/api_versions.py:209 +#, python-format +msgid "" +"Invalid client version '%(version)s'. Major part must be one of: " +"'%(major)s'" +msgstr "" + +#: novaclient/api_versions.py:259 +msgid "Server doesn't support microversions" +msgstr "" + +#: novaclient/api_versions.py:263 +#, python-format +msgid "" +"The specified version isn't supported by server. The valid version range " +"is '%(min)s' to '%(max)s'" +msgstr "" + +#: novaclient/api_versions.py:276 +msgid "The server isn't backward compatible with Nova V2 REST API" +msgstr "" + +#: novaclient/api_versions.py:283 +#, python-format +msgid "" +"Server version is too old. The client valid version range is " +"'%(client_min)s' to '%(client_max)s'. The server valid version range is " +"'%(server_min)s' to '%(server_max)s'." +msgstr "" + +#: novaclient/api_versions.py:292 +#, python-format +msgid "" +"Server version is too new. The client valid version range is " +"'%(client_min)s' to '%(client_max)s'. The server valid version range is " +"'%(server_min)s' to '%(server_max)s'." +msgstr "" + +#: novaclient/client.py:488 +msgid "Found more than one valid endpoint. Use a more restrictive filter" +msgstr "" + +#: novaclient/client.py:494 +msgid "Could not find any suitable endpoint. Correct region?" +msgstr "" + +#: novaclient/client.py:526 +#, python-format +msgid "Authentication requires 'auth_url', which should be specified in '%s'" +msgstr "" + +#: novaclient/client.py:776 +msgid "The version should be explicit, not latest." +msgstr "" + +#: novaclient/shell.py:68 +#, python-format +msgid "%s must be a float" +msgstr "" + +#: novaclient/shell.py:71 +#, python-format +msgid "%s must be greater than 0" +msgstr "" + +#: novaclient/shell.py:135 +msgid "Unable to save empty management url/auth token" +msgstr "" + +#: novaclient/shell.py:219 +#, python-format +msgid "" +"error: %(errmsg)s\n" +"Try '%(mainp)s help %(subp)s' for more information.\n" +msgstr "" + +#: novaclient/shell.py:287 +msgid "Print debugging output" +msgstr "" + +#: novaclient/shell.py:294 +msgid "Use the auth token cache. Defaults to False if env[OS_CACHE] is not set." +msgstr "" + +#: novaclient/shell.py:301 +msgid "Print call timing info" +msgstr "" + +#: novaclient/shell.py:320 +msgid "Defaults to env[OS_TENANT_NAME]." +msgstr "" + +#: novaclient/shell.py:329 +msgid "Defaults to env[OS_TENANT_ID]." +msgstr "" + +#: novaclient/shell.py:339 +msgid "Defaults to env[OS_REGION_NAME]." +msgstr "" + +#: novaclient/shell.py:356 +msgid "Defaults to compute for most actions" +msgstr "" + +#: novaclient/shell.py:365 +msgid "Defaults to env[NOVA_SERVICE_NAME]" +msgstr "" + +#: novaclient/shell.py:374 +msgid "Defaults to env[NOVA_VOLUME_SERVICE_NAME]" +msgstr "" + +#: novaclient/shell.py:388 +msgid "Defaults to env[NOVA_ENDPOINT_TYPE], env[OS_ENDPOINT_TYPE] or " +msgstr "" + +#: novaclient/shell.py:407 +msgid "" +"Accepts X, X.Y (where X is major and Y is minor part) or \"X.latest\", " +"defaults to env[OS_COMPUTE_API_VERSION]." +msgstr "" + +#: novaclient/shell.py:459 +#, python-format +msgid " (Supported by API versions '%(start)s' - '%(end)s')" +msgstr "" + +#: novaclient/shell.py:639 +msgid "" +"You must provide a username or user id via --os-username, --os-user-id, " +"env[OS_USERNAME] or env[OS_USER_ID]" +msgstr "" + +#: novaclient/shell.py:645 novaclient/shell.py:688 +msgid "" +"You must provide a project name or project id via --os-project-name, " +"--os-project-id, env[OS_PROJECT_ID] or env[OS_PROJECT_NAME]. You may use " +"os-project and os-tenant interchangeably." +msgstr "" + +#: novaclient/shell.py:658 +msgid "" +"You must provide an auth url via either --os-auth-url or env[OS_AUTH_URL]" +" or specify an auth_system which defines a default url with --os-auth-" +"system or env[OS_AUTH_SYSTEM]" +msgstr "" + +#: novaclient/shell.py:697 +msgid "You must provide an auth url via either --os-auth-url or env[OS_AUTH_URL]" +msgstr "" + +#: novaclient/shell.py:723 +#, python-format +msgid "" +"The specified version isn't supported by client. The valid version range " +"is '%(min)s' to '%(max)s'" +msgstr "" + +#: novaclient/shell.py:818 +msgid "Invalid OpenStack Nova credentials." +msgstr "" + +#: novaclient/shell.py:820 +msgid "Unable to authorize user" +msgstr "" + +#: novaclient/shell.py:873 +#, python-format +msgid "'%s' is not a valid subcommand" +msgstr "" + +#: novaclient/utils.py:61 +#, python-format +msgid "" +"Hook '%(hook_name)s' is attempting to redefine attributes " +"'%(conflicting_keys)s'" +msgstr "" + +#: novaclient/utils.py:232 +#, python-format +msgid "No %(class)s with a name or ID of '%(name)s' exists." +msgstr "" + +#: novaclient/utils.py:237 +#, python-format +msgid "" +"Multiple %(class)s matches found for '%(name)s', use an ID to be more " +"specific." +msgstr "" + +#: novaclient/utils.py:353 +#, python-format +msgid "" +"Invalid key: \"%s\". Keys may only contain letters, numbers, spaces, " +"underscores, periods, colons and hyphens." +msgstr "" + +#: novaclient/openstack/common/cliutils.py:40 +#, python-format +msgid "Missing arguments: %s" +msgstr "" + +#: novaclient/openstack/common/cliutils.py:158 +#, python-format +msgid "" +"Field labels list %(labels)s has different number of elements than fields" +" list %(fields)s" +msgstr "" + +#: novaclient/openstack/common/apiclient/base.py:244 +#: novaclient/openstack/common/apiclient/base.py:401 +#, python-format +msgid "No %(name)s matching %(args)s." +msgstr "" + +#: novaclient/openstack/common/apiclient/client.py:250 +msgid "Cannot find endpoint or token for request" +msgstr "" + +#: novaclient/openstack/common/apiclient/client.py:381 +#, python-format +msgid "" +"Invalid %(api_name)s client version '%(version)s'. Must be one of: " +"%(version_map)s" +msgstr "" + +#: novaclient/openstack/common/apiclient/exceptions.py:84 +#, python-format +msgid "Authentication failed. Missing options: %s" +msgstr "" + +#: novaclient/openstack/common/apiclient/exceptions.py:93 +#, python-format +msgid "AuthSystemNotFound: %r" +msgstr "" + +#: novaclient/openstack/common/apiclient/exceptions.py:116 +#, python-format +msgid "AmbiguousEndpoints: %r" +msgstr "" + +#: novaclient/openstack/common/apiclient/exceptions.py:124 +msgid "HTTP Error" +msgstr "" + +#: novaclient/openstack/common/apiclient/exceptions.py:144 +msgid "HTTP Redirection" +msgstr "" + +#: novaclient/openstack/common/apiclient/exceptions.py:152 +msgid "HTTP Client Error" +msgstr "" + +#: novaclient/openstack/common/apiclient/exceptions.py:161 +msgid "HTTP Server Error" +msgstr "" + +#: novaclient/openstack/common/apiclient/exceptions.py:171 +msgid "Multiple Choices" +msgstr "" + +#: novaclient/openstack/common/apiclient/exceptions.py:180 +msgid "Bad Request" +msgstr "" + +#: novaclient/openstack/common/apiclient/exceptions.py:190 +msgid "Unauthorized" +msgstr "" + +#: novaclient/openstack/common/apiclient/exceptions.py:199 +msgid "Payment Required" +msgstr "" + +#: novaclient/openstack/common/apiclient/exceptions.py:209 +msgid "Forbidden" +msgstr "" + +#: novaclient/openstack/common/apiclient/exceptions.py:219 +msgid "Not Found" +msgstr "" + +#: novaclient/openstack/common/apiclient/exceptions.py:229 +msgid "Method Not Allowed" +msgstr "" + +#: novaclient/openstack/common/apiclient/exceptions.py:239 +msgid "Not Acceptable" +msgstr "" + +#: novaclient/openstack/common/apiclient/exceptions.py:248 +msgid "Proxy Authentication Required" +msgstr "" + +#: novaclient/openstack/common/apiclient/exceptions.py:257 +msgid "Request Timeout" +msgstr "" + +#: novaclient/openstack/common/apiclient/exceptions.py:267 +msgid "Conflict" +msgstr "" + +#: novaclient/openstack/common/apiclient/exceptions.py:277 +msgid "Gone" +msgstr "" + +#: novaclient/openstack/common/apiclient/exceptions.py:287 +msgid "Length Required" +msgstr "" + +#: novaclient/openstack/common/apiclient/exceptions.py:297 +msgid "Precondition Failed" +msgstr "" + +#: novaclient/openstack/common/apiclient/exceptions.py:306 +msgid "Request Entity Too Large" +msgstr "" + +#: novaclient/openstack/common/apiclient/exceptions.py:323 +msgid "Request-URI Too Long" +msgstr "" + +#: novaclient/openstack/common/apiclient/exceptions.py:333 +msgid "Unsupported Media Type" +msgstr "" + +#: novaclient/openstack/common/apiclient/exceptions.py:343 +msgid "Requested Range Not Satisfiable" +msgstr "" + +#: novaclient/openstack/common/apiclient/exceptions.py:352 +msgid "Expectation Failed" +msgstr "" + +#: novaclient/openstack/common/apiclient/exceptions.py:362 +msgid "Unprocessable Entity" +msgstr "" + +#: novaclient/openstack/common/apiclient/exceptions.py:371 +msgid "Internal Server Error" +msgstr "" + +#: novaclient/openstack/common/apiclient/exceptions.py:382 +msgid "Not Implemented" +msgstr "" + +#: novaclient/openstack/common/apiclient/exceptions.py:392 +msgid "Bad Gateway" +msgstr "" + +#: novaclient/openstack/common/apiclient/exceptions.py:401 +msgid "Service Unavailable" +msgstr "" + +#: novaclient/openstack/common/apiclient/exceptions.py:411 +msgid "Gateway Timeout" +msgstr "" + +#: novaclient/openstack/common/apiclient/exceptions.py:420 +msgid "HTTP Version Not Supported" +msgstr "" + +#: novaclient/openstack/common/apiclient/utils.py:86 +#, python-format +msgid "No %(name)s with a name or ID of '%(name_or_id)s' exists." +msgstr "" + +#: novaclient/openstack/common/apiclient/utils.py:94 +#, python-format +msgid "" +"Multiple %(name)s matches found for '%(name_or_id)s', use an ID to be " +"more specific." +msgstr "" + +#: novaclient/v2/flavor_access.py:40 +msgid "Unknown list options." +msgstr "" + +#: novaclient/v2/flavor_access.py:50 +msgid "Sorry, query by tenant not supported." +msgstr "" + +#: novaclient/v2/flavors.py:177 +msgid "Ram must be an integer." +msgstr "" + +#: novaclient/v2/flavors.py:181 +msgid "VCPUs must be an integer." +msgstr "" + +#: novaclient/v2/flavors.py:185 +msgid "Disk must be an integer." +msgstr "" + +#: novaclient/v2/flavors.py:193 +msgid "Swap must be an integer." +msgstr "" + +#: novaclient/v2/flavors.py:197 +msgid "Ephemeral must be an integer." +msgstr "" + +#: novaclient/v2/flavors.py:201 +msgid "rxtx_factor must be a float." +msgstr "" + +#: novaclient/v2/flavors.py:206 +msgid "is_public must be a boolean." +msgstr "" + +#: novaclient/v2/networks.py:121 +msgid "Must disassociate either host or project or both" +msgstr "" + +#: novaclient/v2/security_group_default_rules.py:47 +#: novaclient/v2/security_group_rules.py:52 +msgid "From port must be an integer." +msgstr "" + +#: novaclient/v2/security_group_default_rules.py:51 +#: novaclient/v2/security_group_rules.py:56 +msgid "To port must be an integer." +msgstr "" + +#: novaclient/v2/security_group_default_rules.py:53 +#: novaclient/v2/security_group_rules.py:58 +msgid "IP protocol must be 'tcp', 'udp', or 'icmp'." +msgstr "" + +#: novaclient/v2/servers.py:524 +msgid "Only one of 'v4-fixed-ip' and 'v6-fixed-ip' may be provided." +msgstr "" + +#: novaclient/v2/shell.py:133 +#, python-format +msgid "Invalid ephemeral argument '%s'." +msgstr "" + +#: novaclient/v2/shell.py:166 +msgid "you need to specify a Flavor ID " +msgstr "" + +#: novaclient/v2/shell.py:175 +msgid "num_instances should be >= 1" +msgstr "" + +#: novaclient/v2/shell.py:179 +msgid "Don't mix num-instances and max/min-count" +msgstr "" + +#: novaclient/v2/shell.py:183 +msgid "min_count should be >= 1" +msgstr "" + +#: novaclient/v2/shell.py:188 +msgid "max_count should be >= 1" +msgstr "" + +#: novaclient/v2/shell.py:193 +msgid "min_count should be <= max_count" +msgstr "" + +#: novaclient/v2/shell.py:205 novaclient/v2/shell.py:1561 +#, python-format +msgid "Can't open '%(src)s': %(exc)s" +msgstr "" + +#: novaclient/v2/shell.py:208 novaclient/v2/shell.py:1564 +#, python-format +msgid "" +"Invalid file argument '%s'. File arguments must be of the form '--file " +"'" +msgstr "" + +#: novaclient/v2/shell.py:222 +#, python-format +msgid "Can't open '%(user_data)s': %(exc)s" +msgstr "" + +#: novaclient/v2/shell.py:254 +msgid "" +"you need to specify at least one source ID (Image, Snapshot, or Volume), " +"a block device mapping or provide a set of properties to match against an" +" image" +msgstr "" + +#: novaclient/v2/shell.py:260 +msgid "" +"you can't mix old block devices (--block-device-mapping) with the new " +"ones (--block-device, --boot-volume, --snapshot, --ephemeral, --swap)" +msgstr "" + +#: novaclient/v2/shell.py:266 +#, python-format +msgid "" +"Invalid nic argument '%s'. Nic arguments must be of the form --nic , " +"with at minimum net-id or port-id (but not both) specified." +msgstr "" + +#: novaclient/v2/shell.py:333 +msgid "Name or ID of flavor (see 'nova flavor-list')." +msgstr "" + +#: novaclient/v2/shell.py:338 +msgid "Name or ID of image (see 'nova image-list'). " +msgstr "" + +#: novaclient/v2/shell.py:345 +msgid "Image metadata property (see 'nova image-show'). " +msgstr "" + +#: novaclient/v2/shell.py:350 +msgid "Volume ID to boot from." +msgstr "" + +#: novaclient/v2/shell.py:355 +msgid "Snapshot ID to boot from (will create a volume)." +msgstr "" + +#: novaclient/v2/shell.py:367 +msgid "Boot at least servers (limited by quota)." +msgstr "" + +#: novaclient/v2/shell.py:373 +msgid "Boot up to servers (limited by quota)." +msgstr "" + +#: novaclient/v2/shell.py:379 novaclient/v2/shell.py:1528 +msgid "" +"Record arbitrary key/value metadata to /meta_data.json on the metadata " +"server. Can be specified multiple times." +msgstr "" + +#: novaclient/v2/shell.py:387 +msgid "" +"Store arbitrary files from locally to on the new " +"server. Limited by the injected_files quota value." +msgstr "" + +#: novaclient/v2/shell.py:393 +msgid "" +"Key name of keypair that should be created earlier with the " +"command keypair-add" +msgstr "" + +#: novaclient/v2/shell.py:398 novaclient/v2/shell.py:1522 +msgid "Name for the new server" +msgstr "" + +#: novaclient/v2/shell.py:403 +msgid "user data file to pass to be exposed by the metadata server." +msgstr "" + +#: novaclient/v2/shell.py:411 +msgid "The availability zone for server placement." +msgstr "" + +#: novaclient/v2/shell.py:419 +msgid "Comma separated list of security group names." +msgstr "" + +#: novaclient/v2/shell.py:428 +msgid "" +"Block device mapping in the format =:::." +msgstr "" + +#: novaclient/v2/shell.py:439 +msgid "" +"Block device mapping with the keys: id=UUID (image_id, snapshot_id or " +"volume_id only if using source image, snapshot or volume) source=source " +"type (image, snapshot, volume or blank), dest=destination type of the " +"block device (volume or local), bus=device's bus (e.g. uml, lxc, virtio, " +"...; if omitted, hypervisor driver chooses a suitable default, honoured " +"only if device type is supplied) type=device type (e.g. disk, cdrom, ...;" +" defaults to 'disk') device=name of the device (e.g. vda, xda, ...; if " +"omitted, hypervisor driver chooses suitable device depending on selected " +"bus), size=size of the block device in MB(for swap) and in GB(for other " +"formats) (if omitted, hypervisor driver calculates size), format=device " +"will be formatted (e.g. swap, ntfs, ...; optional), bootindex=integer " +"used for ordering the boot disks (for image backed instances it is equal " +"to 0, for others need to be specified) and shutdown=shutdown behaviour " +"(either preserve or remove, for local destination set to remove)." +msgstr "" + +#: novaclient/v2/shell.py:464 +msgid "Create and attach a local swap block device of MB." +msgstr "" + +#: novaclient/v2/shell.py:470 +msgid "" +"Create and attach a local ephemeral block device of GB and format " +"it to ." +msgstr "" + +#: novaclient/v2/shell.py:478 +msgid "Send arbitrary key/value pairs to the scheduler for custom use." +msgstr "" + +#: novaclient/v2/shell.py:487 +msgid "" +"Create a NIC on the server. Specify option multiple times to create " +"multiple NICs. net-id: attach NIC to network with this UUID (either port-" +"id or net-id must be provided), v4-fixed-ip: IPv4 fixed address for NIC " +"(optional), v6-fixed-ip: IPv6 fixed address for NIC (optional), port-id: " +"attach NIC to port with this UUID (either port-id or net-id must be " +"provided)." +msgstr "" + +#: novaclient/v2/shell.py:500 +msgid "Enable config drive" +msgstr "" + +#: novaclient/v2/shell.py:506 +msgid "Report the new server boot progress until it completes." +msgstr "" + +#: novaclient/v2/shell.py:512 +msgid "Admin password for the instance" +msgstr "" + +#: novaclient/v2/shell.py:537 +msgid "UUID of the project to create the cloudpipe for." +msgstr "" + +#: novaclient/v2/shell.py:543 +msgid "New IP Address." +msgstr "" + +#: novaclient/v2/shell.py:544 +msgid "New Port." +msgstr "" + +#: novaclient/v2/shell.py:558 +#, python-format +msgid "" +"\r" +"Server %(action)s... %(progress)s%% complete" +msgstr "" + +#: novaclient/v2/shell.py:561 +#, python-format +msgid "" +"\r" +"Server %(action)s..." +msgstr "" + +#: novaclient/v2/shell.py:581 +msgid "" +"\n" +"Finished" +msgstr "" + +#: novaclient/v2/shell.py:585 +#, python-format +msgid "" +"\n" +"Error %s server" +msgstr "" + +#: novaclient/v2/shell.py:589 +#, python-format +msgid "" +"\n" +"Deleted %s server" +msgstr "" + +#: novaclient/v2/shell.py:670 +msgid "Get extra-specs of each flavor." +msgstr "" + +#: novaclient/v2/shell.py:676 +msgid "Display all flavors (Admin only)." +msgstr "" + +#: novaclient/v2/shell.py:689 +msgid "Name or ID of the flavor to delete" +msgstr "" + +#: novaclient/v2/shell.py:700 novaclient/v2/shell.py:760 +msgid "Name or ID of flavor" +msgstr "" + +#: novaclient/v2/shell.py:710 +msgid "Name of the new flavor" +msgstr "" + +#: novaclient/v2/shell.py:714 +msgid "" +"Unique ID (integer or UUID) for the new flavor. If specifying 'auto', a " +"UUID will be generated as id" +msgstr "" + +#: novaclient/v2/shell.py:719 +msgid "Memory size in MB" +msgstr "" + +#: novaclient/v2/shell.py:723 +msgid "Disk size in GB" +msgstr "" + +#: novaclient/v2/shell.py:727 +msgid "Ephemeral space size in GB (default 0)" +msgstr "" + +#: novaclient/v2/shell.py:732 +msgid "Number of vcpus" +msgstr "" + +#: novaclient/v2/shell.py:736 +msgid "Swap space size in MB (default 0)" +msgstr "" + +#: novaclient/v2/shell.py:741 +msgid "RX/TX factor (default 1)" +msgstr "" + +#: novaclient/v2/shell.py:746 +msgid "Make flavor accessible to the public (default true)" +msgstr "" + +#: novaclient/v2/shell.py:765 +msgid "Actions: 'set' or 'unset'" +msgstr "" + +#: novaclient/v2/shell.py:772 +msgid "Extra_specs to set/unset (only key is necessary on unset)" +msgstr "" + +#: novaclient/v2/shell.py:787 +msgid "Filter results by flavor name or ID." +msgstr "" + +#: novaclient/v2/shell.py:790 +msgid "Filter results by tenant ID." +msgstr "" + +#: novaclient/v2/shell.py:794 +msgid "Unable to filter results by both --flavor and --tenant." +msgstr "" + +#: novaclient/v2/shell.py:799 +msgid "Failed to get access list for public flavor type." +msgstr "" + +#: novaclient/v2/shell.py:805 +msgid "Unable to get all access lists. Specify --flavor or --tenant" +msgstr "" + +#: novaclient/v2/shell.py:820 +msgid "Flavor name or ID to add access for the given tenant." +msgstr "" + +#: novaclient/v2/shell.py:823 +msgid "Tenant ID to add flavor access for." +msgstr "" + +#: novaclient/v2/shell.py:835 +msgid "Flavor name or ID to remove access for the given tenant." +msgstr "" + +#: novaclient/v2/shell.py:838 +msgid "Tenant ID to remove flavor access for." +msgstr "" + +#: novaclient/v2/shell.py:849 +msgid "The ID of the project." +msgstr "" + +#: novaclient/v2/shell.py:869 novaclient/v2/shell.py:1328 +msgid "" +"Comma-separated list of fields to display. Use the show command to see " +"which fields are available." +msgstr "" + +#: novaclient/v2/shell.py:891 novaclient/v2/shell.py:901 +msgid "uuid or label of network" +msgstr "" + +#: novaclient/v2/shell.py:927 novaclient/v2/shell.py:941 +#: novaclient/v2/shell.py:954 +msgid "uuid of network" +msgstr "" + +#: novaclient/v2/shell.py:945 +msgid "Name of host" +msgstr "" + +#: novaclient/v2/shell.py:977 +msgid "Label for network" +msgstr "" + +#: novaclient/v2/shell.py:982 +msgid "IPv4 subnet (ex: 10.0.0.0/8)" +msgstr "" + +#: novaclient/v2/shell.py:986 +msgid "IPv6 subnet (ex: fe80::/64" +msgstr "" + +#: novaclient/v2/shell.py:992 +msgid "The vlan ID to be assigned to the project." +msgstr "" + +#: novaclient/v2/shell.py:998 +msgid "" +"First vlan ID to be assigned to the project. Subsequent vlan IDs will be " +"assigned incrementally." +msgstr "" + +#: novaclient/v2/shell.py:1005 +msgid "vpn start" +msgstr "" + +#: novaclient/v2/shell.py:1009 +msgid "gateway" +msgstr "" + +#: novaclient/v2/shell.py:1013 +msgid "IPv6 gateway" +msgstr "" + +#: novaclient/v2/shell.py:1018 +msgid "VIFs on this network are connected to this bridge." +msgstr "" + +#: novaclient/v2/shell.py:1023 +msgid "The bridge is connected to this interface." +msgstr "" + +#: novaclient/v2/shell.py:1028 +msgid "Multi host" +msgstr "" + +#: novaclient/v2/shell.py:1032 +msgid "First DNS" +msgstr "" + +#: novaclient/v2/shell.py:1037 +msgid "Second DNS" +msgstr "" + +#: novaclient/v2/shell.py:1042 +msgid "Network UUID" +msgstr "" + +#: novaclient/v2/shell.py:1047 +msgid "IPv4 subnet for fixed IPs (ex: 10.20.0.0/16)" +msgstr "" + +#: novaclient/v2/shell.py:1052 +msgid "Project ID" +msgstr "" + +#: novaclient/v2/shell.py:1057 +msgid "Network interface priority" +msgstr "" + +#: novaclient/v2/shell.py:1062 +msgid "MTU for network" +msgstr "" + +#: novaclient/v2/shell.py:1067 +msgid "Enable dhcp" +msgstr "" + +#: novaclient/v2/shell.py:1071 +msgid "Dhcp-server (defaults to gateway address)" +msgstr "" + +#: novaclient/v2/shell.py:1076 +msgid "Share address" +msgstr "" + +#: novaclient/v2/shell.py:1080 +msgid "Start of allowed addresses for instances" +msgstr "" + +#: novaclient/v2/shell.py:1084 +msgid "End of allowed addresses for instances" +msgstr "" + +#: novaclient/v2/shell.py:1090 +msgid "Must specify either fixed_range_v4 or fixed_range_v6" +msgstr "" + +#: novaclient/v2/shell.py:1111 +msgid "Number of images to return per request." +msgstr "" + +#: novaclient/v2/shell.py:1131 novaclient/v2/shell.py:1207 +msgid "Name or ID of image" +msgstr "" + +#: novaclient/v2/shell.py:1136 novaclient/v2/shell.py:1852 +#: novaclient/v2/contrib/metadata_extensions.py:29 +msgid "Actions: 'set' or 'delete'" +msgstr "" + +#: novaclient/v2/shell.py:1143 +msgid "Metadata to add/update or delete (only key is necessary on delete)" +msgstr "" + +#: novaclient/v2/shell.py:1216 +msgid "Name or ID of image(s)." +msgstr "" + +#: novaclient/v2/shell.py:1223 +#, python-format +msgid "Delete for image %(image)s failed: %(e)s" +msgstr "" + +#: novaclient/v2/shell.py:1232 +msgid "Only return servers that match reservation-id." +msgstr "" + +#: novaclient/v2/shell.py:1241 +msgid "Search with regular expression match by IP address." +msgstr "" + +#: novaclient/v2/shell.py:1247 +msgid "Search with regular expression match by IPv6 address." +msgstr "" + +#: novaclient/v2/shell.py:1253 +msgid "Search with regular expression match by name" +msgstr "" + +#: novaclient/v2/shell.py:1259 +msgid "Search with regular expression match by server name." +msgstr "" + +#: novaclient/v2/shell.py:1268 +msgid "Search by server status" +msgstr "" + +#: novaclient/v2/shell.py:1274 +msgid "Search by flavor name or ID" +msgstr "" + +#: novaclient/v2/shell.py:1280 +msgid "Search by image name or ID" +msgstr "" + +#: novaclient/v2/shell.py:1286 +msgid "Search servers by hostname to which they are assigned (Admin only)." +msgstr "" + +#: novaclient/v2/shell.py:1297 novaclient/v2/shell.py:2037 +#: novaclient/v2/shell.py:2805 +msgid "Display information from all tenants (Admin only)." +msgstr "" + +#: novaclient/v2/shell.py:1310 +msgid "" +"Display information from single tenant (Admin only). The --all-tenants " +"option must also be provided." +msgstr "" + +#: novaclient/v2/shell.py:1317 +msgid "Display information from single user (Admin only)." +msgstr "" + +#: novaclient/v2/shell.py:1323 +msgid "Only display deleted servers (Admin only)." +msgstr "" + +#: novaclient/v2/shell.py:1335 +msgid "Get only uuid and name." +msgstr "" + +#: novaclient/v2/shell.py:1410 +#, python-format +msgid "Unknown sort direction: %s" +msgstr "" + +#: novaclient/v2/shell.py:1461 +msgid "Perform a hard reboot (instead of a soft one)." +msgstr "" + +#: novaclient/v2/shell.py:1465 novaclient/v2/shell.py:1651 +#: novaclient/v2/shell.py:1664 novaclient/v2/shell.py:1945 +#: novaclient/v2/shell.py:3519 +msgid "Name or ID of server(s)." +msgstr "" + +#: novaclient/v2/shell.py:1471 +msgid "Poll until reboot is complete." +msgstr "" + +#: novaclient/v2/shell.py:1478 +#, python-format +msgid "Request to reboot server %s has been accepted." +msgstr "" + +#: novaclient/v2/shell.py:1479 +msgid "Unable to reboot the specified server(s)." +msgstr "" + +#: novaclient/v2/shell.py:1486 +#, python-format +msgid "Wait for server %s reboot." +msgstr "" + +#: novaclient/v2/shell.py:1487 +msgid "Wait for specified server(s) failed." +msgstr "" + +#: novaclient/v2/shell.py:1490 novaclient/v2/shell.py:1585 +#: novaclient/v2/shell.py:1607 novaclient/v2/shell.py:1613 +#: novaclient/v2/shell.py:1619 novaclient/v2/shell.py:1636 +#: novaclient/v2/shell.py:1642 novaclient/v2/shell.py:1674 +#: novaclient/v2/shell.py:1682 novaclient/v2/shell.py:1688 +#: novaclient/v2/shell.py:1694 novaclient/v2/shell.py:1700 +#: novaclient/v2/shell.py:1724 novaclient/v2/shell.py:1730 +#: novaclient/v2/shell.py:1736 novaclient/v2/shell.py:1742 +#: novaclient/v2/shell.py:1748 novaclient/v2/shell.py:1766 +#: novaclient/v2/shell.py:1772 novaclient/v2/shell.py:1785 +#: novaclient/v2/shell.py:1828 novaclient/v2/shell.py:1931 +#: novaclient/v2/shell.py:1974 novaclient/v2/shell.py:1985 +#: novaclient/v2/shell.py:2147 novaclient/v2/shell.py:2170 +#: novaclient/v2/shell.py:2189 novaclient/v2/shell.py:2299 +#: novaclient/v2/shell.py:2317 novaclient/v2/shell.py:2335 +#: novaclient/v2/shell.py:2353 novaclient/v2/shell.py:2375 +#: novaclient/v2/shell.py:2392 novaclient/v2/shell.py:2407 +#: novaclient/v2/shell.py:2420 novaclient/v2/shell.py:2432 +#: novaclient/v2/shell.py:2449 novaclient/v2/shell.py:2456 +#: novaclient/v2/shell.py:2468 novaclient/v2/shell.py:2479 +#: novaclient/v2/shell.py:2490 novaclient/v2/shell.py:3486 +#: novaclient/v2/shell.py:3542 novaclient/v2/shell.py:3857 +#: novaclient/v2/shell.py:4304 novaclient/v2/shell.py:4346 +#: novaclient/v2/shell.py:4356 novaclient/v2/shell.py:4381 +msgid "Name or ID of server." +msgstr "" + +#: novaclient/v2/shell.py:1491 +msgid "Name or ID of new image." +msgstr "" + +#: novaclient/v2/shell.py:1497 +msgid "Set the provided admin password on the rebuilt server." +msgstr "" + +#: novaclient/v2/shell.py:1506 +msgid "Report the server rebuild progress until it completes." +msgstr "" + +#: novaclient/v2/shell.py:1512 novaclient/v2/shell.py:1930 +msgid "Skips flavor/image lookups when showing servers" +msgstr "" + +#: novaclient/v2/shell.py:1517 +msgid "Preserve the default ephemeral storage partition on rebuild." +msgstr "" + +#: novaclient/v2/shell.py:1536 +msgid "" +"Store arbitrary files from locally to on the new " +"server. You may store up to 5 files." +msgstr "" + +#: novaclient/v2/shell.py:1578 +msgid "Name (old name) or ID of server." +msgstr "" + +#: novaclient/v2/shell.py:1579 +msgid "New name for the server." +msgstr "" + +#: novaclient/v2/shell.py:1589 +msgid "Name or ID of new flavor." +msgstr "" + +#: novaclient/v2/shell.py:1595 +msgid "Report the server resize progress until it completes." +msgstr "" + +#: novaclient/v2/shell.py:1625 +msgid "Report the server migration progress until it completes." +msgstr "" + +#: novaclient/v2/shell.py:1657 +#, python-format +msgid "Request to stop server %s has been accepted." +msgstr "" + +#: novaclient/v2/shell.py:1658 +msgid "Unable to stop the specified server(s)." +msgstr "" + +#: novaclient/v2/shell.py:1670 +#, python-format +msgid "Request to start server %s has been accepted." +msgstr "" + +#: novaclient/v2/shell.py:1671 +msgid "Unable to start the specified server(s)." +msgstr "" + +#: novaclient/v2/shell.py:1705 +msgid "The admin password to be set in the rescue environment." +msgstr "" + +#: novaclient/v2/shell.py:1710 +msgid "The image to rescue with." +msgstr "" + +#: novaclient/v2/shell.py:1757 +msgid "" +"Name or ID of a server for which the network cache should be refreshed " +"from neutron (Admin only)." +msgstr "" + +#: novaclient/v2/shell.py:1781 +msgid "Passwords do not match." +msgstr "" + +#: novaclient/v2/shell.py:1786 +msgid "Name of snapshot." +msgstr "" + +#: novaclient/v2/shell.py:1792 +msgid "Print image info." +msgstr "" + +#: novaclient/v2/shell.py:1798 +msgid "Report the snapshot progress and poll until image creation is complete." +msgstr "" + +#: novaclient/v2/shell.py:1829 +msgid "Name of the backup image." +msgstr "" + +#: novaclient/v2/shell.py:1832 +msgid "The backup type, like \"daily\" or \"weekly\"." +msgstr "" + +#: novaclient/v2/shell.py:1835 +msgid "Int parameter representing how many backups to keep around." +msgstr "" + +#: novaclient/v2/shell.py:1847 +msgid "Name or ID of server" +msgstr "" + +#: novaclient/v2/shell.py:1859 novaclient/v2/contrib/metadata_extensions.py:36 +msgid "Metadata to set or delete (only key is necessary on delete)" +msgstr "" + +#: novaclient/v2/shell.py:1896 +msgid "Flavor not found" +msgstr "" + +#: novaclient/v2/shell.py:1915 +msgid "Image not found" +msgstr "" + +#: novaclient/v2/shell.py:1917 +msgid "Attempt to boot from volume - no image supplied" +msgstr "" + +#: novaclient/v2/shell.py:1942 +msgid "Delete server(s) in another tenant by name (Admin only)." +msgstr "" + +#: novaclient/v2/shell.py:1952 +#, python-format +msgid "Request to delete server %s has been accepted." +msgstr "" + +#: novaclient/v2/shell.py:1953 +msgid "Unable to delete the specified server(s)." +msgstr "" + +#: novaclient/v2/shell.py:1978 +msgid "Network ID." +msgstr "" + +#: novaclient/v2/shell.py:1986 novaclient/v2/shell.py:2421 +#: novaclient/v2/shell.py:2433 novaclient/v2/shell.py:2450 +#: novaclient/v2/shell.py:2457 +msgid "IP Address." +msgstr "" + +#: novaclient/v2/shell.py:2062 +msgid "Name or ID of the volume." +msgstr "" + +#: novaclient/v2/shell.py:2074 +msgid "Size of volume in GB" +msgstr "" + +#: novaclient/v2/shell.py:2079 +msgid "Optional snapshot id to create the volume from. (Default=None)" +msgstr "" + +#: novaclient/v2/shell.py:2086 +msgid "Optional image id to create the volume from. (Default=None)" +msgstr "" + +#: novaclient/v2/shell.py:2092 +msgid "Optional volume name. (Default=None)" +msgstr "" + +#: novaclient/v2/shell.py:2100 +msgid "Optional volume description. (Default=None)" +msgstr "" + +#: novaclient/v2/shell.py:2108 +msgid "Optional volume type. (Default=None)" +msgstr "" + +#: novaclient/v2/shell.py:2114 +msgid "Optional Availability Zone for volume. (Default=None)" +msgstr "" + +#: novaclient/v2/shell.py:2132 +msgid "Name or ID of the volume(s) to delete." +msgstr "" + +#: novaclient/v2/shell.py:2140 +#, python-format +msgid "Delete for volume %(volume)s failed: %(e)s" +msgstr "" + +#: novaclient/v2/shell.py:2151 novaclient/v2/shell.py:2178 +msgid "ID of the volume to attach." +msgstr "" + +#: novaclient/v2/shell.py:2154 +msgid "" +"Name of the device e.g. /dev/vdb. Use \"auto\" for autoassign (if " +"supported)" +msgstr "" + +#: novaclient/v2/shell.py:2174 +msgid "Attachment ID of the volume." +msgstr "" + +#: novaclient/v2/shell.py:2193 +msgid "ID of the volume to detach." +msgstr "" + +#: novaclient/v2/shell.py:2212 +msgid "Name or ID of the snapshot." +msgstr "" + +#: novaclient/v2/shell.py:2223 +msgid "ID of the volume to snapshot" +msgstr "" + +#: novaclient/v2/shell.py:2227 +msgid "" +"Optional flag to indicate whether to snapshot a volume even if its " +"attached to a server. (Default=False)" +msgstr "" + +#: novaclient/v2/shell.py:2234 +msgid "Optional snapshot name. (Default=None)" +msgstr "" + +#: novaclient/v2/shell.py:2242 +msgid "Optional snapshot description. (Default=None)" +msgstr "" + +#: novaclient/v2/shell.py:2259 +msgid "Name or ID of the snapshot to delete." +msgstr "" + +#: novaclient/v2/shell.py:2281 +msgid "Name of the new volume type" +msgstr "" + +#: novaclient/v2/shell.py:2292 +msgid "Unique ID of the volume type to delete" +msgstr "" + +#: novaclient/v2/shell.py:2303 +msgid "Type of vnc console (\"novnc\" or \"xvpvnc\")." +msgstr "" + +#: novaclient/v2/shell.py:2321 +msgid "Type of spice console (\"spice-html5\")." +msgstr "" + +#: novaclient/v2/shell.py:2339 +msgid "Type of rdp console (\"rdp-html5\")." +msgstr "" + +#: novaclient/v2/shell.py:2356 +msgid "Type of serial console, default=\"serial\"." +msgstr "" + +#: novaclient/v2/shell.py:2361 +msgid "Invalid parameter value for 'console_type', currently supported 'serial'." +msgstr "" + +#: novaclient/v2/shell.py:2379 +msgid "" +"Private key (used locally to decrypt password) (Optional). When " +"specified, the command displays the clear (decrypted) VM password. When " +"not specified, the ciphered VM password is displayed." +msgstr "" + +#: novaclient/v2/shell.py:2412 +msgid "Length in lines to tail." +msgstr "" + +#: novaclient/v2/shell.py:2426 novaclient/v2/shell.py:2438 +msgid "Fixed IP Address to associate with." +msgstr "" + +#: novaclient/v2/shell.py:2472 novaclient/v2/shell.py:2483 +msgid "Name of Security Group." +msgstr "" + +#: novaclient/v2/shell.py:2501 +msgid "Name of Floating IP Pool. (Optional)" +msgstr "" + +#: novaclient/v2/shell.py:2509 +msgid "IP of Floating IP." +msgstr "" + +#: novaclient/v2/shell.py:2516 +#, python-format +msgid "Floating IP %s not found." +msgstr "" + +#: novaclient/v2/shell.py:2532 +msgid "Filter by host" +msgstr "" + +#: novaclient/v2/shell.py:2542 +msgid "Address range to create" +msgstr "" + +#: novaclient/v2/shell.py:2545 +msgid "Pool for new Floating IPs" +msgstr "" + +#: novaclient/v2/shell.py:2548 +msgid "Interface for new Floating IPs" +msgstr "" + +#: novaclient/v2/shell.py:2554 +msgid "Address range to delete" +msgstr "" + +#: novaclient/v2/shell.py:2575 novaclient/v2/shell.py:2594 +#: novaclient/v2/shell.py:2605 novaclient/v2/shell.py:2612 +#: novaclient/v2/shell.py:2618 novaclient/v2/shell.py:2634 +msgid "DNS domain" +msgstr "" + +#: novaclient/v2/shell.py:2576 novaclient/v2/shell.py:2592 +msgid "IP address" +msgstr "" + +#: novaclient/v2/shell.py:2577 novaclient/v2/shell.py:2593 +#: novaclient/v2/shell.py:2606 +msgid "DNS name" +msgstr "" + +#: novaclient/v2/shell.py:2582 +msgid "You must specify either --ip or --name" +msgstr "" + +#: novaclient/v2/shell.py:2598 +msgid "dns type (e.g. \"A\")" +msgstr "" + +#: novaclient/v2/shell.py:2623 +msgid "Limit access to this domain to servers in the specified availability zone." +msgstr "" + +#: novaclient/v2/shell.py:2637 +msgid "Limit access to this domain to users of the specified project." +msgstr "" + +#: novaclient/v2/shell.py:2690 +#, python-format +msgid "" +"Multiple security group matches found for name '%s', use an ID to be more" +" specific." +msgstr "" + +#: novaclient/v2/shell.py:2695 +#, python-format +msgid "Secgroup ID or name '%s' not found." +msgstr "" + +#: novaclient/v2/shell.py:2703 novaclient/v2/shell.py:2731 +#: novaclient/v2/shell.py:2773 novaclient/v2/shell.py:2788 +#: novaclient/v2/shell.py:2825 novaclient/v2/shell.py:2835 +#: novaclient/v2/shell.py:2874 +msgid "ID or name of security group." +msgstr "" + +#: novaclient/v2/shell.py:2707 novaclient/v2/shell.py:2735 +#: novaclient/v2/shell.py:2843 novaclient/v2/shell.py:2882 +#: novaclient/v2/shell.py:4488 novaclient/v2/shell.py:4512 +msgid "IP protocol (icmp, tcp, udp)." +msgstr "" + +#: novaclient/v2/shell.py:2711 novaclient/v2/shell.py:2739 +#: novaclient/v2/shell.py:2847 novaclient/v2/shell.py:2886 +#: novaclient/v2/shell.py:4492 novaclient/v2/shell.py:4516 +msgid "Port at start of range." +msgstr "" + +#: novaclient/v2/shell.py:2715 novaclient/v2/shell.py:2743 +#: novaclient/v2/shell.py:2851 novaclient/v2/shell.py:2890 +#: novaclient/v2/shell.py:4496 novaclient/v2/shell.py:4520 +msgid "Port at end of range." +msgstr "" + +#: novaclient/v2/shell.py:2716 novaclient/v2/shell.py:2744 +#: novaclient/v2/shell.py:4497 novaclient/v2/shell.py:4521 +msgid "CIDR for address range." +msgstr "" + +#: novaclient/v2/shell.py:2757 novaclient/v2/shell.py:2915 +#: novaclient/v2/shell.py:4535 +msgid "Rule not found" +msgstr "" + +#: novaclient/v2/shell.py:2760 novaclient/v2/shell.py:2774 +msgid "Name of security group." +msgstr "" + +#: novaclient/v2/shell.py:2763 novaclient/v2/shell.py:2777 +msgid "Description of security group." +msgstr "" + +#: novaclient/v2/shell.py:2839 novaclient/v2/shell.py:2878 +msgid "ID or name of source group." +msgstr "" + +#: novaclient/v2/shell.py:2861 novaclient/v2/shell.py:2900 +msgid "ip_proto, from_port, and to_port must be specified together" +msgstr "" + +#: novaclient/v2/shell.py:2928 +msgid "Name of key." +msgstr "" + +#: novaclient/v2/shell.py:2933 +msgid "Path to a public ssh key." +msgstr "" + +#: novaclient/v2/shell.py:2941 +msgid "Keypair type. Can be ssh or x509." +msgstr "" + +#: novaclient/v2/shell.py:2956 +#, python-format +msgid "Can't open or read '%(key)s': %(exc)s" +msgstr "" + +#: novaclient/v2/shell.py:2967 +msgid "Keypair name to delete." +msgstr "" + +#: novaclient/v2/shell.py:2995 +#, python-format +msgid "Public key: %s" +msgstr "" + +#: novaclient/v2/shell.py:3001 +msgid "Name or ID of keypair" +msgstr "" + +#: novaclient/v2/shell.py:3019 novaclient/v2/shell.py:3112 +msgid "Display information from single tenant (Admin only)." +msgstr "" + +#: novaclient/v2/shell.py:3025 novaclient/v2/shell.py:3118 +msgid "Include reservations count." +msgstr "" + +#: novaclient/v2/shell.py:3129 novaclient/v2/shell.py:3178 +msgid "Usage range start date ex 2012-01-20 (default: 4 weeks ago)" +msgstr "" + +#: novaclient/v2/shell.py:3134 novaclient/v2/shell.py:3182 +msgid "Usage range end date, ex 2012-01-20 (default: tomorrow)" +msgstr "" + +#: novaclient/v2/shell.py:3165 novaclient/v2/shell.py:3224 +#, python-format +msgid "Usage from %(start)s to %(end)s:" +msgstr "" + +#: novaclient/v2/shell.py:3188 +msgid "UUID of tenant to get usage for." +msgstr "" + +#: novaclient/v2/shell.py:3232 +msgid "None" +msgstr "" + +#: novaclient/v2/shell.py:3240 +msgid "Filename for the private key [Default: pk.pem]" +msgstr "" + +#: novaclient/v2/shell.py:3246 +msgid "Filename for the X.509 certificate [Default: cert.pem]" +msgstr "" + +#: novaclient/v2/shell.py:3251 +#, python-format +msgid "Unable to write privatekey - %s exists." +msgstr "" + +#: novaclient/v2/shell.py:3254 +#, python-format +msgid "Unable to write x509 cert - %s exists." +msgstr "" + +#: novaclient/v2/shell.py:3263 +#, python-format +msgid "Wrote private key to %s" +msgstr "" + +#: novaclient/v2/shell.py:3269 +#, python-format +msgid "Wrote x509 certificate to %s" +msgstr "" + +#: novaclient/v2/shell.py:3277 +msgid "Filename to write the x509 root cert." +msgstr "" + +#: novaclient/v2/shell.py:3281 +#, python-format +msgid "" +"Unable to write x509 root cert - %s" +" exists." +msgstr "" + +#: novaclient/v2/shell.py:3287 +#, python-format +msgid "Wrote x509 root cert to %s" +msgstr "" + +#: novaclient/v2/shell.py:3294 novaclient/v2/shell.py:3315 +msgid "type of hypervisor." +msgstr "" + +#: novaclient/v2/shell.py:3303 +msgid "type of os." +msgstr "" + +#: novaclient/v2/shell.py:3307 +msgid "type of architecture" +msgstr "" + +#: novaclient/v2/shell.py:3308 novaclient/v2/shell.py:3331 +msgid "version" +msgstr "" + +#: novaclient/v2/shell.py:3309 novaclient/v2/shell.py:3332 +msgid "url" +msgstr "" + +#: novaclient/v2/shell.py:3310 +msgid "md5 hash" +msgstr "" + +#: novaclient/v2/shell.py:3324 novaclient/v2/shell.py:3330 +msgid "id of the agent-build" +msgstr "" + +#: novaclient/v2/shell.py:3333 +msgid "md5hash" +msgstr "" + +#: novaclient/v2/shell.py:3353 novaclient/v2/shell.py:3381 +msgid "Name of aggregate." +msgstr "" + +#: novaclient/v2/shell.py:3359 +msgid "The availability zone of the aggregate (optional)." +msgstr "" + +#: novaclient/v2/shell.py:3369 +msgid "Name or ID of aggregate to delete." +msgstr "" + +#: novaclient/v2/shell.py:3374 +#, python-format +msgid "Aggregate %s has been successfully deleted." +msgstr "" + +#: novaclient/v2/shell.py:3380 novaclient/v2/shell.py:3402 +msgid "Name or ID of aggregate to update." +msgstr "" + +#: novaclient/v2/shell.py:3387 +msgid "The availability zone of the aggregate." +msgstr "" + +#: novaclient/v2/shell.py:3396 +#, python-format +msgid "Aggregate %s has been successfully updated." +msgstr "" + +#: novaclient/v2/shell.py:3409 +msgid "" +"Metadata to add/update to aggregate. Specify only the key to delete a " +"metadata item." +msgstr "" + +#: novaclient/v2/shell.py:3417 +msgid "metadata already exists" +msgstr "" + +#: novaclient/v2/shell.py:3420 +#, python-format +msgid "metadata key %s does not exist hence can not be deleted" +msgstr "" + +#: novaclient/v2/shell.py:3424 +#, python-format +msgid "Metadata has been successfully updated for aggregate %s." +msgstr "" + +#: novaclient/v2/shell.py:3431 novaclient/v2/shell.py:3447 +#: novaclient/v2/shell.py:3463 +msgid "Name or ID of aggregate." +msgstr "" + +#: novaclient/v2/shell.py:3434 +msgid "The host to add to the aggregate." +msgstr "" + +#: novaclient/v2/shell.py:3439 +#, python-format +msgid "Host %(host)s has been successfully added for aggregate %(aggregate_id)s " +msgstr "" + +#: novaclient/v2/shell.py:3450 +msgid "The host to remove from the aggregate." +msgstr "" + +#: novaclient/v2/shell.py:3455 +#, python-format +msgid "" +"Host %(host)s has been successfully removed from aggregate " +"%(aggregate_id)s " +msgstr "" + +#: novaclient/v2/shell.py:3489 +msgid "destination host name." +msgstr "" + +#: novaclient/v2/shell.py:3495 +msgid "True in case of block_migration. (Default=False:live_migration)" +msgstr "" + +#: novaclient/v2/shell.py:3505 +msgid "Allow overcommit.(Default=False)" +msgstr "" + +#: novaclient/v2/shell.py:3523 +msgid "" +"Request the server be reset to \"active\" state instead of \"error\" " +"state (the default)." +msgstr "" + +#: novaclient/v2/shell.py:3552 novaclient/v2/shell.py:3575 +#: novaclient/v2/shell.py:3583 novaclient/v2/shell.py:3602 +#: novaclient/v2/shell.py:3641 novaclient/v2/shell.py:3662 +#: novaclient/v2/shell.py:3686 novaclient/v2/contrib/metadata_extensions.py:24 +msgid "Name of host." +msgstr "" + +#: novaclient/v2/shell.py:3557 novaclient/v2/shell.py:3576 +#: novaclient/v2/shell.py:3584 novaclient/v2/shell.py:3603 +msgid "Service binary." +msgstr "" + +#: novaclient/v2/shell.py:3588 +msgid "Reason for disabling service." +msgstr "" + +#: novaclient/v2/shell.py:3607 +msgid "Unset the force state down of service" +msgstr "" + +#: novaclient/v2/shell.py:3616 +msgid "Id of service." +msgstr "" + +#: novaclient/v2/shell.py:3622 novaclient/v2/shell.py:3629 +#: novaclient/v2/shell.py:3635 +msgid "Fixed IP Address." +msgstr "" + +#: novaclient/v2/shell.py:3653 +msgid "" +"Filters the list, returning only those hosts in the availability zone " +"." +msgstr "" + +#: novaclient/v2/shell.py:3665 +msgid "Either enable or disable a host." +msgstr "" + +#: novaclient/v2/shell.py:3671 +msgid "Either put or resume host to/from maintenance." +msgstr "" + +#: novaclient/v2/shell.py:3690 +msgid "A power action: startup, reboot, or shutdown." +msgstr "" + +#: novaclient/v2/shell.py:3706 +msgid "List hypervisors matching the given ." +msgstr "" + +#: novaclient/v2/shell.py:3721 +msgid "The hypervisor hostname (or pattern) to search for." +msgstr "" + +#: novaclient/v2/shell.py:3750 +msgid "Name or ID of the hypervisor to show the details of." +msgstr "" + +#: novaclient/v2/shell.py:3753 +msgid "Wrap the output to a specified length. Default is 40 or 0 to disable" +msgstr "" + +#: novaclient/v2/shell.py:3764 +msgid "Name or ID of the hypervisor to show the uptime of." +msgstr "" + +#: novaclient/v2/shell.py:3811 +#, python-format +msgid "" +"WARNING: %(service)s has no endpoint in %(region)s! Available endpoints " +"for this service:" +msgstr "" + +#: novaclient/v2/shell.py:3839 +msgid "wrap PKI tokens to a specified length, or 0 to disable" +msgstr "" + +#: novaclient/v2/shell.py:3864 +msgid "Optional flag to indicate which port to use for ssh. (Default=22)" +msgstr "" + +#: novaclient/v2/shell.py:3878 +msgid "" +"Optional flag to indicate which IP type to use. Possible values includes" +" fixed and floating (the Default)." +msgstr "" + +#: novaclient/v2/shell.py:3882 +msgid "Network to use for the ssh." +msgstr "" + +#: novaclient/v2/shell.py:3888 +msgid "" +"Optional flag to indicate whether to use an IPv6 address attached to a " +"server. (Defaults to IPv4 address)" +msgstr "" + +#: novaclient/v2/shell.py:3891 +msgid "Login to use." +msgstr "" + +#: novaclient/v2/shell.py:3896 +msgid "Private key file, same as the -i option to the ssh command." +msgstr "" + +#: novaclient/v2/shell.py:3901 +msgid "Extra options to pass to ssh. see: man ssh" +msgstr "" + +#: novaclient/v2/shell.py:3919 +#, python-format +msgid "Server '%(server)s' is not attached to network '%(network)s'" +msgstr "" + +#: novaclient/v2/shell.py:3925 +#, python-format +msgid "" +"Server '%(server)s' is attached to more than one network. Please pick the" +" network to use." +msgstr "" + +#: novaclient/v2/shell.py:3929 +#, python-format +msgid "Server '%(server)s' is not attached to any network." +msgstr "" + +#: novaclient/v2/shell.py:3942 +#, python-format +msgid "" +"No address that would match network '%(network)s' and type " +"'%(address_type)s' of version %(pretty_version)s has been found for " +"server '%(server)s'." +msgstr "" + +#: novaclient/v2/shell.py:3949 +#, python-format +msgid "More than one %(pretty_version)s %(address_type)s addressfound." +msgstr "" + +#: novaclient/v2/shell.py:4012 +msgid "ID of tenant to list the quotas for." +msgstr "" + +#: novaclient/v2/shell.py:4017 +msgid "ID of user to list the quotas for." +msgstr "" + +#: novaclient/v2/shell.py:4036 +msgid "ID of tenant to list the default quotas for." +msgstr "" + +#: novaclient/v2/shell.py:4054 +msgid "ID of tenant to set the quotas for." +msgstr "" + +#: novaclient/v2/shell.py:4059 +msgid "ID of user to set the quotas for." +msgstr "" + +#: novaclient/v2/shell.py:4064 novaclient/v2/shell.py:4205 +msgid "New value for the \"instances\" quota." +msgstr "" + +#: novaclient/v2/shell.py:4069 novaclient/v2/shell.py:4210 +msgid "New value for the \"cores\" quota." +msgstr "" + +#: novaclient/v2/shell.py:4074 novaclient/v2/shell.py:4215 +msgid "New value for the \"ram\" quota." +msgstr "" + +#: novaclient/v2/shell.py:4080 novaclient/v2/shell.py:4221 +msgid "New value for the \"floating-ips\" quota." +msgstr "" + +#: novaclient/v2/shell.py:4090 novaclient/v2/shell.py:4231 +msgid "New value for the \"fixed-ips\" quota." +msgstr "" + +#: novaclient/v2/shell.py:4096 novaclient/v2/shell.py:4237 +msgid "New value for the \"metadata-items\" quota." +msgstr "" + +#: novaclient/v2/shell.py:4106 novaclient/v2/shell.py:4247 +msgid "New value for the \"injected-files\" quota." +msgstr "" + +#: novaclient/v2/shell.py:4116 novaclient/v2/shell.py:4257 +msgid "New value for the \"injected-file-content-bytes\" quota." +msgstr "" + +#: novaclient/v2/shell.py:4126 novaclient/v2/shell.py:4267 +msgid "New value for the \"injected-file-path-bytes\" quota." +msgstr "" + +#: novaclient/v2/shell.py:4132 novaclient/v2/shell.py:4273 +msgid "New value for the \"key-pairs\" quota." +msgstr "" + +#: novaclient/v2/shell.py:4138 novaclient/v2/shell.py:4279 +msgid "New value for the \"security-groups\" quota." +msgstr "" + +#: novaclient/v2/shell.py:4144 novaclient/v2/shell.py:4285 +msgid "New value for the \"security-group-rules\" quota." +msgstr "" + +#: novaclient/v2/shell.py:4150 novaclient/v2/shell.py:4291 +msgid "New value for the \"server-groups\" quota." +msgstr "" + +#: novaclient/v2/shell.py:4156 novaclient/v2/shell.py:4297 +msgid "New value for the \"server-group-members\" quota." +msgstr "" + +#: novaclient/v2/shell.py:4162 +msgid "" +"Whether force update the quota even if the already used and reserved " +"exceeds the new quota" +msgstr "" + +#: novaclient/v2/shell.py:4174 +msgid "ID of tenant to delete quota for." +msgstr "" + +#: novaclient/v2/shell.py:4178 +msgid "ID of user to delete quota for." +msgstr "" + +#: novaclient/v2/shell.py:4190 +msgid "Name of quota class to list the quotas for." +msgstr "" + +#: novaclient/v2/shell.py:4200 +msgid "Name of quota class to set the quotas for." +msgstr "" + +#: novaclient/v2/shell.py:4307 +msgid "" +"Name or ID of the target host. If no host is specified, the scheduler " +"will choose one." +msgstr "" + +#: novaclient/v2/shell.py:4313 +msgid "" +"Set the provided admin password on the evacuated server. Not applicable " +"with on-shared-storage flag" +msgstr "" + +#: novaclient/v2/shell.py:4320 +msgid "Specifies whether server files are located on shared storage" +msgstr "" + +#: novaclient/v2/shell.py:4360 novaclient/v2/shell.py:4382 +msgid "Port ID." +msgstr "" + +#: novaclient/v2/shell.py:4365 +msgid "Network ID" +msgstr "" + +#: novaclient/v2/shell.py:4370 +msgid "Requested fixed IP." +msgstr "" + +#: novaclient/v2/shell.py:4470 +msgid "Display server groups from all projects (Admin only)." +msgstr "" + +#: novaclient/v2/shell.py:4538 +msgid "Server group name." +msgstr "" + +#: novaclient/v2/shell.py:4554 +msgid "Policies for the server groups (\"affinity\" or \"anti-affinity\")" +msgstr "" + +#: novaclient/v2/shell.py:4563 +msgid "at least one policy must be specified" +msgstr "" + +#: novaclient/v2/shell.py:4575 +msgid "Unique ID(s) of the server group to delete" +msgstr "" + +#: novaclient/v2/shell.py:4583 +#, python-format +msgid "Server group %s has been successfully deleted." +msgstr "" + +#: novaclient/v2/shell.py:4586 +#, python-format +msgid "Delete for server group %(sg)s failed: %(e)s" +msgstr "" + +#: novaclient/v2/shell.py:4589 +msgid "Unable to delete any of the specified server groups." +msgstr "" + +#: novaclient/v2/shell.py:4596 +msgid "Unique ID of the server group to get" +msgstr "" + +#: novaclient/v2/contrib/baremetal.py:158 +msgid "Name of nova compute host which will control this baremetal node" +msgstr "" + +#: novaclient/v2/contrib/baremetal.py:164 +msgid "Number of CPUs in the node" +msgstr "" + +#: novaclient/v2/contrib/baremetal.py:169 +msgid "Megabytes of RAM in the node" +msgstr "" + +#: novaclient/v2/contrib/baremetal.py:174 +msgid "Gigabytes of local storage in the node" +msgstr "" + +#: novaclient/v2/contrib/baremetal.py:178 +msgid "MAC address to provision the node" +msgstr "" + +#: novaclient/v2/contrib/baremetal.py:182 +msgid "Power management IP for the node" +msgstr "" + +#: novaclient/v2/contrib/baremetal.py:186 +msgid "Username for the node's power management" +msgstr "" + +#: novaclient/v2/contrib/baremetal.py:190 +msgid "Password for the node's power management" +msgstr "" + +#: novaclient/v2/contrib/baremetal.py:195 +msgid "ShellInABox port?" +msgstr "" + +#: novaclient/v2/contrib/baremetal.py:211 +msgid "ID of the node to delete." +msgstr "" + +#: novaclient/v2/contrib/baremetal.py:293 +#: novaclient/v2/contrib/baremetal.py:303 +#: novaclient/v2/contrib/baremetal.py:325 +#: novaclient/v2/contrib/baremetal.py:335 +msgid "ID of node" +msgstr "" + +#: novaclient/v2/contrib/baremetal.py:307 +#: novaclient/v2/contrib/baremetal.py:329 +msgid "MAC address of interface" +msgstr "" + +#: novaclient/v2/contrib/baremetal.py:312 +msgid "OpenFlow Datapath ID of interface" +msgstr "" + +#: novaclient/v2/contrib/baremetal.py:317 +msgid "OpenFlow port number of interface" +msgstr "" + +#: novaclient/v2/contrib/cells.py:53 +msgid "Name of the cell." +msgstr "" + +#: novaclient/v2/contrib/cells.py:63 +msgid "Name of the cell to get the capacities." +msgstr "" + +#: novaclient/v2/contrib/cells.py:68 +#, python-format +msgid "Ram Available: %s MB" +msgstr "" + +#: novaclient/v2/contrib/cells.py:71 +#, python-format +msgid "" +"\n" +"Disk Available: %s MB" +msgstr "" + +#: novaclient/v2/contrib/host_evacuate.py:34 +#, python-format +msgid "Error while evacuating instance: %s" +msgstr "" + +#: novaclient/v2/contrib/host_evacuate.py:46 +msgid "" +"Name of target host. If no host is specified the scheduler will select a " +"target." +msgstr "" + +#: novaclient/v2/contrib/host_evacuate.py:53 +msgid "Specifies whether all instances files are on shared storage" +msgstr "" + +#: novaclient/v2/contrib/host_evacuate_live.py:35 +#, python-format +msgid "Error while live migrating instance: %s" +msgstr "" + +#: novaclient/v2/contrib/host_evacuate_live.py:46 +msgid "Name of target host." +msgstr "" + +#: novaclient/v2/contrib/host_evacuate_live.py:51 +msgid "Enable block migration." +msgstr "" + +#: novaclient/v2/contrib/host_evacuate_live.py:56 +msgid "Enable disk overcommit." +msgstr "" + +#: novaclient/v2/contrib/host_servers_migrate.py:33 +#, python-format +msgid "Error while migrating instance: %s" +msgstr "" + +#: novaclient/v2/contrib/instance_action.py:47 +msgid "Name or UUID of the server to show an action for." +msgstr "" + +#: novaclient/v2/contrib/instance_action.py:51 +msgid "Request ID of the action to get." +msgstr "" + +#: novaclient/v2/contrib/instance_action.py:65 +msgid "Name or UUID of the server to list actions for." +msgstr "" + +#: novaclient/v2/contrib/migrations.py:61 +msgid "Fetch migrations for the given host." +msgstr "" + +#: novaclient/v2/contrib/migrations.py:66 +msgid "Fetch migrations for the given status." +msgstr "" + +#: novaclient/v2/contrib/migrations.py:71 +msgid "Fetch migrations for the given cell_name." +msgstr "" + +#: novaclient/v2/contrib/tenant_networks.py:79 +#: novaclient/v2/contrib/tenant_networks.py:94 +msgid "Network label (ex. my_new_network)" +msgstr "" + +#: novaclient/v2/contrib/tenant_networks.py:83 +#: novaclient/v2/contrib/tenant_networks.py:98 +msgid "IP block to allocate from (ex. 172.16.0.0/24 or 2001:DB8::/64)" +msgstr "" + diff --git a/setup.cfg b/setup.cfg index 07cb81b0f..357594759 100644 --- a/setup.cfg +++ b/setup.cfg @@ -36,6 +36,20 @@ all_files = 1 [upload_sphinx] upload-dir = doc/build/html +[compile_catalog] +domain = python-novaclient +directory = python-novaclient/locale + +[update_catalog] +domain = python-novaclient +output_dir = python-novaclient/locale +input_file = python-novaclient/locale/python-novaclient.pot + +[extract_messages] +keywords = _ gettext ngettext l_ lazy_gettext +mapping_file = babel.cfg +output_file = python-novaclient/locale/python-novaclient.pot + [wheel] universal = 1 From d0b44893ac4fc3cda215cba20b8b9330e9fa92c3 Mon Sep 17 00:00:00 2001 From: melanie witt Date: Tue, 8 Sep 2015 20:08:37 +0000 Subject: [PATCH 0829/1705] Add unit tests for different help commands Related-Bug: #1491677 Change-Id: I7e6b74e60dafa0e869a5b3355793cda1cf08c389 --- novaclient/tests/unit/test_shell.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/novaclient/tests/unit/test_shell.py b/novaclient/tests/unit/test_shell.py index 56eb7f0c3..923e70123 100644 --- a/novaclient/tests/unit/test_shell.py +++ b/novaclient/tests/unit/test_shell.py @@ -162,17 +162,27 @@ def test_invalid_timeout(self): for r in required: self.assertIn(r, stderr) - def test_help(self): + def _test_help(self, command): required = [ '.*?^usage: ', '.*?^\s+set-password\s+Change the admin password', '.*?^See "nova help COMMAND" for help on a specific command', ] - stdout, stderr = self.shell('help') + stdout, stderr = self.shell(command) for r in required: self.assertThat((stdout + stderr), matchers.MatchesRegex(r, re.DOTALL | re.MULTILINE)) + def test_help(self): + self._test_help('help') + + def test_help_option(self): + self._test_help('--help') + self._test_help('-h') + + def test_help_no_subcommand(self): + self._test_help('') + def test_help_on_subcommand(self): required = [ '.*?^usage: nova set-password', From b2221c8a7020ac66197360f594ba58b27434cf7d Mon Sep 17 00:00:00 2001 From: Daniel Wallace Date: Tue, 8 Sep 2015 20:13:39 -0500 Subject: [PATCH 0830/1705] Fix bugs with rackspace not all apis have the versions available Rackspace api does not open up for querying versions. This causes it to still break when using the rackspace-auth-plugin add tests for api_version Unauthorized make sure that the list can still run even if the api_version check is unauthorized. Closes-Bug: #1493974 Closes-Bug: #1491579 Change-Id: I038b84bad5b747a0688aef989f1337aee835b945 --- novaclient/tests/unit/v2/test_versions.py | 8 ++++++++ novaclient/v2/versions.py | 22 ++++++++++++---------- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/novaclient/tests/unit/v2/test_versions.py b/novaclient/tests/unit/v2/test_versions.py index da348751f..aa8e84a6e 100644 --- a/novaclient/tests/unit/v2/test_versions.py +++ b/novaclient/tests/unit/v2/test_versions.py @@ -73,3 +73,11 @@ def test_get_current_with_session_client(self, mock_is_session_client): def test_get_current_with_rax_workaround(self, session, get): self.cs.callback = [] self.assertIsNone(self.cs.versions.get_current()) + + @mock.patch.object(versions.VersionManager, '_is_session_client', + return_value=False) + @mock.patch.object(versions.VersionManager, '_list', + side_effect=exc.Unauthorized("401 RAX")) + def test_get_current_with_rax_auth_plugin_workaround(self, session, _list): + self.cs.callback = [] + self.assertIsNone(self.cs.versions.get_current()) diff --git a/novaclient/v2/versions.py b/novaclient/v2/versions.py index 17a20df99..796ea7df5 100644 --- a/novaclient/v2/versions.py +++ b/novaclient/v2/versions.py @@ -37,7 +37,7 @@ class VersionManager(base.ManagerWithFind): def _is_session_client(self): return isinstance(self.api.client, client.SessionClient) - def get_current(self): + def _get_current(self): """Returns info about current version.""" if self._is_session_client(): url = self.api.client.get_endpoint().rsplit("/", 1)[0] @@ -46,15 +46,7 @@ def get_current(self): # that's actually a 300 redirect to /v2/... because of how # paste works. So adding the end slash is really important. url = "%s/" % url - try: - return self._get(url, "version") - except exc.Unauthorized: - # NOTE(sdague): RAX's repose configuration blocks - # access to the versioned endpoint, which is - # definitely non-compliant behavior. However, there is - # no defcore test for this yet. Remove this code block - # once we land things in defcore. - return None + return self._get(url, "version") else: # NOTE(andreykurilin): HTTPClient doesn't have ability to send get # request without token in the url, so `self._get` doesn't work. @@ -65,6 +57,16 @@ def get_current(self): if link["href"].rstrip('/') == url: return version + def get_current(self): + try: + return self._get_current() + except exc.Unauthorized: + # NOTE(sdague): RAX's repose configuration blocks access to the + # versioned endpoint, which is definitely non-compliant behavior. + # However, there is no defcore test for this yet. Remove this code + # block once we land things in defcore. + return None + def list(self): """List all versions.""" From a96e9d57c56e53f4e02701d2ae9f9194bb6e3d5b Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Wed, 9 Sep 2015 14:02:42 +0300 Subject: [PATCH 0831/1705] Restrict direct usage of novaclient.v2.client A lot of project uses incorrect import of versioned novaclient client obj (i.e. novaclient.v2.client.Client). It leads to unability to change interface of such inner classes. This patch updates docs to include warning note and add warning message to `novaclient.v2.client.Client` object. Change-Id: Ifeba391716d3d51d6a75a53cad405e1ec595e27b Related-Bug: #1493576 --- README.rst | 2 +- doc/source/api.rst | 37 +++++++++++++++++++++++++---- novaclient/client.py | 25 ++++++++++++++++++-- novaclient/v2/client.py | 52 +++++++++++------------------------------ 4 files changed, 69 insertions(+), 47 deletions(-) diff --git a/README.rst b/README.rst index 00a361de8..9725cd521 100644 --- a/README.rst +++ b/README.rst @@ -1,5 +1,5 @@ Python bindings to the OpenStack Nova API -================================================== +========================================= This is a client for the OpenStack Nova API. There's a Python API (the ``novaclient`` module), and a command-line script (``nova``). Each diff --git a/doc/source/api.rst b/doc/source/api.rst index 8bcb219b3..ded67ed60 100644 --- a/doc/source/api.rst +++ b/doc/source/api.rst @@ -1,5 +1,5 @@ The :mod:`novaclient` Python API -================================== +================================ .. module:: novaclient :synopsis: A client for the OpenStack Nova API. @@ -14,7 +14,10 @@ First create a client instance with your credentials:: >>> from novaclient import client >>> nova = client.Client(VERSION, USERNAME, PASSWORD, PROJECT_ID, AUTH_URL) -Here ``VERSION`` can be: ``1.1``, ``2``. +Here ``VERSION`` can be a string or ``novaclient.api_versions.APIVersion`` obj. +If you prefer string value, you can use ``1.1`` (deprecated now), ``2`` or +``2.X`` (where X is a microversion). + Alternatively, you can create a client instance using the keystoneclient session API:: @@ -23,9 +26,9 @@ session API:: >>> from keystoneclient import session >>> from novaclient import client >>> auth = v2.Password(auth_url=AUTH_URL, - username=USERNAME, - password=PASSWORD, - tenant_name=PROJECT_ID) + ... username=USERNAME, + ... password=PASSWORD, + ... tenant_name=PROJECT_ID) >>> sess = session.Session(auth=auth) >>> nova = client.Client(VERSION, session=sess) @@ -33,6 +36,23 @@ For more information on this keystoneclient API, see `Using Sessions`_. .. _Using Sessions: http://docs.openstack.org/developer/python-keystoneclient/using-sessions.html +It is also possible to use an instance as a context manager in which case +there will be a session kept alive for the duration of the with statement:: + + >>> from novaclient import client + >>> with client.Client(VERSION, USERNAME, PASSWORD, + ... PROJECT_ID, AUTH_URL) as nova: + ... nova.servers.list() + ... nova.flavors.list() + ... + +It is also possible to have a permanent (process-long) connection pool, +by passing a connection_pool=True:: + + >>> from novaclient import client + >>> nova = client.Client(VERSION, USERNAME, PASSWORD, PROJECT_ID, + ... AUTH_URL, connection_pool=True) + Then call methods on its managers:: >>> nova.servers.list() @@ -51,6 +71,13 @@ Then call methods on its managers:: >>> nova.servers.create("my-server", flavor=fl) +.. warning:: Direct initialization of ``novaclient.v2.client.Client`` object + can cause you to "shoot yourself in the foot". See launchpad bug-report + `1493576`_ for more details. + +.. _1493576: https://launchpad.net/bugs/1493576 + + Reference --------- diff --git a/novaclient/client.py b/novaclient/client.py index 159255540..f7ac8702a 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -785,6 +785,27 @@ def get_client_class(version): def Client(version, *args, **kwargs): - """Initialize client object based on given version.""" + """Initialize client object based on given version. + + HOW-TO: + The simplest way to create a client instance is initialization with your + credentials:: + + >>> from novaclient import client + >>> nova = client.Client(VERSION, USERNAME, PASSWORD, + ... PROJECT_ID, AUTH_URL) + + Here ``VERSION`` can be a string or + ``novaclient.api_versions.APIVersion`` obj. If you prefer string value, + you can use ``1.1`` (deprecated now), ``2`` or ``2.X`` + (where X is a microversion). + + + Alternatively, you can create a client instance using the keystoneclient + session API. See "The novaclient Python API" page at + python-novaclient's doc. + """ api_version, client_class = _get_client_class_and_version(version) - return client_class(api_version=api_version, *args, **kwargs) + kwargs.pop("direct_use", None) + return client_class(api_version=api_version, direct_use=False, + *args, **kwargs) diff --git a/novaclient/v2/client.py b/novaclient/v2/client.py index 1c195d5a1..d48c062f8 100644 --- a/novaclient/v2/client.py +++ b/novaclient/v2/client.py @@ -14,6 +14,7 @@ # under the License. from novaclient import client +from novaclient.i18n import _LW from novaclient.v2 import agents from novaclient.v2 import aggregates from novaclient.v2 import availability_zones @@ -53,44 +54,8 @@ class Client(object): """ Top-level object to access the OpenStack Compute API. - Create an instance with your creds:: - - >>> client = Client(USERNAME, PASSWORD, PROJECT_ID, AUTH_URL) - - Or, alternatively, you can create a client instance using the - keystoneclient.session API:: - - >>> from keystoneclient.auth.identity import v2 - >>> from keystoneclient import session - >>> from novaclient import client - >>> auth = v2.Password(auth_url=AUTH_URL, - username=USERNAME, - password=PASSWORD, - tenant_name=PROJECT_ID) - >>> sess = session.Session(auth=auth) - >>> nova = client.Client(VERSION, session=sess) - - Then call methods on its managers:: - - >>> nova.servers.list() - ... - >>> nova.flavors.list() - ... - - It is also possible to use an instance as a context manager in which - case there will be a session kept alive for the duration of the with - statement:: - - >>> with Client(USERNAME, PASSWORD, PROJECT_ID, AUTH_URL) as client: - ... client.servers.list() - ... client.flavors.list() - ... - - It is also possible to have a permanent (process-long) connection pool, - by passing a connection_pool=True:: - - >>> client = Client(USERNAME, PASSWORD, PROJECT_ID, - ... AUTH_URL, connection_pool=True) + .. warning:: All scripts and projects should not initialize this class + directly. It should be done via `novaclient.client.Client` interface. """ def __init__(self, username=None, api_key=None, project_id=None, @@ -103,7 +68,7 @@ def __init__(self, username=None, api_key=None, project_id=None, auth_system='keystone', auth_plugin=None, auth_token=None, cacert=None, tenant_id=None, user_id=None, connection_pool=False, session=None, auth=None, - api_version=None, **kwargs): + api_version=None, direct_use=True, **kwargs): """ :param str username: Username :param str api_key: API Key @@ -136,6 +101,15 @@ def __init__(self, username=None, api_key=None, project_id=None, :param api_version: Compute API version :type api_version: novaclient.api_versions.APIVersion """ + if direct_use: + import warnings + + warnings.warn( + _LW("'novaclient.v2.client.Client' is not designed to be " + "initialized directly. It is inner class of novaclient. " + "Please, use 'novaclient.client.Client' instead. " + "Related lp bug-report: 1493576")) + # FIXME(comstud): Rename the api_key argument above when we # know it's not being used as keyword argument From 147a1a6ee421f9a45a562f013e233d29d43258e4 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Wed, 9 Sep 2015 17:18:14 +0300 Subject: [PATCH 0832/1705] Deprecate 'novaclient.client.get_client_class' This method is redundant and blocks versioned client classes to change interface. `novaclient.client.Client` should be used instead. Releated-Bug: #1493576 Change-Id: I73cea2c6062419d75646e9239c6194f4d1ffd2b1 --- novaclient/client.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/novaclient/client.py b/novaclient/client.py index f7ac8702a..399d98e22 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -30,6 +30,7 @@ import os import pkgutil import re +import warnings from keystoneclient import adapter from keystoneclient import session @@ -48,7 +49,7 @@ from novaclient import api_versions from novaclient import exceptions from novaclient import extension as ext -from novaclient.i18n import _ +from novaclient.i18n import _, _LW from novaclient import service_catalog from novaclient import utils @@ -780,6 +781,8 @@ def _get_client_class_and_version(version): def get_client_class(version): """Returns Client class based on given version.""" + warnings.warn(_LW("'get_client_class' is deprecated. " + "Please use `novaclient.client.Client` instead.")) _api_version, client_class = _get_client_class_and_version(version) return client_class From 599ee29536aaa2ae7f9a7426e653cefddb229743 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Thu, 10 Sep 2015 15:16:54 +0300 Subject: [PATCH 0833/1705] Set api_version to 2.0 by default for v2.Client `novaclient.v2.client.Client` class should use V2.0 API by default, if api_version argument is empty. Change-Id: Id44a68f5d52f6f6055c3abe520bb916329552d09 --- novaclient/v2/client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/novaclient/v2/client.py b/novaclient/v2/client.py index d48c062f8..971506149 100644 --- a/novaclient/v2/client.py +++ b/novaclient/v2/client.py @@ -13,6 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient import api_versions from novaclient import client from novaclient.i18n import _LW from novaclient.v2 import agents @@ -129,7 +130,7 @@ def __init__(self, username=None, api_key=None, project_id=None, self.limits = limits.LimitsManager(self) self.servers = servers.ServerManager(self) self.versions = versions.VersionManager(self) - self.api_version = api_version + self.api_version = api_version or api_versions.APIVersion("2.0") # extensions self.agents = agents.AgentsManager(self) From 5153dcda807c554769081626c10c43d16adea671 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Wed, 9 Sep 2015 01:04:45 +0300 Subject: [PATCH 0834/1705] [BugFix] Change parameters for legacy bdm `_parse_block_device_mapping` method was design to support both bdm v1 and v2. The implementation is based on the fact that API side ignores unknown/incorrect parameters, so `_parse_block_device_mapping` compose equal parameter for bdm v1 and bdm v2. Since Nova V2.1 contains schema checks, such implementation stoped working. Despite the fact that novaclient.v2.servers contains separate logic for bdm v2, we can make `_parse_block_device_mapping` works only with legacy bdm and in future patches make it works for both bdm v1 and bdm v2. Change-Id: I37c00ac77b1a3b500221d779533532e9f43e5277 Closes-Bug: #1491737 --- novaclient/base.py | 11 ++---- novaclient/tests/functional/base.py | 43 +++++++++++++++++++++ novaclient/tests/functional/test_quotas.py | 15 ++----- novaclient/tests/functional/test_servers.py | 24 ++++++++++++ novaclient/tests/unit/v2/test_shell.py | 5 +-- 5 files changed, 75 insertions(+), 23 deletions(-) diff --git a/novaclient/base.py b/novaclient/base.py index 74c7e2fe2..a07bd2497 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -274,6 +274,9 @@ class BootingManagerWithFind(ManagerWithFind): """Like a `ManagerWithFind`, but has the ability to boot servers.""" def _parse_block_device_mapping(self, block_device_mapping): + """Parses legacy block device mapping.""" + # FIXME(andreykurilin): make it work with block device mapping v2 + bdm = [] for device_name, mapping in six.iteritems(block_device_mapping): @@ -285,15 +288,9 @@ def _parse_block_device_mapping(self, block_device_mapping): mapping_parts = mapping.split(':') source_id = mapping_parts[0] - bdm_dict['uuid'] = source_id - bdm_dict['boot_index'] = 0 - if len(mapping_parts) == 1: - bdm_dict['volume_id'] = source_id - bdm_dict['source_type'] = 'volume' - elif len(mapping_parts) > 1: + if len(mapping_parts) > 1: source_type = mapping_parts[1] - bdm_dict['source_type'] = source_type if source_type.startswith('snap'): bdm_dict['snapshot_id'] = source_id else: diff --git a/novaclient/tests/functional/base.py b/novaclient/tests/functional/base.py index f39dbf0e5..8b0e17266 100644 --- a/novaclient/tests/functional/base.py +++ b/novaclient/tests/functional/base.py @@ -193,6 +193,28 @@ def wait_for_volume_status(self, volume, status, timeout=60, self.fail("Volume %s did not reach status %s after %d s" % (volume.id, status, timeout)) + def wait_for_resource_delete(self, resource, manager, + timeout=60, poll_interval=1): + """Wait until getting the resource raises NotFound exception. + + :param resource: Resource object. + :param manager: Manager object with get method. + :param timeout: timeout in seconds + :param poll_interval: poll interval in seconds + """ + start_time = time.time() + while time.time() - start_time < timeout: + try: + manager.get(resource) + except Exception as e: + if getattr(e, "http_status", None) == 404: + break + else: + raise + time.sleep(poll_interval) + else: + self.fail("The resource '%s' still exists." % resource.id) + def name_generate(self, prefix='Entity'): """Generate randomized name for some entity. @@ -200,3 +222,24 @@ def name_generate(self, prefix='Entity'): """ name = "%s-%s" % (prefix, six.text_type(uuid.uuid4())) return name + + def _get_value_from_the_table(self, table, key): + """Parses table to get desired value. + + EXAMPLE of the table: + # +-------------+----------------------------------+ + # | Property | Value | + # +-------------+----------------------------------+ + # | description | | + # | enabled | True | + # | id | 582df899eabc47018c96713c2f7196ba | + # | name | admin | + # +-------------+----------------------------------+ + """ + lines = table.split("\n") + for line in lines: + if "|" in line: + l_property, l_value = line.split("|")[1:3] + if l_property.strip() == key: + return l_value.strip() + raise ValueError("Property '%s' is missing from the table." % key) diff --git a/novaclient/tests/functional/test_quotas.py b/novaclient/tests/functional/test_quotas.py index 3b75ed5f6..cd6392fd5 100644 --- a/novaclient/tests/functional/test_quotas.py +++ b/novaclient/tests/functional/test_quotas.py @@ -25,19 +25,10 @@ class TestQuotasNovaClient(base.ClientTestBase): 'server_groups', 'server_group_members'] def test_quotas_update(self): - # `nova quota-update` requires tenant-id. EXAMPLE of keystone output: - # +-------------+----------------------------------+ - # | Property | Value | - # +-------------+----------------------------------+ - # | description | | - # | enabled | True | - # | id | 582df899eabc47018c96713c2f7196ba | - # | name | admin | - # +-------------+----------------------------------+ + # `nova quota-update` requires tenant-id. tenant_info = self.cli_clients.keystone( - "tenant-get", params=self.cli_clients.tenant_name).split("\n") - tenant_id = [l.rsplit("|", 2)[-2].strip() - for l in tenant_info if "id" in l][0] + "tenant-get", params=self.cli_clients.tenant_name) + tenant_id = self._get_value_from_the_table(tenant_info, "id") self.addCleanup(self.client.quotas.delete, tenant_id) diff --git a/novaclient/tests/functional/test_servers.py b/novaclient/tests/functional/test_servers.py index 9de2cb2c5..75be3f472 100644 --- a/novaclient/tests/functional/test_servers.py +++ b/novaclient/tests/functional/test_servers.py @@ -16,6 +16,30 @@ from novaclient.v2 import shell +class TestServersBootNovaClient(base.ClientTestBase): + """Servers boot functional tests. + """ + + def test_boot_server_with_legacy_bdm(self): + volume_size = 1 + volume_name = str(uuid.uuid4()) + volume = self.client.volumes.create(size=volume_size, + display_name=volume_name, + imageRef=self.image.id) + self.wait_for_volume_status(volume, "available") + + server_info = self.nova("boot", params=( + "%(name)s --flavor %(flavor)s --poll " + "--block-device-mapping vda=%(volume_id)s:::1" % { + "name": str(uuid.uuid4()), "flavor": + self.flavor.id, + "volume_id": volume.id})) + server_id = self._get_value_from_the_table(server_info, "id") + + self.client.servers.delete(server_id) + self.wait_for_resource_delete(server_id, self.client.servers) + + class TestServersListNovaClient(base.ClientTestBase): """Servers list functional tests. """ diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 9a2ae0c70..7d4c41150 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -274,10 +274,7 @@ def test_boot_no_image_bdms(self): { 'volume_id': 'blah', 'delete_on_termination': '0', - 'device_name': 'vda', - 'uuid': 'blah', - 'boot_index': 0, - 'source_type': '' + 'device_name': 'vda' } ], 'imageRef': '', From b547a3da506c049ceabe50895c01fb0e2f63fcfe Mon Sep 17 00:00:00 2001 From: Daniel Wallace Date: Wed, 9 Sep 2015 14:19:09 -0500 Subject: [PATCH 0835/1705] make sure os_password is set for auth_plugins os_password is not set anywhere for auth_plugins. It is only set for keystone sessions. This makes sure that it gets set before being passed to Client. Closes-Bug: #1493977 Change-Id: I94421fd6fa58be391dfe8a9a14479bb4c17c5231 --- novaclient/shell.py | 3 +++ novaclient/tests/unit/test_shell.py | 16 ++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/novaclient/shell.py b/novaclient/shell.py index 0f1dd98e1..93f7bbe0a 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -683,6 +683,9 @@ def main(self, argv): project_name=project_name, project_domain_id=args.os_project_domain_id, project_domain_name=args.os_project_domain_name) + else: + # set password for auth plugins + os_password = args.os_password if not do_help and not any([args.os_tenant_id, args.os_tenant_name, args.os_project_id, args.os_project_name]): diff --git a/novaclient/tests/unit/test_shell.py b/novaclient/tests/unit/test_shell.py index 56eb7f0c3..866d94ac0 100644 --- a/novaclient/tests/unit/test_shell.py +++ b/novaclient/tests/unit/test_shell.py @@ -58,6 +58,13 @@ 'OS_ENDPOINT_TYPE': 'osURL', 'OS_COMPUTE_API_VERSION': '2'} +FAKE_ENV5 = {'OS_USERNAME': 'username', + 'OS_PASSWORD': 'password', + 'OS_TENANT_NAME': 'tenant_name', + 'OS_AUTH_URL': 'http://no.where/v2.0', + 'OS_COMPUTE_API_VERSION': '2', + 'OS_AUTH_SYSTEM': 'rackspace'} + def _create_ver_list(versions): return {'versions': {'values': versions}} @@ -495,6 +502,15 @@ def test_microversion_with_specific_version_without_microversions(self): self.shell, '--os-compute-api-version 2.3 list') + @mock.patch('novaclient.client.Client') + def test_custom_auth_plugin(self, mock_client): + self.make_env(fake_env=FAKE_ENV5) + self.shell('list') + password = mock_client.call_args_list[0][0][2] + client_kwargs = mock_client.call_args_list[0][1] + self.assertEqual(password, 'password') + self.assertIs(client_kwargs['session'], None) + class TestLoadVersionedActions(utils.TestCase): From f636481f015c380b610eb8c7e67571a3b4807c55 Mon Sep 17 00:00:00 2001 From: Xiaowei Qian Date: Tue, 8 Sep 2015 08:01:56 +0000 Subject: [PATCH 0836/1705] Modify "nova keypair-show" Positional arguments help information CLI "nova keypair-show ", Positional arguments: Name or ID of keypair But in fact, it does not support ID,when using ID, the error is as follows: [root]# nova keypair-show 631 ERROR (CommandError): No keypair with a name or ID of '631' exists. So it is needed to change the Positional arguments help information from "Name or ID of keypair" to "Name of keypair". Change-Id: I4cd022a6aa1ca937e7aeb33847dc3ce2d6d5690e Closes-Bug: #1493212 Signed-off-by: Xiaowei Qian --- novaclient/v2/shell.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index e969cf5e7..6ec17ebcf 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -2998,7 +2998,7 @@ def _print_keypair(keypair): @cliutils.arg( 'keypair', metavar='', - help=_("Name or ID of keypair")) + help=_("Name of keypair.")) def do_keypair_show(cs, args): """Show details about the given keypair.""" keypair = _find_keypair(cs, args.keypair) @@ -3006,7 +3006,7 @@ def do_keypair_show(cs, args): def _find_keypair(cs, keypair): - """Get a keypair by name or ID.""" + """Get a keypair by name.""" return utils.find_resource(cs.keypairs, keypair) From 6f58fa2c9af349ca2188e141ad920f2d0a205d96 Mon Sep 17 00:00:00 2001 From: Wang Wei Date: Fri, 11 Sep 2015 01:41:38 +0000 Subject: [PATCH 0837/1705] Fix mistakes in comments Each can only delete an aggregate by API, so, change 'aggregates' to 'aggregate' Also, change 'Set a aggregate metadata' to 'Set aggregate metadata' Change-Id: Ieb4a8b2445678da8741794613aede948ba51e5ce --- novaclient/v2/aggregates.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/novaclient/v2/aggregates.py b/novaclient/v2/aggregates.py index c2df31526..29d90c0db 100644 --- a/novaclient/v2/aggregates.py +++ b/novaclient/v2/aggregates.py @@ -85,11 +85,11 @@ def remove_host(self, aggregate, host): body, "aggregate") def set_metadata(self, aggregate, metadata): - """Set a aggregate metadata, replacing the existing metadata.""" + """Set aggregate metadata, replacing the existing metadata.""" body = {'set_metadata': {'metadata': metadata}} return self._create("/os-aggregates/%s/action" % base.getid(aggregate), body, "aggregate") def delete(self, aggregate): - """Delete the specified aggregates.""" + """Delete the specified aggregate.""" self._delete('/os-aggregates/%s' % (base.getid(aggregate))) From e9f1391bf85e9c7513565b108feb6601480589ef Mon Sep 17 00:00:00 2001 From: venkatamahesh Date: Sun, 13 Sep 2015 14:44:56 +0530 Subject: [PATCH 0838/1705] Fix the homepage url in setup.cfg Link for home-page is https://www.openstack.org but instead of it source tree link is there So it is changed Change-Id: Ie788e5a6c66d71b59cdedf5fcf14725181983629 --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 357594759..174a80f87 100644 --- a/setup.cfg +++ b/setup.cfg @@ -6,7 +6,7 @@ description-file = license = Apache License, Version 2.0 author = OpenStack author-email = openstack-dev@lists.openstack.org -home-page = https://git.openstack.org/cgit/openstack/python-novaclient +home-page = https://www.openstack.org classifier = Development Status :: 5 - Production/Stable Environment :: Console From b4e9af99d7a353990fadb5eb7708f08d55550e9f Mon Sep 17 00:00:00 2001 From: melanie witt Date: Tue, 15 Sep 2015 00:20:23 +0000 Subject: [PATCH 0839/1705] Remove redundant help command test and commonize This is a follow up patch to https://review.openstack.org/#/c/221488/ which added unit tests for 'nova -h' and 'nova --help' and commonized some code. It was missed that a test for 'nova' without any options or subcommands already existed as "test_help_no_options" This patch also refactors "test_help_on_subcommand" use the common "_test_help" function since that was also missed. Change-Id: Iccaf91207ba35001c8f58abb550cd1c3be542b38 --- novaclient/tests/unit/test_shell.py | 32 ++++++++--------------------- 1 file changed, 9 insertions(+), 23 deletions(-) diff --git a/novaclient/tests/unit/test_shell.py b/novaclient/tests/unit/test_shell.py index 844306fc1..d1dc23d58 100644 --- a/novaclient/tests/unit/test_shell.py +++ b/novaclient/tests/unit/test_shell.py @@ -169,12 +169,13 @@ def test_invalid_timeout(self): for r in required: self.assertIn(r, stderr) - def _test_help(self, command): - required = [ - '.*?^usage: ', - '.*?^\s+set-password\s+Change the admin password', - '.*?^See "nova help COMMAND" for help on a specific command', - ] + def _test_help(self, command, required=None): + if required is None: + required = [ + '.*?^usage: ', + '.*?^\s+set-password\s+Change the admin password', + '.*?^See "nova help COMMAND" for help on a specific command', + ] stdout, stderr = self.shell(command) for r in required: self.assertThat((stdout + stderr), @@ -187,7 +188,7 @@ def test_help_option(self): self._test_help('--help') self._test_help('-h') - def test_help_no_subcommand(self): + def test_help_no_options(self): self._test_help('') def test_help_on_subcommand(self): @@ -196,22 +197,7 @@ def test_help_on_subcommand(self): '.*?^Change the admin password', '.*?^Positional arguments:', ] - stdout, stderr = self.shell('help set-password') - for r in required: - self.assertThat((stdout + stderr), - matchers.MatchesRegex(r, re.DOTALL | re.MULTILINE)) - - def test_help_no_options(self): - self.make_env() - required = [ - '.*?^usage: ', - '.*?^\s+set-password\s+Change the admin password', - '.*?^See "nova help COMMAND" for help on a specific command', - ] - stdout, stderr = self.shell('') - for r in required: - self.assertThat((stdout + stderr), - matchers.MatchesRegex(r, re.DOTALL | re.MULTILINE)) + self._test_help('help set-password', required=required) def test_bash_completion(self): self.make_env() From 48f9c5a046426de81a5087ce9604c030e7165c22 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Mon, 14 Sep 2015 15:44:43 +0200 Subject: [PATCH 0840/1705] Fix unicode issue in shell.main() error handling shell.main() uses safe_encode() to encode the exception name and exception message. On Python 3, it displays: ERROR (b'Exception'): b'message' instead of: ERROR (Exception): message Don't encode exception name and message on Python 3. Use exception_to_unicode() of oslo_utils.encodeutils to get the exception message as Unicode. On Python 2, let print() encodes the Unicode to the encoding of sys.stderr. Blueprint nova-python3 Change-Id: Ib5568546d2aedb2b28c05d840b9ba34f697384ec --- novaclient/shell.py | 12 +++++------- novaclient/tests/unit/test_shell.py | 10 ++++++++++ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/novaclient/shell.py b/novaclient/shell.py index 93f7bbe0a..583cddb66 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -31,7 +31,6 @@ from oslo_utils import encodeutils from oslo_utils import importutils from oslo_utils import strutils -import six HAS_KEYRING = False all_errors = ValueError @@ -897,12 +896,11 @@ def main(): try: argv = [encodeutils.safe_decode(a) for a in sys.argv[1:]] OpenStackComputeShell().main(argv) - - except Exception as e: - logger.debug(e, exc_info=1) - details = {'name': encodeutils.safe_encode(e.__class__.__name__), - 'msg': encodeutils.safe_encode(six.text_type(e))} - print("ERROR (%(name)s): %(msg)s" % details, + except Exception as exc: + logger.debug(exc, exc_info=1) + print("ERROR (%s): %s" + % (exc.__class__.__name__, + encodeutils.exception_to_unicode(exc)), file=sys.stderr) sys.exit(1) except KeyboardInterrupt: diff --git a/novaclient/tests/unit/test_shell.py b/novaclient/tests/unit/test_shell.py index 844306fc1..a2c86b6d0 100644 --- a/novaclient/tests/unit/test_shell.py +++ b/novaclient/tests/unit/test_shell.py @@ -521,6 +521,16 @@ def test_custom_auth_plugin(self, mock_client): self.assertEqual(password, 'password') self.assertIs(client_kwargs['session'], None) + @mock.patch.object(novaclient.shell.OpenStackComputeShell, 'main') + def test_main_error_handling(self, mock_compute_shell): + class MyException(Exception): + pass + with mock.patch('sys.stderr', six.StringIO()): + mock_compute_shell.side_effect = MyException('message') + self.assertRaises(SystemExit, novaclient.shell.main) + err = sys.stderr.getvalue() + self.assertEqual(err, 'ERROR (MyException): message\n') + class TestLoadVersionedActions(utils.TestCase): From 5fdb861ad4835513c0cb78251cce85ac73f51dd4 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Wed, 10 Jun 2015 13:40:05 -0700 Subject: [PATCH 0841/1705] Show reserved status for a fixed_ip if available Commit 8886590f30daf736ae30a95b3e4a77cb586d4f02 adds the v2.4 microversion to nova so that we can get back the reserved value of a fixed IP. This change adds support to novaclient to display the reserved status from the fixed_ip resource if it's available in the response. Related blueprint show-reserved-status-in-os-fixed-ips-api Change-Id: Idf3df1728ba87006d1b0f82a1c01712202670e0a --- novaclient/tests/functional/base.py | 33 ++++++++++ novaclient/tests/functional/test_fixedips.py | 67 ++++++++++++++++++++ novaclient/v2/shell.py | 14 +++- 3 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 novaclient/tests/functional/test_fixedips.py diff --git a/novaclient/tests/functional/base.py b/novaclient/tests/functional/base.py index 8b0e17266..0f73aff17 100644 --- a/novaclient/tests/functional/base.py +++ b/novaclient/tests/functional/base.py @@ -243,3 +243,36 @@ def _get_value_from_the_table(self, table, key): if l_property.strip() == key: return l_value.strip() raise ValueError("Property '%s' is missing from the table." % key) + + def _get_column_value_from_single_row_table(self, table, column): + """Get the value for the column in the single-row table + + Example table: + + +----------+-------------+----------+----------+ + | address | cidr | hostname | host | + +----------+-------------+----------+----------+ + | 10.0.0.3 | 10.0.0.0/24 | test | myhost | + +----------+-------------+----------+----------+ + + :param table: newline-separated table with |-separated cells + :param column: name of the column to look for + :raises: ValueError if the column value is not found + """ + lines = table.split("\n") + # Determine the column header index first. + column_index = -1 + for line in lines: + if "|" in line: + if column_index == -1: + headers = line.split("|")[1:-1] + for index, header in enumerate(headers): + if header.strip() == column: + column_index = index + break + else: + # We expect a single-row table so we should be able to get + # the value now using the column index. + return line.split("|")[1:-1][column_index].strip() + + raise ValueError("Unable to find value for column '%s'.") diff --git a/novaclient/tests/functional/test_fixedips.py b/novaclient/tests/functional/test_fixedips.py new file mode 100644 index 000000000..f6983070f --- /dev/null +++ b/novaclient/tests/functional/test_fixedips.py @@ -0,0 +1,67 @@ +# Copyright 2015 IBM Corp. +# 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 oslo_utils import strutils + +from novaclient.tests.functional import base +from novaclient.v2 import shell + + +class TestFixedIPsNovaClient(base.ClientTestBase): + """FixedIPs functional tests.""" + + API_VERSION = '2.1' + + def nova(self, *args, **kwargs): + flags = '--os-compute-api-version %s ' % self.API_VERSION + return self.cli_clients.nova(flags=flags, *args, **kwargs) + + def _create_server(self): + name = self.name_generate(prefix='server') + server = self.client.servers.create(name, self.image, self.flavor) + shell._poll_for_status( + self.client.servers.get, server.id, + 'building', ['active']) + self.addCleanup(server.delete) + return server + + def _test_fixedip_get(self, expect_reserved=False): + server = self._create_server() + networks = server.networks + self.assertIn('private', networks) + fixed_ip = networks['private'][0] + table = self.nova('fixed-ip-get %s' % fixed_ip) + addr = self._get_column_value_from_single_row_table(table, 'address') + self.assertEqual(fixed_ip, addr) + if expect_reserved: + reserved = self._get_column_value_from_single_row_table(table, + 'reserved') + # By default the fixed IP should not be reserved. + self.assertEqual(False, strutils.bool_from_string(reserved, + strict=True)) + else: + self.assertRaises(ValueError, + self._get_column_value_from_single_row_table, + table, 'reserved') + + def test_fixedip_get(self): + self._test_fixedip_get() + + +class TestFixedIPsNovaClientV24(TestFixedIPsNovaClient): + """FixedIPs functional tests for v2.4 nova-api microversion.""" + + API_VERSION = '2.4' + + def test_fixedip_get(self): + self._test_fixedip_get(expect_reserved=True) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 6a2d3dad5..dcd27843e 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -3621,11 +3621,23 @@ def do_service_delete(cs, args): cs.services.delete(args.id) +@api_versions.wraps("2.0", "2.3") +def _print_fixed_ip(cs, fixed_ip): + fields = ['address', 'cidr', 'hostname', 'host'] + utils.print_list([fixed_ip], fields) + + +@api_versions.wraps("2.4") +def _print_fixed_ip(cs, fixed_ip): + fields = ['address', 'cidr', 'hostname', 'host', 'reserved'] + utils.print_list([fixed_ip], fields) + + @cliutils.arg('fixed_ip', metavar='', help=_('Fixed IP Address.')) def do_fixed_ip_get(cs, args): """Retrieve info on a fixed IP.""" result = cs.fixed_ips.get(args.fixed_ip) - utils.print_list([result], ['address', 'cidr', 'hostname', 'host']) + _print_fixed_ip(cs, result) @cliutils.arg('fixed_ip', metavar='', help=_('Fixed IP Address.')) From b1204446ef17e7df244b23d7552da24a6d86b53f Mon Sep 17 00:00:00 2001 From: Atsushi SAKAI Date: Mon, 14 Sep 2015 18:13:01 +0900 Subject: [PATCH 0842/1705] Fix nova bash-completion needs authentication CLI-Reference generator requires (openstack-doc-tools/bin/doc-tools-update-cli-reference) that command line help and bash-completion running without authentication. For this purpose, remove authentication check in command and its test. Fix nova bash-completion works without authentication. Fix nova bash-completion test without setting authentication infomation. Define skip_auth for descriminate "nova bash-completion" and do_help. Change-Id: Ic1a7f84b1b9cb7e8be6721c1b8096f49e9e9ab33 Closes-Bug: #1495424 --- novaclient/shell.py | 15 ++++++++++----- novaclient/tests/unit/test_shell.py | 1 - 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/novaclient/shell.py b/novaclient/shell.py index 93f7bbe0a..e6321b03c 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -559,6 +559,10 @@ def main(self, argv): do_help = ('help' in argv) or ( '--help' in argv) or ('-h' in argv) or not argv + # bash-completion should not require authentification + skip_auth = do_help or ( + 'bash-completion' in argv) + # Discover available auth plugins novaclient.auth_plugin.discover_auth_systems() @@ -630,7 +634,7 @@ def main(self, argv): # FIXME(usrleon): Here should be restrict for project id same as # for os_username or os_password but for compatibility it is not. - if must_auth and not do_help: + if must_auth and not skip_auth: if auth_plugin: auth_plugin.parse_opts(args) @@ -687,8 +691,9 @@ def main(self, argv): # set password for auth plugins os_password = args.os_password - if not do_help and not any([args.os_tenant_id, args.os_tenant_name, - args.os_project_id, args.os_project_name]): + if (not skip_auth and + not any([args.os_tenant_id, args.os_tenant_name, + args.os_project_id, args.os_project_name])): raise exc.CommandError(_("You must provide a project name or" " project id via --os-project-name," " --os-project-id, env[OS_PROJECT_ID]" @@ -696,7 +701,7 @@ def main(self, argv): " use os-project and os-tenant" " interchangeably.")) - if not os_auth_url and not do_help: + if not os_auth_url and not skip_auth: raise exc.CommandError( _("You must provide an auth url " "via either --os-auth-url or env[OS_AUTH_URL]")) @@ -718,7 +723,7 @@ def main(self, argv): cacert=cacert, timeout=timeout, session=keystone_session, auth=keystone_auth) - if not do_help: + if not skip_auth: if not api_version.is_latest(): if api_version > api_versions.APIVersion("2.0"): if not api_version.matches(novaclient.API_MIN_VERSION, diff --git a/novaclient/tests/unit/test_shell.py b/novaclient/tests/unit/test_shell.py index 844306fc1..63ece2057 100644 --- a/novaclient/tests/unit/test_shell.py +++ b/novaclient/tests/unit/test_shell.py @@ -214,7 +214,6 @@ def test_help_no_options(self): matchers.MatchesRegex(r, re.DOTALL | re.MULTILINE)) def test_bash_completion(self): - self.make_env() stdout, stderr = self.shell('bash-completion') # just check we have some output required = [ From 5c59684ae720d751a858bf8cd198eac013db3dc1 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Thu, 17 Sep 2015 12:16:56 +0000 Subject: [PATCH 0843/1705] Updated from global requirements Change-Id: Iadb3dfd4b07a1eef7a31799d3cc1239aaade08b2 --- requirements.txt | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 5270cf4d8..ba60838bb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ # 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. -pbr<2.0,>=1.6 +pbr>=1.6 argparse iso8601>=0.1.9 oslo.i18n>=1.5.0 # Apache-2.0 diff --git a/setup.py b/setup.py index d8080d05c..782bb21f0 100644 --- a/setup.py +++ b/setup.py @@ -25,5 +25,5 @@ pass setuptools.setup( - setup_requires=['pbr>=1.3'], + setup_requires=['pbr>=1.8'], pbr=True) From 420782cc010ab1bab1973a7076f3b8256422d6e1 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Thu, 17 Sep 2015 16:05:33 +0300 Subject: [PATCH 0844/1705] [docs] Fix length of underline Title length must match the underline Change-Id: Ia7a2bf54a6697c63c423bacd53dfb125b9c620f6 --- doc/source/shell.rst | 2 +- novaclient/tests/functional/README.rst | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/doc/source/shell.rst b/doc/source/shell.rst index 52bcfb49d..368195f7d 100644 --- a/doc/source/shell.rst +++ b/doc/source/shell.rst @@ -1,5 +1,5 @@ The :program:`nova` shell utility -========================================= +================================= .. program:: nova .. highlight:: bash diff --git a/novaclient/tests/functional/README.rst b/novaclient/tests/functional/README.rst index be4ebd079..727c40aba 100644 --- a/novaclient/tests/functional/README.rst +++ b/novaclient/tests/functional/README.rst @@ -1,9 +1,9 @@ -===================================== +==================================== python-novaclient functional testing -===================================== +==================================== Idea ------- +---- Over time we have noticed two issues with novaclient unit tests. @@ -22,7 +22,7 @@ multiple occasions). Testing Theory ----------------- +-------------- We are treating python-novaclient as legacy code, so we do not want to spend a lot of effort adding in missing features. In the future the CLI will move to @@ -38,7 +38,7 @@ native support to the CLI for the required functionality would involve a non trivial amount of work. Functional Test Guidelines ---------------------------- +-------------------------- * Consume credentials via standard client environmental variables:: From 485201453a5d7a9b6b6b81d737edfce7fefb97a2 Mon Sep 17 00:00:00 2001 From: Richard Jones Date: Thu, 17 Sep 2015 15:05:35 +1000 Subject: [PATCH 0845/1705] Encode unicode filter arguments for server list Previously unicode arguments were not encoded so unicode exceptions were thrown when the arguments were URL encoded. Change-Id: I69a5ac8dcf584ad5b5604caf3c4cb16bb59d3fe3 Partial-Bug: 1472999 --- novaclient/tests/unit/v2/test_servers.py | 6 ++++++ novaclient/v2/servers.py | 2 ++ 2 files changed, 8 insertions(+) diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index 741cab55f..f311ab7d6 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -43,6 +43,12 @@ def test_list_servers(self): for s in sl: self.assertIsInstance(s, servers.Server) + def test_filter_servers_unicode(self): + sl = self.cs.servers.list(search_opts={'name': u't€sting'}) + self.assert_called('GET', '/servers/detail?name=t%E2%82%ACsting') + for s in sl: + self.assertIsInstance(s, servers.Server) + def test_list_all_servers(self): # use marker just to identify this call in fixtures sl = self.cs.servers.list(limit=-1, marker=1234) diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index 3c34f95a4..d21bc5fbe 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -570,6 +570,8 @@ def list(self, detailed=True, search_opts=None, marker=None, limit=None, for opt, val in six.iteritems(search_opts): if val: + if isinstance(val, six.text_type): + val = val.encode('utf-8') qparams[opt] = val detail = "" From ac6a04864a8c472a412e606594812b197fa080d4 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 18 Sep 2015 16:42:28 +0000 Subject: [PATCH 0846/1705] Updated from global requirements Change-Id: I991327116276dc2e4f2d7a4b52d293305caa5b4e --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index eb8bb3a65..3bb675e63 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -15,4 +15,4 @@ oslosphinx>=2.5.0 # Apache-2.0 testrepository>=0.0.18 testscenarios>=0.4 testtools>=1.4.0 -tempest-lib>=0.6.1 +tempest-lib>=0.8.0 From 3e99d3f87553abc23e4d04d9f9f4d8191c39fe16 Mon Sep 17 00:00:00 2001 From: Andreas Jaeger Date: Sat, 19 Sep 2015 15:00:08 +0200 Subject: [PATCH 0847/1705] Improve help strings For consistency: * Adjust capitalization, e.g. use ID or UUID. * Slight wording improvements. * Add "." at end of all strings. Change-Id: I252c023438c4fe82873a1616b36391f2a2048853 --- novaclient/shell.py | 16 ++-- novaclient/v2/shell.py | 200 +++++++++++++++++++++-------------------- 2 files changed, 109 insertions(+), 107 deletions(-) diff --git a/novaclient/shell.py b/novaclient/shell.py index 5da855aa7..9a7d06cb6 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -283,7 +283,7 @@ def get_base_parser(self): '--debug', default=False, action='store_true', - help=_("Print debugging output")) + help=_("Print debugging output.")) parser.add_argument( '--os-cache', @@ -297,12 +297,12 @@ def get_base_parser(self): '--timings', default=False, action='store_true', - help=_("Print call timing info")) + help=_("Print call timing info.")) parser.add_argument( '--os-auth-token', default=cliutils.env('OS_AUTH_TOKEN'), - help='Defaults to env[OS_AUTH_TOKEN]') + help='Defaults to env[OS_AUTH_TOKEN].') parser.add_argument( '--os_username', @@ -352,7 +352,7 @@ def get_base_parser(self): parser.add_argument( '--service-type', metavar='', - help=_('Defaults to compute for most actions')) + help=_('Defaults to compute for most actions.')) parser.add_argument( '--service_type', help=argparse.SUPPRESS) @@ -361,7 +361,7 @@ def get_base_parser(self): '--service-name', metavar='', default=cliutils.env('NOVA_SERVICE_NAME'), - help=_('Defaults to env[NOVA_SERVICE_NAME]')) + help=_('Defaults to env[NOVA_SERVICE_NAME].')) parser.add_argument( '--service_name', help=argparse.SUPPRESS) @@ -370,7 +370,7 @@ def get_base_parser(self): '--volume-service-name', metavar='', default=cliutils.env('NOVA_VOLUME_SERVICE_NAME'), - help=_('Defaults to env[NOVA_VOLUME_SERVICE_NAME]')) + help=_('Defaults to env[NOVA_VOLUME_SERVICE_NAME].')) parser.add_argument( '--volume_service_name', help=argparse.SUPPRESS) @@ -415,7 +415,7 @@ def get_base_parser(self): dest='bypass_url', default=cliutils.env('NOVACLIENT_BYPASS_URL'), help="Use this API endpoint instead of the Service Catalog. " - "Defaults to env[NOVACLIENT_BYPASS_URL]") + "Defaults to env[NOVACLIENT_BYPASS_URL].") parser.add_argument('--bypass_url', help=argparse.SUPPRESS) @@ -869,7 +869,7 @@ def do_bash_completion(self, _args): 'command', metavar='', nargs='?', - help='Display help for ') + help='Display help for .') def do_help(self, args): """ Display help about this program or one of its subcommands. diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index dcd27843e..0db32811f 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -391,11 +391,11 @@ def _boot(cs, args): default=os.environ.get('NOVACLIENT_DEFAULT_KEY_NAME'), metavar='', help=_("Key name of keypair that should be created earlier with \ - the command keypair-add")) + the command keypair-add.")) @cliutils.arg( '--key_name', help=argparse.SUPPRESS) -@cliutils.arg('name', metavar='', help=_('Name for the new server')) +@cliutils.arg('name', metavar='', help=_('Name for the new server.')) @cliutils.arg( '--user-data', default=None, @@ -497,7 +497,7 @@ def _boot(cs, args): metavar="", dest='config_drive', default=False, - help=_("Enable config drive")) + help=_("Enable config drive.")) @cliutils.arg( '--poll', dest='poll', @@ -509,7 +509,7 @@ def _boot(cs, args): dest='admin_pass', metavar='', default=None, - help=_('Admin password for the instance')) + help=_('Admin password for the instance.')) def do_boot(cs, args): """Boot a new server.""" boot_args, boot_kwargs = _boot(cs, args) @@ -686,7 +686,7 @@ def do_flavor_list(cs, args): @cliutils.arg( 'flavor', metavar='', - help=_("Name or ID of the flavor to delete")) + help=_("Name or ID of the flavor to delete.")) def do_flavor_delete(cs, args): """Delete a specific flavor""" flavorid = _find_flavor(cs, args.flavor) @@ -697,7 +697,7 @@ def do_flavor_delete(cs, args): @cliutils.arg( 'flavor', metavar='', - help=_("Name or ID of flavor")) + help=_("Name or ID of flavor.")) def do_flavor_show(cs, args): """Show details about the given flavor.""" flavor = _find_flavor(cs, args.flavor) @@ -707,24 +707,24 @@ def do_flavor_show(cs, args): @cliutils.arg( 'name', metavar='', - help=_("Name of the new flavor")) + help=_("Name of the new flavor.")) @cliutils.arg( 'id', metavar='', help=_("Unique ID (integer or UUID) for the new flavor." - " If specifying 'auto', a UUID will be generated as id")) + " If specifying 'auto', a UUID will be generated as ID.")) @cliutils.arg( 'ram', metavar='', - help=_("Memory size in MB")) + help=_("Memory size in MB.")) @cliutils.arg( 'disk', metavar='', - help=_("Disk size in GB")) + help=_("Disk size in GB.")) @cliutils.arg( '--ephemeral', metavar='', - help=_("Ephemeral space size in GB (default 0)"), + help=_("Ephemeral space size in GB (default 0)."), default=0) @cliutils.arg( 'vcpus', @@ -733,21 +733,21 @@ def do_flavor_show(cs, args): @cliutils.arg( '--swap', metavar='', - help=_("Swap space size in MB (default 0)"), + help=_("Swap space size in MB (default 0)."), default=0) @cliutils.arg( '--rxtx-factor', metavar='', - help=_("RX/TX factor (default 1)"), + help=_("RX/TX factor (default 1)."), default=1.0) @cliutils.arg( '--is-public', metavar='', - help=_("Make flavor accessible to the public (default true)"), + help=_("Make flavor accessible to the public (default true)."), type=lambda v: strutils.bool_from_string(v, True), default=True) def do_flavor_create(cs, args): - """Create a new flavor""" + """Create a new flavor.""" f = cs.flavors.create(args.name, args.ram, args.vcpus, args.disk, args.id, args.ephemeral, args.swap, args.rxtx_factor, args.is_public) @@ -757,19 +757,19 @@ def do_flavor_create(cs, args): @cliutils.arg( 'flavor', metavar='', - help=_("Name or ID of flavor")) + help=_("Name or ID of flavor.")) @cliutils.arg( 'action', metavar='', choices=['set', 'unset'], - help=_("Actions: 'set' or 'unset'")) + help=_("Actions: 'set' or 'unset'.")) @cliutils.arg( 'metadata', metavar='', nargs='+', action='append', default=[], - help=_('Extra_specs to set/unset (only key is necessary on unset)')) + help=_('Extra_specs to set/unset (only key is necessary on unset).')) def do_flavor_key(cs, args): """Set or unset extra_spec for a flavor.""" flavor = _find_flavor(cs, args.flavor) @@ -888,7 +888,7 @@ def do_network_list(cs, args): @cliutils.arg( 'network', metavar='', - help=_("uuid or label of network")) + help=_("UUID or label of network.")) def do_network_show(cs, args): """Show details about the given network.""" network = utils.find_resource(cs.networks, args.network) @@ -898,7 +898,7 @@ def do_network_show(cs, args): @cliutils.arg( 'network', metavar='', - help=_("uuid or label of network")) + help=_("UUID or label of network.")) def do_network_delete(cs, args): """Delete network by label or id.""" network = utils.find_resource(cs.networks, args.network) @@ -924,7 +924,7 @@ def do_network_delete(cs, args): @cliutils.arg( 'network', metavar='', - help=_("uuid of network")) + help=_("UUID of network.")) def do_network_disassociate(cs, args): """Disassociate host and/or project from the given network.""" if args.host_only: @@ -938,7 +938,7 @@ def do_network_disassociate(cs, args): @cliutils.arg( 'network', metavar='', - help=_("uuid of network")) + help=_("UUID of network.")) @cliutils.arg( 'host', metavar='', @@ -951,7 +951,7 @@ def do_network_associate_host(cs, args): @cliutils.arg( 'network', metavar='', - help=_("uuid of network")) + help=_("UUID of network.")) def do_network_associate_project(cs, args): """Associate project with network.""" cs.networks.associate_project(args.network) @@ -1029,46 +1029,46 @@ def _filter_network_create_options(args): @cliutils.arg( '--dns1', dest="dns1", - metavar="", help=_('First DNS')) + metavar="", help=_('First DNS.')) @cliutils.arg( '--dns2', dest="dns2", metavar="", - help=_('Second DNS')) + help=_('Second DNS.')) @cliutils.arg( '--uuid', dest="uuid", metavar="", - help=_('Network UUID')) + help=_('Network UUID.')) @cliutils.arg( '--fixed-cidr', dest="fixed_cidr", metavar='', - help=_('IPv4 subnet for fixed IPs (ex: 10.20.0.0/16)')) + help=_('IPv4 subnet for fixed IPs (ex: 10.20.0.0/16).')) @cliutils.arg( '--project-id', dest="project_id", metavar="", - help=_('Project ID')) + help=_('Project ID.')) @cliutils.arg( '--priority', dest="priority", metavar="", - help=_('Network interface priority')) + help=_('Network interface priority.')) @cliutils.arg( '--mtu', dest="mtu", type=int, - help=_('MTU for network')) + help=_('MTU for network.')) @cliutils.arg( '--enable-dhcp', dest="enable_dhcp", metavar="<'T'|'F'>", - help=_('Enable dhcp')) + help=_('Enable DHCP.')) @cliutils.arg( '--dhcp-server', dest="dhcp_server", - help=_('Dhcp-server (defaults to gateway address)')) + help=_('DHCP-server address (defaults to gateway address)')) @cliutils.arg( '--share-address', dest="share_address", @@ -1077,11 +1077,11 @@ def _filter_network_create_options(args): @cliutils.arg( '--allowed-start', dest="allowed_start", - help=_('Start of allowed addresses for instances')) + help=_('Start of allowed addresses for instances.')) @cliutils.arg( '--allowed-end', dest="allowed_end", - help=_('End of allowed addresses for instances')) + help=_('End of allowed addresses for instances.')) def do_network_create(cs, args): """Create a network.""" @@ -1128,12 +1128,12 @@ def parse_server_name(image): @cliutils.arg( 'image', metavar='', - help=_("Name or ID of image")) + help=_("Name or ID of image.")) @cliutils.arg( 'action', metavar='', choices=['set', 'delete'], - help=_("Actions: 'set' or 'delete'")) + help=_("Actions: 'set' or 'delete'.")) @cliutils.arg( 'metadata', metavar='', @@ -1141,7 +1141,7 @@ def parse_server_name(image): action='append', default=[], help=_('Metadata to add/update or delete (only key is necessary on ' - 'delete)')) + 'delete).')) def do_image_meta(cs, args): """Set or Delete metadata on an image.""" image = _find_image(cs, args.image) @@ -1204,7 +1204,7 @@ def _print_flavor(flavor): @cliutils.arg( 'image', metavar='', - help=_("Name or ID of image")) + help=_("Name or ID of image.")) def do_image_show(cs, args): """Show details about the given image.""" image = _find_image(cs, args.image) @@ -1250,7 +1250,7 @@ def do_image_delete(cs, args): dest='name', metavar='', default=None, - help=_('Search with regular expression match by name')) + help=_('Search with regular expression match by name.')) @cliutils.arg( '--instance-name', dest='instance_name', @@ -1265,19 +1265,19 @@ def do_image_delete(cs, args): dest='status', metavar='', default=None, - help=_('Search by server status')) + help=_('Search by server status.')) @cliutils.arg( '--flavor', dest='flavor', metavar='', default=None, - help=_('Search by flavor name or ID')) + help=_('Search by flavor name or ID.')) @cliutils.arg( '--image', dest='image', metavar='', default=None, - help=_('Search by image name or ID')) + help=_('Search by image name or ID.')) @cliutils.arg( '--host', dest='host', @@ -1332,7 +1332,7 @@ def do_image_delete(cs, args): dest='minimal', action="store_true", default=False, - help=_('Get only uuid and name.')) + help=_('Get only UUID and name.')) @cliutils.arg( '--sort', dest='sort', @@ -1345,7 +1345,7 @@ def do_image_delete(cs, args): dest='marker', metavar='', default=None, - help=('The last server uuid of the previous page; displays list of servers' + help=('The last server UUID of the previous page; displays list of servers' ' after "marker".')) @cliutils.arg( '--limit', @@ -1509,7 +1509,7 @@ def do_reboot(cs, args): dest='minimal', action="store_true", default=False, - help=_('Skips flavor/image lookups when showing servers')) + help=_('Skips flavor/image lookups when showing servers.')) @cliutils.arg( '--preserve-ephemeral', action="store_true", @@ -1519,7 +1519,7 @@ def do_reboot(cs, args): '--name', metavar='', default=None, - help=_('Name for the new server')) + help=_('Name for the new server.')) @cliutils.arg( '--meta', metavar="", @@ -1844,19 +1844,19 @@ def do_backup(cs, args): @cliutils.arg( 'server', metavar='', - help=_("Name or ID of server")) + help=_("Name or ID of server.")) @cliutils.arg( 'action', metavar='', choices=['set', 'delete'], - help=_("Actions: 'set' or 'delete'")) + help=_("Actions: 'set' or 'delete'.")) @cliutils.arg( 'metadata', metavar='', nargs='+', action='append', default=[], - help=_('Metadata to set or delete (only key is necessary on delete)')) + help=_('Metadata to set or delete (only key is necessary on delete).')) def do_meta(cs, args): """Set or Delete metadata on a server.""" server = _find_server(cs, args.server) @@ -1927,7 +1927,7 @@ def _print_server(cs, args, server=None): dest='minimal', action="store_true", default=False, - help=_('Skips flavor/image lookups when showing servers')) + help=_('Skips flavor/image lookups when showing servers.')) @cliutils.arg('server', metavar='', help=_('Name or ID of server.')) def do_show(cs, args): """Show details about the given server.""" @@ -2289,7 +2289,7 @@ def do_volume_type_create(cs, args): @cliutils.arg( 'id', metavar='', - help=_("Unique ID of the volume type to delete")) + help=_("Unique ID of the volume type to delete.")) def do_volume_type_delete(cs, args): """DEPRECATED: Delete a specific volume type.""" emit_volume_deprecation_warning('volume-type-delete') @@ -2529,7 +2529,7 @@ def do_floating_ip_pool_list(cs, _args): @cliutils.arg( '--host', dest='host', metavar='', default=None, - help=_('Filter by host')) + help=_('Filter by host.')) def do_floating_ip_bulk_list(cs, args): """List all floating IPs (nova-network only).""" utils.print_list(cs.floating_ips_bulk.list(args.host), ['project_id', @@ -2539,19 +2539,21 @@ def do_floating_ip_bulk_list(cs, args): 'interface']) -@cliutils.arg('ip_range', metavar='', help=_('Address range to create')) +@cliutils.arg('ip_range', metavar='', + help=_('Address range to create.')) @cliutils.arg( '--pool', dest='pool', metavar='', default=None, - help=_('Pool for new Floating IPs')) + help=_('Pool for new Floating IPs.')) @cliutils.arg( '--interface', metavar='', default=None, - help=_('Interface for new Floating IPs')) + help=_('Interface for new Floating IPs.')) def do_floating_ip_bulk_create(cs, args): """Bulk create floating IPs by range (nova-network only).""" cs.floating_ips_bulk.create(args.ip_range, args.pool, args.interface) -@cliutils.arg('ip_range', metavar='', help=_('Address range to delete')) +@cliutils.arg('ip_range', metavar='', + help=_('Address range to delete.')) def do_floating_ip_bulk_delete(cs, args): """Bulk delete floating IPs by range (nova-network only).""" cs.floating_ips_bulk.delete(args.ip_range) @@ -2572,9 +2574,9 @@ def do_dns_domains(cs, args): _print_domain_list(domains) -@cliutils.arg('domain', metavar='', help=_('DNS domain')) -@cliutils.arg('--ip', metavar='', help=_('IP address'), default=None) -@cliutils.arg('--name', metavar='', help=_('DNS name'), default=None) +@cliutils.arg('domain', metavar='', help=_('DNS domain.')) +@cliutils.arg('--ip', metavar='', help=_('IP address.'), default=None) +@cliutils.arg('--name', metavar='', help=_('DNS name.'), default=None) def do_dns_list(cs, args): """List current DNS entries for domain and IP or domain and name.""" if not (args.ip or args.name): @@ -2589,33 +2591,33 @@ def do_dns_list(cs, args): _print_dns_list(entries) -@cliutils.arg('ip', metavar='', help=_('IP address')) -@cliutils.arg('name', metavar='', help=_('DNS name')) -@cliutils.arg('domain', metavar='', help=_('DNS domain')) +@cliutils.arg('ip', metavar='', help=_('IP address.')) +@cliutils.arg('name', metavar='', help=_('DNS name.')) +@cliutils.arg('domain', metavar='', help=_('DNS domain.')) @cliutils.arg( '--type', metavar='', - help=_('dns type (e.g. "A")'), + help=_('DNS type (e.g. "A")'), default='A') def do_dns_create(cs, args): - """Create a DNS entry for domain, name and IP.""" + """Create a DNS entry for domain, name, and IP.""" cs.dns_entries.create(args.domain, args.name, args.ip, args.type) -@cliutils.arg('domain', metavar='', help=_('DNS domain')) -@cliutils.arg('name', metavar='', help=_('DNS name')) +@cliutils.arg('domain', metavar='', help=_('DNS domain.')) +@cliutils.arg('name', metavar='', help=_('DNS name.')) def do_dns_delete(cs, args): """Delete the specified DNS entry.""" cs.dns_entries.delete(args.domain, args.name) -@cliutils.arg('domain', metavar='', help=_('DNS domain')) +@cliutils.arg('domain', metavar='', help=_('DNS domain.')) def do_dns_delete_domain(cs, args): """Delete the specified DNS domain.""" cs.dns_domains.delete(args.domain) -@cliutils.arg('domain', metavar='', help=_('DNS domain')) +@cliutils.arg('domain', metavar='', help=_('DNS domain.')) @cliutils.arg( '--availability-zone', metavar='', @@ -2631,7 +2633,7 @@ def do_dns_create_private_domain(cs, args): args.availability_zone) -@cliutils.arg('domain', metavar='', help=_('DNS domain')) +@cliutils.arg('domain', metavar='', help=_('DNS domain.')) @cliutils.arg( '--project', metavar='', help=_('Limit access to this domain to users ' @@ -3126,12 +3128,12 @@ def do_limits(cs, args): @cliutils.arg( '--start', metavar='', - help=_('Usage range start date ex 2012-01-20 (default: 4 weeks ago)'), + help=_('Usage range start date ex 2012-01-20. (default: 4 weeks ago)'), default=None) @cliutils.arg( '--end', metavar='', - help=_('Usage range end date, ex 2012-01-20 (default: tomorrow)'), + help=_('Usage range end date, ex 2012-01-20. (default: tomorrow)'), default=None) def do_usage_list(cs, args): """List usage data for all tenants.""" @@ -3175,11 +3177,11 @@ def simplify_usage(u): @cliutils.arg( '--start', metavar='', - help=_('Usage range start date ex 2012-01-20 (default: 4 weeks ago)'), + help=_('Usage range start date ex 2012-01-20. (default: 4 weeks ago)'), default=None) @cliutils.arg( '--end', metavar='', - help=_('Usage range end date, ex 2012-01-20 (default: tomorrow)'), + help=_('Usage range end date, ex 2012-01-20. (default: tomorrow)'), default=None) @cliutils.arg( '--tenant', @@ -3237,13 +3239,13 @@ def simplify_usage(u): metavar='', nargs='?', default='pk.pem', - help=_('Filename for the private key [Default: pk.pem]')) + help=_('Filename for the private key. [Default: pk.pem]')) @cliutils.arg( 'cert_filename', metavar='', nargs='?', default='cert.pem', - help=_('Filename for the X.509 certificate [Default: cert.pem]')) + help=_('Filename for the X.509 certificate. [Default: cert.pem]')) def do_x509_create_cert(cs, args): """Create x509 cert for a user in tenant.""" @@ -3291,7 +3293,7 @@ def do_x509_get_root_cert(cs, args): '--hypervisor', metavar='', default=None, - help=_('type of hypervisor.')) + help=_('Type of hypervisor.')) def do_agent_list(cs, args): """List all builds.""" result = cs.agents.list(args.hypervisor) @@ -3300,19 +3302,19 @@ def do_agent_list(cs, args): utils.print_list(result, columns) -@cliutils.arg('os', metavar='', help=_('type of os.')) +@cliutils.arg('os', metavar='', help=_('Type of OS.')) @cliutils.arg( 'architecture', metavar='', - help=_('type of architecture')) -@cliutils.arg('version', metavar='', help=_('version')) -@cliutils.arg('url', metavar='', help=_('url')) -@cliutils.arg('md5hash', metavar='', help=_('md5 hash')) + help=_('Type of architecture.')) +@cliutils.arg('version', metavar='', help=_('Version.')) +@cliutils.arg('url', metavar='', help=_('URL.')) +@cliutils.arg('md5hash', metavar='', help=_('MD5 hash.')) @cliutils.arg( 'hypervisor', metavar='', default='xen', - help=_('type of hypervisor.')) + help=_('Type of hypervisor.')) def do_agent_create(cs, args): """Create new agent build.""" result = cs.agents.create(args.os, args.architecture, @@ -3321,16 +3323,16 @@ def do_agent_create(cs, args): utils.print_dict(result._info.copy()) -@cliutils.arg('id', metavar='', help=_('id of the agent-build')) +@cliutils.arg('id', metavar='', help=_('ID of the agent-build.')) def do_agent_delete(cs, args): """Delete existing agent build.""" cs.agents.delete(args.id) -@cliutils.arg('id', metavar='', help=_('id of the agent-build')) -@cliutils.arg('version', metavar='', help=_('version')) -@cliutils.arg('url', metavar='', help=_('url')) -@cliutils.arg('md5hash', metavar='', help=_('md5hash')) +@cliutils.arg('id', metavar='', help=_('ID of the agent-build.')) +@cliutils.arg('version', metavar='', help=_('Version.')) +@cliutils.arg('url', metavar='', help=_('URL')) +@cliutils.arg('md5hash', metavar='', help=_('MD5 hash.')) def do_agent_modify(cs, args): """Modify existing agent build.""" result = cs.agents.update(args.id, args.version, @@ -3486,7 +3488,7 @@ def parser_hosts(fields): @cliutils.arg('server', metavar='', help=_('Name or ID of server.')) @cliutils.arg( 'host', metavar='', default=None, nargs='?', - help=_('destination host name.')) + help=_('Destination host name.')) @cliutils.arg( '--block-migrate', action='store_true', @@ -3502,7 +3504,7 @@ def parser_hosts(fields): action='store_true', dest='disk_over_commit', default=False, - help=_('Allow overcommit.(Default=False)')) + help=_('Allow overcommit. (Default=False)')) @cliutils.arg( '--disk_over_commit', action='store_true', @@ -3606,7 +3608,7 @@ def do_service_disable(cs, args): @cliutils.arg( '--unset', dest='force_down', - help=_("Unset the force state down of service"), + help=_("Unset the force state down of service."), action='store_false', default=True) def do_service_force_down(cs, args): @@ -3615,7 +3617,7 @@ def do_service_force_down(cs, args): utils.print_list([result], ['Host', 'Binary', 'Forced down']) -@cliutils.arg('id', metavar='', help=_('Id of service.')) +@cliutils.arg('id', metavar='', help=_('ID of service.')) def do_service_delete(cs, args): """Delete the service.""" cs.services.delete(args.id) @@ -3850,7 +3852,7 @@ def _get_first_endpoint(endpoints, region): @cliutils.arg( '--wrap', dest='wrap', metavar='', default=64, - help=_('wrap PKI tokens to a specified length, or 0 to disable')) + help=_('Wrap PKI tokens to a specified length, or 0 to disable.')) def do_credentials(cs, _args): """Show user credentials returned from auth.""" if isinstance(cs.client, client.SessionClient): @@ -3912,7 +3914,7 @@ def do_credentials(cs, _args): @cliutils.arg( '--extra-opts', dest='extra', - help=_('Extra options to pass to ssh. see: man ssh'), + help=_('Extra options to pass to ssh. see: man ssh.'), default='') def do_ssh(cs, args): """SSH into a server.""" @@ -4174,7 +4176,7 @@ def do_quota_defaults(cs, args): action="store_true", default=None, help=_('Whether force update the quota even if the already used and ' - 'reserved exceeds the new quota')) + 'reserved exceeds the new quota.')) def do_quota_update(cs, args): """Update the quotas for a tenant/user.""" @@ -4325,13 +4327,13 @@ def do_quota_class_update(cs, args): dest='password', metavar='', help=_("Set the provided admin password on the evacuated server. Not" - " applicable with on-shared-storage flag")) + " applicable with on-shared-storage flag.")) @cliutils.arg( '--on-shared-storage', dest='on_shared_storage', action="store_true", default=False, - help=_('Specifies whether server files are located on shared storage')) + help=_('Specifies whether server files are located on shared storage.')) def do_evacuate(cs, args): """Evacuate server from failed host.""" @@ -4565,7 +4567,7 @@ def do_secgroup_delete_default_rule(cs, args): metavar='', default=argparse.SUPPRESS, nargs='*', - help=_('Policies for the server groups ("affinity" or "anti-affinity")')) + help=_('Policies for the server groups. ("affinity" or "anti-affinity")')) @cliutils.arg( '--policy', default=[], @@ -4586,7 +4588,7 @@ def do_server_group_create(cs, args): 'id', metavar='', nargs='+', - help=_("Unique ID(s) of the server group to delete")) + help=_("Unique ID(s) of the server group to delete.")) def do_server_group_delete(cs, args): """Delete specific server group(s).""" failure_count = 0 @@ -4607,7 +4609,7 @@ def do_server_group_delete(cs, args): @cliutils.arg( 'id', metavar='', - help=_("Unique ID of the server group to get")) + help=_("Unique ID of the server group to get.")) def do_server_group_get(cs, args): """Get a specific server group.""" server_group = cs.server_groups.get(args.id) From 2793331d01d17ff95b5111048c249f7e06ed7f62 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Thu, 17 Sep 2015 14:45:20 +0300 Subject: [PATCH 0848/1705] Specify api_version for functional tests Most of functional tests are written for V2.0/V2.1 API version, but launched on "latest" compute api version('--os-compute-api-version' flag is not ovveriden + default value of it is "latest"). This patch adds "COMPUTE_API_VERSION" parameter for base TestCase, which sets --os-compute-api-version flag while running "nova" shell. Change-Id: I5d711438addaba5282a15fd2b9dde1800f7d7b91 --- novaclient/tests/functional/base.py | 11 ++++++++--- novaclient/tests/functional/test_fixedips.py | 8 ++------ novaclient/tests/functional/test_instances.py | 2 ++ novaclient/tests/functional/test_keypairs.py | 6 +++--- novaclient/tests/functional/test_quotas.py | 2 ++ novaclient/tests/functional/test_readonly_nova.py | 2 ++ novaclient/tests/functional/test_servers.py | 4 ++++ novaclient/tests/functional/test_volumes_api.py | 2 ++ 8 files changed, 25 insertions(+), 12 deletions(-) diff --git a/novaclient/tests/functional/base.py b/novaclient/tests/functional/base.py index 0f73aff17..b4aca058d 100644 --- a/novaclient/tests/functional/base.py +++ b/novaclient/tests/functional/base.py @@ -70,6 +70,8 @@ class ClientTestBase(testtools.TestCase): * initially just check return codes, and later test command outputs """ + COMPUTE_API_VERSION = None + log_format = ('%(asctime)s %(process)d %(levelname)-8s ' '[%(name)s] %(message)s') @@ -170,9 +172,12 @@ def setUp(self): uri=auth_url, cli_dir=cli_dir) - def nova(self, *args, **kwargs): - return self.cli_clients.nova(*args, - **kwargs) + def nova(self, action, flags='', params='', fail_ok=False, + endpoint_type='publicURL', merge_stderr=False): + if self.COMPUTE_API_VERSION: + flags += " --os-compute-api-version %s " % self.COMPUTE_API_VERSION + return self.cli_clients.nova(action, flags, params, fail_ok, + endpoint_type, merge_stderr) def wait_for_volume_status(self, volume, status, timeout=60, poll_interval=1): diff --git a/novaclient/tests/functional/test_fixedips.py b/novaclient/tests/functional/test_fixedips.py index f6983070f..095492470 100644 --- a/novaclient/tests/functional/test_fixedips.py +++ b/novaclient/tests/functional/test_fixedips.py @@ -20,11 +20,7 @@ class TestFixedIPsNovaClient(base.ClientTestBase): """FixedIPs functional tests.""" - API_VERSION = '2.1' - - def nova(self, *args, **kwargs): - flags = '--os-compute-api-version %s ' % self.API_VERSION - return self.cli_clients.nova(flags=flags, *args, **kwargs) + COMPUTE_API_VERSION = '2.1' def _create_server(self): name = self.name_generate(prefix='server') @@ -61,7 +57,7 @@ def test_fixedip_get(self): class TestFixedIPsNovaClientV24(TestFixedIPsNovaClient): """FixedIPs functional tests for v2.4 nova-api microversion.""" - API_VERSION = '2.4' + COMPUTE_API_VERSION = '2.4' def test_fixedip_get(self): self._test_fixedip_get(expect_reserved=True) diff --git a/novaclient/tests/functional/test_instances.py b/novaclient/tests/functional/test_instances.py index 749f34a79..d512106ec 100644 --- a/novaclient/tests/functional/test_instances.py +++ b/novaclient/tests/functional/test_instances.py @@ -15,6 +15,8 @@ class TestInstanceCLI(base.ClientTestBase): + COMPUTE_API_VERSION = "2.1" + def test_attach_volume(self): """Test we can attach a volume via the cli. diff --git a/novaclient/tests/functional/test_keypairs.py b/novaclient/tests/functional/test_keypairs.py index 1d5b09e64..f980349bb 100644 --- a/novaclient/tests/functional/test_keypairs.py +++ b/novaclient/tests/functional/test_keypairs.py @@ -23,6 +23,8 @@ class TestKeypairsNovaClient(base.ClientTestBase): """Keypairs functional tests. """ + COMPUTE_API_VERSION = "2.1" + def _serialize_kwargs(self, kwargs): kwargs_pairs = ['--%(key)s %(val)s' % {'key': key.replace('_', '-'), 'val': val} @@ -96,9 +98,7 @@ class TestKeypairsNovaClientV22(TestKeypairsNovaClient): """Keypairs functional tests for v2.2 nova-api microversion. """ - def nova(self, *args, **kwargs): - return self.cli_clients.nova(flags='--os-compute-api-version 2.2 ', - *args, **kwargs) + COMPUTE_API_VERSION = "2.2" def test_create_keypair(self): keypair = super(TestKeypairsNovaClientV22, self).test_create_keypair() diff --git a/novaclient/tests/functional/test_quotas.py b/novaclient/tests/functional/test_quotas.py index cd6392fd5..d0ee710a2 100644 --- a/novaclient/tests/functional/test_quotas.py +++ b/novaclient/tests/functional/test_quotas.py @@ -17,6 +17,8 @@ class TestQuotasNovaClient(base.ClientTestBase): """Nova quotas functional tests. """ + COMPUTE_API_VERSION = "2.1" + _quota_resources = ['instances', 'cores', 'ram', 'floating_ips', 'fixed_ips', 'metadata_items', 'injected_files', 'injected_file_content_bytes', diff --git a/novaclient/tests/functional/test_readonly_nova.py b/novaclient/tests/functional/test_readonly_nova.py index 17cfc3097..d7a183e56 100644 --- a/novaclient/tests/functional/test_readonly_nova.py +++ b/novaclient/tests/functional/test_readonly_nova.py @@ -24,6 +24,8 @@ class SimpleReadOnlyNovaClientTest(base.ClientTestBase): This only exercises client commands that are read only. """ + COMPUTE_API_VERSION = "2.1" + def test_admin_fake_action(self): self.assertRaises(exceptions.CommandFailed, self.nova, diff --git a/novaclient/tests/functional/test_servers.py b/novaclient/tests/functional/test_servers.py index 75be3f472..a29021342 100644 --- a/novaclient/tests/functional/test_servers.py +++ b/novaclient/tests/functional/test_servers.py @@ -20,6 +20,8 @@ class TestServersBootNovaClient(base.ClientTestBase): """Servers boot functional tests. """ + COMPUTE_API_VERSION = "2.1" + def test_boot_server_with_legacy_bdm(self): volume_size = 1 volume_name = str(uuid.uuid4()) @@ -44,6 +46,8 @@ class TestServersListNovaClient(base.ClientTestBase): """Servers list functional tests. """ + COMPUTE_API_VERSION = "2.1" + def _create_servers(self, name, number): network = self.client.networks.list()[0] servers = [] diff --git a/novaclient/tests/functional/test_volumes_api.py b/novaclient/tests/functional/test_volumes_api.py index fcfd9f122..b574ad783 100644 --- a/novaclient/tests/functional/test_volumes_api.py +++ b/novaclient/tests/functional/test_volumes_api.py @@ -32,6 +32,8 @@ def wait_for_delete(test, name, thing, get_func): class TestVolumesAPI(base.ClientTestBase): + COMPUTE_API_VERSION = "2.1" + def test_volumes_snapshots_types_create_get_list_delete(self): # Create a volume volume = self.client.volumes.create(1) From 0455932c3af6d97bc29c41a2595d713f9b909ad2 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Thu, 17 Sep 2015 15:03:56 +0300 Subject: [PATCH 0849/1705] Split functional tests for legacy(V2.1) and microversions It would be nice to ensure that fundamental use-cases of novaclient work for both legacy(V2.1) and for microversions(latest or specified microversion). Change-Id: Icae73dd221c9787fd08facc8642c5b564e82393a --- novaclient/tests/functional/v2/__init__.py | 0 .../tests/functional/{ => v2}/fake_crypto.py | 0 .../tests/functional/v2/legacy/__init__.py | 0 .../{ => v2/legacy}/test_fixedips.py | 9 ---- .../{ => v2/legacy}/test_instances.py | 0 .../{ => v2/legacy}/test_keypairs.py | 32 +----------- .../functional/{ => v2/legacy}/test_quotas.py | 0 .../{ => v2/legacy}/test_readonly_nova.py | 0 .../{ => v2/legacy}/test_servers.py | 0 .../{ => v2/legacy}/test_volumes_api.py | 0 .../tests/functional/v2/test_fixedips.py | 23 ++++++++ .../tests/functional/v2/test_instances.py | 18 +++++++ .../tests/functional/v2/test_keypairs.py | 44 ++++++++++++++++ novaclient/tests/functional/v2/test_quotas.py | 52 +++++++++++++++++++ .../tests/functional/v2/test_readonly_nova.py | 25 +++++++++ .../tests/functional/v2/test_servers.py | 27 ++++++++++ .../tests/functional/v2/test_volumes_api.py | 18 +++++++ 17 files changed, 208 insertions(+), 40 deletions(-) create mode 100644 novaclient/tests/functional/v2/__init__.py rename novaclient/tests/functional/{ => v2}/fake_crypto.py (100%) create mode 100644 novaclient/tests/functional/v2/legacy/__init__.py rename novaclient/tests/functional/{ => v2/legacy}/test_fixedips.py (89%) rename novaclient/tests/functional/{ => v2/legacy}/test_instances.py (100%) rename novaclient/tests/functional/{ => v2/legacy}/test_keypairs.py (72%) rename novaclient/tests/functional/{ => v2/legacy}/test_quotas.py (100%) rename novaclient/tests/functional/{ => v2/legacy}/test_readonly_nova.py (100%) rename novaclient/tests/functional/{ => v2/legacy}/test_servers.py (100%) rename novaclient/tests/functional/{ => v2/legacy}/test_volumes_api.py (100%) create mode 100644 novaclient/tests/functional/v2/test_fixedips.py create mode 100644 novaclient/tests/functional/v2/test_instances.py create mode 100644 novaclient/tests/functional/v2/test_keypairs.py create mode 100644 novaclient/tests/functional/v2/test_quotas.py create mode 100644 novaclient/tests/functional/v2/test_readonly_nova.py create mode 100644 novaclient/tests/functional/v2/test_servers.py create mode 100644 novaclient/tests/functional/v2/test_volumes_api.py diff --git a/novaclient/tests/functional/v2/__init__.py b/novaclient/tests/functional/v2/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/novaclient/tests/functional/fake_crypto.py b/novaclient/tests/functional/v2/fake_crypto.py similarity index 100% rename from novaclient/tests/functional/fake_crypto.py rename to novaclient/tests/functional/v2/fake_crypto.py diff --git a/novaclient/tests/functional/v2/legacy/__init__.py b/novaclient/tests/functional/v2/legacy/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/novaclient/tests/functional/test_fixedips.py b/novaclient/tests/functional/v2/legacy/test_fixedips.py similarity index 89% rename from novaclient/tests/functional/test_fixedips.py rename to novaclient/tests/functional/v2/legacy/test_fixedips.py index 095492470..7082c110d 100644 --- a/novaclient/tests/functional/test_fixedips.py +++ b/novaclient/tests/functional/v2/legacy/test_fixedips.py @@ -52,12 +52,3 @@ def _test_fixedip_get(self, expect_reserved=False): def test_fixedip_get(self): self._test_fixedip_get() - - -class TestFixedIPsNovaClientV24(TestFixedIPsNovaClient): - """FixedIPs functional tests for v2.4 nova-api microversion.""" - - COMPUTE_API_VERSION = '2.4' - - def test_fixedip_get(self): - self._test_fixedip_get(expect_reserved=True) diff --git a/novaclient/tests/functional/test_instances.py b/novaclient/tests/functional/v2/legacy/test_instances.py similarity index 100% rename from novaclient/tests/functional/test_instances.py rename to novaclient/tests/functional/v2/legacy/test_instances.py diff --git a/novaclient/tests/functional/test_keypairs.py b/novaclient/tests/functional/v2/legacy/test_keypairs.py similarity index 72% rename from novaclient/tests/functional/test_keypairs.py rename to novaclient/tests/functional/v2/legacy/test_keypairs.py index f980349bb..3b7efefcf 100644 --- a/novaclient/tests/functional/test_keypairs.py +++ b/novaclient/tests/functional/v2/legacy/test_keypairs.py @@ -16,7 +16,7 @@ from tempest_lib import exceptions from novaclient.tests.functional import base -from novaclient.tests.functional import fake_crypto +from novaclient.tests.functional.v2 import fake_crypto class TestKeypairsNovaClient(base.ClientTestBase): @@ -92,33 +92,3 @@ def test_delete_keypair(self): # keypair-show should fail if no keypair with given name is found. self.assertRaises(exceptions.CommandFailed, self._show_keypair, key_name) - - -class TestKeypairsNovaClientV22(TestKeypairsNovaClient): - """Keypairs functional tests for v2.2 nova-api microversion. - """ - - COMPUTE_API_VERSION = "2.2" - - def test_create_keypair(self): - keypair = super(TestKeypairsNovaClientV22, self).test_create_keypair() - self.assertIn('ssh', keypair) - - def test_create_keypair_x509(self): - key_name = self._create_keypair(key_type='x509') - keypair = self._show_keypair(key_name) - self.assertIn(key_name, keypair) - self.assertIn('x509', keypair) - - def test_import_keypair(self): - pub_key, fingerprint = fake_crypto.get_ssh_pub_key_and_fingerprint() - pub_key_file = self._create_public_key_file(pub_key) - keypair = self._test_import_keypair(fingerprint, pub_key=pub_key_file) - self.assertIn('ssh', keypair) - - def test_import_keypair_x509(self): - certif, fingerprint = fake_crypto.get_x509_cert_and_fingerprint() - pub_key_file = self._create_public_key_file(certif) - keypair = self._test_import_keypair(fingerprint, key_type='x509', - pub_key=pub_key_file) - self.assertIn('x509', keypair) diff --git a/novaclient/tests/functional/test_quotas.py b/novaclient/tests/functional/v2/legacy/test_quotas.py similarity index 100% rename from novaclient/tests/functional/test_quotas.py rename to novaclient/tests/functional/v2/legacy/test_quotas.py diff --git a/novaclient/tests/functional/test_readonly_nova.py b/novaclient/tests/functional/v2/legacy/test_readonly_nova.py similarity index 100% rename from novaclient/tests/functional/test_readonly_nova.py rename to novaclient/tests/functional/v2/legacy/test_readonly_nova.py diff --git a/novaclient/tests/functional/test_servers.py b/novaclient/tests/functional/v2/legacy/test_servers.py similarity index 100% rename from novaclient/tests/functional/test_servers.py rename to novaclient/tests/functional/v2/legacy/test_servers.py diff --git a/novaclient/tests/functional/test_volumes_api.py b/novaclient/tests/functional/v2/legacy/test_volumes_api.py similarity index 100% rename from novaclient/tests/functional/test_volumes_api.py rename to novaclient/tests/functional/v2/legacy/test_volumes_api.py diff --git a/novaclient/tests/functional/v2/test_fixedips.py b/novaclient/tests/functional/v2/test_fixedips.py new file mode 100644 index 000000000..15d530d59 --- /dev/null +++ b/novaclient/tests/functional/v2/test_fixedips.py @@ -0,0 +1,23 @@ +# Copyright 2015 IBM Corp. +# 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 novaclient.tests.functional.v2.legacy import test_fixedips + + +class TestFixedIPsNovaClientV24(test_fixedips.TestFixedIPsNovaClient): + """FixedIPs functional tests for v2.4 nova-api microversion.""" + + COMPUTE_API_VERSION = '2.4' + + def test_fixedip_get(self): + self._test_fixedip_get(expect_reserved=True) diff --git a/novaclient/tests/functional/v2/test_instances.py b/novaclient/tests/functional/v2/test_instances.py new file mode 100644 index 000000000..289c53df4 --- /dev/null +++ b/novaclient/tests/functional/v2/test_instances.py @@ -0,0 +1,18 @@ +# 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 novaclient.tests.functional.v2.legacy import test_instances + + +class TestInstanceCLI(test_instances.TestInstanceCLI): + + COMPUTE_API_VERSION = "2.latest" diff --git a/novaclient/tests/functional/v2/test_keypairs.py b/novaclient/tests/functional/v2/test_keypairs.py new file mode 100644 index 000000000..35768de7b --- /dev/null +++ b/novaclient/tests/functional/v2/test_keypairs.py @@ -0,0 +1,44 @@ +# 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 novaclient.tests.functional.v2 import fake_crypto +from novaclient.tests.functional.v2.legacy import test_keypairs + + +class TestKeypairsNovaClientV22(test_keypairs.TestKeypairsNovaClient): + """Keypairs functional tests for v2.2 nova-api microversion. + """ + + COMPUTE_API_VERSION = "2.2" + + def test_create_keypair(self): + keypair = super(TestKeypairsNovaClientV22, self).test_create_keypair() + self.assertIn('ssh', keypair) + + def test_create_keypair_x509(self): + key_name = self._create_keypair(key_type='x509') + keypair = self._show_keypair(key_name) + self.assertIn(key_name, keypair) + self.assertIn('x509', keypair) + + def test_import_keypair(self): + pub_key, fingerprint = fake_crypto.get_ssh_pub_key_and_fingerprint() + pub_key_file = self._create_public_key_file(pub_key) + keypair = self._test_import_keypair(fingerprint, pub_key=pub_key_file) + self.assertIn('ssh', keypair) + + def test_import_keypair_x509(self): + certif, fingerprint = fake_crypto.get_x509_cert_and_fingerprint() + pub_key_file = self._create_public_key_file(certif) + keypair = self._test_import_keypair(fingerprint, key_type='x509', + pub_key=pub_key_file) + self.assertIn('x509', keypair) diff --git a/novaclient/tests/functional/v2/test_quotas.py b/novaclient/tests/functional/v2/test_quotas.py new file mode 100644 index 000000000..14b5bdfef --- /dev/null +++ b/novaclient/tests/functional/v2/test_quotas.py @@ -0,0 +1,52 @@ +# 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 novaclient.tests.functional.v2.legacy import test_quotas + + +class TestQuotasNovaClient(test_quotas.TestQuotasNovaClient): + """Nova quotas functional tests. + """ + + COMPUTE_API_VERSION = "2.latest" + + _quota_resources = ['instances', 'cores', 'ram', + 'floating_ips', 'fixed_ips', 'metadata_items', + 'injected_files', 'injected_file_content_bytes', + 'injected_file_path_bytes', 'key_pairs', + 'security_groups', 'security_group_rules', + 'server_groups', 'server_group_members'] + + def test_quotas_update(self): + # `nova quota-update` requires tenant-id. + tenant_info = self.cli_clients.keystone( + "tenant-get", params=self.cli_clients.tenant_name) + tenant_id = self._get_value_from_the_table(tenant_info, "id") + + self.addCleanup(self.client.quotas.delete, tenant_id) + + original_quotas = self.client.quotas.get(tenant_id) + + difference = 10 + params = [tenant_id] + for quota_name in self._quota_resources: + params.append("--%(name)s %(value)s" % { + "name": quota_name.replace("_", "-"), + "value": getattr(original_quotas, quota_name) + difference}) + + self.nova("quota-update", params=" ".join(params)) + + updated_quotas = self.client.quotas.get(tenant_id) + + for quota_name in self._quota_resources: + self.assertEqual(getattr(original_quotas, quota_name), + getattr(updated_quotas, quota_name) - difference) diff --git a/novaclient/tests/functional/v2/test_readonly_nova.py b/novaclient/tests/functional/v2/test_readonly_nova.py new file mode 100644 index 000000000..cfe91635a --- /dev/null +++ b/novaclient/tests/functional/v2/test_readonly_nova.py @@ -0,0 +1,25 @@ +# 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 novaclient.tests.functional.v2.legacy import test_readonly_nova + + +class SimpleReadOnlyNovaClientTest( + test_readonly_nova.SimpleReadOnlyNovaClientTest): + + """ + read only functional python-novaclient tests. + + This only exercises client commands that are read only. + """ + + COMPUTE_API_VERSION = "2.latest" diff --git a/novaclient/tests/functional/v2/test_servers.py b/novaclient/tests/functional/v2/test_servers.py new file mode 100644 index 000000000..c3b2fdcd2 --- /dev/null +++ b/novaclient/tests/functional/v2/test_servers.py @@ -0,0 +1,27 @@ +# 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 novaclient.tests.functional.v2.legacy import test_servers + + +class TestServersBootNovaClient(test_servers.TestServersBootNovaClient): + """Servers boot functional tests. + """ + + COMPUTE_API_VERSION = "2.latest" + + +class TestServersListNovaClient(test_servers.TestServersListNovaClient): + """Servers list functional tests. + """ + + COMPUTE_API_VERSION = "2.latest" diff --git a/novaclient/tests/functional/v2/test_volumes_api.py b/novaclient/tests/functional/v2/test_volumes_api.py new file mode 100644 index 000000000..8bf35d508 --- /dev/null +++ b/novaclient/tests/functional/v2/test_volumes_api.py @@ -0,0 +1,18 @@ +# 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 novaclient.tests.functional.v2.legacy import test_volumes_api + + +class TestVolumesAPI(test_volumes_api.TestVolumesAPI): + + COMPUTE_API_VERSION = "2.latest" From 41ea5276b0e4cf176b06c35248db17c9e22bd480 Mon Sep 17 00:00:00 2001 From: rsritesh Date: Mon, 21 Sep 2015 12:36:41 +0200 Subject: [PATCH 0850/1705] Modify nova help list message for --tenant The --all-tenants option does not have impact when --tenant is used. Help message about --all-tenants inside the help section of --tenant creates uncertainty. Change-Id: Iaddf2370f349c6da712e0096a6ecbcbc61d37836 Closes-Bug: #1497151 --- novaclient/v2/shell.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index dcd27843e..9fbf4e358 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -1307,8 +1307,7 @@ def do_image_delete(cs, args): dest='tenant', metavar='', nargs='?', - help=_('Display information from single tenant (Admin only). ' - 'The --all-tenants option must also be provided.')) + help=_('Display information from single tenant (Admin only).')) @cliutils.arg( '--user', dest='user', From c3a1cbdcd2dade0d67144c786483c30f5b576edf Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 21 Sep 2015 14:54:04 +0000 Subject: [PATCH 0851/1705] Change ignore-errors to ignore_errors Needed for coverage 4.0 Change-Id: I45d7ab91004750f304470381d369f8be1c9b5004 --- .coveragerc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.coveragerc b/.coveragerc index 9f280d178..933b5e5ac 100644 --- a/.coveragerc +++ b/.coveragerc @@ -4,4 +4,4 @@ source = novaclient omit = novaclient/openstack/* [report] -ignore-errors = True +ignore_errors = True From 3b0e81e20a174b3f981f99cc454edf7d93850699 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Thu, 24 Sep 2015 06:00:44 -0700 Subject: [PATCH 0852/1705] Use dictionary literal for dictionary creation Dictionary creation could be rewritten as a dictionary literal. for example: params = {} params['group_id'] = source_group.id could be rewritten as params = {'group_id': source_group.id} TrivialFix Change-Id: Ide92a59a8b8ba3b2f151ff2c907eee07b367967e --- novaclient/v2/shell.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 2f2c61d5d..75f8cc0a7 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -2854,8 +2854,7 @@ def do_secgroup_add_group_rule(cs, args): """Add a source group rule to a security group.""" secgroup = _get_secgroup(cs, args.secgroup) source_group = _get_secgroup(cs, args.source_group) - params = {} - params['group_id'] = source_group.id + params = {'group_id': source_group.id} if args.ip_proto or args.from_port or args.to_port: if not (args.ip_proto and args.from_port and args.to_port): @@ -2893,8 +2892,7 @@ def do_secgroup_delete_group_rule(cs, args): """Delete a source group rule from a security group.""" secgroup = _get_secgroup(cs, args.secgroup) source_group = _get_secgroup(cs, args.source_group) - params = {} - params['group_name'] = source_group.name + params = {'group_name': source_group.name} if args.ip_proto or args.from_port or args.to_port: if not (args.ip_proto and args.from_port and args.to_port): From 0cd58123be5e2c2bc2f1233a3e15fb35b2ead1a0 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Wed, 26 Aug 2015 16:19:25 +0300 Subject: [PATCH 0853/1705] Remove novaclient.v1_1 module A long time ago in a galaxy far far away...this module was deprecated (see 0a60aae852d2688861d0b4ba097a1a00529f0611 for more details) and it's time to remove it(one full release of deprecation (Liberty) is ended). Also, let's stop testing compute-api-version=1.1 . Change-Id: I2ce177d8074921a54b4e3d1bfe3b794a80df9358 --- novaclient/tests/unit/v2/test_shell.py | 11 ------- novaclient/v1_1/__init__.py | 43 -------------------------- 2 files changed, 54 deletions(-) delete mode 100644 novaclient/v1_1/__init__.py diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index ed68c383e..eae4528de 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2503,17 +2503,6 @@ def test_list_server_group_with_all_projects(self): self.assert_called('GET', '/os-server-groups?all_projects') -class ShellTestV11(ShellTest): - FAKE_ENV = { - 'NOVA_USERNAME': 'username', - 'NOVA_PASSWORD': 'password', - 'NOVA_PROJECT_ID': 'project_id', - 'OS_COMPUTE_API_VERSION': '1.1', - 'NOVA_URL': 'http://no.where', - 'OS_AUTH_URL': 'http://no.where/v2.0', - } - - class ShellWithSessionClientTest(ShellTest): def setUp(self): diff --git a/novaclient/v1_1/__init__.py b/novaclient/v1_1/__init__.py deleted file mode 100644 index 9da768172..000000000 --- a/novaclient/v1_1/__init__.py +++ /dev/null @@ -1,43 +0,0 @@ -# -# All Rights Reserved. -# -# 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. - -# NOTE(akurilin): This module is left for backward compatibility. Feel free to -# remove it, when openstack project will use correct way to -# obtain novaclient object. -# Known problems: -# * python-openstackclient - -# https://bugs.launchpad.net/python-openstackclient/+bug/1418024 -# * neutron - https://bugs.launchpad.net/neutron/+bug/1418017 - - -import sys -import warnings - -from novaclient import v2 - -warnings.warn("Module novaclient.v1_1 is deprecated (taken as a basis for " - "novaclient.v2). " - "The preferable way to get client class or object you can find " - "in novaclient.client module.") - - -class MovedModule(object): - def __init__(self, new_module): - self.new_module = new_module - - def __getattr__(self, attr): - return getattr(self.new_module, attr) - -sys.modules["novaclient.v1_1"] = MovedModule(v2) From 3866bfb2979b0434ae62feaf9637818447f51c12 Mon Sep 17 00:00:00 2001 From: Kevin_Zheng Date: Tue, 29 Sep 2015 15:25:29 +0800 Subject: [PATCH 0854/1705] Add --metadata as optional input when do nova image-create Currently, when using nova image-create, we are not able to input metadata for the snapshot, and it is always set to be None. This patch add --metadata as an optional input when calling nova image-create. Change-Id: Ic72dc3652a77cfad099e144423302042a47f6e1e Closes-bug: #1500727 --- novaclient/tests/unit/v2/test_shell.py | 9 +++++++++ novaclient/v2/shell.py | 10 +++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index ed68c383e..ca661ce81 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -804,6 +804,15 @@ def test_create_image(self): {'createImage': {'name': 'mysnapshot', 'metadata': {}}}, ) + def test_create_image_with_metadata(self): + self.run_command( + 'image-create sample-server mysnapshot --metadata mykey=123') + self.assert_called( + 'POST', '/servers/1234/action', + {'createImage': {'name': 'mysnapshot', + 'metadata': {'mykey': '123'}}}, + ) + def test_create_image_show(self): output, _ = self.run_command( 'image-create sample-server mysnapshot --show') diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 2f2c61d5d..db0e28486 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -1783,6 +1783,13 @@ def do_set_password(cs, args): @cliutils.arg('server', metavar='', help=_('Name or ID of server.')) @cliutils.arg('name', metavar='', help=_('Name of snapshot.')) +@cliutils.arg( + '--metadata', + metavar="", + action='append', + default=[], + help=_("Record arbitrary key/value metadata to /meta_data.json " + "on the metadata server. Can be specified multiple times.")) @cliutils.arg( '--show', dest='show', @@ -1799,7 +1806,8 @@ def do_set_password(cs, args): def do_image_create(cs, args): """Create a new image by taking a snapshot of a running server.""" server = _find_server(cs, args.server) - image_uuid = cs.servers.create_image(server, args.name) + meta = dict(v.split('=', 1) for v in args.metadata) or None + image_uuid = cs.servers.create_image(server, args.name, meta) if args.poll: _poll_for_status(cs.images.get, image_uuid, 'snapshotting', From 3350a5713da9b75100d94a87908f6cb4184eb240 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Wed, 30 Sep 2015 12:59:24 +0300 Subject: [PATCH 0855/1705] Change default service_type for volumes managers Currently, novaclient's gates are broken due to missed 'volume' service_type. This patch changes default service type for all volume related managers to "volumev2" and leave ability to set "volume" service_type. Change-Id: Ia1e1d3def1e6127cc2b97797c577c15265f879bd Closes-Bug: #1501258 --- novaclient/base.py | 13 ++++++++----- novaclient/v2/volume_snapshots.py | 13 +++++++++---- novaclient/v2/volume_types.py | 12 ++++++++---- novaclient/v2/volumes.py | 12 ++++++++---- 4 files changed, 33 insertions(+), 17 deletions(-) diff --git a/novaclient/base.py b/novaclient/base.py index a07bd2497..96aaec1d1 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -89,13 +89,16 @@ def _list(self, url, response_key, obj_class=None, body=None): for res in data if res] @contextlib.contextmanager - def alternate_service_type(self, service_type): + def alternate_service_type(self, default, allowed_types=()): original_service_type = self.api.client.service_type - self.api.client.service_type = service_type - try: + if original_service_type in allowed_types: yield - finally: - self.api.client.service_type = original_service_type + else: + self.api.client.service_type = default + try: + yield + finally: + self.api.client.service_type = original_service_type @contextlib.contextmanager def completion_cache(self, cache_type, obj_class, mode): diff --git a/novaclient/v2/volume_snapshots.py b/novaclient/v2/volume_snapshots.py index ffdb812c0..6cf597a1e 100644 --- a/novaclient/v2/volume_snapshots.py +++ b/novaclient/v2/volume_snapshots.py @@ -61,7 +61,8 @@ def create(self, volume_id, force=False, display_name=None, 'deprecated and will be removed after Nova 13.0.0 is ' 'released. Use python-cinderclient or ' 'python-openstacksdk instead.', DeprecationWarning) - with self.alternate_service_type('volume'): + with self.alternate_service_type( + 'volumev2', allowed_types=('volume', 'volumev2')): body = {'snapshot': {'volume_id': volume_id, 'force': force, 'display_name': display_name, @@ -79,7 +80,9 @@ def get(self, snapshot_id): 'deprecated and will be removed after Nova 13.0.0 is ' 'released. Use python-cinderclient or ' 'python-openstacksdk instead.', DeprecationWarning) - with self.alternate_service_type('volume'): + with self.alternate_service_type( + 'volumev2', allowed_types=('volume', 'volumev2')): + return self._get("/snapshots/%s" % snapshot_id, "snapshot") def list(self, detailed=True): @@ -92,7 +95,8 @@ def list(self, detailed=True): 'deprecated and will be removed after Nova 13.0.0 is ' 'released. Use python-cinderclient or ' 'python-openstacksdk instead.', DeprecationWarning) - with self.alternate_service_type('volume'): + with self.alternate_service_type( + 'volumev2', allowed_types=('volume', 'volumev2')): if detailed is True: return self._list("/snapshots/detail", "snapshots") else: @@ -108,5 +112,6 @@ def delete(self, snapshot): 'deprecated and will be removed after Nova 13.0.0 is ' 'released. Use python-cinderclient or ' 'python-openstacksdk instead.', DeprecationWarning) - with self.alternate_service_type('volume'): + with self.alternate_service_type( + 'volumev2', allowed_types=('volume', 'volumev2')): self._delete("/snapshots/%s" % base.getid(snapshot)) diff --git a/novaclient/v2/volume_types.py b/novaclient/v2/volume_types.py index 861c4a357..4cd629018 100644 --- a/novaclient/v2/volume_types.py +++ b/novaclient/v2/volume_types.py @@ -47,7 +47,8 @@ def list(self): 'and will be removed after Nova 13.0.0 is released. Use ' 'python-cinderclient or python-openstacksdk instead.', DeprecationWarning) - with self.alternate_service_type('volume'): + with self.alternate_service_type( + 'volumev2', allowed_types=('volume', 'volumev2')): return self._list("/types", "volume_types") def get(self, volume_type): @@ -61,7 +62,8 @@ def get(self, volume_type): 'and will be removed after Nova 13.0.0 is released. Use ' 'python-cinderclient or python-openstacksdk instead.', DeprecationWarning) - with self.alternate_service_type('volume'): + with self.alternate_service_type( + 'volumev2', allowed_types=('volume', 'volumev2')): return self._get("/types/%s" % base.getid(volume_type), "volume_type") @@ -75,7 +77,8 @@ def delete(self, volume_type): 'and will be removed after Nova 13.0.0 is released. Use ' 'python-cinderclient or python-openstacksdk instead.', DeprecationWarning) - with self.alternate_service_type('volume'): + with self.alternate_service_type( + 'volumev2', allowed_types=('volume', 'volumev2')): self._delete("/types/%s" % base.getid(volume_type)) def create(self, name): @@ -89,7 +92,8 @@ def create(self, name): 'and will be removed after Nova 13.0.0 is released. Use ' 'python-cinderclient or python-openstacksdk instead.', DeprecationWarning) - with self.alternate_service_type('volume'): + with self.alternate_service_type( + 'volumev2', allowed_types=('volume', 'volumev2')): body = { "volume_type": { "name": name, diff --git a/novaclient/v2/volumes.py b/novaclient/v2/volumes.py index d0446c5fd..802cf500d 100644 --- a/novaclient/v2/volumes.py +++ b/novaclient/v2/volumes.py @@ -68,7 +68,8 @@ def create(self, size, snapshot_id=None, display_name=None, '13.0.0 is released. Use python-cinderclient or ' 'python-openstacksdk instead.', DeprecationWarning) # NOTE(melwitt): Ensure we use the volume endpoint for this call - with self.alternate_service_type('volume'): + with self.alternate_service_type( + 'volumev2', allowed_types=('volume', 'volumev2')): body = {'volume': {'size': size, 'snapshot_id': snapshot_id, 'display_name': display_name, @@ -89,7 +90,8 @@ def get(self, volume_id): 'method is deprecated and will be removed after Nova ' '13.0.0 is released. Use python-cinderclient or ' 'python-openstacksdk instead.', DeprecationWarning) - with self.alternate_service_type('volume'): + with self.alternate_service_type( + 'volumev2', allowed_types=('volume', 'volumev2')): return self._get("/volumes/%s" % volume_id, "volume") def list(self, detailed=True, search_opts=None): @@ -102,7 +104,8 @@ def list(self, detailed=True, search_opts=None): 'method is deprecated and will be removed after Nova ' '13.0.0 is released. Use python-cinderclient or ' 'python-openstacksdk instead.', DeprecationWarning) - with self.alternate_service_type('volume'): + with self.alternate_service_type( + 'volumev2', allowed_types=('volume', 'volumev2')): search_opts = search_opts or {} if 'name' in search_opts.keys(): @@ -128,7 +131,8 @@ def delete(self, volume): 'method is deprecated and will be removed after Nova ' '13.0.0 is released. Use python-cinderclient or ' 'python-openstacksdk instead.', DeprecationWarning) - with self.alternate_service_type('volume'): + with self.alternate_service_type( + 'volumev2', allowed_types=('volume', 'volumev2')): self._delete("/volumes/%s" % base.getid(volume)) def create_server_volume(self, server_id, volume_id, device=None): From abd0630badaff18ca998db2feccb058e8ee75f23 Mon Sep 17 00:00:00 2001 From: melanie witt Date: Wed, 30 Sep 2015 22:55:03 +0000 Subject: [PATCH 0856/1705] Always send volume_id when booting with legacy bdm Commit 5153dcda807c554769081626c10c43d16adea671 removed bdm v2-only request parameters to pass nova api v2.1 schema validation, but also removed the ability to boot with legacy bdm specifying volume_id only. This adds volume_id back to the request for legacy bdm when no other parameters are specified. Closes-Bug: #1501435 Change-Id: Ie8c56c28492793990ef7ed6dc54768cef9e28a98 --- novaclient/base.py | 4 +++- .../functional/v2/legacy/test_servers.py | 21 ++++++++++++++++--- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/novaclient/base.py b/novaclient/base.py index 96aaec1d1..82c98904a 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -292,7 +292,9 @@ def _parse_block_device_mapping(self, block_device_mapping): mapping_parts = mapping.split(':') source_id = mapping_parts[0] - if len(mapping_parts) > 1: + if len(mapping_parts) == 1: + bdm_dict['volume_id'] = source_id + elif len(mapping_parts) > 1: source_type = mapping_parts[1] if source_type.startswith('snap'): bdm_dict['snapshot_id'] = source_id diff --git a/novaclient/tests/functional/v2/legacy/test_servers.py b/novaclient/tests/functional/v2/legacy/test_servers.py index a29021342..0e968562a 100644 --- a/novaclient/tests/functional/v2/legacy/test_servers.py +++ b/novaclient/tests/functional/v2/legacy/test_servers.py @@ -22,7 +22,7 @@ class TestServersBootNovaClient(base.ClientTestBase): COMPUTE_API_VERSION = "2.1" - def test_boot_server_with_legacy_bdm(self): + def _boot_server_with_legacy_bdm(self, bdm_params=()): volume_size = 1 volume_name = str(uuid.uuid4()) volume = self.client.volumes.create(size=volume_size, @@ -30,17 +30,32 @@ def test_boot_server_with_legacy_bdm(self): imageRef=self.image.id) self.wait_for_volume_status(volume, "available") + bdm_params = ':'.join(bdm_params) + if bdm_params: + bdm_params = ''.join((':', bdm_params)) + server_info = self.nova("boot", params=( "%(name)s --flavor %(flavor)s --poll " - "--block-device-mapping vda=%(volume_id)s:::1" % { + "--block-device-mapping vda=%(volume_id)s%(bdm_params)s" % { "name": str(uuid.uuid4()), "flavor": self.flavor.id, - "volume_id": volume.id})) + "volume_id": volume.id, + "bdm_params": bdm_params})) server_id = self._get_value_from_the_table(server_info, "id") self.client.servers.delete(server_id) self.wait_for_resource_delete(server_id, self.client.servers) + def test_boot_server_with_legacy_bdm(self): + # bdm v1 format + # ::: + # params = (type, size, delete-on-terminate) + params = ('', '', '1') + self._boot_server_with_legacy_bdm(bdm_params=params) + + def test_boot_server_with_legacy_bdm_volume_id_only(self): + self._boot_server_with_legacy_bdm() + class TestServersListNovaClient(base.ClientTestBase): """Servers list functional tests. From d045019f0f2d28f26730b3617f7d7b993506b6e8 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Thu, 1 Oct 2015 09:19:14 -0700 Subject: [PATCH 0857/1705] Set DEFAULT_OS_COMPUTE_API_VERSION to 2.5 The client currently implements support for microversions 2.2, 2.4 and 2.11. According the nova API microversion history: http://docs.openstack.org/developer/nova/api_microversion_history.html 2.3 just returns extra attributes in the response so it's not a problem for the client. We should expose those at some point but it's not a breaking change. 2.5 is a server-side only change since the client allows filtering servers by IPv6 address but the server wasn't honoring that until 2.5. There are no client side changes for that microversion. 2.6 is the first backwards incompatible microversion since it replaces the old specific console APIs (RDP/serial/SPICE/VNC) with a new generic remote-consoles API. The client must be updated to handle this since requesting any microversion >=2.6 for consoles will fail right now. This is exposed on the CLI right now because the CLI defaults to the 2.latest microversion. So even though the client actually supports the 2.11 microversion, we shouldn't default to higher than 2.5 because we have gaps in handling 2.6->2.10. This change fixes the default for the CLI by setting DEFAULT_OS_COMPUTE_API_VERSION=2.5. Partial-Bug: #1500688 Change-Id: I52074f9a3e7faa6a7a51c3fa9766100acf25dee2 --- novaclient/__init__.py | 6 ++++++ novaclient/shell.py | 11 ++++++++++- novaclient/tests/unit/test_shell.py | 8 ++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 5e6ccb0bb..2e86783f9 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -20,4 +20,10 @@ __version__ = pbr.version.VersionInfo('python-novaclient').version_string() API_MIN_VERSION = api_versions.APIVersion("2.1") +# The max version should be the latest version that is supported in the client, +# not necessarily the latest that the server can provide. This is what a user +# can opt into with the --os-compute-api-version option on the CLI. Note that +# there may be gaps in supported microversions before this max version which is +# why novaclient.shell.DEFAULT_OS_COMPUTE_API_VERSION is not 2.latest or +# necessarily equal to API_MAX_VERSION. API_MAX_VERSION = api_versions.APIVersion("2.11") diff --git a/novaclient/shell.py b/novaclient/shell.py index 9a7d06cb6..05651734c 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -51,7 +51,16 @@ from novaclient import utils DEFAULT_MAJOR_OS_COMPUTE_API_VERSION = "2.0" -DEFAULT_OS_COMPUTE_API_VERSION = "2.latest" +# We default to the highest *incremental* version that we know we can support. +# There should not be gaps in support for this version even if +# novaclient.API_MAX_VERSION is higher. The difference is API_MAX_VERSION +# caps what the user can request (they are opting into something), whereas +# DEFAULT_OS_COMPUTE_API_VERSION should be the highest version that the client +# actually supports without gaps in between. When a higher incremental version +# is implemented in the client, this version should be updated. Note that this +# value should never be 2.latest since what's latest changes depending on which +# cloud provider you're talking to. +DEFAULT_OS_COMPUTE_API_VERSION = '2.5' DEFAULT_NOVA_ENDPOINT_TYPE = 'publicURL' DEFAULT_NOVA_SERVICE_TYPE = "compute" diff --git a/novaclient/tests/unit/test_shell.py b/novaclient/tests/unit/test_shell.py index 37a1ab69d..cf76b92c1 100644 --- a/novaclient/tests/unit/test_shell.py +++ b/novaclient/tests/unit/test_shell.py @@ -516,6 +516,14 @@ class MyException(Exception): err = sys.stderr.getvalue() self.assertEqual(err, 'ERROR (MyException): message\n') + def test_default_os_compute_api_version(self): + default_version = api_versions.APIVersion( + novaclient.shell.DEFAULT_OS_COMPUTE_API_VERSION) + # The default should never be the latest. + self.assertFalse(default_version.is_latest()) + # The default should be less than or equal to API_MAX_VERSION. + self.assertLessEqual(default_version, novaclient.API_MAX_VERSION) + class TestLoadVersionedActions(utils.TestCase): From 3697782616fa0dbc9141736e4f6027bf7bb1ea15 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 2 Oct 2015 17:20:41 +0000 Subject: [PATCH 0858/1705] Updated from global requirements Change-Id: I3fcd0aa48f225b311ad4d64e355e059983944b58 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 3bb675e63..141d8d182 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -15,4 +15,4 @@ oslosphinx>=2.5.0 # Apache-2.0 testrepository>=0.0.18 testscenarios>=0.4 testtools>=1.4.0 -tempest-lib>=0.8.0 +tempest-lib>=0.9.0 From 4bd50d5e4111fa82b9aa5a9781cdf20b84d990b1 Mon Sep 17 00:00:00 2001 From: Andrew Laski Date: Fri, 2 Oct 2015 14:23:04 -0400 Subject: [PATCH 0859/1705] Test that microversions are not skipped When raising API_MAX_VERSION to support a new Nova API microversion there should be support added to some method to account for the change. A test has been added to catch if support has not been added. In the event that no methods need to change there is an exclusion mechanism in the test so the version and a note can be added there. Change-Id: Iebc1be2557e1b6eec327b703568805578e564172 --- novaclient/tests/unit/v2/test_shell.py | 30 ++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index ed68c383e..49446a58b 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -27,6 +27,8 @@ import six from six.moves import builtins +import novaclient +from novaclient import api_versions import novaclient.client from novaclient import exceptions import novaclient.shell @@ -2502,6 +2504,34 @@ def test_list_server_group_with_all_projects(self): self.run_command('server-group-list --all-projects') self.assert_called('GET', '/os-server-groups?all_projects') + def test_versions(self): + exclusions = set([ + 1, # Same as version 2.0 + 3, # Not implemented when test added, should not apply to adds. + 5, # Not implemented when test added, should not apply to adds. + 6, # Not implemented when test added, should not apply to adds. + 7, # Not implemented when test added, should not apply to adds. + 8, # Not implemented when test added, should not apply to adds. + 9, # Not implemented when test added, should not apply to adds. + 10, # Not implemented when test added, should not apply to adds. + ]) + versions_supported = set(range(0, + novaclient.API_MAX_VERSION.ver_minor + 1)) + + versions_covered = set() + for key, values in api_versions._SUBSTITUTIONS.items(): + for value in values: + if value.start_version.ver_major == 2: + versions_covered.add(value.start_version.ver_minor) + + versions_not_covered = versions_supported - versions_covered + unaccounted_for = versions_not_covered - exclusions + + failure_msg = ('Minor versions %s have been skipped. Please do not ' + 'raise API_MAX_VERSION without adding support or ' + 'excluding them.' % sorted(unaccounted_for)) + self.assertEqual(set([]), unaccounted_for, failure_msg) + class ShellTestV11(ShellTest): FAKE_ENV = { From 469d06837cca11151ad0f25507875b5154d5826d Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Mon, 5 Oct 2015 18:12:23 +0000 Subject: [PATCH 0860/1705] Revert "Allow display project-id for server groups" This reverts commit 01c2e60eb3f3718884dd77cb05b4cde76052fb9d The server side change isn't in yet, there is a spec proposed: I167141676ef4f597a1c022c1fd5dc96fd55d02ad So this should have never landed in the client. And when it does it should be using microversions, like we have for the v2.4 support added in: 5fdb861ad4835513c0cb78251cce85ac73f51dd4 Change-Id: If3196093036c72f0f86cdca98a016b9f4fff0923 --- novaclient/tests/unit/v2/test_shell.py | 3 +-- novaclient/v2/shell.py | 8 +------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 828170bd3..8b148c04c 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2471,8 +2471,7 @@ def test_create_server_group(self): self.run_command('server-group-create wjsg affinity') self.assert_called('POST', '/os-server-groups', {'server_group': {'name': 'wjsg', - 'policies': ['affinity']}}, - pos=0) + 'policies': ['affinity']}}) def test_delete_multi_server_groups(self): self.run_command('server-group-delete 12345 56789') diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 5be248d55..9e840a70d 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -4422,13 +4422,7 @@ def do_availability_zone_list(cs, _args): def _print_server_group_details(server_group): - # This is for compatible with Nova v2 API, remove after v2 - # is dropped. - if hasattr(server_group, 'project_id'): - columns = ['Id', 'Name', 'Project_id', 'Policies', - 'Members', 'Metadata'] - else: - columns = ['Id', 'Name', 'Policies', 'Members', 'Metadata'] + columns = ['Id', 'Name', 'Policies', 'Members', 'Metadata'] utils.print_list(server_group, columns) From d10486f064630bb04d0e084df456514770e62160 Mon Sep 17 00:00:00 2001 From: Diana Clarke Date: Tue, 6 Oct 2015 22:32:33 -0400 Subject: [PATCH 0861/1705] Fix incorrect help for 'nova flavor-create' - like , the argument must also be unique - argument can be a string, integer, or UUID (not just an integer or UUID like it currently says) Change-Id: I8cf561bd361bc13caeb5cbceb662e1c51961f210 Closes-Bug: #1503503 --- novaclient/v2/shell.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 0563b752b..c8a0598ea 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -707,12 +707,12 @@ def do_flavor_show(cs, args): @cliutils.arg( 'name', metavar='', - help=_("Name of the new flavor.")) + help=_("Unique name of the new flavor.")) @cliutils.arg( 'id', metavar='', - help=_("Unique ID (integer or UUID) for the new flavor." - " If specifying 'auto', a UUID will be generated as ID.")) + help=_("Unique ID of the new flavor." + " Specifying 'auto' will generated a UUID for the ID.")) @cliutils.arg( 'ram', metavar='', From afaa8a2d1ea601ec4beae33dd012b03fa38f74f1 Mon Sep 17 00:00:00 2001 From: Thomas Bechtold Date: Tue, 13 Oct 2015 19:12:58 +0200 Subject: [PATCH 0862/1705] Increase timeout when testing admin timeout When running functional tests in a slow virtual environment, the given timeout of 10 seconds to list thr available instances is not enough. Increase the timeout to not run into a "timed out (HTTP 408)". Change-Id: Ie2345e6858bcc97095863d5d388bacf9c29b7682 --- novaclient/tests/functional/v2/legacy/test_readonly_nova.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/tests/functional/v2/legacy/test_readonly_nova.py b/novaclient/tests/functional/v2/legacy/test_readonly_nova.py index d7a183e56..7abbeee3f 100644 --- a/novaclient/tests/functional/v2/legacy/test_readonly_nova.py +++ b/novaclient/tests/functional/v2/legacy/test_readonly_nova.py @@ -179,7 +179,7 @@ def test_admin_debug_list(self): self.nova('list', flags='--debug') def test_admin_timeout(self): - self.nova('list', flags='--timeout %d' % 10) + self.nova('list', flags='--timeout %d' % 60) def test_admin_timing(self): self.nova('list', flags='--timing') From 49e76e670b61e537b693da0ecb961dc8e497b9db Mon Sep 17 00:00:00 2001 From: liyingjun Date: Wed, 14 Oct 2015 10:37:02 +0800 Subject: [PATCH 0863/1705] Add pagination params for flavor list Mission 'marker' and 'limit' params for `nova flavor-list` shell command. It would be nice to have this when there are many flavors. Change-Id: I4579b63bc6c711f1b5eaf07b54d393ebcbfd8277 Closes-bug: #1505874 --- novaclient/tests/unit/v2/test_shell.py | 4 ++++ novaclient/v2/shell.py | 19 ++++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index e74fbdcc9..c5de99a77 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -708,6 +708,10 @@ def test_flavor_list_with_all(self): self.run_command('flavor-list --all') self.assert_called('GET', '/flavors/detail?is_public=None') + def test_flavor_list_with_limit_and_marker(self): + self.run_command('flavor-list --marker 1 --limit 2') + self.assert_called('GET', '/flavors/detail?limit=2&marker=1') + def test_flavor_show(self): self.run_command('flavor-show 1') self.assert_called_anytime('GET', '/flavors/1') diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 0563b752b..41345d129 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -674,12 +674,29 @@ def _print_flavor_list(flavors, show_extra_specs=False): action='store_true', default=False, help=_('Display all flavors (Admin only).')) +@cliutils.arg( + '--marker', + dest='marker', + metavar='', + default=None, + help=('The last flavor ID of the previous page; displays list of flavors' + ' after "marker".')) +@cliutils.arg( + '--limit', + dest='limit', + metavar='', + type=int, + default=None, + help=("Maximum number of flavors to display. If limit == -1, all flavors " + "will be displayed. If limit is bigger than " + "'osapi_max_limit' option of Nova API, limit 'osapi_max_limit' will " + "be used instead.")) def do_flavor_list(cs, args): """Print a list of available 'flavors' (sizes of servers).""" if args.all: flavors = cs.flavors.list(is_public=None) else: - flavors = cs.flavors.list() + flavors = cs.flavors.list(marker=args.marker, limit=args.limit) _print_flavor_list(flavors, args.extra_specs) From 986ed2ae0bf4ef6d111f21c372944ba8e003a18c Mon Sep 17 00:00:00 2001 From: liyingjun Date: Wed, 14 Oct 2015 15:53:46 +0800 Subject: [PATCH 0864/1705] Add sort_dir/key to flavor list 'marker' and 'limit' are already added to flavor list, but 'sort_key' and 'sort_dir' which are useful for pagination are missing. Closes-bug: #1505939 Change-Id: Id78595f15fbb289133092a32238238e3c4a9d0a0 --- novaclient/tests/unit/v2/test_flavors.py | 5 +++++ novaclient/v2/flavors.py | 9 ++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/novaclient/tests/unit/v2/test_flavors.py b/novaclient/tests/unit/v2/test_flavors.py index b78d6cde9..ff564b265 100644 --- a/novaclient/tests/unit/v2/test_flavors.py +++ b/novaclient/tests/unit/v2/test_flavors.py @@ -49,6 +49,11 @@ def test_list_flavors_with_marker_limit(self): self.cs.flavors.list(marker=1234, limit=4) self.cs.assert_called('GET', '/flavors/detail?limit=4&marker=1234') + def test_list_flavors_with_sort_key_dir(self): + self.cs.flavors.list(sort_key='id', sort_dir='asc') + self.cs.assert_called('GET', + '/flavors/detail?sort_dir=asc&sort_key=id') + def test_list_flavors_is_public_none(self): fl = self.cs.flavors.list(is_public=None) self.cs.assert_called('GET', '/flavors/detail?is_public=None') diff --git a/novaclient/v2/flavors.py b/novaclient/v2/flavors.py index cf8591d74..86748f03f 100644 --- a/novaclient/v2/flavors.py +++ b/novaclient/v2/flavors.py @@ -93,7 +93,8 @@ class FlavorManager(base.ManagerWithFind): resource_class = Flavor is_alphanum_id_allowed = True - def list(self, detailed=True, is_public=True, marker=None, limit=None): + def list(self, detailed=True, is_public=True, marker=None, limit=None, + sort_key=None, sort_dir=None): """ Get a list of all flavors. @@ -101,6 +102,8 @@ def list(self, detailed=True, is_public=True, marker=None, limit=None): :param limit: maximum number of flavors to return (optional). :param marker: Begin returning flavors that appear later in the flavor list than that represented by this flavor id (optional). + :param sort_key: Flavors list sort key (optional). + :param sort_dir: Flavors list sort direction (optional). """ qparams = {} # is_public is ternary - None means give all flavors. @@ -110,6 +113,10 @@ def list(self, detailed=True, is_public=True, marker=None, limit=None): qparams['marker'] = str(marker) if limit: qparams['limit'] = int(limit) + if sort_key: + qparams['sort_key'] = str(sort_key) + if sort_dir: + qparams['sort_dir'] = str(sort_dir) if not is_public: qparams['is_public'] = is_public qparams = sorted(qparams.items(), key=lambda x: x[0]) From a820c45543dc843c1b75aa65903558a51b4ba037 Mon Sep 17 00:00:00 2001 From: melanie witt Date: Fri, 16 Oct 2015 22:41:33 +0000 Subject: [PATCH 0865/1705] Use v2.0 in clouds.yaml auth_url for functional test job Commit 050a0d5b304a013e23cd5909abf6e11b7dda5f18 changed the defaults in devstack to use the keystone v3 api. This broke the novaclient functional test job because novaclient doesn't yet support v3 and relies on having "v2.0" in the auth_url. This appends or replaces the version in the clouds.yaml auth_url to v2.0 to enable the functional test job to work until v3 support is added to novaclient. Closes-Bug: #1506103 Change-Id: I75c5efd039b71c8855acf21c5516ccbeeaaeadb3 --- novaclient/tests/functional/hooks/post_test_hook.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/novaclient/tests/functional/hooks/post_test_hook.sh b/novaclient/tests/functional/hooks/post_test_hook.sh index 1fa2618ba..a00b42aa3 100755 --- a/novaclient/tests/functional/hooks/post_test_hook.sh +++ b/novaclient/tests/functional/hooks/post_test_hook.sh @@ -33,6 +33,11 @@ sudo chown -R jenkins:stack $NOVACLIENT_DIR # ensure clouds.yaml exists mkdir -p ~/.config/openstack sudo cp -a ~stack/.config/openstack/clouds.yaml ~/.config/openstack +# FIXME(melwitt): Currently, novaclient requires version in the +# auth url in order to work and it doesn't support keystone v3. +# Use v2.0 in the auth url in clouds.yaml to enable the functional +# test job to work until novaclient is updated to support v3 +sudo sed -i -e 's/auth_url: \([^v ]\+\)\(\/v[0-9]\+\.\?[0-9]*\)\?$/auth_url: \1\/v2.0/g' ~/.config/openstack/clouds.yaml sudo chown -R jenkins:stack ~/.config/openstack # Go to the novaclient dir From 1c401eda3256d2e4e4ce98f6b6382947dc6df021 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Mon, 19 Oct 2015 12:23:05 +0000 Subject: [PATCH 0866/1705] Updated from global requirements Change-Id: I012f4a23a16cf153eff4a9ddf1fcb0fee955a402 --- requirements.txt | 6 +++--- test-requirements.txt | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index ba60838bb..6f725c3bf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,10 +6,10 @@ argparse iso8601>=0.1.9 oslo.i18n>=1.5.0 # Apache-2.0 oslo.serialization>=1.4.0 # Apache-2.0 -oslo.utils>=2.0.0 # Apache-2.0 +oslo.utils>=2.4.0 # Apache-2.0 PrettyTable<0.8,>=0.7 -requests>=2.5.2 +requests!=2.8.0,>=2.5.2 simplejson>=2.2.0 six>=1.9.0 Babel>=1.3 -python-keystoneclient>=1.6.0 +python-keystoneclient!=1.8.0,>=1.6.0 diff --git a/test-requirements.txt b/test-requirements.txt index 141d8d182..82a702895 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -15,4 +15,4 @@ oslosphinx>=2.5.0 # Apache-2.0 testrepository>=0.0.18 testscenarios>=0.4 testtools>=1.4.0 -tempest-lib>=0.9.0 +tempest-lib>=0.10.0 From 262719dfc3f1d036e540ba9939a0fbd9be99adc7 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Mon, 19 Oct 2015 23:32:55 +0000 Subject: [PATCH 0867/1705] Updated from global requirements Change-Id: Idf2e155ce650d64c0af1025c6e1961a8cdb9289b --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 6f725c3bf..1fdee4cdb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ argparse iso8601>=0.1.9 oslo.i18n>=1.5.0 # Apache-2.0 oslo.serialization>=1.4.0 # Apache-2.0 -oslo.utils>=2.4.0 # Apache-2.0 +oslo.utils!=2.6.0,>=2.4.0 # Apache-2.0 PrettyTable<0.8,>=0.7 requests!=2.8.0,>=2.5.2 simplejson>=2.2.0 From 09bb834b05106b36ff0ced47bfbd44ef915cbad6 Mon Sep 17 00:00:00 2001 From: Kevin_Zheng Date: Thu, 8 Oct 2015 09:38:34 +0800 Subject: [PATCH 0868/1705] Refactor parsing metadata to a common function Currently the metadata is parsed using dict(v.split('=', 1) for v in metadata) and was used in several places, it could be better if we refactor it to a common function. It will make it more readable and easier to use in the future. Change-Id: Ib0fcaacdbef65cdcb85ecc0c304f4933bef2627c Closes-Bug: #1503939 --- novaclient/tests/unit/v2/test_shell.py | 6 ++++++ novaclient/v2/shell.py | 10 +++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index e74fbdcc9..67759f70e 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -956,6 +956,12 @@ def test_list_with_limit(self): self.run_command('list --limit 3') self.assert_called('GET', '/servers/detail?limit=3') + def test_meta_parsing(self): + meta = ['key1=meta1', 'key2=meta2'] + ref = {'key1': 'meta1', 'key2': 'meta2'} + parsed_meta = novaclient.v2.shell._meta_parsing(meta) + self.assertEqual(ref, parsed_meta) + def test_reboot(self): self.run_command('reboot sample-server') self.assert_called('POST', '/servers/1234/action', diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index c8a0598ea..5474bd764 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -79,6 +79,10 @@ def _key_value_pairing(text): raise argparse.ArgumentTypeError(msg) +def _meta_parsing(metadata): + return dict(v.split('=', 1) for v in metadata) + + def _match_image(cs, wanted_properties): image_list = cs.images.list() images_matched = [] @@ -194,7 +198,7 @@ def _boot(cs, args): flavor = _find_flavor(cs, args.flavor) - meta = dict(v.split('=', 1) for v in args.meta) + meta = _meta_parsing(args.meta) files = {} for f in args.files: @@ -1547,7 +1551,7 @@ def do_rebuild(cs, args): kwargs = utils.get_resource_manager_extra_kwargs(do_rebuild, args) kwargs['preserve_ephemeral'] = args.preserve_ephemeral kwargs['name'] = args.name - meta = dict(v.split('=', 1) for v in args.meta) + meta = _meta_parsing(args.meta) kwargs['meta'] = meta files = {} @@ -1806,7 +1810,7 @@ def do_set_password(cs, args): def do_image_create(cs, args): """Create a new image by taking a snapshot of a running server.""" server = _find_server(cs, args.server) - meta = dict(v.split('=', 1) for v in args.metadata) or None + meta = _meta_parsing(args.metadata) or None image_uuid = cs.servers.create_image(server, args.name, meta) if args.poll: From 727f1044bfe237a692adc77550cb27d9d53629c7 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 18 Oct 2015 13:16:17 -0400 Subject: [PATCH 0869/1705] Rely on devstack for clouds.yaml Rather than copying it ourselves, just consume the one that devstack is putting there for us. Change-Id: Ifb9f8a55e0eb342db55a3518c375ea79679ff076 --- novaclient/tests/functional/hooks/post_test_hook.sh | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/novaclient/tests/functional/hooks/post_test_hook.sh b/novaclient/tests/functional/hooks/post_test_hook.sh index a00b42aa3..a07bd0c63 100755 --- a/novaclient/tests/functional/hooks/post_test_hook.sh +++ b/novaclient/tests/functional/hooks/post_test_hook.sh @@ -30,15 +30,11 @@ export NOVACLIENT_DIR="$BASE/new/python-novaclient" sudo chown -R jenkins:stack $NOVACLIENT_DIR -# ensure clouds.yaml exists -mkdir -p ~/.config/openstack -sudo cp -a ~stack/.config/openstack/clouds.yaml ~/.config/openstack # FIXME(melwitt): Currently, novaclient requires version in the # auth url in order to work and it doesn't support keystone v3. # Use v2.0 in the auth url in clouds.yaml to enable the functional # test job to work until novaclient is updated to support v3 -sudo sed -i -e 's/auth_url: \([^v ]\+\)\(\/v[0-9]\+\.\?[0-9]*\)\?$/auth_url: \1\/v2.0/g' ~/.config/openstack/clouds.yaml -sudo chown -R jenkins:stack ~/.config/openstack +sudo sed -i -e 's/auth_url: \([^v ]\+\)\(\/v[0-9]\+\.\?[0-9]*\)\?$/auth_url: \1\/v2.0/g' /etc/openstack/clouds.yaml # Go to the novaclient dir cd $NOVACLIENT_DIR From 28bf8ddb06a618bec280c9fbc21b4be6af2c1787 Mon Sep 17 00:00:00 2001 From: He Jie Xu Date: Wed, 21 Oct 2015 10:36:53 +0800 Subject: [PATCH 0870/1705] Correct usage of API_MAX_VERSION and DEFAULT_OS_COMPUTE_API_VERSION Commit d045019f0f2d28f26730b3617f7d7b993506b6e8 use the variables API_MAX_VERSION and DEFAULT_OS_COMPUTE_API_VERSION with wrong way. API_MAX_VERSION means the max version of client supported. This variable only be changed when client support the new version. And it's must bumped sequentially. DEFAULT_OS_COMPUTE_API_VERSION is default value of option '--os-compute-api-version', this value decided the default behaviour of nova client. The nova client CLI behaviour should be that client negotiate with server side to find out most recent version betweeyn client and server. So the value should be '2.latest'. And it won't be changed except we decided to change the default behaviour of nova client CLI. Change-Id: Iba9bfa136245bd2899c427ac0c231a30c00bd7f3 Closes-Bug: #1508244 --- novaclient/__init__.py | 11 +++++----- novaclient/shell.py | 15 +++++-------- novaclient/tests/unit/test_shell.py | 33 ++++++++++++++++++++++------- 3 files changed, 35 insertions(+), 24 deletions(-) diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 2e86783f9..6018232bc 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -21,9 +21,8 @@ API_MIN_VERSION = api_versions.APIVersion("2.1") # The max version should be the latest version that is supported in the client, -# not necessarily the latest that the server can provide. This is what a user -# can opt into with the --os-compute-api-version option on the CLI. Note that -# there may be gaps in supported microversions before this max version which is -# why novaclient.shell.DEFAULT_OS_COMPUTE_API_VERSION is not 2.latest or -# necessarily equal to API_MAX_VERSION. -API_MAX_VERSION = api_versions.APIVersion("2.11") +# not necessarily the latest that the server can provide. This is only bumped +# when client supported the max version, and bumped sequentially, otherwise +# the client may break due to server side new version may include some +# backward incompatible change. +API_MAX_VERSION = api_versions.APIVersion("2.5") diff --git a/novaclient/shell.py b/novaclient/shell.py index 05651734c..39bef130d 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -51,16 +51,11 @@ from novaclient import utils DEFAULT_MAJOR_OS_COMPUTE_API_VERSION = "2.0" -# We default to the highest *incremental* version that we know we can support. -# There should not be gaps in support for this version even if -# novaclient.API_MAX_VERSION is higher. The difference is API_MAX_VERSION -# caps what the user can request (they are opting into something), whereas -# DEFAULT_OS_COMPUTE_API_VERSION should be the highest version that the client -# actually supports without gaps in between. When a higher incremental version -# is implemented in the client, this version should be updated. Note that this -# value should never be 2.latest since what's latest changes depending on which -# cloud provider you're talking to. -DEFAULT_OS_COMPUTE_API_VERSION = '2.5' +# The default behaviour of nova client CLI is that CLI negotiates with server +# to find out the most recent version between client and server, and +# '2.latest' means to that. This value never be changed until we decided to +# change the default behaviour of nova client CLI. +DEFAULT_OS_COMPUTE_API_VERSION = '2.latest' DEFAULT_NOVA_ENDPOINT_TYPE = 'publicURL' DEFAULT_NOVA_SERVICE_TYPE = "compute" diff --git a/novaclient/tests/unit/test_shell.py b/novaclient/tests/unit/test_shell.py index cf76b92c1..a32c14257 100644 --- a/novaclient/tests/unit/test_shell.py +++ b/novaclient/tests/unit/test_shell.py @@ -65,6 +65,12 @@ 'OS_COMPUTE_API_VERSION': '2', 'OS_AUTH_SYSTEM': 'rackspace'} +FAKE_ENV6 = {'OS_USERNAME': 'username', + 'OS_PASSWORD': 'password', + 'OS_TENANT_NAME': 'tenant_name', + 'OS_AUTH_URL': 'http://no.where/v2.0', + 'OS_AUTH_SYSTEM': 'rackspace'} + def _create_ver_list(versions): return {'versions': {'values': versions}} @@ -424,6 +430,25 @@ def test_keyring_saver_helper(self, mock_client, keyring_saver = mock_client_instance.client.keyring_saver self.assertIsInstance(keyring_saver, novaclient.shell.SecretsHelper) + @mock.patch('novaclient.client.Client') + def test_microversion_with_default_behaviour(self, mock_client): + self.make_env(fake_env=FAKE_ENV6) + self.mock_server_version_range.return_value = ( + api_versions.APIVersion("2.1"), api_versions.APIVersion("2.3")) + self.shell('list') + client_args = mock_client.call_args_list[1][0] + self.assertEqual(api_versions.APIVersion("2.3"), client_args[0]) + + @mock.patch('novaclient.client.Client') + def test_microversion_with_default_behaviour_with_legacy_server( + self, mock_client): + self.make_env(fake_env=FAKE_ENV6) + self.mock_server_version_range.return_value = ( + api_versions.APIVersion(), api_versions.APIVersion()) + self.shell('list') + client_args = mock_client.call_args_list[1][0] + self.assertEqual(api_versions.APIVersion("2.0"), client_args[0]) + @mock.patch('novaclient.client.Client') def test_microversion_with_latest(self, mock_client): self.make_env() @@ -516,14 +541,6 @@ class MyException(Exception): err = sys.stderr.getvalue() self.assertEqual(err, 'ERROR (MyException): message\n') - def test_default_os_compute_api_version(self): - default_version = api_versions.APIVersion( - novaclient.shell.DEFAULT_OS_COMPUTE_API_VERSION) - # The default should never be the latest. - self.assertFalse(default_version.is_latest()) - # The default should be less than or equal to API_MAX_VERSION. - self.assertLessEqual(default_version, novaclient.API_MAX_VERSION) - class TestLoadVersionedActions(utils.TestCase): From cce7cd32775bc9d77a5ad57971be5d7c36e3aee5 Mon Sep 17 00:00:00 2001 From: Mark Doffman Date: Thu, 1 Oct 2015 14:46:13 -0500 Subject: [PATCH 0871/1705] Support the server side remote-console API changes In microversion 2.6 the remote-console API was changed. See http://docs.openstack.org/developer/nova/api_microversion_history.html for details of those changes. This adds support for the new remote-console API. Change-Id: I67098d6ce8c71f3ac0073628fd3b0aa23cd81918 Closes-Bug: #1500688 --- novaclient/__init__.py | 2 +- .../functional/v2/legacy/test_consoles.py | 67 +++++++++++++++++++ .../tests/functional/v2/test_consoles.py | 32 +++++++++ novaclient/tests/unit/fixture_data/servers.py | 18 +++++ novaclient/tests/unit/v2/test_servers.py | 39 +++++++++++ novaclient/v2/servers.py | 61 +++++++++++++++++ novaclient/v2/shell.py | 22 ++++-- 7 files changed, 236 insertions(+), 5 deletions(-) create mode 100644 novaclient/tests/functional/v2/legacy/test_consoles.py create mode 100644 novaclient/tests/functional/v2/test_consoles.py diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 6018232bc..004202371 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.5") +API_MAX_VERSION = api_versions.APIVersion("2.6") diff --git a/novaclient/tests/functional/v2/legacy/test_consoles.py b/novaclient/tests/functional/v2/legacy/test_consoles.py new file mode 100644 index 000000000..5ca022a72 --- /dev/null +++ b/novaclient/tests/functional/v2/legacy/test_consoles.py @@ -0,0 +1,67 @@ +# Copyright 2015 IBM Corp. +# 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 tempest_lib import exceptions + +from novaclient.tests.functional import base +from novaclient.v2 import shell + + +class TestConsolesNovaClient(base.ClientTestBase): + """Consoles functional tests.""" + + COMPUTE_API_VERSION = "2.1" + + def _create_server(self): + name = self.name_generate(prefix='server') + server = self.client.servers.create(name, self.image, self.flavor) + shell._poll_for_status( + self.client.servers.get, server.id, + 'building', ['active']) + self.addCleanup(server.delete) + return server + + def _test_console_get(self, command): + server = self._create_server() + completed_command = command % server.id + self.assertRaises(exceptions.CommandFailed, + self.nova, completed_command) + + try: + self.nova(completed_command) + except exceptions.CommandFailed as cf: + self.assertTrue('HTTP 400' in str(cf.stderr)) + + def _test_vnc_console_get(self): + self._test_console_get('get-vnc-console %s novnc') + + def _test_spice_console_get(self): + self._test_console_get('get-spice-console %s spice-html5') + + def _test_rdp_console_get(self): + self._test_console_get('get-rdp-console %s rdp-html5') + + def _test_serial_console_get(self): + self._test_console_get('get-serial-console %s') + + def test_vnc_console_get(self): + self._test_vnc_console_get() + + def test_spice_console_get(self): + self._test_spice_console_get() + + def test_rdp_console_get(self): + self._test_rdp_console_get() + + def test_serial_console_get(self): + self._test_serial_console_get() diff --git a/novaclient/tests/functional/v2/test_consoles.py b/novaclient/tests/functional/v2/test_consoles.py new file mode 100644 index 000000000..37615af7f --- /dev/null +++ b/novaclient/tests/functional/v2/test_consoles.py @@ -0,0 +1,32 @@ +# Copyright 2015 IBM Corp. +# 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 novaclient.tests.functional.v2.legacy import test_consoles + + +class TestConsolesNovaClientV26(test_consoles.TestConsolesNovaClient): + """Consoles functional tests for >=v2.6 api microversions.""" + + COMPUTE_API_VERSION = "2.6" + + def test_vnc_console_get(self): + self._test_vnc_console_get() + + def test_spice_console_get(self): + self._test_spice_console_get() + + def test_rdp_console_get(self): + self._test_rdp_console_get() + + def test_serial_console_get(self): + self._test_serial_console_get() diff --git a/novaclient/tests/unit/fixture_data/servers.py b/novaclient/tests/unit/fixture_data/servers.py index 5f45b528c..595dcc9ec 100644 --- a/novaclient/tests/unit/fixture_data/servers.py +++ b/novaclient/tests/unit/fixture_data/servers.py @@ -224,6 +224,10 @@ def setUp(self): json=self.post_servers, headers=self.json_headers) + self.requests.register_uri('POST', self.url('1234', 'remote-consoles'), + json=self.post_servers_1234_remote_consoles, + headers=self.json_headers) + self.requests.register_uri('POST', self.url('1234', 'action'), json=self.post_servers_1234_action, headers=self.json_headers) @@ -387,6 +391,20 @@ def post_servers(self, request, context): return {'server': body} + def post_servers_1234_remote_consoles(self, request, context): + _body = '' + body = jsonutils.loads(request.body) + context.status_code = 202 + assert len(body.keys()) == 1 + assert 'remote_console' in body.keys() + assert 'protocol' in body['remote_console'].keys() + protocol = body['remote_console']['protocol'] + + _body = {'protocol': protocol, 'type': 'novnc', + 'url': 'http://example.com:6080/vnc_auto.html?token=XYZ'} + + return {'remote_console': _body} + def post_servers_1234_action(self, request, context): _body = '' body = jsonutils.loads(request.body) diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index f311ab7d6..3b973538c 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -20,6 +20,7 @@ from oslo_serialization import jsonutils import six +from novaclient import api_versions from novaclient import exceptions from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit.fixture_data import floatingips @@ -794,3 +795,41 @@ def test_interface_detach(self): s = self.cs.servers.get(1234) s.interface_detach('port-id') self.assert_called('DELETE', '/servers/1234/os-interface/port-id') + + +class ServersV26Test(ServersTest): + def setUp(self): + super(ServersV26Test, self).setUp() + self.cs.api_version = api_versions.APIVersion("2.6") + + def test_get_vnc_console(self): + s = self.cs.servers.get(1234) + s.get_vnc_console('fake') + self.assert_called('POST', '/servers/1234/remote-consoles') + + self.cs.servers.get_vnc_console(s, 'fake') + self.assert_called('POST', '/servers/1234/remote-consoles') + + def test_get_spice_console(self): + s = self.cs.servers.get(1234) + s.get_spice_console('fake') + self.assert_called('POST', '/servers/1234/remote-consoles') + + self.cs.servers.get_spice_console(s, 'fake') + self.assert_called('POST', '/servers/1234/remote-consoles') + + def test_get_serial_console(self): + s = self.cs.servers.get(1234) + s.get_serial_console('fake') + self.assert_called('POST', '/servers/1234/remote-consoles') + + self.cs.servers.get_serial_console(s, 'fake') + self.assert_called('POST', '/servers/1234/remote-consoles') + + def test_get_rdp_console(self): + s = self.cs.servers.get(1234) + s.get_rdp_console('fake') + self.assert_called('POST', '/servers/1234/remote-consoles') + + self.cs.servers.get_rdp_console(s, 'fake') + self.assert_called('POST', '/servers/1234/remote-consoles') diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index d21bc5fbe..43e316cab 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -25,6 +25,7 @@ import six from six.moves.urllib import parse +from novaclient import api_versions from novaclient import base from novaclient import crypto from novaclient.i18n import _ @@ -661,6 +662,7 @@ def remove_floating_ip(self, server, address): address = address.ip if hasattr(address, 'ip') else address self._action('removeFloatingIp', server, {'address': address}) + @api_versions.wraps('2.0', '2.5') def get_vnc_console(self, server, console_type): """ Get a vnc console for an instance @@ -672,6 +674,7 @@ def get_vnc_console(self, server, console_type): return self._action('os-getVNCConsole', server, {'type': console_type})[1] + @api_versions.wraps('2.0', '2.5') def get_spice_console(self, server, console_type): """ Get a spice console for an instance @@ -683,6 +686,7 @@ def get_spice_console(self, server, console_type): return self._action('os-getSPICEConsole', server, {'type': console_type})[1] + @api_versions.wraps('2.0', '2.5') def get_rdp_console(self, server, console_type): """ Get a rdp console for an instance @@ -694,6 +698,7 @@ def get_rdp_console(self, server, console_type): return self._action('os-getRDPConsole', server, {'type': console_type})[1] + @api_versions.wraps('2.0', '2.5') def get_serial_console(self, server, console_type): """ Get a serial console for an instance @@ -705,6 +710,54 @@ def get_serial_console(self, server, console_type): return self._action('os-getSerialConsole', server, {'type': console_type})[1] + @api_versions.wraps('2.6') + def get_vnc_console(self, server, console_type): + """ + Get a vnc console for an instance + + :param server: The :class:`Server` (or its ID) to add an IP to. + :param console_type: Type of vnc console to get ('novnc' or 'xvpvnc') + """ + + return self._console(server, + {'protocol': 'vnc', 'type': console_type})[1] + + @api_versions.wraps('2.6') + def get_spice_console(self, server, console_type): + """ + Get a spice console for an instance + + :param server: The :class:`Server` (or its ID) to add an IP to. + :param console_type: Type of spice console to get ('spice-html5') + """ + + return self._console(server, + {'protocol': 'spice', 'type': console_type})[1] + + @api_versions.wraps('2.6') + def get_rdp_console(self, server, console_type): + """ + Get a rdp console for an instance + + :param server: The :class:`Server` (or its ID) to add an IP to. + :param console_type: Type of rdp console to get ('rdp-html5') + """ + + return self._console(server, + {'protocol': 'rdp', 'type': console_type})[1] + + @api_versions.wraps('2.6') + def get_serial_console(self, server, console_type): + """ + Get a serial console for an instance + + :param server: The :class:`Server` (or its ID) to add an IP to. + :param console_type: Type of serial console to get ('serial') + """ + + return self._console(server, + {'protocol': 'serial', 'type': console_type})[1] + def get_password(self, server, private_key=None): """ Get admin password of an instance @@ -1277,3 +1330,11 @@ def _action(self, action, server, info=None, **kwargs): self.run_hooks('modify_body_for_action', body, **kwargs) url = '/servers/%s/action' % base.getid(server) return self.api.client.post(url, body=body) + + def _console(self, server, info=None, **kwargs): + """ + Retrieve a console of a particular protocol -- vnc/spice/rdp/serial + """ + body = {'remote_console': info} + url = '/servers/%s/remote-consoles' % base.getid(server) + return self.api.client.post(url, body=body) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index c8a0598ea..bb5a3f857 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -2303,6 +2303,16 @@ def do_volume_type_delete(cs, args): cs.volume_types.delete(args.id) +@api_versions.wraps('2.0', '2.5') +def console_dict_accessor(cs, data): + return data['console'] + + +@api_versions.wraps('2.6') +def console_dict_accessor(cs, data): + return data['remote_console'] + + @cliutils.arg('server', metavar='', help=_('Name or ID of server.')) @cliutils.arg( 'console_type', @@ -2318,7 +2328,8 @@ def __init__(self, console_dict): self.type = console_dict['type'] self.url = console_dict['url'] - utils.print_list([VNCConsole(data['console'])], ['Type', 'Url']) + utils.print_list([VNCConsole(console_dict_accessor(cs, data))], + ['Type', 'Url']) @cliutils.arg('server', metavar='', help=_('Name or ID of server.')) @@ -2336,7 +2347,8 @@ def __init__(self, console_dict): self.type = console_dict['type'] self.url = console_dict['url'] - utils.print_list([SPICEConsole(data['console'])], ['Type', 'Url']) + utils.print_list([SPICEConsole(console_dict_accessor(cs, data))], + ['Type', 'Url']) @cliutils.arg('server', metavar='', help=_('Name or ID of server.')) @@ -2354,7 +2366,8 @@ def __init__(self, console_dict): self.type = console_dict['type'] self.url = console_dict['url'] - utils.print_list([RDPConsole(data['console'])], ['Type', 'Url']) + utils.print_list([RDPConsole(console_dict_accessor(cs, data))], + ['Type', 'Url']) @cliutils.arg('server', metavar='', help=_('Name or ID of server.')) @@ -2376,7 +2389,8 @@ def __init__(self, console_dict): self.type = console_dict['type'] self.url = console_dict['url'] - utils.print_list([SerialConsole(data['console'])], ['Type', 'Url']) + utils.print_list([SerialConsole(console_dict_accessor(cs, data))], + ['Type', 'Url']) @cliutils.arg('server', metavar='', help=_('Name or ID of server.')) From 4f16fe65c41ac1158f0493fcbd5c8109bd053872 Mon Sep 17 00:00:00 2001 From: Sean Dague Date: Wed, 14 Oct 2015 09:09:57 -0400 Subject: [PATCH 0872/1705] make project_id optional in urls for version discovery Currently there is a baked in assumption throughout the nova ecosystem that endpoint urls will end in {project_id}. This means that the version document for a specific major API version is "endpoint - last_chunk_of_url". This is not a discoverable URL, it's one that we heuristically generate. GET /v2.1/{project_id} is a 404. If we make Nova able to handle endpoint ids without {project_id} we run into a problem with novaclient, which is that end_point is now /v2.1. The heuristic gives us a version url of /. Which returns a very different doc than /v2.1. This causes all novaclient commands to explode. In order to support this transition we need to make both Nova and Novaclient handle endpoints with and without {project_id}. The proposal is thus to try the raw value of the endpoint first. If it works, great. If it fails with a 404, fall back to the heuristics. While this change and the nova change are technically independent, we should land this one as soon as possible, because clients before this change will not work with Nova deploys that get rid of project_id in their service catalog in the future. Part of bp:service-catalog-tng Co-Authored-By: Augustina Ragwitz Change-Id: Id4a2f73cbcfcf36aea750375fd92b1c2d3cd824e --- novaclient/tests/unit/v2/fakes.py | 29 +++++++++++++++++-- novaclient/tests/unit/v2/test_versions.py | 18 ++++++++++++ novaclient/v2/versions.py | 35 ++++++++++++++++++----- 3 files changed, 73 insertions(+), 9 deletions(-) diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index e50cddd0f..66ce6b207 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -18,6 +18,7 @@ import mock from oslo_utils import strutils +import re import six from six.moves.urllib import parse @@ -27,6 +28,12 @@ from novaclient.tests.unit import utils from novaclient.v2 import client +# regex to compare callback to result of get_endpoint() +ENDPOINT_RE = re.compile( + r"^(get_http:__nova_api:8774_)(v\d(_\d)?)_(\w{32})$") + +ENDPOINT_TYPE_RE = re.compile(r"^(v\d(\.\d)?)$") + class FakeClient(fakes.FakeClient, client.Client): @@ -49,7 +56,13 @@ def __init__(self, **kwargs): self.projectid = 'projectid' self.user = 'user' self.region_name = 'region_name' - self.endpoint_type = 'endpoint_type' + + # determines which endpoint to return in get_endpoint() + if 'endpoint_type' in kwargs: + self.endpoint_type = kwargs['endpoint_type'] + else: + self.endpoint_type = 'endpoint_type' + self.service_type = 'service_type' self.service_name = 'service_name' self.volume_service_name = 'volume_service_name' @@ -86,6 +99,12 @@ def _cs_request(self, url, method, **kwargs): callback = "get_versions" elif callback == "get_http:__nova_api:8774_v2_1": callback = "get_current_version" + elif ENDPOINT_RE.search(callback): + # compare callback to result of get_endpoint() + # NOTE(sdague): if we try to call a thing that doesn't + # exist, just return a 404. This allows the stack to act + # more like we'd expect when making REST calls. + raise exceptions.NotFound('404') if not hasattr(self, callback): raise AssertionError('Called unknown API method: %s %s, ' @@ -104,7 +123,13 @@ def _cs_request(self, url, method, **kwargs): return r, body def get_endpoint(self): - return "http://nova-api:8774/v2.1/190a755eef2e4aac9f06aa6be9786385" + # check if endpoint matches expected format (eg, v2.1) + if (hasattr(self, 'endpoint_type') + and ENDPOINT_TYPE_RE.search(self.endpoint_type)): + return "http://nova-api:8774/%s/" % self.endpoint_type + else: + return ( + "http://nova-api:8774/v2.1/190a755eef2e4aac9f06aa6be9786385") def get_versions(self): return (200, {}, { diff --git a/novaclient/tests/unit/v2/test_versions.py b/novaclient/tests/unit/v2/test_versions.py index aa8e84a6e..2504b104d 100644 --- a/novaclient/tests/unit/v2/test_versions.py +++ b/novaclient/tests/unit/v2/test_versions.py @@ -81,3 +81,21 @@ def test_get_current_with_rax_workaround(self, session, get): def test_get_current_with_rax_auth_plugin_workaround(self, session, _list): self.cs.callback = [] self.assertIsNone(self.cs.versions.get_current()) + + @mock.patch.object(versions.VersionManager, '_is_session_client', + return_value=True) + def test_get_endpoint_without_project_id(self, mock_is_session_client): + # create a fake client such that get_endpoint() + # doesn't return uuid in url + endpoint_type = 'v2.1' + expected_endpoint = 'http://nova-api:8774/v2.1/' + cs_2_1 = fakes.FakeClient(endpoint_type=endpoint_type) + + result = cs_2_1.versions.get_current() + self.assertEqual(result.manager.api.client.endpoint_type, + endpoint_type, "Check endpoint_type was set") + self.assertEqual(result.manager.api.client.management_url, + expected_endpoint, "Check endpoint without uuid") + + # check that the full request works as expected + cs_2_1.assert_called('GET', 'http://nova-api:8774/v2.1/') diff --git a/novaclient/v2/versions.py b/novaclient/v2/versions.py index 796ea7df5..ee33bd93a 100644 --- a/novaclient/v2/versions.py +++ b/novaclient/v2/versions.py @@ -39,14 +39,35 @@ def _is_session_client(self): def _get_current(self): """Returns info about current version.""" + # TODO(sdague): we've now got to make up to 3 HTTP requests to + # determine what version we are running, due to differences in + # deployments and versions. We really need to cache the + # results of this per endpoint and keep the results of it for + # some reasonable TTL (like 24 hours) to reduce our round trip + # traffic. if self._is_session_client(): - url = self.api.client.get_endpoint().rsplit("/", 1)[0] - # NOTE(sdague): many service providers don't really - # implement GET / in the expected way, if we do a GET /v2 - # that's actually a 300 redirect to /v2/... because of how - # paste works. So adding the end slash is really important. - url = "%s/" % url - return self._get(url, "version") + try: + # Assume that the value of get_endpoint() is something + # we can get the version of. This is a 404 for Nova < + # Mitaka if the service catalog contains project_id. + # + # TODO(sdague): add microversion for when this will + # change + url = "%s" % self.api.client.get_endpoint() + return self._get(url, "version") + except exc.NotFound: + # If that's a 404, we can instead try hacking together + # an endpoint root url by chopping off the last 2 /s. + # This is kind of gross, but we've had this baked in + # so long people got used to this hard coding. + # + # NOTE(sdague): many service providers don't really + # implement GET / in the expected way, if we do a GET + # /v2 that's actually a 300 redirect to + # /v2/... because of how paste works. So adding the + # end slash is really important. + url = "%s/" % url.rsplit("/", 1)[0] + return self._get(url, "version") else: # NOTE(andreykurilin): HTTPClient doesn't have ability to send get # request without token in the url, so `self._get` doesn't work. From 2961e82bfa6e375ac3c17933fac532ed316ac976 Mon Sep 17 00:00:00 2001 From: Jason Dunsmore Date: Thu, 15 Oct 2015 12:00:39 -0500 Subject: [PATCH 0873/1705] Do not expose exceptions from requests library Novaclient expects the requests library to always be able to get a response back and doesn't handle other cases where it might raise an exception even before the request is completed (e.g. ConnectionError). These exceptions should be captured and properly converted to some native novaclient exception. Change-Id: Iec7247b715c38c29910b30e76413cead4e951a37 Co-Authored-By: Kuo-tung Kao Closes-Bug: #1406586 --- novaclient/client.py | 22 +++++------ novaclient/exceptions.py | 23 +++++++++++ novaclient/tests/unit/test_client.py | 59 ++++++++++++++++++++++++++++ novaclient/utils.py | 24 +++++++++++ 4 files changed, 117 insertions(+), 11 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index 399d98e22..5787a2c22 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -84,13 +84,12 @@ def request(self, url, method, **kwargs): # NOTE(jamielennox): The standard call raises errors from # keystoneclient, where we need to raise the novaclient errors. raise_exc = kwargs.pop('raise_exc', True) - with utils.record_time(self.times, self.timings, method, url): - resp, body = super(SessionClient, self).request(url, - method, - raise_exc=False, - **kwargs) - if raise_exc and resp.status_code >= 400: - raise exceptions.from_response(resp, body, url, method) + with utils.converted_exceptions(): + with utils.record_time(self.times, self.timings, method, url): + resp, body = super(SessionClient, self).request( + url, method, raise_exc=False, **kwargs) + if raise_exc and resp.status_code >= 400: + raise exceptions.from_response(resp, body, url, method) return resp, body @@ -360,10 +359,11 @@ def request(self, url, method, **kwargs): if session: request_func = session.request - resp = request_func( - method, - url, - **kwargs) + with utils.converted_exceptions(): + resp = request_func( + method, + url, + **kwargs) self.http_log_resp(resp) diff --git a/novaclient/exceptions.py b/novaclient/exceptions.py index ede19bd8c..99bd5e7e8 100644 --- a/novaclient/exceptions.py +++ b/novaclient/exceptions.py @@ -283,3 +283,26 @@ def from_response(response, body, url, method=None): class ResourceNotFound(Exception): """Error in getting the resource.""" pass + + +class RequestTimeout(ClientException): + """ + HTTP 408 - Request Timeout + """ + http_status = 408 + message = "Request Timeout" + + +class TooManyRedirects(Exception): + """Too many redirects.""" + pass + + +class ConnectionError(Exception): + """A Connection error occurred.""" + pass + + +class RequestException(Exception): + """An ambiguous exception occurred.""" + pass diff --git a/novaclient/tests/unit/test_client.py b/novaclient/tests/unit/test_client.py index 5038e1837..9d39115ac 100644 --- a/novaclient/tests/unit/test_client.py +++ b/novaclient/tests/unit/test_client.py @@ -21,6 +21,7 @@ from keystoneclient import adapter import mock import requests +import six import novaclient.client import novaclient.extension @@ -438,6 +439,35 @@ def test_timings(self, m_request): self.assertEqual(1, len(client.times)) self.assertEqual('GET http://no.where', client.times[0][0]) + @mock.patch.object(requests, 'request') + def test_request_exception_conversions(self, m_request): + client = novaclient.client.HTTPClient(user='zqfan', password='') + + exc = requests.HTTPError() + exc.response = requests.Response() + exc.response.status_code = 12345 + m_request.side_effect = exc + exc = self.assertRaises(novaclient.exceptions.ClientException, + client.request, "http://www.openstack.org", + 'GET') + self.assertIn("HTTP 12345", six.text_type(exc)) + + m_request.side_effect = requests.ConnectionError() + self.assertRaises(novaclient.exceptions.ConnectionError, + client.request, "http://www.openstack.org", 'GET') + + m_request.side_effect = requests.Timeout() + self.assertRaises(novaclient.exceptions.RequestTimeout, + client.request, "http://www.openstack.org", 'GET') + + m_request.side_effect = requests.TooManyRedirects() + self.assertRaises(novaclient.exceptions.TooManyRedirects, + client.request, "http://www.openstack.org", 'GET') + + m_request.side_effect = requests.RequestException() + self.assertRaises(novaclient.exceptions.RequestException, + client.request, "http://www.openstack.org", 'GET') + class SessionClientTest(utils.TestCase): @@ -454,3 +484,32 @@ def test_timings(self, m_request): client.request("http://no.where", 'GET') self.assertEqual(1, len(client.times)) self.assertEqual('GET http://no.where', client.times[0][0]) + + @mock.patch.object(adapter.LegacyJsonAdapter, 'request') + def test_request_exception_conversions(self, m_request): + client = novaclient.client.SessionClient(session=mock.MagicMock()) + + exc = requests.HTTPError() + exc.response = requests.Response() + exc.response.status_code = 12345 + m_request.side_effect = exc + exc = self.assertRaises(novaclient.exceptions.ClientException, + client.request, "http://www.openstack.org", + 'GET') + self.assertIn("HTTP 12345", six.text_type(exc)) + + m_request.side_effect = requests.ConnectionError() + self.assertRaises(novaclient.exceptions.ConnectionError, + client.request, "http://www.openstack.org", 'GET') + + m_request.side_effect = requests.Timeout() + self.assertRaises(novaclient.exceptions.RequestTimeout, + client.request, "http://www.openstack.org", 'GET') + + m_request.side_effect = requests.TooManyRedirects() + self.assertRaises(novaclient.exceptions.TooManyRedirects, + client.request, "http://www.openstack.org", 'GET') + + m_request.side_effect = requests.RequestException() + self.assertRaises(novaclient.exceptions.RequestException, + client.request, "http://www.openstack.org", 'GET') diff --git a/novaclient/utils.py b/novaclient/utils.py index b420a29d9..c771ac409 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -22,6 +22,7 @@ from oslo_utils import encodeutils import pkg_resources import prettytable +import requests import six from novaclient import exceptions @@ -376,6 +377,29 @@ def record_time(times, enabled, *args): times.append((' '.join(args), start, end)) +@contextlib.contextmanager +def converted_exceptions(): + try: + yield + + except requests.HTTPError as e: + status_code = e.response.status_code + raise exceptions.ClientException(status_code, + encodeutils.exception_to_unicode(e)) + + except requests.ConnectionError as e: + raise exceptions.ConnectionError(encodeutils.exception_to_unicode(e)) + + except requests.Timeout as e: + raise exceptions.RequestTimeout(encodeutils.exception_to_unicode(e)) + + except requests.TooManyRedirects as e: + raise exceptions.TooManyRedirects(encodeutils.exception_to_unicode(e)) + + except requests.RequestException as e: + raise exceptions.RequestException(encodeutils.exception_to_unicode(e)) + + def get_function_name(func): if six.PY2: if hasattr(func, "im_class"): From 217e7c184907ab80b33fb7eb378de162f8976114 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 23 Oct 2015 17:58:43 +0000 Subject: [PATCH 0874/1705] Updated from global requirements Change-Id: I574ff7048614d5a267d4063771850e47872ea0fd --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1fdee4cdb..daef63704 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ pbr>=1.6 argparse iso8601>=0.1.9 oslo.i18n>=1.5.0 # Apache-2.0 -oslo.serialization>=1.4.0 # Apache-2.0 +oslo.serialization>=1.10.0 # Apache-2.0 oslo.utils!=2.6.0,>=2.4.0 # Apache-2.0 PrettyTable<0.8,>=0.7 requests!=2.8.0,>=2.5.2 From c6a3c766ee336a6ba873b041a5ec5711a20a911a Mon Sep 17 00:00:00 2001 From: Ravi Shekhar Jethani Date: Tue, 29 Sep 2015 23:58:09 -0700 Subject: [PATCH 0875/1705] Check flavor option before image checks Currently flavor option is checked after performing the image checks which results in unnecessary image checks even if flavor option is not passed at all. Moved flavor option check at the beginning of the method to eliminate the expensive operations performed as part of image check. Closes-Bug: #1502866 Change-Id: Ic68690b3d985d78ba0312bf5421989ccba7ceea8 --- novaclient/v2/shell.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index bb5a3f857..67f5dc93c 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -150,6 +150,9 @@ def _parse_block_device_mapping_v2(args, image): def _boot(cs, args): """Boot a new server.""" + if not args.flavor: + raise exceptions.CommandError(_("you need to specify a Flavor ID.")) + if args.image: image = _find_image(cs, args.image) else: @@ -162,9 +165,6 @@ def _boot(cs, args): # are selecting the first of many? image = images[0] - if not args.flavor: - raise exceptions.CommandError(_("you need to specify a Flavor ID ")) - min_count = 1 max_count = 1 # Don't let user mix num_instances and max_count/min_count. From 63c7a576e194f48f0edb5a0b2d9c4f1d37516e74 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Wed, 28 Oct 2015 07:07:28 -0700 Subject: [PATCH 0876/1705] Revert "Do not expose exceptions from requests library" This reverts commit 2961e82bfa6e375ac3c17933fac532ed316ac976 This broke cinder unit tests that have a timeout test for novaclient where they mock the requests library raising a Timeout exception. That test expects to get a requests Timeout passed through but with the change being reverted it was getting a RequestTimeout exception from novaclient, which breaks the test and fails the gate runs. We could change the cinder unit test to handle both the requests.Timeout and the new novaclient RequestTimeout, but that's just papering over the cinder failure, we wouldn't know what other clients are failing in the same way because they have code working around the requests exceptions getting passed through, so we should treat this like an API change. I'd be in favor of making the same change again iff the novaclient exceptions extended the requests exception types so it would be transparent to consumers, since novaclient.exceptions.RequestTimeout would be a requests.Timeout via inheritance. But given the gate breakage and it being summit week I'm inclined to revert first and think about a better long term fix later when people are back to discuss. Change-Id: I368728588e5997eef860a168539eb66c58f2e72a Closes-Bug: #1510790 --- novaclient/client.py | 22 +++++------ novaclient/exceptions.py | 23 ----------- novaclient/tests/unit/test_client.py | 59 ---------------------------- novaclient/utils.py | 24 ----------- 4 files changed, 11 insertions(+), 117 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index 5787a2c22..399d98e22 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -84,12 +84,13 @@ def request(self, url, method, **kwargs): # NOTE(jamielennox): The standard call raises errors from # keystoneclient, where we need to raise the novaclient errors. raise_exc = kwargs.pop('raise_exc', True) - with utils.converted_exceptions(): - with utils.record_time(self.times, self.timings, method, url): - resp, body = super(SessionClient, self).request( - url, method, raise_exc=False, **kwargs) - if raise_exc and resp.status_code >= 400: - raise exceptions.from_response(resp, body, url, method) + with utils.record_time(self.times, self.timings, method, url): + resp, body = super(SessionClient, self).request(url, + method, + raise_exc=False, + **kwargs) + if raise_exc and resp.status_code >= 400: + raise exceptions.from_response(resp, body, url, method) return resp, body @@ -359,11 +360,10 @@ def request(self, url, method, **kwargs): if session: request_func = session.request - with utils.converted_exceptions(): - resp = request_func( - method, - url, - **kwargs) + resp = request_func( + method, + url, + **kwargs) self.http_log_resp(resp) diff --git a/novaclient/exceptions.py b/novaclient/exceptions.py index 99bd5e7e8..ede19bd8c 100644 --- a/novaclient/exceptions.py +++ b/novaclient/exceptions.py @@ -283,26 +283,3 @@ def from_response(response, body, url, method=None): class ResourceNotFound(Exception): """Error in getting the resource.""" pass - - -class RequestTimeout(ClientException): - """ - HTTP 408 - Request Timeout - """ - http_status = 408 - message = "Request Timeout" - - -class TooManyRedirects(Exception): - """Too many redirects.""" - pass - - -class ConnectionError(Exception): - """A Connection error occurred.""" - pass - - -class RequestException(Exception): - """An ambiguous exception occurred.""" - pass diff --git a/novaclient/tests/unit/test_client.py b/novaclient/tests/unit/test_client.py index 9d39115ac..5038e1837 100644 --- a/novaclient/tests/unit/test_client.py +++ b/novaclient/tests/unit/test_client.py @@ -21,7 +21,6 @@ from keystoneclient import adapter import mock import requests -import six import novaclient.client import novaclient.extension @@ -439,35 +438,6 @@ def test_timings(self, m_request): self.assertEqual(1, len(client.times)) self.assertEqual('GET http://no.where', client.times[0][0]) - @mock.patch.object(requests, 'request') - def test_request_exception_conversions(self, m_request): - client = novaclient.client.HTTPClient(user='zqfan', password='') - - exc = requests.HTTPError() - exc.response = requests.Response() - exc.response.status_code = 12345 - m_request.side_effect = exc - exc = self.assertRaises(novaclient.exceptions.ClientException, - client.request, "http://www.openstack.org", - 'GET') - self.assertIn("HTTP 12345", six.text_type(exc)) - - m_request.side_effect = requests.ConnectionError() - self.assertRaises(novaclient.exceptions.ConnectionError, - client.request, "http://www.openstack.org", 'GET') - - m_request.side_effect = requests.Timeout() - self.assertRaises(novaclient.exceptions.RequestTimeout, - client.request, "http://www.openstack.org", 'GET') - - m_request.side_effect = requests.TooManyRedirects() - self.assertRaises(novaclient.exceptions.TooManyRedirects, - client.request, "http://www.openstack.org", 'GET') - - m_request.side_effect = requests.RequestException() - self.assertRaises(novaclient.exceptions.RequestException, - client.request, "http://www.openstack.org", 'GET') - class SessionClientTest(utils.TestCase): @@ -484,32 +454,3 @@ def test_timings(self, m_request): client.request("http://no.where", 'GET') self.assertEqual(1, len(client.times)) self.assertEqual('GET http://no.where', client.times[0][0]) - - @mock.patch.object(adapter.LegacyJsonAdapter, 'request') - def test_request_exception_conversions(self, m_request): - client = novaclient.client.SessionClient(session=mock.MagicMock()) - - exc = requests.HTTPError() - exc.response = requests.Response() - exc.response.status_code = 12345 - m_request.side_effect = exc - exc = self.assertRaises(novaclient.exceptions.ClientException, - client.request, "http://www.openstack.org", - 'GET') - self.assertIn("HTTP 12345", six.text_type(exc)) - - m_request.side_effect = requests.ConnectionError() - self.assertRaises(novaclient.exceptions.ConnectionError, - client.request, "http://www.openstack.org", 'GET') - - m_request.side_effect = requests.Timeout() - self.assertRaises(novaclient.exceptions.RequestTimeout, - client.request, "http://www.openstack.org", 'GET') - - m_request.side_effect = requests.TooManyRedirects() - self.assertRaises(novaclient.exceptions.TooManyRedirects, - client.request, "http://www.openstack.org", 'GET') - - m_request.side_effect = requests.RequestException() - self.assertRaises(novaclient.exceptions.RequestException, - client.request, "http://www.openstack.org", 'GET') diff --git a/novaclient/utils.py b/novaclient/utils.py index c771ac409..b420a29d9 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -22,7 +22,6 @@ from oslo_utils import encodeutils import pkg_resources import prettytable -import requests import six from novaclient import exceptions @@ -377,29 +376,6 @@ def record_time(times, enabled, *args): times.append((' '.join(args), start, end)) -@contextlib.contextmanager -def converted_exceptions(): - try: - yield - - except requests.HTTPError as e: - status_code = e.response.status_code - raise exceptions.ClientException(status_code, - encodeutils.exception_to_unicode(e)) - - except requests.ConnectionError as e: - raise exceptions.ConnectionError(encodeutils.exception_to_unicode(e)) - - except requests.Timeout as e: - raise exceptions.RequestTimeout(encodeutils.exception_to_unicode(e)) - - except requests.TooManyRedirects as e: - raise exceptions.TooManyRedirects(encodeutils.exception_to_unicode(e)) - - except requests.RequestException as e: - raise exceptions.RequestException(encodeutils.exception_to_unicode(e)) - - def get_function_name(func): if six.PY2: if hasattr(func, "im_class"): From 036d42d7087a3c83a5feec0ad0a4c6af35df72c9 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Wed, 28 Oct 2015 21:25:02 +0000 Subject: [PATCH 0877/1705] Revert "Remove novaclient.v1_1 module" This reverts commit 0cd58123be5e2c2bc2f1233a3e15fb35b2ead1a0 This is a backward incompatible change that is blocking our ability to release 2.34 for 63c7a57, which was a revert for another backward incompatible change (2961e82) which slipped into 2.33. Once we release 2.34 with the bug fixes, we'll revert this revert and release that as novaclient 3.0. Change-Id: If79d37df3e99004897f1bb2d8564d2f1d8aa0978 --- novaclient/tests/unit/v2/test_shell.py | 11 +++++++ novaclient/v1_1/__init__.py | 43 ++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 novaclient/v1_1/__init__.py diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index eae4528de..ed68c383e 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2503,6 +2503,17 @@ def test_list_server_group_with_all_projects(self): self.assert_called('GET', '/os-server-groups?all_projects') +class ShellTestV11(ShellTest): + FAKE_ENV = { + 'NOVA_USERNAME': 'username', + 'NOVA_PASSWORD': 'password', + 'NOVA_PROJECT_ID': 'project_id', + 'OS_COMPUTE_API_VERSION': '1.1', + 'NOVA_URL': 'http://no.where', + 'OS_AUTH_URL': 'http://no.where/v2.0', + } + + class ShellWithSessionClientTest(ShellTest): def setUp(self): diff --git a/novaclient/v1_1/__init__.py b/novaclient/v1_1/__init__.py new file mode 100644 index 000000000..9da768172 --- /dev/null +++ b/novaclient/v1_1/__init__.py @@ -0,0 +1,43 @@ +# +# All Rights Reserved. +# +# 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. + +# NOTE(akurilin): This module is left for backward compatibility. Feel free to +# remove it, when openstack project will use correct way to +# obtain novaclient object. +# Known problems: +# * python-openstackclient - +# https://bugs.launchpad.net/python-openstackclient/+bug/1418024 +# * neutron - https://bugs.launchpad.net/neutron/+bug/1418017 + + +import sys +import warnings + +from novaclient import v2 + +warnings.warn("Module novaclient.v1_1 is deprecated (taken as a basis for " + "novaclient.v2). " + "The preferable way to get client class or object you can find " + "in novaclient.client module.") + + +class MovedModule(object): + def __init__(self, new_module): + self.new_module = new_module + + def __getattr__(self, attr): + return getattr(self.new_module, attr) + +sys.modules["novaclient.v1_1"] = MovedModule(v2) From f7e2129f887fe9925240ac8f63befdc7e3f01aa1 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Thu, 29 Oct 2015 14:04:45 +0000 Subject: [PATCH 0878/1705] Remove novaclient.v1_1 module This reverts commit 036d42d7087a3c83a5feec0ad0a4c6af35df72c9 Once novaclient 2.34 is released we can merge this and release as 3.0.0. The dependency is on the 2.34 release request. Change-Id: I1e3a8a8079b954fe60371c2d38ec23a10727c408 Depends-On: I4fc044e518a5cc5ea7b309a2824984a7035945bd --- novaclient/tests/unit/v2/test_shell.py | 11 ------- novaclient/v1_1/__init__.py | 43 -------------------------- 2 files changed, 54 deletions(-) delete mode 100644 novaclient/v1_1/__init__.py diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index ed68c383e..eae4528de 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2503,17 +2503,6 @@ def test_list_server_group_with_all_projects(self): self.assert_called('GET', '/os-server-groups?all_projects') -class ShellTestV11(ShellTest): - FAKE_ENV = { - 'NOVA_USERNAME': 'username', - 'NOVA_PASSWORD': 'password', - 'NOVA_PROJECT_ID': 'project_id', - 'OS_COMPUTE_API_VERSION': '1.1', - 'NOVA_URL': 'http://no.where', - 'OS_AUTH_URL': 'http://no.where/v2.0', - } - - class ShellWithSessionClientTest(ShellTest): def setUp(self): diff --git a/novaclient/v1_1/__init__.py b/novaclient/v1_1/__init__.py deleted file mode 100644 index 9da768172..000000000 --- a/novaclient/v1_1/__init__.py +++ /dev/null @@ -1,43 +0,0 @@ -# -# All Rights Reserved. -# -# 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. - -# NOTE(akurilin): This module is left for backward compatibility. Feel free to -# remove it, when openstack project will use correct way to -# obtain novaclient object. -# Known problems: -# * python-openstackclient - -# https://bugs.launchpad.net/python-openstackclient/+bug/1418024 -# * neutron - https://bugs.launchpad.net/neutron/+bug/1418017 - - -import sys -import warnings - -from novaclient import v2 - -warnings.warn("Module novaclient.v1_1 is deprecated (taken as a basis for " - "novaclient.v2). " - "The preferable way to get client class or object you can find " - "in novaclient.client module.") - - -class MovedModule(object): - def __init__(self, new_module): - self.new_module = new_module - - def __getattr__(self, attr): - return getattr(self.new_module, attr) - -sys.modules["novaclient.v1_1"] = MovedModule(v2) From 5f5ec354be6f5f5c3b1e4767d5b5593fdeba7fcc Mon Sep 17 00:00:00 2001 From: Augustina Ragwitz Date: Wed, 21 Oct 2015 22:16:10 -0700 Subject: [PATCH 0879/1705] Add v2 support for optional project_id in version discovery urls This is part of the work to allow Nova and Novaclient to handle endpoints both with and without project id's. v2 version information returns a different data structure than later versions. This patch updates the mock tests to return a different data structure corresponding to v2 or v2.x depending on the endpoint_type. A caller using the mock test class can set the attribute endpoint_type to a version number, and the method get_endpoint, will return the appropriate data structure. This is more of a workaround given the current state of mocking Nova api calls in our unit tests. Part of bp:service-catalog-tng Change-Id: I61984ee592f7a5d7f1a91c9e2ff3038c0245f797 --- novaclient/tests/unit/v2/fakes.py | 63 +++++++++++++++++++---- novaclient/tests/unit/v2/test_versions.py | 18 +++++++ 2 files changed, 70 insertions(+), 11 deletions(-) diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 66ce6b207..c4ad64a0d 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -29,10 +29,16 @@ from novaclient.v2 import client # regex to compare callback to result of get_endpoint() +# checks version number (vX or vX.X where X is a number) +# and also checks if the id is on the end ENDPOINT_RE = re.compile( - r"^(get_http:__nova_api:8774_)(v\d(_\d)?)_(\w{32})$") + r"^get_http:__nova_api:8774_v\d(_\d)?_\w{32}$") -ENDPOINT_TYPE_RE = re.compile(r"^(v\d(\.\d)?)$") +# accepts formats like v2 or v2.1 +ENDPOINT_TYPE_RE = re.compile(r"^v\d(\.\d)?$") + +# accepts formats like v2 or v2_1 +CALLBACK_RE = re.compile(r"^get_http:__nova_api:8774_v\d(_\d)?$") class FakeClient(fakes.FakeClient, client.Client): @@ -58,6 +64,8 @@ def __init__(self, **kwargs): self.region_name = 'region_name' # determines which endpoint to return in get_endpoint() + # NOTE(augustina): this is a hacky workaround, ultimately + # we need to fix our whole mocking architecture (fixtures?) if 'endpoint_type' in kwargs: self.endpoint_type = kwargs['endpoint_type'] else: @@ -97,7 +105,7 @@ def _cs_request(self, url, method, **kwargs): # To get API version information, it is necessary to GET # a nova endpoint directly without "v2/". callback = "get_versions" - elif callback == "get_http:__nova_api:8774_v2_1": + elif CALLBACK_RE.search(callback): callback = "get_current_version" elif ENDPOINT_RE.search(callback): # compare callback to result of get_endpoint() @@ -149,14 +157,47 @@ def get_versions(self): ]}) def get_current_version(self): - return (200, {}, { - "version": {"status": "CURRENT", - "updated": "2013-07-23T11:33:21Z", - "links": [{"href": "http://nova-api:8774/v2.1/", - "rel": "self"}], - "min_version": "2.1", - "version": "2.3", - "id": "v2.1"}}) + versions = { + # v2 doesn't contain version or min_version fields + "v2": { + "version": { + "status": "SUPPORTED", + "updated": "2011-01-21T11:33:21Z", + "links": [{ + "href": "http://nova-api:8774/v2/", + "rel": "self" + }], + "id": "v2.0" + } + }, + "v2.1": { + "version": { + "status": "CURRENT", + "updated": "2013-07-23T11:33:21Z", + "links": [{ + "href": "http://nova-api:8774/v2.1/", + "rel": "self" + }], + "min_version": "2.1", + "version": "2.3", + "id": "v2.1" + } + } + } + + # if an endpoint_type that matches a version wasn't initialized, + # default to v2.1 + endpoint = 'v2.1' + + if hasattr(self, 'endpoint_type'): + if ENDPOINT_TYPE_RE.search(self.endpoint_type): + if self.endpoint_type in versions: + endpoint = self.endpoint_type + else: + raise AssertionError( + "Unknown endpoint_type:%s" % self.endpoint_type) + + return (200, {}, versions[endpoint]) # # agents diff --git a/novaclient/tests/unit/v2/test_versions.py b/novaclient/tests/unit/v2/test_versions.py index 2504b104d..3feab8be5 100644 --- a/novaclient/tests/unit/v2/test_versions.py +++ b/novaclient/tests/unit/v2/test_versions.py @@ -99,3 +99,21 @@ def test_get_endpoint_without_project_id(self, mock_is_session_client): # check that the full request works as expected cs_2_1.assert_called('GET', 'http://nova-api:8774/v2.1/') + + @mock.patch.object(versions.VersionManager, '_is_session_client', + return_value=True) + def test_v2_get_endpoint_without_project_id(self, mock_is_session_client): + # create a fake client such that get_endpoint() + # doesn't return uuid in url + endpoint_type = 'v2' + expected_endpoint = 'http://nova-api:8774/v2/' + cs_2 = fakes.FakeClient(endpoint_type=endpoint_type) + + result = cs_2.versions.get_current() + self.assertEqual(result.manager.api.client.endpoint_type, + endpoint_type, "Check v2 endpoint_type was set") + self.assertEqual(result.manager.api.client.management_url, + expected_endpoint, "Check v2 endpoint without uuid") + + # check that the full request works as expected + cs_2.assert_called('GET', 'http://nova-api:8774/v2/') From 2b214493115c673e30a55085f8dfd4d386ae9f20 Mon Sep 17 00:00:00 2001 From: jichenjc Date: Sat, 7 Nov 2015 00:58:47 +0800 Subject: [PATCH 0880/1705] Remove --tenant suggestion for flavor-access-list when help flavor-access-list, nova client tells user to try --tenant while try with it , nova client tells user it's not implemented. We should avoid this kind of suggestion. Change-Id: Ia35f9d35d6c4855122385de2a5cc013fcba1f126 Closes-Bug: 1515978 --- novaclient/v2/shell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index d5361ddc2..f41ec9441 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -807,7 +807,7 @@ def do_flavor_access_list(cs, args): kwargs = {'tenant': args.tenant} else: raise exceptions.CommandError(_("Unable to get all access lists. " - "Specify --flavor or --tenant")) + "Specify --flavor")) try: access_list = cs.flavor_access.list(**kwargs) From e9268e9864f76f00f16f476a4f9a9e0a246091e7 Mon Sep 17 00:00:00 2001 From: Anna Babich Date: Thu, 8 Oct 2015 18:22:50 +0300 Subject: [PATCH 0881/1705] Functional tests for os-extended-server-attributes The added tests check basic set of extended server attributes and additional attributes exposed by microversion 2.3 To avoid leak resources, cleanup part has been refactored Change-Id: I0c8f096134838b90e17720f2a28e649998a02e3b --- .../v2/legacy/test_extended_attributes.py | 59 +++++++++++++++++++ .../functional/v2/test_extended_attributes.py | 44 ++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 novaclient/tests/functional/v2/legacy/test_extended_attributes.py create mode 100644 novaclient/tests/functional/v2/test_extended_attributes.py diff --git a/novaclient/tests/functional/v2/legacy/test_extended_attributes.py b/novaclient/tests/functional/v2/legacy/test_extended_attributes.py new file mode 100644 index 000000000..55da98826 --- /dev/null +++ b/novaclient/tests/functional/v2/legacy/test_extended_attributes.py @@ -0,0 +1,59 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import json + +from novaclient.tests.functional import base +from novaclient.v2 import shell + + +class TestExtAttrNovaClient(base.ClientTestBase): + """Functional tests for os-extended-server-attributes""" + + COMPUTE_API_VERSION = "2.1" + + def _create_server_and_attach_volume(self): + name = self.name_generate(prefix='server') + server = self.client.servers.create(name, self.image, self.flavor) + self.addCleanup(server.delete) + shell._poll_for_status( + self.client.servers.get, server.id, + 'building', ['active']) + volume = self.client.volumes.create(1) + self.addCleanup(self.nova, 'volume-delete', params=volume.id) + self.wait_for_volume_status(volume, 'available') + self.nova('volume-attach', params="%s %s" % (name, volume.id)) + self.addCleanup(self._release_volume, server, volume) + self.wait_for_volume_status(volume, 'in-use') + return server, volume + + def _release_volume(self, server, volume): + self.nova('volume-detach', params="%s %s" % (server.id, volume.id)) + self.wait_for_volume_status(volume, 'available') + + def test_extended_server_attributes(self): + server, volume = self._create_server_and_attach_volume() + table = self.nova('show %s' % server.id) + # Check that attributes listed below exist in 'nova show' table and + # they are exactly Property attributes (not an instance's name, e.g.) + # The _get_value_from_the_table() will raise an exception + # if attr is not a key (first column) of the table dict + for attr in ['OS-EXT-SRV-ATTR:host', + 'OS-EXT-SRV-ATTR:hypervisor_hostname', + 'OS-EXT-SRV-ATTR:instance_name']: + self._get_value_from_the_table(table, attr) + # Check that attribute given below also exists in 'nova show' table + # as a key (first column) of table dict + volume_attr = self._get_value_from_the_table( + table, 'os-extended-volumes:volumes_attached') + # Check that 'id' exists as a key of volume_attr dict + self.assertIn('id', json.loads(volume_attr)[0]) diff --git a/novaclient/tests/functional/v2/test_extended_attributes.py b/novaclient/tests/functional/v2/test_extended_attributes.py new file mode 100644 index 000000000..6fddcc957 --- /dev/null +++ b/novaclient/tests/functional/v2/test_extended_attributes.py @@ -0,0 +1,44 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import json + +from novaclient.tests.functional.v2.legacy import test_extended_attributes + + +class TestExtAttrNovaClientV23(test_extended_attributes.TestExtAttrNovaClient): + """Functional tests for os-extended-server-attributes, microversion 2.3""" + + COMPUTE_API_VERSION = "2.3" + + def test_extended_server_attributes(self): + server, volume = self._create_server_and_attach_volume() + table = self.nova('show %s' % server.id) + # Check that attributes listed below exist in 'nova show' table and + # they are exactly Property attributes (not an instance's name, e.g.) + # The _get_value_from_the_table() will raise an exception + # if attr is not a key of the table dict (first column) + for attr in ['OS-EXT-SRV-ATTR:reservation_id', + 'OS-EXT-SRV-ATTR:launch_index', + 'OS-EXT-SRV-ATTR:ramdisk_id', + 'OS-EXT-SRV-ATTR:kernel_id', + 'OS-EXT-SRV-ATTR:hostname', + 'OS-EXT-SRV-ATTR:root_device_name', + 'OS-EXT-SRV-ATTR:user_data']: + self._get_value_from_the_table(table, attr) + # Check that attribute given below also exists in 'nova show' table + # as a key (first column) of table dict + volume_attr = self._get_value_from_the_table( + table, 'os-extended-volumes:volumes_attached') + # Check that 'delete_on_termination' exists as a key + # of volume_attr dict + self.assertIn('delete_on_termination', json.loads(volume_attr)[0]) From 6a18757509b8ff46eac2e1206ce430aa98bfec6b Mon Sep 17 00:00:00 2001 From: Sujitha Date: Tue, 10 Nov 2015 22:41:54 +0000 Subject: [PATCH 0882/1705] Added command for device to cinder volume mapping Added new sub command volume-attachments which displays the list of volumes attached to a server. Change-Id: I1f56a6fa18c35f3df2bdd11b9cf83bd5c5d9e182 Closes-Bug: #1116593 --- novaclient/tests/unit/v2/test_shell.py | 4 ++++ novaclient/v2/shell.py | 17 +++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index e74fbdcc9..dea585d02 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2266,6 +2266,10 @@ def test_volume_show(self): pos=-1 ) + def test_volume_attachments(self): + self.run_command('volume-attachments 1234') + self.assert_called('GET', '/servers/1234/os-volume_attachments') + def test_volume_create(self): _, err = self.run_command('volume-create 2 --display-name Work') self.assertIn('Command volume-create is deprecated', err) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index bb5a3f857..3a01c67a8 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -2032,6 +2032,12 @@ def _translate_availability_zone_keys(collection): [('zoneName', 'name'), ('zoneState', 'status')]) +def _translate_volume_attachments_keys(collection): + _translate_keys(collection, + [('serverId', 'server_id'), + ('volumeId', 'volume_id')]) + + @cliutils.arg( '--all-tenants', dest='all_tenants', @@ -2204,6 +2210,17 @@ def do_volume_detach(cs, args): args.attachment_id) +@cliutils.arg( + 'server', + metavar='', + help=_('Name or ID of server.')) +def do_volume_attachments(cs, args): + """List all the volumes attached to a server""" + volumes = cs.volumes.get_server_volumes(_find_server(cs, args.server).id) + _translate_volume_attachments_keys(volumes) + utils.print_list(volumes, ['ID', 'DEVICE', 'SERVER ID', 'VOLUME ID']) + + def do_volume_snapshot_list(cs, _args): """DEPRECATED: List all the snapshots.""" emit_volume_deprecation_warning('volume-snapshot-list') From 7d800a9e657cb9cef6c68662a786df1e667b839e Mon Sep 17 00:00:00 2001 From: Vincent Untz Date: Tue, 17 Nov 2015 07:19:25 +0100 Subject: [PATCH 0883/1705] Fix typo in error message Change-Id: I9ff3babd0460c41e05f18b34c3b46032edbf1459 --- novaclient/tests/functional/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/tests/functional/base.py b/novaclient/tests/functional/base.py index b4aca058d..86d652322 100644 --- a/novaclient/tests/functional/base.py +++ b/novaclient/tests/functional/base.py @@ -135,7 +135,7 @@ def setUp(self): if cloud_config is None: raise NoCloudConfigException( - "Cloud not find a cloud named functional_admin or a cloud" + "Could not find a cloud named functional_admin or a cloud" " named devstack. Please check your clouds.yaml file and" " try again.") auth_info = cloud_config.config['auth'] From ee714bfc503c3ef43e54b5197bc638c733443176 Mon Sep 17 00:00:00 2001 From: Davanum Srinivas Date: Thu, 12 Nov 2015 14:08:50 -0500 Subject: [PATCH 0884/1705] Last sync from oslo-incubator oslo-incubator will cease to host common code soon. This is hopefully the very last sync from oslo-incubator. Changes: 7f37bf8 Correct a typo f4b09ce Sort keys in table output in print_dict 0753799 Allow specifying a table header for the value column in... Change-Id: I7eb71518e3c9c0c51b08188fa83298dd9eac93dc --- novaclient/openstack/common/cliutils.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/novaclient/openstack/common/cliutils.py b/novaclient/openstack/common/cliutils.py index 080dd2d44..fd1744e09 100644 --- a/novaclient/openstack/common/cliutils.py +++ b/novaclient/openstack/common/cliutils.py @@ -140,7 +140,7 @@ def isunauthenticated(func): def print_list(objs, fields, formatters=None, sortby_index=0, mixed_case_fields=None, field_labels=None): - """Print a list or objects as a table, one row per object. + """Print a list of objects as a table, one row per object. :param objs: iterable of :class:`Resource` :param fields: attributes that correspond to columns, in order @@ -186,16 +186,17 @@ def print_list(objs, fields, formatters=None, sortby_index=0, print(encodeutils.safe_encode(pt.get_string(**kwargs))) -def print_dict(dct, dict_property="Property", wrap=0): +def print_dict(dct, dict_property="Property", wrap=0, dict_value='Value'): """Print a `dict` as a table of two columns. :param dct: `dict` to print :param dict_property: name of the first column :param wrap: wrapping for the second column + :param dict_value: header label for the value (second) column """ - pt = prettytable.PrettyTable([dict_property, 'Value']) + pt = prettytable.PrettyTable([dict_property, dict_value]) pt.align = 'l' - for k, v in six.iteritems(dct): + for k, v in sorted(dct.items()): # convert dict to str to check length if isinstance(v, dict): v = six.text_type(v) From ad54d38cb9efa4f355812601fcab8d2a4a3a96bc Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Wed, 18 Nov 2015 20:51:47 +0000 Subject: [PATCH 0885/1705] Updated from global requirements Change-Id: I6258b3a4aa6b395f6756b94a71d323447a84e9c0 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index daef63704..4f3bdfabc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ argparse iso8601>=0.1.9 oslo.i18n>=1.5.0 # Apache-2.0 oslo.serialization>=1.10.0 # Apache-2.0 -oslo.utils!=2.6.0,>=2.4.0 # Apache-2.0 +oslo.utils>=2.8.0 # Apache-2.0 PrettyTable<0.8,>=0.7 requests!=2.8.0,>=2.5.2 simplejson>=2.2.0 From 18ea9dca18c8399d996e1e076121413708730404 Mon Sep 17 00:00:00 2001 From: ricolin Date: Thu, 15 Oct 2015 17:27:15 +0800 Subject: [PATCH 0886/1705] improve readme contents Add more information in README.rst Change-Id: I290c0871e3ba665f4c066a3d34cc2214455d68fa --- README.rst | 38 +++++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/README.rst b/README.rst index 9725cd521..f93a34c6f 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,14 @@ Python bindings to the OpenStack Nova API ========================================= +.. image:: https://img.shields.io/pypi/v/python-novaclient.svg + :target: https://pypi.python.org/pypi/python-novaclient/ + :alt: Latest Version + +.. image:: https://img.shields.io/pypi/dm/python-novaclient.svg + :target: https://pypi.python.org/pypi/python-novaclient/ + :alt: Downloads + This is a client for the OpenStack Nova API. There's a Python API (the ``novaclient`` module), and a command-line script (``nova``). Each implements 100% of the OpenStack Nova API. @@ -12,17 +20,26 @@ command-line tool. You may also want to look at the .. _OpenStack CLI Guide: http://docs.openstack.org/cli-reference/content/novaclient_commands.html .. _OpenStack API documentation: http://docs.openstack.org/api/quick-start/content/ -The project is hosted on `Launchpad`_, where bugs can be filed. The code is -hosted on `Github`_. Patches must be submitted using `Gerrit`_, *not* Github -pull requests. - -.. _Github: https://github.com/openstack/python-novaclient -.. _Launchpad: https://launchpad.net/python-novaclient -.. _Gerrit: http://docs.openstack.org/infra/manual/developers.html#development-workflow - python-novaclient is licensed under the Apache License like the rest of OpenStack. +* License: Apache License, Version 2.0 +* `PyPi`_ - package installation +* `Online Documentation`_ +* `Blueprints`_ - feature specifications +* `Bugs`_ - issue tracking +* `Source`_ +* `Specs`_ +* `How to Contribute`_ + +.. _PyPi: https://pypi.python.org/pypi/python-novaclient +.. _Online Documentation: http://docs.openstack.org/developer/python-novaclient +.. _Blueprints: https://blueprints.launchpad.net/python-novaclient +.. _Bugs: https://bugs.launchpad.net/python-novaclient +.. _Source: https://git.openstack.org/cgit/openstack/python-novaclient +.. _How to Contribute: http://docs.openstack.org/infra/manual/developers.html +.. _Specs: http://specs.openstack.org/openstack/nova-specs/ + .. contents:: Contents: :local: @@ -78,11 +95,6 @@ To use with keystone as the authentication system:: [...] -* License: Apache License, Version 2.0 -* Documentation: http://docs.openstack.org/developer/python-novaclient -* Source: http://git.openstack.org/cgit/openstack/python-novaclient -* Bugs: http://bugs.launchpad.net/python-novaclient - Testing ------- From 608aa525f10102239659170b8892df8a7900a0ad Mon Sep 17 00:00:00 2001 From: jichenjc Date: Sat, 21 Nov 2015 10:42:13 +0800 Subject: [PATCH 0887/1705] Print current nova default nova API microversion nova client only print server side min and max version, but not print client side min and max version. Change-Id: Ic48d7eff3ae84bf7897e97a2eff1302a7c64bc2a Closes-Bug: 1517870 --- novaclient/v2/shell.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 3513daed3..42ab9cea3 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -34,6 +34,7 @@ from oslo_utils import uuidutils import six +import novaclient from novaclient import api_versions from novaclient import client from novaclient import exceptions @@ -4674,4 +4675,12 @@ def do_version_list(cs, args): columns = ["Id", "Status", "Updated", "Min Version", "Version"] else: columns = ["Id", "Status", "Updated"] + + print(_("Client supported API versions:")) + print(_("Minimum version %(v)s") % + {'v': novaclient.API_MIN_VERSION.get_string()}) + print(_("Maximum version %(v)s") % + {'v': novaclient.API_MAX_VERSION.get_string()}) + + print (_("\nServer supported API versions:")) utils.print_list(result, columns) From 6c3df9d141df8a89b28b0b5b7276b43485a59b8b Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Sat, 21 Nov 2015 16:22:56 +0000 Subject: [PATCH 0888/1705] Updated from global requirements Change-Id: Iea54d70a609dfd263ff9a3ad0c92cbf8c7fc4b88 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 82a702895..41b09fbd9 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -11,7 +11,7 @@ mock>=1.2 requests-mock>=0.6.0 # Apache-2.0 sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2 os-client-config!=1.6.2,>=1.4.0 -oslosphinx>=2.5.0 # Apache-2.0 +oslosphinx!=3.4.0,>=2.5.0 # Apache-2.0 testrepository>=0.0.18 testscenarios>=0.4 testtools>=1.4.0 From 8b8edc72e0b9eedbcadbef2643dca0c69dd35dba Mon Sep 17 00:00:00 2001 From: jichenjc Date: Sat, 21 Nov 2015 01:33:10 +0800 Subject: [PATCH 0889/1705] Not transform to str on potential unicode fields some fields such as instance.display_name can be unicode, nova client should not translate it to 'str'. otherwise it will report UnicodeEncodeError. Change-Id: I4f6011105b3b11dbbcb23f3a7c1bbcf7f20bcc8c Closes-Bug: 1518141 --- novaclient/tests/unit/test_utils.py | 31 +++++++++++++++++++++++++++++ novaclient/utils.py | 4 ++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/novaclient/tests/unit/test_utils.py b/novaclient/tests/unit/test_utils.py index d4eff35c2..2eb3f0695 100644 --- a/novaclient/tests/unit/test_utils.py +++ b/novaclient/tests/unit/test_utils.py @@ -14,6 +14,7 @@ import sys import mock +from oslo_utils import encodeutils import six from novaclient import base @@ -215,6 +216,21 @@ def test_print_list_sort_by_integer(self): '+------+-------+\n', sys.stdout.getvalue()) + @mock.patch('sys.stdout', six.StringIO()) + def test_print_unicode_list(self): + objs = [_FakeResult("k", u'\u2026')] + utils.print_list(objs, ["Name", "Value"]) + if six.PY3: + s = u'\u2026' + else: + s = encodeutils.safe_encode(u'\u2026') + self.assertEqual('+------+-------+\n' + '| Name | Value |\n' + '+------+-------+\n' + '| k | %s |\n' + '+------+-------+\n' % s, + sys.stdout.getvalue()) + # without sorting @mock.patch('sys.stdout', six.StringIO()) def test_print_list_sort_by_none(self): @@ -280,6 +296,21 @@ def test_print_large_dict_list(self): '+----------+------------------------------------------+\n', sys.stdout.getvalue()) + @mock.patch('sys.stdout', six.StringIO()) + def test_print_unicode_dict(self): + dict = {'k': u'\u2026'} + utils.print_dict(dict) + if six.PY3: + s = u'\u2026' + else: + s = encodeutils.safe_encode(u'\u2026') + self.assertEqual('+----------+-------+\n' + '| Property | Value |\n' + '+----------+-------+\n' + '| k | %s |\n' + '+----------+-------+\n' % s, + sys.stdout.getvalue()) + class FlattenTestCase(test_utils.TestCase): def test_flattening(self): diff --git a/novaclient/utils.py b/novaclient/utils.py index b420a29d9..981635c33 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -98,7 +98,7 @@ def print_list(objs, fields, formatters={}, sortby_index=None): if data is None: data = '-' # '\r' would break the table, so remove it. - data = str(data).replace("\r", "") + data = six.text_type(data).replace("\r", "") row.append(data) pt.add_row(row) @@ -162,7 +162,7 @@ def print_dict(d, dict_property="Property", dict_value="Value", wrap=0): if isinstance(v, (dict, list)): v = jsonutils.dumps(v) if wrap > 0: - v = textwrap.fill(str(v), wrap) + v = textwrap.fill(six.text_type(v), wrap) # if value has a newline, add in multiple rows # e.g. fault with stacktrace if v and isinstance(v, six.string_types) and (r'\n' in v or '\r' in v): From 4393d9052423851a530b1a9df056d31aeec0597a Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Mon, 23 Nov 2015 15:16:06 +0200 Subject: [PATCH 0890/1705] Check response headers for microversions novaclient provides version negotiation on CLI layer. In case of usage novaclient as a lib, user can specify api_version with microversion part and can try communicate with Nova endpoint which doesn't support microversions. Nova endpoint will not check X-OpenStack-Nova-API-Version header and will execute request(it can be correct or not). It this case we should warn user about his mistake and about "incorrect" response. Change-Id: I7a5ab964b7b2b2a49cc80c22bf67ad5548afb30b --- novaclient/api_versions.py | 12 +++++++++- novaclient/client.py | 3 +++ novaclient/tests/unit/test_api_versions.py | 26 ++++++++++++++++++++++ 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/novaclient/api_versions.py b/novaclient/api_versions.py index 06fcc52bd..047a047db 100644 --- a/novaclient/api_versions.py +++ b/novaclient/api_versions.py @@ -30,6 +30,7 @@ LOG.addHandler(logging.StreamHandler()) +HEADER_NAME = "X-OpenStack-Nova-API-Version" # key is a deprecated version and value is an alternative version. DEPRECATED_VERSIONS = {"1.1": "2"} @@ -306,7 +307,16 @@ def update_headers(headers, api_version): """Set 'X-OpenStack-Nova-API-Version' header if api_version is not null""" if not api_version.is_null() and api_version.ver_minor != 0: - headers["X-OpenStack-Nova-API-Version"] = api_version.get_string() + headers[HEADER_NAME] = api_version.get_string() + + +def check_headers(response, api_version): + """Checks that 'X-OpenStack-Nova-API-Version' header in response.""" + if api_version.ver_minor > 0 and HEADER_NAME not in response.headers: + LOG.warning(_LW( + "Your request was processed by a Nova API which does not support " + "microversions (%s header is missing from response). " + "Warning: Response may be incorrect."), HEADER_NAME) def add_substitution(versioned_method): diff --git a/novaclient/client.py b/novaclient/client.py index 399d98e22..0fc998389 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -89,6 +89,7 @@ def request(self, url, method, **kwargs): method, raise_exc=False, **kwargs) + api_versions.check_headers(resp, self.api_version) if raise_exc and resp.status_code >= 400: raise exceptions.from_response(resp, body, url, method) @@ -365,6 +366,8 @@ def request(self, url, method, **kwargs): url, **kwargs) + api_versions.check_headers(resp, self.api_version) + self.http_log_resp(resp) if resp.text: diff --git a/novaclient/tests/unit/test_api_versions.py b/novaclient/tests/unit/test_api_versions.py index 0159d198b..da35fdf99 100644 --- a/novaclient/tests/unit/test_api_versions.py +++ b/novaclient/tests/unit/test_api_versions.py @@ -145,6 +145,32 @@ def test_api_version_is_not_null(self): headers) +class CheckHeadersTestCase(utils.TestCase): + def setUp(self): + super(CheckHeadersTestCase, self).setUp() + mock_log_patch = mock.patch("novaclient.api_versions.LOG") + self.mock_log = mock_log_patch.start() + self.addCleanup(mock_log_patch.stop) + + def test_microversion_is_specified(self): + response = mock.MagicMock(headers={api_versions.HEADER_NAME: ""}) + api_versions.check_headers(response, api_versions.APIVersion("2.2")) + self.assertFalse(self.mock_log.warning.called) + + response = mock.MagicMock(headers={}) + api_versions.check_headers(response, api_versions.APIVersion("2.2")) + self.assertTrue(self.mock_log.warning.called) + + def test_microversion_is_not_specified(self): + response = mock.MagicMock(headers={api_versions.HEADER_NAME: ""}) + api_versions.check_headers(response, api_versions.APIVersion("2.2")) + self.assertFalse(self.mock_log.warning.called) + + response = mock.MagicMock(headers={}) + api_versions.check_headers(response, api_versions.APIVersion("2.0")) + self.assertFalse(self.mock_log.warning.called) + + class GetAPIVersionTestCase(utils.TestCase): def test_get_available_client_versions(self): output = api_versions.get_available_major_versions() From 3e8cee01d5f3a6678cb641f81f88782f57a8c61a Mon Sep 17 00:00:00 2001 From: Sylvain Bauza Date: Mon, 23 Nov 2015 21:01:40 +0100 Subject: [PATCH 0891/1705] Add reno for release notes management Since reno is the new tool for Relnotes, we need to add it to novaclient and provide some Sphinx docs for Liberty and Mitaka. Change-Id: Ie42642a0e0037311cfa14cd8bf6b7041b62a4675 --- .gitignore | 3 + releasenotes/notes/.placeholder | 0 releasenotes/source/_static/.placeholder | 0 releasenotes/source/_templates/.placeholder | 0 releasenotes/source/conf.py | 260 ++++++++++++++++++++ releasenotes/source/index.rst | 18 ++ releasenotes/source/liberty.rst | 6 + releasenotes/source/unreleased.rst | 5 + test-requirements.txt | 3 + tox.ini | 4 +- 10 files changed, 298 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/.placeholder create mode 100644 releasenotes/source/_static/.placeholder create mode 100644 releasenotes/source/_templates/.placeholder create mode 100644 releasenotes/source/conf.py create mode 100644 releasenotes/source/index.rst create mode 100644 releasenotes/source/liberty.rst create mode 100644 releasenotes/source/unreleased.rst diff --git a/.gitignore b/.gitignore index 5e1b43c4c..a0bd6f33a 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,6 @@ ChangeLog novaclient/versioninfo *.egg *egg-info + +# Files created by releasenotes build +releasenotes/build diff --git a/releasenotes/notes/.placeholder b/releasenotes/notes/.placeholder new file mode 100644 index 000000000..e69de29bb diff --git a/releasenotes/source/_static/.placeholder b/releasenotes/source/_static/.placeholder new file mode 100644 index 000000000..e69de29bb diff --git a/releasenotes/source/_templates/.placeholder b/releasenotes/source/_templates/.placeholder new file mode 100644 index 000000000..e69de29bb diff --git a/releasenotes/source/conf.py b/releasenotes/source/conf.py new file mode 100644 index 000000000..996aff6d6 --- /dev/null +++ b/releasenotes/source/conf.py @@ -0,0 +1,260 @@ +# -*- coding: utf-8 -*- +# +# Nova Client Release Notes documentation build configuration file, created by +# sphinx-quickstart on Mon Nov 23 20:38:38 2015. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +#sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'oslosphinx', + 'reno.sphinxext', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'Nova Client Release Notes' +copyright = u'2015, Nova developers' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +import pbr.version +nova_version = pbr.version.VersionInfo('python-novaclient') +# The short X.Y version. +version = nova_version.canonical_version_string() +# The full version, including alpha/beta/rc tags. +release = nova_version.version_string_with_vcs() + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = [] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +#keep_warnings = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'default' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +#html_extra_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'NovaClientReleaseNotestdoc' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { +# The paper size ('letterpaper' or 'a4paper'). +#'papersize': 'letterpaper', + +# The font size ('10pt', '11pt' or '12pt'). +#'pointsize': '10pt', + +# Additional stuff for the LaTeX preamble. +#'preamble': '', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + ('index', 'PythonNovaClient.tex', u'Nova Client Release Notes Documentation', + u'Nova developers', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'pythonnovaclient', u'Nova Client Release Notes Documentation', + [u'Nova developers'], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ('index', 'PythonNovaClient', u'Nova Client Release Notes Documentation', + u'Nova developers', 'PythonNovaClient', 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +#texinfo_no_detailmenu = False diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst new file mode 100644 index 000000000..6d8e6df7f --- /dev/null +++ b/releasenotes/source/index.rst @@ -0,0 +1,18 @@ +Welcome to Nova Client Release Notes documentation! +=================================================== + +Contents +======== + +.. toctree:: + :maxdepth: 2 + + liberty + unreleased + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`search` diff --git a/releasenotes/source/liberty.rst b/releasenotes/source/liberty.rst new file mode 100644 index 000000000..36217be84 --- /dev/null +++ b/releasenotes/source/liberty.rst @@ -0,0 +1,6 @@ +============================== + Liberty Series Release Notes +============================== + +.. release-notes:: + :branch: origin/stable/liberty diff --git a/releasenotes/source/unreleased.rst b/releasenotes/source/unreleased.rst new file mode 100644 index 000000000..875030f9d --- /dev/null +++ b/releasenotes/source/unreleased.rst @@ -0,0 +1,5 @@ +============================ +Current Series Release Notes +============================ + +.. release-notes:: diff --git a/test-requirements.txt b/test-requirements.txt index 41b09fbd9..252ca9790 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -16,3 +16,6 @@ testrepository>=0.0.18 testscenarios>=0.4 testtools>=1.4.0 tempest-lib>=0.10.0 + +# releasenotes +reno>=0.1.1 # Apache2 diff --git a/tox.ini b/tox.ini index 3ac09aef5..6fe9d7bab 100644 --- a/tox.ini +++ b/tox.ini @@ -30,6 +30,8 @@ commands = {posargs} commands = python setup.py build_sphinx +[testenv:releasenotes] +commands = sphinx-build -a -E -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html [testenv:functional] @@ -54,7 +56,7 @@ downloadcache = ~/cache/pip # Additional checks are also ignored on purpose: F811, F821 ignore = F811,F821,H404,H405 show-source = True -exclude=.venv,.git,.tox,dist,*openstack/common*,*lib/python*,*egg,build,doc/source/conf.py +exclude=.venv,.git,.tox,dist,*openstack/common*,*lib/python*,*egg,build,doc/source/conf.py,releasenotes [hacking] import_exceptions = novaclient.i18n From 297231c1081a3fb56afcece1efdf613b81473670 Mon Sep 17 00:00:00 2001 From: jichenjc Date: Tue, 24 Nov 2015 10:46:53 +0800 Subject: [PATCH 0892/1705] Add accessIPv4 and accessIPv6 when create server nova support accessIPv4 and accessIPv6 as input param but nova client don't support it as input param when doing create operation. Because it's already support in v2.1 so no microversion introduced. Change-Id: Idd50fe921f8c931ee28902f90ffdde6bfba34359 Closes-Bug: 1522359 --- novaclient/tests/unit/v2/test_servers.py | 24 ++++++++++++++++++++++++ novaclient/tests/unit/v2/test_shell.py | 17 +++++++++++++++++ novaclient/v2/servers.py | 17 ++++++++++++++--- novaclient/v2/shell.py | 16 +++++++++++++++- 4 files changed, 70 insertions(+), 4 deletions(-) diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index 3b973538c..558f7b654 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -204,6 +204,30 @@ def wrapped_boot(url, key, *boot_args, **boot_kwargs): self.assert_called('POST', '/servers') self.assertIsInstance(s, servers.Server) + def test_create_server_boot_with_address(self): + old_boot = self.cs.servers._boot + access_ip_v6 = '::1' + access_ip_v4 = '10.10.10.10' + + def wrapped_boot(url, key, *boot_args, **boot_kwargs): + self.assertEqual(boot_kwargs['access_ip_v6'], access_ip_v6) + self.assertEqual(boot_kwargs['access_ip_v4'], access_ip_v4) + return old_boot(url, key, *boot_args, **boot_kwargs) + + with mock.patch.object(self.cs.servers, '_boot', wrapped_boot): + s = self.cs.servers.create( + name="My server", + image=1, + flavor=1, + meta={'foo': 'bar'}, + userdata="hello moto", + key_name="fakekey", + access_ip_v6=access_ip_v6, + access_ip_v4=access_ip_v4 + ) + self.assert_called('POST', '/servers') + self.assertIsInstance(s, servers.Server) + def test_create_server_userdata_file_object(self): s = self.cs.servers.create( name="My server", diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 7a6a8e2cc..aa8a0a75d 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -234,6 +234,23 @@ def test_boot_config_drive(self): }}, ) + def test_boot_access_ip(self): + self.run_command( + 'boot --flavor 1 --image 1 --access-ip-v4 10.10.10.10 ' + '--access-ip-v6 ::1 some-server') + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': '1', + 'accessIPv4': '10.10.10.10', + 'accessIPv6': '::1', + 'max_count': 1, + 'min_count': 1 + }}, + ) + def test_boot_config_drive_custom(self): self.run_command( 'boot --flavor 1 --image 1 --config-drive /dev/hda some-server') diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index 43e316cab..5922e946a 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -421,7 +421,8 @@ def _boot(self, resource_url, response_key, name, image, flavor, max_count=None, security_groups=None, key_name=None, availability_zone=None, block_device_mapping=None, block_device_mapping_v2=None, nics=None, scheduler_hints=None, - config_drive=None, admin_pass=None, disk_config=None, **kwargs): + config_drive=None, admin_pass=None, disk_config=None, + access_ip_v4=None, access_ip_v6=None, **kwargs): """ Create (boot) a new server. """ @@ -537,6 +538,12 @@ def _boot(self, resource_url, response_key, name, image, flavor, if disk_config is not None: body['server']['OS-DCF:diskConfig'] = disk_config + if access_ip_v4 is not None: + body['server']['accessIPv4'] = access_ip_v4 + + if access_ip_v6 is not None: + body['server']['accessIPv6'] = access_ip_v6 + return self._create(resource_url, body, response_key, return_raw=return_raw, **kwargs) @@ -916,7 +923,8 @@ def create(self, name, image, flavor, meta=None, files=None, key_name=None, availability_zone=None, block_device_mapping=None, block_device_mapping_v2=None, nics=None, scheduler_hints=None, - config_drive=None, disk_config=None, admin_pass=None, **kwargs): + config_drive=None, disk_config=None, admin_pass=None, + access_ip_v4=None, access_ip_v6=None, **kwargs): # TODO(anthony): indicate in doc string if param is an extension # and/or optional """ @@ -961,6 +969,8 @@ def create(self, name, image, flavor, meta=None, files=None, values are 'AUTO' or 'MANUAL'. :param admin_pass: (optional extension) add a user supplied admin password. + :param access_ip_v4: (optional extension) add alternative access ip v4 + :param access_ip_v6: (optional extension) add alternative access ip v6 """ if not min_count: min_count = 1 @@ -977,7 +987,8 @@ def create(self, name, image, flavor, meta=None, files=None, max_count=max_count, security_groups=security_groups, key_name=key_name, availability_zone=availability_zone, scheduler_hints=scheduler_hints, config_drive=config_drive, - disk_config=disk_config, admin_pass=admin_pass, **kwargs) + disk_config=disk_config, admin_pass=admin_pass, + access_ip_v4=access_ip_v4, access_ip_v6=access_ip_v6, **kwargs) if block_device_mapping: resource_url = "/os-volumes_boot" diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 42ab9cea3..e859fa736 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -326,7 +326,9 @@ def _boot(cs, args): nics=nics, scheduler_hints=hints, config_drive=config_drive, - admin_pass=args.admin_pass) + admin_pass=args.admin_pass, + access_ip_v4=args.access_ip_v4, + access_ip_v6=args.access_ip_v6) return boot_args, boot_kwargs @@ -515,6 +517,18 @@ def _boot(cs, args): metavar='', default=None, help=_('Admin password for the instance.')) +@cliutils.arg( + '--access-ip-v4', + dest='access_ip_v4', + metavar='', + default=None, + help=_('Alternative access ip v4 of the instance.')) +@cliutils.arg( + '--access-ip-v6', + dest='access_ip_v6', + metavar='', + default=None, + help=_('Alternative access ip v6 of the instance.')) def do_boot(cs, args): """Boot a new server.""" boot_args, boot_kwargs = _boot(cs, args) From fadfbea7de1f07e8b664dee2469f3b35e011596d Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Tue, 24 Nov 2015 14:45:58 +0000 Subject: [PATCH 0893/1705] Updated from global requirements Change-Id: I2b15450f0ce8b074fef50ed1dc61915fd9ed081f --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 252ca9790..1409995df 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -18,4 +18,4 @@ testtools>=1.4.0 tempest-lib>=0.10.0 # releasenotes -reno>=0.1.1 # Apache2 +reno>=0.1.1 # Apache2 From c3dda7636cb7cf1d613c027703b88b41144dec32 Mon Sep 17 00:00:00 2001 From: melanie witt Date: Sat, 21 Nov 2015 00:15:21 +0000 Subject: [PATCH 0894/1705] Fix repr of a host from a hosts.list() The os-hosts API uses different attribute names depending on whether the host was returned as part of a list or not. A host returned from 'show host' has the attribute "host" whereas a host returned from 'list hosts' has the attribute "host_name". This adds a host_name property to the Host class that will set the "host" attribute if necessary. Although this doesn't exactly mirror the responses coming back from nova api, it will make it easier for users to use the objects interchangeably for hosts.list() operations and hosts.update() operations, for example. Co-Authored-By: Chung Chih, Hung Closes-Bug: #1434167 Change-Id: I5c339bdd1ab867d972759ade9a10b86bfda1e70a --- novaclient/tests/unit/fixture_data/hosts.py | 4 ++-- novaclient/tests/unit/v2/test_hosts.py | 5 +++++ novaclient/v2/hosts.py | 12 ++++++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/novaclient/tests/unit/fixture_data/hosts.py b/novaclient/tests/unit/fixture_data/hosts.py index 28ff17ef1..160eb3989 100644 --- a/novaclient/tests/unit/fixture_data/hosts.py +++ b/novaclient/tests/unit/fixture_data/hosts.py @@ -62,12 +62,12 @@ def get_os_hosts(request, context): return { 'hosts': [ { - 'host': 'host1', + 'host_name': 'host1', 'service': service or 'nova-compute', 'zone': zone }, { - 'host': 'host1', + 'host_name': 'host1', 'service': service or 'nova-cert', 'zone': zone } diff --git a/novaclient/tests/unit/v2/test_hosts.py b/novaclient/tests/unit/v2/test_hosts.py index d99627b09..8f77e01b4 100644 --- a/novaclient/tests/unit/v2/test_hosts.py +++ b/novaclient/tests/unit/v2/test_hosts.py @@ -85,3 +85,8 @@ def test_host_shutdown(self): def test_hosts_repr(self): hs = self.cs.hosts.get('host') self.assertEqual('', repr(hs[0])) + + def test_hosts_list_repr(self): + hs = self.cs.hosts.list() + for h in hs: + self.assertEqual('' % h.host_name, repr(h)) diff --git a/novaclient/v2/hosts.py b/novaclient/v2/hosts.py index af1756ad2..0f979973b 100644 --- a/novaclient/v2/hosts.py +++ b/novaclient/v2/hosts.py @@ -40,6 +40,18 @@ def shutdown(self): def reboot(self): return self.manager.host_action(self.host, 'reboot') + @property + def host_name(self): + return self.host + + @host_name.setter + def host_name(self, value): + # A host from hosts.list() has the attribute "host_name" instead of + # "host." This sets "host" if that's the case. Even though it doesn't + # exactly mirror the response format, it enables users to work with + # host objects from list and non-list operations interchangeably. + self.host = value + class HostManager(base.ManagerWithFind): resource_class = Host From 245624541a650cedb2f2e4c96b35d235f3696230 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 27 Nov 2015 22:42:13 +0000 Subject: [PATCH 0895/1705] Updated from global requirements Change-Id: I5d9a308a020f8e99837c4f0a29b87e2542b2cffe --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4f3bdfabc..c2bf2a6c5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ oslo.i18n>=1.5.0 # Apache-2.0 oslo.serialization>=1.10.0 # Apache-2.0 oslo.utils>=2.8.0 # Apache-2.0 PrettyTable<0.8,>=0.7 -requests!=2.8.0,>=2.5.2 +requests>=2.8.1 simplejson>=2.2.0 six>=1.9.0 Babel>=1.3 From bb7956bfaa1ebc2e87cf4ba9f40d9ae75339f53d Mon Sep 17 00:00:00 2001 From: Sylvain Bauza Date: Sat, 28 Nov 2015 00:57:58 +0100 Subject: [PATCH 0896/1705] force releasenotes warnings to be treated as errors Per http://lists.openstack.org/pipermail/openstack-dev/2015-November/080521.html, we need to make sure that there are no warnings for reno. Change-Id: I899d223f5347633a17ac5c7fb3991a3cd1f4c781 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 6fe9d7bab..3154330e9 100644 --- a/tox.ini +++ b/tox.ini @@ -31,7 +31,7 @@ commands = python setup.py build_sphinx [testenv:releasenotes] -commands = sphinx-build -a -E -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html +commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html [testenv:functional] From 6036114aa440c0e64e3862da6220b9bce40be8cd Mon Sep 17 00:00:00 2001 From: xiexs Date: Sun, 29 Nov 2015 00:58:28 +0800 Subject: [PATCH 0897/1705] Optimize "open" method with context manager Use opening context manager to open a file. Change-Id: I50e4f2df54737e1ea5b4c7fbe8639f1cd981a627 --- novaclient/tests/unit/v2/test_shell.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 23b85978a..023f1e408 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -169,7 +169,8 @@ def test_boot_key(self): def test_boot_user_data(self): testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt') - data = open(testfile).read().encode('utf-8') + with open(testfile) as testfile_fd: + data = testfile_fd.read().encode('utf-8') expected_file_data = base64.b64encode(data).decode('utf-8') self.run_command( 'boot --flavor 1 --image 1 --user_data %s some-server' % testfile) @@ -542,7 +543,8 @@ def test_boot_nics_netid_and_portid(self): def test_boot_files(self): testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt') - data = open(testfile).read() + with open(testfile) as testfile_fd: + data = testfile_fd.read() expected = base64.b64encode(data.encode('utf-8')).decode('utf-8') cmd = ('boot some-server --flavor 1 --image 1' From 7a9d4e58bf9d066e5e16604cce58ed4b40ca8011 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Mon, 30 Nov 2015 15:30:04 +0200 Subject: [PATCH 0898/1705] [microversions] Increase max version to 2.7 2.7 - Check the is_public attribute of a flavor before adding tenant access to it. Reject the request with HTTPConflict error. Since novaclient already have Conflict expection, there is no required changes on client side. Change-Id: I48dca20b3795185363367bac0e2ee65ce938593f --- novaclient/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 004202371..1a4aa4043 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.6") +API_MAX_VERSION = api_versions.APIVersion("2.7") From 81f8fa655ccecd409fe6dcda0d3763592c053e57 Mon Sep 17 00:00:00 2001 From: Chuck Carmack Date: Mon, 23 Nov 2015 18:49:29 +0000 Subject: [PATCH 0899/1705] Remove python 2.6 support from python-novaclient Since oslo is removing support for python 2.6, nova needs to also remove support from clients and libraries. This commit is to remove support from python-novaclient. -- Python 2.6 compatibilty code was removed. -- Python 2.6 was removed as a tox environment, install venv, and from the classifiers in setup.cfg. -- Release notes have been updated. UpgradeImpact Co-Authored-By: Andrey Kurilin Change-Id: I3f587ff38d478d075af5fd014e2b4b8416e185d4 Closes-bug: 1518390 --- novaclient/client.py | 4 ---- .../notes/remove-py26-support-f31379e86f40d975.yaml | 3 +++ setup.cfg | 5 +++-- tools/install_venv_common.py | 6 +++--- tox.ini | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) create mode 100644 releasenotes/notes/remove-py26-support-f31379e86f40d975.yaml diff --git a/novaclient/client.py b/novaclient/client.py index 0fc998389..2bd6f0562 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -736,10 +736,6 @@ def discover_extensions(version): def _discover_via_python_path(): for (module_loader, name, _ispkg) in pkgutil.iter_modules(): if name.endswith('_python_novaclient_ext'): - if not hasattr(module_loader, 'load_module'): - # Python 2.6 compat: actually get an ImpImporter obj - module_loader = module_loader.find_module(name) - module = module_loader.load_module(name) if hasattr(module, 'extension_name'): name = module.extension_name diff --git a/releasenotes/notes/remove-py26-support-f31379e86f40d975.yaml b/releasenotes/notes/remove-py26-support-f31379e86f40d975.yaml new file mode 100644 index 000000000..4f144875c --- /dev/null +++ b/releasenotes/notes/remove-py26-support-f31379e86f40d975.yaml @@ -0,0 +1,3 @@ +--- +upgrade: + - Python 2.6 support has been removed from python-novaclient. diff --git a/setup.cfg b/setup.cfg index 174a80f87..e5173d752 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,9 +16,10 @@ classifier = License :: OSI Approved :: Apache Software License Operating System :: OS Independent Programming Language :: Python - Programming Language :: Python :: 2.6 + Programming Language :: Python :: 2 Programming Language :: Python :: 2.7 - Programming Language :: Python :: 3.3 + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.4 [files] packages = diff --git a/tools/install_venv_common.py b/tools/install_venv_common.py index e279159ab..962b2f930 100644 --- a/tools/install_venv_common.py +++ b/tools/install_venv_common.py @@ -17,7 +17,7 @@ virtual environments. Since this script is used to bootstrap a virtualenv from the system's Python -environment, it should be kept strictly compatible with Python 2.6. +environment, it should be kept strictly compatible with Python 2.7. Synced in from openstack-common """ @@ -47,8 +47,8 @@ def die(self, message, *args): sys.exit(1) def check_python_version(self): - if sys.version_info < (2, 6): - self.die("Need Python Version >= 2.6") + if sys.version_info < (2, 7): + self.die("Need Python Version >= 2.7") def run_command_with_code(self, cmd, redirect_output=True, check_exit_code=True): diff --git a/tox.ini b/tox.ini index 6fe9d7bab..4373434e3 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ # noted to use py34 you need virtualenv >= 1.11.4 [tox] -envlist = py26,py27,py33,py34,pypy,pep8,docs +envlist = py27,py33,py34,pypy,pep8,docs minversion = 1.6 skipsdist = True From 9314c89ddeb5fea8b11ca760f883b56f95079384 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Tue, 1 Dec 2015 06:10:06 +0000 Subject: [PATCH 0900/1705] Updated from global requirements Change-Id: I617a357f6b09d39a702f7478e641d240413c7943 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 1409995df..c991f9567 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -8,7 +8,7 @@ discover fixtures>=1.3.1 keyring!=3.3,>=2.1 mock>=1.2 -requests-mock>=0.6.0 # Apache-2.0 +requests-mock>=0.7.0 # Apache-2.0 sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2 os-client-config!=1.6.2,>=1.4.0 oslosphinx!=3.4.0,>=2.5.0 # Apache-2.0 From b6e44f893a13dbb2e07cf5b9c7eb6e4691053f12 Mon Sep 17 00:00:00 2001 From: lei zhang Date: Tue, 1 Dec 2015 21:05:18 +0800 Subject: [PATCH 0901/1705] Fix a Typo in Docstring This patch fix a typo in def remove_security_group(self, server, security_group) Change-Id: I2dc4250496707a6213797cb120f36b3415a774c5 --- novaclient/v2/servers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index bfab3f446..c90a411a5 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -1272,7 +1272,7 @@ def add_security_group(self, server, security_group): def remove_security_group(self, server, security_group): """ - Add a Security Group to an instance + Remove a Security Group to an instance :param server: ID of the instance. :param security_group: The name of security group to remove. From 47ee8b967687c109b958672ed788e2367fd2df12 Mon Sep 17 00:00:00 2001 From: "Chung Chih, Hung" Date: Fri, 20 Nov 2015 06:53:39 +0000 Subject: [PATCH 0902/1705] Help msg about libvirt always default device names Supplying a device name for any of the block devices specified as part of the `nova boot` or `nova volume-attach` call will not be honoured by any libvirt compute nodes. We should modify documentation make this more clear. Change-Id: I3209c2829225492df25ff9ab9098071372957bd5 Closes-Bug: 1479214 --- novaclient/v2/shell.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 3513daed3..e1cab4c77 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -451,7 +451,8 @@ def _boot(cs, args): "type=device type (e.g. disk, cdrom, ...; defaults to 'disk') " "device=name of the device (e.g. vda, xda, ...; " "if omitted, hypervisor driver chooses suitable device " - "depending on selected bus), " + "depending on selected bus; note the libvirt driver always " + "uses default device names), " "size=size of the block device in MB(for swap) and in " "GB(for other formats) " "(if omitted, hypervisor driver calculates size), " @@ -2186,7 +2187,8 @@ def do_volume_delete(cs, args): @cliutils.arg( 'device', metavar='', default=None, nargs='?', help=_('Name of the device e.g. /dev/vdb. ' - 'Use "auto" for autoassign (if supported)')) + 'Use "auto" for autoassign (if supported). ' + 'Libvirt driver will use default device name.')) def do_volume_attach(cs, args): """Attach a volume to a server.""" if args.device == 'auto': From aa73df2649496f049118cfa7dae74a759edf9dd9 Mon Sep 17 00:00:00 2001 From: Ghanshyam Date: Wed, 2 Dec 2015 19:36:45 +0900 Subject: [PATCH 0903/1705] Fix H404/405 violations in client.py,base.py,api_version.py There is a lot of H404/405 violations in novaclient, and it is better to fix those to have a better doc string for class/methods. This patch fixes these violations for below files- -api_versions.py -base.py -client.py As there are lot of violations and cannot be fixed in single patches, So separating those in multiple patches for easy review. Change-Id: I30a714fc3b0b317f7ffa4a99fbb224b4d5f5477b Partial-Bug: #1521899 --- novaclient/api_versions.py | 30 ++++++++++++++++++++++-------- novaclient/base.py | 21 +++++++++------------ novaclient/client.py | 17 ++++++++++------- 3 files changed, 41 insertions(+), 27 deletions(-) diff --git a/novaclient/api_versions.py b/novaclient/api_versions.py index 047a047db..cdd708b65 100644 --- a/novaclient/api_versions.py +++ b/novaclient/api_versions.py @@ -40,13 +40,22 @@ class APIVersion(object): - """This class represents an API Version with convenience - methods for manipulation and comparison of version - numbers that we need to do to implement microversions. + """This class represents an API Version Request. + + This class provides convenience methods for manipulation + and comparison of version numbers that we need to do to + implement microversions. """ def __init__(self, version_str=None): - """Create an API version object.""" + """Create an API version object. + + :param version_string: String representation of APIVersionRequest. + Correct format is 'X.Y', where 'X' and 'Y' + are int values. None value should be used + to create Null APIVersionRequest, which is + equal to 0.0 + """ self.ver_major = 0 self.ver_minor = 0 @@ -120,7 +129,9 @@ def __ge__(self, other): return self > other or self == other def matches(self, min_version, max_version): - """Returns whether the version object represents a version + """Matches the version object. + + Returns whether the version object represents a version greater than or equal to the minimum version and less than or equal to the maximum version. @@ -145,7 +156,9 @@ def matches(self, min_version, max_version): return min_version <= self <= max_version def get_string(self): - """Converts object to string representation which if used to create + """Version string representation. + + Converts object to string representation which if used to create an APIVersion object results in the same version. """ if self.is_null(): @@ -242,14 +255,15 @@ def _get_server_version_range(client): def discover_version(client, requested_version): - """Checks ``requested_version`` and returns the most recent version + """Discover most recent version supported by API and client. + + Checks ``requested_version`` and returns the most recent version supported by both the API and the client. :param client: client object :param requested_version: requested version represented by APIVersion obj :returns: APIVersion """ - server_start_version, server_end_version = _get_server_version_range( client) diff --git a/novaclient/base.py b/novaclient/base.py index 82c98904a..e86e2290c 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -36,7 +36,8 @@ def getid(obj): - """ + """Get object's ID or object. + Abstracts the common pattern of allowing both an object or an object's ID as a parameter when dealing with relationships. """ @@ -47,7 +48,8 @@ def getid(obj): class Manager(base.HookableMixin): - """ + """Manager for API service. + Managers interact with a particular type of API (servers, flavors, images, etc.) and provide CRUD operations for them. """ @@ -102,7 +104,8 @@ def alternate_service_type(self, default, allowed_types=()): @contextlib.contextmanager def completion_cache(self, cache_type, obj_class, mode): - """ + """The completion cache for bash autocompletion. + The completion cache store items that can be used for bash autocompletion, like UUIDs or human-friendly IDs. @@ -192,18 +195,14 @@ def _update(self, url, body, response_key=None, **kwargs): @six.add_metaclass(abc.ABCMeta) class ManagerWithFind(Manager): - """ - Like a `Manager`, but with additional `find()`/`findall()` methods. - """ + """Like a `Manager`, but with additional `find()`/`findall()` methods.""" @abc.abstractmethod def list(self): pass def find(self, **kwargs): - """ - Find a single item with attributes matching ``**kwargs``. - """ + """Find a single item with attributes matching ``**kwargs``.""" matches = self.findall(**kwargs) num_matches = len(matches) if num_matches == 0: @@ -215,9 +214,7 @@ def find(self, **kwargs): return matches[0] def findall(self, **kwargs): - """ - Find all items with attributes matching ``**kwargs``. - """ + """Find all items with attributes matching ``**kwargs``.""" found = [] searches = kwargs.items() diff --git a/novaclient/client.py b/novaclient/client.py index 0fc998389..0660abd4d 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -60,9 +60,7 @@ def __init__(self): self._adapters = {} def get(self, url): - """ - Store and reuse HTTP adapters per Service URL. - """ + """Store and reuse HTTP adapters per Service URL.""" if url not in self._adapters: self._adapters[url] = session.TCPKeepAliveAdapter() @@ -103,9 +101,10 @@ def reset_timings(self): def _original_only(f): - """Indicates and enforces that this function can only be used if we are - using the original HTTPClient object. + """Decorator to indicate and enforce original HTTPClient object. + Indicates and enforces that this function can only be used if we are + using the original HTTPClient object. We use this to specify that if you use the newer Session HTTP client then you are aware that the way you use your client has been updated and certain functions are no longer allowed to be used. @@ -471,7 +470,9 @@ def get_service_url(self, service_type): return self.services_url[service_type] def _extract_service_catalog(self, url, resp, body, extract_token=True): - """See what the auth service told us and process the response. + """Extract service catalog from input resource body. + + See what the auth service told us and process the response. We may get redirected to another site, fail or actually get back a service catalog with a token and our endpoints. """ @@ -505,7 +506,9 @@ def _extract_service_catalog(self, url, resp, body, extract_token=True): raise exceptions.from_response(resp, body, url) def _fetch_endpoints_from_auth(self, url): - """We have a token, but don't know the final endpoint for + """Fetch endpoint using token. + + We have a token, but don't know the final endpoint for the region. We have to go back to the auth service and ask again. This request requires an admin-level token to work. The proxy token supplied could be from a low-level enduser. From 1431f1b823749d7b83721dd91e8c1d8921c0d93e Mon Sep 17 00:00:00 2001 From: chen-li Date: Wed, 2 Dec 2015 23:55:59 +0800 Subject: [PATCH 0904/1705] Enable pass instance name as parameter in nova cli After the proposed bug fix, the admin can also start/stop/reset-state instances of other tenants by passing instance name as the parameter. Initially it could be done only through ids. One additional argument --all-tenants is added so that the instance name is searched across all tenants. Previous command was: nova start/stop/reset-state New command is: nova start/stop/reset-state --all-tenants Co-Authored-By: Jyotsna (cherry picked from commit Ie5f8c5c15dee6060b979a34ed0aa01262e05e122) Change-Id: I66f277b16b378a7908d8b951fe39d039df4ef6a4 Closes-Bug: #1297963 --- novaclient/tests/unit/v2/test_shell.py | 22 +++++++++++++++++++++ novaclient/v2/shell.py | 27 +++++++++++++++++++++++--- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 23b85978a..fb8e2cd32 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -1033,10 +1033,24 @@ def test_start(self): self.run_command('start sample-server') self.assert_called('POST', '/servers/1234/action', {'os-start': None}) + def test_start_with_all_tenants(self): + self.run_command('start sample-server --all-tenants') + self.assert_called('GET', + '/servers?all_tenants=1&name=sample-server', pos=0) + self.assert_called('GET', '/servers/1234', pos=1) + self.assert_called('POST', '/servers/1234/action', {'os-start': None}) + def test_stop(self): self.run_command('stop sample-server') self.assert_called('POST', '/servers/1234/action', {'os-stop': None}) + def test_stop_with_all_tenants(self): + self.run_command('stop sample-server --all-tenants') + self.assert_called('GET', + '/servers?all_tenants=1&name=sample-server', pos=0) + self.assert_called('GET', '/servers/1234', pos=1) + self.assert_called('POST', '/servers/1234/action', {'os-stop': None}) + def test_pause(self): self.run_command('pause sample-server') self.assert_called('POST', '/servers/1234/action', {'pause': None}) @@ -1617,6 +1631,14 @@ def test_reset_state(self): self.assert_called('POST', '/servers/1234/action', {'os-resetState': {'state': 'active'}}) + def test_reset_state_with_all_tenants(self): + self.run_command('reset-state sample-server --all-tenants') + self.assert_called('GET', + '/servers?all_tenants=1&name=sample-server', pos=0) + self.assert_called('GET', '/servers/1234', pos=1) + self.assert_called('POST', '/servers/1234/action', + {'os-resetState': {'state': 'error'}}) + def test_reset_state_multiple(self): self.run_command('reset-state sample-server sample-server2') self.assert_called('POST', '/servers/1234/action', diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 3513daed3..82977b090 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -1665,27 +1665,41 @@ def do_unpause(cs, args): _find_server(cs, args.server).unpause() +@cliutils.arg( + '--all-tenants', + action='store_const', + const=1, + default=0, + help=_('Stop server(s) in another tenant by name (Admin only).')) @cliutils.arg( 'server', metavar='', nargs='+', help=_('Name or ID of server(s).')) def do_stop(cs, args): """Stop the server(s).""" + find_args = {'all_tenants': args.all_tenants} utils.do_action_on_many( - lambda s: _find_server(cs, s).stop(), + lambda s: _find_server(cs, s, **find_args).stop(), args.server, _("Request to stop server %s has been accepted."), _("Unable to stop the specified server(s).")) +@cliutils.arg( + '--all-tenants', + action='store_const', + const=1, + default=0, + help=_('Start server(s) in another tenant by name (Admin only).')) @cliutils.arg( 'server', metavar='', nargs='+', help=_('Name or ID of server(s).')) def do_start(cs, args): """Start the server(s).""" + find_args = {'all_tenants': args.all_tenants} utils.do_action_on_many( - lambda s: _find_server(cs, s).start(), + lambda s: _find_server(cs, s, **find_args).start(), args.server, _("Request to start server %s has been accepted."), _("Unable to start the specified server(s).")) @@ -3573,6 +3587,12 @@ def do_live_migration(cs, args): args.disk_over_commit) +@cliutils.arg( + '--all-tenants', + action='store_const', + const=1, + default=0, + help=_('Reset state server(s) in another tenant by name (Admin only).')) @cliutils.arg( 'server', metavar='', nargs='+', help=_('Name or ID of server(s).')) @@ -3584,10 +3604,11 @@ def do_live_migration(cs, args): def do_reset_state(cs, args): """Reset the state of a server.""" failure_flag = False + find_args = {'all_tenants': args.all_tenants} for server in args.server: try: - _find_server(cs, server).reset_state(args.state) + _find_server(cs, server, **find_args).reset_state(args.state) msg = "Reset state for server %s succeeded; new state is %s" print(msg % (server, args.state)) except Exception as e: From 9fe53065ef59a6ed0f9dfe500ad2ad3fb1f1afe0 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Mon, 30 Nov 2015 17:07:35 +0200 Subject: [PATCH 0905/1705] [microversions] Add support for 2.8 2.8 - Add new protocol for VM console (mks) Also, this patch removes code duplication and fixes docstrings for consoles methods. Co-Authored-By: jichenjc Change-Id: Ic24183a5118872581b30f82734fa9d6ce0e53544 --- novaclient/__init__.py | 2 +- novaclient/tests/unit/v2/test_servers.py | 14 +++++++ novaclient/v2/servers.py | 34 ++++++++++++---- novaclient/v2/shell.py | 51 +++++++++++------------- 4 files changed, 65 insertions(+), 36 deletions(-) diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 1a4aa4043..14673cf8c 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.7") +API_MAX_VERSION = api_versions.APIVersion("2.8") diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index 3b973538c..3a8cdabe5 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -833,3 +833,17 @@ def test_get_rdp_console(self): self.cs.servers.get_rdp_console(s, 'fake') self.assert_called('POST', '/servers/1234/remote-consoles') + + +class ServersV28Test(ServersV26Test): + def setUp(self): + super(ServersV28Test, self).setUp() + self.cs.api_version = api_versions.APIVersion("2.8") + + def test_get_mks_console(self): + s = self.cs.servers.get(1234) + s.get_mks_console() + self.assert_called('POST', '/servers/1234/remote-consoles') + + self.cs.servers.get_mks_console(s) + self.assert_called('POST', '/servers/1234/remote-consoles') diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index 43e316cab..24599235c 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -95,6 +95,13 @@ def get_serial_console(self, console_type): """ return self.manager.get_serial_console(self, console_type) + def get_mks_console(self): + """ + Get mks console for a Server. + + """ + return self.manager.get_mks_console(self) + def get_password(self, private_key=None): """ Get password for a Server. @@ -667,7 +674,7 @@ def get_vnc_console(self, server, console_type): """ Get a vnc console for an instance - :param server: The :class:`Server` (or its ID) to add an IP to. + :param server: The :class:`Server` (or its ID) to get console for. :param console_type: Type of vnc console to get ('novnc' or 'xvpvnc') """ @@ -679,7 +686,7 @@ def get_spice_console(self, server, console_type): """ Get a spice console for an instance - :param server: The :class:`Server` (or its ID) to add an IP to. + :param server: The :class:`Server` (or its ID) to get console for. :param console_type: Type of spice console to get ('spice-html5') """ @@ -691,7 +698,7 @@ def get_rdp_console(self, server, console_type): """ Get a rdp console for an instance - :param server: The :class:`Server` (or its ID) to add an IP to. + :param server: The :class:`Server` (or its ID) to get console for. :param console_type: Type of rdp console to get ('rdp-html5') """ @@ -703,7 +710,7 @@ def get_serial_console(self, server, console_type): """ Get a serial console for an instance - :param server: The :class:`Server` (or its ID) to add an IP to. + :param server: The :class:`Server` (or its ID) to get console for. :param console_type: Type of serial console to get ('serial') """ @@ -715,7 +722,7 @@ def get_vnc_console(self, server, console_type): """ Get a vnc console for an instance - :param server: The :class:`Server` (or its ID) to add an IP to. + :param server: The :class:`Server` (or its ID) to get console for. :param console_type: Type of vnc console to get ('novnc' or 'xvpvnc') """ @@ -727,7 +734,7 @@ def get_spice_console(self, server, console_type): """ Get a spice console for an instance - :param server: The :class:`Server` (or its ID) to add an IP to. + :param server: The :class:`Server` (or its ID) to get console for. :param console_type: Type of spice console to get ('spice-html5') """ @@ -739,7 +746,7 @@ def get_rdp_console(self, server, console_type): """ Get a rdp console for an instance - :param server: The :class:`Server` (or its ID) to add an IP to. + :param server: The :class:`Server` (or its ID) to get console for. :param console_type: Type of rdp console to get ('rdp-html5') """ @@ -751,13 +758,24 @@ def get_serial_console(self, server, console_type): """ Get a serial console for an instance - :param server: The :class:`Server` (or its ID) to add an IP to. + :param server: The :class:`Server` (or its ID) to get console for. :param console_type: Type of serial console to get ('serial') """ return self._console(server, {'protocol': 'serial', 'type': console_type})[1] + @api_versions.wraps('2.8') + def get_mks_console(self, server): + """ + Get a mks console for an instance + + :param server: The :class:`Server` (or its ID) to get console for. + """ + + return self._console(server, + {'protocol': 'mks', 'type': 'webmks'})[1] + def get_password(self, server, private_key=None): """ Get admin password of an instance diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 3513daed3..3de786efe 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -2351,6 +2351,17 @@ def console_dict_accessor(cs, data): return data['remote_console'] +class Console(object): + def __init__(self, console_dict): + self.type = console_dict['type'] + self.url = console_dict['url'] + + +def print_console(cs, data): + utils.print_list([Console(console_dict_accessor(cs, data))], + ['Type', 'Url']) + + @cliutils.arg('server', metavar='', help=_('Name or ID of server.')) @cliutils.arg( 'console_type', @@ -2361,13 +2372,7 @@ def do_get_vnc_console(cs, args): server = _find_server(cs, args.server) data = server.get_vnc_console(args.console_type) - class VNCConsole(object): - def __init__(self, console_dict): - self.type = console_dict['type'] - self.url = console_dict['url'] - - utils.print_list([VNCConsole(console_dict_accessor(cs, data))], - ['Type', 'Url']) + print_console(cs, data) @cliutils.arg('server', metavar='', help=_('Name or ID of server.')) @@ -2380,13 +2385,7 @@ def do_get_spice_console(cs, args): server = _find_server(cs, args.server) data = server.get_spice_console(args.console_type) - class SPICEConsole(object): - def __init__(self, console_dict): - self.type = console_dict['type'] - self.url = console_dict['url'] - - utils.print_list([SPICEConsole(console_dict_accessor(cs, data))], - ['Type', 'Url']) + print_console(cs, data) @cliutils.arg('server', metavar='', help=_('Name or ID of server.')) @@ -2399,13 +2398,7 @@ def do_get_rdp_console(cs, args): server = _find_server(cs, args.server) data = server.get_rdp_console(args.console_type) - class RDPConsole(object): - def __init__(self, console_dict): - self.type = console_dict['type'] - self.url = console_dict['url'] - - utils.print_list([RDPConsole(console_dict_accessor(cs, data))], - ['Type', 'Url']) + print_console(cs, data) @cliutils.arg('server', metavar='', help=_('Name or ID of server.')) @@ -2422,13 +2415,17 @@ def do_get_serial_console(cs, args): server = _find_server(cs, args.server) data = server.get_serial_console(args.console_type) - class SerialConsole(object): - def __init__(self, console_dict): - self.type = console_dict['type'] - self.url = console_dict['url'] + print_console(cs, data) - utils.print_list([SerialConsole(console_dict_accessor(cs, data))], - ['Type', 'Url']) + +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@api_versions.wraps('2.8') +def do_get_mks_console(cs, args): + """Get a serial console to a server.""" + server = _find_server(cs, args.server) + data = server.get_mks_console() + + print_console(cs, data) @cliutils.arg('server', metavar='', help=_('Name or ID of server.')) From 0a535e32871bf9579bca3a8afc53c930058c23f3 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Mon, 30 Nov 2015 17:29:33 +0200 Subject: [PATCH 0906/1705] [microversions] Increase max version to 2.9 2.9 - Add a new locked attribute to the detailed view of servers. locked will be true if anyone is currently holding a lock on the server, false otherwise. This microversion doesn't require any change in novaclient's code. Just added functional tests and changed version Change-Id: I7f33757e6f03f172e5a13ade0aa5e8d3a10dbf01 --- novaclient/__init__.py | 2 +- .../tests/functional/v2/test_servers.py | 30 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 14673cf8c..94ff3e494 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.8") +API_MAX_VERSION = api_versions.APIVersion("2.9") diff --git a/novaclient/tests/functional/v2/test_servers.py b/novaclient/tests/functional/v2/test_servers.py index c3b2fdcd2..9cb034b47 100644 --- a/novaclient/tests/functional/v2/test_servers.py +++ b/novaclient/tests/functional/v2/test_servers.py @@ -10,6 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +import uuid + +from novaclient.tests.functional import base from novaclient.tests.functional.v2.legacy import test_servers @@ -25,3 +28,30 @@ class TestServersListNovaClient(test_servers.TestServersListNovaClient): """ COMPUTE_API_VERSION = "2.latest" + + +class TestServerLockV29(base.ClientTestBase): + + COMPUTE_API_VERSION = "2.9" + + def _show_server_and_check_lock_attr(self, server, value): + output = self.nova("show %s" % server.id) + self.assertEqual(str(value), + self._get_value_from_the_table(output, "locked")) + + def test_attribute_presented(self): + # prepare + name = str(uuid.uuid4()) + network = self.client.networks.list()[0] + server = self.client.servers.create( + name, self.image, self.flavor, nics=[{"net-id": network.id}]) + self.addCleanup(server.delete) + + # testing + self._show_server_and_check_lock_attr(server, False) + + self.nova("lock %s" % server.id) + self._show_server_and_check_lock_attr(server, True) + + self.nova("unlock %s" % server.id) + self._show_server_and_check_lock_attr(server, False) From e767f26ccead2dd897de84456afb58b5b40b1501 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Pitucha?= Date: Mon, 7 Dec 2015 15:25:09 +1100 Subject: [PATCH 0907/1705] Fix multiline string with missing space Change-Id: Ie59447b92f20cc4fb931ad8311f46bc1b1158abb --- novaclient/v2/shell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 5296084de..daed9560b 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -4017,7 +4017,7 @@ def do_ssh(cs, args): 'network': args.network, 'address_type': address_type, 'pretty_version': pretty_version, 'server': args.server}) elif len(matching_addresses) > 1: - msg = _("More than one %(pretty_version)s %(address_type)s address" + msg = _("More than one %(pretty_version)s %(address_type)s address " "found.") raise exceptions.CommandError(msg % {'pretty_version': pretty_version, 'address_type': address_type}) From 573bdaa290d87dfbf8d517395e44fd9737fdfb18 Mon Sep 17 00:00:00 2001 From: Ghanshyam Date: Mon, 7 Dec 2015 14:17:56 +0900 Subject: [PATCH 0908/1705] Fix H404/405 violations in novaclient/tests/* There is a lot of H404/405 violations in novaclient, and it is better to fix those to have a better doc string for class/methods. This patch fixes these violations for files under novaclient/tests/ folder. As there are lot of violations and cannot be fixed in single patches, So separating those in multiple patches for easy review. Partial-Bug: #1521899 Change-Id: I6e712ece14c745013bfba0bee9d77e7875dd2263 --- novaclient/tests/functional/base.py | 3 +- .../functional/v2/legacy/test_keypairs.py | 3 +- .../tests/functional/v2/legacy/test_quotas.py | 3 +- .../v2/legacy/test_readonly_nova.py | 3 +- .../functional/v2/legacy/test_servers.py | 6 +-- .../tests/functional/v2/test_keypairs.py | 3 +- novaclient/tests/functional/v2/test_quotas.py | 3 +- .../tests/functional/v2/test_readonly_nova.py | 3 +- .../tests/functional/v2/test_servers.py | 6 +-- novaclient/tests/unit/fakes.py | 47 +++++++++++++++++-- novaclient/tests/unit/utils.py | 6 +-- novaclient/tests/unit/v2/test_shell.py | 15 +++--- 12 files changed, 64 insertions(+), 37 deletions(-) diff --git a/novaclient/tests/functional/base.py b/novaclient/tests/functional/base.py index 86d652322..27ce9ef08 100644 --- a/novaclient/tests/functional/base.py +++ b/novaclient/tests/functional/base.py @@ -59,7 +59,8 @@ class NoCloudConfigException(Exception): class ClientTestBase(testtools.TestCase): - """ + """Base test class for read only python-novaclient commands. + This is a first pass at a simple read only python-novaclient test. This only exercises client commands that are read only. diff --git a/novaclient/tests/functional/v2/legacy/test_keypairs.py b/novaclient/tests/functional/v2/legacy/test_keypairs.py index 3b7efefcf..9828df02e 100644 --- a/novaclient/tests/functional/v2/legacy/test_keypairs.py +++ b/novaclient/tests/functional/v2/legacy/test_keypairs.py @@ -20,8 +20,7 @@ class TestKeypairsNovaClient(base.ClientTestBase): - """Keypairs functional tests. - """ + """Keypairs functional tests.""" COMPUTE_API_VERSION = "2.1" diff --git a/novaclient/tests/functional/v2/legacy/test_quotas.py b/novaclient/tests/functional/v2/legacy/test_quotas.py index d0ee710a2..1cd8ac889 100644 --- a/novaclient/tests/functional/v2/legacy/test_quotas.py +++ b/novaclient/tests/functional/v2/legacy/test_quotas.py @@ -14,8 +14,7 @@ class TestQuotasNovaClient(base.ClientTestBase): - """Nova quotas functional tests. - """ + """Nova quotas functional tests.""" COMPUTE_API_VERSION = "2.1" diff --git a/novaclient/tests/functional/v2/legacy/test_readonly_nova.py b/novaclient/tests/functional/v2/legacy/test_readonly_nova.py index 7abbeee3f..47e5844c3 100644 --- a/novaclient/tests/functional/v2/legacy/test_readonly_nova.py +++ b/novaclient/tests/functional/v2/legacy/test_readonly_nova.py @@ -18,8 +18,7 @@ class SimpleReadOnlyNovaClientTest(base.ClientTestBase): - """ - read only functional python-novaclient tests. + """Read only functional python-novaclient tests. This only exercises client commands that are read only. """ diff --git a/novaclient/tests/functional/v2/legacy/test_servers.py b/novaclient/tests/functional/v2/legacy/test_servers.py index 0e968562a..746d556c8 100644 --- a/novaclient/tests/functional/v2/legacy/test_servers.py +++ b/novaclient/tests/functional/v2/legacy/test_servers.py @@ -17,8 +17,7 @@ class TestServersBootNovaClient(base.ClientTestBase): - """Servers boot functional tests. - """ + """Servers boot functional tests.""" COMPUTE_API_VERSION = "2.1" @@ -58,8 +57,7 @@ def test_boot_server_with_legacy_bdm_volume_id_only(self): class TestServersListNovaClient(base.ClientTestBase): - """Servers list functional tests. - """ + """Servers list functional tests.""" COMPUTE_API_VERSION = "2.1" diff --git a/novaclient/tests/functional/v2/test_keypairs.py b/novaclient/tests/functional/v2/test_keypairs.py index 35768de7b..c02c2cac5 100644 --- a/novaclient/tests/functional/v2/test_keypairs.py +++ b/novaclient/tests/functional/v2/test_keypairs.py @@ -15,8 +15,7 @@ class TestKeypairsNovaClientV22(test_keypairs.TestKeypairsNovaClient): - """Keypairs functional tests for v2.2 nova-api microversion. - """ + """Keypairs functional tests for v2.2 nova-api microversion.""" COMPUTE_API_VERSION = "2.2" diff --git a/novaclient/tests/functional/v2/test_quotas.py b/novaclient/tests/functional/v2/test_quotas.py index 14b5bdfef..5d836e2c4 100644 --- a/novaclient/tests/functional/v2/test_quotas.py +++ b/novaclient/tests/functional/v2/test_quotas.py @@ -14,8 +14,7 @@ class TestQuotasNovaClient(test_quotas.TestQuotasNovaClient): - """Nova quotas functional tests. - """ + """Nova quotas functional tests.""" COMPUTE_API_VERSION = "2.latest" diff --git a/novaclient/tests/functional/v2/test_readonly_nova.py b/novaclient/tests/functional/v2/test_readonly_nova.py index cfe91635a..7ef948eea 100644 --- a/novaclient/tests/functional/v2/test_readonly_nova.py +++ b/novaclient/tests/functional/v2/test_readonly_nova.py @@ -16,8 +16,7 @@ class SimpleReadOnlyNovaClientTest( test_readonly_nova.SimpleReadOnlyNovaClientTest): - """ - read only functional python-novaclient tests. + """Read only functional python-novaclient tests. This only exercises client commands that are read only. """ diff --git a/novaclient/tests/functional/v2/test_servers.py b/novaclient/tests/functional/v2/test_servers.py index 9cb034b47..590e543bf 100644 --- a/novaclient/tests/functional/v2/test_servers.py +++ b/novaclient/tests/functional/v2/test_servers.py @@ -17,15 +17,13 @@ class TestServersBootNovaClient(test_servers.TestServersBootNovaClient): - """Servers boot functional tests. - """ + """Servers boot functional tests.""" COMPUTE_API_VERSION = "2.latest" class TestServersListNovaClient(test_servers.TestServersListNovaClient): - """Servers list functional tests. - """ + """Servers list functional tests.""" COMPUTE_API_VERSION = "2.latest" diff --git a/novaclient/tests/unit/fakes.py b/novaclient/tests/unit/fakes.py index ef6b59b52..78e10f04f 100644 --- a/novaclient/tests/unit/fakes.py +++ b/novaclient/tests/unit/fakes.py @@ -36,8 +36,40 @@ def assert_has_keys(dict, required=[], optional=[]): class FakeClient(object): def assert_called(self, method, url, body=None, pos=-1): - """ - Assert than an API method was just called. + """Assert than an HTTP method was called at given order/position. + + :param method: HTTP method name which is expected to be called + :param url: Expected request url to be called with given method + :param body: Expected request body to be called with given method + and url. Default is None. + :param pos: Order of the expected method call. If multiple methods + calls are made in single API request, then, order of each + method call can be checked by passing expected order to + this arg. + Default is -1 which means most recent call. + + Usage:: + 1. self.run_command('flavor-list --extra-specs') + self.assert_called('GET', '/flavors/aa1/os-extra_specs') + + 2. self.run_command(["boot", "--image", "1", + "--flavor", "512 MB Server", + "--max-count", "3", "server"]) + self.assert_called('GET', '/images/1', pos=0) + self.assert_called('GET', '/flavors/512 MB Server', pos=1) + self.assert_called('GET', '/flavors?is_public=None', pos=2) + self.assert_called('GET', '/flavors/2', pos=3) + self.assert_called( + 'POST', '/servers', + { + 'server': { + 'flavorRef': '2', + 'name': 'server', + 'imageRef': '1', + 'min_count': 1, + 'max_count': 3, + } + }, pos=4) """ expected = (method, url) called = self.client.callstack[pos][0:2] @@ -54,8 +86,15 @@ def assert_called(self, method, url, body=None, pos=-1): (self.client.callstack[pos][2], body)) def assert_called_anytime(self, method, url, body=None): - """ - Assert than an API method was called anytime in the test. + """Assert than an HTTP method was called anytime in the test. + + :param method: HTTP method name which is expected to be called + :param url: Expected request url to be called with given method + :param body: Expected request body to be called with given method + and url. Default is None. + Usage:: + self.run_command('flavor-list --extra-specs') + self.assert_called_anytime('GET', '/flavors/detail') """ expected = (method, url) diff --git a/novaclient/tests/unit/utils.py b/novaclient/tests/unit/utils.py index 6aaf0ed20..4c5ec4932 100644 --- a/novaclient/tests/unit/utils.py +++ b/novaclient/tests/unit/utils.py @@ -104,9 +104,9 @@ def assert_called(self, method, path, body=None): class TestResponse(requests.Response): - """ - Class used to wrap requests.Response and provide some - convenience to initialize with a dict + """Class used to wrap requests.Response. + + Provide some convenience to initialize with a dict. """ def __init__(self, data): diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 7a6a8e2cc..58d05edae 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2613,16 +2613,14 @@ def test_with_non_unique_name(self): class GetFirstEndpointTest(utils.TestCase): def test_only_one_endpoint(self): - """If there is only one endpoint, it is returned.""" + # If there is only one endpoint, it is returned. endpoint = {"url": "test"} result = novaclient.v2.shell._get_first_endpoint([endpoint], "XYZ") self.assertEqual(endpoint, result) def test_multiple_endpoints(self): - """If there are multiple endpoints, the first one of the appropriate - region is returned. - - """ + # If there are multiple endpoints, the first one of the appropriate + # region is returned. endpoints = [ {"region": "XYZ"}, {"region": "ORD", "number": 1}, @@ -2632,10 +2630,9 @@ def test_multiple_endpoints(self): self.assertEqual(endpoints[1], result) def test_multiple_endpoints_but_none_suitable(self): - """If there are multiple endpoints but none of them are suitable, an - exception is raised. + # If there are multiple endpoints but none of them are suitable, an + # exception is raised. - """ endpoints = [ {"region": "XYZ"}, {"region": "PQR"}, @@ -2646,7 +2643,7 @@ def test_multiple_endpoints_but_none_suitable(self): endpoints, "ORD") def test_no_endpoints(self): - """If there are no endpoints available, an exception is raised.""" + # If there are no endpoints available, an exception is raised. self.assertRaises(LookupError, novaclient.v2.shell._get_first_endpoint, [], "ORD") From a37c4328a16a209803244e212fc99a74fad7ca4e Mon Sep 17 00:00:00 2001 From: Ghanshyam Date: Mon, 7 Dec 2015 17:35:19 +0900 Subject: [PATCH 0909/1705] Fix H404/405 violations in novaclient/v2/[a-f] There is a lot of H404/405 violations in novaclient, and it is better to fix those to have a better doc string for class/methods. This patch fixes these violations for [a-f] files under novaclient/v2 folder. As there are lot of violations and cannot be fixed in single patches, So separating those in multiple patches for easy review. Change-Id: Ibe5afef349d1be6d70903e06bee402ca30738ea1 Partial-Bug: #1521899 --- novaclient/v2/availability_zones.py | 14 +++----- novaclient/v2/certs.py | 12 ++----- novaclient/v2/client.py | 9 +++-- novaclient/v2/cloudpipe.py | 10 +++--- novaclient/v2/fixed_ips.py | 7 ++-- novaclient/v2/flavor_access.py | 4 +-- novaclient/v2/flavors.py | 55 +++++++++++------------------ novaclient/v2/floating_ip_pools.py | 4 +-- novaclient/v2/floating_ips.py | 19 +++------- novaclient/v2/floating_ips_bulk.py | 12 ++----- novaclient/v2/fping.py | 18 ++++------ 11 files changed, 56 insertions(+), 108 deletions(-) diff --git a/novaclient/v2/availability_zones.py b/novaclient/v2/availability_zones.py index bf5903785..41ee1d1e8 100644 --- a/novaclient/v2/availability_zones.py +++ b/novaclient/v2/availability_zones.py @@ -22,9 +22,7 @@ class AvailabilityZone(base.Resource): - """ - An availability zone object. - """ + """An availability zone object.""" NAME_ATTR = 'display_name' def __repr__(self): @@ -32,17 +30,15 @@ def __repr__(self): class AvailabilityZoneManager(base.ManagerWithFind): - """ - Manage :class:`AvailabilityZone` resources. - """ + """Manage :class:`AvailabilityZone` resources.""" resource_class = AvailabilityZone return_parameter_name = "availabilityZoneInfo" def list(self, detailed=True): - """ - Get a list of all availability zones. + """Get a list of all availability zones. - :rtype: list of :class:`AvailabilityZone` + :param detailed: If True, list availability zones with details. + :returns: list of :class:`AvailabilityZone` """ if detailed is True: return self._list("/os-availability-zone/detail", diff --git a/novaclient/v2/certs.py b/novaclient/v2/certs.py index 2c3006c6f..0ed1e1476 100644 --- a/novaclient/v2/certs.py +++ b/novaclient/v2/certs.py @@ -30,19 +30,13 @@ def __repr__(self): class CertificateManager(base.Manager): - """ - Manage :class:`Certificate` resources. - """ + """Manage :class:`Certificate` resources.""" resource_class = Certificate def create(self): - """ - Create a x509 certificate for a user in tenant. - """ + """Create a x509 certificate for a user in tenant.""" return self._create('/os-certificates', {}, 'certificate') def get(self): - """ - Get root certificate. - """ + """Get root certificate.""" return self._get("/os-certificates/root", 'certificate') diff --git a/novaclient/v2/client.py b/novaclient/v2/client.py index 971506149..a3ec98fe4 100644 --- a/novaclient/v2/client.py +++ b/novaclient/v2/client.py @@ -52,8 +52,7 @@ class Client(object): - """ - Top-level object to access the OpenStack Compute API. + """Top-level object to access the OpenStack Compute API. .. warning:: All scripts and projects should not initialize this class directly. It should be done via `novaclient.client.Client` interface. @@ -70,7 +69,8 @@ def __init__(self, username=None, api_key=None, project_id=None, cacert=None, tenant_id=None, user_id=None, connection_pool=False, session=None, auth=None, api_version=None, direct_use=True, **kwargs): - """ + """Initialization of Client object. + :param str username: Username :param str api_key: API Key :param str project_id: Project ID @@ -226,8 +226,7 @@ def reset_timings(self): @client._original_only def authenticate(self): - """ - Authenticate against the server. + """Authenticate against the server. Normally this is called automatically when you first access the API, but you can call this method to force authentication right now. diff --git a/novaclient/v2/cloudpipe.py b/novaclient/v2/cloudpipe.py index c0926ed40..809914527 100644 --- a/novaclient/v2/cloudpipe.py +++ b/novaclient/v2/cloudpipe.py @@ -32,8 +32,7 @@ class CloudpipeManager(base.ManagerWithFind): resource_class = Cloudpipe def create(self, project): - """ - Launch a cloudpipe instance. + """Launch a cloudpipe instance. :param project: UUID of the project (tenant) for the cloudpipe """ @@ -42,13 +41,12 @@ def create(self, project): return_raw=True) def list(self): - """ - Get a list of cloudpipe instances. - """ + """Get a list of cloudpipe instances.""" return self._list('/os-cloudpipe', 'cloudpipes') def update(self, address, port): - """ + """Configure cloudpipe parameters for the project. + Update VPN address and port for all networks associated with the project defined by authentication diff --git a/novaclient/v2/fixed_ips.py b/novaclient/v2/fixed_ips.py index 4e7c0e951..7d1377ac0 100644 --- a/novaclient/v2/fixed_ips.py +++ b/novaclient/v2/fixed_ips.py @@ -29,8 +29,7 @@ class FixedIPsManager(base.Manager): resource_class = FixedIP def get(self, fixed_ip): - """ - Show information for a Fixed IP + """Show information for a Fixed IP. :param fixed_ip: Fixed IP address to get info for """ @@ -38,7 +37,7 @@ def get(self, fixed_ip): "fixed_ip") def reserve(self, fixed_ip): - """Reserve a Fixed IP + """Reserve a Fixed IP. :param fixed_ip: Fixed IP address to reserve """ @@ -47,7 +46,7 @@ def reserve(self, fixed_ip): body=body) def unreserve(self, fixed_ip): - """Unreserve a Fixed IP + """Unreserve a Fixed IP. :param fixed_ip: Fixed IP address to unreserve """ diff --git a/novaclient/v2/flavor_access.py b/novaclient/v2/flavor_access.py index ca78bdff7..4e6fcd4c2 100644 --- a/novaclient/v2/flavor_access.py +++ b/novaclient/v2/flavor_access.py @@ -26,9 +26,7 @@ def __repr__(self): class FlavorAccessManager(base.ManagerWithFind): - """ - Manage :class:`FlavorAccess` resources. - """ + """Manage :class:`FlavorAccess` resources.""" resource_class = FlavorAccess def list(self, **kwargs): diff --git a/novaclient/v2/flavors.py b/novaclient/v2/flavors.py index 86748f03f..3ce545802 100644 --- a/novaclient/v2/flavors.py +++ b/novaclient/v2/flavors.py @@ -26,9 +26,7 @@ class Flavor(base.Resource): - """ - A flavor is an available hardware configuration for a server. - """ + """A flavor is an available hardware configuration for a server.""" HUMAN_ID = True def __repr__(self): @@ -36,29 +34,22 @@ def __repr__(self): @property def ephemeral(self): - """ - Provide a user-friendly accessor to OS-FLV-EXT-DATA:ephemeral - """ + """Provide a user-friendly accessor to OS-FLV-EXT-DATA:ephemeral.""" return self._info.get("OS-FLV-EXT-DATA:ephemeral", 'N/A') @property def is_public(self): - """ - Provide a user-friendly accessor to os-flavor-access:is_public - """ + """Provide a user-friendly accessor to os-flavor-access:is_public.""" return self._info.get("os-flavor-access:is_public", 'N/A') def get_keys(self): - """ - Get extra specs from a flavor. - """ + """Get extra specs from a flavor.""" _resp, body = self.manager.api.client.get( "/flavors/%s/os-extra_specs" % base.getid(self)) return body["extra_specs"] def set_keys(self, metadata): - """ - Set extra specs on a flavor. + """Set extra specs on a flavor. :param metadata: A dict of key/value pairs to be set """ @@ -70,8 +61,7 @@ def set_keys(self, metadata): "extra_specs", return_raw=True) def unset_keys(self, keys): - """ - Unset extra specs on a flavor. + """Unset extra specs on a flavor. :param keys: A list of keys to be unset """ @@ -80,30 +70,30 @@ def unset_keys(self, keys): "/flavors/%s/os-extra_specs/%s" % (base.getid(self), k)) def delete(self): - """ - Delete this flavor. - """ + """Delete this flavor.""" self.manager.delete(self) class FlavorManager(base.ManagerWithFind): - """ - Manage :class:`Flavor` resources. - """ + """Manage :class:`Flavor` resources.""" resource_class = Flavor is_alphanum_id_allowed = True def list(self, detailed=True, is_public=True, marker=None, limit=None, sort_key=None, sort_dir=None): - """ - Get a list of all flavors. + """Get a list of all flavors. - :rtype: list of :class:`Flavor`. - :param limit: maximum number of flavors to return (optional). + :param detailed: Whether flavor needs to be return with details + (optional). + :param is_public: Filter flavors with provided access type (optional). + None means give all flavors and only admin has query + access to all flavor types. :param marker: Begin returning flavors that appear later in the flavor list than that represented by this flavor id (optional). + :param limit: maximum number of flavors to return (optional). :param sort_key: Flavors list sort key (optional). :param sort_dir: Flavors list sort direction (optional). + :returns: list of :class:`Flavor`. """ qparams = {} # is_public is ternary - None means give all flavors. @@ -129,17 +119,15 @@ def list(self, detailed=True, is_public=True, marker=None, limit=None, return self._list("/flavors%s%s" % (detail, query_string), "flavors") def get(self, flavor): - """ - Get a specific flavor. + """Get a specific flavor. :param flavor: The ID of the :class:`Flavor` to get. - :rtype: :class:`Flavor` + :returns: :class:`Flavor` """ return self._get("/flavors/%s" % base.getid(flavor), "flavor") def delete(self, flavor): - """ - Delete a specific flavor. + """Delete a specific flavor. :param flavor: The ID of the :class:`Flavor` to get. """ @@ -163,8 +151,7 @@ def _build_body(self, name, ram, vcpus, disk, id, swap, def create(self, name, ram, vcpus, disk, flavorid="auto", ephemeral=0, swap=0, rxtx_factor=1.0, is_public=True): - """ - Create a flavor. + """Create a flavor. :param name: Descriptive name of the flavor :param ram: Memory in MB for the flavor @@ -175,7 +162,7 @@ def create(self, name, ram, vcpus, disk, flavorid="auto", flavor in cases where you cannot simply pass ``None``. :param swap: Swap space in MB :param rxtx_factor: RX/TX factor - :rtype: :class:`Flavor` + :returns: :class:`Flavor` """ try: diff --git a/novaclient/v2/floating_ip_pools.py b/novaclient/v2/floating_ip_pools.py index 7666bd57f..9ea94a273 100644 --- a/novaclient/v2/floating_ip_pools.py +++ b/novaclient/v2/floating_ip_pools.py @@ -26,7 +26,5 @@ class FloatingIPPoolManager(base.ManagerWithFind): resource_class = FloatingIPPool def list(self): - """ - Retrieve a list of all floating ip pools. - """ + """Retrieve a list of all floating ip pools.""" return self._list('/os-floating-ip-pools', 'floating_ip_pools') diff --git a/novaclient/v2/floating_ips.py b/novaclient/v2/floating_ips.py index 0eb75b79b..e3bc525bb 100644 --- a/novaclient/v2/floating_ips.py +++ b/novaclient/v2/floating_ips.py @@ -19,9 +19,7 @@ class FloatingIP(base.Resource): def delete(self): - """ - Delete this floating IP - """ + """Delete this floating IP""" self.manager.delete(self) @@ -29,28 +27,21 @@ class FloatingIPManager(base.ManagerWithFind): resource_class = FloatingIP def list(self): - """ - List floating IPs - """ + """List floating IPs""" return self._list("/os-floating-ips", "floating_ips") def create(self, pool=None): - """ - Create (allocate) a floating IP for a tenant - """ + """Create (allocate) a floating IP for a tenant""" return self._create("/os-floating-ips", {'pool': pool}, "floating_ip") def delete(self, floating_ip): - """ - Delete (deallocate) a floating IP for a tenant + """Delete (deallocate) a floating IP for a tenant :param floating_ip: The floating IP address to delete. """ self._delete("/os-floating-ips/%s" % base.getid(floating_ip)) def get(self, floating_ip): - """ - Retrieve a floating IP - """ + """Retrieve a floating IP""" return self._get("/os-floating-ips/%s" % base.getid(floating_ip), "floating_ip") diff --git a/novaclient/v2/floating_ips_bulk.py b/novaclient/v2/floating_ips_bulk.py index b0fdebf62..4249af60d 100644 --- a/novaclient/v2/floating_ips_bulk.py +++ b/novaclient/v2/floating_ips_bulk.py @@ -29,9 +29,7 @@ class FloatingIPBulkManager(base.ManagerWithFind): resource_class = FloatingIPRange def list(self, host=None): - """ - List all floating IPs - """ + """List all floating IPs.""" if host is None: return self._list('/os-floating-ips-bulk', 'floating_ip_info', @@ -42,9 +40,7 @@ def list(self, host=None): obj_class=floating_ips.FloatingIP) def create(self, ip_range, pool=None, interface=None): - """ - Create floating IPs by range - """ + """Create floating IPs by range.""" body = {"floating_ips_bulk_create": {'ip_range': ip_range}} if pool is not None: body['floating_ips_bulk_create']['pool'] = pool @@ -55,8 +51,6 @@ def create(self, ip_range, pool=None, interface=None): 'floating_ips_bulk_create') def delete(self, ip_range): - """ - Delete floating IPs by range - """ + """Delete floating IPs by range.""" body = {"ip_range": ip_range} return self._update('/os-floating-ips-bulk/delete', body) diff --git a/novaclient/v2/fping.py b/novaclient/v2/fping.py index 5e8b74bb9..31c7acb96 100644 --- a/novaclient/v2/fping.py +++ b/novaclient/v2/fping.py @@ -21,9 +21,7 @@ class Fping(base.Resource): - """ - A server to fping. - """ + """A server to fping.""" HUMAN_ID = True def __repr__(self): @@ -31,16 +29,13 @@ def __repr__(self): class FpingManager(base.ManagerWithFind): - """ - Manage :class:`Fping` resources. - """ + """Manage :class:`Fping` resources.""" resource_class = Fping def list(self, all_tenants=False, include=[], exclude=[]): - """ - Fping all servers. + """Fping all servers. - :rtype: list of :class:`Fping`. + :returns: list of :class:`Fping`. """ params = [] if all_tenants: @@ -55,10 +50,9 @@ def list(self, all_tenants=False, include=[], exclude=[]): return self._list(uri, "servers") def get(self, server): - """ - Fping a specific server. + """Fping a specific server. :param server: ID of the server to fping. - :rtype: :class:`Fping` + :returns: :class:`Fping` """ return self._get("/os-fping/%s" % base.getid(server), "server") From 6a98b786b78e2e5ba9a70d021427ed8da49ed615 Mon Sep 17 00:00:00 2001 From: Javeme Date: Mon, 7 Dec 2015 19:49:43 +0800 Subject: [PATCH 0910/1705] encode the url parameters Change-Id: I880c0be5edbba8de560bac155126b23755635eac --- novaclient/v2/fping.py | 9 +++++---- novaclient/v2/services.py | 8 +++++--- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/novaclient/v2/fping.py b/novaclient/v2/fping.py index 5e8b74bb9..c6ed2487e 100644 --- a/novaclient/v2/fping.py +++ b/novaclient/v2/fping.py @@ -16,6 +16,7 @@ """ Fping interface. """ +from six.moves import urllib from novaclient import base @@ -44,14 +45,14 @@ def list(self, all_tenants=False, include=[], exclude=[]): """ params = [] if all_tenants: - params.append("all_tenants=1") + params.append(("all_tenants", 1)) if include: - params.append("include=%s" % ",".join(include)) + params.append(("include", ",".join(include))) elif exclude: - params.append("exclude=%s" % ",".join(exclude)) + params.append(("exclude", ",".join(exclude))) uri = "/os-fping" if params: - uri = "%s?%s" % (uri, "&".join(params)) + uri = "%s?%s" % (uri, urllib.parse.urlencode(params)) return self._list(uri, "servers") def get(self, server): diff --git a/novaclient/v2/services.py b/novaclient/v2/services.py index fcf800938..4358be1c3 100644 --- a/novaclient/v2/services.py +++ b/novaclient/v2/services.py @@ -16,6 +16,8 @@ """ service interface """ +from six.moves import urllib + from novaclient import api_versions from novaclient import base @@ -42,11 +44,11 @@ def list(self, host=None, binary=None): url = "/os-services" filters = [] if host: - filters.append("host=%s" % host) + filters.append(("host", host)) if binary: - filters.append("binary=%s" % binary) + filters.append(("binary", binary)) if filters: - url = "%s?%s" % (url, "&".join(filters)) + url = "%s?%s" % (url, urllib.parse.urlencode(filters)) return self._list(url, "services") @api_versions.wraps("2.0", "2.10") From 96a7e1ddc477420846cb7e192ef7fc04a49a6a8c Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Mon, 7 Dec 2015 22:11:21 +0200 Subject: [PATCH 0911/1705] [microversions] update test_versions with implemented versions Change-Id: Ifc401e00fc6436ac85ade3fa119ef9a47607a5ec --- novaclient/tests/unit/v2/test_shell.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 7a6a8e2cc..0518e8191 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2533,10 +2533,8 @@ def test_versions(self): 1, # Same as version 2.0 3, # Not implemented when test added, should not apply to adds. 5, # Not implemented when test added, should not apply to adds. - 6, # Not implemented when test added, should not apply to adds. - 7, # Not implemented when test added, should not apply to adds. - 8, # Not implemented when test added, should not apply to adds. - 9, # Not implemented when test added, should not apply to adds. + 7, # doesn't require any changes in novaclient + 9, # doesn't require any changes in novaclient 10, # Not implemented when test added, should not apply to adds. ]) versions_supported = set(range(0, From 65658ed6fca868430a4d9e074e4345c7b7f4bfc9 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Mon, 30 Nov 2015 19:44:11 +0200 Subject: [PATCH 0912/1705] [microversions] Add support for 2.10 2.10 - Added user_id parameter to os-keypairs plugin, as well as a new property in the request body, for the create operation. Administrators will be able to list, get details and delete keypairs owned by users other than themselves and to create new keypairs on behalf of their users. Change-Id: I13ca3f8a4dd9cf11bec79966bb8a2ab48847be22 --- novaclient/__init__.py | 2 +- .../tests/functional/v2/test_keypairs.py | 77 +++++++++++++++++++ novaclient/tests/unit/v2/test_shell.py | 1 - novaclient/v2/keypairs.py | 60 ++++++++++++++- novaclient/v2/shell.py | 58 +++++++++++++- 5 files changed, 194 insertions(+), 4 deletions(-) diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 94ff3e494..37c73086a 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.9") +API_MAX_VERSION = api_versions.APIVersion("2.10") diff --git a/novaclient/tests/functional/v2/test_keypairs.py b/novaclient/tests/functional/v2/test_keypairs.py index 35768de7b..171902f6f 100644 --- a/novaclient/tests/functional/v2/test_keypairs.py +++ b/novaclient/tests/functional/v2/test_keypairs.py @@ -10,6 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +import tempest_lib.cli.base + +from novaclient.tests.functional import base from novaclient.tests.functional.v2 import fake_crypto from novaclient.tests.functional.v2.legacy import test_keypairs @@ -42,3 +45,77 @@ def test_import_keypair_x509(self): keypair = self._test_import_keypair(fingerprint, key_type='x509', pub_key=pub_key_file) self.assertIn('x509', keypair) + + +class TestKeypairsNovaClientV210(base.ClientTestBase): + """Keypairs functional tests for v2.10 nova-api microversion. + """ + + COMPUTE_API_VERSION = "2.10" + + def setUp(self): + super(TestKeypairsNovaClientV210, self).setUp() + user_name = self.name_generate("v2.10") + password = "password" + user = self.cli_clients.keystone( + "user-create --name %(name)s --pass %(pass)s --tenant %(tenant)s" % + {"name": user_name, "pass": password, + "tenant": self.cli_clients.tenant_name}) + self.user_id = self._get_value_from_the_table(user, "id") + self.addCleanup(self.cli_clients.keystone, + "user-delete %s" % self.user_id) + self.cli_clients_2 = tempest_lib.cli.base.CLIClient( + username=user_name, + password=password, + tenant_name=self.cli_clients.tenant_name, + uri=self.cli_clients.uri, + cli_dir=self.cli_clients.cli_dir) + + def another_nova(self, action, flags='', params='', fail_ok=False, + endpoint_type='publicURL', merge_stderr=False): + flags += " --os-compute-api-version %s " % self.COMPUTE_API_VERSION + return self.cli_clients_2.nova(action, flags, params, fail_ok, + endpoint_type, merge_stderr) + + def test_create_and_list_keypair(self): + name = self.name_generate("v2_10") + self.nova("keypair-add %s --user %s" % (name, self.user_id)) + self.addCleanup(self.another_nova, "keypair-delete %s" % name) + output = self.nova("keypair-list") + self.assertRaises(ValueError, self._get_value_from_the_table, + output, name) + output_1 = self.another_nova("keypair-list") + output_2 = self.nova("keypair-list --user %s" % self.user_id) + self.assertEqual(output_1, output_2) + # it should be table with one key-pair + self.assertEqual(name, self._get_column_value_from_single_row_table( + output_1, "Name")) + + output_1 = self.another_nova("keypair-show %s " % name) + output_2 = self.nova("keypair-show --user %s %s" % (self.user_id, + name)) + self.assertEqual(output_1, output_2) + self.assertEqual(self.user_id, + self._get_value_from_the_table(output_1, "user_id")) + + def test_create_and_delete(self): + name = self.name_generate("v2_10") + + def cleanup(): + # We should check keypair existence and remove it from correct user + # if keypair is presented + o = self.another_nova("keypair-list") + if name in o: + self.another_nova("keypair-delete %s" % name) + + self.nova("keypair-add %s --user %s" % (name, self.user_id)) + self.addCleanup(cleanup) + output = self.another_nova("keypair-list") + self.assertEqual(name, self._get_column_value_from_single_row_table( + output, "Name")) + + self.nova("keypair-delete %s --user %s " % (name, self.user_id)) + output = self.another_nova("keypair-list") + self.assertRaises( + ValueError, + self._get_column_value_from_single_row_table, output, "Name") diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 0518e8191..36093ef7c 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2535,7 +2535,6 @@ def test_versions(self): 5, # Not implemented when test added, should not apply to adds. 7, # doesn't require any changes in novaclient 9, # doesn't require any changes in novaclient - 10, # Not implemented when test added, should not apply to adds. ]) versions_supported = set(range(0, novaclient.API_MAX_VERSION.ver_minor + 1)) diff --git a/novaclient/v2/keypairs.py b/novaclient/v2/keypairs.py index 021960517..70ab8da57 100644 --- a/novaclient/v2/keypairs.py +++ b/novaclient/v2/keypairs.py @@ -56,6 +56,7 @@ class KeypairManager(base.ManagerWithFind): keypair_prefix = "os-keypairs" is_alphanum_id_allowed = True + @api_versions.wraps("2.0", "2.9") def get(self, keypair): """ Get a keypair. @@ -66,6 +67,20 @@ def get(self, keypair): return self._get("/%s/%s" % (self.keypair_prefix, base.getid(keypair)), "keypair") + @api_versions.wraps("2.10") + def get(self, keypair, user_id=None): + """ + Get a keypair. + + :param keypair: The ID of the keypair to get. + :param user_id: Id of key-pair owner (Admin only). + :rtype: :class:`Keypair` + """ + query_string = "?user_id=%s" % user_id if user_id else "" + url = "/%s/%s%s" % (self.keypair_prefix, base.getid(keypair), + query_string) + return self._get(url, "keypair") + @api_versions.wraps("2.0", "2.1") def create(self, name, public_key=None): """ @@ -79,7 +94,7 @@ def create(self, name, public_key=None): body['keypair']['public_key'] = public_key return self._create('/%s' % self.keypair_prefix, body, 'keypair') - @api_versions.wraps("2.2") + @api_versions.wraps("2.2", "2.9") def create(self, name, public_key=None, key_type="ssh"): """ Create a keypair @@ -94,6 +109,25 @@ def create(self, name, public_key=None, key_type="ssh"): body['keypair']['public_key'] = public_key return self._create('/%s' % self.keypair_prefix, body, 'keypair') + @api_versions.wraps("2.10") + def create(self, name, public_key=None, key_type="ssh", user_id=None): + """ + Create a keypair + + :param name: name for the keypair to create + :param public_key: existing public key to import + :param key_type: keypair type to create + :param user_id: user to add. + """ + body = {'keypair': {'name': name, + 'type': key_type}} + if public_key: + body['keypair']['public_key'] = public_key + if user_id: + body['keypair']['user_id'] = user_id + return self._create('/%s' % self.keypair_prefix, body, 'keypair') + + @api_versions.wraps("2.0", "2.9") def delete(self, key): """ Delete a keypair @@ -102,8 +136,32 @@ def delete(self, key): """ self._delete('/%s/%s' % (self.keypair_prefix, base.getid(key))) + @api_versions.wraps("2.10") + def delete(self, key, user_id=None): + """ + Delete a keypair + + :param key: The :class:`Keypair` (or its ID) to delete. + :param user_id: Id of key-pair owner (Admin only). + """ + query_string = "?user_id=%s" % user_id if user_id else "" + url = '/%s/%s%s' % (self.keypair_prefix, base.getid(key), query_string) + self._delete(url) + + @api_versions.wraps("2.0", "2.9") def list(self): """ Get a list of keypairs. """ return self._list('/%s' % self.keypair_prefix, 'keypairs') + + @api_versions.wraps("2.10") + def list(self, user_id=None): + """ + Get a list of keypairs. + + :param user_id: Id of key-pairs owner (Admin only). + """ + query_string = "?user_id=%s" % user_id if user_id else "" + url = '/%s%s' % (self.keypair_prefix, query_string) + return self._list(url, 'keypairs') diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 5296084de..aee1b8c48 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -2977,11 +2977,17 @@ def _keypair_create(cs, args, name, pub_key): return cs.keypairs.create(name, pub_key) -@api_versions.wraps("2.2") +@api_versions.wraps("2.2", "2.9") def _keypair_create(cs, args, name, pub_key): return cs.keypairs.create(name, pub_key, key_type=args.key_type) +@api_versions.wraps("2.10") +def _keypair_create(cs, args, name, pub_key): + return cs.keypairs.create(name, pub_key, key_type=args.key_type, + user_id=args.user) + + @cliutils.arg('name', metavar='', help=_('Name of key.')) @cliutils.arg( '--pub-key', @@ -2997,6 +3003,12 @@ def _keypair_create(cs, args, name, pub_key): default='ssh', help=_('Keypair type. Can be ssh or x509.'), start_version="2.2") +@cliutils.arg( + '--user', + metavar='', + default=None, + help=_('ID of user to whom to add key-pair (Admin only).'), + start_version="2.10") def do_keypair_add(cs, args): """Create a new key pair for use with servers.""" name = args.name @@ -3021,6 +3033,7 @@ def do_keypair_add(cs, args): print(private_key) +@api_versions.wraps("2.0", "2.9") @cliutils.arg('name', metavar='', help=_('Keypair name to delete.')) def do_keypair_delete(cs, args): """Delete keypair given by its name.""" @@ -3028,6 +3041,18 @@ def do_keypair_delete(cs, args): cs.keypairs.delete(name) +@api_versions.wraps("2.10") +@cliutils.arg('name', metavar='', help=_('Keypair name to delete.')) +@cliutils.arg( + '--user', + metavar='', + default=None, + help=_('Id of key-pair owner (Admin only).')) +def do_keypair_delete(cs, args): + """Delete keypair given by its name.""" + cs.keypairs.delete(args.name, args.user) + + @api_versions.wraps("2.0", "2.1") def _get_keypairs_list_columns(cs, args): return ['Name', 'Fingerprint'] @@ -3038,6 +3063,7 @@ def _get_keypairs_list_columns(cs, args): return ['Name', 'Type', 'Fingerprint'] +@api_versions.wraps("2.0", "2.9") def do_keypair_list(cs, args): """Print a list of keypairs for a user""" keypairs = cs.keypairs.list() @@ -3045,6 +3071,19 @@ def do_keypair_list(cs, args): utils.print_list(keypairs, columns) +@api_versions.wraps("2.10") +@cliutils.arg( + '--user', + metavar='', + default=None, + help=_('List key-pairs of specified user id (Admin only).')) +def do_keypair_list(cs, args): + """Print a list of keypairs for a user""" + keypairs = cs.keypairs.list(args.user) + columns = _get_keypairs_list_columns(cs, args) + utils.print_list(keypairs, columns) + + def _print_keypair(keypair): kp = keypair._info.copy() pk = kp.pop('public_key') @@ -3052,6 +3091,7 @@ def _print_keypair(keypair): print(_("Public key: %s") % pk) +@api_versions.wraps("2.0", "2.9") @cliutils.arg( 'keypair', metavar='', @@ -3062,6 +3102,22 @@ def do_keypair_show(cs, args): _print_keypair(keypair) +@api_versions.wraps("2.10") +@cliutils.arg( + 'keypair', + metavar='', + help=_("Name of keypair.")) +@cliutils.arg( + '--user', + metavar='', + default=None, + help=_('Id of key-pair owner (Admin only).')) +def do_keypair_show(cs, args): + """Show details about the given keypair.""" + keypair = cs.keypairs.get(args.keypair, args.user) + _print_keypair(keypair) + + def _find_keypair(cs, keypair): """Get a keypair by name.""" return utils.find_resource(cs.keypairs, keypair) From 529a4921263f73a70e3c07a8a50fd08cc82c450b Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Thu, 3 Dec 2015 19:26:08 +0200 Subject: [PATCH 0913/1705] [microversions] Increase max version to 2.11 2.11 - Exposed attribute forced_down for os-services. Added ability to change the forced_down attribute by calling an update. This microversion was previously added in [1], but since the support for 2.6-2.10 microversion was missed, the max version was decreased to 2.5[2]. Now, support is added for all microversion 2.1-2.10, so we can enable 2.11 microversion. [1] - I2b80ac32a95fe80363b4ad95d8d89fff097935a3 [2] - I52074f9a3e7faa6a7a51c3fa9766100acf25dee2 & Iba9bfa136245bd2899c427ac0c231a30c00bd7f3 Change-Id: Iab3224a5c3691437e64ab4e06c85ccc2a7deea13 --- novaclient/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 37c73086a..f34afe6b4 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.10") +API_MAX_VERSION = api_versions.APIVersion("2.11") From 0ec6bedf428d030c57e744d2cba90ec3811cb8c6 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Mon, 7 Dec 2015 18:06:13 +0200 Subject: [PATCH 0914/1705] [microversions] Increase max version to 2.12 2.12 - Exposes VIF net-id attribute in os-virtual-interfaces. User will be able to get Virtual Interfaces net-id in Virtual Interfaces list and can determine in which network a Virtual Interface is plugged into. novaclient.base.Resource class is a parent class of VirtualInterface resource. Since `Resource` is a universal interface for any resources and doesn't require hard-code attributes which describe partial resource, there are no required changes to support this microversion in "novaclient as a lib". "novaclient as CLI" doesn't provide an interface for VirtualInterface yet (see bug 1522424 for more details). When it will be implemented, we will need to check that new attribute is displayed correctly. Based on the fact that 1522424 is not resolved and novaclient.base.Resource class has enough tests, this patch doesn't require any tests. Also this patch fixes representation of VirtualInterface resource. Change-Id: I18cf23847d3b2b01f5a6ffae2ebc4bede54babce --- novaclient/__init__.py | 2 +- novaclient/tests/unit/v2/test_shell.py | 2 ++ novaclient/v2/virtual_interfaces.py | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/novaclient/__init__.py b/novaclient/__init__.py index f34afe6b4..f407d9e34 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.11") +API_MAX_VERSION = api_versions.APIVersion("2.12") diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 36093ef7c..1f177806e 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2535,6 +2535,8 @@ def test_versions(self): 5, # Not implemented when test added, should not apply to adds. 7, # doesn't require any changes in novaclient 9, # doesn't require any changes in novaclient + # TODO(andreykurilin): remove 12 when 1522424 will be resolved + 12, # doesn't require any changes in novaclient ]) versions_supported = set(range(0, novaclient.API_MAX_VERSION.ver_minor + 1)) diff --git a/novaclient/v2/virtual_interfaces.py b/novaclient/v2/virtual_interfaces.py index 9c04e8d86..c2a0adf4a 100644 --- a/novaclient/v2/virtual_interfaces.py +++ b/novaclient/v2/virtual_interfaces.py @@ -22,7 +22,7 @@ class VirtualInterface(base.Resource): def __repr__(self): - pass + return "" class VirtualInterfaceManager(base.ManagerWithFind): From 00229390382a457473ab70fbb00decbf17603666 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Tue, 8 Dec 2015 01:42:00 +0000 Subject: [PATCH 0915/1705] Updated from global requirements Change-Id: Ice2396d08b3abf254f08949a072a31fa349652be --- test-requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test-requirements.txt b/test-requirements.txt index c991f9567..0ab103545 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -6,7 +6,7 @@ hacking<0.11,>=0.10.0 coverage>=3.6 discover fixtures>=1.3.1 -keyring!=3.3,>=2.1 +keyring>=5.5.1 mock>=1.2 requests-mock>=0.7.0 # Apache-2.0 sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2 @@ -15,7 +15,7 @@ oslosphinx!=3.4.0,>=2.5.0 # Apache-2.0 testrepository>=0.0.18 testscenarios>=0.4 testtools>=1.4.0 -tempest-lib>=0.10.0 +tempest-lib>=0.11.0 # releasenotes reno>=0.1.1 # Apache2 From 8aaf0a908076746025a677029fa511bf0f0b1f23 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Mon, 7 Dec 2015 20:05:49 +0200 Subject: [PATCH 0916/1705] Fix help message in case of microversions novaclient should display help message of the proper versioned method (based on api_version value). Closes-Bug: #1523649 Closes-Bug: #1523651 Change-Id: I67f7fc2befde9d312400e9a1569e590bd763156e --- novaclient/api_versions.py | 6 ++--- novaclient/shell.py | 28 +++++++++++++------- novaclient/tests/unit/fake_actions_module.py | 5 ++++ novaclient/tests/unit/test_shell.py | 24 +++++++++++++---- 4 files changed, 45 insertions(+), 18 deletions(-) diff --git a/novaclient/api_versions.py b/novaclient/api_versions.py index cdd708b65..cd33467ce 100644 --- a/novaclient/api_versions.py +++ b/novaclient/api_versions.py @@ -343,7 +343,7 @@ def get_substitutions(func_name, api_version=None): if api_version and not api_version.is_null(): return [m for m in substitutions if api_version.matches(m.start_version, m.end_version)] - return substitutions + return sorted(substitutions, key=lambda m: m.start_version) def wraps(start_version, end_version=None): @@ -368,9 +368,7 @@ def substitution(obj, *args, **kwargs): raise exceptions.VersionNotFoundForAPIMethod( obj.api_version.get_string(), name) - method = max(methods, key=lambda f: f.start_version) - - return method.func(obj, *args, **kwargs) + return methods[-1].func(obj, *args, **kwargs) if hasattr(func, 'arguments'): for cli_args, cli_kwargs in func.arguments: diff --git a/novaclient/shell.py b/novaclient/shell.py index 39bef130d..e77df4584 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -59,6 +59,9 @@ DEFAULT_NOVA_ENDPOINT_TYPE = 'publicURL' DEFAULT_NOVA_SERVICE_TYPE = "compute" +HINT_HELP_MSG = (" [hint: use '--os-compute-api-version' flag to show help " + "message for proper version]") + logger = logging.getLogger(__name__) @@ -466,19 +469,26 @@ def _find_actions(self, subparsers, actions_module, version, do_help): callback = getattr(actions_module, attr) desc = callback.__doc__ or '' if hasattr(callback, "versioned"): + additional_msg = "" subs = api_versions.get_substitutions( utils.get_function_name(callback)) if do_help: - desc += msg % {'start': subs[0].start_version.get_string(), - 'end': subs[-1].end_version.get_string()} - else: - for versioned_method in subs: + additional_msg = msg % { + 'start': subs[0].start_version.get_string(), + 'end': subs[-1].end_version.get_string()} + if version.is_latest(): + additional_msg += HINT_HELP_MSG + subs = [versioned_method for versioned_method in subs if version.matches(versioned_method.start_version, - versioned_method.end_version): - callback = versioned_method.func - break - else: - continue + versioned_method.end_version)] + if subs: + # use the "latest" substitution + callback = subs[-1].func + else: + # there is no proper versioned method + continue + desc = callback.__doc__ or desc + desc += additional_msg action_help = desc.strip() arguments = getattr(callback, 'arguments', []) diff --git a/novaclient/tests/unit/fake_actions_module.py b/novaclient/tests/unit/fake_actions_module.py index 2bd0e2f04..f927b98af 100644 --- a/novaclient/tests/unit/fake_actions_module.py +++ b/novaclient/tests/unit/fake_actions_module.py @@ -27,6 +27,11 @@ def do_fake_action(): return 2 +@api_versions.wraps("2.0") +def do_another_fake_action(): + return 0 + + @cliutils.arg( '--foo', start_version='2.1', diff --git a/novaclient/tests/unit/test_shell.py b/novaclient/tests/unit/test_shell.py index a32c14257..8e739f5ba 100644 --- a/novaclient/tests/unit/test_shell.py +++ b/novaclient/tests/unit/test_shell.py @@ -582,12 +582,27 @@ def test_load_versioned_actions_with_help(self): shell = novaclient.shell.OpenStackComputeShell() shell.subcommands = {} shell._find_actions(subparsers, fake_actions_module, - api_versions.APIVersion("2.10000"), True) + api_versions.APIVersion("2.15"), True) self.assertIn('fake-action', shell.subcommands.keys()) - expected_desc = ("(Supported by API versions '%(start)s' - " + expected_desc = (" (Supported by API versions '%(start)s' - " "'%(end)s')") % {'start': '2.10', 'end': '2.30'} - self.assertIn(expected_desc, - shell.subcommands['fake-action'].description) + self.assertEqual(expected_desc, + shell.subcommands['fake-action'].description) + + def test_load_versioned_actions_with_help_on_latest(self): + parser = novaclient.shell.NovaClientArgumentParser() + subparsers = parser.add_subparsers(metavar='') + shell = novaclient.shell.OpenStackComputeShell() + shell.subcommands = {} + shell._find_actions(subparsers, fake_actions_module, + api_versions.APIVersion("2.latest"), True) + self.assertIn('another-fake-action', shell.subcommands.keys()) + expected_desc = (" (Supported by API versions '%(start)s' - " + "'%(end)s')%(hint)s") % { + 'start': '2.0', 'end': '2.latest', + 'hint': novaclient.shell.HINT_HELP_MSG} + self.assertEqual(expected_desc, + shell.subcommands['another-fake-action'].description) @mock.patch.object(novaclient.shell.NovaClientArgumentParser, 'add_argument') @@ -641,7 +656,6 @@ def test_load_versioned_actions_with_args_and_help(self, mock_add_arg): shell._find_actions(subparsers, fake_actions_module, api_versions.APIVersion("2.4"), True) mock_add_arg.assert_has_calls([ - mock.call('-h', '--help', action='help', help='==SUPPRESS=='), mock.call('-h', '--help', action='help', help='==SUPPRESS=='), mock.call('--foo', help=" (Supported by API versions '2.1' - '2.2')"), From 11e0782f62e82a4a7e11dcbddd90403d5082af68 Mon Sep 17 00:00:00 2001 From: Zhihai Date: Wed, 18 Nov 2015 11:21:33 +0000 Subject: [PATCH 0917/1705] Change the logic for the client to retrive resources The ID (primary key) of a resource can not be retrived by the client. There is no sense to provide the functionality to get an resource by its ID. Especially when an instance is named by an interger, client will try to get the instance with the ID of that interger value. This patch will change the order in which the novaclient retrive resources. First by UUID, then by name, finally by the ID. This will also reduce api call overhead, since UUID is used mostly. Change-Id: I0bfa5cb03a04625c3def5608dd4529e52d50f944 Closes-Bug: #1516924 --- novaclient/tests/unit/v2/fakes.py | 43 +++++++++++- novaclient/tests/unit/v2/test_shell.py | 97 ++++++++++++++------------ novaclient/utils.py | 25 +++---- 3 files changed, 107 insertions(+), 58 deletions(-) diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index c4ad64a0d..56c141304 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -40,6 +40,10 @@ # accepts formats like v2 or v2_1 CALLBACK_RE = re.compile(r"^get_http:__nova_api:8774_v\d(_\d)?$") +# fake image uuids +FAKE_IMAGE_UUID_1 = 'c99d7632-bd66-4be9-aed5-3dd14b223a76' +FAKE_IMAGE_UUID_2 = 'f27f479a-ddda-419a-9bbc-d6b56b210161' + class FakeClient(fakes.FakeClient, client.Client): @@ -362,7 +366,7 @@ def get_servers_detail(self, **kw): "id": 1234, "name": "sample-server", "image": { - "id": 2, + "id": FAKE_IMAGE_UUID_2, "name": "sample image", }, "flavor": { @@ -1091,7 +1095,9 @@ def put_os_floating_ips_bulk_delete(self, **kw): def get_images(self, **kw): return (200, {}, {'images': [ {'id': 1, 'name': 'CentOS 5.2'}, - {'id': 2, 'name': 'My Server Backup'} + {'id': 2, 'name': 'My Server Backup'}, + {'id': FAKE_IMAGE_UUID_1, 'name': 'CentOS 5.2'}, + {'id': FAKE_IMAGE_UUID_2, 'name': 'My Server Backup'} ]}) def get_images_detail(self, **kw): @@ -1126,6 +1132,27 @@ def get_images_detail(self, **kw): "status": "DELETED", "fault": {'message': 'Image has been deleted.'}, "links": {}, + }, + { + 'id': FAKE_IMAGE_UUID_1, + 'name': 'CentOS 5.2', + "updated": "2010-10-10T12:00:00Z", + "created": "2010-08-10T12:00:00Z", + "status": "ACTIVE", + "metadata": { + "test_key": "test_value", + }, + "links": {}, + }, + { + "id": FAKE_IMAGE_UUID_2, + "name": "My Server Backup", + "serverId": 1234, + "updated": "2010-10-10T12:00:00Z", + "created": "2010-08-10T12:00:00Z", + "status": "SAVING", + "progress": 80, + "links": {}, } ]}) @@ -1141,6 +1168,12 @@ def get_images_456(self, **kw): def get_images_457(self, **kw): return (200, {}, {'image': self.get_images_detail()[2]['images'][2]}) + def get_images_c99d7632_bd66_4be9_aed5_3dd14b223a76(self, **kw): + return (200, {}, {'image': self.get_images_detail()[2]['images'][3]}) + + def get_images_f27f479a_ddda_419a_9bbc_d6b56b210161(self, **kw): + return (200, {}, {'image': self.get_images_detail()[2]['images'][4]}) + def get_images_3e861307_73a6_4d1f_8d68_f68b03223032(self): raise exceptions.NotFound('404') @@ -1164,6 +1197,12 @@ def delete_images_1(self, **kw): def delete_images_2(self, **kw): return (204, {}, None) + def delete_images_c99d7632_bd66_4be9_aed5_3dd14b223a76(self, **kw): + return (204, {}, None) + + def delete_images_f27f479a_ddda_419a_9bbc_d6b56b210161(self, **kw): + return (204, {}, None) + def delete_images_1_metadata_test_key(self, **kw): return (204, {}, None) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 67759f70e..bc52d6978 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -36,6 +36,9 @@ from novaclient.tests.unit.v2 import fakes import novaclient.v2.shell +FAKE_UUID_1 = fakes.FAKE_IMAGE_UUID_1 +FAKE_UUID_2 = fakes.FAKE_IMAGE_UUID_2 + class ShellFixture(fixtures.Fixture): def setUp(self): @@ -672,10 +675,10 @@ def test_boot_with_poll_to_check_VM_state_error(self): 'boot --flavor 1 --image 1 some-bad-server --poll') def test_boot_named_flavor(self): - self.run_command(["boot", "--image", "1", + self.run_command(["boot", "--image", FAKE_UUID_1, "--flavor", "512 MB Server", "--max-count", "3", "server"]) - self.assert_called('GET', '/images/1', pos=0) + self.assert_called('GET', '/images/' + FAKE_UUID_1, pos=0) self.assert_called('GET', '/flavors/512 MB Server', pos=1) self.assert_called('GET', '/flavors?is_public=None', pos=2) self.assert_called('GET', '/flavors/2', pos=3) @@ -685,7 +688,7 @@ def test_boot_named_flavor(self): 'server': { 'flavorRef': '2', 'name': 'server', - 'imageRef': '1', + 'imageRef': FAKE_UUID_1, 'min_count': 1, 'max_count': 3, } @@ -848,9 +851,11 @@ def test_image_delete(self): self.assert_called('DELETE', '/images/1') def test_image_delete_multiple(self): - self.run_command('image-delete 1 2') - self.assert_called('DELETE', '/images/1', pos=-3) - self.assert_called('DELETE', '/images/2', pos=-1) + self.run_command('image-delete %s %s' % (FAKE_UUID_1, FAKE_UUID_2)) + self.assert_called('GET', '/images/' + FAKE_UUID_1, pos=0) + self.assert_called('DELETE', '/images/' + FAKE_UUID_1, pos=1) + self.assert_called('GET', '/images/' + FAKE_UUID_2, pos=2) + self.assert_called('DELETE', '/images/' + FAKE_UUID_2, pos=3) def test_list(self): self.run_command('list') @@ -978,52 +983,54 @@ def test_reboot_many(self): {'reboot': {'type': 'SOFT'}}, pos=-1) def test_rebuild(self): - output, _ = self.run_command('rebuild sample-server 1') - self.assert_called('GET', '/servers?name=sample-server', pos=-6) - self.assert_called('GET', '/servers/1234', pos=-5) - self.assert_called('GET', '/images/1', pos=-4) + output, _ = self.run_command('rebuild sample-server %s' % FAKE_UUID_1) + self.assert_called('GET', '/servers?name=sample-server', pos=0) + self.assert_called('GET', '/servers/1234', pos=1) + self.assert_called('GET', '/images/%s' % FAKE_UUID_1, pos=2) self.assert_called('POST', '/servers/1234/action', - {'rebuild': {'imageRef': 1}}, pos=-3) - self.assert_called('GET', '/flavors/1', pos=-2) - self.assert_called('GET', '/images/2') + {'rebuild': {'imageRef': FAKE_UUID_1}}, pos=3) + self.assert_called('GET', '/flavors/1', pos=4) + self.assert_called('GET', '/images/%s' % FAKE_UUID_2, pos=5) self.assertIn('adminPass', output) def test_rebuild_password(self): - output, _ = self.run_command('rebuild sample-server 1' - ' --rebuild-password asdf') - self.assert_called('GET', '/servers?name=sample-server', pos=-6) - self.assert_called('GET', '/servers/1234', pos=-5) - self.assert_called('GET', '/images/1', pos=-4) + output, _ = self.run_command('rebuild sample-server %s' + ' --rebuild-password asdf' + % FAKE_UUID_1) + self.assert_called('GET', '/servers?name=sample-server', pos=0) + self.assert_called('GET', '/servers/1234', pos=1) + self.assert_called('GET', '/images/%s' % FAKE_UUID_1, pos=2) self.assert_called('POST', '/servers/1234/action', - {'rebuild': {'imageRef': 1, 'adminPass': 'asdf'}}, - pos=-3) - self.assert_called('GET', '/flavors/1', pos=-2) - self.assert_called('GET', '/images/2') + {'rebuild': {'imageRef': FAKE_UUID_1, + 'adminPass': 'asdf'}}, pos=3) + self.assert_called('GET', '/flavors/1', pos=4) + self.assert_called('GET', '/images/%s' % FAKE_UUID_2, pos=5) self.assertIn('adminPass', output) def test_rebuild_preserve_ephemeral(self): - self.run_command('rebuild sample-server 1 --preserve-ephemeral') - self.assert_called('GET', '/servers?name=sample-server', pos=-6) - self.assert_called('GET', '/servers/1234', pos=-5) - self.assert_called('GET', '/images/1', pos=-4) + self.run_command('rebuild sample-server %s --preserve-ephemeral' + % FAKE_UUID_1) + self.assert_called('GET', '/servers?name=sample-server', pos=0) + self.assert_called('GET', '/servers/1234', pos=1) + self.assert_called('GET', '/images/%s' % FAKE_UUID_1, pos=2) self.assert_called('POST', '/servers/1234/action', - {'rebuild': {'imageRef': 1, - 'preserve_ephemeral': True}}, pos=-3) - self.assert_called('GET', '/flavors/1', pos=-2) - self.assert_called('GET', '/images/2') + {'rebuild': {'imageRef': FAKE_UUID_1, + 'preserve_ephemeral': True}}, pos=3) + self.assert_called('GET', '/flavors/1', pos=4) + self.assert_called('GET', '/images/%s' % FAKE_UUID_2, pos=5) def test_rebuild_name_meta(self): - self.run_command('rebuild sample-server 1 --name asdf --meta ' - 'foo=bar') - self.assert_called('GET', '/servers?name=sample-server', pos=-6) - self.assert_called('GET', '/servers/1234', pos=-5) - self.assert_called('GET', '/images/1', pos=-4) + self.run_command('rebuild sample-server %s --name asdf --meta ' + 'foo=bar' % FAKE_UUID_1) + self.assert_called('GET', '/servers?name=sample-server', pos=0) + self.assert_called('GET', '/servers/1234', pos=1) + self.assert_called('GET', '/images/%s' % FAKE_UUID_1, pos=2) self.assert_called('POST', '/servers/1234/action', - {'rebuild': {'imageRef': 1, + {'rebuild': {'imageRef': FAKE_UUID_1, 'name': 'asdf', - 'metadata': {'foo': 'bar'}}}, pos=-3) - self.assert_called('GET', '/flavors/1', pos=-2) - self.assert_called('GET', '/images/2') + 'metadata': {'foo': 'bar'}}}, pos=3) + self.assert_called('GET', '/flavors/1', pos=4) + self.assert_called('GET', '/images/%s' % FAKE_UUID_2, pos=5) def test_start(self): self.run_command('start sample-server') @@ -1137,9 +1144,11 @@ def test_scrub(self): def test_show(self): self.run_command('show 1234') - self.assert_called('GET', '/servers/1234', pos=-3) - self.assert_called('GET', '/flavors/1', pos=-2) - self.assert_called('GET', '/images/2') + self.assert_called('GET', '/servers?name=1234', pos=0) + self.assert_called('GET', '/servers?name=1234', pos=1) + self.assert_called('GET', '/servers/1234', pos=2) + self.assert_called('GET', '/flavors/1', pos=3) + self.assert_called('GET', '/images/%s' % FAKE_UUID_2, pos=4) def test_show_no_image(self): self.run_command('show 9012') @@ -1192,7 +1201,7 @@ def test_restore(self): def test_delete_two_with_two_existent(self): self.run_command('delete 1234 5678') - self.assert_called('DELETE', '/servers/1234', pos=-3) + self.assert_called('DELETE', '/servers/1234', pos=-5) self.assert_called('DELETE', '/servers/5678', pos=-1) self.run_command('delete sample-server sample-server2') self.assert_called('GET', @@ -1937,7 +1946,7 @@ def test_network_list_fields(self): def test_network_show(self): self.run_command('network-show 1') - self.assert_called('GET', '/os-networks/1') + self.assert_called('GET', '/os-networks') def test_cloudpipe_list(self): self.run_command('cloudpipe-list') diff --git a/novaclient/utils.py b/novaclient/utils.py index b420a29d9..f1a4bd825 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -197,13 +197,7 @@ def find_resource(manager, name_or_id, **find_args): except exceptions.NotFound: pass - # try to get entity as integer id - try: - return manager.get(int(name_or_id)) - except (TypeError, ValueError, exceptions.NotFound): - pass - - # now try to get entity as uuid + # first try to get entity as uuid try: tmp_id = encodeutils.safe_encode(name_or_id) @@ -215,6 +209,7 @@ def find_resource(manager, name_or_id, **find_args): except (TypeError, ValueError, exceptions.NotFound): pass + # then try to get entity as name try: try: resource = getattr(manager, 'resource_class', None) @@ -225,14 +220,11 @@ def find_resource(manager, name_or_id, **find_args): except exceptions.NotFound: pass - # finally try to find entity by human_id + # then try to find entity by human_id try: return manager.find(human_id=name_or_id, **find_args) except exceptions.NotFound: - msg = (_("No %(class)s with a name or ID of '%(name)s' exists.") % - {'class': manager.resource_class.__name__.lower(), - 'name': name_or_id}) - raise exceptions.CommandError(msg) + pass except exceptions.NoUniqueMatch: msg = (_("Multiple %(class)s matches found for '%(name)s', use an ID " "to be more specific.") % @@ -240,6 +232,15 @@ def find_resource(manager, name_or_id, **find_args): 'name': name_or_id}) raise exceptions.CommandError(msg) + # finally try to get entity as integer id + try: + return manager.get(int(name_or_id)) + except (TypeError, ValueError, exceptions.NotFound): + msg = (_("No %(class)s with a name or ID of '%(name)s' exists.") % + {'class': manager.resource_class.__name__.lower(), + 'name': name_or_id}) + raise exceptions.CommandError(msg) + def _format_servers_list_networks(server): output = [] From e919c9b792bff0d77435bb3b62d93c9d77904374 Mon Sep 17 00:00:00 2001 From: Javeme Date: Mon, 7 Dec 2015 20:56:36 +0800 Subject: [PATCH 0918/1705] remove the default arguments "[]" remove the default arguments "[]" when the function is defined. ref: http://docs.python-guide.org/en/latest/writing/gotchas/ Change-Id: I71ee8fbf5dec12d93bbed0e336124ad833014845 --- novaclient/tests/unit/fakes.py | 4 +++- novaclient/v2/fping.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/novaclient/tests/unit/fakes.py b/novaclient/tests/unit/fakes.py index 78e10f04f..039422dcb 100644 --- a/novaclient/tests/unit/fakes.py +++ b/novaclient/tests/unit/fakes.py @@ -22,7 +22,9 @@ from novaclient import base -def assert_has_keys(dict, required=[], optional=[]): +def assert_has_keys(dict, required=None, optional=None): + required = required or [] + optional = optional or [] keys = dict.keys() for k in required: try: diff --git a/novaclient/v2/fping.py b/novaclient/v2/fping.py index 31c7acb96..2c0e8e5dd 100644 --- a/novaclient/v2/fping.py +++ b/novaclient/v2/fping.py @@ -32,11 +32,13 @@ class FpingManager(base.ManagerWithFind): """Manage :class:`Fping` resources.""" resource_class = Fping - def list(self, all_tenants=False, include=[], exclude=[]): + def list(self, all_tenants=False, include=None, exclude=None): """Fping all servers. :returns: list of :class:`Fping`. """ + include = include or [] + exclude = exclude or [] params = [] if all_tenants: params.append("all_tenants=1") From 363bc3eafc85df01dc0d4bc032e3b7a1b22d0714 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Mon, 7 Dec 2015 18:36:36 +0200 Subject: [PATCH 0919/1705] [functional-tests] fix version of novaclient instance setUp method of base class for functional tests creates an instance of novaclient.client.Client with const version. This logic is wrong since we have tests for microversions. Change-Id: Ibd3ee5c730ae1fb3c19f512728f2a0a3693763e8 --- novaclient/tests/functional/base.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/novaclient/tests/functional/base.py b/novaclient/tests/functional/base.py index 86d652322..78f3a32b7 100644 --- a/novaclient/tests/functional/base.py +++ b/novaclient/tests/functional/base.py @@ -20,6 +20,8 @@ import tempest_lib.cli.base import testtools +import novaclient +import novaclient.api_versions import novaclient.client @@ -145,11 +147,12 @@ def setUp(self): tenant = auth_info['project_name'] auth_url = auth_info['auth_url'] - # TODO(sdague): we made a lot of fun of the glanceclient team - # for version as int in first parameter. I guess we know where - # they copied it from. + if self.COMPUTE_API_VERSION == "2.latest": + version = novaclient.API_MAX_VERSION.get_string() + else: + version = self.COMPUTE_API_VERSION or "2" self.client = novaclient.client.Client( - 2, user, passwd, tenant, + version, user, passwd, tenant, auth_url=auth_url) # pick some reasonable flavor / image combo From 9857dd2523ec572783705bd24fc22c41a85aecbb Mon Sep 17 00:00:00 2001 From: jichenjc Date: Fri, 11 Dec 2015 20:08:02 +0800 Subject: [PATCH 0920/1705] [functional-test] add v2.8 functional test add functional test for v2.8 API (webmks console type enablement). Also, fixed an issue related to v2.8 microversion enablement for the console API. Change-Id: I7660a189cc3051dd7dbc802ad2d40a4995b44960 --- novaclient/tests/functional/v2/test_consoles.py | 9 +++++++++ novaclient/v2/shell.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/novaclient/tests/functional/v2/test_consoles.py b/novaclient/tests/functional/v2/test_consoles.py index 37615af7f..084277b0a 100644 --- a/novaclient/tests/functional/v2/test_consoles.py +++ b/novaclient/tests/functional/v2/test_consoles.py @@ -30,3 +30,12 @@ def test_rdp_console_get(self): def test_serial_console_get(self): self._test_serial_console_get() + + +class TestConsolesNovaClientV28(test_consoles.TestConsolesNovaClient): + """Consoles functional tests for >=v2.8 api microversions.""" + + COMPUTE_API_VERSION = "2.8" + + def test_webmks_console_get(self): + self._test_console_get('get-mks-console %s ') diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 05a0e74d4..d2f32ab8a 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -2435,8 +2435,8 @@ def do_get_serial_console(cs, args): print_console(cs, data) -@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) @api_versions.wraps('2.8') +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) def do_get_mks_console(cs, args): """Get a serial console to a server.""" server = _find_server(cs, args.server) From c6dd7c7ba9e9fc3dfa45fa568c26529a63b03431 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 11 Dec 2015 15:25:55 +0000 Subject: [PATCH 0921/1705] Updated from global requirements Change-Id: I810adfef5817fad05b840349473dee220cf2aec2 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c2bf2a6c5..32826add3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ argparse iso8601>=0.1.9 oslo.i18n>=1.5.0 # Apache-2.0 oslo.serialization>=1.10.0 # Apache-2.0 -oslo.utils>=2.8.0 # Apache-2.0 +oslo.utils!=3.1.0,>=2.8.0 # Apache-2.0 PrettyTable<0.8,>=0.7 requests>=2.8.1 simplejson>=2.2.0 From 6ac9f3a4127711310a52433935c27d7446d5099c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Nov=C3=BD?= Date: Fri, 11 Dec 2015 23:11:57 +0100 Subject: [PATCH 0922/1705] Deprecated tox -downloadcache option removed Caching is enabled by default from pip version 6.0 More info: https://testrun.org/tox/latest/config.html#confval-downloadcache=path https://pip.pypa.io/en/stable/reference/pip_install/#caching Change-Id: I22e710aae8bf34938a4f9ee85c566876a36c7bb1 --- tox.ini | 3 --- 1 file changed, 3 deletions(-) diff --git a/tox.ini b/tox.ini index f27b5fc9d..84b4f68b8 100644 --- a/tox.ini +++ b/tox.ini @@ -42,9 +42,6 @@ commands = python setup.py testr --testr-args='--concurrency=1 {posargs}' [testenv:cover] commands = python setup.py testr --coverage --testr-args='{posargs}' -[tox:jenkins] -downloadcache = ~/cache/pip - [flake8] # Following checks should be enabled in the future. # From 1f11840dd84f3570330d1fcd53d1e8eea5ff7922 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 10 Dec 2015 13:49:14 -0500 Subject: [PATCH 0923/1705] Migrate to keystoneauth from keystoneclient As a stepping stone to the os-client-config patch, first switch to using keystoneauth, its Session and its argparse registration and plugin loading to sort out any issues with that level of plumbing. The next patch will layer on the ability to use os-client-config for argument processing and client construction. Change-Id: Id681e5eb56b47d06000620f7c92c9b0c5f8d4408 --- doc/source/api.rst | 24 ++-- novaclient/client.py | 10 +- novaclient/exceptions.py | 2 +- novaclient/shell.py | 121 +++++++----------- novaclient/tests/unit/fixture_data/client.py | 14 +- novaclient/tests/unit/test_auth_plugins.py | 2 +- novaclient/tests/unit/test_client.py | 4 +- novaclient/tests/unit/test_service_catalog.py | 2 +- novaclient/tests/unit/test_shell.py | 2 +- novaclient/tests/unit/v2/test_auth.py | 2 +- novaclient/tests/unit/v2/test_client.py | 2 +- novaclient/v2/shell.py | 18 ++- .../notes/keystoneauth-8ec1e6be14cdbae3.yaml | 11 ++ requirements.txt | 2 +- test-requirements.txt | 1 + 15 files changed, 112 insertions(+), 105 deletions(-) create mode 100644 releasenotes/notes/keystoneauth-8ec1e6be14cdbae3.yaml diff --git a/doc/source/api.rst b/doc/source/api.rst index ded67ed60..3b179e49b 100644 --- a/doc/source/api.rst +++ b/doc/source/api.rst @@ -19,22 +19,28 @@ If you prefer string value, you can use ``1.1`` (deprecated now), ``2`` or ``2.X`` (where X is a microversion). -Alternatively, you can create a client instance using the keystoneclient +Alternatively, you can create a client instance using the keystoneauth session API:: - >>> from keystoneclient.auth.identity import v2 - >>> from keystoneclient import session + >>> from keystoneauth1 import loading + >>> from keystoneauth1 import session >>> from novaclient import client - >>> auth = v2.Password(auth_url=AUTH_URL, - ... username=USERNAME, - ... password=PASSWORD, - ... tenant_name=PROJECT_ID) + >>> loader = loading.get_plugin_loader('password') + >>> auth = loader.Password(auth_url=AUTH_URL, + ... username=USERNAME, + ... password=PASSWORD, + ... project_id=PROJECT_ID) >>> sess = session.Session(auth=auth) >>> nova = client.Client(VERSION, session=sess) -For more information on this keystoneclient API, see `Using Sessions`_. +If you have PROJECT_NAME instead of a PROJECT_ID, use the project_name +parameter. Similarly, if your cloud uses keystone v3 and you have a DOMAIN_NAME +or DOMAIN_ID, provide it as `user_domain_(name|id)` and if you are using a +PROJECT_NAME also provide the domain information as `project_domain_(name|id)`. -.. _Using Sessions: http://docs.openstack.org/developer/python-keystoneclient/using-sessions.html +For more information on this keystoneauth API, see `Using Sessions`_. + +.. _Using Sessions: http://docs.openstack.org/developer/keystoneauth/using-sessions.html It is also possible to use an instance as a context manager in which case there will be a session kept alive for the duration of the with statement:: diff --git a/novaclient/client.py b/novaclient/client.py index 889d09224..11190687d 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -32,8 +32,8 @@ import re import warnings -from keystoneclient import adapter -from keystoneclient import session +from keystoneauth1 import adapter +from keystoneauth1 import session from oslo_utils import importutils from oslo_utils import netutils import pkg_resources @@ -80,7 +80,7 @@ def request(self, url, method, **kwargs): kwargs.setdefault('headers', kwargs.get('headers', {})) api_versions.update_headers(kwargs["headers"], self.api_version) # NOTE(jamielennox): The standard call raises errors from - # keystoneclient, where we need to raise the novaclient errors. + # keystoneauth1, where we need to raise the novaclient errors. raise_exc = kwargs.pop('raise_exc', True) with utils.record_time(self.times, self.timings, method, url): resp, body = super(SessionClient, self).request(url, @@ -680,6 +680,8 @@ def _construct_http_client(username=None, password=None, project_id=None, user_id=None, connection_pool=False, session=None, auth=None, user_agent='python-novaclient', interface=None, api_version=None, **kwargs): + # TODO(mordred): If not session, just make a Session, then return + # SessionClient always if session: return SessionClient(session=session, auth=auth, @@ -806,7 +808,7 @@ def Client(version, *args, **kwargs): (where X is a microversion). - Alternatively, you can create a client instance using the keystoneclient + Alternatively, you can create a client instance using the keystoneauth session API. See "The novaclient Python API" page at python-novaclient's doc. """ diff --git a/novaclient/exceptions.py b/novaclient/exceptions.py index ede19bd8c..cbe701083 100644 --- a/novaclient/exceptions.py +++ b/novaclient/exceptions.py @@ -231,7 +231,7 @@ class HTTPNotImplemented(ClientException): class InvalidUsage(RuntimeError): """This function call is invalid in the way you are using this client. - Due to the transition to using keystoneclient some function calls are no + Due to the transition to using keystoneauth some function calls are no longer available. You should make a similar call to the session object instead. """ diff --git a/novaclient/shell.py b/novaclient/shell.py index 39bef130d..1a1c22d35 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -23,11 +23,9 @@ import getpass import logging import sys +import warnings -from keystoneclient.auth.identity.generic import password -from keystoneclient.auth.identity.generic import token -from keystoneclient.auth.identity import v3 as identity -from keystoneclient import session as ksession +from keystoneauth1 import loading from oslo_utils import encodeutils from oslo_utils import importutils from oslo_utils import strutils @@ -246,23 +244,33 @@ def _get_option_tuples(self, option_string): class OpenStackComputeShell(object): times = [] - def _append_global_identity_args(self, parser): + def _append_global_identity_args(self, parser, argv): # Register the CLI arguments that have moved to the session object. - ksession.Session.register_cli_options(parser) + loading.register_session_argparse_arguments(parser) + # Peek into argv to see if os-auth-token or os-token were given, + # in which case, the token auth plugin is what the user wants + # else, we'll default to password + default_auth_plugin = 'password' + if 'os-token' in argv: + default_auth_plugin = 'token' + loading.register_auth_argparse_arguments( + parser, argv, default=default_auth_plugin) parser.set_defaults(insecure=cliutils.env('NOVACLIENT_INSECURE', default=False)) - - identity.Password.register_argparse_arguments(parser) + parser.set_defaults(os_auth_url=cliutils.env('OS_AUTH_URL', + 'NOVA_URL')) parser.set_defaults(os_username=cliutils.env('OS_USERNAME', 'NOVA_USERNAME')) parser.set_defaults(os_password=cliutils.env('OS_PASSWORD', 'NOVA_PASSWORD')) - parser.set_defaults(os_auth_url=cliutils.env('OS_AUTH_URL', - 'NOVA_URL')) + parser.set_defaults(os_project_name=cliutils.env( + 'OS_PROJECT_NAME', 'OS_TENANT_NAME', 'NOVA_PROJECT_ID')) + parser.set_defaults(os_project_id=cliutils.env( + 'OS_PROJECT_ID', 'OS_TENANT_ID')) - def get_base_parser(self): + def get_base_parser(self, argv): parser = NovaClientArgumentParser( prog='nova', description=__doc__.strip(), @@ -305,8 +313,7 @@ def get_base_parser(self): parser.add_argument( '--os-auth-token', - default=cliutils.env('OS_AUTH_TOKEN'), - help='Defaults to env[OS_AUTH_TOKEN].') + help=argparse.SUPPRESS) parser.add_argument( '--os_username', @@ -316,21 +323,10 @@ def get_base_parser(self): '--os_password', help=argparse.SUPPRESS) - parser.add_argument( - '--os-tenant-name', - metavar='', - default=cliutils.env('OS_TENANT_NAME', 'NOVA_PROJECT_ID'), - help=_('Defaults to env[OS_TENANT_NAME].')) parser.add_argument( '--os_tenant_name', help=argparse.SUPPRESS) - parser.add_argument( - '--os-tenant-id', - metavar='', - default=cliutils.env('OS_TENANT_ID'), - help=_('Defaults to env[OS_TENANT_ID].')) - parser.add_argument( '--os_auth_url', help=argparse.SUPPRESS) @@ -348,7 +344,7 @@ def get_base_parser(self): '--os-auth-system', metavar='', default=cliutils.env('OS_AUTH_SYSTEM'), - help='Defaults to env[OS_AUTH_SYSTEM].') + help=argparse.SUPPRESS) parser.add_argument( '--os_auth_system', help=argparse.SUPPRESS) @@ -426,12 +422,12 @@ def get_base_parser(self): # The auth-system-plugins might require some extra options novaclient.auth_plugin.load_auth_system_opts(parser) - self._append_global_identity_args(parser) + self._append_global_identity_args(parser, argv) return parser - def get_subcommand_parser(self, version, do_help=False): - parser = self.get_base_parser() + def get_subcommand_parser(self, version, do_help=False, argv=None): + parser = self.get_base_parser(argv) self.subcommands = {} subparsers = parser.add_subparsers(metavar='') @@ -529,23 +525,9 @@ def setup_debugging(self, debug): format=streamformat) logging.getLogger('iso8601').setLevel(logging.WARNING) - def _get_keystone_auth(self, session, auth_url, **kwargs): - auth_token = kwargs.pop('auth_token', None) - if auth_token: - return token.Token(auth_url, auth_token, **kwargs) - else: - return password.Password( - auth_url, - username=kwargs.pop('username'), - user_id=kwargs.pop('user_id'), - password=kwargs.pop('password'), - user_domain_id=kwargs.pop('user_domain_id'), - user_domain_name=kwargs.pop('user_domain_name'), - **kwargs) - def main(self, argv): # Parse args once to find version and debug settings - parser = self.get_base_parser() + parser = self.get_base_parser(argv) # NOTE(dtroyer): Hackery to handle --endpoint_type due to argparse # thinking usage-list --end is ambiguous; but it @@ -554,6 +536,10 @@ def main(self, argv): if '--endpoint_type' in argv: spot = argv.index('--endpoint_type') argv[spot] = '--endpoint-type' + # For backwards compat with old os-auth-token parameter + if '--os-auth-token' in argv: + spot = argv.index('--os-auth-token') + argv[spot] = '--os-token' (args, args_list) = parser.parse_known_args(argv) @@ -579,8 +565,10 @@ def main(self, argv): os_username = args.os_username os_user_id = args.os_user_id os_password = None # Fetched and set later as needed - os_tenant_name = args.os_tenant_name - os_tenant_id = args.os_tenant_id + os_project_name = getattr( + args, 'os_project_name', getattr(args, 'os_tenant_name', None)) + os_project_id = getattr( + args, 'os_project_id', getattr(args, 'os_tenant_id', None)) os_auth_url = args.os_auth_url os_region_name = args.os_region_name os_auth_system = args.os_auth_system @@ -603,10 +591,14 @@ def main(self, argv): # Finally, authenticate unless we have both. # Note if we don't auth we probably don't have a tenant ID so we can't # cache the token. - auth_token = args.os_auth_token if args.os_auth_token else None + auth_token = getattr(args, 'os_token', None) management_url = bypass_url if bypass_url else None if os_auth_system and os_auth_system != "keystone": + warnings.warn(_( + 'novaclient auth plugins that are not keystone are deprecated.' + ' Auth plugins should now be done as plugins to keystoneauth' + ' and selected with --os-auth-type or OS_AUTH_TYPE')) auth_plugin = novaclient.auth_plugin.load_plugin(os_auth_system) else: auth_plugin = None @@ -648,8 +640,7 @@ def main(self, argv): "or user id via --os-username, --os-user-id, " "env[OS_USERNAME] or env[OS_USER_ID]")) - if not any([args.os_tenant_name, args.os_tenant_id, - args.os_project_id, args.os_project_name]): + if not any([os_project_name, os_project_id]): raise exc.CommandError(_("You must provide a project name or" " project id via --os-project-name," " --os-project-id, env[OS_PROJECT_ID]" @@ -669,34 +660,20 @@ def main(self, argv): "default url with --os-auth-system " "or env[OS_AUTH_SYSTEM]")) - project_id = args.os_project_id or args.os_tenant_id - project_name = args.os_project_name or args.os_tenant_name if use_session: # Not using Nova auth plugin, so use keystone with utils.record_time(self.times, args.timings, 'auth_url', args.os_auth_url): - keystone_session = (ksession.Session - .load_from_cli_options(args)) - keystone_auth = self._get_keystone_auth( - keystone_session, - args.os_auth_url, - username=args.os_username, - user_id=args.os_user_id, - user_domain_id=args.os_user_domain_id, - user_domain_name=args.os_user_domain_name, - password=args.os_password, - auth_token=args.os_auth_token, - project_id=project_id, - project_name=project_name, - project_domain_id=args.os_project_domain_id, - project_domain_name=args.os_project_domain_name) + keystone_session = ( + loading.load_session_from_argparse_arguments(args)) + keystone_auth = ( + loading.load_auth_from_argparse_arguments(args)) else: # set password for auth plugins os_password = args.os_password if (not skip_auth and - not any([args.os_tenant_id, args.os_tenant_name, - args.os_project_id, args.os_project_name])): + not any([os_project_name, os_project_id])): raise exc.CommandError(_("You must provide a project name or" " project id via --os-project-name," " --os-project-id, env[OS_PROJECT_ID]" @@ -713,8 +690,8 @@ def main(self, argv): # microversion, so we just pass version 2 at here. self.cs = client.Client( api_versions.APIVersion("2.0"), - os_username, os_password, os_tenant_name, - tenant_id=os_tenant_id, user_id=os_user_id, + os_username, os_password, os_project_name, + tenant_id=os_project_id, user_id=os_user_id, auth_url=os_auth_url, insecure=insecure, region_name=os_region_name, endpoint_type=endpoint_type, extensions=self.extensions, service_type=service_type, @@ -745,7 +722,7 @@ def main(self, argv): self._run_extension_hooks('__pre_parse_args__') subcommand_parser = self.get_subcommand_parser( - api_version, do_help=do_help) + api_version, do_help=do_help, argv=argv) self.parser = subcommand_parser if args.help or not argv: @@ -777,8 +754,8 @@ def main(self, argv): # Recreate client object with discovered version. self.cs = client.Client( api_version, - os_username, os_password, os_tenant_name, - tenant_id=os_tenant_id, user_id=os_user_id, + os_username, os_password, os_project_name, + tenant_id=os_project_id, user_id=os_user_id, auth_url=os_auth_url, insecure=insecure, region_name=os_region_name, endpoint_type=endpoint_type, extensions=self.extensions, service_type=service_type, diff --git a/novaclient/tests/unit/fixture_data/client.py b/novaclient/tests/unit/fixture_data/client.py index 5f933b750..8aff839b8 100644 --- a/novaclient/tests/unit/fixture_data/client.py +++ b/novaclient/tests/unit/fixture_data/client.py @@ -11,9 +11,9 @@ # under the License. import fixtures -from keystoneclient.auth.identity import v2 -from keystoneclient import fixture -from keystoneclient import session +from keystoneauth1 import fixture +from keystoneauth1 import loading +from keystoneauth1 import session from novaclient.v2 import client as v2client @@ -33,6 +33,7 @@ def __init__(self, requests, self.token = fixture.V2Token() self.token.set_scope() + self.discovery = fixture.V2Discovery(href=self.identity_url) s = self.token.add_service('compute') s.add_endpoint(self.compute_url) @@ -48,6 +49,9 @@ def setUp(self): self.requests.register_uri('POST', auth_url, json=self.token, headers=headers) + self.requests.register_uri('GET', self.identity_url, + json=self.discovery, + headers=headers) self.client = self.new_client() def new_client(self): @@ -61,5 +65,7 @@ class SessionV1(V1): def new_client(self): self.session = session.Session() - self.session.auth = v2.Password(self.identity_url, 'xx', 'xx') + loader = loading.get_plugin_loader('password') + self.session.auth = loader.load_from_options( + auth_url=self.identity_url, username='xx', password='xx') return v2client.Client(session=self.session) diff --git a/novaclient/tests/unit/test_auth_plugins.py b/novaclient/tests/unit/test_auth_plugins.py index 9dc02b02a..2257b2a88 100644 --- a/novaclient/tests/unit/test_auth_plugins.py +++ b/novaclient/tests/unit/test_auth_plugins.py @@ -15,7 +15,7 @@ import argparse -from keystoneclient import fixture +from keystoneauth1 import fixture import mock import pkg_resources import requests diff --git a/novaclient/tests/unit/test_client.py b/novaclient/tests/unit/test_client.py index 5038e1837..171ec9d4c 100644 --- a/novaclient/tests/unit/test_client.py +++ b/novaclient/tests/unit/test_client.py @@ -18,7 +18,7 @@ import logging import fixtures -from keystoneclient import adapter +from keystoneauth1 import adapter import mock import requests @@ -30,7 +30,7 @@ class ClientConnectionPoolTest(utils.TestCase): - @mock.patch("keystoneclient.session.TCPKeepAliveAdapter") + @mock.patch("keystoneauth1.session.TCPKeepAliveAdapter") def test_get(self, mock_http_adapter): mock_http_adapter.side_effect = lambda: mock.Mock() pool = novaclient.client._ClientConnectionPool() diff --git a/novaclient/tests/unit/test_service_catalog.py b/novaclient/tests/unit/test_service_catalog.py index 33be77791..ff19dcd5b 100644 --- a/novaclient/tests/unit/test_service_catalog.py +++ b/novaclient/tests/unit/test_service_catalog.py @@ -11,7 +11,7 @@ # License for the specific language governing permissions and limitations # under the License. -from keystoneclient import fixture +from keystoneauth1 import fixture from novaclient import exceptions from novaclient import service_catalog diff --git a/novaclient/tests/unit/test_shell.py b/novaclient/tests/unit/test_shell.py index a32c14257..c3003877d 100644 --- a/novaclient/tests/unit/test_shell.py +++ b/novaclient/tests/unit/test_shell.py @@ -16,7 +16,7 @@ import sys import fixtures -from keystoneclient import fixture +from keystoneauth1 import fixture import mock import prettytable import requests_mock diff --git a/novaclient/tests/unit/v2/test_auth.py b/novaclient/tests/unit/v2/test_auth.py index 696428a4a..a62866a62 100644 --- a/novaclient/tests/unit/v2/test_auth.py +++ b/novaclient/tests/unit/v2/test_auth.py @@ -14,7 +14,7 @@ import copy import json -from keystoneclient import fixture +from keystoneauth1 import fixture import mock import requests diff --git a/novaclient/tests/unit/v2/test_client.py b/novaclient/tests/unit/v2/test_client.py index b4c59ce38..5bbbe51fb 100644 --- a/novaclient/tests/unit/v2/test_client.py +++ b/novaclient/tests/unit/v2/test_client.py @@ -12,7 +12,7 @@ import uuid -from keystoneclient import session +from keystoneauth1 import session from novaclient.tests.unit import utils from novaclient.v2 import client diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 05a0e74d4..8f0727d48 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -27,6 +27,7 @@ import os import sys import time +import warnings from oslo_utils import encodeutils from oslo_utils import strutils @@ -3874,10 +3875,11 @@ def ensure_service_catalog_present(cs): def do_endpoints(cs, _args): """Discover endpoints that get returned from the authenticate services.""" + warnings.warn( + "nova endpoints is deprecated, use openstack catalog list instead") if isinstance(cs.client, client.SessionClient): - auth = cs.client.auth - sc = auth.get_access(cs.client.session).service_catalog - for service in sc.get_data(): + access = cs.client.auth.get_access(cs.client.session) + for service in access.service_catalog.catalog: _print_endpoints(service, cs.client.region_name) else: ensure_service_catalog_present(cs) @@ -3926,12 +3928,14 @@ def _get_first_endpoint(endpoints, region): help=_('Wrap PKI tokens to a specified length, or 0 to disable.')) def do_credentials(cs, _args): """Show user credentials returned from auth.""" + warnings.warn( + "nova credentials is deprecated, use openstack client instead") if isinstance(cs.client, client.SessionClient): - auth = cs.client.auth - sc = auth.get_access(cs.client.session).service_catalog - utils.print_dict(sc.catalog['user'], 'User Credentials', + access = cs.client.auth.get_access(cs.client.session) + utils.print_dict(access._user, 'User Credentials', wrap=int(_args.wrap)) - utils.print_dict(sc.get_token(), 'Token', wrap=int(_args.wrap)) + if hasattr(access, '_token'): + utils.print_dict(access._token, 'Token', wrap=int(_args.wrap)) else: ensure_service_catalog_present(cs) catalog = cs.client.service_catalog.catalog diff --git a/releasenotes/notes/keystoneauth-8ec1e6be14cdbae3.yaml b/releasenotes/notes/keystoneauth-8ec1e6be14cdbae3.yaml new file mode 100644 index 000000000..d9e2d7ffa --- /dev/null +++ b/releasenotes/notes/keystoneauth-8ec1e6be14cdbae3.yaml @@ -0,0 +1,11 @@ +--- +features: +- keystoneauth plugins are now supported. +upgrade: +- novaclient now requires the keystoneauth library. +deprecations: +- novaclient auth strategy plugins are deprecated. Please use + keystoneauth auth plugins instead. +- nova credentials is deprecated. Please use openstack token issue +- nova endpoints is deprecated. Please use openstack catalog list + instead. diff --git a/requirements.txt b/requirements.txt index 32826add3..84d5f79f0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,7 @@ # process, which may cause wedges in the gate later. pbr>=1.6 argparse +keystoneauth1>=2.1.0 iso8601>=0.1.9 oslo.i18n>=1.5.0 # Apache-2.0 oslo.serialization>=1.10.0 # Apache-2.0 @@ -12,4 +13,3 @@ requests>=2.8.1 simplejson>=2.2.0 six>=1.9.0 Babel>=1.3 -python-keystoneclient!=1.8.0,>=1.6.0 diff --git a/test-requirements.txt b/test-requirements.txt index 0ab103545..8ffcf36cd 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -8,6 +8,7 @@ discover fixtures>=1.3.1 keyring>=5.5.1 mock>=1.2 +python-keystoneclient!=1.8.0,>=1.6.0 requests-mock>=0.7.0 # Apache-2.0 sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2 os-client-config!=1.6.2,>=1.4.0 From 0a736dd5df30267458532e6b10cddaa1a64fbd17 Mon Sep 17 00:00:00 2001 From: shu-mutou Date: Tue, 15 Dec 2015 16:26:20 +0900 Subject: [PATCH 0924/1705] Drop py33 support "Python 3.3 support is being dropped since OpenStack Liberty." written in following URL. https://wiki.openstack.org/wiki/Python3 And already the infra team and the oslo team are dropping py33 support from their projects. Since we rely on oslo for a lot of our work, and depend on infra for our CI, we should drop py33 support too. Change-Id: Ic9d4f64154a5418baa159a4efdcd9dd652d142ff Closes-Bug: #1526170 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index f27b5fc9d..82da64e9e 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ # noted to use py34 you need virtualenv >= 1.11.4 [tox] -envlist = py27,py33,py34,pypy,pep8,docs +envlist = py27,py34,pypy,pep8,docs minversion = 1.6 skipsdist = True From 6f587155bb4c787eaa327e9ff4c40c4a1982733c Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Tue, 15 Dec 2015 19:00:06 +0000 Subject: [PATCH 0925/1705] Updated from global requirements Change-Id: I759f0730a31307bfb97811464d3d8a32461aae34 --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 32826add3..262d7127c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,9 +6,9 @@ argparse iso8601>=0.1.9 oslo.i18n>=1.5.0 # Apache-2.0 oslo.serialization>=1.10.0 # Apache-2.0 -oslo.utils!=3.1.0,>=2.8.0 # Apache-2.0 +oslo.utils>=3.2.0 # Apache-2.0 PrettyTable<0.8,>=0.7 -requests>=2.8.1 +requests!=2.9.0,>=2.8.1 simplejson>=2.2.0 six>=1.9.0 Babel>=1.3 From 56bc67d4bfcb15b6dc30feede2d51090a5ec81a4 Mon Sep 17 00:00:00 2001 From: "sonu.kumar" Date: Thu, 17 Dec 2015 16:35:02 +0530 Subject: [PATCH 0926/1705] Removes MANIFEST.in as it is not needed explicitely by PBR This patch removes `MANIFEST.in` file as pbr generates a sensible manifest from git files and some standard files and it removes the need for an explicit `MANIFEST.in` file. Change-Id: I749be997516b0d3af7bd1f6c1a4fce245b392d54 --- MANIFEST.in | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 MANIFEST.in diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 86ea96825..000000000 --- a/MANIFEST.in +++ /dev/null @@ -1,5 +0,0 @@ -include AUTHORS -include ChangeLog - -exclude .gitignore -exclude .gitreview From 6b0fe2a424630f6ebcb1cf0c42d5ac8590705875 Mon Sep 17 00:00:00 2001 From: jichenjc Date: Tue, 24 Nov 2015 23:37:58 +0800 Subject: [PATCH 0927/1705] Allow command line for virtual-interface-list There is virtual-interface-list API support in nova but no command line interface in novaclient, this patch adds it. Closes-Bug: 1522424 Change-Id: Ib3078125beb7beaa08a3408486db54e0d10763e6 --- .../v2/legacy/test_virtual_interface.py | 40 +++++++++++++++++++ novaclient/tests/unit/v2/test_shell.py | 4 ++ novaclient/v2/shell.py | 10 +++++ 3 files changed, 54 insertions(+) create mode 100644 novaclient/tests/functional/v2/legacy/test_virtual_interface.py diff --git a/novaclient/tests/functional/v2/legacy/test_virtual_interface.py b/novaclient/tests/functional/v2/legacy/test_virtual_interface.py new file mode 100644 index 000000000..30c090aa7 --- /dev/null +++ b/novaclient/tests/functional/v2/legacy/test_virtual_interface.py @@ -0,0 +1,40 @@ +# Copyright 2015 IBM Corp. +# 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 novaclient.tests.functional import base +from novaclient.v2 import shell + + +class TestConsolesNovaClient(base.ClientTestBase): + """Consoles functional tests.""" + + COMPUTE_API_VERSION = "2.1" + + def _create_server(self): + name = self.name_generate(prefix='server') + network = self.client.networks.list()[0] + server = self.client.servers.create( + name, self.image, self.flavor, nics=[{"net-id": network.id}]) + shell._poll_for_status( + self.client.servers.get, server.id, + 'building', ['active']) + self.addCleanup(server.delete) + return server + + def _test_virtual_interface_list(self, command): + server = self._create_server() + completed_command = command % server.id + self.nova(completed_command) + + def test_virtual_interface_list(self): + self._test_virtual_interface_list('virtual-interface-list %s') diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 7a6a8e2cc..67679d691 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2528,6 +2528,10 @@ def test_list_server_group_with_all_projects(self): self.run_command('server-group-list --all-projects') self.assert_called('GET', '/os-server-groups?all_projects') + def test_list_server_os_virtual_interfaces(self): + self.run_command('virtual-interface-list 1234') + self.assert_called('GET', '/servers/1234/os-virtual-interfaces') + def test_versions(self): exclusions = set([ 1, # Same as version 2.0 diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 5296084de..a9b0d46ec 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -36,6 +36,7 @@ import novaclient from novaclient import api_versions +from novaclient import base from novaclient import client from novaclient import exceptions from novaclient.i18n import _ @@ -4681,3 +4682,12 @@ def do_version_list(cs, args): print (_("\nServer supported API versions:")) utils.print_list(result, columns) + + +@cliutils.arg('server', metavar='', help=_('ID of server.')) +def do_virtual_interface_list(cs, args): + """Show virtual interface info about the given server.""" + server = _find_server(cs, args.server) + interface_list = cs.virtual_interfaces.list(base.getid(server)) + columns = ['Id', 'Mac address'] + utils.print_list(interface_list, columns) From bbdedc681d8c0c1a8347fb203809239517c55269 Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanino Date: Fri, 4 Dec 2015 17:40:04 -0500 Subject: [PATCH 0928/1705] Validation for arguments of list command passed by "--fields" When user specifies some attributes using "--fields" subcommand for "nova list" and "nova network-list", these attributes are shown in the list even if the server or network don't have these attributes in their object. Before passing attributes via "--fields", we should validate values and if non-existent attributes are included, error will be raised. Change-Id: I3eb4810993259515ee33a72e8bd99f9951721b60 Closes-Bug: #1522989 --- novaclient/tests/unit/v2/test_shell.py | 11 ++++++++ novaclient/v2/shell.py | 35 +++++++++++++++++++------- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 7a6a8e2cc..3fd39e5d8 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -954,6 +954,12 @@ def test_list_fields(self): self.assertIn('OS-EXT-MOD: Some Thing', output) self.assertIn('mod_some_thing_value', output) + def test_list_invalid_fields(self): + self.assertRaises(exceptions.CommandError, + self.run_command, + 'list --fields host,security_groups,' + 'OS-EXT-MOD:some_thing,invalid') + def test_list_with_marker(self): self.run_command('list --marker some-uuid') self.assert_called('GET', '/servers/detail?marker=some-uuid') @@ -1941,6 +1947,11 @@ def test_network_list_fields(self): self.assertIn('1234', output) self.assertIn('4ffc664c198e435e9853f2538fbcd7a7', output) + def test_network_list_invalid_fields(self): + self.assertRaises(exceptions.CommandError, + self.run_command, + 'network-list --fields vlan,project_id,invalid') + def test_network_show(self): self.run_command('network-show 1') self.assert_called('GET', '/os-networks/1') diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 5296084de..d425b8e5f 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -897,11 +897,19 @@ def do_network_list(cs, args): formatters = {} field_titles = [] + non_existent_fields = [] if args.fields: for field in args.fields.split(','): + if network_list and not hasattr(network_list[0], field): + non_existent_fields.append(field) + continue field_title, formatter = utils._make_field_formatter(field, {}) field_titles.append(field_title) formatters[field_title] = formatter + if non_existent_fields: + raise exceptions.CommandError( + _("Non-existent fields are specified: %s") + % non_existent_fields) columns = columns + field_titles utils.print_list(network_list, columns) @@ -1407,15 +1415,6 @@ def do_list(cs, args): filters = {'flavor': lambda f: f['id'], 'security_groups': utils._format_security_groups} - formatters = {} - field_titles = [] - if args.fields: - for field in args.fields.split(','): - field_title, formatter = utils._make_field_formatter(field, - filters) - field_titles.append(field_title) - formatters[field_title] = formatter - id_col = 'ID' detailed = not args.minimal @@ -1446,6 +1445,24 @@ def do_list(cs, args): ('hostId', 'host_id')] _translate_keys(servers, convert) _translate_extended_states(servers) + + formatters = {} + field_titles = [] + non_existent_fields = [] + if args.fields: + for field in args.fields.split(','): + if servers and not hasattr(servers[0], field): + non_existent_fields.append(field) + continue + field_title, formatter = utils._make_field_formatter(field, + filters) + field_titles.append(field_title) + formatters[field_title] = formatter + if non_existent_fields: + raise exceptions.CommandError( + _("Non-existent fields are specified: %s") + % non_existent_fields) + if args.minimal: columns = [ id_col, From 01961d68a23b90896f6f0fbe136112f0226f2f5c Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Thu, 17 Dec 2015 16:24:33 +0200 Subject: [PATCH 0929/1705] Cleanup needless code from oslo-incubator oslo-incubator was deprecated and all modules used by novaclient were removed from oslo-incubator master branch. This patch removes all unused code from novaclient's repo. Change-Id: Ie3c542d19da6ed4d25ec490faf6a86a82f48cd34 --- novaclient/base.py | 145 ++++- novaclient/extension.py | 3 +- novaclient/openstack/common/_i18n.py | 45 -- .../openstack/common/apiclient/__init__.py | 0 novaclient/openstack/common/apiclient/auth.py | 234 -------- novaclient/openstack/common/apiclient/base.py | 533 ------------------ .../openstack/common/apiclient/client.py | 388 ------------- .../openstack/common/apiclient/exceptions.py | 479 ---------------- .../openstack/common/apiclient/fake_client.py | 189 ------- .../openstack/common/apiclient/utils.py | 100 ---- novaclient/openstack/common/cliutils.py | 260 +-------- novaclient/shell.py | 48 +- novaclient/tests/unit/test_shell.py | 3 +- novaclient/utils.py | 92 ++- novaclient/v2/shell.py | 4 +- openstack-common.conf | 8 - 16 files changed, 255 insertions(+), 2276 deletions(-) delete mode 100644 novaclient/openstack/common/_i18n.py delete mode 100644 novaclient/openstack/common/apiclient/__init__.py delete mode 100644 novaclient/openstack/common/apiclient/auth.py delete mode 100644 novaclient/openstack/common/apiclient/base.py delete mode 100644 novaclient/openstack/common/apiclient/client.py delete mode 100644 novaclient/openstack/common/apiclient/exceptions.py delete mode 100644 novaclient/openstack/common/apiclient/fake_client.py delete mode 100644 novaclient/openstack/common/apiclient/utils.py delete mode 100644 openstack-common.conf diff --git a/novaclient/base.py b/novaclient/base.py index e86e2290c..4958500eb 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -21,18 +21,17 @@ import abc import contextlib +import copy import hashlib import inspect import os import threading +from oslo_utils import strutils import six from novaclient import exceptions -from novaclient.openstack.common.apiclient import base -from novaclient.openstack.common import cliutils - -Resource = base.Resource +from novaclient import utils def getid(obj): @@ -47,7 +46,135 @@ def getid(obj): return obj -class Manager(base.HookableMixin): +# TODO(aababilov): call run_hooks() in HookableMixin's child classes +class HookableMixin(object): + """Mixin so classes can register and run hooks.""" + _hooks_map = {} + + @classmethod + def add_hook(cls, hook_type, hook_func): + """Add a new hook of specified type. + + :param cls: class that registers hooks + :param hook_type: hook type, e.g., '__pre_parse_args__' + :param hook_func: hook function + """ + if hook_type not in cls._hooks_map: + cls._hooks_map[hook_type] = [] + + cls._hooks_map[hook_type].append(hook_func) + + @classmethod + def run_hooks(cls, hook_type, *args, **kwargs): + """Run all hooks of specified type. + + :param cls: class that registers hooks + :param hook_type: hook type, e.g., '__pre_parse_args__' + :param args: args to be passed to every hook function + :param kwargs: kwargs to be passed to every hook function + """ + hook_funcs = cls._hooks_map.get(hook_type) or [] + for hook_func in hook_funcs: + hook_func(*args, **kwargs) + + +class Resource(object): + """Base class for OpenStack resources (tenant, user, etc.). + + This is pretty much just a bag for attributes. + """ + + HUMAN_ID = False + NAME_ATTR = 'name' + + def __init__(self, manager, info, loaded=False): + """Populate and bind to a manager. + + :param manager: BaseManager object + :param info: dictionary representing resource attributes + :param loaded: prevent lazy-loading if set to True + """ + self.manager = manager + self._info = info + self._add_details(info) + self._loaded = loaded + + def __repr__(self): + reprkeys = sorted(k + for k in self.__dict__.keys() + if k[0] != '_' and k != 'manager') + info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys) + return "<%s %s>" % (self.__class__.__name__, info) + + @property + def human_id(self): + """Human-readable ID which can be used for bash completion. + """ + if self.HUMAN_ID: + name = getattr(self, self.NAME_ATTR, None) + if name is not None: + return strutils.to_slug(name) + return None + + def _add_details(self, info): + for (k, v) in six.iteritems(info): + try: + setattr(self, k, v) + self._info[k] = v + except AttributeError: + # In this case we already defined the attribute on the class + pass + + def __getattr__(self, k): + if k not in self.__dict__: + # NOTE(bcwaldon): disallow lazy-loading if already loaded once + if not self.is_loaded(): + self.get() + return self.__getattr__(k) + + raise AttributeError(k) + else: + return self.__dict__[k] + + def get(self): + """Support for lazy loading details. + + Some clients, such as novaclient have the option to lazy load the + details, details which can be loaded with this function. + """ + # set_loaded() first ... so if we have to bail, we know we tried. + self.set_loaded(True) + if not hasattr(self.manager, 'get'): + return + + new = self.manager.get(self.id) + if new: + self._add_details(new._info) + if self.manager.client.last_request_id: + self._add_details( + {'x_request_id': self.manager.client.last_request_id}) + + def __eq__(self, other): + if not isinstance(other, Resource): + return NotImplemented + # two resources of different types are not equal + if not isinstance(other, self.__class__): + return False + if hasattr(self, 'id') and hasattr(other, 'id'): + return self.id == other.id + return self._info == other._info + + def is_loaded(self): + return self._loaded + + def set_loaded(self, val): + self._loaded = val + + def to_dict(self): + return copy.deepcopy(self._info) + + +class Manager(HookableMixin): """Manager for API service. Managers interact with a particular type of API (servers, flavors, images, @@ -119,13 +246,13 @@ def completion_cache(self, cache_type, obj_class, mode): # NOTE(wryan): This lock protects read and write access to the # completion caches with self.cache_lock: - base_dir = cliutils.env('NOVACLIENT_UUID_CACHE_DIR', - default="~/.novaclient") + base_dir = utils.env('NOVACLIENT_UUID_CACHE_DIR', + default="~/.novaclient") # NOTE(sirp): Keep separate UUID caches for each username + # endpoint pair - username = cliutils.env('OS_USERNAME', 'NOVA_USERNAME') - url = cliutils.env('OS_URL', 'NOVA_URL') + username = utils.env('OS_USERNAME', 'NOVA_USERNAME') + url = utils.env('OS_URL', 'NOVA_URL') uniqifier = hashlib.md5(username.encode('utf-8') + url.encode('utf-8')).hexdigest() diff --git a/novaclient/extension.py b/novaclient/extension.py index 9cfcc13d9..e297a636f 100644 --- a/novaclient/extension.py +++ b/novaclient/extension.py @@ -14,11 +14,10 @@ # under the License. from novaclient import base -from novaclient.openstack.common.apiclient import base as common_base from novaclient import utils -class Extension(common_base.HookableMixin): +class Extension(base.HookableMixin): """Extension descriptor.""" SUPPORTED_HOOKS = ('__pre_parse_args__', '__post_parse_args__') diff --git a/novaclient/openstack/common/_i18n.py b/novaclient/openstack/common/_i18n.py deleted file mode 100644 index c07496727..000000000 --- a/novaclient/openstack/common/_i18n.py +++ /dev/null @@ -1,45 +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. - -"""oslo.i18n integration module. - -See http://docs.openstack.org/developer/oslo.i18n/usage.html - -""" - -try: - import oslo_i18n - - # NOTE(dhellmann): This reference to o-s-l-o will be replaced by the - # application name when this module is synced into the separate - # repository. It is OK to have more than one translation function - # using the same domain, since there will still only be one message - # catalog. - _translators = oslo_i18n.TranslatorFactory(domain='novaclient') - - # The primary translation function using the well-known name "_" - _ = _translators.primary - - # Translators for log levels. - # - # The abbreviated names are meant to reflect the usual use of a short - # name like '_'. The "L" is for "log" and the other letter comes from - # the level. - _LI = _translators.log_info - _LW = _translators.log_warning - _LE = _translators.log_error - _LC = _translators.log_critical -except ImportError: - # NOTE(dims): Support for cases where a project wants to use - # code from oslo-incubator, but is not ready to be internationalized - # (like tempest) - _ = _LI = _LW = _LE = _LC = lambda x: x diff --git a/novaclient/openstack/common/apiclient/__init__.py b/novaclient/openstack/common/apiclient/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/novaclient/openstack/common/apiclient/auth.py b/novaclient/openstack/common/apiclient/auth.py deleted file mode 100644 index 68ed09fb5..000000000 --- a/novaclient/openstack/common/apiclient/auth.py +++ /dev/null @@ -1,234 +0,0 @@ -# Copyright 2013 OpenStack Foundation -# Copyright 2013 Spanish National Research Council. -# All Rights Reserved. -# -# 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. - -# E0202: An attribute inherited from %s hide this method -# pylint: disable=E0202 - -######################################################################## -# -# THIS MODULE IS DEPRECATED -# -# Please refer to -# https://etherpad.openstack.org/p/kilo-novaclient-library-proposals for -# the discussion leading to this deprecation. -# -# We recommend checking out the python-openstacksdk project -# (https://launchpad.net/python-openstacksdk) instead. -# -######################################################################## - -import abc -import argparse -import os - -import six -from stevedore import extension - -from novaclient.openstack.common.apiclient import exceptions - - -_discovered_plugins = {} - - -def discover_auth_systems(): - """Discover the available auth-systems. - - This won't take into account the old style auth-systems. - """ - global _discovered_plugins - _discovered_plugins = {} - - def add_plugin(ext): - _discovered_plugins[ext.name] = ext.plugin - - ep_namespace = "novaclient.openstack.common.apiclient.auth" - mgr = extension.ExtensionManager(ep_namespace) - mgr.map(add_plugin) - - -def load_auth_system_opts(parser): - """Load options needed by the available auth-systems into a parser. - - This function will try to populate the parser with options from the - available plugins. - """ - group = parser.add_argument_group("Common auth options") - BaseAuthPlugin.add_common_opts(group) - for name, auth_plugin in six.iteritems(_discovered_plugins): - group = parser.add_argument_group( - "Auth-system '%s' options" % name, - conflict_handler="resolve") - auth_plugin.add_opts(group) - - -def load_plugin(auth_system): - try: - plugin_class = _discovered_plugins[auth_system] - except KeyError: - raise exceptions.AuthSystemNotFound(auth_system) - return plugin_class(auth_system=auth_system) - - -def load_plugin_from_args(args): - """Load required plugin and populate it with options. - - Try to guess auth system if it is not specified. Systems are tried in - alphabetical order. - - :type args: argparse.Namespace - :raises: AuthPluginOptionsMissing - """ - auth_system = args.os_auth_system - if auth_system: - plugin = load_plugin(auth_system) - plugin.parse_opts(args) - plugin.sufficient_options() - return plugin - - for plugin_auth_system in sorted(six.iterkeys(_discovered_plugins)): - plugin_class = _discovered_plugins[plugin_auth_system] - plugin = plugin_class() - plugin.parse_opts(args) - try: - plugin.sufficient_options() - except exceptions.AuthPluginOptionsMissing: - continue - return plugin - raise exceptions.AuthPluginOptionsMissing(["auth_system"]) - - -@six.add_metaclass(abc.ABCMeta) -class BaseAuthPlugin(object): - """Base class for authentication plugins. - - An authentication plugin needs to override at least the authenticate - method to be a valid plugin. - """ - - auth_system = None - opt_names = [] - common_opt_names = [ - "auth_system", - "username", - "password", - "tenant_name", - "token", - "auth_url", - ] - - def __init__(self, auth_system=None, **kwargs): - self.auth_system = auth_system or self.auth_system - self.opts = dict((name, kwargs.get(name)) - for name in self.opt_names) - - @staticmethod - def _parser_add_opt(parser, opt): - """Add an option to parser in two variants. - - :param opt: option name (with underscores) - """ - dashed_opt = opt.replace("_", "-") - env_var = "OS_%s" % opt.upper() - arg_default = os.environ.get(env_var, "") - arg_help = "Defaults to env[%s]." % env_var - parser.add_argument( - "--os-%s" % dashed_opt, - metavar="<%s>" % dashed_opt, - default=arg_default, - help=arg_help) - parser.add_argument( - "--os_%s" % opt, - metavar="<%s>" % dashed_opt, - help=argparse.SUPPRESS) - - @classmethod - def add_opts(cls, parser): - """Populate the parser with the options for this plugin. - """ - for opt in cls.opt_names: - # use `BaseAuthPlugin.common_opt_names` since it is never - # changed in child classes - if opt not in BaseAuthPlugin.common_opt_names: - cls._parser_add_opt(parser, opt) - - @classmethod - def add_common_opts(cls, parser): - """Add options that are common for several plugins. - """ - for opt in cls.common_opt_names: - cls._parser_add_opt(parser, opt) - - @staticmethod - def get_opt(opt_name, args): - """Return option name and value. - - :param opt_name: name of the option, e.g., "username" - :param args: parsed arguments - """ - return (opt_name, getattr(args, "os_%s" % opt_name, None)) - - def parse_opts(self, args): - """Parse the actual auth-system options if any. - - This method is expected to populate the attribute `self.opts` with a - dict containing the options and values needed to make authentication. - """ - self.opts.update(dict(self.get_opt(opt_name, args) - for opt_name in self.opt_names)) - - def authenticate(self, http_client): - """Authenticate using plugin defined method. - - The method usually analyses `self.opts` and performs - a request to authentication server. - - :param http_client: client object that needs authentication - :type http_client: HTTPClient - :raises: AuthorizationFailure - """ - self.sufficient_options() - self._do_authenticate(http_client) - - @abc.abstractmethod - def _do_authenticate(self, http_client): - """Protected method for authentication. - """ - - def sufficient_options(self): - """Check if all required options are present. - - :raises: AuthPluginOptionsMissing - """ - missing = [opt - for opt in self.opt_names - if not self.opts.get(opt)] - if missing: - raise exceptions.AuthPluginOptionsMissing(missing) - - @abc.abstractmethod - def token_and_endpoint(self, endpoint_type, service_type): - """Return token and endpoint. - - :param service_type: Service type of the endpoint - :type service_type: string - :param endpoint_type: Type of endpoint. - Possible values: public or publicURL, - internal or internalURL, - admin or adminURL - :type endpoint_type: string - :returns: tuple of token and endpoint strings - :raises: EndpointException - """ diff --git a/novaclient/openstack/common/apiclient/base.py b/novaclient/openstack/common/apiclient/base.py deleted file mode 100644 index 89688151f..000000000 --- a/novaclient/openstack/common/apiclient/base.py +++ /dev/null @@ -1,533 +0,0 @@ -# Copyright 2010 Jacob Kaplan-Moss -# Copyright 2011 OpenStack Foundation -# Copyright 2012 Grid Dynamics -# Copyright 2013 OpenStack Foundation -# All Rights Reserved. -# -# 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. - -""" -Base utilities to build API operation managers and objects on top of. -""" - -######################################################################## -# -# THIS MODULE IS DEPRECATED -# -# Please refer to -# https://etherpad.openstack.org/p/kilo-novaclient-library-proposals for -# the discussion leading to this deprecation. -# -# We recommend checking out the python-openstacksdk project -# (https://launchpad.net/python-openstacksdk) instead. -# -######################################################################## - - -# E1102: %s is not callable -# pylint: disable=E1102 - -import abc -import copy - -from oslo_utils import strutils -import six -from six.moves.urllib import parse - -from novaclient.openstack.common._i18n import _ -from novaclient.openstack.common.apiclient import exceptions - - -def getid(obj): - """Return id if argument is a Resource. - - Abstracts the common pattern of allowing both an object or an object's ID - (UUID) as a parameter when dealing with relationships. - """ - try: - if obj.uuid: - return obj.uuid - except AttributeError: - pass - try: - return obj.id - except AttributeError: - return obj - - -# TODO(aababilov): call run_hooks() in HookableMixin's child classes -class HookableMixin(object): - """Mixin so classes can register and run hooks.""" - _hooks_map = {} - - @classmethod - def add_hook(cls, hook_type, hook_func): - """Add a new hook of specified type. - - :param cls: class that registers hooks - :param hook_type: hook type, e.g., '__pre_parse_args__' - :param hook_func: hook function - """ - if hook_type not in cls._hooks_map: - cls._hooks_map[hook_type] = [] - - cls._hooks_map[hook_type].append(hook_func) - - @classmethod - def run_hooks(cls, hook_type, *args, **kwargs): - """Run all hooks of specified type. - - :param cls: class that registers hooks - :param hook_type: hook type, e.g., '__pre_parse_args__' - :param args: args to be passed to every hook function - :param kwargs: kwargs to be passed to every hook function - """ - hook_funcs = cls._hooks_map.get(hook_type) or [] - for hook_func in hook_funcs: - hook_func(*args, **kwargs) - - -class BaseManager(HookableMixin): - """Basic manager type providing common operations. - - Managers interact with a particular type of API (servers, flavors, images, - etc.) and provide CRUD operations for them. - """ - resource_class = None - - def __init__(self, client): - """Initializes BaseManager with `client`. - - :param client: instance of BaseClient descendant for HTTP requests - """ - super(BaseManager, self).__init__() - self.client = client - - def _list(self, url, response_key=None, obj_class=None, json=None): - """List the collection. - - :param url: a partial URL, e.g., '/servers' - :param response_key: the key to be looked up in response dictionary, - e.g., 'servers'. If response_key is None - all response body - will be used. - :param obj_class: class for constructing the returned objects - (self.resource_class will be used by default) - :param json: data that will be encoded as JSON and passed in POST - request (GET will be sent by default) - """ - if json: - body = self.client.post(url, json=json).json() - else: - body = self.client.get(url).json() - - if obj_class is None: - obj_class = self.resource_class - - data = body[response_key] if response_key is not None else body - # NOTE(ja): keystone returns values as list as {'values': [ ... ]} - # unlike other services which just return the list... - try: - data = data['values'] - except (KeyError, TypeError): - pass - - return [obj_class(self, res, loaded=True) for res in data if res] - - def _get(self, url, response_key=None): - """Get an object from collection. - - :param url: a partial URL, e.g., '/servers' - :param response_key: the key to be looked up in response dictionary, - e.g., 'server'. If response_key is None - all response body - will be used. - """ - body = self.client.get(url).json() - data = body[response_key] if response_key is not None else body - return self.resource_class(self, data, loaded=True) - - def _head(self, url): - """Retrieve request headers for an object. - - :param url: a partial URL, e.g., '/servers' - """ - resp = self.client.head(url) - return resp.status_code == 204 - - def _post(self, url, json, response_key=None, return_raw=False): - """Create an object. - - :param url: a partial URL, e.g., '/servers' - :param json: data that will be encoded as JSON and passed in POST - request (GET will be sent by default) - :param response_key: the key to be looked up in response dictionary, - e.g., 'server'. If response_key is None - all response body - will be used. - :param return_raw: flag to force returning raw JSON instead of - Python object of self.resource_class - """ - body = self.client.post(url, json=json).json() - data = body[response_key] if response_key is not None else body - if return_raw: - return data - return self.resource_class(self, data) - - def _put(self, url, json=None, response_key=None): - """Update an object with PUT method. - - :param url: a partial URL, e.g., '/servers' - :param json: data that will be encoded as JSON and passed in POST - request (GET will be sent by default) - :param response_key: the key to be looked up in response dictionary, - e.g., 'servers'. If response_key is None - all response body - will be used. - """ - resp = self.client.put(url, json=json) - # PUT requests may not return a body - if resp.content: - body = resp.json() - if response_key is not None: - return self.resource_class(self, body[response_key]) - else: - return self.resource_class(self, body) - - def _patch(self, url, json=None, response_key=None): - """Update an object with PATCH method. - - :param url: a partial URL, e.g., '/servers' - :param json: data that will be encoded as JSON and passed in POST - request (GET will be sent by default) - :param response_key: the key to be looked up in response dictionary, - e.g., 'servers'. If response_key is None - all response body - will be used. - """ - body = self.client.patch(url, json=json).json() - if response_key is not None: - return self.resource_class(self, body[response_key]) - else: - return self.resource_class(self, body) - - def _delete(self, url): - """Delete an object. - - :param url: a partial URL, e.g., '/servers/my-server' - """ - return self.client.delete(url) - - -@six.add_metaclass(abc.ABCMeta) -class ManagerWithFind(BaseManager): - """Manager with additional `find()`/`findall()` methods.""" - - @abc.abstractmethod - def list(self): - pass - - def find(self, **kwargs): - """Find a single item with attributes matching ``**kwargs``. - - This isn't very efficient: it loads the entire list then filters on - the Python side. - """ - matches = self.findall(**kwargs) - num_matches = len(matches) - if num_matches == 0: - msg = _("No %(name)s matching %(args)s.") % { - 'name': self.resource_class.__name__, - 'args': kwargs - } - raise exceptions.NotFound(msg) - elif num_matches > 1: - raise exceptions.NoUniqueMatch() - else: - return matches[0] - - def findall(self, **kwargs): - """Find all items with attributes matching ``**kwargs``. - - This isn't very efficient: it loads the entire list then filters on - the Python side. - """ - found = [] - searches = kwargs.items() - - for obj in self.list(): - try: - if all(getattr(obj, attr) == value - for (attr, value) in searches): - found.append(obj) - except AttributeError: - continue - - return found - - -class CrudManager(BaseManager): - """Base manager class for manipulating entities. - - Children of this class are expected to define a `collection_key` and `key`. - - - `collection_key`: Usually a plural noun by convention (e.g. `entities`); - used to refer collections in both URL's (e.g. `/v3/entities`) and JSON - objects containing a list of member resources (e.g. `{'entities': [{}, - {}, {}]}`). - - `key`: Usually a singular noun by convention (e.g. `entity`); used to - refer to an individual member of the collection. - - """ - collection_key = None - key = None - - def build_url(self, base_url=None, **kwargs): - """Builds a resource URL for the given kwargs. - - Given an example collection where `collection_key = 'entities'` and - `key = 'entity'`, the following URL's could be generated. - - By default, the URL will represent a collection of entities, e.g.:: - - /entities - - If kwargs contains an `entity_id`, then the URL will represent a - specific member, e.g.:: - - /entities/{entity_id} - - :param base_url: if provided, the generated URL will be appended to it - """ - url = base_url if base_url is not None else '' - - url += '/%s' % self.collection_key - - # do we have a specific entity? - entity_id = kwargs.get('%s_id' % self.key) - if entity_id is not None: - url += '/%s' % entity_id - - return url - - def _filter_kwargs(self, kwargs): - """Drop null values and handle ids.""" - for key, ref in six.iteritems(kwargs.copy()): - if ref is None: - kwargs.pop(key) - else: - if isinstance(ref, Resource): - kwargs.pop(key) - kwargs['%s_id' % key] = getid(ref) - return kwargs - - def create(self, **kwargs): - kwargs = self._filter_kwargs(kwargs) - return self._post( - self.build_url(**kwargs), - {self.key: kwargs}, - self.key) - - def get(self, **kwargs): - kwargs = self._filter_kwargs(kwargs) - return self._get( - self.build_url(**kwargs), - self.key) - - def head(self, **kwargs): - kwargs = self._filter_kwargs(kwargs) - return self._head(self.build_url(**kwargs)) - - def list(self, base_url=None, **kwargs): - """List the collection. - - :param base_url: if provided, the generated URL will be appended to it - """ - kwargs = self._filter_kwargs(kwargs) - - return self._list( - '%(base_url)s%(query)s' % { - 'base_url': self.build_url(base_url=base_url, **kwargs), - 'query': '?%s' % parse.urlencode(kwargs) if kwargs else '', - }, - self.collection_key) - - def put(self, base_url=None, **kwargs): - """Update an element. - - :param base_url: if provided, the generated URL will be appended to it - """ - kwargs = self._filter_kwargs(kwargs) - - return self._put(self.build_url(base_url=base_url, **kwargs)) - - def update(self, **kwargs): - kwargs = self._filter_kwargs(kwargs) - params = kwargs.copy() - params.pop('%s_id' % self.key) - - return self._patch( - self.build_url(**kwargs), - {self.key: params}, - self.key) - - def delete(self, **kwargs): - kwargs = self._filter_kwargs(kwargs) - - return self._delete( - self.build_url(**kwargs)) - - def find(self, base_url=None, **kwargs): - """Find a single item with attributes matching ``**kwargs``. - - :param base_url: if provided, the generated URL will be appended to it - """ - kwargs = self._filter_kwargs(kwargs) - - rl = self._list( - '%(base_url)s%(query)s' % { - 'base_url': self.build_url(base_url=base_url, **kwargs), - 'query': '?%s' % parse.urlencode(kwargs) if kwargs else '', - }, - self.collection_key) - num = len(rl) - - if num == 0: - msg = _("No %(name)s matching %(args)s.") % { - 'name': self.resource_class.__name__, - 'args': kwargs - } - raise exceptions.NotFound(msg) - elif num > 1: - raise exceptions.NoUniqueMatch - else: - return rl[0] - - -class Extension(HookableMixin): - """Extension descriptor.""" - - SUPPORTED_HOOKS = ('__pre_parse_args__', '__post_parse_args__') - manager_class = None - - def __init__(self, name, module): - super(Extension, self).__init__() - self.name = name - self.module = module - self._parse_extension_module() - - def _parse_extension_module(self): - self.manager_class = None - for attr_name, attr_value in self.module.__dict__.items(): - if attr_name in self.SUPPORTED_HOOKS: - self.add_hook(attr_name, attr_value) - else: - try: - if issubclass(attr_value, BaseManager): - self.manager_class = attr_value - except TypeError: - pass - - def __repr__(self): - return "" % self.name - - -class Resource(object): - """Base class for OpenStack resources (tenant, user, etc.). - - This is pretty much just a bag for attributes. - """ - - HUMAN_ID = False - NAME_ATTR = 'name' - - def __init__(self, manager, info, loaded=False): - """Populate and bind to a manager. - - :param manager: BaseManager object - :param info: dictionary representing resource attributes - :param loaded: prevent lazy-loading if set to True - """ - self.manager = manager - self._info = info - self._add_details(info) - self._loaded = loaded - - def __repr__(self): - reprkeys = sorted(k - for k in self.__dict__.keys() - if k[0] != '_' and k != 'manager') - info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys) - return "<%s %s>" % (self.__class__.__name__, info) - - @property - def human_id(self): - """Human-readable ID which can be used for bash completion. - """ - if self.HUMAN_ID: - name = getattr(self, self.NAME_ATTR, None) - if name is not None: - return strutils.to_slug(name) - return None - - def _add_details(self, info): - for (k, v) in six.iteritems(info): - try: - setattr(self, k, v) - self._info[k] = v - except AttributeError: - # In this case we already defined the attribute on the class - pass - - def __getattr__(self, k): - if k not in self.__dict__: - # NOTE(bcwaldon): disallow lazy-loading if already loaded once - if not self.is_loaded(): - self.get() - return self.__getattr__(k) - - raise AttributeError(k) - else: - return self.__dict__[k] - - def get(self): - """Support for lazy loading details. - - Some clients, such as novaclient have the option to lazy load the - details, details which can be loaded with this function. - """ - # set_loaded() first ... so if we have to bail, we know we tried. - self.set_loaded(True) - if not hasattr(self.manager, 'get'): - return - - new = self.manager.get(self.id) - if new: - self._add_details(new._info) - if self.manager.client.last_request_id: - self._add_details( - {'x_request_id': self.manager.client.last_request_id}) - - def __eq__(self, other): - if not isinstance(other, Resource): - return NotImplemented - # two resources of different types are not equal - if not isinstance(other, self.__class__): - return False - if hasattr(self, 'id') and hasattr(other, 'id'): - return self.id == other.id - return self._info == other._info - - def is_loaded(self): - return self._loaded - - def set_loaded(self, val): - self._loaded = val - - def to_dict(self): - return copy.deepcopy(self._info) diff --git a/novaclient/openstack/common/apiclient/client.py b/novaclient/openstack/common/apiclient/client.py deleted file mode 100644 index fa317c2da..000000000 --- a/novaclient/openstack/common/apiclient/client.py +++ /dev/null @@ -1,388 +0,0 @@ -# Copyright 2010 Jacob Kaplan-Moss -# Copyright 2011 OpenStack Foundation -# Copyright 2011 Piston Cloud Computing, Inc. -# Copyright 2013 Alessio Ababilov -# Copyright 2013 Grid Dynamics -# Copyright 2013 OpenStack Foundation -# All Rights Reserved. -# -# 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. - -""" -OpenStack Client interface. Handles the REST calls and responses. -""" - -# E0202: An attribute inherited from %s hide this method -# pylint: disable=E0202 - -import hashlib -import logging -import time - -try: - import simplejson as json -except ImportError: - import json - -from oslo_utils import encodeutils -from oslo_utils import importutils -import requests - -from novaclient.openstack.common._i18n import _ -from novaclient.openstack.common.apiclient import exceptions - -_logger = logging.getLogger(__name__) -SENSITIVE_HEADERS = ('X-Auth-Token', 'X-Subject-Token',) - - -class HTTPClient(object): - """This client handles sending HTTP requests to OpenStack servers. - - Features: - - - share authentication information between several clients to different - services (e.g., for compute and image clients); - - reissue authentication request for expired tokens; - - encode/decode JSON bodies; - - raise exceptions on HTTP errors; - - pluggable authentication; - - store authentication information in a keyring; - - store time spent for requests; - - register clients for particular services, so one can use - `http_client.identity` or `http_client.compute`; - - log requests and responses in a format that is easy to copy-and-paste - into terminal and send the same request with curl. - """ - - user_agent = "novaclient.openstack.common.apiclient" - - def __init__(self, - auth_plugin, - region_name=None, - endpoint_type="publicURL", - original_ip=None, - verify=True, - cert=None, - timeout=None, - timings=False, - keyring_saver=None, - debug=False, - user_agent=None, - http=None): - self.auth_plugin = auth_plugin - - self.endpoint_type = endpoint_type - self.region_name = region_name - - self.original_ip = original_ip - self.timeout = timeout - self.verify = verify - self.cert = cert - - self.keyring_saver = keyring_saver - self.debug = debug - self.user_agent = user_agent or self.user_agent - - self.times = [] # [("item", starttime, endtime), ...] - self.timings = timings - - # requests within the same session can reuse TCP connections from pool - self.http = http or requests.Session() - - self.cached_token = None - self.last_request_id = None - - def _safe_header(self, name, value): - if name in SENSITIVE_HEADERS: - # because in python3 byte string handling is ... ug - v = value.encode('utf-8') - h = hashlib.sha1(v) - d = h.hexdigest() - return encodeutils.safe_decode(name), "{SHA1}%s" % d - else: - return (encodeutils.safe_decode(name), - encodeutils.safe_decode(value)) - - def _http_log_req(self, method, url, kwargs): - if not self.debug: - return - - string_parts = [ - "curl -g -i", - "-X '%s'" % method, - "'%s'" % url, - ] - - for element in kwargs['headers']: - header = ("-H '%s: %s'" % - self._safe_header(element, kwargs['headers'][element])) - string_parts.append(header) - - _logger.debug("REQ: %s" % " ".join(string_parts)) - if 'data' in kwargs: - _logger.debug("REQ BODY: %s\n" % (kwargs['data'])) - - def _http_log_resp(self, resp): - if not self.debug: - return - _logger.debug( - "RESP: [%s] %s\n", - resp.status_code, - resp.headers) - if resp._content_consumed: - _logger.debug( - "RESP BODY: %s\n", - resp.text) - - def serialize(self, kwargs): - if kwargs.get('json') is not None: - kwargs['headers']['Content-Type'] = 'application/json' - kwargs['data'] = json.dumps(kwargs['json']) - try: - del kwargs['json'] - except KeyError: - pass - - def get_timings(self): - return self.times - - def reset_timings(self): - self.times = [] - - def request(self, method, url, **kwargs): - """Send an http request with the specified characteristics. - - Wrapper around `requests.Session.request` to handle tasks such as - setting headers, JSON encoding/decoding, and error handling. - - :param method: method of HTTP request - :param url: URL of HTTP request - :param kwargs: any other parameter that can be passed to - requests.Session.request (such as `headers`) or `json` - that will be encoded as JSON and used as `data` argument - """ - kwargs.setdefault("headers", {}) - kwargs["headers"]["User-Agent"] = self.user_agent - if self.original_ip: - kwargs["headers"]["Forwarded"] = "for=%s;by=%s" % ( - self.original_ip, self.user_agent) - if self.timeout is not None: - kwargs.setdefault("timeout", self.timeout) - kwargs.setdefault("verify", self.verify) - if self.cert is not None: - kwargs.setdefault("cert", self.cert) - self.serialize(kwargs) - - self._http_log_req(method, url, kwargs) - if self.timings: - start_time = time.time() - resp = self.http.request(method, url, **kwargs) - if self.timings: - self.times.append(("%s %s" % (method, url), - start_time, time.time())) - self._http_log_resp(resp) - - self.last_request_id = resp.headers.get('x-openstack-request-id') - - if resp.status_code >= 400: - _logger.debug( - "Request returned failure status: %s", - resp.status_code) - raise exceptions.from_response(resp, method, url) - - return resp - - @staticmethod - def concat_url(endpoint, url): - """Concatenate endpoint and final URL. - - E.g., "http://keystone/v2.0/" and "/tokens" are concatenated to - "http://keystone/v2.0/tokens". - - :param endpoint: the base URL - :param url: the final URL - """ - return "%s/%s" % (endpoint.rstrip("/"), url.strip("/")) - - def client_request(self, client, method, url, **kwargs): - """Send an http request using `client`'s endpoint and specified `url`. - - If request was rejected as unauthorized (possibly because the token is - expired), issue one authorization attempt and send the request once - again. - - :param client: instance of BaseClient descendant - :param method: method of HTTP request - :param url: URL of HTTP request - :param kwargs: any other parameter that can be passed to - `HTTPClient.request` - """ - - filter_args = { - "endpoint_type": client.endpoint_type or self.endpoint_type, - "service_type": client.service_type, - } - token, endpoint = (self.cached_token, client.cached_endpoint) - just_authenticated = False - if not (token and endpoint): - try: - token, endpoint = self.auth_plugin.token_and_endpoint( - **filter_args) - except exceptions.EndpointException: - pass - if not (token and endpoint): - self.authenticate() - just_authenticated = True - token, endpoint = self.auth_plugin.token_and_endpoint( - **filter_args) - if not (token and endpoint): - raise exceptions.AuthorizationFailure( - _("Cannot find endpoint or token for request")) - - old_token_endpoint = (token, endpoint) - kwargs.setdefault("headers", {})["X-Auth-Token"] = token - self.cached_token = token - client.cached_endpoint = endpoint - # Perform the request once. If we get Unauthorized, then it - # might be because the auth token expired, so try to - # re-authenticate and try again. If it still fails, bail. - try: - return self.request( - method, self.concat_url(endpoint, url), **kwargs) - except exceptions.Unauthorized as unauth_ex: - if just_authenticated: - raise - self.cached_token = None - client.cached_endpoint = None - if self.auth_plugin.opts.get('token'): - self.auth_plugin.opts['token'] = None - if self.auth_plugin.opts.get('endpoint'): - self.auth_plugin.opts['endpoint'] = None - self.authenticate() - try: - token, endpoint = self.auth_plugin.token_and_endpoint( - **filter_args) - except exceptions.EndpointException: - raise unauth_ex - if (not (token and endpoint) or - old_token_endpoint == (token, endpoint)): - raise unauth_ex - self.cached_token = token - client.cached_endpoint = endpoint - kwargs["headers"]["X-Auth-Token"] = token - return self.request( - method, self.concat_url(endpoint, url), **kwargs) - - def add_client(self, base_client_instance): - """Add a new instance of :class:`BaseClient` descendant. - - `self` will store a reference to `base_client_instance`. - - Example: - - >>> def test_clients(): - ... from keystoneclient.auth import keystone - ... from openstack.common.apiclient import client - ... auth = keystone.KeystoneAuthPlugin( - ... username="user", password="pass", tenant_name="tenant", - ... auth_url="http://auth:5000/v2.0") - ... openstack_client = client.HTTPClient(auth) - ... # create nova client - ... from novaclient.v1_1 import client - ... client.Client(openstack_client) - ... # create keystone client - ... from keystoneclient.v2_0 import client - ... client.Client(openstack_client) - ... # use them - ... openstack_client.identity.tenants.list() - ... openstack_client.compute.servers.list() - """ - service_type = base_client_instance.service_type - if service_type and not hasattr(self, service_type): - setattr(self, service_type, base_client_instance) - - def authenticate(self): - self.auth_plugin.authenticate(self) - # Store the authentication results in the keyring for later requests - if self.keyring_saver: - self.keyring_saver.save(self) - - -class BaseClient(object): - """Top-level object to access the OpenStack API. - - This client uses :class:`HTTPClient` to send requests. :class:`HTTPClient` - will handle a bunch of issues such as authentication. - """ - - service_type = None - endpoint_type = None # "publicURL" will be used - cached_endpoint = None - - def __init__(self, http_client, extensions=None): - self.http_client = http_client - http_client.add_client(self) - - # Add in any extensions... - if extensions: - for extension in extensions: - if extension.manager_class: - setattr(self, extension.name, - extension.manager_class(self)) - - def client_request(self, method, url, **kwargs): - return self.http_client.client_request( - self, method, url, **kwargs) - - @property - def last_request_id(self): - return self.http_client.last_request_id - - def head(self, url, **kwargs): - return self.client_request("HEAD", url, **kwargs) - - def get(self, url, **kwargs): - return self.client_request("GET", url, **kwargs) - - def post(self, url, **kwargs): - return self.client_request("POST", url, **kwargs) - - def put(self, url, **kwargs): - return self.client_request("PUT", url, **kwargs) - - def delete(self, url, **kwargs): - return self.client_request("DELETE", url, **kwargs) - - def patch(self, url, **kwargs): - return self.client_request("PATCH", url, **kwargs) - - @staticmethod - def get_class(api_name, version, version_map): - """Returns the client class for the requested API version - - :param api_name: the name of the API, e.g. 'compute', 'image', etc - :param version: the requested API version - :param version_map: a dict of client classes keyed by version - :rtype: a client class for the requested API version - """ - try: - client_path = version_map[str(version)] - except (KeyError, ValueError): - msg = _("Invalid %(api_name)s client version '%(version)s'. " - "Must be one of: %(version_map)s") % { - 'api_name': api_name, - 'version': version, - 'version_map': ', '.join(version_map.keys())} - raise exceptions.UnsupportedVersion(msg) - - return importutils.import_class(client_path) diff --git a/novaclient/openstack/common/apiclient/exceptions.py b/novaclient/openstack/common/apiclient/exceptions.py deleted file mode 100644 index 540eefd34..000000000 --- a/novaclient/openstack/common/apiclient/exceptions.py +++ /dev/null @@ -1,479 +0,0 @@ -# Copyright 2010 Jacob Kaplan-Moss -# Copyright 2011 Nebula, Inc. -# Copyright 2013 Alessio Ababilov -# Copyright 2013 OpenStack Foundation -# All Rights Reserved. -# -# 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. - -""" -Exception definitions. -""" - -######################################################################## -# -# THIS MODULE IS DEPRECATED -# -# Please refer to -# https://etherpad.openstack.org/p/kilo-novaclient-library-proposals for -# the discussion leading to this deprecation. -# -# We recommend checking out the python-openstacksdk project -# (https://launchpad.net/python-openstacksdk) instead. -# -######################################################################## - -import inspect -import sys - -import six - -from novaclient.openstack.common._i18n import _ - - -class ClientException(Exception): - """The base exception class for all exceptions this library raises. - """ - pass - - -class ValidationError(ClientException): - """Error in validation on API client side.""" - pass - - -class UnsupportedVersion(ClientException): - """User is trying to use an unsupported version of the API.""" - pass - - -class CommandError(ClientException): - """Error in CLI tool.""" - pass - - -class AuthorizationFailure(ClientException): - """Cannot authorize API client.""" - pass - - -class ConnectionError(ClientException): - """Cannot connect to API service.""" - pass - - -class ConnectionRefused(ConnectionError): - """Connection refused while trying to connect to API service.""" - pass - - -class AuthPluginOptionsMissing(AuthorizationFailure): - """Auth plugin misses some options.""" - def __init__(self, opt_names): - super(AuthPluginOptionsMissing, self).__init__( - _("Authentication failed. Missing options: %s") % - ", ".join(opt_names)) - self.opt_names = opt_names - - -class AuthSystemNotFound(AuthorizationFailure): - """User has specified an AuthSystem that is not installed.""" - def __init__(self, auth_system): - super(AuthSystemNotFound, self).__init__( - _("AuthSystemNotFound: %r") % auth_system) - self.auth_system = auth_system - - -class NoUniqueMatch(ClientException): - """Multiple entities found instead of one.""" - pass - - -class EndpointException(ClientException): - """Something is rotten in Service Catalog.""" - pass - - -class EndpointNotFound(EndpointException): - """Could not find requested endpoint in Service Catalog.""" - pass - - -class AmbiguousEndpoints(EndpointException): - """Found more than one matching endpoint in Service Catalog.""" - def __init__(self, endpoints=None): - super(AmbiguousEndpoints, self).__init__( - _("AmbiguousEndpoints: %r") % endpoints) - self.endpoints = endpoints - - -class HttpError(ClientException): - """The base exception class for all HTTP exceptions. - """ - http_status = 0 - message = _("HTTP Error") - - def __init__(self, message=None, details=None, - response=None, request_id=None, - url=None, method=None, http_status=None): - self.http_status = http_status or self.http_status - self.message = message or self.message - self.details = details - self.request_id = request_id - self.response = response - self.url = url - self.method = method - formatted_string = "%s (HTTP %s)" % (self.message, self.http_status) - if request_id: - formatted_string += " (Request-ID: %s)" % request_id - super(HttpError, self).__init__(formatted_string) - - -class HTTPRedirection(HttpError): - """HTTP Redirection.""" - message = _("HTTP Redirection") - - -class HTTPClientError(HttpError): - """Client-side HTTP error. - - Exception for cases in which the client seems to have erred. - """ - message = _("HTTP Client Error") - - -class HttpServerError(HttpError): - """Server-side HTTP error. - - Exception for cases in which the server is aware that it has - erred or is incapable of performing the request. - """ - message = _("HTTP Server Error") - - -class MultipleChoices(HTTPRedirection): - """HTTP 300 - Multiple Choices. - - Indicates multiple options for the resource that the client may follow. - """ - - http_status = 300 - message = _("Multiple Choices") - - -class BadRequest(HTTPClientError): - """HTTP 400 - Bad Request. - - The request cannot be fulfilled due to bad syntax. - """ - http_status = 400 - message = _("Bad Request") - - -class Unauthorized(HTTPClientError): - """HTTP 401 - Unauthorized. - - Similar to 403 Forbidden, but specifically for use when authentication - is required and has failed or has not yet been provided. - """ - http_status = 401 - message = _("Unauthorized") - - -class PaymentRequired(HTTPClientError): - """HTTP 402 - Payment Required. - - Reserved for future use. - """ - http_status = 402 - message = _("Payment Required") - - -class Forbidden(HTTPClientError): - """HTTP 403 - Forbidden. - - The request was a valid request, but the server is refusing to respond - to it. - """ - http_status = 403 - message = _("Forbidden") - - -class NotFound(HTTPClientError): - """HTTP 404 - Not Found. - - The requested resource could not be found but may be available again - in the future. - """ - http_status = 404 - message = _("Not Found") - - -class MethodNotAllowed(HTTPClientError): - """HTTP 405 - Method Not Allowed. - - A request was made of a resource using a request method not supported - by that resource. - """ - http_status = 405 - message = _("Method Not Allowed") - - -class NotAcceptable(HTTPClientError): - """HTTP 406 - Not Acceptable. - - The requested resource is only capable of generating content not - acceptable according to the Accept headers sent in the request. - """ - http_status = 406 - message = _("Not Acceptable") - - -class ProxyAuthenticationRequired(HTTPClientError): - """HTTP 407 - Proxy Authentication Required. - - The client must first authenticate itself with the proxy. - """ - http_status = 407 - message = _("Proxy Authentication Required") - - -class RequestTimeout(HTTPClientError): - """HTTP 408 - Request Timeout. - - The server timed out waiting for the request. - """ - http_status = 408 - message = _("Request Timeout") - - -class Conflict(HTTPClientError): - """HTTP 409 - Conflict. - - Indicates that the request could not be processed because of conflict - in the request, such as an edit conflict. - """ - http_status = 409 - message = _("Conflict") - - -class Gone(HTTPClientError): - """HTTP 410 - Gone. - - Indicates that the resource requested is no longer available and will - not be available again. - """ - http_status = 410 - message = _("Gone") - - -class LengthRequired(HTTPClientError): - """HTTP 411 - Length Required. - - The request did not specify the length of its content, which is - required by the requested resource. - """ - http_status = 411 - message = _("Length Required") - - -class PreconditionFailed(HTTPClientError): - """HTTP 412 - Precondition Failed. - - The server does not meet one of the preconditions that the requester - put on the request. - """ - http_status = 412 - message = _("Precondition Failed") - - -class RequestEntityTooLarge(HTTPClientError): - """HTTP 413 - Request Entity Too Large. - - The request is larger than the server is willing or able to process. - """ - http_status = 413 - message = _("Request Entity Too Large") - - def __init__(self, *args, **kwargs): - try: - self.retry_after = int(kwargs.pop('retry_after')) - except (KeyError, ValueError): - self.retry_after = 0 - - super(RequestEntityTooLarge, self).__init__(*args, **kwargs) - - -class RequestUriTooLong(HTTPClientError): - """HTTP 414 - Request-URI Too Long. - - The URI provided was too long for the server to process. - """ - http_status = 414 - message = _("Request-URI Too Long") - - -class UnsupportedMediaType(HTTPClientError): - """HTTP 415 - Unsupported Media Type. - - The request entity has a media type which the server or resource does - not support. - """ - http_status = 415 - message = _("Unsupported Media Type") - - -class RequestedRangeNotSatisfiable(HTTPClientError): - """HTTP 416 - Requested Range Not Satisfiable. - - The client has asked for a portion of the file, but the server cannot - supply that portion. - """ - http_status = 416 - message = _("Requested Range Not Satisfiable") - - -class ExpectationFailed(HTTPClientError): - """HTTP 417 - Expectation Failed. - - The server cannot meet the requirements of the Expect request-header field. - """ - http_status = 417 - message = _("Expectation Failed") - - -class UnprocessableEntity(HTTPClientError): - """HTTP 422 - Unprocessable Entity. - - The request was well-formed but was unable to be followed due to semantic - errors. - """ - http_status = 422 - message = _("Unprocessable Entity") - - -class InternalServerError(HttpServerError): - """HTTP 500 - Internal Server Error. - - A generic error message, given when no more specific message is suitable. - """ - http_status = 500 - message = _("Internal Server Error") - - -# NotImplemented is a python keyword. -class HttpNotImplemented(HttpServerError): - """HTTP 501 - Not Implemented. - - The server either does not recognize the request method, or it lacks - the ability to fulfill the request. - """ - http_status = 501 - message = _("Not Implemented") - - -class BadGateway(HttpServerError): - """HTTP 502 - Bad Gateway. - - The server was acting as a gateway or proxy and received an invalid - response from the upstream server. - """ - http_status = 502 - message = _("Bad Gateway") - - -class ServiceUnavailable(HttpServerError): - """HTTP 503 - Service Unavailable. - - The server is currently unavailable. - """ - http_status = 503 - message = _("Service Unavailable") - - -class GatewayTimeout(HttpServerError): - """HTTP 504 - Gateway Timeout. - - The server was acting as a gateway or proxy and did not receive a timely - response from the upstream server. - """ - http_status = 504 - message = _("Gateway Timeout") - - -class HttpVersionNotSupported(HttpServerError): - """HTTP 505 - HttpVersion Not Supported. - - The server does not support the HTTP protocol version used in the request. - """ - http_status = 505 - message = _("HTTP Version Not Supported") - - -# _code_map contains all the classes that have http_status attribute. -_code_map = dict( - (getattr(obj, 'http_status', None), obj) - for name, obj in six.iteritems(vars(sys.modules[__name__])) - if inspect.isclass(obj) and getattr(obj, 'http_status', False) -) - - -def from_response(response, method, url): - """Returns an instance of :class:`HttpError` or subclass based on response. - - :param response: instance of `requests.Response` class - :param method: HTTP method used for request - :param url: URL used for request - """ - - req_id = response.headers.get("x-openstack-request-id") - # NOTE(hdd) true for older versions of nova and cinder - if not req_id: - req_id = response.headers.get("x-compute-request-id") - kwargs = { - "http_status": response.status_code, - "response": response, - "method": method, - "url": url, - "request_id": req_id, - } - if "retry-after" in response.headers: - kwargs["retry_after"] = response.headers["retry-after"] - - content_type = response.headers.get("Content-Type", "") - if content_type.startswith("application/json"): - try: - body = response.json() - except ValueError: - pass - else: - if isinstance(body, dict): - error = body.get(list(body)[0]) - if isinstance(error, dict): - kwargs["message"] = (error.get("message") or - error.get("faultstring")) - kwargs["details"] = (error.get("details") or - six.text_type(body)) - elif content_type.startswith("text/"): - kwargs["details"] = getattr(response, 'text', '') - - try: - cls = _code_map[response.status_code] - except KeyError: - if 500 <= response.status_code < 600: - cls = HttpServerError - elif 400 <= response.status_code < 500: - cls = HTTPClientError - else: - cls = HttpError - return cls(**kwargs) diff --git a/novaclient/openstack/common/apiclient/fake_client.py b/novaclient/openstack/common/apiclient/fake_client.py deleted file mode 100644 index 2d2a728b1..000000000 --- a/novaclient/openstack/common/apiclient/fake_client.py +++ /dev/null @@ -1,189 +0,0 @@ -# Copyright 2013 OpenStack Foundation -# All Rights Reserved. -# -# 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. - -""" -A fake server that "responds" to API methods with pre-canned responses. - -All of these responses come from the spec, so if for some reason the spec's -wrong the tests might raise AssertionError. I've indicated in comments the -places where actual behavior differs from the spec. -""" - -######################################################################## -# -# THIS MODULE IS DEPRECATED -# -# Please refer to -# https://etherpad.openstack.org/p/kilo-novaclient-library-proposals for -# the discussion leading to this deprecation. -# -# We recommend checking out the python-openstacksdk project -# (https://launchpad.net/python-openstacksdk) instead. -# -######################################################################## - -# W0102: Dangerous default value %s as argument -# pylint: disable=W0102 - -import json - -import requests -import six -from six.moves.urllib import parse - -from novaclient.openstack.common.apiclient import client - - -def assert_has_keys(dct, required=None, optional=None): - required = required or [] - optional = optional or [] - for k in required: - try: - assert k in dct - except AssertionError: - extra_keys = set(dct.keys()).difference(set(required + optional)) - raise AssertionError("found unexpected keys: %s" % - list(extra_keys)) - - -class TestResponse(requests.Response): - """Wrap requests.Response and provide a convenient initialization. - """ - - def __init__(self, data): - super(TestResponse, self).__init__() - self._content_consumed = True - if isinstance(data, dict): - self.status_code = data.get('status_code', 200) - # Fake the text attribute to streamline Response creation - text = data.get('text', "") - if isinstance(text, (dict, list)): - self._content = json.dumps(text) - default_headers = { - "Content-Type": "application/json", - } - else: - self._content = text - default_headers = {} - if six.PY3 and isinstance(self._content, six.string_types): - self._content = self._content.encode('utf-8', 'strict') - self.headers = data.get('headers') or default_headers - else: - self.status_code = data - - def __eq__(self, other): - return (self.status_code == other.status_code and - self.headers == other.headers and - self._content == other._content) - - -class FakeHTTPClient(client.HTTPClient): - - def __init__(self, *args, **kwargs): - self.callstack = [] - self.fixtures = kwargs.pop("fixtures", None) or {} - if not args and "auth_plugin" not in kwargs: - args = (None, ) - super(FakeHTTPClient, self).__init__(*args, **kwargs) - - def assert_called(self, method, url, body=None, pos=-1): - """Assert than an API method was just called. - """ - expected = (method, url) - called = self.callstack[pos][0:2] - assert self.callstack, \ - "Expected %s %s but no calls were made." % expected - - assert expected == called, 'Expected %s %s; got %s %s' % \ - (expected + called) - - if body is not None: - if self.callstack[pos][3] != body: - raise AssertionError('%r != %r' % - (self.callstack[pos][3], body)) - - def assert_called_anytime(self, method, url, body=None): - """Assert than an API method was called anytime in the test. - """ - expected = (method, url) - - assert self.callstack, \ - "Expected %s %s but no calls were made." % expected - - found = False - entry = None - for entry in self.callstack: - if expected == entry[0:2]: - found = True - break - - assert found, 'Expected %s %s; got %s' % \ - (method, url, self.callstack) - if body is not None: - assert entry[3] == body, "%s != %s" % (entry[3], body) - - self.callstack = [] - - def clear_callstack(self): - self.callstack = [] - - def authenticate(self): - pass - - def client_request(self, client, method, url, **kwargs): - # Check that certain things are called correctly - if method in ["GET", "DELETE"]: - assert "json" not in kwargs - - # Note the call - self.callstack.append( - (method, - url, - kwargs.get("headers") or {}, - kwargs.get("json") or kwargs.get("data"))) - try: - fixture = self.fixtures[url][method] - except KeyError: - pass - else: - return TestResponse({"headers": fixture[0], - "text": fixture[1]}) - - # Call the method - args = parse.parse_qsl(parse.urlparse(url)[4]) - kwargs.update(args) - munged_url = url.rsplit('?', 1)[0] - munged_url = munged_url.strip('/').replace('/', '_').replace('.', '_') - munged_url = munged_url.replace('-', '_') - - callback = "%s_%s" % (method.lower(), munged_url) - - if not hasattr(self, callback): - raise AssertionError('Called unknown API method: %s %s, ' - 'expected fakes method name: %s' % - (method, url, callback)) - - resp = getattr(self, callback)(**kwargs) - if len(resp) == 3: - status, headers, body = resp - else: - status, body = resp - headers = {} - self.last_request_id = headers.get('x-openstack-request-id') - return TestResponse({ - "status_code": status, - "text": body, - "headers": headers, - }) diff --git a/novaclient/openstack/common/apiclient/utils.py b/novaclient/openstack/common/apiclient/utils.py deleted file mode 100644 index c8938ef54..000000000 --- a/novaclient/openstack/common/apiclient/utils.py +++ /dev/null @@ -1,100 +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. - -######################################################################## -# -# THIS MODULE IS DEPRECATED -# -# Please refer to -# https://etherpad.openstack.org/p/kilo-novaclient-library-proposals for -# the discussion leading to this deprecation. -# -# We recommend checking out the python-openstacksdk project -# (https://launchpad.net/python-openstacksdk) instead. -# -######################################################################## - -from oslo_utils import encodeutils -from oslo_utils import uuidutils -import six - -from novaclient.openstack.common._i18n import _ -from novaclient.openstack.common.apiclient import exceptions - - -def find_resource(manager, name_or_id, **find_args): - """Look for resource in a given manager. - - Used as a helper for the _find_* methods. - Example: - - .. code-block:: python - - def _find_hypervisor(cs, hypervisor): - #Get a hypervisor by name or ID. - return cliutils.find_resource(cs.hypervisors, hypervisor) - """ - # first try to get entity as integer id - try: - return manager.get(int(name_or_id)) - except (TypeError, ValueError, exceptions.NotFound): - pass - - # now try to get entity as uuid - try: - if six.PY2: - tmp_id = encodeutils.safe_encode(name_or_id) - else: - tmp_id = encodeutils.safe_decode(name_or_id) - - if uuidutils.is_uuid_like(tmp_id): - return manager.get(tmp_id) - except (TypeError, ValueError, exceptions.NotFound): - pass - - # for str id which is not uuid - if getattr(manager, 'is_alphanum_id_allowed', False): - try: - return manager.get(name_or_id) - except exceptions.NotFound: - pass - - try: - try: - return manager.find(human_id=name_or_id, **find_args) - except exceptions.NotFound: - pass - - # finally try to find entity by name - try: - resource = getattr(manager, 'resource_class', None) - name_attr = resource.NAME_ATTR if resource else 'name' - kwargs = {name_attr: name_or_id} - kwargs.update(find_args) - return manager.find(**kwargs) - except exceptions.NotFound: - msg = _("No %(name)s with a name or " - "ID of '%(name_or_id)s' exists.") % \ - { - "name": manager.resource_class.__name__.lower(), - "name_or_id": name_or_id - } - raise exceptions.CommandError(msg) - except exceptions.NoUniqueMatch: - msg = _("Multiple %(name)s matches found for " - "'%(name_or_id)s', use an ID to be more specific.") % \ - { - "name": manager.resource_class.__name__.lower(), - "name_or_id": name_or_id - } - raise exceptions.CommandError(msg) diff --git a/novaclient/openstack/common/cliutils.py b/novaclient/openstack/common/cliutils.py index fd1744e09..d132a0ca4 100644 --- a/novaclient/openstack/common/cliutils.py +++ b/novaclient/openstack/common/cliutils.py @@ -12,261 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. -# W0603: Using the global statement -# W0621: Redefining name %s from outer scope -# pylint: disable=W0603,W0621 +from novaclient import utils -from __future__ import print_function - -import getpass -import inspect -import os -import sys -import textwrap - -from oslo_utils import encodeutils -from oslo_utils import strutils -import prettytable -import six -from six import moves - -from novaclient.openstack.common._i18n import _ - - -class MissingArgs(Exception): - """Supplied arguments are not sufficient for calling a function.""" - def __init__(self, missing): - self.missing = missing - msg = _("Missing arguments: %s") % ", ".join(missing) - super(MissingArgs, self).__init__(msg) - - -def validate_args(fn, *args, **kwargs): - """Check that the supplied args are sufficient for calling a function. - - >>> validate_args(lambda a: None) - Traceback (most recent call last): - ... - MissingArgs: Missing argument(s): a - >>> validate_args(lambda a, b, c, d: None, 0, c=1) - Traceback (most recent call last): - ... - MissingArgs: Missing argument(s): b, d - - :param fn: the function to check - :param arg: the positional arguments supplied - :param kwargs: the keyword arguments supplied - """ - argspec = inspect.getargspec(fn) - - num_defaults = len(argspec.defaults or []) - required_args = argspec.args[:len(argspec.args) - num_defaults] - - def isbound(method): - return getattr(method, '__self__', None) is not None - - if isbound(fn): - required_args.pop(0) - - missing = [arg for arg in required_args if arg not in kwargs] - missing = missing[len(args):] - if missing: - raise MissingArgs(missing) - - -def arg(*args, **kwargs): - """Decorator for CLI args. - - Example: - - >>> @arg("name", help="Name of the new entity") - ... def entity_create(args): - ... pass - """ - def _decorator(func): - add_arg(func, *args, **kwargs) - return func - return _decorator - - -def env(*args, **kwargs): - """Returns the first environment variable set. - - If all are empty, defaults to '' or keyword arg `default`. - """ - for arg in args: - value = os.environ.get(arg) - if value: - return value - return kwargs.get('default', '') - - -def add_arg(func, *args, **kwargs): - """Bind CLI arguments to a shell.py `do_foo` function.""" - - if not hasattr(func, 'arguments'): - func.arguments = [] - - # NOTE(sirp): avoid dups that can occur when the module is shared across - # tests. - if (args, kwargs) not in func.arguments: - # Because of the semantics of decorator composition if we just append - # to the options list positional options will appear to be backwards. - func.arguments.insert(0, (args, kwargs)) - - -def unauthenticated(func): - """Adds 'unauthenticated' attribute to decorated function. - - Usage: - - >>> @unauthenticated - ... def mymethod(f): - ... pass - """ - func.unauthenticated = True - return func - - -def isunauthenticated(func): - """Checks if the function does not require authentication. - - Mark such functions with the `@unauthenticated` decorator. - - :returns: bool - """ - return getattr(func, 'unauthenticated', False) - - -def print_list(objs, fields, formatters=None, sortby_index=0, - mixed_case_fields=None, field_labels=None): - """Print a list of objects as a table, one row per object. - - :param objs: iterable of :class:`Resource` - :param fields: attributes that correspond to columns, in order - :param formatters: `dict` of callables for field formatting - :param sortby_index: index of the field for sorting table rows - :param mixed_case_fields: fields corresponding to object attributes that - have mixed case names (e.g., 'serverId') - :param field_labels: Labels to use in the heading of the table, default to - fields. - """ - formatters = formatters or {} - mixed_case_fields = mixed_case_fields or [] - field_labels = field_labels or fields - if len(field_labels) != len(fields): - raise ValueError(_("Field labels list %(labels)s has different number " - "of elements than fields list %(fields)s"), - {'labels': field_labels, 'fields': fields}) - - if sortby_index is None: - kwargs = {} - else: - kwargs = {'sortby': field_labels[sortby_index]} - pt = prettytable.PrettyTable(field_labels) - pt.align = 'l' - - for o in objs: - row = [] - for field in fields: - if field in formatters: - row.append(formatters[field](o)) - else: - if field in mixed_case_fields: - field_name = field.replace(' ', '_') - else: - field_name = field.lower().replace(' ', '_') - data = getattr(o, field_name, '') - row.append(data) - pt.add_row(row) - - if six.PY3: - print(encodeutils.safe_encode(pt.get_string(**kwargs)).decode()) - else: - print(encodeutils.safe_encode(pt.get_string(**kwargs))) - - -def print_dict(dct, dict_property="Property", wrap=0, dict_value='Value'): - """Print a `dict` as a table of two columns. - - :param dct: `dict` to print - :param dict_property: name of the first column - :param wrap: wrapping for the second column - :param dict_value: header label for the value (second) column - """ - pt = prettytable.PrettyTable([dict_property, dict_value]) - pt.align = 'l' - for k, v in sorted(dct.items()): - # convert dict to str to check length - if isinstance(v, dict): - v = six.text_type(v) - if wrap > 0: - v = textwrap.fill(six.text_type(v), wrap) - # if value has a newline, add in multiple rows - # e.g. fault with stacktrace - if v and isinstance(v, six.string_types) and r'\n' in v: - lines = v.strip().split(r'\n') - col1 = k - for line in lines: - pt.add_row([col1, line]) - col1 = '' - else: - pt.add_row([k, v]) - - if six.PY3: - print(encodeutils.safe_encode(pt.get_string()).decode()) - else: - print(encodeutils.safe_encode(pt.get_string())) - - -def get_password(max_password_prompts=3): - """Read password from TTY.""" - verify = strutils.bool_from_string(env("OS_VERIFY_PASSWORD")) - pw = None - if hasattr(sys.stdin, "isatty") and sys.stdin.isatty(): - # Check for Ctrl-D - try: - for __ in moves.range(max_password_prompts): - pw1 = getpass.getpass("OS Password: ") - if verify: - pw2 = getpass.getpass("Please verify: ") - else: - pw2 = pw1 - if pw1 == pw2 and pw1: - pw = pw1 - break - except EOFError: - pass - return pw - - -def service_type(stype): - """Adds 'service_type' attribute to decorated function. - - Usage: - - .. code-block:: python - - @service_type('volume') - def mymethod(f): - ... - """ - def inner(f): - f.service_type = stype - return f - return inner - - -def get_service_type(f): - """Retrieves service type from function.""" - return getattr(f, 'service_type', None) - - -def pretty_choice_list(l): - return ', '.join("'%s'" % i for i in l) - - -def exit(msg=''): - if msg: - print (msg, file=sys.stderr) - sys.exit(1) +arg = utils.arg +add_arg = utils.add_arg diff --git a/novaclient/shell.py b/novaclient/shell.py index 6a75d83c4..ec174fa2d 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -152,7 +152,7 @@ def password(self): self._password = self.args.os_password else: verify_pass = strutils.bool_from_string( - cliutils.env("OS_VERIFY_PASSWORD", default=False), True) + utils.env("OS_VERIFY_PASSWORD", default=False), True) self._password = self._prompt_password(verify_pass) if not self._password: raise exc.CommandError( @@ -259,18 +259,17 @@ def _append_global_identity_args(self, parser, argv): loading.register_auth_argparse_arguments( parser, argv, default=default_auth_plugin) - parser.set_defaults(insecure=cliutils.env('NOVACLIENT_INSECURE', + parser.set_defaults(insecure=utils.env('NOVACLIENT_INSECURE', default=False)) - parser.set_defaults(os_auth_url=cliutils.env('OS_AUTH_URL', - 'NOVA_URL')) - - parser.set_defaults(os_username=cliutils.env('OS_USERNAME', - 'NOVA_USERNAME')) - parser.set_defaults(os_password=cliutils.env('OS_PASSWORD', - 'NOVA_PASSWORD')) - parser.set_defaults(os_project_name=cliutils.env( + parser.set_defaults(os_auth_url=utils.env('OS_AUTH_URL', 'NOVA_URL')) + + parser.set_defaults(os_username=utils.env('OS_USERNAME', + 'NOVA_USERNAME')) + parser.set_defaults(os_password=utils.env('OS_PASSWORD', + 'NOVA_PASSWORD')) + parser.set_defaults(os_project_name=utils.env( 'OS_PROJECT_NAME', 'OS_TENANT_NAME', 'NOVA_PROJECT_ID')) - parser.set_defaults(os_project_id=cliutils.env( + parser.set_defaults(os_project_id=utils.env( 'OS_PROJECT_ID', 'OS_TENANT_ID')) def get_base_parser(self, argv): @@ -303,7 +302,7 @@ def get_base_parser(self, argv): parser.add_argument( '--os-cache', default=strutils.bool_from_string( - cliutils.env('OS_CACHE', default=False), True), + utils.env('OS_CACHE', default=False), True), action='store_true', help=_("Use the auth token cache. Defaults to False if " "env[OS_CACHE] is not set.")) @@ -337,7 +336,7 @@ def get_base_parser(self, argv): parser.add_argument( '--os-region-name', metavar='', - default=cliutils.env('OS_REGION_NAME', 'NOVA_REGION_NAME'), + default=utils.env('OS_REGION_NAME', 'NOVA_REGION_NAME'), help=_('Defaults to env[OS_REGION_NAME].')) parser.add_argument( '--os_region_name', @@ -346,7 +345,7 @@ def get_base_parser(self, argv): parser.add_argument( '--os-auth-system', metavar='', - default=cliutils.env('OS_AUTH_SYSTEM'), + default=utils.env('OS_AUTH_SYSTEM'), help=argparse.SUPPRESS) parser.add_argument( '--os_auth_system', @@ -363,7 +362,7 @@ def get_base_parser(self, argv): parser.add_argument( '--service-name', metavar='', - default=cliutils.env('NOVA_SERVICE_NAME'), + default=utils.env('NOVA_SERVICE_NAME'), help=_('Defaults to env[NOVA_SERVICE_NAME].')) parser.add_argument( '--service_name', @@ -372,7 +371,7 @@ def get_base_parser(self, argv): parser.add_argument( '--volume-service-name', metavar='', - default=cliutils.env('NOVA_VOLUME_SERVICE_NAME'), + default=utils.env('NOVA_VOLUME_SERVICE_NAME'), help=_('Defaults to env[NOVA_VOLUME_SERVICE_NAME].')) parser.add_argument( '--volume_service_name', @@ -382,9 +381,8 @@ def get_base_parser(self, argv): '--os-endpoint-type', metavar='', dest='endpoint_type', - default=cliutils.env( - 'NOVA_ENDPOINT_TYPE', - default=cliutils.env( + default=utils.env( + 'NOVA_ENDPOINT_TYPE', default=utils.env( 'OS_ENDPOINT_TYPE', default=DEFAULT_NOVA_ENDPOINT_TYPE)), help=_('Defaults to env[NOVA_ENDPOINT_TYPE], ' @@ -404,8 +402,8 @@ def get_base_parser(self, argv): parser.add_argument( '--os-compute-api-version', metavar='', - default=cliutils.env('OS_COMPUTE_API_VERSION', - default=DEFAULT_OS_COMPUTE_API_VERSION), + default=utils.env('OS_COMPUTE_API_VERSION', + default=DEFAULT_OS_COMPUTE_API_VERSION), help=_('Accepts X, X.Y (where X is major and Y is minor part) or ' '"X.latest", defaults to env[OS_COMPUTE_API_VERSION].')) parser.add_argument( @@ -416,7 +414,7 @@ def get_base_parser(self, argv): '--bypass-url', metavar='', dest='bypass_url', - default=cliutils.env('NOVACLIENT_BYPASS_URL'), + default=utils.env('NOVACLIENT_BYPASS_URL'), help="Use this API endpoint instead of the Service Catalog. " "Defaults to env[NOVACLIENT_BYPASS_URL].") parser.add_argument('--bypass_url', @@ -751,10 +749,10 @@ def main(self, argv): return 0 if not args.service_type: - service_type = (cliutils.get_service_type(args.func) or + service_type = (utils.get_service_type(args.func) or DEFAULT_NOVA_SERVICE_TYPE) - if cliutils.isunauthenticated(args.func): + if utils.isunauthenticated(args.func): # NOTE(alex_xu): We need authentication for discover microversion. # But the subcommands may needn't it. If the subcommand needn't, # we clear the session arguements. @@ -807,7 +805,7 @@ def main(self, argv): try: # This does a couple of bits which are useful even if we've # got the token + service URL already. It exits fast in that case. - if not cliutils.isunauthenticated(args.func): + if not utils.isunauthenticated(args.func): if not use_session: # Only call authenticate() if Nova auth plugin is used. # If keystone is used, authentication is handled as part diff --git a/novaclient/tests/unit/test_shell.py b/novaclient/tests/unit/test_shell.py index bba95cc3d..11db83c43 100644 --- a/novaclient/tests/unit/test_shell.py +++ b/novaclient/tests/unit/test_shell.py @@ -118,8 +118,7 @@ def setUp(self): self.useFixture(fixtures.MonkeyPatch( 'novaclient.client.Client', mock.MagicMock())) - self.nc_util = mock.patch( - 'novaclient.openstack.common.cliutils.isunauthenticated').start() + self.nc_util = mock.patch('novaclient.utils.isunauthenticated').start() self.nc_util.return_value = False self.mock_server_version_range = mock.patch( 'novaclient.api_versions._get_server_version_range').start() diff --git a/novaclient/utils.py b/novaclient/utils.py index 0b5e6286a..ef183030c 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -13,6 +13,7 @@ import contextlib import json +import os import re import textwrap import time @@ -26,12 +27,94 @@ from novaclient import exceptions from novaclient.i18n import _ -from novaclient.openstack.common import cliutils VALID_KEY_REGEX = re.compile(r"[\w\.\- :]+$", re.UNICODE) +def env(*args, **kwargs): + """Returns the first environment variable set. + + If all are empty, defaults to '' or keyword arg `default`. + """ + for arg in args: + value = os.environ.get(arg) + if value: + return value + return kwargs.get('default', '') + + +def get_service_type(f): + """Retrieves service type from function.""" + return getattr(f, 'service_type', None) + + +def unauthenticated(func): + """Adds 'unauthenticated' attribute to decorated function. + + Usage: + + >>> @unauthenticated + ... def mymethod(f): + ... pass + """ + func.unauthenticated = True + return func + + +def isunauthenticated(func): + """Checks if the function does not require authentication. + + Mark such functions with the `@unauthenticated` decorator. + + :returns: bool + """ + return getattr(func, 'unauthenticated', False) + + +def arg(*args, **kwargs): + """Decorator for CLI args. + + Example: + + >>> @arg("name", help="Name of the new entity") + ... def entity_create(args): + ... pass + """ + def _decorator(func): + add_arg(func, *args, **kwargs) + return func + return _decorator + + +def add_arg(func, *args, **kwargs): + """Bind CLI arguments to a shell.py `do_foo` function.""" + + if not hasattr(func, 'arguments'): + func.arguments = [] + + # NOTE(sirp): avoid dups that can occur when the module is shared across + # tests. + if (args, kwargs) not in func.arguments: + # Because of the semantics of decorator composition if we just append + # to the options list positional options will appear to be backwards. + func.arguments.insert(0, (args, kwargs)) + + +def service_type(stype): + """Adds 'service_type' attribute to decorated function. + Usage: + .. code-block:: python + @service_type('volume') + def mymethod(f): + ... + """ + def inner(f): + f.service_type = stype + return f + return inner + + def add_resource_manager_extra_kwargs_hook(f, hook): """Add hook to bind CLI arguments to ResourceManager calls. @@ -69,10 +152,13 @@ def get_resource_manager_extra_kwargs(f, args, allow_conflicts=False): return extra_kwargs +def pretty_choice_list(l): + return ', '.join("'%s'" % i for i in l) + + def pretty_choice_dict(d): """Returns a formatted dict as 'key=value'.""" - return cliutils.pretty_choice_list( - ['%s=%s' % (k, d[k]) for k in sorted(d.keys())]) + return pretty_choice_list(['%s=%s' % (k, d[k]) for k in sorted(d.keys())]) def print_list(objs, fields, formatters={}, sortby_index=None): diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 0fe49a8b6..0a5142d87 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -3618,7 +3618,7 @@ def parser_metadata(fields): return utils.pretty_choice_dict(getattr(fields, 'metadata', {}) or {}) def parser_hosts(fields): - return cliutils.pretty_choice_list(getattr(fields, 'hosts', [])) + return utils.pretty_choice_list(getattr(fields, 'hosts', [])) formatters = { 'Metadata': parser_metadata, @@ -4600,7 +4600,7 @@ def _treeizeAvailabilityZone(zone): return result -@cliutils.service_type('compute') +@utils.service_type('compute') def do_availability_zone_list(cs, _args): """List all the availability zones.""" try: diff --git a/openstack-common.conf b/openstack-common.conf deleted file mode 100644 index a987faae9..000000000 --- a/openstack-common.conf +++ /dev/null @@ -1,8 +0,0 @@ -[DEFAULT] - -# The list of modules to copy from oslo-incubator -module=apiclient -module=cliutils - -# The base module to hold the copy of openstack.common -base=novaclient From cb7b49600d7802792d5bd4c25c659fdd28d180b7 Mon Sep 17 00:00:00 2001 From: Sean Dague Date: Fri, 18 Dec 2015 07:20:53 -0500 Subject: [PATCH 0930/1705] document search_opts parameter Based on irc conversation, the value to be passed to search_opts isn't super clear. Document it for posterity. Change-Id: I58a9b8f09056f16f58be4ef7b5b78e3ea3132faa --- novaclient/v2/servers.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index bfab3f446..15f1cf0d3 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -569,7 +569,11 @@ def list(self, detailed=True, search_opts=None, marker=None, limit=None, Get a list of servers. :param detailed: Whether to return detailed server info (optional). - :param search_opts: Search options to filter out servers (optional). + :param search_opts: Search options to filter out servers which don't + match the search_opts (optional). The search opts format is a + dictionary of key / value pairs that will be appended to the query + string. For a complete list of keys see: + http://developer.openstack.org/api-ref-compute-v2.html#listServers :param marker: Begin returning servers that appear later in the server list than that represented by this server id (optional). :param limit: Maximum number of servers to return (optional). @@ -577,6 +581,16 @@ def list(self, detailed=True, search_opts=None, marker=None, limit=None, :param sort_dirs: List of sort directions :rtype: list of :class:`Server` + + Examples: + + client.servers.list() - returns detailed list of servers + + client.servers.list(search_opts={'status': 'ERROR'}) - + returns list of servers in error state. + + client.servers.list(limit=10) - returns only 10 servers + """ if search_opts is None: search_opts = {} From fecf833ba5d150756f31ab5ae03d69f630d7f955 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Mon, 21 Dec 2015 23:45:21 +0000 Subject: [PATCH 0931/1705] Updated from global requirements Change-Id: I59d6b3f54c21b1f4cc86bb58f58ec1ef5f11dee4 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 8ffcf36cd..de8e472fa 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -16,7 +16,7 @@ oslosphinx!=3.4.0,>=2.5.0 # Apache-2.0 testrepository>=0.0.18 testscenarios>=0.4 testtools>=1.4.0 -tempest-lib>=0.11.0 +tempest-lib>=0.12.0 # releasenotes reno>=0.1.1 # Apache2 From 9cd9adb38c7643db34af0eebc28462a36428732f Mon Sep 17 00:00:00 2001 From: Sergey Nikitin Date: Tue, 22 Dec 2015 14:00:08 +0300 Subject: [PATCH 0932/1705] Fixed test_shell which can't test microversions>=2.4 Method run_command() gets max and min API verisons from FakeHTTPClient.get_versions(). That's why max version which could be tested by run)command is 2.3 (because it's hardcoded in get_versions()). get_versions() should return max and min versions from novaclient.__init__.py Closes-Bug: #1528527 Change-Id: I489c4714cacb345d3b45d5987113093c038cf151 --- novaclient/tests/unit/v2/fakes.py | 9 +++++---- novaclient/tests/unit/v2/test_shell.py | 4 ++++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 56c141304..69612c30d 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -22,6 +22,7 @@ import six from six.moves.urllib import parse +import novaclient from novaclient import client as base_client from novaclient import exceptions from novaclient.tests.unit import fakes @@ -155,8 +156,8 @@ def get_versions(self): {"status": "CURRENT", "updated": "2013-07-23T11:33:21Z", "links": [{"href": "http://nova-api:8774/v2.1/", "rel": "self"}], - "min_version": "2.1", - "version": "2.3", + "min_version": novaclient.API_MIN_VERSION.get_string(), + "version": novaclient.API_MAX_VERSION.get_string(), "id": "v2.1"} ]}) @@ -182,8 +183,8 @@ def get_current_version(self): "href": "http://nova-api:8774/v2.1/", "rel": "self" }], - "min_version": "2.1", - "version": "2.3", + "min_version": novaclient.API_MIN_VERSION.get_string(), + "version": novaclient.API_MAX_VERSION.get_string(), "id": "v2.1" } } diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index d803a888a..d6d545582 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2618,6 +2618,10 @@ def test_versions(self): 'excluding them.' % sorted(unaccounted_for)) self.assertEqual(set([]), unaccounted_for, failure_msg) + def test_list_v2_10(self): + self.run_command('list', api_version='2.10') + self.assert_called('GET', '/servers/detail') + class ShellWithSessionClientTest(ShellTest): From 84c3cacd13c34a156aebbf8c67ac074bab4f80fe Mon Sep 17 00:00:00 2001 From: Janonymous Date: Mon, 21 Dec 2015 18:22:50 +0530 Subject: [PATCH 0933/1705] Put py34 first in the env order of tox To solve the problem of "db type could not be determined" on py34 we have to run first the py34 env to, then, run py27. This patch puts py34 first on the tox.ini list of envs to avoid this problem to happen. Change-Id: I06c6a276b75eb2a3d69e94f467f26122d856d17c Closes-bug: #1489059 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 5e582f80c..ba335c1dc 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ # noted to use py34 you need virtualenv >= 1.11.4 [tox] -envlist = py27,py34,pypy,pep8,docs +envlist = py34,py27,pypy,pep8,docs minversion = 1.6 skipsdist = True From fd450d8c60e174677ccc9d4f08dec17eb053b644 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Tue, 15 Dec 2015 17:51:33 +0200 Subject: [PATCH 0934/1705] [microversions] share one object for shell arguments I8d599b712b17dcfc0be940a61c537d2dfe1b715b change provides a wrong fix for an issue with cli arguments. _find_actions method is designed to find all action methods(do_server_list, do_server_show and etc). Since one module can include several versions of one action(after microversion implementation) and only the last one will be registrated in module, _find_actions method was adopted to handle versioned methods. Now it checks that discovered method is related to versioning stuff and replace it by appropriate(in terms of microversions) functions. In this case, the substituation is used only to determine function name and that it relates to versioning methods. That is why the change(see a change-id above) is not help at all. We should share list object named "arguments"(it used by all action methods to store cli arguments) with substitution and original method(which will be used by _find_action). It will allow to put api_versions.wraps and cliutils.arg decorators in any order. Change-Id: Ief316a8597555db6cb02c9f23406b9f1f09f8313 --- novaclient/api_versions.py | 10 ++++--- novaclient/tests/unit/test_api_versions.py | 31 +++++++++------------- 2 files changed, 19 insertions(+), 22 deletions(-) diff --git a/novaclient/api_versions.py b/novaclient/api_versions.py index cdd708b65..ff3b2a237 100644 --- a/novaclient/api_versions.py +++ b/novaclient/api_versions.py @@ -22,7 +22,6 @@ import novaclient from novaclient import exceptions from novaclient.i18n import _, _LW -from novaclient.openstack.common import cliutils from novaclient import utils LOG = logging.getLogger(__name__) @@ -372,9 +371,12 @@ def substitution(obj, *args, **kwargs): return method.func(obj, *args, **kwargs) - if hasattr(func, 'arguments'): - for cli_args, cli_kwargs in func.arguments: - cliutils.add_arg(substitution, *cli_args, **cli_kwargs) + # Let's share "arguments" with original method and substitution to + # allow put cliutils.arg and wraps decorators in any order + if not hasattr(func, 'arguments'): + func.arguments = [] + substitution.arguments = func.arguments + return substitution return decor diff --git a/novaclient/tests/unit/test_api_versions.py b/novaclient/tests/unit/test_api_versions.py index da35fdf99..f33b28dde 100644 --- a/novaclient/tests/unit/test_api_versions.py +++ b/novaclient/tests/unit/test_api_versions.py @@ -20,6 +20,7 @@ from novaclient import exceptions from novaclient.openstack.common import cliutils from novaclient.tests.unit import utils +from novaclient import utils as nutils from novaclient.v2 import versions @@ -277,27 +278,21 @@ def some_func(*args, **kwargs): checker.assert_called_once_with(*((obj,) + some_args), **some_kwargs) - def test_cli_args_are_copied(self): - - @api_versions.wraps("2.2", "2.6") - @cliutils.arg("name_1", help="Name of the something") - @cliutils.arg("action_1", help="Some action") - def some_func_1(cs, args): - pass - - @cliutils.arg("name_2", help="Name of the something") - @cliutils.arg("action_2", help="Some action") - @api_versions.wraps("2.2", "2.6") - def some_func_2(cs, args): + def test_arguments_property_is_copied(self): + @cliutils.arg("argument_1") + @api_versions.wraps("2.666", "2.777") + @cliutils.arg("argument_2") + def some_func(): pass - args_1 = [(('name_1',), {'help': 'Name of the something'}), - (('action_1',), {'help': 'Some action'})] - self.assertEqual(args_1, some_func_1.arguments) + versioned_method = api_versions.get_substitutions( + nutils.get_function_name(some_func), + api_versions.APIVersion("2.700"))[0] - args_2 = [(('name_2',), {'help': 'Name of the something'}), - (('action_2',), {'help': 'Some action'})] - self.assertEqual(args_2, some_func_2.arguments) + self.assertEqual(some_func.arguments, + versioned_method.func.arguments) + self.assertIn((("argument_1",), {}), versioned_method.func.arguments) + self.assertIn((("argument_2",), {}), versioned_method.func.arguments) class DiscoverVersionTestCase(utils.TestCase): From b6677ebc036afa730e088115b41aeac7069594cd Mon Sep 17 00:00:00 2001 From: Kevin_Zheng Date: Tue, 15 Dec 2015 15:25:17 +0800 Subject: [PATCH 0935/1705] [microversions] Add support for API microversion 2.13 Nova now supports API microversion 2.13 to allow return project_id and user_id for os-server-groups API. Sync this to the client side. Change-Id: Ia09ab4bd5c693ed95b0f5dd9bc709b7597f7034e Closes-Bug: #1526143 --- novaclient/__init__.py | 2 +- .../v2/legacy/test_server_groups.py | 49 +++++++++++++++++ .../tests/functional/v2/test_server_groups.py | 53 +++++++++++++++++++ novaclient/v2/shell.py | 16 ++++-- 4 files changed, 115 insertions(+), 5 deletions(-) create mode 100644 novaclient/tests/functional/v2/legacy/test_server_groups.py create mode 100644 novaclient/tests/functional/v2/test_server_groups.py diff --git a/novaclient/__init__.py b/novaclient/__init__.py index f407d9e34..7f5808a43 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.12") +API_MAX_VERSION = api_versions.APIVersion("2.13") diff --git a/novaclient/tests/functional/v2/legacy/test_server_groups.py b/novaclient/tests/functional/v2/legacy/test_server_groups.py new file mode 100644 index 000000000..ef7ff9ea1 --- /dev/null +++ b/novaclient/tests/functional/v2/legacy/test_server_groups.py @@ -0,0 +1,49 @@ +# Copyright 2015 Huawei Technology corp. +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import uuid + +from novaclient.tests.functional import base + + +class TestServerGroupClient(base.ClientTestBase): + """Server groups v2.1 functional tests.""" + + COMPUTE_API_VERSION = "2.1" + + def _create_sg(self, policy): + sg_name = 'server_group-' + str(uuid.uuid4()) + output = self.nova('server-group-create %s %s' % (sg_name, policy)) + sg_id = self._get_column_value_from_single_row_table(output, "Id") + return sg_id + + def test_create_server_group(self): + sg_id = self._create_sg("affinity") + self.addCleanup(self.nova, 'server-group-delete %s' % sg_id) + sg = self.nova('server-group-get %s' % sg_id) + result = self._get_column_value_from_single_row_table(sg, "Id") + self.assertEqual(sg_id, result) + + def test_list_server_group(self): + sg_id = self._create_sg("affinity") + self.addCleanup(self.nova, 'server-group-delete %s' % sg_id) + sg = self.nova('server-group-list') + result = self._get_column_value_from_single_row_table(sg, "Id") + self.assertEqual(sg_id, result) + + def test_delete_server_group(self): + sg_id = self._create_sg("affinity") + sg = self.nova('server-group-get %s' % sg_id) + result = self._get_column_value_from_single_row_table(sg, "Id") + self.assertIsNotNone(result) + self.nova('server-group-delete %s' % sg_id) diff --git a/novaclient/tests/functional/v2/test_server_groups.py b/novaclient/tests/functional/v2/test_server_groups.py new file mode 100644 index 000000000..b2ad1262e --- /dev/null +++ b/novaclient/tests/functional/v2/test_server_groups.py @@ -0,0 +1,53 @@ +# Copyright 2015 Huawei Technology corp. +# 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 novaclient.tests.functional.v2.legacy import test_server_groups + + +class TestServerGroupClientV213(test_server_groups.TestServerGroupClient): + """Server groups v2.13 functional tests.""" + + COMPUTE_API_VERSION = "2.latest" + + def test_create_server_group(self): + sg_id = self._create_sg("affinity") + self.addCleanup(self.nova, 'server-group-delete %s' % sg_id) + sg = self.nova('server-group-get %s' % sg_id) + result = self._get_column_value_from_single_row_table(sg, "Id") + self._get_column_value_from_single_row_table( + sg, "User Id") + self._get_column_value_from_single_row_table( + sg, "Project Id") + self.assertEqual(sg_id, result) + + def test_list_server_groups(self): + sg_id = self._create_sg("affinity") + self.addCleanup(self.nova, 'server-group-delete %s' % sg_id) + sg = self.nova("server-group-list") + result = self._get_column_value_from_single_row_table(sg, "Id") + self._get_column_value_from_single_row_table( + sg, "User Id") + self._get_column_value_from_single_row_table( + sg, "Project Id") + self.assertEqual(sg_id, result) + + def test_get_server_group(self): + sg_id = self._create_sg("affinity") + self.addCleanup(self.nova, 'server-group-delete %s' % sg_id) + sg = self.nova('server-group-get %s' % sg_id) + result = self._get_column_value_from_single_row_table(sg, "Id") + self._get_column_value_from_single_row_table( + sg, "User Id") + self._get_column_value_from_single_row_table( + sg, "Project Id") + self.assertEqual(sg_id, result) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 5b6028e77..0e602ef04 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -4594,11 +4594,19 @@ def do_availability_zone_list(cs, _args): sortby_index=None) -def _print_server_group_details(server_group): +@api_versions.wraps("2.0", "2.12") +def _print_server_group_details(cs, server_group): columns = ['Id', 'Name', 'Policies', 'Members', 'Metadata'] utils.print_list(server_group, columns) +@api_versions.wraps("2.13") +def _print_server_group_details(cs, server_group): # noqa + columns = ['Id', 'Name', 'Project Id', 'User Id', + 'Policies', 'Members', 'Metadata'] + utils.print_list(server_group, columns) + + @cliutils.arg( '--all-projects', dest='all_projects', @@ -4608,7 +4616,7 @@ def _print_server_group_details(server_group): def do_server_group_list(cs, args): """Print a list of all server groups.""" server_groups = cs.server_groups.list(args.all_projects) - _print_server_group_details(server_groups) + _print_server_group_details(cs, server_groups) def do_secgroup_list_default_rules(cs, args): @@ -4702,7 +4710,7 @@ def do_server_group_create(cs, args): kwargs = {'name': args.name, 'policies': args.policy} server_group = cs.server_groups.create(**kwargs) - _print_server_group_details([server_group]) + _print_server_group_details(cs, [server_group]) @cliutils.arg( @@ -4734,7 +4742,7 @@ def do_server_group_delete(cs, args): def do_server_group_get(cs, args): """Get a specific server group.""" server_group = cs.server_groups.get(args.id) - _print_server_group_details([server_group]) + _print_server_group_details(cs, [server_group]) def do_version_list(cs, args): From dd6b3cd3941e5af7fa24e0b6d5f45207bdfdd641 Mon Sep 17 00:00:00 2001 From: Zhihai Song Date: Wed, 23 Dec 2015 15:11:40 +0800 Subject: [PATCH 0936/1705] Validate the fixed ip address passed with --nic Currently fixed ip address passed with --nic is not validated. This patch add the validation to the fixed address. Change-Id: I032cc9ce9333b723d37e94b81d699cc0d78d36bf Closes-Bug: #1528455 --- novaclient/tests/unit/v2/test_shell.py | 10 ++++++++++ novaclient/v2/shell.py | 9 +++++++++ 2 files changed, 19 insertions(+) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index d803a888a..4b4cb318a 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -561,6 +561,16 @@ def test_boot_nics_netid_and_portid(self): '--nic port-id=some=port,net-id=some=net some-server') self.assertRaises(exceptions.CommandError, self.run_command, cmd) + def test_boot_nics_invalid_ipv4(self): + cmd = ('boot --image 1 --flavor 1 ' + '--nic net-id=a=c,v4-fixed-ip=2001:db9:0:1::10 some-server') + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + def test_boot_nics_invalid_ipv6(self): + cmd = ('boot --image 1 --flavor 1 ' + '--nic net-id=a=c,v6-fixed-ip=10.0.0.1 some-server') + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + def test_boot_files(self): testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt') with open(testfile) as testfile_fd: diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index c0725ab6a..b2c654250 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -30,6 +30,7 @@ import warnings from oslo_utils import encodeutils +from oslo_utils import netutils from oslo_utils import strutils from oslo_utils import timeutils from oslo_utils import uuidutils @@ -288,6 +289,14 @@ def _boot(cs, args): else: raise exceptions.CommandError(err_msg) + if nic_info['v4-fixed-ip'] and not netutils.is_valid_ipv4( + nic_info['v4-fixed-ip']): + raise exceptions.CommandError(_("Invalid ipv4 address.")) + + if nic_info['v6-fixed-ip'] and not netutils.is_valid_ipv6( + nic_info['v6-fixed-ip']): + raise exceptions.CommandError(_("Invalid ipv6 address.")) + if bool(nic_info['net-id']) == bool(nic_info['port-id']): raise exceptions.CommandError(err_msg) From 8f320313f3b9454a6c47db9665304fba09eab245 Mon Sep 17 00:00:00 2001 From: Andreas Jaeger Date: Sun, 27 Dec 2015 22:13:31 +0100 Subject: [PATCH 0937/1705] Fix help strings Reviewing the import of novaclient 3.1.0 into the CLI Reference (change Ib39cdfdd563a2a53f0e6de1f8d2dc4f8c4678c15), a few inconsistencies where found and fixed: * Missing "." at end of help string * Inconsistent capitalization of "DEPRECATED, use" * Inconsistent capitalization of "Set or delete" * It's IPv4 and IPv6 * It's ID Change-Id: I425cd6575ca31efd92fc7f0be6192031886fcc41 --- novaclient/shell.py | 4 ++-- novaclient/tests/unit/test_shell.py | 6 +++--- novaclient/v2/contrib/tenant_networks.py | 2 +- novaclient/v2/shell.py | 20 ++++++++++---------- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/novaclient/shell.py b/novaclient/shell.py index 6a75d83c4..1e08b8146 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -647,12 +647,12 @@ def main(self, argv): if not os_username and not os_user_id: raise exc.CommandError( _("You must provide a username " - "or user id via --os-username, --os-user-id, " + "or user ID via --os-username, --os-user-id, " "env[OS_USERNAME] or env[OS_USER_ID]")) if not any([os_project_name, os_project_id]): raise exc.CommandError(_("You must provide a project name or" - " project id via --os-project-name," + " project ID via --os-project-name," " --os-project-id, env[OS_PROJECT_ID]" " or env[OS_PROJECT_NAME]. You may" " use os-project and os-tenant" diff --git a/novaclient/tests/unit/test_shell.py b/novaclient/tests/unit/test_shell.py index bba95cc3d..67c913aea 100644 --- a/novaclient/tests/unit/test_shell.py +++ b/novaclient/tests/unit/test_shell.py @@ -104,7 +104,7 @@ def test_not_really_ambiguous_option(self): class ShellTest(utils.TestCase): _msg_no_tenant_project = ("You must provide a project name or project" - " id via --os-project-name, --os-project-id," + " ID via --os-project-name, --os-project-id," " env[OS_PROJECT_ID] or env[OS_PROJECT_NAME]." " You may use os-project and os-tenant" " interchangeably.") @@ -219,7 +219,7 @@ def test_bash_completion(self): matchers.MatchesRegex(r, re.DOTALL | re.MULTILINE)) def test_no_username(self): - required = ('You must provide a username or user id' + required = ('You must provide a username or user ID' ' via --os-username, --os-user-id,' ' env[OS_USERNAME] or env[OS_USER_ID]') self.make_env(exclude='OS_USERNAME') @@ -231,7 +231,7 @@ def test_no_username(self): self.fail('CommandError not raised') def test_no_user_id(self): - required = ('You must provide a username or user id' + required = ('You must provide a username or user ID' ' via --os-username, --os-user-id,' ' env[OS_USERNAME] or env[OS_USER_ID]') self.make_env(exclude='OS_USER_ID', fake_env=FAKE_ENV2) diff --git a/novaclient/v2/contrib/tenant_networks.py b/novaclient/v2/contrib/tenant_networks.py index 0b56ae8cc..0bc569336 100644 --- a/novaclient/v2/contrib/tenant_networks.py +++ b/novaclient/v2/contrib/tenant_networks.py @@ -44,7 +44,7 @@ def create(self, label, cidr): @cliutils.arg('network_id', metavar='', help='ID of network') def do_net(cs, args): """ - DEPRECATED, Use tenant-network-show instead. + DEPRECATED, use tenant-network-show instead. """ do_tenant_network_show(cs, args) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index c0725ab6a..ea1571cb4 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -525,13 +525,13 @@ def _boot(cs, args): dest='access_ip_v4', metavar='', default=None, - help=_('Alternative access ip v4 of the instance.')) + help=_('Alternative access IPv4 of the instance.')) @cliutils.arg( '--access-ip-v6', dest='access_ip_v6', metavar='', default=None, - help=_('Alternative access ip v6 of the instance.')) + help=_('Alternative access IPv6 of the instance.')) def do_boot(cs, args): """Boot a new server.""" boot_args, boot_kwargs = _boot(cs, args) @@ -1190,7 +1190,7 @@ def parse_server_name(image): help=_('Metadata to add/update or delete (only key is necessary on ' 'delete).')) def do_image_meta(cs, args): - """Set or Delete metadata on an image.""" + """Set or delete metadata on an image.""" image = _find_image(cs, args.image) metadata = _extract_metadata(args) @@ -1935,7 +1935,7 @@ def do_backup(cs, args): default=[], help=_('Metadata to set or delete (only key is necessary on delete).')) def do_meta(cs, args): - """Set or Delete metadata on a server.""" + """Set or delete metadata on a server.""" server = _find_server(cs, args.server) metadata = _extract_metadata(args) @@ -2159,14 +2159,14 @@ def do_volume_show(cs, args): '--snapshot-id', metavar='', default=None, - help=_('Optional snapshot id to create the volume from. (Default=None)')) + help=_('Optional snapshot ID to create the volume from. (Default=None)')) @cliutils.arg( '--snapshot_id', help=argparse.SUPPRESS) @cliutils.arg( '--image-id', metavar='', - help=_('Optional image id to create the volume from. (Default=None)'), + help=_('Optional image ID to create the volume from. (Default=None)'), default=None) @cliutils.arg( '--display-name', @@ -2286,7 +2286,7 @@ def do_volume_detach(cs, args): metavar='', help=_('Name or ID of server.')) def do_volume_attachments(cs, args): - """List all the volumes attached to a server""" + """List all the volumes attached to a server.""" volumes = cs.volumes.get_server_volumes(_find_server(cs, args.server).id) _translate_volume_attachments_keys(volumes) utils.print_list(volumes, ['ID', 'DEVICE', 'SERVER ID', 'VOLUME ID']) @@ -3096,7 +3096,7 @@ def do_keypair_delete(cs, args): '--user', metavar='', default=None, - help=_('Id of key-pair owner (Admin only).')) + help=_('ID of key-pair owner (Admin only).')) def do_keypair_delete(cs, args): """Delete keypair given by its name.""" cs.keypairs.delete(args.name, args.user) @@ -3125,7 +3125,7 @@ def do_keypair_list(cs, args): '--user', metavar='', default=None, - help=_('List key-pairs of specified user id (Admin only).')) + help=_('List key-pairs of specified user ID (Admin only).')) def do_keypair_list(cs, args): """Print a list of keypairs for a user""" keypairs = cs.keypairs.list(args.user) @@ -3160,7 +3160,7 @@ def do_keypair_show(cs, args): '--user', metavar='', default=None, - help=_('Id of key-pair owner (Admin only).')) + help=_('ID of key-pair owner (Admin only).')) def do_keypair_show(cs, args): """Show details about the given keypair.""" keypair = cs.keypairs.get(args.keypair, args.user) From 2283b46a2f63b9fdb5c3c20a1ddfbba4c37d858e Mon Sep 17 00:00:00 2001 From: hgangwx Date: Wed, 30 Dec 2015 14:59:29 +0800 Subject: [PATCH 0938/1705] Wrong usage of "a/an" Wrong usage of "a/an" in the messages: "It will be deprecated after an suitable deprecation" "Delete metadata from an server" "Get a list of actions performed on an server" "based on an requests response" "Get a aggregate by name or ID" Should be: "It will be deprecated after a suitable deprecation" "Delete metadata from a server" "Get a list of actions performed on a server" "based on a requests response" "Get an aggregate by name or ID" Totally 5 occurrences in python-novaclient base code. Change-Id: Iabe764f8b547b39431deb34221266695e0e79fa9 --- novaclient/exceptions.py | 2 +- novaclient/v2/contrib/instance_action.py | 2 +- novaclient/v2/servers.py | 2 +- novaclient/v2/shell.py | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/novaclient/exceptions.py b/novaclient/exceptions.py index cbe701083..94591f26d 100644 --- a/novaclient/exceptions.py +++ b/novaclient/exceptions.py @@ -241,7 +241,7 @@ class InvalidUsage(RuntimeError): def from_response(response, body, url, method=None): """ Return an instance of an ClientException or subclass - based on an requests response. + based on a requests response. Usage:: diff --git a/novaclient/v2/contrib/instance_action.py b/novaclient/v2/contrib/instance_action.py index 10e518ea2..2a7c6b8df 100644 --- a/novaclient/v2/contrib/instance_action.py +++ b/novaclient/v2/contrib/instance_action.py @@ -35,7 +35,7 @@ def get(self, server, request_id): def list(self, server): """ - Get a list of actions performed on an server. + Get a list of actions performed on a server. """ return self._list('/servers/%s/os-instance-actions' % base.getid(server), 'instanceActions') diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index bfab3f446..6c5f69f3a 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -1222,7 +1222,7 @@ def get_console_output(self, server, length=None): def delete_meta(self, server, keys): """ - Delete metadata from an server + Delete metadata from a server :param server: The :class:`Server` to add metadata to :param keys: A list of metadata keys to delete from the server """ diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index c0725ab6a..16effea99 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -3501,7 +3501,7 @@ def do_agent_modify(cs, args): def _find_aggregate(cs, aggregate): - """Get a aggregate by name or ID.""" + """Get an aggregate by name or ID.""" return utils.find_resource(cs.aggregates, aggregate) @@ -4718,7 +4718,7 @@ def do_secgroup_delete_default_rule(cs, args): @cliutils.arg('name', metavar='', help=_('Server group name.')) # NOTE(wingwj): The '--policy' way is still reserved here for preserving # the backwards compatibility of CLI, even if a user won't get this usage -# in '--help' description. It will be deprecated after an suitable deprecation +# in '--help' description. It will be deprecated after a suitable deprecation # period(probably 2 coordinated releases or so). # # Moreover, we imagine that a given user will use only positional parameters or From 48c9a91de6951fd428eb866fe9356c5ebf9601a1 Mon Sep 17 00:00:00 2001 From: Jim Rollenhagen Date: Wed, 30 Dec 2015 23:58:20 +0000 Subject: [PATCH 0939/1705] Fix extension loading from python path on Python 2.7 Commit 81f8fa655ccecd409fe6dcda0d3763592c053e57 broke extension loading from pythonpath on python 2.7 due to the removal of code that was erroneously marked for 2.6 compatibility. Put it back. Change-Id: Ic04f4d57953967fde9817ff5119ce0182453a86d Closes-Bug: #1530230 --- novaclient/client.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/novaclient/client.py b/novaclient/client.py index 11190687d..b2c003795 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -741,6 +741,8 @@ def discover_extensions(version): def _discover_via_python_path(): for (module_loader, name, _ispkg) in pkgutil.iter_modules(): if name.endswith('_python_novaclient_ext'): + if not hasattr(module_loader, 'load_module'): + module_loader = module_loader.find_module(name) module = module_loader.load_module(name) if hasattr(module, 'extension_name'): name = module.extension_name From 5a3956c3995cf4a81199e96345b91f6ef9f5d29d Mon Sep 17 00:00:00 2001 From: Balazs Gibizer Date: Fri, 18 Dec 2015 16:42:23 +0100 Subject: [PATCH 0940/1705] [microversions] Add support for 2.14 In I54bfa1275e188573c1b95d770d89160a86cdf52c the onSharedStorage flag is removed from the evacuate API. This patch removes it from the novaclient as well. Implements: bp remove-shared-storage-flag-in-evacuate-api Change-Id: I5ae75fdac226f0246f22a4d5245c1e4952571fc1 --- novaclient/__init__.py | 2 +- novaclient/tests/unit/fixture_data/servers.py | 2 +- novaclient/tests/unit/v2/test_servers.py | 14 +++++++ novaclient/v2/servers.py | 37 +++++++++++++++++-- novaclient/v2/shell.py | 10 +++-- 5 files changed, 56 insertions(+), 9 deletions(-) diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 7f5808a43..50ad863f5 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.13") +API_MAX_VERSION = api_versions.APIVersion("2.14") diff --git a/novaclient/tests/unit/fixture_data/servers.py b/novaclient/tests/unit/fixture_data/servers.py index 595dcc9ec..4aff762a6 100644 --- a/novaclient/tests/unit/fixture_data/servers.py +++ b/novaclient/tests/unit/fixture_data/servers.py @@ -449,7 +449,7 @@ def post_servers_1234_action(self, request, context): keys = list(body[action]) if 'adminPass' in keys: keys.remove('adminPass') - assert set(keys) == set(['host', 'onSharedStorage']) + assert 'host' in keys else: raise AssertionError("Unexpected server action: %s" % action) return {'server': _body} diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index 7cec07e1f..1e0324a00 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -871,3 +871,17 @@ def test_get_mks_console(self): self.cs.servers.get_mks_console(s) self.assert_called('POST', '/servers/1234/remote-consoles') + + +class ServersV214Test(ServersV28Test): + def setUp(self): + super(ServersV214Test, self).setUp() + self.cs.api_version = api_versions.APIVersion("2.14") + + def test_evacuate(self): + s = self.cs.servers.get(1234) + s.evacuate('fake_target_host') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.evacuate(s, 'fake_target_host', + password='NewAdminPassword') + self.assert_called('POST', '/servers/1234/action') diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index bfab3f446..fdf77dfd1 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -388,17 +388,28 @@ def list_security_group(self): """ return self.manager.list_security_group(self) - def evacuate(self, host=None, on_shared_storage=True, password=None): + def evacuate(self, host=None, on_shared_storage=None, password=None): """ Evacuate an instance from failed host to specified host. :param host: Name of the target host :param on_shared_storage: Specifies whether instance files located - on shared storage + on shared storage. After microversion 2.14, this + parameter must have its default value of None. :param password: string to set as admin password on the evacuated server. """ - return self.manager.evacuate(self, host, on_shared_storage, password) + if api_versions.APIVersion("2.14") <= self.manager.api_version: + if on_shared_storage is not None: + raise ValueError("Setting 'on_shared_storage' argument is " + "prohibited after microversion 2.14.") + return self.manager.evacuate(self, host, password) + else: + # microversions 2.0 - 2.13 + if on_shared_storage is None: + on_shared_storage = True + return self.manager.evacuate(self, host, on_shared_storage, + password) def interface_list(self): """ @@ -1291,6 +1302,7 @@ def list_security_group(self, server): base.getid(server), 'security_groups', security_groups.SecurityGroup) + @api_versions.wraps("2.0", "2.13") def evacuate(self, server, host=None, on_shared_storage=True, password=None): """ @@ -1312,6 +1324,25 @@ def evacuate(self, server, host=None, on_shared_storage=True, return self._action('evacuate', server, body) + @api_versions.wraps("2.14") + def evacuate(self, server, host=None, password=None): + """ + Evacuate a server instance. + + :param server: The :class:`Server` (or its ID) to share onto. + :param host: Name of the target host. + :param password: string to set as password on the evacuated server. + """ + + body = {} + if host is not None: + body['host'] = host + + if password is not None: + body['adminPass'] = password + + return self._action('evacuate', server, body) + def interface_list(self, server): """ List attached network interfaces diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 0e602ef04..a4571ecb1 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -4454,19 +4454,21 @@ def do_quota_class_update(cs, args): dest='password', metavar='', help=_("Set the provided admin password on the evacuated server. Not" - " applicable with on-shared-storage flag.")) + " applicable if the server is on shared storage.")) @cliutils.arg( '--on-shared-storage', dest='on_shared_storage', action="store_true", default=False, - help=_('Specifies whether server files are located on shared storage.')) + help=_('Specifies whether server files are located on shared storage.'), + start_version='2.0', + end_version='2.13') def do_evacuate(cs, args): """Evacuate server from failed host.""" server = _find_server(cs, args.server) - - res = server.evacuate(args.host, args.on_shared_storage, args.password)[1] + on_shared_storage = getattr(args, 'on_shared_storage', None) + res = server.evacuate(args.host, on_shared_storage, args.password)[1] if type(res) is dict: utils.print_dict(res) From f9f9a4654c370fcfc5ae977596fdb659392a0fed Mon Sep 17 00:00:00 2001 From: Sean Dague Date: Mon, 4 Jan 2016 12:31:15 +0000 Subject: [PATCH 0941/1705] Add python 2.7 comment Change-Id: Ie4183da314bd7ca0719f45f0d9b3c26f8707e266 --- novaclient/client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/novaclient/client.py b/novaclient/client.py index b2c003795..de85905ab 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -741,6 +741,7 @@ def discover_extensions(version): def _discover_via_python_path(): for (module_loader, name, _ispkg) in pkgutil.iter_modules(): if name.endswith('_python_novaclient_ext'): + # NOTE(sdague): needed for python 2.x compatibility. if not hasattr(module_loader, 'load_module'): module_loader = module_loader.find_module(name) module = module_loader.load_module(name) From 06be50b97a28f490951ca44f9880be161a54ecc1 Mon Sep 17 00:00:00 2001 From: yangyapeng Date: Mon, 4 Jan 2016 09:39:02 -0500 Subject: [PATCH 0942/1705] Use assertTrue/False instead of assertEqual(T/F) The usage of assertEqual(True/False, ***) should be changed to a meaningful format of assertTrue/False(***). Change-Id: I66110e5fc1afdf9dd6824b1f7bcb7b2c091a272d Closes-Bug: #1512207 --- novaclient/tests/functional/v2/legacy/test_fixedips.py | 3 +-- novaclient/tests/unit/v2/test_services.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/novaclient/tests/functional/v2/legacy/test_fixedips.py b/novaclient/tests/functional/v2/legacy/test_fixedips.py index 7082c110d..0206a140c 100644 --- a/novaclient/tests/functional/v2/legacy/test_fixedips.py +++ b/novaclient/tests/functional/v2/legacy/test_fixedips.py @@ -43,8 +43,7 @@ def _test_fixedip_get(self, expect_reserved=False): reserved = self._get_column_value_from_single_row_table(table, 'reserved') # By default the fixed IP should not be reserved. - self.assertEqual(False, strutils.bool_from_string(reserved, - strict=True)) + self.assertFalse(strutils.bool_from_string(reserved, strict=True)) else: self.assertRaises(ValueError, self._get_column_value_from_single_row_table, diff --git a/novaclient/tests/unit/v2/test_services.py b/novaclient/tests/unit/v2/test_services.py index f7ab553a0..f182e2d1c 100644 --- a/novaclient/tests/unit/v2/test_services.py +++ b/novaclient/tests/unit/v2/test_services.py @@ -122,4 +122,4 @@ def test_services_force_down(self): force_down=False) self.cs.assert_called('PUT', '/os-services/force-down', values) self.assertIsInstance(service, self._get_service_type()) - self.assertEqual(False, service.forced_down) + self.assertFalse(service.forced_down) From 6d023a6bc299cb355f0ee90d626603456aa982f8 Mon Sep 17 00:00:00 2001 From: LiuNanke Date: Thu, 7 Jan 2016 02:55:45 +0800 Subject: [PATCH 0943/1705] Replace assertTrue(isinstance()) by optimal assert assertTrue(isinstance(A, B)) or assertEqual(type(A), B) in tests should be replaced by assertIsInstance(A, B) provided by testtools. Change-Id: Ie4fe7b40e3f4350db94b350395269340ee6acf11 Related-bug: #1268480 --- .../tests/unit/v2/test_security_group_rules.py | 4 ++-- novaclient/tests/unit/v2/test_server_groups.py | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/novaclient/tests/unit/v2/test_security_group_rules.py b/novaclient/tests/unit/v2/test_security_group_rules.py index 837519821..6c0c8ac75 100644 --- a/novaclient/tests/unit/v2/test_security_group_rules.py +++ b/novaclient/tests/unit/v2/test_security_group_rules.py @@ -43,7 +43,7 @@ def test_create_security_group_rule(self): } self.assert_called('POST', '/os-security-group-rules', body) - self.assertTrue(isinstance(sg, security_group_rules.SecurityGroupRule)) + self.assertIsInstance(sg, security_group_rules.SecurityGroupRule) def test_create_security_group_group_rule(self): sg = self.cs.security_group_rules.create(1, "tcp", 1, 65535, @@ -61,7 +61,7 @@ def test_create_security_group_group_rule(self): } self.assert_called('POST', '/os-security-group-rules', body) - self.assertTrue(isinstance(sg, security_group_rules.SecurityGroupRule)) + self.assertIsInstance(sg, security_group_rules.SecurityGroupRule) def test_invalid_parameters_create(self): self.assertRaises(exceptions.CommandError, diff --git a/novaclient/tests/unit/v2/test_server_groups.py b/novaclient/tests/unit/v2/test_server_groups.py index 20b3f4f43..8648632c0 100644 --- a/novaclient/tests/unit/v2/test_server_groups.py +++ b/novaclient/tests/unit/v2/test_server_groups.py @@ -28,15 +28,15 @@ def test_list_server_groups(self): result = self.cs.server_groups.list() self.assert_called('GET', '/os-server-groups') for server_group in result: - self.assertTrue(isinstance(server_group, - server_groups.ServerGroup)) + self.assertIsInstance(server_group, + server_groups.ServerGroup) def test_list_server_groups_with_all_projects(self): result = self.cs.server_groups.list(all_projects=True) self.assert_called('GET', '/os-server-groups?all_projects') for server_group in result: - self.assertTrue(isinstance(server_group, - server_groups.ServerGroup)) + self.assertIsInstance(server_group, + server_groups.ServerGroup) def test_create_server_group(self): kwargs = {'name': 'ig1', @@ -44,15 +44,15 @@ def test_create_server_group(self): server_group = self.cs.server_groups.create(**kwargs) body = {'server_group': kwargs} self.assert_called('POST', '/os-server-groups', body) - self.assertTrue(isinstance(server_group, - server_groups.ServerGroup)) + self.assertIsInstance(server_group, + server_groups.ServerGroup) def test_get_server_group(self): id = '2cbd51f4-fafe-4cdb-801b-cf913a6f288b' server_group = self.cs.server_groups.get(id) self.assert_called('GET', '/os-server-groups/%s' % id) - self.assertTrue(isinstance(server_group, - server_groups.ServerGroup)) + self.assertIsInstance(server_group, + server_groups.ServerGroup) def test_delete_server_group(self): id = '2cbd51f4-fafe-4cdb-801b-cf913a6f288b' From ff980114b6990daf60fb22554b1f00c89a22dbb1 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Thu, 7 Jan 2016 04:57:56 +0000 Subject: [PATCH 0944/1705] Updated from global requirements Change-Id: I19f98a76b8e0d595fe75842db7b23453ef7e102a --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index de8e472fa..60a1c2c87 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -11,7 +11,7 @@ mock>=1.2 python-keystoneclient!=1.8.0,>=1.6.0 requests-mock>=0.7.0 # Apache-2.0 sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2 -os-client-config!=1.6.2,>=1.4.0 +os-client-config>=1.13.1 oslosphinx!=3.4.0,>=2.5.0 # Apache-2.0 testrepository>=0.0.18 testscenarios>=0.4 From 08c4b746b2b894a5cb178c762850a1730e1c636e Mon Sep 17 00:00:00 2001 From: "Kevin L. Mitchell" Date: Thu, 7 Jan 2016 11:07:37 -0600 Subject: [PATCH 0945/1705] Add a mechanism to call out deprecated options We have had deprecated command line options in novaclient for a long time, but no warnings about the deprecations were ever emitted, despite the help text being suppressed. It would be nice to finally get rid of those deprecated options. This change sets up the precondition to accomplish this, by emitting appropriate warnings when a deprecated option is used. Note: The "use" text for the deprecated options has been deliberately chosen to reduce the number of translations required. Change-Id: Ibe13faa56c5abca97f85f9c5172ef5a5591b5f71 --- novaclient/shell.py | 201 +++++++++++++++++++++- novaclient/tests/unit/test_shell.py | 256 ++++++++++++++++++++++++++++ novaclient/v2/shell.py | 112 +++++++++++- 3 files changed, 558 insertions(+), 11 deletions(-) diff --git a/novaclient/shell.py b/novaclient/shell.py index a43c02f84..18d83aa06 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -29,6 +29,7 @@ from oslo_utils import encodeutils from oslo_utils import importutils from oslo_utils import strutils +import six HAS_KEYRING = False all_errors = ValueError @@ -63,6 +64,153 @@ logger = logging.getLogger(__name__) +class DeprecatedAction(argparse.Action): + """An argparse action for deprecated options. + + This class is an ``argparse.Action`` subclass that allows command + line options to be explicitly deprecated. It modifies the help + text for the option to indicate that it's deprecated (unless help + has been suppressed using ``argparse.SUPPRESS``), and provides a + means to specify an alternate option to use using the ``use`` + keyword argument to ``argparse.ArgumentParser.add_argument()``. + The original action may be specified with the ``real_action`` + keyword argument, which has the same interpretation as the + ``action`` argument to ``argparse.ArgumentParser.add_argument()``, + with the addition of the special "nothing" action which completely + ignores the option (other than emitting the deprecation warning). + Note that the deprecation warning is only emitted once per + specific option string. + + Note: If the ``real_action`` keyword argument specifies an unknown + action, no warning will be emitted unless the action is used, due + to limitations with the method used to resolve the action names. + """ + + def __init__(self, option_strings, dest, help=None, + real_action=None, use=None, **kwargs): + """Initialize a ``DeprecatedAction`` instance. + + :param option_strings: The recognized option strings. + :param dest: The attribute that will be set. + :param help: Help text. This will be updated to indicate the + deprecation, and if ``use`` is provided, that + text will be included as well. + :param real_action: The actual action to invoke. This is + interpreted the same way as the ``action`` + parameter. + :param use: Text explaining which option to use instead. + """ + + # Update the help text + if not help: + if use: + help = _('Deprecated; %(use)s') % {'use': use} + else: + help = _('Deprecated') + elif help != argparse.SUPPRESS: + if use: + help = _('%(help)s (Deprecated; %(use)s)') % { + 'help': help, + 'use': use, + } + else: + help = _('%(help)s (Deprecated)') % {'help': help} + + # Initialize ourself appropriately + super(DeprecatedAction, self).__init__( + option_strings, dest, help=help, **kwargs) + + # 'emitted' tracks which warnings we've emitted + self.emitted = set() + self.use = use + + # Select the appropriate action + if real_action == 'nothing': + # NOTE(Vek): "nothing" is distinct from a real_action=None + # argument. When real_action=None, the argparse default + # action of "store" is used; when real_action='nothing', + # however, we explicitly inhibit doing anything with the + # option + self.real_action_args = False + self.real_action = None + elif real_action is None or isinstance(real_action, six.string_types): + # Specified by string (or None); we have to have a parser + # to look up the actual action, so defer to later + self.real_action_args = (option_strings, dest, help, kwargs) + self.real_action = real_action + else: + self.real_action_args = False + self.real_action = real_action( + option_strings, dest, help=help, **kwargs) + + def _get_action(self, parser): + """Retrieve the action callable. + + This internal method is used to retrieve the callable + implementing the action. If ``real_action`` was specified as + ``None`` or one of the standard string names, an internal + method of the ``argparse.ArgumentParser`` instance is used to + resolve it into an actual action class, which is then + instantiated. This is cached, in case the action is called + multiple times. + + :param parser: The ``argparse.ArgumentParser`` instance. + + :returns: The action callable. + """ + + # If a lookup is needed, look up the action in the parser + if self.real_action_args is not False: + option_strings, dest, help, kwargs = self.real_action_args + action_class = parser._registry_get('action', self.real_action) + + # Did we find the action class? + if action_class is None: + print(_('WARNING: Programming error: Unknown real action ' + '"%s"') % self.real_action, file=sys.stderr) + self.real_action = None + else: + # OK, instantiate the action class + self.real_action = action_class( + option_strings, dest, help=help, **kwargs) + + # It's been resolved, no further need to look it up + self.real_action_args = False + + return self.real_action + + def __call__(self, parser, namespace, values, option_string): + """Implement the action. + + Emits the deprecation warning message (only once for any given + option string), then calls the real action (if any). + + :param parser: The ``argparse.ArgumentParser`` instance. + :param namespace: The ``argparse.Namespace`` object which + should have an attribute set. + :param values: Any arguments provided to the option. + :param option_string: The option string that was used. + """ + + action = self._get_action(parser) + + # Only emit the deprecation warning once per option + if option_string not in self.emitted: + if self.use: + print(_('WARNING: Option "%(option)s" is deprecated; ' + '%(use)s') % { + 'option': option_string, + 'use': self.use, + }, file=sys.stderr) + else: + print(_('WARNING: Option "%(option)s" is deprecated') % + {'option': option_string}, file=sys.stderr) + self.emitted.add(option_string) + + if action: + action(parser, namespace, values, option_string) + + def positive_non_zero_float(text): if text is None: return None @@ -313,24 +461,32 @@ def get_base_parser(self, argv): action='store_true', help=_("Print call timing info.")) - parser.add_argument( - '--os-auth-token', - help=argparse.SUPPRESS) - parser.add_argument( '--os_username', + action=DeprecatedAction, + use=_('use "%s"; this option will be removed ' + 'in novaclient 3.3.0.') % '--os-username', help=argparse.SUPPRESS) parser.add_argument( '--os_password', + action=DeprecatedAction, + use=_('use "%s"; this option will be removed ' + 'in novaclient 3.3.0.') % '--os-password', help=argparse.SUPPRESS) parser.add_argument( '--os_tenant_name', + action=DeprecatedAction, + use=_('use "%s"; this option will be removed ' + 'in novaclient 3.3.0.') % '--os-tenant-name', help=argparse.SUPPRESS) parser.add_argument( '--os_auth_url', + action=DeprecatedAction, + use=_('use "%s"; this option will be removed ' + 'in novaclient 3.3.0.') % '--os-auth-url', help=argparse.SUPPRESS) parser.add_argument( @@ -340,6 +496,9 @@ def get_base_parser(self, argv): help=_('Defaults to env[OS_REGION_NAME].')) parser.add_argument( '--os_region_name', + action=DeprecatedAction, + use=_('use "%s"; this option will be removed ' + 'in novaclient 3.3.0.') % '--os-region-name', help=argparse.SUPPRESS) parser.add_argument( @@ -349,6 +508,9 @@ def get_base_parser(self, argv): help=argparse.SUPPRESS) parser.add_argument( '--os_auth_system', + action=DeprecatedAction, + use=_('use "%s"; this option will be removed ' + 'in novaclient 3.3.0.') % '--os-auth-system', help=argparse.SUPPRESS) parser.add_argument( @@ -357,6 +519,9 @@ def get_base_parser(self, argv): help=_('Defaults to compute for most actions.')) parser.add_argument( '--service_type', + action=DeprecatedAction, + use=_('use "%s"; this option will be removed ' + 'in novaclient 3.3.0.') % '--service-type', help=argparse.SUPPRESS) parser.add_argument( @@ -366,6 +531,9 @@ def get_base_parser(self, argv): help=_('Defaults to env[NOVA_SERVICE_NAME].')) parser.add_argument( '--service_name', + action=DeprecatedAction, + use=_('use "%s"; this option will be removed ' + 'in novaclient 3.3.0.') % '--service-name', help=argparse.SUPPRESS) parser.add_argument( @@ -375,6 +543,9 @@ def get_base_parser(self, argv): help=_('Defaults to env[NOVA_VOLUME_SERVICE_NAME].')) parser.add_argument( '--volume_service_name', + action=DeprecatedAction, + use=_('use "%s"; this option will be removed ' + 'in novaclient 3.3.0.') % '--volume-service-name', help=argparse.SUPPRESS) parser.add_argument( @@ -391,6 +562,9 @@ def get_base_parser(self, argv): parser.add_argument( '--endpoint-type', + action=DeprecatedAction, + use=_('use "%s"; this option will be removed ' + 'in novaclient 3.3.0.') % '--os-endpoint-type', help=argparse.SUPPRESS) # NOTE(dtroyer): We can't add --endpoint_type here due to argparse # thinking usage-list --end is ambiguous; but it @@ -408,6 +582,9 @@ def get_base_parser(self, argv): '"X.latest", defaults to env[OS_COMPUTE_API_VERSION].')) parser.add_argument( '--os_compute_api_version', + action=DeprecatedAction, + use=_('use "%s"; this option will be removed ' + 'in novaclient 3.3.0.') % '--os-compute-api-version', help=argparse.SUPPRESS) parser.add_argument( @@ -417,8 +594,12 @@ def get_base_parser(self, argv): default=utils.env('NOVACLIENT_BYPASS_URL'), help="Use this API endpoint instead of the Service Catalog. " "Defaults to env[NOVACLIENT_BYPASS_URL].") - parser.add_argument('--bypass_url', - help=argparse.SUPPRESS) + parser.add_argument( + '--bypass_url', + action=DeprecatedAction, + use=_('use "%s"; this option will be removed ' + 'in novaclient 3.3.0.') % '--bypass-url', + help=argparse.SUPPRESS) # The auth-system-plugins might require some extra options novaclient.auth_plugin.load_auth_system_opts(parser) @@ -544,10 +725,18 @@ def main(self, argv): if '--endpoint_type' in argv: spot = argv.index('--endpoint_type') argv[spot] = '--endpoint-type' + # NOTE(Vek): Not emitting a warning here, as that will + # occur when "--endpoint-type" is processed + # For backwards compat with old os-auth-token parameter if '--os-auth-token' in argv: spot = argv.index('--os-auth-token') argv[spot] = '--os-token' + print(_('WARNING: Option "%(option)s" is deprecated; %(use)s') % { + 'option': '--os-auth-token', + 'use': _('use "%s"; this option will be removed in ' + 'novaclient 3.3.0.') % '--os-token', + }, file=sys.stderr) (args, args_list) = parser.parse_known_args(argv) diff --git a/novaclient/tests/unit/test_shell.py b/novaclient/tests/unit/test_shell.py index 82559cf24..38f569546 100644 --- a/novaclient/tests/unit/test_shell.py +++ b/novaclient/tests/unit/test_shell.py @@ -11,6 +11,7 @@ # License for the specific language governing permissions and limitations # under the License. +import argparse import distutils.version as dist_version import re import sys @@ -76,6 +77,261 @@ def _create_ver_list(versions): return {'versions': {'values': versions}} +class DeprecatedActionTest(utils.TestCase): + @mock.patch.object(argparse.Action, '__init__', return_value=None) + def test_init_emptyhelp_nouse(self, mock_init): + result = novaclient.shell.DeprecatedAction( + 'option_strings', 'dest', a=1, b=2, c=3) + + self.assertEqual(result.emitted, set()) + self.assertEqual(result.use, None) + self.assertEqual(result.real_action_args, + ('option_strings', 'dest', 'Deprecated', + {'a': 1, 'b': 2, 'c': 3})) + self.assertEqual(result.real_action, None) + mock_init.assert_called_once_with( + 'option_strings', 'dest', help='Deprecated', a=1, b=2, c=3) + + @mock.patch.object(novaclient.shell.argparse.Action, '__init__', + return_value=None) + def test_init_emptyhelp_withuse(self, mock_init): + result = novaclient.shell.DeprecatedAction( + 'option_strings', 'dest', use='use this instead', a=1, b=2, c=3) + + self.assertEqual(result.emitted, set()) + self.assertEqual(result.use, 'use this instead') + self.assertEqual(result.real_action_args, + ('option_strings', 'dest', + 'Deprecated; use this instead', + {'a': 1, 'b': 2, 'c': 3})) + self.assertEqual(result.real_action, None) + mock_init.assert_called_once_with( + 'option_strings', 'dest', help='Deprecated; use this instead', + a=1, b=2, c=3) + + @mock.patch.object(argparse.Action, '__init__', return_value=None) + def test_init_withhelp_nouse(self, mock_init): + result = novaclient.shell.DeprecatedAction( + 'option_strings', 'dest', help='some help', a=1, b=2, c=3) + + self.assertEqual(result.emitted, set()) + self.assertEqual(result.use, None) + self.assertEqual(result.real_action_args, + ('option_strings', 'dest', + 'some help (Deprecated)', + {'a': 1, 'b': 2, 'c': 3})) + self.assertEqual(result.real_action, None) + mock_init.assert_called_once_with( + 'option_strings', 'dest', help='some help (Deprecated)', + a=1, b=2, c=3) + + @mock.patch.object(novaclient.shell.argparse.Action, '__init__', + return_value=None) + def test_init_withhelp_withuse(self, mock_init): + result = novaclient.shell.DeprecatedAction( + 'option_strings', 'dest', help='some help', + use='use this instead', a=1, b=2, c=3) + + self.assertEqual(result.emitted, set()) + self.assertEqual(result.use, 'use this instead') + self.assertEqual(result.real_action_args, + ('option_strings', 'dest', + 'some help (Deprecated; use this instead)', + {'a': 1, 'b': 2, 'c': 3})) + self.assertEqual(result.real_action, None) + mock_init.assert_called_once_with( + 'option_strings', 'dest', + help='some help (Deprecated; use this instead)', + a=1, b=2, c=3) + + @mock.patch.object(argparse.Action, '__init__', return_value=None) + def test_init_suppresshelp_nouse(self, mock_init): + result = novaclient.shell.DeprecatedAction( + 'option_strings', 'dest', help=argparse.SUPPRESS, a=1, b=2, c=3) + + self.assertEqual(result.emitted, set()) + self.assertEqual(result.use, None) + self.assertEqual(result.real_action_args, + ('option_strings', 'dest', argparse.SUPPRESS, + {'a': 1, 'b': 2, 'c': 3})) + self.assertEqual(result.real_action, None) + mock_init.assert_called_once_with( + 'option_strings', 'dest', help=argparse.SUPPRESS, a=1, b=2, c=3) + + @mock.patch.object(novaclient.shell.argparse.Action, '__init__', + return_value=None) + def test_init_suppresshelp_withuse(self, mock_init): + result = novaclient.shell.DeprecatedAction( + 'option_strings', 'dest', help=argparse.SUPPRESS, + use='use this instead', a=1, b=2, c=3) + + self.assertEqual(result.emitted, set()) + self.assertEqual(result.use, 'use this instead') + self.assertEqual(result.real_action_args, + ('option_strings', 'dest', argparse.SUPPRESS, + {'a': 1, 'b': 2, 'c': 3})) + self.assertEqual(result.real_action, None) + mock_init.assert_called_once_with( + 'option_strings', 'dest', help=argparse.SUPPRESS, a=1, b=2, c=3) + + @mock.patch.object(argparse.Action, '__init__', return_value=None) + def test_init_action_nothing(self, mock_init): + result = novaclient.shell.DeprecatedAction( + 'option_strings', 'dest', real_action='nothing', a=1, b=2, c=3) + + self.assertEqual(result.emitted, set()) + self.assertEqual(result.use, None) + self.assertEqual(result.real_action_args, False) + self.assertEqual(result.real_action, None) + mock_init.assert_called_once_with( + 'option_strings', 'dest', help='Deprecated', a=1, b=2, c=3) + + @mock.patch.object(argparse.Action, '__init__', return_value=None) + def test_init_action_string(self, mock_init): + result = novaclient.shell.DeprecatedAction( + 'option_strings', 'dest', real_action='store', a=1, b=2, c=3) + + self.assertEqual(result.emitted, set()) + self.assertEqual(result.use, None) + self.assertEqual(result.real_action_args, + ('option_strings', 'dest', 'Deprecated', + {'a': 1, 'b': 2, 'c': 3})) + self.assertEqual(result.real_action, 'store') + mock_init.assert_called_once_with( + 'option_strings', 'dest', help='Deprecated', a=1, b=2, c=3) + + @mock.patch.object(argparse.Action, '__init__', return_value=None) + def test_init_action_other(self, mock_init): + action = mock.Mock() + result = novaclient.shell.DeprecatedAction( + 'option_strings', 'dest', real_action=action, a=1, b=2, c=3) + + self.assertEqual(result.emitted, set()) + self.assertEqual(result.use, None) + self.assertEqual(result.real_action_args, False) + self.assertEqual(result.real_action, action.return_value) + mock_init.assert_called_once_with( + 'option_strings', 'dest', help='Deprecated', a=1, b=2, c=3) + action.assert_called_once_with( + 'option_strings', 'dest', help='Deprecated', a=1, b=2, c=3) + + @mock.patch.object(sys, 'stderr', six.StringIO()) + def test_get_action_nolookup(self): + action_class = mock.Mock() + parser = mock.Mock(**{ + '_registry_get.return_value': action_class, + }) + obj = novaclient.shell.DeprecatedAction( + 'option_strings', 'dest', real_action='nothing', const=1) + obj.real_action = 'action' + + result = obj._get_action(parser) + + self.assertEqual(result, 'action') + self.assertEqual(obj.real_action, 'action') + self.assertFalse(parser._registry_get.called) + self.assertFalse(action_class.called) + self.assertEqual(sys.stderr.getvalue(), '') + + @mock.patch.object(sys, 'stderr', six.StringIO()) + def test_get_action_lookup_noresult(self): + parser = mock.Mock(**{ + '_registry_get.return_value': None, + }) + obj = novaclient.shell.DeprecatedAction( + 'option_strings', 'dest', real_action='store', const=1) + + result = obj._get_action(parser) + + self.assertEqual(result, None) + self.assertEqual(obj.real_action, None) + parser._registry_get.assert_called_once_with( + 'action', 'store') + self.assertEqual(sys.stderr.getvalue(), + 'WARNING: Programming error: Unknown real action ' + '"store"\n') + + @mock.patch.object(sys, 'stderr', six.StringIO()) + def test_get_action_lookup_withresult(self): + action_class = mock.Mock() + parser = mock.Mock(**{ + '_registry_get.return_value': action_class, + }) + obj = novaclient.shell.DeprecatedAction( + 'option_strings', 'dest', real_action='store', const=1) + + result = obj._get_action(parser) + + self.assertEqual(result, action_class.return_value) + self.assertEqual(obj.real_action, action_class.return_value) + parser._registry_get.assert_called_once_with( + 'action', 'store') + action_class.assert_called_once_with( + 'option_strings', 'dest', help='Deprecated', const=1) + self.assertEqual(sys.stderr.getvalue(), '') + + @mock.patch.object(sys, 'stderr', six.StringIO()) + @mock.patch.object(novaclient.shell.DeprecatedAction, '_get_action') + def test_call_unemitted_nouse(self, mock_get_action): + obj = novaclient.shell.DeprecatedAction( + 'option_strings', 'dest') + + obj('parser', 'namespace', 'values', 'option_string') + + self.assertEqual(obj.emitted, set(['option_string'])) + mock_get_action.assert_called_once_with('parser') + mock_get_action.return_value.assert_called_once_with( + 'parser', 'namespace', 'values', 'option_string') + self.assertEqual(sys.stderr.getvalue(), + 'WARNING: Option "option_string" is deprecated\n') + + @mock.patch.object(sys, 'stderr', six.StringIO()) + @mock.patch.object(novaclient.shell.DeprecatedAction, '_get_action') + def test_call_unemitted_withuse(self, mock_get_action): + obj = novaclient.shell.DeprecatedAction( + 'option_strings', 'dest', use='use this instead') + + obj('parser', 'namespace', 'values', 'option_string') + + self.assertEqual(obj.emitted, set(['option_string'])) + mock_get_action.assert_called_once_with('parser') + mock_get_action.return_value.assert_called_once_with( + 'parser', 'namespace', 'values', 'option_string') + self.assertEqual(sys.stderr.getvalue(), + 'WARNING: Option "option_string" is deprecated; ' + 'use this instead\n') + + @mock.patch.object(sys, 'stderr', six.StringIO()) + @mock.patch.object(novaclient.shell.DeprecatedAction, '_get_action') + def test_call_emitted_nouse(self, mock_get_action): + obj = novaclient.shell.DeprecatedAction( + 'option_strings', 'dest') + obj.emitted.add('option_string') + + obj('parser', 'namespace', 'values', 'option_string') + + self.assertEqual(obj.emitted, set(['option_string'])) + mock_get_action.assert_called_once_with('parser') + mock_get_action.return_value.assert_called_once_with( + 'parser', 'namespace', 'values', 'option_string') + self.assertEqual(sys.stderr.getvalue(), '') + + @mock.patch.object(sys, 'stderr', six.StringIO()) + @mock.patch.object(novaclient.shell.DeprecatedAction, '_get_action') + def test_call_emitted_withuse(self, mock_get_action): + obj = novaclient.shell.DeprecatedAction( + 'option_strings', 'dest', use='use this instead') + obj.emitted.add('option_string') + + obj('parser', 'namespace', 'values', 'option_string') + + self.assertEqual(obj.emitted, set(['option_string'])) + mock_get_action.assert_called_once_with('parser') + mock_get_action.return_value.assert_called_once_with( + 'parser', 'namespace', 'values', 'option_string') + self.assertEqual(sys.stderr.getvalue(), '') + + class ParserTest(utils.TestCase): def setUp(self): diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index cbb7f122d..1eb4fc02a 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -43,6 +43,7 @@ from novaclient import exceptions from novaclient.i18n import _ from novaclient.openstack.common import cliutils +from novaclient import shell from novaclient import utils from novaclient.v2 import availability_zones from novaclient.v2 import quotas @@ -376,6 +377,9 @@ def _boot(cs, args): default=None, type=int, metavar='', + action=shell.DeprecatedAction, + use=_('use "--min-count" and "--max-count"; this option will be removed ' + 'in novaclient 3.3.0.'), help=argparse.SUPPRESS) @cliutils.arg( '--min-count', @@ -412,6 +416,9 @@ def _boot(cs, args): the command keypair-add.")) @cliutils.arg( '--key_name', + action=shell.DeprecatedAction, + use=_('use "%s"; this option will be removed in ' + 'novaclient 3.3.0.') % '--key-name', help=argparse.SUPPRESS) @cliutils.arg('name', metavar='', help=_('Name for the new server.')) @cliutils.arg( @@ -421,6 +428,9 @@ def _boot(cs, args): help=_("user data file to pass to be exposed by the metadata server.")) @cliutils.arg( '--user_data', + action=shell.DeprecatedAction, + use=_('use "%s"; this option will be removed in ' + 'novaclient 3.3.0.') % '--user-data', help=argparse.SUPPRESS) @cliutils.arg( '--availability-zone', @@ -429,6 +439,9 @@ def _boot(cs, args): help=_("The availability zone for server placement.")) @cliutils.arg( '--availability_zone', + action=shell.DeprecatedAction, + use=_('use "%s"; this option will be removed in ' + 'novaclient 3.3.0.') % '--availability-zone', help=argparse.SUPPRESS) @cliutils.arg( '--security-groups', @@ -437,6 +450,9 @@ def _boot(cs, args): help=_("Comma separated list of security group names.")) @cliutils.arg( '--security_groups', + action=shell.DeprecatedAction, + use=_('use "%s"; this option will be removed in ' + 'novaclient 3.3.0.') % '--security-groups', help=argparse.SUPPRESS) @cliutils.arg( '--block-device-mapping', @@ -447,7 +463,10 @@ def _boot(cs, args): "=:::.")) @cliutils.arg( '--block_device_mapping', - action='append', + real_action='append', + action=shell.DeprecatedAction, + use=_('use "%s"; this option will be removed in ' + 'novaclient 3.3.0.') % '--block-device-mapping', help=argparse.SUPPRESS) @cliutils.arg( '--block-device', @@ -1288,6 +1307,9 @@ def do_image_delete(cs, args): help=_('Only return servers that match reservation-id.')) @cliutils.arg( '--reservation_id', + action=shell.DeprecatedAction, + use=_('use "%s"; this option will be removed in ' + 'novaclient 3.3.0.') % '--reservation-id', help=argparse.SUPPRESS) @cliutils.arg( '--ip', @@ -1315,6 +1337,9 @@ def do_image_delete(cs, args): help=_('Search with regular expression match by server name.')) @cliutils.arg( '--instance_name', + action=shell.DeprecatedAction, + use=_('use "%s"; this option will be removed in ' + 'novaclient 3.3.0.') % '--instance-name', help=argparse.SUPPRESS) @cliutils.arg( '--status', @@ -1356,6 +1381,9 @@ def do_image_delete(cs, args): nargs='?', type=int, const=1, + action=shell.DeprecatedAction, + use=_('use "%s"; this option will be removed in ' + 'novaclient 3.3.0.') % '--all-tenants', help=argparse.SUPPRESS) @cliutils.arg( '--tenant', @@ -1561,6 +1589,9 @@ def do_reboot(cs, args): help=_("Set the provided admin password on the rebuilt server.")) @cliutils.arg( '--rebuild_password', + action=shell.DeprecatedAction, + use=_('use "%s"; this option will be removed in ' + 'novaclient 3.3.0.') % '--rebuild-password', help=argparse.SUPPRESS) @cliutils.arg( '--poll', @@ -2132,6 +2163,9 @@ def _translate_volume_attachments_keys(collection): nargs='?', type=int, const=1, + action=shell.DeprecatedAction, + use=_('use "%s"; this option will be removed in ' + 'novaclient 3.3.0.') % '--all-tenants', help=argparse.SUPPRESS) def do_volume_list(cs, args): """DEPRECATED: List all the volumes.""" @@ -2171,6 +2205,9 @@ def do_volume_show(cs, args): help=_('Optional snapshot ID to create the volume from. (Default=None)')) @cliutils.arg( '--snapshot_id', + action=shell.DeprecatedAction, + use=_('use "%s"; this option will be removed in ' + 'novaclient 3.3.0.') % '--snapshot-id', help=argparse.SUPPRESS) @cliutils.arg( '--image-id', @@ -2184,6 +2221,9 @@ def do_volume_show(cs, args): help=_('Optional volume name. (Default=None)')) @cliutils.arg( '--display_name', + action=shell.DeprecatedAction, + use=_('use "%s"; this option will be removed in ' + 'novaclient 3.3.0.') % '--display-name', help=argparse.SUPPRESS) @cliutils.arg( '--display-description', @@ -2192,6 +2232,9 @@ def do_volume_show(cs, args): help=_('Optional volume description. (Default=None)')) @cliutils.arg( '--display_description', + action=shell.DeprecatedAction, + use=_('use "%s"; this option will be removed in ' + 'novaclient 3.3.0.') % '--display-description', help=argparse.SUPPRESS) @cliutils.arg( '--volume-type', @@ -2200,6 +2243,9 @@ def do_volume_show(cs, args): help=_('Optional volume type. (Default=None)')) @cliutils.arg( '--volume_type', + action=shell.DeprecatedAction, + use=_('use "%s"; this option will be removed in ' + 'novaclient 3.3.0.') % '--volume-type', help=argparse.SUPPRESS) @cliutils.arg( '--availability-zone', metavar='', @@ -2338,6 +2384,9 @@ def do_volume_snapshot_show(cs, args): help=_('Optional snapshot name. (Default=None)')) @cliutils.arg( '--display_name', + action=shell.DeprecatedAction, + use=_('use "%s"; this option will be removed in ' + 'novaclient 3.3.0.') % '--display-name', help=argparse.SUPPRESS) @cliutils.arg( '--display-description', @@ -2346,6 +2395,9 @@ def do_volume_snapshot_show(cs, args): help=_('Optional snapshot description. (Default=None)')) @cliutils.arg( '--display_description', + action=shell.DeprecatedAction, + use=_('use "%s"; this option will be removed in ' + 'novaclient 3.3.0.') % '--display-description', help=argparse.SUPPRESS) def do_volume_snapshot_create(cs, args): """DEPRECATED: Add a new snapshot.""" @@ -2462,8 +2514,16 @@ def do_get_rdp_console(cs, args): @cliutils.arg('server', metavar='', help=_('Name or ID of server.')) @cliutils.arg( - '--console_type', default='serial', + '--console-type', + default='serial', help=_('Type of serial console, default="serial".')) +@cliutils.arg( + '--console_type', + default='serial', + action=shell.DeprecatedAction, + use=_('use "%s"; this option will be removed in ' + 'novaclient 3.3.0.') % '--console-type', + help=argparse.SUPPRESS) def do_get_serial_console(cs, args): """Get a serial console to a server.""" if args.console_type not in ('serial',): @@ -2741,6 +2801,9 @@ def do_dns_delete_domain(cs, args): 'in the specified availability zone.')) @cliutils.arg( '--availability_zone', + action=shell.DeprecatedAction, + use=_('use "%s"; this option will be removed in ' + 'novaclient 3.3.0.') % '--availability-zone', help=argparse.SUPPRESS) def do_dns_create_private_domain(cs, args): """Create the specified DNS domain.""" @@ -2925,6 +2988,9 @@ def do_secgroup_delete(cs, args): nargs='?', type=int, const=1, + action=shell.DeprecatedAction, + use=_('use "%s"; this option will be removed in ' + 'novaclient 3.3.0.') % '--all-tenants', help=argparse.SUPPRESS) def do_secgroup_list(cs, args): """List security groups for the current tenant.""" @@ -3054,6 +3120,9 @@ def _keypair_create(cs, args, name, pub_key): help=_('Path to a public ssh key.')) @cliutils.arg( '--pub_key', + action=shell.DeprecatedAction, + use=_('use "%s"; this option will be removed in ' + 'novaclient 3.3.0.') % '--pub-key', help=argparse.SUPPRESS) @cliutils.arg( '--key-type', @@ -3666,7 +3735,10 @@ def parser_hosts(fields): help=_('True in case of block_migration. (Default=False:live_migration)')) @cliutils.arg( '--block_migrate', - action='store_true', + real_action='store_true', + action=shell.DeprecatedAction, + use=_('use "%s"; this option will be removed in ' + 'novaclient 3.3.0.') % '--block-migrate', help=argparse.SUPPRESS) @cliutils.arg( '--disk-over-commit', @@ -3676,7 +3748,10 @@ def parser_hosts(fields): help=_('Allow overcommit. (Default=False)')) @cliutils.arg( '--disk_over_commit', - action='store_true', + real_action='store_true', + action=shell.DeprecatedAction, + use=_('use "%s"; this option will be removed in ' + 'novaclient 3.3.0.') % '--disk-over-commit', help=argparse.SUPPRESS) def do_live_migration(cs, args): """Migrate running server to a new machine.""" @@ -4276,6 +4351,9 @@ def do_quota_defaults(cs, args): @cliutils.arg( '--floating_ips', type=int, + action=shell.DeprecatedAction, + use=_('use "%s"; this option will be removed in ' + 'novaclient 3.3.0.') % '--floating-ips', help=argparse.SUPPRESS) @cliutils.arg( '--fixed-ips', @@ -4292,6 +4370,9 @@ def do_quota_defaults(cs, args): @cliutils.arg( '--metadata_items', type=int, + action=shell.DeprecatedAction, + use=_('use "%s"; this option will be removed in ' + 'novaclient 3.3.0.') % '--metadata-items', help=argparse.SUPPRESS) @cliutils.arg( '--injected-files', @@ -4302,6 +4383,9 @@ def do_quota_defaults(cs, args): @cliutils.arg( '--injected_files', type=int, + action=shell.DeprecatedAction, + use=_('use "%s"; this option will be removed in ' + 'novaclient 3.3.0.') % '--injected-files', help=argparse.SUPPRESS) @cliutils.arg( '--injected-file-content-bytes', @@ -4312,6 +4396,9 @@ def do_quota_defaults(cs, args): @cliutils.arg( '--injected_file_content_bytes', type=int, + action=shell.DeprecatedAction, + use=_('use "%s"; this option will be removed in ' + 'novaclient 3.3.0.') % '--injected-file-content-bytes', help=argparse.SUPPRESS) @cliutils.arg( '--injected-file-path-bytes', @@ -4417,6 +4504,9 @@ def do_quota_class_show(cs, args): @cliutils.arg( '--floating_ips', type=int, + action=shell.DeprecatedAction, + use=_('use "%s"; this option will be removed in ' + 'novaclient 3.3.0.') % '--floating-ips', help=argparse.SUPPRESS) @cliutils.arg( '--fixed-ips', @@ -4433,6 +4523,9 @@ def do_quota_class_show(cs, args): @cliutils.arg( '--metadata_items', type=int, + action=shell.DeprecatedAction, + use=_('use "%s"; this option will be removed in ' + 'novaclient 3.3.0.') % '--metadata-items', help=argparse.SUPPRESS) @cliutils.arg( '--injected-files', @@ -4443,6 +4536,9 @@ def do_quota_class_show(cs, args): @cliutils.arg( '--injected_files', type=int, + action=shell.DeprecatedAction, + use=_('use "%s"; this option will be removed in ' + 'novaclient 3.3.0.') % '--injected-files', help=argparse.SUPPRESS) @cliutils.arg( '--injected-file-content-bytes', @@ -4453,6 +4549,9 @@ def do_quota_class_show(cs, args): @cliutils.arg( '--injected_file_content_bytes', type=int, + action=shell.DeprecatedAction, + use=_('use "%s"; this option will be removed in ' + 'novaclient 3.3.0.') % '--injected-file-content-bytes', help=argparse.SUPPRESS) @cliutils.arg( '--injected-file-path-bytes', @@ -4744,7 +4843,10 @@ def do_secgroup_delete_default_rule(cs, args): @cliutils.arg( '--policy', default=[], - action='append', + real_action='append', + action=shell.DeprecatedAction, + use=_('use positional parameters; this option will be removed in ' + 'novaclient 3.3.0.'), help=argparse.SUPPRESS) def do_server_group_create(cs, args): """Create a new server group with the specified details.""" From fa3a8edf20c6e83524c125a156aa13d2bcd30165 Mon Sep 17 00:00:00 2001 From: Dirk Mueller Date: Thu, 7 Jan 2016 14:13:34 +0100 Subject: [PATCH 0946/1705] Fix W503 line break before binary operator According to PEP8 the line break should be always after a binary operator. Change-Id: I9d0f6909d0e649fe740b748b831894ccb4e80b15 --- novaclient/client.py | 6 +++--- novaclient/tests/unit/v2/fakes.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index de85905ab..003815245 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -595,9 +595,9 @@ def authenticate(self): def _save_keys(self): # Store the token/mgmt url in the keyring for later requests. - if (self.keyring_saver and self.os_cache and not self.keyring_saved - and self.auth_token and self.management_url - and self.tenant_id): + if (self.keyring_saver and self.os_cache and not self.keyring_saved and + self.auth_token and self.management_url and + self.tenant_id): self.keyring_saver.save(self.auth_token, self.management_url, self.tenant_id) diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 69612c30d..3da50491e 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -137,8 +137,8 @@ def _cs_request(self, url, method, **kwargs): def get_endpoint(self): # check if endpoint matches expected format (eg, v2.1) - if (hasattr(self, 'endpoint_type') - and ENDPOINT_TYPE_RE.search(self.endpoint_type)): + if (hasattr(self, 'endpoint_type') and + ENDPOINT_TYPE_RE.search(self.endpoint_type)): return "http://nova-api:8774/%s/" % self.endpoint_type else: return ( From 2ac32e0f186ec0a2944d9738be3cac09f735f85d Mon Sep 17 00:00:00 2001 From: Jake Yip Date: Tue, 12 Jan 2016 09:32:28 +1100 Subject: [PATCH 0947/1705] Fix broken link in documentation Documentation on developer.openstack.org points to API 2.1 now Change-Id: Ic49279bf2ad66b2467ae49fe87243011e3a8c362 --- novaclient/v2/servers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index 9f78d7fc2..ced864aba 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -573,7 +573,7 @@ def list(self, detailed=True, search_opts=None, marker=None, limit=None, match the search_opts (optional). The search opts format is a dictionary of key / value pairs that will be appended to the query string. For a complete list of keys see: - http://developer.openstack.org/api-ref-compute-v2.html#listServers + http://developer.openstack.org/api-ref-compute-v2.1.html#listServers :param marker: Begin returning servers that appear later in the server list than that represented by this server id (optional). :param limit: Maximum number of servers to return (optional). From 14f63647e9d9b6f44a3e99c545b6cb97fe31f0ff Mon Sep 17 00:00:00 2001 From: "Chaozhe.Chen" Date: Mon, 11 Jan 2016 16:45:25 +0800 Subject: [PATCH 0948/1705] Test: Clean v2 client userwarning Problem: When we run nova client unit test, there are lots of UserWarnings printed on the screen. These warnings mean to remind users not to use v2.client directly. Solution: 1. In top level tests such as some tests in test_auth_plugins.py test_client.py and fixture_data/client.py, we use updated usage novaclient.client instead of using novaclient.v2.client directly. 2. In v2 unit tests, we clean those warnings with setting direct_use False. Change-Id: I70682e54874860f1d67d29325811c9da616bb705 Closes-Bug: #1532711 --- novaclient/tests/unit/fixture_data/client.py | 12 ++++----- novaclient/tests/unit/test_auth_plugins.py | 18 ++++++------- novaclient/tests/unit/test_client.py | 28 ++++++++++---------- novaclient/tests/unit/v2/contrib/fakes.py | 3 ++- novaclient/tests/unit/v2/fakes.py | 5 ++-- novaclient/tests/unit/v2/test_auth.py | 28 ++++++++++++-------- novaclient/tests/unit/v2/test_client.py | 6 +++-- 7 files changed, 55 insertions(+), 45 deletions(-) diff --git a/novaclient/tests/unit/fixture_data/client.py b/novaclient/tests/unit/fixture_data/client.py index 8aff839b8..d4d602073 100644 --- a/novaclient/tests/unit/fixture_data/client.py +++ b/novaclient/tests/unit/fixture_data/client.py @@ -15,7 +15,7 @@ from keystoneauth1 import loading from keystoneauth1 import session -from novaclient.v2 import client as v2client +from novaclient import client IDENTITY_URL = 'http://identityserver:5000/v2.0' COMPUTE_URL = 'http://compute.host' @@ -55,10 +55,10 @@ def setUp(self): self.client = self.new_client() def new_client(self): - return v2client.Client(username='xx', - api_key='xx', - project_id='xx', - auth_url=self.identity_url) + return client.Client("2", username='xx', + api_key='xx', + project_id='xx', + auth_url=self.identity_url) class SessionV1(V1): @@ -68,4 +68,4 @@ def new_client(self): loader = loading.get_plugin_loader('password') self.session.auth = loader.load_from_options( auth_url=self.identity_url, username='xx', password='xx') - return v2client.Client(session=self.session) + return client.Client("2", session=self.session) diff --git a/novaclient/tests/unit/test_auth_plugins.py b/novaclient/tests/unit/test_auth_plugins.py index 2257b2a88..78dd393da 100644 --- a/novaclient/tests/unit/test_auth_plugins.py +++ b/novaclient/tests/unit/test_auth_plugins.py @@ -26,9 +26,9 @@ import simplejson as json from novaclient import auth_plugin +from novaclient import client from novaclient import exceptions from novaclient.tests.unit import utils -from novaclient.v2 import client def mock_http_request(resp=None): @@ -80,7 +80,7 @@ def mock_iter_entry_points(_type, name): @mock.patch.object(requests, "request", mock_request) def test_auth_call(): plugin = auth_plugin.DeprecatedAuthPlugin("fake") - cs = client.Client("username", "password", "project_id", + cs = client.Client("2", "username", "password", "project_id", utils.AUTH_URL_V2, auth_system="fake", auth_plugin=plugin) cs.client.authenticate() @@ -110,7 +110,7 @@ def mock_iter_entry_points(_t, name=None): def test_auth_call(): auth_plugin.discover_auth_systems() plugin = auth_plugin.DeprecatedAuthPlugin("notexists") - cs = client.Client("username", "password", "project_id", + cs = client.Client("2", "username", "password", "project_id", utils.AUTH_URL_V2, auth_system="notexists", auth_plugin=plugin) self.assertRaises(exceptions.AuthSystemNotFound, @@ -158,7 +158,7 @@ def mock_iter_entry_points(_type, name): @mock.patch.object(requests, "request", mock_request) def test_auth_call(): plugin = auth_plugin.DeprecatedAuthPlugin("fakewithauthurl") - cs = client.Client("username", "password", "project_id", + cs = client.Client("2", "username", "password", "project_id", auth_system="fakewithauthurl", auth_plugin=plugin) cs.client.authenticate() @@ -186,7 +186,7 @@ def auth_url(self): plugin = auth_plugin.DeprecatedAuthPlugin("fakewithauthurl") self.assertRaises( exceptions.EndpointNotFound, - client.Client, "username", "password", "project_id", + client.Client, "2", "username", "password", "project_id", auth_system="fakewithauthurl", auth_plugin=plugin) @@ -213,7 +213,7 @@ def authenticate(self, cls, auth_url): auth_plugin.discover_auth_systems() plugin = auth_plugin.load_plugin("fake") - cs = client.Client("username", "password", "project_id", + cs = client.Client("2", "username", "password", "project_id", utils.AUTH_URL_V2, auth_system="fake", auth_plugin=plugin) cs.client.authenticate() @@ -304,7 +304,7 @@ def get_auth_url(self): auth_plugin.discover_auth_systems() plugin = auth_plugin.load_plugin("fake") - cs = client.Client("username", "password", "project_id", + cs = client.Client("2", "username", "password", "project_id", auth_system="fakewithauthurl", auth_plugin=plugin) self.assertEqual("http://faked/v2.0", cs.client.auth_url) @@ -330,7 +330,7 @@ class FakePlugin(auth_plugin.BaseAuthPlugin): self.assertRaises( exceptions.EndpointNotFound, - client.Client, "username", "password", "project_id", + client.Client, "2", "username", "password", "project_id", auth_system="fake", auth_plugin=plugin) @mock.patch.object(pkg_resources, "iter_entry_points") @@ -354,5 +354,5 @@ class FakePlugin(auth_plugin.BaseAuthPlugin): self.assertRaises( exceptions.EndpointNotFound, - client.Client, "username", "password", "project_id", + client.Client, "2", "username", "password", "project_id", auth_system="fake", auth_plugin=plugin) diff --git a/novaclient/tests/unit/test_client.py b/novaclient/tests/unit/test_client.py index 171ec9d4c..f63fdd4aa 100644 --- a/novaclient/tests/unit/test_client.py +++ b/novaclient/tests/unit/test_client.py @@ -171,38 +171,38 @@ def test_get_client_class_latest(self): novaclient.client.get_client_class, '2.latest') def test_client_with_os_cache_enabled(self): - cs = novaclient.v2.client.Client("user", "password", "project_id", - auth_url="foo/v2", os_cache=True) + cs = novaclient.client.Client("2", "user", "password", "project_id", + auth_url="foo/v2", os_cache=True) self.assertTrue(cs.os_cache) self.assertTrue(cs.client.os_cache) def test_client_with_os_cache_disabled(self): - cs = novaclient.v2.client.Client("user", "password", "project_id", - auth_url="foo/v2", os_cache=False) + cs = novaclient.client.Client("2", "user", "password", "project_id", + auth_url="foo/v2", os_cache=False) self.assertFalse(cs.os_cache) self.assertFalse(cs.client.os_cache) def test_client_with_no_cache_enabled(self): - cs = novaclient.v2.client.Client("user", "password", "project_id", - auth_url="foo/v2", no_cache=True) + cs = novaclient.client.Client("2", "user", "password", "project_id", + auth_url="foo/v2", no_cache=True) self.assertFalse(cs.os_cache) self.assertFalse(cs.client.os_cache) def test_client_with_no_cache_disabled(self): - cs = novaclient.v2.client.Client("user", "password", "project_id", - auth_url="foo/v2", no_cache=False) + cs = novaclient.client.Client("2", "user", "password", "project_id", + auth_url="foo/v2", no_cache=False) self.assertTrue(cs.os_cache) self.assertTrue(cs.client.os_cache) def test_client_set_management_url_v1_1(self): - cs = novaclient.v2.client.Client("user", "password", "project_id", - auth_url="foo/v2") + cs = novaclient.client.Client("2", "user", "password", "project_id", + auth_url="foo/v2") cs.set_management_url("blabla") self.assertEqual("blabla", cs.client.management_url) def test_client_get_reset_timings_v1_1(self): - cs = novaclient.v2.client.Client("user", "password", "project_id", - auth_url="foo/v2") + cs = novaclient.client.Client("2", "user", "password", "project_id", + auth_url="foo/v2") self.assertEqual(0, len(cs.get_timings())) cs.client.times.append("somevalue") self.assertEqual(1, len(cs.get_timings())) @@ -215,8 +215,8 @@ def test_client_get_reset_timings_v1_1(self): def test_contextmanager_v1_1(self, mock_http_client): fake_client = mock.Mock() mock_http_client.return_value = fake_client - with novaclient.v2.client.Client("user", "password", "project_id", - auth_url="foo/v2"): + with novaclient.client.Client("2", "user", "password", "project_id", + auth_url="foo/v2"): pass self.assertTrue(fake_client.open_session.called) self.assertTrue(fake_client.close_session.called) diff --git a/novaclient/tests/unit/v2/contrib/fakes.py b/novaclient/tests/unit/v2/contrib/fakes.py index 870339584..8e82f9c34 100644 --- a/novaclient/tests/unit/v2/contrib/fakes.py +++ b/novaclient/tests/unit/v2/contrib/fakes.py @@ -20,7 +20,8 @@ class FakeClient(fakes.FakeClient): def __init__(self, *args, **kwargs): client.Client.__init__(self, 'username', 'password', 'project_id', 'auth_url', - extensions=kwargs.get('extensions')) + extensions=kwargs.get('extensions'), + direct_use=False) self.client = FakeHTTPClient(**kwargs) diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 69612c30d..ceaceeed6 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -51,7 +51,8 @@ class FakeClient(fakes.FakeClient, client.Client): def __init__(self, api_version=None, *args, **kwargs): client.Client.__init__(self, 'username', 'password', 'project_id', 'auth_url', - extensions=kwargs.get('extensions')) + extensions=kwargs.get('extensions'), + direct_use=False) self.api_version = api_version self.client = FakeHTTPClient(**kwargs) @@ -2425,7 +2426,7 @@ def __init__(self, api_version, *args, **kwargs): client.Client.__init__(self, 'username', 'password', 'project_id', 'auth_url', extensions=kwargs.get('extensions'), - api_version=api_version) + api_version=api_version, direct_use=False) self.client = FakeSessionMockClient(**kwargs) diff --git a/novaclient/tests/unit/v2/test_auth.py b/novaclient/tests/unit/v2/test_auth.py index a62866a62..70d9467a3 100644 --- a/novaclient/tests/unit/v2/test_auth.py +++ b/novaclient/tests/unit/v2/test_auth.py @@ -36,7 +36,8 @@ def get_token(self, **kwargs): def test_authenticate_success(self): cs = client.Client("username", "password", "project_id", - utils.AUTH_URL_V2, service_type='compute') + utils.AUTH_URL_V2, service_type='compute', + direct_use=False) resp = self.get_token() auth_response = utils.TestResponse({ @@ -83,7 +84,7 @@ def test_auth_call(): def test_authenticate_failure(self): cs = client.Client("username", "password", "project_id", - utils.AUTH_URL_V2) + utils.AUTH_URL_V2, direct_use=False) resp = {"unauthorized": {"message": "Unauthorized", "code": "401"}} auth_response = utils.TestResponse({ "status_code": 401, @@ -100,7 +101,8 @@ def test_auth_call(): def test_v1_auth_redirect(self): cs = client.Client("username", "password", "project_id", - utils.AUTH_URL_V1, service_type='compute') + utils.AUTH_URL_V1, service_type='compute', + direct_use=False) dict_correct_response = self.get_token() correct_response = json.dumps(dict_correct_response) dict_responses = [ @@ -165,7 +167,8 @@ def test_auth_call(): def test_v2_auth_redirect(self): cs = client.Client("username", "password", "project_id", - utils.AUTH_URL_V2, service_type='compute') + utils.AUTH_URL_V2, service_type='compute', + direct_use=False) dict_correct_response = self.get_token() correct_response = json.dumps(dict_correct_response) dict_responses = [ @@ -230,7 +233,8 @@ def test_auth_call(): def test_ambiguous_endpoints(self): cs = client.Client("username", "password", "project_id", - utils.AUTH_URL_V2, service_type='compute') + utils.AUTH_URL_V2, service_type='compute', + direct_use=False) resp = self.get_token() # duplicate existing service @@ -253,7 +257,8 @@ def test_auth_call(): def test_authenticate_with_token_success(self): cs = client.Client("username", None, "project_id", - utils.AUTH_URL_V2, service_type='compute') + utils.AUTH_URL_V2, service_type='compute', + direct_use=False) cs.client.auth_token = "FAKE_ID" resp = self.get_token(token_id="FAKE_ID") auth_response = utils.TestResponse({ @@ -295,7 +300,8 @@ def test_authenticate_with_token_success(self): self.assertEqual(cs.client.auth_token, token_id) def test_authenticate_with_token_failure(self): - cs = client.Client("username", None, "project_id", utils.AUTH_URL_V2) + cs = client.Client("username", None, "project_id", utils.AUTH_URL_V2, + direct_use=False) cs.client.auth_token = "FAKE_ID" resp = {"unauthorized": {"message": "Unauthorized", "code": "401"}} auth_response = utils.TestResponse({ @@ -312,7 +318,7 @@ def test_authenticate_with_token_failure(self): class AuthenticationTests(utils.TestCase): def test_authenticate_success(self): cs = client.Client("username", "password", - "project_id", utils.AUTH_URL) + "project_id", utils.AUTH_URL, direct_use=False) management_url = 'https://localhost/v1.1/443470' auth_response = utils.TestResponse({ 'status_code': 204, @@ -348,7 +354,7 @@ def test_auth_call(): def test_authenticate_failure(self): cs = client.Client("username", "password", - "project_id", utils.AUTH_URL) + "project_id", utils.AUTH_URL, direct_use=False) auth_response = utils.TestResponse({'status_code': 401}) mock_request = mock.Mock(return_value=(auth_response)) @@ -360,7 +366,7 @@ def test_auth_call(): def test_auth_automatic(self): cs = client.Client("username", "password", - "project_id", utils.AUTH_URL) + "project_id", utils.AUTH_URL, direct_use=False) http_client = cs.client http_client.management_url = '' http_client.get_service_url = mock.Mock(return_value='') @@ -377,7 +383,7 @@ def test_auth_call(m): def test_auth_manual(self): cs = client.Client("username", "password", - "project_id", utils.AUTH_URL) + "project_id", utils.AUTH_URL, direct_use=False) @mock.patch.object(cs.client, 'authenticate') def test_auth_call(m): diff --git a/novaclient/tests/unit/v2/test_client.py b/novaclient/tests/unit/v2/test_client.py index 5bbbe51fb..dc92c9327 100644 --- a/novaclient/tests/unit/v2/test_client.py +++ b/novaclient/tests/unit/v2/test_client.py @@ -28,7 +28,8 @@ def test_adapter_properties(self): s = session.Session() c = client.Client(session=s, user_agent=user_agent, - endpoint_override=endpoint_override) + endpoint_override=endpoint_override, + direct_use=False) self.assertEqual(user_agent, c.client.user_agent) self.assertEqual(endpoint_override, c.client.endpoint_override) @@ -40,6 +41,7 @@ def test_passing_interface(self): s = session.Session() c = client.Client(session=s, interface=interface, - endpoint_type=endpoint_type) + endpoint_type=endpoint_type, + direct_use=False) self.assertEqual(interface, c.client.interface) From 54b625aae84ddc21b8f58975a80c4e4bd660da6b Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Tue, 12 Jan 2016 05:05:56 +0000 Subject: [PATCH 0949/1705] Updated from global requirements Change-Id: Ief924564960d7733f129fd02d0e9898527393151 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 60a1c2c87..acd35b975 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -16,7 +16,7 @@ oslosphinx!=3.4.0,>=2.5.0 # Apache-2.0 testrepository>=0.0.18 testscenarios>=0.4 testtools>=1.4.0 -tempest-lib>=0.12.0 +tempest-lib>=0.13.0 # releasenotes reno>=0.1.1 # Apache2 From 7cc26eee72196e4ad0ef46f17f4682be0a76c666 Mon Sep 17 00:00:00 2001 From: Balazs Gibizer Date: Tue, 12 Jan 2016 14:52:03 +0100 Subject: [PATCH 0950/1705] [microversions] Add support for 2.15 With I3f156d5e5df4d9642bb4b0ffac30a6288459ce61 nova introduced microversion 2.15 which adds two new policies soft-affinty and soft-anti-affinity for the server-group api. This patch bumps the nova client microversion support to 2.15. The novaclient is transparent regarding the policy value of the server-group api so no further changes are needed. However the help text of the policies parameter mentioned the possible policies. This was removed as the current framework does not support providing different help text for a parameter depending on the microversion. Implements: blueprint soft-affinity-for-server-group Change-Id: I739ed1dd3e4c15e28a269c4f980a12a74fb1def0 --- novaclient/__init__.py | 2 +- novaclient/tests/unit/v2/test_shell.py | 1 + novaclient/v2/shell.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 50ad863f5..84c17c97d 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.14") +API_MAX_VERSION = api_versions.APIVersion("2.15") diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index bff97ab92..9135cb14e 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2554,6 +2554,7 @@ def test_versions(self): 9, # doesn't require any changes in novaclient # TODO(andreykurilin): remove 12 when 1522424 will be resolved 12, # doesn't require any changes in novaclient + 15, # doesn't require any changes in novaclient ]) versions_supported = set(range(0, novaclient.API_MAX_VERSION.ver_minor + 1)) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index a4571ecb1..e04ecb83a 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -4698,7 +4698,7 @@ def do_secgroup_delete_default_rule(cs, args): metavar='', default=argparse.SUPPRESS, nargs='*', - help=_('Policies for the server groups. ("affinity" or "anti-affinity")')) + help=_('Policies for the server groups.')) @cliutils.arg( '--policy', default=[], From 426f6afb3ce09ae1325d10930eba134e0573f78e Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Wed, 13 Jan 2016 14:57:24 +0200 Subject: [PATCH 0951/1705] Add functional tests launcher for py3 env It would be nice to check not only unit test on py3 env, but functional tests too. Also, this patch fixes usage of xrange in functional tests. Change-Id: I15ef204022583a40a02b3f2a48771347ddc95f3e --- novaclient/tests/functional/api/test_servers.py | 2 +- novaclient/tests/functional/hooks/post_test_hook.sh | 2 +- tox.ini | 7 +++++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/novaclient/tests/functional/api/test_servers.py b/novaclient/tests/functional/api/test_servers.py index 5615eb849..df6671fa2 100644 --- a/novaclient/tests/functional/api/test_servers.py +++ b/novaclient/tests/functional/api/test_servers.py @@ -22,7 +22,7 @@ def test_server_ips(self): server_name, self.image, self.flavor) self.addCleanup(initial_server.delete) - for x in xrange(60): + for x in range(60): server = self.client.servers.get(initial_server) if server.status == "ACTIVE": break diff --git a/novaclient/tests/functional/hooks/post_test_hook.sh b/novaclient/tests/functional/hooks/post_test_hook.sh index a07bd0c63..f58d8bb6f 100755 --- a/novaclient/tests/functional/hooks/post_test_hook.sh +++ b/novaclient/tests/functional/hooks/post_test_hook.sh @@ -43,7 +43,7 @@ cd $NOVACLIENT_DIR echo "Running novaclient functional test suite" set +e # Preserve env for OS_ credentials -sudo -E -H -u jenkins tox -efunctional +sudo -E -H -u jenkins tox -e ${TOX_ENV:-functional} EXIT_CODE=$? set -e diff --git a/tox.ini b/tox.ini index ba335c1dc..845523a4c 100644 --- a/tox.ini +++ b/tox.ini @@ -35,6 +35,13 @@ commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasen [testenv:functional] +basepython = python2.7 +setenv = + OS_TEST_PATH = ./novaclient/tests/functional +commands = python setup.py testr --testr-args='--concurrency=1 {posargs}' + +[testenv:functional-py34] +basepython = python3.4 setenv = OS_TEST_PATH = ./novaclient/tests/functional commands = python setup.py testr --testr-args='--concurrency=1 {posargs}' From 1adad6978953fc9dcb1d83a9f42bcdab61909b7b Mon Sep 17 00:00:00 2001 From: Andreas Jaeger Date: Sat, 16 Jan 2016 17:36:06 +0100 Subject: [PATCH 0952/1705] Fix URLs for CLI Reference and API The CLI Reference files has been moved, update the link. Replace link to API Quick Start with link to API reference for nova. Change-Id: Ic933d44cddc24702648c69498e632efefda7bf88 --- README.rst | 4 ++-- doc/source/man/nova.rst | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index f93a34c6f..e93f1da13 100644 --- a/README.rst +++ b/README.rst @@ -17,8 +17,8 @@ See the `OpenStack CLI guide`_ for information on how to use the ``nova`` command-line tool. You may also want to look at the `OpenStack API documentation`_. -.. _OpenStack CLI Guide: http://docs.openstack.org/cli-reference/content/novaclient_commands.html -.. _OpenStack API documentation: http://docs.openstack.org/api/quick-start/content/ +.. _OpenStack CLI Guide: http://docs.openstack.org/cli-reference/nova.html +.. _OpenStack API documentation: http://developer.openstack.org/api-ref-compute-v2.1.html python-novaclient is licensed under the Apache License like the rest of OpenStack. diff --git a/doc/source/man/nova.rst b/doc/source/man/nova.rst index 78093c7ef..56e56b59d 100644 --- a/doc/source/man/nova.rst +++ b/doc/source/man/nova.rst @@ -76,7 +76,7 @@ Terminate an instance:: SEE ALSO ======== -OpenStack Nova CLI Guide: http://docs.openstack.org/cli-reference/content/novaclient_commands.html +OpenStack Nova CLI Guide: http://docs.openstack.org/cli-reference/nova.html BUGS From 3346dc826a03e6e5236db8e3b7e4cc418dd06811 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Sun, 17 Jan 2016 01:15:47 +0000 Subject: [PATCH 0953/1705] Updated from global requirements Change-Id: I38b118679fae02c3e37a8f145b28dd7340eb1bc7 --- requirements.txt | 18 +++++++++--------- test-requirements.txt | 24 ++++++++++++------------ 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/requirements.txt b/requirements.txt index cb3ea1438..5f3912486 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,15 +1,15 @@ # 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. -pbr>=1.6 -argparse -keystoneauth1>=2.1.0 -iso8601>=0.1.9 +pbr>=1.6 # Apache-2.0 +argparse # PSF +keystoneauth1>=2.1.0 # Apache-2.0 +iso8601>=0.1.9 # MIT oslo.i18n>=1.5.0 # Apache-2.0 oslo.serialization>=1.10.0 # Apache-2.0 oslo.utils>=3.2.0 # Apache-2.0 -PrettyTable<0.8,>=0.7 -requests!=2.9.0,>=2.8.1 -simplejson>=2.2.0 -six>=1.9.0 -Babel>=1.3 +PrettyTable<0.8,>=0.7 # BSD +requests!=2.9.0,>=2.8.1 # Apache-2.0 +simplejson>=2.2.0 # MIT +six>=1.9.0 # MIT +Babel>=1.3 # BSD diff --git a/test-requirements.txt b/test-requirements.txt index acd35b975..5b93a1750 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -3,20 +3,20 @@ # process, which may cause wedges in the gate later. hacking<0.11,>=0.10.0 -coverage>=3.6 -discover -fixtures>=1.3.1 -keyring>=5.5.1 -mock>=1.2 -python-keystoneclient!=1.8.0,>=1.6.0 +coverage>=3.6 # Apache-2.0 +discover # BSD +fixtures>=1.3.1 # Apache-2.0/BSD +keyring>=5.5.1 # MIT/PSF +mock>=1.2 # BSD +python-keystoneclient!=1.8.0,>=1.6.0 # Apache-2.0 requests-mock>=0.7.0 # Apache-2.0 -sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2 -os-client-config>=1.13.1 +sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2 # BSD +os-client-config>=1.13.1 # Apache-2.0 oslosphinx!=3.4.0,>=2.5.0 # Apache-2.0 -testrepository>=0.0.18 -testscenarios>=0.4 -testtools>=1.4.0 -tempest-lib>=0.13.0 +testrepository>=0.0.18 # Apache-2.0/BSD +testscenarios>=0.4 # Apache-2.0/BSD +testtools>=1.4.0 # MIT +tempest-lib>=0.13.0 # Apache-2.0 # releasenotes reno>=0.1.1 # Apache2 From e7711cddafdcf036ec709002779c8e9f99c6d0aa Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Mon, 18 Jan 2016 22:45:55 +0000 Subject: [PATCH 0954/1705] Updated from global requirements Change-Id: Id51d3606a6d29f909e8c40edfeed0505d8144250 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 5f3912486..76b731540 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ keystoneauth1>=2.1.0 # Apache-2.0 iso8601>=0.1.9 # MIT oslo.i18n>=1.5.0 # Apache-2.0 oslo.serialization>=1.10.0 # Apache-2.0 -oslo.utils>=3.2.0 # Apache-2.0 +oslo.utils>=3.4.0 # Apache-2.0 PrettyTable<0.8,>=0.7 # BSD requests!=2.9.0,>=2.8.1 # Apache-2.0 simplejson>=2.2.0 # MIT From 0d99c693f4ff5abb5d024f25a1d7264897eaf050 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Tue, 19 Jan 2016 13:52:46 +0000 Subject: [PATCH 0955/1705] Updated from global requirements Change-Id: I552f35171e15991a1f57c0d09fa20fe5042cc8e7 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 5b93a1750..638cff28e 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -8,7 +8,7 @@ discover # BSD fixtures>=1.3.1 # Apache-2.0/BSD keyring>=5.5.1 # MIT/PSF mock>=1.2 # BSD -python-keystoneclient!=1.8.0,>=1.6.0 # Apache-2.0 +python-keystoneclient!=1.8.0,!=2.1.0,>=1.6.0 # Apache-2.0 requests-mock>=0.7.0 # Apache-2.0 sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2 # BSD os-client-config>=1.13.1 # Apache-2.0 From f6e0dcd34c8dedf354fad127a0db96242d622fe6 Mon Sep 17 00:00:00 2001 From: Anna Babich Date: Mon, 21 Dec 2015 17:53:08 +0200 Subject: [PATCH 0956/1705] Functional tests for flavors with public and non-public access The added tests check granting access actions for public and non-public flavors The test for microversion 2.7 checks that the appropriate error is returned when attempting to grant an access to a public flavor for the given tenant The base TenantTest class has been added to avoid code duplication in test_keypairs and test_flavor_access tests Also, test_keypairs code has been modified in accordance with TenantTestBase class usage Change-Id: Icf21d8aceaa191aee8b8ffc7c52e97ebe9509b3d --- novaclient/tests/functional/base.py | 38 +++++++++++ .../v2/legacy/test_flavor_access.py | 66 +++++++++++++++++++ .../tests/functional/v2/test_flavor_access.py | 33 ++++++++++ .../tests/functional/v2/test_keypairs.py | 31 +-------- 4 files changed, 139 insertions(+), 29 deletions(-) create mode 100644 novaclient/tests/functional/v2/legacy/test_flavor_access.py create mode 100644 novaclient/tests/functional/v2/test_flavor_access.py diff --git a/novaclient/tests/functional/base.py b/novaclient/tests/functional/base.py index 09ec414e1..16e5f3781 100644 --- a/novaclient/tests/functional/base.py +++ b/novaclient/tests/functional/base.py @@ -285,3 +285,41 @@ def _get_column_value_from_single_row_table(self, table, column): return line.split("|")[1:-1][column_index].strip() raise ValueError("Unable to find value for column '%s'.") + + +class TenantTestBase(ClientTestBase): + """Base test class for additional tenant and user creation which + could be required in various test scenarios + """ + + def setUp(self): + super(TenantTestBase, self).setUp() + tenant = self.cli_clients.keystone( + 'tenant-create --name %s --enabled True' % + self.name_generate('v' + self.COMPUTE_API_VERSION)) + self.tenant_name = self._get_value_from_the_table(tenant, "name") + self.tenant_id = self._get_value_from_the_table( + self.cli_clients.keystone( + 'tenant-get %s' % self.tenant_name), 'id') + self.addCleanup(self.cli_clients.keystone, + "tenant-delete %s" % self.tenant_name) + user_name = self.name_generate('v' + self.COMPUTE_API_VERSION) + password = 'password' + user = self.cli_clients.keystone( + "user-create --name %(name)s --pass %(pass)s --tenant %(tenant)s" % + {"name": user_name, "pass": password, "tenant": self.tenant_name}) + self.user_id = self._get_value_from_the_table(user, "id") + self.addCleanup(self.cli_clients.keystone, + "user-delete %s" % self.user_id) + self.cli_clients_2 = tempest_lib.cli.base.CLIClient( + username=user_name, + password=password, + tenant_name=self.tenant_name, + uri=self.cli_clients.uri, + cli_dir=self.cli_clients.cli_dir) + + def another_nova(self, action, flags='', params='', fail_ok=False, + endpoint_type='publicURL', merge_stderr=False): + flags += " --os-compute-api-version %s " % self.COMPUTE_API_VERSION + return self.cli_clients_2.nova(action, flags, params, fail_ok, + endpoint_type, merge_stderr) diff --git a/novaclient/tests/functional/v2/legacy/test_flavor_access.py b/novaclient/tests/functional/v2/legacy/test_flavor_access.py new file mode 100644 index 000000000..d82d04a2d --- /dev/null +++ b/novaclient/tests/functional/v2/legacy/test_flavor_access.py @@ -0,0 +1,66 @@ +# 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 novaclient.tests.functional import base + + +class TestFlvAccessNovaClient(base.TenantTestBase): + """Functional tests for flavors with public and non-public access""" + + COMPUTE_API_VERSION = "2.1" + + def test_public_flavor_list(self): + # Check that flavors with public access are available for both admin + # and non-admin tenants + flavor_list1 = self.nova('flavor-list') + flavor_list2 = self.another_nova('flavor-list') + self.assertEqual(flavor_list1, flavor_list2) + + def test_non_public_flavor_list(self): + # Check that non-public flavor appears in flavor list + # only for admin tenant and only with --all attribute + # and doesn't appear for non-admin tenant + flv_name = self.name_generate(prefix='flv') + self.nova('flavor-create --is-public false %s auto 512 1 1' % flv_name) + self.addCleanup(self.nova, 'flavor-delete %s' % flv_name) + flavor_list1 = self.nova('flavor-list') + self.assertNotIn(flv_name, flavor_list1) + flavor_list2 = self.nova('flavor-list --all') + flavor_list3 = self.another_nova('flavor-list --all') + self.assertIn(flv_name, flavor_list2) + self.assertNotIn(flv_name, flavor_list3) + + def test_add_access_non_public_flavor(self): + # Check that it's allowed to grant an access to non-public flavor for + # the given tenant + flv_name = self.name_generate(prefix='flv') + self.nova('flavor-create --is-public false %s auto 512 1 1' % flv_name) + self.addCleanup(self.nova, 'flavor-delete %s' % flv_name) + self.nova('flavor-access-add', params="%s %s" % + (flv_name, self.tenant_id)) + self.assertIn(self.tenant_id, + self.nova('flavor-access-list --flavor %s' % flv_name)) + + def test_add_access_public_flavor(self): + # For microversion < 2.7 the 'flavor-access-add' operation is executed + # successfully for public flavor, but the next operation, + # 'flavor-access-list --flavor %(name_of_public_flavor)' returns + # CommandError: Failed to get access list for public flavor type. + flv_name = self.name_generate(prefix='flv') + self.nova('flavor-create %s auto 512 1 1' % flv_name) + self.addCleanup(self.nova, 'flavor-delete %s' % flv_name) + self.nova('flavor-access-add %s %s' % (flv_name, self.tenant_id)) + output = self.nova('flavor-access-list --flavor %s' % flv_name, + fail_ok=True, merge_stderr=True) + self.assertIn("ERROR (CommandError): " + "Failed to get access list for public flavor type.\n", + output) diff --git a/novaclient/tests/functional/v2/test_flavor_access.py b/novaclient/tests/functional/v2/test_flavor_access.py new file mode 100644 index 000000000..f86a1499f --- /dev/null +++ b/novaclient/tests/functional/v2/test_flavor_access.py @@ -0,0 +1,33 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient.tests.functional.v2.legacy import test_flavor_access + + +class TestFlvAccessNovaClientV27(test_flavor_access.TestFlvAccessNovaClient): + """Check that an attempt to grant an access to a public flavor + for the given tenant fails with Conflict error in accordance with + 2.7 microversion REST API History + """ + + COMPUTE_API_VERSION = "2.7" + + def test_add_access_public_flavor(self): + flv_name = self.name_generate('v' + self.COMPUTE_API_VERSION) + self.nova('flavor-create %s auto 512 1 1' % flv_name) + self.addCleanup(self.nova, 'flavor-delete %s' % flv_name) + output = self.nova('flavor-access-add %s %s' % + (flv_name, self.tenant_id), + fail_ok=True, merge_stderr=True) + self.assertIn("ERROR (Conflict): " + "Can not add access to a public flavor. (HTTP 409) ", + output) diff --git a/novaclient/tests/functional/v2/test_keypairs.py b/novaclient/tests/functional/v2/test_keypairs.py index f330e2d0f..c55333e92 100644 --- a/novaclient/tests/functional/v2/test_keypairs.py +++ b/novaclient/tests/functional/v2/test_keypairs.py @@ -10,8 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import tempest_lib.cli.base - from novaclient.tests.functional import base from novaclient.tests.functional.v2 import fake_crypto from novaclient.tests.functional.v2.legacy import test_keypairs @@ -46,36 +44,11 @@ def test_import_keypair_x509(self): self.assertIn('x509', keypair) -class TestKeypairsNovaClientV210(base.ClientTestBase): - """Keypairs functional tests for v2.10 nova-api microversion. - """ +class TestKeypairsNovaClientV210(base.TenantTestBase): + """Keypairs functional tests for v2.10 nova-api microversion.""" COMPUTE_API_VERSION = "2.10" - def setUp(self): - super(TestKeypairsNovaClientV210, self).setUp() - user_name = self.name_generate("v2.10") - password = "password" - user = self.cli_clients.keystone( - "user-create --name %(name)s --pass %(pass)s --tenant %(tenant)s" % - {"name": user_name, "pass": password, - "tenant": self.cli_clients.tenant_name}) - self.user_id = self._get_value_from_the_table(user, "id") - self.addCleanup(self.cli_clients.keystone, - "user-delete %s" % self.user_id) - self.cli_clients_2 = tempest_lib.cli.base.CLIClient( - username=user_name, - password=password, - tenant_name=self.cli_clients.tenant_name, - uri=self.cli_clients.uri, - cli_dir=self.cli_clients.cli_dir) - - def another_nova(self, action, flags='', params='', fail_ok=False, - endpoint_type='publicURL', merge_stderr=False): - flags += " --os-compute-api-version %s " % self.COMPUTE_API_VERSION - return self.cli_clients_2.nova(action, flags, params, fail_ok, - endpoint_type, merge_stderr) - def test_create_and_list_keypair(self): name = self.name_generate("v2_10") self.nova("keypair-add %s --user %s" % (name, self.user_id)) From 20c65aac87d13d2954dcd3b381625f6447065bc7 Mon Sep 17 00:00:00 2001 From: Anna Babich Date: Thu, 10 Dec 2015 14:42:58 +0200 Subject: [PATCH 0957/1705] Functional tests for os-services The added tests check functionality of os-services actions except 'service-delete' (it's hard to recover after test running) Also 'forced_down' attribute exposed by microversion 2.11 is checked here Change-Id: I6652bac4ce49d5174592f19b1b4d91ac5bd51500 --- .../functional/v2/legacy/test_os_services.py | 60 +++++++++++++++++++ .../tests/functional/v2/test_os_services.py | 36 +++++++++++ 2 files changed, 96 insertions(+) create mode 100644 novaclient/tests/functional/v2/legacy/test_os_services.py create mode 100644 novaclient/tests/functional/v2/test_os_services.py diff --git a/novaclient/tests/functional/v2/legacy/test_os_services.py b/novaclient/tests/functional/v2/legacy/test_os_services.py new file mode 100644 index 000000000..3ffc2d81e --- /dev/null +++ b/novaclient/tests/functional/v2/legacy/test_os_services.py @@ -0,0 +1,60 @@ +# 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 novaclient.tests.functional import base + + +class TestOsServicesNovaClient(base.ClientTestBase): + """Functional tests for os-services attributes""" + + COMPUTE_API_VERSION = "2.1" + + def test_os_services_list(self): + table = self.nova('service-list') + for serv in self.client.services.list(): + self.assertIn(serv.binary, table) + + def test_os_service_disable_enable(self): + # Disable and enable Nova services in accordance with list of nova + # services returned by client + # NOTE(sdague): service disable has the chance in racing + # with other tests. Now functional tests for novaclient are launched + # in serial way (https://review.openstack.org/#/c/217768/), but + # it's a potential issue for making these tests parallel in the future + for serv in self.client.services.list(): + host = self._get_column_value_from_single_row_table( + self.nova('service-list --binary %s' % serv.binary), 'Host') + service = self.nova('service-disable %s %s' % (host, serv.binary)) + self.addCleanup(self.nova, 'service-enable', + params="%s %s" % (host, serv.binary)) + status = self._get_column_value_from_single_row_table( + service, 'Status') + self.assertEqual('disabled', status) + service = self.nova('service-enable %s %s' % (host, serv.binary)) + status = self._get_column_value_from_single_row_table( + service, 'Status') + self.assertEqual('enabled', status) + + def test_os_service_disable_log_reason(self): + for serv in self.client.services.list(): + host = self._get_column_value_from_single_row_table( + self.nova('service-list --binary %s' % serv.binary), 'Host') + service = self.nova('service-disable --reason test_disable %s %s' + % (host, serv.binary)) + self.addCleanup(self.nova, 'service-enable', + params="%s %s" % (host, serv.binary)) + status = self._get_column_value_from_single_row_table( + service, 'Status') + log_reason = self._get_column_value_from_single_row_table( + service, 'Disabled Reason') + self.assertEqual('disabled', status) + self.assertEqual('test_disable', log_reason) diff --git a/novaclient/tests/functional/v2/test_os_services.py b/novaclient/tests/functional/v2/test_os_services.py new file mode 100644 index 000000000..9fef43397 --- /dev/null +++ b/novaclient/tests/functional/v2/test_os_services.py @@ -0,0 +1,36 @@ +# 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 novaclient.tests.functional.v2.legacy import test_os_services + + +class TestOsServicesNovaClientV211(test_os_services.TestOsServicesNovaClient): + """Functional tests for os-services attributes, microversion 2.11""" + + COMPUTE_API_VERSION = "2.11" + + def test_os_services_force_down_force_up(self): + for serv in self.client.services.list(): + host = self._get_column_value_from_single_row_table( + self.nova('service-list --binary %s' % serv.binary), 'Host') + service = self.nova('service-force-down %s %s' + % (host, serv.binary)) + self.addCleanup(self.nova, 'service-force-down --unset', + params="%s %s" % (host, serv.binary)) + status = self._get_column_value_from_single_row_table( + service, 'Forced down') + self.assertEqual('True', status) + service = self.nova('service-force-down --unset %s %s' + % (host, serv.binary)) + status = self._get_column_value_from_single_row_table( + service, 'Forced down') + self.assertEqual('False', status) From 48605fcb430126dc9b80dc73311d8dbc4e2ce801 Mon Sep 17 00:00:00 2001 From: Tomi Juvonen Date: Mon, 18 Jan 2016 13:27:16 +0200 Subject: [PATCH 0958/1705] Added support for Nova microversion 2.16 Added support for new host_status attribute in endpoints servers/detail and servers/{server_id}. Partially implements blueprint get-valid-server-state Change-Id: If72fbcaa02267077dc082a7557c593f35474c4bb --- novaclient/__init__.py | 2 +- novaclient/tests/unit/v2/test_shell.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 84c17c97d..c4d263ed5 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.15") +API_MAX_VERSION = api_versions.APIVersion("2.16") diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 9135cb14e..4533687c3 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2555,6 +2555,7 @@ def test_versions(self): # TODO(andreykurilin): remove 12 when 1522424 will be resolved 12, # doesn't require any changes in novaclient 15, # doesn't require any changes in novaclient + 16, # doesn't require any changes in novaclient ]) versions_supported = set(range(0, novaclient.API_MAX_VERSION.ver_minor + 1)) From 60b05f7128c466d0a8fa1c105c7aeb05c14d8264 Mon Sep 17 00:00:00 2001 From: Andreas Jaeger Date: Wed, 20 Jan 2016 19:21:40 +0100 Subject: [PATCH 0959/1705] Remove argparse from requirements argparse was external in python 2.6 but not anymore, remove it from requirements. This should help with pip 8.0 that gets confused in this situation. Installation of the external argparse is not needed. Change-Id: Ib7e74912b36c1b5ccb514e31fac35efeff57378d --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 76b731540..7697b511e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,6 @@ # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. pbr>=1.6 # Apache-2.0 -argparse # PSF keystoneauth1>=2.1.0 # Apache-2.0 iso8601>=0.1.9 # MIT oslo.i18n>=1.5.0 # Apache-2.0 From a54a1d16dbfcb1c8c09f3658a5fe9d463fe00adf Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Tue, 22 Dec 2015 16:14:49 +0200 Subject: [PATCH 0960/1705] [microversions] Extend shell with 2.12 Shell was not extended in original patch[1], which turned on v2.12, since there was not any support of vif in shell yet. Since shell entry point for legacy vif was merged[2], we need to add support for v2.12 in shell too. [1] - I18cf23847d3b2b01f5a6ffae2ebc4bede54babce [2] - Ib3078125beb7beaa08a3408486db54e0d10763e6 Change-Id: Ie38d099b2babfd8424c7d15bd3dfe8bd75e51136 --- .../v2/legacy/test_virtual_interface.py | 16 +++++------ .../functional/v2/test_virtual_interface.py | 28 +++++++++++++++++++ novaclient/tests/unit/v2/test_shell.py | 6 ++-- novaclient/v2/shell.py | 16 +++++++++-- 4 files changed, 52 insertions(+), 14 deletions(-) create mode 100644 novaclient/tests/functional/v2/test_virtual_interface.py diff --git a/novaclient/tests/functional/v2/legacy/test_virtual_interface.py b/novaclient/tests/functional/v2/legacy/test_virtual_interface.py index 30c090aa7..fe76dfbe5 100644 --- a/novaclient/tests/functional/v2/legacy/test_virtual_interface.py +++ b/novaclient/tests/functional/v2/legacy/test_virtual_interface.py @@ -15,8 +15,8 @@ from novaclient.v2 import shell -class TestConsolesNovaClient(base.ClientTestBase): - """Consoles functional tests.""" +class TestVirtualInterfacesNovaClient(base.ClientTestBase): + """Virtual Interfaces functional tests.""" COMPUTE_API_VERSION = "2.1" @@ -31,10 +31,10 @@ def _create_server(self): self.addCleanup(server.delete) return server - def _test_virtual_interface_list(self, command): - server = self._create_server() - completed_command = command % server.id - self.nova(completed_command) - def test_virtual_interface_list(self): - self._test_virtual_interface_list('virtual-interface-list %s') + server = self._create_server() + output = self.nova('virtual-interface-list %s' % server.id) + self.assertTrue(len(output.split("\n")) > 5, + "Output table of `virtual-interface-list` for the test" + " server should not be empty.") + return output diff --git a/novaclient/tests/functional/v2/test_virtual_interface.py b/novaclient/tests/functional/v2/test_virtual_interface.py new file mode 100644 index 000000000..46f682485 --- /dev/null +++ b/novaclient/tests/functional/v2/test_virtual_interface.py @@ -0,0 +1,28 @@ +# 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 novaclient.tests.functional.v2.legacy import test_virtual_interface + + +class TestVirtualInterfacesNovaClient( + test_virtual_interface.TestVirtualInterfacesNovaClient): + """Virtual Interfaces functional tests.""" + + COMPUTE_API_VERSION = "2.latest" + + def test_virtual_interface_list(self): + output = super(TestVirtualInterfacesNovaClient, + self).test_virtual_interface_list() + network = self.client.networks.list()[0] + self.assertEqual(network.id, + self._get_column_value_from_single_row_table( + output, "Network ID")) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index b058a0433..7fce58722 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2604,12 +2604,10 @@ def test_list_server_os_virtual_interfaces(self): def test_versions(self): exclusions = set([ 1, # Same as version 2.0 - 3, # Not implemented when test added, should not apply to adds. - 5, # Not implemented when test added, should not apply to adds. + 3, # doesn't require any changes in novaclient + 5, # doesn't require any changes in novaclient 7, # doesn't require any changes in novaclient 9, # doesn't require any changes in novaclient - # TODO(andreykurilin): remove 12 when 1522424 will be resolved - 12, # doesn't require any changes in novaclient 15, # doesn't require any changes in novaclient 16, # doesn't require any changes in novaclient ]) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 60058dcf2..6f1aefe9c 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -4919,10 +4919,22 @@ def do_version_list(cs, args): utils.print_list(result, columns) +@api_versions.wraps("2.0", "2.11") +def _print_virtual_interface_list(cs, interface_list): + columns = ['Id', 'Mac address'] + utils.print_list(interface_list, columns) + + +@api_versions.wraps("2.12") +def _print_virtual_interface_list(cs, interface_list): + columns = ['Id', 'Mac address', 'Network ID'] + formatters = {"Network ID": lambda o: o.net_id} + utils.print_list(interface_list, columns, formatters) + + @cliutils.arg('server', metavar='', help=_('ID of server.')) def do_virtual_interface_list(cs, args): """Show virtual interface info about the given server.""" server = _find_server(cs, args.server) interface_list = cs.virtual_interfaces.list(base.getid(server)) - columns = ['Id', 'Mac address'] - utils.print_list(interface_list, columns) + _print_virtual_interface_list(cs, interface_list) From ca4e838aadb90b27236e4b6a65f023e1ae253bcf Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Sat, 23 Jan 2016 10:35:52 +0000 Subject: [PATCH 0961/1705] Updated from global requirements Change-Id: Ib1a5293498921aed5e42c14aa7a64878166419d7 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 76b731540..c87ddb0dc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ pbr>=1.6 # Apache-2.0 argparse # PSF keystoneauth1>=2.1.0 # Apache-2.0 iso8601>=0.1.9 # MIT -oslo.i18n>=1.5.0 # Apache-2.0 +oslo.i18n>=2.1.0 # Apache-2.0 oslo.serialization>=1.10.0 # Apache-2.0 oslo.utils>=3.4.0 # Apache-2.0 PrettyTable<0.8,>=0.7 # BSD From 6791a8be69529cc9557761183ea8cb00d253fe51 Mon Sep 17 00:00:00 2001 From: jichenjc Date: Sat, 23 Jan 2016 22:06:48 +0800 Subject: [PATCH 0962/1705] Allow restore command with name param when we try to restore an instance, it's already deleted state, so we need to add 'delete=True' as default to restore command, otherwise, the search function can't find the instance whose state is deleted with the given name Change-Id: I81cdc46897d76333452069a691824264ad504518 Closes-Bug: 1534644 --- novaclient/base.py | 4 ++++ novaclient/tests/unit/v2/test_shell.py | 8 ++++++++ novaclient/v2/contrib/deferred_delete.py | 2 +- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/novaclient/base.py b/novaclient/base.py index 4958500eb..bca3779e8 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -380,6 +380,10 @@ def findall(self, **kwargs): all_tenants = kwargs['all_tenants'] list_kwargs['search_opts']['all_tenants'] = all_tenants searches = [(k, v) for k, v in searches if k != 'all_tenants'] + if "deleted" in kwargs: + deleted = kwargs['deleted'] + list_kwargs['search_opts']['deleted'] = deleted + searches = [(k, v) for k, v in searches if k != 'deleted'] listing = self.list(**list_kwargs) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index b058a0433..63e178629 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -1252,6 +1252,14 @@ def test_restore(self): self.run_command('restore sample-server') self.assert_called('POST', '/servers/1234/action', {'restore': None}) + def test_restore_withname(self): + self.run_command('restore sample-server') + self.assert_called('GET', + '/servers?deleted=True&name=sample-server', pos=0) + self.assert_called('GET', '/servers/1234', pos=1) + self.assert_called('POST', '/servers/1234/action', {'restore': None}, + pos=2) + def test_delete_two_with_two_existent(self): self.run_command('delete 1234 5678') self.assert_called('DELETE', '/servers/1234', pos=-5) diff --git a/novaclient/v2/contrib/deferred_delete.py b/novaclient/v2/contrib/deferred_delete.py index 5d8d7d9e3..35e24d0d4 100644 --- a/novaclient/v2/contrib/deferred_delete.py +++ b/novaclient/v2/contrib/deferred_delete.py @@ -25,4 +25,4 @@ def do_force_delete(cs, args): @cliutils.arg('server', metavar='', help='Name or ID of server.') def do_restore(cs, args): """Restore a soft-deleted server.""" - utils.find_resource(cs.servers, args.server).restore() + utils.find_resource(cs.servers, args.server, deleted=True).restore() From 6cbb22583b94660cfd78d8ee0068778d5279ceca Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Mon, 25 Jan 2016 14:07:27 +0200 Subject: [PATCH 0963/1705] [microversions] Add support for 2.17 Description of microversion: Add a new API for triggering crash dump in an instance. Different operation systems in instance may need different configurations to trigger crash dump. Note: - it is hard to write a functional test for this microversion, since it requires instance in error state - all possible failed responses already covered by novaclient.exceptions Nova's change: I6ed777ff637254b4b79417008f9055dd19fc7405 Change-Id: If03b1864bbe7074c720b946fc2700bd5d07debc3 --- novaclient/__init__.py | 2 +- novaclient/tests/unit/v2/fakes.py | 2 ++ novaclient/tests/unit/v2/test_servers.py | 13 +++++++++++++ novaclient/v2/servers.py | 9 +++++++++ novaclient/v2/shell.py | 9 +++++++++ 5 files changed, 34 insertions(+), 1 deletion(-) diff --git a/novaclient/__init__.py b/novaclient/__init__.py index c4d263ed5..1a2ae922e 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.16") +API_MAX_VERSION = api_versions.APIVersion("2.17") diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 23bca8857..a91230200 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -713,6 +713,8 @@ def check_server_actions(cls, body): elif action == 'createBackup': assert set(body[action]) == set(['name', 'backup_type', 'rotation']) + elif action == 'trigger_crash_dump': + assert body[action] is None else: return False return True diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index 1e0324a00..8fb3c6079 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -885,3 +885,16 @@ def test_evacuate(self): self.cs.servers.evacuate(s, 'fake_target_host', password='NewAdminPassword') self.assert_called('POST', '/servers/1234/action') + + +class ServersV217Test(ServersV214Test): + def setUp(self): + super(ServersV217Test, self).setUp() + self.cs.api_version = api_versions.APIVersion("2.17") + + def test_trigger_crash_dump(self): + s = self.cs.servers.get(1234) + s.trigger_crash_dump() + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.trigger_crash_dump(s) + self.assert_called('POST', '/servers/1234/action') diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index 1b43e49b3..4e8e60b89 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -429,6 +429,10 @@ def interface_detach(self, port_id): """ return self.manager.interface_detach(self, port_id) + def trigger_crash_dump(self): + """Trigger crash dump in an instance""" + return self.manager.trigger_crash_dump(self) + class ServerManager(base.BootingManagerWithFind): resource_class = Server @@ -1396,6 +1400,11 @@ def interface_detach(self, server, port_id): self._delete('/servers/%s/os-interface/%s' % (base.getid(server), port_id)) + @api_versions.wraps("2.17") + def trigger_crash_dump(self, server): + """Trigger crash dump in an instance""" + return self._action("trigger_crash_dump", server) + def _action(self, action, server, info=None, **kwargs): """ Perform a server "action" -- reboot/rebuild/resize/etc. diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 60058dcf2..41ec74956 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -4685,6 +4685,15 @@ def do_interface_detach(cs, args): utils.print_dict(res) +@api_versions.wraps("2.17") +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +def do_trigger_crash_dump(cs, args): + """Trigger crash dump in an instance.""" + server = _find_server(cs, args.server) + + server.trigger_crash_dump() + + def _treeizeAvailabilityZone(zone): """Build a tree view for availability zones.""" AvailabilityZone = availability_zones.AvailabilityZone From 2220c56375cac56911b52900e5458c6901557040 Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Fri, 25 Dec 2015 11:26:40 +0900 Subject: [PATCH 0964/1705] Add wrapper classes for return-request-id-to-caller Added wrapper classes which are inherited from base data types str, list, tuple and dict. Each of these wrapper classes and the Resource class contain a 'request_ids' attribute which is populated with a 'x-compute-request-id' or a 'x-openstack-request-id' received in a header from a response body. This change is required to return 'request_id' from client to log request_id mappings of cross projects. This patch is one of a series of patches for implementing return-request-id-to-caller. Co-authored-by: Ankit Agrawal Change-Id: I422c4f4ee59991ca89a0a16f548b537c8b61bb97 Implements: blueprint return-request-id-to-caller --- novaclient/base.py | 157 +++++++++++++++++++--- novaclient/tests/unit/fakes.py | 4 + novaclient/tests/unit/test_base.py | 87 ++++++++++++ novaclient/tests/unit/test_utils.py | 11 +- novaclient/tests/unit/v2/fakes.py | 4 + novaclient/tests/unit/v2/test_versions.py | 4 +- novaclient/v2/servers.py | 2 +- 7 files changed, 246 insertions(+), 23 deletions(-) diff --git a/novaclient/base.py b/novaclient/base.py index 4958500eb..5e83cdff8 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -28,6 +28,7 @@ import threading from oslo_utils import strutils +from requests import Response import six from novaclient import exceptions @@ -78,7 +79,43 @@ def run_hooks(cls, hook_type, *args, **kwargs): hook_func(*args, **kwargs) -class Resource(object): +class RequestIdMixin(object): + """Wrapper class to expose x-openstack-request-id to the caller. + """ + def request_ids_setup(self): + self.x_openstack_request_ids = [] + + @property + def request_ids(self): + return self.x_openstack_request_ids + + def append_request_ids(self, resp): + """Add request_ids as an attribute to the object + + :param resp: Response object or list of Response objects + """ + if isinstance(resp, list): + # Add list of request_ids if response is of type list. + for resp_obj in resp: + self._append_request_id(resp_obj) + elif resp is not None: + # Add request_ids if response contains single object. + self._append_request_id(resp) + + def _append_request_id(self, resp): + if isinstance(resp, Response): + # Extract 'x-openstack-request-id' from headers if + # response is a Response object. + request_id = (resp.headers.get('x-openstack-request-id') or + resp.headers.get('x-compute-request-id')) + else: + # If resp is of type string or None. + request_id = resp + if request_id not in self.x_openstack_request_ids: + self.x_openstack_request_ids.append(request_id) + + +class Resource(RequestIdMixin): """Base class for OpenStack resources (tenant, user, etc.). This is pretty much just a bag for attributes. @@ -87,22 +124,27 @@ class Resource(object): HUMAN_ID = False NAME_ATTR = 'name' - def __init__(self, manager, info, loaded=False): + def __init__(self, manager, info, loaded=False, resp=None): """Populate and bind to a manager. :param manager: BaseManager object :param info: dictionary representing resource attributes :param loaded: prevent lazy-loading if set to True + :param resp: Response or list of Response objects """ self.manager = manager self._info = info self._add_details(info) self._loaded = loaded + self.request_ids_setup() + self.append_request_ids(resp) def __repr__(self): reprkeys = sorted(k for k in self.__dict__.keys() - if k[0] != '_' and k != 'manager') + if k[0] != '_' and + k not in ['manager', 'request_ids', + 'x_openstack_request_ids']) info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys) return "<%s %s>" % (self.__class__.__name__, info) @@ -150,9 +192,9 @@ def get(self): new = self.manager.get(self.id) if new: self._add_details(new._info) - if self.manager.client.last_request_id: - self._add_details( - {'x_request_id': self.manager.client.last_request_id}) + # The 'request_ids' attribute has been added, + # so store the request id to it instead of _info + self.append_request_ids(new.request_ids) def __eq__(self, other): if not isinstance(other, Resource): @@ -196,9 +238,9 @@ def api_version(self): def _list(self, url, response_key, obj_class=None, body=None): if body: - _resp, body = self.api.client.post(url, body=body) + resp, body = self.api.client.post(url, body=body) else: - _resp, body = self.api.client.get(url) + resp, body = self.api.client.get(url) if obj_class is None: obj_class = self.resource_class @@ -214,8 +256,9 @@ def _list(self, url, response_key, obj_class=None, body=None): with self.completion_cache('human_id', obj_class, mode="w"): with self.completion_cache('uuid', obj_class, mode="w"): - return [obj_class(self, res, loaded=True) - for res in data if res] + items = [obj_class(self, res, loaded=True) + for res in data if res] + return ListWithMeta(items, resp) @contextlib.contextmanager def alternate_service_type(self, default, allowed_types=()): @@ -294,30 +337,51 @@ def write_to_completion_cache(self, cache_type, val): cache.write("%s\n" % val) def _get(self, url, response_key): - _resp, body = self.api.client.get(url) - return self.resource_class(self, body[response_key], loaded=True) + resp, body = self.api.client.get(url) + return self.resource_class(self, body[response_key], loaded=True, + resp=resp) def _create(self, url, body, response_key, return_raw=False, **kwargs): self.run_hooks('modify_body_for_create', body, **kwargs) - _resp, body = self.api.client.post(url, body=body) + resp, body = self.api.client.post(url, body=body) if return_raw: - return body[response_key] + return self.convert_into_with_meta(body[response_key], resp) with self.completion_cache('human_id', self.resource_class, mode="a"): with self.completion_cache('uuid', self.resource_class, mode="a"): - return self.resource_class(self, body[response_key]) + return self.resource_class(self, body[response_key], resp=resp) def _delete(self, url): - _resp, _body = self.api.client.delete(url) + resp, body = self.api.client.delete(url) + return self.convert_into_with_meta(body, resp) def _update(self, url, body, response_key=None, **kwargs): self.run_hooks('modify_body_for_update', body, **kwargs) - _resp, body = self.api.client.put(url, body=body) + resp, body = self.api.client.put(url, body=body) if body: if response_key: - return self.resource_class(self, body[response_key]) + return self.resource_class(self, body[response_key], resp=resp) else: - return self.resource_class(self, body) + return self.resource_class(self, body, resp=resp) + else: + return StrWithMeta(body, resp) + + def convert_into_with_meta(self, item, resp): + if isinstance(item, six.string_types): + if six.PY2 and isinstance(item, six.text_type): + return UnicodeWithMeta(item, resp) + else: + return StrWithMeta(item, resp) + elif isinstance(item, six.binary_type): + return BytesWithMeta(item, resp) + elif isinstance(item, list): + return ListWithMeta(item, resp) + elif isinstance(item, tuple): + return TupleWithMeta(item, resp) + elif item is None: + return TupleWithMeta((), resp) + else: + return DictWithMeta(item, resp) @six.add_metaclass(abc.ABCMeta) @@ -338,11 +402,12 @@ def find(self, **kwargs): elif num_matches > 1: raise exceptions.NoUniqueMatch else: + matches[0].append_request_ids(matches.request_ids) return matches[0] def findall(self, **kwargs): """Find all items with attributes matching ``**kwargs``.""" - found = [] + found = ListWithMeta([], None) searches = kwargs.items() detailed = True @@ -382,6 +447,7 @@ def findall(self, **kwargs): searches = [(k, v) for k, v in searches if k != 'all_tenants'] listing = self.list(**list_kwargs) + found.append_request_ids(listing.request_ids) for obj in listing: try: @@ -433,3 +499,54 @@ def _parse_block_device_mapping(self, block_device_mapping): bdm.append(bdm_dict) return bdm + + +class ListWithMeta(list, RequestIdMixin): + def __init__(self, values, resp): + super(ListWithMeta, self).__init__(values) + self.request_ids_setup() + self.append_request_ids(resp) + + +class DictWithMeta(dict, RequestIdMixin): + def __init__(self, values, resp): + super(DictWithMeta, self).__init__(values) + self.request_ids_setup() + self.append_request_ids(resp) + + +class TupleWithMeta(tuple, RequestIdMixin): + def __new__(cls, values, resp): + return super(TupleWithMeta, cls).__new__(cls, values) + + def __init__(self, values, resp): + self.request_ids_setup() + self.append_request_ids(resp) + + +class StrWithMeta(str, RequestIdMixin): + def __new__(cls, value, resp): + return super(StrWithMeta, cls).__new__(cls, value) + + def __init__(self, values, resp): + self.request_ids_setup() + self.append_request_ids(resp) + + +class BytesWithMeta(six.binary_type, RequestIdMixin): + def __new__(cls, value, resp): + return super(BytesWithMeta, cls).__new__(cls, value) + + def __init__(self, values, resp): + self.request_ids_setup() + self.append_request_ids(resp) + + +if six.PY2: + class UnicodeWithMeta(six.text_type, RequestIdMixin): + def __new__(cls, value, resp): + return super(UnicodeWithMeta, cls).__new__(cls, value) + + def __init__(self, values, resp): + self.request_ids_setup() + self.append_request_ids(resp) diff --git a/novaclient/tests/unit/fakes.py b/novaclient/tests/unit/fakes.py index 039422dcb..c69648657 100644 --- a/novaclient/tests/unit/fakes.py +++ b/novaclient/tests/unit/fakes.py @@ -21,6 +21,10 @@ from novaclient import base +# fake request id +FAKE_REQUEST_ID = 'req-3fdea7c2-e3e3-48b5-a656-6b12504c49a1' +FAKE_REQUEST_ID_LIST = [FAKE_REQUEST_ID] + def assert_has_keys(dict, required=None, optional=None): required = required or [] diff --git a/novaclient/tests/unit/test_base.py b/novaclient/tests/unit/test_base.py index b7bceb7b8..9fa75364c 100644 --- a/novaclient/tests/unit/test_base.py +++ b/novaclient/tests/unit/test_base.py @@ -11,6 +11,9 @@ # License for the specific language governing permissions and limitations # under the License. +from requests import Response +import six + from novaclient import base from novaclient import exceptions from novaclient.tests.unit import utils @@ -21,6 +24,18 @@ cs = fakes.FakeClient() +def create_response_obj_with_header(): + resp = Response() + resp.headers['x-openstack-request-id'] = fakes.FAKE_REQUEST_ID + return resp + + +def create_response_obj_with_compute_header(): + resp = Response() + resp.headers['x-compute-request-id'] = fakes.FAKE_REQUEST_ID + return resp + + class BaseTest(utils.TestCase): def test_resource_repr(self): @@ -67,3 +82,75 @@ def test_findall_invalid_attribute(self): self.assertRaises(exceptions.NotFound, cs.flavors.find, vegetable='carrot') + + def test_resource_object_with_request_ids(self): + resp_obj = create_response_obj_with_header() + r = base.Resource(None, {"name": "1"}, resp=resp_obj) + self.assertEqual(fakes.FAKE_REQUEST_ID_LIST, r.request_ids) + + def test_resource_object_with_compute_request_ids(self): + resp_obj = create_response_obj_with_compute_header() + r = base.Resource(None, {"name": "1"}, resp=resp_obj) + self.assertEqual(fakes.FAKE_REQUEST_ID_LIST, r.request_ids) + + +class ListWithMetaTest(utils.TestCase): + def test_list_with_meta(self): + resp = create_response_obj_with_header() + obj = base.ListWithMeta([], resp) + self.assertEqual([], obj) + # Check request_ids attribute is added to obj + self.assertTrue(hasattr(obj, 'request_ids')) + self.assertEqual(fakes.FAKE_REQUEST_ID_LIST, obj.request_ids) + + +class DictWithMetaTest(utils.TestCase): + def test_dict_with_meta(self): + resp = create_response_obj_with_header() + obj = base.DictWithMeta({}, resp) + self.assertEqual({}, obj) + # Check request_ids attribute is added to obj + self.assertTrue(hasattr(obj, 'request_ids')) + self.assertEqual(fakes.FAKE_REQUEST_ID_LIST, obj.request_ids) + + +class TupleWithMetaTest(utils.TestCase): + def test_tuple_with_meta(self): + resp = create_response_obj_with_header() + expected_tuple = (1, 2) + obj = base.TupleWithMeta(expected_tuple, resp) + self.assertEqual(expected_tuple, obj) + # Check request_ids attribute is added to obj + self.assertTrue(hasattr(obj, 'request_ids')) + self.assertEqual(fakes.FAKE_REQUEST_ID_LIST, obj.request_ids) + + +class StrWithMetaTest(utils.TestCase): + def test_str_with_meta(self): + resp = create_response_obj_with_header() + obj = base.StrWithMeta("test-str", resp) + self.assertEqual("test-str", obj) + # Check request_ids attribute is added to obj + self.assertTrue(hasattr(obj, 'request_ids')) + self.assertEqual(fakes.FAKE_REQUEST_ID_LIST, obj.request_ids) + + +class BytesWithMetaTest(utils.TestCase): + def test_bytes_with_meta(self): + resp = create_response_obj_with_header() + obj = base.BytesWithMeta(b'test-bytes', resp) + self.assertEqual(b'test-bytes', obj) + # Check request_ids attribute is added to obj + self.assertTrue(hasattr(obj, 'request_ids')) + self.assertEqual(fakes.FAKE_REQUEST_ID_LIST, obj.request_ids) + + +if six.PY2: + class UnicodeWithMetaTest(utils.TestCase): + def test_unicode_with_meta(self): + resp = create_response_obj_with_header() + obj = base.UnicodeWithMeta(u'test-unicode', resp) + self.assertEqual(u'test-unicode', obj) + # Check request_ids attribute is added to obj + self.assertTrue(hasattr(obj, 'request_ids')) + self.assertEqual(fakes.FAKE_REQUEST_ID_LIST, obj.request_ids) diff --git a/novaclient/tests/unit/test_utils.py b/novaclient/tests/unit/test_utils.py index 2eb3f0695..cfe4fd767 100644 --- a/novaclient/tests/unit/test_utils.py +++ b/novaclient/tests/unit/test_utils.py @@ -19,6 +19,7 @@ from novaclient import base from novaclient import exceptions +from novaclient.tests.unit import fakes from novaclient.tests.unit import utils as test_utils from novaclient import utils @@ -28,6 +29,8 @@ class FakeResource(object): NAME_ATTR = 'name' + request_ids = fakes.FAKE_REQUEST_ID_LIST + def __init__(self, _id, properties): self.id = _id try: @@ -35,6 +38,9 @@ def __init__(self, _id, properties): except KeyError: pass + def append_request_ids(self, resp): + pass + class FakeManager(base.ManagerWithFind): @@ -63,7 +69,7 @@ def get(self, resource_id): raise exceptions.NotFound(resource_id) def list(self): - return self.resources + return base.ListWithMeta(self.resources, fakes.FAKE_REQUEST_ID_LIST) class FakeDisplayResource(object): @@ -76,6 +82,9 @@ def __init__(self, _id, properties): except KeyError: pass + def append_request_ids(self, resp): + pass + class FakeDisplayManager(FakeManager): diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index a91230200..149835b06 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -45,6 +45,10 @@ FAKE_IMAGE_UUID_1 = 'c99d7632-bd66-4be9-aed5-3dd14b223a76' FAKE_IMAGE_UUID_2 = 'f27f479a-ddda-419a-9bbc-d6b56b210161' +# fake request id +FAKE_REQUEST_ID = fakes.FAKE_REQUEST_ID +FAKE_REQUEST_ID_LIST = fakes.FAKE_REQUEST_ID_LIST + class FakeClient(fakes.FakeClient, client.Client): diff --git a/novaclient/tests/unit/v2/test_versions.py b/novaclient/tests/unit/v2/test_versions.py index 3feab8be5..d0397f167 100644 --- a/novaclient/tests/unit/v2/test_versions.py +++ b/novaclient/tests/unit/v2/test_versions.py @@ -14,6 +14,7 @@ import mock +from novaclient import base from novaclient import exceptions as exc from novaclient.tests.unit import utils from novaclient.tests.unit.v2 import fakes @@ -47,7 +48,7 @@ def test_get_current_with_http_client(self, mock_list, None, {"links": [{"href": "http://nova-api:8774/v2.1"}]}, loaded=True) - mock_list.return_value = [ + all_versions = [ versions.Version( None, {"links": [{"href": "http://url/v1"}]}, loaded=True), versions.Version( @@ -57,6 +58,7 @@ def test_get_current_with_http_client(self, mock_list, current_version, versions.Version( None, {"links": [{"href": "http://url/v21"}]}, loaded=True)] + mock_list.return_value = base.ListWithMeta(all_versions, None) self.assertEqual(current_version, self.cs.versions.get_current()) @mock.patch.object(versions.VersionManager, '_is_session_client', diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index 4e8e60b89..cd2d2f4b5 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -622,7 +622,7 @@ def list(self, detailed=True, search_opts=None, marker=None, limit=None, if detailed: detail = "/detail" - result = [] + result = base.ListWithMeta([], None) while True: if marker: qparams['marker'] = marker From f9aa277c62a05648e030dfeaf21d653ec6a388f3 Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Fri, 25 Dec 2015 11:40:22 +0900 Subject: [PATCH 0965/1705] Add return-request-id-to-caller function(1/5) Add return-request-id-to-caller function to 'Server' resource and 'ServerManager'. The methods in the resource class and resource manager return a wrapper class that has 'request_ids' property. The caller can get request ids of the callee via the property. * novaclient/v2/servers.py Co-authored-by: Ankit Agrawal Change-Id: I8c2d3abcebba7823f42e2f27356e73bc6d7ade42 Implements: blueprint return-request-id-to-caller --- novaclient/tests/unit/fixture_data/base.py | 5 +- novaclient/tests/unit/fixture_data/servers.py | 21 +- novaclient/tests/unit/utils.py | 3 + novaclient/tests/unit/v2/fakes.py | 4 +- novaclient/tests/unit/v2/test_servers.py | 361 ++++++++++----- novaclient/v2/servers.py | 417 +++++++++++++----- 6 files changed, 581 insertions(+), 230 deletions(-) diff --git a/novaclient/tests/unit/fixture_data/base.py b/novaclient/tests/unit/fixture_data/base.py index 6a2e238b5..899a60929 100644 --- a/novaclient/tests/unit/fixture_data/base.py +++ b/novaclient/tests/unit/fixture_data/base.py @@ -13,13 +13,16 @@ import fixtures from six.moves.urllib import parse +from novaclient.tests.unit.v2 import fakes + COMPUTE_URL = 'http://compute.host' class Fixture(fixtures.Fixture): base_url = None - json_headers = {'Content-Type': 'application/json'} + json_headers = {'Content-Type': 'application/json', + 'x-openstack-request-id': fakes.FAKE_REQUEST_ID} def __init__(self, requests, compute_url=COMPUTE_URL): super(Fixture, self).__init__() diff --git a/novaclient/tests/unit/fixture_data/servers.py b/novaclient/tests/unit/fixture_data/servers.py index 4aff762a6..4d2df95b3 100644 --- a/novaclient/tests/unit/fixture_data/servers.py +++ b/novaclient/tests/unit/fixture_data/servers.py @@ -180,12 +180,14 @@ def setUp(self): headers=self.json_headers) for s in (1234, 5678): - self.requests.register_uri('DELETE', self.url(s), status_code=202) + self.requests.register_uri('DELETE', self.url(s), status_code=202, + headers=self.json_headers) for k in ('test_key', 'key1', 'key2'): self.requests.register_uri('DELETE', self.url(1234, 'metadata', k), - status_code=204) + status_code=204, + headers=self.json_headers) metadata1 = {'metadata': {'test_key': 'test_value'}} self.requests.register_uri('POST', self.url(1234, 'metadata'), @@ -218,7 +220,8 @@ def setUp(self): self.requests.register_uri('GET', self.url('1234', 'os-security-groups'), - json=get_security_groups) + json=get_security_groups, + headers=self.json_headers) self.requests.register_uri('POST', self.url(), json=self.post_servers, @@ -311,7 +314,8 @@ def post_os_volumes_boot(request, context): self.requests.register_uri('DELETE', self.url(1234, 'os-server-password'), - status_code=202) + status_code=202, + headers=self.json_headers) class V1(Base): @@ -342,10 +346,12 @@ def setUp(self): self.requests.register_uri('GET', self.url('1234', 'diagnostics'), - json=self.diagnostic) + json=self.diagnostic, + headers=self.json_headers) self.requests.register_uri('DELETE', - self.url('1234', 'os-interface', 'port-id')) + self.url('1234', 'os-interface', 'port-id'), + headers=self.json_headers) # Testing with the following password and key # @@ -371,7 +377,8 @@ def setUp(self): 'Hi/fmZZNQQqj1Ijq0caOIw=='} self.requests.register_uri('GET', self.url(1234, 'os-server-password'), - json=get_server_password) + json=get_server_password, + headers=self.json_headers) def post_servers(self, request, context): body = jsonutils.loads(request.body) diff --git a/novaclient/tests/unit/utils.py b/novaclient/tests/unit/utils.py index 4c5ec4932..c4fd70f56 100644 --- a/novaclient/tests/unit/utils.py +++ b/novaclient/tests/unit/utils.py @@ -66,6 +66,9 @@ def setUp(self): stderr = self.useFixture(fixtures.StringStream('stderr')).stream self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr)) + def assert_request_id(self, request_id_mixin, request_id_list): + self.assertEqual(request_id_list, request_id_mixin.request_ids) + class FixturedTestCase(testscenarios.TestWithScenarios, TestCase): diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 149835b06..ceb6672ec 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -48,6 +48,7 @@ # fake request id FAKE_REQUEST_ID = fakes.FAKE_REQUEST_ID FAKE_REQUEST_ID_LIST = fakes.FAKE_REQUEST_ID_LIST +FAKE_RESPONSE_HEADERS = {'x-openstack-request-id': FAKE_REQUEST_ID} class FakeClient(fakes.FakeClient, client.Client): @@ -724,7 +725,7 @@ def check_server_actions(cls, body): return True def post_servers_1234_action(self, body, **kw): - _headers = None + _headers = dict() _body = None resp = 202 assert len(body.keys()) == 1 @@ -770,6 +771,7 @@ def post_servers_1234_action(self, body, **kw): assert set(keys) == set(['onSharedStorage']) else: raise AssertionError("Unexpected server action: %s" % action) + _headers.update(FAKE_RESPONSE_HEADERS) return (resp, _headers, _body) def post_servers_5678_action(self, body, **kw): diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index 8fb3c6079..171a5dbc3 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -26,6 +26,7 @@ from novaclient.tests.unit.fixture_data import floatingips from novaclient.tests.unit.fixture_data import servers as data from novaclient.tests.unit import utils +from novaclient.tests.unit.v2 import fakes from novaclient.v2 import servers @@ -40,12 +41,14 @@ def setUp(self): def test_list_servers(self): sl = self.cs.servers.list() + self.assert_request_id(sl, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/servers/detail') for s in sl: self.assertIsInstance(s, servers.Server) def test_filter_servers_unicode(self): sl = self.cs.servers.list(search_opts={'name': u't€sting'}) + self.assert_request_id(sl, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/servers/detail?name=t%E2%82%ACsting') for s in sl: self.assertIsInstance(s, servers.Server) @@ -53,6 +56,7 @@ def test_filter_servers_unicode(self): def test_list_all_servers(self): # use marker just to identify this call in fixtures sl = self.cs.servers.list(limit=-1, marker=1234) + self.assert_request_id(sl, fakes.FAKE_REQUEST_ID_LIST) self.assertEqual(2, len(sl)) @@ -66,12 +70,14 @@ def test_list_all_servers(self): def test_list_servers_undetailed(self): sl = self.cs.servers.list(detailed=False) + self.assert_request_id(sl, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/servers') for s in sl: self.assertIsInstance(s, servers.Server) def test_list_servers_with_marker_limit(self): sl = self.cs.servers.list(marker=1234, limit=2) + self.assert_request_id(sl, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/servers/detail?limit=2&marker=1234') for s in sl: self.assertIsInstance(s, servers.Server) @@ -79,6 +85,7 @@ def test_list_servers_with_marker_limit(self): def test_list_servers_sort_single(self): sl = self.cs.servers.list(sort_keys=['display_name'], sort_dirs=['asc']) + self.assert_request_id(sl, fakes.FAKE_REQUEST_ID_LIST) self.assert_called( 'GET', '/servers/detail?sort_dir=asc&sort_key=display_name') @@ -88,6 +95,7 @@ def test_list_servers_sort_single(self): def test_list_servers_sort_multiple(self): sl = self.cs.servers.list(sort_keys=['display_name', 'id'], sort_dirs=['asc', 'desc']) + self.assert_request_id(sl, fakes.FAKE_REQUEST_ID_LIST) self.assert_called( 'GET', ('/servers/detail?sort_dir=asc&sort_dir=desc&' @@ -97,6 +105,7 @@ def test_list_servers_sort_multiple(self): def test_get_server_details(self): s = self.cs.servers.get(1234) + self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/servers/1234') self.assertIsInstance(s, servers.Server) self.assertEqual(1234, s.id) @@ -122,6 +131,7 @@ def test_create_server(self): '/tmp/foo.txt': six.StringIO('data'), # a stream } ) + self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers') self.assertIsInstance(s, servers.Server) @@ -152,6 +162,7 @@ def test_create_server_from_volume(): block_device_mapping=bdm, nics=nics ) + self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/os-volumes_boot') self.assertIsInstance(s, servers.Server) @@ -179,6 +190,7 @@ def wrapped_boot(url, key, *boot_args, **boot_kwargs): key_name="fakekey", block_device_mapping_v2=bdm ) + self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/os-volumes_boot') self.assertIsInstance(s, servers.Server) @@ -201,6 +213,7 @@ def wrapped_boot(url, key, *boot_args, **boot_kwargs): key_name="fakekey", nics=nics ) + self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers') self.assertIsInstance(s, servers.Server) @@ -225,6 +238,7 @@ def wrapped_boot(url, key, *boot_args, **boot_kwargs): access_ip_v6=access_ip_v6, access_ip_v4=access_ip_v4 ) + self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers') self.assertIsInstance(s, servers.Server) @@ -240,6 +254,7 @@ def test_create_server_userdata_file_object(self): '/tmp/foo.txt': six.StringIO('data'), # a stream }, ) + self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers') self.assertIsInstance(s, servers.Server) @@ -256,6 +271,7 @@ def test_create_server_userdata_unicode(self): '/tmp/foo.txt': six.StringIO('data'), # a stream }, ) + self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers') self.assertIsInstance(s, servers.Server) @@ -272,6 +288,7 @@ def test_create_server_userdata_utf8(self): '/tmp/foo.txt': six.StringIO('data'), # a stream }, ) + self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers') self.assertIsInstance(s, servers.Server) @@ -285,6 +302,7 @@ def test_create_server_admin_pass(self): admin_pass=test_password, key_name=test_key ) + self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers') self.assertIsInstance(s, servers.Server) body = jsonutils.loads(self.requests.last_request.body) @@ -308,6 +326,7 @@ def test_create_server_userdata_bin(self): '/tmp/foo.txt': six.StringIO('data'), # a stream }, ) + self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers') self.assertIsInstance(s, servers.Server) # verify userdata matches original @@ -323,6 +342,7 @@ def _create_disk_config(self, disk_config): flavor=1, disk_config=disk_config ) + self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers') self.assertIsInstance(s, servers.Server) @@ -343,8 +363,10 @@ def test_update_server(self): # Update via instance s.update(name='hi') + self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('PUT', '/servers/1234') s.update(name='hi') + self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('PUT', '/servers/1234') # Silly, but not an error @@ -352,33 +374,41 @@ def test_update_server(self): # Update via manager self.cs.servers.update(s, name='hi') + self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('PUT', '/servers/1234') def test_delete_server(self): s = self.cs.servers.get(1234) - s.delete() + ret = s.delete() + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('DELETE', '/servers/1234') - self.cs.servers.delete(1234) + ret = self.cs.servers.delete(1234) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('DELETE', '/servers/1234') - self.cs.servers.delete(s) + ret = self.cs.servers.delete(s) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('DELETE', '/servers/1234') def test_delete_server_meta(self): - self.cs.servers.delete_meta(1234, ['test_key']) + ret = self.cs.servers.delete_meta(1234, ['test_key']) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('DELETE', '/servers/1234/metadata/test_key') def test_set_server_meta(self): - self.cs.servers.set_meta(1234, {'test_key': 'test_value'}) + m = self.cs.servers.set_meta(1234, {'test_key': 'test_value'}) + self.assert_request_id(m, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/metadata', {'metadata': {'test_key': 'test_value'}}) def test_set_server_meta_item(self): - self.cs.servers.set_meta_item(1234, 'test_key', 'test_value') + m = self.cs.servers.set_meta_item(1234, 'test_key', 'test_value') + self.assert_request_id(m, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('PUT', '/servers/1234/metadata/test_key', {'meta': {'test_key': 'test_value'}}) def test_find(self): server = self.cs.servers.find(name='sample-server') + self.assert_request_id(server, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/servers/1234') self.assertEqual('sample-server', server.name) @@ -386,33 +416,41 @@ def test_find(self): flavor={"id": 1, "name": "256 MB Server"}) sl = self.cs.servers.findall(flavor={"id": 1, "name": "256 MB Server"}) + self.assert_request_id(sl, fakes.FAKE_REQUEST_ID_LIST) self.assertEqual([1234, 5678, 9012], [s.id for s in sl]) def test_reboot_server(self): s = self.cs.servers.get(1234) - s.reboot() + ret = s.reboot() + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') - self.cs.servers.reboot(s, reboot_type='HARD') + ret = self.cs.servers.reboot(s, reboot_type='HARD') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') def test_rebuild_server(self): s = self.cs.servers.get(1234) - s.rebuild(image=1) + ret = s.rebuild(image=1) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') - self.cs.servers.rebuild(s, image=1) + ret = self.cs.servers.rebuild(s, image=1) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') - s.rebuild(image=1, password='5678') + ret = s.rebuild(image=1, password='5678') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') - self.cs.servers.rebuild(s, image=1, password='5678') + ret = self.cs.servers.rebuild(s, image=1, password='5678') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') def _rebuild_resize_disk_config(self, disk_config, operation="rebuild"): s = self.cs.servers.get(1234) if operation == "rebuild": - s.rebuild(image=1, disk_config=disk_config) + ret = s.rebuild(image=1, disk_config=disk_config) elif operation == "resize": - s.resize(flavor=1, disk_config=disk_config) + ret = s.resize(flavor=1, disk_config=disk_config) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') # verify disk config param was used in the request: @@ -430,7 +468,8 @@ def test_rebuild_server_disk_config_manual(self): def test_rebuild_server_preserve_ephemeral(self): s = self.cs.servers.get(1234) - s.rebuild(image=1, preserve_ephemeral=True) + ret = s.rebuild(image=1, preserve_ephemeral=True) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') body = jsonutils.loads(self.requests.last_request.body) d = body['rebuild'] @@ -440,7 +479,8 @@ def test_rebuild_server_preserve_ephemeral(self): def test_rebuild_server_name_meta_files(self): files = {'/etc/passwd': 'some data'} s = self.cs.servers.get(1234) - s.rebuild(image=1, name='new', meta={'foo': 'bar'}, files=files) + ret = s.rebuild(image=1, name='new', meta={'foo': 'bar'}, files=files) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) body = jsonutils.loads(self.requests.last_request.body) d = body['rebuild'] self.assertEqual('new', d['name']) @@ -450,7 +490,8 @@ def test_rebuild_server_name_meta_files(self): def test_resize_server(self): s = self.cs.servers.get(1234) - s.resize(flavor=1) + ret = s.resize(flavor=1) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') self.cs.servers.resize(s, flavor=1) self.assert_called('POST', '/servers/1234/action') @@ -463,165 +504,210 @@ def test_resize_server_disk_config_manual(self): def test_confirm_resized_server(self): s = self.cs.servers.get(1234) - s.confirm_resize() + ret = s.confirm_resize() + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') self.cs.servers.confirm_resize(s) self.assert_called('POST', '/servers/1234/action') def test_revert_resized_server(self): s = self.cs.servers.get(1234) - s.revert_resize() + ret = s.revert_resize() + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') - self.cs.servers.revert_resize(s) + ret = self.cs.servers.revert_resize(s) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') def test_migrate_server(self): s = self.cs.servers.get(1234) - s.migrate() + ret = s.migrate() + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') - self.cs.servers.migrate(s) + ret = self.cs.servers.migrate(s) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') def test_add_fixed_ip(self): s = self.cs.servers.get(1234) - s.add_fixed_ip(1) + fip = s.add_fixed_ip(1) + self.assert_request_id(fip, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') - self.cs.servers.add_fixed_ip(s, 1) + fip = self.cs.servers.add_fixed_ip(s, 1) + self.assert_request_id(fip, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') def test_remove_fixed_ip(self): s = self.cs.servers.get(1234) - s.remove_fixed_ip('10.0.0.1') + ret = s.remove_fixed_ip('10.0.0.1') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') - self.cs.servers.remove_fixed_ip(s, '10.0.0.1') + ret = self.cs.servers.remove_fixed_ip(s, '10.0.0.1') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') def test_add_floating_ip(self): s = self.cs.servers.get(1234) - s.add_floating_ip('11.0.0.1') + fip = s.add_floating_ip('11.0.0.1') + self.assert_request_id(fip, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') - self.cs.servers.add_floating_ip(s, '11.0.0.1') + fip = self.cs.servers.add_floating_ip(s, '11.0.0.1') + self.assert_request_id(fip, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') f = self.cs.floating_ips.list()[0] - self.cs.servers.add_floating_ip(s, f) + fip = self.cs.servers.add_floating_ip(s, f) + self.assert_request_id(fip, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') - s.add_floating_ip(f) + fip = s.add_floating_ip(f) + self.assert_request_id(fip, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') def test_add_floating_ip_to_fixed(self): s = self.cs.servers.get(1234) - s.add_floating_ip('11.0.0.1', fixed_address='12.0.0.1') + fip = s.add_floating_ip('11.0.0.1', fixed_address='12.0.0.1') + self.assert_request_id(fip, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') - self.cs.servers.add_floating_ip(s, '11.0.0.1', - fixed_address='12.0.0.1') + fip = self.cs.servers.add_floating_ip(s, '11.0.0.1', + fixed_address='12.0.0.1') + self.assert_request_id(fip, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') f = self.cs.floating_ips.list()[0] - self.cs.servers.add_floating_ip(s, f) + fip = self.cs.servers.add_floating_ip(s, f) + self.assert_request_id(fip, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') - s.add_floating_ip(f) + fip = s.add_floating_ip(f) + self.assert_request_id(fip, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') def test_remove_floating_ip(self): s = self.cs.servers.get(1234) - s.remove_floating_ip('11.0.0.1') + ret = s.remove_floating_ip('11.0.0.1') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') - self.cs.servers.remove_floating_ip(s, '11.0.0.1') + ret = self.cs.servers.remove_floating_ip(s, '11.0.0.1') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') f = self.cs.floating_ips.list()[0] - self.cs.servers.remove_floating_ip(s, f) + ret = self.cs.servers.remove_floating_ip(s, f) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') - s.remove_floating_ip(f) + ret = s.remove_floating_ip(f) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') def test_stop(self): s = self.cs.servers.get(1234) - s.stop() + resp, ret = s.stop() + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') - self.cs.servers.stop(s) + resp, ret = self.cs.servers.stop(s) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') def test_force_delete(self): s = self.cs.servers.get(1234) - s.force_delete() + resp, ret = s.force_delete() + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') - self.cs.servers.force_delete(s) + resp, ret = self.cs.servers.force_delete(s) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') def test_restore(self): s = self.cs.servers.get(1234) - s.restore() + resp, ret = s.restore() + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') - self.cs.servers.restore(s) + resp, ret = self.cs.servers.restore(s) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') def test_start(self): s = self.cs.servers.get(1234) - s.start() + ret = s.start() + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') - self.cs.servers.start(s) + ret = self.cs.servers.start(s) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') def test_rescue(self): s = self.cs.servers.get(1234) - s.rescue() + resp, ret = s.rescue() + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') - self.cs.servers.rescue(s) + resp, ret = self.cs.servers.rescue(s) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') def test_rescue_password(self): s = self.cs.servers.get(1234) - s.rescue(password='asdf') + resp, ret = s.rescue(password='asdf') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action', {'rescue': {'adminPass': 'asdf'}}) - self.cs.servers.rescue(s, password='asdf') + resp, ret = self.cs.servers.rescue(s, password='asdf') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action', {'rescue': {'adminPass': 'asdf'}}) def test_rescue_image(self): s = self.cs.servers.get(1234) - s.rescue(image=1) + resp, ret = s.rescue(image=1) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action', {'rescue': {'rescue_image_ref': 1}}) - self.cs.servers.rescue(s, image=1) + resp, ret = self.cs.servers.rescue(s, image=1) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action', {'rescue': {'rescue_image_ref': 1}}) def test_unrescue(self): s = self.cs.servers.get(1234) - s.unrescue() + ret = s.unrescue() + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') - self.cs.servers.unrescue(s) + ret = self.cs.servers.unrescue(s) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') def test_lock(self): s = self.cs.servers.get(1234) - s.lock() + ret = s.lock() + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') - self.cs.servers.lock(s) + ret = self.cs.servers.lock(s) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') def test_unlock(self): s = self.cs.servers.get(1234) - s.unlock() + ret = s.unlock() + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') - self.cs.servers.unlock(s) + ret = self.cs.servers.unlock(s) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') def test_backup(self): s = self.cs.servers.get(1234) - s.backup('back1', 'daily', 1) + sb = s.backup('back1', 'daily', 1) + self.assert_request_id(sb, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') - self.cs.servers.backup(s, 'back1', 'daily', 2) + sb = self.cs.servers.backup(s, 'back1', 'daily', 2) + self.assert_request_id(sb, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') def test_get_console_output_without_length(self): success = 'foo' s = self.cs.servers.get(1234) - s.get_console_output() + co = s.get_console_output() + self.assert_request_id(co, fakes.FAKE_REQUEST_ID_LIST) self.assertEqual(success, s.get_console_output()) self.assert_called('POST', '/servers/1234/action') - self.cs.servers.get_console_output(s) + co = self.cs.servers.get_console_output(s) + self.assert_request_id(co, fakes.FAKE_REQUEST_ID_LIST) self.assertEqual(success, self.cs.servers.get_console_output(s)) self.assert_called('POST', '/servers/1234/action') @@ -629,11 +715,13 @@ def test_get_console_output_with_length(self): success = 'foo' s = self.cs.servers.get(1234) - s.get_console_output(length=50) + co = s.get_console_output(length=50) + self.assert_request_id(co, fakes.FAKE_REQUEST_ID_LIST) self.assertEqual(success, s.get_console_output(length=50)) self.assert_called('POST', '/servers/1234/action') - self.cs.servers.get_console_output(s, length=50) + co = self.cs.servers.get_console_output(s, length=50) + self.assert_request_id(co, fakes.FAKE_REQUEST_ID_LIST) self.assertEqual(success, self.cs.servers.get_console_output(s, length=50)) self.assert_called('POST', '/servers/1234/action') @@ -654,133 +742,168 @@ def test_get_console_output_with_length(self): def test_get_password(self): s = self.cs.servers.get(1234) - self.assertEqual(b'FooBar123', - s.get_password('novaclient/tests/unit/idfake.pem')) + password = s.get_password('novaclient/tests/unit/idfake.pem') + self.assert_request_id(password, fakes.FAKE_REQUEST_ID_LIST) + self.assertEqual(b'FooBar123', password) self.assert_called('GET', '/servers/1234/os-server-password') def test_get_password_without_key(self): s = self.cs.servers.get(1234) + password = s.get_password() + self.assert_request_id(password, fakes.FAKE_REQUEST_ID_LIST) self.assertEqual( 'OIuEuQttO8Rk93BcKlwHQsziDAnkAm/V6V8VPToA8ZeUaUBWwS0gwo2K6Y61Z96r' 'qG447iRz0uTEEYq3RAYJk1mh3mMIRVl27t8MtIecR5ggVVbz1S9AwXJQypDKl0ho' 'QFvhCBcMWPohyGewDJOhDbtuN1IoFI9G55ZvFwCm5y7m7B2aVcoLeIsJZE4PLsIw' '/y5a6Z3/AoJZYGG7IH5WN88UROU3B9JZGFB2qtPLQTOvDMZLUhoPRIJeHiVSlo1N' 'tI2/++UsXVg3ow6ItqCJGgdNuGG5JB+bslDHWPxROpesEIHdczk46HCpHQN8f1sk' - 'Hi/fmZZNQQqj1Ijq0caOIw==', s.get_password()) + 'Hi/fmZZNQQqj1Ijq0caOIw==', password) self.assert_called('GET', '/servers/1234/os-server-password') def test_clear_password(self): s = self.cs.servers.get(1234) - s.clear_password() + ret = s.clear_password() + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('DELETE', '/servers/1234/os-server-password') def test_get_server_diagnostics(self): s = self.cs.servers.get(1234) - diagnostics = s.diagnostics() + resp, diagnostics = s.diagnostics() + self.assert_request_id(diagnostics, fakes.FAKE_REQUEST_ID_LIST) self.assertIsNotNone(diagnostics) self.assert_called('GET', '/servers/1234/diagnostics') - diagnostics_from_manager = self.cs.servers.diagnostics(1234) + resp, diagnostics_from_manager = self.cs.servers.diagnostics(1234) + self.assert_request_id(diagnostics_from_manager, + fakes.FAKE_REQUEST_ID_LIST) self.assertIsNotNone(diagnostics_from_manager) self.assert_called('GET', '/servers/1234/diagnostics') - self.assertEqual(diagnostics[1], diagnostics_from_manager[1]) + self.assertEqual(diagnostics, diagnostics_from_manager) def test_get_vnc_console(self): s = self.cs.servers.get(1234) - s.get_vnc_console('fake') + vc = s.get_vnc_console('fake') + self.assert_request_id(vc, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') - self.cs.servers.get_vnc_console(s, 'fake') + vc = self.cs.servers.get_vnc_console(s, 'fake') + self.assert_request_id(vc, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') def test_get_spice_console(self): s = self.cs.servers.get(1234) - s.get_spice_console('fake') + sc = s.get_spice_console('fake') + self.assert_request_id(sc, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') - self.cs.servers.get_spice_console(s, 'fake') + sc = self.cs.servers.get_spice_console(s, 'fake') + self.assert_request_id(sc, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') def test_get_serial_console(self): s = self.cs.servers.get(1234) - s.get_serial_console('fake') + sc = s.get_serial_console('fake') + self.assert_request_id(sc, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') - self.cs.servers.get_serial_console(s, 'fake') + sc = self.cs.servers.get_serial_console(s, 'fake') + self.assert_request_id(sc, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') def test_get_rdp_console(self): s = self.cs.servers.get(1234) - s.get_rdp_console('fake') + rc = s.get_rdp_console('fake') + self.assert_request_id(rc, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') - self.cs.servers.get_rdp_console(s, 'fake') + rc = self.cs.servers.get_rdp_console(s, 'fake') + self.assert_request_id(rc, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') def test_create_image(self): s = self.cs.servers.get(1234) - s.create_image('123') + im = s.create_image('123') + self.assert_request_id(im, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') - s.create_image('123', {}) + im = s.create_image('123', {}) + self.assert_request_id(im, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') - self.cs.servers.create_image(s, '123') + im = self.cs.servers.create_image(s, '123') + self.assert_request_id(im, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + im = self.cs.servers.create_image(s, '123', {}) + self.assert_request_id(im, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') - self.cs.servers.create_image(s, '123', {}) def test_live_migrate_server(self): s = self.cs.servers.get(1234) - s.live_migrate(host='hostname', block_migration=False, - disk_over_commit=False) + ret = s.live_migrate(host='hostname', block_migration=False, + disk_over_commit=False) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') - self.cs.servers.live_migrate(s, host='hostname', block_migration=False, - disk_over_commit=False) + ret = self.cs.servers.live_migrate(s, host='hostname', + block_migration=False, + disk_over_commit=False) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') def test_reset_state(self): s = self.cs.servers.get(1234) - s.reset_state('newstate') + ret = s.reset_state('newstate') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') - self.cs.servers.reset_state(s, 'newstate') + ret = self.cs.servers.reset_state(s, 'newstate') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') def test_reset_network(self): s = self.cs.servers.get(1234) - s.reset_network() + ret = s.reset_network() + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') - self.cs.servers.reset_network(s) + ret = self.cs.servers.reset_network(s) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') def test_add_security_group(self): s = self.cs.servers.get(1234) - s.add_security_group('newsg') + sg = s.add_security_group('newsg') + self.assert_request_id(sg, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') - self.cs.servers.add_security_group(s, 'newsg') + sg = self.cs.servers.add_security_group(s, 'newsg') + self.assert_request_id(sg, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') def test_remove_security_group(self): s = self.cs.servers.get(1234) - s.remove_security_group('oldsg') + ret = s.remove_security_group('oldsg') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') - self.cs.servers.remove_security_group(s, 'oldsg') + ret = self.cs.servers.remove_security_group(s, 'oldsg') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') def test_list_security_group(self): s = self.cs.servers.get(1234) - s.list_security_group() + sgs = s.list_security_group() + self.assert_request_id(sgs, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/servers/1234/os-security-groups') def test_evacuate(self): s = self.cs.servers.get(1234) - s.evacuate('fake_target_host', 'True') + resp, ret = s.evacuate('fake_target_host', 'True') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') - self.cs.servers.evacuate(s, 'fake_target_host', - 'False', 'NewAdminPassword') + resp, ret = self.cs.servers.evacuate(s, 'fake_target_host', + 'False', 'NewAdminPassword') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') def test_interface_list(self): s = self.cs.servers.get(1234) - s.interface_list() + il = s.interface_list() + self.assert_request_id(il, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/servers/1234/os-interface') def test_interface_list_result_string_representable(self): @@ -812,12 +935,14 @@ def test_interface_list_result_string_representable(self): def test_interface_attach(self): s = self.cs.servers.get(1234) - s.interface_attach(None, None, None) + ret = s.interface_attach(None, None, None) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/os-interface') def test_interface_detach(self): s = self.cs.servers.get(1234) - s.interface_detach('port-id') + ret = s.interface_detach('port-id') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('DELETE', '/servers/1234/os-interface/port-id') @@ -828,34 +953,42 @@ def setUp(self): def test_get_vnc_console(self): s = self.cs.servers.get(1234) - s.get_vnc_console('fake') + vc = s.get_vnc_console('fake') + self.assert_request_id(vc, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/remote-consoles') - self.cs.servers.get_vnc_console(s, 'fake') + vc = self.cs.servers.get_vnc_console(s, 'fake') + self.assert_request_id(vc, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/remote-consoles') def test_get_spice_console(self): s = self.cs.servers.get(1234) - s.get_spice_console('fake') + sc = s.get_spice_console('fake') + self.assert_request_id(sc, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/remote-consoles') - self.cs.servers.get_spice_console(s, 'fake') + sc = self.cs.servers.get_spice_console(s, 'fake') + self.assert_request_id(sc, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/remote-consoles') def test_get_serial_console(self): s = self.cs.servers.get(1234) - s.get_serial_console('fake') + sc = s.get_serial_console('fake') + self.assert_request_id(sc, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/remote-consoles') - self.cs.servers.get_serial_console(s, 'fake') + sc = self.cs.servers.get_serial_console(s, 'fake') + self.assert_request_id(sc, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/remote-consoles') def test_get_rdp_console(self): s = self.cs.servers.get(1234) - s.get_rdp_console('fake') + rc = s.get_rdp_console('fake') + self.assert_request_id(rc, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/remote-consoles') - self.cs.servers.get_rdp_console(s, 'fake') + rc = self.cs.servers.get_rdp_console(s, 'fake') + self.assert_request_id(rc, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/remote-consoles') @@ -866,10 +999,12 @@ def setUp(self): def test_get_mks_console(self): s = self.cs.servers.get(1234) - s.get_mks_console() + mksc = s.get_mks_console() + self.assert_request_id(mksc, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/remote-consoles') - self.cs.servers.get_mks_console(s) + mksc = self.cs.servers.get_mks_console(s) + self.assert_request_id(mksc, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/remote-consoles') diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index cd2d2f4b5..7120df1ab 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -44,16 +44,19 @@ def __repr__(self): def delete(self): """ Delete (i.e. shut down and delete the image) this server. + + :returns: An instance of novaclient.base.TupleWithMeta """ - self.manager.delete(self) + return self.manager.delete(self) def update(self, name=None): """ Update the name for this server. :param name: Update the server's name. + :returns: :class:`Server` """ - self.manager.update(self, name=name) + return self.manager.update(self, name=name) def get_console_output(self, length=None): """ @@ -126,8 +129,9 @@ def add_fixed_ip(self, network_id): Add an IP address on a network. :param network_id: The ID of the network the IP should be on. + :returns: An instance of novaclient.base.TupleWithMeta """ - self.manager.add_fixed_ip(self, network_id) + return self.manager.add_fixed_ip(self, network_id) def add_floating_ip(self, address, fixed_address=None): """ @@ -136,76 +140,101 @@ def add_floating_ip(self, address, fixed_address=None): :param address: The IP address or FloatingIP to add to the instance :param fixed_address: The fixedIP address the FloatingIP is to be associated with (optional) + :returns: An instance of novaclient.base.TupleWithMeta """ - self.manager.add_floating_ip(self, address, fixed_address) + return self.manager.add_floating_ip(self, address, fixed_address) def remove_floating_ip(self, address): """ Remove floating IP from an instance :param address: The IP address or FloatingIP to remove + :returns: An instance of novaclient.base.TupleWithMeta """ - self.manager.remove_floating_ip(self, address) + return self.manager.remove_floating_ip(self, address) def stop(self): """ Stop -- Stop the running server. + + :returns: A Response object and an instance of + novaclient.base.TupleWithMeta """ - self.manager.stop(self) + return self.manager.stop(self) def force_delete(self): """ Force delete -- Force delete a server. + + :returns: A Response object and an instance of + novaclient.base.TupleWithMeta """ - self.manager.force_delete(self) + return self.manager.force_delete(self) def restore(self): """ Restore -- Restore a server in 'soft-deleted' state. + + :returns: A Response object and an instance of + novaclient.base.TupleWithMeta """ - self.manager.restore(self) + return self.manager.restore(self) def start(self): """ Start -- Start the paused server. + + :returns: An instance of novaclient.base.TupleWithMeta """ - self.manager.start(self) + return self.manager.start(self) def pause(self): """ Pause -- Pause the running server. + + :returns: An instance of novaclient.base.TupleWithMeta """ - self.manager.pause(self) + return self.manager.pause(self) def unpause(self): """ Unpause -- Unpause the paused server. + + :returns: An instance of novaclient.base.TupleWithMeta """ - self.manager.unpause(self) + return self.manager.unpause(self) def lock(self): """ Lock -- Lock the instance from certain operations. + + :returns: An instance of novaclient.base.TupleWithMeta """ - self.manager.lock(self) + return self.manager.lock(self) def unlock(self): """ Unlock -- Remove instance lock. + + :returns: An instance of novaclient.base.TupleWithMeta """ - self.manager.unlock(self) + return self.manager.unlock(self) def suspend(self): """ Suspend -- Suspend the running server. + + :returns: An instance of novaclient.base.TupleWithMeta """ - self.manager.suspend(self) + return self.manager.suspend(self) def resume(self): """ Resume -- Resume the suspended server. + + :returns: An instance of novaclient.base.TupleWithMeta """ - self.manager.resume(self) + return self.manager.resume(self) def rescue(self, password=None, image=None): """ @@ -213,32 +242,42 @@ def rescue(self, password=None, image=None): :param password: The admin password to be set in the rescue instance. :param image: The :class:`Image` to rescue with. + :returns: A Response object and an instance of + novaclient.base.DictWithMeta """ return self.manager.rescue(self, password, image) def unrescue(self): """ Unrescue -- Unrescue the rescued server. + + :returns: An instance of novaclient.base.TupleWithMeta """ - self.manager.unrescue(self) + return self.manager.unrescue(self) def shelve(self): """ Shelve -- Shelve the server. + + :returns: An instance of novaclient.base.TupleWithMeta """ - self.manager.shelve(self) + return self.manager.shelve(self) def shelve_offload(self): """ Shelve_offload -- Remove a shelved server from the compute node. + + :returns: An instance of novaclient.base.TupleWithMeta """ - self.manager.shelve_offload(self) + return self.manager.shelve_offload(self) def unshelve(self): """ Unshelve -- Unshelve the server. + + :returns: An instance of novaclient.base.TupleWithMeta """ - self.manager.unshelve(self) + return self.manager.unshelve(self) def diagnostics(self): """Diagnostics -- Retrieve server diagnostics.""" @@ -247,24 +286,28 @@ def diagnostics(self): def migrate(self): """ Migrate a server to a new host. + + :returns: An instance of novaclient.base.TupleWithMeta """ - self.manager.migrate(self) + return self.manager.migrate(self) def remove_fixed_ip(self, address): """ Remove an IP address. :param address: The IP address to remove. + :returns: An instance of novaclient.base.TupleWithMeta """ - self.manager.remove_fixed_ip(self, address) + return self.manager.remove_fixed_ip(self, address) def change_password(self, password): """ Update the admin password for a server. :param password: string to set as the admin password on the server + :returns: An instance of novaclient.base.TupleWithMeta """ - self.manager.change_password(self, password) + return self.manager.change_password(self, password) def reboot(self, reboot_type=REBOOT_SOFT): """ @@ -272,8 +315,9 @@ def reboot(self, reboot_type=REBOOT_SOFT): :param reboot_type: either :data:`REBOOT_SOFT` for a software-level reboot, or `REBOOT_HARD` for a virtual power cycle hard reboot. + :returns: An instance of novaclient.base.TupleWithMeta """ - self.manager.reboot(self, reboot_type) + return self.manager.reboot(self, reboot_type) def rebuild(self, image, password=None, preserve_ephemeral=False, **kwargs): @@ -295,13 +339,14 @@ def resize(self, flavor, **kwargs): Resize the server's resources. :param flavor: the :class:`Flavor` (or its ID) to resize to. + :returns: An instance of novaclient.base.TupleWithMeta Until a resize event is confirmed with :meth:`confirm_resize`, the old server will be kept around and you'll be able to roll back to the old flavor quickly with :meth:`revert_resize`. All resizes are automatically confirmed after 24 hours. """ - self.manager.resize(self, flavor, **kwargs) + return self.manager.resize(self, flavor, **kwargs) def create_image(self, image_name, metadata=None): """ @@ -320,20 +365,25 @@ def backup(self, backup_name, backup_type, rotation): :param backup_type: The backup type, like 'daily' or 'weekly' :param rotation: Int parameter representing how many backups to keep around. + :returns: An instance of novaclient.base.TupleWithMeta """ - self.manager.backup(self, backup_name, backup_type, rotation) + return self.manager.backup(self, backup_name, backup_type, rotation) def confirm_resize(self): """ Confirm that the resize worked, thus removing the original server. + + :returns: An instance of novaclient.base.TupleWithMeta """ - self.manager.confirm_resize(self) + return self.manager.confirm_resize(self) def revert_resize(self): """ Revert a previous resize, switching back to the old server. + + :returns: An instance of novaclient.base.TupleWithMeta """ - self.manager.revert_resize(self) + return self.manager.revert_resize(self) @property def networks(self): @@ -353,34 +403,48 @@ def live_migrate(self, host=None, disk_over_commit=False): """ Migrates a running instance to a new machine. + + :param host: destination host name. + :param block_migration: if True, do block_migration. + :param disk_over_commit: if True, Allow overcommit. + :returns: An instance of novaclient.base.TupleWithMeta """ - self.manager.live_migrate(self, host, - block_migration, - disk_over_commit) + return self.manager.live_migrate(self, host, + block_migration, + disk_over_commit) def reset_state(self, state='error'): """ Reset the state of an instance to active or error. + + :returns: An instance of novaclient.base.TupleWithMeta """ - self.manager.reset_state(self, state) + return self.manager.reset_state(self, state) def reset_network(self): """ Reset network of an instance. + + :returns: An instance of novaclient.base.TupleWithMeta """ - self.manager.reset_network(self) + return self.manager.reset_network(self) def add_security_group(self, security_group): """ Add a security group to an instance. + + :param security_group: The name of security group to add + :returns: An instance of novaclient.base.DictWithMeta """ - self.manager.add_security_group(self, security_group) + return self.manager.add_security_group(self, security_group) def remove_security_group(self, security_group): """ Remove a security group from an instance. + + :returns: An instance of novaclient.base.TupleWithMeta """ - self.manager.remove_security_group(self, security_group) + return self.manager.remove_security_group(self, security_group) def list_security_group(self): """ @@ -398,6 +462,8 @@ def evacuate(self, host=None, on_shared_storage=None, password=None): parameter must have its default value of None. :param password: string to set as admin password on the evacuated server. + :returns: A Response object and an instance of + novaclient.base.TupleWithMeta """ if api_versions.APIVersion("2.14") <= self.manager.api_version: if on_shared_storage is not None: @@ -651,6 +717,7 @@ def list(self, detailed=True, search_opts=None, marker=None, limit=None, servers = self._list("/servers%s%s" % (detail, query_string), "servers") result.extend(servers) + result.append_request_ids(servers.request_ids) if not servers or limit != -1: break @@ -663,8 +730,9 @@ def add_fixed_ip(self, server, network_id): :param server: The :class:`Server` (or its ID) to add an IP to. :param network_id: The ID of the network the IP should be on. + :returns: An instance of novaclient.base.TupleWithMeta """ - self._action('addFixedIp', server, {'networkId': network_id}) + return self._action('addFixedIp', server, {'networkId': network_id}) def remove_fixed_ip(self, server, address): """ @@ -672,8 +740,9 @@ def remove_fixed_ip(self, server, address): :param server: The :class:`Server` (or its ID) to add an IP to. :param address: The IP address to remove. + :returns: An instance of novaclient.base.TupleWithMeta """ - self._action('removeFixedIp', server, {'address': address}) + return self._action('removeFixedIp', server, {'address': address}) def add_floating_ip(self, server, address, fixed_address=None): """ @@ -683,16 +752,18 @@ def add_floating_ip(self, server, address, fixed_address=None): :param address: The FloatingIP or string floating address to add. :param fixed_address: The FixedIP the floatingIP should be associated with (optional) + :returns: An instance of novaclient.base.TupleWithMeta """ address = address.ip if hasattr(address, 'ip') else address if fixed_address: if hasattr(fixed_address, 'ip'): fixed_address = fixed_address.ip - self._action('addFloatingIp', server, - {'address': address, 'fixed_address': fixed_address}) + return self._action('addFloatingIp', server, + {'address': address, + 'fixed_address': fixed_address}) else: - self._action('addFloatingIp', server, {'address': address}) + return self._action('addFloatingIp', server, {'address': address}) def remove_floating_ip(self, server, address): """ @@ -700,10 +771,11 @@ def remove_floating_ip(self, server, address): :param server: The :class:`Server` (or its ID) to remove an IP from. :param address: The FloatingIP or string floating address to remove. + :returns: An instance of novaclient.base.TupleWithMeta """ address = address.ip if hasattr(address, 'ip') else address - self._action('removeFloatingIp', server, {'address': address}) + return self._action('removeFloatingIp', server, {'address': address}) @api_versions.wraps('2.0', '2.5') def get_vnc_console(self, server, console_type): @@ -712,10 +784,10 @@ def get_vnc_console(self, server, console_type): :param server: The :class:`Server` (or its ID) to get console for. :param console_type: Type of vnc console to get ('novnc' or 'xvpvnc') + :returns: An instance of novaclient.base.DictWithMeta """ - return self._action('os-getVNCConsole', server, - {'type': console_type})[1] + return self._action('os-getVNCConsole', server, {'type': console_type}) @api_versions.wraps('2.0', '2.5') def get_spice_console(self, server, console_type): @@ -724,10 +796,11 @@ def get_spice_console(self, server, console_type): :param server: The :class:`Server` (or its ID) to get console for. :param console_type: Type of spice console to get ('spice-html5') + :returns: An instance of novaclient.base.DictWithMeta """ return self._action('os-getSPICEConsole', server, - {'type': console_type})[1] + {'type': console_type}) @api_versions.wraps('2.0', '2.5') def get_rdp_console(self, server, console_type): @@ -736,10 +809,11 @@ def get_rdp_console(self, server, console_type): :param server: The :class:`Server` (or its ID) to get console for. :param console_type: Type of rdp console to get ('rdp-html5') + :returns: An instance of novaclient.base.DictWithMeta """ return self._action('os-getRDPConsole', server, - {'type': console_type})[1] + {'type': console_type}) @api_versions.wraps('2.0', '2.5') def get_serial_console(self, server, console_type): @@ -748,10 +822,11 @@ def get_serial_console(self, server, console_type): :param server: The :class:`Server` (or its ID) to get console for. :param console_type: Type of serial console to get ('serial') + :returns: An instance of novaclient.base.DictWithMeta """ return self._action('os-getSerialConsole', server, - {'type': console_type})[1] + {'type': console_type}) @api_versions.wraps('2.6') def get_vnc_console(self, server, console_type): @@ -760,10 +835,11 @@ def get_vnc_console(self, server, console_type): :param server: The :class:`Server` (or its ID) to get console for. :param console_type: Type of vnc console to get ('novnc' or 'xvpvnc') + :returns: An instance of novaclient.base.DictWithMeta """ return self._console(server, - {'protocol': 'vnc', 'type': console_type})[1] + {'protocol': 'vnc', 'type': console_type}) @api_versions.wraps('2.6') def get_spice_console(self, server, console_type): @@ -772,10 +848,11 @@ def get_spice_console(self, server, console_type): :param server: The :class:`Server` (or its ID) to get console for. :param console_type: Type of spice console to get ('spice-html5') + :returns: An instance of novaclient.base.DictWithMeta """ return self._console(server, - {'protocol': 'spice', 'type': console_type})[1] + {'protocol': 'spice', 'type': console_type}) @api_versions.wraps('2.6') def get_rdp_console(self, server, console_type): @@ -784,10 +861,11 @@ def get_rdp_console(self, server, console_type): :param server: The :class:`Server` (or its ID) to get console for. :param console_type: Type of rdp console to get ('rdp-html5') + :returns: An instance of novaclient.base.DictWithMeta """ return self._console(server, - {'protocol': 'rdp', 'type': console_type})[1] + {'protocol': 'rdp', 'type': console_type}) @api_versions.wraps('2.6') def get_serial_console(self, server, console_type): @@ -796,10 +874,11 @@ def get_serial_console(self, server, console_type): :param server: The :class:`Server` (or its ID) to get console for. :param console_type: Type of serial console to get ('serial') + :returns: An instance of novaclient.base.DictWithMeta """ return self._console(server, - {'protocol': 'serial', 'type': console_type})[1] + {'protocol': 'serial', 'type': console_type}) @api_versions.wraps('2.8') def get_mks_console(self, server): @@ -807,10 +886,11 @@ def get_mks_console(self, server): Get a mks console for an instance :param server: The :class:`Server` (or its ID) to get console for. + :returns: An instance of novaclient.base.DictWithMeta """ return self._console(server, - {'protocol': 'mks', 'type': 'webmks'})[1] + {'protocol': 'mks', 'type': 'webmks'}) def get_password(self, server, private_key=None): """ @@ -825,17 +905,20 @@ def get_password(self, server, private_key=None): password is to be returned :param private_key: The private key to decrypt password (optional) + :returns: An instance of novaclient.base.StrWithMeta or + novaclient.base.BytesWithMeta or + novaclient.base.UnicodeWithMeta """ - _resp, body = self.api.client.get("/servers/%s/os-server-password" - % base.getid(server)) + resp, body = self.api.client.get("/servers/%s/os-server-password" + % base.getid(server)) ciphered_pw = body.get('password', '') if body else '' if private_key and ciphered_pw: try: - return crypto.decrypt_password(private_key, ciphered_pw) + ciphered_pw = crypto.decrypt_password(private_key, ciphered_pw) except Exception as exc: - return '%sFailed to decrypt:\n%s' % (exc, ciphered_pw) - return ciphered_pw + ciphered_pw = '%sFailed to decrypt:\n%s' % (exc, ciphered_pw) + return self.convert_into_with_meta(ciphered_pw, resp) def clear_password(self, server): """ @@ -853,62 +936,99 @@ def clear_password(self, server): def stop(self, server): """ Stop the server. + + :param server: The :class:`Server` (or its ID) to stop + :returns: A Response object and an instance of + novaclient.base.TupleWithMeta """ - return self._action('os-stop', server, None) + resp, body = self._action_return_resp_and_body('os-stop', server, None) + return resp, self.convert_into_with_meta(body, resp) def force_delete(self, server): """ Force delete the server. + + :param server: The :class:`Server` (or its ID) to force delete + :returns: A Response object and an instance of + novaclient.base.TupleWithMeta """ - return self._action('forceDelete', server, None) + resp, body = self._action_return_resp_and_body('forceDelete', server, + None) + return resp, self.convert_into_with_meta(body, resp) def restore(self, server): """ Restore soft-deleted server. + + :param server: The :class:`Server` (or its ID) to restore + :returns: A Response object and an instance of + novaclient.base.TupleWithMeta """ - return self._action('restore', server, None) + resp, body = self._action_return_resp_and_body('restore', server, None) + return resp, self.convert_into_with_meta(body, resp) def start(self, server): """ Start the server. + + :param server: The :class:`Server` (or its ID) to start + :returns: An instance of novaclient.base.TupleWithMeta """ - self._action('os-start', server, None) + return self._action('os-start', server, None) def pause(self, server): """ Pause the server. + + :param server: The :class:`Server` (or its ID) to pause + :returns: An instance of novaclient.base.TupleWithMeta """ - self._action('pause', server, None) + return self._action('pause', server, None) def unpause(self, server): """ Unpause the server. + + :param server: The :class:`Server` (or its ID) to unpause + :returns: An instance of novaclient.base.TupleWithMeta """ - self._action('unpause', server, None) + return self._action('unpause', server, None) def lock(self, server): """ Lock the server. + + :param server: The :class:`Server` (or its ID) to lock + :returns: An instance of novaclient.base.TupleWithMeta """ - self._action('lock', server, None) + return self._action('lock', server, None) def unlock(self, server): """ Unlock the server. + + :param server: The :class:`Server` (or its ID) to unlock + :returns: An instance of novaclient.base.TupleWithMeta """ - self._action('unlock', server, None) + return self._action('unlock', server, None) def suspend(self, server): """ Suspend the server. + + :param server: The :class:`Server` (or its ID) to suspend + :returns: An instance of novaclient.base.TupleWithMeta """ - self._action('suspend', server, None) + return self._action('suspend', server, None) def resume(self, server): """ Resume the server. + + :param server: The :class:`Server` (or its ID) to resume + :returns: An instance of novaclient.base.TupleWithMeta """ - self._action('resume', server, None) + return self._action('resume', server, None) def rescue(self, server, password=None, image=None): """ @@ -917,52 +1037,82 @@ def rescue(self, server, password=None, image=None): :param server: The :class:`Server` to rescue. :param password: The admin password to be set in the rescue instance. :param image: The :class:`Image` to rescue with. + :returns: A Response object and an instance of + novaclient.base.DictWithMeta """ info = {} if password: info['adminPass'] = password if image: info['rescue_image_ref'] = base.getid(image) - return self._action('rescue', server, info or None) + resp, body = self._action_return_resp_and_body('rescue', server, + info or None) + # For compatibility, return Response object as a first return value + return resp, base.DictWithMeta(body, resp) def unrescue(self, server): """ Unrescue the server. + + :param server: The :class:`Server` (or its ID) to unrescue + :returns: An instance of novaclient.base.TupleWithMeta """ - self._action('unrescue', server, None) + return self._action('unrescue', server, None) def shelve(self, server): """ Shelve the server. + + :param server: The :class:`Server` (or its ID) to shelve + :returns: An instance of novaclient.base.TupleWithMeta """ - self._action('shelve', server, None) + return self._action('shelve', server, None) def shelve_offload(self, server): """ Remove a shelved instance from the compute node. + + :param server: The :class:`Server` (or its ID) to shelve offload + :returns: An instance of novaclient.base.TupleWithMeta """ - self._action('shelveOffload', server, None) + return self._action('shelveOffload', server, None) def unshelve(self, server): """ Unshelve the server. + + :param server: The :class:`Server` (or its ID) to unshelve + :returns: An instance of novaclient.base.TupleWithMeta """ - self._action('unshelve', server, None) + return self._action('unshelve', server, None) def ips(self, server): """ Return IP Addresses associated with the server. Often a cheaper call then getting all the details for a server. + + :param server: The :class:`Server` (or its ID) for which + the IP adresses are to be returned + :returns: An instance of novaclient.base.DictWithMeta """ - _resp, body = self.api.client.get("/servers/%s/ips" % - base.getid(server)) - return body['addresses'] + resp, body = self.api.client.get("/servers/%s/ips" % + base.getid(server)) + return base.DictWithMeta(body['addresses'], resp) def diagnostics(self, server): - """Retrieve server diagnostics.""" - return self.api.client.get("/servers/%s/diagnostics" % - base.getid(server)) + """ + Retrieve server diagnostics. + + :param server: The :class:`Server` (or its ID) for which + diagnostics to be returned + :returns: A Respose object and an instance of + novaclient.base.DictWithMeta + """ + resp, body = self.api.client.get("/servers/%s/diagnostics" % + base.getid(server)) + # For compatibility, return Response object as a first return value + return resp, base.DictWithMeta(body, resp) def create(self, name, image, flavor, meta=None, files=None, reservation_id=None, min_count=None, @@ -1073,14 +1223,21 @@ def update(self, server, name=None): def change_password(self, server, password): """ Update the password for a server. + + :param server: The :class:`Server` (or its ID) for which the admin + password is to be changed + :returns: An instance of novaclient.base.TupleWithMeta """ - self._action("changePassword", server, {"adminPass": password}) + return self._action("changePassword", server, {"adminPass": password}) def delete(self, server): """ Delete (i.e. shut down and delete the image) this server. + + :param server: The :class:`Server` (or its ID) to delete + :returns: An instance of novaclient.base.TupleWithMeta """ - self._delete("/servers/%s" % base.getid(server)) + return self._delete("/servers/%s" % base.getid(server)) def reboot(self, server, reboot_type=REBOOT_SOFT): """ @@ -1089,8 +1246,9 @@ def reboot(self, server, reboot_type=REBOOT_SOFT): :param server: The :class:`Server` (or its ID) to share onto. :param reboot_type: either :data:`REBOOT_SOFT` for a software-level reboot, or `REBOOT_HARD` for a virtual power cycle hard reboot. + :returns: An instance of novaclient.base.TupleWithMeta """ - self._action('reboot', server, {'type': reboot_type}) + return self._action('reboot', server, {'type': reboot_type}) def rebuild(self, server, image, password=None, disk_config=None, preserve_ephemeral=False, name=None, meta=None, files=None, @@ -1113,6 +1271,7 @@ def rebuild(self, server, image, password=None, disk_config=None, are the file contents (either as a string or as a file-like object). A maximum of five entries is allowed, and each file must be 10k or less. + :returns: :class:`Server` """ body = {'imageRef': base.getid(image)} if password is not None: @@ -1140,16 +1299,18 @@ def rebuild(self, server, image, password=None, disk_config=None, 'contents': cont, }) - _resp, body = self._action('rebuild', server, body, **kwargs) - return Server(self, body['server']) + resp, body = self._action_return_resp_and_body('rebuild', server, + body, **kwargs) + return Server(self, body['server'], resp=resp) def migrate(self, server): """ Migrate a server to a new host. :param server: The :class:`Server` (or its ID). + :returns: An instance of novaclient.base.TupleWithMeta """ - self._action('migrate', server) + return self._action('migrate', server) def resize(self, server, flavor, disk_config=None, **kwargs): """ @@ -1159,6 +1320,7 @@ def resize(self, server, flavor, disk_config=None, **kwargs): :param flavor: the :class:`Flavor` (or its ID) to resize to. :param disk_config: partitioning mode to use on the rebuilt server. Valid values are 'AUTO' or 'MANUAL' + :returns: An instance of novaclient.base.TupleWithMeta Until a resize event is confirmed with :meth:`confirm_resize`, the old server will be kept around and you'll be able to roll back to the old @@ -1169,23 +1331,25 @@ def resize(self, server, flavor, disk_config=None, **kwargs): if disk_config is not None: info['OS-DCF:diskConfig'] = disk_config - self._action('resize', server, info=info, **kwargs) + return self._action('resize', server, info=info, **kwargs) def confirm_resize(self, server): """ Confirm that the resize worked, thus removing the original server. :param server: The :class:`Server` (or its ID) to share onto. + :returns: An instance of novaclient.base.TupleWithMeta """ - self._action('confirmResize', server) + return self._action('confirmResize', server) def revert_resize(self, server): """ Revert a previous resize, switching back to the old server. :param server: The :class:`Server` (or its ID) to share onto. + :returns: An instance of novaclient.base.TupleWithMeta """ - self._action('revertResize', server) + return self._action('revertResize', server) def create_image(self, server, image_name, metadata=None): """ @@ -1194,12 +1358,15 @@ def create_image(self, server, image_name, metadata=None): :param server: The :class:`Server` (or its ID) to share onto. :param image_name: Name to give the snapshot image :param metadata: Metadata to give newly-created image entity + :returns: An instance of novaclient.base.StrWithMeta + (The snapshot image's UUID) """ body = {'name': image_name, 'metadata': metadata or {}} - resp = self._action('createImage', server, body)[0] + resp, body = self._action_return_resp_and_body('createImage', server, + body) location = resp.headers['location'] image_uuid = location.split('/')[-1] - return image_uuid + return base.StrWithMeta(image_uuid, resp) def backup(self, server, backup_name, backup_type, rotation): """ @@ -1210,11 +1377,12 @@ def backup(self, server, backup_name, backup_type, rotation): :param backup_type: The backup type, like 'daily' or 'weekly' :param rotation: Int parameter representing how many backups to keep around. + :returns: An instance of novaclient.base.TupleWithMeta """ body = {'name': backup_name, 'backup_type': backup_type, 'rotation': rotation} - self._action('createBackup', server, body) + return self._action('createBackup', server, body) def set_meta(self, server, metadata): """ @@ -1244,19 +1412,29 @@ def get_console_output(self, server, length=None): :param server: The :class:`Server` (or its ID) whose console output you would like to retrieve. :param length: The number of tail loglines you would like to retrieve. + :returns: An instance of novaclient.base.StrWithMeta or + novaclient.base.UnicodeWithMeta """ - return self._action('os-getConsoleOutput', - server, - {'length': length})[1]['output'] + resp, body = self._action_return_resp_and_body('os-getConsoleOutput', + server, + {'length': length}) + return self.convert_into_with_meta(body['output'], resp) def delete_meta(self, server, keys): """ Delete metadata from a server + :param server: The :class:`Server` to add metadata to :param keys: A list of metadata keys to delete from the server + :returns: An instance of novaclient.base.TupleWithMeta """ + result = base.TupleWithMeta((), None) for k in keys: - self._delete("/servers/%s/metadata/%s" % (base.getid(server), k)) + ret = self._delete("/servers/%s/metadata/%s" % + (base.getid(server), k)) + result.append_request_ids(ret.request_ids) + + return result def live_migrate(self, server, host, block_migration, disk_over_commit): """ @@ -1266,12 +1444,12 @@ def live_migrate(self, server, host, block_migration, disk_over_commit): :param host: destination host name. :param block_migration: if True, do block_migration. :param disk_over_commit: if True, Allow overcommit. - + :returns: An instance of novaclient.base.TupleWithMeta """ - self._action('os-migrateLive', server, - {'host': host, - 'block_migration': block_migration, - 'disk_over_commit': disk_over_commit}) + return self._action('os-migrateLive', server, + {'host': host, + 'block_migration': block_migration, + 'disk_over_commit': disk_over_commit}) def reset_state(self, server, state='error'): """ @@ -1280,14 +1458,18 @@ def reset_state(self, server, state='error'): :param server: ID of the instance to reset the state of. :param state: Desired state; either 'active' or 'error'. Defaults to 'error'. + :returns: An instance of novaclient.base.TupleWithMeta """ - self._action('os-resetState', server, dict(state=state)) + return self._action('os-resetState', server, dict(state=state)) def reset_network(self, server): """ Reset network of an instance. + + :param server: The :class:`Server` for network is to be reset + :returns: An instance of novaclient.base.TupleWithMeta """ - self._action('resetNetwork', server) + return self._action('resetNetwork', server) def add_security_group(self, server, security_group): """ @@ -1295,9 +1477,10 @@ def add_security_group(self, server, security_group): :param server: ID of the instance. :param security_group: The name of security group to add. - + :returns: An instance of novaclient.base.DictWithMeta """ - self._action('addSecurityGroup', server, {'name': security_group}) + return self._action('addSecurityGroup', server, + {'name': security_group}) def remove_security_group(self, server, security_group): """ @@ -1305,9 +1488,10 @@ def remove_security_group(self, server, security_group): :param server: ID of the instance. :param security_group: The name of security group to remove. - + :returns: An instance of novaclient.base.TupleWithMeta """ - self._action('removeSecurityGroup', server, {'name': security_group}) + return self._action('removeSecurityGroup', server, + {'name': security_group}) def list_security_group(self, server): """ @@ -1331,6 +1515,8 @@ def evacuate(self, server, host=None, on_shared_storage=True, :param on_shared_storage: Specifies whether instance files located on shared storage :param password: string to set as password on the evacuated server. + :returns: A Response object and an instance of + novaclient.base.TupleWithMeta """ body = {'onSharedStorage': on_shared_storage} @@ -1340,7 +1526,9 @@ def evacuate(self, server, host=None, on_shared_storage=True, if password is not None: body['adminPass'] = password - return self._action('evacuate', server, body) + resp, body = self._action_return_resp_and_body('evacuate', server, + body) + return resp, self.convert_into_with_meta(body, resp) @api_versions.wraps("2.14") def evacuate(self, server, host=None, password=None): @@ -1359,7 +1547,9 @@ def evacuate(self, server, host=None, password=None): if password is not None: body['adminPass'] = password - return self._action('evacuate', server, body) + resp, body = self._action_return_resp_and_body('evacuate', server, + body) + return resp, self.convert_into_with_meta(body, resp) def interface_list(self, server): """ @@ -1396,9 +1586,10 @@ def interface_detach(self, server, port_id): :param server: The :class:`Server` (or its ID) to detach from. :param port_id: The port to detach. + :returns: An instance of novaclient.base.TupleWithMeta """ - self._delete('/servers/%s/os-interface/%s' % (base.getid(server), - port_id)) + return self._delete('/servers/%s/os-interface/%s' % + (base.getid(server), port_id)) @api_versions.wraps("2.17") def trigger_crash_dump(self, server): @@ -1409,6 +1600,15 @@ def _action(self, action, server, info=None, **kwargs): """ Perform a server "action" -- reboot/rebuild/resize/etc. """ + resp, body = self._action_return_resp_and_body(action, server, + info=info, **kwargs) + return self.convert_into_with_meta(body, resp) + + def _action_return_resp_and_body(self, action, server, info=None, + **kwargs): + """ + Perform a server "action" and return response headers and body + """ body = {action: info} self.run_hooks('modify_body_for_action', body, **kwargs) url = '/servers/%s/action' % base.getid(server) @@ -1420,4 +1620,5 @@ def _console(self, server, info=None, **kwargs): """ body = {'remote_console': info} url = '/servers/%s/remote-consoles' % base.getid(server) - return self.api.client.post(url, body=body) + resp, body = self.api.client.post(url, body=body) + return self.convert_into_with_meta(body, resp) From f194a5abadfaf996c305e9c12aabdd53443773fb Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Fri, 25 Dec 2015 11:55:05 +0900 Subject: [PATCH 0966/1705] Add return-request-id-to-caller function(2/5) Add return-request-id-to-caller function to resources and resource managers in the following files. The methods in the resource class and resource manager return a wrapper class that has 'request_ids' property. The caller can get request ids of the callee via the property. * novaclient/v2/agents.py * novaclient/v2/aggregates.py * novaclient/v2/availability_zones.py * novaclient/v2/certs.py * novaclient/v2/cloudpipe.py * novaclient/v2/fixed_ips.py * novaclient/v2/flavor_access.py * novaclient/v2/flavors.py * novaclient/v2/floating_ip_dns.py * novaclient/v2/floating_ip_pools.py * novaclient/v2/floating_ips_bulk.py * novaclient/v2/floating_ips.py * novaclient/v2/fping.py * novaclient/v2/hosts.py * novaclient/v2/hypervisors.py * novaclient/v2/images.py Co-authored-by: Ankit Agrawal Change-Id: Ic624d532f1a468dd45105bbb5aa6d8a730984338 Implements: blueprint return-request-id-to-caller --- .../tests/unit/fixture_data/aggregates.py | 3 +- .../tests/unit/fixture_data/floatingips.py | 5 +- novaclient/tests/unit/fixture_data/hosts.py | 2 +- .../tests/unit/fixture_data/hypervisors.py | 2 +- novaclient/tests/unit/fixture_data/images.py | 5 +- novaclient/tests/unit/v2/fakes.py | 25 ++++---- novaclient/tests/unit/v2/test_agents.py | 11 +++- novaclient/tests/unit/v2/test_aggregates.py | 28 ++++++++- .../tests/unit/v2/test_availability_zone.py | 3 + novaclient/tests/unit/v2/test_certs.py | 3 + novaclient/tests/unit/v2/test_cloudpipe.py | 6 +- novaclient/tests/unit/v2/test_fixed_ips.py | 8 ++- .../tests/unit/v2/test_flavor_access.py | 3 + novaclient/tests/unit/v2/test_flavors.py | 40 ++++++++++--- .../tests/unit/v2/test_floating_ip_dns.py | 25 +++++--- .../tests/unit/v2/test_floating_ip_pools.py | 2 + novaclient/tests/unit/v2/test_floating_ips.py | 13 +++- .../tests/unit/v2/test_floating_ips_bulk.py | 6 ++ novaclient/tests/unit/v2/test_fping.py | 7 +++ novaclient/tests/unit/v2/test_hosts.py | 16 ++++- novaclient/tests/unit/v2/test_hypervisors.py | 9 +++ novaclient/tests/unit/v2/test_images.py | 18 ++++-- novaclient/v2/agents.py | 9 ++- novaclient/v2/aggregates.py | 16 ++++- novaclient/v2/cloudpipe.py | 10 +++- novaclient/v2/fixed_ips.py | 12 ++-- novaclient/v2/flavor_access.py | 8 ++- novaclient/v2/flavors.py | 28 ++++++--- novaclient/v2/floating_ip_dns.py | 59 +++++++++++++++---- novaclient/v2/floating_ips.py | 11 +++- novaclient/v2/hosts.py | 13 +++- novaclient/v2/images.py | 15 ++++- 32 files changed, 328 insertions(+), 93 deletions(-) diff --git a/novaclient/tests/unit/fixture_data/aggregates.py b/novaclient/tests/unit/fixture_data/aggregates.py index ed743ed1f..96c3d2072 100644 --- a/novaclient/tests/unit/fixture_data/aggregates.py +++ b/novaclient/tests/unit/fixture_data/aggregates.py @@ -49,4 +49,5 @@ def setUp(self): json=get_aggregates_1, headers=self.json_headers) - self.requests.register_uri('DELETE', self.url(1), status_code=202) + self.requests.register_uri('DELETE', self.url(1), status_code=202, + headers=self.json_headers) diff --git a/novaclient/tests/unit/fixture_data/floatingips.py b/novaclient/tests/unit/fixture_data/floatingips.py index f5ad1c10f..6ddc9e360 100644 --- a/novaclient/tests/unit/fixture_data/floatingips.py +++ b/novaclient/tests/unit/fixture_data/floatingips.py @@ -83,10 +83,11 @@ def setUp(self): headers=self.json_headers, status_code=205) - self.requests.register_uri('DELETE', self.url('testdomain')) + self.requests.register_uri('DELETE', self.url('testdomain'), + headers=self.json_headers) url = self.url('testdomain', 'entries', 'testname') - self.requests.register_uri('DELETE', url) + self.requests.register_uri('DELETE', url, headers=self.json_headers) def put_dns_testdomain_entries_testname(request, context): body = jsonutils.loads(request.body) diff --git a/novaclient/tests/unit/fixture_data/hosts.py b/novaclient/tests/unit/fixture_data/hosts.py index 160eb3989..7c9b3be80 100644 --- a/novaclient/tests/unit/fixture_data/hosts.py +++ b/novaclient/tests/unit/fixture_data/hosts.py @@ -36,7 +36,7 @@ def setUp(self): ] } - headers = {'Content-Type': 'application/json'} + headers = self.json_headers self.requests.register_uri('GET', self.url('host'), json=get_os_hosts_host, diff --git a/novaclient/tests/unit/fixture_data/hypervisors.py b/novaclient/tests/unit/fixture_data/hypervisors.py index 62952f081..229dfd754 100644 --- a/novaclient/tests/unit/fixture_data/hypervisors.py +++ b/novaclient/tests/unit/fixture_data/hypervisors.py @@ -27,7 +27,7 @@ def setUp(self): ] } - self.headers = {'Content-Type': 'application/json'} + self.headers = self.json_headers self.requests.register_uri('GET', self.url(), json=get_os_hypervisors, diff --git a/novaclient/tests/unit/fixture_data/images.py b/novaclient/tests/unit/fixture_data/images.py index 0eb8a14f1..9498b8e32 100644 --- a/novaclient/tests/unit/fixture_data/images.py +++ b/novaclient/tests/unit/fixture_data/images.py @@ -30,7 +30,7 @@ def setUp(self): ] } - headers = {'Content-Type': 'application/json'} + headers = self.json_headers self.requests.register_uri('GET', self.url(), json=get_images, @@ -78,7 +78,8 @@ def post_images_1_metadata(request, context): headers=headers) for u in (1, '1/metadata/test_key'): - self.requests.register_uri('DELETE', self.url(u), status_code=204) + self.requests.register_uri('DELETE', self.url(u), status_code=204, + headers=headers) class V3(V1): diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index ceb6672ec..668ee95c3 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -809,7 +809,7 @@ def get_flavors(self, **kw): if k not in ['id', 'name']: del flavor[k] - return (200, {}, flavors) + return (200, FAKE_RESPONSE_HEADERS, flavors) def get_flavors_detail(self, **kw): flavors = {'flavors': [ @@ -852,12 +852,12 @@ def get_flavors_detail(self, **kw): if not v['os-flavor-access:is_public'] ] - return (200, {}, flavors) + return (200, FAKE_RESPONSE_HEADERS, flavors) def get_flavors_1(self, **kw): return ( 200, - {}, + FAKE_RESPONSE_HEADERS, {'flavor': self.get_flavors_detail(is_public='None')[2]['flavors'][0]} ) @@ -874,7 +874,7 @@ def get_flavors_3(self, **kw): # Diablo has no ephemeral return ( 200, - {}, + FAKE_RESPONSE_HEADERS, {'flavor': { 'id': 3, 'name': '256 MB Server', @@ -896,7 +896,7 @@ def get_flavors_aa1(self, **kw): # Alphanumeric flavor id are allowed. return ( 200, - {}, + FAKE_RESPONSE_HEADERS, {'flavor': self.get_flavors_detail(is_public='None')[2]['flavors'][3]} ) @@ -910,15 +910,15 @@ def get_flavors_4(self, **kw): ) def delete_flavors_flavordelete(self, **kw): - return (202, {}, None) + return (202, FAKE_RESPONSE_HEADERS, None) def delete_flavors_2(self, **kw): - return (202, {}, None) + return (202, FAKE_RESPONSE_HEADERS, None) def post_flavors(self, body, **kw): return ( 202, - {}, + FAKE_RESPONSE_HEADERS, {'flavor': self.get_flavors_detail(is_public='None')[2]['flavors'][0]} ) @@ -952,7 +952,7 @@ def post_flavors_1_os_extra_specs(self, body, **kw): required=['k1']) return ( 200, - {}, + FAKE_RESPONSE_HEADERS, {'extra_specs': {"k1": "v1"}}) def post_flavors_4_os_extra_specs(self, body, **kw): @@ -960,7 +960,7 @@ def post_flavors_4_os_extra_specs(self, body, **kw): return ( 200, - {}, + FAKE_RESPONSE_HEADERS, body) def delete_flavors_1_os_extra_specs_k1(self, **kw): @@ -975,12 +975,13 @@ def get_flavors_1_os_flavor_access(self, **kw): def get_flavors_2_os_flavor_access(self, **kw): return ( - 200, {}, + 200, FAKE_RESPONSE_HEADERS, {'flavor_access': [{'flavor_id': '2', 'tenant_id': 'proj1'}, {'flavor_id': '2', 'tenant_id': 'proj2'}]}) def post_flavors_2_action(self, body, **kw): - return (202, {}, self.get_flavors_2_os_flavor_access()[2]) + return (202, FAKE_RESPONSE_HEADERS, + self.get_flavors_2_os_flavor_access()[2]) # # Floating IPs diff --git a/novaclient/tests/unit/v2/test_agents.py b/novaclient/tests/unit/v2/test_agents.py index e7fa7d6ed..8cf67c1a6 100644 --- a/novaclient/tests/unit/v2/test_agents.py +++ b/novaclient/tests/unit/v2/test_agents.py @@ -16,6 +16,7 @@ from novaclient.tests.unit.fixture_data import agents as data from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit import utils +from novaclient.tests.unit.v2 import fakes from novaclient.v2 import agents @@ -50,7 +51,8 @@ def stub_hypervisors(self, hypervisor='kvm'): ] } - headers = {'Content-Type': 'application/json'} + headers = {'Content-Type': 'application/json', + 'x-openstack-request-id': fakes.FAKE_REQUEST_ID} self.requests.register_uri('GET', self.data_fixture.url(), json=get_os_agents, headers=headers) @@ -59,6 +61,7 @@ def test_list_agents(self): self.stub_hypervisors() ags = self.cs.agents.list() self.assert_called('GET', '/os-agents') + self.assert_request_id(ags, fakes.FAKE_REQUEST_ID_LIST) for a in ags: self.assertIsInstance(a, agents.Agent) self.assertEqual('kvm', a.hypervisor) @@ -67,6 +70,7 @@ def test_list_agents_with_hypervisor(self): self.stub_hypervisors('xen') ags = self.cs.agents.list('xen') self.assert_called('GET', '/os-agents?hypervisor=xen') + self.assert_request_id(ags, fakes.FAKE_REQUEST_ID_LIST) for a in ags: self.assertIsInstance(a, agents.Agent) self.assertEqual('xen', a.hypervisor) @@ -76,6 +80,7 @@ def test_agents_create(self): '/xxx/xxx/xxx', 'add6bb58e139be103324d04d82d8f546', 'xen') + self.assert_request_id(ag, fakes.FAKE_REQUEST_ID_LIST) body = {'agent': {'url': '/xxx/xxx/xxx', 'hypervisor': 'xen', 'md5hash': 'add6bb58e139be103324d04d82d8f546', @@ -86,7 +91,8 @@ def test_agents_create(self): self.assertEqual(1, ag._info.copy()['id']) def test_agents_delete(self): - self.cs.agents.delete('1') + ret = self.cs.agents.delete('1') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('DELETE', '/os-agents/1') def _build_example_update_body(self): @@ -99,6 +105,7 @@ def test_agents_modify(self): ag = self.cs.agents.update('1', '8.0', '/yyy/yyyy/yyyy', 'add6bb58e139be103324d04d82d8f546') + self.assert_request_id(ag, fakes.FAKE_REQUEST_ID_LIST) body = self._build_example_update_body() self.assert_called('PUT', '/os-agents/1', body) self.assertEqual(1, ag.id) diff --git a/novaclient/tests/unit/v2/test_aggregates.py b/novaclient/tests/unit/v2/test_aggregates.py index 97a3ffc2b..1de128238 100644 --- a/novaclient/tests/unit/v2/test_aggregates.py +++ b/novaclient/tests/unit/v2/test_aggregates.py @@ -16,6 +16,7 @@ from novaclient.tests.unit.fixture_data import aggregates as data from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit import utils +from novaclient.tests.unit.v2 import fakes from novaclient.v2 import aggregates @@ -28,6 +29,7 @@ class AggregatesTest(utils.FixturedTestCase): def test_list_aggregates(self): result = self.cs.aggregates.list() + self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/os-aggregates') for aggregate in result: self.assertIsInstance(aggregate, aggregates.Aggregate) @@ -35,24 +37,29 @@ def test_list_aggregates(self): def test_create_aggregate(self): body = {"aggregate": {"name": "test", "availability_zone": "nova1"}} aggregate = self.cs.aggregates.create("test", "nova1") + self.assert_request_id(aggregate, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/os-aggregates', body) self.assertIsInstance(aggregate, aggregates.Aggregate) def test_get(self): aggregate = self.cs.aggregates.get("1") + self.assert_request_id(aggregate, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/os-aggregates/1') self.assertIsInstance(aggregate, aggregates.Aggregate) aggregate2 = self.cs.aggregates.get(aggregate) + self.assert_request_id(aggregate2, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/os-aggregates/1') self.assertIsInstance(aggregate2, aggregates.Aggregate) def test_get_details(self): aggregate = self.cs.aggregates.get_details("1") + self.assert_request_id(aggregate, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/os-aggregates/1') self.assertIsInstance(aggregate, aggregates.Aggregate) aggregate2 = self.cs.aggregates.get_details(aggregate) + self.assert_request_id(aggregate2, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/os-aggregates/1') self.assertIsInstance(aggregate2, aggregates.Aggregate) @@ -62,10 +69,12 @@ def test_update(self): body = {"aggregate": values} result1 = aggregate.update(values) + self.assert_request_id(result1, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('PUT', '/os-aggregates/1', body) self.assertIsInstance(result1, aggregates.Aggregate) result2 = self.cs.aggregates.update(2, values) + self.assert_request_id(result2, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('PUT', '/os-aggregates/2', body) self.assertIsInstance(result2, aggregates.Aggregate) @@ -75,6 +84,7 @@ def test_update_with_availability_zone(self): body = {"aggregate": values} result3 = self.cs.aggregates.update(aggregate, values) + self.assert_request_id(result3, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('PUT', '/os-aggregates/1', body) self.assertIsInstance(result3, aggregates.Aggregate) @@ -84,14 +94,17 @@ def test_add_host(self): body = {"add_host": {"host": "host1"}} result1 = aggregate.add_host(host) + self.assert_request_id(result1, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/os-aggregates/1/action', body) self.assertIsInstance(result1, aggregates.Aggregate) result2 = self.cs.aggregates.add_host("2", host) + self.assert_request_id(result2, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/os-aggregates/2/action', body) self.assertIsInstance(result2, aggregates.Aggregate) result3 = self.cs.aggregates.add_host(aggregate, host) + self.assert_request_id(result3, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/os-aggregates/1/action', body) self.assertIsInstance(result3, aggregates.Aggregate) @@ -101,14 +114,17 @@ def test_remove_host(self): body = {"remove_host": {"host": "host1"}} result1 = aggregate.remove_host(host) + self.assert_request_id(result1, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/os-aggregates/1/action', body) self.assertIsInstance(result1, aggregates.Aggregate) result2 = self.cs.aggregates.remove_host("2", host) + self.assert_request_id(result2, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/os-aggregates/2/action', body) self.assertIsInstance(result2, aggregates.Aggregate) result3 = self.cs.aggregates.remove_host(aggregate, host) + self.assert_request_id(result3, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/os-aggregates/1/action', body) self.assertIsInstance(result3, aggregates.Aggregate) @@ -118,24 +134,30 @@ def test_set_metadata(self): body = {"set_metadata": {"metadata": metadata}} result1 = aggregate.set_metadata(metadata) + self.assert_request_id(result1, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/os-aggregates/1/action', body) self.assertIsInstance(result1, aggregates.Aggregate) result2 = self.cs.aggregates.set_metadata(2, metadata) + self.assert_request_id(result2, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/os-aggregates/2/action', body) self.assertIsInstance(result2, aggregates.Aggregate) result3 = self.cs.aggregates.set_metadata(aggregate, metadata) + self.assert_request_id(result3, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/os-aggregates/1/action', body) self.assertIsInstance(result3, aggregates.Aggregate) def test_delete_aggregate(self): aggregate = self.cs.aggregates.list()[0] - aggregate.delete() + result1 = aggregate.delete() + self.assert_request_id(result1, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('DELETE', '/os-aggregates/1') - self.cs.aggregates.delete('1') + result2 = self.cs.aggregates.delete('1') + self.assert_request_id(result2, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('DELETE', '/os-aggregates/1') - self.cs.aggregates.delete(aggregate) + result3 = self.cs.aggregates.delete(aggregate) + self.assert_request_id(result3, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('DELETE', '/os-aggregates/1') diff --git a/novaclient/tests/unit/v2/test_availability_zone.py b/novaclient/tests/unit/v2/test_availability_zone.py index a7d6d82e2..ac2bf0f2b 100644 --- a/novaclient/tests/unit/v2/test_availability_zone.py +++ b/novaclient/tests/unit/v2/test_availability_zone.py @@ -19,6 +19,7 @@ from novaclient.tests.unit.fixture_data import availability_zones as data from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit import utils +from novaclient.tests.unit.v2 import fakes from novaclient.v2 import availability_zones @@ -45,6 +46,7 @@ def _assertZone(self, zone, name, status): def test_list_availability_zone(self): zones = self.cs.availability_zones.list(detailed=False) + self.assert_request_id(zones, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/os-availability-zone') for zone in zones: @@ -65,6 +67,7 @@ def test_list_availability_zone(self): def test_detail_availability_zone(self): zones = self.cs.availability_zones.list(detailed=True) + self.assert_request_id(zones, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/os-availability-zone/detail') for zone in zones: diff --git a/novaclient/tests/unit/v2/test_certs.py b/novaclient/tests/unit/v2/test_certs.py index 6ec2258fc..d0e899586 100644 --- a/novaclient/tests/unit/v2/test_certs.py +++ b/novaclient/tests/unit/v2/test_certs.py @@ -14,6 +14,7 @@ from novaclient.tests.unit.fixture_data import certs as data from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit import utils +from novaclient.tests.unit.v2 import fakes from novaclient.v2 import certs @@ -27,10 +28,12 @@ class CertsTest(utils.FixturedTestCase): def test_create_cert(self): cert = self.cs.certs.create() + self.assert_request_id(cert, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/os-certificates') self.assertIsInstance(cert, self.cert_type) def test_get_root_cert(self): cert = self.cs.certs.get() + self.assert_request_id(cert, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/os-certificates/root') self.assertIsInstance(cert, self.cert_type) diff --git a/novaclient/tests/unit/v2/test_cloudpipe.py b/novaclient/tests/unit/v2/test_cloudpipe.py index 59b18c362..10a44d7c0 100644 --- a/novaclient/tests/unit/v2/test_cloudpipe.py +++ b/novaclient/tests/unit/v2/test_cloudpipe.py @@ -16,6 +16,7 @@ from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit.fixture_data import cloudpipe as data from novaclient.tests.unit import utils +from novaclient.tests.unit.v2 import fakes from novaclient.v2 import cloudpipe @@ -28,6 +29,7 @@ class CloudpipeTest(utils.FixturedTestCase): def test_list_cloudpipes(self): cp = self.cs.cloudpipe.list() + self.assert_request_id(cp, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/os-cloudpipe') for c in cp: self.assertIsInstance(c, cloudpipe.Cloudpipe) @@ -35,12 +37,14 @@ def test_list_cloudpipes(self): def test_create(self): project = "test" cp = self.cs.cloudpipe.create(project) + self.assert_request_id(cp, fakes.FAKE_REQUEST_ID_LIST) body = {'cloudpipe': {'project_id': project}} self.assert_called('POST', '/os-cloudpipe', body) self.assertIsInstance(cp, six.string_types) def test_update(self): - self.cs.cloudpipe.update("192.168.1.1", 2345) + cp = self.cs.cloudpipe.update("192.168.1.1", 2345) + self.assert_request_id(cp, fakes.FAKE_REQUEST_ID_LIST) body = {'configure_project': {'vpn_ip': "192.168.1.1", 'vpn_port': 2345}} self.assert_called('PUT', '/os-cloudpipe/configure-project', body) diff --git a/novaclient/tests/unit/v2/test_fixed_ips.py b/novaclient/tests/unit/v2/test_fixed_ips.py index 02ba39b6d..d7d74bf26 100644 --- a/novaclient/tests/unit/v2/test_fixed_ips.py +++ b/novaclient/tests/unit/v2/test_fixed_ips.py @@ -16,6 +16,7 @@ from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit.fixture_data import fixedips as data from novaclient.tests.unit import utils +from novaclient.tests.unit.v2 import fakes class FixedIpsTest(utils.FixturedTestCase): @@ -27,6 +28,7 @@ class FixedIpsTest(utils.FixturedTestCase): def test_get_fixed_ip(self): info = self.cs.fixed_ips.get(fixed_ip='192.168.1.1') + self.assert_request_id(info, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/os-fixed-ips/192.168.1.1') self.assertEqual('192.168.1.0/24', info.cidr) self.assertEqual('192.168.1.1', info.address) @@ -35,10 +37,12 @@ def test_get_fixed_ip(self): def test_reserve_fixed_ip(self): body = {"reserve": None} - self.cs.fixed_ips.reserve(fixed_ip='192.168.1.1') + fixedip = self.cs.fixed_ips.reserve(fixed_ip='192.168.1.1') + self.assert_request_id(fixedip, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/os-fixed-ips/192.168.1.1/action', body) def test_unreserve_fixed_ip(self): body = {"unreserve": None} - self.cs.fixed_ips.unreserve(fixed_ip='192.168.1.1') + fixedip = self.cs.fixed_ips.unreserve(fixed_ip='192.168.1.1') + self.assert_request_id(fixedip, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/os-fixed-ips/192.168.1.1/action', body) diff --git a/novaclient/tests/unit/v2/test_flavor_access.py b/novaclient/tests/unit/v2/test_flavor_access.py index 7d1bca18c..c6cafdf86 100644 --- a/novaclient/tests/unit/v2/test_flavor_access.py +++ b/novaclient/tests/unit/v2/test_flavor_access.py @@ -26,6 +26,7 @@ class FlavorAccessTest(utils.TestCase): def test_list_access_by_flavor_private(self): kwargs = {'flavor': cs.flavors.get(2)} r = cs.flavor_access.list(**kwargs) + self.assert_request_id(r, fakes.FAKE_REQUEST_ID_LIST) cs.assert_called('GET', '/flavors/2/os-flavor-access') for a in r: self.assertIsInstance(a, flavor_access.FlavorAccess) @@ -34,6 +35,7 @@ def test_add_tenant_access(self): flavor = cs.flavors.get(2) tenant = 'proj2' r = cs.flavor_access.add_tenant_access(flavor, tenant) + self.assert_request_id(r, fakes.FAKE_REQUEST_ID_LIST) body = { "addTenantAccess": { @@ -49,6 +51,7 @@ def test_remove_tenant_access(self): flavor = cs.flavors.get(2) tenant = 'proj2' r = cs.flavor_access.remove_tenant_access(flavor, tenant) + self.assert_request_id(r, fakes.FAKE_REQUEST_ID_LIST) body = { "removeTenantAccess": { diff --git a/novaclient/tests/unit/v2/test_flavors.py b/novaclient/tests/unit/v2/test_flavors.py index ff564b265..70eb8e40a 100644 --- a/novaclient/tests/unit/v2/test_flavors.py +++ b/novaclient/tests/unit/v2/test_flavors.py @@ -15,6 +15,7 @@ import mock +from novaclient import base from novaclient import exceptions from novaclient.tests.unit import utils from novaclient.tests.unit.v2 import fakes @@ -35,45 +36,53 @@ def _get_flavor_type(self): def test_list_flavors(self): fl = self.cs.flavors.list() + self.assert_request_id(fl, fakes.FAKE_REQUEST_ID_LIST) self.cs.assert_called('GET', '/flavors/detail') for flavor in fl: self.assertIsInstance(flavor, self.flavor_type) def test_list_flavors_undetailed(self): fl = self.cs.flavors.list(detailed=False) + self.assert_request_id(fl, fakes.FAKE_REQUEST_ID_LIST) self.cs.assert_called('GET', '/flavors') for flavor in fl: self.assertIsInstance(flavor, self.flavor_type) def test_list_flavors_with_marker_limit(self): - self.cs.flavors.list(marker=1234, limit=4) + fl = self.cs.flavors.list(marker=1234, limit=4) + self.assert_request_id(fl, fakes.FAKE_REQUEST_ID_LIST) self.cs.assert_called('GET', '/flavors/detail?limit=4&marker=1234') def test_list_flavors_with_sort_key_dir(self): - self.cs.flavors.list(sort_key='id', sort_dir='asc') + fl = self.cs.flavors.list(sort_key='id', sort_dir='asc') + self.assert_request_id(fl, fakes.FAKE_REQUEST_ID_LIST) self.cs.assert_called('GET', '/flavors/detail?sort_dir=asc&sort_key=id') def test_list_flavors_is_public_none(self): fl = self.cs.flavors.list(is_public=None) + self.assert_request_id(fl, fakes.FAKE_REQUEST_ID_LIST) self.cs.assert_called('GET', '/flavors/detail?is_public=None') for flavor in fl: self.assertIsInstance(flavor, self.flavor_type) def test_list_flavors_is_public_false(self): fl = self.cs.flavors.list(is_public=False) + self.assert_request_id(fl, fakes.FAKE_REQUEST_ID_LIST) self.cs.assert_called('GET', '/flavors/detail?is_public=False') for flavor in fl: self.assertIsInstance(flavor, self.flavor_type) def test_list_flavors_is_public_true(self): fl = self.cs.flavors.list(is_public=True) + self.assert_request_id(fl, fakes.FAKE_REQUEST_ID_LIST) self.cs.assert_called('GET', '/flavors/detail') for flavor in fl: self.assertIsInstance(flavor, self.flavor_type) def test_get_flavor_details(self): f = self.cs.flavors.get(1) + self.assert_request_id(f, fakes.FAKE_REQUEST_ID_LIST) self.cs.assert_called('GET', '/flavors/1') self.assertIsInstance(f, self.flavor_type) self.assertEqual(256, f.ram) @@ -83,6 +92,7 @@ def test_get_flavor_details(self): def test_get_flavor_details_alphanum_id(self): f = self.cs.flavors.get('aa1') + self.assert_request_id(f, fakes.FAKE_REQUEST_ID_LIST) self.cs.assert_called('GET', '/flavors/aa1') self.assertIsInstance(f, self.flavor_type) self.assertEqual(128, f.ram) @@ -92,6 +102,7 @@ def test_get_flavor_details_alphanum_id(self): def test_get_flavor_details_diablo(self): f = self.cs.flavors.get(3) + self.assert_request_id(f, fakes.FAKE_REQUEST_ID_LIST) self.cs.assert_called('GET', '/flavors/3') self.assertIsInstance(f, self.flavor_type) self.assertEqual(256, f.ram) @@ -101,10 +112,12 @@ def test_get_flavor_details_diablo(self): def test_find(self): f = self.cs.flavors.find(ram=256) + self.assert_request_id(f, fakes.FAKE_REQUEST_ID_LIST) self.cs.assert_called('GET', '/flavors/detail') self.assertEqual('256 MB Server', f.name) f = self.cs.flavors.find(disk=0) + self.assert_request_id(f, fakes.FAKE_REQUEST_ID_LIST) self.assertEqual('128 MB Server', f.name) self.assertRaises(exceptions.NotFound, self.cs.flavors.find, @@ -129,6 +142,7 @@ def _create_body(self, name, ram, vcpus, disk, ephemeral, id, swap, def test_create(self): f = self.cs.flavors.create("flavorcreate", 512, 1, 10, 1234, ephemeral=10, is_public=False) + self.assert_request_id(f, fakes.FAKE_REQUEST_ID_LIST) body = self._create_body("flavorcreate", 512, 1, 10, 10, 1234, 0, 1.0, False) @@ -141,6 +155,7 @@ def test_create_with_id_as_string(self): f = self.cs.flavors.create("flavorcreate", 512, 1, 10, flavor_id, ephemeral=10, is_public=False) + self.assert_request_id(f, fakes.FAKE_REQUEST_ID_LIST) body = self._create_body("flavorcreate", 512, 1, 10, 10, flavor_id, 0, 1.0, False) @@ -150,6 +165,7 @@ def test_create_with_id_as_string(self): def test_create_ephemeral_ispublic_defaults(self): f = self.cs.flavors.create("flavorcreate", 512, 1, 10, 1234) + self.assert_request_id(f, fakes.FAKE_REQUEST_ID_LIST) body = self._create_body("flavorcreate", 512, 1, 10, 0, 1234, 0, 1.0, True) @@ -181,22 +197,26 @@ def test_invalid_parameters_create(self): ephemeral=0, rxtx_factor=1.0, is_public='invalid') def test_delete(self): - self.cs.flavors.delete("flavordelete") + ret = self.cs.flavors.delete("flavordelete") + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.cs.assert_called('DELETE', '/flavors/flavordelete') def test_delete_with_flavor_instance(self): f = self.cs.flavors.get(2) - self.cs.flavors.delete(f) + ret = self.cs.flavors.delete(f) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.cs.assert_called('DELETE', '/flavors/2') def test_delete_with_flavor_instance_method(self): f = self.cs.flavors.get(2) - f.delete() + ret = f.delete() + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.cs.assert_called('DELETE', '/flavors/2') def test_set_keys(self): f = self.cs.flavors.get(1) - f.set_keys({'k1': 'v1'}) + fk = f.set_keys({'k1': 'v1'}) + self.assert_request_id(fk, fakes.FAKE_REQUEST_ID_LIST) self.cs.assert_called('POST', '/flavors/1/os-extra_specs', {"extra_specs": {'k1': 'v1'}}) @@ -206,7 +226,8 @@ def test_set_with_valid_keys(self): f = self.cs.flavors.get(4) for key in valid_keys: - f.set_keys({key: 'v4'}) + fk = f.set_keys({key: 'v4'}) + self.assert_request_id(fk, fakes.FAKE_REQUEST_ID_LIST) self.cs.assert_called('POST', '/flavors/4/os-extra_specs', {"extra_specs": {key: 'v4'}}) @@ -221,7 +242,10 @@ def test_set_with_invalid_keys(self): def test_unset_keys(self, mock_delete): f = self.cs.flavors.get(1) keys = ['k1', 'k2'] - f.unset_keys(keys) + mock_delete.return_value = base.TupleWithMeta( + (), fakes.FAKE_REQUEST_ID_LIST) + fu = f.unset_keys(keys) + self.assert_request_id(fu, fakes.FAKE_REQUEST_ID_LIST) mock_delete.assert_has_calls([ mock.call("/flavors/1/os-extra_specs/k1"), mock.call("/flavors/1/os-extra_specs/k2") diff --git a/novaclient/tests/unit/v2/test_floating_ip_dns.py b/novaclient/tests/unit/v2/test_floating_ip_dns.py index ec20e56c5..8d9f912ea 100644 --- a/novaclient/tests/unit/v2/test_floating_ip_dns.py +++ b/novaclient/tests/unit/v2/test_floating_ip_dns.py @@ -14,6 +14,7 @@ from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit.fixture_data import floatingips as data from novaclient.tests.unit import utils +from novaclient.tests.unit.v2 import fakes from novaclient.v2 import floating_ip_dns @@ -25,6 +26,7 @@ class FloatingIPDNSDomainTest(utils.FixturedTestCase): def test_dns_domains(self): domainlist = self.cs.dns_domains.domains() + self.assert_request_id(domainlist, fakes.FAKE_REQUEST_ID_LIST) self.assertEqual(2, len(domainlist)) for entry in domainlist: @@ -34,17 +36,20 @@ def test_dns_domains(self): self.assertEqual('example.com', domainlist[1].domain) def test_create_private_domain(self): - self.cs.dns_domains.create_private(self.testdomain, 'test_avzone') + pd = self.cs.dns_domains.create_private(self.testdomain, 'test_avzone') + self.assert_request_id(pd, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('PUT', '/os-floating-ip-dns/%s' % self.testdomain) def test_create_public_domain(self): - self.cs.dns_domains.create_public(self.testdomain, 'test_project') + pd = self.cs.dns_domains.create_public(self.testdomain, 'test_project') + self.assert_request_id(pd, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('PUT', '/os-floating-ip-dns/%s' % self.testdomain) def test_delete_domain(self): - self.cs.dns_domains.delete(self.testdomain) + ret = self.cs.dns_domains.delete(self.testdomain) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('DELETE', '/os-floating-ip-dns/%s' % self.testdomain) @@ -61,6 +66,7 @@ class FloatingIPDNSEntryTest(utils.FixturedTestCase): def test_get_dns_entries_by_ip(self): entries = self.cs.dns_entries.get_for_ip(self.testdomain, ip=self.testip) + self.assert_request_id(entries, fakes.FAKE_REQUEST_ID_LIST) self.assertEqual(2, len(entries)) for entry in entries: @@ -73,19 +79,22 @@ def test_get_dns_entries_by_ip(self): def test_get_dns_entry_by_name(self): entry = self.cs.dns_entries.get(self.testdomain, self.testname) + self.assert_request_id(entry, fakes.FAKE_REQUEST_ID_LIST) self.assertIsInstance(entry, floating_ip_dns.FloatingIPDNSEntry) self.assertEqual(entry.name, self.testname) def test_create_entry(self): - self.cs.dns_entries.create(self.testdomain, - self.testname, - self.testip, - self.testtype) + entry = self.cs.dns_entries.create(self.testdomain, + self.testname, + self.testip, + self.testtype) + self.assert_request_id(entry, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('PUT', '/os-floating-ip-dns/%s/entries/%s' % (self.testdomain, self.testname)) def test_delete_entry(self): - self.cs.dns_entries.delete(self.testdomain, self.testname) + ret = self.cs.dns_entries.delete(self.testdomain, self.testname) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('DELETE', '/os-floating-ip-dns/%s/entries/%s' % (self.testdomain, self.testname)) diff --git a/novaclient/tests/unit/v2/test_floating_ip_pools.py b/novaclient/tests/unit/v2/test_floating_ip_pools.py index a138fc285..7eac3df5b 100644 --- a/novaclient/tests/unit/v2/test_floating_ip_pools.py +++ b/novaclient/tests/unit/v2/test_floating_ip_pools.py @@ -17,6 +17,7 @@ from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit.fixture_data import floatingips as data from novaclient.tests.unit import utils +from novaclient.tests.unit.v2 import fakes from novaclient.v2 import floating_ip_pools @@ -27,6 +28,7 @@ class TestFloatingIPPools(utils.FixturedTestCase): def test_list_floating_ips(self): fl = self.cs.floating_ip_pools.list() + self.assert_request_id(fl, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/os-floating-ip-pools') for f in fl: self.assertIsInstance(f, floating_ip_pools.FloatingIPPool) diff --git a/novaclient/tests/unit/v2/test_floating_ips.py b/novaclient/tests/unit/v2/test_floating_ips.py index a91113945..f038ecc95 100644 --- a/novaclient/tests/unit/v2/test_floating_ips.py +++ b/novaclient/tests/unit/v2/test_floating_ips.py @@ -17,6 +17,7 @@ from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit.fixture_data import floatingips as data from novaclient.tests.unit import utils +from novaclient.tests.unit.v2 import fakes from novaclient.v2 import floating_ips @@ -27,27 +28,33 @@ class FloatingIPsTest(utils.FixturedTestCase): def test_list_floating_ips(self): fips = self.cs.floating_ips.list() + self.assert_request_id(fips, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/os-floating-ips') for fip in fips: self.assertIsInstance(fip, floating_ips.FloatingIP) def test_delete_floating_ip(self): fl = self.cs.floating_ips.list()[0] - fl.delete() + ret = fl.delete() + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('DELETE', '/os-floating-ips/1') - self.cs.floating_ips.delete(1) + ret = self.cs.floating_ips.delete(1) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('DELETE', '/os-floating-ips/1') - self.cs.floating_ips.delete(fl) + ret = self.cs.floating_ips.delete(fl) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('DELETE', '/os-floating-ips/1') def test_create_floating_ip(self): fl = self.cs.floating_ips.create() + self.assert_request_id(fl, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/os-floating-ips') self.assertIsNone(fl.pool) self.assertIsInstance(fl, floating_ips.FloatingIP) def test_create_floating_ip_with_pool(self): fl = self.cs.floating_ips.create('nova') + self.assert_request_id(fl, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/os-floating-ips') self.assertEqual('nova', fl.pool) self.assertIsInstance(fl, floating_ips.FloatingIP) diff --git a/novaclient/tests/unit/v2/test_floating_ips_bulk.py b/novaclient/tests/unit/v2/test_floating_ips_bulk.py index e5cefb6ea..37eb2fa1d 100644 --- a/novaclient/tests/unit/v2/test_floating_ips_bulk.py +++ b/novaclient/tests/unit/v2/test_floating_ips_bulk.py @@ -16,6 +16,7 @@ from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit.fixture_data import floatingips as data from novaclient.tests.unit import utils +from novaclient.tests.unit.v2 import fakes from novaclient.v2 import floating_ips @@ -26,18 +27,21 @@ class FloatingIPsBulkTest(utils.FixturedTestCase): def test_list_floating_ips_bulk(self): fl = self.cs.floating_ips_bulk.list() + self.assert_request_id(fl, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/os-floating-ips-bulk') for f in fl: self.assertIsInstance(f, floating_ips.FloatingIP) def test_list_floating_ips_bulk_host_filter(self): fl = self.cs.floating_ips_bulk.list('testHost') + self.assert_request_id(fl, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/os-floating-ips-bulk/testHost') for f in fl: self.assertIsInstance(f, floating_ips.FloatingIP) def test_create_floating_ips_bulk(self): fl = self.cs.floating_ips_bulk.create('192.168.1.0/30') + self.assert_request_id(fl, fakes.FAKE_REQUEST_ID_LIST) body = {'floating_ips_bulk_create': {'ip_range': '192.168.1.0/30'}} self.assert_called('POST', '/os-floating-ips-bulk', body) self.assertEqual(fl.ip_range, @@ -46,6 +50,7 @@ def test_create_floating_ips_bulk(self): def test_create_floating_ips_bulk_with_pool_and_host(self): fl = self.cs.floating_ips_bulk.create('192.168.1.0/30', 'poolTest', 'interfaceTest') + self.assert_request_id(fl, fakes.FAKE_REQUEST_ID_LIST) body = {'floating_ips_bulk_create': { 'ip_range': '192.168.1.0/30', 'pool': 'poolTest', 'interface': 'interfaceTest'}} @@ -59,6 +64,7 @@ def test_create_floating_ips_bulk_with_pool_and_host(self): def test_delete_floating_ips_bulk(self): fl = self.cs.floating_ips_bulk.delete('192.168.1.0/30') + self.assert_request_id(fl, fakes.FAKE_REQUEST_ID_LIST) body = {'ip_range': '192.168.1.0/30'} self.assert_called('PUT', '/os-floating-ips-bulk/delete', body) self.assertEqual(fl.floating_ips_bulk_delete, body['ip_range']) diff --git a/novaclient/tests/unit/v2/test_fping.py b/novaclient/tests/unit/v2/test_fping.py index 0dd7cd30d..ed3416d92 100644 --- a/novaclient/tests/unit/v2/test_fping.py +++ b/novaclient/tests/unit/v2/test_fping.py @@ -16,6 +16,7 @@ from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit.fixture_data import fping as data from novaclient.tests.unit import utils +from novaclient.tests.unit.v2 import fakes from novaclient.v2 import fping @@ -26,10 +27,12 @@ class FpingTest(utils.FixturedTestCase): def test_fping_repr(self): r = self.cs.fping.get(1) + self.assert_request_id(r, fakes.FAKE_REQUEST_ID_LIST) self.assertEqual("", repr(r)) def test_list_fpings(self): fl = self.cs.fping.list() + self.assert_request_id(fl, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/os-fping') for f in fl: self.assertIsInstance(f, fping.Fping) @@ -38,24 +41,28 @@ def test_list_fpings(self): def test_list_fpings_all_tenants(self): fl = self.cs.fping.list(all_tenants=True) + self.assert_request_id(fl, fakes.FAKE_REQUEST_ID_LIST) for f in fl: self.assertIsInstance(f, fping.Fping) self.assert_called('GET', '/os-fping?all_tenants=1') def test_list_fpings_exclude(self): fl = self.cs.fping.list(exclude=['1']) + self.assert_request_id(fl, fakes.FAKE_REQUEST_ID_LIST) for f in fl: self.assertIsInstance(f, fping.Fping) self.assert_called('GET', '/os-fping?exclude=1') def test_list_fpings_include(self): fl = self.cs.fping.list(include=['1']) + self.assert_request_id(fl, fakes.FAKE_REQUEST_ID_LIST) for f in fl: self.assertIsInstance(f, fping.Fping) self.assert_called('GET', '/os-fping?include=1') def test_get_fping(self): f = self.cs.fping.get(1) + self.assert_request_id(f, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/os-fping/1') self.assertIsInstance(f, fping.Fping) self.assertEqual("fake-project", f.project_id) diff --git a/novaclient/tests/unit/v2/test_hosts.py b/novaclient/tests/unit/v2/test_hosts.py index 8f77e01b4..70527ce87 100644 --- a/novaclient/tests/unit/v2/test_hosts.py +++ b/novaclient/tests/unit/v2/test_hosts.py @@ -14,6 +14,7 @@ from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit.fixture_data import hosts as data from novaclient.tests.unit import utils +from novaclient.tests.unit.v2 import fakes from novaclient.v2 import hosts @@ -24,12 +25,14 @@ class HostsTest(utils.FixturedTestCase): def test_describe_resource(self): hs = self.cs.hosts.get('host') + self.assert_request_id(hs, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/os-hosts/host') for h in hs: self.assertIsInstance(h, hosts.Host) def test_list_host(self): hs = self.cs.hosts.list() + self.assert_request_id(hs, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/os-hosts') for h in hs: self.assertIsInstance(h, hosts.Host) @@ -37,6 +40,7 @@ def test_list_host(self): def test_list_host_with_zone(self): hs = self.cs.hosts.list('nova') + self.assert_request_id(hs, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/os-hosts?zone=nova') for h in hs: self.assertIsInstance(h, hosts.Host) @@ -46,6 +50,7 @@ def test_update_enable(self): host = self.cs.hosts.get('sample_host')[0] values = {"status": "enabled"} result = host.update(values) + self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('PUT', '/os-hosts/sample_host', values) self.assertIsInstance(result, hosts.Host) @@ -53,6 +58,7 @@ def test_update_maintenance(self): host = self.cs.hosts.get('sample_host')[0] values = {"maintenance_mode": "enable"} result = host.update(values) + self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('PUT', '/os-hosts/sample_host', values) self.assertIsInstance(result, hosts.Host) @@ -61,24 +67,28 @@ def test_update_both(self): values = {"status": "enabled", "maintenance_mode": "enable"} result = host.update(values) + self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('PUT', '/os-hosts/sample_host', values) self.assertIsInstance(result, hosts.Host) def test_host_startup(self): host = self.cs.hosts.get('sample_host')[0] - host.startup() + resp, result = host.startup() + self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) self.assert_called( 'GET', '/os-hosts/sample_host/startup') def test_host_reboot(self): host = self.cs.hosts.get('sample_host')[0] - host.reboot() + resp, result = host.reboot() + self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) self.assert_called( 'GET', '/os-hosts/sample_host/reboot') def test_host_shutdown(self): host = self.cs.hosts.get('sample_host')[0] - host.shutdown() + resp, result = host.shutdown() + self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) self.assert_called( 'GET', '/os-hosts/sample_host/shutdown') diff --git a/novaclient/tests/unit/v2/test_hypervisors.py b/novaclient/tests/unit/v2/test_hypervisors.py index b297c1247..a522b8537 100644 --- a/novaclient/tests/unit/v2/test_hypervisors.py +++ b/novaclient/tests/unit/v2/test_hypervisors.py @@ -16,6 +16,7 @@ from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit.fixture_data import hypervisors as data from novaclient.tests.unit import utils +from novaclient.tests.unit.v2 import fakes class HypervisorsTest(utils.FixturedTestCase): @@ -33,6 +34,7 @@ def test_hypervisor_index(self): dict(id=5678, hypervisor_hostname='hyper2')] result = self.cs.hypervisors.list(False) + self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/os-hypervisors') for idx, hyper in enumerate(result): @@ -76,6 +78,7 @@ def test_hypervisor_detail(self): disk_available_least=100)] result = self.cs.hypervisors.list() + self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/os-hypervisors/detail') for idx, hyper in enumerate(result): @@ -87,6 +90,7 @@ def test_hypervisor_search(self): dict(id=5678, hypervisor_hostname='hyper2')] result = self.cs.hypervisors.search('hyper') + self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/os-hypervisors/hyper/search') for idx, hyper in enumerate(result): @@ -107,6 +111,7 @@ def test_hypervisor_servers(self): ] result = self.cs.hypervisors.search('hyper', True) + self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/os-hypervisors/hyper/servers') for idx, hyper in enumerate(result): @@ -133,6 +138,7 @@ def test_hypervisor_get(self): disk_available_least=100) result = self.cs.hypervisors.get(1234) + self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/os-hypervisors/1234') self.compare_to_expected(expected, result) @@ -144,6 +150,7 @@ def test_hypervisor_uptime(self): uptime="fake uptime") result = self.cs.hypervisors.uptime(1234) + self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/os-hypervisors/1234/uptime') self.compare_to_expected(expected, result) @@ -165,12 +172,14 @@ def test_hypervisor_statistics(self): ) result = self.cs.hypervisors.statistics() + self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/os-hypervisors/statistics') self.compare_to_expected(expected, result) def test_hypervisor_statistics_data_model(self): result = self.cs.hypervisor_stats.statistics() + self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/os-hypervisors/statistics') # Test for Bug #1370415, the line below used to raise AttributeError diff --git a/novaclient/tests/unit/v2/test_images.py b/novaclient/tests/unit/v2/test_images.py index f4a6ef86b..2c90e2dfc 100644 --- a/novaclient/tests/unit/v2/test_images.py +++ b/novaclient/tests/unit/v2/test_images.py @@ -14,6 +14,7 @@ from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit.fixture_data import images as data from novaclient.tests.unit import utils +from novaclient.tests.unit.v2 import fakes from novaclient.v2 import images @@ -24,6 +25,7 @@ class ImagesTest(utils.FixturedTestCase): def test_list_images(self): il = self.cs.images.list() + self.assert_request_id(il, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/images/detail') for i in il: self.assertIsInstance(i, images.Image) @@ -31,39 +33,47 @@ def test_list_images(self): def test_list_images_undetailed(self): il = self.cs.images.list(detailed=False) + self.assert_request_id(il, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/images') for i in il: self.assertIsInstance(i, images.Image) def test_list_images_with_marker_limit(self): - self.cs.images.list(marker=1234, limit=4) + il = self.cs.images.list(marker=1234, limit=4) + self.assert_request_id(il, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/images/detail?limit=4&marker=1234') def test_get_image_details(self): i = self.cs.images.get(1) + self.assert_request_id(i, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/images/1') self.assertIsInstance(i, images.Image) self.assertEqual(1, i.id) self.assertEqual('CentOS 5.2', i.name) def test_delete_image(self): - self.cs.images.delete(1) + i = self.cs.images.delete(1) + self.assert_request_id(i, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('DELETE', '/images/1') def test_delete_meta(self): - self.cs.images.delete_meta(1, {'test_key': 'test_value'}) + i = self.cs.images.delete_meta(1, {'test_key': 'test_value'}) + self.assert_request_id(i, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('DELETE', '/images/1/metadata/test_key') def test_set_meta(self): - self.cs.images.set_meta(1, {'test_key': 'test_value'}) + i = self.cs.images.set_meta(1, {'test_key': 'test_value'}) + self.assert_request_id(i, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/images/1/metadata', {"metadata": {'test_key': 'test_value'}}) def test_find(self): i = self.cs.images.find(name="CentOS 5.2") + self.assert_request_id(i, fakes.FAKE_REQUEST_ID_LIST) self.assertEqual(1, i.id) self.assert_called('GET', '/images/1') iml = self.cs.images.findall(status='SAVING') + self.assert_request_id(iml, fakes.FAKE_REQUEST_ID_LIST) self.assertEqual(1, len(iml)) self.assertEqual('My Server Backup', iml[0].name) diff --git a/novaclient/v2/agents.py b/novaclient/v2/agents.py index b5372cbd8..0a6c223e5 100644 --- a/novaclient/v2/agents.py +++ b/novaclient/v2/agents.py @@ -63,5 +63,10 @@ def create(self, os, architecture, version, return self._create('/os-agents', body, 'agent') def delete(self, id): - """Deletes an existing agent build.""" - self._delete('/os-agents/%s' % id) + """ + Deletes an existing agent build. + + :param id: The agent's id to delete + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self._delete('/os-agents/%s' % id) diff --git a/novaclient/v2/aggregates.py b/novaclient/v2/aggregates.py index 29d90c0db..9d4dff822 100644 --- a/novaclient/v2/aggregates.py +++ b/novaclient/v2/aggregates.py @@ -38,7 +38,12 @@ def set_metadata(self, metadata): return self.manager.set_metadata(self, metadata) def delete(self): - self.manager.delete(self) + """ + Delete the own aggregate. + + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self.manager.delete(self) class AggregateManager(base.ManagerWithFind): @@ -91,5 +96,10 @@ def set_metadata(self, aggregate, metadata): body, "aggregate") def delete(self, aggregate): - """Delete the specified aggregate.""" - self._delete('/os-aggregates/%s' % (base.getid(aggregate))) + """ + Delete the specified aggregate. + + :param aggregate: The aggregate to delete + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self._delete('/os-aggregates/%s' % (base.getid(aggregate))) diff --git a/novaclient/v2/cloudpipe.py b/novaclient/v2/cloudpipe.py index 809914527..cf8040b17 100644 --- a/novaclient/v2/cloudpipe.py +++ b/novaclient/v2/cloudpipe.py @@ -25,7 +25,12 @@ def __repr__(self): return "" % self.project_id def delete(self): - self.manager.delete(self) + """ + Delete the own cloudpipe instance + + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self.manager.delete(self) class CloudpipeManager(base.ManagerWithFind): @@ -52,8 +57,9 @@ def update(self, address, port): :param address: IP address :param port: Port number + :returns: An instance of novaclient.base.TupleWithMeta """ body = {'configure_project': {'vpn_ip': address, 'vpn_port': port}} - self._update("/os-cloudpipe/configure-project", body) + return self._update("/os-cloudpipe/configure-project", body) diff --git a/novaclient/v2/fixed_ips.py b/novaclient/v2/fixed_ips.py index 7d1377ac0..232ac6fd2 100644 --- a/novaclient/v2/fixed_ips.py +++ b/novaclient/v2/fixed_ips.py @@ -40,16 +40,20 @@ def reserve(self, fixed_ip): """Reserve a Fixed IP. :param fixed_ip: Fixed IP address to reserve + :returns: An instance of novaclient.base.TupleWithMeta """ body = {"reserve": None} - self.api.client.post('/os-fixed-ips/%s/action' % base.getid(fixed_ip), - body=body) + resp, body = self.api.client.post('/os-fixed-ips/%s/action' % + base.getid(fixed_ip), body=body) + return self.convert_into_with_meta(body, resp) def unreserve(self, fixed_ip): """Unreserve a Fixed IP. :param fixed_ip: Fixed IP address to unreserve + :returns: An instance of novaclient.base.TupleWithMeta """ body = {"unreserve": None} - self.api.client.post('/os-fixed-ips/%s/action' % base.getid(fixed_ip), - body=body) + resp, body = self.api.client.post('/os-fixed-ips/%s/action' % + base.getid(fixed_ip), body=body) + return self.convert_into_with_meta(body, resp) diff --git a/novaclient/v2/flavor_access.py b/novaclient/v2/flavor_access.py index 4e6fcd4c2..73855ee97 100644 --- a/novaclient/v2/flavor_access.py +++ b/novaclient/v2/flavor_access.py @@ -62,7 +62,9 @@ def _action(self, action, flavor, info, **kwargs): body = {action: info} self.run_hooks('modify_body_for_action', body, **kwargs) url = '/flavors/%s/action' % base.getid(flavor) - _resp, body = self.api.client.post(url, body=body) + resp, body = self.api.client.post(url, body=body) - return [self.resource_class(self, res) - for res in body['flavor_access']] + items = [self.resource_class(self, res) + for res in body['flavor_access']] + + return base.ListWithMeta(items, resp) diff --git a/novaclient/v2/flavors.py b/novaclient/v2/flavors.py index 3ce545802..43801d17d 100644 --- a/novaclient/v2/flavors.py +++ b/novaclient/v2/flavors.py @@ -43,10 +43,14 @@ def is_public(self): return self._info.get("os-flavor-access:is_public", 'N/A') def get_keys(self): - """Get extra specs from a flavor.""" - _resp, body = self.manager.api.client.get( + """ + Get extra specs from a flavor. + + :returns: An instance of novaclient.base.DictWithMeta + """ + resp, body = self.manager.api.client.get( "/flavors/%s/os-extra_specs" % base.getid(self)) - return body["extra_specs"] + return self.manager.convert_into_with_meta(body["extra_specs"], resp) def set_keys(self, metadata): """Set extra specs on a flavor. @@ -64,14 +68,23 @@ def unset_keys(self, keys): """Unset extra specs on a flavor. :param keys: A list of keys to be unset + :returns: An instance of novaclient.base.TupleWithMeta """ + result = base.TupleWithMeta((), None) for k in keys: - self.manager._delete( + ret = self.manager._delete( "/flavors/%s/os-extra_specs/%s" % (base.getid(self), k)) + result.append_request_ids(ret.request_ids) + + return result def delete(self): - """Delete this flavor.""" - self.manager.delete(self) + """ + Delete this flavor. + + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self.manager.delete(self) class FlavorManager(base.ManagerWithFind): @@ -130,8 +143,9 @@ def delete(self, flavor): """Delete a specific flavor. :param flavor: The ID of the :class:`Flavor` to get. + :returns: An instance of novaclient.base.TupleWithMeta """ - self._delete("/flavors/%s" % base.getid(flavor)) + return self._delete("/flavors/%s" % base.getid(flavor)) def _build_body(self, name, ram, vcpus, disk, id, swap, ephemeral, rxtx_factor, is_public): diff --git a/novaclient/v2/floating_ip_dns.py b/novaclient/v2/floating_ip_dns.py index 41df0f628..d6ccb8d5c 100644 --- a/novaclient/v2/floating_ip_dns.py +++ b/novaclient/v2/floating_ip_dns.py @@ -30,21 +30,38 @@ def _quote_domain(domain): class FloatingIPDNSDomain(base.Resource): def delete(self): - self.manager.delete(self.domain) + """ + Delete the own Floating IP DNS domain. + + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self.manager.delete(self.domain) def create(self): + """ + Create a Floating IP DNS domain. + + :returns: An instance of novaclient.base.DictWithMeta + """ if self.scope == 'public': - self.manager.create_public(self.domain, self.project) + return self.manager.create_public(self.domain, self.project) else: - self.manager.create_private(self.domain, self.availability_zone) + return self.manager.create_private(self.domain, + self.availability_zone) def get(self): + """ + Get the own Floating IP DNS domain. + + :returns: An instance of novaclient.base.TupleWithMeta or + novaclient.base.ListWithMeta + """ entries = self.manager.domains() for entry in entries: if entry.get('domain') == self.domain: return entry - return None + return base.TupleWithMeta((), entries.request_ids) class FloatingIPDNSDomainManager(base.Manager): @@ -70,16 +87,32 @@ def create_public(self, fqdomain, project): body, 'domain_entry') def delete(self, fqdomain): - """Delete the specified domain.""" - self._delete("/os-floating-ip-dns/%s" % _quote_domain(fqdomain)) + """ + Delete the specified domain. + + :param fqdomain: The domain to delete + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self._delete("/os-floating-ip-dns/%s" % _quote_domain(fqdomain)) class FloatingIPDNSEntry(base.Resource): def delete(self): - self.manager.delete(self.name, self.domain) + """ + Delete the own Floating IP DNS entry. + + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self.manager.delete(self.name, self.domain) def create(self): - self.manager.create(self.domain, self.name, self.ip, self.dns_type) + """ + Create a Floating IP DNS entry. + + :returns: :class:`FloatingIPDNSEntry` + """ + return self.manager.create(self.domain, self.name, self.ip, + self.dns_type) def get(self): return self.manager.get(self.domain, self.name) @@ -116,6 +149,10 @@ def modify_ip(self, domain, name, ip): (_quote_domain(domain), name), body, "dns_entry") def delete(self, domain, name): - """Delete entry specified by name and domain.""" - self._delete("/os-floating-ip-dns/%s/entries/%s" % - (_quote_domain(domain), name)) + """ + Delete entry specified by name and domain. + + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self._delete("/os-floating-ip-dns/%s/entries/%s" % + (_quote_domain(domain), name)) diff --git a/novaclient/v2/floating_ips.py b/novaclient/v2/floating_ips.py index e3bc525bb..b25bb33f4 100644 --- a/novaclient/v2/floating_ips.py +++ b/novaclient/v2/floating_ips.py @@ -19,8 +19,12 @@ class FloatingIP(base.Resource): def delete(self): - """Delete this floating IP""" - self.manager.delete(self) + """ + Delete this floating IP + + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self.manager.delete(self) class FloatingIPManager(base.ManagerWithFind): @@ -38,8 +42,9 @@ def delete(self, floating_ip): """Delete (deallocate) a floating IP for a tenant :param floating_ip: The floating IP address to delete. + :returns: An instance of novaclient.base.TupleWithMeta """ - self._delete("/os-floating-ips/%s" % base.getid(floating_ip)) + return self._delete("/os-floating-ips/%s" % base.getid(floating_ip)) def get(self, floating_ip): """Retrieve a floating IP""" diff --git a/novaclient/v2/hosts.py b/novaclient/v2/hosts.py index 0f979973b..ff78fb30b 100644 --- a/novaclient/v2/hosts.py +++ b/novaclient/v2/hosts.py @@ -69,9 +69,18 @@ def update(self, host, values): return self._update("/os-hosts/%s" % host, values) def host_action(self, host, action): - """Perform an action on a host.""" + """ + Perform an action on a host. + + :param host: The host to perform an action + :param actiob: The action to perform + :returns: A Response object and an instance of + novaclient.base.DictWithMeta + """ url = '/os-hosts/{0}/{1}'.format(host, action) - return self.api.client.get(url) + resp, body = self.api.client.get(url) + # For compatibility, return Response object as a first return value + return resp, self.convert_into_with_meta(body, resp) def list(self, zone=None): url = '/os-hosts' diff --git a/novaclient/v2/images.py b/novaclient/v2/images.py index 32001b465..39e9dc06c 100644 --- a/novaclient/v2/images.py +++ b/novaclient/v2/images.py @@ -33,8 +33,10 @@ def __repr__(self): def delete(self): """ Delete this image. + + :returns: An instance of novaclient.base.TupleWithMeta """ - self.manager.delete(self) + return self.manager.delete(self) class ImageManager(base.ManagerWithFind): @@ -81,8 +83,9 @@ def delete(self, image): that you didn't create. :param image: The :class:`Image` (or its ID) to delete. + :returns: An instance of novaclient.base.TupleWithMeta """ - self._delete("/images/%s" % base.getid(image)) + return self._delete("/images/%s" % base.getid(image)) def set_meta(self, image, metadata): """ @@ -101,6 +104,12 @@ def delete_meta(self, image, keys): :param image: The :class:`Image` to delete metadata :param keys: A list of metadata keys to delete from the image + :returns: An instance of novaclient.base.TupleWithMeta """ + result = base.TupleWithMeta((), None) for k in keys: - self._delete("/images/%s/metadata/%s" % (base.getid(image), k)) + ret = self._delete("/images/%s/metadata/%s" % + (base.getid(image), k)) + result.append_request_ids(ret.request_ids) + + return result From 15d7b403bba2db83726f20c8395e6a7655dbe8d1 Mon Sep 17 00:00:00 2001 From: Pavel Kholkin Date: Tue, 19 Jan 2016 18:06:05 +0300 Subject: [PATCH 0967/1705] Support to boot a VM with network name Bug_description: Today, in order to specify networking arguments, the nova client only accept uuid's. It would be nice if we could extend the client option to allow names to be accepted too. Solution: This patch supports provisioning of VM by mentioning network name. _boot method currently validates for network ID to boot a VM. Updates: * new parameter 'net-name' is added * when 'net-name' is specified network ID is retrieved * network ID is used for further processing of vm boot request * if multiple networks with the same name exist then NoUniqueMatch exception is raised * help text is updated DocImpact Closes-Bug: #1496180 Co-Authored-By: Pavel Kholkin Change-Id: Ifb14a76749901ee106cdb807e38820a2c25e1320 --- .../functional/v2/legacy/test_servers.py | 13 +++++ novaclient/tests/unit/v2/test_shell.py | 58 +++++++++++++++++++ novaclient/v2/shell.py | 42 +++++++++++--- 3 files changed, 106 insertions(+), 7 deletions(-) diff --git a/novaclient/tests/functional/v2/legacy/test_servers.py b/novaclient/tests/functional/v2/legacy/test_servers.py index 746d556c8..23d3835d5 100644 --- a/novaclient/tests/functional/v2/legacy/test_servers.py +++ b/novaclient/tests/functional/v2/legacy/test_servers.py @@ -55,6 +55,19 @@ def test_boot_server_with_legacy_bdm(self): def test_boot_server_with_legacy_bdm_volume_id_only(self): self._boot_server_with_legacy_bdm() + def test_boot_server_with_net_name(self): + network = self.client.networks.list()[0] + server_info = self.nova("boot", params=( + "%(name)s --flavor %(flavor)s --image %(image)s --poll " + "--nic net-name=%(net-name)s" % {"name": str(uuid.uuid4()), + "image": self.image.id, + "flavor": self.flavor.id, + "net-name": network.label})) + server_id = self._get_value_from_the_table(server_info, "id") + + self.client.servers.delete(server_id) + self.wait_for_resource_delete(server_id, self.client.servers) + class TestServersListNovaClient(base.ClientTestBase): """Servers list functional tests.""" diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index b058a0433..b6b01045c 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -571,6 +571,64 @@ def test_boot_nics_invalid_ipv6(self): '--nic net-id=a=c,v6-fixed-ip=10.0.0.1 some-server') self.assertRaises(exceptions.CommandError, self.run_command, cmd) + def test_boot_nics_net_id_twice(self): + cmd = ('boot --image 1 --flavor 1 ' + '--nic net-id=net-id1,net-id=net-id2 some-server') + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + @mock.patch( + 'novaclient.tests.unit.v2.fakes.FakeHTTPClient.get_os_networks') + def test_boot_nics_net_name(self, mock_networks_list): + mock_networks_list.return_value = (200, {}, { + 'networks': [{"label": "some-net", 'id': '1'}]}) + + cmd = ('boot --image 1 --flavor 1 ' + '--nic net-name=some-net some-server') + self.run_command(cmd) + self.assert_called_anytime( + 'POST', '/servers', + { + 'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': '1', + 'min_count': 1, + 'max_count': 1, + 'networks': [ + {'uuid': '1'}, + ], + }, + }, + ) + + def test_boot_nics_net_name_not_found(self): + cmd = ('boot --image 1 --flavor 1 ' + '--nic net-name=some-net some-server') + self.assertRaises(exceptions.ResourceNotFound, self.run_command, cmd) + + @mock.patch( + 'novaclient.tests.unit.v2.fakes.FakeHTTPClient.get_os_networks') + def test_boot_nics_net_name_multiple_matches(self, mock_networks_list): + mock_networks_list.return_value = (200, {}, { + 'networks': [{"label": "some-net", 'id': '1'}, + {"label": "some-net", 'id': '2'}]}) + + cmd = ('boot --image 1 --flavor 1 ' + '--nic net-name=some-net some-server') + self.assertRaises(exceptions.NoUniqueMatch, self.run_command, cmd) + + @mock.patch('novaclient.v2.shell._find_network_id', return_value='net-id') + def test_boot_nics_net_name_and_net_id(self, mock_find_network_id): + cmd = ('boot --image 1 --flavor 1 ' + '--nic net-name=some-net,net-id=some-id some-server') + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + @mock.patch('novaclient.v2.shell._find_network_id', return_value='net-id') + def test_boot_nics_net_name_and_port_id(self, mock_find_network_id): + cmd = ('boot --image 1 --flavor 1 ' + '--nic net-name=some-net,port-id=some-id some-server') + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + def test_boot_files(self): testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt') with open(testfile) as testfile_fd: diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 41ec74956..6057deb10 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -273,11 +273,12 @@ def _boot(cs, args): nics = [] for nic_str in args.nics: err_msg = (_("Invalid nic argument '%s'. Nic arguments must be of " - "the form --nic , with at minimum " - "net-id or port-id (but not both) specified.") % nic_str) + "the form --nic , with only one of net-id, net-name " + "or port-id specified.") % nic_str) nic_info = {"net-id": "", "v4-fixed-ip": "", "v6-fixed-ip": "", - "port-id": ""} + "port-id": "", "net-name": ""} for kv_str in nic_str.split(","): try: @@ -286,6 +287,13 @@ def _boot(cs, args): raise exceptions.CommandError(err_msg) if k in nic_info: + # if user has given a net-name resolve it to network ID + if k == 'net-name': + k = 'net-id' + v = _find_network_id(cs, v) + # if some argument was given multiple times + if nic_info[k]: + raise exceptions.CommandError(err_msg) nic_info[k] = v else: raise exceptions.CommandError(err_msg) @@ -517,15 +525,16 @@ def _boot(cs, args): "use.")) @cliutils.arg( '--nic', - metavar="", + metavar="", action='append', dest='nics', default=[], help=_("Create a NIC on the server. " "Specify option multiple times to create multiple NICs. " "net-id: attach NIC to network with this UUID " - "(either port-id or net-id must be provided), " + "net-name: attach NIC to network with this name " + "(either port-id or net-id or net-name must be provided), " "v4-fixed-ip: IPv4 fixed address for NIC (optional), " "v6-fixed-ip: IPv6 fixed address for NIC (optional), " "port-id: attach NIC to port with this UUID " @@ -2088,6 +2097,25 @@ def _find_flavor(cs, flavor): return cs.flavors.find(ram=flavor) +def _find_network_id(cs, net_name): + """Get unique network ID from network name.""" + network_id = None + for net_info in cs.networks.list(): + if net_name == net_info.label: + if network_id is not None: + msg = (_("Multiple network name matches found for name '%s', " + "use network ID to be more specific.") % net_name) + raise exceptions.NoUniqueMatch(msg) + else: + network_id = net_info.id + + if network_id is None: + msg = (_("No network name match for name '%s'") % net_name) + raise exceptions.ResourceNotFound(msg % {'network': net_name}) + else: + return network_id + + @cliutils.arg('server', metavar='', help=_('Name or ID of server.')) @cliutils.arg( 'network_id', From 0414bab3d3c1a202060f6fc18c79f8d22f993817 Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Fri, 25 Dec 2015 12:34:02 +0900 Subject: [PATCH 0968/1705] Add return-request-id-to-caller function(3/5) Add return-request-id-to-caller function to resources and resource managers in the following files. The methods in the resource class and resource manager return a wrapper class that has 'request_ids' property. The caller can get request ids of the callee via the property. * novaclient/v2/keypairs.py * novaclient/v2/limits.py * novaclient/v2/networks.py * novaclient/v2/quota_classes.py * novaclient/v2/quotas.py * novaclient/v2/security_group_default_rules.py * novaclient/v2/security_group_rules.py * novaclient/v2/security_groups.py * novaclient/v2/server_groups.py * novaclient/v2/services.py * novaclient/v2/usage.py * novaclient/v2/versions.py Co-authored-by: Ankit Agrawal Change-Id: I9203f70a0eef5686b590fbff35563f2cf8b6f586 Implements: blueprint return-request-id-to-caller --- .../tests/unit/fixture_data/keypairs.py | 5 +- novaclient/tests/unit/fixture_data/limits.py | 2 +- .../tests/unit/fixture_data/networks.py | 8 ++- novaclient/tests/unit/fixture_data/quotas.py | 5 +- .../unit/fixture_data/security_group_rules.py | 5 +- .../unit/fixture_data/security_groups.py | 5 +- .../tests/unit/fixture_data/server_groups.py | 7 ++- novaclient/tests/unit/v2/fakes.py | 61 ++++++++++--------- novaclient/tests/unit/v2/test_keypairs.py | 16 ++++- novaclient/tests/unit/v2/test_limits.py | 5 ++ novaclient/tests/unit/v2/test_networks.py | 26 +++++--- .../tests/unit/v2/test_quota_classes.py | 4 +- novaclient/tests/unit/v2/test_quotas.py | 19 ++++-- .../unit/v2/test_security_group_rules.py | 10 ++- .../tests/unit/v2/test_security_groups.py | 14 ++++- .../tests/unit/v2/test_server_groups.py | 11 +++- novaclient/tests/unit/v2/test_services.py | 11 +++- novaclient/tests/unit/v2/test_usage.py | 2 + novaclient/tests/unit/v2/test_versions.py | 19 ++++-- novaclient/v2/keypairs.py | 13 +++- novaclient/v2/networks.py | 41 +++++++++---- novaclient/v2/quotas.py | 9 ++- novaclient/v2/security_group_default_rules.py | 11 +++- novaclient/v2/security_group_rules.py | 10 ++- novaclient/v2/security_groups.py | 18 ++++-- novaclient/v2/server_groups.py | 10 ++- novaclient/v2/versions.py | 1 + 27 files changed, 249 insertions(+), 99 deletions(-) diff --git a/novaclient/tests/unit/fixture_data/keypairs.py b/novaclient/tests/unit/fixture_data/keypairs.py index 64d102ea5..ee60e7aac 100644 --- a/novaclient/tests/unit/fixture_data/keypairs.py +++ b/novaclient/tests/unit/fixture_data/keypairs.py @@ -24,7 +24,7 @@ def setUp(self): super(V1, self).setUp() keypair = {'fingerprint': 'FAKE_KEYPAIR', 'name': 'test'} - headers = {'Content-Type': 'application/json'} + headers = self.json_headers self.requests.register_uri('GET', self.url(), json={'keypairs': [keypair]}, @@ -34,7 +34,8 @@ def setUp(self): json={'keypair': keypair}, headers=headers) - self.requests.register_uri('DELETE', self.url('test'), status_code=202) + self.requests.register_uri('DELETE', self.url('test'), status_code=202, + headers=headers) def post_os_keypairs(request, context): body = jsonutils.loads(request.body) diff --git a/novaclient/tests/unit/fixture_data/limits.py b/novaclient/tests/unit/fixture_data/limits.py index 55d324d95..6c1d306cd 100644 --- a/novaclient/tests/unit/fixture_data/limits.py +++ b/novaclient/tests/unit/fixture_data/limits.py @@ -74,7 +74,7 @@ def setUp(self): }, } - headers = {'Content-Type': 'application/json'} + headers = self.json_headers self.requests.register_uri('GET', self.url(), json=get_limits, headers=headers) diff --git a/novaclient/tests/unit/fixture_data/networks.py b/novaclient/tests/unit/fixture_data/networks.py index d2743fc60..9471605c6 100644 --- a/novaclient/tests/unit/fixture_data/networks.py +++ b/novaclient/tests/unit/fixture_data/networks.py @@ -33,7 +33,7 @@ def setUp(self): ] } - headers = {'Content-Type': 'application/json'} + headers = self.json_headers self.requests.register_uri('GET', self.url(), json=get_os_networks, @@ -55,8 +55,10 @@ def post_os_networks(request, context): self.requests.register_uri('DELETE', self.url('networkdelete'), - status_code=202) + status_code=202, + headers=headers) for u in ('add', 'networkdisassociate/action', 'networktest/action', '1/action', '2/action'): - self.requests.register_uri('POST', self.url(u), status_code=202) + self.requests.register_uri('POST', self.url(u), status_code=202, + headers=headers) diff --git a/novaclient/tests/unit/fixture_data/quotas.py b/novaclient/tests/unit/fixture_data/quotas.py index 83f448f49..487cfa003 100644 --- a/novaclient/tests/unit/fixture_data/quotas.py +++ b/novaclient/tests/unit/fixture_data/quotas.py @@ -23,7 +23,7 @@ def setUp(self): uuid = '97f4c221-bff4-4578-b030-0df4ef119353' uuid2 = '97f4c221bff44578b0300df4ef119353' test_json = {'quota_set': self.test_quota('test')} - self.headers = {'Content-Type': 'application/json'} + self.headers = self.json_headers for u in ('test', 'tenant-id', 'tenant-id/defaults', '%s/defaults' % uuid2): @@ -47,7 +47,8 @@ def setUp(self): headers=self.headers) for u in ('test', uuid2): - self.requests.register_uri('DELETE', self.url(u), status_code=202) + self.requests.register_uri('DELETE', self.url(u), status_code=202, + headers=self.headers) def test_quota(self, tenant_id='test'): return { diff --git a/novaclient/tests/unit/fixture_data/security_group_rules.py b/novaclient/tests/unit/fixture_data/security_group_rules.py index d0806c5ce..e4285fe70 100644 --- a/novaclient/tests/unit/fixture_data/security_group_rules.py +++ b/novaclient/tests/unit/fixture_data/security_group_rules.py @@ -33,14 +33,15 @@ def setUp(self): 'cidr': '10.0.0.0/8' } - headers = {'Content-Type': 'application/json'} + headers = self.json_headers self.requests.register_uri('GET', self.url(), json={'security_group_rules': [rule]}, headers=headers) for u in (1, 11, 12): - self.requests.register_uri('DELETE', self.url(u), status_code=202) + self.requests.register_uri('DELETE', self.url(u), status_code=202, + headers=headers) def post_rules(request, context): body = jsonutils.loads(request.body) diff --git a/novaclient/tests/unit/fixture_data/security_groups.py b/novaclient/tests/unit/fixture_data/security_groups.py index c1a3073cf..28023093f 100644 --- a/novaclient/tests/unit/fixture_data/security_groups.py +++ b/novaclient/tests/unit/fixture_data/security_groups.py @@ -62,7 +62,7 @@ def setUp(self): } get_groups = {'security_groups': [security_group_1, security_group_2]} - headers = {'Content-Type': 'application/json'} + headers = self.json_headers self.requests.register_uri('GET', self.url(), json=get_groups, @@ -73,7 +73,8 @@ def setUp(self): json=get_group_1, headers=headers) - self.requests.register_uri('DELETE', self.url(1), status_code=202) + self.requests.register_uri('DELETE', self.url(1), status_code=202, + headers=headers) def post_os_security_groups(request, context): body = jsonutils.loads(request.body) diff --git a/novaclient/tests/unit/fixture_data/server_groups.py b/novaclient/tests/unit/fixture_data/server_groups.py index bf4c82d0e..64a398810 100644 --- a/novaclient/tests/unit/fixture_data/server_groups.py +++ b/novaclient/tests/unit/fixture_data/server_groups.py @@ -52,7 +52,7 @@ def setUp(self): } ] - headers = {'Content-Type': 'application/json'} + headers = self.json_headers self.requests.register_uri('GET', self.url(), json={'server_groups': server_groups}, @@ -62,7 +62,8 @@ def setUp(self): server_j = jsonutils.dumps({'server_group': server}) def _register(method, *args): - self.requests.register_uri(method, self.url(*args), text=server_j) + self.requests.register_uri(method, self.url(*args), text=server_j, + headers=headers) _register('POST') _register('POST', server['id']) @@ -71,4 +72,4 @@ def _register(method, *args): _register('POST', server['id'], '/action') self.requests.register_uri('DELETE', self.url(server['id']), - status_code=202) + status_code=202, headers=headers) diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 668ee95c3..50192d0a4 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -151,7 +151,7 @@ def get_endpoint(self): "http://nova-api:8774/v2.1/190a755eef2e4aac9f06aa6be9786385") def get_versions(self): - return (200, {}, { + return (200, FAKE_RESPONSE_HEADERS, { "versions": [ {"status": "SUPPORTED", "updated": "2011-01-21T11:33:21Z", "links": [{"href": "http://nova-api:8774/v2/", @@ -208,7 +208,7 @@ def get_current_version(self): raise AssertionError( "Unknown endpoint_type:%s" % self.endpoint_type) - return (200, {}, versions[endpoint]) + return (200, FAKE_RESPONSE_HEADERS, versions[endpoint]) # # agents @@ -1402,7 +1402,7 @@ def delete_os_quota_sets_97f4c221bff44578b0300df4ef119353(self, **kw): # def get_os_quota_class_sets_test(self, **kw): - return (200, {}, { + return (200, FAKE_RESPONSE_HEADERS, { 'quota_class_set': { 'id': 'test', 'metadata_items': 1, @@ -1589,7 +1589,7 @@ def post_os_security_group_default_rules(self, body, **kw): # Tenant Usage # def get_os_simple_tenant_usage(self, **kw): - return (200, {}, + return (200, FAKE_RESPONSE_HEADERS, {six.u('tenant_usages'): [{ six.u('total_memory_mb_usage'): 25451.762807466665, six.u('total_vcpus_usage'): 49.71047423333333, @@ -1615,7 +1615,7 @@ def get_os_simple_tenant_usage(self, **kw): six.u('total_local_gb_usage'): 0.0}]}) def get_os_simple_tenant_usage_tenantfoo(self, **kw): - return (200, {}, + return (200, FAKE_RESPONSE_HEADERS, {six.u('tenant_usage'): { six.u('total_memory_mb_usage'): 25451.762807466665, six.u('total_vcpus_usage'): 49.71047423333333, @@ -1756,44 +1756,47 @@ def delete_os_aggregates_1(self, **kw): def get_os_services(self, **kw): host = kw.get('host', 'host1') binary = kw.get('binary', 'nova-compute') - return (200, {}, {'services': [{'binary': binary, - 'host': host, - 'zone': 'nova', - 'status': 'enabled', - 'state': 'up', - 'updated_at': datetime.datetime( - 2012, 10, 29, 13, 42, 2)}, - {'binary': binary, - 'host': host, - 'zone': 'nova', - 'status': 'disabled', - 'state': 'down', - 'updated_at': datetime.datetime( - 2012, 9, 18, 8, 3, 38)}, - ]}) + return (200, FAKE_RESPONSE_HEADERS, + {'services': [{'binary': binary, + 'host': host, + 'zone': 'nova', + 'status': 'enabled', + 'state': 'up', + 'updated_at': datetime.datetime( + 2012, 10, 29, 13, 42, 2)}, + {'binary': binary, + 'host': host, + 'zone': 'nova', + 'status': 'disabled', + 'state': 'down', + 'updated_at': datetime.datetime( + 2012, 9, 18, 8, 3, 38)}, + ]}) def put_os_services_enable(self, body, **kw): - return (200, {}, {'service': {'host': body['host'], - 'binary': body['binary'], - 'status': 'enabled'}}) + return (200, FAKE_RESPONSE_HEADERS, + {'service': {'host': body['host'], + 'binary': body['binary'], + 'status': 'enabled'}}) def put_os_services_disable(self, body, **kw): - return (200, {}, {'service': {'host': body['host'], - 'binary': body['binary'], - 'status': 'disabled'}}) + return (200, FAKE_RESPONSE_HEADERS, + {'service': {'host': body['host'], + 'binary': body['binary'], + 'status': 'disabled'}}) def put_os_services_disable_log_reason(self, body, **kw): - return (200, {}, {'service': { + return (200, FAKE_RESPONSE_HEADERS, {'service': { 'host': body['host'], 'binary': body['binary'], 'status': 'disabled', 'disabled_reason': body['disabled_reason']}}) def delete_os_services_1(self, **kw): - return (204, {}, None) + return (204, FAKE_RESPONSE_HEADERS, None) def put_os_services_force_down(self, body, **kw): - return (200, {}, {'service': { + return (200, FAKE_RESPONSE_HEADERS, {'service': { 'host': body['host'], 'binary': body['binary'], 'forced_down': False}}) diff --git a/novaclient/tests/unit/v2/test_keypairs.py b/novaclient/tests/unit/v2/test_keypairs.py index 109d350a3..19570c76a 100644 --- a/novaclient/tests/unit/v2/test_keypairs.py +++ b/novaclient/tests/unit/v2/test_keypairs.py @@ -15,6 +15,7 @@ from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit.fixture_data import keypairs as data from novaclient.tests.unit import utils +from novaclient.tests.unit.v2 import fakes from novaclient.v2 import keypairs @@ -36,23 +37,28 @@ def _get_keypair_prefix(self): def test_get_keypair(self): kp = self.cs.keypairs.get('test') + self.assert_request_id(kp, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/%s/test' % self.keypair_prefix) self.assertIsInstance(kp, keypairs.Keypair) self.assertEqual('test', kp.name) def test_list_keypairs(self): kps = self.cs.keypairs.list() + self.assert_request_id(kps, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/%s' % self.keypair_prefix) for kp in kps: self.assertIsInstance(kp, keypairs.Keypair) def test_delete_keypair(self): kp = self.cs.keypairs.list()[0] - kp.delete() + ret = kp.delete() + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('DELETE', '/%s/test' % self.keypair_prefix) - self.cs.keypairs.delete('test') + ret = self.cs.keypairs.delete('test') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('DELETE', '/%s/test' % self.keypair_prefix) - self.cs.keypairs.delete(kp) + ret = self.cs.keypairs.delete(kp) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('DELETE', '/%s/test' % self.keypair_prefix) @@ -64,6 +70,7 @@ def setUp(self): def test_create_keypair(self): name = "foo" kp = self.cs.keypairs.create(name) + self.assert_request_id(kp, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/%s' % self.keypair_prefix, body={'keypair': {'name': name}}) self.assertIsInstance(kp, keypairs.Keypair) @@ -72,6 +79,7 @@ def test_import_keypair(self): name = "foo" pub_key = "fake-public-key" kp = self.cs.keypairs.create(name, pub_key) + self.assert_request_id(kp, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/%s' % self.keypair_prefix, body={'keypair': {'name': name, 'public_key': pub_key}}) @@ -87,6 +95,7 @@ def test_create_keypair(self): name = "foo" key_type = "some_type" kp = self.cs.keypairs.create(name, key_type=key_type) + self.assert_request_id(kp, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/%s' % self.keypair_prefix, body={'keypair': {'name': name, 'type': key_type}}) @@ -96,6 +105,7 @@ def test_import_keypair(self): name = "foo" pub_key = "fake-public-key" kp = self.cs.keypairs.create(name, pub_key) + self.assert_request_id(kp, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/%s' % self.keypair_prefix, body={'keypair': {'name': name, 'public_key': pub_key, diff --git a/novaclient/tests/unit/v2/test_limits.py b/novaclient/tests/unit/v2/test_limits.py index a72d6d602..a0b8bcf2a 100644 --- a/novaclient/tests/unit/v2/test_limits.py +++ b/novaclient/tests/unit/v2/test_limits.py @@ -14,6 +14,7 @@ from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit.fixture_data import limits as data from novaclient.tests.unit import utils +from novaclient.tests.unit.v2 import fakes from novaclient.v2 import limits @@ -24,16 +25,19 @@ class LimitsTest(utils.FixturedTestCase): def test_get_limits(self): obj = self.cs.limits.get() + self.assert_request_id(obj, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/limits') self.assertIsInstance(obj, limits.Limits) def test_get_limits_for_a_tenant(self): obj = self.cs.limits.get(tenant_id=1234) + self.assert_request_id(obj, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/limits?tenant_id=1234') self.assertIsInstance(obj, limits.Limits) def test_absolute_limits_reserved(self): obj = self.cs.limits.get(reserved=True) + self.assert_request_id(obj, fakes.FAKE_REQUEST_ID_LIST) expected = ( limits.AbsoluteLimit("maxTotalRAMSize", 51200), @@ -52,6 +56,7 @@ def test_absolute_limits_reserved(self): def test_rate_absolute_limits(self): obj = self.cs.limits.get() + self.assert_request_id(obj, fakes.FAKE_REQUEST_ID_LIST) expected = ( limits.RateLimit('POST', '*', '.*', 10, 2, 'MINUTE', diff --git a/novaclient/tests/unit/v2/test_networks.py b/novaclient/tests/unit/v2/test_networks.py index 8de3fff20..1b17af660 100644 --- a/novaclient/tests/unit/v2/test_networks.py +++ b/novaclient/tests/unit/v2/test_networks.py @@ -14,6 +14,7 @@ from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit.fixture_data import networks as data from novaclient.tests.unit import utils +from novaclient.tests.unit.v2 import fakes from novaclient.v2 import networks @@ -24,21 +25,25 @@ class NetworksTest(utils.FixturedTestCase): def test_list_networks(self): fl = self.cs.networks.list() + self.assert_request_id(fl, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/os-networks') for f in fl: self.assertIsInstance(f, networks.Network) def test_get_network(self): f = self.cs.networks.get(1) + self.assert_request_id(f, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/os-networks/1') self.assertIsInstance(f, networks.Network) def test_delete(self): - self.cs.networks.delete('networkdelete') + ret = self.cs.networks.delete('networkdelete') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('DELETE', '/os-networks/networkdelete') def test_create(self): f = self.cs.networks.create(label='foo') + self.assert_request_id(f, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/os-networks', {'network': {'label': 'foo'}}) self.assertIsInstance(f, networks.Network) @@ -70,38 +75,45 @@ def test_create_allparams(self): } f = self.cs.networks.create(**params) + self.assert_request_id(f, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/os-networks', {'network': params}) self.assertIsInstance(f, networks.Network) def test_associate_project(self): - self.cs.networks.associate_project('networktest') + f = self.cs.networks.associate_project('networktest') + self.assert_request_id(f, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/os-networks/add', {'id': 'networktest'}) def test_associate_host(self): - self.cs.networks.associate_host('networktest', 'testHost') + f = self.cs.networks.associate_host('networktest', 'testHost') + self.assert_request_id(f, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/os-networks/networktest/action', {'associate_host': 'testHost'}) def test_disassociate(self): - self.cs.networks.disassociate('networkdisassociate') + f = self.cs.networks.disassociate('networkdisassociate') + self.assert_request_id(f, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/os-networks/networkdisassociate/action', {'disassociate': None}) def test_disassociate_host_only(self): - self.cs.networks.disassociate('networkdisassociate', True, False) + f = self.cs.networks.disassociate('networkdisassociate', True, False) + self.assert_request_id(f, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/os-networks/networkdisassociate/action', {'disassociate_host': None}) def test_disassociate_project(self): - self.cs.networks.disassociate('networkdisassociate', False, True) + f = self.cs.networks.disassociate('networkdisassociate', False, True) + self.assert_request_id(f, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/os-networks/networkdisassociate/action', {'disassociate_project': None}) def test_add(self): - self.cs.networks.add('networkadd') + f = self.cs.networks.add('networkadd') + self.assert_request_id(f, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/os-networks/add', {'id': 'networkadd'}) diff --git a/novaclient/tests/unit/v2/test_quota_classes.py b/novaclient/tests/unit/v2/test_quota_classes.py index 467ff830d..c45c731ef 100644 --- a/novaclient/tests/unit/v2/test_quota_classes.py +++ b/novaclient/tests/unit/v2/test_quota_classes.py @@ -24,11 +24,13 @@ class QuotaClassSetsTest(utils.TestCase): def test_class_quotas_get(self): class_name = 'test' - cs.quota_classes.get(class_name) + q = cs.quota_classes.get(class_name) + self.assert_request_id(q, fakes.FAKE_REQUEST_ID_LIST) cs.assert_called('GET', '/os-quota-class-sets/%s' % class_name) def test_update_quota(self): q = cs.quota_classes.get('test') + self.assert_request_id(q, fakes.FAKE_REQUEST_ID_LIST) q.update(cores=2) cs.assert_called('PUT', '/os-quota-class-sets/test') diff --git a/novaclient/tests/unit/v2/test_quotas.py b/novaclient/tests/unit/v2/test_quotas.py index 76bfcfe8e..d8144a55c 100644 --- a/novaclient/tests/unit/v2/test_quotas.py +++ b/novaclient/tests/unit/v2/test_quotas.py @@ -16,6 +16,7 @@ from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit.fixture_data import quotas as data from novaclient.tests.unit import utils +from novaclient.tests.unit.v2 import fakes class QuotaSetsTest(utils.FixturedTestCase): @@ -25,36 +26,42 @@ class QuotaSetsTest(utils.FixturedTestCase): def test_tenant_quotas_get(self): tenant_id = 'test' - self.cs.quotas.get(tenant_id) + q = self.cs.quotas.get(tenant_id) + self.assert_request_id(q, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/os-quota-sets/%s' % tenant_id) def test_user_quotas_get(self): tenant_id = 'test' user_id = 'fake_user' - self.cs.quotas.get(tenant_id, user_id=user_id) + q = self.cs.quotas.get(tenant_id, user_id=user_id) + self.assert_request_id(q, fakes.FAKE_REQUEST_ID_LIST) url = '/os-quota-sets/%s?user_id=%s' % (tenant_id, user_id) self.assert_called('GET', url) def test_tenant_quotas_defaults(self): tenant_id = '97f4c221bff44578b0300df4ef119353' - self.cs.quotas.defaults(tenant_id) + q = self.cs.quotas.defaults(tenant_id) + self.assert_request_id(q, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/os-quota-sets/%s/defaults' % tenant_id) def test_force_update_quota(self): q = self.cs.quotas.get('97f4c221bff44578b0300df4ef119353') - q.update(cores=2, force=True) + qu = q.update(cores=2, force=True) + self.assert_request_id(qu, fakes.FAKE_REQUEST_ID_LIST) self.assert_called( 'PUT', '/os-quota-sets/97f4c221bff44578b0300df4ef119353', {'quota_set': {'force': True, 'cores': 2}}) def test_quotas_delete(self): tenant_id = 'test' - self.cs.quotas.delete(tenant_id) + ret = self.cs.quotas.delete(tenant_id) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('DELETE', '/os-quota-sets/%s' % tenant_id) def test_user_quotas_delete(self): tenant_id = 'test' user_id = 'fake_user' - self.cs.quotas.delete(tenant_id, user_id=user_id) + ret = self.cs.quotas.delete(tenant_id, user_id=user_id) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) url = '/os-quota-sets/%s?user_id=%s' % (tenant_id, user_id) self.assert_called('DELETE', url) diff --git a/novaclient/tests/unit/v2/test_security_group_rules.py b/novaclient/tests/unit/v2/test_security_group_rules.py index 6c0c8ac75..e56e3f13d 100644 --- a/novaclient/tests/unit/v2/test_security_group_rules.py +++ b/novaclient/tests/unit/v2/test_security_group_rules.py @@ -15,6 +15,7 @@ from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit.fixture_data import security_group_rules as data from novaclient.tests.unit import utils +from novaclient.tests.unit.v2 import fakes from novaclient.v2 import security_group_rules @@ -24,12 +25,14 @@ class SecurityGroupRulesTest(utils.FixturedTestCase): data_fixture_class = data.Fixture def test_delete_security_group_rule(self): - self.cs.security_group_rules.delete(1) + ret = self.cs.security_group_rules.delete(1) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('DELETE', '/os-security-group-rules/1') def test_create_security_group_rule(self): sg = self.cs.security_group_rules.create(1, "tcp", 1, 65535, "10.0.0.0/16") + self.assert_request_id(sg, fakes.FAKE_REQUEST_ID_LIST) body = { "security_group_rule": { @@ -48,6 +51,7 @@ def test_create_security_group_rule(self): def test_create_security_group_group_rule(self): sg = self.cs.security_group_rules.create(1, "tcp", 1, 65535, "10.0.0.0/16", 101) + self.assert_request_id(sg, fakes.FAKE_REQUEST_ID_LIST) body = { "security_group_rule": { @@ -80,10 +84,12 @@ def test_invalid_parameters_create(self): def test_security_group_rule_str(self): sg = self.cs.security_group_rules.create(1, "tcp", 1, 65535, "10.0.0.0/16") + self.assert_request_id(sg, fakes.FAKE_REQUEST_ID_LIST) self.assertEqual('1', str(sg)) def test_security_group_rule_del(self): sg = self.cs.security_group_rules.create(1, "tcp", 1, 65535, "10.0.0.0/16") - sg.delete() + ret = sg.delete() + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('DELETE', '/os-security-group-rules/1') diff --git a/novaclient/tests/unit/v2/test_security_groups.py b/novaclient/tests/unit/v2/test_security_groups.py index d05a93aa1..df7c39ae7 100644 --- a/novaclient/tests/unit/v2/test_security_groups.py +++ b/novaclient/tests/unit/v2/test_security_groups.py @@ -14,6 +14,7 @@ from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit.fixture_data import security_groups as data from novaclient.tests.unit import utils +from novaclient.tests.unit.v2 import fakes from novaclient.v2 import security_groups @@ -24,6 +25,7 @@ class SecurityGroupsTest(utils.FixturedTestCase): def _do_test_list_security_groups(self, search_opts, path): sgs = self.cs.security_groups.list(search_opts=search_opts) + self.assert_request_id(sgs, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', path) for sg in sgs: self.assertIsInstance(sg, security_groups.SecurityGroup) @@ -42,27 +44,33 @@ def test_list_security_groups_all_tenants_off(self): def test_get_security_groups(self): sg = self.cs.security_groups.get(1) + self.assert_request_id(sg, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/os-security-groups/1') self.assertIsInstance(sg, security_groups.SecurityGroup) self.assertEqual('1', str(sg)) def test_delete_security_group(self): sg = self.cs.security_groups.list()[0] - sg.delete() + ret = sg.delete() + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('DELETE', '/os-security-groups/1') - self.cs.security_groups.delete(1) + ret = self.cs.security_groups.delete(1) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('DELETE', '/os-security-groups/1') - self.cs.security_groups.delete(sg) + ret = self.cs.security_groups.delete(sg) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('DELETE', '/os-security-groups/1') def test_create_security_group(self): sg = self.cs.security_groups.create("foo", "foo barr") + self.assert_request_id(sg, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/os-security-groups') self.assertIsInstance(sg, security_groups.SecurityGroup) def test_update_security_group(self): sg = self.cs.security_groups.list()[0] secgroup = self.cs.security_groups.update(sg, "update", "update") + self.assert_request_id(secgroup, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('PUT', '/os-security-groups/1') self.assertIsInstance(secgroup, security_groups.SecurityGroup) diff --git a/novaclient/tests/unit/v2/test_server_groups.py b/novaclient/tests/unit/v2/test_server_groups.py index 8648632c0..4ca002df8 100644 --- a/novaclient/tests/unit/v2/test_server_groups.py +++ b/novaclient/tests/unit/v2/test_server_groups.py @@ -16,6 +16,7 @@ from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit.fixture_data import server_groups as data from novaclient.tests.unit import utils +from novaclient.tests.unit.v2 import fakes from novaclient.v2 import server_groups @@ -26,6 +27,7 @@ class ServerGroupsTest(utils.FixturedTestCase): def test_list_server_groups(self): result = self.cs.server_groups.list() + self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/os-server-groups') for server_group in result: self.assertIsInstance(server_group, @@ -33,6 +35,7 @@ def test_list_server_groups(self): def test_list_server_groups_with_all_projects(self): result = self.cs.server_groups.list(all_projects=True) + self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/os-server-groups?all_projects') for server_group in result: self.assertIsInstance(server_group, @@ -42,6 +45,7 @@ def test_create_server_group(self): kwargs = {'name': 'ig1', 'policies': ['anti-affinity']} server_group = self.cs.server_groups.create(**kwargs) + self.assert_request_id(server_group, fakes.FAKE_REQUEST_ID_LIST) body = {'server_group': kwargs} self.assert_called('POST', '/os-server-groups', body) self.assertIsInstance(server_group, @@ -50,17 +54,20 @@ def test_create_server_group(self): def test_get_server_group(self): id = '2cbd51f4-fafe-4cdb-801b-cf913a6f288b' server_group = self.cs.server_groups.get(id) + self.assert_request_id(server_group, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/os-server-groups/%s' % id) self.assertIsInstance(server_group, server_groups.ServerGroup) def test_delete_server_group(self): id = '2cbd51f4-fafe-4cdb-801b-cf913a6f288b' - self.cs.server_groups.delete(id) + ret = self.cs.server_groups.delete(id) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('DELETE', '/os-server-groups/%s' % id) def test_delete_server_group_object(self): id = '2cbd51f4-fafe-4cdb-801b-cf913a6f288b' server_group = self.cs.server_groups.get(id) - server_group.delete() + ret = server_group.delete() + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('DELETE', '/os-server-groups/%s' % id) diff --git a/novaclient/tests/unit/v2/test_services.py b/novaclient/tests/unit/v2/test_services.py index f182e2d1c..16f068e7b 100644 --- a/novaclient/tests/unit/v2/test_services.py +++ b/novaclient/tests/unit/v2/test_services.py @@ -33,6 +33,7 @@ def _get_service_type(self): def test_list_services(self): svs = self.cs.services.list() + self.assert_request_id(svs, fakes.FAKE_REQUEST_ID_LIST) self.cs.assert_called('GET', '/os-services') for s in svs: self.assertIsInstance(s, self._get_service_type()) @@ -42,6 +43,7 @@ def test_list_services(self): def test_list_services_with_hostname(self): svs = self.cs.services.list(host='host2') + self.assert_request_id(svs, fakes.FAKE_REQUEST_ID_LIST) self.cs.assert_called('GET', '/os-services?host=host2') for s in svs: self.assertIsInstance(s, self._get_service_type()) @@ -50,6 +52,7 @@ def test_list_services_with_hostname(self): def test_list_services_with_binary(self): svs = self.cs.services.list(binary='nova-cert') + self.assert_request_id(svs, fakes.FAKE_REQUEST_ID_LIST) self.cs.assert_called('GET', '/os-services?binary=nova-cert') for s in svs: self.assertIsInstance(s, self._get_service_type()) @@ -58,6 +61,7 @@ def test_list_services_with_binary(self): def test_list_services_with_host_binary(self): svs = self.cs.services.list(host='host2', binary='nova-cert') + self.assert_request_id(svs, fakes.FAKE_REQUEST_ID_LIST) self.cs.assert_called('GET', '/os-services?host=host2&binary=nova-cert') for s in svs: @@ -74,17 +78,20 @@ def _update_body(self, host, binary, disabled_reason=None): def test_services_enable(self): service = self.cs.services.enable('host1', 'nova-cert') + self.assert_request_id(service, fakes.FAKE_REQUEST_ID_LIST) values = self._update_body("host1", "nova-cert") self.cs.assert_called('PUT', '/os-services/enable', values) self.assertIsInstance(service, self._get_service_type()) self.assertEqual('enabled', service.status) def test_services_delete(self): - self.cs.services.delete('1') + ret = self.cs.services.delete('1') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.cs.assert_called('DELETE', '/os-services/1') def test_services_disable(self): service = self.cs.services.disable('host1', 'nova-cert') + self.assert_request_id(service, fakes.FAKE_REQUEST_ID_LIST) values = self._update_body("host1", "nova-cert") self.cs.assert_called('PUT', '/os-services/disable', values) self.assertIsInstance(service, self._get_service_type()) @@ -93,6 +100,7 @@ def test_services_disable(self): def test_services_disable_log_reason(self): service = self.cs.services.disable_log_reason( 'compute1', 'nova-compute', 'disable bad host') + self.assert_request_id(service, fakes.FAKE_REQUEST_ID_LIST) values = self._update_body("compute1", "nova-compute", "disable bad host") self.cs.assert_called('PUT', '/os-services/disable-log-reason', values) @@ -118,6 +126,7 @@ def _update_body(self, host, binary, disabled_reason=None, def test_services_force_down(self): service = self.cs.services.force_down( 'compute1', 'nova-compute', False) + self.assert_request_id(service, fakes.FAKE_REQUEST_ID_LIST) values = self._update_body("compute1", "nova-compute", force_down=False) self.cs.assert_called('PUT', '/os-services/force-down', values) diff --git a/novaclient/tests/unit/v2/test_usage.py b/novaclient/tests/unit/v2/test_usage.py index 768d98ce4..61a8c2cb1 100644 --- a/novaclient/tests/unit/v2/test_usage.py +++ b/novaclient/tests/unit/v2/test_usage.py @@ -33,6 +33,7 @@ def _get_usage_type(self): def test_usage_list(self, detailed=False): now = datetime.datetime.now() usages = self.cs.usage.list(now, now, detailed) + self.assert_request_id(usages, fakes.FAKE_REQUEST_ID_LIST) self.cs.assert_called( 'GET', @@ -49,6 +50,7 @@ def test_usage_list_detailed(self): def test_usage_get(self): now = datetime.datetime.now() u = self.cs.usage.get("tenantfoo", now, now) + self.assert_request_id(u, fakes.FAKE_REQUEST_ID_LIST) self.cs.assert_called( 'GET', diff --git a/novaclient/tests/unit/v2/test_versions.py b/novaclient/tests/unit/v2/test_versions.py index d0397f167..f88f534b7 100644 --- a/novaclient/tests/unit/v2/test_versions.py +++ b/novaclient/tests/unit/v2/test_versions.py @@ -30,13 +30,15 @@ def setUp(self): @mock.patch.object(versions.VersionManager, '_is_session_client', return_value=False) def test_list_services_with_http_client(self, mock_is_session_client): - self.cs.versions.list() + vl = self.cs.versions.list() + self.assert_request_id(vl, fakes.FAKE_REQUEST_ID_LIST) self.cs.assert_called('GET', None) @mock.patch.object(versions.VersionManager, '_is_session_client', return_value=True) def test_list_services_with_session_client(self, mock_is_session_client): - self.cs.versions.list() + vl = self.cs.versions.list() + self.assert_request_id(vl, fakes.FAKE_REQUEST_ID_LIST) self.cs.assert_called('GET', 'http://nova-api:8774/') @mock.patch.object(versions.VersionManager, '_is_session_client', @@ -44,6 +46,8 @@ def test_list_services_with_session_client(self, mock_is_session_client): @mock.patch.object(versions.VersionManager, 'list') def test_get_current_with_http_client(self, mock_list, mock_is_session_client): + headers = {'x-openstack-request-id': fakes.FAKE_REQUEST_ID} + resp = utils.TestResponse({"headers": headers}) current_version = versions.Version( None, {"links": [{"href": "http://nova-api:8774/v2.1"}]}, loaded=True) @@ -58,14 +62,17 @@ def test_get_current_with_http_client(self, mock_list, current_version, versions.Version( None, {"links": [{"href": "http://url/v21"}]}, loaded=True)] - mock_list.return_value = base.ListWithMeta(all_versions, None) - self.assertEqual(current_version, self.cs.versions.get_current()) + mock_list.return_value = base.ListWithMeta(all_versions, resp) + v = self.cs.versions.get_current() + self.assert_request_id(v, fakes.FAKE_REQUEST_ID_LIST) + self.assertEqual(current_version, v) @mock.patch.object(versions.VersionManager, '_is_session_client', return_value=True) def test_get_current_with_session_client(self, mock_is_session_client): self.cs.callback = [] - self.cs.versions.get_current() + v = self.cs.versions.get_current() + self.assert_request_id(v, fakes.FAKE_REQUEST_ID_LIST) self.cs.assert_called('GET', 'http://nova-api:8774/v2.1/') @mock.patch.object(versions.VersionManager, '_is_session_client', @@ -94,6 +101,7 @@ def test_get_endpoint_without_project_id(self, mock_is_session_client): cs_2_1 = fakes.FakeClient(endpoint_type=endpoint_type) result = cs_2_1.versions.get_current() + self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) self.assertEqual(result.manager.api.client.endpoint_type, endpoint_type, "Check endpoint_type was set") self.assertEqual(result.manager.api.client.management_url, @@ -112,6 +120,7 @@ def test_v2_get_endpoint_without_project_id(self, mock_is_session_client): cs_2 = fakes.FakeClient(endpoint_type=endpoint_type) result = cs_2.versions.get_current() + self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) self.assertEqual(result.manager.api.client.endpoint_type, endpoint_type, "Check v2 endpoint_type was set") self.assertEqual(result.manager.api.client.management_url, diff --git a/novaclient/v2/keypairs.py b/novaclient/v2/keypairs.py index 70ab8da57..505435c6c 100644 --- a/novaclient/v2/keypairs.py +++ b/novaclient/v2/keypairs.py @@ -48,7 +48,12 @@ def id(self): return self.name def delete(self): - self.manager.delete(self) + """ + Delete this keypair. + + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self.manager.delete(self) class KeypairManager(base.ManagerWithFind): @@ -133,8 +138,9 @@ def delete(self, key): Delete a keypair :param key: The :class:`Keypair` (or its ID) to delete. + :returns: An instance of novaclient.base.TupleWithMeta """ - self._delete('/%s/%s' % (self.keypair_prefix, base.getid(key))) + return self._delete('/%s/%s' % (self.keypair_prefix, base.getid(key))) @api_versions.wraps("2.10") def delete(self, key, user_id=None): @@ -143,10 +149,11 @@ def delete(self, key, user_id=None): :param key: The :class:`Keypair` (or its ID) to delete. :param user_id: Id of key-pair owner (Admin only). + :returns: An instance of novaclient.base.TupleWithMeta """ query_string = "?user_id=%s" % user_id if user_id else "" url = '/%s/%s%s' % (self.keypair_prefix, base.getid(key), query_string) - self._delete(url) + return self._delete(url) @api_versions.wraps("2.0", "2.9") def list(self): diff --git a/novaclient/v2/networks.py b/novaclient/v2/networks.py index 06dee2704..22f3d537c 100644 --- a/novaclient/v2/networks.py +++ b/novaclient/v2/networks.py @@ -33,7 +33,12 @@ def __repr__(self): return "" % self.label def delete(self): - self.manager.delete(self) + """ + Delete this network. + + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self.manager.delete(self) class NetworkManager(base.ManagerWithFind): @@ -65,8 +70,9 @@ def delete(self, network): Delete a specific network. :param network: The ID of the :class:`Network` to delete. + :returns: An instance of novaclient.base.TupleWithMeta """ - self._delete("/os-networks/%s" % base.getid(network)) + return self._delete("/os-networks/%s" % base.getid(network)) def create(self, **kwargs): """ @@ -109,6 +115,7 @@ def disassociate(self, network, disassociate_host=True, :param network: The ID of the :class:`Network`. :param disassociate_host: Whether to disassociate the host :param disassociate_project: Whether to disassociate the project + :returns: An instance of novaclient.base.TupleWithMeta """ if disassociate_host and disassociate_project: body = {"disassociate": None} @@ -120,8 +127,10 @@ def disassociate(self, network, disassociate_host=True, raise exceptions.CommandError( _("Must disassociate either host or project or both")) - self.api.client.post("/os-networks/%s/action" % - base.getid(network), body=body) + resp, body = self.api.client.post("/os-networks/%s/action" % + base.getid(network), body=body) + + return self.convert_into_with_meta(body, resp) def associate_host(self, network, host): """ @@ -129,10 +138,13 @@ def associate_host(self, network, host): :param network: The ID of the :class:`Network`. :param host: The name of the host to associate the network with + :returns: An instance of novaclient.base.TupleWithMeta """ - self.api.client.post("/os-networks/%s/action" % - base.getid(network), - body={"associate_host": host}) + resp, body = self.api.client.post("/os-networks/%s/action" % + base.getid(network), + body={"associate_host": host}) + + return self.convert_into_with_meta(body, resp) def associate_project(self, network): """ @@ -141,8 +153,12 @@ def associate_project(self, network): The project is defined by the project authenticated against :param network: The ID of the :class:`Network`. + :returns: An instance of novaclient.base.TupleWithMeta """ - self.api.client.post("/os-networks/add", body={"id": network}) + resp, body = self.api.client.post("/os-networks/add", + body={"id": network}) + + return self.convert_into_with_meta(body, resp) def add(self, network=None): """ @@ -150,7 +166,10 @@ def add(self, network=None): automatically or provided explicitly. :param network: The ID of the :class:`Network` to associate (optional). + :returns: An instance of novaclient.base.TupleWithMeta """ - self.api.client.post( - "/os-networks/add", - body={"id": base.getid(network) if network else None}) + resp, body = self.api.client.post("/os-networks/add", + body={"id": base.getid(network) + if network else None}) + + return self.convert_into_with_meta(body, resp) diff --git a/novaclient/v2/quotas.py b/novaclient/v2/quotas.py index a77e16e27..e05435b35 100644 --- a/novaclient/v2/quotas.py +++ b/novaclient/v2/quotas.py @@ -61,8 +61,15 @@ def defaults(self, tenant_id): 'quota_set') def delete(self, tenant_id, user_id=None): + """ + Delete quota for a tenant or for a user. + + :param tenant_id: A tenant for which quota is to be deleted + :param user_id: A user for which quota is to be deleted + :returns: An instance of novaclient.base.TupleWithMeta + """ if user_id: url = '/os-quota-sets/%s?user_id=%s' % (tenant_id, user_id) else: url = '/os-quota-sets/%s' % tenant_id - self._delete(url) + return self._delete(url) diff --git a/novaclient/v2/security_group_default_rules.py b/novaclient/v2/security_group_default_rules.py index d82a1d818..6516a460d 100644 --- a/novaclient/v2/security_group_default_rules.py +++ b/novaclient/v2/security_group_default_rules.py @@ -24,7 +24,12 @@ def __str__(self): return str(self.id) def delete(self): - self.manager.delete(self) + """ + Delete this security group default rule. + + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self.manager.delete(self) class SecurityGroupDefaultRuleManager(base.Manager): @@ -67,8 +72,10 @@ def delete(self, rule): Delete a security group default rule :param rule: The security group default rule to delete (ID or Class) + :returns: An instance of novaclient.base.TupleWithMeta """ - self._delete('/os-security-group-default-rules/%s' % base.getid(rule)) + return self._delete('/os-security-group-default-rules/%s' % + base.getid(rule)) def list(self): """ diff --git a/novaclient/v2/security_group_rules.py b/novaclient/v2/security_group_rules.py index 4cb51b01b..c10603721 100644 --- a/novaclient/v2/security_group_rules.py +++ b/novaclient/v2/security_group_rules.py @@ -27,7 +27,12 @@ def __str__(self): return str(self.id) def delete(self): - self.manager.delete(self) + """ + Delete this security group rule. + + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self.manager.delete(self) class SecurityGroupRuleManager(base.Manager): @@ -74,5 +79,6 @@ def delete(self, rule): Delete a security group rule :param rule: The security group rule to delete (ID or Class) + :returns: An instance of novaclient.base.TupleWithMeta """ - self._delete('/os-security-group-rules/%s' % base.getid(rule)) + return self._delete('/os-security-group-rules/%s' % base.getid(rule)) diff --git a/novaclient/v2/security_groups.py b/novaclient/v2/security_groups.py index 40d1e7ff7..a6bf63794 100644 --- a/novaclient/v2/security_groups.py +++ b/novaclient/v2/security_groups.py @@ -28,10 +28,20 @@ def __str__(self): return str(self.id) def delete(self): - self.manager.delete(self) + """ + Delete this security group. + + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self.manager.delete(self) def update(self): - self.manager.update(self) + """ + Update this security group. + + :returns: :class:`SecurityGroup` + """ + return self.manager.update(self) class SecurityGroupManager(base.ManagerWithFind): @@ -66,9 +76,9 @@ def delete(self, group): Delete a security group :param group: The security group to delete (group or ID) - :rtype: None + :returns: An instance of novaclient.base.TupleWithMeta """ - self._delete('/os-security-groups/%s' % base.getid(group)) + return self._delete('/os-security-groups/%s' % base.getid(group)) def get(self, group_id): """ diff --git a/novaclient/v2/server_groups.py b/novaclient/v2/server_groups.py index c699c65a7..9b0ae870d 100644 --- a/novaclient/v2/server_groups.py +++ b/novaclient/v2/server_groups.py @@ -30,7 +30,12 @@ def __repr__(self): return '' % self.id def delete(self): - self.manager.delete(self.id) + """ + Delete this server group. + + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self.manager.delete(self.id) class ServerGroupsManager(base.ManagerWithFind): @@ -60,8 +65,9 @@ def delete(self, id): """Delete a specific server group. :param id: The ID of the :class:`ServerGroup` to delete. + :returns: An instance of novaclient.base.TupleWithMeta """ - self._delete('/os-server-groups/%s' % id) + return self._delete('/os-server-groups/%s' % id) def create(self, **kwargs): """Create (allocate) a server group. diff --git a/novaclient/v2/versions.py b/novaclient/v2/versions.py index ee33bd93a..905da0731 100644 --- a/novaclient/v2/versions.py +++ b/novaclient/v2/versions.py @@ -76,6 +76,7 @@ def _get_current(self): for version in all_versions: for link in version.links: if link["href"].rstrip('/') == url: + version.append_request_ids(all_versions.request_ids) return version def get_current(self): From 9f10d7d0b0fc03ea6843c61519db020fab77f9e0 Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Fri, 25 Dec 2015 12:45:49 +0900 Subject: [PATCH 0969/1705] Add return-request-id-to-caller function(4/5) Add return-request-id-to-caller function to resources and resource managers in the following files. The methods in the resource class and resource manager return a wrapper class that has 'request_ids' property. The caller can get request ids of the callee via the property. * novaclient/v2/contrib/assisted_volume_snapshots.py * novaclient/v2/contrib/baremetal.py * novaclient/v2/contrib/cells.py * novaclient/v2/contrib/instance_action.py * novaclient/v2/contrib/list_extensions.py * novaclient/v2/contrib/migrations.py * novaclient/v2/contrib/server_external_events.py * novaclient/v2/contrib/tenant_networks.py Co-authored-by: Ankit Agrawal Change-Id: I58ec61eb585d145cb7d638bcb690b3ebee0461e9 Implements: blueprint return-request-id-to-caller --- novaclient/tests/unit/v2/contrib/fakes.py | 30 +++++++++++-------- .../contrib/test_assisted_volume_snapshots.py | 6 ++-- .../tests/unit/v2/contrib/test_baremetal.py | 13 ++++++-- .../tests/unit/v2/contrib/test_cells.py | 9 ++++-- .../unit/v2/contrib/test_instance_actions.py | 6 ++-- .../unit/v2/contrib/test_list_extensions.py | 1 + .../tests/unit/v2/contrib/test_migrations.py | 2 ++ .../v2/contrib/test_server_external_events.py | 1 + .../unit/v2/contrib/test_tenant_networks.py | 12 +++++--- novaclient/tests/unit/v2/fakes.py | 12 ++++---- .../v2/contrib/assisted_volume_snapshots.py | 15 ++++++++-- novaclient/v2/contrib/baremetal.py | 18 ++++++----- novaclient/v2/contrib/tenant_networks.py | 15 ++++++++-- 13 files changed, 95 insertions(+), 45 deletions(-) diff --git a/novaclient/tests/unit/v2/contrib/fakes.py b/novaclient/tests/unit/v2/contrib/fakes.py index 8e82f9c34..921d87ebd 100644 --- a/novaclient/tests/unit/v2/contrib/fakes.py +++ b/novaclient/tests/unit/v2/contrib/fakes.py @@ -15,6 +15,9 @@ from novaclient.tests.unit.v2 import fakes from novaclient.v2 import client +FAKE_REQUEST_ID_LIST = fakes.FAKE_REQUEST_ID_LIST +FAKE_RESPONSE_HEADERS = fakes.FAKE_RESPONSE_HEADERS + class FakeClient(fakes.FakeClient): def __init__(self, *args, **kwargs): @@ -27,29 +30,29 @@ def __init__(self, *args, **kwargs): class FakeHTTPClient(fakes.FakeHTTPClient): def get_os_tenant_networks(self): - return (200, {}, { + return (200, FAKE_RESPONSE_HEADERS, { 'networks': [{"label": "1", "cidr": "10.0.0.0/24", 'project_id': '4ffc664c198e435e9853f2538fbcd7a7', 'id': '1'}]}) def get_os_tenant_networks_1(self, **kw): - return (200, {}, { + return (200, FAKE_RESPONSE_HEADERS, { 'network': {"label": "1", "cidr": "10.0.0.0/24", 'project_id': '4ffc664c198e435e9853f2538fbcd7a7', 'id': '1'}}) def post_os_tenant_networks(self, **kw): - return (201, {}, { + return (201, FAKE_RESPONSE_HEADERS, { 'network': {"label": "1", "cidr": "10.0.0.0/24", 'project_id': '4ffc664c198e435e9853f2538fbcd7a7', 'id': '1'}}) def delete_os_tenant_networks_1(self, **kw): - return (204, {}, None) + return (204, FAKE_RESPONSE_HEADERS, None) def get_os_baremetal_nodes(self, **kw): return ( - 200, {}, { + 200, FAKE_RESPONSE_HEADERS, { 'nodes': [ { "id": 1, @@ -72,7 +75,7 @@ def get_os_baremetal_nodes(self, **kw): def get_os_baremetal_nodes_1(self, **kw): return ( - 200, {}, { + 200, FAKE_RESPONSE_HEADERS, { 'node': { "id": 1, "instance_uuid": None, @@ -93,7 +96,7 @@ def get_os_baremetal_nodes_1(self, **kw): def post_os_baremetal_nodes(self, **kw): return ( - 200, {}, { + 200, FAKE_RESPONSE_HEADERS, { 'node': { "id": 1, "instance_uuid": None, @@ -112,14 +115,14 @@ def post_os_baremetal_nodes(self, **kw): ) def delete_os_baremetal_nodes_1(self, **kw): - return (202, {}, {}) + return (202, FAKE_RESPONSE_HEADERS, {}) def post_os_baremetal_nodes_1_action(self, **kw): body = kw['body'] action = list(body)[0] if action == "add_interface": return ( - 200, {}, { + 200, FAKE_RESPONSE_HEADERS, { 'interface': { "id": 2, "address": "bb:cc:dd:ee:ff:aa", @@ -129,18 +132,19 @@ def post_os_baremetal_nodes_1_action(self, **kw): } ) elif action == "remove_interface": - return (202, {}, {}) + return (202, FAKE_RESPONSE_HEADERS, {}) else: return (500, {}, {}) def post_os_assisted_volume_snapshots(self, **kw): - return (202, {}, {'snapshot': {'id': 'blah', 'volumeId': '1'}}) + return (202, FAKE_RESPONSE_HEADERS, + {'snapshot': {'id': 'blah', 'volumeId': '1'}}) def delete_os_assisted_volume_snapshots_x(self, **kw): - return (202, {}, {}) + return (202, FAKE_RESPONSE_HEADERS, {}) def post_os_server_external_events(self, **kw): - return (200, {}, { + return (200, FAKE_RESPONSE_HEADERS, { 'events': [ {'name': 'test-event', 'status': 'completed', diff --git a/novaclient/tests/unit/v2/contrib/test_assisted_volume_snapshots.py b/novaclient/tests/unit/v2/contrib/test_assisted_volume_snapshots.py index 8ab732aed..8cce98664 100644 --- a/novaclient/tests/unit/v2/contrib/test_assisted_volume_snapshots.py +++ b/novaclient/tests/unit/v2/contrib/test_assisted_volume_snapshots.py @@ -32,11 +32,13 @@ class AssistedVolumeSnapshotsTestCase(utils.TestCase): def test_create_snap(self): - cs.assisted_volume_snapshots.create('1', {}) + vs = cs.assisted_volume_snapshots.create('1', {}) + self.assert_request_id(vs, fakes.FAKE_REQUEST_ID_LIST) cs.assert_called('POST', '/os-assisted-volume-snapshots') def test_delete_snap(self): - cs.assisted_volume_snapshots.delete('x', {}) + vs = cs.assisted_volume_snapshots.delete('x', {}) + self.assert_request_id(vs, fakes.FAKE_REQUEST_ID_LIST) cs.assert_called( 'DELETE', '/os-assisted-volume-snapshots/x?delete_info={}') diff --git a/novaclient/tests/unit/v2/contrib/test_baremetal.py b/novaclient/tests/unit/v2/contrib/test_baremetal.py index a5ee41467..e44cd140e 100644 --- a/novaclient/tests/unit/v2/contrib/test_baremetal.py +++ b/novaclient/tests/unit/v2/contrib/test_baremetal.py @@ -30,35 +30,42 @@ class BaremetalExtensionTest(utils.TestCase): def test_list_nodes(self): nl = cs.baremetal.list() + self.assert_request_id(nl, fakes.FAKE_REQUEST_ID_LIST) cs.assert_called('GET', '/os-baremetal-nodes') for n in nl: self.assertIsInstance(n, baremetal.BareMetalNode) def test_get_node(self): n = cs.baremetal.get(1) + self.assert_request_id(n, fakes.FAKE_REQUEST_ID_LIST) cs.assert_called('GET', '/os-baremetal-nodes/1') self.assertIsInstance(n, baremetal.BareMetalNode) def test_create_node(self): n = cs.baremetal.create("service_host", 1, 1024, 2048, "aa:bb:cc:dd:ee:ff") + self.assert_request_id(n, fakes.FAKE_REQUEST_ID_LIST) cs.assert_called('POST', '/os-baremetal-nodes') self.assertIsInstance(n, baremetal.BareMetalNode) def test_delete_node(self): n = cs.baremetal.get(1) - cs.baremetal.delete(n) + ret = cs.baremetal.delete(n) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) cs.assert_called('DELETE', '/os-baremetal-nodes/1') def test_node_add_interface(self): i = cs.baremetal.add_interface(1, "bb:cc:dd:ee:ff:aa", 1, 2) + self.assert_request_id(i, fakes.FAKE_REQUEST_ID_LIST) cs.assert_called('POST', '/os-baremetal-nodes/1/action') self.assertIsInstance(i, baremetal.BareMetalNodeInterface) def test_node_remove_interface(self): - cs.baremetal.remove_interface(1, "bb:cc:dd:ee:ff:aa") + ret = cs.baremetal.remove_interface(1, "bb:cc:dd:ee:ff:aa") + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) cs.assert_called('POST', '/os-baremetal-nodes/1/action') def test_node_list_interfaces(self): - cs.baremetal.list_interfaces(1) + il = cs.baremetal.list_interfaces(1) + self.assert_request_id(il, fakes.FAKE_REQUEST_ID_LIST) cs.assert_called('GET', '/os-baremetal-nodes/1') diff --git a/novaclient/tests/unit/v2/contrib/test_cells.py b/novaclient/tests/unit/v2/contrib/test_cells.py index e411502bc..dd242c2b7 100644 --- a/novaclient/tests/unit/v2/contrib/test_cells.py +++ b/novaclient/tests/unit/v2/contrib/test_cells.py @@ -29,14 +29,17 @@ class CellsExtensionTests(utils.TestCase): def test_get_cells(self): cell_name = 'child_cell' - cs.cells.get(cell_name) + cell = cs.cells.get(cell_name) + self.assert_request_id(cell, fakes.FAKE_REQUEST_ID_LIST) cs.assert_called('GET', '/os-cells/%s' % cell_name) def test_get_capacities_for_a_given_cell(self): cell_name = 'child_cell' - cs.cells.capacities(cell_name) + ca = cs.cells.capacities(cell_name) + self.assert_request_id(ca, fakes.FAKE_REQUEST_ID_LIST) cs.assert_called('GET', '/os-cells/%s/capacities' % cell_name) def test_get_capacities_for_all_cells(self): - cs.cells.capacities() + ca = cs.cells.capacities() + self.assert_request_id(ca, fakes.FAKE_REQUEST_ID_LIST) cs.assert_called('GET', '/os-cells/capacities') diff --git a/novaclient/tests/unit/v2/contrib/test_instance_actions.py b/novaclient/tests/unit/v2/contrib/test_instance_actions.py index 0b0400a29..4f17fbd59 100644 --- a/novaclient/tests/unit/v2/contrib/test_instance_actions.py +++ b/novaclient/tests/unit/v2/contrib/test_instance_actions.py @@ -29,7 +29,8 @@ class InstanceActionExtensionTests(utils.TestCase): def test_list_instance_actions(self): server_uuid = '1234' - cs.instance_action.list(server_uuid) + ial = cs.instance_action.list(server_uuid) + self.assert_request_id(ial, fakes.FAKE_REQUEST_ID_LIST) cs.assert_called( 'GET', '/servers/%s/os-instance-actions' % server_uuid) @@ -37,7 +38,8 @@ def test_list_instance_actions(self): def test_get_instance_action(self): server_uuid = '1234' request_id = 'req-abcde12345' - cs.instance_action.get(server_uuid, request_id) + ia = cs.instance_action.get(server_uuid, request_id) + self.assert_request_id(ia, fakes.FAKE_REQUEST_ID_LIST) cs.assert_called( 'GET', '/servers/%s/os-instance-actions/%s' % (server_uuid, request_id)) diff --git a/novaclient/tests/unit/v2/contrib/test_list_extensions.py b/novaclient/tests/unit/v2/contrib/test_list_extensions.py index 3fa5253ba..3c7729397 100644 --- a/novaclient/tests/unit/v2/contrib/test_list_extensions.py +++ b/novaclient/tests/unit/v2/contrib/test_list_extensions.py @@ -27,6 +27,7 @@ class ListExtensionsTests(utils.TestCase): def test_list_extensions(self): all_exts = cs.list_extensions.show_all() + self.assert_request_id(all_exts, fakes.FAKE_REQUEST_ID_LIST) cs.assert_called('GET', '/extensions') self.assertTrue(len(all_exts) > 0) for r in all_exts: diff --git a/novaclient/tests/unit/v2/contrib/test_migrations.py b/novaclient/tests/unit/v2/contrib/test_migrations.py index 881fd1e50..29cac5d44 100644 --- a/novaclient/tests/unit/v2/contrib/test_migrations.py +++ b/novaclient/tests/unit/v2/contrib/test_migrations.py @@ -26,12 +26,14 @@ class MigrationsTest(utils.TestCase): def test_list_migrations(self): ml = cs.migrations.list() + self.assert_request_id(ml, fakes.FAKE_REQUEST_ID_LIST) cs.assert_called('GET', '/os-migrations') for m in ml: self.assertIsInstance(m, migrations.Migration) def test_list_migrations_with_filters(self): ml = cs.migrations.list('host1', 'finished', 'child1') + self.assert_request_id(ml, fakes.FAKE_REQUEST_ID_LIST) cs.assert_called('GET', '/os-migrations?cell_name=child1&host=host1' diff --git a/novaclient/tests/unit/v2/contrib/test_server_external_events.py b/novaclient/tests/unit/v2/contrib/test_server_external_events.py index 1b941c9d6..b843bd8c1 100644 --- a/novaclient/tests/unit/v2/contrib/test_server_external_events.py +++ b/novaclient/tests/unit/v2/contrib/test_server_external_events.py @@ -40,5 +40,6 @@ def test_external_event(self): 'status': 'completed', 'tag': 'tag'}] result = cs.server_external_events.create(events) + self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) self.assertEqual(events, result) cs.assert_called('POST', '/os-server-external-events') diff --git a/novaclient/tests/unit/v2/contrib/test_tenant_networks.py b/novaclient/tests/unit/v2/contrib/test_tenant_networks.py index 13159ce5f..2f0d7e3f9 100644 --- a/novaclient/tests/unit/v2/contrib/test_tenant_networks.py +++ b/novaclient/tests/unit/v2/contrib/test_tenant_networks.py @@ -29,18 +29,22 @@ class TenantNetworkExtensionTests(utils.TestCase): def test_list_tenant_networks(self): nets = cs.tenant_networks.list() + self.assert_request_id(nets, fakes.FAKE_REQUEST_ID_LIST) cs.assert_called('GET', '/os-tenant-networks') self.assertTrue(len(nets) > 0) def test_get_tenant_network(self): - cs.tenant_networks.get(1) + net = cs.tenant_networks.get(1) + self.assert_request_id(net, fakes.FAKE_REQUEST_ID_LIST) cs.assert_called('GET', '/os-tenant-networks/1') def test_create_tenant_networks(self): - cs.tenant_networks.create(label="net", - cidr="10.0.0.0/24") + net = cs.tenant_networks.create(label="net", + cidr="10.0.0.0/24") + self.assert_request_id(net, fakes.FAKE_REQUEST_ID_LIST) cs.assert_called('POST', '/os-tenant-networks') def test_delete_tenant_networks(self): - cs.tenant_networks.delete(1) + ret = cs.tenant_networks.delete(1) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) cs.assert_called('DELETE', '/os-tenant-networks/1') diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 50192d0a4..2958d33e7 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -296,7 +296,7 @@ def get_extensions(self, **kw): "updated": "2011-11-03T00:00:00+00:00" }, ] - return (200, {}, { + return (200, FAKE_RESPONSE_HEADERS, { "extensions": exts, }) @@ -2304,7 +2304,7 @@ def delete_servers_1234_os_volume_attachments_Work(self, **kw): return (200, {}, {}) def get_servers_1234_os_instance_actions(self, **kw): - return (200, {}, { + return (200, FAKE_RESPONSE_HEADERS, { "instanceActions": [{"instance_uuid": "1234", "user_id": "b968c25e04ab405f9fe4e6ca54cce9a5", @@ -2315,7 +2315,7 @@ def get_servers_1234_os_instance_actions(self, **kw): "project_id": "04019601fe3648c0abd4f4abfb9e6106"}]}) def get_servers_1234_os_instance_actions_req_abcde12345(self, **kw): - return (200, {}, { + return (200, FAKE_RESPONSE_HEADERS, { "instanceAction": {"instance_uuid": "1234", "user_id": "b968c25e04ab405f9fe4e6ca54cce9a5", @@ -2352,7 +2352,7 @@ def get_os_cells_child_cell(self, **kw): 'rpc_port': 5673, 'loaded': True }} - return (200, {}, cell) + return (200, FAKE_RESPONSE_HEADERS, cell) def get_os_cells_capacities(self, **kw): cell_capacities_response = {"cell": {"capacities": {"ram_free": { @@ -2360,7 +2360,7 @@ def get_os_cells_capacities(self, **kw): "16384": 0}, "total_mb": 7680}, "disk_free": { "units_by_mb": {"81920": 11, "20480": 46, "40960": 23, "163840": 5, "0": 0}, "total_mb": 1052672}}}} - return (200, {}, cell_capacities_response) + return (200, FAKE_RESPONSE_HEADERS, cell_capacities_response) def get_os_cells_child_cell_capacities(self, **kw): return self.get_os_cells_capacities() @@ -2381,7 +2381,7 @@ def get_os_migrations(self, **kw): "status": "Done", "updated_at": "2012-10-29T13:42:02.000000" }]} - return (200, {}, migrations) + return (200, FAKE_RESPONSE_HEADERS, migrations) def post_os_server_external_events(self, **kw): return (200, {}, {'events': [ diff --git a/novaclient/v2/contrib/assisted_volume_snapshots.py b/novaclient/v2/contrib/assisted_volume_snapshots.py index ce1a0413c..a6c6a163d 100644 --- a/novaclient/v2/contrib/assisted_volume_snapshots.py +++ b/novaclient/v2/contrib/assisted_volume_snapshots.py @@ -28,8 +28,10 @@ def __repr__(self): def delete(self): """ Delete this snapshot. + + :returns: An instance of novaclient.base.TupleWithMeta """ - self.manager.delete(self) + return self.manager.delete(self) class AssistedSnapshotManager(base.Manager): @@ -41,8 +43,15 @@ def create(self, volume_id, create_info): return self._create('/os-assisted-volume-snapshots', body, 'snapshot') def delete(self, snapshot, delete_info): - self._delete("/os-assisted-volume-snapshots/%s?delete_info=%s" % ( - base.getid(snapshot), json.dumps(delete_info))) + """ + Delete a specified assisted volume snapshot. + + :param snapshot: an assisted volume snapshot to delete + :param delete_info: Information for snapshot deletion + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self._delete("/os-assisted-volume-snapshots/%s?delete_info=%s" % + (base.getid(snapshot), json.dumps(delete_info))) manager_class = AssistedSnapshotManager name = 'assisted_volume_snapshots' diff --git a/novaclient/v2/contrib/baremetal.py b/novaclient/v2/contrib/baremetal.py index d23a38e53..e03e20f1b 100644 --- a/novaclient/v2/contrib/baremetal.py +++ b/novaclient/v2/contrib/baremetal.py @@ -88,8 +88,9 @@ def delete(self, node): Delete a baremetal node. :param node: The :class:`BareMetalNode` to delete. + :returns: An instance of novaclient.base.TupleWithMeta """ - self._delete('/os-baremetal-nodes/%s' % base.getid(node)) + return self._delete('/os-baremetal-nodes/%s' % base.getid(node)) def get(self, node_id): """ @@ -122,8 +123,8 @@ def add_interface(self, node_id, address, datapath_id=0, port_no=0): 'datapath_id': datapath_id, 'port_no': port_no}} url = '/os-baremetal-nodes/%s/action' % node_id - _resp, body = self.api.client.post(url, body=body) - return BareMetalNodeInterface(self, body['interface']) + resp, body = self.api.client.post(url, body=body) + return BareMetalNodeInterface(self, body['interface'], resp=resp) def remove_interface(self, node_id, address): """ @@ -131,21 +132,24 @@ def remove_interface(self, node_id, address): :param node_id: The ID of the node to modify. :param address: The MAC address to remove. - :rtype: bool + :returns: An instance of novaclient.base.TupleWithMeta """ req_body = {'remove_interface': {'address': address}} url = '/os-baremetal-nodes/%s/action' % node_id - self.api.client.post(url, body=req_body) + resp, body = self.api.client.post(url, body=req_body) + + return self.convert_into_with_meta(body, resp) def list_interfaces(self, node_id): """ List the interfaces on a baremetal node. :param node_id: The ID of the node to list. - :rtype: list + :rtype: novaclient.base.ListWithMeta """ - interfaces = [] + interfaces = base.ListWithMeta([], None) node = self._get("/os-baremetal-nodes/%s" % node_id, 'node') + interfaces.append_request_ids(node.request_ids) for interface in node.interfaces: interface_object = BareMetalNodeInterface(self, interface) interfaces.append(interface_object) diff --git a/novaclient/v2/contrib/tenant_networks.py b/novaclient/v2/contrib/tenant_networks.py index 0bc569336..e124cbec3 100644 --- a/novaclient/v2/contrib/tenant_networks.py +++ b/novaclient/v2/contrib/tenant_networks.py @@ -20,7 +20,12 @@ class TenantNetwork(base.Resource): def delete(self): - self.manager.delete(network=self) + """ + Delete this project network. + + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self.manager.delete(network=self) class TenantNetworkManager(base.ManagerWithFind): @@ -34,7 +39,13 @@ def get(self, network): 'network') def delete(self, network): - self._delete('/os-tenant-networks/%s' % base.getid(network)) + """ + Delete a specified project network. + + :param network: a project network to delete + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self._delete('/os-tenant-networks/%s' % base.getid(network)) def create(self, label, cidr): body = {'network': {'label': label, 'cidr': cidr}} From 209f17224224e000a7e2c1ed3e698d7f7cb36864 Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Fri, 25 Dec 2015 12:55:24 +0900 Subject: [PATCH 0970/1705] Add return-request-id-to-caller function(5/5) Add return-request-id-to-caller function to resources and resource managers in the following files. The methods in the resource class and resource manager return a wrapper class that has 'request_ids' property. The caller can get request ids of the callee via the property. * novaclient/v2/volume_snapshots.py * novaclient/v2/volumes.py * novaclient/v2/volume_types.py Co-authored-by: Ankit Agrawal Change-Id: I4de8c5cf685a5970acc92f8a58a1db26a8d9ffc7 Implements: blueprint return-request-id-to-caller --- novaclient/tests/unit/v2/fakes.py | 21 +++++++++++---------- novaclient/tests/unit/v2/test_volumes.py | 20 ++++++++++++++++---- novaclient/v2/volume_snapshots.py | 7 +++++-- novaclient/v2/volume_types.py | 3 ++- novaclient/v2/volumes.py | 12 ++++++++---- 5 files changed, 42 insertions(+), 21 deletions(-) diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 2958d33e7..e0efff064 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -2184,7 +2184,7 @@ def delete_servers_1234_os_interface_port_id(self, **kw): # I suppose they are outdated and should be updated after Cinder released def get_volumes_detail(self, **kw): - return (200, {}, {"volumes": [ + return (200, FAKE_RESPONSE_HEADERS, {"volumes": [ { "display_name": "Work", "display_description": "volume for work", @@ -2211,7 +2211,7 @@ def get_volumes_detail(self, **kw): "metadata": {}}]}) def get_volumes(self, **kw): - return (200, {}, {"volumes": [ + return (200, FAKE_RESPONSE_HEADERS, {"volumes": [ { "display_name": "Work", "display_description": "volume for work", @@ -2238,7 +2238,7 @@ def get_volumes(self, **kw): "metadata": {}}]}) def get_volumes_15e59938_07d5_11e1_90e3_e3dffe0c5983(self, **kw): - return (200, {}, { + return (200, FAKE_RESPONSE_HEADERS, { "volume": self.get_volumes_detail()[2]['volumes'][0]}) def get_volumes_15e59938_07d5_11e1_90e3_ee32ba30feaa(self, **kw): @@ -2246,7 +2246,7 @@ def get_volumes_15e59938_07d5_11e1_90e3_ee32ba30feaa(self, **kw): "volume": self.get_volumes_detail()[2]['volumes'][1]}) def post_volumes(self, **kw): - return (200, {}, {"volume": + return (200, FAKE_RESPONSE_HEADERS, {"volume": {"status": "creating", "display_name": "vol-007", "attachments": [(0)], @@ -2260,22 +2260,23 @@ def post_volumes(self, **kw): "size": 1}}) def delete_volumes_15e59938_07d5_11e1_90e3_e3dffe0c5983(self, **kw): - return (200, {}, {}) + return (200, FAKE_RESPONSE_HEADERS, {}) def delete_volumes_15e59938_07d5_11e1_90e3_ee32ba30feaa(self, **kw): return (200, {}, {}) def post_servers_1234_os_volume_attachments(self, **kw): - return (200, {}, { + return (200, FAKE_RESPONSE_HEADERS, { "volumeAttachment": {"device": "/dev/vdb", "volumeId": 2}}) def put_servers_1234_os_volume_attachments_Work(self, **kw): - return (200, {}, {"volumeAttachment": {"volumeId": 2}}) + return (200, FAKE_RESPONSE_HEADERS, + {"volumeAttachment": {"volumeId": 2}}) def get_servers_1234_os_volume_attachments(self, **kw): - return (200, {}, { + return (200, FAKE_RESPONSE_HEADERS, { "volumeAttachments": [ {"display_name": "Work", "display_description": "volume for work", @@ -2288,7 +2289,7 @@ def get_servers_1234_os_volume_attachments(self, **kw): "metadata": {}}]}) def get_servers_1234_os_volume_attachments_Work(self, **kw): - return (200, {}, { + return (200, FAKE_RESPONSE_HEADERS, { "volumeAttachment": {"display_name": "Work", "display_description": "volume for work", @@ -2301,7 +2302,7 @@ def get_servers_1234_os_volume_attachments_Work(self, **kw): "metadata": {}}}) def delete_servers_1234_os_volume_attachments_Work(self, **kw): - return (200, {}, {}) + return (200, FAKE_RESPONSE_HEADERS, {}) def get_servers_1234_os_instance_actions(self, **kw): return (200, FAKE_RESPONSE_HEADERS, { diff --git a/novaclient/tests/unit/v2/test_volumes.py b/novaclient/tests/unit/v2/test_volumes.py index 2e5f3d47a..caaf20f66 100644 --- a/novaclient/tests/unit/v2/test_volumes.py +++ b/novaclient/tests/unit/v2/test_volumes.py @@ -30,6 +30,7 @@ class VolumesTest(utils.TestCase): @mock.patch.object(warnings, 'warn') def test_list_volumes(self, mock_warn): vl = cs.volumes.list() + self.assert_request_id(vl, fakes.FAKE_REQUEST_ID_LIST) cs.assert_called('GET', '/volumes/detail') for v in vl: self.assertIsInstance(v, volumes.Volume) @@ -38,6 +39,7 @@ def test_list_volumes(self, mock_warn): @mock.patch.object(warnings, 'warn') def test_list_volumes_undetailed(self, mock_warn): vl = cs.volumes.list(detailed=False) + self.assert_request_id(vl, fakes.FAKE_REQUEST_ID_LIST) cs.assert_called('GET', '/volumes') for v in vl: self.assertIsInstance(v, volumes.Volume) @@ -47,6 +49,7 @@ def test_list_volumes_undetailed(self, mock_warn): def test_get_volume_details(self, mock_warn): vol_id = '15e59938-07d5-11e1-90e3-e3dffe0c5983' v = cs.volumes.get(vol_id) + self.assert_request_id(v, fakes.FAKE_REQUEST_ID_LIST) cs.assert_called('GET', '/volumes/%s' % vol_id) self.assertIsInstance(v, volumes.Volume) self.assertEqual(v.id, vol_id) @@ -59,6 +62,7 @@ def test_create_volume(self, mock_warn): display_name="My volume", display_description="My volume desc", ) + self.assert_request_id(v, fakes.FAKE_REQUEST_ID_LIST) cs.assert_called('POST', '/volumes') self.assertIsInstance(v, volumes.Volume) self.assertEqual(1, mock_warn.call_count) @@ -67,11 +71,14 @@ def test_create_volume(self, mock_warn): def test_delete_volume(self, mock_warn): vol_id = '15e59938-07d5-11e1-90e3-e3dffe0c5983' v = cs.volumes.get(vol_id) - v.delete() + ret = v.delete() + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) cs.assert_called('DELETE', '/volumes/%s' % vol_id) - cs.volumes.delete(vol_id) + ret = cs.volumes.delete(vol_id) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) cs.assert_called('DELETE', '/volumes/%s' % vol_id) - cs.volumes.delete(v) + ret = cs.volumes.delete(v) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) cs.assert_called('DELETE', '/volumes/%s' % vol_id) self.assertEqual(4, mock_warn.call_count) @@ -81,6 +88,7 @@ def test_create_server_volume(self): volume_id='15e59938-07d5-11e1-90e3-e3dffe0c5983', device='/dev/vdb' ) + self.assert_request_id(v, fakes.FAKE_REQUEST_ID_LIST) cs.assert_called('POST', '/servers/1234/os-volume_attachments') self.assertIsInstance(v, volumes.Volume) @@ -91,20 +99,24 @@ def test_update_server_volume(self): attachment_id='Work', new_volume_id=vol_id ) + self.assert_request_id(v, fakes.FAKE_REQUEST_ID_LIST) cs.assert_called('PUT', '/servers/1234/os-volume_attachments/Work') self.assertIsInstance(v, volumes.Volume) def test_get_server_volume(self): v = cs.volumes.get_server_volume(1234, 'Work') + self.assert_request_id(v, fakes.FAKE_REQUEST_ID_LIST) cs.assert_called('GET', '/servers/1234/os-volume_attachments/Work') self.assertIsInstance(v, volumes.Volume) def test_list_server_volumes(self): vl = cs.volumes.get_server_volumes(1234) + self.assert_request_id(vl, fakes.FAKE_REQUEST_ID_LIST) cs.assert_called('GET', '/servers/1234/os-volume_attachments') for v in vl: self.assertIsInstance(v, volumes.Volume) def test_delete_server_volume(self): - cs.volumes.delete_server_volume(1234, 'Work') + ret = cs.volumes.delete_server_volume(1234, 'Work') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) cs.assert_called('DELETE', '/servers/1234/os-volume_attachments/Work') diff --git a/novaclient/v2/volume_snapshots.py b/novaclient/v2/volume_snapshots.py index 6cf597a1e..e31e8c183 100644 --- a/novaclient/v2/volume_snapshots.py +++ b/novaclient/v2/volume_snapshots.py @@ -34,8 +34,10 @@ def __repr__(self): def delete(self): """ DEPRECATED: Delete this snapshot. + + :returns: An instance of novaclient.base.TupleWithMeta """ - self.manager.delete(self) + return self.manager.delete(self) class SnapshotManager(base.ManagerWithFind): @@ -107,6 +109,7 @@ def delete(self, snapshot): DEPRECATED: Delete a snapshot. :param snapshot: The :class:`Snapshot` to delete. + :returns: An instance of novaclient.base.TupleWithMeta """ warnings.warn('The novaclient.v2.volume_snapshots module is ' 'deprecated and will be removed after Nova 13.0.0 is ' @@ -114,4 +117,4 @@ def delete(self, snapshot): 'python-openstacksdk instead.', DeprecationWarning) with self.alternate_service_type( 'volumev2', allowed_types=('volume', 'volumev2')): - self._delete("/snapshots/%s" % base.getid(snapshot)) + return self._delete("/snapshots/%s" % base.getid(snapshot)) diff --git a/novaclient/v2/volume_types.py b/novaclient/v2/volume_types.py index 4cd629018..03ac13cc4 100644 --- a/novaclient/v2/volume_types.py +++ b/novaclient/v2/volume_types.py @@ -72,6 +72,7 @@ def delete(self, volume_type): DEPRECATED: Delete a specific volume_type. :param volume_type: The ID of the :class:`VolumeType` to get. + :returns: An instance of novaclient.base.TupleWithMeta """ warnings.warn('The novaclient.v2.volume_types module is deprecated ' 'and will be removed after Nova 13.0.0 is released. Use ' @@ -79,7 +80,7 @@ def delete(self, volume_type): DeprecationWarning) with self.alternate_service_type( 'volumev2', allowed_types=('volume', 'volumev2')): - self._delete("/types/%s" % base.getid(volume_type)) + return self._delete("/types/%s" % base.getid(volume_type)) def create(self, name): """ diff --git a/novaclient/v2/volumes.py b/novaclient/v2/volumes.py index 802cf500d..455cb64b2 100644 --- a/novaclient/v2/volumes.py +++ b/novaclient/v2/volumes.py @@ -38,8 +38,10 @@ def __repr__(self): def delete(self): """ DEPRECATED: Delete this volume. + + :returns: An instance of novaclient.base.TupleWithMeta """ - self.manager.delete(self) + return self.manager.delete(self) class VolumeManager(base.ManagerWithFind): @@ -126,6 +128,7 @@ def delete(self, volume): DEPRECATED: Delete a volume. :param volume: The :class:`Volume` to delete. + :returns: An instance of novaclient.base.TupleWithMeta """ warnings.warn('The novaclient.v2.volumes.VolumeManager.delete() ' 'method is deprecated and will be removed after Nova ' @@ -133,7 +136,7 @@ def delete(self, volume): 'python-openstacksdk instead.', DeprecationWarning) with self.alternate_service_type( 'volumev2', allowed_types=('volume', 'volumev2')): - self._delete("/volumes/%s" % base.getid(volume)) + return self._delete("/volumes/%s" % base.getid(volume)) def create_server_volume(self, server_id, volume_id, device=None): """ @@ -193,6 +196,7 @@ def delete_server_volume(self, server_id, attachment_id): :param server_id: The ID of the server :param attachment_id: The ID of the attachment + :returns: An instance of novaclient.base.TupleWithMeta """ - self._delete("/servers/%s/os-volume_attachments/%s" % - (server_id, attachment_id,)) + return self._delete("/servers/%s/os-volume_attachments/%s" % + (server_id, attachment_id,)) From e808f328c2b2d4f19cf81f7e3325399012cf8f01 Mon Sep 17 00:00:00 2001 From: Andreas Jaeger Date: Thu, 28 Jan 2016 09:13:39 +0100 Subject: [PATCH 0971/1705] Update translation setup Follow new infra setup for translations, see spec http://specs.openstack.org/openstack-infra/infra-specs/specs/translation_setup.html for full details. This basically renames python-novaclient/locale/python-novaclient.pot to novaclient/locale/novaclient. For this we need to update setup.cfg. The domain name is already correct in novaclient/i18n.py. The project has no translations currently, let's remove the outdated pot file, the updated scripts work without them. So, we can just delete the file and once there are translations, an updated pot file together with translations can be imported automatically. Change-Id: Ifeabedf157f0338c1e76dc5ab8ab41e2e331ad87 --- .../locale/python-novaclient.pot | 2171 ----------------- setup.cfg | 12 +- 2 files changed, 6 insertions(+), 2177 deletions(-) delete mode 100644 python-novaclient/locale/python-novaclient.pot diff --git a/python-novaclient/locale/python-novaclient.pot b/python-novaclient/locale/python-novaclient.pot deleted file mode 100644 index 9d226bda9..000000000 --- a/python-novaclient/locale/python-novaclient.pot +++ /dev/null @@ -1,2171 +0,0 @@ -# Translations template for python-novaclient. -# Copyright (C) 2015 ORGANIZATION -# This file is distributed under the same license as the python-novaclient -# project. -# FIRST AUTHOR , 2015. -# -#, fuzzy -msgid "" -msgstr "" -"Project-Id-Version: python-novaclient 2.28.2.dev1\n" -"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2015-09-08 10:09-0700\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME \n" -"Language-Team: LANGUAGE \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=utf-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 1.3\n" - -#: novaclient/api_versions.py:38 -#, python-format -msgid "'%(other)s' should be an instance of '%(cls)s'" -msgstr "" - -#: novaclient/api_versions.py:64 -#, python-format -msgid "" -"Invalid format of client version '%s'. Expected format 'X.Y', where X is " -"a major part and Y is a minor part of version." -msgstr "" - -#: novaclient/api_versions.py:136 -msgid "Null APIVersion doesn't support 'matches'." -msgstr "" - -#: novaclient/api_versions.py:152 -msgid "Null APIVersion cannot be converted to string." -msgstr "" - -#: novaclient/api_versions.py:204 -#, python-format -msgid "Invalid client version '%(version)s'. Major part should be '%(major)s'" -msgstr "" - -#: novaclient/api_versions.py:209 -#, python-format -msgid "" -"Invalid client version '%(version)s'. Major part must be one of: " -"'%(major)s'" -msgstr "" - -#: novaclient/api_versions.py:259 -msgid "Server doesn't support microversions" -msgstr "" - -#: novaclient/api_versions.py:263 -#, python-format -msgid "" -"The specified version isn't supported by server. The valid version range " -"is '%(min)s' to '%(max)s'" -msgstr "" - -#: novaclient/api_versions.py:276 -msgid "The server isn't backward compatible with Nova V2 REST API" -msgstr "" - -#: novaclient/api_versions.py:283 -#, python-format -msgid "" -"Server version is too old. The client valid version range is " -"'%(client_min)s' to '%(client_max)s'. The server valid version range is " -"'%(server_min)s' to '%(server_max)s'." -msgstr "" - -#: novaclient/api_versions.py:292 -#, python-format -msgid "" -"Server version is too new. The client valid version range is " -"'%(client_min)s' to '%(client_max)s'. The server valid version range is " -"'%(server_min)s' to '%(server_max)s'." -msgstr "" - -#: novaclient/client.py:488 -msgid "Found more than one valid endpoint. Use a more restrictive filter" -msgstr "" - -#: novaclient/client.py:494 -msgid "Could not find any suitable endpoint. Correct region?" -msgstr "" - -#: novaclient/client.py:526 -#, python-format -msgid "Authentication requires 'auth_url', which should be specified in '%s'" -msgstr "" - -#: novaclient/client.py:776 -msgid "The version should be explicit, not latest." -msgstr "" - -#: novaclient/shell.py:68 -#, python-format -msgid "%s must be a float" -msgstr "" - -#: novaclient/shell.py:71 -#, python-format -msgid "%s must be greater than 0" -msgstr "" - -#: novaclient/shell.py:135 -msgid "Unable to save empty management url/auth token" -msgstr "" - -#: novaclient/shell.py:219 -#, python-format -msgid "" -"error: %(errmsg)s\n" -"Try '%(mainp)s help %(subp)s' for more information.\n" -msgstr "" - -#: novaclient/shell.py:287 -msgid "Print debugging output" -msgstr "" - -#: novaclient/shell.py:294 -msgid "Use the auth token cache. Defaults to False if env[OS_CACHE] is not set." -msgstr "" - -#: novaclient/shell.py:301 -msgid "Print call timing info" -msgstr "" - -#: novaclient/shell.py:320 -msgid "Defaults to env[OS_TENANT_NAME]." -msgstr "" - -#: novaclient/shell.py:329 -msgid "Defaults to env[OS_TENANT_ID]." -msgstr "" - -#: novaclient/shell.py:339 -msgid "Defaults to env[OS_REGION_NAME]." -msgstr "" - -#: novaclient/shell.py:356 -msgid "Defaults to compute for most actions" -msgstr "" - -#: novaclient/shell.py:365 -msgid "Defaults to env[NOVA_SERVICE_NAME]" -msgstr "" - -#: novaclient/shell.py:374 -msgid "Defaults to env[NOVA_VOLUME_SERVICE_NAME]" -msgstr "" - -#: novaclient/shell.py:388 -msgid "Defaults to env[NOVA_ENDPOINT_TYPE], env[OS_ENDPOINT_TYPE] or " -msgstr "" - -#: novaclient/shell.py:407 -msgid "" -"Accepts X, X.Y (where X is major and Y is minor part) or \"X.latest\", " -"defaults to env[OS_COMPUTE_API_VERSION]." -msgstr "" - -#: novaclient/shell.py:459 -#, python-format -msgid " (Supported by API versions '%(start)s' - '%(end)s')" -msgstr "" - -#: novaclient/shell.py:639 -msgid "" -"You must provide a username or user id via --os-username, --os-user-id, " -"env[OS_USERNAME] or env[OS_USER_ID]" -msgstr "" - -#: novaclient/shell.py:645 novaclient/shell.py:688 -msgid "" -"You must provide a project name or project id via --os-project-name, " -"--os-project-id, env[OS_PROJECT_ID] or env[OS_PROJECT_NAME]. You may use " -"os-project and os-tenant interchangeably." -msgstr "" - -#: novaclient/shell.py:658 -msgid "" -"You must provide an auth url via either --os-auth-url or env[OS_AUTH_URL]" -" or specify an auth_system which defines a default url with --os-auth-" -"system or env[OS_AUTH_SYSTEM]" -msgstr "" - -#: novaclient/shell.py:697 -msgid "You must provide an auth url via either --os-auth-url or env[OS_AUTH_URL]" -msgstr "" - -#: novaclient/shell.py:723 -#, python-format -msgid "" -"The specified version isn't supported by client. The valid version range " -"is '%(min)s' to '%(max)s'" -msgstr "" - -#: novaclient/shell.py:818 -msgid "Invalid OpenStack Nova credentials." -msgstr "" - -#: novaclient/shell.py:820 -msgid "Unable to authorize user" -msgstr "" - -#: novaclient/shell.py:873 -#, python-format -msgid "'%s' is not a valid subcommand" -msgstr "" - -#: novaclient/utils.py:61 -#, python-format -msgid "" -"Hook '%(hook_name)s' is attempting to redefine attributes " -"'%(conflicting_keys)s'" -msgstr "" - -#: novaclient/utils.py:232 -#, python-format -msgid "No %(class)s with a name or ID of '%(name)s' exists." -msgstr "" - -#: novaclient/utils.py:237 -#, python-format -msgid "" -"Multiple %(class)s matches found for '%(name)s', use an ID to be more " -"specific." -msgstr "" - -#: novaclient/utils.py:353 -#, python-format -msgid "" -"Invalid key: \"%s\". Keys may only contain letters, numbers, spaces, " -"underscores, periods, colons and hyphens." -msgstr "" - -#: novaclient/openstack/common/cliutils.py:40 -#, python-format -msgid "Missing arguments: %s" -msgstr "" - -#: novaclient/openstack/common/cliutils.py:158 -#, python-format -msgid "" -"Field labels list %(labels)s has different number of elements than fields" -" list %(fields)s" -msgstr "" - -#: novaclient/openstack/common/apiclient/base.py:244 -#: novaclient/openstack/common/apiclient/base.py:401 -#, python-format -msgid "No %(name)s matching %(args)s." -msgstr "" - -#: novaclient/openstack/common/apiclient/client.py:250 -msgid "Cannot find endpoint or token for request" -msgstr "" - -#: novaclient/openstack/common/apiclient/client.py:381 -#, python-format -msgid "" -"Invalid %(api_name)s client version '%(version)s'. Must be one of: " -"%(version_map)s" -msgstr "" - -#: novaclient/openstack/common/apiclient/exceptions.py:84 -#, python-format -msgid "Authentication failed. Missing options: %s" -msgstr "" - -#: novaclient/openstack/common/apiclient/exceptions.py:93 -#, python-format -msgid "AuthSystemNotFound: %r" -msgstr "" - -#: novaclient/openstack/common/apiclient/exceptions.py:116 -#, python-format -msgid "AmbiguousEndpoints: %r" -msgstr "" - -#: novaclient/openstack/common/apiclient/exceptions.py:124 -msgid "HTTP Error" -msgstr "" - -#: novaclient/openstack/common/apiclient/exceptions.py:144 -msgid "HTTP Redirection" -msgstr "" - -#: novaclient/openstack/common/apiclient/exceptions.py:152 -msgid "HTTP Client Error" -msgstr "" - -#: novaclient/openstack/common/apiclient/exceptions.py:161 -msgid "HTTP Server Error" -msgstr "" - -#: novaclient/openstack/common/apiclient/exceptions.py:171 -msgid "Multiple Choices" -msgstr "" - -#: novaclient/openstack/common/apiclient/exceptions.py:180 -msgid "Bad Request" -msgstr "" - -#: novaclient/openstack/common/apiclient/exceptions.py:190 -msgid "Unauthorized" -msgstr "" - -#: novaclient/openstack/common/apiclient/exceptions.py:199 -msgid "Payment Required" -msgstr "" - -#: novaclient/openstack/common/apiclient/exceptions.py:209 -msgid "Forbidden" -msgstr "" - -#: novaclient/openstack/common/apiclient/exceptions.py:219 -msgid "Not Found" -msgstr "" - -#: novaclient/openstack/common/apiclient/exceptions.py:229 -msgid "Method Not Allowed" -msgstr "" - -#: novaclient/openstack/common/apiclient/exceptions.py:239 -msgid "Not Acceptable" -msgstr "" - -#: novaclient/openstack/common/apiclient/exceptions.py:248 -msgid "Proxy Authentication Required" -msgstr "" - -#: novaclient/openstack/common/apiclient/exceptions.py:257 -msgid "Request Timeout" -msgstr "" - -#: novaclient/openstack/common/apiclient/exceptions.py:267 -msgid "Conflict" -msgstr "" - -#: novaclient/openstack/common/apiclient/exceptions.py:277 -msgid "Gone" -msgstr "" - -#: novaclient/openstack/common/apiclient/exceptions.py:287 -msgid "Length Required" -msgstr "" - -#: novaclient/openstack/common/apiclient/exceptions.py:297 -msgid "Precondition Failed" -msgstr "" - -#: novaclient/openstack/common/apiclient/exceptions.py:306 -msgid "Request Entity Too Large" -msgstr "" - -#: novaclient/openstack/common/apiclient/exceptions.py:323 -msgid "Request-URI Too Long" -msgstr "" - -#: novaclient/openstack/common/apiclient/exceptions.py:333 -msgid "Unsupported Media Type" -msgstr "" - -#: novaclient/openstack/common/apiclient/exceptions.py:343 -msgid "Requested Range Not Satisfiable" -msgstr "" - -#: novaclient/openstack/common/apiclient/exceptions.py:352 -msgid "Expectation Failed" -msgstr "" - -#: novaclient/openstack/common/apiclient/exceptions.py:362 -msgid "Unprocessable Entity" -msgstr "" - -#: novaclient/openstack/common/apiclient/exceptions.py:371 -msgid "Internal Server Error" -msgstr "" - -#: novaclient/openstack/common/apiclient/exceptions.py:382 -msgid "Not Implemented" -msgstr "" - -#: novaclient/openstack/common/apiclient/exceptions.py:392 -msgid "Bad Gateway" -msgstr "" - -#: novaclient/openstack/common/apiclient/exceptions.py:401 -msgid "Service Unavailable" -msgstr "" - -#: novaclient/openstack/common/apiclient/exceptions.py:411 -msgid "Gateway Timeout" -msgstr "" - -#: novaclient/openstack/common/apiclient/exceptions.py:420 -msgid "HTTP Version Not Supported" -msgstr "" - -#: novaclient/openstack/common/apiclient/utils.py:86 -#, python-format -msgid "No %(name)s with a name or ID of '%(name_or_id)s' exists." -msgstr "" - -#: novaclient/openstack/common/apiclient/utils.py:94 -#, python-format -msgid "" -"Multiple %(name)s matches found for '%(name_or_id)s', use an ID to be " -"more specific." -msgstr "" - -#: novaclient/v2/flavor_access.py:40 -msgid "Unknown list options." -msgstr "" - -#: novaclient/v2/flavor_access.py:50 -msgid "Sorry, query by tenant not supported." -msgstr "" - -#: novaclient/v2/flavors.py:177 -msgid "Ram must be an integer." -msgstr "" - -#: novaclient/v2/flavors.py:181 -msgid "VCPUs must be an integer." -msgstr "" - -#: novaclient/v2/flavors.py:185 -msgid "Disk must be an integer." -msgstr "" - -#: novaclient/v2/flavors.py:193 -msgid "Swap must be an integer." -msgstr "" - -#: novaclient/v2/flavors.py:197 -msgid "Ephemeral must be an integer." -msgstr "" - -#: novaclient/v2/flavors.py:201 -msgid "rxtx_factor must be a float." -msgstr "" - -#: novaclient/v2/flavors.py:206 -msgid "is_public must be a boolean." -msgstr "" - -#: novaclient/v2/networks.py:121 -msgid "Must disassociate either host or project or both" -msgstr "" - -#: novaclient/v2/security_group_default_rules.py:47 -#: novaclient/v2/security_group_rules.py:52 -msgid "From port must be an integer." -msgstr "" - -#: novaclient/v2/security_group_default_rules.py:51 -#: novaclient/v2/security_group_rules.py:56 -msgid "To port must be an integer." -msgstr "" - -#: novaclient/v2/security_group_default_rules.py:53 -#: novaclient/v2/security_group_rules.py:58 -msgid "IP protocol must be 'tcp', 'udp', or 'icmp'." -msgstr "" - -#: novaclient/v2/servers.py:524 -msgid "Only one of 'v4-fixed-ip' and 'v6-fixed-ip' may be provided." -msgstr "" - -#: novaclient/v2/shell.py:133 -#, python-format -msgid "Invalid ephemeral argument '%s'." -msgstr "" - -#: novaclient/v2/shell.py:166 -msgid "you need to specify a Flavor ID " -msgstr "" - -#: novaclient/v2/shell.py:175 -msgid "num_instances should be >= 1" -msgstr "" - -#: novaclient/v2/shell.py:179 -msgid "Don't mix num-instances and max/min-count" -msgstr "" - -#: novaclient/v2/shell.py:183 -msgid "min_count should be >= 1" -msgstr "" - -#: novaclient/v2/shell.py:188 -msgid "max_count should be >= 1" -msgstr "" - -#: novaclient/v2/shell.py:193 -msgid "min_count should be <= max_count" -msgstr "" - -#: novaclient/v2/shell.py:205 novaclient/v2/shell.py:1561 -#, python-format -msgid "Can't open '%(src)s': %(exc)s" -msgstr "" - -#: novaclient/v2/shell.py:208 novaclient/v2/shell.py:1564 -#, python-format -msgid "" -"Invalid file argument '%s'. File arguments must be of the form '--file " -"'" -msgstr "" - -#: novaclient/v2/shell.py:222 -#, python-format -msgid "Can't open '%(user_data)s': %(exc)s" -msgstr "" - -#: novaclient/v2/shell.py:254 -msgid "" -"you need to specify at least one source ID (Image, Snapshot, or Volume), " -"a block device mapping or provide a set of properties to match against an" -" image" -msgstr "" - -#: novaclient/v2/shell.py:260 -msgid "" -"you can't mix old block devices (--block-device-mapping) with the new " -"ones (--block-device, --boot-volume, --snapshot, --ephemeral, --swap)" -msgstr "" - -#: novaclient/v2/shell.py:266 -#, python-format -msgid "" -"Invalid nic argument '%s'. Nic arguments must be of the form --nic , " -"with at minimum net-id or port-id (but not both) specified." -msgstr "" - -#: novaclient/v2/shell.py:333 -msgid "Name or ID of flavor (see 'nova flavor-list')." -msgstr "" - -#: novaclient/v2/shell.py:338 -msgid "Name or ID of image (see 'nova image-list'). " -msgstr "" - -#: novaclient/v2/shell.py:345 -msgid "Image metadata property (see 'nova image-show'). " -msgstr "" - -#: novaclient/v2/shell.py:350 -msgid "Volume ID to boot from." -msgstr "" - -#: novaclient/v2/shell.py:355 -msgid "Snapshot ID to boot from (will create a volume)." -msgstr "" - -#: novaclient/v2/shell.py:367 -msgid "Boot at least servers (limited by quota)." -msgstr "" - -#: novaclient/v2/shell.py:373 -msgid "Boot up to servers (limited by quota)." -msgstr "" - -#: novaclient/v2/shell.py:379 novaclient/v2/shell.py:1528 -msgid "" -"Record arbitrary key/value metadata to /meta_data.json on the metadata " -"server. Can be specified multiple times." -msgstr "" - -#: novaclient/v2/shell.py:387 -msgid "" -"Store arbitrary files from locally to on the new " -"server. Limited by the injected_files quota value." -msgstr "" - -#: novaclient/v2/shell.py:393 -msgid "" -"Key name of keypair that should be created earlier with the " -"command keypair-add" -msgstr "" - -#: novaclient/v2/shell.py:398 novaclient/v2/shell.py:1522 -msgid "Name for the new server" -msgstr "" - -#: novaclient/v2/shell.py:403 -msgid "user data file to pass to be exposed by the metadata server." -msgstr "" - -#: novaclient/v2/shell.py:411 -msgid "The availability zone for server placement." -msgstr "" - -#: novaclient/v2/shell.py:419 -msgid "Comma separated list of security group names." -msgstr "" - -#: novaclient/v2/shell.py:428 -msgid "" -"Block device mapping in the format =:::." -msgstr "" - -#: novaclient/v2/shell.py:439 -msgid "" -"Block device mapping with the keys: id=UUID (image_id, snapshot_id or " -"volume_id only if using source image, snapshot or volume) source=source " -"type (image, snapshot, volume or blank), dest=destination type of the " -"block device (volume or local), bus=device's bus (e.g. uml, lxc, virtio, " -"...; if omitted, hypervisor driver chooses a suitable default, honoured " -"only if device type is supplied) type=device type (e.g. disk, cdrom, ...;" -" defaults to 'disk') device=name of the device (e.g. vda, xda, ...; if " -"omitted, hypervisor driver chooses suitable device depending on selected " -"bus), size=size of the block device in MB(for swap) and in GB(for other " -"formats) (if omitted, hypervisor driver calculates size), format=device " -"will be formatted (e.g. swap, ntfs, ...; optional), bootindex=integer " -"used for ordering the boot disks (for image backed instances it is equal " -"to 0, for others need to be specified) and shutdown=shutdown behaviour " -"(either preserve or remove, for local destination set to remove)." -msgstr "" - -#: novaclient/v2/shell.py:464 -msgid "Create and attach a local swap block device of MB." -msgstr "" - -#: novaclient/v2/shell.py:470 -msgid "" -"Create and attach a local ephemeral block device of GB and format " -"it to ." -msgstr "" - -#: novaclient/v2/shell.py:478 -msgid "Send arbitrary key/value pairs to the scheduler for custom use." -msgstr "" - -#: novaclient/v2/shell.py:487 -msgid "" -"Create a NIC on the server. Specify option multiple times to create " -"multiple NICs. net-id: attach NIC to network with this UUID (either port-" -"id or net-id must be provided), v4-fixed-ip: IPv4 fixed address for NIC " -"(optional), v6-fixed-ip: IPv6 fixed address for NIC (optional), port-id: " -"attach NIC to port with this UUID (either port-id or net-id must be " -"provided)." -msgstr "" - -#: novaclient/v2/shell.py:500 -msgid "Enable config drive" -msgstr "" - -#: novaclient/v2/shell.py:506 -msgid "Report the new server boot progress until it completes." -msgstr "" - -#: novaclient/v2/shell.py:512 -msgid "Admin password for the instance" -msgstr "" - -#: novaclient/v2/shell.py:537 -msgid "UUID of the project to create the cloudpipe for." -msgstr "" - -#: novaclient/v2/shell.py:543 -msgid "New IP Address." -msgstr "" - -#: novaclient/v2/shell.py:544 -msgid "New Port." -msgstr "" - -#: novaclient/v2/shell.py:558 -#, python-format -msgid "" -"\r" -"Server %(action)s... %(progress)s%% complete" -msgstr "" - -#: novaclient/v2/shell.py:561 -#, python-format -msgid "" -"\r" -"Server %(action)s..." -msgstr "" - -#: novaclient/v2/shell.py:581 -msgid "" -"\n" -"Finished" -msgstr "" - -#: novaclient/v2/shell.py:585 -#, python-format -msgid "" -"\n" -"Error %s server" -msgstr "" - -#: novaclient/v2/shell.py:589 -#, python-format -msgid "" -"\n" -"Deleted %s server" -msgstr "" - -#: novaclient/v2/shell.py:670 -msgid "Get extra-specs of each flavor." -msgstr "" - -#: novaclient/v2/shell.py:676 -msgid "Display all flavors (Admin only)." -msgstr "" - -#: novaclient/v2/shell.py:689 -msgid "Name or ID of the flavor to delete" -msgstr "" - -#: novaclient/v2/shell.py:700 novaclient/v2/shell.py:760 -msgid "Name or ID of flavor" -msgstr "" - -#: novaclient/v2/shell.py:710 -msgid "Name of the new flavor" -msgstr "" - -#: novaclient/v2/shell.py:714 -msgid "" -"Unique ID (integer or UUID) for the new flavor. If specifying 'auto', a " -"UUID will be generated as id" -msgstr "" - -#: novaclient/v2/shell.py:719 -msgid "Memory size in MB" -msgstr "" - -#: novaclient/v2/shell.py:723 -msgid "Disk size in GB" -msgstr "" - -#: novaclient/v2/shell.py:727 -msgid "Ephemeral space size in GB (default 0)" -msgstr "" - -#: novaclient/v2/shell.py:732 -msgid "Number of vcpus" -msgstr "" - -#: novaclient/v2/shell.py:736 -msgid "Swap space size in MB (default 0)" -msgstr "" - -#: novaclient/v2/shell.py:741 -msgid "RX/TX factor (default 1)" -msgstr "" - -#: novaclient/v2/shell.py:746 -msgid "Make flavor accessible to the public (default true)" -msgstr "" - -#: novaclient/v2/shell.py:765 -msgid "Actions: 'set' or 'unset'" -msgstr "" - -#: novaclient/v2/shell.py:772 -msgid "Extra_specs to set/unset (only key is necessary on unset)" -msgstr "" - -#: novaclient/v2/shell.py:787 -msgid "Filter results by flavor name or ID." -msgstr "" - -#: novaclient/v2/shell.py:790 -msgid "Filter results by tenant ID." -msgstr "" - -#: novaclient/v2/shell.py:794 -msgid "Unable to filter results by both --flavor and --tenant." -msgstr "" - -#: novaclient/v2/shell.py:799 -msgid "Failed to get access list for public flavor type." -msgstr "" - -#: novaclient/v2/shell.py:805 -msgid "Unable to get all access lists. Specify --flavor or --tenant" -msgstr "" - -#: novaclient/v2/shell.py:820 -msgid "Flavor name or ID to add access for the given tenant." -msgstr "" - -#: novaclient/v2/shell.py:823 -msgid "Tenant ID to add flavor access for." -msgstr "" - -#: novaclient/v2/shell.py:835 -msgid "Flavor name or ID to remove access for the given tenant." -msgstr "" - -#: novaclient/v2/shell.py:838 -msgid "Tenant ID to remove flavor access for." -msgstr "" - -#: novaclient/v2/shell.py:849 -msgid "The ID of the project." -msgstr "" - -#: novaclient/v2/shell.py:869 novaclient/v2/shell.py:1328 -msgid "" -"Comma-separated list of fields to display. Use the show command to see " -"which fields are available." -msgstr "" - -#: novaclient/v2/shell.py:891 novaclient/v2/shell.py:901 -msgid "uuid or label of network" -msgstr "" - -#: novaclient/v2/shell.py:927 novaclient/v2/shell.py:941 -#: novaclient/v2/shell.py:954 -msgid "uuid of network" -msgstr "" - -#: novaclient/v2/shell.py:945 -msgid "Name of host" -msgstr "" - -#: novaclient/v2/shell.py:977 -msgid "Label for network" -msgstr "" - -#: novaclient/v2/shell.py:982 -msgid "IPv4 subnet (ex: 10.0.0.0/8)" -msgstr "" - -#: novaclient/v2/shell.py:986 -msgid "IPv6 subnet (ex: fe80::/64" -msgstr "" - -#: novaclient/v2/shell.py:992 -msgid "The vlan ID to be assigned to the project." -msgstr "" - -#: novaclient/v2/shell.py:998 -msgid "" -"First vlan ID to be assigned to the project. Subsequent vlan IDs will be " -"assigned incrementally." -msgstr "" - -#: novaclient/v2/shell.py:1005 -msgid "vpn start" -msgstr "" - -#: novaclient/v2/shell.py:1009 -msgid "gateway" -msgstr "" - -#: novaclient/v2/shell.py:1013 -msgid "IPv6 gateway" -msgstr "" - -#: novaclient/v2/shell.py:1018 -msgid "VIFs on this network are connected to this bridge." -msgstr "" - -#: novaclient/v2/shell.py:1023 -msgid "The bridge is connected to this interface." -msgstr "" - -#: novaclient/v2/shell.py:1028 -msgid "Multi host" -msgstr "" - -#: novaclient/v2/shell.py:1032 -msgid "First DNS" -msgstr "" - -#: novaclient/v2/shell.py:1037 -msgid "Second DNS" -msgstr "" - -#: novaclient/v2/shell.py:1042 -msgid "Network UUID" -msgstr "" - -#: novaclient/v2/shell.py:1047 -msgid "IPv4 subnet for fixed IPs (ex: 10.20.0.0/16)" -msgstr "" - -#: novaclient/v2/shell.py:1052 -msgid "Project ID" -msgstr "" - -#: novaclient/v2/shell.py:1057 -msgid "Network interface priority" -msgstr "" - -#: novaclient/v2/shell.py:1062 -msgid "MTU for network" -msgstr "" - -#: novaclient/v2/shell.py:1067 -msgid "Enable dhcp" -msgstr "" - -#: novaclient/v2/shell.py:1071 -msgid "Dhcp-server (defaults to gateway address)" -msgstr "" - -#: novaclient/v2/shell.py:1076 -msgid "Share address" -msgstr "" - -#: novaclient/v2/shell.py:1080 -msgid "Start of allowed addresses for instances" -msgstr "" - -#: novaclient/v2/shell.py:1084 -msgid "End of allowed addresses for instances" -msgstr "" - -#: novaclient/v2/shell.py:1090 -msgid "Must specify either fixed_range_v4 or fixed_range_v6" -msgstr "" - -#: novaclient/v2/shell.py:1111 -msgid "Number of images to return per request." -msgstr "" - -#: novaclient/v2/shell.py:1131 novaclient/v2/shell.py:1207 -msgid "Name or ID of image" -msgstr "" - -#: novaclient/v2/shell.py:1136 novaclient/v2/shell.py:1852 -#: novaclient/v2/contrib/metadata_extensions.py:29 -msgid "Actions: 'set' or 'delete'" -msgstr "" - -#: novaclient/v2/shell.py:1143 -msgid "Metadata to add/update or delete (only key is necessary on delete)" -msgstr "" - -#: novaclient/v2/shell.py:1216 -msgid "Name or ID of image(s)." -msgstr "" - -#: novaclient/v2/shell.py:1223 -#, python-format -msgid "Delete for image %(image)s failed: %(e)s" -msgstr "" - -#: novaclient/v2/shell.py:1232 -msgid "Only return servers that match reservation-id." -msgstr "" - -#: novaclient/v2/shell.py:1241 -msgid "Search with regular expression match by IP address." -msgstr "" - -#: novaclient/v2/shell.py:1247 -msgid "Search with regular expression match by IPv6 address." -msgstr "" - -#: novaclient/v2/shell.py:1253 -msgid "Search with regular expression match by name" -msgstr "" - -#: novaclient/v2/shell.py:1259 -msgid "Search with regular expression match by server name." -msgstr "" - -#: novaclient/v2/shell.py:1268 -msgid "Search by server status" -msgstr "" - -#: novaclient/v2/shell.py:1274 -msgid "Search by flavor name or ID" -msgstr "" - -#: novaclient/v2/shell.py:1280 -msgid "Search by image name or ID" -msgstr "" - -#: novaclient/v2/shell.py:1286 -msgid "Search servers by hostname to which they are assigned (Admin only)." -msgstr "" - -#: novaclient/v2/shell.py:1297 novaclient/v2/shell.py:2037 -#: novaclient/v2/shell.py:2805 -msgid "Display information from all tenants (Admin only)." -msgstr "" - -#: novaclient/v2/shell.py:1310 -msgid "" -"Display information from single tenant (Admin only). The --all-tenants " -"option must also be provided." -msgstr "" - -#: novaclient/v2/shell.py:1317 -msgid "Display information from single user (Admin only)." -msgstr "" - -#: novaclient/v2/shell.py:1323 -msgid "Only display deleted servers (Admin only)." -msgstr "" - -#: novaclient/v2/shell.py:1335 -msgid "Get only uuid and name." -msgstr "" - -#: novaclient/v2/shell.py:1410 -#, python-format -msgid "Unknown sort direction: %s" -msgstr "" - -#: novaclient/v2/shell.py:1461 -msgid "Perform a hard reboot (instead of a soft one)." -msgstr "" - -#: novaclient/v2/shell.py:1465 novaclient/v2/shell.py:1651 -#: novaclient/v2/shell.py:1664 novaclient/v2/shell.py:1945 -#: novaclient/v2/shell.py:3519 -msgid "Name or ID of server(s)." -msgstr "" - -#: novaclient/v2/shell.py:1471 -msgid "Poll until reboot is complete." -msgstr "" - -#: novaclient/v2/shell.py:1478 -#, python-format -msgid "Request to reboot server %s has been accepted." -msgstr "" - -#: novaclient/v2/shell.py:1479 -msgid "Unable to reboot the specified server(s)." -msgstr "" - -#: novaclient/v2/shell.py:1486 -#, python-format -msgid "Wait for server %s reboot." -msgstr "" - -#: novaclient/v2/shell.py:1487 -msgid "Wait for specified server(s) failed." -msgstr "" - -#: novaclient/v2/shell.py:1490 novaclient/v2/shell.py:1585 -#: novaclient/v2/shell.py:1607 novaclient/v2/shell.py:1613 -#: novaclient/v2/shell.py:1619 novaclient/v2/shell.py:1636 -#: novaclient/v2/shell.py:1642 novaclient/v2/shell.py:1674 -#: novaclient/v2/shell.py:1682 novaclient/v2/shell.py:1688 -#: novaclient/v2/shell.py:1694 novaclient/v2/shell.py:1700 -#: novaclient/v2/shell.py:1724 novaclient/v2/shell.py:1730 -#: novaclient/v2/shell.py:1736 novaclient/v2/shell.py:1742 -#: novaclient/v2/shell.py:1748 novaclient/v2/shell.py:1766 -#: novaclient/v2/shell.py:1772 novaclient/v2/shell.py:1785 -#: novaclient/v2/shell.py:1828 novaclient/v2/shell.py:1931 -#: novaclient/v2/shell.py:1974 novaclient/v2/shell.py:1985 -#: novaclient/v2/shell.py:2147 novaclient/v2/shell.py:2170 -#: novaclient/v2/shell.py:2189 novaclient/v2/shell.py:2299 -#: novaclient/v2/shell.py:2317 novaclient/v2/shell.py:2335 -#: novaclient/v2/shell.py:2353 novaclient/v2/shell.py:2375 -#: novaclient/v2/shell.py:2392 novaclient/v2/shell.py:2407 -#: novaclient/v2/shell.py:2420 novaclient/v2/shell.py:2432 -#: novaclient/v2/shell.py:2449 novaclient/v2/shell.py:2456 -#: novaclient/v2/shell.py:2468 novaclient/v2/shell.py:2479 -#: novaclient/v2/shell.py:2490 novaclient/v2/shell.py:3486 -#: novaclient/v2/shell.py:3542 novaclient/v2/shell.py:3857 -#: novaclient/v2/shell.py:4304 novaclient/v2/shell.py:4346 -#: novaclient/v2/shell.py:4356 novaclient/v2/shell.py:4381 -msgid "Name or ID of server." -msgstr "" - -#: novaclient/v2/shell.py:1491 -msgid "Name or ID of new image." -msgstr "" - -#: novaclient/v2/shell.py:1497 -msgid "Set the provided admin password on the rebuilt server." -msgstr "" - -#: novaclient/v2/shell.py:1506 -msgid "Report the server rebuild progress until it completes." -msgstr "" - -#: novaclient/v2/shell.py:1512 novaclient/v2/shell.py:1930 -msgid "Skips flavor/image lookups when showing servers" -msgstr "" - -#: novaclient/v2/shell.py:1517 -msgid "Preserve the default ephemeral storage partition on rebuild." -msgstr "" - -#: novaclient/v2/shell.py:1536 -msgid "" -"Store arbitrary files from locally to on the new " -"server. You may store up to 5 files." -msgstr "" - -#: novaclient/v2/shell.py:1578 -msgid "Name (old name) or ID of server." -msgstr "" - -#: novaclient/v2/shell.py:1579 -msgid "New name for the server." -msgstr "" - -#: novaclient/v2/shell.py:1589 -msgid "Name or ID of new flavor." -msgstr "" - -#: novaclient/v2/shell.py:1595 -msgid "Report the server resize progress until it completes." -msgstr "" - -#: novaclient/v2/shell.py:1625 -msgid "Report the server migration progress until it completes." -msgstr "" - -#: novaclient/v2/shell.py:1657 -#, python-format -msgid "Request to stop server %s has been accepted." -msgstr "" - -#: novaclient/v2/shell.py:1658 -msgid "Unable to stop the specified server(s)." -msgstr "" - -#: novaclient/v2/shell.py:1670 -#, python-format -msgid "Request to start server %s has been accepted." -msgstr "" - -#: novaclient/v2/shell.py:1671 -msgid "Unable to start the specified server(s)." -msgstr "" - -#: novaclient/v2/shell.py:1705 -msgid "The admin password to be set in the rescue environment." -msgstr "" - -#: novaclient/v2/shell.py:1710 -msgid "The image to rescue with." -msgstr "" - -#: novaclient/v2/shell.py:1757 -msgid "" -"Name or ID of a server for which the network cache should be refreshed " -"from neutron (Admin only)." -msgstr "" - -#: novaclient/v2/shell.py:1781 -msgid "Passwords do not match." -msgstr "" - -#: novaclient/v2/shell.py:1786 -msgid "Name of snapshot." -msgstr "" - -#: novaclient/v2/shell.py:1792 -msgid "Print image info." -msgstr "" - -#: novaclient/v2/shell.py:1798 -msgid "Report the snapshot progress and poll until image creation is complete." -msgstr "" - -#: novaclient/v2/shell.py:1829 -msgid "Name of the backup image." -msgstr "" - -#: novaclient/v2/shell.py:1832 -msgid "The backup type, like \"daily\" or \"weekly\"." -msgstr "" - -#: novaclient/v2/shell.py:1835 -msgid "Int parameter representing how many backups to keep around." -msgstr "" - -#: novaclient/v2/shell.py:1847 -msgid "Name or ID of server" -msgstr "" - -#: novaclient/v2/shell.py:1859 novaclient/v2/contrib/metadata_extensions.py:36 -msgid "Metadata to set or delete (only key is necessary on delete)" -msgstr "" - -#: novaclient/v2/shell.py:1896 -msgid "Flavor not found" -msgstr "" - -#: novaclient/v2/shell.py:1915 -msgid "Image not found" -msgstr "" - -#: novaclient/v2/shell.py:1917 -msgid "Attempt to boot from volume - no image supplied" -msgstr "" - -#: novaclient/v2/shell.py:1942 -msgid "Delete server(s) in another tenant by name (Admin only)." -msgstr "" - -#: novaclient/v2/shell.py:1952 -#, python-format -msgid "Request to delete server %s has been accepted." -msgstr "" - -#: novaclient/v2/shell.py:1953 -msgid "Unable to delete the specified server(s)." -msgstr "" - -#: novaclient/v2/shell.py:1978 -msgid "Network ID." -msgstr "" - -#: novaclient/v2/shell.py:1986 novaclient/v2/shell.py:2421 -#: novaclient/v2/shell.py:2433 novaclient/v2/shell.py:2450 -#: novaclient/v2/shell.py:2457 -msgid "IP Address." -msgstr "" - -#: novaclient/v2/shell.py:2062 -msgid "Name or ID of the volume." -msgstr "" - -#: novaclient/v2/shell.py:2074 -msgid "Size of volume in GB" -msgstr "" - -#: novaclient/v2/shell.py:2079 -msgid "Optional snapshot id to create the volume from. (Default=None)" -msgstr "" - -#: novaclient/v2/shell.py:2086 -msgid "Optional image id to create the volume from. (Default=None)" -msgstr "" - -#: novaclient/v2/shell.py:2092 -msgid "Optional volume name. (Default=None)" -msgstr "" - -#: novaclient/v2/shell.py:2100 -msgid "Optional volume description. (Default=None)" -msgstr "" - -#: novaclient/v2/shell.py:2108 -msgid "Optional volume type. (Default=None)" -msgstr "" - -#: novaclient/v2/shell.py:2114 -msgid "Optional Availability Zone for volume. (Default=None)" -msgstr "" - -#: novaclient/v2/shell.py:2132 -msgid "Name or ID of the volume(s) to delete." -msgstr "" - -#: novaclient/v2/shell.py:2140 -#, python-format -msgid "Delete for volume %(volume)s failed: %(e)s" -msgstr "" - -#: novaclient/v2/shell.py:2151 novaclient/v2/shell.py:2178 -msgid "ID of the volume to attach." -msgstr "" - -#: novaclient/v2/shell.py:2154 -msgid "" -"Name of the device e.g. /dev/vdb. Use \"auto\" for autoassign (if " -"supported)" -msgstr "" - -#: novaclient/v2/shell.py:2174 -msgid "Attachment ID of the volume." -msgstr "" - -#: novaclient/v2/shell.py:2193 -msgid "ID of the volume to detach." -msgstr "" - -#: novaclient/v2/shell.py:2212 -msgid "Name or ID of the snapshot." -msgstr "" - -#: novaclient/v2/shell.py:2223 -msgid "ID of the volume to snapshot" -msgstr "" - -#: novaclient/v2/shell.py:2227 -msgid "" -"Optional flag to indicate whether to snapshot a volume even if its " -"attached to a server. (Default=False)" -msgstr "" - -#: novaclient/v2/shell.py:2234 -msgid "Optional snapshot name. (Default=None)" -msgstr "" - -#: novaclient/v2/shell.py:2242 -msgid "Optional snapshot description. (Default=None)" -msgstr "" - -#: novaclient/v2/shell.py:2259 -msgid "Name or ID of the snapshot to delete." -msgstr "" - -#: novaclient/v2/shell.py:2281 -msgid "Name of the new volume type" -msgstr "" - -#: novaclient/v2/shell.py:2292 -msgid "Unique ID of the volume type to delete" -msgstr "" - -#: novaclient/v2/shell.py:2303 -msgid "Type of vnc console (\"novnc\" or \"xvpvnc\")." -msgstr "" - -#: novaclient/v2/shell.py:2321 -msgid "Type of spice console (\"spice-html5\")." -msgstr "" - -#: novaclient/v2/shell.py:2339 -msgid "Type of rdp console (\"rdp-html5\")." -msgstr "" - -#: novaclient/v2/shell.py:2356 -msgid "Type of serial console, default=\"serial\"." -msgstr "" - -#: novaclient/v2/shell.py:2361 -msgid "Invalid parameter value for 'console_type', currently supported 'serial'." -msgstr "" - -#: novaclient/v2/shell.py:2379 -msgid "" -"Private key (used locally to decrypt password) (Optional). When " -"specified, the command displays the clear (decrypted) VM password. When " -"not specified, the ciphered VM password is displayed." -msgstr "" - -#: novaclient/v2/shell.py:2412 -msgid "Length in lines to tail." -msgstr "" - -#: novaclient/v2/shell.py:2426 novaclient/v2/shell.py:2438 -msgid "Fixed IP Address to associate with." -msgstr "" - -#: novaclient/v2/shell.py:2472 novaclient/v2/shell.py:2483 -msgid "Name of Security Group." -msgstr "" - -#: novaclient/v2/shell.py:2501 -msgid "Name of Floating IP Pool. (Optional)" -msgstr "" - -#: novaclient/v2/shell.py:2509 -msgid "IP of Floating IP." -msgstr "" - -#: novaclient/v2/shell.py:2516 -#, python-format -msgid "Floating IP %s not found." -msgstr "" - -#: novaclient/v2/shell.py:2532 -msgid "Filter by host" -msgstr "" - -#: novaclient/v2/shell.py:2542 -msgid "Address range to create" -msgstr "" - -#: novaclient/v2/shell.py:2545 -msgid "Pool for new Floating IPs" -msgstr "" - -#: novaclient/v2/shell.py:2548 -msgid "Interface for new Floating IPs" -msgstr "" - -#: novaclient/v2/shell.py:2554 -msgid "Address range to delete" -msgstr "" - -#: novaclient/v2/shell.py:2575 novaclient/v2/shell.py:2594 -#: novaclient/v2/shell.py:2605 novaclient/v2/shell.py:2612 -#: novaclient/v2/shell.py:2618 novaclient/v2/shell.py:2634 -msgid "DNS domain" -msgstr "" - -#: novaclient/v2/shell.py:2576 novaclient/v2/shell.py:2592 -msgid "IP address" -msgstr "" - -#: novaclient/v2/shell.py:2577 novaclient/v2/shell.py:2593 -#: novaclient/v2/shell.py:2606 -msgid "DNS name" -msgstr "" - -#: novaclient/v2/shell.py:2582 -msgid "You must specify either --ip or --name" -msgstr "" - -#: novaclient/v2/shell.py:2598 -msgid "dns type (e.g. \"A\")" -msgstr "" - -#: novaclient/v2/shell.py:2623 -msgid "Limit access to this domain to servers in the specified availability zone." -msgstr "" - -#: novaclient/v2/shell.py:2637 -msgid "Limit access to this domain to users of the specified project." -msgstr "" - -#: novaclient/v2/shell.py:2690 -#, python-format -msgid "" -"Multiple security group matches found for name '%s', use an ID to be more" -" specific." -msgstr "" - -#: novaclient/v2/shell.py:2695 -#, python-format -msgid "Secgroup ID or name '%s' not found." -msgstr "" - -#: novaclient/v2/shell.py:2703 novaclient/v2/shell.py:2731 -#: novaclient/v2/shell.py:2773 novaclient/v2/shell.py:2788 -#: novaclient/v2/shell.py:2825 novaclient/v2/shell.py:2835 -#: novaclient/v2/shell.py:2874 -msgid "ID or name of security group." -msgstr "" - -#: novaclient/v2/shell.py:2707 novaclient/v2/shell.py:2735 -#: novaclient/v2/shell.py:2843 novaclient/v2/shell.py:2882 -#: novaclient/v2/shell.py:4488 novaclient/v2/shell.py:4512 -msgid "IP protocol (icmp, tcp, udp)." -msgstr "" - -#: novaclient/v2/shell.py:2711 novaclient/v2/shell.py:2739 -#: novaclient/v2/shell.py:2847 novaclient/v2/shell.py:2886 -#: novaclient/v2/shell.py:4492 novaclient/v2/shell.py:4516 -msgid "Port at start of range." -msgstr "" - -#: novaclient/v2/shell.py:2715 novaclient/v2/shell.py:2743 -#: novaclient/v2/shell.py:2851 novaclient/v2/shell.py:2890 -#: novaclient/v2/shell.py:4496 novaclient/v2/shell.py:4520 -msgid "Port at end of range." -msgstr "" - -#: novaclient/v2/shell.py:2716 novaclient/v2/shell.py:2744 -#: novaclient/v2/shell.py:4497 novaclient/v2/shell.py:4521 -msgid "CIDR for address range." -msgstr "" - -#: novaclient/v2/shell.py:2757 novaclient/v2/shell.py:2915 -#: novaclient/v2/shell.py:4535 -msgid "Rule not found" -msgstr "" - -#: novaclient/v2/shell.py:2760 novaclient/v2/shell.py:2774 -msgid "Name of security group." -msgstr "" - -#: novaclient/v2/shell.py:2763 novaclient/v2/shell.py:2777 -msgid "Description of security group." -msgstr "" - -#: novaclient/v2/shell.py:2839 novaclient/v2/shell.py:2878 -msgid "ID or name of source group." -msgstr "" - -#: novaclient/v2/shell.py:2861 novaclient/v2/shell.py:2900 -msgid "ip_proto, from_port, and to_port must be specified together" -msgstr "" - -#: novaclient/v2/shell.py:2928 -msgid "Name of key." -msgstr "" - -#: novaclient/v2/shell.py:2933 -msgid "Path to a public ssh key." -msgstr "" - -#: novaclient/v2/shell.py:2941 -msgid "Keypair type. Can be ssh or x509." -msgstr "" - -#: novaclient/v2/shell.py:2956 -#, python-format -msgid "Can't open or read '%(key)s': %(exc)s" -msgstr "" - -#: novaclient/v2/shell.py:2967 -msgid "Keypair name to delete." -msgstr "" - -#: novaclient/v2/shell.py:2995 -#, python-format -msgid "Public key: %s" -msgstr "" - -#: novaclient/v2/shell.py:3001 -msgid "Name or ID of keypair" -msgstr "" - -#: novaclient/v2/shell.py:3019 novaclient/v2/shell.py:3112 -msgid "Display information from single tenant (Admin only)." -msgstr "" - -#: novaclient/v2/shell.py:3025 novaclient/v2/shell.py:3118 -msgid "Include reservations count." -msgstr "" - -#: novaclient/v2/shell.py:3129 novaclient/v2/shell.py:3178 -msgid "Usage range start date ex 2012-01-20 (default: 4 weeks ago)" -msgstr "" - -#: novaclient/v2/shell.py:3134 novaclient/v2/shell.py:3182 -msgid "Usage range end date, ex 2012-01-20 (default: tomorrow)" -msgstr "" - -#: novaclient/v2/shell.py:3165 novaclient/v2/shell.py:3224 -#, python-format -msgid "Usage from %(start)s to %(end)s:" -msgstr "" - -#: novaclient/v2/shell.py:3188 -msgid "UUID of tenant to get usage for." -msgstr "" - -#: novaclient/v2/shell.py:3232 -msgid "None" -msgstr "" - -#: novaclient/v2/shell.py:3240 -msgid "Filename for the private key [Default: pk.pem]" -msgstr "" - -#: novaclient/v2/shell.py:3246 -msgid "Filename for the X.509 certificate [Default: cert.pem]" -msgstr "" - -#: novaclient/v2/shell.py:3251 -#, python-format -msgid "Unable to write privatekey - %s exists." -msgstr "" - -#: novaclient/v2/shell.py:3254 -#, python-format -msgid "Unable to write x509 cert - %s exists." -msgstr "" - -#: novaclient/v2/shell.py:3263 -#, python-format -msgid "Wrote private key to %s" -msgstr "" - -#: novaclient/v2/shell.py:3269 -#, python-format -msgid "Wrote x509 certificate to %s" -msgstr "" - -#: novaclient/v2/shell.py:3277 -msgid "Filename to write the x509 root cert." -msgstr "" - -#: novaclient/v2/shell.py:3281 -#, python-format -msgid "" -"Unable to write x509 root cert - %s" -" exists." -msgstr "" - -#: novaclient/v2/shell.py:3287 -#, python-format -msgid "Wrote x509 root cert to %s" -msgstr "" - -#: novaclient/v2/shell.py:3294 novaclient/v2/shell.py:3315 -msgid "type of hypervisor." -msgstr "" - -#: novaclient/v2/shell.py:3303 -msgid "type of os." -msgstr "" - -#: novaclient/v2/shell.py:3307 -msgid "type of architecture" -msgstr "" - -#: novaclient/v2/shell.py:3308 novaclient/v2/shell.py:3331 -msgid "version" -msgstr "" - -#: novaclient/v2/shell.py:3309 novaclient/v2/shell.py:3332 -msgid "url" -msgstr "" - -#: novaclient/v2/shell.py:3310 -msgid "md5 hash" -msgstr "" - -#: novaclient/v2/shell.py:3324 novaclient/v2/shell.py:3330 -msgid "id of the agent-build" -msgstr "" - -#: novaclient/v2/shell.py:3333 -msgid "md5hash" -msgstr "" - -#: novaclient/v2/shell.py:3353 novaclient/v2/shell.py:3381 -msgid "Name of aggregate." -msgstr "" - -#: novaclient/v2/shell.py:3359 -msgid "The availability zone of the aggregate (optional)." -msgstr "" - -#: novaclient/v2/shell.py:3369 -msgid "Name or ID of aggregate to delete." -msgstr "" - -#: novaclient/v2/shell.py:3374 -#, python-format -msgid "Aggregate %s has been successfully deleted." -msgstr "" - -#: novaclient/v2/shell.py:3380 novaclient/v2/shell.py:3402 -msgid "Name or ID of aggregate to update." -msgstr "" - -#: novaclient/v2/shell.py:3387 -msgid "The availability zone of the aggregate." -msgstr "" - -#: novaclient/v2/shell.py:3396 -#, python-format -msgid "Aggregate %s has been successfully updated." -msgstr "" - -#: novaclient/v2/shell.py:3409 -msgid "" -"Metadata to add/update to aggregate. Specify only the key to delete a " -"metadata item." -msgstr "" - -#: novaclient/v2/shell.py:3417 -msgid "metadata already exists" -msgstr "" - -#: novaclient/v2/shell.py:3420 -#, python-format -msgid "metadata key %s does not exist hence can not be deleted" -msgstr "" - -#: novaclient/v2/shell.py:3424 -#, python-format -msgid "Metadata has been successfully updated for aggregate %s." -msgstr "" - -#: novaclient/v2/shell.py:3431 novaclient/v2/shell.py:3447 -#: novaclient/v2/shell.py:3463 -msgid "Name or ID of aggregate." -msgstr "" - -#: novaclient/v2/shell.py:3434 -msgid "The host to add to the aggregate." -msgstr "" - -#: novaclient/v2/shell.py:3439 -#, python-format -msgid "Host %(host)s has been successfully added for aggregate %(aggregate_id)s " -msgstr "" - -#: novaclient/v2/shell.py:3450 -msgid "The host to remove from the aggregate." -msgstr "" - -#: novaclient/v2/shell.py:3455 -#, python-format -msgid "" -"Host %(host)s has been successfully removed from aggregate " -"%(aggregate_id)s " -msgstr "" - -#: novaclient/v2/shell.py:3489 -msgid "destination host name." -msgstr "" - -#: novaclient/v2/shell.py:3495 -msgid "True in case of block_migration. (Default=False:live_migration)" -msgstr "" - -#: novaclient/v2/shell.py:3505 -msgid "Allow overcommit.(Default=False)" -msgstr "" - -#: novaclient/v2/shell.py:3523 -msgid "" -"Request the server be reset to \"active\" state instead of \"error\" " -"state (the default)." -msgstr "" - -#: novaclient/v2/shell.py:3552 novaclient/v2/shell.py:3575 -#: novaclient/v2/shell.py:3583 novaclient/v2/shell.py:3602 -#: novaclient/v2/shell.py:3641 novaclient/v2/shell.py:3662 -#: novaclient/v2/shell.py:3686 novaclient/v2/contrib/metadata_extensions.py:24 -msgid "Name of host." -msgstr "" - -#: novaclient/v2/shell.py:3557 novaclient/v2/shell.py:3576 -#: novaclient/v2/shell.py:3584 novaclient/v2/shell.py:3603 -msgid "Service binary." -msgstr "" - -#: novaclient/v2/shell.py:3588 -msgid "Reason for disabling service." -msgstr "" - -#: novaclient/v2/shell.py:3607 -msgid "Unset the force state down of service" -msgstr "" - -#: novaclient/v2/shell.py:3616 -msgid "Id of service." -msgstr "" - -#: novaclient/v2/shell.py:3622 novaclient/v2/shell.py:3629 -#: novaclient/v2/shell.py:3635 -msgid "Fixed IP Address." -msgstr "" - -#: novaclient/v2/shell.py:3653 -msgid "" -"Filters the list, returning only those hosts in the availability zone " -"." -msgstr "" - -#: novaclient/v2/shell.py:3665 -msgid "Either enable or disable a host." -msgstr "" - -#: novaclient/v2/shell.py:3671 -msgid "Either put or resume host to/from maintenance." -msgstr "" - -#: novaclient/v2/shell.py:3690 -msgid "A power action: startup, reboot, or shutdown." -msgstr "" - -#: novaclient/v2/shell.py:3706 -msgid "List hypervisors matching the given ." -msgstr "" - -#: novaclient/v2/shell.py:3721 -msgid "The hypervisor hostname (or pattern) to search for." -msgstr "" - -#: novaclient/v2/shell.py:3750 -msgid "Name or ID of the hypervisor to show the details of." -msgstr "" - -#: novaclient/v2/shell.py:3753 -msgid "Wrap the output to a specified length. Default is 40 or 0 to disable" -msgstr "" - -#: novaclient/v2/shell.py:3764 -msgid "Name or ID of the hypervisor to show the uptime of." -msgstr "" - -#: novaclient/v2/shell.py:3811 -#, python-format -msgid "" -"WARNING: %(service)s has no endpoint in %(region)s! Available endpoints " -"for this service:" -msgstr "" - -#: novaclient/v2/shell.py:3839 -msgid "wrap PKI tokens to a specified length, or 0 to disable" -msgstr "" - -#: novaclient/v2/shell.py:3864 -msgid "Optional flag to indicate which port to use for ssh. (Default=22)" -msgstr "" - -#: novaclient/v2/shell.py:3878 -msgid "" -"Optional flag to indicate which IP type to use. Possible values includes" -" fixed and floating (the Default)." -msgstr "" - -#: novaclient/v2/shell.py:3882 -msgid "Network to use for the ssh." -msgstr "" - -#: novaclient/v2/shell.py:3888 -msgid "" -"Optional flag to indicate whether to use an IPv6 address attached to a " -"server. (Defaults to IPv4 address)" -msgstr "" - -#: novaclient/v2/shell.py:3891 -msgid "Login to use." -msgstr "" - -#: novaclient/v2/shell.py:3896 -msgid "Private key file, same as the -i option to the ssh command." -msgstr "" - -#: novaclient/v2/shell.py:3901 -msgid "Extra options to pass to ssh. see: man ssh" -msgstr "" - -#: novaclient/v2/shell.py:3919 -#, python-format -msgid "Server '%(server)s' is not attached to network '%(network)s'" -msgstr "" - -#: novaclient/v2/shell.py:3925 -#, python-format -msgid "" -"Server '%(server)s' is attached to more than one network. Please pick the" -" network to use." -msgstr "" - -#: novaclient/v2/shell.py:3929 -#, python-format -msgid "Server '%(server)s' is not attached to any network." -msgstr "" - -#: novaclient/v2/shell.py:3942 -#, python-format -msgid "" -"No address that would match network '%(network)s' and type " -"'%(address_type)s' of version %(pretty_version)s has been found for " -"server '%(server)s'." -msgstr "" - -#: novaclient/v2/shell.py:3949 -#, python-format -msgid "More than one %(pretty_version)s %(address_type)s addressfound." -msgstr "" - -#: novaclient/v2/shell.py:4012 -msgid "ID of tenant to list the quotas for." -msgstr "" - -#: novaclient/v2/shell.py:4017 -msgid "ID of user to list the quotas for." -msgstr "" - -#: novaclient/v2/shell.py:4036 -msgid "ID of tenant to list the default quotas for." -msgstr "" - -#: novaclient/v2/shell.py:4054 -msgid "ID of tenant to set the quotas for." -msgstr "" - -#: novaclient/v2/shell.py:4059 -msgid "ID of user to set the quotas for." -msgstr "" - -#: novaclient/v2/shell.py:4064 novaclient/v2/shell.py:4205 -msgid "New value for the \"instances\" quota." -msgstr "" - -#: novaclient/v2/shell.py:4069 novaclient/v2/shell.py:4210 -msgid "New value for the \"cores\" quota." -msgstr "" - -#: novaclient/v2/shell.py:4074 novaclient/v2/shell.py:4215 -msgid "New value for the \"ram\" quota." -msgstr "" - -#: novaclient/v2/shell.py:4080 novaclient/v2/shell.py:4221 -msgid "New value for the \"floating-ips\" quota." -msgstr "" - -#: novaclient/v2/shell.py:4090 novaclient/v2/shell.py:4231 -msgid "New value for the \"fixed-ips\" quota." -msgstr "" - -#: novaclient/v2/shell.py:4096 novaclient/v2/shell.py:4237 -msgid "New value for the \"metadata-items\" quota." -msgstr "" - -#: novaclient/v2/shell.py:4106 novaclient/v2/shell.py:4247 -msgid "New value for the \"injected-files\" quota." -msgstr "" - -#: novaclient/v2/shell.py:4116 novaclient/v2/shell.py:4257 -msgid "New value for the \"injected-file-content-bytes\" quota." -msgstr "" - -#: novaclient/v2/shell.py:4126 novaclient/v2/shell.py:4267 -msgid "New value for the \"injected-file-path-bytes\" quota." -msgstr "" - -#: novaclient/v2/shell.py:4132 novaclient/v2/shell.py:4273 -msgid "New value for the \"key-pairs\" quota." -msgstr "" - -#: novaclient/v2/shell.py:4138 novaclient/v2/shell.py:4279 -msgid "New value for the \"security-groups\" quota." -msgstr "" - -#: novaclient/v2/shell.py:4144 novaclient/v2/shell.py:4285 -msgid "New value for the \"security-group-rules\" quota." -msgstr "" - -#: novaclient/v2/shell.py:4150 novaclient/v2/shell.py:4291 -msgid "New value for the \"server-groups\" quota." -msgstr "" - -#: novaclient/v2/shell.py:4156 novaclient/v2/shell.py:4297 -msgid "New value for the \"server-group-members\" quota." -msgstr "" - -#: novaclient/v2/shell.py:4162 -msgid "" -"Whether force update the quota even if the already used and reserved " -"exceeds the new quota" -msgstr "" - -#: novaclient/v2/shell.py:4174 -msgid "ID of tenant to delete quota for." -msgstr "" - -#: novaclient/v2/shell.py:4178 -msgid "ID of user to delete quota for." -msgstr "" - -#: novaclient/v2/shell.py:4190 -msgid "Name of quota class to list the quotas for." -msgstr "" - -#: novaclient/v2/shell.py:4200 -msgid "Name of quota class to set the quotas for." -msgstr "" - -#: novaclient/v2/shell.py:4307 -msgid "" -"Name or ID of the target host. If no host is specified, the scheduler " -"will choose one." -msgstr "" - -#: novaclient/v2/shell.py:4313 -msgid "" -"Set the provided admin password on the evacuated server. Not applicable " -"with on-shared-storage flag" -msgstr "" - -#: novaclient/v2/shell.py:4320 -msgid "Specifies whether server files are located on shared storage" -msgstr "" - -#: novaclient/v2/shell.py:4360 novaclient/v2/shell.py:4382 -msgid "Port ID." -msgstr "" - -#: novaclient/v2/shell.py:4365 -msgid "Network ID" -msgstr "" - -#: novaclient/v2/shell.py:4370 -msgid "Requested fixed IP." -msgstr "" - -#: novaclient/v2/shell.py:4470 -msgid "Display server groups from all projects (Admin only)." -msgstr "" - -#: novaclient/v2/shell.py:4538 -msgid "Server group name." -msgstr "" - -#: novaclient/v2/shell.py:4554 -msgid "Policies for the server groups (\"affinity\" or \"anti-affinity\")" -msgstr "" - -#: novaclient/v2/shell.py:4563 -msgid "at least one policy must be specified" -msgstr "" - -#: novaclient/v2/shell.py:4575 -msgid "Unique ID(s) of the server group to delete" -msgstr "" - -#: novaclient/v2/shell.py:4583 -#, python-format -msgid "Server group %s has been successfully deleted." -msgstr "" - -#: novaclient/v2/shell.py:4586 -#, python-format -msgid "Delete for server group %(sg)s failed: %(e)s" -msgstr "" - -#: novaclient/v2/shell.py:4589 -msgid "Unable to delete any of the specified server groups." -msgstr "" - -#: novaclient/v2/shell.py:4596 -msgid "Unique ID of the server group to get" -msgstr "" - -#: novaclient/v2/contrib/baremetal.py:158 -msgid "Name of nova compute host which will control this baremetal node" -msgstr "" - -#: novaclient/v2/contrib/baremetal.py:164 -msgid "Number of CPUs in the node" -msgstr "" - -#: novaclient/v2/contrib/baremetal.py:169 -msgid "Megabytes of RAM in the node" -msgstr "" - -#: novaclient/v2/contrib/baremetal.py:174 -msgid "Gigabytes of local storage in the node" -msgstr "" - -#: novaclient/v2/contrib/baremetal.py:178 -msgid "MAC address to provision the node" -msgstr "" - -#: novaclient/v2/contrib/baremetal.py:182 -msgid "Power management IP for the node" -msgstr "" - -#: novaclient/v2/contrib/baremetal.py:186 -msgid "Username for the node's power management" -msgstr "" - -#: novaclient/v2/contrib/baremetal.py:190 -msgid "Password for the node's power management" -msgstr "" - -#: novaclient/v2/contrib/baremetal.py:195 -msgid "ShellInABox port?" -msgstr "" - -#: novaclient/v2/contrib/baremetal.py:211 -msgid "ID of the node to delete." -msgstr "" - -#: novaclient/v2/contrib/baremetal.py:293 -#: novaclient/v2/contrib/baremetal.py:303 -#: novaclient/v2/contrib/baremetal.py:325 -#: novaclient/v2/contrib/baremetal.py:335 -msgid "ID of node" -msgstr "" - -#: novaclient/v2/contrib/baremetal.py:307 -#: novaclient/v2/contrib/baremetal.py:329 -msgid "MAC address of interface" -msgstr "" - -#: novaclient/v2/contrib/baremetal.py:312 -msgid "OpenFlow Datapath ID of interface" -msgstr "" - -#: novaclient/v2/contrib/baremetal.py:317 -msgid "OpenFlow port number of interface" -msgstr "" - -#: novaclient/v2/contrib/cells.py:53 -msgid "Name of the cell." -msgstr "" - -#: novaclient/v2/contrib/cells.py:63 -msgid "Name of the cell to get the capacities." -msgstr "" - -#: novaclient/v2/contrib/cells.py:68 -#, python-format -msgid "Ram Available: %s MB" -msgstr "" - -#: novaclient/v2/contrib/cells.py:71 -#, python-format -msgid "" -"\n" -"Disk Available: %s MB" -msgstr "" - -#: novaclient/v2/contrib/host_evacuate.py:34 -#, python-format -msgid "Error while evacuating instance: %s" -msgstr "" - -#: novaclient/v2/contrib/host_evacuate.py:46 -msgid "" -"Name of target host. If no host is specified the scheduler will select a " -"target." -msgstr "" - -#: novaclient/v2/contrib/host_evacuate.py:53 -msgid "Specifies whether all instances files are on shared storage" -msgstr "" - -#: novaclient/v2/contrib/host_evacuate_live.py:35 -#, python-format -msgid "Error while live migrating instance: %s" -msgstr "" - -#: novaclient/v2/contrib/host_evacuate_live.py:46 -msgid "Name of target host." -msgstr "" - -#: novaclient/v2/contrib/host_evacuate_live.py:51 -msgid "Enable block migration." -msgstr "" - -#: novaclient/v2/contrib/host_evacuate_live.py:56 -msgid "Enable disk overcommit." -msgstr "" - -#: novaclient/v2/contrib/host_servers_migrate.py:33 -#, python-format -msgid "Error while migrating instance: %s" -msgstr "" - -#: novaclient/v2/contrib/instance_action.py:47 -msgid "Name or UUID of the server to show an action for." -msgstr "" - -#: novaclient/v2/contrib/instance_action.py:51 -msgid "Request ID of the action to get." -msgstr "" - -#: novaclient/v2/contrib/instance_action.py:65 -msgid "Name or UUID of the server to list actions for." -msgstr "" - -#: novaclient/v2/contrib/migrations.py:61 -msgid "Fetch migrations for the given host." -msgstr "" - -#: novaclient/v2/contrib/migrations.py:66 -msgid "Fetch migrations for the given status." -msgstr "" - -#: novaclient/v2/contrib/migrations.py:71 -msgid "Fetch migrations for the given cell_name." -msgstr "" - -#: novaclient/v2/contrib/tenant_networks.py:79 -#: novaclient/v2/contrib/tenant_networks.py:94 -msgid "Network label (ex. my_new_network)" -msgstr "" - -#: novaclient/v2/contrib/tenant_networks.py:83 -#: novaclient/v2/contrib/tenant_networks.py:98 -msgid "IP block to allocate from (ex. 172.16.0.0/24 or 2001:DB8::/64)" -msgstr "" - diff --git a/setup.cfg b/setup.cfg index e5173d752..b9c9008d0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -38,18 +38,18 @@ all_files = 1 upload-dir = doc/build/html [compile_catalog] -domain = python-novaclient -directory = python-novaclient/locale +domain = novaclient +directory = novaclient/locale [update_catalog] -domain = python-novaclient -output_dir = python-novaclient/locale -input_file = python-novaclient/locale/python-novaclient.pot +domain = novaclient +output_dir = novaclient/locale +input_file = novaclient/locale/novaclient.pot [extract_messages] keywords = _ gettext ngettext l_ lazy_gettext mapping_file = babel.cfg -output_file = python-novaclient/locale/python-novaclient.pot +output_file = novaclient/locale/novaclient.pot [wheel] universal = 1 From 96c5c93f64c894c127436711ec824fcf72c96d97 Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Thu, 28 Jan 2016 13:46:43 +0900 Subject: [PATCH 0972/1705] Add release notes for return-request-id-to-caller Co-authored-by: Ankit Agrawal Change-Id: I5e66448036cfcb5367a6ab7af27eb765f6a2c456 Implements: blueprint return-request-id-to-caller --- ...request-id-to-caller-52c5423794b33f8b.yaml | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 releasenotes/notes/return-request-id-to-caller-52c5423794b33f8b.yaml diff --git a/releasenotes/notes/return-request-id-to-caller-52c5423794b33f8b.yaml b/releasenotes/notes/return-request-id-to-caller-52c5423794b33f8b.yaml new file mode 100644 index 000000000..e390c1629 --- /dev/null +++ b/releasenotes/notes/return-request-id-to-caller-52c5423794b33f8b.yaml @@ -0,0 +1,26 @@ +--- +prelude: > + Methods in manager classes and resource classes return wrapper classes + that wrap values returned originally. + For example, a wrapper class for list, a wrapper class for dict, + a wrapper class for str and so on. + The wrapper classes have a 'request_ids' property for request IDs + returned from Nova (nova-api). So the caller can get the + Nova's request IDs, then output them to logs with its own request ID. + The function to output them to the logs will be implemented + in other projects (cinder, heat, etc.). +features: + - Methods in manager classes and resource classes return wrapper classes + that wrap values returned originally. + For example, a wrapper class for list, a wrapper class for dict, + a wrapper class for str and so on. + The wrapper classes have a 'request_ids' property for request IDs + returned from Nova (nova-api). So the caller can get the + Nova's request IDs, then output them to logs with its own request ID. + The function to output them to the logs will be implemented + in other projects (cinder, heat, etc.). +upgrade: + - In case that methods return a response object and body originally and + body is None, the methods return the wrapper class for tuple as 'body' + instead of the wrapper class for None. + The wrapper class for None has not been added. From 0105fd120659baf3445b994af29837755d155c73 Mon Sep 17 00:00:00 2001 From: Bo Wang Date: Mon, 1 Feb 2016 20:17:47 +0800 Subject: [PATCH 0973/1705] Use # noqa to ignore one line but not whole file "# flake8: noqa" option disables all checks for the whole file. To disable one line we should use "# noqa". Closes Bug: #1540254 Change-Id: I34c8a2218f974cd7e78c60e9c1fec26751c4e82d --- tools/install_venv.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/install_venv.py b/tools/install_venv.py index cc2184378..d51c8d90d 100644 --- a/tools/install_venv.py +++ b/tools/install_venv.py @@ -22,7 +22,7 @@ import os import sys -import install_venv_common as install_venv # flake8: noqa +import install_venv_common as install_venv def print_help(project, venv, root): @@ -42,7 +42,7 @@ def print_help(project, venv, root): $ %(root)s/tools/with_venv.sh """ - print help % dict(project=project, venv=venv, root=root) + print(help % dict(project=project, venv=venv, root=root)) def main(argv): From 2131f777eb8b7cc3b1bd96cbd717cad5a307cd68 Mon Sep 17 00:00:00 2001 From: Cao Shufeng Date: Mon, 1 Feb 2016 15:25:45 +0800 Subject: [PATCH 0974/1705] Remove unnecessary filter from Resource's __repr__() function 'request_ids' is a function of class RequestIdMixin. Resource().__dict__.keys() doesn't contains a function. So it not necessary to filter it. Change-Id: I73af5d1be39769f70cbe4c1ec83144d56f94f91a --- novaclient/base.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/novaclient/base.py b/novaclient/base.py index 184145f98..caaea4ec7 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -143,8 +143,7 @@ def __repr__(self): reprkeys = sorted(k for k in self.__dict__.keys() if k[0] != '_' and - k not in ['manager', 'request_ids', - 'x_openstack_request_ids']) + k not in ['manager', 'x_openstack_request_ids']) info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys) return "<%s %s>" % (self.__class__.__name__, info) From ba39fd3caad7e62cbc27e98e9dc4de7b90ca4534 Mon Sep 17 00:00:00 2001 From: Cao Shufeng Date: Fri, 5 Feb 2016 16:19:03 +0800 Subject: [PATCH 0975/1705] Fix omission of request_ids returned to user The subclass of ManagerWithFind implements get() function, and all the get() functions returns an object with request_ids. Let's expose these request_ids to users directly. Change-Id: Icb3c480c637c39412ed085f4523fb93e36fea0a7 Closes-Bug: 1541694 --- novaclient/base.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/novaclient/base.py b/novaclient/base.py index caaea4ec7..154c20155 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -459,7 +459,9 @@ def findall(self, **kwargs): if detailed: found.append(obj) else: - found.append(self.get(obj.id)) + detail = self.get(obj.id) + found.append(detail) + found.append_request_ids(detail.request_ids) except AttributeError: continue From 0d0749d39ffd122387fde6ee5e9b029f93e53459 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Tue, 26 Jan 2016 12:39:52 +0200 Subject: [PATCH 0976/1705] Make _poll_for_status more user-friendly NOTE: _poll_for_status is a private method, which is used only in shell module, so we can modify it without backward compatibility. _poll_for_status is used for various resources with different interfaces. In case of snapshotting, it works with Image resources, which doesn't have "fault" attribute in "error" state. To prevent AttributeError, we should take this into account. Also, an exception raised by _poll_for_status(InstanceInErrorState) should not be hardcoded for one type of resource, so this exception is renamed to ResourceInErrorState. An exception InstanceInDeletedState, which is also can be raised by _poll_for_status, is not modified, since I don't know cases when it can be used with resources other than Server. Change-Id: Ie0ee96999376cbf608caa1cf8521dbef5c939b52 Closes-Bug: #1538073 --- novaclient/exceptions.py | 12 +++- novaclient/tests/unit/v2/test_shell.py | 90 +++++++++++++++++++++++++- novaclient/v2/shell.py | 4 +- 3 files changed, 100 insertions(+), 6 deletions(-) diff --git a/novaclient/exceptions.py b/novaclient/exceptions.py index 94591f26d..b9bdf1140 100644 --- a/novaclient/exceptions.py +++ b/novaclient/exceptions.py @@ -77,9 +77,15 @@ def __str__(self): return "ConnectionRefused: %s" % repr(self.response) -class InstanceInErrorState(Exception): - """Instance is in the error state.""" - pass +class ResourceInErrorState(Exception): + """Resource is in the error state.""" + + def __init__(self, obj): + msg = "`%s` resource is in the error state" % obj.__class__.__name__ + fault_msg = getattr(obj, "fault", {}).get("message") + if fault_msg: + msg += "due to '%s'" % fault_msg + self.message = "%s." % msg class VersionNotFoundForAPIMethod(Exception): diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index c079d9c6e..0869a1105 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -29,6 +29,7 @@ import novaclient from novaclient import api_versions +from novaclient import base import novaclient.client from novaclient import exceptions import novaclient.shell @@ -758,7 +759,7 @@ def test_boot_with_poll(self, poll_method): ['active'])]) def test_boot_with_poll_to_check_VM_state_error(self): - self.assertRaises(exceptions.InstanceInErrorState, self.run_command, + self.assertRaises(exceptions.ResourceInErrorState, self.run_command, 'boot --flavor 1 --image 1 some-bad-server --poll') def test_boot_named_flavor(self): @@ -2789,3 +2790,90 @@ def test_no_endpoints(self): self.assertRaises(LookupError, novaclient.v2.shell._get_first_endpoint, [], "ORD") + + +class PollForStatusTestCase(utils.TestCase): + @mock.patch("novaclient.v2.shell.time") + def test_simple_usage(self, mock_time): + poll_period = 3 + some_id = "uuuuuuuuuuuiiiiiiiii" + updated_objects = ( + base.Resource(None, info={"not_default_field": "INPROGRESS"}), + base.Resource(None, info={"not_default_field": "OK"})) + poll_fn = mock.MagicMock(side_effect=updated_objects) + + novaclient.v2.shell._poll_for_status( + poll_fn=poll_fn, + obj_id=some_id, + status_field="not_default_field", + final_ok_states=["ok"], + poll_period=poll_period, + # just want to test printing in separate tests + action="some", + silent=True, + show_progress=False + ) + self.assertEqual([mock.call(poll_period)], + mock_time.sleep.call_args_list) + self.assertEqual([mock.call(some_id)] * 2, poll_fn.call_args_list) + + @mock.patch("novaclient.v2.shell.sys.stdout") + @mock.patch("novaclient.v2.shell.time") + def test_print_progress(self, mock_time, mock_stdout): + updated_objects = ( + base.Resource(None, info={"status": "INPROGRESS", "progress": 0}), + base.Resource(None, info={"status": "INPROGRESS", "progress": 50}), + base.Resource(None, info={"status": "OK", "progress": 100})) + poll_fn = mock.MagicMock(side_effect=updated_objects) + action = "some" + + novaclient.v2.shell._poll_for_status( + poll_fn=poll_fn, + obj_id="uuuuuuuuuuuiiiiiiiii", + final_ok_states=["ok"], + poll_period="3", + action=action, + show_progress=True, + silent=False) + + stdout_arg_list = [ + mock.call("\n"), + mock.call("\rServer %s... 0%% complete" % action), + mock.call("\rServer %s... 50%% complete" % action), + mock.call("\rServer %s... 100%% complete" % action), + mock.call("\nFinished"), + mock.call("\n")] + self.assertEqual( + stdout_arg_list, + mock_stdout.write.call_args_list + ) + + @mock.patch("novaclient.v2.shell.time") + def test_error_state(self, mock_time): + fault_msg = "Oops" + updated_objects = ( + base.Resource(None, info={"status": "error", + "fault": {"message": fault_msg}}), + base.Resource(None, info={"status": "error"})) + poll_fn = mock.MagicMock(side_effect=updated_objects) + action = "some" + + self.assertRaises(exceptions.ResourceInErrorState, + novaclient.v2.shell._poll_for_status, + poll_fn=poll_fn, + obj_id="uuuuuuuuuuuiiiiiiiii", + final_ok_states=["ok"], + poll_period="3", + action=action, + show_progress=True, + silent=False) + + self.assertRaises(exceptions.ResourceInErrorState, + novaclient.v2.shell._poll_for_status, + poll_fn=poll_fn, + obj_id="uuuuuuuuuuuiiiiiiiii", + final_ok_states=["ok"], + poll_period="3", + action=action, + show_progress=True, + silent=False) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 9ff95a510..f16d59031 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -642,11 +642,11 @@ def print_progress(progress): elif status == "error": if not silent: print(_("\nError %s server") % action) - raise exceptions.InstanceInErrorState(obj.fault['message']) + raise exceptions.ResourceInErrorState(obj) elif status == "deleted": if not silent: print(_("\nDeleted %s server") % action) - raise exceptions.InstanceInDeletedState(obj.fault['message']) + raise exceptions.InstanceInDeletedState(obj.fault["message"]) if not silent: print_progress(progress) From 6634c72cdadd4fac487d0a93668ed45df1db062f Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Mon, 8 Feb 2016 02:43:58 +0000 Subject: [PATCH 0977/1705] Updated from global requirements Change-Id: Iacadd617dff797b26acc355bcdcaa11979ed1589 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 638cff28e..6a301d012 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -16,7 +16,7 @@ oslosphinx!=3.4.0,>=2.5.0 # Apache-2.0 testrepository>=0.0.18 # Apache-2.0/BSD testscenarios>=0.4 # Apache-2.0/BSD testtools>=1.4.0 # MIT -tempest-lib>=0.13.0 # Apache-2.0 +tempest-lib>=0.14.0 # Apache-2.0 # releasenotes reno>=0.1.1 # Apache2 From e378d32353ab7e3a604c23ff765bc82823563301 Mon Sep 17 00:00:00 2001 From: Vincent Untz Date: Tue, 17 Nov 2015 08:58:41 +0100 Subject: [PATCH 0978/1705] Fix running functional tests against deployment with insecure SSL Change-Id: I106d571e7ddeaeaa1de1ffeff3e88e1eb6898032 --- novaclient/tests/functional/README.rst | 5 +++++ novaclient/tests/functional/base.py | 9 +++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/novaclient/tests/functional/README.rst b/novaclient/tests/functional/README.rst index 727c40aba..b343b33d4 100644 --- a/novaclient/tests/functional/README.rst +++ b/novaclient/tests/functional/README.rst @@ -47,4 +47,9 @@ Functional Test Guidelines OS_TENANT_NAME OS_AUTH_URL +* Usage of insecure SSL can be configured via the standard client environment + variable:: + + OS_INSECURE + * Try not to require an additional configuration file diff --git a/novaclient/tests/functional/base.py b/novaclient/tests/functional/base.py index 16e5f3781..09e3a1990 100644 --- a/novaclient/tests/functional/base.py +++ b/novaclient/tests/functional/base.py @@ -147,6 +147,10 @@ def setUp(self): passwd = auth_info['password'] tenant = auth_info['project_name'] auth_url = auth_info['auth_url'] + if 'insecure' in cloud_config.config: + insecure = cloud_config.config['insecure'] + else: + insecure = False if self.COMPUTE_API_VERSION == "2.latest": version = novaclient.API_MAX_VERSION.get_string() @@ -154,7 +158,7 @@ def setUp(self): version = self.COMPUTE_API_VERSION or "2" self.client = novaclient.client.Client( version, user, passwd, tenant, - auth_url=auth_url) + auth_url=auth_url, insecure=insecure) # pick some reasonable flavor / image combo self.flavor = pick_flavor(self.client.flavors.list()) @@ -174,7 +178,8 @@ def setUp(self): password=passwd, tenant_name=tenant, uri=auth_url, - cli_dir=cli_dir) + cli_dir=cli_dir, + insecure=insecure) def nova(self, action, flags='', params='', fail_ok=False, endpoint_type='publicURL', merge_stderr=False): From ecaca2279e1779c0c9f68c98e08d6ff2c9bb1bdb Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Wed, 10 Feb 2016 13:49:53 +0200 Subject: [PATCH 0979/1705] [microversions] Skip microversion 2.18 2.18 - Establishes a set of routes that makes project_id an optional construct in v2.1. The change on Nova-API side adds only check for existence of "project_id" at the url. It doesn't check microversion, so all latest and previous microversions can work with or without project_id. Proof: all functional tests are succeed on this change (several of them use latest microversion). To cut down "project_id" from the url at the novaclient side, we need to modify ``novaclient.client.HttpClient`` and ``novaclient.client.SessionClient``. This change requires splitting Nova's related requests and the others(authentication, volume...), which are still require "project_id" at the url. It is a complex task, which can be skipped for now to unblock implementation of further microversions. Change-Id: Ia6e608aac41d2f2d59b9504d21647e4f88af3335 --- novaclient/__init__.py | 2 +- novaclient/tests/unit/v2/test_shell.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 1a2ae922e..d0859b114 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.17") +API_MAX_VERSION = api_versions.APIVersion("2.18") diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 0869a1105..8e110c578 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2677,6 +2677,14 @@ def test_versions(self): 9, # doesn't require any changes in novaclient 15, # doesn't require any changes in novaclient 16, # doesn't require any changes in novaclient + 18, # NOTE(andreykurilin): this microversion requires changes in + # HttpClient and our SessionClient, which is based on + # keystoneauth1.session. Skipping this complicated change + # allows to unblock implementation further microversions + # before feature-freeze + # (we can do it, since nova-api change didn't actually add + # new microversion, just an additional checks. See + # https://review.openstack.org/#/c/233076/ for more details) ]) versions_supported = set(range(0, novaclient.API_MAX_VERSION.ver_minor + 1)) From f535e6945b64509e2b9a1298fc502697691aa595 Mon Sep 17 00:00:00 2001 From: Cao Shufeng Date: Fri, 5 Feb 2016 16:59:21 +0800 Subject: [PATCH 0980/1705] Provide user with coherent interface to get request_ids Currently, users must use different interface to get request_ids. 1. Commonly, ListWithMeta, DictWithMeta or TupleWithMeta, etc is returned to user. And request_ids is returned as an attribute of *WithMeta. For a server delete action: result = serverManager.delete(server) # result = TupelWithMeta request_ids = result.request_ids 2. Some places reutrn a tuple which contains request_ids in one of its elements. For a server stop action: result = serverManager.stop(server) # result = (Response, TupleWithMeta) request_ids = result[1].request_ids Such kind of difference makes user confused. This change provides a backwards compatibility solution for users who don't care about request_ids as TupleWithMeta is subclass of tuple. Change-Id: I164bb95abce6dc96cf455033f02822ccb0d87b8f Closes-Bug: 1542179 --- novaclient/tests/unit/v2/test_hosts.py | 6 +-- novaclient/tests/unit/v2/test_servers.py | 36 ++++++++--------- novaclient/v2/hosts.py | 6 +-- novaclient/v2/servers.py | 50 +++++++++--------------- 4 files changed, 42 insertions(+), 56 deletions(-) diff --git a/novaclient/tests/unit/v2/test_hosts.py b/novaclient/tests/unit/v2/test_hosts.py index 70527ce87..aa5186546 100644 --- a/novaclient/tests/unit/v2/test_hosts.py +++ b/novaclient/tests/unit/v2/test_hosts.py @@ -73,21 +73,21 @@ def test_update_both(self): def test_host_startup(self): host = self.cs.hosts.get('sample_host')[0] - resp, result = host.startup() + result = host.startup() self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) self.assert_called( 'GET', '/os-hosts/sample_host/startup') def test_host_reboot(self): host = self.cs.hosts.get('sample_host')[0] - resp, result = host.reboot() + result = host.reboot() self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) self.assert_called( 'GET', '/os-hosts/sample_host/reboot') def test_host_shutdown(self): host = self.cs.hosts.get('sample_host')[0] - resp, result = host.shutdown() + result = host.shutdown() self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) self.assert_called( 'GET', '/os-hosts/sample_host/shutdown') diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index 171a5dbc3..277abff36 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -597,28 +597,28 @@ def test_remove_floating_ip(self): def test_stop(self): s = self.cs.servers.get(1234) - resp, ret = s.stop() + ret = s.stop() self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') - resp, ret = self.cs.servers.stop(s) + ret = self.cs.servers.stop(s) self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') def test_force_delete(self): s = self.cs.servers.get(1234) - resp, ret = s.force_delete() + ret = s.force_delete() self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') - resp, ret = self.cs.servers.force_delete(s) + ret = self.cs.servers.force_delete(s) self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') def test_restore(self): s = self.cs.servers.get(1234) - resp, ret = s.restore() + ret = s.restore() self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') - resp, ret = self.cs.servers.restore(s) + ret = self.cs.servers.restore(s) self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') @@ -633,31 +633,31 @@ def test_start(self): def test_rescue(self): s = self.cs.servers.get(1234) - resp, ret = s.rescue() + ret = s.rescue() self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') - resp, ret = self.cs.servers.rescue(s) + ret = self.cs.servers.rescue(s) self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') def test_rescue_password(self): s = self.cs.servers.get(1234) - resp, ret = s.rescue(password='asdf') + ret = s.rescue(password='asdf') self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action', {'rescue': {'adminPass': 'asdf'}}) - resp, ret = self.cs.servers.rescue(s, password='asdf') + ret = self.cs.servers.rescue(s, password='asdf') self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action', {'rescue': {'adminPass': 'asdf'}}) def test_rescue_image(self): s = self.cs.servers.get(1234) - resp, ret = s.rescue(image=1) + ret = s.rescue(image=1) self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action', {'rescue': {'rescue_image_ref': 1}}) - resp, ret = self.cs.servers.rescue(s, image=1) + ret = self.cs.servers.rescue(s, image=1) self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action', {'rescue': {'rescue_image_ref': 1}}) @@ -768,18 +768,18 @@ def test_clear_password(self): def test_get_server_diagnostics(self): s = self.cs.servers.get(1234) - resp, diagnostics = s.diagnostics() + diagnostics = s.diagnostics() self.assert_request_id(diagnostics, fakes.FAKE_REQUEST_ID_LIST) self.assertIsNotNone(diagnostics) self.assert_called('GET', '/servers/1234/diagnostics') - resp, diagnostics_from_manager = self.cs.servers.diagnostics(1234) + diagnostics_from_manager = self.cs.servers.diagnostics(1234) self.assert_request_id(diagnostics_from_manager, fakes.FAKE_REQUEST_ID_LIST) self.assertIsNotNone(diagnostics_from_manager) self.assert_called('GET', '/servers/1234/diagnostics') - self.assertEqual(diagnostics, diagnostics_from_manager) + self.assertEqual(diagnostics[1], diagnostics_from_manager[1]) def test_get_vnc_console(self): s = self.cs.servers.get(1234) @@ -892,11 +892,11 @@ def test_list_security_group(self): def test_evacuate(self): s = self.cs.servers.get(1234) - resp, ret = s.evacuate('fake_target_host', 'True') + ret = s.evacuate('fake_target_host', 'True') self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') - resp, ret = self.cs.servers.evacuate(s, 'fake_target_host', - 'False', 'NewAdminPassword') + ret = self.cs.servers.evacuate(s, 'fake_target_host', + 'False', 'NewAdminPassword') self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') diff --git a/novaclient/v2/hosts.py b/novaclient/v2/hosts.py index ff78fb30b..28518567d 100644 --- a/novaclient/v2/hosts.py +++ b/novaclient/v2/hosts.py @@ -74,13 +74,11 @@ def host_action(self, host, action): :param host: The host to perform an action :param actiob: The action to perform - :returns: A Response object and an instance of - novaclient.base.DictWithMeta + returns: An instance of novaclient.base.TupleWithMeta """ url = '/os-hosts/{0}/{1}'.format(host, action) resp, body = self.api.client.get(url) - # For compatibility, return Response object as a first return value - return resp, self.convert_into_with_meta(body, resp) + return base.TupleWithMeta((resp, body), resp) def list(self, zone=None): url = '/os-hosts' diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index 7120df1ab..d5baa679c 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -157,8 +157,7 @@ def stop(self): """ Stop -- Stop the running server. - :returns: A Response object and an instance of - novaclient.base.TupleWithMeta + :returns: An instance of novaclient.base.TupleWithMeta """ return self.manager.stop(self) @@ -166,8 +165,7 @@ def force_delete(self): """ Force delete -- Force delete a server. - :returns: A Response object and an instance of - novaclient.base.TupleWithMeta + :returns: An instance of novaclient.base.TupleWithMeta """ return self.manager.force_delete(self) @@ -175,8 +173,7 @@ def restore(self): """ Restore -- Restore a server in 'soft-deleted' state. - :returns: A Response object and an instance of - novaclient.base.TupleWithMeta + :returns: An instance of novaclient.base.TupleWithMeta """ return self.manager.restore(self) @@ -242,8 +239,7 @@ def rescue(self, password=None, image=None): :param password: The admin password to be set in the rescue instance. :param image: The :class:`Image` to rescue with. - :returns: A Response object and an instance of - novaclient.base.DictWithMeta + :returns: An instance of novaclient.base.TupleWithMeta """ return self.manager.rescue(self, password, image) @@ -462,8 +458,7 @@ def evacuate(self, host=None, on_shared_storage=None, password=None): parameter must have its default value of None. :param password: string to set as admin password on the evacuated server. - :returns: A Response object and an instance of - novaclient.base.TupleWithMeta + :returns: An instance of novaclient.base.TupleWithMeta """ if api_versions.APIVersion("2.14") <= self.manager.api_version: if on_shared_storage is not None: @@ -938,34 +933,31 @@ def stop(self, server): Stop the server. :param server: The :class:`Server` (or its ID) to stop - :returns: A Response object and an instance of - novaclient.base.TupleWithMeta + :returns: An instance of novaclient.base.TupleWithMeta """ resp, body = self._action_return_resp_and_body('os-stop', server, None) - return resp, self.convert_into_with_meta(body, resp) + return base.TupleWithMeta((resp, body), resp) def force_delete(self, server): """ Force delete the server. :param server: The :class:`Server` (or its ID) to force delete - :returns: A Response object and an instance of - novaclient.base.TupleWithMeta + :returns: An instance of novaclient.base.TupleWithMeta """ resp, body = self._action_return_resp_and_body('forceDelete', server, None) - return resp, self.convert_into_with_meta(body, resp) + return base.TupleWithMeta((resp, body), resp) def restore(self, server): """ Restore soft-deleted server. :param server: The :class:`Server` (or its ID) to restore - :returns: A Response object and an instance of - novaclient.base.TupleWithMeta + :returns: An instance of novaclient.base.TupleWithMeta """ resp, body = self._action_return_resp_and_body('restore', server, None) - return resp, self.convert_into_with_meta(body, resp) + return base.TupleWithMeta((resp, body), resp) def start(self, server): """ @@ -1037,8 +1029,7 @@ def rescue(self, server, password=None, image=None): :param server: The :class:`Server` to rescue. :param password: The admin password to be set in the rescue instance. :param image: The :class:`Image` to rescue with. - :returns: A Response object and an instance of - novaclient.base.DictWithMeta + :returns: An instance of novaclient.base.TupleWithMeta """ info = {} if password: @@ -1047,8 +1038,7 @@ def rescue(self, server, password=None, image=None): info['rescue_image_ref'] = base.getid(image) resp, body = self._action_return_resp_and_body('rescue', server, info or None) - # For compatibility, return Response object as a first return value - return resp, base.DictWithMeta(body, resp) + return base.TupleWithMeta((resp, body), resp) def unrescue(self, server): """ @@ -1106,13 +1096,11 @@ def diagnostics(self, server): :param server: The :class:`Server` (or its ID) for which diagnostics to be returned - :returns: A Respose object and an instance of - novaclient.base.DictWithMeta + :returns: An instance of novaclient.base.TupleWithMeta """ resp, body = self.api.client.get("/servers/%s/diagnostics" % base.getid(server)) - # For compatibility, return Response object as a first return value - return resp, base.DictWithMeta(body, resp) + return base.TupleWithMeta((resp, body), resp) def create(self, name, image, flavor, meta=None, files=None, reservation_id=None, min_count=None, @@ -1515,8 +1503,7 @@ def evacuate(self, server, host=None, on_shared_storage=True, :param on_shared_storage: Specifies whether instance files located on shared storage :param password: string to set as password on the evacuated server. - :returns: A Response object and an instance of - novaclient.base.TupleWithMeta + :returns: An instance of novaclient.base.TupleWithMeta """ body = {'onSharedStorage': on_shared_storage} @@ -1528,7 +1515,7 @@ def evacuate(self, server, host=None, on_shared_storage=True, resp, body = self._action_return_resp_and_body('evacuate', server, body) - return resp, self.convert_into_with_meta(body, resp) + return base.TupleWithMeta((resp, body), resp) @api_versions.wraps("2.14") def evacuate(self, server, host=None, password=None): @@ -1538,6 +1525,7 @@ def evacuate(self, server, host=None, password=None): :param server: The :class:`Server` (or its ID) to share onto. :param host: Name of the target host. :param password: string to set as password on the evacuated server. + :returns: An instance of novaclient.base.TupleWithMeta """ body = {} @@ -1549,7 +1537,7 @@ def evacuate(self, server, host=None, password=None): resp, body = self._action_return_resp_and_body('evacuate', server, body) - return resp, self.convert_into_with_meta(body, resp) + return base.TupleWithMeta((resp, body), resp) def interface_list(self, server): """ From 8a2ed13620e78f4b82e68028e86745e8c86240b0 Mon Sep 17 00:00:00 2001 From: Ralf Haferkamp Date: Wed, 28 Oct 2015 15:33:22 +0100 Subject: [PATCH 0981/1705] Allow to specify a network for functional tests Currently the tests just attach instances to the first network returned by "net-list". That might however not be the right thing in some environments. This change allows to override the default network via the environment variable "OS_NOVACLIENT_NETWORK". If not specified the test will fallback to the old behaviour and just use the first network. Closes-Bug: #1510975 Change-Id: Ie682111127584a33d8e96377d812d3a6352c760d --- novaclient/tests/functional/api/test_servers.py | 5 +++-- novaclient/tests/functional/base.py | 16 ++++++++++++++++ .../tests/functional/v2/legacy/test_instances.py | 3 +-- .../tests/functional/v2/legacy/test_servers.py | 4 ++-- tox.ini | 2 ++ 5 files changed, 24 insertions(+), 6 deletions(-) diff --git a/novaclient/tests/functional/api/test_servers.py b/novaclient/tests/functional/api/test_servers.py index df6671fa2..936bfeac9 100644 --- a/novaclient/tests/functional/api/test_servers.py +++ b/novaclient/tests/functional/api/test_servers.py @@ -19,7 +19,8 @@ class TestServersAPI(base.ClientTestBase): def test_server_ips(self): server_name = "test_server" initial_server = self.client.servers.create( - server_name, self.image, self.flavor) + server_name, self.image, self.flavor, + nics=[{"net-id": self.network.id}]) self.addCleanup(initial_server.delete) for x in range(60): @@ -32,4 +33,4 @@ def test_server_ips(self): self.fail("Server %s did not go ACTIVE after 60s" % server) ips = self.client.servers.ips(server) - self.assertIn('private', ips) + self.assertIn(self.network.label, ips) diff --git a/novaclient/tests/functional/base.py b/novaclient/tests/functional/base.py index 09e3a1990..e148d0b8b 100644 --- a/novaclient/tests/functional/base.py +++ b/novaclient/tests/functional/base.py @@ -45,6 +45,16 @@ def pick_image(images): raise NoImageException() +def pick_network(networks): + network_name = os.environ.get('OS_NOVACLIENT_NETWORK') + if network_name: + for network in networks: + if network.label == network_name: + return network + raise NoNetworkException() + return networks[0] + + class NoImageException(Exception): """We couldn't find an acceptable image.""" pass @@ -55,6 +65,11 @@ class NoFlavorException(Exception): pass +class NoNetworkException(Exception): + """We couldn't find an acceptable network.""" + pass + + class NoCloudConfigException(Exception): """We couldn't find a cloud configuration.""" pass @@ -163,6 +178,7 @@ def setUp(self): # pick some reasonable flavor / image combo self.flavor = pick_flavor(self.client.flavors.list()) self.image = pick_image(self.client.images.list()) + self.network = pick_network(self.client.networks.list()) # create a CLI client in case we'd like to do CLI # testing. tempest_lib does this really weird thing where it diff --git a/novaclient/tests/functional/v2/legacy/test_instances.py b/novaclient/tests/functional/v2/legacy/test_instances.py index d512106ec..39e78ee51 100644 --- a/novaclient/tests/functional/v2/legacy/test_instances.py +++ b/novaclient/tests/functional/v2/legacy/test_instances.py @@ -40,10 +40,9 @@ def test_attach_volume(self): name = self.name_generate('Instance') # Boot via the cli, as we're primarily testing the cli in this test - network = self.client.networks.list()[0] self.nova('boot', params="--flavor %s --image %s %s --nic net-id=%s --poll" % - (self.flavor.name, self.image.name, name, network.id)) + (self.flavor.name, self.image.name, name, self.network.id)) # Be nice about cleaning up, however, use the API for this to avoid # parsing text. diff --git a/novaclient/tests/functional/v2/legacy/test_servers.py b/novaclient/tests/functional/v2/legacy/test_servers.py index 23d3835d5..e13cd1ef2 100644 --- a/novaclient/tests/functional/v2/legacy/test_servers.py +++ b/novaclient/tests/functional/v2/legacy/test_servers.py @@ -75,11 +75,11 @@ class TestServersListNovaClient(base.ClientTestBase): COMPUTE_API_VERSION = "2.1" def _create_servers(self, name, number): - network = self.client.networks.list()[0] servers = [] for i in range(number): servers.append(self.client.servers.create( - name, self.image, self.flavor, nics=[{"net-id": network.id}])) + name, self.image, self.flavor, + nics=[{"net-id": self.network.id}])) shell._poll_for_status( self.client.servers.get, servers[-1].id, 'building', ['active']) diff --git a/tox.ini b/tox.ini index 845523a4c..b8a5004de 100644 --- a/tox.ini +++ b/tox.ini @@ -36,12 +36,14 @@ commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasen [testenv:functional] basepython = python2.7 +passenv = OS_NOVACLIENT_TEST_NETWORK setenv = OS_TEST_PATH = ./novaclient/tests/functional commands = python setup.py testr --testr-args='--concurrency=1 {posargs}' [testenv:functional-py34] basepython = python3.4 +passenv = OS_NOVACLIENT_TEST_NETWORK setenv = OS_TEST_PATH = ./novaclient/tests/functional commands = python setup.py testr --testr-args='--concurrency=1 {posargs}' From 1d1e43957dc785231c50a105229814b06eb74752 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Wed, 10 Feb 2016 16:05:48 +0200 Subject: [PATCH 0982/1705] [microversions] Add support for 2.19 2.19 - Allow the user to set and get the server description. The user will be able to set the description when creating, rebuilding, or updating a server, and get the description as part of the server details. Methods `rebuild` and `create` of novaclient.v2.servers.ServerManager were not wrapped with `api_versions.wraps` decorator to reduce code and docsting duplication. Version checks added inside these methods. Change-Id: I75b804c6edd0cdf02c2cd002d0b5968fec8da545 --- novaclient/__init__.py | 2 +- novaclient/exceptions.py | 18 ++++++ .../tests/functional/v2/test_servers.py | 35 +++++++++++ novaclient/tests/unit/v2/test_servers.py | 44 +++++++++++++ novaclient/v2/servers.py | 61 +++++++++++++++++-- novaclient/v2/shell.py | 54 +++++++++++++++- 6 files changed, 206 insertions(+), 8 deletions(-) diff --git a/novaclient/__init__.py b/novaclient/__init__.py index d0859b114..e9b2a802c 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.18") +API_MAX_VERSION = api_versions.APIVersion("2.19") diff --git a/novaclient/exceptions.py b/novaclient/exceptions.py index b9bdf1140..c502e9641 100644 --- a/novaclient/exceptions.py +++ b/novaclient/exceptions.py @@ -24,6 +24,24 @@ class UnsupportedVersion(Exception): pass +class UnsupportedAttribute(AttributeError): + """Indicates that the user is trying to transmit the argument to a method, + which is not supported by selected version. + """ + + def __init__(self, argument_name, start_version, end_version=None): + if end_version: + self.message = ( + "'%(name)s' argument is only allowed for microversions " + "%(start)s - %(end)s." % {"name": argument_name, + "start": start_version, + "end": end_version}) + else: + self.message = ( + "'%(name)s' argument is only allowed since microversion " + "%(start)s." % {"name": argument_name, "start": start_version}) + + class CommandError(Exception): pass diff --git a/novaclient/tests/functional/v2/test_servers.py b/novaclient/tests/functional/v2/test_servers.py index 590e543bf..de5e57d97 100644 --- a/novaclient/tests/functional/v2/test_servers.py +++ b/novaclient/tests/functional/v2/test_servers.py @@ -53,3 +53,38 @@ def test_attribute_presented(self): self.nova("unlock %s" % server.id) self._show_server_and_check_lock_attr(server, False) + + +class TestServersDescription(base.ClientTestBase): + + COMPUTE_API_VERSION = "2.19" + + def _boot_server_with_description(self): + name = str(uuid.uuid4()) + network = self.client.networks.list()[0] + descr = "Some words about this test VM." + server = self.client.servers.create( + name, self.image, self.flavor, nics=[{"net-id": network.id}], + description=descr) + self.addCleanup(server.delete) + + self.assertEqual(descr, server.description) + + return server, descr + + def test_create(self): + server, descr = self._boot_server_with_description() + + output = self.nova("show %s" % server.id) + self.assertEqual(descr, self._get_value_from_the_table(output, + "description")) + + def test_update(self): + server, descr = self._boot_server_with_description() + + # remove description + self.nova("update %s --description ''" % server.id) + + output = self.nova("show %s" % server.id) + self.assertEqual("-", self._get_value_from_the_table(output, + "description")) diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index 171a5dbc3..b8a8bd16b 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -945,6 +945,18 @@ def test_interface_detach(self): self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('DELETE', '/servers/1234/os-interface/port-id') + def test_create_server_with_description(self): + self.assertRaises(exceptions.UnsupportedAttribute, + self.cs.servers.create, + name="My server", + description="descr", + image=1, + flavor=1, + meta={'foo': 'bar'}, + userdata="hello moto", + key_name="fakekey" + ) + class ServersV26Test(ServersTest): def setUp(self): @@ -1033,3 +1045,35 @@ def test_trigger_crash_dump(self): self.assert_called('POST', '/servers/1234/action') self.cs.servers.trigger_crash_dump(s) self.assert_called('POST', '/servers/1234/action') + + +class ServersV219Test(ServersV217Test): + def setUp(self): + super(ServersV219Test, self).setUp() + self.cs.api_version = api_versions.APIVersion("2.19") + + def test_create_server_with_description(self): + self.cs.servers.create( + name="My server", + description="descr", + image=1, + flavor=1, + meta={'foo': 'bar'}, + userdata="hello moto", + key_name="fakekey" + ) + self.assert_called('POST', '/servers') + + def test_update_server_with_description(self): + s = self.cs.servers.get(1234) + + s.update(description='hi') + s.update(name='hi', description='hi') + self.assert_called('PUT', '/servers/1234') + + def test_rebuild_with_description(self): + s = self.cs.servers.get(1234) + + ret = s.rebuild(image="1", description="descr") + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index 7120df1ab..961ae61da 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -28,6 +28,7 @@ from novaclient import api_versions from novaclient import base from novaclient import crypto +from novaclient import exceptions from novaclient.i18n import _ from novaclient.v2 import security_groups @@ -49,14 +50,22 @@ def delete(self): """ return self.manager.delete(self) - def update(self, name=None): + def update(self, name=None, description=None): """ - Update the name for this server. + Update the name and the description for this server. :param name: Update the server's name. + :param description: Update the server's description( + allowed for 2.19-latest). :returns: :class:`Server` """ - return self.manager.update(self, name=name) + if (description is not None and + self.manager.api_version < api_versions.APIVersion("2.19")): + raise exceptions.UnsupportedAttribute("description", "2.19") + update_kwargs = {"name": name} + if description is not None: + update_kwargs["description"] = description + return self.manager.update(self, **update_kwargs) def get_console_output(self, length=None): """ @@ -510,7 +519,8 @@ def _boot(self, resource_url, response_key, name, image, flavor, availability_zone=None, block_device_mapping=None, block_device_mapping_v2=None, nics=None, scheduler_hints=None, config_drive=None, admin_pass=None, disk_config=None, - access_ip_v4=None, access_ip_v6=None, **kwargs): + access_ip_v4=None, access_ip_v6=None, description=None, + **kwargs): """ Create (boot) a new server. """ @@ -632,6 +642,9 @@ def _boot(self, resource_url, response_key, name, image, flavor, if access_ip_v6 is not None: body['server']['accessIPv6'] = access_ip_v6 + if description: + body['server']['description'] = description + return self._create(resource_url, body, response_key, return_raw=return_raw, **kwargs) @@ -1168,6 +1181,8 @@ def create(self, name, image, flavor, meta=None, files=None, password. :param access_ip_v4: (optional extension) add alternative access ip v4 :param access_ip_v6: (optional extension) add alternative access ip v6 + :param description: optional description of the server (allowed since + microversion 2.19) """ if not min_count: min_count = 1 @@ -1178,6 +1193,10 @@ def create(self, name, image, flavor, meta=None, files=None, boot_args = [name, image, flavor] + descr_microversion = api_versions.APIVersion("2.19") + if "description" in kwargs and self.api_version < descr_microversion: + raise exceptions.UnsupportedAttribute("description", "2.19") + boot_kwargs = dict( meta=meta, files=files, userdata=userdata, reservation_id=reservation_id, min_count=min_count, @@ -1202,9 +1221,10 @@ def create(self, name, image, flavor, meta=None, files=None, return self._boot(resource_url, response_key, *boot_args, **boot_kwargs) + @api_versions.wraps("2.0", "2.18") def update(self, server, name=None): """ - Update the name or the password for a server. + Update the name for a server. :param server: The :class:`Server` (or its ID) to update. :param name: Update the server's name. @@ -1220,6 +1240,29 @@ def update(self, server, name=None): return self._update("/servers/%s" % base.getid(server), body, "server") + @api_versions.wraps("2.19") + def update(self, server, name=None, description=None): + """ + Update the name or the description for a server. + + :param server: The :class:`Server` (or its ID) to update. + :param name: Update the server's name. + :param description: Update the server's description. If it equals to + empty string(i.g. ""), the server description will be removed. + """ + if name is None and description is None: + return + + body = {"server": {}} + if name: + body["server"]["name"] = name + if description == "": + body["server"]["description"] = None + elif description: + body["server"]["description"] = description + + return self._update("/servers/%s" % base.getid(server), body, "server") + def change_password(self, server, password): """ Update the password for a server. @@ -1271,8 +1314,14 @@ def rebuild(self, server, image, password=None, disk_config=None, are the file contents (either as a string or as a file-like object). A maximum of five entries is allowed, and each file must be 10k or less. + :param description: optional description of the server (allowed since + microversion 2.19) :returns: :class:`Server` """ + descr_microversion = api_versions.APIVersion("2.19") + if "description" in kwargs and self.api_version < descr_microversion: + raise exceptions.UnsupportedAttribute("description", "2.19") + body = {'imageRef': base.getid(image)} if password is not None: body['adminPass'] = password @@ -1282,6 +1331,8 @@ def rebuild(self, server, image, password=None, disk_config=None, body['preserve_ephemeral'] = True if name is not None: body['name'] = name + if "description" in kwargs: + body["description"] = kwargs["description"] if meta: body['metadata'] = meta if files: diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index f16d59031..95b892d89 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -350,6 +350,9 @@ def _boot(cs, args): access_ip_v4=args.access_ip_v4, access_ip_v6=args.access_ip_v6) + if 'description' in args: + boot_kwargs["description"] = args.description + return boot_args, boot_kwargs @@ -569,6 +572,13 @@ def _boot(cs, args): metavar='', default=None, help=_('Alternative access IPv6 of the instance.')) +@cliutils.arg( + '--description', + metavar='', + dest='description', + default=None, + help=_('Description for the server.'), + start_version="2.19") def do_boot(cs, args): """Boot a new server.""" boot_args, boot_kwargs = _boot(cs, args) @@ -1624,6 +1634,13 @@ def do_reboot(cs, args): metavar='', default=None, help=_('Name for the new server.')) +@cliutils.arg( + '--description', + metavar='', + dest='description', + default=None, + help=_('New description for the server.'), + start_version="2.19") @cliutils.arg( '--meta', metavar="", @@ -1652,6 +1669,8 @@ def do_rebuild(cs, args): kwargs = utils.get_resource_manager_extra_kwargs(do_rebuild, args) kwargs['preserve_ephemeral'] = args.preserve_ephemeral kwargs['name'] = args.name + if 'description' in args: + kwargs['description'] = args.description meta = _meta_parsing(args.meta) kwargs['meta'] = meta @@ -1682,8 +1701,39 @@ def do_rebuild(cs, args): help=_('Name (old name) or ID of server.')) @cliutils.arg('name', metavar='', help=_('New name for the server.')) def do_rename(cs, args): - """Rename a server.""" - _find_server(cs, args.server).update(name=args.name) + """DEPRECATED, use update instead.""" + do_update(cs, args) + + +@cliutils.arg( + 'server', metavar='', + help=_('Name (old name) or ID of server.')) +@cliutils.arg( + '--name', + metavar='', + dest='name', + default=None, + help=_('New name for the server.')) +@cliutils.arg( + '--description', + metavar='', + dest='description', + default=None, + help=_('New description for the server. If it equals to empty string ' + '(i.g. ""), the server description will be removed.'), + start_version="2.19") +def do_update(cs, args): + """Update the name or the description for a server.""" + update_kwargs = {} + if args.name: + update_kwargs["name"] = args.name + # NOTE(andreykurilin): `do_update` method is used by `do_rename` method, + # which do not have description argument at all. When `do_rename` will be + # removed after deprecation period, feel free to change the check below to: + # `if args.description:` + if "description" in args and args.description is not None: + update_kwargs["description"] = args.description + _find_server(cs, args.server).update(**update_kwargs) @cliutils.arg('server', metavar='', help=_('Name or ID of server.')) From 55a97ea2675bb572c90a1c5be5039540221e1745 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Tue, 16 Feb 2016 16:54:07 +0200 Subject: [PATCH 0983/1705] [microversions] Turn off check for header in response We need to check header only for nova-related calls. While we are unable to determine nova's calls, let's just temporary disable it now and turn on it in the future with 2.18 microversion. Change-Id: I92cecb140fc478f0cf37783d2fcfcaccd935bdfc NOTE: this check affects only novaclient-as-a-lib --- novaclient/client.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index 003815245..bd1faf7a8 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -87,7 +87,9 @@ def request(self, url, method, **kwargs): method, raise_exc=False, **kwargs) - api_versions.check_headers(resp, self.api_version) + # TODO(andreykurilin): uncomment this line, when we will be able to + # check only nova-related calls + # api_versions.check_headers(resp, self.api_version) if raise_exc and resp.status_code >= 400: raise exceptions.from_response(resp, body, url, method) @@ -365,7 +367,9 @@ def request(self, url, method, **kwargs): url, **kwargs) - api_versions.check_headers(resp, self.api_version) + # TODO(andreykurilin): uncomment this line, when we will be able to + # check only nova-related calls + # api_versions.check_headers(resp, self.api_version) self.http_log_resp(resp) From f279b086d78eaf84d6225aa3f4366f9c908d4f23 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Wed, 10 Feb 2016 17:28:43 +0200 Subject: [PATCH 0984/1705] [microversions] Enable 2.20 2.20 - From this version of the API user can call detach and attach volumes for instances which are in shelved and shelved_offloaded state. This change changes only server-side checks. Change-Id: I15a988c730d2fb0be4416dc79b63d357ae990ff7 --- novaclient/__init__.py | 2 +- novaclient/tests/unit/v2/test_shell.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/novaclient/__init__.py b/novaclient/__init__.py index e9b2a802c..b9f008cca 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.19") +API_MAX_VERSION = api_versions.APIVersion("2.20") diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 8e110c578..eb86eee90 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2685,6 +2685,7 @@ def test_versions(self): # (we can do it, since nova-api change didn't actually add # new microversion, just an additional checks. See # https://review.openstack.org/#/c/233076/ for more details) + 20, # doesn't require any changes in novaclient ]) versions_supported = set(range(0, novaclient.API_MAX_VERSION.ver_minor + 1)) From 083ce7eeeada3e4d73c55e4e2d7239febb3993f6 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Tue, 22 Dec 2015 16:41:03 +0200 Subject: [PATCH 0985/1705] [functional] Move code for boot vm to base testcase It would be nice to reduce code duplication. Change-Id: Ie430b64c7e08f6cba8771c85cc1894fa71357ccb --- novaclient/tests/functional/base.py | 15 +++++++++++++++ .../tests/functional/v2/legacy/test_consoles.py | 10 ---------- .../v2/legacy/test_extended_attributes.py | 10 ++-------- .../tests/functional/v2/legacy/test_fixedips.py | 12 +----------- .../tests/functional/v2/legacy/test_servers.py | 13 +------------ .../v2/legacy/test_virtual_interface.py | 12 ------------ novaclient/tests/functional/v2/test_servers.py | 15 ++------------- 7 files changed, 21 insertions(+), 66 deletions(-) diff --git a/novaclient/tests/functional/base.py b/novaclient/tests/functional/base.py index e148d0b8b..42cef9e15 100644 --- a/novaclient/tests/functional/base.py +++ b/novaclient/tests/functional/base.py @@ -23,6 +23,7 @@ import novaclient import novaclient.api_versions import novaclient.client +import novaclient.v2.shell # The following are simple filter functions that filter our available @@ -307,6 +308,20 @@ def _get_column_value_from_single_row_table(self, table, column): raise ValueError("Unable to find value for column '%s'.") + def _create_server(self, name=None, with_network=True, **kwargs): + name = name or self.name_generate(prefix='server') + if with_network: + nics = [{"net-id": self.network.id}] + else: + nics = None + server = self.client.servers.create(name, self.image, self.flavor, + nics=nics, **kwargs) + self.addCleanup(server.delete) + novaclient.v2.shell._poll_for_status( + self.client.servers.get, server.id, + 'building', ['active']) + return server + class TenantTestBase(ClientTestBase): """Base test class for additional tenant and user creation which diff --git a/novaclient/tests/functional/v2/legacy/test_consoles.py b/novaclient/tests/functional/v2/legacy/test_consoles.py index 5ca022a72..318541f9b 100644 --- a/novaclient/tests/functional/v2/legacy/test_consoles.py +++ b/novaclient/tests/functional/v2/legacy/test_consoles.py @@ -14,7 +14,6 @@ from tempest_lib import exceptions from novaclient.tests.functional import base -from novaclient.v2 import shell class TestConsolesNovaClient(base.ClientTestBase): @@ -22,15 +21,6 @@ class TestConsolesNovaClient(base.ClientTestBase): COMPUTE_API_VERSION = "2.1" - def _create_server(self): - name = self.name_generate(prefix='server') - server = self.client.servers.create(name, self.image, self.flavor) - shell._poll_for_status( - self.client.servers.get, server.id, - 'building', ['active']) - self.addCleanup(server.delete) - return server - def _test_console_get(self, command): server = self._create_server() completed_command = command % server.id diff --git a/novaclient/tests/functional/v2/legacy/test_extended_attributes.py b/novaclient/tests/functional/v2/legacy/test_extended_attributes.py index 55da98826..ef579e91f 100644 --- a/novaclient/tests/functional/v2/legacy/test_extended_attributes.py +++ b/novaclient/tests/functional/v2/legacy/test_extended_attributes.py @@ -13,7 +13,6 @@ import json from novaclient.tests.functional import base -from novaclient.v2 import shell class TestExtAttrNovaClient(base.ClientTestBase): @@ -22,16 +21,11 @@ class TestExtAttrNovaClient(base.ClientTestBase): COMPUTE_API_VERSION = "2.1" def _create_server_and_attach_volume(self): - name = self.name_generate(prefix='server') - server = self.client.servers.create(name, self.image, self.flavor) - self.addCleanup(server.delete) - shell._poll_for_status( - self.client.servers.get, server.id, - 'building', ['active']) + server = self._create_server() volume = self.client.volumes.create(1) self.addCleanup(self.nova, 'volume-delete', params=volume.id) self.wait_for_volume_status(volume, 'available') - self.nova('volume-attach', params="%s %s" % (name, volume.id)) + self.nova('volume-attach', params="%s %s" % (server.name, volume.id)) self.addCleanup(self._release_volume, server, volume) self.wait_for_volume_status(volume, 'in-use') return server, volume diff --git a/novaclient/tests/functional/v2/legacy/test_fixedips.py b/novaclient/tests/functional/v2/legacy/test_fixedips.py index 0206a140c..283faa5a4 100644 --- a/novaclient/tests/functional/v2/legacy/test_fixedips.py +++ b/novaclient/tests/functional/v2/legacy/test_fixedips.py @@ -14,7 +14,6 @@ from oslo_utils import strutils from novaclient.tests.functional import base -from novaclient.v2 import shell class TestFixedIPsNovaClient(base.ClientTestBase): @@ -22,17 +21,8 @@ class TestFixedIPsNovaClient(base.ClientTestBase): COMPUTE_API_VERSION = '2.1' - def _create_server(self): - name = self.name_generate(prefix='server') - server = self.client.servers.create(name, self.image, self.flavor) - shell._poll_for_status( - self.client.servers.get, server.id, - 'building', ['active']) - self.addCleanup(server.delete) - return server - def _test_fixedip_get(self, expect_reserved=False): - server = self._create_server() + server = self._create_server(with_network=False) networks = server.networks self.assertIn('private', networks) fixed_ip = networks['private'][0] diff --git a/novaclient/tests/functional/v2/legacy/test_servers.py b/novaclient/tests/functional/v2/legacy/test_servers.py index e13cd1ef2..129988412 100644 --- a/novaclient/tests/functional/v2/legacy/test_servers.py +++ b/novaclient/tests/functional/v2/legacy/test_servers.py @@ -13,7 +13,6 @@ import uuid from novaclient.tests.functional import base -from novaclient.v2 import shell class TestServersBootNovaClient(base.ClientTestBase): @@ -75,17 +74,7 @@ class TestServersListNovaClient(base.ClientTestBase): COMPUTE_API_VERSION = "2.1" def _create_servers(self, name, number): - servers = [] - for i in range(number): - servers.append(self.client.servers.create( - name, self.image, self.flavor, - nics=[{"net-id": self.network.id}])) - shell._poll_for_status( - self.client.servers.get, servers[-1].id, - 'building', ['active']) - - self.addCleanup(servers[-1].delete) - return servers + return [self._create_server(name) for i in range(number)] def test_list_with_limit(self): name = str(uuid.uuid4()) diff --git a/novaclient/tests/functional/v2/legacy/test_virtual_interface.py b/novaclient/tests/functional/v2/legacy/test_virtual_interface.py index fe76dfbe5..f37b19be2 100644 --- a/novaclient/tests/functional/v2/legacy/test_virtual_interface.py +++ b/novaclient/tests/functional/v2/legacy/test_virtual_interface.py @@ -12,7 +12,6 @@ # under the License. from novaclient.tests.functional import base -from novaclient.v2 import shell class TestVirtualInterfacesNovaClient(base.ClientTestBase): @@ -20,17 +19,6 @@ class TestVirtualInterfacesNovaClient(base.ClientTestBase): COMPUTE_API_VERSION = "2.1" - def _create_server(self): - name = self.name_generate(prefix='server') - network = self.client.networks.list()[0] - server = self.client.servers.create( - name, self.image, self.flavor, nics=[{"net-id": network.id}]) - shell._poll_for_status( - self.client.servers.get, server.id, - 'building', ['active']) - self.addCleanup(server.delete) - return server - def test_virtual_interface_list(self): server = self._create_server() output = self.nova('virtual-interface-list %s' % server.id) diff --git a/novaclient/tests/functional/v2/test_servers.py b/novaclient/tests/functional/v2/test_servers.py index de5e57d97..cf6f45a17 100644 --- a/novaclient/tests/functional/v2/test_servers.py +++ b/novaclient/tests/functional/v2/test_servers.py @@ -10,8 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import uuid - from novaclient.tests.functional import base from novaclient.tests.functional.v2.legacy import test_servers @@ -39,11 +37,7 @@ def _show_server_and_check_lock_attr(self, server, value): def test_attribute_presented(self): # prepare - name = str(uuid.uuid4()) - network = self.client.networks.list()[0] - server = self.client.servers.create( - name, self.image, self.flavor, nics=[{"net-id": network.id}]) - self.addCleanup(server.delete) + server = self._create_server() # testing self._show_server_and_check_lock_attr(server, False) @@ -60,13 +54,8 @@ class TestServersDescription(base.ClientTestBase): COMPUTE_API_VERSION = "2.19" def _boot_server_with_description(self): - name = str(uuid.uuid4()) - network = self.client.networks.list()[0] descr = "Some words about this test VM." - server = self.client.servers.create( - name, self.image, self.flavor, nics=[{"net-id": network.id}], - description=descr) - self.addCleanup(server.delete) + server = self._create_server(description=descr) self.assertEqual(descr, server.description) From c18ccb1bfae574b4b496c138e9192fc737ed9c20 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Tue, 16 Feb 2016 15:35:57 +0200 Subject: [PATCH 0986/1705] Add a way to discover only contrib extensions Several OS projects(cinder, neutron, osc...) use `novaclient.discover_extensions` for initialization novaclient.client.Client with novaclient.v2.contrib extensions. In this case, it would be nice to provide a way to not discover extension via python path an entry-point. Change-Id: I030f4c55c2795c7f7973f5f12e54b9819c4a5578 Closes-Bug: #1509500 --- novaclient/client.py | 27 +++++++++------ novaclient/tests/unit/test_client.py | 51 ++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 11 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index 003815245..579a315d4 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -723,19 +723,24 @@ def _construct_http_client(username=None, password=None, project_id=None, api_version=api_version) -def discover_extensions(version): +def discover_extensions(version, only_contrib=False): + """Returns the list of extensions, which can be discovered by python path, + contrib path and by entry-point 'novaclient.extension'. + + :param version: api version + :type version: str or novaclient.api_versions.APIVersion + :param only_contrib: search only in contrib directory or not + :type only_contrib: bool + """ if not isinstance(version, api_versions.APIVersion): version = api_versions.get_api_version(version) - extensions = [] - for name, module in itertools.chain( - _discover_via_python_path(), - _discover_via_contrib_path(version), - _discover_via_entry_points()): - - extension = ext.Extension(name, module) - extensions.append(extension) - - return extensions + if only_contrib: + chain = _discover_via_contrib_path(version) + else: + chain = itertools.chain(_discover_via_python_path(), + _discover_via_contrib_path(version), + _discover_via_entry_points()) + return [ext.Extension(name, module) for name, module in chain] def _discover_via_python_path(): diff --git a/novaclient/tests/unit/test_client.py b/novaclient/tests/unit/test_client.py index f63fdd4aa..1325bf727 100644 --- a/novaclient/tests/unit/test_client.py +++ b/novaclient/tests/unit/test_client.py @@ -22,6 +22,7 @@ import mock import requests +import novaclient.api_versions import novaclient.client import novaclient.extension from novaclient.tests.unit import utils @@ -454,3 +455,53 @@ def test_timings(self, m_request): client.request("http://no.where", 'GET') self.assertEqual(1, len(client.times)) self.assertEqual('GET http://no.where', client.times[0][0]) + + +class DiscoverExtensionTest(utils.TestCase): + + @mock.patch("novaclient.client._discover_via_entry_points") + @mock.patch("novaclient.client._discover_via_contrib_path") + @mock.patch("novaclient.client._discover_via_python_path") + @mock.patch("novaclient.extension.Extension") + def test_discover_all(self, mock_extension, + mock_discover_via_python_path, + mock_discover_via_contrib_path, + mock_discover_via_entry_points): + def make_gen(start, end): + def f(*args, **kwargs): + for i in range(start, end): + yield "name-%s" % i, i + return f + + mock_discover_via_python_path.side_effect = make_gen(0, 3) + mock_discover_via_contrib_path.side_effect = make_gen(3, 5) + mock_discover_via_entry_points.side_effect = make_gen(5, 6) + + version = novaclient.api_versions.APIVersion("2.0") + + result = novaclient.client.discover_extensions(version) + + self.assertEqual([mock.call("name-%s" % i, i) for i in range(0, 6)], + mock_extension.call_args_list) + mock_discover_via_python_path.assert_called_once_with() + mock_discover_via_contrib_path.assert_called_once_with(version) + mock_discover_via_entry_points.assert_called_once_with() + self.assertEqual([mock_extension()] * 6, result) + + @mock.patch("novaclient.client._discover_via_entry_points") + @mock.patch("novaclient.client._discover_via_contrib_path") + @mock.patch("novaclient.client._discover_via_python_path") + @mock.patch("novaclient.extension.Extension") + def test_discover_only_contrib(self, mock_extension, + mock_discover_via_python_path, + mock_discover_via_contrib_path, + mock_discover_via_entry_points): + mock_discover_via_contrib_path.return_value = [("name", "module")] + + version = novaclient.api_versions.APIVersion("2.0") + + novaclient.client.discover_extensions(version, only_contrib=True) + mock_discover_via_contrib_path.assert_called_once_with(version) + self.assertFalse(mock_discover_via_python_path.called) + self.assertFalse(mock_discover_via_entry_points.called) + mock_extension.assert_called_once_with("name", "module") From 20682bd3d06bd764528404614bab2ee425f1a821 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Sat, 20 Feb 2016 22:00:34 +0000 Subject: [PATCH 0987/1705] Updated from global requirements Change-Id: Icccd779513d7f282f507744e2d6f64f459d13e5f --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 559c8a01d..ae6170c3b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ keystoneauth1>=2.1.0 # Apache-2.0 iso8601>=0.1.9 # MIT oslo.i18n>=2.1.0 # Apache-2.0 oslo.serialization>=1.10.0 # Apache-2.0 -oslo.utils>=3.4.0 # Apache-2.0 +oslo.utils>=3.5.0 # Apache-2.0 PrettyTable<0.8,>=0.7 # BSD requests!=2.9.0,>=2.8.1 # Apache-2.0 simplejson>=2.2.0 # MIT From 345469de684fc4acdb8c46d714b625b3d49cc10e Mon Sep 17 00:00:00 2001 From: jichenjc Date: Sun, 21 Feb 2016 06:19:17 +0800 Subject: [PATCH 0988/1705] Deprecate run_test.sh per nova commint 2dc5d9f8632c83c9bc7ee6871c6631edf69a5570 indicated run_tests.sh should be deprecated to avoid confusion to people and encourage usage of tox. Change-Id: I23940b4da55226f6fa4504ed3839cc6620d0f579 --- run_tests.sh | 202 +++++++++++---------------------------------------- 1 file changed, 42 insertions(+), 160 deletions(-) diff --git a/run_tests.sh b/run_tests.sh index a7d66dd12..9cee6751b 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -2,163 +2,45 @@ set -eu -function usage { - echo "Usage: $0 [OPTION]..." - echo "Run python-novaclient test suite" - echo "" - echo " -V, --virtual-env Always use virtualenv. Install automatically if not present" - echo " -N, --no-virtual-env Don't use virtualenv. Run tests in local environment" - echo " -s, --no-site-packages Isolate the virtualenv from the global Python environment" - echo " -x, --stop Stop running tests after the first error or failure." - echo " -f, --force Force a clean re-build of the virtual environment. Useful when dependencies have been added." - echo " -p, --pep8 Just run pep8" - echo " -P, --no-pep8 Don't run pep8" - echo " -c, --coverage Generate coverage report" - echo " -h, --help Print this usage message" - echo " --hide-elapsed Don't print the elapsed time for each test along with slow test list" - echo "" - echo "Note: with no options specified, the script will try to run the tests in a virtual environment," - echo " If no virtualenv is found, the script will ask if you would like to create one. If you " - echo " prefer to run tests NOT in a virtual environment, simply pass the -N option." - exit -} - -function process_option { - case "$1" in - -h|--help) usage;; - -V|--virtual-env) always_venv=1; never_venv=0;; - -N|--no-virtual-env) always_venv=0; never_venv=1;; - -s|--no-site-packages) no_site_packages=1;; - -f|--force) force=1;; - -p|--pep8) just_pep8=1;; - -P|--no-pep8) no_pep8=1;; - -c|--coverage) coverage=1;; - -*) testropts="$testropts $1";; - *) testrargs="$testrargs $1" - esac -} - -venv=.venv -with_venv=tools/with_venv.sh -always_venv=0 -never_venv=0 -force=0 -no_site_packages=0 -installvenvopts= -testrargs= -testropts= -wrapper="" -just_pep8=0 -no_pep8=0 -coverage=0 - -LANG=en_US.UTF-8 -LANGUAGE=en_US:en -LC_ALL=C - -for arg in "$@"; do - process_option $arg -done - -if [ $no_site_packages -eq 1 ]; then - installvenvopts="--no-site-packages" -fi - -function init_testr { - if [ ! -d .testrepository ]; then - ${wrapper} testr init - fi -} - -function run_tests { - # Cleanup *pyc - ${wrapper} find . -type f -name "*.pyc" -delete - - if [ $coverage -eq 1 ]; then - # Do not test test_coverage_ext when gathering coverage. - if [ "x$testrargs" = "x" ]; then - testrargs="^(?!.*test_coverage_ext).*$" - fi - export PYTHON="${wrapper} coverage run --source novaclient --parallel-mode" - fi - # Just run the test suites in current environment - set +e - TESTRTESTS="$TESTRTESTS $testrargs" - echo "Running \`${wrapper} $TESTRTESTS\`" - ${wrapper} $TESTRTESTS - RESULT=$? - set -e - - copy_subunit_log - - return $RESULT -} - -function copy_subunit_log { - LOGNAME=`cat .testrepository/next-stream` - LOGNAME=$(($LOGNAME - 1)) - LOGNAME=".testrepository/${LOGNAME}" - cp $LOGNAME subunit.log -} - -function run_pep8 { - echo "Running flake8 ..." - ${wrapper} flake8 -} - -TESTRTESTS="testr run --parallel $testropts" - -if [ $never_venv -eq 0 ] -then - # Remove the virtual environment if --force used - if [ $force -eq 1 ]; then - echo "Cleaning virtualenv..." - rm -rf ${venv} - fi - if [ -e ${venv} ]; then - wrapper="${with_venv}" - else - if [ $always_venv -eq 1 ]; then - # Automatically install the virtualenv - python tools/install_venv.py $installvenvopts - wrapper="${with_venv}" - else - echo -e "No virtual environment found...create one? (Y/n) \c" - read use_ve - if [ "x$use_ve" = "xY" -o "x$use_ve" = "x" -o "x$use_ve" = "xy" ]; then - # Install the virtualenv and run the test suite in it - python tools/install_venv.py $installvenvopts - wrapper=${with_venv} - fi - fi - fi -fi - -# Delete old coverage data from previous runs -if [ $coverage -eq 1 ]; then - ${wrapper} coverage erase -fi - -if [ $just_pep8 -eq 1 ]; then - run_pep8 - exit -fi - -init_testr -run_tests - -# NOTE(sirp): we only want to run pep8 when we're running the full-test suite, -# not when we're running tests individually. To handle this, we need to -# distinguish between options (noseopts), which begin with a '-', and -# arguments (testrargs). -if [ -z "$testrargs" ]; then - if [ $no_pep8 -eq 0 ]; then - run_pep8 - fi -fi - -if [ $coverage -eq 1 ]; then - echo "Generating coverage report in covhtml/" - ${wrapper} coverage combine - ${wrapper} coverage html --include='novaclient/*' --omit='novaclient/openstack/common/*' -d covhtml -i -fi +cat < Date: Thu, 18 Feb 2016 14:09:12 +0900 Subject: [PATCH 0989/1705] Fix string interpolation at logging call Skip creating the formatted log message if the message is not going to be emitted because of the log level. TrivialFix Change-Id: Ic35b67bc43c77a086409bed57b846f729300d5c0 --- novaclient/api_versions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/api_versions.py b/novaclient/api_versions.py index 803f61558..5f7198c0d 100644 --- a/novaclient/api_versions.py +++ b/novaclient/api_versions.py @@ -232,7 +232,7 @@ def get_api_version(version_string): if version_string in DEPRECATED_VERSIONS: LOG.warning( _LW("Version %(deprecated_version)s is deprecated, using " - "alternative version %(alternative)s instead.") % + "alternative version %(alternative)s instead."), {"deprecated_version": version_string, "alternative": DEPRECATED_VERSIONS[version_string]}) version_string = DEPRECATED_VERSIONS[version_string] From f5a25fe997b19ff71a335ece755c8904dc8136c8 Mon Sep 17 00:00:00 2001 From: Anna Babich Date: Mon, 15 Feb 2016 12:33:22 +0200 Subject: [PATCH 0990/1705] Functional tests for trigger-crash-dump (microversion 2.17) It's a resource-consuming task to implement full-flow (up to getting and reading a dump file) functional test for trigger-crash-dump. We need to upload Ubuntu image for booting an instance based on it, and to install kdump with its further configuring on this instance. Here, the "light" version of functional test is proposed. It's based on knowledge that trigger-crash-dump uses a NMI injection, and when the 'trigger-crash-dump' operation is executed, instance's kernel receives the MNI signal, and an appropriate message will appear in the instance's log. Wait_for_server_os_boot() method has been added to ClientTestBase to check if instance's operating system is completely booted. The _create_server() method has been removed since the change which puts this method to the base test class has been merged. Change-Id: I2313c5d37a7cf87a8d75e37c93aab136cf028ec1 --- novaclient/tests/functional/base.py | 20 +++ .../functional/v2/test_trigger_crash_dump.py | 146 ++++++++++++++++++ 2 files changed, 166 insertions(+) create mode 100644 novaclient/tests/functional/v2/test_trigger_crash_dump.py diff --git a/novaclient/tests/functional/base.py b/novaclient/tests/functional/base.py index 16e5f3781..45d6c27dd 100644 --- a/novaclient/tests/functional/base.py +++ b/novaclient/tests/functional/base.py @@ -24,6 +24,9 @@ import novaclient.api_versions import novaclient.client +BOOT_IS_COMPLETE = ("login as 'cirros' user. default password: " + "'cubswin:)'. use 'sudo' for root.") + # The following are simple filter functions that filter our available # image / flavor list so that they can be used in standard testing. @@ -202,6 +205,23 @@ def wait_for_volume_status(self, volume, status, timeout=60, self.fail("Volume %s did not reach status %s after %d s" % (volume.id, status, timeout)) + def wait_for_server_os_boot(self, server_id, timeout=60, + poll_interval=1): + """Wait until instance's operating system is completely booted. + + :param server_id: uuid4 id of given instance + :param timeout: timeout in seconds + :param poll_interval: poll interval in seconds + """ + start_time = time.time() + while time.time() - start_time < timeout: + if BOOT_IS_COMPLETE in self.nova('console-log %s ' % server_id): + break + time.sleep(poll_interval) + else: + self.fail("Server %s did not boot after %d s" + % (server_id, timeout)) + def wait_for_resource_delete(self, resource, manager, timeout=60, poll_interval=1): """Wait until getting the resource raises NotFound exception. diff --git a/novaclient/tests/functional/v2/test_trigger_crash_dump.py b/novaclient/tests/functional/v2/test_trigger_crash_dump.py new file mode 100644 index 000000000..a41d7315d --- /dev/null +++ b/novaclient/tests/functional/v2/test_trigger_crash_dump.py @@ -0,0 +1,146 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import time + +from novaclient.tests.functional import base +from novaclient.v2 import shell + + +class TestTriggerCrashDumpNovaClientV217(base.TenantTestBase): + """Functional tests for trigger crash dump""" + + COMPUTE_API_VERSION = "2.17" + + # It's a resource-consuming task to implement full-flow (up to getting + # and reading a dump file) functional test for trigger-crash-dump. + # We need to upload Ubuntu image for booting an instance based on it, + # and to install kdump with its further configuring on this instance. + # Here, the "light" version of functional test is proposed. + # It's based on knowledge that trigger-crash-dump uses a NMI injection, + # and when the 'trigger-crash-dump' operation is executed, + # instance's kernel receives the NMI signal, and an appropriate + # message will appear in the instance's log. + + # The server status must be ACTIVE, PAUSED, RESCUED, RESIZED or ERROR. + # If not, the conflictingRequest(409) code is returned + + def _wait_for_nmi(self, server_id, timeout=60, + poll_interval=1): + start_time = time.time() + while time.time() - start_time < timeout: + if 'trigger_crash_dump' in self.nova('instance-action-list %s ' % + server_id): + break + time.sleep(poll_interval) + else: + self.fail("Trigger crash dump hasn't been executed for server %s" + % (server_id, timeout)) + + def test_trigger_crash_dump_in_active_state(self): + server = self._create_server() + self.wait_for_server_os_boot(server.id) + self.nova('trigger-crash-dump %s ' % server.id) + self._wait_for_nmi(server.id) + output = self.nova('console-log %s ' % server.id) + self.assertIn("Uhhuh. NMI received for unknown reason ", output) + + def test_trigger_crash_dump_in_error_state(self): + server = self._create_server() + self.wait_for_server_os_boot(server.id) + self.nova('reset-state %s ' % server.id) + shell._poll_for_status( + self.client.servers.get, server.id, + 'active', ['error']) + self.nova('trigger-crash-dump %s ' % server.id) + self._wait_for_nmi(server.id) + output = self.nova('console-log %s ' % server.id) + self.assertIn("Uhhuh. NMI received for unknown reason ", output) + + def test_trigger_crash_dump_in_paused_state(self): + server = self._create_server() + self.wait_for_server_os_boot(server.id) + self.nova('pause %s ' % server.id) + shell._poll_for_status( + self.client.servers.get, server.id, + 'active', ['paused']) + self.nova('trigger-crash-dump %s ' % server.id) + self._wait_for_nmi(server.id) + output = self.nova('console-log %s ' % server.id) + # In PAUSED state a server's kernel shouldn't react onto NMI + self.assertNotIn("Uhhuh. NMI received for unknown reason ", output) + + def test_trigger_crash_dump_in_rescued_state(self): + server = self._create_server() + self.wait_for_server_os_boot(server.id) + self.nova('rescue %s ' % server.id) + shell._poll_for_status( + self.client.servers.get, server.id, + 'active', ['rescue']) + self.wait_for_server_os_boot(server.id) + self.nova('trigger-crash-dump %s ' % server.id) + self._wait_for_nmi(server.id) + output = self.nova('console-log %s ' % server.id) + self.assertIn("Uhhuh. NMI received for unknown reason ", output) + + def test_trigger_crash_dump_in_resized_state(self): + server = self._create_server() + self.wait_for_server_os_boot(server.id) + self.nova('resize %s %s' % (server.id, 'm1.small')) + shell._poll_for_status( + self.client.servers.get, server.id, + 'active', ['verify_resize']) + self.nova('trigger-crash-dump %s ' % server.id) + self._wait_for_nmi(server.id) + output = self.nova('console-log %s ' % server.id) + self.assertIn("Uhhuh. NMI received for unknown reason ", output) + + def test_trigger_crash_dump_in_shutoff_state(self): + server = self._create_server() + self.wait_for_server_os_boot(server.id) + self.nova('stop %s ' % server.id) + shell._poll_for_status( + self.client.servers.get, server.id, + 'active', ['shutoff']) + output = self.nova('trigger-crash-dump %s ' % + server.id, fail_ok=True, merge_stderr=True) + self.assertIn("ERROR (Conflict): " + "Cannot 'trigger_crash_dump' instance %s " + "while it is in vm_state stopped (HTTP 409) " % + server.id, output) + + # If the specified server is locked, the conflictingRequest(409) code + # is returned to a user without administrator privileges. + def test_trigger_crash_dump_in_locked_state_admin(self): + server = self._create_server() + self.wait_for_server_os_boot(server.id) + self.nova('lock %s ' % server.id) + self.nova('trigger-crash-dump %s ' % server.id) + self._wait_for_nmi(server.id) + output = self.nova('console-log %s ' % server.id) + self.assertIn("Uhhuh. NMI received for unknown reason ", output) + + def test_trigger_crash_dump_in_locked_state_nonadmin(self): + name = self.name_generate(prefix='server') + server = self.another_nova('boot --flavor %s --image %s --poll %s' % + (self.flavor.name, self.image.name, name)) + self.addCleanup(self.another_nova, 'delete', params=name) + server_id = self._get_value_from_the_table( + server, 'id') + self.wait_for_server_os_boot(server_id) + self.another_nova('lock %s ' % server_id) + self.addCleanup(self.another_nova, 'unlock', params=name) + output = self.another_nova('trigger-crash-dump %s ' % + server_id, fail_ok=True, merge_stderr=True) + self.assertIn("ERROR (Conflict): Instance %s is in an invalid " + "state for 'trigger_crash_dump' (HTTP 409) " % + server_id, output) From f55e426dd4bd2373a00b0299e1aef8394bf9361c Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Tue, 23 Feb 2016 17:47:59 +0900 Subject: [PATCH 0991/1705] Use assertIsNone instead of assertEqual(None, ***) Instead of using assertEqual(None, ***), developers should use assertIsNone(***). Change-Id: I55e9161a4df9197546603385228ea85528de43a7 Closes-Bug: #1510006 --- novaclient/tests/unit/test_shell.py | 30 ++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/novaclient/tests/unit/test_shell.py b/novaclient/tests/unit/test_shell.py index 38f569546..e7485624d 100644 --- a/novaclient/tests/unit/test_shell.py +++ b/novaclient/tests/unit/test_shell.py @@ -84,11 +84,11 @@ def test_init_emptyhelp_nouse(self, mock_init): 'option_strings', 'dest', a=1, b=2, c=3) self.assertEqual(result.emitted, set()) - self.assertEqual(result.use, None) + self.assertIsNone(result.use) self.assertEqual(result.real_action_args, ('option_strings', 'dest', 'Deprecated', {'a': 1, 'b': 2, 'c': 3})) - self.assertEqual(result.real_action, None) + self.assertIsNone(result.real_action) mock_init.assert_called_once_with( 'option_strings', 'dest', help='Deprecated', a=1, b=2, c=3) @@ -104,7 +104,7 @@ def test_init_emptyhelp_withuse(self, mock_init): ('option_strings', 'dest', 'Deprecated; use this instead', {'a': 1, 'b': 2, 'c': 3})) - self.assertEqual(result.real_action, None) + self.assertIsNone(result.real_action) mock_init.assert_called_once_with( 'option_strings', 'dest', help='Deprecated; use this instead', a=1, b=2, c=3) @@ -115,12 +115,12 @@ def test_init_withhelp_nouse(self, mock_init): 'option_strings', 'dest', help='some help', a=1, b=2, c=3) self.assertEqual(result.emitted, set()) - self.assertEqual(result.use, None) + self.assertIsNone(result.use) self.assertEqual(result.real_action_args, ('option_strings', 'dest', 'some help (Deprecated)', {'a': 1, 'b': 2, 'c': 3})) - self.assertEqual(result.real_action, None) + self.assertIsNone(result.real_action) mock_init.assert_called_once_with( 'option_strings', 'dest', help='some help (Deprecated)', a=1, b=2, c=3) @@ -138,7 +138,7 @@ def test_init_withhelp_withuse(self, mock_init): ('option_strings', 'dest', 'some help (Deprecated; use this instead)', {'a': 1, 'b': 2, 'c': 3})) - self.assertEqual(result.real_action, None) + self.assertIsNone(result.real_action) mock_init.assert_called_once_with( 'option_strings', 'dest', help='some help (Deprecated; use this instead)', @@ -150,11 +150,11 @@ def test_init_suppresshelp_nouse(self, mock_init): 'option_strings', 'dest', help=argparse.SUPPRESS, a=1, b=2, c=3) self.assertEqual(result.emitted, set()) - self.assertEqual(result.use, None) + self.assertIsNone(result.use) self.assertEqual(result.real_action_args, ('option_strings', 'dest', argparse.SUPPRESS, {'a': 1, 'b': 2, 'c': 3})) - self.assertEqual(result.real_action, None) + self.assertIsNone(result.real_action) mock_init.assert_called_once_with( 'option_strings', 'dest', help=argparse.SUPPRESS, a=1, b=2, c=3) @@ -170,7 +170,7 @@ def test_init_suppresshelp_withuse(self, mock_init): self.assertEqual(result.real_action_args, ('option_strings', 'dest', argparse.SUPPRESS, {'a': 1, 'b': 2, 'c': 3})) - self.assertEqual(result.real_action, None) + self.assertIsNone(result.real_action) mock_init.assert_called_once_with( 'option_strings', 'dest', help=argparse.SUPPRESS, a=1, b=2, c=3) @@ -180,9 +180,9 @@ def test_init_action_nothing(self, mock_init): 'option_strings', 'dest', real_action='nothing', a=1, b=2, c=3) self.assertEqual(result.emitted, set()) - self.assertEqual(result.use, None) + self.assertIsNone(result.use) self.assertEqual(result.real_action_args, False) - self.assertEqual(result.real_action, None) + self.assertIsNone(result.real_action) mock_init.assert_called_once_with( 'option_strings', 'dest', help='Deprecated', a=1, b=2, c=3) @@ -192,7 +192,7 @@ def test_init_action_string(self, mock_init): 'option_strings', 'dest', real_action='store', a=1, b=2, c=3) self.assertEqual(result.emitted, set()) - self.assertEqual(result.use, None) + self.assertIsNone(result.use) self.assertEqual(result.real_action_args, ('option_strings', 'dest', 'Deprecated', {'a': 1, 'b': 2, 'c': 3})) @@ -207,7 +207,7 @@ def test_init_action_other(self, mock_init): 'option_strings', 'dest', real_action=action, a=1, b=2, c=3) self.assertEqual(result.emitted, set()) - self.assertEqual(result.use, None) + self.assertIsNone(result.use) self.assertEqual(result.real_action_args, False) self.assertEqual(result.real_action, action.return_value) mock_init.assert_called_once_with( @@ -243,8 +243,8 @@ def test_get_action_lookup_noresult(self): result = obj._get_action(parser) - self.assertEqual(result, None) - self.assertEqual(obj.real_action, None) + self.assertIsNone(result) + self.assertIsNone(obj.real_action) parser._registry_get.assert_called_once_with( 'action', 'store') self.assertEqual(sys.stderr.getvalue(), From 68c16f82d59fc7b17070adf22b756746b503159a Mon Sep 17 00:00:00 2001 From: jichenjc Date: Wed, 24 Feb 2016 14:08:40 +0800 Subject: [PATCH 0992/1705] Prepare to move extension into core plugin As nova stable_api.rst indicated: As the extension will be removed from Nove V2.1 REST API. So the concept of core API and extension API is eliminated also. There is no difference between Nova V2.1 REST API, all of them are part of Nova stable REST API. so we can move all extensions from contrib to core, this patch is prelude of https://review.openstack.org/#/c/285213/ and its following patchs. Change-Id: Ibd5df4bcc70a4b4854fd519330d89c751f7409dc --- novaclient/client.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/novaclient/client.py b/novaclient/client.py index fe2d12e2c..5fa7060dd 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -54,6 +54,12 @@ from novaclient import utils +# TODO(jichenjc): when an extension in contrib is moved to core extension, +# Add the name into the following list, then after last patch merged, +# remove the whole function +extensions_ignored_name = ["__init__"] + + class _ClientConnectionPool(object): def __init__(self): @@ -768,7 +774,7 @@ def _discover_via_contrib_path(version): for ext_path in glob.iglob(ext_glob): name = os.path.basename(ext_path)[:-3] - if name == "__init__": + if name in extensions_ignored_name: continue module = imp.load_source(name, ext_path) From ca5b06f6ae9426eb20ee9e58022c49c219d4a067 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Wed, 24 Feb 2016 17:11:41 +0200 Subject: [PATCH 0993/1705] [microversions] fix help msg for versioned args If command has arguments with one name, but for different versions(see an example below), both arguments will be displayed. This patch fixes this issue. Example of arguments: @cliutils.args("name", help="Name of a good action.", start_version="2.1", end_version="2.20") @cliutils.args("name", help="Name of a very good action.", start_version="2.21") def do_something_good(cs, args): pass Example of helpoutput before patch: Positional arguments: Name of a good action. (Supported by API versions '2.0' - '2.20') Name of a very good action. (Supported by API versions '2.21' - '2.latest') Change-Id: I59f155675e2aae642a5b90cb70008eb5647ffe79 --- novaclient/shell.py | 5 ++-- novaclient/tests/unit/fake_actions_module.py | 13 ++++++++++ novaclient/tests/unit/test_shell.py | 25 ++++++++++++++++++-- 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/novaclient/shell.py b/novaclient/shell.py index 18d83aa06..1f182cc53 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -694,9 +694,8 @@ def _find_actions(self, subparsers, actions_module, version, do_help): kwargs["help"] = kwargs.get("help", "") + (msg % { "start": start_version.get_string(), "end": end_version.get_string()}) - else: - if not version.matches(start_version, end_version): - continue + if not version.matches(start_version, end_version): + continue kw = kwargs.copy() kw.pop("start_version", None) kw.pop("end_version", None) diff --git a/novaclient/tests/unit/fake_actions_module.py b/novaclient/tests/unit/fake_actions_module.py index f927b98af..8d4b0175b 100644 --- a/novaclient/tests/unit/fake_actions_module.py +++ b/novaclient/tests/unit/fake_actions_module.py @@ -42,3 +42,16 @@ def do_another_fake_action(): end_version='2.4') def do_fake_action2(): return 3 + + +@cliutils.arg( + '--foo', + help='first foo', + start_version='2.10', + end_version='2.20') +@cliutils.arg( + '--foo', + help='second foo', + start_version='2.21') +def do_fake_action3(): + return 3 diff --git a/novaclient/tests/unit/test_shell.py b/novaclient/tests/unit/test_shell.py index 38f569546..2600f3b79 100644 --- a/novaclient/tests/unit/test_shell.py +++ b/novaclient/tests/unit/test_shell.py @@ -912,11 +912,32 @@ def test_load_versioned_actions_with_args_and_help(self, mock_add_arg): api_versions.APIVersion("2.4"), True) mock_add_arg.assert_has_calls([ mock.call('-h', '--help', action='help', help='==SUPPRESS=='), - mock.call('--foo', - help=" (Supported by API versions '2.1' - '2.2')"), mock.call('--bar', help=" (Supported by API versions '2.3' - '2.4')")]) + @mock.patch.object(novaclient.shell.NovaClientArgumentParser, + 'add_argument') + def test_load_actions_with_versioned_args(self, mock_add_arg): + parser = novaclient.shell.NovaClientArgumentParser(add_help=False) + subparsers = parser.add_subparsers(metavar='') + shell = novaclient.shell.OpenStackComputeShell() + shell.subcommands = {} + shell._find_actions(subparsers, fake_actions_module, + api_versions.APIVersion("2.20"), False) + self.assertIn(mock.call('--foo', help="first foo"), + mock_add_arg.call_args_list) + self.assertNotIn(mock.call('--foo', help="second foo"), + mock_add_arg.call_args_list) + + mock_add_arg.reset_mock() + + shell._find_actions(subparsers, fake_actions_module, + api_versions.APIVersion("2.21"), False) + self.assertNotIn(mock.call('--foo', help="first foo"), + mock_add_arg.call_args_list) + self.assertIn(mock.call('--foo', help="second foo"), + mock_add_arg.call_args_list) + class ShellTestKeystoneV3(ShellTest): def make_env(self, exclude=None, fake_env=FAKE_ENV): From cd88097ff59ad40002ac4f624b53efe72bc84386 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Wed, 10 Feb 2016 17:37:25 +0200 Subject: [PATCH 0994/1705] [microversions] Enable 2.21 2.21 - The os-instance-actions API now returns information from deleted instances. Change-Id: Iff514e4fa9135207c6f8e32e444d45b1b61d8c7c --- novaclient/__init__.py | 2 +- novaclient/tests/functional/base.py | 6 +- .../functional/v2/test_instance_action.py | 60 +++++++++++++++++++ novaclient/tests/unit/test_utils.py | 9 +++ novaclient/utils.py | 10 +++- novaclient/v2/contrib/instance_action.py | 43 +++++++++++-- 6 files changed, 120 insertions(+), 10 deletions(-) create mode 100644 novaclient/tests/functional/v2/test_instance_action.py diff --git a/novaclient/__init__.py b/novaclient/__init__.py index b9f008cca..f5588d094 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.20") +API_MAX_VERSION = api_versions.APIVersion("2.21") diff --git a/novaclient/tests/functional/base.py b/novaclient/tests/functional/base.py index 42cef9e15..14a1e93a7 100644 --- a/novaclient/tests/functional/base.py +++ b/novaclient/tests/functional/base.py @@ -308,7 +308,8 @@ def _get_column_value_from_single_row_table(self, table, column): raise ValueError("Unable to find value for column '%s'.") - def _create_server(self, name=None, with_network=True, **kwargs): + def _create_server(self, name=None, with_network=True, add_cleanup=True, + **kwargs): name = name or self.name_generate(prefix='server') if with_network: nics = [{"net-id": self.network.id}] @@ -316,7 +317,8 @@ def _create_server(self, name=None, with_network=True, **kwargs): nics = None server = self.client.servers.create(name, self.image, self.flavor, nics=nics, **kwargs) - self.addCleanup(server.delete) + if add_cleanup: + self.addCleanup(server.delete) novaclient.v2.shell._poll_for_status( self.client.servers.get, server.id, 'building', ['active']) diff --git a/novaclient/tests/functional/v2/test_instance_action.py b/novaclient/tests/functional/v2/test_instance_action.py new file mode 100644 index 000000000..a4557277e --- /dev/null +++ b/novaclient/tests/functional/v2/test_instance_action.py @@ -0,0 +1,60 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import uuid + +import six +from tempest_lib import exceptions + +from novaclient.tests.functional import base + + +class TestInstanceActionCLI(base.ClientTestBase): + + COMPUTE_API_VERSION = "2.21" + + def _test_cmd_with_not_existing_instance(self, cmd, args): + try: + self.nova("%s %s" % (cmd, args)) + except exceptions.CommandFailed as e: + self.assertIn("ERROR (NotFound):", six.text_type(e)) + else: + self.fail("%s is not failed on non existing instance." % cmd) + + def test_show_action_with_not_existing_instance(self): + name_or_uuid = str(uuid.uuid4()) + request_id = str(uuid.uuid4()) + self._test_cmd_with_not_existing_instance( + "instance-action", "%s %s" % (name_or_uuid, request_id)) + + def test_list_actions_with_not_existing_instance(self): + name_or_uuid = str(uuid.uuid4()) + self._test_cmd_with_not_existing_instance("instance-action-list", + name_or_uuid) + + def test_show_and_list_actions_on_deleted_instance(self): + server = self._create_server(add_cleanup=False) + server.delete() + self.wait_for_resource_delete(server, self.client.servers) + + output = self.nova("instance-action-list %s" % server.id) + # NOTE(andreykurilin): output is not a single row table, so we can + # obtain just "create" action. It should be enough for testing + # "nova instance-action " command + request_id = self._get_column_value_from_single_row_table( + output, "Request_ID") + + output = self.nova("instance-action %s %s" % (server.id, request_id)) + + # ensure that obtained action is "create". + self.assertEqual("create", + self._get_value_from_the_table(output, "action")) diff --git a/novaclient/tests/unit/test_utils.py b/novaclient/tests/unit/test_utils.py index cfe4fd767..f3d88c21e 100644 --- a/novaclient/tests/unit/test_utils.py +++ b/novaclient/tests/unit/test_utils.py @@ -158,6 +158,15 @@ def test_find_in_alphanum_allowed_manager_by_str_id_(self): output = utils.find_resource(alphanum_manager, '01234') self.assertEqual(output, alphanum_manager.get('01234')) + def test_find_without_wrapping_exception(self): + alphanum_manager = FakeManager(True) + self.assertRaises(exceptions.NotFound, utils.find_resource, + alphanum_manager, 'not_exist', wrap_exception=False) + res = alphanum_manager.resources[0] + alphanum_manager.resources.append(res) + self.assertRaises(exceptions.NoUniqueMatch, utils.find_resource, + alphanum_manager, res.name, wrap_exception=False) + class _FakeResult(object): def __init__(self, name, value): diff --git a/novaclient/utils.py b/novaclient/utils.py index ef183030c..1e34e0783 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -273,7 +273,7 @@ def print_dict(d, dict_property="Property", dict_value="Value", wrap=0): print(result) -def find_resource(manager, name_or_id, **find_args): +def find_resource(manager, name_or_id, wrap_exception=True, **find_args): """Helper for the _find_* methods.""" # for str id which is not uuid (for Flavor, Keypair and hypervsior in cells # environments search currently) @@ -316,7 +316,9 @@ def find_resource(manager, name_or_id, **find_args): "to be more specific.") % {'class': manager.resource_class.__name__.lower(), 'name': name_or_id}) - raise exceptions.CommandError(msg) + if wrap_exception: + raise exceptions.CommandError(msg) + raise exceptions.NoUniqueMatch(msg) # finally try to get entity as integer id try: @@ -325,7 +327,9 @@ def find_resource(manager, name_or_id, **find_args): msg = (_("No %(class)s with a name or ID of '%(name)s' exists.") % {'class': manager.resource_class.__name__.lower(), 'name': name_or_id}) - raise exceptions.CommandError(msg) + if wrap_exception: + raise exceptions.CommandError(msg) + raise exceptions.NotFound(404, msg) def _format_servers_list_networks(server): diff --git a/novaclient/v2/contrib/instance_action.py b/novaclient/v2/contrib/instance_action.py index 2a7c6b8df..99f11a750 100644 --- a/novaclient/v2/contrib/instance_action.py +++ b/novaclient/v2/contrib/instance_action.py @@ -15,7 +15,11 @@ import pprint +import six + +from novaclient import api_versions from novaclient import base +from novaclient import exceptions from novaclient.i18n import _ from novaclient.openstack.common import cliutils from novaclient import utils @@ -41,17 +45,41 @@ def list(self, server): base.getid(server), 'instanceActions') +@api_versions.wraps("2.0", "2.20") +def _find_server(cs, args): + return utils.find_resource(cs.servers, args.server) + + +@api_versions.wraps("2.21") +def _find_server(cs, args): + try: + return utils.find_resource(cs.servers, args.server, + wrap_exception=False) + except exceptions.NoUniqueMatch as e: + raise exceptions.CommandError(six.text_type(e)) + except exceptions.NotFound: + # The server can be deleted + return args.server + + @cliutils.arg( 'server', metavar='', - help=_('Name or UUID of the server to show an action for.')) + help=_('Name or UUID of the server to show actions for.'), + start_version="2.0", end_version="2.20") +@cliutils.arg( + 'server', + metavar='', + help=_('Name or UUID of the server to show actions for. Only UUID can be ' + 'used to show actions for a deleted server.'), + start_version="2.21") @cliutils.arg( 'request_id', metavar='', help=_('Request ID of the action to get.')) def do_instance_action(cs, args): """Show an action.""" - server = utils.find_resource(cs.servers, args.server) + server = _find_server(cs, args) action_resource = cs.instance_action.get(server, args.request_id) action = action_resource._info if 'events' in action: @@ -62,10 +90,17 @@ def do_instance_action(cs, args): @cliutils.arg( 'server', metavar='', - help=_('Name or UUID of the server to list actions for.')) + help=_('Name or UUID of the server to list actions for.'), + start_version="2.0", end_version="2.20") +@cliutils.arg( + 'server', + metavar='', + help=_('Name or UUID of the server to list actions for. Only UUID can be ' + 'used to list actions on a deleted server.'), + start_version="2.21") def do_instance_action_list(cs, args): """List actions on a server.""" - server = utils.find_resource(cs.servers, args.server) + server = _find_server(cs, args) actions = cs.instance_action.list(server) utils.print_list(actions, ['Action', 'Request_ID', 'Message', 'Start_Time'], From e43c66a0aaf1609b0be5f931759aed0537b32b19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20G=C3=B3rski?= Date: Wed, 24 Feb 2016 23:09:43 +0000 Subject: [PATCH 0995/1705] Adds missing internationalization for help message MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change-Id: I676daed583cac49f72593c79e9f79f7c9908a57c Related-Bug: #1491492 Signed-off-by: Bartosz Górski --- novaclient/shell.py | 14 +++++++------- novaclient/v2/shell.py | 30 +++++++++++++++--------------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/novaclient/shell.py b/novaclient/shell.py index 1f182cc53..b4da9e2c6 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -592,8 +592,8 @@ def get_base_parser(self, argv): metavar='', dest='bypass_url', default=utils.env('NOVACLIENT_BYPASS_URL'), - help="Use this API endpoint instead of the Service Catalog. " - "Defaults to env[NOVACLIENT_BYPASS_URL].") + help=_("Use this API endpoint instead of the Service Catalog. " + "Defaults to env[NOVACLIENT_BYPASS_URL].")) parser.add_argument( '--bypass_url', action=DeprecatedAction, @@ -1046,7 +1046,7 @@ def do_bash_completion(self, _args): 'command', metavar='', nargs='?', - help='Display help for .') + help=_('Display help for .')) def do_help(self, args): """ Display help about this program or one of its subcommands. @@ -1080,13 +1080,13 @@ def main(): OpenStackComputeShell().main(argv) except Exception as exc: logger.debug(exc, exc_info=1) - print("ERROR (%s): %s" - % (exc.__class__.__name__, - encodeutils.exception_to_unicode(exc)), + print(_("ERROR (%(type)s): %(msg)s") % { + 'type': exc.__class__.__name__, + 'msg': encodeutils.exception_to_unicode(exc)}, file=sys.stderr) sys.exit(1) except KeyboardInterrupt: - print("... terminating nova client", file=sys.stderr) + print(_("... terminating nova client"), file=sys.stderr) sys.exit(130) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 95b892d89..3f97d6185 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -748,18 +748,18 @@ def _print_flavor_list(flavors, show_extra_specs=False): dest='marker', metavar='', default=None, - help=('The last flavor ID of the previous page; displays list of flavors' - ' after "marker".')) + help=_('The last flavor ID of the previous page; displays list of flavors' + ' after "marker".')) @cliutils.arg( '--limit', dest='limit', metavar='', type=int, default=None, - help=("Maximum number of flavors to display. If limit == -1, all flavors " - "will be displayed. If limit is bigger than " - "'osapi_max_limit' option of Nova API, limit 'osapi_max_limit' will " - "be used instead.")) + help=_("Maximum number of flavors to display. If limit == -1, all flavors " + "will be displayed. If limit is bigger than 'osapi_max_limit' " + "option of Nova API, limit 'osapi_max_limit' will be used " + "instead.")) def do_flavor_list(cs, args): """Print a list of available 'flavors' (sizes of servers).""" if args.all: @@ -1439,26 +1439,26 @@ def do_image_delete(cs, args): '--sort', dest='sort', metavar='[:]', - help=('Comma-separated list of sort keys and directions in the form' - ' of [:]. The direction defaults to descending if' - ' not specified.')) + help=_('Comma-separated list of sort keys and directions in the form ' + 'of [:]. The direction defaults to descending if ' + 'not specified.')) @cliutils.arg( '--marker', dest='marker', metavar='', default=None, - help=('The last server UUID of the previous page; displays list of servers' - ' after "marker".')) + help=_('The last server UUID of the previous page; displays list of ' + 'servers after "marker".')) @cliutils.arg( '--limit', dest='limit', metavar='', type=int, default=None, - help=("Maximum number of servers to display. If limit == -1, all servers " - "will be displayed. If limit is bigger than " - "'osapi_max_limit' option of Nova API, limit 'osapi_max_limit' will " - "be used instead.")) + help=_("Maximum number of servers to display. If limit == -1, all servers " + "will be displayed. If limit is bigger than 'osapi_max_limit' " + "option of Nova API, limit 'osapi_max_limit' will be used " + "instead.")) def do_list(cs, args): """List active servers.""" imageid = None From 62c76301a281b41282531f6419d55823106270d1 Mon Sep 17 00:00:00 2001 From: Pawel Koniszewski Date: Mon, 15 Feb 2016 19:34:44 +0100 Subject: [PATCH 0996/1705] Support for forcing live migration to complete In API microversion 2.22 in Nova there is new ServerMigrations resource that allows opertators to force on-going live migration to complete: https://review.openstack.org/#/c/245921/ This patch implements new method in python-novaclient to take advantage of the new API: nova live-migration-force-complete Change-Id: I823c20b4e0c7b63e905f564a7dff13d3fb314a26 Implements blueprint pause-vm-during-live-migration --- novaclient/__init__.py | 2 +- .../unit/fixture_data/server_migrations.py | 27 ++++++++++++ novaclient/tests/unit/v2/fakes.py | 3 ++ .../tests/unit/v2/test_server_migrations.py | 33 +++++++++++++++ novaclient/tests/unit/v2/test_shell.py | 6 +++ novaclient/v2/client.py | 3 ++ novaclient/v2/server_migrations.py | 42 +++++++++++++++++++ novaclient/v2/shell.py | 9 ++++ 8 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 novaclient/tests/unit/fixture_data/server_migrations.py create mode 100644 novaclient/tests/unit/v2/test_server_migrations.py create mode 100644 novaclient/v2/server_migrations.py diff --git a/novaclient/__init__.py b/novaclient/__init__.py index f5588d094..509d2a20d 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.21") +API_MAX_VERSION = api_versions.APIVersion("2.22") diff --git a/novaclient/tests/unit/fixture_data/server_migrations.py b/novaclient/tests/unit/fixture_data/server_migrations.py new file mode 100644 index 000000000..3aed49a54 --- /dev/null +++ b/novaclient/tests/unit/fixture_data/server_migrations.py @@ -0,0 +1,27 @@ +# Copyright 2016 OpenStack Foundation +# All Rights Reserved. +# +# 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 novaclient.tests.unit.fixture_data import base + + +class Fixture(base.Fixture): + base_url = 'servers' + + def setUp(self): + super(Fixture, self).setUp() + url = self.url('1234', 'migrations', '1', 'action') + self.requests.register_uri('POST', url, + status_code=202, + headers=self.json_headers) diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index e0efff064..d3d1e4051 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -2432,6 +2432,9 @@ def delete_os_server_groups_2cbd51f4_fafe_4cdb_801b_cf913a6f288b( self, **kw): return (202, {}, None) + def post_servers_1234_migrations_1_action(self, body): + return (202, {}, None) + class FakeSessionClient(fakes.FakeClient, client.Client): diff --git a/novaclient/tests/unit/v2/test_server_migrations.py b/novaclient/tests/unit/v2/test_server_migrations.py new file mode 100644 index 000000000..12c5a269e --- /dev/null +++ b/novaclient/tests/unit/v2/test_server_migrations.py @@ -0,0 +1,33 @@ +# Copyright 2016 OpenStack Foundation +# All Rights Reserved. +# +# 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 novaclient import api_versions +from novaclient.tests.unit.fixture_data import client +from novaclient.tests.unit.fixture_data import server_migrations as data +from novaclient.tests.unit import utils + + +class ServerMigrationsTest(utils.FixturedTestCase): + client_fixture_class = client.V1 + data_fixture_class = data.Fixture + + def setUp(self): + super(ServerMigrationsTest, self).setUp() + self.cs.api_version = api_versions.APIVersion("2.22") + + def test_live_migration_force_complete(self): + body = {'force_complete': None} + self.cs.server_migrations.live_migrate_force_complete(1234, 1) + self.assert_called('POST', '/servers/1234/migrations/1/action', body) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index eb86eee90..cb30ce40a 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -1681,6 +1681,12 @@ def test_live_migration(self): 'block_migration': True, 'disk_over_commit': True}}) + def test_live_migration_force_complete(self): + self.run_command('live-migration-force-complete sample-server 1', + api_version='2.22') + self.assert_called('POST', '/servers/1234/migrations/1/action', + {'force_complete': None}) + def test_host_evacuate_live_with_no_target_host(self): self.run_command('host-evacuate-live hyper') self.assert_called('GET', '/os-hypervisors/hyper/servers', pos=0) diff --git a/novaclient/v2/client.py b/novaclient/v2/client.py index a3ec98fe4..56339a346 100644 --- a/novaclient/v2/client.py +++ b/novaclient/v2/client.py @@ -41,6 +41,7 @@ from novaclient.v2 import security_group_rules from novaclient.v2 import security_groups from novaclient.v2 import server_groups +from novaclient.v2 import server_migrations from novaclient.v2 import servers from novaclient.v2 import services from novaclient.v2 import usage @@ -167,6 +168,8 @@ def __init__(self, username=None, api_key=None, project_id=None, self.availability_zones = \ availability_zones.AvailabilityZoneManager(self) self.server_groups = server_groups.ServerGroupsManager(self) + self.server_migrations = \ + server_migrations.ServerMigrationsManager(self) # Add in any extensions... if extensions: diff --git a/novaclient/v2/server_migrations.py b/novaclient/v2/server_migrations.py new file mode 100644 index 000000000..476c0a80e --- /dev/null +++ b/novaclient/v2/server_migrations.py @@ -0,0 +1,42 @@ +# Copyright 2016 OpenStack Foundation +# All Rights Reserved. +# +# 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 novaclient import api_versions +from novaclient import base + + +class ServerMigration(base.Resource): + def __repr__(self): + return "" + + +class ServerMigrationsManager(base.Manager): + resource_class = ServerMigration + + @api_versions.wraps("2.22") + def live_migrate_force_complete(self, server, migration): + """ + Force on-going live migration to complete + + :param server: The :class:`Server` (or its ID) + :param migration: Migration id that will be forced to complete + :returns: An instance of novaclient.base.TupleWithMeta + """ + body = {'force_complete': None} + resp, body = self.api.client.post( + '/servers/%s/migrations/%s/action' % (base.getid(server), + base.getid(migration)), + body=body) + return self.convert_into_with_meta(body, resp) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 95b892d89..d5087ca60 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -3838,6 +3838,15 @@ def do_live_migration(cs, args): args.disk_over_commit) +@api_versions.wraps("2.22") +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg('migration', metavar='', help=_('ID of migration.')) +def do_live_migration_force_complete(cs, args): + """Force on-going live migration to complete.""" + server = _find_server(cs, args.server) + cs.server_migrations.live_migrate_force_complete(server, args.migration) + + @cliutils.arg( '--all-tenants', action='store_const', From 5c1acb91f71308b44f0005cbbad0edf4f34c00ce Mon Sep 17 00:00:00 2001 From: Cao ShuFeng Date: Sun, 28 Feb 2016 14:51:51 +0800 Subject: [PATCH 0997/1705] Use isinstance instead of type With the patch sets of request_ids, the *Manager classes return 'DictWithMeta' or 'ListWithMeta' rather than 'dict' or 'list'. This change adjust conditional statements to use isinstance when comparing variables. Isinstance supports inheritance type checking better than type. The effected subcommands are: evacuate interface-list interface-attach interface-detach Change-Id: I0c1291110c1386d2ff027cb149a5aff20019e6f7 Closes-Bug: 1550870 --- novaclient/v2/shell.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 95b892d89..9a84979a7 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -4698,7 +4698,7 @@ def do_evacuate(cs, args): server = _find_server(cs, args.server) on_shared_storage = getattr(args, 'on_shared_storage', None) res = server.evacuate(args.host, on_shared_storage, args.password)[1] - if type(res) is dict: + if isinstance(res, dict): utils.print_dict(res) @@ -4723,7 +4723,7 @@ def do_interface_list(cs, args): server = _find_server(cs, args.server) res = server.interface_list() - if type(res) is list: + if isinstance(res, list): _print_interfaces(res) @@ -4748,7 +4748,7 @@ def do_interface_attach(cs, args): server = _find_server(cs, args.server) res = server.interface_attach(args.port_id, args.net_id, args.fixed_ip) - if type(res) is dict: + if isinstance(res, dict): utils.print_dict(res) @@ -4759,7 +4759,7 @@ def do_interface_detach(cs, args): server = _find_server(cs, args.server) res = server.interface_detach(args.port_id) - if type(res) is dict: + if isinstance(res, dict): utils.print_dict(res) From e0c7d2c6731744374ea83385305624254454b0ed Mon Sep 17 00:00:00 2001 From: ShaoHe Feng Date: Mon, 29 Feb 2016 23:07:33 +0100 Subject: [PATCH 0998/1705] Add two server-migration commands and bump migration-list command 1. Add two new commands Add nova client server-migration-list and server-migration-show 2. Bump and old command Add migration_type field for migration-list command Partially implements blueprint live-migration-progress-report Depends-On: Ia92ecbe3c99082e3a34adf4fd29041b1a95ef21e Change-Id: I071198fa9ba0699383bdebf4fab54714a435e6c3 --- novaclient/__init__.py | 2 +- .../unit/fixture_data/server_migrations.py | 51 ++++++++++ .../tests/unit/v2/contrib/test_migrations.py | 12 +++ novaclient/tests/unit/v2/fakes.py | 95 +++++++++++++++---- .../tests/unit/v2/test_server_migrations.py | 49 ++++++++++ novaclient/tests/unit/v2/test_shell.py | 14 +++ novaclient/v2/contrib/migrations.py | 15 ++- novaclient/v2/server_migrations.py | 27 +++++- novaclient/v2/shell.py | 36 +++++++ 9 files changed, 279 insertions(+), 22 deletions(-) diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 509d2a20d..c7bff3351 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.22") +API_MAX_VERSION = api_versions.APIVersion("2.23") diff --git a/novaclient/tests/unit/fixture_data/server_migrations.py b/novaclient/tests/unit/fixture_data/server_migrations.py index 3aed49a54..93f299fbe 100644 --- a/novaclient/tests/unit/fixture_data/server_migrations.py +++ b/novaclient/tests/unit/fixture_data/server_migrations.py @@ -25,3 +25,54 @@ def setUp(self): self.requests.register_uri('POST', url, status_code=202, headers=self.json_headers) + + get_migrations = {'migrations': [ + { + "created_at": "2016-01-29T13:42:02.000000", + "dest_compute": "compute2", + "dest_host": "1.2.3.4", + "dest_node": "node2", + "id": 1, + "server_uuid": "4cfba335-03d8-49b2-8c52-e69043d1e8fe", + "source_compute": "compute1", + "source_node": "node1", + "status": "running", + "memory_total_bytes": 123456, + "memory_processed_bytes": 12345, + "memory_remaining_bytes": 120000, + "disk_total_bytes": 234567, + "disk_processed_bytes": 23456, + "disk_remaining_bytes": 230000, + "updated_at": "2016-01-29T13:42:02.000000" + }]} + + url = self.url('1234', 'migrations') + self.requests.register_uri('GET', url, + status_code=200, + json=get_migrations, + headers=self.json_headers) + + get_migration = {'migration': { + "created_at": "2016-01-29T13:42:02.000000", + "dest_compute": "compute2", + "dest_host": "1.2.3.4", + "dest_node": "node2", + "id": 1, + "server_uuid": "4cfba335-03d8-49b2-8c52-e69043d1e8fe", + "source_compute": "compute1", + "source_node": "node1", + "status": "running", + "memory_total_bytes": 123456, + "memory_processed_bytes": 12345, + "memory_remaining_bytes": 120000, + "disk_total_bytes": 234567, + "disk_processed_bytes": 23456, + "disk_remaining_bytes": 230000, + "updated_at": "2016-01-29T13:42:02.000000" + }} + + url = self.url('1234', 'migrations', '1') + self.requests.register_uri('GET', url, + status_code=200, + json=get_migration, + headers=self.json_headers) diff --git a/novaclient/tests/unit/v2/contrib/test_migrations.py b/novaclient/tests/unit/v2/contrib/test_migrations.py index 29cac5d44..8d12633e1 100644 --- a/novaclient/tests/unit/v2/contrib/test_migrations.py +++ b/novaclient/tests/unit/v2/contrib/test_migrations.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient import api_versions from novaclient import extension from novaclient.tests.unit import utils from novaclient.tests.unit.v2 import fakes @@ -30,6 +31,17 @@ def test_list_migrations(self): cs.assert_called('GET', '/os-migrations') for m in ml: self.assertIsInstance(m, migrations.Migration) + self.assertRaises(AttributeError, getattr, m, "migration_type") + + def test_list_migrations_v223(self): + cs = fakes.FakeClient(extensions=extensions, + api_version=api_versions.APIVersion("2.23")) + ml = cs.migrations.list() + self.assert_request_id(ml, fakes.FAKE_REQUEST_ID_LIST) + cs.assert_called('GET', '/os-migrations') + for m in ml: + self.assertIsInstance(m, migrations.Migration) + self.assertEqual(m.migration_type, 'live-migration') def test_list_migrations_with_filters(self): ml = cs.migrations.list('host1', 'finished', 'child1') diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index d3d1e4051..85f4b74bc 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -23,8 +23,10 @@ from six.moves.urllib import parse import novaclient +from novaclient import api_versions from novaclient import client as base_client from novaclient import exceptions +from novaclient.i18n import _ from novaclient.tests.unit import fakes from novaclient.tests.unit import utils from novaclient.v2 import client @@ -58,7 +60,8 @@ def __init__(self, api_version=None, *args, **kwargs): 'project_id', 'auth_url', extensions=kwargs.get('extensions'), direct_use=False) - self.api_version = api_version + self.api_version = api_version or api_versions.APIVersion("2.1") + kwargs["api_version"] = self.api_version self.client = FakeHTTPClient(**kwargs) @@ -91,6 +94,7 @@ def __init__(self, **kwargs): self.http_log_debug = 'http_log_debug' self.last_request_id = None self.management_url = self.get_endpoint() + self.api_version = kwargs.get("api_version") def _cs_request(self, url, method, **kwargs): # Check that certain things are called correctly @@ -2367,21 +2371,26 @@ def get_os_cells_child_cell_capacities(self, **kw): return self.get_os_cells_capacities() def get_os_migrations(self, **kw): - migrations = {'migrations': [ - { - "created_at": "2012-10-29T13:42:02.000000", - "dest_compute": "compute2", - "dest_host": "1.2.3.4", - "dest_node": "node2", - "id": 1234, - "instance_uuid": "instance_id_123", - "new_instance_type_id": 2, - "old_instance_type_id": 1, - "source_compute": "compute1", - "source_node": "node1", - "status": "Done", - "updated_at": "2012-10-29T13:42:02.000000" - }]} + migration = { + "created_at": "2012-10-29T13:42:02.000000", + "dest_compute": "compute2", + "dest_host": "1.2.3.4", + "dest_node": "node2", + "id": 1234, + "instance_uuid": "instance_id_123", + "new_instance_type_id": 2, + "old_instance_type_id": 1, + "source_compute": "compute1", + "source_node": "node1", + "status": "Done", + "updated_at": "2012-10-29T13:42:02.000000" + } + + if self.api_version >= api_versions.APIVersion("2.23"): + migration.update({"migration_type": "live-migration"}) + + migrations = {'migrations': [migration]} + return (200, FAKE_RESPONSE_HEADERS, migrations) def post_os_server_external_events(self, **kw): @@ -2435,6 +2444,57 @@ def delete_os_server_groups_2cbd51f4_fafe_4cdb_801b_cf913a6f288b( def post_servers_1234_migrations_1_action(self, body): return (202, {}, None) + def get_servers_1234_migrations_1(self, **kw): + # TODO(Shaohe Feng) this condition check can be a decorator + if self.api_version < api_versions.APIVersion("2.23"): + raise exceptions.UnsupportedVersion(_("Unsupport version %s") + % self.api_version) + migration = {"migration": { + "created_at": "2016-01-29T13:42:02.000000", + "dest_compute": "compute2", + "dest_host": "1.2.3.4", + "dest_node": "node2", + "id": 1, + "server_uuid": "4cfba335-03d8-49b2-8c52-e69043d1e8fe", + "source_compute": "compute1", + "source_node": "node1", + "status": "running", + "memory_total_bytes": 123456, + "memory_processed_bytes": 12345, + "memory_remaining_bytes": 120000, + "disk_total_bytes": 234567, + "disk_processed_bytes": 23456, + "disk_remaining_bytes": 230000, + "updated_at": "2016-01-29T13:42:02.000000" + }} + return (200, FAKE_RESPONSE_HEADERS, migration) + + def get_servers_1234_migrations(self, **kw): + # TODO(Shaohe Feng) this condition check can be a decorator + if self.api_version < api_versions.APIVersion("2.23"): + raise exceptions.UnsupportedVersion(_("Unsupport version %s") + % self.api_version) + migrations = {'migrations': [ + { + "created_at": "2016-01-29T13:42:02.000000", + "dest_compute": "compute2", + "dest_host": "1.2.3.4", + "dest_node": "node2", + "id": 1, + "server_uuid": "4cfba335-03d8-49b2-8c52-e69043d1e8fe", + "source_compute": "compute1", + "source_node": "node1", + "status": "running", + "memory_total_bytes": 123456, + "memory_processed_bytes": 12345, + "memory_remaining_bytes": 120000, + "disk_total_bytes": 234567, + "disk_processed_bytes": 23456, + "disk_remaining_bytes": 230000, + "updated_at": "2016-01-29T13:42:02.000000" + }]} + return (200, FAKE_RESPONSE_HEADERS, migrations) + class FakeSessionClient(fakes.FakeClient, client.Client): @@ -2443,6 +2503,7 @@ def __init__(self, api_version, *args, **kwargs): 'project_id', 'auth_url', extensions=kwargs.get('extensions'), api_version=api_version, direct_use=False) + kwargs['api_version'] = api_version self.client = FakeSessionMockClient(**kwargs) @@ -2461,7 +2522,7 @@ def __init__(self, *args, **kwargs): self.interface = None self.region_name = None self.version = None - + self.api_version = kwargs.get('api_version') self.auth.get_auth_ref.return_value.project_id = 'tenant_id' def request(self, url, method, **kwargs): diff --git a/novaclient/tests/unit/v2/test_server_migrations.py b/novaclient/tests/unit/v2/test_server_migrations.py index 12c5a269e..e4a2b4bac 100644 --- a/novaclient/tests/unit/v2/test_server_migrations.py +++ b/novaclient/tests/unit/v2/test_server_migrations.py @@ -14,9 +14,12 @@ # under the License. from novaclient import api_versions +from novaclient import base from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit.fixture_data import server_migrations as data from novaclient.tests.unit import utils +from novaclient.tests.unit.v2 import fakes +from novaclient.v2 import server_migrations class ServerMigrationsTest(utils.FixturedTestCase): @@ -31,3 +34,49 @@ def test_live_migration_force_complete(self): body = {'force_complete': None} self.cs.server_migrations.live_migrate_force_complete(1234, 1) self.assert_called('POST', '/servers/1234/migrations/1/action', body) + + +class ServerMigrationsTestV223(ServerMigrationsTest): + + migration = { + "created_at": "2016-01-29T13:42:02.000000", + "dest_compute": "compute2", + "dest_host": "1.2.3.4", + "dest_node": "node2", + "id": 1, + "server_uuid": "4cfba335-03d8-49b2-8c52-e69043d1e8fe", + "source_compute": "compute1", + "source_node": "node1", + "status": "running", + "memory_total_bytes": 123456, + "memory_processed_bytes": 12345, + "memory_remaining_bytes": 120000, + "disk_total_bytes": 234567, + "disk_processed_bytes": 23456, + "disk_remaining_bytes": 230000, + "updated_at": "2016-01-29T13:42:02.000000" + } + + def setUp(self): + super(ServerMigrationsTestV223, self).setUp() + self.cs.api_version = api_versions.APIVersion("2.23") + + def test_list_migrations(self): + ml = self.cs.server_migrations.list(1234) + + self.assertIsInstance(ml, base.ListWithMeta) + self.assert_request_id(ml, fakes.FAKE_REQUEST_ID_LIST) + for k in self.migration: + self.assertEqual(self.migration[k], getattr(ml[0], k)) + + self.assert_called('GET', '/servers/1234/migrations') + + def test_get_migration(self): + migration = self.cs.server_migrations.get(1234, 1) + + self.assertIsInstance(migration, server_migrations.ServerMigration) + for k in migration._info: + self.assertEqual(self.migration[k], migration._info[k]) + self.assert_request_id(migration, fakes.FAKE_REQUEST_ID_LIST) + + self.assert_called('GET', '/servers/1234/migrations/1') diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index cb30ce40a..7d1502eb2 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -1687,6 +1687,16 @@ def test_live_migration_force_complete(self): self.assert_called('POST', '/servers/1234/migrations/1/action', {'force_complete': None}) + def test_list_migrations(self): + self.run_command('server-migration-list sample-server', + api_version='2.23') + self.assert_called('GET', '/servers/1234/migrations') + + def test_get_migration(self): + self.run_command('server-migration-show sample-server 1', + api_version='2.23') + self.assert_called('GET', '/servers/1234/migrations/1') + def test_host_evacuate_live_with_no_target_host(self): self.run_command('host-evacuate-live hyper') self.assert_called('GET', '/os-hypervisors/hyper/servers', pos=0) @@ -2501,6 +2511,10 @@ def test_migration_list(self): self.run_command('migration-list') self.assert_called('GET', '/os-migrations') + def test_migration_list_v223(self): + self.run_command('migration-list', api_version="2.23") + self.assert_called('GET', '/os-migrations') + def test_migration_list_with_filters(self): self.run_command('migration-list --host host1 --cell_name child1 ' '--status finished') diff --git a/novaclient/v2/contrib/migrations.py b/novaclient/v2/contrib/migrations.py index 15959818a..6af813182 100644 --- a/novaclient/v2/contrib/migrations.py +++ b/novaclient/v2/contrib/migrations.py @@ -16,6 +16,7 @@ from six.moves.urllib import parse +from novaclient import api_versions from novaclient import base from novaclient.i18n import _ from novaclient.openstack.common import cliutils @@ -71,11 +72,11 @@ def list(self, host=None, status=None, cell_name=None): help=_('Fetch migrations for the given cell_name.')) def do_migration_list(cs, args): """Print a list of migrations.""" - _print_migrations(cs.migrations.list(args.host, args.status, - args.cell_name)) + migrations = cs.migrations.list(args.host, args.status, args.cell_name) + _print_migrations(cs, migrations) -def _print_migrations(migrations): +def _print_migrations(cs, migrations): fields = ['Source Node', 'Dest Node', 'Source Compute', 'Dest Compute', 'Dest Host', 'Status', 'Instance UUID', 'Old Flavor', 'New Flavor', 'Created At', 'Updated At'] @@ -86,6 +87,14 @@ def old_flavor(migration): def new_flavor(migration): return migration.new_instance_type_id + def migration_type(migration): + return migration.migration_type + formatters = {'Old Flavor': old_flavor, 'New Flavor': new_flavor} + if cs.api_version >= api_versions.APIVersion("2.23"): + fields.insert(0, "Id") + fields.append("Type") + formatters.update({"Type": migration_type}) + utils.print_list(migrations, fields, formatters) diff --git a/novaclient/v2/server_migrations.py b/novaclient/v2/server_migrations.py index 476c0a80e..68ee94c9b 100644 --- a/novaclient/v2/server_migrations.py +++ b/novaclient/v2/server_migrations.py @@ -22,7 +22,7 @@ def __repr__(self): return "" -class ServerMigrationsManager(base.Manager): +class ServerMigrationsManager(base.ManagerWithFind): resource_class = ServerMigration @api_versions.wraps("2.22") @@ -40,3 +40,28 @@ def live_migrate_force_complete(self, server, migration): base.getid(migration)), body=body) return self.convert_into_with_meta(body, resp) + + @api_versions.wraps("2.23") + def get(self, server, migration): + """ + Get a migration of a specified server + + :param server: The :class:`Server` (or its ID) + :param migration: Migration id that will be gotten. + :returns: An instance of + novaclient.v2.server_migrations.ServerMigration + """ + return self._get('/servers/%s/migrations/%s' % + (base.getid(server), base.getid(migration)), + 'migration') + + @api_versions.wraps("2.23") + def list(self, server): + """ + Get a migrations list of a specified server + + :param server: The :class:`Server` (or its ID) + :returns: An instance of novaclient.base.ListWithMeta + """ + return self._list( + '/servers/%s/migrations' % base.getid(server), "migrations") diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 9693fc713..1dc728f82 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -3847,6 +3847,42 @@ def do_live_migration_force_complete(cs, args): cs.server_migrations.live_migrate_force_complete(server, args.migration) +@api_versions.wraps("2.23") +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +def do_server_migration_list(cs, args): + """Get the migrations list of specified server.""" + server = _find_server(cs, args.server) + migrations = cs.server_migrations.list(server) + + fields = ['Id', 'Source Node', 'Dest Node', 'Source Compute', + 'Dest Compute', 'Dest Host', 'Status', 'Server UUID', + 'Created At', 'Updated At'] + + format_name = ["Total Memory Bytes", "Processed Memory Bytes", + "Remaining Memory Bytes", "Total Disk Bytes", + "Processed Disk Bytes", "Remaining Disk Bytes"] + + format_key = ["memory_total_bytes", "memory_processed_bytes", + "memory_remaining_bytes", "disk_total_bytes", + "disk_processed_bytes", "disk_remaining_bytes"] + + formatters = map(lambda field: utils._make_field_formatter(field)[1], + format_key) + formatters = dict(zip(format_name, formatters)) + + utils.print_list(migrations, fields + format_name, formatters) + + +@api_versions.wraps("2.23") +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg('migration', metavar='', help=_('ID of migration.')) +def do_server_migration_show(cs, args): + """Get the migration of specified server.""" + server = _find_server(cs, args.server) + migration = cs.server_migrations.get(server, args.migration) + utils.print_dict(migration._info) + + @cliutils.arg( '--all-tenants', action='store_const', From 77e50cc91b328b1f7681cfc6f31bc41e40ab214e Mon Sep 17 00:00:00 2001 From: Andrea Rosa Date: Mon, 22 Feb 2016 16:27:52 +0000 Subject: [PATCH 0999/1705] Support for abort an ongoing live migration In Nova API microversion 2.24 there is a new operation which allows to abort a running live migration. This change is to enable this feature at the client side implementing a new method to call the new nova API: nova live-migration-abort Implements blueprint: abort-live-migration Depends-On: I1ff861e54997a069894b542bd764ac3ef1b3dbb2 Change-Id: Ic2ead126e0cf48aa54a083e97cb9d1303a5a9bbd --- novaclient/__init__.py | 2 +- .../tests/unit/fixture_data/server_migrations.py | 4 ++++ novaclient/tests/unit/v2/fakes.py | 3 +++ .../tests/unit/v2/test_server_migrations.py | 10 ++++++++++ novaclient/tests/unit/v2/test_shell.py | 5 +++++ novaclient/v2/server_migrations.py | 15 ++++++++++++++- novaclient/v2/shell.py | 9 +++++++++ 7 files changed, 46 insertions(+), 2 deletions(-) diff --git a/novaclient/__init__.py b/novaclient/__init__.py index c7bff3351..af127d4e8 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.23") +API_MAX_VERSION = api_versions.APIVersion("2.24") diff --git a/novaclient/tests/unit/fixture_data/server_migrations.py b/novaclient/tests/unit/fixture_data/server_migrations.py index 93f299fbe..5e946eef5 100644 --- a/novaclient/tests/unit/fixture_data/server_migrations.py +++ b/novaclient/tests/unit/fixture_data/server_migrations.py @@ -76,3 +76,7 @@ def setUp(self): status_code=200, json=get_migration, headers=self.json_headers) + url = self.url('1234', 'migrations', '1') + self.requests.register_uri('DELETE', url, + status_code=202, + headers=self.json_headers) diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 85f4b74bc..0d84abf9b 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -2495,6 +2495,9 @@ def get_servers_1234_migrations(self, **kw): }]} return (200, FAKE_RESPONSE_HEADERS, migrations) + def delete_servers_1234_migrations_1(self): + return (202, {}, None) + class FakeSessionClient(fakes.FakeClient, client.Client): diff --git a/novaclient/tests/unit/v2/test_server_migrations.py b/novaclient/tests/unit/v2/test_server_migrations.py index e4a2b4bac..d0d8cdcc7 100644 --- a/novaclient/tests/unit/v2/test_server_migrations.py +++ b/novaclient/tests/unit/v2/test_server_migrations.py @@ -80,3 +80,13 @@ def test_get_migration(self): self.assert_request_id(migration, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/servers/1234/migrations/1') + + +class ServerMigrationsTestV224(ServerMigrationsTest): + def setUp(self): + super(ServerMigrationsTestV224, self).setUp() + self.cs.api_version = api_versions.APIVersion("2.24") + + def test_live_migration_abort(self): + self.cs.server_migrations.live_migration_abort(1234, 1) + self.assert_called('DELETE', '/servers/1234/migrations/1') diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 7d1502eb2..8c0f0f0f5 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -1697,6 +1697,11 @@ def test_get_migration(self): api_version='2.23') self.assert_called('GET', '/servers/1234/migrations/1') + def test_live_migration_abort(self): + self.run_command('live-migration-abort sample-server 1', + api_version='2.24') + self.assert_called('DELETE', '/servers/1234/migrations/1') + def test_host_evacuate_live_with_no_target_host(self): self.run_command('host-evacuate-live hyper') self.assert_called('GET', '/os-hypervisors/hyper/servers', pos=0) diff --git a/novaclient/v2/server_migrations.py b/novaclient/v2/server_migrations.py index 68ee94c9b..a85e85f27 100644 --- a/novaclient/v2/server_migrations.py +++ b/novaclient/v2/server_migrations.py @@ -64,4 +64,17 @@ def list(self, server): :returns: An instance of novaclient.base.ListWithMeta """ return self._list( - '/servers/%s/migrations' % base.getid(server), "migrations") + '/servers/%s/migrations' % (base.getid(server)), "migrations") + + @api_versions.wraps("2.24") + def live_migration_abort(self, server, migration): + """ + Cancel an ongoing live migration + + :param server: The :class:`Server` (or its ID) + :param migration: Migration id that will be cancelled + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self._delete( + '/servers/%s/migrations/%s' % (base.getid(server), + base.getid(migration))) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 1dc728f82..5847a11f2 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -3883,6 +3883,15 @@ def do_server_migration_show(cs, args): utils.print_dict(migration._info) +@api_versions.wraps("2.24") +@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@cliutils.arg('migration', metavar='', help=_('ID of migration.')) +def do_live_migration_abort(cs, args): + """Abort an on-going live migration.""" + server = _find_server(cs, args.server) + cs.server_migrations.live_migration_abort(server, args.migration) + + @cliutils.arg( '--all-tenants', action='store_const', From ae598280acc46dd4ee20af78a5780a450f96b084 Mon Sep 17 00:00:00 2001 From: Eli Qiao Date: Sat, 27 Feb 2016 09:59:01 +0800 Subject: [PATCH 1000/1705] [microversion] Bump to 2.25 microversion v2.25 will change parameter of os-migrateLive, this patch adds supports to that changes. os-migrateLive will abandon disk_over_commit and the default value of block_migration will be set to `auto` Depends-on: Ibb0d50f0f7444028ef9d0c294aea41edf0024b31 Implements: blueprint making-live-migration-api-friendly Change-Id: I01b22593724616bc0a7793c509ecabf095d6927d --- novaclient/__init__.py | 2 +- novaclient/tests/unit/fixture_data/servers.py | 5 ++ novaclient/tests/unit/v2/fakes.py | 11 ++- novaclient/tests/unit/v2/test_servers.py | 75 ++++++++++++++++++- novaclient/tests/unit/v2/test_shell.py | 16 ++++ novaclient/v2/servers.py | 55 ++++++++++++-- novaclient/v2/shell.py | 27 +++++-- 7 files changed, 171 insertions(+), 20 deletions(-) diff --git a/novaclient/__init__.py b/novaclient/__init__.py index af127d4e8..0f8b98308 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.24") +API_MAX_VERSION = api_versions.APIVersion("2.25") diff --git a/novaclient/tests/unit/fixture_data/servers.py b/novaclient/tests/unit/fixture_data/servers.py index 4d2df95b3..9b68a6560 100644 --- a/novaclient/tests/unit/fixture_data/servers.py +++ b/novaclient/tests/unit/fixture_data/servers.py @@ -425,6 +425,11 @@ def post_servers_1234_action(self, request, context): # if we found 'action' in method check_server_actions and # raise AssertionError if we didn't find 'action' at all. pass + elif action == 'os-migrateLive': + # Fixme(eliqiao): body of os-migrateLive changes from v2.25 + # but we can not specify version in data_fixture now and this is + # V1 data, so just let it pass + pass elif action == 'rebuild': body = body[action] adminPass = body.get('adminPass', 'randompassword') diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 0d84abf9b..6b24d2f76 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -710,9 +710,6 @@ def check_server_actions(cls, body): assert list(body[action]) == ['adminPass'] elif action in cls.type_actions: assert list(body[action]) == ['type'] - elif action == 'os-migrateLive': - assert set(body[action].keys()) == set(['host', 'block_migration', - 'disk_over_commit']) elif action == 'os-resetState': assert list(body[action]) == ['state'] elif action == 'resetNetwork': @@ -741,6 +738,14 @@ def post_servers_1234_action(self, body, **kw): # if we found 'action' in method check_server_actions and # raise AssertionError if we didn't find 'action' at all. pass + elif action == 'os-migrateLive': + if self.api_version < api_versions.APIVersion("2.25"): + assert set(body[action].keys()) == set(['host', + 'block_migration', + 'disk_over_commit']) + else: + assert set(body[action].keys()) == set(['host', + 'block_migration']) elif action == 'rebuild': body = body[action] adminPass = body.get('adminPass', 'randompassword') diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index b8a8bd16b..a99eda98d 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -841,12 +841,28 @@ def test_live_migrate_server(self): ret = s.live_migrate(host='hostname', block_migration=False, disk_over_commit=False) self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action', + {'os-migrateLive': {'host': 'hostname', + 'block_migration': False, + 'disk_over_commit': False}}) ret = self.cs.servers.live_migrate(s, host='hostname', block_migration=False, disk_over_commit=False) self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('POST', '/servers/1234/action') + self.assert_called('POST', '/servers/1234/action', + {'os-migrateLive': {'host': 'hostname', + 'block_migration': False, + 'disk_over_commit': False}}) + + def test_live_migrate_server_block_migration_none(self): + s = self.cs.servers.get(1234) + ret = s.live_migrate(host='hostname', block_migration=None, + disk_over_commit=None) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action', + {'os-migrateLive': {'host': 'hostname', + 'block_migration': False, + 'disk_over_commit': False}}) def test_reset_state(self): s = self.cs.servers.get(1234) @@ -1077,3 +1093,58 @@ def test_rebuild_with_description(self): ret = s.rebuild(image="1", description="descr") self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') + + +class ServersV225Test(ServersV219Test): + def setUp(self): + super(ServersV219Test, self).setUp() + self.cs.api_version = api_versions.APIVersion("2.25") + + def test_live_migrate_server(self): + s = self.cs.servers.get(1234) + ret = s.live_migrate(host='hostname', block_migration='auto') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action', + {'os-migrateLive': {'host': 'hostname', + 'block_migration': 'auto'}}) + ret = self.cs.servers.live_migrate(s, host='hostname', + block_migration='auto') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action', + {'os-migrateLive': {'host': 'hostname', + 'block_migration': 'auto'}}) + + def test_live_migrate_server_block_migration_true(self): + s = self.cs.servers.get(1234) + ret = s.live_migrate(host='hostname', block_migration=True) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action', + {'os-migrateLive': {'host': 'hostname', + 'block_migration': True}}) + + ret = self.cs.servers.live_migrate(s, host='hostname', + block_migration=True) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action', + {'os-migrateLive': {'host': 'hostname', + 'block_migration': True}}) + + def test_live_migrate_server_block_migration_none(self): + s = self.cs.servers.get(1234) + ret = s.live_migrate(host='hostname', block_migration=None) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action', + {'os-migrateLive': {'host': 'hostname', + 'block_migration': 'auto'}}) + + ret = self.cs.servers.live_migrate(s, host='hostname', + block_migration='auto') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action', + {'os-migrateLive': {'host': 'hostname', + 'block_migration': 'auto'}}) + + def test_live_migrate_server_with_disk_over_commit(self): + s = self.cs.servers.get(1234) + self.assertRaises(ValueError, s.live_migrate, 'hostname', + 'auto', 'True') diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 8c0f0f0f5..68bc1b4e4 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -1681,6 +1681,22 @@ def test_live_migration(self): 'block_migration': True, 'disk_over_commit': True}}) + def test_live_migration_v225(self): + self.run_command('live-migration sample-server hostname', + api_version='2.25') + self.assert_called('POST', '/servers/1234/action', + {'os-migrateLive': {'host': 'hostname', + 'block_migration': 'auto'}}) + self.run_command('live-migration sample-server hostname' + ' --block-migrate', api_version='2.25') + self.assert_called('POST', '/servers/1234/action', + {'os-migrateLive': {'host': 'hostname', + 'block_migration': True}}) + self.run_command('live-migration sample-server', api_version='2.25') + self.assert_called('POST', '/servers/1234/action', + {'os-migrateLive': {'host': None, + 'block_migration': 'auto'}}) + def test_live_migration_force_complete(self): self.run_command('live-migration-force-complete sample-server 1', api_version='2.22') diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index 961ae61da..6c1e9ee0a 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -408,19 +408,42 @@ def networks(self): return {} def live_migrate(self, host=None, - block_migration=False, - disk_over_commit=False): + block_migration=None, + disk_over_commit=None): """ Migrates a running instance to a new machine. :param host: destination host name. - :param block_migration: if True, do block_migration. - :param disk_over_commit: if True, Allow overcommit. + :param block_migration: if True, do block_migration, the default + value None will be mapped to False for 2.0 - + 2.24, 'auto' for higher than 2.25 + :param disk_over_commit: if True, allow disk over commit, the default + value None will be mapped to False. It will + not be supported since 2.25 :returns: An instance of novaclient.base.TupleWithMeta """ - return self.manager.live_migrate(self, host, - block_migration, - disk_over_commit) + + if (self.manager.api_version < api_versions.APIVersion("2.25")): + # NOTE(eliqiao): We do this to keep old version api has same + # default value if user don't pass these parameters when using + # SDK + if block_migration is None: + block_migration = False + if disk_over_commit is None: + disk_over_commit = False + + return self.manager.live_migrate(self, host, + block_migration, + disk_over_commit) + else: + if block_migration is None: + block_migration = 'auto' + if disk_over_commit is not None: + raise ValueError("Setting 'disk_over_commit' argument is " + "prohibited after microversion 2.25.") + + return self.manager.live_migrate(self, host, + block_migration) def reset_state(self, state='error'): """ @@ -1487,6 +1510,7 @@ def delete_meta(self, server, keys): return result + @api_versions.wraps('2.0', '2.24') def live_migrate(self, server, host, block_migration, disk_over_commit): """ Migrates a running instance to a new machine. @@ -1494,7 +1518,7 @@ def live_migrate(self, server, host, block_migration, disk_over_commit): :param server: instance id which comes from nova list. :param host: destination host name. :param block_migration: if True, do block_migration. - :param disk_over_commit: if True, Allow overcommit. + :param disk_over_commit: if True, allow disk overcommit. :returns: An instance of novaclient.base.TupleWithMeta """ return self._action('os-migrateLive', server, @@ -1502,6 +1526,21 @@ def live_migrate(self, server, host, block_migration, disk_over_commit): 'block_migration': block_migration, 'disk_over_commit': disk_over_commit}) + @api_versions.wraps('2.25') + def live_migrate(self, server, host, block_migration): + """ + Migrates a running instance to a new machine. + + :param server: instance id which comes from nova list. + :param host: destination host name. + :param block_migration: if True, do block_migration, can be set as + 'auto' + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self._action('os-migrateLive', server, + {'host': host, + 'block_migration': block_migration}) + def reset_state(self, server, state='error'): """ Reset the state of an instance to active or error. diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 5847a11f2..85c3d51c2 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -3810,7 +3810,15 @@ def parser_hosts(fields): action='store_true', dest='block_migrate', default=False, - help=_('True in case of block_migration. (Default=False:live_migration)')) + help=_('True in case of block_migration. (Default=False:live_migration)'), + start_version="2.0", end_version="2.24") +@cliutils.arg( + '--block-migrate', + action='store_true', + dest='block_migrate', + default="auto", + help=_('True in case of block_migration. (Default=auto:live_migration)'), + start_version="2.25") @cliutils.arg( '--block_migrate', real_action='store_true', @@ -3823,19 +3831,26 @@ def parser_hosts(fields): action='store_true', dest='disk_over_commit', default=False, - help=_('Allow overcommit. (Default=False)')) + help=_('Allow overcommit. (Default=False)'), + start_version="2.0", end_version="2.24") @cliutils.arg( '--disk_over_commit', real_action='store_true', action=shell.DeprecatedAction, use=_('use "%s"; this option will be removed in ' 'novaclient 3.3.0.') % '--disk-over-commit', - help=argparse.SUPPRESS) + help=argparse.SUPPRESS, + start_version="2.0", end_version="2.24") def do_live_migration(cs, args): """Migrate running server to a new machine.""" - _find_server(cs, args.server).live_migrate(args.host, - args.block_migrate, - args.disk_over_commit) + + if 'disk_over_commit' in args: + _find_server(cs, args.server).live_migrate(args.host, + args.block_migrate, + args.disk_over_commit) + else: + _find_server(cs, args.server).live_migrate(args.host, + args.block_migrate) @api_versions.wraps("2.22") From 54ed91583f25338422d8575d11a3bfd72d45d81b Mon Sep 17 00:00:00 2001 From: meretiko Date: Wed, 2 Mar 2016 14:21:41 +0200 Subject: [PATCH 1001/1705] The novaclient Python API doc keystoneauth example fixed The novaclient Python API document's keystoneauth session example is not working. AttributeError: 'Password' object has no attribute 'Password' is fixed with this patch Change-Id: I0c06279eed936619a770e84207bb02e79f42f6a7 Closes-Bug: #1552206 --- doc/source/api.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/source/api.rst b/doc/source/api.rst index 3b179e49b..3166ddf62 100644 --- a/doc/source/api.rst +++ b/doc/source/api.rst @@ -26,10 +26,10 @@ session API:: >>> from keystoneauth1 import session >>> from novaclient import client >>> loader = loading.get_plugin_loader('password') - >>> auth = loader.Password(auth_url=AUTH_URL, - ... username=USERNAME, - ... password=PASSWORD, - ... project_id=PROJECT_ID) + >>> auth = loader.load_from_options(auth_url=AUTH_URL, + ... username=USERNAME, + ... password=PASSWORD, + ... project_id=PROJECT_ID) >>> sess = session.Session(auth=auth) >>> nova = client.Client(VERSION, session=sess) From 93913c84b67d3aefe79bef106fbe9a83f0bcdb71 Mon Sep 17 00:00:00 2001 From: Kevin_Zheng Date: Tue, 1 Mar 2016 16:35:01 +0800 Subject: [PATCH 1002/1705] Use novaclient/utils directly and remove openstack/common (1/4) We can use novaclient/utils directly and get rid of openstack/common folder. This is the first part. Change-Id: Iaec234fbcf4d0f8c7e8f2175eae11d3083a62090 Partial-Bug: #1551603 --- novaclient/shell.py | 3 +- novaclient/tests/unit/fake_actions_module.py | 10 ++--- novaclient/tests/unit/test_api_versions.py | 5 +-- novaclient/v2/contrib/baremetal.py | 37 +++++++++---------- novaclient/v2/contrib/cells.py | 5 +-- novaclient/v2/contrib/deferred_delete.py | 5 +-- novaclient/v2/contrib/host_evacuate.py | 7 ++-- novaclient/v2/contrib/host_evacuate_live.py | 11 +++--- novaclient/v2/contrib/host_servers_migrate.py | 3 +- novaclient/v2/contrib/instance_action.py | 11 +++--- novaclient/v2/contrib/metadata_extensions.py | 8 ++-- novaclient/v2/contrib/migrations.py | 7 ++-- novaclient/v2/contrib/tenant_networks.py | 17 ++++----- 13 files changed, 59 insertions(+), 70 deletions(-) diff --git a/novaclient/shell.py b/novaclient/shell.py index b4da9e2c6..3d38ac99d 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -46,7 +46,6 @@ from novaclient import exceptions as exc import novaclient.extension from novaclient.i18n import _ -from novaclient.openstack.common import cliutils from novaclient import utils DEFAULT_MAJOR_OS_COMPUTE_API_VERSION = "2.0" @@ -1042,7 +1041,7 @@ def do_bash_completion(self, _args): commands.remove('bash_completion') print(' '.join(commands | options)) - @cliutils.arg( + @utils.arg( 'command', metavar='', nargs='?', diff --git a/novaclient/tests/unit/fake_actions_module.py b/novaclient/tests/unit/fake_actions_module.py index 8d4b0175b..eb180486b 100644 --- a/novaclient/tests/unit/fake_actions_module.py +++ b/novaclient/tests/unit/fake_actions_module.py @@ -14,7 +14,7 @@ # under the License. from novaclient import api_versions -from novaclient.openstack.common import cliutils +from novaclient import utils @api_versions.wraps("2.10", "2.20") @@ -32,11 +32,11 @@ def do_another_fake_action(): return 0 -@cliutils.arg( +@utils.arg( '--foo', start_version='2.1', end_version='2.2') -@cliutils.arg( +@utils.arg( '--bar', start_version='2.3', end_version='2.4') @@ -44,12 +44,12 @@ def do_fake_action2(): return 3 -@cliutils.arg( +@utils.arg( '--foo', help='first foo', start_version='2.10', end_version='2.20') -@cliutils.arg( +@utils.arg( '--foo', help='second foo', start_version='2.21') diff --git a/novaclient/tests/unit/test_api_versions.py b/novaclient/tests/unit/test_api_versions.py index f33b28dde..ce27e631a 100644 --- a/novaclient/tests/unit/test_api_versions.py +++ b/novaclient/tests/unit/test_api_versions.py @@ -18,7 +18,6 @@ import novaclient from novaclient import api_versions from novaclient import exceptions -from novaclient.openstack.common import cliutils from novaclient.tests.unit import utils from novaclient import utils as nutils from novaclient.v2 import versions @@ -279,9 +278,9 @@ def some_func(*args, **kwargs): checker.assert_called_once_with(*((obj,) + some_args), **some_kwargs) def test_arguments_property_is_copied(self): - @cliutils.arg("argument_1") + @nutils.arg("argument_1") @api_versions.wraps("2.666", "2.777") - @cliutils.arg("argument_2") + @nutils.arg("argument_2") def some_func(): pass diff --git a/novaclient/v2/contrib/baremetal.py b/novaclient/v2/contrib/baremetal.py index e03e20f1b..911944212 100644 --- a/novaclient/v2/contrib/baremetal.py +++ b/novaclient/v2/contrib/baremetal.py @@ -19,7 +19,6 @@ from novaclient import base from novaclient.i18n import _ -from novaclient.openstack.common import cliutils from novaclient import utils @@ -156,43 +155,43 @@ def list_interfaces(self, node_id): return interfaces -@cliutils.arg( +@utils.arg( 'service_host', metavar='', help=_('Name of nova compute host which will control this baremetal ' 'node')) -@cliutils.arg( +@utils.arg( 'cpus', metavar='', type=int, help=_('Number of CPUs in the node')) -@cliutils.arg( +@utils.arg( 'memory_mb', metavar='', type=int, help=_('Megabytes of RAM in the node')) -@cliutils.arg( +@utils.arg( 'local_gb', metavar='', type=int, help=_('Gigabytes of local storage in the node')) -@cliutils.arg( +@utils.arg( 'prov_mac_address', metavar='', help=_('MAC address to provision the node')) -@cliutils.arg( +@utils.arg( '--pm_address', default=None, metavar='', help=_('Power management IP for the node')) -@cliutils.arg( +@utils.arg( '--pm_user', default=None, metavar='', help=_('Username for the node\'s power management')) -@cliutils.arg( +@utils.arg( '--pm_password', default=None, metavar='', help=_('Password for the node\'s power management')) -@cliutils.arg( +@utils.arg( '--terminal_port', default=None, metavar='', type=int, @@ -209,7 +208,7 @@ def do_baremetal_node_create(cs, args): _print_baremetal_resource(node) -@cliutils.arg( +@utils.arg( 'node', metavar='', help=_('ID of the node to delete.')) @@ -291,7 +290,7 @@ def _print_baremetal_node_interfaces(interfaces): ]) -@cliutils.arg( +@utils.arg( 'node', metavar='', help=_("ID of node")) @@ -301,20 +300,20 @@ def do_baremetal_node_show(cs, args): _print_baremetal_resource(node) -@cliutils.arg( +@utils.arg( 'node', metavar='', help=_("ID of node")) -@cliutils.arg( +@utils.arg( 'address', metavar='
', help=_("MAC address of interface")) -@cliutils.arg( +@utils.arg( '--datapath_id', default=0, metavar='', help=_("OpenFlow Datapath ID of interface")) -@cliutils.arg( +@utils.arg( '--port_no', default=0, metavar='', @@ -326,8 +325,8 @@ def do_baremetal_interface_add(cs, args): _print_baremetal_resource(bmif) -@cliutils.arg('node', metavar='', help=_("ID of node")) -@cliutils.arg( +@utils.arg('node', metavar='', help=_("ID of node")) +@utils.arg( 'address', metavar='
', help=_("MAC address of interface")) @@ -336,7 +335,7 @@ def do_baremetal_interface_remove(cs, args): cs.baremetal.remove_interface(args.node, args.address) -@cliutils.arg('node', metavar='', help=_("ID of node")) +@utils.arg('node', metavar='', help=_("ID of node")) def do_baremetal_interface_list(cs, args): """List network interfaces associated with a baremetal node.""" interfaces = cs.baremetal.list_interfaces(args.node) diff --git a/novaclient/v2/contrib/cells.py b/novaclient/v2/contrib/cells.py index 7099a5a00..e5de8e5ee 100644 --- a/novaclient/v2/contrib/cells.py +++ b/novaclient/v2/contrib/cells.py @@ -15,7 +15,6 @@ from novaclient import base from novaclient.i18n import _ -from novaclient.openstack.common import cliutils from novaclient import utils @@ -47,7 +46,7 @@ def capacities(self, cell_name=None): return self._get("/os-cells/%s" % path, "cell") -@cliutils.arg( +@utils.arg( 'cell', metavar='', help=_('Name of the cell.')) @@ -57,7 +56,7 @@ def do_cell_show(cs, args): utils.print_dict(cell._info) -@cliutils.arg( +@utils.arg( '--cell', metavar='', help=_("Name of the cell to get the capacities."), diff --git a/novaclient/v2/contrib/deferred_delete.py b/novaclient/v2/contrib/deferred_delete.py index 35e24d0d4..3d49104aa 100644 --- a/novaclient/v2/contrib/deferred_delete.py +++ b/novaclient/v2/contrib/deferred_delete.py @@ -12,17 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. -from novaclient.openstack.common import cliutils from novaclient import utils -@cliutils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('server', metavar='', help='Name or ID of server.') def do_force_delete(cs, args): """Force delete a server.""" utils.find_resource(cs.servers, args.server).force_delete() -@cliutils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('server', metavar='', help='Name or ID of server.') def do_restore(cs, args): """Restore a soft-deleted server.""" utils.find_resource(cs.servers, args.server, deleted=True).restore() diff --git a/novaclient/v2/contrib/host_evacuate.py b/novaclient/v2/contrib/host_evacuate.py index b08e28cd1..8b1016378 100644 --- a/novaclient/v2/contrib/host_evacuate.py +++ b/novaclient/v2/contrib/host_evacuate.py @@ -15,7 +15,6 @@ from novaclient import base from novaclient.i18n import _ -from novaclient.openstack.common import cliutils from novaclient import utils @@ -38,14 +37,14 @@ def _server_evacuate(cs, server, args): "error_message": error_message}) -@cliutils.arg('host', metavar='', help='Name of host.') -@cliutils.arg( +@utils.arg('host', metavar='', help='Name of host.') +@utils.arg( '--target_host', metavar='', default=None, help=_('Name of target host. If no host is specified the scheduler will ' 'select a target.')) -@cliutils.arg( +@utils.arg( '--on-shared-storage', dest='on_shared_storage', action="store_true", diff --git a/novaclient/v2/contrib/host_evacuate_live.py b/novaclient/v2/contrib/host_evacuate_live.py index f91599bbd..794c3a8fa 100644 --- a/novaclient/v2/contrib/host_evacuate_live.py +++ b/novaclient/v2/contrib/host_evacuate_live.py @@ -14,7 +14,6 @@ # under the License. from novaclient.i18n import _ -from novaclient.openstack.common import cliutils from novaclient import utils @@ -38,23 +37,23 @@ def __init__(self, server_uuid, live_migration_accepted, error_message) -@cliutils.arg('host', metavar='', help='Name of host.') -@cliutils.arg( +@utils.arg('host', metavar='', help='Name of host.') +@utils.arg( '--target-host', metavar='', default=None, help=_('Name of target host.')) -@cliutils.arg( +@utils.arg( '--block-migrate', action='store_true', default=False, help=_('Enable block migration.')) -@cliutils.arg( +@utils.arg( '--disk-over-commit', action='store_true', default=False, help=_('Enable disk overcommit.')) -@cliutils.arg( +@utils.arg( '--max-servers', type=int, dest='max_servers', diff --git a/novaclient/v2/contrib/host_servers_migrate.py b/novaclient/v2/contrib/host_servers_migrate.py index 470f161f1..95268186b 100644 --- a/novaclient/v2/contrib/host_servers_migrate.py +++ b/novaclient/v2/contrib/host_servers_migrate.py @@ -15,7 +15,6 @@ from novaclient import base from novaclient.i18n import _ -from novaclient.openstack.common import cliutils from novaclient import utils @@ -37,7 +36,7 @@ def _server_migrate(cs, server): "error_message": error_message}) -@cliutils.arg('host', metavar='', help='Name of host.') +@utils.arg('host', metavar='', help='Name of host.') def do_host_servers_migrate(cs, args): """Migrate all instances of the specified host to other available hosts.""" hypervisors = cs.hypervisors.search(args.host, servers=True) diff --git a/novaclient/v2/contrib/instance_action.py b/novaclient/v2/contrib/instance_action.py index 99f11a750..d5bd4762b 100644 --- a/novaclient/v2/contrib/instance_action.py +++ b/novaclient/v2/contrib/instance_action.py @@ -21,7 +21,6 @@ from novaclient import base from novaclient import exceptions from novaclient.i18n import _ -from novaclient.openstack.common import cliutils from novaclient import utils @@ -62,18 +61,18 @@ def _find_server(cs, args): return args.server -@cliutils.arg( +@utils.arg( 'server', metavar='', help=_('Name or UUID of the server to show actions for.'), start_version="2.0", end_version="2.20") -@cliutils.arg( +@utils.arg( 'server', metavar='', help=_('Name or UUID of the server to show actions for. Only UUID can be ' 'used to show actions for a deleted server.'), start_version="2.21") -@cliutils.arg( +@utils.arg( 'request_id', metavar='', help=_('Request ID of the action to get.')) @@ -87,12 +86,12 @@ def do_instance_action(cs, args): utils.print_dict(action) -@cliutils.arg( +@utils.arg( 'server', metavar='', help=_('Name or UUID of the server to list actions for.'), start_version="2.0", end_version="2.20") -@cliutils.arg( +@utils.arg( 'server', metavar='', help=_('Name or UUID of the server to list actions for. Only UUID can be ' diff --git a/novaclient/v2/contrib/metadata_extensions.py b/novaclient/v2/contrib/metadata_extensions.py index ad843b746..8d2d22f89 100644 --- a/novaclient/v2/contrib/metadata_extensions.py +++ b/novaclient/v2/contrib/metadata_extensions.py @@ -14,20 +14,20 @@ # under the License. from novaclient.i18n import _ -from novaclient.openstack.common import cliutils +from novaclient import utils from novaclient.v2 import shell -@cliutils.arg( +@utils.arg( 'host', metavar='', help=_('Name of host.')) -@cliutils.arg( +@utils.arg( 'action', metavar='', choices=['set', 'delete'], help=_("Actions: 'set' or 'delete'")) -@cliutils.arg( +@utils.arg( 'metadata', metavar='', nargs='+', diff --git a/novaclient/v2/contrib/migrations.py b/novaclient/v2/contrib/migrations.py index 6af813182..47856444d 100644 --- a/novaclient/v2/contrib/migrations.py +++ b/novaclient/v2/contrib/migrations.py @@ -19,7 +19,6 @@ from novaclient import api_versions from novaclient import base from novaclient.i18n import _ -from novaclient.openstack.common import cliutils from novaclient import utils @@ -55,17 +54,17 @@ def list(self, host=None, status=None, cell_name=None): return self._list("/os-migrations%s" % query_string, "migrations") -@cliutils.arg( +@utils.arg( '--host', dest='host', metavar='', help=_('Fetch migrations for the given host.')) -@cliutils.arg( +@utils.arg( '--status', dest='status', metavar='', help=_('Fetch migrations for the given status.')) -@cliutils.arg( +@utils.arg( '--cell_name', dest='cell_name', metavar='', diff --git a/novaclient/v2/contrib/tenant_networks.py b/novaclient/v2/contrib/tenant_networks.py index e124cbec3..56c989abc 100644 --- a/novaclient/v2/contrib/tenant_networks.py +++ b/novaclient/v2/contrib/tenant_networks.py @@ -14,7 +14,6 @@ from novaclient import base from novaclient.i18n import _ -from novaclient.openstack.common import cliutils from novaclient import utils @@ -52,7 +51,7 @@ def create(self, label, cidr): return self._create('/os-tenant-networks', body, 'network') -@cliutils.arg('network_id', metavar='', help='ID of network') +@utils.arg('network_id', metavar='', help='ID of network') def do_net(cs, args): """ DEPRECATED, use tenant-network-show instead. @@ -60,7 +59,7 @@ def do_net(cs, args): do_tenant_network_show(cs, args) -@cliutils.arg('network_id', metavar='', help='ID of network') +@utils.arg('network_id', metavar='', help='ID of network') def do_tenant_network_show(cs, args): """ Show a tenant network. @@ -84,11 +83,11 @@ def do_tenant_network_list(cs, args): utils.print_list(networks, ['ID', 'Label', 'CIDR']) -@cliutils.arg( +@utils.arg( 'label', metavar='', help=_('Network label (ex. my_new_network)')) -@cliutils.arg( +@utils.arg( 'cidr', metavar='', help=_('IP block to allocate from (ex. 172.16.0.0/24 or 2001:DB8::/64)')) @@ -99,11 +98,11 @@ def do_net_create(cs, args): do_tenant_network_create(cs, args) -@cliutils.arg( +@utils.arg( 'label', metavar='', help=_('Network label (ex. my_new_network)')) -@cliutils.arg( +@utils.arg( 'cidr', metavar='', help=_('IP block to allocate from (ex. 172.16.0.0/24 or 2001:DB8::/64)')) @@ -115,7 +114,7 @@ def do_tenant_network_create(cs, args): utils.print_dict(network._info) -@cliutils.arg('network_id', metavar='', help='ID of network') +@utils.arg('network_id', metavar='', help='ID of network') def do_net_delete(cs, args): """ DEPRECATED, use tenant-network-delete instead. @@ -123,7 +122,7 @@ def do_net_delete(cs, args): do_tenant_network_delete(cs, args) -@cliutils.arg('network_id', metavar='', help='ID of network') +@utils.arg('network_id', metavar='', help='ID of network') def do_tenant_network_delete(cs, args): """ Delete a tenant network. From a787d33eb1d1bb5eb04858938ba9785f3767cd4c Mon Sep 17 00:00:00 2001 From: Kevin_Zheng Date: Wed, 2 Mar 2016 11:05:08 +0800 Subject: [PATCH 1003/1705] Use novaclient/utils directly and remove openstack/common (2/4) We can use novaclient/utils directly and get rid of openstack/common folder. This is the second part. Change-Id: I12b03aa0a13c95ae949adf7e876c675ce309bae5 Partial-Bug: #1551603 --- novaclient/v2/shell.py | 330 ++++++++++++++++++++--------------------- 1 file changed, 165 insertions(+), 165 deletions(-) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 977364882..8136f5c20 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -356,34 +356,34 @@ def _boot(cs, args): return boot_args, boot_kwargs -@cliutils.arg( +@utils.arg( '--flavor', default=None, metavar='', help=_("Name or ID of flavor (see 'nova flavor-list').")) -@cliutils.arg( +@utils.arg( '--image', default=None, metavar='', help=_("Name or ID of image (see 'nova image-list'). ")) -@cliutils.arg( +@utils.arg( '--image-with', default=[], type=_key_value_pairing, action='append', metavar='', help=_("Image metadata property (see 'nova image-show'). ")) -@cliutils.arg( +@utils.arg( '--boot-volume', default=None, metavar="", help=_("Volume ID to boot from.")) -@cliutils.arg( +@utils.arg( '--snapshot', default=None, metavar="", help=_("Snapshot ID to boot from (will create a volume).")) -@cliutils.arg( +@utils.arg( '--num-instances', default=None, type=int, @@ -392,26 +392,26 @@ def _boot(cs, args): use=_('use "--min-count" and "--max-count"; this option will be removed ' 'in novaclient 3.3.0.'), help=argparse.SUPPRESS) -@cliutils.arg( +@utils.arg( '--min-count', default=None, type=int, metavar='', help=_("Boot at least servers (limited by quota).")) -@cliutils.arg( +@utils.arg( '--max-count', default=None, type=int, metavar='', help=_("Boot up to servers (limited by quota).")) -@cliutils.arg( +@utils.arg( '--meta', metavar="", action='append', default=[], help=_("Record arbitrary key/value metadata to /meta_data.json " "on the metadata server. Can be specified multiple times.")) -@cliutils.arg( +@utils.arg( '--file', metavar="", action='append', @@ -419,67 +419,67 @@ def _boot(cs, args): default=[], help=_("Store arbitrary files from locally to " "on the new server. Limited by the injected_files quota value.")) -@cliutils.arg( +@utils.arg( '--key-name', default=os.environ.get('NOVACLIENT_DEFAULT_KEY_NAME'), metavar='', help=_("Key name of keypair that should be created earlier with \ the command keypair-add.")) -@cliutils.arg( +@utils.arg( '--key_name', action=shell.DeprecatedAction, use=_('use "%s"; this option will be removed in ' 'novaclient 3.3.0.') % '--key-name', help=argparse.SUPPRESS) -@cliutils.arg('name', metavar='', help=_('Name for the new server.')) -@cliutils.arg( +@utils.arg('name', metavar='', help=_('Name for the new server.')) +@utils.arg( '--user-data', default=None, metavar='', help=_("user data file to pass to be exposed by the metadata server.")) -@cliutils.arg( +@utils.arg( '--user_data', action=shell.DeprecatedAction, use=_('use "%s"; this option will be removed in ' 'novaclient 3.3.0.') % '--user-data', help=argparse.SUPPRESS) -@cliutils.arg( +@utils.arg( '--availability-zone', default=None, metavar='', help=_("The availability zone for server placement.")) -@cliutils.arg( +@utils.arg( '--availability_zone', action=shell.DeprecatedAction, use=_('use "%s"; this option will be removed in ' 'novaclient 3.3.0.') % '--availability-zone', help=argparse.SUPPRESS) -@cliutils.arg( +@utils.arg( '--security-groups', default=None, metavar='', help=_("Comma separated list of security group names.")) -@cliutils.arg( +@utils.arg( '--security_groups', action=shell.DeprecatedAction, use=_('use "%s"; this option will be removed in ' 'novaclient 3.3.0.') % '--security-groups', help=argparse.SUPPRESS) -@cliutils.arg( +@utils.arg( '--block-device-mapping', metavar="", action='append', default=[], help=_("Block device mapping in the format " "=:::.")) -@cliutils.arg( +@utils.arg( '--block_device_mapping', real_action='append', action=shell.DeprecatedAction, use=_('use "%s"; this option will be removed in ' 'novaclient 3.3.0.') % '--block-device-mapping', help=argparse.SUPPRESS) -@cliutils.arg( +@utils.arg( '--block-device', metavar="key1=value1[,key2=value2...]", action='append', @@ -506,19 +506,19 @@ def _boot(cs, args): "for others need to be specified) and " "shutdown=shutdown behaviour (either preserve or remove, " "for local destination set to remove).")) -@cliutils.arg( +@utils.arg( '--swap', metavar="", default=None, help=_("Create and attach a local swap block device of MB.")) -@cliutils.arg( +@utils.arg( '--ephemeral', metavar="size=[,format=]", action='append', default=[], help=_("Create and attach a local ephemeral block device of GB " "and format it to .")) -@cliutils.arg( +@utils.arg( '--hint', action='append', dest='scheduler_hints', @@ -526,7 +526,7 @@ def _boot(cs, args): metavar='', help=_("Send arbitrary key/value pairs to the scheduler for custom " "use.")) -@cliutils.arg( +@utils.arg( '--nic', metavar="", @@ -542,37 +542,37 @@ def _boot(cs, args): "v6-fixed-ip: IPv6 fixed address for NIC (optional), " "port-id: attach NIC to port with this UUID " "(either port-id or net-id must be provided).")) -@cliutils.arg( +@utils.arg( '--config-drive', metavar="", dest='config_drive', default=False, help=_("Enable config drive.")) -@cliutils.arg( +@utils.arg( '--poll', dest='poll', action="store_true", default=False, help=_('Report the new server boot progress until it completes.')) -@cliutils.arg( +@utils.arg( '--admin-pass', dest='admin_pass', metavar='', default=None, help=_('Admin password for the instance.')) -@cliutils.arg( +@utils.arg( '--access-ip-v4', dest='access_ip_v4', metavar='', default=None, help=_('Alternative access IPv4 of the instance.')) -@cliutils.arg( +@utils.arg( '--access-ip-v6', dest='access_ip_v6', metavar='', default=None, help=_('Alternative access IPv6 of the instance.')) -@cliutils.arg( +@utils.arg( '--description', metavar='', dest='description', @@ -600,7 +600,7 @@ def do_cloudpipe_list(cs, _args): utils.print_list(cloudpipes, columns) -@cliutils.arg( +@utils.arg( 'project', metavar='', help=_('UUID of the project to create the cloudpipe for.')) @@ -609,8 +609,8 @@ def do_cloudpipe_create(cs, args): cs.cloudpipe.create(args.project) -@cliutils.arg('address', metavar='', help=_('New IP Address.')) -@cliutils.arg('port', metavar='', help=_('New Port.')) +@utils.arg('address', metavar='', help=_('New IP Address.')) +@utils.arg('port', metavar='', help=_('New Port.')) def do_cloudpipe_configure(cs, args): """Update the VPN IP/port of a cloudpipe instance.""" cs.cloudpipe.update(args.address, args.port) @@ -731,26 +731,26 @@ def _print_flavor_list(flavors, show_extra_specs=False): utils.print_list(flavors, headers, formatters) -@cliutils.arg( +@utils.arg( '--extra-specs', dest='extra_specs', action='store_true', default=False, help=_('Get extra-specs of each flavor.')) -@cliutils.arg( +@utils.arg( '--all', dest='all', action='store_true', default=False, help=_('Display all flavors (Admin only).')) -@cliutils.arg( +@utils.arg( '--marker', dest='marker', metavar='', default=None, help=_('The last flavor ID of the previous page; displays list of flavors' ' after "marker".')) -@cliutils.arg( +@utils.arg( '--limit', dest='limit', metavar='', @@ -769,7 +769,7 @@ def do_flavor_list(cs, args): _print_flavor_list(flavors, args.extra_specs) -@cliutils.arg( +@utils.arg( 'flavor', metavar='', help=_("Name or ID of the flavor to delete.")) @@ -780,7 +780,7 @@ def do_flavor_delete(cs, args): _print_flavor_list([flavorid]) -@cliutils.arg( +@utils.arg( 'flavor', metavar='', help=_("Name or ID of flavor.")) @@ -790,43 +790,43 @@ def do_flavor_show(cs, args): _print_flavor(flavor) -@cliutils.arg( +@utils.arg( 'name', metavar='', help=_("Unique name of the new flavor.")) -@cliutils.arg( +@utils.arg( 'id', metavar='', help=_("Unique ID of the new flavor." " Specifying 'auto' will generated a UUID for the ID.")) -@cliutils.arg( +@utils.arg( 'ram', metavar='', help=_("Memory size in MB.")) -@cliutils.arg( +@utils.arg( 'disk', metavar='', help=_("Disk size in GB.")) -@cliutils.arg( +@utils.arg( '--ephemeral', metavar='', help=_("Ephemeral space size in GB (default 0)."), default=0) -@cliutils.arg( +@utils.arg( 'vcpus', metavar='', help=_("Number of vcpus")) -@cliutils.arg( +@utils.arg( '--swap', metavar='', help=_("Swap space size in MB (default 0)."), default=0) -@cliutils.arg( +@utils.arg( '--rxtx-factor', metavar='', help=_("RX/TX factor (default 1)."), default=1.0) -@cliutils.arg( +@utils.arg( '--is-public', metavar='', help=_("Make flavor accessible to the public (default true)."), @@ -840,16 +840,16 @@ def do_flavor_create(cs, args): _print_flavor_list([f]) -@cliutils.arg( +@utils.arg( 'flavor', metavar='', help=_("Name or ID of flavor.")) -@cliutils.arg( +@utils.arg( 'action', metavar='', choices=['set', 'unset'], help=_("Actions: 'set' or 'unset'.")) -@cliutils.arg( +@utils.arg( 'metadata', metavar='', nargs='+', @@ -867,11 +867,11 @@ def do_flavor_key(cs, args): flavor.unset_keys(keypair.keys()) -@cliutils.arg( +@utils.arg( '--flavor', metavar='', help=_("Filter results by flavor name or ID.")) -@cliutils.arg( +@utils.arg( '--tenant', metavar='', help=_('Filter results by tenant ID.')) def do_flavor_access_list(cs, args): @@ -900,11 +900,11 @@ def do_flavor_access_list(cs, args): utils.print_list(access_list, columns) -@cliutils.arg( +@utils.arg( 'flavor', metavar='', help=_("Flavor name or ID to add access for the given tenant.")) -@cliutils.arg( +@utils.arg( 'tenant', metavar='', help=_('Tenant ID to add flavor access for.')) def do_flavor_access_add(cs, args): @@ -915,11 +915,11 @@ def do_flavor_access_add(cs, args): utils.print_list(access_list, columns) -@cliutils.arg( +@utils.arg( 'flavor', metavar='', help=_("Flavor name or ID to remove access for the given tenant.")) -@cliutils.arg( +@utils.arg( 'tenant', metavar='', help=_('Tenant ID to remove flavor access for.')) def do_flavor_access_remove(cs, args): @@ -930,7 +930,7 @@ def do_flavor_access_remove(cs, args): utils.print_list(access_list, columns) -@cliutils.arg( +@utils.arg( 'project_id', metavar='', help=_('The ID of the project.')) def do_scrub(cs, args): @@ -948,7 +948,7 @@ def do_scrub(cs, args): cs.security_groups.delete(group) -@cliutils.arg( +@utils.arg( '--fields', default=None, metavar='', @@ -979,7 +979,7 @@ def do_network_list(cs, args): utils.print_list(network_list, columns) -@cliutils.arg( +@utils.arg( 'network', metavar='', help=_("UUID or label of network.")) @@ -989,7 +989,7 @@ def do_network_show(cs, args): utils.print_dict(network._info) -@cliutils.arg( +@utils.arg( 'network', metavar='', help=_("UUID or label of network.")) @@ -999,7 +999,7 @@ def do_network_delete(cs, args): network.delete() -@cliutils.arg( +@utils.arg( '--host-only', dest='host_only', metavar='<0|1>', @@ -1007,7 +1007,7 @@ def do_network_delete(cs, args): type=int, const=1, default=0) -@cliutils.arg( +@utils.arg( '--project-only', dest='project_only', metavar='<0|1>', @@ -1015,7 +1015,7 @@ def do_network_delete(cs, args): type=int, const=1, default=0) -@cliutils.arg( +@utils.arg( 'network', metavar='', help=_("UUID of network.")) @@ -1029,11 +1029,11 @@ def do_network_disassociate(cs, args): cs.networks.disassociate(args.network, True, True) -@cliutils.arg( +@utils.arg( 'network', metavar='', help=_("UUID of network.")) -@cliutils.arg( +@utils.arg( 'host', metavar='', help=_("Name of host")) @@ -1042,7 +1042,7 @@ def do_network_associate_host(cs, args): cs.networks.associate_host(args.network, args.host) -@cliutils.arg( +@utils.arg( 'network', metavar='', help=_("UUID of network.")) @@ -1065,114 +1065,114 @@ def _filter_network_create_options(args): return kwargs -@cliutils.arg( +@utils.arg( 'label', metavar='', help=_("Label for network")) -@cliutils.arg( +@utils.arg( '--fixed-range-v4', dest='cidr', metavar='', help=_("IPv4 subnet (ex: 10.0.0.0/8)")) -@cliutils.arg( +@utils.arg( '--fixed-range-v6', dest="cidr_v6", help=_('IPv6 subnet (ex: fe80::/64')) -@cliutils.arg( +@utils.arg( '--vlan', dest='vlan', type=int, metavar='', help=_("The vlan ID to be assigned to the project.")) -@cliutils.arg( +@utils.arg( '--vlan-start', dest='vlan_start', type=int, metavar='', help=_('First vlan ID to be assigned to the project. Subsequent vlan ' 'IDs will be assigned incrementally.')) -@cliutils.arg( +@utils.arg( '--vpn', dest='vpn_start', type=int, metavar='', help=_("vpn start")) -@cliutils.arg( +@utils.arg( '--gateway', dest="gateway", help=_('gateway')) -@cliutils.arg( +@utils.arg( '--gateway-v6', dest="gateway_v6", help=_('IPv6 gateway')) -@cliutils.arg( +@utils.arg( '--bridge', dest="bridge", metavar='', help=_('VIFs on this network are connected to this bridge.')) -@cliutils.arg( +@utils.arg( '--bridge-interface', dest="bridge_interface", metavar='', help=_('The bridge is connected to this interface.')) -@cliutils.arg( +@utils.arg( '--multi-host', dest="multi_host", metavar="<'T'|'F'>", help=_('Multi host')) -@cliutils.arg( +@utils.arg( '--dns1', dest="dns1", metavar="", help=_('First DNS.')) -@cliutils.arg( +@utils.arg( '--dns2', dest="dns2", metavar="", help=_('Second DNS.')) -@cliutils.arg( +@utils.arg( '--uuid', dest="uuid", metavar="", help=_('Network UUID.')) -@cliutils.arg( +@utils.arg( '--fixed-cidr', dest="fixed_cidr", metavar='', help=_('IPv4 subnet for fixed IPs (ex: 10.20.0.0/16).')) -@cliutils.arg( +@utils.arg( '--project-id', dest="project_id", metavar="", help=_('Project ID.')) -@cliutils.arg( +@utils.arg( '--priority', dest="priority", metavar="", help=_('Network interface priority.')) -@cliutils.arg( +@utils.arg( '--mtu', dest="mtu", type=int, help=_('MTU for network.')) -@cliutils.arg( +@utils.arg( '--enable-dhcp', dest="enable_dhcp", metavar="<'T'|'F'>", help=_('Enable DHCP.')) -@cliutils.arg( +@utils.arg( '--dhcp-server', dest="dhcp_server", help=_('DHCP-server address (defaults to gateway address)')) -@cliutils.arg( +@utils.arg( '--share-address', dest="share_address", metavar="<'T'|'F'>", help=_('Share address')) -@cliutils.arg( +@utils.arg( '--allowed-start', dest="allowed_start", help=_('Start of allowed addresses for instances.')) -@cliutils.arg( +@utils.arg( '--allowed-end', dest="allowed_end", help=_('End of allowed addresses for instances.')) @@ -1198,7 +1198,7 @@ def do_network_create(cs, args): cs.networks.create(**kwargs) -@cliutils.arg( +@utils.arg( '--limit', dest="limit", metavar="", @@ -1219,16 +1219,16 @@ def parse_server_name(image): fmts, sortby_index=1) -@cliutils.arg( +@utils.arg( 'image', metavar='', help=_("Name or ID of image.")) -@cliutils.arg( +@utils.arg( 'action', metavar='', choices=['set', 'delete'], help=_("Actions: 'set' or 'delete'.")) -@cliutils.arg( +@utils.arg( 'metadata', metavar='', nargs='+', @@ -1295,7 +1295,7 @@ def _print_flavor(flavor): utils.print_dict(info) -@cliutils.arg( +@utils.arg( 'image', metavar='', help=_("Name or ID of image.")) @@ -1305,7 +1305,7 @@ def do_image_show(cs, args): _print_image(image) -@cliutils.arg( +@utils.arg( 'image', metavar='', nargs='+', help=_('Name or ID of image(s).')) def do_image_delete(cs, args): @@ -1318,74 +1318,74 @@ def do_image_delete(cs, args): {'image': image, 'e': e}) -@cliutils.arg( +@utils.arg( '--reservation-id', dest='reservation_id', metavar='', default=None, help=_('Only return servers that match reservation-id.')) -@cliutils.arg( +@utils.arg( '--reservation_id', action=shell.DeprecatedAction, use=_('use "%s"; this option will be removed in ' 'novaclient 3.3.0.') % '--reservation-id', help=argparse.SUPPRESS) -@cliutils.arg( +@utils.arg( '--ip', dest='ip', metavar='', default=None, help=_('Search with regular expression match by IP address.')) -@cliutils.arg( +@utils.arg( '--ip6', dest='ip6', metavar='', default=None, help=_('Search with regular expression match by IPv6 address.')) -@cliutils.arg( +@utils.arg( '--name', dest='name', metavar='', default=None, help=_('Search with regular expression match by name.')) -@cliutils.arg( +@utils.arg( '--instance-name', dest='instance_name', metavar='', default=None, help=_('Search with regular expression match by server name.')) -@cliutils.arg( +@utils.arg( '--instance_name', action=shell.DeprecatedAction, use=_('use "%s"; this option will be removed in ' 'novaclient 3.3.0.') % '--instance-name', help=argparse.SUPPRESS) -@cliutils.arg( +@utils.arg( '--status', dest='status', metavar='', default=None, help=_('Search by server status.')) -@cliutils.arg( +@utils.arg( '--flavor', dest='flavor', metavar='', default=None, help=_('Search by flavor name or ID.')) -@cliutils.arg( +@utils.arg( '--image', dest='image', metavar='', default=None, help=_('Search by image name or ID.')) -@cliutils.arg( +@utils.arg( '--host', dest='host', metavar='', default=None, help=_('Search servers by hostname to which they are assigned (Admin ' 'only).')) -@cliutils.arg( +@utils.arg( '--all-tenants', dest='all_tenants', metavar='<0|1>', @@ -1395,7 +1395,7 @@ def do_image_delete(cs, args): default=int(strutils.bool_from_string( os.environ.get("ALL_TENANTS", 'false'), True)), help=_('Display information from all tenants (Admin only).')) -@cliutils.arg( +@utils.arg( '--all_tenants', nargs='?', type=int, @@ -1404,52 +1404,52 @@ def do_image_delete(cs, args): use=_('use "%s"; this option will be removed in ' 'novaclient 3.3.0.') % '--all-tenants', help=argparse.SUPPRESS) -@cliutils.arg( +@utils.arg( '--tenant', # nova db searches by project_id dest='tenant', metavar='', nargs='?', help=_('Display information from single tenant (Admin only).')) -@cliutils.arg( +@utils.arg( '--user', dest='user', metavar='', nargs='?', help=_('Display information from single user (Admin only).')) -@cliutils.arg( +@utils.arg( '--deleted', dest='deleted', action="store_true", default=False, help=_('Only display deleted servers (Admin only).')) -@cliutils.arg( +@utils.arg( '--fields', default=None, metavar='', help=_('Comma-separated list of fields to display. ' 'Use the show command to see which fields are available.')) -@cliutils.arg( +@utils.arg( '--minimal', dest='minimal', action="store_true", default=False, help=_('Get only UUID and name.')) -@cliutils.arg( +@utils.arg( '--sort', dest='sort', metavar='[:]', help=_('Comma-separated list of sort keys and directions in the form ' 'of [:]. The direction defaults to descending if ' 'not specified.')) -@cliutils.arg( +@utils.arg( '--marker', dest='marker', metavar='', default=None, help=_('The last server UUID of the previous page; displays list of ' 'servers after "marker".')) -@cliutils.arg( +@utils.arg( '--limit', dest='limit', metavar='', @@ -1563,18 +1563,18 @@ def do_list(cs, args): formatters, sortby_index=sortby_index) -@cliutils.arg( +@utils.arg( '--hard', dest='reboot_type', action='store_const', const=servers.REBOOT_HARD, default=servers.REBOOT_SOFT, help=_('Perform a hard reboot (instead of a soft one).')) -@cliutils.arg( +@utils.arg( 'server', metavar='', nargs='+', help=_('Name or ID of server(s).')) -@cliutils.arg( +@utils.arg( '--poll', dest='poll', action="store_true", @@ -1598,57 +1598,57 @@ def do_reboot(cs, args): _("Wait for specified server(s) failed.")) -@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) -@cliutils.arg('image', metavar='', help=_("Name or ID of new image.")) -@cliutils.arg( +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('image', metavar='', help=_("Name or ID of new image.")) +@utils.arg( '--rebuild-password', dest='rebuild_password', metavar='', default=False, help=_("Set the provided admin password on the rebuilt server.")) -@cliutils.arg( +@utils.arg( '--rebuild_password', action=shell.DeprecatedAction, use=_('use "%s"; this option will be removed in ' 'novaclient 3.3.0.') % '--rebuild-password', help=argparse.SUPPRESS) -@cliutils.arg( +@utils.arg( '--poll', dest='poll', action="store_true", default=False, help=_('Report the server rebuild progress until it completes.')) -@cliutils.arg( +@utils.arg( '--minimal', dest='minimal', action="store_true", default=False, help=_('Skips flavor/image lookups when showing servers.')) -@cliutils.arg( +@utils.arg( '--preserve-ephemeral', action="store_true", default=False, help=_('Preserve the default ephemeral storage partition on rebuild.')) -@cliutils.arg( +@utils.arg( '--name', metavar='', default=None, help=_('Name for the new server.')) -@cliutils.arg( +@utils.arg( '--description', metavar='', dest='description', default=None, help=_('New description for the server.'), start_version="2.19") -@cliutils.arg( +@utils.arg( '--meta', metavar="", action='append', default=[], help=_("Record arbitrary key/value metadata to /meta_data.json " "on the metadata server. Can be specified multiple times.")) -@cliutils.arg( +@utils.arg( '--file', metavar="", action='append', @@ -1696,25 +1696,25 @@ def do_rebuild(cs, args): _poll_for_status(cs.servers.get, server.id, 'rebuilding', ['active']) -@cliutils.arg( +@utils.arg( 'server', metavar='', help=_('Name (old name) or ID of server.')) -@cliutils.arg('name', metavar='', help=_('New name for the server.')) +@utils.arg('name', metavar='', help=_('New name for the server.')) def do_rename(cs, args): """DEPRECATED, use update instead.""" do_update(cs, args) -@cliutils.arg( +@utils.arg( 'server', metavar='', help=_('Name (old name) or ID of server.')) -@cliutils.arg( +@utils.arg( '--name', metavar='', dest='name', default=None, help=_('New name for the server.')) -@cliutils.arg( +@utils.arg( '--description', metavar='', dest='description', @@ -1736,12 +1736,12 @@ def do_update(cs, args): _find_server(cs, args.server).update(**update_kwargs) -@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) -@cliutils.arg( +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg( 'flavor', metavar='', help=_("Name or ID of new flavor.")) -@cliutils.arg( +@utils.arg( '--poll', dest='poll', action="store_true", @@ -1758,20 +1758,20 @@ def do_resize(cs, args): ['active', 'verify_resize']) -@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('server', metavar='', help=_('Name or ID of server.')) def do_resize_confirm(cs, args): """Confirm a previous resize.""" _find_server(cs, args.server).confirm_resize() -@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('server', metavar='', help=_('Name or ID of server.')) def do_resize_revert(cs, args): """Revert a previous resize (and return to the previous VM).""" _find_server(cs, args.server).revert_resize() -@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) -@cliutils.arg( +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg( '--poll', dest='poll', action="store_true", @@ -1787,25 +1787,25 @@ def do_migrate(cs, args): ['active', 'verify_resize']) -@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('server', metavar='', help=_('Name or ID of server.')) def do_pause(cs, args): """Pause a server.""" _find_server(cs, args.server).pause() -@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('server', metavar='', help=_('Name or ID of server.')) def do_unpause(cs, args): """Unpause a server.""" _find_server(cs, args.server).unpause() -@cliutils.arg( +@utils.arg( '--all-tenants', action='store_const', const=1, default=0, help=_('Stop server(s) in another tenant by name (Admin only).')) -@cliutils.arg( +@utils.arg( 'server', metavar='', nargs='+', help=_('Name or ID of server(s).')) @@ -1819,13 +1819,13 @@ def do_stop(cs, args): _("Unable to stop the specified server(s).")) -@cliutils.arg( +@utils.arg( '--all-tenants', action='store_const', const=1, default=0, help=_('Start server(s) in another tenant by name (Admin only).')) -@cliutils.arg( +@utils.arg( 'server', metavar='', nargs='+', help=_('Name or ID of server(s).')) @@ -1839,7 +1839,7 @@ def do_start(cs, args): _("Unable to start the specified server(s).")) -@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('server', metavar='', help=_('Name or ID of server.')) def do_lock(cs, args): """Lock a server. A normal (non-admin) user will not be able to execute actions on a locked server. @@ -1847,31 +1847,31 @@ def do_lock(cs, args): _find_server(cs, args.server).lock() -@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('server', metavar='', help=_('Name or ID of server.')) def do_unlock(cs, args): """Unlock a server.""" _find_server(cs, args.server).unlock() -@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('server', metavar='', help=_('Name or ID of server.')) def do_suspend(cs, args): """Suspend a server.""" _find_server(cs, args.server).suspend() -@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('server', metavar='', help=_('Name or ID of server.')) def do_resume(cs, args): """Resume a server.""" _find_server(cs, args.server).resume() -@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) -@cliutils.arg( +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg( '--password', metavar='', dest='password', help=_('The admin password to be set in the rescue environment.')) -@cliutils.arg( +@utils.arg( '--image', metavar='', dest='image', @@ -1889,38 +1889,38 @@ def do_rescue(cs, args): utils.print_dict(_find_server(cs, args.server).rescue(**kwargs)[1]) -@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('server', metavar='', help=_('Name or ID of server.')) def do_unrescue(cs, args): """Restart the server from normal boot disk again.""" _find_server(cs, args.server).unrescue() -@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('server', metavar='', help=_('Name or ID of server.')) def do_shelve(cs, args): """Shelve a server.""" _find_server(cs, args.server).shelve() -@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('server', metavar='', help=_('Name or ID of server.')) def do_shelve_offload(cs, args): """Remove a shelved server from the compute node.""" _find_server(cs, args.server).shelve_offload() -@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('server', metavar='', help=_('Name or ID of server.')) def do_unshelve(cs, args): """Unshelve a server.""" _find_server(cs, args.server).unshelve() -@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('server', metavar='', help=_('Name or ID of server.')) def do_diagnostics(cs, args): """Retrieve server diagnostics.""" server = _find_server(cs, args.server) utils.print_dict(cs.servers.diagnostics(server)[1], wrap=80) -@cliutils.arg( +@utils.arg( 'server', metavar='', help=_('Name or ID of a server for which the network cache should ' 'be refreshed from neutron (Admin only).')) @@ -1931,7 +1931,7 @@ def do_refresh_network(cs, args): 'name': 'network-changed'}]) -@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('server', metavar='', help=_('Name or ID of server.')) def do_root_password(cs, args): """DEPRECATED, use set-password instead.""" do_set_password(cs, args) From 49410c06a5e15920282b67ca7db7e84f7c23665b Mon Sep 17 00:00:00 2001 From: Kevin_Zheng Date: Wed, 2 Mar 2016 11:25:15 +0800 Subject: [PATCH 1004/1705] Use novaclient/utils directly and remove openstack/common We can use novaclient/utils directly and get rid of openstack/common folder. This is the third part. Change-Id: I405044af3912d86da66df05413edfc724bc102d0 Partial-Bug: #1551603 --- novaclient/v2/shell.py | 364 ++++++++++++++++++++--------------------- 1 file changed, 182 insertions(+), 182 deletions(-) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 8136f5c20..4a188646c 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -1937,7 +1937,7 @@ def do_root_password(cs, args): do_set_password(cs, args) -@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('server', metavar='', help=_('Name or ID of server.')) def do_set_password(cs, args): """ Change the admin password for a server. @@ -1950,22 +1950,22 @@ def do_set_password(cs, args): server.change_password(p1) -@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) -@cliutils.arg('name', metavar='', help=_('Name of snapshot.')) -@cliutils.arg( +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('name', metavar='', help=_('Name of snapshot.')) +@utils.arg( '--metadata', metavar="", action='append', default=[], help=_("Record arbitrary key/value metadata to /meta_data.json " "on the metadata server. Can be specified multiple times.")) -@cliutils.arg( +@utils.arg( '--show', dest='show', action="store_true", default=False, help=_('Print image info.')) -@cliutils.arg( +@utils.arg( '--poll', dest='poll', action="store_true", @@ -2001,12 +2001,12 @@ def do_image_create(cs, args): _print_image(cs.images.get(image_uuid)) -@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) -@cliutils.arg('name', metavar='', help=_('Name of the backup image.')) -@cliutils.arg( +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('name', metavar='', help=_('Name of the backup image.')) +@utils.arg( 'backup_type', metavar='', help=_('The backup type, like "daily" or "weekly".')) -@cliutils.arg( +@utils.arg( 'rotation', metavar='', help=_('Int parameter representing how many backups to keep ' 'around.')) @@ -2017,16 +2017,16 @@ def do_backup(cs, args): args.rotation) -@cliutils.arg( +@utils.arg( 'server', metavar='', help=_("Name or ID of server.")) -@cliutils.arg( +@utils.arg( 'action', metavar='', choices=['set', 'delete'], help=_("Actions: 'set' or 'delete'.")) -@cliutils.arg( +@utils.arg( 'metadata', metavar='', nargs='+', @@ -2098,25 +2098,25 @@ def _print_server(cs, args, server=None): utils.print_dict(info) -@cliutils.arg( +@utils.arg( '--minimal', dest='minimal', action="store_true", default=False, help=_('Skips flavor/image lookups when showing servers.')) -@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('server', metavar='', help=_('Name or ID of server.')) def do_show(cs, args): """Show details about the given server.""" _print_server(cs, args) -@cliutils.arg( +@utils.arg( '--all-tenants', action='store_const', const=1, default=0, help=_('Delete server(s) in another tenant by name (Admin only).')) -@cliutils.arg( +@utils.arg( 'server', metavar='', nargs='+', help=_('Name or ID of server(s).')) def do_delete(cs, args): @@ -2166,8 +2166,8 @@ def _find_network_id(cs, net_name): return network_id -@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) -@cliutils.arg( +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg( 'network_id', metavar='', help=_('Network ID.')) @@ -2177,8 +2177,8 @@ def do_add_fixed_ip(cs, args): server.add_fixed_ip(args.network_id) -@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) -@cliutils.arg('address', metavar='
', help=_('IP Address.')) +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('address', metavar='
', help=_('IP Address.')) def do_remove_fixed_ip(cs, args): """Remove an IP address from a server.""" server = _find_server(cs, args.server) @@ -2226,7 +2226,7 @@ def _translate_volume_attachments_keys(collection): ('volumeId', 'volume_id')]) -@cliutils.arg( +@utils.arg( '--all-tenants', dest='all_tenants', metavar='<0|1>', @@ -2236,7 +2236,7 @@ def _translate_volume_attachments_keys(collection): default=int(strutils.bool_from_string( os.environ.get("ALL_TENANTS", 'false'), True)), help=_('Display information from all tenants (Admin only).')) -@cliutils.arg( +@utils.arg( '--all_tenants', nargs='?', type=int, @@ -2260,7 +2260,7 @@ def do_volume_list(cs, args): 'Size', 'Volume Type', 'Attached to']) -@cliutils.arg( +@utils.arg( 'volume', metavar='', help=_('Name or ID of the volume.')) @@ -2271,61 +2271,61 @@ def do_volume_show(cs, args): _print_volume(volume) -@cliutils.arg( +@utils.arg( 'size', metavar='', type=int, help=_('Size of volume in GB')) -@cliutils.arg( +@utils.arg( '--snapshot-id', metavar='', default=None, help=_('Optional snapshot ID to create the volume from. (Default=None)')) -@cliutils.arg( +@utils.arg( '--snapshot_id', action=shell.DeprecatedAction, use=_('use "%s"; this option will be removed in ' 'novaclient 3.3.0.') % '--snapshot-id', help=argparse.SUPPRESS) -@cliutils.arg( +@utils.arg( '--image-id', metavar='', help=_('Optional image ID to create the volume from. (Default=None)'), default=None) -@cliutils.arg( +@utils.arg( '--display-name', metavar='', default=None, help=_('Optional volume name. (Default=None)')) -@cliutils.arg( +@utils.arg( '--display_name', action=shell.DeprecatedAction, use=_('use "%s"; this option will be removed in ' 'novaclient 3.3.0.') % '--display-name', help=argparse.SUPPRESS) -@cliutils.arg( +@utils.arg( '--display-description', metavar='', default=None, help=_('Optional volume description. (Default=None)')) -@cliutils.arg( +@utils.arg( '--display_description', action=shell.DeprecatedAction, use=_('use "%s"; this option will be removed in ' 'novaclient 3.3.0.') % '--display-description', help=argparse.SUPPRESS) -@cliutils.arg( +@utils.arg( '--volume-type', metavar='', default=None, help=_('Optional volume type. (Default=None)')) -@cliutils.arg( +@utils.arg( '--volume_type', action=shell.DeprecatedAction, use=_('use "%s"; this option will be removed in ' 'novaclient 3.3.0.') % '--volume-type', help=argparse.SUPPRESS) -@cliutils.arg( +@utils.arg( '--availability-zone', metavar='', help=_('Optional Availability Zone for volume. (Default=None)'), default=None) @@ -2342,7 +2342,7 @@ def do_volume_create(cs, args): _print_volume(volume) -@cliutils.arg( +@utils.arg( 'volume', metavar='', nargs='+', help=_('Name or ID of the volume(s) to delete.')) @@ -2357,15 +2357,15 @@ def do_volume_delete(cs, args): {'volume': volume, 'e': e}) -@cliutils.arg( +@utils.arg( 'server', metavar='', help=_('Name or ID of server.')) -@cliutils.arg( +@utils.arg( 'volume', metavar='', help=_('ID of the volume to attach.')) -@cliutils.arg( +@utils.arg( 'device', metavar='', default=None, nargs='?', help=_('Name of the device e.g. /dev/vdb. ' 'Use "auto" for autoassign (if supported). ' @@ -2381,15 +2381,15 @@ def do_volume_attach(cs, args): _print_volume(volume) -@cliutils.arg( +@utils.arg( 'server', metavar='', help=_('Name or ID of server.')) -@cliutils.arg( +@utils.arg( 'attachment_id', metavar='', help=_('Attachment ID of the volume.')) -@cliutils.arg( +@utils.arg( 'new_volume', metavar='', help=_('ID of the volume to attach.')) @@ -2400,11 +2400,11 @@ def do_volume_update(cs, args): args.new_volume) -@cliutils.arg( +@utils.arg( 'server', metavar='', help=_('Name or ID of server.')) -@cliutils.arg( +@utils.arg( 'attachment_id', metavar='', help=_('ID of the volume to detach.')) @@ -2414,7 +2414,7 @@ def do_volume_detach(cs, args): args.attachment_id) -@cliutils.arg( +@utils.arg( 'server', metavar='', help=_('Name or ID of server.')) @@ -2434,7 +2434,7 @@ def do_volume_snapshot_list(cs, _args): 'Size']) -@cliutils.arg( +@utils.arg( 'snapshot', metavar='', help=_('Name or ID of the snapshot.')) @@ -2445,33 +2445,33 @@ def do_volume_snapshot_show(cs, args): _print_volume_snapshot(snapshot) -@cliutils.arg( +@utils.arg( 'volume_id', metavar='', help=_('ID of the volume to snapshot')) -@cliutils.arg( +@utils.arg( '--force', metavar='', help=_('Optional flag to indicate whether to snapshot a volume even if ' 'its attached to a server. (Default=False)'), default=False) -@cliutils.arg( +@utils.arg( '--display-name', metavar='', default=None, help=_('Optional snapshot name. (Default=None)')) -@cliutils.arg( +@utils.arg( '--display_name', action=shell.DeprecatedAction, use=_('use "%s"; this option will be removed in ' 'novaclient 3.3.0.') % '--display-name', help=argparse.SUPPRESS) -@cliutils.arg( +@utils.arg( '--display-description', metavar='', default=None, help=_('Optional snapshot description. (Default=None)')) -@cliutils.arg( +@utils.arg( '--display_description', action=shell.DeprecatedAction, use=_('use "%s"; this option will be removed in ' @@ -2487,7 +2487,7 @@ def do_volume_snapshot_create(cs, args): _print_volume_snapshot(snapshot) -@cliutils.arg( +@utils.arg( 'snapshot', metavar='', help=_('Name or ID of the snapshot to delete.')) @@ -2509,7 +2509,7 @@ def do_volume_type_list(cs, args): _print_volume_type_list(vtypes) -@cliutils.arg( +@utils.arg( 'name', metavar='', help=_("Name of the new volume type")) @@ -2520,7 +2520,7 @@ def do_volume_type_create(cs, args): _print_volume_type_list([vtype]) -@cliutils.arg( +@utils.arg( 'id', metavar='', help=_("Unique ID of the volume type to delete.")) @@ -2551,8 +2551,8 @@ def print_console(cs, data): ['Type', 'Url']) -@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) -@cliutils.arg( +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg( 'console_type', metavar='', help=_('Type of vnc console ("novnc" or "xvpvnc").')) @@ -2564,8 +2564,8 @@ def do_get_vnc_console(cs, args): print_console(cs, data) -@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) -@cliutils.arg( +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg( 'console_type', metavar='', help=_('Type of spice console ("spice-html5").')) @@ -2577,8 +2577,8 @@ def do_get_spice_console(cs, args): print_console(cs, data) -@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) -@cliutils.arg( +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg( 'console_type', metavar='', help=_('Type of rdp console ("rdp-html5").')) @@ -2590,12 +2590,12 @@ def do_get_rdp_console(cs, args): print_console(cs, data) -@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) -@cliutils.arg( +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg( '--console-type', default='serial', help=_('Type of serial console, default="serial".')) -@cliutils.arg( +@utils.arg( '--console_type', default='serial', action=shell.DeprecatedAction, @@ -2616,7 +2616,7 @@ def do_get_serial_console(cs, args): @api_versions.wraps('2.8') -@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('server', metavar='', help=_('Name or ID of server.')) def do_get_mks_console(cs, args): """Get a serial console to a server.""" server = _find_server(cs, args.server) @@ -2625,8 +2625,8 @@ def do_get_mks_console(cs, args): print_console(cs, data) -@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) -@cliutils.arg( +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg( 'private_key', metavar='', help=_('Private key (used locally to decrypt password) (Optional). ' @@ -2642,7 +2642,7 @@ def do_get_password(cs, args): print(data) -@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('server', metavar='', help=_('Name or ID of server.')) def do_clear_password(cs, args): """Clear the admin password for a server.""" server = _find_server(cs, args.server) @@ -2657,8 +2657,8 @@ def _print_floating_ip_list(floating_ips): ['Id', 'IP', 'Server Id', 'Fixed IP', 'Pool']) -@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) -@cliutils.arg( +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg( '--length', metavar='', default=None, @@ -2670,9 +2670,9 @@ def do_console_log(cs, args): print(data) -@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) -@cliutils.arg('address', metavar='
', help=_('IP Address.')) -@cliutils.arg( +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('address', metavar='
', help=_('IP Address.')) +@utils.arg( '--fixed-address', metavar='', default=None, @@ -2682,9 +2682,9 @@ def do_add_floating_ip(cs, args): _associate_floating_ip(cs, args) -@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) -@cliutils.arg('address', metavar='
', help=_('IP Address.')) -@cliutils.arg( +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('address', metavar='
', help=_('IP Address.')) +@utils.arg( '--fixed-address', metavar='', default=None, @@ -2699,15 +2699,15 @@ def _associate_floating_ip(cs, args): server.add_floating_ip(args.address, args.fixed_address) -@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) -@cliutils.arg('address', metavar='
', help=_('IP Address.')) +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('address', metavar='
', help=_('IP Address.')) def do_remove_floating_ip(cs, args): """DEPRECATED, use floating-ip-disassociate instead.""" _disassociate_floating_ip(cs, args) -@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) -@cliutils.arg('address', metavar='
', help=_('IP Address.')) +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('address', metavar='
', help=_('IP Address.')) def do_floating_ip_disassociate(cs, args): """Disassociate a floating IP address from a server.""" _disassociate_floating_ip(cs, args) @@ -2718,8 +2718,8 @@ def _disassociate_floating_ip(cs, args): server.remove_floating_ip(args.address) -@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) -@cliutils.arg( +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg( 'secgroup', metavar='', help=_('Name of Security Group.')) @@ -2729,8 +2729,8 @@ def do_add_secgroup(cs, args): server.add_security_group(args.secgroup) -@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) -@cliutils.arg( +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg( 'secgroup', metavar='', help=_('Name of Security Group.')) @@ -2740,7 +2740,7 @@ def do_remove_secgroup(cs, args): server.remove_security_group(args.secgroup) -@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('server', metavar='', help=_('Name or ID of server.')) def do_list_secgroup(cs, args): """List Security Group(s) of a server.""" server = _find_server(cs, args.server) @@ -2748,7 +2748,7 @@ def do_list_secgroup(cs, args): _print_secgroups(groups) -@cliutils.arg( +@utils.arg( 'pool', metavar='', help=_('Name of Floating IP Pool. (Optional)'), @@ -2759,7 +2759,7 @@ def do_floating_ip_create(cs, args): _print_floating_ip_list([cs.floating_ips.create(pool=args.pool)]) -@cliutils.arg('address', metavar='
', help=_('IP of Floating IP.')) +@utils.arg('address', metavar='
', help=_('IP of Floating IP.')) def do_floating_ip_delete(cs, args): """De-allocate a floating IP.""" floating_ips = cs.floating_ips.list() @@ -2780,7 +2780,7 @@ def do_floating_ip_pool_list(cs, _args): utils.print_list(cs.floating_ip_pools.list(), ['name']) -@cliutils.arg( +@utils.arg( '--host', dest='host', metavar='', default=None, help=_('Filter by host.')) def do_floating_ip_bulk_list(cs, args): @@ -2792,12 +2792,12 @@ def do_floating_ip_bulk_list(cs, args): 'interface']) -@cliutils.arg('ip_range', metavar='', - help=_('Address range to create.')) -@cliutils.arg( +@utils.arg('ip_range', metavar='', + help=_('Address range to create.')) +@utils.arg( '--pool', dest='pool', metavar='', default=None, help=_('Pool for new Floating IPs.')) -@cliutils.arg( +@utils.arg( '--interface', metavar='', default=None, help=_('Interface for new Floating IPs.')) def do_floating_ip_bulk_create(cs, args): @@ -2805,8 +2805,8 @@ def do_floating_ip_bulk_create(cs, args): cs.floating_ips_bulk.create(args.ip_range, args.pool, args.interface) -@cliutils.arg('ip_range', metavar='', - help=_('Address range to delete.')) +@utils.arg('ip_range', metavar='', + help=_('Address range to delete.')) def do_floating_ip_bulk_delete(cs, args): """Bulk delete floating IPs by range (nova-network only).""" cs.floating_ips_bulk.delete(args.ip_range) @@ -2827,9 +2827,9 @@ def do_dns_domains(cs, args): _print_domain_list(domains) -@cliutils.arg('domain', metavar='', help=_('DNS domain.')) -@cliutils.arg('--ip', metavar='', help=_('IP address.'), default=None) -@cliutils.arg('--name', metavar='', help=_('DNS name.'), default=None) +@utils.arg('domain', metavar='', help=_('DNS domain.')) +@utils.arg('--ip', metavar='', help=_('IP address.'), default=None) +@utils.arg('--name', metavar='', help=_('DNS name.'), default=None) def do_dns_list(cs, args): """List current DNS entries for domain and IP or domain and name.""" if not (args.ip or args.name): @@ -2844,10 +2844,10 @@ def do_dns_list(cs, args): _print_dns_list(entries) -@cliutils.arg('ip', metavar='', help=_('IP address.')) -@cliutils.arg('name', metavar='', help=_('DNS name.')) -@cliutils.arg('domain', metavar='', help=_('DNS domain.')) -@cliutils.arg( +@utils.arg('ip', metavar='', help=_('IP address.')) +@utils.arg('name', metavar='', help=_('DNS name.')) +@utils.arg('domain', metavar='', help=_('DNS domain.')) +@utils.arg( '--type', metavar='', help=_('DNS type (e.g. "A")'), @@ -2857,27 +2857,27 @@ def do_dns_create(cs, args): cs.dns_entries.create(args.domain, args.name, args.ip, args.type) -@cliutils.arg('domain', metavar='', help=_('DNS domain.')) -@cliutils.arg('name', metavar='', help=_('DNS name.')) +@utils.arg('domain', metavar='', help=_('DNS domain.')) +@utils.arg('name', metavar='', help=_('DNS name.')) def do_dns_delete(cs, args): """Delete the specified DNS entry.""" cs.dns_entries.delete(args.domain, args.name) -@cliutils.arg('domain', metavar='', help=_('DNS domain.')) +@utils.arg('domain', metavar='', help=_('DNS domain.')) def do_dns_delete_domain(cs, args): """Delete the specified DNS domain.""" cs.dns_domains.delete(args.domain) -@cliutils.arg('domain', metavar='', help=_('DNS domain.')) -@cliutils.arg( +@utils.arg('domain', metavar='', help=_('DNS domain.')) +@utils.arg( '--availability-zone', metavar='', default=None, help=_('Limit access to this domain to servers ' 'in the specified availability zone.')) -@cliutils.arg( +@utils.arg( '--availability_zone', action=shell.DeprecatedAction, use=_('use "%s"; this option will be removed in ' @@ -2889,8 +2889,8 @@ def do_dns_create_private_domain(cs, args): args.availability_zone) -@cliutils.arg('domain', metavar='', help=_('DNS domain.')) -@cliutils.arg( +@utils.arg('domain', metavar='', help=_('DNS domain.')) +@utils.arg( '--project', metavar='', help=_('Limit access to this domain to users ' 'of the specified project.'), @@ -2955,23 +2955,23 @@ def _get_secgroup(cs, secgroup): return match_found -@cliutils.arg( +@utils.arg( 'secgroup', metavar='', help=_('ID or name of security group.')) -@cliutils.arg( +@utils.arg( 'ip_proto', metavar='', help=_('IP protocol (icmp, tcp, udp).')) -@cliutils.arg( +@utils.arg( 'from_port', metavar='', help=_('Port at start of range.')) -@cliutils.arg( +@utils.arg( 'to_port', metavar='', help=_('Port at end of range.')) -@cliutils.arg('cidr', metavar='', help=_('CIDR for address range.')) +@utils.arg('cidr', metavar='', help=_('CIDR for address range.')) def do_secgroup_add_rule(cs, args): """Add a rule to a security group.""" secgroup = _get_secgroup(cs, args.secgroup) @@ -2983,23 +2983,23 @@ def do_secgroup_add_rule(cs, args): _print_secgroup_rules([rule]) -@cliutils.arg( +@utils.arg( 'secgroup', metavar='', help=_('ID or name of security group.')) -@cliutils.arg( +@utils.arg( 'ip_proto', metavar='', help=_('IP protocol (icmp, tcp, udp).')) -@cliutils.arg( +@utils.arg( 'from_port', metavar='', help=_('Port at start of range.')) -@cliutils.arg( +@utils.arg( 'to_port', metavar='', help=_('Port at end of range.')) -@cliutils.arg('cidr', metavar='', help=_('CIDR for address range.')) +@utils.arg('cidr', metavar='', help=_('CIDR for address range.')) def do_secgroup_delete_rule(cs, args): """Delete a rule from a security group.""" secgroup = _get_secgroup(cs, args.secgroup) @@ -3015,8 +3015,8 @@ def do_secgroup_delete_rule(cs, args): raise exceptions.CommandError(_("Rule not found")) -@cliutils.arg('name', metavar='', help=_('Name of security group.')) -@cliutils.arg( +@utils.arg('name', metavar='', help=_('Name of security group.')) +@utils.arg( 'description', metavar='', help=_('Description of security group.')) def do_secgroup_create(cs, args): @@ -3025,12 +3025,12 @@ def do_secgroup_create(cs, args): _print_secgroups([secgroup]) -@cliutils.arg( +@utils.arg( 'secgroup', metavar='', help=_('ID or name of security group.')) -@cliutils.arg('name', metavar='', help=_('Name of security group.')) -@cliutils.arg( +@utils.arg('name', metavar='', help=_('Name of security group.')) +@utils.arg( 'description', metavar='', help=_('Description of security group.')) def do_secgroup_update(cs, args): @@ -3040,7 +3040,7 @@ def do_secgroup_update(cs, args): _print_secgroups([secgroup]) -@cliutils.arg( +@utils.arg( 'secgroup', metavar='', help=_('ID or name of security group.')) @@ -3051,7 +3051,7 @@ def do_secgroup_delete(cs, args): _print_secgroups([secgroup]) -@cliutils.arg( +@utils.arg( '--all-tenants', dest='all_tenants', metavar='<0|1>', @@ -3061,7 +3061,7 @@ def do_secgroup_delete(cs, args): default=int(strutils.bool_from_string( os.environ.get("ALL_TENANTS", 'false'), True)), help=_('Display information from all tenants (Admin only).')) -@cliutils.arg( +@utils.arg( '--all_tenants', nargs='?', type=int, @@ -3080,7 +3080,7 @@ def do_secgroup_list(cs, args): utils.print_list(groups, columns) -@cliutils.arg( +@utils.arg( 'secgroup', metavar='', help=_('ID or name of security group.')) @@ -3090,23 +3090,23 @@ def do_secgroup_list_rules(cs, args): _print_secgroup_rules(secgroup.rules) -@cliutils.arg( +@utils.arg( 'secgroup', metavar='', help=_('ID or name of security group.')) -@cliutils.arg( +@utils.arg( 'source_group', metavar='', help=_('ID or name of source group.')) -@cliutils.arg( +@utils.arg( 'ip_proto', metavar='', help=_('IP protocol (icmp, tcp, udp).')) -@cliutils.arg( +@utils.arg( 'from_port', metavar='', help=_('Port at start of range.')) -@cliutils.arg( +@utils.arg( 'to_port', metavar='', help=_('Port at end of range.')) @@ -3128,23 +3128,23 @@ def do_secgroup_add_group_rule(cs, args): _print_secgroup_rules([rule]) -@cliutils.arg( +@utils.arg( 'secgroup', metavar='', help=_('ID or name of security group.')) -@cliutils.arg( +@utils.arg( 'source_group', metavar='', help=_('ID or name of source group.')) -@cliutils.arg( +@utils.arg( 'ip_proto', metavar='', help=_('IP protocol (icmp, tcp, udp).')) -@cliutils.arg( +@utils.arg( 'from_port', metavar='', help=_('Port at start of range.')) -@cliutils.arg( +@utils.arg( 'to_port', metavar='', help=_('Port at end of range.')) @@ -3190,25 +3190,25 @@ def _keypair_create(cs, args, name, pub_key): user_id=args.user) -@cliutils.arg('name', metavar='', help=_('Name of key.')) -@cliutils.arg( +@utils.arg('name', metavar='', help=_('Name of key.')) +@utils.arg( '--pub-key', metavar='', default=None, help=_('Path to a public ssh key.')) -@cliutils.arg( +@utils.arg( '--pub_key', action=shell.DeprecatedAction, use=_('use "%s"; this option will be removed in ' 'novaclient 3.3.0.') % '--pub-key', help=argparse.SUPPRESS) -@cliutils.arg( +@utils.arg( '--key-type', metavar='', default='ssh', help=_('Keypair type. Can be ssh or x509.'), start_version="2.2") -@cliutils.arg( +@utils.arg( '--user', metavar='', default=None, @@ -3239,7 +3239,7 @@ def do_keypair_add(cs, args): @api_versions.wraps("2.0", "2.9") -@cliutils.arg('name', metavar='', help=_('Keypair name to delete.')) +@utils.arg('name', metavar='', help=_('Keypair name to delete.')) def do_keypair_delete(cs, args): """Delete keypair given by its name.""" name = _find_keypair(cs, args.name) @@ -3247,8 +3247,8 @@ def do_keypair_delete(cs, args): @api_versions.wraps("2.10") -@cliutils.arg('name', metavar='', help=_('Keypair name to delete.')) -@cliutils.arg( +@utils.arg('name', metavar='', help=_('Keypair name to delete.')) +@utils.arg( '--user', metavar='', default=None, @@ -3277,7 +3277,7 @@ def do_keypair_list(cs, args): @api_versions.wraps("2.10") -@cliutils.arg( +@utils.arg( '--user', metavar='', default=None, @@ -3297,7 +3297,7 @@ def _print_keypair(keypair): @api_versions.wraps("2.0", "2.9") -@cliutils.arg( +@utils.arg( 'keypair', metavar='', help=_("Name of keypair.")) @@ -3308,11 +3308,11 @@ def do_keypair_show(cs, args): @api_versions.wraps("2.10") -@cliutils.arg( +@utils.arg( 'keypair', metavar='', help=_("Name of keypair.")) -@cliutils.arg( +@utils.arg( '--user', metavar='', default=None, @@ -3328,14 +3328,14 @@ def _find_keypair(cs, keypair): return utils.find_resource(cs.keypairs, keypair) -@cliutils.arg( +@utils.arg( '--tenant', # nova db searches by project_id dest='tenant', metavar='', nargs='?', help=_('Display information from single tenant (Admin only).')) -@cliutils.arg( +@utils.arg( '--reserved', dest='reserved', action='store_true', @@ -3421,14 +3421,14 @@ def _print_rate_limits(limits): utils.print_list(limits, columns) -@cliutils.arg( +@utils.arg( '--tenant', # nova db searches by project_id dest='tenant', metavar='', nargs='?', help=_('Display information from single tenant (Admin only).')) -@cliutils.arg( +@utils.arg( '--reserved', dest='reserved', action='store_true', @@ -3441,12 +3441,12 @@ def do_limits(cs, args): _print_absolute_limits(limits.absolute) -@cliutils.arg( +@utils.arg( '--start', metavar='', help=_('Usage range start date ex 2012-01-20. (default: 4 weeks ago)'), default=None) -@cliutils.arg( +@utils.arg( '--end', metavar='', help=_('Usage range end date, ex 2012-01-20. (default: tomorrow)'), @@ -3490,16 +3490,16 @@ def simplify_usage(u): utils.print_list(usage_list, rows) -@cliutils.arg( +@utils.arg( '--start', metavar='', help=_('Usage range start date ex 2012-01-20. (default: 4 weeks ago)'), default=None) -@cliutils.arg( +@utils.arg( '--end', metavar='', help=_('Usage range end date, ex 2012-01-20. (default: tomorrow)'), default=None) -@cliutils.arg( +@utils.arg( '--tenant', metavar='', default=None, @@ -3550,13 +3550,13 @@ def simplify_usage(u): print(_('None')) -@cliutils.arg( +@utils.arg( 'pk_filename', metavar='', nargs='?', default='pk.pem', help=_('Filename for the private key. [Default: pk.pem]')) -@cliutils.arg( +@utils.arg( 'cert_filename', metavar='', nargs='?', @@ -3587,7 +3587,7 @@ def do_x509_create_cert(cs, args): print(_("Wrote x509 certificate to %s") % args.cert_filename) -@cliutils.arg( +@utils.arg( 'filename', metavar='', nargs='?', @@ -3605,7 +3605,7 @@ def do_x509_get_root_cert(cs, args): print(_("Wrote x509 root cert to %s") % args.filename) -@cliutils.arg( +@utils.arg( '--hypervisor', metavar='', default=None, @@ -3618,15 +3618,15 @@ def do_agent_list(cs, args): utils.print_list(result, columns) -@cliutils.arg('os', metavar='', help=_('Type of OS.')) -@cliutils.arg( +@utils.arg('os', metavar='', help=_('Type of OS.')) +@utils.arg( 'architecture', metavar='', help=_('Type of architecture.')) -@cliutils.arg('version', metavar='', help=_('Version.')) -@cliutils.arg('url', metavar='', help=_('URL.')) -@cliutils.arg('md5hash', metavar='', help=_('MD5 hash.')) -@cliutils.arg( +@utils.arg('version', metavar='', help=_('Version.')) +@utils.arg('url', metavar='', help=_('URL.')) +@utils.arg('md5hash', metavar='', help=_('MD5 hash.')) +@utils.arg( 'hypervisor', metavar='', default='xen', @@ -3639,16 +3639,16 @@ def do_agent_create(cs, args): utils.print_dict(result._info.copy()) -@cliutils.arg('id', metavar='', help=_('ID of the agent-build.')) +@utils.arg('id', metavar='', help=_('ID of the agent-build.')) def do_agent_delete(cs, args): """Delete existing agent build.""" cs.agents.delete(args.id) -@cliutils.arg('id', metavar='', help=_('ID of the agent-build.')) -@cliutils.arg('version', metavar='', help=_('Version.')) -@cliutils.arg('url', metavar='', help=_('URL')) -@cliutils.arg('md5hash', metavar='', help=_('MD5 hash.')) +@utils.arg('id', metavar='', help=_('ID of the agent-build.')) +@utils.arg('version', metavar='', help=_('Version.')) +@utils.arg('url', metavar='', help=_('URL')) +@utils.arg('md5hash', metavar='', help=_('MD5 hash.')) def do_agent_modify(cs, args): """Modify existing agent build.""" result = cs.agents.update(args.id, args.version, @@ -3668,8 +3668,8 @@ def do_aggregate_list(cs, args): utils.print_list(aggregates, columns) -@cliutils.arg('name', metavar='', help=_('Name of aggregate.')) -@cliutils.arg( +@utils.arg('name', metavar='', help=_('Name of aggregate.')) +@utils.arg( 'availability_zone', metavar='', default=None, @@ -3681,7 +3681,7 @@ def do_aggregate_create(cs, args): _print_aggregate_details(aggregate) -@cliutils.arg( +@utils.arg( 'aggregate', metavar='', help=_('Name or ID of aggregate to delete.')) @@ -3692,12 +3692,12 @@ def do_aggregate_delete(cs, args): print(_("Aggregate %s has been successfully deleted.") % aggregate.id) -@cliutils.arg( +@utils.arg( 'aggregate', metavar='', help=_('Name or ID of aggregate to update.')) -@cliutils.arg('name', metavar='', help=_('Name of aggregate.')) -@cliutils.arg( +@utils.arg('name', metavar='', help=_('Name of aggregate.')) +@utils.arg( 'availability_zone', metavar='', nargs='?', @@ -3715,10 +3715,10 @@ def do_aggregate_update(cs, args): _print_aggregate_details(aggregate) -@cliutils.arg( +@utils.arg( 'aggregate', metavar='', help=_('Name or ID of aggregate to update.')) -@cliutils.arg( +@utils.arg( 'metadata', metavar='', nargs='+', From 16c36ce79ee00adccbf52ec4dcdecfe9ce1e3a90 Mon Sep 17 00:00:00 2001 From: Kevin_Zheng Date: Thu, 3 Mar 2016 09:24:48 +0800 Subject: [PATCH 1005/1705] Use novaclient/utils directly and remove openstack/common (4/4) We can use novaclient/utils directly and get rid of openstack/common folder. This is the last part. Change-Id: I8103adafde7d8b3a101181366639314740f9a25a Partial-Bug: #1551603 --- novaclient/v2/shell.py | 265 ++++++++++++++++++++--------------------- 1 file changed, 132 insertions(+), 133 deletions(-) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 4a188646c..1e40ca855 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -42,7 +42,6 @@ from novaclient import client from novaclient import exceptions from novaclient.i18n import _ -from novaclient.openstack.common import cliutils from novaclient import shell from novaclient import utils from novaclient.v2 import availability_zones @@ -3744,10 +3743,10 @@ def do_aggregate_set_metadata(cs, args): _print_aggregate_details(aggregate) -@cliutils.arg( +@utils.arg( 'aggregate', metavar='', help=_('Name or ID of aggregate.')) -@cliutils.arg( +@utils.arg( 'host', metavar='', help=_('The host to add to the aggregate.')) def do_aggregate_add_host(cs, args): @@ -3760,10 +3759,10 @@ def do_aggregate_add_host(cs, args): _print_aggregate_details(aggregate) -@cliutils.arg( +@utils.arg( 'aggregate', metavar='', help=_('Name or ID of aggregate.')) -@cliutils.arg( +@utils.arg( 'host', metavar='', help=_('The host to remove from the aggregate.')) def do_aggregate_remove_host(cs, args): @@ -3776,7 +3775,7 @@ def do_aggregate_remove_host(cs, args): _print_aggregate_details(aggregate) -@cliutils.arg( +@utils.arg( 'aggregate', metavar='', help=_('Name or ID of aggregate.')) def do_aggregate_details(cs, args): @@ -3801,39 +3800,39 @@ def parser_hosts(fields): utils.print_list([aggregate], columns, formatters=formatters) -@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) -@cliutils.arg( +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg( 'host', metavar='', default=None, nargs='?', help=_('Destination host name.')) -@cliutils.arg( +@utils.arg( '--block-migrate', action='store_true', dest='block_migrate', default=False, help=_('True in case of block_migration. (Default=False:live_migration)'), start_version="2.0", end_version="2.24") -@cliutils.arg( +@utils.arg( '--block-migrate', action='store_true', dest='block_migrate', default="auto", help=_('True in case of block_migration. (Default=auto:live_migration)'), start_version="2.25") -@cliutils.arg( +@utils.arg( '--block_migrate', real_action='store_true', action=shell.DeprecatedAction, use=_('use "%s"; this option will be removed in ' 'novaclient 3.3.0.') % '--block-migrate', help=argparse.SUPPRESS) -@cliutils.arg( +@utils.arg( '--disk-over-commit', action='store_true', dest='disk_over_commit', default=False, help=_('Allow overcommit. (Default=False)'), start_version="2.0", end_version="2.24") -@cliutils.arg( +@utils.arg( '--disk_over_commit', real_action='store_true', action=shell.DeprecatedAction, @@ -3854,8 +3853,8 @@ def do_live_migration(cs, args): @api_versions.wraps("2.22") -@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) -@cliutils.arg('migration', metavar='', help=_('ID of migration.')) +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('migration', metavar='', help=_('ID of migration.')) def do_live_migration_force_complete(cs, args): """Force on-going live migration to complete.""" server = _find_server(cs, args.server) @@ -3863,7 +3862,7 @@ def do_live_migration_force_complete(cs, args): @api_versions.wraps("2.23") -@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('server', metavar='', help=_('Name or ID of server.')) def do_server_migration_list(cs, args): """Get the migrations list of specified server.""" server = _find_server(cs, args.server) @@ -3889,8 +3888,8 @@ def do_server_migration_list(cs, args): @api_versions.wraps("2.23") -@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) -@cliutils.arg('migration', metavar='', help=_('ID of migration.')) +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('migration', metavar='', help=_('ID of migration.')) def do_server_migration_show(cs, args): """Get the migration of specified server.""" server = _find_server(cs, args.server) @@ -3899,24 +3898,24 @@ def do_server_migration_show(cs, args): @api_versions.wraps("2.24") -@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) -@cliutils.arg('migration', metavar='', help=_('ID of migration.')) +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('migration', metavar='', help=_('ID of migration.')) def do_live_migration_abort(cs, args): """Abort an on-going live migration.""" server = _find_server(cs, args.server) cs.server_migrations.live_migration_abort(server, args.migration) -@cliutils.arg( +@utils.arg( '--all-tenants', action='store_const', const=1, default=0, help=_('Reset state server(s) in another tenant by name (Admin only).')) -@cliutils.arg( +@utils.arg( 'server', metavar='', nargs='+', help=_('Name or ID of server(s).')) -@cliutils.arg( +@utils.arg( '--active', action='store_const', dest='state', default='error', const='active', help=_('Request the server be reset to "active" state instead ' @@ -3941,18 +3940,18 @@ def do_reset_state(cs, args): raise exceptions.CommandError(msg) -@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('server', metavar='', help=_('Name or ID of server.')) def do_reset_network(cs, args): """Reset network of a server.""" _find_server(cs, args.server).reset_network() -@cliutils.arg( +@utils.arg( '--host', metavar='', default=None, help=_('Name of host.')) -@cliutils.arg( +@utils.arg( '--binary', metavar='', default=None, @@ -3974,17 +3973,17 @@ def do_service_list(cs, args): utils.print_list(result, columns) -@cliutils.arg('host', metavar='', help=_('Name of host.')) -@cliutils.arg('binary', metavar='', help=_('Service binary.')) +@utils.arg('host', metavar='', help=_('Name of host.')) +@utils.arg('binary', metavar='', help=_('Service binary.')) def do_service_enable(cs, args): """Enable the service.""" result = cs.services.enable(args.host, args.binary) utils.print_list([result], ['Host', 'Binary', 'Status']) -@cliutils.arg('host', metavar='', help=_('Name of host.')) -@cliutils.arg('binary', metavar='', help=_('Service binary.')) -@cliutils.arg( +@utils.arg('host', metavar='', help=_('Name of host.')) +@utils.arg('binary', metavar='', help=_('Service binary.')) +@utils.arg( '--reason', metavar='', help=_('Reason for disabling service.')) @@ -4001,9 +4000,9 @@ def do_service_disable(cs, args): @api_versions.wraps("2.11") -@cliutils.arg('host', metavar='', help=_('Name of host.')) -@cliutils.arg('binary', metavar='', help=_('Service binary.')) -@cliutils.arg( +@utils.arg('host', metavar='', help=_('Name of host.')) +@utils.arg('binary', metavar='', help=_('Service binary.')) +@utils.arg( '--unset', dest='force_down', help=_("Unset the force state down of service."), @@ -4015,7 +4014,7 @@ def do_service_force_down(cs, args): utils.print_list([result], ['Host', 'Binary', 'Forced down']) -@cliutils.arg('id', metavar='', help=_('ID of service.')) +@utils.arg('id', metavar='', help=_('ID of service.')) def do_service_delete(cs, args): """Delete the service.""" cs.services.delete(args.id) @@ -4033,26 +4032,26 @@ def _print_fixed_ip(cs, fixed_ip): utils.print_list([fixed_ip], fields) -@cliutils.arg('fixed_ip', metavar='', help=_('Fixed IP Address.')) +@utils.arg('fixed_ip', metavar='', help=_('Fixed IP Address.')) def do_fixed_ip_get(cs, args): """Retrieve info on a fixed IP.""" result = cs.fixed_ips.get(args.fixed_ip) _print_fixed_ip(cs, result) -@cliutils.arg('fixed_ip', metavar='', help=_('Fixed IP Address.')) +@utils.arg('fixed_ip', metavar='', help=_('Fixed IP Address.')) def do_fixed_ip_reserve(cs, args): """Reserve a fixed IP.""" cs.fixed_ips.reserve(args.fixed_ip) -@cliutils.arg('fixed_ip', metavar='', help=_('Fixed IP Address.')) +@utils.arg('fixed_ip', metavar='', help=_('Fixed IP Address.')) def do_fixed_ip_unreserve(cs, args): """Unreserve a fixed IP.""" cs.fixed_ips.unreserve(args.fixed_ip) -@cliutils.arg('host', metavar='', help=_('Name of host.')) +@utils.arg('host', metavar='', help=_('Name of host.')) def do_host_describe(cs, args): """Describe a specific host.""" result = cs.hosts.get(args.host) @@ -4060,7 +4059,7 @@ def do_host_describe(cs, args): utils.print_list(result, columns) -@cliutils.arg( +@utils.arg( '--zone', metavar='', default=None, @@ -4073,11 +4072,11 @@ def do_host_list(cs, args): utils.print_list(result, columns) -@cliutils.arg('host', metavar='', help=_('Name of host.')) -@cliutils.arg( +@utils.arg('host', metavar='', help=_('Name of host.')) +@utils.arg( '--status', metavar='', default=None, dest='status', help=_('Either enable or disable a host.')) -@cliutils.arg( +@utils.arg( '--maintenance', metavar='', default=None, @@ -4097,8 +4096,8 @@ def do_host_update(cs, args): utils.print_list([result], columns) -@cliutils.arg('host', metavar='', help=_('Name of host.')) -@cliutils.arg( +@utils.arg('host', metavar='', help=_('Name of host.')) +@utils.arg( '--action', metavar='', dest='action', choices=['startup', 'shutdown', 'reboot'], help=_('A power action: startup, reboot, or shutdown.')) @@ -4113,7 +4112,7 @@ def _find_hypervisor(cs, hypervisor): return utils.find_resource(cs.hypervisors, hypervisor) -@cliutils.arg( +@utils.arg( '--matching', metavar='', default=None, @@ -4129,7 +4128,7 @@ def do_hypervisor_list(cs, args): utils.print_list(cs.hypervisors.list(False), columns) -@cliutils.arg( +@utils.arg( 'hostname', metavar='', help=_('The hypervisor hostname (or pattern) to search for.')) @@ -4158,11 +4157,11 @@ def __init__(self, **kwargs): 'Hypervisor Hostname']) -@cliutils.arg( +@utils.arg( 'hypervisor', metavar='', help=_('Name or ID of the hypervisor to show the details of.')) -@cliutils.arg( +@utils.arg( '--wrap', dest='wrap', metavar='', default=40, help=_('Wrap the output to a specified length. ' 'Default is 40 or 0 to disable')) @@ -4172,7 +4171,7 @@ def do_hypervisor_show(cs, args): utils.print_dict(utils.flatten_dict(hyper._info), wrap=int(args.wrap)) -@cliutils.arg( +@utils.arg( 'hypervisor', metavar='', help=_('Name or ID of the hypervisor to show the uptime of.')) @@ -4249,7 +4248,7 @@ def _get_first_endpoint(endpoints, region): raise LookupError("No suitable endpoint found") -@cliutils.arg( +@utils.arg( '--wrap', dest='wrap', metavar='', default=64, help=_('Wrap PKI tokens to a specified length, or 0 to disable.')) def do_credentials(cs, _args): @@ -4271,8 +4270,8 @@ def do_credentials(cs, _args): wrap=int(_args.wrap)) -@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) -@cliutils.arg( +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg( '--port', dest='port', action='store', @@ -4280,13 +4279,13 @@ def do_credentials(cs, _args): default=22, help=_('Optional flag to indicate which port to use for ssh. ' '(Default=22)')) -@cliutils.arg( +@utils.arg( '--private', dest='private', action='store_true', default=False, help=argparse.SUPPRESS) -@cliutils.arg( +@utils.arg( '--address-type', dest='address_type', action='store', @@ -4294,25 +4293,25 @@ def do_credentials(cs, _args): default='floating', help=_('Optional flag to indicate which IP type to use. Possible values ' 'includes fixed and floating (the Default).')) -@cliutils.arg( +@utils.arg( '--network', metavar='', help=_('Network to use for the ssh.'), default=None) -@cliutils.arg( +@utils.arg( '--ipv6', dest='ipv6', action='store_true', default=False, help=_('Optional flag to indicate whether to use an IPv6 address ' 'attached to a server. (Defaults to IPv4 address)')) -@cliutils.arg( +@utils.arg( '--login', metavar='', help=_('Login to use.'), default="root") -@cliutils.arg( +@utils.arg( '-i', '--identity', dest='identity', help=_('Private key file, same as the -i option to the ssh command.'), default='') -@cliutils.arg( +@utils.arg( '--extra-opts', dest='extra', help=_('Extra options to pass to ssh. see: man ssh.'), @@ -4422,12 +4421,12 @@ def _quota_update(manager, identifier, args): manager.update(identifier, **updates) -@cliutils.arg( +@utils.arg( '--tenant', metavar='', default=None, help=_('ID of tenant to list the quotas for.')) -@cliutils.arg( +@utils.arg( '--user', metavar='', default=None, @@ -4446,7 +4445,7 @@ def do_quota_show(cs, args): _quota_show(cs.quotas.get(project_id, user_id=args.user)) -@cliutils.arg( +@utils.arg( '--tenant', metavar='', default=None, @@ -4465,125 +4464,125 @@ def do_quota_defaults(cs, args): _quota_show(cs.quotas.defaults(project_id)) -@cliutils.arg( +@utils.arg( 'tenant', metavar='', help=_('ID of tenant to set the quotas for.')) -@cliutils.arg( +@utils.arg( '--user', metavar='', default=None, help=_('ID of user to set the quotas for.')) -@cliutils.arg( +@utils.arg( '--instances', metavar='', type=int, default=None, help=_('New value for the "instances" quota.')) -@cliutils.arg( +@utils.arg( '--cores', metavar='', type=int, default=None, help=_('New value for the "cores" quota.')) -@cliutils.arg( +@utils.arg( '--ram', metavar='', type=int, default=None, help=_('New value for the "ram" quota.')) -@cliutils.arg( +@utils.arg( '--floating-ips', metavar='', type=int, default=None, help=_('New value for the "floating-ips" quota.')) -@cliutils.arg( +@utils.arg( '--floating_ips', type=int, action=shell.DeprecatedAction, use=_('use "%s"; this option will be removed in ' 'novaclient 3.3.0.') % '--floating-ips', help=argparse.SUPPRESS) -@cliutils.arg( +@utils.arg( '--fixed-ips', metavar='', type=int, default=None, help=_('New value for the "fixed-ips" quota.')) -@cliutils.arg( +@utils.arg( '--metadata-items', metavar='', type=int, default=None, help=_('New value for the "metadata-items" quota.')) -@cliutils.arg( +@utils.arg( '--metadata_items', type=int, action=shell.DeprecatedAction, use=_('use "%s"; this option will be removed in ' 'novaclient 3.3.0.') % '--metadata-items', help=argparse.SUPPRESS) -@cliutils.arg( +@utils.arg( '--injected-files', metavar='', type=int, default=None, help=_('New value for the "injected-files" quota.')) -@cliutils.arg( +@utils.arg( '--injected_files', type=int, action=shell.DeprecatedAction, use=_('use "%s"; this option will be removed in ' 'novaclient 3.3.0.') % '--injected-files', help=argparse.SUPPRESS) -@cliutils.arg( +@utils.arg( '--injected-file-content-bytes', metavar='', type=int, default=None, help=_('New value for the "injected-file-content-bytes" quota.')) -@cliutils.arg( +@utils.arg( '--injected_file_content_bytes', type=int, action=shell.DeprecatedAction, use=_('use "%s"; this option will be removed in ' 'novaclient 3.3.0.') % '--injected-file-content-bytes', help=argparse.SUPPRESS) -@cliutils.arg( +@utils.arg( '--injected-file-path-bytes', metavar='', type=int, default=None, help=_('New value for the "injected-file-path-bytes" quota.')) -@cliutils.arg( +@utils.arg( '--key-pairs', metavar='', type=int, default=None, help=_('New value for the "key-pairs" quota.')) -@cliutils.arg( +@utils.arg( '--security-groups', metavar='', type=int, default=None, help=_('New value for the "security-groups" quota.')) -@cliutils.arg( +@utils.arg( '--security-group-rules', metavar='', type=int, default=None, help=_('New value for the "security-group-rules" quota.')) -@cliutils.arg( +@utils.arg( '--server-groups', metavar='', type=int, default=None, help=_('New value for the "server-groups" quota.')) -@cliutils.arg( +@utils.arg( '--server-group-members', metavar='', type=int, default=None, help=_('New value for the "server-group-members" quota.')) -@cliutils.arg( +@utils.arg( '--force', dest='force', action="store_true", @@ -4596,12 +4595,12 @@ def do_quota_update(cs, args): _quota_update(cs.quotas, args.tenant, args) -@cliutils.arg( +@utils.arg( '--tenant', metavar='', required=True, help=_('ID of tenant to delete quota for.')) -@cliutils.arg( +@utils.arg( '--user', metavar='', help=_('ID of user to delete quota for.')) @@ -4613,7 +4612,7 @@ def do_quota_delete(cs, args): cs.quotas.delete(args.tenant, user_id=args.user) -@cliutils.arg( +@utils.arg( 'class_name', metavar='', help=_('Name of quota class to list the quotas for.')) @@ -4623,114 +4622,114 @@ def do_quota_class_show(cs, args): _quota_show(cs.quota_classes.get(args.class_name)) -@cliutils.arg( +@utils.arg( 'class_name', metavar='', help=_('Name of quota class to set the quotas for.')) -@cliutils.arg( +@utils.arg( '--instances', metavar='', type=int, default=None, help=_('New value for the "instances" quota.')) -@cliutils.arg( +@utils.arg( '--cores', metavar='', type=int, default=None, help=_('New value for the "cores" quota.')) -@cliutils.arg( +@utils.arg( '--ram', metavar='', type=int, default=None, help=_('New value for the "ram" quota.')) -@cliutils.arg( +@utils.arg( '--floating-ips', metavar='', type=int, default=None, help=_('New value for the "floating-ips" quota.')) -@cliutils.arg( +@utils.arg( '--floating_ips', type=int, action=shell.DeprecatedAction, use=_('use "%s"; this option will be removed in ' 'novaclient 3.3.0.') % '--floating-ips', help=argparse.SUPPRESS) -@cliutils.arg( +@utils.arg( '--fixed-ips', metavar='', type=int, default=None, help=_('New value for the "fixed-ips" quota.')) -@cliutils.arg( +@utils.arg( '--metadata-items', metavar='', type=int, default=None, help=_('New value for the "metadata-items" quota.')) -@cliutils.arg( +@utils.arg( '--metadata_items', type=int, action=shell.DeprecatedAction, use=_('use "%s"; this option will be removed in ' 'novaclient 3.3.0.') % '--metadata-items', help=argparse.SUPPRESS) -@cliutils.arg( +@utils.arg( '--injected-files', metavar='', type=int, default=None, help=_('New value for the "injected-files" quota.')) -@cliutils.arg( +@utils.arg( '--injected_files', type=int, action=shell.DeprecatedAction, use=_('use "%s"; this option will be removed in ' 'novaclient 3.3.0.') % '--injected-files', help=argparse.SUPPRESS) -@cliutils.arg( +@utils.arg( '--injected-file-content-bytes', metavar='', type=int, default=None, help=_('New value for the "injected-file-content-bytes" quota.')) -@cliutils.arg( +@utils.arg( '--injected_file_content_bytes', type=int, action=shell.DeprecatedAction, use=_('use "%s"; this option will be removed in ' 'novaclient 3.3.0.') % '--injected-file-content-bytes', help=argparse.SUPPRESS) -@cliutils.arg( +@utils.arg( '--injected-file-path-bytes', metavar='', type=int, default=None, help=_('New value for the "injected-file-path-bytes" quota.')) -@cliutils.arg( +@utils.arg( '--key-pairs', metavar='', type=int, default=None, help=_('New value for the "key-pairs" quota.')) -@cliutils.arg( +@utils.arg( '--security-groups', metavar='', type=int, default=None, help=_('New value for the "security-groups" quota.')) -@cliutils.arg( +@utils.arg( '--security-group-rules', metavar='', type=int, default=None, help=_('New value for the "security-group-rules" quota.')) -@cliutils.arg( +@utils.arg( '--server-groups', metavar='', type=int, default=None, help=_('New value for the "server-groups" quota.')) -@cliutils.arg( +@utils.arg( '--server-group-members', metavar='', type=int, @@ -4742,18 +4741,18 @@ def do_quota_class_update(cs, args): _quota_update(cs.quota_classes, args.class_name, args) -@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) -@cliutils.arg( +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg( 'host', metavar='', nargs='?', help=_("Name or ID of the target host. " "If no host is specified, the scheduler will choose one.")) -@cliutils.arg( +@utils.arg( '--password', dest='password', metavar='', help=_("Set the provided admin password on the evacuated server. Not" " applicable if the server is on shared storage.")) -@cliutils.arg( +@utils.arg( '--on-shared-storage', dest='on_shared_storage', action="store_true", @@ -4786,7 +4785,7 @@ def __init__(self, interface): utils.print_list([FormattedInterface(i) for i in interfaces], columns) -@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('server', metavar='', help=_('Name or ID of server.')) def do_interface_list(cs, args): """List interfaces attached to a server.""" server = _find_server(cs, args.server) @@ -4796,18 +4795,18 @@ def do_interface_list(cs, args): _print_interfaces(res) -@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) -@cliutils.arg( +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg( '--port-id', metavar='', help=_('Port ID.'), dest="port_id") -@cliutils.arg( +@utils.arg( '--net-id', metavar='', help=_('Network ID'), default=None, dest="net_id") -@cliutils.arg( +@utils.arg( '--fixed-ip', metavar='', help=_('Requested fixed IP.'), @@ -4821,8 +4820,8 @@ def do_interface_attach(cs, args): utils.print_dict(res) -@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) -@cliutils.arg('port_id', metavar='', help=_('Port ID.')) +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('port_id', metavar='', help=_('Port ID.')) def do_interface_detach(cs, args): """Detach a network interface from a server.""" server = _find_server(cs, args.server) @@ -4833,7 +4832,7 @@ def do_interface_detach(cs, args): @api_versions.wraps("2.17") -@cliutils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('server', metavar='', help=_('Name or ID of server.')) def do_trigger_crash_dump(cs, args): """Trigger crash dump in an instance.""" server = _find_server(cs, args.server) @@ -4916,7 +4915,7 @@ def _print_server_group_details(cs, server_group): # noqa utils.print_list(server_group, columns) -@cliutils.arg( +@utils.arg( '--all-projects', dest='all_projects', action='store_true', @@ -4936,19 +4935,19 @@ def do_secgroup_list_default_rules(cs, args): show_source_group=False) -@cliutils.arg( +@utils.arg( 'ip_proto', metavar='', help=_('IP protocol (icmp, tcp, udp).')) -@cliutils.arg( +@utils.arg( 'from_port', metavar='', help=_('Port at start of range.')) -@cliutils.arg( +@utils.arg( 'to_port', metavar='', help=_('Port at end of range.')) -@cliutils.arg('cidr', metavar='', help=_('CIDR for address range.')) +@utils.arg('cidr', metavar='', help=_('CIDR for address range.')) def do_secgroup_add_default_rule(cs, args): """Add a rule to the set of rules that will be added to the 'default' security group for new tenants (nova-network only). @@ -4960,19 +4959,19 @@ def do_secgroup_add_default_rule(cs, args): _print_secgroup_rules([rule], show_source_group=False) -@cliutils.arg( +@utils.arg( 'ip_proto', metavar='', help=_('IP protocol (icmp, tcp, udp).')) -@cliutils.arg( +@utils.arg( 'from_port', metavar='', help=_('Port at start of range.')) -@cliutils.arg( +@utils.arg( 'to_port', metavar='', help=_('Port at end of range.')) -@cliutils.arg('cidr', metavar='', help=_('CIDR for address range.')) +@utils.arg('cidr', metavar='', help=_('CIDR for address range.')) def do_secgroup_delete_default_rule(cs, args): """Delete a rule from the set of rules that will be added to the 'default' security group for new tenants (nova-network only). @@ -4989,7 +4988,7 @@ def do_secgroup_delete_default_rule(cs, args): raise exceptions.CommandError(_("Rule not found")) -@cliutils.arg('name', metavar='', help=_('Server group name.')) +@utils.arg('name', metavar='', help=_('Server group name.')) # NOTE(wingwj): The '--policy' way is still reserved here for preserving # the backwards compatibility of CLI, even if a user won't get this usage # in '--help' description. It will be deprecated after a suitable deprecation @@ -5000,13 +4999,13 @@ def do_secgroup_delete_default_rule(cs, args): # the possibility that they might mix them here. That usage is unsupported. # The related discussion can be found in # https://review.openstack.org/#/c/96382/2/. -@cliutils.arg( +@utils.arg( 'policy', metavar='', default=argparse.SUPPRESS, nargs='*', help=_('Policies for the server groups.')) -@cliutils.arg( +@utils.arg( '--policy', default=[], real_action='append', @@ -5025,7 +5024,7 @@ def do_server_group_create(cs, args): _print_server_group_details(cs, [server_group]) -@cliutils.arg( +@utils.arg( 'id', metavar='', nargs='+', @@ -5047,7 +5046,7 @@ def do_server_group_delete(cs, args): "specified server groups.")) -@cliutils.arg( +@utils.arg( 'id', metavar='', help=_("Unique ID of the server group to get.")) @@ -5088,7 +5087,7 @@ def _print_virtual_interface_list(cs, interface_list): utils.print_list(interface_list, columns, formatters) -@cliutils.arg('server', metavar='', help=_('ID of server.')) +@utils.arg('server', metavar='', help=_('ID of server.')) def do_virtual_interface_list(cs, args): """Show virtual interface info about the given server.""" server = _find_server(cs, args.server) From 471e08a4d41f9d1bcf4703b4a0c64731a94f2510 Mon Sep 17 00:00:00 2001 From: Ken'ichi Ohmichi Date: Sun, 6 Mar 2016 06:34:04 +0900 Subject: [PATCH 1006/1705] Remove console expectation from NMI tests The original tests expect the server console contains a message "Uhhuh. NMI received for unknown reason" but the expectation is wrong because this message is fallback one when disabling crash dump, that means the test expects the crash dump is disable. That is not the purpose of this NMI feature. In addition, this message is output on x86 cpu architecture only because the message is implemented in https://github.com/torvalds/linux/blob/master/arch/x86/kernel/nmi.c#L304 So if we use the other cpu architectures on the test, this test will fail. Let's remove the expectation from the tests. Change-Id: I62fe2e00433fd216def25d99a2f20d17b988fb84 Closes-Bug: #1553597 --- .../tests/functional/v2/test_trigger_crash_dump.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/novaclient/tests/functional/v2/test_trigger_crash_dump.py b/novaclient/tests/functional/v2/test_trigger_crash_dump.py index a41d7315d..ece7add2b 100644 --- a/novaclient/tests/functional/v2/test_trigger_crash_dump.py +++ b/novaclient/tests/functional/v2/test_trigger_crash_dump.py @@ -51,8 +51,6 @@ def test_trigger_crash_dump_in_active_state(self): self.wait_for_server_os_boot(server.id) self.nova('trigger-crash-dump %s ' % server.id) self._wait_for_nmi(server.id) - output = self.nova('console-log %s ' % server.id) - self.assertIn("Uhhuh. NMI received for unknown reason ", output) def test_trigger_crash_dump_in_error_state(self): server = self._create_server() @@ -63,8 +61,6 @@ def test_trigger_crash_dump_in_error_state(self): 'active', ['error']) self.nova('trigger-crash-dump %s ' % server.id) self._wait_for_nmi(server.id) - output = self.nova('console-log %s ' % server.id) - self.assertIn("Uhhuh. NMI received for unknown reason ", output) def test_trigger_crash_dump_in_paused_state(self): server = self._create_server() @@ -75,9 +71,6 @@ def test_trigger_crash_dump_in_paused_state(self): 'active', ['paused']) self.nova('trigger-crash-dump %s ' % server.id) self._wait_for_nmi(server.id) - output = self.nova('console-log %s ' % server.id) - # In PAUSED state a server's kernel shouldn't react onto NMI - self.assertNotIn("Uhhuh. NMI received for unknown reason ", output) def test_trigger_crash_dump_in_rescued_state(self): server = self._create_server() @@ -89,8 +82,6 @@ def test_trigger_crash_dump_in_rescued_state(self): self.wait_for_server_os_boot(server.id) self.nova('trigger-crash-dump %s ' % server.id) self._wait_for_nmi(server.id) - output = self.nova('console-log %s ' % server.id) - self.assertIn("Uhhuh. NMI received for unknown reason ", output) def test_trigger_crash_dump_in_resized_state(self): server = self._create_server() @@ -101,8 +92,6 @@ def test_trigger_crash_dump_in_resized_state(self): 'active', ['verify_resize']) self.nova('trigger-crash-dump %s ' % server.id) self._wait_for_nmi(server.id) - output = self.nova('console-log %s ' % server.id) - self.assertIn("Uhhuh. NMI received for unknown reason ", output) def test_trigger_crash_dump_in_shutoff_state(self): server = self._create_server() @@ -126,8 +115,6 @@ def test_trigger_crash_dump_in_locked_state_admin(self): self.nova('lock %s ' % server.id) self.nova('trigger-crash-dump %s ' % server.id) self._wait_for_nmi(server.id) - output = self.nova('console-log %s ' % server.id) - self.assertIn("Uhhuh. NMI received for unknown reason ", output) def test_trigger_crash_dump_in_locked_state_nonadmin(self): name = self.name_generate(prefix='server') From 3a538f6d862ae9fb041b496ae53498cf79d04df7 Mon Sep 17 00:00:00 2001 From: Cao ShuFeng Date: Thu, 3 Mar 2016 19:28:55 +0800 Subject: [PATCH 1007/1705] Overwrite Usage class's get() function base.Resource's get function will call its manager's get() function with one parameter. However UsageManager's get function takes three parameters. Change-Id: I958a55c4d52cec4d1177371b788b3cf788098a6c Closes-bug: 1552616 --- novaclient/tests/unit/v2/test_usage.py | 17 +++++++++++++++++ novaclient/v2/usage.py | 15 +++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/novaclient/tests/unit/v2/test_usage.py b/novaclient/tests/unit/v2/test_usage.py index 61a8c2cb1..3109e77f3 100644 --- a/novaclient/tests/unit/v2/test_usage.py +++ b/novaclient/tests/unit/v2/test_usage.py @@ -13,6 +13,8 @@ import datetime +import six + from novaclient.tests.unit import utils from novaclient.tests.unit.v2 import fakes from novaclient.v2 import usage @@ -58,3 +60,18 @@ def test_usage_get(self): ("start=%s&" % now.isoformat()) + ("end=%s" % now.isoformat())) self.assertIsInstance(u, usage.Usage) + + def test_usage_class_get(self): + start = six.u('2012-01-22T19:48:41.750722') + stop = six.u('2012-01-22T19:48:41.750722') + + info = {'tenant_id': 'tenantfoo', 'start': start, + 'stop': stop} + u = usage.Usage(self.cs.usage, info) + u.get() + self.assert_request_id(u, fakes.FAKE_REQUEST_ID_LIST) + + self.cs.assert_called( + 'GET', + "/os-simple-tenant-usage/tenantfoo?start=%s&end=%s" % + (start, stop)) diff --git a/novaclient/v2/usage.py b/novaclient/v2/usage.py index 585ce1165..8151df5d3 100644 --- a/novaclient/v2/usage.py +++ b/novaclient/v2/usage.py @@ -15,6 +15,8 @@ Usage interface. """ +import oslo_utils + from novaclient import base @@ -25,6 +27,19 @@ class Usage(base.Resource): def __repr__(self): return "" + def get(self): + fmt = '%Y-%m-%dT%H:%M:%S.%f' + if self.start and self.stop and self.tenant_id: + # set_loaded() first ... so if we have to bail, we know we tried. + self.set_loaded(True) + start = oslo_utils.timeutils.parse_strtime(self.start, fmt=fmt) + stop = oslo_utils.timeutils.parse_strtime(self.stop, fmt=fmt) + + new = self.manager.get(self.tenant_id, start, stop) + if new: + self._add_details(new._info) + self.append_request_ids(new.request_ids) + class UsageManager(base.ManagerWithFind): """ From 8691eeccd8acb941b38ad83a12e3df37d94e3125 Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Mon, 7 Mar 2016 13:44:36 +0900 Subject: [PATCH 1008/1705] Remove an unused method in novaclient/shell.py The 'positive_non_zero_float' method has not been used since Ifc9ddc08614e28358229db84cb9b54552ca36d51. TrivialFix Change-Id: Ice0c8f15dbc1a027710e0ace6c8fa872b964c3b6 --- novaclient/shell.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/novaclient/shell.py b/novaclient/shell.py index b4da9e2c6..cfdb6ad20 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -211,20 +211,6 @@ def __call__(self, parser, namespace, values, option_string): action(parser, namespace, values, option_string) -def positive_non_zero_float(text): - if text is None: - return None - try: - value = float(text) - except ValueError: - msg = _("%s must be a float") % text - raise argparse.ArgumentTypeError(msg) - if value <= 0: - msg = _("%s must be greater than 0") % text - raise argparse.ArgumentTypeError(msg) - return value - - class SecretsHelper(object): def __init__(self, args, client): self.args = args From 0ee5aea617c3e747d9ce2b3d92e8f38adb474988 Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Mon, 7 Mar 2016 14:24:56 +0900 Subject: [PATCH 1009/1705] Fix a typo in novaclient/v2/hosts.py TrivialFix Change-Id: I6992edbb768f14d0d4f0a7d79521876832e12714 --- novaclient/v2/hosts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/v2/hosts.py b/novaclient/v2/hosts.py index 28518567d..88789e242 100644 --- a/novaclient/v2/hosts.py +++ b/novaclient/v2/hosts.py @@ -73,7 +73,7 @@ def host_action(self, host, action): Perform an action on a host. :param host: The host to perform an action - :param actiob: The action to perform + :param action: The action to perform returns: An instance of novaclient.base.TupleWithMeta """ url = '/os-hosts/{0}/{1}'.format(host, action) From a571ebd72a154a71a9936322bbe5c29bbc0bde1c Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Tue, 1 Mar 2016 17:24:26 +0900 Subject: [PATCH 1010/1705] Remove unused code in tests/unit/v2/fakes.py TrivialFix Change-Id: I9c089f50f0b48bb7f41af9c4d6fbb8355d668a74 --- novaclient/tests/unit/v2/fakes.py | 275 ------------------------------ 1 file changed, 275 deletions(-) diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 6b24d2f76..e3e2b1dd6 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -625,29 +625,6 @@ def get_servers_1234_os_security_groups(self, **kw): 'rules': []}] }) - # - # Server Addresses - # - - def get_servers_1234_ips(self, **kw): - return (200, {}, { - 'addresses': - self.get_servers_1234()[1]['server']['addresses']}) - - def get_servers_1234_ips_public(self, **kw): - return (200, {}, { - 'public': - self.get_servers_1234_ips()[1]['addresses']['public']}) - - def get_servers_1234_ips_private(self, **kw): - return ( - 200, {}, - {'private': - self.get_servers_1234_ips()[1]['addresses']['private']}) - - def delete_servers_1234_ips_public_1_2_3_4(self, **kw): - return (202, {}, None) - # # Server password # @@ -979,9 +956,6 @@ def delete_flavors_1_os_extra_specs_k1(self, **kw): # Flavor access # - def get_flavors_1_os_flavor_access(self, **kw): - return (404, {}, None) - def get_flavors_2_os_flavor_access(self, **kw): return ( 200, FAKE_RESPONSE_HEADERS, @@ -996,13 +970,6 @@ def post_flavors_2_action(self, body, **kw): # Floating IPs # - def get_os_floating_ip_pools(self): - return ( - 200, - {}, - {'floating_ip_pools': [{'name': 'foo'}, {'name': 'bar'}]} - ) - def get_os_floating_ips(self, **kw): return ( 200, @@ -1090,12 +1057,6 @@ def get_os_floating_ips_bulk(self, **kw): {'id': 2, 'fixed_ip': '10.0.0.2', 'ip': '11.0.0.2'}, ]}) - def get_os_floating_ips_bulk_testHost(self, **kw): - return (200, {}, {'floating_ip_info': [ - {'id': 1, 'fixed_ip': '10.0.0.1', 'ip': '11.0.0.1'}, - {'id': 2, 'fixed_ip': '10.0.0.2', 'ip': '11.0.0.2'}, - ]}) - def post_os_floating_ips_bulk(self, **kw): params = kw.get('body').get('floating_ips_bulk_create') pool = params.get('pool', 'defaultPool') @@ -1197,11 +1158,6 @@ def get_images_f27f479a_ddda_419a_9bbc_d6b56b210161(self, **kw): def get_images_3e861307_73a6_4d1f_8d68_f68b03223032(self): raise exceptions.NotFound('404') - def post_images(self, body, **kw): - assert list(body) == ['image'] - fakes.assert_has_keys(body['image'], required=['serverId', 'name']) - return (202, {}, self.get_images_1()[2]) - def post_images_1_metadata(self, body, **kw): assert list(body) == ['metadata'] fakes.assert_has_keys(body['metadata'], @@ -1214,9 +1170,6 @@ def post_images_1_metadata(self, body, **kw): def delete_images_1(self, **kw): return (204, {}, None) - def delete_images_2(self, **kw): - return (204, {}, None) - def delete_images_c99d7632_bd66_4be9_aed5_3dd14b223a76(self, **kw): return (204, {}, None) @@ -1270,22 +1223,6 @@ def get_servers_1234_os_virtual_interfaces(self, **kw): # Quotas # - def get_os_quota_sets_test(self, **kw): - return (200, {}, { - 'quota_set': { - 'tenant_id': 'test', - 'metadata_items': [], - 'injected_file_content_bytes': 1, - 'injected_file_path_bytes': 1, - 'ram': 1, - 'floating_ips': 1, - 'instances': 1, - 'injected_files': 1, - 'cores': 1, - 'keypairs': 1, - 'security_groups': 1, - 'security_group_rules': 1}}) - def get_os_quota_sets_tenant_id(self, **kw): return (200, {}, { 'quota_set': { @@ -1318,38 +1255,6 @@ def get_os_quota_sets_97f4c221bff44578b0300df4ef119353(self, **kw): 'security_groups': 1, 'security_group_rules': 1}}) - def put_os_quota_sets_97f4c221_bff4_4578_b030_0df4ef119353(self, **kw): - return (200, {}, { - 'quota_set': { - 'tenant_id': '97f4c221-bff4-4578-b030-0df4ef119353', - 'metadata_items': [], - 'injected_file_content_bytes': 1, - 'injected_file_path_bytes': 1, - 'ram': 1, - 'floating_ips': 1, - 'instances': 1, - 'injected_files': 1, - 'cores': 1, - 'keypairs': 1, - 'security_groups': 1, - 'security_group_rules': 1}}) - - def get_os_quota_sets_97f4c221_bff4_4578_b030_0df4ef119353(self, **kw): - return (200, {}, { - 'quota_set': { - 'tenant_id': '97f4c221-bff4-4578-b030-0df4ef119353', - 'metadata_items': [], - 'injected_file_content_bytes': 1, - 'injected_file_path_bytes': 1, - 'ram': 1, - 'floating_ips': 1, - 'instances': 1, - 'injected_files': 1, - 'cores': 1, - 'keypairs': 1, - 'security_groups': 1, - 'security_group_rules': 1}}) - def get_os_quota_sets_97f4c221bff44578b0300df4ef119353_defaults(self): return (200, {}, { 'quota_set': { @@ -1400,9 +1305,6 @@ def put_os_quota_sets_97f4c221bff44578b0300df4ef119353(self, body, **kw): 'security_groups': 1, 'security_group_rules': 1}}) - def delete_os_quota_sets_test(self, **kw): - return (202, {}, {}) - def delete_os_quota_sets_97f4c221bff44578b0300df4ef119353(self, **kw): return (202, {}, {}) @@ -1511,11 +1413,6 @@ def get_os_security_groups(self, **kw): "rules": []} ]}) - def get_os_security_groups_1(self, **kw): - return (200, {}, {"security_group": - {'id': 1, 'name': 'test', 'description': 'FAKE_SECURITY_GROUP'} - }) - def delete_os_security_groups_1(self, **kw): return (202, {}, None) @@ -1543,9 +1440,6 @@ def get_os_security_group_rules(self, **kw): 'cidr': '10.0.0.0/8'} ]}) - def delete_os_security_group_rules_1(self, **kw): - return (202, {}, None) - def delete_os_security_group_rules_11(self, **kw): return (202, {}, None) @@ -1575,25 +1469,6 @@ def get_os_security_group_default_rules(self, **kw): 'to_port': 22, 'cidr': '10.0.0.0/8'} ]}) - def delete_os_security_group_default_rules_1(self, **kw): - return (202, {}, None) - - def delete_os_security_group_default_rules_11(self, **kw): - return (202, {}, None) - - def delete_os_security_group_default_rules_12(self, **kw): - return (202, {}, None) - - def post_os_security_group_default_rules(self, body, **kw): - assert list(body) == ['security_group_default_rule'] - fakes.assert_has_keys(body['security_group_default_rule'], - optional=['ip_protocol', 'from_port', - 'to_port', 'cidr']) - rules = self.get_os_security_group_default_rules() - r = {'security_group_default_rule': - rules[2]['security_group_default_rules'][0]} - return (202, {}, r) - # # Tenant Usage # @@ -1686,23 +1561,6 @@ def get_os_simple_tenant_usage_tenant_id(self, **kw): six.u('started_at'): six.u('2012-01-20 18:06:06.479998')}], six.u('start'): six.u('2011-12-25 19:48:41.750687'), six.u('total_local_gb_usage'): 0.0}}) - # - # Certificates - # - - def get_os_certificates_root(self, **kw): - return ( - 200, - {}, - {'certificate': {'private_key': None, 'data': 'foo'}} - ) - - def post_os_certificates(self, **kw): - return ( - 200, - {}, - {'certificate': {'private_key': 'foo', 'data': 'bar'}} - ) # # Aggregates @@ -1741,18 +1599,9 @@ def post_os_aggregates(self, body, **kw): def put_os_aggregates_1(self, body, **kw): return self._return_aggregate() - def put_os_aggregates_2(self, body, **kw): - return self._return_aggregate() - - def put_os_aggregates_3(self, body, **kw): - return self._return_aggregate_3() - def post_os_aggregates_1_action(self, body, **kw): return self._return_aggregate() - def post_os_aggregates_2_action(self, body, **kw): - return self._return_aggregate() - def post_os_aggregates_3_action(self, body, **kw): return self._return_aggregate_3() @@ -1825,16 +1674,6 @@ def post_os_fixed_ips_192_168_1_1_action(self, body, **kw): # # Hosts # - def get_os_hosts_host(self, *kw): - return (200, {}, {'host': - [{'resource': {'project': '(total)', 'host': 'dummy', - 'cpu': 16, 'memory_mb': 32234, 'disk_gb': 128}}, - {'resource': {'project': '(used_now)', 'host': 'dummy', - 'cpu': 1, 'memory_mb': 2075, 'disk_gb': 45}}, - {'resource': {'project': '(used_max)', 'host': 'dummy', - 'cpu': 1, 'memory_mb': 2048, 'disk_gb': 30}}, - {'resource': {'project': 'admin', 'host': 'dummy', - 'cpu': 1, 'memory_mb': 2048, 'disk_gb': 30}}]}) def get_os_hosts(self, **kw): zone = kw.get('zone', 'nova1') @@ -1845,9 +1684,6 @@ def get_os_hosts(self, **kw): 'service': 'nova-cert', 'zone': zone}]}) - def get_os_hosts_sample_host(self, *kw): - return (200, {}, {'host': [{'resource': {'host': 'sample_host'}}], }) - def put_os_hosts_sample_host_1(self, body, **kw): return (200, {}, {'host': 'sample-host_1', 'status': 'enabled'}) @@ -1873,56 +1709,12 @@ def get_os_hosts_sample_host_shutdown(self, **kw): return (200, {}, {'host': 'sample_host', 'power_action': 'shutdown'}) - def put_os_hosts_sample_host(self, body, **kw): - result = {'host': 'dummy'} - result.update(body) - return (200, {}, result) - def get_os_hypervisors(self, **kw): return (200, {}, { "hypervisors": [ {'id': 1234, 'hypervisor_hostname': 'hyper1'}, {'id': 5678, 'hypervisor_hostname': 'hyper2'}]}) - def get_os_hypervisors_detail(self, **kw): - return (200, {}, { - "hypervisors": [ - {'id': 1234, - 'service': {'id': 1, 'host': 'compute1'}, - 'vcpus': 4, - 'memory_mb': 10 * 1024, - 'local_gb': 250, - 'vcpus_used': 2, - 'memory_mb_used': 5 * 1024, - 'local_gb_used': 125, - 'hypervisor_type': "xen", - 'hypervisor_version': 3, - 'hypervisor_hostname': "hyper1", - 'free_ram_mb': 5 * 1024, - 'free_disk_gb': 125, - 'current_workload': 2, - 'running_vms': 2, - 'cpu_info': 'cpu_info', - 'disk_available_least': 100}, - {'id': 2, - 'service': {'id': 2, 'host': "compute2"}, - 'vcpus': 4, - 'memory_mb': 10 * 1024, - 'local_gb': 250, - 'vcpus_used': 2, - 'memory_mb_used': 5 * 1024, - 'local_gb_used': 125, - 'hypervisor_type': "xen", - 'hypervisor_version': 3, - 'hypervisor_hostname': "hyper2", - 'free_ram_mb': 5 * 1024, - 'free_disk_gb': 125, - 'current_workload': 2, - 'running_vms': 2, - 'cpu_info': 'cpu_info', - 'disk_available_least': 100}] - }) - def get_os_hypervisors_statistics(self, **kw): return (200, {}, { "hypervisor_statistics": { @@ -2052,54 +1844,12 @@ def delete_os_networks_1(self, **kw): def post_os_networks(self, **kw): return (202, {}, {'network': kw}) - def get_os_networks_1(self, **kw): - return (200, {}, {'network': {"label": "1", "cidr": "10.0.0.0/24", - "id": "1"}}) - - def delete_os_networks_networkdelete(self, **kw): - return (202, {}, None) - def post_os_networks_add(self, **kw): return (202, {}, None) - def post_os_networks_networkdisassociate_action(self, **kw): - return (202, {}, None) - - def get_os_fping(self, **kw): - return ( - 200, {}, { - 'servers': [ - { - "id": "1", - "project_id": "fake-project", - "alive": True, - }, - { - "id": "2", - "project_id": "fake-project", - "alive": True, - }, - ] - } - ) - - def get_os_fping_1(self, **kw): - return ( - 200, {}, { - 'server': { - "id": "1", - "project_id": "fake-project", - "alive": True, - } - } - ) - def post_os_networks_1_action(self, **kw): return (202, {}, None) - def post_os_networks_networktest_action(self, **kw): - return (202, {}, None) - def post_os_networks_2_action(self, **kw): return (202, {}, None) @@ -2120,17 +1870,6 @@ def post_os_tenant_networks(self, **kw): def delete_os_tenant_networks_1(self, **kw): return (202, {}, None) - def get_os_availability_zone(self, **kw): - return (200, {}, { - "availabilityZoneInfo": [ - {"zoneName": "zone-1", - "zoneState": {"available": True}, - "hosts": None}, - {"zoneName": "zone-2", - "zoneState": {"available": False}, - "hosts": None}] - }) - def get_os_availability_zone_detail(self, **kw): return (200, {}, { "availabilityZoneInfo": [ @@ -2432,20 +2171,6 @@ def _return_server_group(self): def post_os_server_groups(self, body, **kw): return self._return_server_group() - def get_os_server_groups_2cbd51f4_fafe_4cdb_801b_cf913a6f288b(self, **kw): - return self._return_server_group() - - def put_os_server_groups_2cbd51f4_fafe_4cdb_801b_cf913a6f288b(self, **kw): - return self._return_server_group() - - def post_os_server_groups_2cbd51f4_fafe_4cdb_801b_cf913a6f288b_action( - self, body, **kw): - return self._return_server_group() - - def delete_os_server_groups_2cbd51f4_fafe_4cdb_801b_cf913a6f288b( - self, **kw): - return (202, {}, None) - def post_servers_1234_migrations_1_action(self, body): return (202, {}, None) From c2ec89d285fbb29800e421d7dcb44347eb5b8ec2 Mon Sep 17 00:00:00 2001 From: Sven Anderson Date: Fri, 4 Mar 2016 19:36:03 +0100 Subject: [PATCH 1011/1705] Return a less dramatic message for public flavors. Getting the access list of a public flavor bails out with a dramatic error message, suggesting that something is broken. This changes the message to an unagitated "Access list not available for public flavors." Change-Id: I432496c24b23de8fa58de93f2cbed8bed1d540b1 --- novaclient/tests/functional/v2/legacy/test_flavor_access.py | 6 ++---- novaclient/tests/unit/v2/test_shell.py | 4 ++++ novaclient/v2/shell.py | 4 ++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/novaclient/tests/functional/v2/legacy/test_flavor_access.py b/novaclient/tests/functional/v2/legacy/test_flavor_access.py index d82d04a2d..ec83b4936 100644 --- a/novaclient/tests/functional/v2/legacy/test_flavor_access.py +++ b/novaclient/tests/functional/v2/legacy/test_flavor_access.py @@ -54,13 +54,11 @@ def test_add_access_public_flavor(self): # For microversion < 2.7 the 'flavor-access-add' operation is executed # successfully for public flavor, but the next operation, # 'flavor-access-list --flavor %(name_of_public_flavor)' returns - # CommandError: Failed to get access list for public flavor type. + # a CommandError flv_name = self.name_generate(prefix='flv') self.nova('flavor-create %s auto 512 1 1' % flv_name) self.addCleanup(self.nova, 'flavor-delete %s' % flv_name) self.nova('flavor-access-add %s %s' % (flv_name, self.tenant_id)) output = self.nova('flavor-access-list --flavor %s' % flv_name, fail_ok=True, merge_stderr=True) - self.assertIn("ERROR (CommandError): " - "Failed to get access list for public flavor type.\n", - output) + self.assertIn("CommandError", output) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 68bc1b4e4..2fcb1d0b9 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -851,6 +851,10 @@ def test_flavor_access_list_no_filter(self): cmd = 'flavor-access-list' self.assertRaises(exceptions.CommandError, self.run_command, cmd) + def test_flavor_access_list_public(self): + cmd = 'flavor-access-list --flavor 1' + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + def test_flavor_access_add_by_id(self): self.run_command('flavor-access-add 2 proj2') self.assert_called('POST', '/flavors/2/action', diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 977364882..b16c87baa 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -882,8 +882,8 @@ def do_flavor_access_list(cs, args): elif args.flavor: flavor = _find_flavor(cs, args.flavor) if flavor.is_public: - raise exceptions.CommandError(_("Failed to get access list " - "for public flavor type.")) + raise exceptions.CommandError(_("Access list not available " + "for public flavors.")) kwargs = {'flavor': flavor} elif args.tenant: kwargs = {'tenant': args.tenant} From d8e2f0c1a4e9e2b87b9245b8de94495be376a7de Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Mon, 7 Mar 2016 16:00:12 -0500 Subject: [PATCH 1012/1705] Make it clear that host-servers-migrate is a cold migration Let's be clear in the help text that host-servers-migrate is a cold migration since we have similar sounding commands host-evacuate and host-evacuate-live (which is not actually evacuate, it's live migration). Change-Id: I17ee230fc1c29369c40492523c9d97d25f7ee023 --- novaclient/v2/contrib/host_servers_migrate.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/novaclient/v2/contrib/host_servers_migrate.py b/novaclient/v2/contrib/host_servers_migrate.py index 95268186b..96e606526 100644 --- a/novaclient/v2/contrib/host_servers_migrate.py +++ b/novaclient/v2/contrib/host_servers_migrate.py @@ -38,7 +38,9 @@ def _server_migrate(cs, server): @utils.arg('host', metavar='', help='Name of host.') def do_host_servers_migrate(cs, args): - """Migrate all instances of the specified host to other available hosts.""" + """Cold migrate all instances off the specified host to other available + hosts. + """ hypervisors = cs.hypervisors.search(args.host, servers=True) response = [] for hyper in hypervisors: From 680320169b410c26bae9d858ab6c82c8fc4d6178 Mon Sep 17 00:00:00 2001 From: Cao ShuFeng Date: Tue, 8 Mar 2016 10:12:27 +0800 Subject: [PATCH 1013/1705] Remove additional 'timeout' element Remove additional 'timeout' element from trigger crash dump test and rename _wait_for_nmi to _assert_nmi so that we get at least one 'assert' for each test. Change-Id: I70372a8c725827ee9b5285ed92ca06b35eb0edbc Closes-bug: 1554306 --- .../functional/v2/test_trigger_crash_dump.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/novaclient/tests/functional/v2/test_trigger_crash_dump.py b/novaclient/tests/functional/v2/test_trigger_crash_dump.py index ece7add2b..4b6b06731 100644 --- a/novaclient/tests/functional/v2/test_trigger_crash_dump.py +++ b/novaclient/tests/functional/v2/test_trigger_crash_dump.py @@ -34,8 +34,7 @@ class TestTriggerCrashDumpNovaClientV217(base.TenantTestBase): # The server status must be ACTIVE, PAUSED, RESCUED, RESIZED or ERROR. # If not, the conflictingRequest(409) code is returned - def _wait_for_nmi(self, server_id, timeout=60, - poll_interval=1): + def _assert_nmi(self, server_id, timeout=60, poll_interval=1): start_time = time.time() while time.time() - start_time < timeout: if 'trigger_crash_dump' in self.nova('instance-action-list %s ' % @@ -44,13 +43,13 @@ def _wait_for_nmi(self, server_id, timeout=60, time.sleep(poll_interval) else: self.fail("Trigger crash dump hasn't been executed for server %s" - % (server_id, timeout)) + % server_id) def test_trigger_crash_dump_in_active_state(self): server = self._create_server() self.wait_for_server_os_boot(server.id) self.nova('trigger-crash-dump %s ' % server.id) - self._wait_for_nmi(server.id) + self._assert_nmi(server.id) def test_trigger_crash_dump_in_error_state(self): server = self._create_server() @@ -60,7 +59,7 @@ def test_trigger_crash_dump_in_error_state(self): self.client.servers.get, server.id, 'active', ['error']) self.nova('trigger-crash-dump %s ' % server.id) - self._wait_for_nmi(server.id) + self._assert_nmi(server.id) def test_trigger_crash_dump_in_paused_state(self): server = self._create_server() @@ -70,7 +69,7 @@ def test_trigger_crash_dump_in_paused_state(self): self.client.servers.get, server.id, 'active', ['paused']) self.nova('trigger-crash-dump %s ' % server.id) - self._wait_for_nmi(server.id) + self._assert_nmi(server.id) def test_trigger_crash_dump_in_rescued_state(self): server = self._create_server() @@ -81,7 +80,7 @@ def test_trigger_crash_dump_in_rescued_state(self): 'active', ['rescue']) self.wait_for_server_os_boot(server.id) self.nova('trigger-crash-dump %s ' % server.id) - self._wait_for_nmi(server.id) + self._assert_nmi(server.id) def test_trigger_crash_dump_in_resized_state(self): server = self._create_server() @@ -91,7 +90,7 @@ def test_trigger_crash_dump_in_resized_state(self): self.client.servers.get, server.id, 'active', ['verify_resize']) self.nova('trigger-crash-dump %s ' % server.id) - self._wait_for_nmi(server.id) + self._assert_nmi(server.id) def test_trigger_crash_dump_in_shutoff_state(self): server = self._create_server() @@ -114,7 +113,7 @@ def test_trigger_crash_dump_in_locked_state_admin(self): self.wait_for_server_os_boot(server.id) self.nova('lock %s ' % server.id) self.nova('trigger-crash-dump %s ' % server.id) - self._wait_for_nmi(server.id) + self._assert_nmi(server.id) def test_trigger_crash_dump_in_locked_state_nonadmin(self): name = self.name_generate(prefix='server') From b80d8cb6e6cd1e86c7dc3c99c3e7d92641c00097 Mon Sep 17 00:00:00 2001 From: "abhishek.talwar" Date: Wed, 9 Mar 2016 14:52:18 +0530 Subject: [PATCH 1014/1705] nova add-secgroup help updated with secgroup id As per nova add-secgroup help, the user need to enter secgroup name's to add it to a server. But as the command works fine with secgroup id too, so updated the help. Change-Id: If13dd619808b7ff87c214f17ef71296166d870ab Closes-Bug: #1554930 --- novaclient/v2/shell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index cb7e7a707..42798f817 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -2721,7 +2721,7 @@ def _disassociate_floating_ip(cs, args): @utils.arg( 'secgroup', metavar='', - help=_('Name of Security Group.')) + help=_('Name or ID of Security Group.')) def do_add_secgroup(cs, args): """Add a Security Group to a server.""" server = _find_server(cs, args.server) From 97d63e3ecc9c702baa4f280231dda314e308f0a4 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Wed, 9 Mar 2016 12:43:26 +0300 Subject: [PATCH 1015/1705] Wrap interface_list by proper resource class Server resource is not the right one to represent data for interfaces. This patch adds new resource class NetworkInterface for this task. Change-Id: I02cfe520643522006333f6e1e80e029959af6e1c Closes-Bug: #1554907 --- novaclient/v2/servers.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index e8a8dc01c..b81e0475e 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -527,6 +527,15 @@ def trigger_crash_dump(self): return self.manager.trigger_crash_dump(self) +class NetworkInterface(base.Resource): + @property + def id(self): + return self.port_id + + def __repr__(self): + return '' % self.id + + class ServerManager(base.BootingManagerWithFind): resource_class = Server @@ -1636,7 +1645,7 @@ def interface_list(self, server): :param server: The :class:`Server` (or its ID) to query. """ return self._list('/servers/%s/os-interface' % base.getid(server), - 'interfaceAttachments') + 'interfaceAttachments', obj_class=NetworkInterface) def interface_attach(self, server, port_id, net_id, fixed_ip): """ From a7bffe8a71bb051b8c4df873fb8cb187c0f83643 Mon Sep 17 00:00:00 2001 From: Doug Hellmann Date: Thu, 10 Mar 2016 13:01:27 -0500 Subject: [PATCH 1016/1705] Update reno for stable/mitaka Change-Id: I9081add2d40865a4e379de4ed7dfc46c5e69fbbc --- releasenotes/source/index.rst | 1 + releasenotes/source/mitaka.rst | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 releasenotes/source/mitaka.rst diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index 6d8e6df7f..b054c42fe 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -8,6 +8,7 @@ Contents :maxdepth: 2 liberty + mitaka unreleased diff --git a/releasenotes/source/mitaka.rst b/releasenotes/source/mitaka.rst new file mode 100644 index 000000000..e54560965 --- /dev/null +++ b/releasenotes/source/mitaka.rst @@ -0,0 +1,6 @@ +=================================== + Mitaka Series Release Notes +=================================== + +.. release-notes:: + :branch: origin/stable/mitaka From 7cefdd36a6c397279fd88b5fad017de58282cc1e Mon Sep 17 00:00:00 2001 From: Kevin_Zheng Date: Tue, 1 Mar 2016 15:25:45 +0800 Subject: [PATCH 1017/1705] Add changes-since support when list servers Nova API supports change-since filter when list servers: https://github.com/openstack/nova/blob/master/nova/api/openstack/compute/servers.py#L325-L331 but in python-novaclient we don't. This patch add the support for change-since when list servers. Closes-bug: #1551591 depends-on: Ic2f239f634f917a5771b0401a5073546c710c036 Change-Id: I27f2d1e33a56d357e247111b338c93861716cfee --- .../functional/v2/legacy/test_servers.py | 11 ++++++++++ novaclient/tests/unit/v2/test_shell.py | 9 +++++++++ novaclient/v2/shell.py | 20 ++++++++++++++++++- 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/novaclient/tests/functional/v2/legacy/test_servers.py b/novaclient/tests/functional/v2/legacy/test_servers.py index 129988412..34cf18569 100644 --- a/novaclient/tests/functional/v2/legacy/test_servers.py +++ b/novaclient/tests/functional/v2/legacy/test_servers.py @@ -13,6 +13,7 @@ import uuid from novaclient.tests.functional import base +from oslo_utils import timeutils class TestServersBootNovaClient(base.ClientTestBase): @@ -84,6 +85,16 @@ def test_list_with_limit(self): servers = output.split("\n")[3:-2] self.assertEqual(1, len(servers), output) + def test_list_with_changes_since(self): + now = timeutils.isotime() + name = str(uuid.uuid4()) + self._create_servers(name, 1) + output = self.nova("list", params="--changes-since %s" % now) + self.assertIn(name, output, output) + now = timeutils.isotime() + output = self.nova("list", params="--changes-since %s" % now) + self.assertNotIn(name, output, output) + def test_list_all_servers(self): name = str(uuid.uuid4()) precreated_servers = self._create_servers(name, 3) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 2fcb1d0b9..635ffbecd 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -1063,6 +1063,15 @@ def test_list_with_limit(self): self.run_command('list --limit 3') self.assert_called('GET', '/servers/detail?limit=3') + def test_list_with_changes_since(self): + self.run_command('list --changes-since 2016-02-29T06:23:22') + self.assert_called( + 'GET', '/servers/detail?changes-since=2016-02-29T06%3A23%3A22') + + def test_list_with_changes_since_invalid_value(self): + self.assertRaises(exceptions.CommandError, + self.run_command, 'list --changes-since 0123456789') + def test_meta_parsing(self): meta = ['key1=meta1', 'key2=meta2'] ref = {'key1': 'meta1', 'key2': 'meta2'} diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 42798f817..c41df7321 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -1458,6 +1458,14 @@ def do_image_delete(cs, args): "will be displayed. If limit is bigger than 'osapi_max_limit' " "option of Nova API, limit 'osapi_max_limit' will be used " "instead.")) +@utils.arg( + '--changes-since', + dest='changes_since', + metavar='', + default=None, + help=_("List only servers changed after a certain point of time." + "The provided time should be an ISO 8061 formated time." + "ex 2016-03-04T06:27:59Z .")) def do_list(cs, args): """List active servers.""" imageid = None @@ -1482,7 +1490,8 @@ def do_list(cs, args): 'user_id': args.user, 'host': args.host, 'deleted': args.deleted, - 'instance_name': args.instance_name} + 'instance_name': args.instance_name, + 'changes-since': args.changes_since} filters = {'flavor': lambda f: f['id'], 'security_groups': utils._format_security_groups} @@ -1504,6 +1513,13 @@ def do_list(cs, args): sort_keys.append(sort_key) sort_dirs.append(sort_dir) + if search_opts['changes-since']: + try: + timeutils.parse_isotime(search_opts['changes-since']) + except ValueError: + raise exceptions.CommandError(_('Invalid changes-since value: %s') + % search_opts['changes-since']) + servers = cs.servers.list(detailed=detailed, search_opts=search_opts, sort_keys=sort_keys, @@ -1554,6 +1570,8 @@ def do_list(cs, args): # Tenant ID as well if search_opts['all_tenants']: columns.insert(2, 'Tenant ID') + if search_opts['changes-since']: + columns.append('Updated') formatters['Networks'] = utils._format_servers_list_networks sortby_index = 1 if args.sort: From f6bcac477241770368aee85533312e58f0429a4b Mon Sep 17 00:00:00 2001 From: Balazs Gibizer Date: Wed, 16 Mar 2016 17:46:04 +0100 Subject: [PATCH 1018/1705] Validate shutdown value of --block-device The shutdown value of the --block-device parameter was stated to to be either 'remove' or 'preserve' but the code only coverted everything to False that was not equal to 'remove'. This patch adds strict validation that rejects values other than 'remove' or 'preserve'. Closes-bug: #1558157 Change-Id: I89a6c4fe90ec4d8155f4a7b93006d1972654a223 --- novaclient/tests/unit/v2/test_shell.py | 7 +++++++ novaclient/v2/shell.py | 6 ++++++ 2 files changed, 13 insertions(+) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index cb30ce40a..916bd4438 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -464,6 +464,13 @@ def test_boot_no_image_bdms_v2(self): }}, ) + def test_boot_bdms_v2_invalid_shutdown_value(self): + self.assertRaises(exceptions.CommandError, self.run_command, + ('boot --flavor 1 --image 1 --block-device ' + 'id=fake-id,source=volume,dest=volume,device=vda,' + 'size=1,format=ext4,type=disk,shutdown=foobar ' + 'some-server')) + def test_boot_metadata(self): self.run_command('boot --image 1 --flavor 1 --meta foo=bar=pants' ' --meta spam=eggs some-server ') diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 9693fc713..2fd793c92 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -127,6 +127,12 @@ def _parse_block_device_mapping_v2(args, image): # default for local block devices when not specified. if 'delete_on_termination' in bdm_dict: action = bdm_dict['delete_on_termination'] + if action not in ['remove', 'preserve']: + raise exceptions.CommandError( + _("The value of shutdown key of --block-device shall be " + "either 'remove' or 'preserve' but it was '%(action)s'") + % {'action': action}) + bdm_dict['delete_on_termination'] = (action == 'remove') elif bdm_dict.get('destination_type') == 'local': bdm_dict['delete_on_termination'] = True From fa377e7fca44e81247639efebd7cae5cb377e57e Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Fri, 18 Mar 2016 19:17:47 -0400 Subject: [PATCH 1019/1705] Handle error response for webob>=1.6.0 WebOb change https://github.com/Pylons/webob/pull/230 changed the way in which the error response body is formatted such that it's no longer a nested dict. So we have to handle both the old convention of an error message key to the response body error dict and the new way with just the error body dict. This was reported upstream: https://github.com/Pylons/webob/issues/235 But given this was apparently implemented as a long-overdue change in WebOb the behavior is not likely to change. Change-Id: If653a247d842786d2824b4b3a5c0cde1383ed7ab Closes-Bug: #1559072 --- novaclient/exceptions.py | 18 +++++++-- novaclient/tests/unit/test_exceptions.py | 48 ++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 novaclient/tests/unit/test_exceptions.py diff --git a/novaclient/exceptions.py b/novaclient/exceptions.py index c502e9641..31dd48715 100644 --- a/novaclient/exceptions.py +++ b/novaclient/exceptions.py @@ -294,9 +294,21 @@ def from_response(response, body, url, method=None): details = "n/a" if hasattr(body, 'keys'): - error = body[list(body)[0]] - message = error.get('message') - details = error.get('details') + # NOTE(mriedem): WebOb<1.6.0 will return a nested dict structure + # where the error keys to the message/details/code. WebOb>=1.6.0 + # returns just a response body as a single dict, not nested, + # so we have to handle both cases (since we can't trust what we're + # given with content_type: application/json either way. + if 'message' in body: + # WebOb 1.6.0 case + message = body.get('message') + details = body.get('details') + else: + # WebOb<1.6.0 where we assume there is a single error message + # key to the body that has the message and details. + error = body[list(body)[0]] + message = error.get('message') + details = error.get('details') kwargs['message'] = message kwargs['details'] = details diff --git a/novaclient/tests/unit/test_exceptions.py b/novaclient/tests/unit/test_exceptions.py new file mode 100644 index 000000000..34ce11d16 --- /dev/null +++ b/novaclient/tests/unit/test_exceptions.py @@ -0,0 +1,48 @@ +# Copyright 2016 IBM Corp. +# +# 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 novaclient import exceptions +from novaclient.tests.unit import utils as test_utils + + +class ExceptionsTestCase(test_utils.TestCase): + + def _test_from_response(self, body, expected_message): + data = { + 'status_code': 404, + 'headers': { + 'content-type': 'application/json', + 'x-openstack-request-id': ( + 'req-d9df03b0-4150-4b53-8157-7560ccf39f75'), + } + } + response = test_utils.TestResponse(data) + fake_url = 'http://localhost:8774/v2.1/fake/flavors/test' + error = exceptions.from_response(response, body, fake_url, 'GET') + self.assertIsInstance(error, exceptions.NotFound) + self.assertEqual(expected_message, error.message) + + def test_from_response_webob_pre_1_6_0(self): + # Tests error responses before webob 1.6.0 where the error details + # are nested in the response body. + message = "Flavor test could not be found." + self._test_from_response( + {"itemNotFound": {"message": message, "code": 404}}, + message) + + def test_from_response_webob_post_1_6_0(self): + # Tests error responses from webob 1.6.0 where the error details + # are in the response body. + message = "Flavor test could not be found." + self._test_from_response({"message": message, "code": 404}, message) From bf68a0cf10f510c2e60fddd6a97a13731905a23d Mon Sep 17 00:00:00 2001 From: "abhishek.talwar" Date: Mon, 14 Mar 2016 15:10:11 +0530 Subject: [PATCH 1020/1705] aggregate-details changed to aggregate-show To show details of aggregate, we have aggregate-details command. But all other commands to show details of any resource is *-show like flavor-show, keypair-show etc. So changed the command to aggregate-show. Change-Id: If4875833a27382a6f3193ec55d6d4cb1852249fd Closes-Bug: #1552646 --- novaclient/tests/unit/v2/test_shell.py | 8 ++++++++ novaclient/v2/shell.py | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 2fcb1d0b9..b2d4fcb57 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -1666,6 +1666,14 @@ def test_aggregate_details_by_name(self): self.run_command('aggregate-details test') self.assert_called('GET', '/os-aggregates') + def test_aggregate_show_by_id(self): + self.run_command('aggregate-show 1') + self.assert_called('GET', '/os-aggregates/1') + + def test_aggregate_show_by_name(self): + self.run_command('aggregate-show test') + self.assert_called('GET', '/os-aggregates') + def test_live_migration(self): self.run_command('live-migration sample-server hostname') self.assert_called('POST', '/servers/1234/action', diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 42798f817..90978c491 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -3779,6 +3779,14 @@ def do_aggregate_remove_host(cs, args): 'aggregate', metavar='', help=_('Name or ID of aggregate.')) def do_aggregate_details(cs, args): + """DEPRECATED, use aggregate-show instead.""" + do_aggregate_show(cs, args) + + +@utils.arg( + 'aggregate', metavar='', + help=_('Name or ID of aggregate.')) +def do_aggregate_show(cs, args): """Show details of the specified aggregate.""" aggregate = _find_aggregate(cs, args.aggregate) _print_aggregate_details(aggregate) From c5272b349b62f7707c146bfd0a46b304c86e866a Mon Sep 17 00:00:00 2001 From: Christopher J Schaefer Date: Tue, 22 Mar 2016 16:34:51 -0500 Subject: [PATCH 1021/1705] Adding tox support for bandit Bandit is a code linter which is used to help identify potential security vulnerabilities. As part of the plan to integrate bandit into each OpenStack project, support for a tox testenv is step one. Later, gate tests will also be incorporated. Change-Id: Ib6ef0a3e8f32f2724314c166d7de50d591c0e949 --- test-requirements.txt | 1 + tox.ini | 3 +++ 2 files changed, 4 insertions(+) diff --git a/test-requirements.txt b/test-requirements.txt index 6a301d012..8dd1c96f7 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -3,6 +3,7 @@ # process, which may cause wedges in the gate later. hacking<0.11,>=0.10.0 +bandit>=0.17.3 # Apache-2.0 coverage>=3.6 # Apache-2.0 discover # BSD fixtures>=1.3.1 # Apache-2.0/BSD diff --git a/tox.ini b/tox.ini index b8a5004de..64961de5b 100644 --- a/tox.ini +++ b/tox.ini @@ -23,6 +23,9 @@ commands = [testenv:pep8] commands = flake8 {posargs} +[testenv:bandit] +commands = bandit -r novaclient -n5 -x tests + [testenv:venv] commands = {posargs} From 7e7e5525d2dea1941fa89270ffb89cc4a7670b4f Mon Sep 17 00:00:00 2001 From: Ronald Bradford Date: Tue, 22 Mar 2016 18:30:11 -0400 Subject: [PATCH 1022/1705] Removed unused Oslo Incubator code This is part of graduating projects to using Oslo Libraries. As this code is not actually used, it is simply removed. Change-Id: Ib28a957d9a4622064ec5da2ae616ac6f0716bcbf --- novaclient/openstack/__init__.py | 0 novaclient/openstack/common/__init__.py | 0 novaclient/openstack/common/cliutils.py | 18 --- tools/install_venv_common.py | 172 ------------------------ tox.ini | 2 +- 5 files changed, 1 insertion(+), 191 deletions(-) delete mode 100644 novaclient/openstack/__init__.py delete mode 100644 novaclient/openstack/common/__init__.py delete mode 100644 novaclient/openstack/common/cliutils.py delete mode 100644 tools/install_venv_common.py diff --git a/novaclient/openstack/__init__.py b/novaclient/openstack/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/novaclient/openstack/common/__init__.py b/novaclient/openstack/common/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/novaclient/openstack/common/cliutils.py b/novaclient/openstack/common/cliutils.py deleted file mode 100644 index d132a0ca4..000000000 --- a/novaclient/openstack/common/cliutils.py +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright 2012 Red Hat, Inc. -# -# 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 novaclient import utils - -arg = utils.arg -add_arg = utils.add_arg diff --git a/tools/install_venv_common.py b/tools/install_venv_common.py deleted file mode 100644 index 962b2f930..000000000 --- a/tools/install_venv_common.py +++ /dev/null @@ -1,172 +0,0 @@ -# Copyright 2013 OpenStack Foundation -# Copyright 2013 IBM Corp. -# -# 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. - -"""Provides methods needed by installation script for OpenStack development -virtual environments. - -Since this script is used to bootstrap a virtualenv from the system's Python -environment, it should be kept strictly compatible with Python 2.7. - -Synced in from openstack-common -""" - -from __future__ import print_function - -import optparse -import os -import subprocess -import sys - - -class InstallVenv(object): - - def __init__(self, root, venv, requirements, - test_requirements, py_version, - project): - self.root = root - self.venv = venv - self.requirements = requirements - self.test_requirements = test_requirements - self.py_version = py_version - self.project = project - - def die(self, message, *args): - print(message % args, file=sys.stderr) - sys.exit(1) - - def check_python_version(self): - if sys.version_info < (2, 7): - self.die("Need Python Version >= 2.7") - - def run_command_with_code(self, cmd, redirect_output=True, - check_exit_code=True): - """Runs a command in an out-of-process shell. - - Returns the output of that command. Working directory is self.root. - """ - if redirect_output: - stdout = subprocess.PIPE - else: - stdout = None - - proc = subprocess.Popen(cmd, cwd=self.root, stdout=stdout) - output = proc.communicate()[0] - if check_exit_code and proc.returncode != 0: - self.die('Command "%s" failed.\n%s', ' '.join(cmd), output) - return (output, proc.returncode) - - def run_command(self, cmd, redirect_output=True, check_exit_code=True): - return self.run_command_with_code(cmd, redirect_output, - check_exit_code)[0] - - def get_distro(self): - if (os.path.exists('/etc/fedora-release') or - os.path.exists('/etc/redhat-release')): - return Fedora( - self.root, self.venv, self.requirements, - self.test_requirements, self.py_version, self.project) - else: - return Distro( - self.root, self.venv, self.requirements, - self.test_requirements, self.py_version, self.project) - - def check_dependencies(self): - self.get_distro().install_virtualenv() - - def create_virtualenv(self, no_site_packages=True): - """Creates the virtual environment and installs PIP. - - Creates the virtual environment and installs PIP only into the - virtual environment. - """ - if not os.path.isdir(self.venv): - print('Creating venv...', end=' ') - if no_site_packages: - self.run_command(['virtualenv', '-q', '--no-site-packages', - self.venv]) - else: - self.run_command(['virtualenv', '-q', self.venv]) - print('done.') - else: - print("venv already exists...") - pass - - def pip_install(self, *args): - self.run_command(['tools/with_venv.sh', - 'pip', 'install', '--upgrade'] + list(args), - redirect_output=False) - - def install_dependencies(self): - print('Installing dependencies with pip (this can take a while)...') - - # First things first, make sure our venv has the latest pip and - # setuptools and pbr - self.pip_install('pip>=1.4') - self.pip_install('setuptools') - self.pip_install('pbr') - - self.pip_install('-r', self.requirements, '-r', self.test_requirements) - - def parse_args(self, argv): - """Parses command-line arguments.""" - parser = optparse.OptionParser() - parser.add_option('-n', '--no-site-packages', - action='store_true', - help="Do not inherit packages from global Python " - "install.") - return parser.parse_args(argv[1:])[0] - - -class Distro(InstallVenv): - - def check_cmd(self, cmd): - return bool(self.run_command(['which', cmd], - check_exit_code=False).strip()) - - def install_virtualenv(self): - if self.check_cmd('virtualenv'): - return - - if self.check_cmd('easy_install'): - print('Installing virtualenv via easy_install...', end=' ') - if self.run_command(['easy_install', 'virtualenv']): - print('Succeeded') - return - else: - print('Failed') - - self.die('ERROR: virtualenv not found.\n\n%s development' - ' requires virtualenv, please install it using your' - ' favorite package management tool' % self.project) - - -class Fedora(Distro): - """This covers all Fedora-based distributions. - - Includes: Fedora, RHEL, CentOS, Scientific Linux - """ - - def check_pkg(self, pkg): - return self.run_command_with_code(['rpm', '-q', pkg], - check_exit_code=False)[1] == 0 - - def install_virtualenv(self): - if self.check_cmd('virtualenv'): - return - - if not self.check_pkg('python-virtualenv'): - self.die("Please install 'python-virtualenv'.") - - super(Fedora, self).install_virtualenv() diff --git a/tox.ini b/tox.ini index b8a5004de..d0cf24c17 100644 --- a/tox.ini +++ b/tox.ini @@ -62,7 +62,7 @@ commands = python setup.py testr --coverage --testr-args='{posargs}' # Additional checks are also ignored on purpose: F811, F821 ignore = F811,F821,H404,H405 show-source = True -exclude=.venv,.git,.tox,dist,*openstack/common*,*lib/python*,*egg,build,doc/source/conf.py,releasenotes +exclude=.venv,.git,.tox,dist,*lib/python*,*egg,build,doc/source/conf.py,releasenotes [hacking] import_exceptions = novaclient.i18n From 92bbae6d09b8db4ed9659de79d172d672902a3a5 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Wed, 23 Mar 2016 12:35:33 -0400 Subject: [PATCH 1023/1705] Use keystoneclient python bindings for testing The keystone CLI has been deprecated for a long time in favor of python-openstackclient and Icbe15814bc4faf33f513f9654440068795eae807 finally removes the CLI from python-keystoneclient. This will be in the 3.0 release of python-keystoneclient. tempest-lib is using the keystone CLI and the novaclient functional tests are using tempest-lib (which is also deprecated now). This change removes the usage of the keystone CLI from python-keystoneclient via tempest-lib and replaces it with simply using the keystone v2 python bindings from the client. Change-Id: I557acefbf363304ce24ac81566bb651cbfe09176 --- novaclient/tests/functional/base.py | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/novaclient/tests/functional/base.py b/novaclient/tests/functional/base.py index d51a19d74..9595915b2 100644 --- a/novaclient/tests/functional/base.py +++ b/novaclient/tests/functional/base.py @@ -15,6 +15,7 @@ import uuid import fixtures +from keystoneclient.v2_0 import client as keystoneclient import os_client_config import six import tempest_lib.cli.base @@ -201,6 +202,11 @@ def setUp(self): cli_dir=cli_dir, insecure=insecure) + self.keystone = keystoneclient.Client(username=user, + password=passwd, + tenant_name=tenant, + auth_url=auth_url) + def nova(self, action, flags='', params='', fail_ok=False, endpoint_type='publicURL', merge_stderr=False): if self.COMPUTE_API_VERSION: @@ -352,27 +358,19 @@ class TenantTestBase(ClientTestBase): def setUp(self): super(TenantTestBase, self).setUp() - tenant = self.cli_clients.keystone( - 'tenant-create --name %s --enabled True' % + tenant = self.keystone.tenants.create( self.name_generate('v' + self.COMPUTE_API_VERSION)) - self.tenant_name = self._get_value_from_the_table(tenant, "name") - self.tenant_id = self._get_value_from_the_table( - self.cli_clients.keystone( - 'tenant-get %s' % self.tenant_name), 'id') - self.addCleanup(self.cli_clients.keystone, - "tenant-delete %s" % self.tenant_name) + self.tenant_id = tenant.id + self.addCleanup(self.keystone.tenants.delete, self.tenant_id) user_name = self.name_generate('v' + self.COMPUTE_API_VERSION) password = 'password' - user = self.cli_clients.keystone( - "user-create --name %(name)s --pass %(pass)s --tenant %(tenant)s" % - {"name": user_name, "pass": password, "tenant": self.tenant_name}) - self.user_id = self._get_value_from_the_table(user, "id") - self.addCleanup(self.cli_clients.keystone, - "user-delete %s" % self.user_id) + self.user_id = self.keystone.users.create( + user_name, password, tenant_id=self.tenant_id).id + self.addCleanup(self.keystone.users.delete, self.user_id) self.cli_clients_2 = tempest_lib.cli.base.CLIClient( username=user_name, password=password, - tenant_name=self.tenant_name, + tenant_name=tenant.name, uri=self.cli_clients.uri, cli_dir=self.cli_clients.cli_dir) From 90fbbb29562a905e8f70badf2c31cfb4ec6841ed Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Fri, 25 Mar 2016 13:17:56 -0400 Subject: [PATCH 1024/1705] Fix host-evacuate-live for 2.25 microversion Change I01b22593724616bc0a7793c509ecabf095d6927d made the live_migrate() method in the ServerManager conditional on the API version requested. This broke the host-evacuate-live command which is calling ServerManager.live_migrate() directly with one too many arguments for the v2.25 version of the method. This updates the host-evacuate-live shell to behave like the live-migration method and be aware of the API version when calling the live_migrate() method. Related to blueprint making-live-migration-api-friendly Change-Id: I4dbeb6ebe03f03799b706be2d787d21484b5c664 Closes-Bug: #1561938 --- novaclient/tests/unit/v2/test_shell.py | 24 +++++++++++++++++++++ novaclient/v2/contrib/host_evacuate_live.py | 21 ++++++++++++++---- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 06372c506..051e24645 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -1749,6 +1749,15 @@ def test_host_evacuate_live_with_no_target_host(self): self.assert_called('POST', '/servers/uuid3/action', body, pos=3) self.assert_called('POST', '/servers/uuid4/action', body, pos=4) + def test_host_evacuate_live_2_25(self): + self.run_command('host-evacuate-live hyper', api_version='2.25') + self.assert_called('GET', '/os-hypervisors/hyper/servers', pos=0) + body = {'os-migrateLive': {'host': None, 'block_migration': 'auto'}} + self.assert_called('POST', '/servers/uuid1/action', body, pos=1) + self.assert_called('POST', '/servers/uuid2/action', body, pos=2) + self.assert_called('POST', '/servers/uuid3/action', body, pos=3) + self.assert_called('POST', '/servers/uuid4/action', body, pos=4) + def test_host_evacuate_live_with_target_host(self): self.run_command('host-evacuate-live hyper ' '--target-host hostname') @@ -1772,6 +1781,16 @@ def test_host_evacuate_live_with_block_migration(self): self.assert_called('POST', '/servers/uuid3/action', body, pos=3) self.assert_called('POST', '/servers/uuid4/action', body, pos=4) + def test_host_evacuate_live_with_block_migration_2_25(self): + self.run_command('host-evacuate-live --block-migrate hyper', + api_version='2.25') + self.assert_called('GET', '/os-hypervisors/hyper/servers', pos=0) + body = {'os-migrateLive': {'host': None, 'block_migration': True}} + self.assert_called('POST', '/servers/uuid1/action', body, pos=1) + self.assert_called('POST', '/servers/uuid2/action', body, pos=2) + self.assert_called('POST', '/servers/uuid3/action', body, pos=3) + self.assert_called('POST', '/servers/uuid4/action', body, pos=4) + def test_host_evacuate_live_with_disk_over_commit(self): self.run_command('host-evacuate-live --disk-over-commit hyper') self.assert_called('GET', '/os-hypervisors/hyper/servers', pos=0) @@ -1783,6 +1802,11 @@ def test_host_evacuate_live_with_disk_over_commit(self): self.assert_called('POST', '/servers/uuid3/action', body, pos=3) self.assert_called('POST', '/servers/uuid4/action', body, pos=4) + def test_host_evacuate_live_with_disk_over_commit_2_25(self): + self.assertRaises(SystemExit, self.run_command, + 'host-evacuate-live --disk-over-commit hyper', + api_version='2.25') + def test_host_evacuate_list_with_max_servers(self): self.run_command('host-evacuate-live --max-servers 1 hyper') self.assert_called('GET', '/os-hypervisors/hyper/servers', pos=0) diff --git a/novaclient/v2/contrib/host_evacuate_live.py b/novaclient/v2/contrib/host_evacuate_live.py index 794c3a8fa..badc4d1b3 100644 --- a/novaclient/v2/contrib/host_evacuate_live.py +++ b/novaclient/v2/contrib/host_evacuate_live.py @@ -27,8 +27,13 @@ def __init__(self, server_uuid, live_migration_accepted, success = True error_message = "" try: - cs.servers.live_migrate(server['uuid'], args.target_host, - args.block_migrate, args.disk_over_commit) + # API 2.0->2.24 + if 'disk_over_commit' in args: + cs.servers.live_migrate(server['uuid'], args.target_host, + args.block_migrate, args.disk_over_commit) + else: # API 2.25+ + cs.servers.live_migrate(server['uuid'], args.target_host, + args.block_migrate) except Exception as e: success = False error_message = _("Error while live migrating instance: %s") % e @@ -47,12 +52,20 @@ def __init__(self, server_uuid, live_migration_accepted, '--block-migrate', action='store_true', default=False, - help=_('Enable block migration.')) + help=_('Enable block migration. (Default=False)'), + start_version="2.0", end_version="2.24") +@utils.arg( + '--block-migrate', + action='store_true', + default="auto", + help=_('Enable block migration. (Default=auto)'), + start_version="2.25") @utils.arg( '--disk-over-commit', action='store_true', default=False, - help=_('Enable disk overcommit.')) + help=_('Enable disk overcommit.'), + start_version="2.0", end_version="2.24") @utils.arg( '--max-servers', type=int, From 31c9e399f2566a1985fbe01cc092c6a42ac5c2b3 Mon Sep 17 00:00:00 2001 From: huangtianhua Date: Mon, 28 Mar 2016 17:33:31 +0800 Subject: [PATCH 1025/1705] Using glance 'image-list'/'image-show' in boot help message Using glance commands instead in boot help message. Change-Id: I0f20d24a9ab32a80bbbc524e1b5ce754aded1814 --- novaclient/v2/shell.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 0b8f816ce..10f04aee1 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -370,14 +370,14 @@ def _boot(cs, args): '--image', default=None, metavar='', - help=_("Name or ID of image (see 'nova image-list'). ")) + help=_("Name or ID of image (see 'glance image-list'). ")) @utils.arg( '--image-with', default=[], type=_key_value_pairing, action='append', metavar='', - help=_("Image metadata property (see 'nova image-show'). ")) + help=_("Image metadata property (see 'glance image-show'). ")) @utils.arg( '--boot-volume', default=None, From 51306fc4c925e48d7904b3b56303bb71bb80610e Mon Sep 17 00:00:00 2001 From: Rui Chen Date: Wed, 30 Mar 2016 10:20:52 +0800 Subject: [PATCH 1026/1705] Fix ServerGroup.NAME_ATTR Finding by specify server group name failed in ServerGroupsManager.find(), ServerGroup.NAME_ATTR is "server_group_name" currently, but NAME_ATTR should be "name", because only "name" attribute exists in ServerGroup object, not "server_group_name". Change-Id: Id6b7676d14e6283d856a069da5ff287dc3228386 Closes-Bug: #1563301 --- .../tests/unit/v2/test_server_groups.py | 22 +++++++++++++++++++ novaclient/v2/server_groups.py | 1 - 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/novaclient/tests/unit/v2/test_server_groups.py b/novaclient/tests/unit/v2/test_server_groups.py index 4ca002df8..f942d4b0b 100644 --- a/novaclient/tests/unit/v2/test_server_groups.py +++ b/novaclient/tests/unit/v2/test_server_groups.py @@ -13,6 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient import exceptions from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit.fixture_data import server_groups as data from novaclient.tests.unit import utils @@ -71,3 +72,24 @@ def test_delete_server_group_object(self): ret = server_group.delete() self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('DELETE', '/os-server-groups/%s' % id) + + def test_find_server_groups_by_name(self): + expected_name = 'ig1' + kwargs = {self.cs.server_groups.resource_class.NAME_ATTR: + expected_name} + server_group = self.cs.server_groups.find(**kwargs) + self.assert_request_id(server_group, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('GET', '/os-server-groups') + self.assertIsInstance(server_group, server_groups.ServerGroup) + actual_name = getattr(server_group, + self.cs.server_groups.resource_class.NAME_ATTR) + self.assertEqual(expected_name, actual_name) + + def test_find_no_existing_server_groups_by_name(self): + expected_name = 'no-exist' + kwargs = {self.cs.server_groups.resource_class.NAME_ATTR: + expected_name} + self.assertRaises(exceptions.NotFound, + self.cs.server_groups.find, + **kwargs) + self.assert_called('GET', '/os-server-groups') diff --git a/novaclient/v2/server_groups.py b/novaclient/v2/server_groups.py index 9b0ae870d..77ee84f5b 100644 --- a/novaclient/v2/server_groups.py +++ b/novaclient/v2/server_groups.py @@ -24,7 +24,6 @@ class ServerGroup(base.Resource): """ A server group. """ - NAME_ATTR = 'server_group_name' def __repr__(self): return '' % self.id From 7d8db71964922197443f85c74ac0e4510e3f58a0 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Thu, 31 Mar 2016 18:18:42 +0300 Subject: [PATCH 1027/1705] [tests] initialize client objects inside setUp All tests from contrib dir inizialize fakeclient at module level. It is bad practice and we should do preparatory steps at setUp method. Change-Id: I2dd2988cd015d1dfb576fe004cb9461b02b55db3 --- .../contrib/test_assisted_volume_snapshots.py | 22 +++++----- .../tests/unit/v2/contrib/test_baremetal.py | 44 +++++++++---------- .../tests/unit/v2/contrib/test_cells.py | 27 ++++++------ .../unit/v2/contrib/test_instance_actions.py | 23 +++++----- .../unit/v2/contrib/test_list_extensions.py | 19 ++++---- .../tests/unit/v2/contrib/test_migrations.py | 27 ++++++------ .../v2/contrib/test_server_external_events.py | 19 ++++---- .../unit/v2/contrib/test_tenant_networks.py | 32 +++++++------- 8 files changed, 110 insertions(+), 103 deletions(-) diff --git a/novaclient/tests/unit/v2/contrib/test_assisted_volume_snapshots.py b/novaclient/tests/unit/v2/contrib/test_assisted_volume_snapshots.py index 8cce98664..374d2d847 100644 --- a/novaclient/tests/unit/v2/contrib/test_assisted_volume_snapshots.py +++ b/novaclient/tests/unit/v2/contrib/test_assisted_volume_snapshots.py @@ -22,23 +22,23 @@ from novaclient.v2.contrib import assisted_volume_snapshots as assisted_snaps -extensions = [ - extension.Extension(assisted_snaps.__name__.split(".")[-1], - assisted_snaps), -] -cs = fakes.FakeClient(extensions=extensions) - - class AssistedVolumeSnapshotsTestCase(utils.TestCase): + def setUp(self): + super(AssistedVolumeSnapshotsTestCase, self).setUp() + extensions = [ + extension.Extension(assisted_snaps.__name__.split(".")[-1], + assisted_snaps), + ] + self.cs = fakes.FakeClient(extensions=extensions) def test_create_snap(self): - vs = cs.assisted_volume_snapshots.create('1', {}) + vs = self.cs.assisted_volume_snapshots.create('1', {}) self.assert_request_id(vs, fakes.FAKE_REQUEST_ID_LIST) - cs.assert_called('POST', '/os-assisted-volume-snapshots') + self.cs.assert_called('POST', '/os-assisted-volume-snapshots') def test_delete_snap(self): - vs = cs.assisted_volume_snapshots.delete('x', {}) + vs = self.cs.assisted_volume_snapshots.delete('x', {}) self.assert_request_id(vs, fakes.FAKE_REQUEST_ID_LIST) - cs.assert_called( + self.cs.assert_called( 'DELETE', '/os-assisted-volume-snapshots/x?delete_info={}') diff --git a/novaclient/tests/unit/v2/contrib/test_baremetal.py b/novaclient/tests/unit/v2/contrib/test_baremetal.py index e44cd140e..406e617af 100644 --- a/novaclient/tests/unit/v2/contrib/test_baremetal.py +++ b/novaclient/tests/unit/v2/contrib/test_baremetal.py @@ -20,52 +20,52 @@ from novaclient.v2.contrib import baremetal -extensions = [ - extension.Extension(baremetal.__name__.split(".")[-1], baremetal), -] -cs = fakes.FakeClient(extensions=extensions) - - class BaremetalExtensionTest(utils.TestCase): + def setUp(self): + super(BaremetalExtensionTest, self).setUp() + extensions = [ + extension.Extension(baremetal.__name__.split(".")[-1], baremetal), + ] + self.cs = fakes.FakeClient(extensions=extensions) def test_list_nodes(self): - nl = cs.baremetal.list() + nl = self.cs.baremetal.list() self.assert_request_id(nl, fakes.FAKE_REQUEST_ID_LIST) - cs.assert_called('GET', '/os-baremetal-nodes') + self.cs.assert_called('GET', '/os-baremetal-nodes') for n in nl: self.assertIsInstance(n, baremetal.BareMetalNode) def test_get_node(self): - n = cs.baremetal.get(1) + n = self.cs.baremetal.get(1) self.assert_request_id(n, fakes.FAKE_REQUEST_ID_LIST) - cs.assert_called('GET', '/os-baremetal-nodes/1') + self.cs.assert_called('GET', '/os-baremetal-nodes/1') self.assertIsInstance(n, baremetal.BareMetalNode) def test_create_node(self): - n = cs.baremetal.create("service_host", 1, 1024, 2048, - "aa:bb:cc:dd:ee:ff") + n = self.cs.baremetal.create("service_host", 1, 1024, 2048, + "aa:bb:cc:dd:ee:ff") self.assert_request_id(n, fakes.FAKE_REQUEST_ID_LIST) - cs.assert_called('POST', '/os-baremetal-nodes') + self.cs.assert_called('POST', '/os-baremetal-nodes') self.assertIsInstance(n, baremetal.BareMetalNode) def test_delete_node(self): - n = cs.baremetal.get(1) - ret = cs.baremetal.delete(n) + n = self.cs.baremetal.get(1) + ret = self.cs.baremetal.delete(n) self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) - cs.assert_called('DELETE', '/os-baremetal-nodes/1') + self.cs.assert_called('DELETE', '/os-baremetal-nodes/1') def test_node_add_interface(self): - i = cs.baremetal.add_interface(1, "bb:cc:dd:ee:ff:aa", 1, 2) + i = self.cs.baremetal.add_interface(1, "bb:cc:dd:ee:ff:aa", 1, 2) self.assert_request_id(i, fakes.FAKE_REQUEST_ID_LIST) - cs.assert_called('POST', '/os-baremetal-nodes/1/action') + self.cs.assert_called('POST', '/os-baremetal-nodes/1/action') self.assertIsInstance(i, baremetal.BareMetalNodeInterface) def test_node_remove_interface(self): - ret = cs.baremetal.remove_interface(1, "bb:cc:dd:ee:ff:aa") + ret = self.cs.baremetal.remove_interface(1, "bb:cc:dd:ee:ff:aa") self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) - cs.assert_called('POST', '/os-baremetal-nodes/1/action') + self.cs.assert_called('POST', '/os-baremetal-nodes/1/action') def test_node_list_interfaces(self): - il = cs.baremetal.list_interfaces(1) + il = self.cs.baremetal.list_interfaces(1) self.assert_request_id(il, fakes.FAKE_REQUEST_ID_LIST) - cs.assert_called('GET', '/os-baremetal-nodes/1') + self.cs.assert_called('GET', '/os-baremetal-nodes/1') diff --git a/novaclient/tests/unit/v2/contrib/test_cells.py b/novaclient/tests/unit/v2/contrib/test_cells.py index dd242c2b7..95a5c4b4e 100644 --- a/novaclient/tests/unit/v2/contrib/test_cells.py +++ b/novaclient/tests/unit/v2/contrib/test_cells.py @@ -19,27 +19,28 @@ from novaclient.v2.contrib import cells -extensions = [ - extension.Extension(cells.__name__.split(".")[-1], - cells), -] -cs = fakes.FakeClient(extensions=extensions) - - class CellsExtensionTests(utils.TestCase): + def setUp(self): + super(CellsExtensionTests, self).setUp() + extensions = [ + extension.Extension(cells.__name__.split(".")[-1], + cells), + ] + self.cs = fakes.FakeClient(extensions=extensions) + def test_get_cells(self): cell_name = 'child_cell' - cell = cs.cells.get(cell_name) + cell = self.cs.cells.get(cell_name) self.assert_request_id(cell, fakes.FAKE_REQUEST_ID_LIST) - cs.assert_called('GET', '/os-cells/%s' % cell_name) + self.cs.assert_called('GET', '/os-cells/%s' % cell_name) def test_get_capacities_for_a_given_cell(self): cell_name = 'child_cell' - ca = cs.cells.capacities(cell_name) + ca = self.cs.cells.capacities(cell_name) self.assert_request_id(ca, fakes.FAKE_REQUEST_ID_LIST) - cs.assert_called('GET', '/os-cells/%s/capacities' % cell_name) + self.cs.assert_called('GET', '/os-cells/%s/capacities' % cell_name) def test_get_capacities_for_all_cells(self): - ca = cs.cells.capacities() + ca = self.cs.cells.capacities() self.assert_request_id(ca, fakes.FAKE_REQUEST_ID_LIST) - cs.assert_called('GET', '/os-cells/capacities') + self.cs.assert_called('GET', '/os-cells/capacities') diff --git a/novaclient/tests/unit/v2/contrib/test_instance_actions.py b/novaclient/tests/unit/v2/contrib/test_instance_actions.py index 4f17fbd59..394b37e40 100644 --- a/novaclient/tests/unit/v2/contrib/test_instance_actions.py +++ b/novaclient/tests/unit/v2/contrib/test_instance_actions.py @@ -19,27 +19,28 @@ from novaclient.v2.contrib import instance_action -extensions = [ - extension.Extension(instance_action.__name__.split(".")[-1], - instance_action), -] -cs = fakes.FakeClient(extensions=extensions) - - class InstanceActionExtensionTests(utils.TestCase): + def setUp(self): + super(InstanceActionExtensionTests, self).setUp() + extensions = [ + extension.Extension(instance_action.__name__.split(".")[-1], + instance_action), + ] + self.cs = fakes.FakeClient(extensions=extensions) + def test_list_instance_actions(self): server_uuid = '1234' - ial = cs.instance_action.list(server_uuid) + ial = self.cs.instance_action.list(server_uuid) self.assert_request_id(ial, fakes.FAKE_REQUEST_ID_LIST) - cs.assert_called( + self.cs.assert_called( 'GET', '/servers/%s/os-instance-actions' % server_uuid) def test_get_instance_action(self): server_uuid = '1234' request_id = 'req-abcde12345' - ia = cs.instance_action.get(server_uuid, request_id) + ia = self.cs.instance_action.get(server_uuid, request_id) self.assert_request_id(ia, fakes.FAKE_REQUEST_ID_LIST) - cs.assert_called( + self.cs.assert_called( 'GET', '/servers/%s/os-instance-actions/%s' % (server_uuid, request_id)) diff --git a/novaclient/tests/unit/v2/contrib/test_list_extensions.py b/novaclient/tests/unit/v2/contrib/test_list_extensions.py index 3c7729397..5c6d369d2 100644 --- a/novaclient/tests/unit/v2/contrib/test_list_extensions.py +++ b/novaclient/tests/unit/v2/contrib/test_list_extensions.py @@ -17,18 +17,19 @@ from novaclient.v2.contrib import list_extensions -extensions = [ - extension.Extension(list_extensions.__name__.split(".")[-1], - list_extensions), -] -cs = fakes.FakeClient(extensions=extensions) - - class ListExtensionsTests(utils.TestCase): + def setUp(self): + super(ListExtensionsTests, self).setUp() + extensions = [ + extension.Extension(list_extensions.__name__.split(".")[-1], + list_extensions), + ] + self.cs = fakes.FakeClient(extensions=extensions) + def test_list_extensions(self): - all_exts = cs.list_extensions.show_all() + all_exts = self.cs.list_extensions.show_all() self.assert_request_id(all_exts, fakes.FAKE_REQUEST_ID_LIST) - cs.assert_called('GET', '/extensions') + self.cs.assert_called('GET', '/extensions') self.assertTrue(len(all_exts) > 0) for r in all_exts: self.assertTrue(len(r.summary) > 0) diff --git a/novaclient/tests/unit/v2/contrib/test_migrations.py b/novaclient/tests/unit/v2/contrib/test_migrations.py index 8d12633e1..d203e64ca 100644 --- a/novaclient/tests/unit/v2/contrib/test_migrations.py +++ b/novaclient/tests/unit/v2/contrib/test_migrations.py @@ -16,25 +16,26 @@ from novaclient.tests.unit.v2 import fakes from novaclient.v2.contrib import migrations -extensions = [ - extension.Extension(migrations.__name__.split(".")[-1], - migrations), -] -cs = fakes.FakeClient(extensions=extensions) - class MigrationsTest(utils.TestCase): + def setUp(self): + super(MigrationsTest, self).setUp() + self.extensions = [ + extension.Extension(migrations.__name__.split(".")[-1], + migrations), + ] + self.cs = fakes.FakeClient(extensions=self.extensions) def test_list_migrations(self): - ml = cs.migrations.list() + ml = self.cs.migrations.list() self.assert_request_id(ml, fakes.FAKE_REQUEST_ID_LIST) - cs.assert_called('GET', '/os-migrations') + self.cs.assert_called('GET', '/os-migrations') for m in ml: self.assertIsInstance(m, migrations.Migration) self.assertRaises(AttributeError, getattr, m, "migration_type") def test_list_migrations_v223(self): - cs = fakes.FakeClient(extensions=extensions, + cs = fakes.FakeClient(extensions=self.extensions, api_version=api_versions.APIVersion("2.23")) ml = cs.migrations.list() self.assert_request_id(ml, fakes.FAKE_REQUEST_ID_LIST) @@ -44,11 +45,11 @@ def test_list_migrations_v223(self): self.assertEqual(m.migration_type, 'live-migration') def test_list_migrations_with_filters(self): - ml = cs.migrations.list('host1', 'finished', 'child1') + ml = self.cs.migrations.list('host1', 'finished', 'child1') self.assert_request_id(ml, fakes.FAKE_REQUEST_ID_LIST) - cs.assert_called('GET', - '/os-migrations?cell_name=child1&host=host1' - '&status=finished') + self.cs.assert_called( + 'GET', + '/os-migrations?cell_name=child1&host=host1&status=finished') for m in ml: self.assertIsInstance(m, migrations.Migration) diff --git a/novaclient/tests/unit/v2/contrib/test_server_external_events.py b/novaclient/tests/unit/v2/contrib/test_server_external_events.py index b843bd8c1..009a0d83a 100644 --- a/novaclient/tests/unit/v2/contrib/test_server_external_events.py +++ b/novaclient/tests/unit/v2/contrib/test_server_external_events.py @@ -22,14 +22,15 @@ from novaclient.v2.contrib import server_external_events as ext_events -extensions = [ - extension.Extension(ext_events.__name__.split(".")[-1], - ext_events), -] -cs = fakes.FakeClient(extensions=extensions) - - class ServerExternalEventsTestCase(utils.TestCase): + def setUp(self): + super(ServerExternalEventsTestCase, self).setUp() + extensions = [ + extension.Extension(ext_events.__name__.split(".")[-1], + ext_events), + ] + self.cs = fakes.FakeClient(extensions=extensions) + def test_external_event(self): events = [{'server_uuid': 'fake-uuid1', 'name': 'test-event', @@ -39,7 +40,7 @@ def test_external_event(self): 'name': 'test-event', 'status': 'completed', 'tag': 'tag'}] - result = cs.server_external_events.create(events) + result = self.cs.server_external_events.create(events) self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) self.assertEqual(events, result) - cs.assert_called('POST', '/os-server-external-events') + self.cs.assert_called('POST', '/os-server-external-events') diff --git a/novaclient/tests/unit/v2/contrib/test_tenant_networks.py b/novaclient/tests/unit/v2/contrib/test_tenant_networks.py index 2f0d7e3f9..e19942729 100644 --- a/novaclient/tests/unit/v2/contrib/test_tenant_networks.py +++ b/novaclient/tests/unit/v2/contrib/test_tenant_networks.py @@ -19,32 +19,34 @@ from novaclient.v2.contrib import tenant_networks -extensions = [ - extension.Extension(tenant_networks.__name__.split(".")[-1], - tenant_networks), -] -cs = fakes.FakeClient(extensions=extensions) +class TenantNetworkExtensionTests(utils.TestCase): + def setUp(self): + super(TenantNetworkExtensionTests, self).setUp() + extensions = [ + extension.Extension(tenant_networks.__name__.split(".")[-1], + tenant_networks), + ] + self.cs = fakes.FakeClient(extensions=extensions) -class TenantNetworkExtensionTests(utils.TestCase): def test_list_tenant_networks(self): - nets = cs.tenant_networks.list() + nets = self.cs.tenant_networks.list() self.assert_request_id(nets, fakes.FAKE_REQUEST_ID_LIST) - cs.assert_called('GET', '/os-tenant-networks') + self.cs.assert_called('GET', '/os-tenant-networks') self.assertTrue(len(nets) > 0) def test_get_tenant_network(self): - net = cs.tenant_networks.get(1) + net = self.cs.tenant_networks.get(1) self.assert_request_id(net, fakes.FAKE_REQUEST_ID_LIST) - cs.assert_called('GET', '/os-tenant-networks/1') + self.cs.assert_called('GET', '/os-tenant-networks/1') def test_create_tenant_networks(self): - net = cs.tenant_networks.create(label="net", - cidr="10.0.0.0/24") + net = self.cs.tenant_networks.create(label="net", + cidr="10.0.0.0/24") self.assert_request_id(net, fakes.FAKE_REQUEST_ID_LIST) - cs.assert_called('POST', '/os-tenant-networks') + self.cs.assert_called('POST', '/os-tenant-networks') def test_delete_tenant_networks(self): - ret = cs.tenant_networks.delete(1) + ret = self.cs.tenant_networks.delete(1) self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) - cs.assert_called('DELETE', '/os-tenant-networks/1') + self.cs.assert_called('DELETE', '/os-tenant-networks/1') From b9807c06485a231cde5ec4df40883dbce53b6bd7 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Fri, 1 Apr 2016 14:45:12 +0300 Subject: [PATCH 1028/1705] Switch to 2.1 default api_version in v2.Client Direct initialization of versioned clients is restricted, but we tries to decrease the number of possible bugs and issues, so default value for api_version was added to v2.Client . Let's set it to 2.1, since 2.0 is too old API version. Change-Id: If127f858f7f670a090a57267f46a69ea4c056cce --- novaclient/v2/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/v2/client.py b/novaclient/v2/client.py index 56339a346..64a1458e1 100644 --- a/novaclient/v2/client.py +++ b/novaclient/v2/client.py @@ -131,7 +131,7 @@ def __init__(self, username=None, api_key=None, project_id=None, self.limits = limits.LimitsManager(self) self.servers = servers.ServerManager(self) self.versions = versions.VersionManager(self) - self.api_version = api_version or api_versions.APIVersion("2.0") + self.api_version = api_version or api_versions.APIVersion("2.1") # extensions self.agents = agents.AgentsManager(self) From a42570268915f42405ed0b0a67c25686b5db22ce Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Fri, 1 Apr 2016 14:44:09 -0400 Subject: [PATCH 1029/1705] Remove deprecated volume(snapshot) commands/bindings 23f13437dd64496fcbc138bbaa9b0ac615a3cf23 deprecated the purely volume or volume-snapshot related CLIs and python API bindings during Kilo. python-cinderclient should be used for the python API bindings now, and python-openstackclient should be used for the CLI for those operations. The alternate_service_type context manager is also removed since it was only used for proxying through to the volume API endpoint. Since the proxy for creating volumes is removed, we have to add python-cinderclient for testing the volume attachment CLIs/APIs that are left intact. Change-Id: I09a6501603667350f49b1b1fa130353a6d5272a2 --- novaclient/base.py | 12 - novaclient/tests/functional/base.py | 6 +- .../v2/legacy/test_extended_attributes.py | 4 +- .../functional/v2/legacy/test_instances.py | 8 +- .../v2/legacy/test_readonly_nova.py | 11 - .../functional/v2/legacy/test_servers.py | 4 +- .../functional/v2/legacy/test_volumes_api.py | 92 ------- .../tests/functional/v2/test_volumes_api.py | 18 -- novaclient/tests/unit/test_service_catalog.py | 13 - novaclient/tests/unit/v2/test_shell.py | 43 --- novaclient/tests/unit/v2/test_volumes.py | 59 ----- novaclient/v2/client.py | 4 - novaclient/v2/shell.py | 244 ------------------ novaclient/v2/volume_snapshots.py | 120 --------- novaclient/v2/volume_types.py | 103 -------- novaclient/v2/volumes.py | 107 +------- .../volume-cli-removal-ffcb94421a356042.yaml | 6 + test-requirements.txt | 1 + 18 files changed, 21 insertions(+), 834 deletions(-) delete mode 100644 novaclient/tests/functional/v2/legacy/test_volumes_api.py delete mode 100644 novaclient/tests/functional/v2/test_volumes_api.py delete mode 100644 novaclient/v2/volume_snapshots.py delete mode 100644 novaclient/v2/volume_types.py create mode 100644 releasenotes/notes/volume-cli-removal-ffcb94421a356042.yaml diff --git a/novaclient/base.py b/novaclient/base.py index 154c20155..dcb57a472 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -259,18 +259,6 @@ def _list(self, url, response_key, obj_class=None, body=None): for res in data if res] return ListWithMeta(items, resp) - @contextlib.contextmanager - def alternate_service_type(self, default, allowed_types=()): - original_service_type = self.api.client.service_type - if original_service_type in allowed_types: - yield - else: - self.api.client.service_type = default - try: - yield - finally: - self.api.client.service_type = original_service_type - @contextlib.contextmanager def completion_cache(self, cache_type, obj_class, mode): """The completion cache for bash autocompletion. diff --git a/novaclient/tests/functional/base.py b/novaclient/tests/functional/base.py index 9595915b2..c58f5b4c3 100644 --- a/novaclient/tests/functional/base.py +++ b/novaclient/tests/functional/base.py @@ -14,6 +14,7 @@ import time import uuid +from cinderclient.v2 import client as cinderclient import fixtures from keystoneclient.v2_0 import client as keystoneclient import os_client_config @@ -206,6 +207,7 @@ def setUp(self): password=passwd, tenant_name=tenant, auth_url=auth_url) + self.cinder = cinderclient.Client(user, passwd, tenant, auth_url) def nova(self, action, flags='', params='', fail_ok=False, endpoint_type='publicURL', merge_stderr=False): @@ -218,14 +220,14 @@ def wait_for_volume_status(self, volume, status, timeout=60, poll_interval=1): """Wait until volume reaches given status. - :param volume_id: uuid4 id of given volume + :param volume: volume resource :param status: expected status of volume :param timeout: timeout in seconds :param poll_interval: poll interval in seconds """ start_time = time.time() while time.time() - start_time < timeout: - volume = self.client.volumes.get(volume.id) + volume = self.cinder.volumes.get(volume.id) if volume.status == status: break time.sleep(poll_interval) diff --git a/novaclient/tests/functional/v2/legacy/test_extended_attributes.py b/novaclient/tests/functional/v2/legacy/test_extended_attributes.py index ef579e91f..06f4008ee 100644 --- a/novaclient/tests/functional/v2/legacy/test_extended_attributes.py +++ b/novaclient/tests/functional/v2/legacy/test_extended_attributes.py @@ -22,8 +22,8 @@ class TestExtAttrNovaClient(base.ClientTestBase): def _create_server_and_attach_volume(self): server = self._create_server() - volume = self.client.volumes.create(1) - self.addCleanup(self.nova, 'volume-delete', params=volume.id) + volume = self.cinder.volumes.create(1) + self.addCleanup(volume.delete) self.wait_for_volume_status(volume, 'available') self.nova('volume-attach', params="%s %s" % (server.name, volume.id)) self.addCleanup(self._release_volume, server, volume) diff --git a/novaclient/tests/functional/v2/legacy/test_instances.py b/novaclient/tests/functional/v2/legacy/test_instances.py index 39e78ee51..3ff07b44b 100644 --- a/novaclient/tests/functional/v2/legacy/test_instances.py +++ b/novaclient/tests/functional/v2/legacy/test_instances.py @@ -52,10 +52,9 @@ def test_attach_volume(self): server = servers[0] self.addCleanup(server.delete) - # create a volume for attachment. We use the CLI because it - # magic routes to cinder, however the low level API does not. - volume = self.client.volumes.create(1) - self.addCleanup(self.nova, 'volume-delete', params=volume.id) + # create a volume for attachment + volume = self.cinder.volumes.create(1) + self.addCleanup(volume.delete) # allow volume to become available self.wait_for_volume_status(volume, 'available') @@ -69,4 +68,3 @@ def test_attach_volume(self): # clean up on success self.nova('volume-detach', params="%s %s" % (name, volume.id)) self.wait_for_volume_status(volume, 'available') - self.nova('volume-delete', params=volume.id) diff --git a/novaclient/tests/functional/v2/legacy/test_readonly_nova.py b/novaclient/tests/functional/v2/legacy/test_readonly_nova.py index 47e5844c3..80ff4f086 100644 --- a/novaclient/tests/functional/v2/legacy/test_readonly_nova.py +++ b/novaclient/tests/functional/v2/legacy/test_readonly_nova.py @@ -131,17 +131,6 @@ def test_admin_usage(self): def test_admin_usage_list(self): self.nova('usage-list') - def test_admin_volume_list(self): - self.nova('volume-list') - - def test_admin_volume_snapshot_list(self): - out = self.nova('volume-snapshot-list', merge_stderr=True) - self.assertIn('Command volume-snapshot-list is deprecated', out) - - def test_admin_volume_type_list(self): - out = self.nova('volume-type-list', merge_stderr=True) - self.assertIn('Command volume-type-list is deprecated', out) - def test_admin_help(self): self.nova('help') diff --git a/novaclient/tests/functional/v2/legacy/test_servers.py b/novaclient/tests/functional/v2/legacy/test_servers.py index 34cf18569..a06b3037a 100644 --- a/novaclient/tests/functional/v2/legacy/test_servers.py +++ b/novaclient/tests/functional/v2/legacy/test_servers.py @@ -24,8 +24,8 @@ class TestServersBootNovaClient(base.ClientTestBase): def _boot_server_with_legacy_bdm(self, bdm_params=()): volume_size = 1 volume_name = str(uuid.uuid4()) - volume = self.client.volumes.create(size=volume_size, - display_name=volume_name, + volume = self.cinder.volumes.create(size=volume_size, + name=volume_name, imageRef=self.image.id) self.wait_for_volume_status(volume, "available") diff --git a/novaclient/tests/functional/v2/legacy/test_volumes_api.py b/novaclient/tests/functional/v2/legacy/test_volumes_api.py deleted file mode 100644 index b574ad783..000000000 --- a/novaclient/tests/functional/v2/legacy/test_volumes_api.py +++ /dev/null @@ -1,92 +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. - -import time - -import six.moves - -from novaclient import exceptions -from novaclient.tests.functional import base - - -def wait_for_delete(test, name, thing, get_func): - thing.delete() - for x in six.moves.range(60): - try: - thing = get_func(thing.id) - except exceptions.NotFound: - break - time.sleep(1) - else: - test.fail('%s %s still not deleted after 60s' % (name, thing.id)) - - -class TestVolumesAPI(base.ClientTestBase): - - COMPUTE_API_VERSION = "2.1" - - def test_volumes_snapshots_types_create_get_list_delete(self): - # Create a volume - volume = self.client.volumes.create(1) - - # Make sure we can still list servers after using the volume endpoint - self.client.servers.list() - - # This cleanup tests volume delete - self.addCleanup(volume.delete) - - # Wait for the volume to become available - self.wait_for_volume_status(volume, 'available') - - # List all volumes - self.client.volumes.list() - - # Create a volume snapshot - snapshot = self.client.volume_snapshots.create(volume.id) - - # This cleanup tests volume snapshot delete. The volume - # can't be deleted until the dependent snapshot is gone - self.addCleanup(wait_for_delete, self, 'Snapshot', snapshot, - self.client.volume_snapshots.get) - - # Wait for the snapshot to become available - for x in six.moves.range(60): - snapshot = self.client.volume_snapshots.get(snapshot.id) - if snapshot.status == 'available': - break - elif snapshot.status == 'error': - self.fail('Snapshot %s is in error state' % snapshot.id) - time.sleep(1) - else: - self.fail('Snapshot %s not available after 60s' % snapshot.id) - - # List snapshots - self.client.volume_snapshots.list() - - # List servers again to make sure things are still good - self.client.servers.list() - - # Create a volume type - name = self.name_generate('VolumeType') - volume_type = self.client.volume_types.create(name) - - # This cleanup tests volume type delete - self.addCleanup(self.client.volume_types.delete, volume_type.id) - - # Get the volume type - volume_type = self.client.volume_types.get(volume_type.id) - - # List all volume types - self.client.volume_types.list() - - # One more servers list - self.client.servers.list() diff --git a/novaclient/tests/functional/v2/test_volumes_api.py b/novaclient/tests/functional/v2/test_volumes_api.py deleted file mode 100644 index 8bf35d508..000000000 --- a/novaclient/tests/functional/v2/test_volumes_api.py +++ /dev/null @@ -1,18 +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 novaclient.tests.functional.v2.legacy import test_volumes_api - - -class TestVolumesAPI(test_volumes_api.TestVolumesAPI): - - COMPUTE_API_VERSION = "2.latest" diff --git a/novaclient/tests/unit/test_service_catalog.py b/novaclient/tests/unit/test_service_catalog.py index ff19dcd5b..bd3e6cb22 100644 --- a/novaclient/tests/unit/test_service_catalog.py +++ b/novaclient/tests/unit/test_service_catalog.py @@ -58,16 +58,3 @@ def test_building_a_service_catalog_insensitive_case(self): # Matching south (and catalog has South). self.assertRaises(exceptions.AmbiguousEndpoints, sc.url_for, 'region', 'south', service_type='volume') - - def test_alternate_service_type(self): - sc = service_catalog.ServiceCatalog(SERVICE_CATALOG) - - self.assertRaises(exceptions.AmbiguousEndpoints, sc.url_for, - service_type='volume') - self.assertEqual("https://volume1.host/v1/1", - sc.url_for('tenantId', '1', service_type='volume')) - self.assertEqual("https://volume1.host/v1.1/2", - sc.url_for('tenantId', '2', service_type='volume')) - - self.assertRaises(exceptions.EndpointNotFound, sc.url_for, - "region", "North", service_type='volume') diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 30728b0c4..1258a6965 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2488,53 +2488,10 @@ def test_interface_detach(self): self.run_command('interface-detach 1234 port_id') self.assert_called('DELETE', '/servers/1234/os-interface/port_id') - def test_volume_list(self): - _, err = self.run_command('volume-list') - self.assertIn('Command volume-list is deprecated', err) - self.assert_called('GET', '/volumes/detail') - - def test_volume_show(self): - _, err = self.run_command('volume-show Work') - self.assertIn('Command volume-show is deprecated', err) - self.assert_called('GET', '/volumes?display_name=Work', pos=-2) - self.assert_called( - 'GET', - '/volumes/15e59938-07d5-11e1-90e3-e3dffe0c5983', - pos=-1 - ) - def test_volume_attachments(self): self.run_command('volume-attachments 1234') self.assert_called('GET', '/servers/1234/os-volume_attachments') - def test_volume_create(self): - _, err = self.run_command('volume-create 2 --display-name Work') - self.assertIn('Command volume-create is deprecated', err) - self.assert_called('POST', '/volumes', - {'volume': - {'display_name': 'Work', - 'imageRef': None, - 'availability_zone': None, - 'volume_type': None, - 'display_description': None, - 'snapshot_id': None, - 'size': 2}}) - - def test_volume_delete(self): - _, err = self.run_command('volume-delete Work') - self.assertIn('Command volume-delete is deprecated', err) - self.assert_called('DELETE', - '/volumes/15e59938-07d5-11e1-90e3-e3dffe0c5983') - - def test_volume_delete_multiple(self): - self.run_command('volume-delete Work Work2') - self.assert_called('DELETE', - '/volumes/15e59938-07d5-11e1-90e3-e3dffe0c5983', - pos=-4) - self.assert_called('DELETE', - '/volumes/15e59938-07d5-11e1-90e3-ee32ba30feaa', - pos=-1) - def test_volume_attach(self): self.run_command('volume-attach sample-server Work /dev/vdb') self.assert_called('POST', '/servers/1234/os-volume_attachments', diff --git a/novaclient/tests/unit/v2/test_volumes.py b/novaclient/tests/unit/v2/test_volumes.py index caaf20f66..6ff7367e2 100644 --- a/novaclient/tests/unit/v2/test_volumes.py +++ b/novaclient/tests/unit/v2/test_volumes.py @@ -13,10 +13,6 @@ # License for the specific language governing permissions and limitations # under the License. -import warnings - -import mock - from novaclient.tests.unit import utils from novaclient.tests.unit.v2 import fakes from novaclient.v2 import volumes @@ -27,61 +23,6 @@ class VolumesTest(utils.TestCase): - @mock.patch.object(warnings, 'warn') - def test_list_volumes(self, mock_warn): - vl = cs.volumes.list() - self.assert_request_id(vl, fakes.FAKE_REQUEST_ID_LIST) - cs.assert_called('GET', '/volumes/detail') - for v in vl: - self.assertIsInstance(v, volumes.Volume) - self.assertEqual(1, mock_warn.call_count) - - @mock.patch.object(warnings, 'warn') - def test_list_volumes_undetailed(self, mock_warn): - vl = cs.volumes.list(detailed=False) - self.assert_request_id(vl, fakes.FAKE_REQUEST_ID_LIST) - cs.assert_called('GET', '/volumes') - for v in vl: - self.assertIsInstance(v, volumes.Volume) - self.assertEqual(1, mock_warn.call_count) - - @mock.patch.object(warnings, 'warn') - def test_get_volume_details(self, mock_warn): - vol_id = '15e59938-07d5-11e1-90e3-e3dffe0c5983' - v = cs.volumes.get(vol_id) - self.assert_request_id(v, fakes.FAKE_REQUEST_ID_LIST) - cs.assert_called('GET', '/volumes/%s' % vol_id) - self.assertIsInstance(v, volumes.Volume) - self.assertEqual(v.id, vol_id) - self.assertEqual(1, mock_warn.call_count) - - @mock.patch.object(warnings, 'warn') - def test_create_volume(self, mock_warn): - v = cs.volumes.create( - size=2, - display_name="My volume", - display_description="My volume desc", - ) - self.assert_request_id(v, fakes.FAKE_REQUEST_ID_LIST) - cs.assert_called('POST', '/volumes') - self.assertIsInstance(v, volumes.Volume) - self.assertEqual(1, mock_warn.call_count) - - @mock.patch.object(warnings, 'warn') - def test_delete_volume(self, mock_warn): - vol_id = '15e59938-07d5-11e1-90e3-e3dffe0c5983' - v = cs.volumes.get(vol_id) - ret = v.delete() - self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) - cs.assert_called('DELETE', '/volumes/%s' % vol_id) - ret = cs.volumes.delete(vol_id) - self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) - cs.assert_called('DELETE', '/volumes/%s' % vol_id) - ret = cs.volumes.delete(v) - self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) - cs.assert_called('DELETE', '/volumes/%s' % vol_id) - self.assertEqual(4, mock_warn.call_count) - def test_create_server_volume(self): v = cs.volumes.create_server_volume( server_id=1234, diff --git a/novaclient/v2/client.py b/novaclient/v2/client.py index 56339a346..5bbd32b27 100644 --- a/novaclient/v2/client.py +++ b/novaclient/v2/client.py @@ -47,8 +47,6 @@ from novaclient.v2 import usage from novaclient.v2 import versions from novaclient.v2 import virtual_interfaces -from novaclient.v2 import volume_snapshots -from novaclient.v2 import volume_types from novaclient.v2 import volumes @@ -143,8 +141,6 @@ def __init__(self, username=None, api_key=None, project_id=None, self.floating_ip_pools = floating_ip_pools.FloatingIPPoolManager(self) self.fping = fping.FpingManager(self) self.volumes = volumes.VolumeManager(self) - self.volume_snapshots = volume_snapshots.SnapshotManager(self) - self.volume_types = volume_types.VolumeTypeManager(self) self.keypairs = keypairs.KeypairManager(self) self.networks = networks.NetworkManager(self) self.quota_classes = quota_classes.QuotaClassSetManager(self) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 0b8f816ce..0baf5ab56 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -66,14 +66,6 @@ } -# NOTE(mriedem): Remove this along with the deprecated commands in the first -# python-novaclient release AFTER the nova server 13.0.0 'M' release. -def emit_volume_deprecation_warning(command_name): - print('WARNING: Command %s is deprecated and will be removed after Nova ' - '13.0.0 is released. Use python-cinderclient or openstackclient ' - 'instead.' % command_name, file=sys.stderr) - - def _key_value_pairing(text): try: (k, v) = text.split('=', 1) @@ -2249,137 +2241,6 @@ def _translate_volume_attachments_keys(collection): ('volumeId', 'volume_id')]) -@utils.arg( - '--all-tenants', - dest='all_tenants', - metavar='<0|1>', - nargs='?', - type=int, - const=1, - default=int(strutils.bool_from_string( - os.environ.get("ALL_TENANTS", 'false'), True)), - help=_('Display information from all tenants (Admin only).')) -@utils.arg( - '--all_tenants', - nargs='?', - type=int, - const=1, - action=shell.DeprecatedAction, - use=_('use "%s"; this option will be removed in ' - 'novaclient 3.3.0.') % '--all-tenants', - help=argparse.SUPPRESS) -def do_volume_list(cs, args): - """DEPRECATED: List all the volumes.""" - emit_volume_deprecation_warning('volume-list') - search_opts = {'all_tenants': args.all_tenants} - volumes = cs.volumes.list(search_opts=search_opts) - _translate_volume_keys(volumes) - - # Create a list of servers to which the volume is attached - for vol in volumes: - servers = [s.get('server_id') for s in vol.attachments] - setattr(vol, 'attached_to', ','.join(map(str, servers))) - utils.print_list(volumes, ['ID', 'Status', 'Display Name', - 'Size', 'Volume Type', 'Attached to']) - - -@utils.arg( - 'volume', - metavar='', - help=_('Name or ID of the volume.')) -def do_volume_show(cs, args): - """DEPRECATED: Show details about a volume.""" - emit_volume_deprecation_warning('volume-show') - volume = _find_volume(cs, args.volume) - _print_volume(volume) - - -@utils.arg( - 'size', - metavar='', - type=int, - help=_('Size of volume in GB')) -@utils.arg( - '--snapshot-id', - metavar='', - default=None, - help=_('Optional snapshot ID to create the volume from. (Default=None)')) -@utils.arg( - '--snapshot_id', - action=shell.DeprecatedAction, - use=_('use "%s"; this option will be removed in ' - 'novaclient 3.3.0.') % '--snapshot-id', - help=argparse.SUPPRESS) -@utils.arg( - '--image-id', - metavar='', - help=_('Optional image ID to create the volume from. (Default=None)'), - default=None) -@utils.arg( - '--display-name', - metavar='', - default=None, - help=_('Optional volume name. (Default=None)')) -@utils.arg( - '--display_name', - action=shell.DeprecatedAction, - use=_('use "%s"; this option will be removed in ' - 'novaclient 3.3.0.') % '--display-name', - help=argparse.SUPPRESS) -@utils.arg( - '--display-description', - metavar='', - default=None, - help=_('Optional volume description. (Default=None)')) -@utils.arg( - '--display_description', - action=shell.DeprecatedAction, - use=_('use "%s"; this option will be removed in ' - 'novaclient 3.3.0.') % '--display-description', - help=argparse.SUPPRESS) -@utils.arg( - '--volume-type', - metavar='', - default=None, - help=_('Optional volume type. (Default=None)')) -@utils.arg( - '--volume_type', - action=shell.DeprecatedAction, - use=_('use "%s"; this option will be removed in ' - 'novaclient 3.3.0.') % '--volume-type', - help=argparse.SUPPRESS) -@utils.arg( - '--availability-zone', metavar='', - help=_('Optional Availability Zone for volume. (Default=None)'), - default=None) -def do_volume_create(cs, args): - """DEPRECATED: Add a new volume.""" - emit_volume_deprecation_warning('volume-create') - volume = cs.volumes.create(args.size, - args.snapshot_id, - args.display_name, - args.display_description, - args.volume_type, - args.availability_zone, - imageRef=args.image_id) - _print_volume(volume) - - -@utils.arg( - 'volume', - metavar='', nargs='+', - help=_('Name or ID of the volume(s) to delete.')) -def do_volume_delete(cs, args): - """DEPRECATED: Remove volume(s).""" - emit_volume_deprecation_warning('volume-delete') - for volume in args.volume: - try: - _find_volume(cs, volume).delete() - except Exception as e: - print(_("Delete for volume %(volume)s failed: %(e)s") % - {'volume': volume, 'e': e}) - - @utils.arg( 'server', metavar='', @@ -2448,111 +2309,6 @@ def do_volume_attachments(cs, args): utils.print_list(volumes, ['ID', 'DEVICE', 'SERVER ID', 'VOLUME ID']) -def do_volume_snapshot_list(cs, _args): - """DEPRECATED: List all the snapshots.""" - emit_volume_deprecation_warning('volume-snapshot-list') - snapshots = cs.volume_snapshots.list() - _translate_volume_snapshot_keys(snapshots) - utils.print_list(snapshots, ['ID', 'Volume ID', 'Status', 'Display Name', - 'Size']) - - -@utils.arg( - 'snapshot', - metavar='', - help=_('Name or ID of the snapshot.')) -def do_volume_snapshot_show(cs, args): - """DEPRECATED: Show details about a snapshot.""" - emit_volume_deprecation_warning('volume-snapshot-show') - snapshot = _find_volume_snapshot(cs, args.snapshot) - _print_volume_snapshot(snapshot) - - -@utils.arg( - 'volume_id', - metavar='', - help=_('ID of the volume to snapshot')) -@utils.arg( - '--force', - metavar='', - help=_('Optional flag to indicate whether to snapshot a volume even if ' - 'its attached to a server. (Default=False)'), - default=False) -@utils.arg( - '--display-name', - metavar='', - default=None, - help=_('Optional snapshot name. (Default=None)')) -@utils.arg( - '--display_name', - action=shell.DeprecatedAction, - use=_('use "%s"; this option will be removed in ' - 'novaclient 3.3.0.') % '--display-name', - help=argparse.SUPPRESS) -@utils.arg( - '--display-description', - metavar='', - default=None, - help=_('Optional snapshot description. (Default=None)')) -@utils.arg( - '--display_description', - action=shell.DeprecatedAction, - use=_('use "%s"; this option will be removed in ' - 'novaclient 3.3.0.') % '--display-description', - help=argparse.SUPPRESS) -def do_volume_snapshot_create(cs, args): - """DEPRECATED: Add a new snapshot.""" - emit_volume_deprecation_warning('volume-snapshot-create') - snapshot = cs.volume_snapshots.create(args.volume_id, - args.force, - args.display_name, - args.display_description) - _print_volume_snapshot(snapshot) - - -@utils.arg( - 'snapshot', - metavar='', - help=_('Name or ID of the snapshot to delete.')) -def do_volume_snapshot_delete(cs, args): - """DEPRECATED: Remove a snapshot.""" - emit_volume_deprecation_warning('volume-snapshot-delete') - snapshot = _find_volume_snapshot(cs, args.snapshot) - snapshot.delete() - - -def _print_volume_type_list(vtypes): - utils.print_list(vtypes, ['ID', 'Name']) - - -def do_volume_type_list(cs, args): - """DEPRECATED: Print a list of available 'volume types'.""" - emit_volume_deprecation_warning('volume-type-list') - vtypes = cs.volume_types.list() - _print_volume_type_list(vtypes) - - -@utils.arg( - 'name', - metavar='', - help=_("Name of the new volume type")) -def do_volume_type_create(cs, args): - """DEPRECATED: Create a new volume type.""" - emit_volume_deprecation_warning('volume-type-create') - vtype = cs.volume_types.create(args.name) - _print_volume_type_list([vtype]) - - -@utils.arg( - 'id', - metavar='', - help=_("Unique ID of the volume type to delete.")) -def do_volume_type_delete(cs, args): - """DEPRECATED: Delete a specific volume type.""" - emit_volume_deprecation_warning('volume-type-delete') - cs.volume_types.delete(args.id) - - @api_versions.wraps('2.0', '2.5') def console_dict_accessor(cs, data): return data['console'] diff --git a/novaclient/v2/volume_snapshots.py b/novaclient/v2/volume_snapshots.py deleted file mode 100644 index e31e8c183..000000000 --- a/novaclient/v2/volume_snapshots.py +++ /dev/null @@ -1,120 +0,0 @@ -# Copyright 2011 Denali Systems, Inc. -# All Rights Reserved. -# -# 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. - -""" -DEPRECATED: Volume snapshot interface (1.1 extension). -""" - -import warnings - -from novaclient import base - - -class Snapshot(base.Resource): - """ - DEPRECATED: A Snapshot is a point-in-time snapshot of an openstack volume. - """ - NAME_ATTR = 'display_name' - - def __repr__(self): - return "" % self.id - - def delete(self): - """ - DEPRECATED: Delete this snapshot. - - :returns: An instance of novaclient.base.TupleWithMeta - """ - return self.manager.delete(self) - - -class SnapshotManager(base.ManagerWithFind): - """ - DEPRECATED: Manage :class:`Snapshot` resources. - """ - resource_class = Snapshot - - def create(self, volume_id, force=False, display_name=None, - display_description=None): - - """ - DEPRECATED: Create a snapshot of the given volume. - - :param volume_id: The ID of the volume to snapshot. - :param force: If force is True, create a snapshot even if the volume is - attached to an instance. Default is False. - :param display_name: Name of the snapshot - :param display_description: Description of the snapshot - :rtype: :class:`Snapshot` - """ - warnings.warn('The novaclient.v2.volume_snapshots module is ' - 'deprecated and will be removed after Nova 13.0.0 is ' - 'released. Use python-cinderclient or ' - 'python-openstacksdk instead.', DeprecationWarning) - with self.alternate_service_type( - 'volumev2', allowed_types=('volume', 'volumev2')): - body = {'snapshot': {'volume_id': volume_id, - 'force': force, - 'display_name': display_name, - 'display_description': display_description}} - return self._create('/snapshots', body, 'snapshot') - - def get(self, snapshot_id): - """ - DEPRECATED: Get a snapshot. - - :param snapshot_id: The ID of the snapshot to get. - :rtype: :class:`Snapshot` - """ - warnings.warn('The novaclient.v2.volume_snapshots module is ' - 'deprecated and will be removed after Nova 13.0.0 is ' - 'released. Use python-cinderclient or ' - 'python-openstacksdk instead.', DeprecationWarning) - with self.alternate_service_type( - 'volumev2', allowed_types=('volume', 'volumev2')): - - return self._get("/snapshots/%s" % snapshot_id, "snapshot") - - def list(self, detailed=True): - """ - DEPRECATED: Get a list of all snapshots. - - :rtype: list of :class:`Snapshot` - """ - warnings.warn('The novaclient.v2.volume_snapshots module is ' - 'deprecated and will be removed after Nova 13.0.0 is ' - 'released. Use python-cinderclient or ' - 'python-openstacksdk instead.', DeprecationWarning) - with self.alternate_service_type( - 'volumev2', allowed_types=('volume', 'volumev2')): - if detailed is True: - return self._list("/snapshots/detail", "snapshots") - else: - return self._list("/snapshots", "snapshots") - - def delete(self, snapshot): - """ - DEPRECATED: Delete a snapshot. - - :param snapshot: The :class:`Snapshot` to delete. - :returns: An instance of novaclient.base.TupleWithMeta - """ - warnings.warn('The novaclient.v2.volume_snapshots module is ' - 'deprecated and will be removed after Nova 13.0.0 is ' - 'released. Use python-cinderclient or ' - 'python-openstacksdk instead.', DeprecationWarning) - with self.alternate_service_type( - 'volumev2', allowed_types=('volume', 'volumev2')): - return self._delete("/snapshots/%s" % base.getid(snapshot)) diff --git a/novaclient/v2/volume_types.py b/novaclient/v2/volume_types.py deleted file mode 100644 index 03ac13cc4..000000000 --- a/novaclient/v2/volume_types.py +++ /dev/null @@ -1,103 +0,0 @@ -# Copyright (c) 2011 Rackspace US, Inc. -# -# 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. - - -""" -DEPRECATED: Volume Type interface. -""" - -import warnings - -from novaclient import base - - -class VolumeType(base.Resource): - """ - DEPRECATED: A Volume Type is the type of volume to be created - """ - def __repr__(self): - return "" % self.name - - -class VolumeTypeManager(base.ManagerWithFind): - """ - DEPRECATED: Manage :class:`VolumeType` resources. - """ - resource_class = VolumeType - - def list(self): - """ - DEPRECATED: Get a list of all volume types. - - :rtype: list of :class:`VolumeType`. - """ - warnings.warn('The novaclient.v2.volume_types module is deprecated ' - 'and will be removed after Nova 13.0.0 is released. Use ' - 'python-cinderclient or python-openstacksdk instead.', - DeprecationWarning) - with self.alternate_service_type( - 'volumev2', allowed_types=('volume', 'volumev2')): - return self._list("/types", "volume_types") - - def get(self, volume_type): - """ - DEPRECATED: Get a specific volume type. - - :param volume_type: The ID of the :class:`VolumeType` to get. - :rtype: :class:`VolumeType` - """ - warnings.warn('The novaclient.v2.volume_types module is deprecated ' - 'and will be removed after Nova 13.0.0 is released. Use ' - 'python-cinderclient or python-openstacksdk instead.', - DeprecationWarning) - with self.alternate_service_type( - 'volumev2', allowed_types=('volume', 'volumev2')): - return self._get("/types/%s" % base.getid(volume_type), - "volume_type") - - def delete(self, volume_type): - """ - DEPRECATED: Delete a specific volume_type. - - :param volume_type: The ID of the :class:`VolumeType` to get. - :returns: An instance of novaclient.base.TupleWithMeta - """ - warnings.warn('The novaclient.v2.volume_types module is deprecated ' - 'and will be removed after Nova 13.0.0 is released. Use ' - 'python-cinderclient or python-openstacksdk instead.', - DeprecationWarning) - with self.alternate_service_type( - 'volumev2', allowed_types=('volume', 'volumev2')): - return self._delete("/types/%s" % base.getid(volume_type)) - - def create(self, name): - """ - DEPRECATED: Create a volume type. - - :param name: Descriptive name of the volume type - :rtype: :class:`VolumeType` - """ - warnings.warn('The novaclient.v2.volume_types module is deprecated ' - 'and will be removed after Nova 13.0.0 is released. Use ' - 'python-cinderclient or python-openstacksdk instead.', - DeprecationWarning) - with self.alternate_service_type( - 'volumev2', allowed_types=('volume', 'volumev2')): - body = { - "volume_type": { - "name": name, - } - } - return self._create("/types", body, "volume_type") diff --git a/novaclient/v2/volumes.py b/novaclient/v2/volumes.py index 455cb64b2..f95b59f34 100644 --- a/novaclient/v2/volumes.py +++ b/novaclient/v2/volumes.py @@ -17,17 +17,12 @@ Volume interface (1.1 extension). """ -import warnings - -import six -from six.moves.urllib import parse - from novaclient import base class Volume(base.Resource): """ - DEPRECATED: A volume is an extra block level storage to the OpenStack + A volume is an extra block level storage to the OpenStack instances. """ NAME_ATTR = 'display_name' @@ -35,109 +30,13 @@ class Volume(base.Resource): def __repr__(self): return "" % self.id - def delete(self): - """ - DEPRECATED: Delete this volume. - - :returns: An instance of novaclient.base.TupleWithMeta - """ - return self.manager.delete(self) - -class VolumeManager(base.ManagerWithFind): +class VolumeManager(base.Manager): """ - DEPRECATED: Manage :class:`Volume` resources. + Manage :class:`Volume` resources. This is really about volume attachments. """ resource_class = Volume - def create(self, size, snapshot_id=None, display_name=None, - display_description=None, volume_type=None, - availability_zone=None, imageRef=None): - """ - DEPRECATED: Create a volume. - - :param size: Size of volume in GB - :param snapshot_id: ID of the snapshot - :param display_name: Name of the volume - :param display_description: Description of the volume - :param volume_type: Type of volume - :param availability_zone: Availability Zone for volume - :rtype: :class:`Volume` - :param imageRef: reference to an image stored in glance - """ - warnings.warn('The novaclient.v2.volumes.VolumeManager.create() ' - 'method is deprecated and will be removed after Nova ' - '13.0.0 is released. Use python-cinderclient or ' - 'python-openstacksdk instead.', DeprecationWarning) - # NOTE(melwitt): Ensure we use the volume endpoint for this call - with self.alternate_service_type( - 'volumev2', allowed_types=('volume', 'volumev2')): - body = {'volume': {'size': size, - 'snapshot_id': snapshot_id, - 'display_name': display_name, - 'display_description': display_description, - 'volume_type': volume_type, - 'availability_zone': availability_zone, - 'imageRef': imageRef}} - return self._create('/volumes', body, 'volume') - - def get(self, volume_id): - """ - DEPRECATED: Get a volume. - - :param volume_id: The ID of the volume to get. - :rtype: :class:`Volume` - """ - warnings.warn('The novaclient.v2.volumes.VolumeManager.get() ' - 'method is deprecated and will be removed after Nova ' - '13.0.0 is released. Use python-cinderclient or ' - 'python-openstacksdk instead.', DeprecationWarning) - with self.alternate_service_type( - 'volumev2', allowed_types=('volume', 'volumev2')): - return self._get("/volumes/%s" % volume_id, "volume") - - def list(self, detailed=True, search_opts=None): - """ - DEPRECATED: Get a list of all volumes. - - :rtype: list of :class:`Volume` - """ - warnings.warn('The novaclient.v2.volumes.VolumeManager.list() ' - 'method is deprecated and will be removed after Nova ' - '13.0.0 is released. Use python-cinderclient or ' - 'python-openstacksdk instead.', DeprecationWarning) - with self.alternate_service_type( - 'volumev2', allowed_types=('volume', 'volumev2')): - search_opts = search_opts or {} - - if 'name' in search_opts.keys(): - search_opts['display_name'] = search_opts.pop('name') - - qparams = dict((k, v) for (k, v) in - six.iteritems(search_opts) if v) - - query_str = '?%s' % parse.urlencode(qparams) if qparams else '' - - if detailed is True: - return self._list("/volumes/detail%s" % query_str, "volumes") - else: - return self._list("/volumes%s" % query_str, "volumes") - - def delete(self, volume): - """ - DEPRECATED: Delete a volume. - - :param volume: The :class:`Volume` to delete. - :returns: An instance of novaclient.base.TupleWithMeta - """ - warnings.warn('The novaclient.v2.volumes.VolumeManager.delete() ' - 'method is deprecated and will be removed after Nova ' - '13.0.0 is released. Use python-cinderclient or ' - 'python-openstacksdk instead.', DeprecationWarning) - with self.alternate_service_type( - 'volumev2', allowed_types=('volume', 'volumev2')): - return self._delete("/volumes/%s" % base.getid(volume)) - def create_server_volume(self, server_id, volume_id, device=None): """ Attach a volume identified by the volume ID to the given server ID diff --git a/releasenotes/notes/volume-cli-removal-ffcb94421a356042.yaml b/releasenotes/notes/volume-cli-removal-ffcb94421a356042.yaml new file mode 100644 index 000000000..6a6495e1c --- /dev/null +++ b/releasenotes/notes/volume-cli-removal-ffcb94421a356042.yaml @@ -0,0 +1,6 @@ +--- +upgrade: + - Volume, volume-type and volume-snapshot create/update/delete/list CLIs + and python API bindings are removed. Use python-cinderclient or + python-openstackclient for CLIs instead. Use python-cinderclient or + python-openstacksdk for python API bindings instead. diff --git a/test-requirements.txt b/test-requirements.txt index 8dd1c96f7..44869e36d 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -10,6 +10,7 @@ fixtures>=1.3.1 # Apache-2.0/BSD keyring>=5.5.1 # MIT/PSF mock>=1.2 # BSD python-keystoneclient!=1.8.0,!=2.1.0,>=1.6.0 # Apache-2.0 +python-cinderclient>=1.3.1 # Apache-2.0 requests-mock>=0.7.0 # Apache-2.0 sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2 # BSD os-client-config>=1.13.1 # Apache-2.0 From e458fab8efdaab327c8f07a0771e4d3571580a41 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Mon, 7 Mar 2016 16:44:24 +0200 Subject: [PATCH 1030/1705] Add default values for domain related options Keystone V3 requires that the options os_user_domain_id/os_user_domain_name and os_project_domain_id/os_project_domain_name be set. These options do not have default values and novaclient will fail if they are not set by user. This patch adds default values for os_project_domain_id and os_user_domain_id in case of Keystone V3 env. This is consistent with what the OpenStack CLI does. Also, two functional tests are added to check authentication via Keystone V2 and V3. Partial-Bug: #1522402 Change-Id: Ieb41a7fbf24107f7250ef34218604fba96531195 --- novaclient/shell.py | 10 ++++++ novaclient/tests/functional/test_auth.py | 42 ++++++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 novaclient/tests/functional/test_auth.py diff --git a/novaclient/shell.py b/novaclient/shell.py index 3d38ac99d..cf94e3587 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -767,6 +767,16 @@ def main(self, argv): os_auth_url = args.os_auth_url os_region_name = args.os_region_name os_auth_system = args.os_auth_system + + if "v2.0" not in os_auth_url: + # NOTE(andreykurilin): assume that keystone V3 is used and try to + # be more user-friendly, i.e provide default values for domains + if (not args.os_project_domain_id and + not args.os_project_domain_name): + setattr(args, "os_project_domain_id", "default") + if not args.os_user_domain_id and not args.os_user_domain_name: + setattr(args, "os_user_domain_id", "default") + endpoint_type = args.endpoint_type insecure = args.insecure service_type = args.service_type diff --git a/novaclient/tests/functional/test_auth.py b/novaclient/tests/functional/test_auth.py new file mode 100644 index 000000000..e37fc789e --- /dev/null +++ b/novaclient/tests/functional/test_auth.py @@ -0,0 +1,42 @@ +# 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 six.moves.urllib import parse +import tempest_lib.cli.base + +from novaclient.tests.functional import base + + +class TestAuthentication(base.ClientTestBase): + def nova(self, action, identity_api_version): + url = parse.urlparse(self.cli_clients.uri) + url = parse.urlunparse((url.scheme, url.netloc, + '/v%s' % identity_api_version, + url.params, url.query, + url.fragment)) + flags = ('--os-username %s --os-tenant-name %s --os-password %s ' + '--os-auth-url %s --endpoint-type publicURL' % ( + self.cli_clients.username, + self.cli_clients.tenant_name, + self.cli_clients.password, + url)) + if self.cli_clients.insecure: + flags += ' --insecure ' + + return tempest_lib.cli.base.execute( + "nova", action, flags, cli_dir=self.cli_clients.cli_dir) + + def test_auth_via_keystone_v2(self): + self.nova("list", identity_api_version="2.0") + + def test_auth_via_keystone_v3(self): + self.nova("list", identity_api_version="3") From 41fccc3828c2167372b375fb269ffd1431692876 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Tue, 5 Apr 2016 13:33:33 +0000 Subject: [PATCH 1031/1705] Updated from global requirements Change-Id: I0cc4f0d0f4d01ad0c73043f99495a525defde397 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 44869e36d..f572bf10e 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -10,7 +10,7 @@ fixtures>=1.3.1 # Apache-2.0/BSD keyring>=5.5.1 # MIT/PSF mock>=1.2 # BSD python-keystoneclient!=1.8.0,!=2.1.0,>=1.6.0 # Apache-2.0 -python-cinderclient>=1.3.1 # Apache-2.0 +python-cinderclient>=1.3.1 # Apache-2.0 requests-mock>=0.7.0 # Apache-2.0 sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2 # BSD os-client-config>=1.13.1 # Apache-2.0 From 0634a38cdcb756199160aa1e476c8212c7145250 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Tue, 5 Apr 2016 17:32:20 -0400 Subject: [PATCH 1032/1705] Remove busted baremetal CLIs and API bindings The os-baremetal-nodes compute API does not proxy baremetal node create/delete or interface add/remove requests to Ironic, so there is no point in even having CLIs or python bindings for these operations in the client. There isn't a point in deprecating these since they are already unusable, so let's just remove them. Change-Id: Id5dd7fbddff87e506460904e352713ae777a8073 Closes-Bug: #1566535 --- .../tests/unit/v2/contrib/test_baremetal.py | 24 --- novaclient/v2/contrib/baremetal.py | 174 ------------------ 2 files changed, 198 deletions(-) diff --git a/novaclient/tests/unit/v2/contrib/test_baremetal.py b/novaclient/tests/unit/v2/contrib/test_baremetal.py index 406e617af..fdcd4f5fb 100644 --- a/novaclient/tests/unit/v2/contrib/test_baremetal.py +++ b/novaclient/tests/unit/v2/contrib/test_baremetal.py @@ -41,30 +41,6 @@ def test_get_node(self): self.cs.assert_called('GET', '/os-baremetal-nodes/1') self.assertIsInstance(n, baremetal.BareMetalNode) - def test_create_node(self): - n = self.cs.baremetal.create("service_host", 1, 1024, 2048, - "aa:bb:cc:dd:ee:ff") - self.assert_request_id(n, fakes.FAKE_REQUEST_ID_LIST) - self.cs.assert_called('POST', '/os-baremetal-nodes') - self.assertIsInstance(n, baremetal.BareMetalNode) - - def test_delete_node(self): - n = self.cs.baremetal.get(1) - ret = self.cs.baremetal.delete(n) - self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) - self.cs.assert_called('DELETE', '/os-baremetal-nodes/1') - - def test_node_add_interface(self): - i = self.cs.baremetal.add_interface(1, "bb:cc:dd:ee:ff:aa", 1, 2) - self.assert_request_id(i, fakes.FAKE_REQUEST_ID_LIST) - self.cs.assert_called('POST', '/os-baremetal-nodes/1/action') - self.assertIsInstance(i, baremetal.BareMetalNodeInterface) - - def test_node_remove_interface(self): - ret = self.cs.baremetal.remove_interface(1, "bb:cc:dd:ee:ff:aa") - self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) - self.cs.assert_called('POST', '/os-baremetal-nodes/1/action') - def test_node_list_interfaces(self): il = self.cs.baremetal.list_interfaces(1) self.assert_request_id(il, fakes.FAKE_REQUEST_ID_LIST) diff --git a/novaclient/v2/contrib/baremetal.py b/novaclient/v2/contrib/baremetal.py index 911944212..4307d6cc1 100644 --- a/novaclient/v2/contrib/baremetal.py +++ b/novaclient/v2/contrib/baremetal.py @@ -46,51 +46,6 @@ class BareMetalNodeManager(base.ManagerWithFind): """ resource_class = BareMetalNode - def create(self, - service_host, - cpus, - memory_mb, - local_gb, - prov_mac_address, - pm_address=None, - pm_user=None, - pm_password=None, - terminal_port=None): - """ - Create a baremetal node. - - :param service_host: Name of controlling compute host - :param cpus: Number of CPUs in the node - :param memory_mb: Megabytes of RAM in the node - :param local_gb: Gigabytes of local storage in the node - :param pm_address: Power management IP for the node - :param pm_user: Username for the node's power management - :param pm_password: Password for the node's power management - :param prov_mac_address: MAC address to provision the node - :param terminal_port: ShellInABox port - :rtype: :class:`BareMetalNode` - """ - body = {'node': {'service_host': service_host, - 'cpus': cpus, - 'memory_mb': memory_mb, - 'local_gb': local_gb, - 'pm_address': pm_address, - 'pm_user': pm_user, - 'pm_password': pm_password, - 'prov_mac_address': prov_mac_address, - 'terminal_port': terminal_port}} - - return self._create('/os-baremetal-nodes', body, 'node') - - def delete(self, node): - """ - Delete a baremetal node. - - :param node: The :class:`BareMetalNode` to delete. - :returns: An instance of novaclient.base.TupleWithMeta - """ - return self._delete('/os-baremetal-nodes/%s' % base.getid(node)) - def get(self, node_id): """ Get a baremetal node. @@ -108,37 +63,6 @@ def list(self): """ return self._list('/os-baremetal-nodes', 'nodes') - def add_interface(self, node_id, address, datapath_id=0, port_no=0): - """ - Add an interface to a baremetal node. - - :param node_id: The ID of the node to modify. - :param address: The MAC address to add. - :param datapath_id: Datapath ID of OpenFlow switch for the interface - :param port_no: OpenFlow port number for the interface - :rtype: :class:`BareMetalNodeInterface` - """ - body = {'add_interface': {'address': address, - 'datapath_id': datapath_id, - 'port_no': port_no}} - url = '/os-baremetal-nodes/%s/action' % node_id - resp, body = self.api.client.post(url, body=body) - return BareMetalNodeInterface(self, body['interface'], resp=resp) - - def remove_interface(self, node_id, address): - """ - Remove an interface from a baremetal node. - - :param node_id: The ID of the node to modify. - :param address: The MAC address to remove. - :returns: An instance of novaclient.base.TupleWithMeta - """ - req_body = {'remove_interface': {'address': address}} - url = '/os-baremetal-nodes/%s/action' % node_id - resp, body = self.api.client.post(url, body=req_body) - - return self.convert_into_with_meta(body, resp) - def list_interfaces(self, node_id): """ List the interfaces on a baremetal node. @@ -155,69 +79,6 @@ def list_interfaces(self, node_id): return interfaces -@utils.arg( - 'service_host', - metavar='', - help=_('Name of nova compute host which will control this baremetal ' - 'node')) -@utils.arg( - 'cpus', - metavar='', - type=int, - help=_('Number of CPUs in the node')) -@utils.arg( - 'memory_mb', - metavar='', - type=int, - help=_('Megabytes of RAM in the node')) -@utils.arg( - 'local_gb', - metavar='', - type=int, - help=_('Gigabytes of local storage in the node')) -@utils.arg( - 'prov_mac_address', - metavar='', - help=_('MAC address to provision the node')) -@utils.arg( - '--pm_address', default=None, - metavar='', - help=_('Power management IP for the node')) -@utils.arg( - '--pm_user', default=None, - metavar='', - help=_('Username for the node\'s power management')) -@utils.arg( - '--pm_password', default=None, - metavar='', - help=_('Password for the node\'s power management')) -@utils.arg( - '--terminal_port', default=None, - metavar='', - type=int, - help=_('ShellInABox port?')) -def do_baremetal_node_create(cs, args): - """Create a baremetal node.""" - node = cs.baremetal.create(args.service_host, args.cpus, - args.memory_mb, args.local_gb, - args.prov_mac_address, - pm_address=args.pm_address, - pm_user=args.pm_user, - pm_password=args.pm_password, - terminal_port=args.terminal_port) - _print_baremetal_resource(node) - - -@utils.arg( - 'node', - metavar='', - help=_('ID of the node to delete.')) -def do_baremetal_node_delete(cs, args): - """Remove a baremetal node and any associated interfaces.""" - node = _find_baremetal_node(cs, args.node) - cs.baremetal.delete(node) - - def _translate_baremetal_node_keys(collection): convert = [('service_host', 'host'), ('local_gb', 'disk_gb'), @@ -300,41 +161,6 @@ def do_baremetal_node_show(cs, args): _print_baremetal_resource(node) -@utils.arg( - 'node', - metavar='', - help=_("ID of node")) -@utils.arg( - 'address', - metavar='
', - help=_("MAC address of interface")) -@utils.arg( - '--datapath_id', - default=0, - metavar='', - help=_("OpenFlow Datapath ID of interface")) -@utils.arg( - '--port_no', - default=0, - metavar='', - help=_("OpenFlow port number of interface")) -def do_baremetal_interface_add(cs, args): - """Add a network interface to a baremetal node.""" - bmif = cs.baremetal.add_interface(args.node, args.address, - args.datapath_id, args.port_no) - _print_baremetal_resource(bmif) - - -@utils.arg('node', metavar='', help=_("ID of node")) -@utils.arg( - 'address', - metavar='
', - help=_("MAC address of interface")) -def do_baremetal_interface_remove(cs, args): - """Remove a network interface from a baremetal node.""" - cs.baremetal.remove_interface(args.node, args.address) - - @utils.arg('node', metavar='', help=_("ID of node")) def do_baremetal_interface_list(cs, args): """List network interfaces associated with a baremetal node.""" From a967e40185d4a8682194604c2225638e1ec88ed1 Mon Sep 17 00:00:00 2001 From: Dao Cong Tien Date: Wed, 6 Apr 2016 09:34:00 +0700 Subject: [PATCH 1033/1705] Fix typos in docstrings and comments Change-Id: Ib94c2b999f8295ec526d4e44883b86ab8b5a0688 --- novaclient/api_versions.py | 10 +++++----- novaclient/shell.py | 4 ++-- novaclient/v2/client.py | 2 +- novaclient/v2/shell.py | 6 +++--- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/novaclient/api_versions.py b/novaclient/api_versions.py index 5f7198c0d..8774ea971 100644 --- a/novaclient/api_versions.py +++ b/novaclient/api_versions.py @@ -49,11 +49,11 @@ class APIVersion(object): def __init__(self, version_str=None): """Create an API version object. - :param version_string: String representation of APIVersionRequest. - Correct format is 'X.Y', where 'X' and 'Y' - are int values. None value should be used - to create Null APIVersionRequest, which is - equal to 0.0 + :param version_str: String representation of APIVersionRequest. + Correct format is 'X.Y', where 'X' and 'Y' + are int values. None value should be used + to create Null APIVersionRequest, which is + equal to 0.0 """ self.ver_major = 0 self.ver_minor = 0 diff --git a/novaclient/shell.py b/novaclient/shell.py index 425fdb16f..df2f581b2 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -729,7 +729,7 @@ def main(self, argv): do_help = ('help' in argv) or ( '--help' in argv) or ('-h' in argv) or not argv - # bash-completion should not require authentification + # bash-completion should not require authentication skip_auth = do_help or ( 'bash-completion' in argv) @@ -938,7 +938,7 @@ def main(self, argv): if utils.isunauthenticated(args.func): # NOTE(alex_xu): We need authentication for discover microversion. # But the subcommands may needn't it. If the subcommand needn't, - # we clear the session arguements. + # we clear the session arguments. keystone_session = None keystone_auth = None diff --git a/novaclient/v2/client.py b/novaclient/v2/client.py index 5bbd32b27..60cf0878d 100644 --- a/novaclient/v2/client.py +++ b/novaclient/v2/client.py @@ -80,7 +80,7 @@ def __init__(self, username=None, api_key=None, project_id=None, :param str proxy_token: Proxy Token :param str region_name: Region Name :param str endpoint_type: Endpoint Type - :param str extensions: Exensions + :param str extensions: Extensions :param str service_type: Service Type :param str service_name: Service Name :param str volume_service_name: Volume Service Name diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index e42239fc0..8c28378eb 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -1462,7 +1462,7 @@ def do_image_delete(cs, args): metavar='', default=None, help=_("List only servers changed after a certain point of time." - "The provided time should be an ISO 8061 formated time." + "The provided time should be an ISO 8061 formatted time." "ex 2016-03-04T06:27:59Z .")) def do_list(cs, args): """List active servers.""" @@ -2064,7 +2064,7 @@ def _print_server(cs, args, server=None): # findall(name=blah) and due a REST /details which is not the same # as a .get() and doesn't get the information about flavors and # images. This fix it as we redo the call with the id which does a - # .get() to get all informations. + # .get() to get all information. if not server: server = _find_server(cs, args.server) @@ -4199,7 +4199,7 @@ def _quota_update(manager, identifier, args): if updates: # default value of force is None to make sure this client - # will be compatibile with old nova server + # will be compatible with old nova server force_update = getattr(args, 'force', None) user_id = getattr(args, 'user', None) if isinstance(manager, quotas.QuotaSetManager): From 77f214bdbd7d9e8ba935ace1dce0290061ddd737 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 8 Apr 2016 00:33:13 +0000 Subject: [PATCH 1034/1705] Updated from global requirements Change-Id: I1a150011a588bde5f0bf77c3f4383b4cfbbf5fe1 --- test-requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test-requirements.txt b/test-requirements.txt index f572bf10e..c42e33cf8 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -6,11 +6,11 @@ hacking<0.11,>=0.10.0 bandit>=0.17.3 # Apache-2.0 coverage>=3.6 # Apache-2.0 discover # BSD -fixtures>=1.3.1 # Apache-2.0/BSD +fixtures<2.0,>=1.3.1 # Apache-2.0/BSD keyring>=5.5.1 # MIT/PSF mock>=1.2 # BSD python-keystoneclient!=1.8.0,!=2.1.0,>=1.6.0 # Apache-2.0 -python-cinderclient>=1.3.1 # Apache-2.0 +python-cinderclient>=1.6.0 # Apache-2.0 requests-mock>=0.7.0 # Apache-2.0 sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2 # BSD os-client-config>=1.13.1 # Apache-2.0 @@ -21,4 +21,4 @@ testtools>=1.4.0 # MIT tempest-lib>=0.14.0 # Apache-2.0 # releasenotes -reno>=0.1.1 # Apache2 +reno>=1.6.2 # Apache2 From d133a664ae19385ded69ee416f04f6243c26285e Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Mon, 11 Apr 2016 14:57:28 +0300 Subject: [PATCH 1035/1705] Make functional tests work with v3 keystone This patch adds support of v3 keystone to novaclient's functional tests. Closes-Bug: 1568527 Change-Id: Iff5fb3180855de7adb3399f6be16bedc8543b4ec --- novaclient/tests/functional/base.py | 76 ++++++++++++++----- .../tests/functional/hooks/post_test_hook.sh | 6 -- .../v2/legacy/test_flavor_access.py | 6 +- .../tests/functional/v2/legacy/test_quotas.py | 4 +- .../tests/functional/v2/test_flavor_access.py | 2 +- novaclient/tests/functional/v2/test_quotas.py | 4 +- 6 files changed, 62 insertions(+), 36 deletions(-) diff --git a/novaclient/tests/functional/base.py b/novaclient/tests/functional/base.py index c58f5b4c3..085ffa6c2 100644 --- a/novaclient/tests/functional/base.py +++ b/novaclient/tests/functional/base.py @@ -16,7 +16,9 @@ from cinderclient.v2 import client as cinderclient import fixtures -from keystoneclient.v2_0 import client as keystoneclient +from keystoneauth1 import loading +from keystoneauth1 import session as ksession +from keystoneclient import client as keystoneclient import os_client_config import six import tempest_lib.cli.base @@ -168,18 +170,25 @@ def setUp(self): passwd = auth_info['password'] tenant = auth_info['project_name'] auth_url = auth_info['auth_url'] + self.project_domain_id = auth_info['project_domain_id'] if 'insecure' in cloud_config.config: - insecure = cloud_config.config['insecure'] + self.insecure = cloud_config.config['insecure'] else: - insecure = False + self.insecure = False if self.COMPUTE_API_VERSION == "2.latest": version = novaclient.API_MAX_VERSION.get_string() else: version = self.COMPUTE_API_VERSION or "2" - self.client = novaclient.client.Client( - version, user, passwd, tenant, - auth_url=auth_url, insecure=insecure) + + loader = loading.get_plugin_loader("password") + auth = loader.load_from_options(username=user, + password=passwd, + project_name=tenant, + auth_url=auth_url) + session = ksession.Session(auth=auth, verify=(not self.insecure)) + + self.client = novaclient.client.Client(version, session=session) # pick some reasonable flavor / image combo self.flavor = pick_flavor(self.client.flavors.list()) @@ -201,13 +210,12 @@ def setUp(self): tenant_name=tenant, uri=auth_url, cli_dir=cli_dir, - insecure=insecure) + insecure=self.insecure) - self.keystone = keystoneclient.Client(username=user, - password=passwd, - tenant_name=tenant, - auth_url=auth_url) - self.cinder = cinderclient.Client(user, passwd, tenant, auth_url) + self.keystone = keystoneclient.Client(session=session, + username=user, + password=passwd) + self.cinder = cinderclient.Client(auth=auth, session=session) def nova(self, action, flags='', params='', fail_ok=False, endpoint_type='publicURL', merge_stderr=False): @@ -352,6 +360,14 @@ def _create_server(self, name=None, with_network=True, add_cleanup=True, 'building', ['active']) return server + def _get_project_id(self, name): + """Obtain project id by project name.""" + if self.keystone.version == "v3": + project = self.keystone.projects.find(name=name) + else: + project = self.keystone.tenants.find(name=name) + return project.id + class TenantTestBase(ClientTestBase): """Base test class for additional tenant and user creation which @@ -360,21 +376,41 @@ class TenantTestBase(ClientTestBase): def setUp(self): super(TenantTestBase, self).setUp() - tenant = self.keystone.tenants.create( - self.name_generate('v' + self.COMPUTE_API_VERSION)) - self.tenant_id = tenant.id - self.addCleanup(self.keystone.tenants.delete, self.tenant_id) user_name = self.name_generate('v' + self.COMPUTE_API_VERSION) + project_name = self.name_generate('v' + self.COMPUTE_API_VERSION) password = 'password' - self.user_id = self.keystone.users.create( - user_name, password, tenant_id=self.tenant_id).id + + if self.keystone.version == "v3": + project = self.keystone.projects.create(project_name, + self.project_domain_id) + self.project_id = project.id + self.addCleanup(self.keystone.projects.delete, self.project_id) + + self.user_id = self.keystone.users.create( + name=user_name, password=password, + default_project=self.project_id).id + + for role in self.keystone.roles.list(): + if "member" in role.name.lower(): + self.keystone.roles.grant(role.id, user=self.user_id, + project=self.project_id) + break + else: + project = self.keystone.tenants.create(project_name) + self.project_id = project.id + self.addCleanup(self.keystone.tenants.delete, self.project_id) + + self.user_id = self.keystone.users.create( + user_name, password, tenant_id=self.project_id).id + self.addCleanup(self.keystone.users.delete, self.user_id) self.cli_clients_2 = tempest_lib.cli.base.CLIClient( username=user_name, password=password, - tenant_name=tenant.name, + tenant_name=project_name, uri=self.cli_clients.uri, - cli_dir=self.cli_clients.cli_dir) + cli_dir=self.cli_clients.cli_dir, + insecure=self.insecure) def another_nova(self, action, flags='', params='', fail_ok=False, endpoint_type='publicURL', merge_stderr=False): diff --git a/novaclient/tests/functional/hooks/post_test_hook.sh b/novaclient/tests/functional/hooks/post_test_hook.sh index f58d8bb6f..766352933 100755 --- a/novaclient/tests/functional/hooks/post_test_hook.sh +++ b/novaclient/tests/functional/hooks/post_test_hook.sh @@ -30,12 +30,6 @@ export NOVACLIENT_DIR="$BASE/new/python-novaclient" sudo chown -R jenkins:stack $NOVACLIENT_DIR -# FIXME(melwitt): Currently, novaclient requires version in the -# auth url in order to work and it doesn't support keystone v3. -# Use v2.0 in the auth url in clouds.yaml to enable the functional -# test job to work until novaclient is updated to support v3 -sudo sed -i -e 's/auth_url: \([^v ]\+\)\(\/v[0-9]\+\.\?[0-9]*\)\?$/auth_url: \1\/v2.0/g' /etc/openstack/clouds.yaml - # Go to the novaclient dir cd $NOVACLIENT_DIR diff --git a/novaclient/tests/functional/v2/legacy/test_flavor_access.py b/novaclient/tests/functional/v2/legacy/test_flavor_access.py index ec83b4936..54671b286 100644 --- a/novaclient/tests/functional/v2/legacy/test_flavor_access.py +++ b/novaclient/tests/functional/v2/legacy/test_flavor_access.py @@ -46,8 +46,8 @@ def test_add_access_non_public_flavor(self): self.nova('flavor-create --is-public false %s auto 512 1 1' % flv_name) self.addCleanup(self.nova, 'flavor-delete %s' % flv_name) self.nova('flavor-access-add', params="%s %s" % - (flv_name, self.tenant_id)) - self.assertIn(self.tenant_id, + (flv_name, self.project_id)) + self.assertIn(self.project_id, self.nova('flavor-access-list --flavor %s' % flv_name)) def test_add_access_public_flavor(self): @@ -58,7 +58,7 @@ def test_add_access_public_flavor(self): flv_name = self.name_generate(prefix='flv') self.nova('flavor-create %s auto 512 1 1' % flv_name) self.addCleanup(self.nova, 'flavor-delete %s' % flv_name) - self.nova('flavor-access-add %s %s' % (flv_name, self.tenant_id)) + self.nova('flavor-access-add %s %s' % (flv_name, self.project_id)) output = self.nova('flavor-access-list --flavor %s' % flv_name, fail_ok=True, merge_stderr=True) self.assertIn("CommandError", output) diff --git a/novaclient/tests/functional/v2/legacy/test_quotas.py b/novaclient/tests/functional/v2/legacy/test_quotas.py index 1cd8ac889..6eeddbe19 100644 --- a/novaclient/tests/functional/v2/legacy/test_quotas.py +++ b/novaclient/tests/functional/v2/legacy/test_quotas.py @@ -27,9 +27,7 @@ class TestQuotasNovaClient(base.ClientTestBase): def test_quotas_update(self): # `nova quota-update` requires tenant-id. - tenant_info = self.cli_clients.keystone( - "tenant-get", params=self.cli_clients.tenant_name) - tenant_id = self._get_value_from_the_table(tenant_info, "id") + tenant_id = self._get_project_id(self.cli_clients.tenant_name) self.addCleanup(self.client.quotas.delete, tenant_id) diff --git a/novaclient/tests/functional/v2/test_flavor_access.py b/novaclient/tests/functional/v2/test_flavor_access.py index f86a1499f..229aa3fc4 100644 --- a/novaclient/tests/functional/v2/test_flavor_access.py +++ b/novaclient/tests/functional/v2/test_flavor_access.py @@ -26,7 +26,7 @@ def test_add_access_public_flavor(self): self.nova('flavor-create %s auto 512 1 1' % flv_name) self.addCleanup(self.nova, 'flavor-delete %s' % flv_name) output = self.nova('flavor-access-add %s %s' % - (flv_name, self.tenant_id), + (flv_name, self.project_id), fail_ok=True, merge_stderr=True) self.assertIn("ERROR (Conflict): " "Can not add access to a public flavor. (HTTP 409) ", diff --git a/novaclient/tests/functional/v2/test_quotas.py b/novaclient/tests/functional/v2/test_quotas.py index 5d836e2c4..fdfb06ac3 100644 --- a/novaclient/tests/functional/v2/test_quotas.py +++ b/novaclient/tests/functional/v2/test_quotas.py @@ -27,9 +27,7 @@ class TestQuotasNovaClient(test_quotas.TestQuotasNovaClient): def test_quotas_update(self): # `nova quota-update` requires tenant-id. - tenant_info = self.cli_clients.keystone( - "tenant-get", params=self.cli_clients.tenant_name) - tenant_id = self._get_value_from_the_table(tenant_info, "id") + tenant_id = self._get_project_id(self.cli_clients.tenant_name) self.addCleanup(self.client.quotas.delete, tenant_id) From a602e598064a7251aa85b1123595eb187fd65b16 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Wed, 6 Apr 2016 16:52:05 -0400 Subject: [PATCH 1036/1705] Deprecate image list/show/delete/update CLIs/APIs This deprecates all of the image CLIs/python API bindings that use the Nova os-images API which is a proxy to the Glance v1 API. This will emit a warning each time a deprecated CLI/API is used and also updates the help docs for the deprecated CLIs and docstrings for APIs. The plan is to do a release once this is merged so people start seeing it and then we'll actually remove the deprecated CLIs/APIs in the first python-novaclient release after the Nova server 15.0.0 'O' release. Depends-On: Iff5fb3180855de7adb3399f6be16bedc8543b4ec Change-Id: I3f60cc7f4c6e27861c4a84b925d573f35f1a1848 --- .../v2/legacy/test_readonly_nova.py | 3 +- novaclient/tests/unit/v2/test_images.py | 31 +++++++++++--- novaclient/tests/unit/v2/test_shell.py | 9 ++-- novaclient/v2/images.py | 41 +++++++++++++++---- novaclient/v2/shell.py | 20 +++++++-- ...mage-api-deprecation-41944dc6fc024918.yaml | 14 +++++++ 6 files changed, 95 insertions(+), 23 deletions(-) create mode 100644 releasenotes/notes/image-api-deprecation-41944dc6fc024918.yaml diff --git a/novaclient/tests/functional/v2/legacy/test_readonly_nova.py b/novaclient/tests/functional/v2/legacy/test_readonly_nova.py index 80ff4f086..7f350199e 100644 --- a/novaclient/tests/functional/v2/legacy/test_readonly_nova.py +++ b/novaclient/tests/functional/v2/legacy/test_readonly_nova.py @@ -88,7 +88,8 @@ def test_admin_hypervisor_list(self): self.nova('hypervisor-list') def test_admin_image_list(self): - self.nova('image-list') + out = self.nova('image-list', merge_stderr=True) + self.assertIn('Command image-list is deprecated', out) @decorators.skip_because(bug="1157349") def test_admin_interface_list(self): diff --git a/novaclient/tests/unit/v2/test_images.py b/novaclient/tests/unit/v2/test_images.py index 2c90e2dfc..bdafdf2cb 100644 --- a/novaclient/tests/unit/v2/test_images.py +++ b/novaclient/tests/unit/v2/test_images.py @@ -11,6 +11,10 @@ # License for the specific language governing permissions and limitations # under the License. +import warnings + +import mock + from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit.fixture_data import images as data from novaclient.tests.unit import utils @@ -23,13 +27,15 @@ class ImagesTest(utils.FixturedTestCase): client_fixture_class = client.V1 data_fixture_class = data.V1 - def test_list_images(self): + @mock.patch.object(warnings, 'warn') + def test_list_images(self, mock_warn): il = self.cs.images.list() self.assert_request_id(il, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/images/detail') for i in il: self.assertIsInstance(i, images.Image) self.assertEqual(2, len(il)) + self.assertEqual(1, mock_warn.call_count) def test_list_images_undetailed(self): il = self.cs.images.list(detailed=False) @@ -43,35 +49,48 @@ def test_list_images_with_marker_limit(self): self.assert_request_id(il, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/images/detail?limit=4&marker=1234') - def test_get_image_details(self): + @mock.patch.object(warnings, 'warn') + def test_get_image_details(self, mock_warn): i = self.cs.images.get(1) self.assert_request_id(i, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/images/1') self.assertIsInstance(i, images.Image) self.assertEqual(1, i.id) self.assertEqual('CentOS 5.2', i.name) + self.assertEqual(1, mock_warn.call_count) - def test_delete_image(self): + @mock.patch.object(warnings, 'warn') + def test_delete_image(self, mock_warn): i = self.cs.images.delete(1) self.assert_request_id(i, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('DELETE', '/images/1') + self.assertEqual(1, mock_warn.call_count) - def test_delete_meta(self): + @mock.patch.object(warnings, 'warn') + def test_delete_meta(self, mock_warn): i = self.cs.images.delete_meta(1, {'test_key': 'test_value'}) self.assert_request_id(i, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('DELETE', '/images/1/metadata/test_key') + self.assertEqual(1, mock_warn.call_count) - def test_set_meta(self): + @mock.patch.object(warnings, 'warn') + def test_set_meta(self, mock_warn): i = self.cs.images.set_meta(1, {'test_key': 'test_value'}) self.assert_request_id(i, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/images/1/metadata', {"metadata": {'test_key': 'test_value'}}) + self.assertEqual(1, mock_warn.call_count) - def test_find(self): + @mock.patch.object(warnings, 'warn') + def test_find(self, mock_warn): i = self.cs.images.find(name="CentOS 5.2") self.assert_request_id(i, fakes.FAKE_REQUEST_ID_LIST) self.assertEqual(1, i.id) self.assert_called('GET', '/images/1') + # This is two warnings because find calls findall which calls list + # which is the first warning, which finds one results and then calls + # get on that, which is the second warning. + self.assertEqual(2, mock_warn.call_count) iml = self.cs.images.findall(status='SAVING') self.assert_request_id(iml, fakes.FAKE_REQUEST_ID_LIST) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 1258a6965..17ce6d670 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -883,11 +883,13 @@ def test_flavor_access_remove_by_name(self): {'removeTenantAccess': {'tenant': 'proj2'}}) def test_image_show(self): - self.run_command('image-show 1') + _, err = self.run_command('image-show 1') + self.assertIn('Command image-show is deprecated', err) self.assert_called('GET', '/images/1') def test_image_meta_set(self): - self.run_command('image-meta 1 set test_key=test_value') + _, err = self.run_command('image-meta 1 set test_key=test_value') + self.assertIn('Command image-meta is deprecated', err) self.assert_called('POST', '/images/1/metadata', {'metadata': {'test_key': 'test_value'}}) @@ -950,7 +952,8 @@ def test_create_image_with_poll_to_check_image_state_deleted(self): 'image-create sample-server mysnapshot_deleted --poll') def test_image_delete(self): - self.run_command('image-delete 1') + _, err = self.run_command('image-delete 1') + self.assertIn('Command image-delete is deprecated', err) self.assert_called('DELETE', '/images/1') def test_image_delete_multiple(self): diff --git a/novaclient/v2/images.py b/novaclient/v2/images.py index 39e9dc06c..dbbc60ce0 100644 --- a/novaclient/v2/images.py +++ b/novaclient/v2/images.py @@ -13,9 +13,11 @@ # under the License. """ -Image interface. +DEPRECATED: Image interface. """ +import warnings + from six.moves.urllib import parse from novaclient import base @@ -23,7 +25,8 @@ class Image(base.Resource): """ - An image is a collection of files used to create or rebuild a server. + DEPRECATED: An image is a collection of files used to create or rebuild a + server. """ HUMAN_ID = True @@ -32,7 +35,7 @@ def __repr__(self): def delete(self): """ - Delete this image. + DEPRECATED: Delete this image. :returns: An instance of novaclient.base.TupleWithMeta """ @@ -41,28 +44,36 @@ def delete(self): class ImageManager(base.ManagerWithFind): """ - Manage :class:`Image` resources. + DEPRECATED: Manage :class:`Image` resources. """ resource_class = Image def get(self, image): """ - Get an image. + DEPRECATED: Get an image. :param image: The ID of the image to get. :rtype: :class:`Image` """ + warnings.warn( + 'The novaclient.v2.images module is deprecated and will be ' + 'removed after Nova 15.0.0 is released. Use python-glanceclient ' + 'or python-openstacksdk instead.', DeprecationWarning) return self._get("/images/%s" % base.getid(image), "image") def list(self, detailed=True, limit=None, marker=None): """ - Get a list of all images. + DEPRECATED: Get a list of all images. :rtype: list of :class:`Image` :param limit: maximum number of images to return. :param marker: Begin returning images that appear later in the image list than that represented by this image id (optional). """ + warnings.warn( + 'The novaclient.v2.images module is deprecated and will be ' + 'removed after Nova 15.0.0 is released. Use python-glanceclient ' + 'or python-openstacksdk instead.', DeprecationWarning) params = {} detail = '' if detailed: @@ -77,7 +88,7 @@ def list(self, detailed=True, limit=None, marker=None): def delete(self, image): """ - Delete an image. + DEPRECATED: Delete an image. It should go without saying that you can't delete an image that you didn't create. @@ -85,27 +96,39 @@ def delete(self, image): :param image: The :class:`Image` (or its ID) to delete. :returns: An instance of novaclient.base.TupleWithMeta """ + warnings.warn( + 'The novaclient.v2.images module is deprecated and will be ' + 'removed after Nova 15.0.0 is released. Use python-glanceclient ' + 'or python-openstacksdk instead.', DeprecationWarning) return self._delete("/images/%s" % base.getid(image)) def set_meta(self, image, metadata): """ - Set an images metadata + DEPRECATED: Set an images metadata :param image: The :class:`Image` to add metadata to :param metadata: A dict of metadata to add to the image """ + warnings.warn( + 'The novaclient.v2.images module is deprecated and will be ' + 'removed after Nova 15.0.0 is released. Use python-glanceclient ' + 'or python-openstacksdk instead.', DeprecationWarning) body = {'metadata': metadata} return self._create("/images/%s/metadata" % base.getid(image), body, "metadata") def delete_meta(self, image, keys): """ - Delete metadata from an image + DEPRECATED: Delete metadata from an image :param image: The :class:`Image` to delete metadata :param keys: A list of metadata keys to delete from the image :returns: An instance of novaclient.base.TupleWithMeta """ + warnings.warn( + 'The novaclient.v2.images module is deprecated and will be ' + 'removed after Nova 15.0.0 is released. Use python-glanceclient ' + 'or python-openstacksdk instead.', DeprecationWarning) result = base.TupleWithMeta((), None) for k in keys: ret = self._delete("/images/%s/metadata/%s" % diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 8c28378eb..769725e42 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -66,6 +66,14 @@ } +# NOTE(mriedem): Remove this along with the deprecated commands in the first +# python-novaclient release AFTER the nova server 15.0.0 'O' release. +def emit_image_deprecation_warning(command_name): + print('WARNING: Command %s is deprecated and will be removed after Nova ' + '15.0.0 is released. Use python-glanceclient or openstackclient ' + 'instead.' % command_name, file=sys.stderr) + + def _key_value_pairing(text): try: (k, v) = text.split('=', 1) @@ -1201,7 +1209,8 @@ def do_network_create(cs, args): metavar="", help=_('Number of images to return per request.')) def do_image_list(cs, _args): - """Print a list of available images to boot from.""" + """DEPRECATED: Print a list of available images to boot from.""" + emit_image_deprecation_warning('image-list') limit = _args.limit image_list = cs.images.list(limit=limit) @@ -1234,7 +1243,8 @@ def parse_server_name(image): help=_('Metadata to add/update or delete (only key is necessary on ' 'delete).')) def do_image_meta(cs, args): - """Set or delete metadata on an image.""" + """DEPRECATED: Set or delete metadata on an image.""" + emit_image_deprecation_warning('image-meta') image = _find_image(cs, args.image) metadata = _extract_metadata(args) @@ -1297,7 +1307,8 @@ def _print_flavor(flavor): metavar='', help=_("Name or ID of image.")) def do_image_show(cs, args): - """Show details about the given image.""" + """DEPRECATED: Show details about the given image.""" + emit_image_deprecation_warning('image-show') image = _find_image(cs, args.image) _print_image(image) @@ -1306,7 +1317,8 @@ def do_image_show(cs, args): 'image', metavar='', nargs='+', help=_('Name or ID of image(s).')) def do_image_delete(cs, args): - """Delete specified image(s).""" + """DEPRECATED: Delete specified image(s).""" + emit_image_deprecation_warning('image-delete') for image in args.image: try: _find_image(cs, image).delete() diff --git a/releasenotes/notes/image-api-deprecation-41944dc6fc024918.yaml b/releasenotes/notes/image-api-deprecation-41944dc6fc024918.yaml new file mode 100644 index 000000000..23d987332 --- /dev/null +++ b/releasenotes/notes/image-api-deprecation-41944dc6fc024918.yaml @@ -0,0 +1,14 @@ +--- +deprecations: + - | + The following CLIs and python API bindings are now deprecated for removal: + + * nova image-delete + * nova image-list + * nova image-meta + * nova image-show + + These will be removed in the first major python-novaclient release after + the Nova 15.0.0 Ocata release. Use python-glanceclient or + python-openstackclient for CLI and python-glanceclient or openstacksdk + for python API bindings. From f6efc861b7b40c8a78743fed661053506fb1301a Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Thu, 14 Apr 2016 02:38:15 +0000 Subject: [PATCH 1037/1705] Updated from global requirements Change-Id: I9bf801b449d32cb8b354aa010a67737f91d000b7 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ae6170c3b..510ca434a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,4 +11,4 @@ PrettyTable<0.8,>=0.7 # BSD requests!=2.9.0,>=2.8.1 # Apache-2.0 simplejson>=2.2.0 # MIT six>=1.9.0 # MIT -Babel>=1.3 # BSD +Babel!=2.3.0,!=2.3.1,!=2.3.2,!=2.3.3,>=1.3 # BSD From fd23ff900093525543e345656cbbe9b4c520c95a Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Fri, 1 Apr 2016 18:48:58 +0300 Subject: [PATCH 1038/1705] Use common find_server from v2.shell - extend v2.shell._find_server with raise_if_notfound param - reuse v2.shell._find_server for instance_action extension - fix instance-action-list on v2.21 (it was unable to work with deleted instances) Change-Id: I7b538124c6ab0ee00164822b324aaf2d37c8c2af --- novaclient/v2/contrib/instance_action.py | 31 +++++++----------------- novaclient/v2/shell.py | 22 ++++++++++++++--- 2 files changed, 28 insertions(+), 25 deletions(-) diff --git a/novaclient/v2/contrib/instance_action.py b/novaclient/v2/contrib/instance_action.py index d5bd4762b..05b847c2b 100644 --- a/novaclient/v2/contrib/instance_action.py +++ b/novaclient/v2/contrib/instance_action.py @@ -15,13 +15,11 @@ import pprint -import six - from novaclient import api_versions from novaclient import base -from novaclient import exceptions from novaclient.i18n import _ from novaclient import utils +from novaclient.v2 import shell class InstanceActionManager(base.ManagerWithFind): @@ -44,23 +42,6 @@ def list(self, server): base.getid(server), 'instanceActions') -@api_versions.wraps("2.0", "2.20") -def _find_server(cs, args): - return utils.find_resource(cs.servers, args.server) - - -@api_versions.wraps("2.21") -def _find_server(cs, args): - try: - return utils.find_resource(cs.servers, args.server, - wrap_exception=False) - except exceptions.NoUniqueMatch as e: - raise exceptions.CommandError(six.text_type(e)) - except exceptions.NotFound: - # The server can be deleted - return args.server - - @utils.arg( 'server', metavar='', @@ -78,7 +59,10 @@ def _find_server(cs, args): help=_('Request ID of the action to get.')) def do_instance_action(cs, args): """Show an action.""" - server = _find_server(cs, args) + if cs.api_version < api_versions.APIVersion("2.21"): + server = shell._find_server(cs, args.server) + else: + server = shell._find_server(cs, args.server, raise_if_notfound=False) action_resource = cs.instance_action.get(server, args.request_id) action = action_resource._info if 'events' in action: @@ -99,7 +83,10 @@ def do_instance_action(cs, args): start_version="2.21") def do_instance_action_list(cs, args): """List actions on a server.""" - server = _find_server(cs, args) + if cs.api_version < api_versions.APIVersion("2.21"): + server = shell._find_server(cs, args.server) + else: + server = shell._find_server(cs, args.server, raise_if_notfound=False) actions = cs.instance_action.list(server) utils.print_list(actions, ['Action', 'Request_ID', 'Message', 'Start_Time'], diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 769725e42..844682675 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -2156,9 +2156,25 @@ def do_delete(cs, args): _("Unable to delete the specified server(s).")) -def _find_server(cs, server, **find_args): - """Get a server by name or ID.""" - return utils.find_resource(cs.servers, server, **find_args) +def _find_server(cs, server, raise_if_notfound=True, **find_args): + """Get a server by name or ID. + + :param cs: NovaClient's instance + :param server: identifier of server + :param raise_if_notfound: raise an exception if server is not found + :param find_args: argument to search server + """ + if raise_if_notfound: + return utils.find_resource(cs.servers, server, **find_args) + else: + try: + return utils.find_resource(cs.servers, server, + wrap_exception=False) + except exceptions.NoUniqueMatch as e: + raise exceptions.CommandError(six.text_type(e)) + except exceptions.NotFound: + # The server can be deleted + return server def _find_image(cs, image): From 2a72b86cd11605808a936024cf7093e01cf1d46e Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Fri, 15 Apr 2016 12:31:26 +0300 Subject: [PATCH 1039/1705] Restrict positional arguments for Client It is hard to deprecate or remove arguments for Client objects. We already have several args which are not used anywhere and we need to do something with them(clean code). Change-Id: I2218ff0c750922a105d21a13e42f193ffd86ec01 --- novaclient/client.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index 5fa7060dd..f2143d685 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -809,7 +809,8 @@ def get_client_class(version): return client_class -def Client(version, *args, **kwargs): +def Client(version, username=None, api_key=None, project_id=None, + auth_url=None, *args, **kwargs): """Initialize client object based on given version. HOW-TO: @@ -830,7 +831,15 @@ def Client(version, *args, **kwargs): session API. See "The novaclient Python API" page at python-novaclient's doc. """ + if args: + warnings.warn("Only VERSION, USERNAME, PASSWORD, PROJECT_ID and " + "AUTH_URL arguments can be specified as positional " + "arguments. All other variables should be keyword " + "arguments. Note that this will become an error in " + "Ocata.") api_version, client_class = _get_client_class_and_version(version) kwargs.pop("direct_use", None) - return client_class(api_version=api_version, direct_use=False, + return client_class(username=username, api_key=api_key, + project_id=project_id, auth_url=auth_url, + api_version=api_version, direct_use=False, *args, **kwargs) From a47850b093709d4687138264183ab8e237fa1b44 Mon Sep 17 00:00:00 2001 From: Gleb Stepanov Date: Fri, 22 Apr 2016 16:13:33 +0300 Subject: [PATCH 1040/1705] Decorate FakeHTTPClient with versions Remove api_version check from get_servers_1234_migrations method and substitute it by api_version.wraps decorator. Change-Id: Iea780ae92bece4e2013778485d244123251e9dcc --- novaclient/tests/unit/v2/fakes.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index e3e2b1dd6..cd2144763 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -26,7 +26,6 @@ from novaclient import api_versions from novaclient import client as base_client from novaclient import exceptions -from novaclient.i18n import _ from novaclient.tests.unit import fakes from novaclient.tests.unit import utils from novaclient.v2 import client @@ -2174,11 +2173,8 @@ def post_os_server_groups(self, body, **kw): def post_servers_1234_migrations_1_action(self, body): return (202, {}, None) + @api_versions.wraps(start_version="2.23") def get_servers_1234_migrations_1(self, **kw): - # TODO(Shaohe Feng) this condition check can be a decorator - if self.api_version < api_versions.APIVersion("2.23"): - raise exceptions.UnsupportedVersion(_("Unsupport version %s") - % self.api_version) migration = {"migration": { "created_at": "2016-01-29T13:42:02.000000", "dest_compute": "compute2", @@ -2199,11 +2195,8 @@ def get_servers_1234_migrations_1(self, **kw): }} return (200, FAKE_RESPONSE_HEADERS, migration) + @api_versions.wraps(start_version="2.23") def get_servers_1234_migrations(self, **kw): - # TODO(Shaohe Feng) this condition check can be a decorator - if self.api_version < api_versions.APIVersion("2.23"): - raise exceptions.UnsupportedVersion(_("Unsupport version %s") - % self.api_version) migrations = {'migrations': [ { "created_at": "2016-01-29T13:42:02.000000", From 2b9310eaed7b896ef397e66d6c36e9a2604cafba Mon Sep 17 00:00:00 2001 From: xiexs Date: Fri, 22 Apr 2016 04:29:08 -0400 Subject: [PATCH 1041/1705] Add a note message for reboot Currently nova reboot only performs a hard-reboot to baremetal nodes as ironic doesn't support the soft-reboot yet. And as absence of this information, end user has to try to excute this command firstly and then probably realize the difference between the baremetal nodes and ordinary VM instance about this command: * For VM instance nova reboot : perform a soft reboot by default. nova reboot --hard: perform a hard reboot. * For baremetal node nova reboot : perform a hard reboot by default. nova reboot --hard: perform a hard reboot as well. For more user-friendly, we'd better add some notes into the help message explicitly. Change-Id: I7b3f6da4b5f8a514c6d923f3546470d9de459003 Partial-Bug: #1485416 --- novaclient/v2/shell.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 769725e42..26ace2bf8 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -1596,7 +1596,10 @@ def do_list(cs, args): action='store_const', const=servers.REBOOT_HARD, default=servers.REBOOT_SOFT, - help=_('Perform a hard reboot (instead of a soft one).')) + help=_('Perform a hard reboot (instead of a soft one). ' + 'Note: Ironic does not currently support soft reboot; ' + 'consequently, bare metal nodes will always do a hard ' + 'reboot, regardless of the use of this option.')) @utils.arg( 'server', metavar='', nargs='+', From 4b4573ef9dfdca67ceebe02726af028960b8948d Mon Sep 17 00:00:00 2001 From: jichenjc Date: Mon, 25 Apr 2016 04:16:49 +0800 Subject: [PATCH 1042/1705] Enhance descriptions for get and clear password per http://paste.openstack.org/show/495955/ it shows nova get-password and clear-password is calling 'os-server-password' and in turn operate on metadata server instead of the operating systme itself, the existing wording lead to confusion in nova client description. Change-Id: If69622c181ad3259855d45cb21909e5b7de53615 --- novaclient/v2/shell.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index baef1db53..ad9fc5d0c 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -2446,7 +2446,10 @@ def do_get_mks_console(cs, args): nargs='?', default=None) def do_get_password(cs, args): - """Get the admin password for a server.""" + """Get the admin password for a server. This operation calls the metadata + service to query metadata information and does not read password + information from the server itself. + """ server = _find_server(cs, args.server) data = server.get_password(args.private_key) print(data) @@ -2454,7 +2457,9 @@ def do_get_password(cs, args): @utils.arg('server', metavar='', help=_('Name or ID of server.')) def do_clear_password(cs, args): - """Clear the admin password for a server.""" + """Clear the admin password for a server from the metadata server. + This action does not actually change the instance server password. + """ server = _find_server(cs, args.server) server.clear_password() From b1cf8125974142f2bed9911861cdb3a701747f01 Mon Sep 17 00:00:00 2001 From: jichenjc Date: Wed, 27 Apr 2016 04:50:51 +0800 Subject: [PATCH 1043/1705] Add info for user_id in v2.10 Add description so the output of client will be: --user List key-pairs of specified user ID (Admin only). (Supported by API versions '2.10' - '2.latest') Change-Id: I2916ae6086fe0ada115b0435ba110943b47c829f --- novaclient/v2/shell.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 058c6a342..6fec86e84 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -3106,7 +3106,8 @@ def do_keypair_delete(cs, args): '--user', metavar='', default=None, - help=_('ID of key-pair owner (Admin only).')) + help=_('ID of key-pair owner (Admin only).'), + start_version="2.10") def do_keypair_delete(cs, args): """Delete keypair given by its name.""" cs.keypairs.delete(args.name, args.user) @@ -3135,7 +3136,8 @@ def do_keypair_list(cs, args): '--user', metavar='', default=None, - help=_('List key-pairs of specified user ID (Admin only).')) + help=_('List key-pairs of specified user ID (Admin only).'), + start_version="2.10") def do_keypair_list(cs, args): """Print a list of keypairs for a user""" keypairs = cs.keypairs.list(args.user) @@ -3170,7 +3172,8 @@ def do_keypair_show(cs, args): '--user', metavar='', default=None, - help=_('ID of key-pair owner (Admin only).')) + help=_('ID of key-pair owner (Admin only).'), + start_version="2.10") def do_keypair_show(cs, args): """Show details about the given keypair.""" keypair = cs.keypairs.get(args.keypair, args.user) From c22cbb6fac2230c3111ca60748edcc1591e26c13 Mon Sep 17 00:00:00 2001 From: ZhiQiang Fan Date: Wed, 27 Apr 2016 20:13:45 +0800 Subject: [PATCH 1044/1705] [Trivial] change some functions in novaclient/utils.py to public There are some functions in novaclient/utils.py which have name with prefix `_`, in Python, this means it is a private function which should be only used in its own module. However, these functions are used in other modules such as novaclient/v2/shell.py. This patch removes the prefix _ for these functions. Change-Id: I7bc8a76fd390a7dd30eecbb5c7e641b6ccfb40c0 --- novaclient/auth_plugin.py | 4 ++-- novaclient/utils.py | 8 ++++---- novaclient/v2/shell.py | 12 ++++++------ 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/novaclient/auth_plugin.py b/novaclient/auth_plugin.py index d729c4d5a..603bf3a29 100644 --- a/novaclient/auth_plugin.py +++ b/novaclient/auth_plugin.py @@ -137,12 +137,12 @@ def authenticate(cls, auth_url): def _load_endpoints(self): ep_name = 'openstack.client.auth_url' - fn = utils._load_entry_point(ep_name, name=self.auth_system) + fn = utils.load_entry_point(ep_name, name=self.auth_system) if fn: self.get_auth_url = fn ep_name = 'openstack.client.authenticate' - fn = utils._load_entry_point(ep_name, name=self.auth_system) + fn = utils.load_entry_point(ep_name, name=self.auth_system) if fn: self.authenticate = fn diff --git a/novaclient/utils.py b/novaclient/utils.py index 1e34e0783..838e1a776 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -332,7 +332,7 @@ def find_resource(manager, name_or_id, wrap_exception=True, **find_args): raise exceptions.NotFound(404, msg) -def _format_servers_list_networks(server): +def format_servers_list_networks(server): output = [] for (network, addresses) in server.networks.items(): if len(addresses) == 0: @@ -344,7 +344,7 @@ def _format_servers_list_networks(server): return '; '.join(output) -def _format_security_groups(groups): +def format_security_groups(groups): return ', '.join(group['name'] for group in groups) @@ -360,7 +360,7 @@ def _format_field_name(attr): return ': '.join(parts) -def _make_field_formatter(attr, filters=None): +def make_field_formatter(attr, filters=None): """ Given an object attribute, return a formatted field name and a formatter suitable for passing to print_list. @@ -412,7 +412,7 @@ def do_action_on_many(action, resources, success_msg, error_msg): raise exceptions.CommandError(error_msg) -def _load_entry_point(ep_name, name=None): +def load_entry_point(ep_name, name=None): """Try to load the entry point ep_name that matches name.""" for ep in pkg_resources.iter_entry_points(ep_name, name=name): try: diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 89bb4b161..baef1db53 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -972,7 +972,7 @@ def do_network_list(cs, args): if network_list and not hasattr(network_list[0], field): non_existent_fields.append(field) continue - field_title, formatter = utils._make_field_formatter(field, {}) + field_title, formatter = utils.make_field_formatter(field, {}) field_titles.append(field_title) formatters[field_title] = formatter if non_existent_fields: @@ -1504,7 +1504,7 @@ def do_list(cs, args): 'changes-since': args.changes_since} filters = {'flavor': lambda f: f['id'], - 'security_groups': utils._format_security_groups} + 'security_groups': utils.format_security_groups} id_col = 'ID' @@ -1552,8 +1552,8 @@ def do_list(cs, args): if servers and not hasattr(servers[0], field): non_existent_fields.append(field) continue - field_title, formatter = utils._make_field_formatter(field, - filters) + field_title, formatter = utils.make_field_formatter(field, + filters) field_titles.append(field_title) formatters[field_title] = formatter if non_existent_fields: @@ -1582,7 +1582,7 @@ def do_list(cs, args): columns.insert(2, 'Tenant ID') if search_opts['changes-since']: columns.append('Updated') - formatters['Networks'] = utils._format_servers_list_networks + formatters['Networks'] = utils.format_servers_list_networks sortby_index = 1 if args.sort: sortby_index = None @@ -3699,7 +3699,7 @@ def do_server_migration_list(cs, args): "memory_remaining_bytes", "disk_total_bytes", "disk_processed_bytes", "disk_remaining_bytes"] - formatters = map(lambda field: utils._make_field_formatter(field)[1], + formatters = map(lambda field: utils.make_field_formatter(field)[1], format_key) formatters = dict(zip(format_name, formatters)) From f59de4fbcc3e6471fbecb90651680729d42403ec Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Thu, 28 Apr 2016 16:17:01 +0000 Subject: [PATCH 1045/1705] Updated from global requirements Change-Id: If01e72af5a045b3458399f3908141f01836584e4 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index c42e33cf8..e33a32223 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -3,7 +3,7 @@ # process, which may cause wedges in the gate later. hacking<0.11,>=0.10.0 -bandit>=0.17.3 # Apache-2.0 +bandit>=1.0.1 # Apache-2.0 coverage>=3.6 # Apache-2.0 discover # BSD fixtures<2.0,>=1.3.1 # Apache-2.0/BSD From dceb77ff4437f32f5b2e8aafa904759a815c17bd Mon Sep 17 00:00:00 2001 From: xiexs Date: Sun, 24 Apr 2016 22:19:22 -0400 Subject: [PATCH 1046/1705] Clean the duplicated columns for "nova network-list" If we specify --fields with default columns name, then there will be an additional column which is absolutely redundant, we need to check for such case. Also do the same for "nova list" Co-Authored-By: ZhiQiang Fan Change-Id: Ife0a62d2d77aee261ce39c30d3a89ba97a253dae Closes-Bug: #1574424 --- novaclient/tests/unit/v2/test_shell.py | 14 ++++ novaclient/v2/shell.py | 109 +++++++++++++++++-------- 2 files changed, 88 insertions(+), 35 deletions(-) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 17ce6d670..92257d612 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -1082,6 +1082,13 @@ def test_list_with_changes_since_invalid_value(self): self.assertRaises(exceptions.CommandError, self.run_command, 'list --changes-since 0123456789') + def test_list_fields_redundant(self): + output, __ = self.run_command('list --fields id,status,status') + header = output.splitlines()[1] + self.assertEqual(1, header.count('ID')) + self.assertEqual(0, header.count('Id')) + self.assertEqual(1, header.count('Status')) + def test_meta_parsing(self): meta = ['key1=meta1', 'key2=meta2'] ref = {'key1': 'meta1', 'key2': 'meta2'} @@ -2169,6 +2176,13 @@ def test_network_list_invalid_fields(self): self.run_command, 'network-list --fields vlan,project_id,invalid') + def test_network_list_redundant_fields(self): + output, __ = self.run_command( + 'network-list --fields label,project_id,project_id') + header = output.splitlines()[1] + self.assertEqual(1, header.count('Label')) + self.assertEqual(1, header.count('Project Id')) + def test_network_show(self): self.run_command('network-show 1') self.assert_called('GET', '/os-networks') diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index baef1db53..5eec6f7af 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -963,24 +963,9 @@ def do_network_list(cs, args): """Print a list of available networks.""" network_list = cs.networks.list() columns = ['ID', 'Label', 'Cidr'] - - formatters = {} - field_titles = [] - non_existent_fields = [] - if args.fields: - for field in args.fields.split(','): - if network_list and not hasattr(network_list[0], field): - non_existent_fields.append(field) - continue - field_title, formatter = utils.make_field_formatter(field, {}) - field_titles.append(field_title) - formatters[field_title] = formatter - if non_existent_fields: - raise exceptions.CommandError( - _("Non-existent fields are specified: %s") - % non_existent_fields) - - columns = columns + field_titles + columns += _get_list_table_columns_and_formatters( + args.fields, network_list, + exclude_fields=(c.lower() for c in columns))[0] utils.print_list(network_list, columns) @@ -1545,28 +1530,17 @@ def do_list(cs, args): _translate_extended_states(servers) formatters = {} - field_titles = [] - non_existent_fields = [] - if args.fields: - for field in args.fields.split(','): - if servers and not hasattr(servers[0], field): - non_existent_fields.append(field) - continue - field_title, formatter = utils.make_field_formatter(field, - filters) - field_titles.append(field_title) - formatters[field_title] = formatter - if non_existent_fields: - raise exceptions.CommandError( - _("Non-existent fields are specified: %s") - % non_existent_fields) + + cols, fmts = _get_list_table_columns_and_formatters( + args.fields, servers, exclude_fields=('id',), filters=filters) if args.minimal: columns = [ id_col, 'Name'] - elif field_titles: - columns = [id_col] + field_titles + elif cols: + columns = [id_col] + cols + formatters.update(fmts) else: columns = [ id_col, @@ -1590,6 +1564,71 @@ def do_list(cs, args): formatters, sortby_index=sortby_index) +def _get_list_table_columns_and_formatters(fields, objs, exclude_fields=(), + filters=None): + """Check and add fields to output columns. + + If there is any value in fields that not an attribute of obj, + CommandError will be raised. + + If fields has duplicate values (case sensitive), we will make them unique + and ignore duplicate ones. + + If exclude_fields is specified, any field both in fields and + exclude_fields will be ignored. + + :param fields: A list of string contains the fields to be printed. + :param objs: An list of object which will be used to check if field is + valid or not. Note, we don't check fields if obj is None or + empty. + :param exclude_fields: A tuple of string which contains the fields to be + excluded. + :param filters: A dictionary defines how to get value from fields, this + is useful when field's value is a complex object such as + dictionary. + + :return: columns, formatters. + columns is a list of string which will be used as table header. + formatters is a dictionary specifies how to display the value + of the field. + They can be [], {}. + :raise: novaclient.exceptions.CommandError + """ + if not fields: + return [], {} + + if not objs: + obj = None + elif isinstance(objs, list): + obj = objs[0] + else: + obj = objs + + columns = [] + formatters = {} + + non_existent_fields = [] + exclude_fields = set(exclude_fields) + + for field in fields.split(','): + if not hasattr(obj, field): + non_existent_fields.append(field) + continue + if field in exclude_fields: + continue + field_title, formatter = utils.make_field_formatter(field, + filters) + columns.append(field_title) + formatters[field_title] = formatter + exclude_fields.add(field) + + if non_existent_fields: + raise exceptions.CommandError( + _("Non-existent fields are specified: %s") % non_existent_fields) + + return columns, formatters + + @utils.arg( '--hard', dest='reboot_type', From 71680abacf6cb32066cba6042b13fa267f7c9433 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Sat, 30 Apr 2016 18:08:36 +0000 Subject: [PATCH 1047/1705] Updated from global requirements Change-Id: I5a864fac6ee28a0394a70b9101b87092084d6cff --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 510ca434a..eac4a5091 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ # process, which may cause wedges in the gate later. pbr>=1.6 # Apache-2.0 keystoneauth1>=2.1.0 # Apache-2.0 -iso8601>=0.1.9 # MIT +iso8601>=0.1.11 # MIT oslo.i18n>=2.1.0 # Apache-2.0 oslo.serialization>=1.10.0 # Apache-2.0 oslo.utils>=3.5.0 # Apache-2.0 From 6eb2c50d526aa5a047d2fcff30f7556a137be542 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 6 May 2016 22:22:35 +0000 Subject: [PATCH 1048/1705] Updated from global requirements Change-Id: I6746189468f8f3c3a0f914fededc98d5910499b5 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index eac4a5091..7b277dde7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,4 +11,4 @@ PrettyTable<0.8,>=0.7 # BSD requests!=2.9.0,>=2.8.1 # Apache-2.0 simplejson>=2.2.0 # MIT six>=1.9.0 # MIT -Babel!=2.3.0,!=2.3.1,!=2.3.2,!=2.3.3,>=1.3 # BSD +Babel>=2.3.4 # BSD From ba00c1d4267cd6a8150fd1282554feb17b53f937 Mon Sep 17 00:00:00 2001 From: venkatamahesh Date: Tue, 17 May 2016 10:13:18 +0530 Subject: [PATCH 1049/1705] Update the home-page with developer documentation Change-Id: I18a6b02e8dceafd1008144e53be5817d83698741 --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index b9c9008d0..0ce97cbd2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -6,7 +6,7 @@ description-file = license = Apache License, Version 2.0 author = OpenStack author-email = openstack-dev@lists.openstack.org -home-page = https://www.openstack.org +home-page = http://docs.openstack.org/developer/python-novaclient classifier = Development Status :: 5 - Production/Stable Environment :: Console From 4187c69184cff6525517552db97f179e99799bfd Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Tue, 17 May 2016 18:06:20 +0000 Subject: [PATCH 1050/1705] Updated from global requirements Change-Id: I05a64dc574712a6bcef9bba4b524879026ddbc96 --- requirements.txt | 2 +- test-requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 7b277dde7..cd26fbe3b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ oslo.i18n>=2.1.0 # Apache-2.0 oslo.serialization>=1.10.0 # Apache-2.0 oslo.utils>=3.5.0 # Apache-2.0 PrettyTable<0.8,>=0.7 # BSD -requests!=2.9.0,>=2.8.1 # Apache-2.0 +requests>=2.10.0 # Apache-2.0 simplejson>=2.2.0 # MIT six>=1.9.0 # MIT Babel>=2.3.4 # BSD diff --git a/test-requirements.txt b/test-requirements.txt index e33a32223..7a99f19d8 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -9,7 +9,7 @@ discover # BSD fixtures<2.0,>=1.3.1 # Apache-2.0/BSD keyring>=5.5.1 # MIT/PSF mock>=1.2 # BSD -python-keystoneclient!=1.8.0,!=2.1.0,>=1.6.0 # Apache-2.0 +python-keystoneclient!=1.8.0,!=2.1.0,>=1.7.0 # Apache-2.0 python-cinderclient>=1.6.0 # Apache-2.0 requests-mock>=0.7.0 # Apache-2.0 sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2 # BSD From 8030879330da432d4791f64fad4df24c7fc16f71 Mon Sep 17 00:00:00 2001 From: Kevin_Zheng Date: Fri, 13 May 2016 10:45:26 +0800 Subject: [PATCH 1051/1705] Name and AZ should as be optional param on aggregate-update 'name' should be an optional parameter not required on aggregate-update, so we can update 'availability_zone' only. Change-Id: I778ab7ec54a376c60f19dcc89fe62fcab6e59e42 Closes-bug: #1280118 --- novaclient/tests/unit/v2/test_shell.py | 32 +++++++++++++++++++++++--- novaclient/v2/shell.py | 24 +++++++++++++++++-- 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 92257d612..121fbbbb7 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -1611,24 +1611,50 @@ def test_aggregate_delete_by_name(self): self.assert_called('DELETE', '/os-aggregates/1') def test_aggregate_update_by_id(self): - self.run_command('aggregate-update 1 new_name') + self.run_command('aggregate-update 1 --name new_name') body = {"aggregate": {"name": "new_name"}} self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) self.assert_called('GET', '/os-aggregates/1', pos=-1) def test_aggregate_update_by_name(self): - self.run_command('aggregate-update test new_name') + self.run_command('aggregate-update test --name new_name ') body = {"aggregate": {"name": "new_name"}} self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) self.assert_called('GET', '/os-aggregates/1', pos=-1) def test_aggregate_update_with_availability_zone_by_id(self): - self.run_command('aggregate-update 1 foo new_zone') + self.run_command('aggregate-update 1 --name foo ' + '--availability-zone new_zone') body = {"aggregate": {"name": "foo", "availability_zone": "new_zone"}} self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) self.assert_called('GET', '/os-aggregates/1', pos=-1) def test_aggregate_update_with_availability_zone_by_name(self): + self.run_command('aggregate-update test --name foo ' + '--availability-zone new_zone') + body = {"aggregate": {"name": "foo", "availability_zone": "new_zone"}} + self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) + self.assert_called('GET', '/os-aggregates/1', pos=-1) + + def test_aggregate_update_by_id_legacy(self): + self.run_command('aggregate-update 1 new_name') + body = {"aggregate": {"name": "new_name"}} + self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) + self.assert_called('GET', '/os-aggregates/1', pos=-1) + + def test_aggregate_update_by_name_legacy(self): + self.run_command('aggregate-update test new_name') + body = {"aggregate": {"name": "new_name"}} + self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) + self.assert_called('GET', '/os-aggregates/1', pos=-1) + + def test_aggregate_update_with_availability_zone_by_id_legacy(self): + self.run_command('aggregate-update 1 foo new_zone') + body = {"aggregate": {"name": "foo", "availability_zone": "new_zone"}} + self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) + self.assert_called('GET', '/os-aggregates/1', pos=-1) + + def test_aggregate_update_with_availability_zone_by_name_legacy(self): self.run_command('aggregate-update test foo new_zone') body = {"aggregate": {"name": "foo", "availability_zone": "new_zone"}} self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 058c6a342..9ce96704e 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -3550,17 +3550,37 @@ def do_aggregate_delete(cs, args): 'aggregate', metavar='', help=_('Name or ID of aggregate to update.')) -@utils.arg('name', metavar='', help=_('Name of aggregate.')) +@utils.arg( + 'name', + nargs='?', + action=shell.DeprecatedAction, + use=_('use "%s"; this option will be removed in ' + 'novaclient 5.0.0.') % '--name', + help=argparse.SUPPRESS) +@utils.arg( + '--name', + dest='name', + help=_('Name of aggregate.')) @utils.arg( 'availability_zone', metavar='', nargs='?', default=None, + action=shell.DeprecatedAction, + use=_('use "%s"; this option will be removed in ' + 'novaclient 5.0.0.') % '--availability_zone', + help=argparse.SUPPRESS) +@utils.arg( + '--availability-zone', + metavar='', + dest='availability_zone', help=_('The availability zone of the aggregate.')) def do_aggregate_update(cs, args): """Update the aggregate's name and optionally availability zone.""" aggregate = _find_aggregate(cs, args.aggregate) - updates = {"name": args.name} + updates = {} + if args.name: + updates["name"] = args.name if args.availability_zone: updates["availability_zone"] = args.availability_zone From ff9b97b2c6473a7679c2f462a01b82201de0940a Mon Sep 17 00:00:00 2001 From: Sergey Nikitin Date: Wed, 18 May 2016 17:30:40 +0300 Subject: [PATCH 1052/1705] Added Support of microverison 2.26 This microversion allows to create/delete/update server tags and to search servers by tags. Implements: blueprint tag-instances Change-Id: I66b6d4a763c507335f27a425bc3d4c2aae377c00 --- novaclient/__init__.py | 2 +- novaclient/tests/unit/fixture_data/servers.py | 44 +++++++++ novaclient/tests/unit/v2/fakes.py | 18 ++++ novaclient/tests/unit/v2/test_servers.py | 42 ++++++++ novaclient/tests/unit/v2/test_shell.py | 46 +++++++++ novaclient/v2/servers.py | 84 ++++++++++++++++ novaclient/v2/shell.py | 96 +++++++++++++++++++ 7 files changed, 331 insertions(+), 1 deletion(-) diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 0f8b98308..8661aef9c 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.25") +API_MAX_VERSION = api_versions.APIVersion("2.26") diff --git a/novaclient/tests/unit/fixture_data/servers.py b/novaclient/tests/unit/fixture_data/servers.py index 9b68a6560..bf0dac922 100644 --- a/novaclient/tests/unit/fixture_data/servers.py +++ b/novaclient/tests/unit/fixture_data/servers.py @@ -316,6 +316,50 @@ def post_os_volumes_boot(request, context): self.url(1234, 'os-server-password'), status_code=202, headers=self.json_headers) + # + # Server tags + # + + self.requests.register_uri('GET', + self.url(1234, 'tags'), + json={'tags': ['tag1', 'tag2']}, + headers=self.json_headers) + + self.requests.register_uri('GET', + self.url(1234, 'tags', 'tag'), + status_code=204, + headers=self.json_headers) + + self.requests.register_uri('DELETE', + self.url(1234, 'tags', 'tag'), + status_code=204, + headers=self.json_headers) + + self.requests.register_uri('DELETE', + self.url(1234, 'tags'), + status_code=204, + headers=self.json_headers) + + def put_server_tag(request, context): + body = jsonutils.loads(request.body) + assert body is None + context.status_code = 201 + return None + + self.requests.register_uri('PUT', + self.url(1234, 'tags', 'tag'), + json=put_server_tag, + headers=self.json_headers) + + def put_server_tags(request, context): + body = jsonutils.loads(request.body) + assert list(body) == ['tags'] + return body + + self.requests.register_uri('PUT', + self.url(1234, 'tags'), + json=put_server_tags, + headers=self.json_headers) class V1(Base): diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index cd2144763..bc4c6e638 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -2221,6 +2221,24 @@ def get_servers_1234_migrations(self, **kw): def delete_servers_1234_migrations_1(self): return (202, {}, None) + def put_servers_1234_tags_tag(self, **kw): + return (201, {}, None) + + def put_servers_1234_tags(self, **kw): + return (201, {}, None) + + def get_servers_1234_tags(self, **kw): + return (200, {}, {'tags': ['tag1', 'tag2']}) + + def delete_servers_1234_tags_tag(self, **kw): + return (204, {}, None) + + def delete_servers_1234_tags(self, **kw): + return (204, {}, None) + + def get_servers_1234_tags_tag(self, **kw): + return (204, {}, None) + class FakeSessionClient(fakes.FakeClient, client.Client): diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index cdbddd0f9..51e35731b 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -1148,3 +1148,45 @@ def test_live_migrate_server_with_disk_over_commit(self): s = self.cs.servers.get(1234) self.assertRaises(ValueError, s.live_migrate, 'hostname', 'auto', 'True') + + +class ServersV226Test(ServersV225Test): + def setUp(self): + super(ServersV219Test, self).setUp() + self.cs.api_version = api_versions.APIVersion("2.26") + + def test_tag_list(self): + s = self.cs.servers.get(1234) + ret = s.tag_list() + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('GET', '/servers/1234/tags') + + def test_tag_delete(self): + s = self.cs.servers.get(1234) + ret = s.delete_tag('tag') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('DELETE', '/servers/1234/tags/tag') + + def test_tag_delete_all(self): + s = self.cs.servers.get(1234) + ret = s.delete_all_tags() + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('DELETE', '/servers/1234/tags') + + def test_tag_add(self): + s = self.cs.servers.get(1234) + ret = s.add_tag('tag') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('PUT', '/servers/1234/tags/tag') + + def test_tags_set(self): + s = self.cs.servers.get(1234) + ret = s.set_tags(['tag1', 'tag2']) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('PUT', '/servers/1234/tags') + + def test_tag_exists(self): + s = self.cs.servers.get(1234) + ret = s.tag_exists('tag') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('GET', '/servers/1234/tags/tag') diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 92257d612..766d7e85f 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2775,6 +2775,52 @@ def test_list_v2_10(self): self.run_command('list', api_version='2.10') self.assert_called('GET', '/servers/detail') + def test_server_tag_add(self): + self.run_command('server-tag-add sample-server tag', + api_version='2.26') + self.assert_called('PUT', '/servers/1234/tags/tag', None) + + def test_server_tag_set(self): + self.run_command('server-tag-set sample-server tag1 tag2', + api_version='2.26') + self.assert_called('PUT', '/servers/1234/tags', + {'tags': ['tag1', 'tag2']}) + + def test_server_tag_list(self): + self.run_command('server-tag-list sample-server', api_version='2.26') + self.assert_called('GET', '/servers/1234/tags') + + def test_server_tag_delete(self): + self.run_command('server-tag-delete sample-server tag', + api_version='2.26') + self.assert_called('DELETE', '/servers/1234/tags/tag') + + def test_server_tag_delete_all(self): + self.run_command('server-tag-delete-all sample-server', + api_version='2.26') + self.assert_called('DELETE', '/servers/1234/tags') + + def test_server_tag_exists(self): + self.run_command('server-tag-exists sample-server tag', + api_version='2.26') + self.assert_called('GET', '/servers/1234/tags/tag') + + def test_list_v2_26_tags(self): + self.run_command('list --tags tag1,tag2', api_version='2.26') + self.assert_called('GET', '/servers/detail?tags=tag1%2Ctag2') + + def test_list_v2_26_tags_any(self): + self.run_command('list --tags-any tag1,tag2', api_version='2.26') + self.assert_called('GET', '/servers/detail?tags-any=tag1%2Ctag2') + + def test_list_v2_26_not_tags(self): + self.run_command('list --not-tags tag1,tag2', api_version='2.26') + self.assert_called('GET', '/servers/detail?not-tags=tag1%2Ctag2') + + def test_list_v2_26_not_tags_any(self): + self.run_command('list --not-tags-any tag1,tag2', api_version='2.26') + self.assert_called('GET', '/servers/detail?not-tags-any=tag1%2Ctag2') + class ShellWithSessionClientTest(ShellTest): diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index b81e0475e..51aef7776 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -526,6 +526,42 @@ def trigger_crash_dump(self): """Trigger crash dump in an instance""" return self.manager.trigger_crash_dump(self) + def tag_list(self): + """ + Get list of tags from an instance. + """ + return self.manager.tag_list(self) + + def delete_tag(self, tag): + """ + Remove single tag from an instance. + """ + return self.manager.delete_tag(self, tag) + + def delete_all_tags(self): + """ + Remove all tags from an instance. + """ + return self.manager.delete_all_tags(self) + + def set_tags(self, tags): + """ + Set list of tags to an instance. + """ + return self.manager.set_tags(self, tags) + + def add_tag(self, tag): + """ + Add single tag to an instance. + """ + return self.manager.add_tag(self, tag) + + def tag_exists(self, tag): + """ + Check if an instance has specified tag. + """ + return self.manager.tag_exists(self, tag) + class NetworkInterface(base.Resource): @property @@ -1709,3 +1745,51 @@ def _console(self, server, info=None, **kwargs): url = '/servers/%s/remote-consoles' % base.getid(server) resp, body = self.api.client.post(url, body=body) return self.convert_into_with_meta(body, resp) + + @api_versions.wraps('2.26') + def tag_list(self, server): + """ + Get list of tags from an instance. + """ + resp, body = self.api.client.get( + "/servers/%s/tags" % base.getid(server)) + return base.ListWithMeta(body['tags'], resp) + + @api_versions.wraps('2.26') + def delete_tag(self, server, tag): + """ + Remove single tag from an instance. + """ + return self._delete("/servers/%s/tags/%s" % (base.getid(server), tag)) + + @api_versions.wraps('2.26') + def delete_all_tags(self, server): + """ + Remove all tags from an instance. + """ + return self._delete("/servers/%s/tags" % base.getid(server)) + + @api_versions.wraps('2.26') + def set_tags(self, server, tags): + """ + Set list of tags to an instance. + """ + body = {"tags": tags} + return self._update("/servers/%s/tags" % base.getid(server), body) + + @api_versions.wraps('2.26') + def add_tag(self, server, tag): + """ + Add single tag to an instance. + """ + return self._update( + "/servers/%s/tags/%s" % (base.getid(server), tag), None) + + @api_versions.wraps('2.26') + def tag_exists(self, server, tag): + """ + Check if an instance has specified tag. + """ + resp, body = self.api.client.get( + "/servers/%s/tags/%s" % (base.getid(server), tag)) + return self.convert_into_with_meta(body, resp) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 058c6a342..bd4d5d3e9 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -1461,6 +1461,45 @@ def do_image_delete(cs, args): help=_("List only servers changed after a certain point of time." "The provided time should be an ISO 8061 formatted time." "ex 2016-03-04T06:27:59Z .")) +@utils.arg( + '--tags', + dest='tags', + metavar='', + default=None, + help=_("The given tags must all be present for a server to be included in " + "the list result. Boolean expression in this case is 't1 AND t2'. " + "Tags must be separated by commas: --tags "), + start_version="2.26") +@utils.arg( + '--tags-any', + dest='tags-any', + metavar='', + default=None, + help=_("If one of the given tags is present the server will be included " + "in the list result. Boolean expression in this case is " + "'t1 OR t2'. Tags must be separated by commas: " + "--tags-any "), + start_version="2.26") +@utils.arg( + '--not-tags', + dest='not-tags', + metavar='', + default=None, + help=_("Only the servers that do not have any of the given tags will" + "be included in the list results. Boolean expression in this case " + "is 'NOT(t1 AND t2)'. Tags must be separated by commas: " + "--not-tags "), + start_version="2.26") +@utils.arg( + '--not-tags-any', + dest='not-tags-any', + metavar='', + default=None, + help=_("Only the servers that do not have at least one of the given tags" + "will be included in the list result. Boolean expression in this " + "case is 'NOT(t1 OR t2)'. Tags must be separated by commas: " + "--not-tags-any "), + start_version="2.26") def do_list(cs, args): """List active servers.""" imageid = None @@ -1488,6 +1527,10 @@ def do_list(cs, args): 'instance_name': args.instance_name, 'changes-since': args.changes_since} + for arg in ('tags', "tags-any", 'not-tags', 'not-tags-any'): + if arg in args: + search_opts[arg] = getattr(args, arg) + filters = {'flavor': lambda f: f['id'], 'security_groups': utils.format_security_groups} @@ -4956,3 +4999,56 @@ def do_virtual_interface_list(cs, args): server = _find_server(cs, args.server) interface_list = cs.virtual_interfaces.list(base.getid(server)) _print_virtual_interface_list(cs, interface_list) + + +@api_versions.wraps("2.26") +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +def do_server_tag_list(cs, args): + """Get list of tags from a server.""" + server = _find_server(cs, args.server) + tags = server.tag_list() + utils.print_list(tags, 'name') + + +@api_versions.wraps("2.26") +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('tag', metavar='', help=_('Tag to add.')) +def do_server_tag_add(cs, args): + """Add single tag to a server.""" + server = _find_server(cs, args.server) + server.add_tag(args.tag) + + +@api_versions.wraps("2.26") +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('tags', metavar='', nargs='+', help=_('Tag(s) to set.')) +def do_server_tag_set(cs, args): + """Set list of tags to a server.""" + server = _find_server(cs, args.server) + server.set_tags(args.tags) + + +@api_versions.wraps("2.26") +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('tag', metavar='', help=_('Tag to delete.')) +def do_server_tag_delete(cs, args): + """Delete single tag from a server.""" + server = _find_server(cs, args.server) + server.delete_tag(args.tag) + + +@api_versions.wraps("2.26") +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +def do_server_tag_delete_all(cs, args): + """Delete all tags from a server.""" + server = _find_server(cs, args.server) + server.delete_all_tags() + + +@api_versions.wraps("2.26") +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('tag', metavar='', help=_('Tag to check if it exists or not.')) +def do_server_tag_exists(cs, args): + """Check if a server has specified tag.""" + server = _find_server(cs, args.server) + server.tag_exists(args.tag) From 1fd68964ea33c9327ccd1bb566ea3ea8bffa725d Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Mon, 16 May 2016 12:08:48 -0400 Subject: [PATCH 1053/1705] Deprecate --tenant option from flavor-access-list The --tenant option in the flavor-access-list command was never implemented and filtering by tenant is not supported in the REST API for os-flavor-access, so to avoid confusion this change deprecates the command line option. It also removes the cruft that was in the python API code so that only 'flavor' is allowed. This it not a backward incompatible change since list() still takes kwargs and still raises a NotImplementedError for anything in kwargs but 'flavor'. Change-Id: I2c36376674f3a7caf5967a16ac0152f17d9fc906 Closes-Bug: #1582284 --- novaclient/tests/unit/v2/test_shell.py | 9 +++------ novaclient/v2/flavor_access.py | 23 ++++++++--------------- novaclient/v2/shell.py | 13 ++++++------- 3 files changed, 17 insertions(+), 28 deletions(-) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 92257d612..342611e9f 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -845,14 +845,11 @@ def test_flavor_access_list_flavor(self): self.run_command('flavor-access-list --flavor 2') self.assert_called('GET', '/flavors/2/os-flavor-access') - # FIXME: flavor-access-list is not implemented yet - # def test_flavor_access_list_tenant(self): - # self.run_command('flavor-access-list --tenant proj2') - # self.assert_called('GET', '/flavors/2/os-flavor-access') - def test_flavor_access_list_bad_filter(self): cmd = 'flavor-access-list --flavor 2 --tenant proj2' - self.assertRaises(exceptions.CommandError, self.run_command, cmd) + _, err = self.run_command(cmd) + # assert the deprecation warning for using --tenant + self.assertIn('WARNING: Option "--tenant" is deprecated', err) def test_flavor_access_list_no_filter(self): cmd = 'flavor-access-list' diff --git a/novaclient/v2/flavor_access.py b/novaclient/v2/flavor_access.py index 73855ee97..ecb0cc57b 100644 --- a/novaclient/v2/flavor_access.py +++ b/novaclient/v2/flavor_access.py @@ -30,22 +30,15 @@ class FlavorAccessManager(base.ManagerWithFind): resource_class = FlavorAccess def list(self, **kwargs): + # NOTE(mriedem): This looks a bit weird, you would normally expect this + # method to just take a flavor arg, but it used to erroneously accept + # flavor or tenant, but never actually implemented support for listing + # flavor access by tenant. We leave the interface unchanged though for + # backward compatibility. if kwargs.get('flavor'): - return self._list_by_flavor(kwargs['flavor']) - elif kwargs.get('tenant'): - return self._list_by_tenant(kwargs['tenant']) - else: - raise NotImplementedError(_('Unknown list options.')) - - def _list_by_flavor(self, flavor): - return self._list('/flavors/%s/os-flavor-access' % base.getid(flavor), - 'flavor_access') - - def _list_by_tenant(self, tenant): - """Print flavor list shared with the given tenant.""" - # TODO(uni): need to figure out a proper URI for list_by_tenant - # since current API already provided current tenant_id information - raise NotImplementedError(_('Sorry, query by tenant not supported.')) + return self._list('/flavors/%s/os-flavor-access' % + base.getid(kwargs['flavor']), 'flavor_access') + raise NotImplementedError(_('Unknown list options.')) def add_tenant_access(self, flavor, tenant): """Add a tenant to the given flavor access list.""" diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 058c6a342..26cc3f662 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -878,20 +878,19 @@ def do_flavor_key(cs, args): help=_("Filter results by flavor name or ID.")) @utils.arg( '--tenant', metavar='', - help=_('Filter results by tenant ID.')) + help=_('Filter results by tenant ID.'), + action=shell.DeprecatedAction, + real_action='nothing', + use=_('this option is not supported, and will be ' + 'removed in version 5.0.0.')) def do_flavor_access_list(cs, args): """Print access information about the given flavor.""" - if args.flavor and args.tenant: - raise exceptions.CommandError(_("Unable to filter results by " - "both --flavor and --tenant.")) - elif args.flavor: + if args.flavor: flavor = _find_flavor(cs, args.flavor) if flavor.is_public: raise exceptions.CommandError(_("Access list not available " "for public flavors.")) kwargs = {'flavor': flavor} - elif args.tenant: - kwargs = {'tenant': args.tenant} else: raise exceptions.CommandError(_("Unable to get all access lists. " "Specify --flavor")) From 16bf62aa69df4fcbffcfe099a94962f8027376f3 Mon Sep 17 00:00:00 2001 From: Kevin_Zheng Date: Thu, 19 May 2016 19:25:51 +0800 Subject: [PATCH 1054/1705] Use tempes.lib instead of tempes_lib As tempest_lib is deprecated. The code has been re-integrated in tempest under the tempest.lib namespace, no modifies could be done to tempest_lib. We should use tempest.lib instead of tempest_lib to make the tests passed when we remove support for --endpoint-type. Change-Id: I864aa967fe6895b8f193a6ec7506dc743b0b3392 --- novaclient/tests/functional/base.py | 8 ++++---- novaclient/tests/functional/test_auth.py | 4 ++-- novaclient/tests/functional/v2/legacy/test_consoles.py | 2 +- novaclient/tests/functional/v2/legacy/test_keypairs.py | 2 +- .../tests/functional/v2/legacy/test_readonly_nova.py | 4 ++-- novaclient/tests/functional/v2/test_instance_action.py | 2 +- test-requirements.txt | 2 +- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/novaclient/tests/functional/base.py b/novaclient/tests/functional/base.py index 085ffa6c2..300179f3c 100644 --- a/novaclient/tests/functional/base.py +++ b/novaclient/tests/functional/base.py @@ -21,7 +21,7 @@ from keystoneclient import client as keystoneclient import os_client_config import six -import tempest_lib.cli.base +import tempest.lib.cli.base import testtools import novaclient @@ -196,7 +196,7 @@ def setUp(self): self.network = pick_network(self.client.networks.list()) # create a CLI client in case we'd like to do CLI - # testing. tempest_lib does this really weird thing where it + # testing. tempest.lib does this really weird thing where it # builds a giant factory of all the CLIs that it knows # about. Eventually that should really be unwound into # something more sensible. @@ -204,7 +204,7 @@ def setUp(self): 'OS_NOVACLIENT_EXEC_DIR', os.path.join(os.path.abspath('.'), '.tox/functional/bin')) - self.cli_clients = tempest_lib.cli.base.CLIClient( + self.cli_clients = tempest.lib.cli.base.CLIClient( username=user, password=passwd, tenant_name=tenant, @@ -404,7 +404,7 @@ def setUp(self): user_name, password, tenant_id=self.project_id).id self.addCleanup(self.keystone.users.delete, self.user_id) - self.cli_clients_2 = tempest_lib.cli.base.CLIClient( + self.cli_clients_2 = tempest.lib.cli.base.CLIClient( username=user_name, password=password, tenant_name=project_name, diff --git a/novaclient/tests/functional/test_auth.py b/novaclient/tests/functional/test_auth.py index e37fc789e..0ee995b7b 100644 --- a/novaclient/tests/functional/test_auth.py +++ b/novaclient/tests/functional/test_auth.py @@ -11,7 +11,7 @@ # under the License. from six.moves.urllib import parse -import tempest_lib.cli.base +import tempest.lib.cli.base from novaclient.tests.functional import base @@ -32,7 +32,7 @@ def nova(self, action, identity_api_version): if self.cli_clients.insecure: flags += ' --insecure ' - return tempest_lib.cli.base.execute( + return tempest.lib.cli.base.execute( "nova", action, flags, cli_dir=self.cli_clients.cli_dir) def test_auth_via_keystone_v2(self): diff --git a/novaclient/tests/functional/v2/legacy/test_consoles.py b/novaclient/tests/functional/v2/legacy/test_consoles.py index 318541f9b..5afec203e 100644 --- a/novaclient/tests/functional/v2/legacy/test_consoles.py +++ b/novaclient/tests/functional/v2/legacy/test_consoles.py @@ -11,7 +11,7 @@ # License for the specific language governing permissions and limitations # under the License. -from tempest_lib import exceptions +from tempest.lib import exceptions from novaclient.tests.functional import base diff --git a/novaclient/tests/functional/v2/legacy/test_keypairs.py b/novaclient/tests/functional/v2/legacy/test_keypairs.py index 9828df02e..85a1a3a77 100644 --- a/novaclient/tests/functional/v2/legacy/test_keypairs.py +++ b/novaclient/tests/functional/v2/legacy/test_keypairs.py @@ -13,7 +13,7 @@ import tempfile import uuid -from tempest_lib import exceptions +from tempest.lib import exceptions from novaclient.tests.functional import base from novaclient.tests.functional.v2 import fake_crypto diff --git a/novaclient/tests/functional/v2/legacy/test_readonly_nova.py b/novaclient/tests/functional/v2/legacy/test_readonly_nova.py index 7f350199e..d8ac5d0e5 100644 --- a/novaclient/tests/functional/v2/legacy/test_readonly_nova.py +++ b/novaclient/tests/functional/v2/legacy/test_readonly_nova.py @@ -10,8 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. -from tempest_lib import decorators -from tempest_lib import exceptions +from tempest.lib import decorators +from tempest.lib import exceptions from novaclient.tests.functional import base diff --git a/novaclient/tests/functional/v2/test_instance_action.py b/novaclient/tests/functional/v2/test_instance_action.py index a4557277e..db95b9b70 100644 --- a/novaclient/tests/functional/v2/test_instance_action.py +++ b/novaclient/tests/functional/v2/test_instance_action.py @@ -13,7 +13,7 @@ import uuid import six -from tempest_lib import exceptions +from tempest.lib import exceptions from novaclient.tests.functional import base diff --git a/test-requirements.txt b/test-requirements.txt index 7a99f19d8..bb4354955 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -18,7 +18,7 @@ oslosphinx!=3.4.0,>=2.5.0 # Apache-2.0 testrepository>=0.0.18 # Apache-2.0/BSD testscenarios>=0.4 # Apache-2.0/BSD testtools>=1.4.0 # MIT -tempest-lib>=0.14.0 # Apache-2.0 +tempest>=11.0.0 # Apache-2.0 # releasenotes reno>=1.6.2 # Apache2 From 92e7508651749710961412bd281666ee782bbe5f Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Mon, 23 May 2016 20:48:56 +0000 Subject: [PATCH 1055/1705] Updated from global requirements Change-Id: I037ab3105e57bb9acb6929f5fd3565aeb881adb2 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index bb4354955..4c96ee3e7 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -10,7 +10,7 @@ fixtures<2.0,>=1.3.1 # Apache-2.0/BSD keyring>=5.5.1 # MIT/PSF mock>=1.2 # BSD python-keystoneclient!=1.8.0,!=2.1.0,>=1.7.0 # Apache-2.0 -python-cinderclient>=1.6.0 # Apache-2.0 +python-cinderclient!=1.7.0,>=1.6.0 # Apache-2.0 requests-mock>=0.7.0 # Apache-2.0 sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2 # BSD os-client-config>=1.13.1 # Apache-2.0 From fc90f825f478ac20762c30ec2c84d538acb3ea84 Mon Sep 17 00:00:00 2001 From: sampathP Date: Tue, 24 May 2016 15:04:52 +0900 Subject: [PATCH 1056/1705] Fix nova host-evacuate for v2.14 From API micro version 2.14, evacuate does not need onSharedStorage flag. This patch check the API micro version and set/ignore the onSharedStorage flag before call evacuate for each instance. Closes-Bug: #1581336 Change-Id: I825653d66f94d36a945b8240ec52285827423375 --- novaclient/tests/unit/v2/test_shell.py | 13 +++++++++++++ novaclient/v2/contrib/host_evacuate.py | 16 +++++++++++++--- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index c5bbd66b6..9e8e228e0 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -1980,6 +1980,19 @@ def test_host_reboot(self): self.assert_called( 'GET', '/os-hosts/sample-host/reboot') + def test_host_evacuate_v2_14(self): + self.run_command('host-evacuate hyper --target target_hyper', + api_version='2.14') + self.assert_called('GET', '/os-hypervisors/hyper/servers', pos=0) + self.assert_called('POST', '/servers/uuid1/action', + {'evacuate': {'host': 'target_hyper'}}, pos=1) + self.assert_called('POST', '/servers/uuid2/action', + {'evacuate': {'host': 'target_hyper'}}, pos=2) + self.assert_called('POST', '/servers/uuid3/action', + {'evacuate': {'host': 'target_hyper'}}, pos=3) + self.assert_called('POST', '/servers/uuid4/action', + {'evacuate': {'host': 'target_hyper'}}, pos=4) + def test_host_evacuate(self): self.run_command('host-evacuate hyper --target target_hyper') self.assert_called('GET', '/os-hypervisors/hyper/servers', pos=0) diff --git a/novaclient/v2/contrib/host_evacuate.py b/novaclient/v2/contrib/host_evacuate.py index 8b1016378..2a0335d30 100644 --- a/novaclient/v2/contrib/host_evacuate.py +++ b/novaclient/v2/contrib/host_evacuate.py @@ -13,6 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient import api_versions from novaclient import base from novaclient.i18n import _ from novaclient import utils @@ -26,8 +27,15 @@ def _server_evacuate(cs, server, args): success = True error_message = "" try: - cs.servers.evacuate(server=server['uuid'], host=args.target_host, - on_shared_storage=args.on_shared_storage) + on_shared_storage = getattr(args, 'on_shared_storage', None) + if api_versions.APIVersion("2.14") <= cs.api_version: + # if microversion >= 2.14 + cs.servers.evacuate(server=server['uuid'], host=args.target_host) + else: + # else microversion 2.0 - 2.13 + cs.servers.evacuate(server=server['uuid'], + host=args.target_host, + on_shared_storage=on_shared_storage) except Exception as e: success = False error_message = _("Error while evacuating instance: %s") % e @@ -49,7 +57,9 @@ def _server_evacuate(cs, server, args): dest='on_shared_storage', action="store_true", default=False, - help=_('Specifies whether all instances files are on shared storage')) + help=_('Specifies whether all instances files are on shared storage'), + start_version='2.0', + end_version='2.13') def do_host_evacuate(cs, args): """Evacuate all instances from failed host.""" hypervisors = cs.hypervisors.search(args.host, servers=True) From 2253d022667c8bb3ef61b535d0ce90e433882ebc Mon Sep 17 00:00:00 2001 From: Kevin_Zheng Date: Fri, 27 May 2016 09:18:01 +0800 Subject: [PATCH 1057/1705] Fix funtional test gate failure caused by keystone client change. After the merge of https://review.openstack.org/#/c/193894/ the path endpoint is used instead of ports. This causes functional test failure. This patch fixes this. Change-Id: Ic052e518cbb8be531a048129ad47cd19ad460268 Closes-bug: #1586222 --- novaclient/tests/functional/test_auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/tests/functional/test_auth.py b/novaclient/tests/functional/test_auth.py index 0ee995b7b..58e303bcb 100644 --- a/novaclient/tests/functional/test_auth.py +++ b/novaclient/tests/functional/test_auth.py @@ -20,7 +20,7 @@ class TestAuthentication(base.ClientTestBase): def nova(self, action, identity_api_version): url = parse.urlparse(self.cli_clients.uri) url = parse.urlunparse((url.scheme, url.netloc, - '/v%s' % identity_api_version, + '/identity/v%s' % identity_api_version, url.params, url.query, url.fragment)) flags = ('--os-username %s --os-tenant-name %s --os-password %s ' From 082ed8036996266d623d725a59527d30846a66d2 Mon Sep 17 00:00:00 2001 From: Chris Dent Date: Tue, 31 May 2016 13:17:47 +0100 Subject: [PATCH 1058/1705] Update to microversion 2.27 This enables the use of the OpenStack-API-Version header, in addition to the older X-OpenStack-Nova-API-Version header. If the client knows that it is in a version less than 2.27, it will not send the newer header. If the client knows that is in a version >= 2.27 if it gets a response that does not have the expected response header (the newer one) it will warn. If it is in an older version, it will warn about the other one missing. A server that is 2.27 or beyond will accept both headers and respond with both. Change-Id: I1f7b1ca0fe795e4ecd333de761d96fff117969c0 --- novaclient/__init__.py | 2 +- novaclient/api_versions.py | 36 ++++++++++++++++------ novaclient/tests/unit/test_api_versions.py | 30 ++++++++++++++++-- novaclient/tests/unit/v2/test_shell.py | 3 ++ 4 files changed, 57 insertions(+), 14 deletions(-) diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 8661aef9c..0e996a22c 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.26") +API_MAX_VERSION = api_versions.APIVersion("2.27") diff --git a/novaclient/api_versions.py b/novaclient/api_versions.py index 8774ea971..ff1da0675 100644 --- a/novaclient/api_versions.py +++ b/novaclient/api_versions.py @@ -29,7 +29,9 @@ LOG.addHandler(logging.StreamHandler()) -HEADER_NAME = "X-OpenStack-Nova-API-Version" +LEGACY_HEADER_NAME = "X-OpenStack-Nova-API-Version" +HEADER_NAME = "OpenStack-API-Version" +SERVICE_TYPE = "compute" # key is a deprecated version and value is an alternative version. DEPRECATED_VERSIONS = {"1.1": "2"} @@ -317,19 +319,25 @@ def discover_version(client, requested_version): def update_headers(headers, api_version): - """Set 'X-OpenStack-Nova-API-Version' header if api_version is not null""" + """Set microversion headers if api_version is not null""" - if not api_version.is_null() and api_version.ver_minor != 0: - headers[HEADER_NAME] = api_version.get_string() + if not api_version.is_null(): + version_string = api_version.get_string() + if api_version.ver_minor != 0: + headers[LEGACY_HEADER_NAME] = version_string + if api_version.ver_minor >= 27: + headers[HEADER_NAME] = '%s %s' % (SERVICE_TYPE, version_string) def check_headers(response, api_version): - """Checks that 'X-OpenStack-Nova-API-Version' header in response.""" - if api_version.ver_minor > 0 and HEADER_NAME not in response.headers: - LOG.warning(_LW( - "Your request was processed by a Nova API which does not support " - "microversions (%s header is missing from response). " - "Warning: Response may be incorrect."), HEADER_NAME) + """Checks that microversion header is in response.""" + if api_version.ver_minor > 0: + if (api_version.ver_minor < 27 + and LEGACY_HEADER_NAME not in response.headers): + _warn_missing_microversion_header(LEGACY_HEADER_NAME) + elif (api_version.ver_minor >= 27 + and HEADER_NAME not in response.headers): + _warn_missing_microversion_header(HEADER_NAME) def add_substitution(versioned_method): @@ -378,3 +386,11 @@ def substitution(obj, *args, **kwargs): return substitution return decor + + +def _warn_missing_microversion_header(header_name): + """Log a warning about missing microversion response header.""" + LOG.warning(_LW( + "Your request was processed by a Nova API which does not support " + "microversions (%s header is missing from response). " + "Warning: Response may be incorrect."), header_name) diff --git a/novaclient/tests/unit/test_api_versions.py b/novaclient/tests/unit/test_api_versions.py index ce27e631a..e49e554d3 100644 --- a/novaclient/tests/unit/test_api_versions.py +++ b/novaclient/tests/unit/test_api_versions.py @@ -144,6 +144,18 @@ def test_api_version_is_not_null(self): {"X-OpenStack-Nova-API-Version": api_version.get_string()}, headers) + def test_api_version_is_gte_27(self): + api_version = api_versions.APIVersion("2.27") + headers = {} + api_versions.update_headers(headers, api_version) + self.assertIn('X-OpenStack-Nova-API-Version', headers) + self.assertIn('OpenStack-API-Version', headers) + self.assertEqual(api_version.get_string(), + headers['X-OpenStack-Nova-API-Version']) + self.assertEqual('%s %s' % (api_versions.SERVICE_TYPE, + api_version.get_string()), + headers['OpenStack-API-Version']) + class CheckHeadersTestCase(utils.TestCase): def setUp(self): @@ -152,8 +164,9 @@ def setUp(self): self.mock_log = mock_log_patch.start() self.addCleanup(mock_log_patch.stop) - def test_microversion_is_specified(self): - response = mock.MagicMock(headers={api_versions.HEADER_NAME: ""}) + def test_legacy_microversion_is_specified(self): + response = mock.MagicMock( + headers={api_versions.LEGACY_HEADER_NAME: ""}) api_versions.check_headers(response, api_versions.APIVersion("2.2")) self.assertFalse(self.mock_log.warning.called) @@ -161,8 +174,19 @@ def test_microversion_is_specified(self): api_versions.check_headers(response, api_versions.APIVersion("2.2")) self.assertTrue(self.mock_log.warning.called) + def test_generic_microversion_is_specified(self): + response = mock.MagicMock( + headers={api_versions.HEADER_NAME: ""}) + api_versions.check_headers(response, api_versions.APIVersion("2.27")) + self.assertFalse(self.mock_log.warning.called) + + response = mock.MagicMock(headers={}) + api_versions.check_headers(response, api_versions.APIVersion("2.27")) + self.assertTrue(self.mock_log.warning.called) + def test_microversion_is_not_specified(self): - response = mock.MagicMock(headers={api_versions.HEADER_NAME: ""}) + response = mock.MagicMock( + headers={api_versions.LEGACY_HEADER_NAME: ""}) api_versions.check_headers(response, api_versions.APIVersion("2.2")) self.assertFalse(self.mock_log.warning.called) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index c5bbd66b6..22d4dcb0c 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2776,6 +2776,9 @@ def test_versions(self): # new microversion, just an additional checks. See # https://review.openstack.org/#/c/233076/ for more details) 20, # doesn't require any changes in novaclient + 27, # NOTE(cdent): 27 adds support for updated microversion + # headers, and is tested in test_api_versions, but is + # not explicitly tested via wraps and _SUBSTITUTIONS. ]) versions_supported = set(range(0, novaclient.API_MAX_VERSION.ver_minor + 1)) From 1d7e946696ce9d0236dd38535b3e2817f80cbfaf Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Wed, 1 Jun 2016 13:54:19 +0000 Subject: [PATCH 1059/1705] Updated from global requirements Change-Id: I8f7c5887407f0f8c14df512719509f196f4e5e94 --- requirements.txt | 2 +- test-requirements.txt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index cd26fbe3b..29825fc4f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ keystoneauth1>=2.1.0 # Apache-2.0 iso8601>=0.1.11 # MIT oslo.i18n>=2.1.0 # Apache-2.0 oslo.serialization>=1.10.0 # Apache-2.0 -oslo.utils>=3.5.0 # Apache-2.0 +oslo.utils>=3.11.0 # Apache-2.0 PrettyTable<0.8,>=0.7 # BSD requests>=2.10.0 # Apache-2.0 simplejson>=2.2.0 # MIT diff --git a/test-requirements.txt b/test-requirements.txt index 4c96ee3e7..68c273185 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -8,9 +8,9 @@ coverage>=3.6 # Apache-2.0 discover # BSD fixtures<2.0,>=1.3.1 # Apache-2.0/BSD keyring>=5.5.1 # MIT/PSF -mock>=1.2 # BSD +mock>=2.0 # BSD python-keystoneclient!=1.8.0,!=2.1.0,>=1.7.0 # Apache-2.0 -python-cinderclient!=1.7.0,>=1.6.0 # Apache-2.0 +python-cinderclient!=1.7.0,!=1.7.1,>=1.6.0 # Apache-2.0 requests-mock>=0.7.0 # Apache-2.0 sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2 # BSD os-client-config>=1.13.1 # Apache-2.0 From 4d455d2e7069b3c20fe83efec8a7d8509abe039a Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Thu, 2 Jun 2016 21:11:42 +0000 Subject: [PATCH 1060/1705] Updated from global requirements Change-Id: I5907188ec26a6c29aee22cac32fccddb3e56cfb2 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 68c273185..3b12cb8dd 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -6,7 +6,7 @@ hacking<0.11,>=0.10.0 bandit>=1.0.1 # Apache-2.0 coverage>=3.6 # Apache-2.0 discover # BSD -fixtures<2.0,>=1.3.1 # Apache-2.0/BSD +fixtures>=3.0.0 # Apache-2.0/BSD keyring>=5.5.1 # MIT/PSF mock>=2.0 # BSD python-keystoneclient!=1.8.0,!=2.1.0,>=1.7.0 # Apache-2.0 From 96fde33630fbf310946ec1a744466df52232e854 Mon Sep 17 00:00:00 2001 From: Anna Babich Date: Wed, 25 May 2016 17:31:06 +0300 Subject: [PATCH 1061/1705] Functional tests for server's description Functional tests for microversion 2.19 have been added. They include several actions with server's description mentioned in Testing section of the appropriate spec [1] for Python nova-client and openstack-client, namely: - Add a description to the tests that create a server - Add a description to the tests that rebuild a server - Set and remove the description on an existing server - Check that the description is returned as part of server details for an individual server and for a server list - [Negative] The description passed to the API is longer than 255 characters - The description passed to the API is an empty string. This is allowed Tests that are not implemented yet: - [Negative] The description passed to the API is not valid printable unicode [1] http://specs.openstack.org/openstack/nova-specs/specs/mitaka/implemented/user-settable-server-description.html Change-Id: Ieab77bf5569871380987769c47e9f4dab2ec9e4b --- .../tests/functional/v2/test_servers.py | 56 ++++++++++++++++++- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/novaclient/tests/functional/v2/test_servers.py b/novaclient/tests/functional/v2/test_servers.py index cf6f45a17..642cfed34 100644 --- a/novaclient/tests/functional/v2/test_servers.py +++ b/novaclient/tests/functional/v2/test_servers.py @@ -10,8 +10,12 @@ # License for the specific language governing permissions and limitations # under the License. +import random +import string + from novaclient.tests.functional import base from novaclient.tests.functional.v2.legacy import test_servers +from novaclient.v2 import shell class TestServersBootNovaClient(test_servers.TestServersBootNovaClient): @@ -62,18 +66,64 @@ def _boot_server_with_description(self): return server, descr def test_create(self): + # Add a description to the tests that create a server server, descr = self._boot_server_with_description() - output = self.nova("show %s" % server.id) self.assertEqual(descr, self._get_value_from_the_table(output, "description")) - def test_update(self): + def test_list_servers_with_description(self): + # Check that the description is returned as part of server details + # for a server list + server, descr = self._boot_server_with_description() + output = self.nova("list --fields description") + self.assertEqual(server.id, + self._get_column_value_from_single_row_table( + output, "ID")) + self.assertEqual(descr, + self._get_column_value_from_single_row_table( + output, "Description")) + + def test_rebuild(self): + # Add a description to the tests that rebuild a server server, descr = self._boot_server_with_description() + descr = "New description for rebuilt VM." + self.nova("rebuild --description '%s' %s %s" % + (descr, server.id, self.image.name)) + shell._poll_for_status( + self.client.servers.get, server.id, + 'rebuild', ['active']) + output = self.nova("show %s" % server.id) + self.assertEqual(descr, self._get_value_from_the_table(output, + "description")) - # remove description + def test_remove_description(self): + # Remove description from server booted with it + server, descr = self._boot_server_with_description() self.nova("update %s --description ''" % server.id) + output = self.nova("show %s" % server.id) + self.assertEqual("-", self._get_value_from_the_table(output, + "description")) + def test_add_remove_description_on_existing_server(self): + # Set and remove the description on an existing server + server = self._create_server() + descr = "Add a description for previously-booted VM." + self.nova("update %s --description '%s'" % (server.id, descr)) + output = self.nova("show %s" % server.id) + self.assertEqual(descr, self._get_value_from_the_table(output, + "description")) + self.nova("update %s --description ''" % server.id) output = self.nova("show %s" % server.id) self.assertEqual("-", self._get_value_from_the_table(output, "description")) + + def test_update_with_description_longer_than_255_symbols(self): + # Negative case for description longer than 255 characters + server = self._create_server() + descr = ''.join(random.choice(string.letters) for i in range(256)) + output = self.nova("update %s --description '%s'" % (server.id, descr), + fail_ok=True, merge_stderr=True) + self.assertIn("\nERROR (BadRequest): Invalid input for field/attribute" + " description. Value: %s. u\'%s\' is too long (HTTP 400)" + % (descr, descr), output) From d16c3692e69c6416a764d8af65b8a3f09ded2e0b Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Mon, 30 May 2016 14:54:25 +0900 Subject: [PATCH 1062/1705] Log request_id for each api call Add the function to log 'x-openstack-request-id' or 'x-compute-request-id' in each API call. If the caller (e.g. heat) uses oslo.log, the caller's request id in oslo.context and the callee's request id can be output in the same log message (same line). Change-Id: I29312ce278ecfae41a688a0ddf76c24cfa0eaf6b Implements: blueprint log-request-id --- novaclient/client.py | 33 ++++++++++++++++--- novaclient/shell.py | 11 ++++++- novaclient/tests/unit/test_client.py | 14 +++++++- novaclient/tests/unit/test_http.py | 25 +++++++------- novaclient/v2/client.py | 10 +++++- .../log-request-id-ce106497e0520fad.yaml | 11 +++++++ 6 files changed, 86 insertions(+), 18 deletions(-) create mode 100644 releasenotes/notes/log-request-id-ce106497e0520fad.yaml diff --git a/novaclient/client.py b/novaclient/client.py index f2143d685..1668af460 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -73,6 +73,18 @@ def get(self, url): return self._adapters[url] +def _log_request_id(logger, resp, service_name): + request_id = (resp.headers.get('x-openstack-request-id') or + resp.headers.get('x-compute-request-id')) + if request_id: + logger.debug('%(method)s call to %(service_name)s for %(url)s ' + 'used request id %(response_request_id)s', + {'method': resp.request.method, + 'service_name': service_name, + 'url': resp.url, + 'response_request_id': request_id}) + + class SessionClient(adapter.LegacyJsonAdapter): def __init__(self, *args, **kwargs): @@ -93,6 +105,11 @@ def request(self, url, method, **kwargs): method, raise_exc=False, **kwargs) + + # if service name is None then use service_type for logging + service = self.service_name or self.service_type + _log_request_id(self.logger, resp, service) + # TODO(andreykurilin): uncomment this line, when we will be able to # check only nova-related calls # api_versions.check_headers(resp, self.api_version) @@ -142,7 +159,8 @@ def __init__(self, user, password, projectid=None, auth_url=None, http_log_debug=False, auth_system='keystone', auth_plugin=None, auth_token=None, cacert=None, tenant_id=None, user_id=None, - connection_pool=False, api_version=None): + connection_pool=False, api_version=None, + logger=None): self.user = user self.user_id = user_id self.password = password @@ -203,9 +221,10 @@ def __init__(self, user, password, projectid=None, auth_url=None, self.auth_plugin = auth_plugin self._session = None self._current_url = None - self._logger = logging.getLogger(__name__) + self._logger = logger or logging.getLogger(__name__) - if self.http_log_debug and not self._logger.handlers: + if (self.http_log_debug and logger is None and + not self._logger.handlers): # Logging level is already set on the root logger ch = logging.StreamHandler() self._logger.addHandler(ch) @@ -320,6 +339,10 @@ def http_log_resp(self, resp): 'headers': resp.headers, 'text': json.dumps(body)}) + # if service name is None then use service_type for logging + service = self.service_name or self.service_type + _log_request_id(self._logger, resp, service) + def open_session(self): if not self._connection_pool: self._session = requests.Session() @@ -706,6 +729,7 @@ def _construct_http_client(username=None, password=None, project_id=None, else: # FIXME(jamielennox): username and password are now optional. Need # to test that they were provided in this mode. + logger = kwargs.get('logger') return HTTPClient(username, password, user_id=user_id, @@ -730,7 +754,8 @@ def _construct_http_client(username=None, password=None, project_id=None, http_log_debug=http_log_debug, cacert=cacert, connection_pool=connection_pool, - api_version=api_version) + api_version=api_version, + logger=logger) def discover_extensions(version, only_contrib=False): diff --git a/novaclient/shell.py b/novaclient/shell.py index df2f581b2..a8b5c0a42 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -380,6 +380,9 @@ def _get_option_tuples(self, option_string): class OpenStackComputeShell(object): times = [] + def __init__(self): + self.client_logger = None + def _append_global_identity_args(self, parser, argv): # Register the CLI arguments that have moved to the session object. loading.register_session_argparse_arguments(parser) @@ -698,6 +701,11 @@ def setup_debugging(self, debug): format=streamformat) logging.getLogger('iso8601').setLevel(logging.WARNING) + self.client_logger = logging.getLogger(client.__name__) + ch = logging.StreamHandler() + self.client_logger.setLevel(logging.DEBUG) + self.client_logger.addHandler(ch) + def main(self, argv): # Parse args once to find version and debug settings parser = self.get_base_parser(argv) @@ -892,7 +900,8 @@ def main(self, argv): timings=args.timings, bypass_url=bypass_url, os_cache=os_cache, http_log_debug=args.debug, cacert=cacert, timeout=timeout, - session=keystone_session, auth=keystone_auth) + session=keystone_session, auth=keystone_auth, + logger=self.client_logger) if not skip_auth: if not api_version.is_latest(): diff --git a/novaclient/tests/unit/test_client.py b/novaclient/tests/unit/test_client.py index 1325bf727..65b421b0f 100644 --- a/novaclient/tests/unit/test_client.py +++ b/novaclient/tests/unit/test_client.py @@ -443,7 +443,8 @@ def test_timings(self, m_request): class SessionClientTest(utils.TestCase): @mock.patch.object(adapter.LegacyJsonAdapter, 'request') - def test_timings(self, m_request): + @mock.patch.object(novaclient.client, '_log_request_id') + def test_timings(self, mock_log_request_id, m_request): m_request.return_value = (mock.MagicMock(status_code=200), None) client = novaclient.client.SessionClient(session=mock.MagicMock()) @@ -456,6 +457,17 @@ def test_timings(self, m_request): self.assertEqual(1, len(client.times)) self.assertEqual('GET http://no.where', client.times[0][0]) + @mock.patch.object(adapter.LegacyJsonAdapter, 'request') + @mock.patch.object(novaclient.client, '_log_request_id') + def test_log_request_id(self, mock_log_request_id, mock_request): + response = mock.MagicMock(status_code=200) + mock_request.return_value = (response, None) + client = novaclient.client.SessionClient(session=mock.MagicMock(), + service_name='compute') + client.request("http://no.where", 'GET') + mock_log_request_id.assert_called_once_with(client.logger, response, + 'compute') + class DiscoverExtensionTest(utils.TestCase): diff --git a/novaclient/tests/unit/test_http.py b/novaclient/tests/unit/test_http.py index a80d94426..588842e49 100644 --- a/novaclient/tests/unit/test_http.py +++ b/novaclient/tests/unit/test_http.py @@ -71,15 +71,16 @@ return_value=retry_after_non_supporting_response) -def get_client(): +def get_client(**kwargs): cl = client.HTTPClient("username", "password", "project_id", - utils.AUTH_URL_V2) + utils.AUTH_URL_V2, + **kwargs) return cl -def get_authed_client(): - cl = get_client() +def get_authed_client(**kwargs): + cl = get_client(**kwargs) cl.management_url = "http://example.com" cl.auth_token = "token" cl.get_service_url = mock.Mock(return_value="http://example.com") @@ -164,14 +165,16 @@ def test_refused_call(): test_refused_call() - def test_client_logger(self): - cl1 = client.HTTPClient("username", "password", "project_id", - "auth_test", http_log_debug=True) - self.assertEqual(1, len(cl1._logger.handlers)) + @mock.patch.object(requests, "request", mock_request) + @mock.patch.object(client, '_log_request_id') + @mock.patch.object(client.HTTPClient, 'http_log_req') + def test_client_logger(self, mock_http_log_req, mock_log_request_id): + cl = get_authed_client(service_name='compute', http_log_debug=True) + self.assertIsNotNone(cl._logger) - cl2 = client.HTTPClient("username", "password", "project_id", - "auth_test", http_log_debug=True) - self.assertEqual(1, len(cl2._logger.handlers)) + cl.post("/hi", body=[1, 2, 3]) + mock_log_request_id.assert_called_once_with(cl._logger, fake_response, + 'compute') @mock.patch.object(requests, 'request', unknown_error_mock_request) def test_unknown_server_error(self): diff --git a/novaclient/v2/client.py b/novaclient/v2/client.py index f646c67b8..c202b754e 100644 --- a/novaclient/v2/client.py +++ b/novaclient/v2/client.py @@ -13,6 +13,8 @@ # License for the specific language governing permissions and limitations # under the License. +import logging + from novaclient import api_versions from novaclient import client from novaclient.i18n import _LW @@ -67,7 +69,7 @@ def __init__(self, username=None, api_key=None, project_id=None, auth_system='keystone', auth_plugin=None, auth_token=None, cacert=None, tenant_id=None, user_id=None, connection_pool=False, session=None, auth=None, - api_version=None, direct_use=True, **kwargs): + api_version=None, direct_use=True, logger=None, **kwargs): """Initialization of Client object. :param str username: Username @@ -99,6 +101,8 @@ def __init__(self, username=None, api_key=None, project_id=None, :param str session: Session :param str auth: Auth :param api_version: Compute API version + :param direct_use: Direct use + :param logger: Logger :type api_version: novaclient.api_versions.APIVersion """ if direct_use: @@ -174,6 +178,9 @@ def __init__(self, username=None, api_key=None, project_id=None, setattr(self, extension.name, extension.manager_class(self)) + if not logger: + logger = logging.getLogger(__name__) + self.client = client._construct_http_client( username=username, password=password, @@ -202,6 +209,7 @@ def __init__(self, username=None, api_key=None, project_id=None, session=session, auth=auth, api_version=api_version, + logger=logger, **kwargs) @client._original_only diff --git a/releasenotes/notes/log-request-id-ce106497e0520fad.yaml b/releasenotes/notes/log-request-id-ce106497e0520fad.yaml new file mode 100644 index 000000000..5a7f6f508 --- /dev/null +++ b/releasenotes/notes/log-request-id-ce106497e0520fad.yaml @@ -0,0 +1,11 @@ +--- +prelude: > + - Log 'x-openstack-request-id' or 'x-compute-request-id' + in each API call. If the caller (e.g. heat) uses oslo.log, + the caller's request id in oslo.context and the callee's + request id can be output in the same log message (same line). +features: + - Log 'x-openstack-request-id' or 'x-compute-request-id' + in each API call. If the caller (e.g. heat) uses oslo.log, + the caller's request id in oslo.context and the callee's + request id can be output in the same log message (same line). From cc728ca86c89cdb74262d251c37be32c30c75848 Mon Sep 17 00:00:00 2001 From: Sergey Nikitin Date: Fri, 10 Jun 2016 12:56:47 +0300 Subject: [PATCH 1063/1705] TrivialFix: Added missed value in string formatting Change-Id: I530e85c3a66988b545403baec13c700c7517e645 --- novaclient/tests/functional/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/tests/functional/base.py b/novaclient/tests/functional/base.py index 300179f3c..095d5ce70 100644 --- a/novaclient/tests/functional/base.py +++ b/novaclient/tests/functional/base.py @@ -342,7 +342,7 @@ def _get_column_value_from_single_row_table(self, table, column): # the value now using the column index. return line.split("|")[1:-1][column_index].strip() - raise ValueError("Unable to find value for column '%s'.") + raise ValueError("Unable to find value for column '%s'." % column) def _create_server(self, name=None, with_network=True, add_cleanup=True, **kwargs): From a70de7e5900fd0e71e49b31e81e3f8c23f0c1c83 Mon Sep 17 00:00:00 2001 From: Sylvain Bauza Date: Fri, 10 Jun 2016 16:02:21 +0200 Subject: [PATCH 1064/1705] Add support for microversion 2.28 microversion 2.28 now returns a dict instead of a string for cpu_info item returned by GET /os-hypervisors/ Fortunately, the utils.flatten_dict() method checks whether it's a JSON field so the behaviour keeps to be the same for the CLI output of nova hypervisor-show. That said, the novaclient API call to HypervisorManager.get() will of course be returning either a dict or a string for the keyed cpu_info given the microversion. Co-Authored-By: Andrey Kurilin Change-Id: I3a8bfcb7672005430ca99bab0d20e25f48a7e0f6 Implements: blueprint nova-api-hypervsor-cpu-info --- novaclient/__init__.py | 2 +- .../functional/v2/legacy/test_hypervisors.py | 30 +++++++++++++++++++ .../tests/functional/v2/test_hypervisors.py | 21 +++++++++++++ novaclient/tests/unit/v2/test_shell.py | 1 + .../microversion-v2_28-abf653ae5cf5c4a9.yaml | 5 ++++ 5 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 novaclient/tests/functional/v2/legacy/test_hypervisors.py create mode 100644 novaclient/tests/functional/v2/test_hypervisors.py create mode 100644 releasenotes/notes/microversion-v2_28-abf653ae5cf5c4a9.yaml diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 0e996a22c..7980a7b3f 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.27") +API_MAX_VERSION = api_versions.APIVersion("2.28") diff --git a/novaclient/tests/functional/v2/legacy/test_hypervisors.py b/novaclient/tests/functional/v2/legacy/test_hypervisors.py new file mode 100644 index 000000000..36edd60e1 --- /dev/null +++ b/novaclient/tests/functional/v2/legacy/test_hypervisors.py @@ -0,0 +1,30 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import six + +from novaclient.tests.functional import base + + +class TestHypervisors(base.ClientTestBase): + + COMPUTE_API_VERSION = "2.1" + + def _test_list(self, cpu_info_type): + hypervisors = self.client.hypervisors.list() + if not len(hypervisors): + self.fail("No hypervisors detected.") + for hypervisor in hypervisors: + self.assertIsInstance(hypervisor.cpu_info, cpu_info_type) + + def test_list(self): + self._test_list(six.text_type) diff --git a/novaclient/tests/functional/v2/test_hypervisors.py b/novaclient/tests/functional/v2/test_hypervisors.py new file mode 100644 index 000000000..51327a6d9 --- /dev/null +++ b/novaclient/tests/functional/v2/test_hypervisors.py @@ -0,0 +1,21 @@ +# 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 novaclient.tests.functional.v2.legacy import test_hypervisors + + +class TestHypervisorsV28(test_hypervisors.TestHypervisors): + + COMPUTE_API_VERSION = "2.28" + + def test_list(self): + self._test_list(dict) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 3ddd72449..4d936ede8 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2792,6 +2792,7 @@ def test_versions(self): 27, # NOTE(cdent): 27 adds support for updated microversion # headers, and is tested in test_api_versions, but is # not explicitly tested via wraps and _SUBSTITUTIONS. + 28, # doesn't require any changes in novaclient ]) versions_supported = set(range(0, novaclient.API_MAX_VERSION.ver_minor + 1)) diff --git a/releasenotes/notes/microversion-v2_28-abf653ae5cf5c4a9.yaml b/releasenotes/notes/microversion-v2_28-abf653ae5cf5c4a9.yaml new file mode 100644 index 000000000..064f23cc9 --- /dev/null +++ b/releasenotes/notes/microversion-v2_28-abf653ae5cf5c4a9.yaml @@ -0,0 +1,5 @@ +--- +upgrade: +- Support v2.28 microversion +- cpu_info property of hypervisor resource is a json now (previously it was + text). From 19ff19e1b8e632e03aff50621d200a258b2014b2 Mon Sep 17 00:00:00 2001 From: Sergey Nikitin Date: Fri, 10 Jun 2016 12:54:55 +0300 Subject: [PATCH 1065/1705] Added functional tests for server tags (microverison 2.26) Also fixed wrong shell output in 'server-tag-list' and removes 'server-tag-show' command. Change-Id: Icfd73e50108c7b1226e51307c0afc3f8f54ff2d6 --- novaclient/tests/functional/base.py | 32 ++++++++++++++ .../tests/functional/v2/test_servers.py | 44 +++++++++++++++++++ novaclient/tests/unit/v2/test_servers.py | 6 --- novaclient/tests/unit/v2/test_shell.py | 5 --- novaclient/v2/servers.py | 15 ------- novaclient/v2/shell.py | 12 +---- 6 files changed, 78 insertions(+), 36 deletions(-) diff --git a/novaclient/tests/functional/base.py b/novaclient/tests/functional/base.py index 095d5ce70..2422033f6 100644 --- a/novaclient/tests/functional/base.py +++ b/novaclient/tests/functional/base.py @@ -344,6 +344,38 @@ def _get_column_value_from_single_row_table(self, table, column): raise ValueError("Unable to find value for column '%s'." % column) + def _get_list_of_values_from_single_column_table(self, table, column): + """Get the list of values for the column in the single-column table + + Example table: + + +------+ + | Tags | + +------+ + | tag1 | + | tag2 | + +------+ + + :param table: newline-separated table with |-separated cells + :param column: name of the column to look for + :raises: ValueError if the single column has some other name + """ + lines = table.split("\n") + column_name = None + values = [] + for line in lines: + if "|" in line: + if not column_name: + column_name = line.split("|")[1].strip() + if column_name != column: + raise ValueError( + "The table has no column %(expected)s " + "but has column %(actual)s." % { + 'expected': column, 'actual': column_name}) + else: + values.append(line.split("|")[1].strip()) + return values + def _create_server(self, name=None, with_network=True, add_cleanup=True, **kwargs): name = name or self.name_generate(prefix='server') diff --git a/novaclient/tests/functional/v2/test_servers.py b/novaclient/tests/functional/v2/test_servers.py index 642cfed34..1eadf066a 100644 --- a/novaclient/tests/functional/v2/test_servers.py +++ b/novaclient/tests/functional/v2/test_servers.py @@ -127,3 +127,47 @@ def test_update_with_description_longer_than_255_symbols(self): self.assertIn("\nERROR (BadRequest): Invalid input for field/attribute" " description. Value: %s. u\'%s\' is too long (HTTP 400)" % (descr, descr), output) + + +class TestServersTagsV226(base.ClientTestBase): + + COMPUTE_API_VERSION = "2.26" + + def _boot_server_with_tags(self): + uuid = self._create_server().id + self.client.servers.set_tags(uuid, ["t1", "t2"]) + return uuid + + def test_show(self): + uuid = self._boot_server_with_tags() + output = self.nova("show %s" % uuid) + self.assertEqual('["t1", "t2"]', self._get_value_from_the_table( + output, "tags")) + + def test_list(self): + uuid = self._boot_server_with_tags() + output = self.nova("server-tag-list %s" % uuid) + tags = self._get_list_of_values_from_single_column_table( + output, "Tag") + self.assertEqual(["t1", "t2"], tags) + + def test_add(self): + uuid = self._boot_server_with_tags() + self.nova("server-tag-add %s t3" % uuid) + self.assertEqual(["t1", "t2", "t3"], + self.client.servers.tag_list(uuid)) + + def test_set(self): + uuid = self._boot_server_with_tags() + self.nova("server-tag-set %s t3 t4" % uuid) + self.assertEqual(["t3", "t4"], self.client.servers.tag_list(uuid)) + + def test_delete(self): + uuid = self._boot_server_with_tags() + self.nova("server-tag-delete %s t2" % uuid) + self.assertEqual(["t1"], self.client.servers.tag_list(uuid)) + + def test_delete_all(self): + uuid = self._boot_server_with_tags() + self.nova("server-tag-delete-all %s" % uuid) + self.assertEqual([], self.client.servers.tag_list(uuid)) diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index 51e35731b..b97ffca90 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -1184,9 +1184,3 @@ def test_tags_set(self): ret = s.set_tags(['tag1', 'tag2']) self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('PUT', '/servers/1234/tags') - - def test_tag_exists(self): - s = self.cs.servers.get(1234) - ret = s.tag_exists('tag') - self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('GET', '/servers/1234/tags/tag') diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 4d936ede8..a36ab82fe 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2840,11 +2840,6 @@ def test_server_tag_delete_all(self): api_version='2.26') self.assert_called('DELETE', '/servers/1234/tags') - def test_server_tag_exists(self): - self.run_command('server-tag-exists sample-server tag', - api_version='2.26') - self.assert_called('GET', '/servers/1234/tags/tag') - def test_list_v2_26_tags(self): self.run_command('list --tags tag1,tag2', api_version='2.26') self.assert_called('GET', '/servers/detail?tags=tag1%2Ctag2') diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index 51aef7776..929e32435 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -556,12 +556,6 @@ def add_tag(self, tag): """ return self.manager.add_tag(self, tag) - def tag_exists(self, tag): - """ - Check if an instance has specified tag. - """ - return self.manager.tag_exists(self, tag) - class NetworkInterface(base.Resource): @property @@ -1784,12 +1778,3 @@ def add_tag(self, server, tag): """ return self._update( "/servers/%s/tags/%s" % (base.getid(server), tag), None) - - @api_versions.wraps('2.26') - def tag_exists(self, server, tag): - """ - Check if an instance has specified tag. - """ - resp, body = self.api.client.get( - "/servers/%s/tags/%s" % (base.getid(server), tag)) - return self.convert_into_with_meta(body, resp) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 1dc06ac2a..042b35435 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -5029,7 +5029,8 @@ def do_server_tag_list(cs, args): """Get list of tags from a server.""" server = _find_server(cs, args.server) tags = server.tag_list() - utils.print_list(tags, 'name') + formatters = {'Tag': lambda o: o} + utils.print_list(tags, ['Tag'], formatters=formatters) @api_versions.wraps("2.26") @@ -5065,12 +5066,3 @@ def do_server_tag_delete_all(cs, args): """Delete all tags from a server.""" server = _find_server(cs, args.server) server.delete_all_tags() - - -@api_versions.wraps("2.26") -@utils.arg('server', metavar='', help=_('Name or ID of server.')) -@utils.arg('tag', metavar='', help=_('Tag to check if it exists or not.')) -def do_server_tag_exists(cs, args): - """Check if a server has specified tag.""" - server = _find_server(cs, args.server) - server.tag_exists(args.tag) From f9bdba2dd707ee77ffdc67869c5517078a26a014 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Thu, 21 Apr 2016 12:00:04 +0300 Subject: [PATCH 1066/1705] Extend microversion stuff to support resource classes Current implementation of novaclient.api_versions.wraps allow to use versioned methods for shell functions and resource managers. As resource managers can have versioned methods, resource objects can have it too, but it was not implemented yet. This patch fixes this omission. Changes: - Add api_version property to base resource class. It is mapped to api_version property of manager class; - Move `novaclient.utils.generate_function_name` to `novaclient.api_versions._generate_function_name`, since this method is specific to microversion stuff and should not used outside api_versions module; - Rewrite _generate_function_name to handle class(owner) name. Previously, it was improssible to have two classes in one module with versioned methods with equal names. - Remove call of generate_function_name from novaclient.shell. Shell module should not take care about function identifiers. get_substitutions accepts object with __id__ property now. - Mark _add_substitution as private method, since it should not be used outside api_versions module - Split all versioned methods of Server resource from novaclient.v2.servers module. Change-Id: Icfce16bfa6f919d7f8451d592f4a8e276b1f1709 --- novaclient/api_versions.py | 37 ++++++-- novaclient/base.py | 4 + novaclient/shell.py | 3 +- novaclient/tests/unit/test_api_versions.py | 55 ++++++++---- novaclient/tests/unit/v2/test_servers.py | 5 -- novaclient/utils.py | 10 --- novaclient/v2/servers.py | 100 ++++++++++++--------- 7 files changed, 133 insertions(+), 81 deletions(-) diff --git a/novaclient/api_versions.py b/novaclient/api_versions.py index ff1da0675..0161b589f 100644 --- a/novaclient/api_versions.py +++ b/novaclient/api_versions.py @@ -16,13 +16,13 @@ import os import pkgutil import re +import traceback from oslo_utils import strutils import novaclient from novaclient import exceptions from novaclient.i18n import _, _LW -from novaclient import utils LOG = logging.getLogger(__name__) if not LOG.handlers: @@ -340,12 +340,32 @@ def check_headers(response, api_version): _warn_missing_microversion_header(HEADER_NAME) -def add_substitution(versioned_method): +def _add_substitution(versioned_method): _SUBSTITUTIONS.setdefault(versioned_method.name, []) _SUBSTITUTIONS[versioned_method.name].append(versioned_method) +def _get_function_name(func): + # NOTE(andreykurilin): Based on the facts: + # - Python 2 does not have __qualname__ property as Python 3 has; + # - we cannot use im_class here, since we need to obtain name of + # function in `wraps` decorator during class initialization + # ("im_class" property does not exist at that moment) + # we need to write own logic to obtain the full function name which + # include module name, owner name(optional) and just function name. + filename, _lineno, _name, line = traceback.extract_stack()[-4] + module, _file_extension = os.path.splitext(filename) + module = module.replace("/", ".") + if module.endswith(func.__module__): + return "%s.[%s].%s" % (func.__module__, line, func.__name__) + else: + return "%s.%s" % (func.__module__, func.__name__) + + def get_substitutions(func_name, api_version=None): + if hasattr(func_name, "__id__"): + func_name = func_name.__id__ + substitutions = _SUBSTITUTIONS.get(func_name, []) if api_version and not api_version.is_null(): return [m for m in substitutions @@ -362,10 +382,11 @@ def wraps(start_version, end_version=None): def decor(func): func.versioned = True - name = utils.get_function_name(func) + name = _get_function_name(func) + versioned_method = VersionedMethod(name, start_version, end_version, func) - add_substitution(versioned_method) + _add_substitution(versioned_method) @functools.wraps(func) def substitution(obj, *args, **kwargs): @@ -374,7 +395,6 @@ def substitution(obj, *args, **kwargs): if not methods: raise exceptions.VersionNotFoundForAPIMethod( obj.api_version.get_string(), name) - return methods[-1].func(obj, *args, **kwargs) # Let's share "arguments" with original method and substitution to @@ -383,6 +403,13 @@ def substitution(obj, *args, **kwargs): func.arguments = [] substitution.arguments = func.arguments + # NOTE(andreykurilin): The way to obtain function's name in Python 2 + # bases on traceback(see _get_function_name for details). Since the + # right versioned method method is used in several places, one object + # can have different names. Let's generate name of function one time + # and use __id__ property in all other places. + substitution.__id__ = name + return substitution return decor diff --git a/novaclient/base.py b/novaclient/base.py index dcb57a472..0200332d6 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -147,6 +147,10 @@ def __repr__(self): info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys) return "<%s %s>" % (self.__class__.__name__, info) + @property + def api_version(self): + return self.manager.api_version + @property def human_id(self): """Human-readable ID which can be used for bash completion. diff --git a/novaclient/shell.py b/novaclient/shell.py index a8b5c0a42..bbfe959d7 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -633,8 +633,7 @@ def _find_actions(self, subparsers, actions_module, version, do_help): desc = callback.__doc__ or '' if hasattr(callback, "versioned"): additional_msg = "" - subs = api_versions.get_substitutions( - utils.get_function_name(callback)) + subs = api_versions.get_substitutions(callback) if do_help: additional_msg = msg % { 'start': subs[0].start_version.get_string(), diff --git a/novaclient/tests/unit/test_api_versions.py b/novaclient/tests/unit/test_api_versions.py index e49e554d3..44ac27915 100644 --- a/novaclient/tests/unit/test_api_versions.py +++ b/novaclient/tests/unit/test_api_versions.py @@ -233,11 +233,9 @@ def _side_effect_of_vers_method(self, *args, **kwargs): m.name = args[0] return m - @mock.patch("novaclient.utils.get_function_name") + @mock.patch("novaclient.api_versions._get_function_name") @mock.patch("novaclient.api_versions.VersionedMethod") def test_end_version_is_none(self, mock_versioned_method, mock_name): - func_name = "foo" - mock_name.return_value = func_name mock_versioned_method.side_effect = self._side_effect_of_vers_method @api_versions.wraps("2.2") @@ -247,15 +245,13 @@ def foo(*args, **kwargs): foo(self._get_obj_with_vers("2.4")) mock_versioned_method.assert_called_once_with( - func_name, api_versions.APIVersion("2.2"), + mock_name.return_value, api_versions.APIVersion("2.2"), api_versions.APIVersion("2.latest"), mock.ANY) - @mock.patch("novaclient.utils.get_function_name") + @mock.patch("novaclient.api_versions._get_function_name") @mock.patch("novaclient.api_versions.VersionedMethod") def test_start_and_end_version_are_presented(self, mock_versioned_method, mock_name): - func_name = "foo" - mock_name.return_value = func_name mock_versioned_method.side_effect = self._side_effect_of_vers_method @api_versions.wraps("2.2", "2.6") @@ -265,14 +261,12 @@ def foo(*args, **kwargs): foo(self._get_obj_with_vers("2.4")) mock_versioned_method.assert_called_once_with( - func_name, api_versions.APIVersion("2.2"), + mock_name.return_value, api_versions.APIVersion("2.2"), api_versions.APIVersion("2.6"), mock.ANY) - @mock.patch("novaclient.utils.get_function_name") + @mock.patch("novaclient.api_versions._get_function_name") @mock.patch("novaclient.api_versions.VersionedMethod") def test_api_version_doesnt_match(self, mock_versioned_method, mock_name): - func_name = "foo" - mock_name.return_value = func_name mock_versioned_method.side_effect = self._side_effect_of_vers_method @api_versions.wraps("2.2", "2.6") @@ -283,7 +277,7 @@ def foo(*args, **kwargs): foo, self._get_obj_with_vers("2.1")) mock_versioned_method.assert_called_once_with( - func_name, api_versions.APIVersion("2.2"), + mock_name.return_value, api_versions.APIVersion("2.2"), api_versions.APIVersion("2.6"), mock.ANY) def test_define_method_is_actually_called(self): @@ -301,7 +295,8 @@ def some_func(*args, **kwargs): checker.assert_called_once_with(*((obj,) + some_args), **some_kwargs) - def test_arguments_property_is_copied(self): + @mock.patch("novaclient.api_versions._get_function_name") + def test_arguments_property_is_copied(self, mock_name): @nutils.arg("argument_1") @api_versions.wraps("2.666", "2.777") @nutils.arg("argument_2") @@ -309,14 +304,44 @@ def some_func(): pass versioned_method = api_versions.get_substitutions( - nutils.get_function_name(some_func), - api_versions.APIVersion("2.700"))[0] + mock_name.return_value, api_versions.APIVersion("2.700"))[0] self.assertEqual(some_func.arguments, versioned_method.func.arguments) self.assertIn((("argument_1",), {}), versioned_method.func.arguments) self.assertIn((("argument_2",), {}), versioned_method.func.arguments) + def test_several_methods_with_same_name_in_one_module(self): + + class A(object): + api_version = api_versions.APIVersion("777.777") + + @api_versions.wraps("777.777") + def f(self): + return 1 + + class B(object): + api_version = api_versions.APIVersion("777.777") + + @api_versions.wraps("777.777") + def f(self): + return 2 + + self.assertEqual(1, A().f()) + self.assertEqual(2, B().f()) + + def test_generate_function_name(self): + expected_name = "novaclient.tests.unit.test_api_versions.fake_func" + + self.assertNotIn(expected_name, api_versions._SUBSTITUTIONS) + + @api_versions.wraps("7777777.7777777") + def fake_func(): + pass + + self.assertIn(expected_name, api_versions._SUBSTITUTIONS) + self.assertEqual(expected_name, fake_func.__id__) + class DiscoverVersionTestCase(utils.TestCase): def setUp(self): diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index b97ffca90..8d451af25 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -1144,11 +1144,6 @@ def test_live_migrate_server_block_migration_none(self): {'os-migrateLive': {'host': 'hostname', 'block_migration': 'auto'}}) - def test_live_migrate_server_with_disk_over_commit(self): - s = self.cs.servers.get(1234) - self.assertRaises(ValueError, s.live_migrate, 'hostname', - 'auto', 'True') - class ServersV226Test(ServersV225Test): def setUp(self): diff --git a/novaclient/utils.py b/novaclient/utils.py index 838e1a776..0170c9524 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -465,13 +465,3 @@ def record_time(times, enabled, *args): yield end = time.time() times.append((' '.join(args), start, end)) - - -def get_function_name(func): - if six.PY2: - if hasattr(func, "im_class"): - return "%s.%s" % (func.im_class, func.__name__) - else: - return "%s.%s" % (func.__module__, func.__name__) - else: - return "%s.%s" % (func.__module__, func.__qualname__) diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index 929e32435..39adeaba4 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -50,18 +50,25 @@ def delete(self): """ return self.manager.delete(self) + @api_versions.wraps("2.0", "2.18") + def update(self, name=None): + """ + Update the name and the description for this server. + + :param name: Update the server's name. + :returns: :class:`Server` + """ + return self.manager.update(self, name=name) + + @api_versions.wraps("2.19") def update(self, name=None, description=None): """ Update the name and the description for this server. :param name: Update the server's name. - :param description: Update the server's description( - allowed for 2.19-latest). + :param description: Update the server's description. :returns: :class:`Server` """ - if (description is not None and - self.manager.api_version < api_versions.APIVersion("2.19")): - raise exceptions.UnsupportedAttribute("description", "2.19") update_kwargs = {"name": name} if description is not None: update_kwargs["description"] = description @@ -403,43 +410,41 @@ def networks(self): except Exception: return {} + @api_versions.wraps("2.0", "2.24") def live_migrate(self, host=None, - block_migration=None, + block_migration=False, disk_over_commit=None): """ Migrates a running instance to a new machine. :param host: destination host name. :param block_migration: if True, do block_migration, the default - value None will be mapped to False for 2.0 - - 2.24, 'auto' for higher than 2.25 + value is False and None will be mapped to False :param disk_over_commit: if True, allow disk over commit, the default - value None will be mapped to False. It will - not be supported since 2.25 + value is None which is mapped to False :returns: An instance of novaclient.base.TupleWithMeta """ + if block_migration is None: + block_migration = False + if disk_over_commit is None: + disk_over_commit = False + return self.manager.live_migrate(self, host, + block_migration, + disk_over_commit) - if (self.manager.api_version < api_versions.APIVersion("2.25")): - # NOTE(eliqiao): We do this to keep old version api has same - # default value if user don't pass these parameters when using - # SDK - if block_migration is None: - block_migration = False - if disk_over_commit is None: - disk_over_commit = False - - return self.manager.live_migrate(self, host, - block_migration, - disk_over_commit) - else: - if block_migration is None: - block_migration = 'auto' - if disk_over_commit is not None: - raise ValueError("Setting 'disk_over_commit' argument is " - "prohibited after microversion 2.25.") + @api_versions.wraps("2.25") + def live_migrate(self, host=None, block_migration=None): + """ + Migrates a running instance to a new machine. - return self.manager.live_migrate(self, host, - block_migration) + :param host: destination host name. + :param block_migration: if True, do block_migration, the default + value is None which is mapped to 'auto'. + :returns: An instance of novaclient.base.TupleWithMeta + """ + if block_migration is None: + block_migration = "auto" + return self.manager.live_migrate(self, host, block_migration) def reset_state(self, state='error'): """ @@ -480,29 +485,31 @@ def list_security_group(self): """ return self.manager.list_security_group(self) - def evacuate(self, host=None, on_shared_storage=None, password=None): + @api_versions.wraps("2.0", "2.13") + def evacuate(self, host=None, on_shared_storage=True, password=None): """ Evacuate an instance from failed host to specified host. :param host: Name of the target host :param on_shared_storage: Specifies whether instance files located - on shared storage. After microversion 2.14, this - parameter must have its default value of None. + on shared storage. :param password: string to set as admin password on the evacuated server. :returns: An instance of novaclient.base.TupleWithMeta """ - if api_versions.APIVersion("2.14") <= self.manager.api_version: - if on_shared_storage is not None: - raise ValueError("Setting 'on_shared_storage' argument is " - "prohibited after microversion 2.14.") - return self.manager.evacuate(self, host, password) - else: - # microversions 2.0 - 2.13 - if on_shared_storage is None: - on_shared_storage = True - return self.manager.evacuate(self, host, on_shared_storage, - password) + return self.manager.evacuate(self, host, on_shared_storage, password) + + @api_versions.wraps("2.14") + def evacuate(self, host=None, password=None): + """ + Evacuate an instance from failed host to specified host. + + :param host: Name of the target host + :param password: string to set as admin password on the evacuated + server. + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self.manager.evacuate(self, host, password) def interface_list(self): """ @@ -526,30 +533,35 @@ def trigger_crash_dump(self): """Trigger crash dump in an instance""" return self.manager.trigger_crash_dump(self) + @api_versions.wraps('2.26') def tag_list(self): """ Get list of tags from an instance. """ return self.manager.tag_list(self) + @api_versions.wraps('2.26') def delete_tag(self, tag): """ Remove single tag from an instance. """ return self.manager.delete_tag(self, tag) + @api_versions.wraps('2.26') def delete_all_tags(self): """ Remove all tags from an instance. """ return self.manager.delete_all_tags(self) + @api_versions.wraps('2.26') def set_tags(self, tags): """ Set list of tags to an instance. """ return self.manager.set_tags(self, tags) + @api_versions.wraps('2.26') def add_tag(self, tag): """ Add single tag to an instance. From 5f63dee3506cce11f3ef8495327304e6b64732e6 Mon Sep 17 00:00:00 2001 From: Joao Targino Date: Thu, 16 Jun 2016 16:17:31 -0300 Subject: [PATCH 1067/1705] Update README to comply with Identity v3 Updated the README instructions to use Identity V3 parameters and removed the reference to the deprecated Nova 2.0 as an auth endpoint. Change-Id: I78bef987986c461651ba4331bc7cc7db97ce91fb --- README.rst | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/README.rst b/README.rst index e93f1da13..524cbb109 100644 --- a/README.rst +++ b/README.rst @@ -51,24 +51,21 @@ Installing this package gets you a shell command, ``nova``, that you can use to interact with any OpenStack cloud. You'll need to provide your OpenStack username and password. You can do this -with the ``--os-username``, ``--os-password`` and ``--os-tenant-name`` +with the ``--os-username``, ``--os-password`` and ``--os-project-name`` params, but it's easier to just set them as environment variables:: - export OS_USERNAME=openstack - export OS_PASSWORD=yadayada - export OS_TENANT_NAME=myproject + export OS_USERNAME= + export OS_PASSWORD= + export OS_PROJECT_NAME= + You will also need to define the authentication url with ``--os-auth-url`` and the version of the API with ``--os-compute-api-version``. Or set them as -an environment variables as well:: - - export OS_AUTH_URL=http://example.com:8774/v2/ - export OS_COMPUTE_API_VERSION=2 +environment variables as well and set the OS_AUTH_URL to the keystone endpoint:: -If you are using Keystone, you need to set the OS_AUTH_URL to the keystone -endpoint:: + export OS_AUTH_URL=http://:5000/v3/ + export OS_COMPUTE_API_VERSION=2.1 - export OS_AUTH_URL=http://example.com:5000/v2.0/ Since Keystone can return multiple regions in the Service Catalog, you can specify the one you want with ``--os-region-name`` (or @@ -85,13 +82,22 @@ There's also a complete Python API, with documentation linked below. To use with keystone as the authentication system:: + >>> from keystoneauth1.identity import v3 + >>> from keystoneauth1 import session >>> from novaclient import client - >>> nt = client.Client(VERSION, USER, PASSWORD, TENANT, AUTH_URL) - >>> nt.flavors.list() + >>> auth = v3.Password(auth_url='http://example.com:5000/v3', + ... username='username', + ... password='password', + ... project_name='project-name', + ... user_domain_id='default', + ... project_domain_id='default') + >>> sess = session.Session(auth=auth) + >>> nova = client.Client("2.1", session=sess) + >>> nova.flavors.list() [...] - >>> nt.servers.list() + >>> nova.servers.list() [...] - >>> nt.keypairs.list() + >>> nova.keypairs.list() [...] From e91a5937b7d8a1a1b5795428121dffeee5ded5b0 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 17 Jun 2016 14:20:50 +0000 Subject: [PATCH 1068/1705] Updated from global requirements Change-Id: I2233447fefbb0978f9dae6f8c1f41dc6f5f99b75 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 29825fc4f..47e21e6b6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. pbr>=1.6 # Apache-2.0 -keystoneauth1>=2.1.0 # Apache-2.0 +keystoneauth1>=2.7.0 # Apache-2.0 iso8601>=0.1.11 # MIT oslo.i18n>=2.1.0 # Apache-2.0 oslo.serialization>=1.10.0 # Apache-2.0 From 00013b2a0a72408eb65ca0e9cfbd15cea1dd07a8 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Tue, 21 Jun 2016 14:22:20 +0300 Subject: [PATCH 1069/1705] [functional] make tests work with only keystone v3 We need to pass domains id to keystone session. Fix gate-novaclient-dsvm-functional-identity-v3-only-nv (except Keystone v2 test) Change-Id: Ibe2d7750e95be2724d541ab3ff4908692385d280 --- novaclient/tests/functional/base.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/novaclient/tests/functional/base.py b/novaclient/tests/functional/base.py index 2422033f6..f14e46fa4 100644 --- a/novaclient/tests/functional/base.py +++ b/novaclient/tests/functional/base.py @@ -16,7 +16,7 @@ from cinderclient.v2 import client as cinderclient import fixtures -from keystoneauth1 import loading +from keystoneauth1 import identity from keystoneauth1 import session as ksession from keystoneclient import client as keystoneclient import os_client_config @@ -170,7 +170,9 @@ def setUp(self): passwd = auth_info['password'] tenant = auth_info['project_name'] auth_url = auth_info['auth_url'] + user_domain_id = auth_info['user_domain_id'] self.project_domain_id = auth_info['project_domain_id'] + if 'insecure' in cloud_config.config: self.insecure = cloud_config.config['insecure'] else: @@ -181,11 +183,12 @@ def setUp(self): else: version = self.COMPUTE_API_VERSION or "2" - loader = loading.get_plugin_loader("password") - auth = loader.load_from_options(username=user, - password=passwd, - project_name=tenant, - auth_url=auth_url) + auth = identity.Password(username=user, + password=passwd, + project_name=tenant, + auth_url=auth_url, + project_domain_id=self.project_domain_id, + user_domain_id=user_domain_id) session = ksession.Session(auth=auth, verify=(not self.insecure)) self.client = novaclient.client.Client(version, session=session) From d38882b6a402cc68a653ef41b478d765a2c7a048 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Tue, 21 Jun 2016 18:05:31 +0000 Subject: [PATCH 1070/1705] Updated from global requirements Change-Id: Id0b42ea6783c5521621f34ac984073fb3de94096 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 3b12cb8dd..9818c31ec 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -12,7 +12,7 @@ mock>=2.0 # BSD python-keystoneclient!=1.8.0,!=2.1.0,>=1.7.0 # Apache-2.0 python-cinderclient!=1.7.0,!=1.7.1,>=1.6.0 # Apache-2.0 requests-mock>=0.7.0 # Apache-2.0 -sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2 # BSD +sphinx!=1.3b1,<1.3,>=1.2.1 # BSD os-client-config>=1.13.1 # Apache-2.0 oslosphinx!=3.4.0,>=2.5.0 # Apache-2.0 testrepository>=0.0.18 # Apache-2.0/BSD From 1aa042e0cb5d724ed120aec998f17f980285490a Mon Sep 17 00:00:00 2001 From: Sylvain Bauza Date: Mon, 20 Jun 2016 18:25:13 +0200 Subject: [PATCH 1071/1705] Add support for microversion 2.29 Now the os-evacuate API supports a new body argument called 'force' which helps the operators to bypass the scheduler call in case they provide a host. Also modifies the host_evacuate helper method in the contrib tree to make sure operators also have the force flag in case they need it for a global call. Change-Id: I5272e9809a7d8be482e87548c6a3b11186c5d1e1 Partially-Implements: blueprint check-destination-on-migrations-newton --- novaclient/__init__.py | 2 +- novaclient/tests/unit/v2/fakes.py | 6 +++- novaclient/tests/unit/v2/test_servers.py | 16 +++++++++ novaclient/tests/unit/v2/test_shell.py | 32 ++++++++++++++++++ novaclient/v2/contrib/host_evacuate.py | 18 ++++++++-- novaclient/v2/servers.py | 43 ++++++++++++++++++++++-- novaclient/v2/shell.py | 16 ++++++++- 7 files changed, 125 insertions(+), 8 deletions(-) diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 7980a7b3f..727328f47 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.28") +API_MAX_VERSION = api_versions.APIVersion("2.29") diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index bc4c6e638..13681e02a 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -753,7 +753,11 @@ def post_servers_1234_action(self, body, **kw): keys.remove('adminPass') if 'host' in keys: keys.remove('host') - assert set(keys) == set(['onSharedStorage']) + if 'onSharedStorage' in keys: + keys.remove('onSharedStorage') + if 'force' in keys: + keys.remove('force') + assert set(keys) == set() else: raise AssertionError("Unexpected server action: %s" % action) _headers.update(FAKE_RESPONSE_HEADERS) diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index 8d451af25..70b06f220 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -1179,3 +1179,19 @@ def test_tags_set(self): ret = s.set_tags(['tag1', 'tag2']) self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('PUT', '/servers/1234/tags') + + +class ServersV229Test(ServersV226Test): + def setUp(self): + super(ServersV229Test, self).setUp() + self.cs.api_version = api_versions.APIVersion("2.29") + + def test_evacuate(self): + s = self.cs.servers.get(1234) + s.evacuate('fake_target_host') + self.assert_called('POST', '/servers/1234/action', + {'evacuate': {'host': 'fake_target_host'}}) + self.cs.servers.evacuate(s, 'fake_target_host', force=True) + self.assert_called('POST', '/servers/1234/action', + {'evacuate': {'host': 'fake_target_host', + 'force': True}}) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index a36ab82fe..d4a552698 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2009,6 +2009,23 @@ def test_host_evacuate(self): {'evacuate': {'host': 'target_hyper', 'onSharedStorage': False}}, pos=4) + def test_host_evacuate_v2_29(self): + self.run_command('host-evacuate hyper --target target_hyper --force', + api_version='2.29') + self.assert_called('GET', '/os-hypervisors/hyper/servers', pos=0) + self.assert_called('POST', '/servers/uuid1/action', + {'evacuate': {'host': 'target_hyper', 'force': True} + }, pos=1) + self.assert_called('POST', '/servers/uuid2/action', + {'evacuate': {'host': 'target_hyper', 'force': True} + }, pos=2) + self.assert_called('POST', '/servers/uuid3/action', + {'evacuate': {'host': 'target_hyper', 'force': True} + }, pos=3) + self.assert_called('POST', '/servers/uuid4/action', + {'evacuate': {'host': 'target_hyper', 'force': True} + }, pos=4) + def test_host_evacuate_with_shared_storage(self): self.run_command( 'host-evacuate --on-shared-storage hyper --target target_hyper') @@ -2410,6 +2427,21 @@ def test_evacuate(self): {'evacuate': {'host': 'new_host', 'onSharedStorage': True}}) + def test_evacuate_v2_29(self): + self.run_command('evacuate sample-server new_host', api_version="2.29") + self.assert_called('POST', '/servers/1234/action', + {'evacuate': {'host': 'new_host'}}) + self.run_command('evacuate sample-server new_host ' + '--password NewAdminPass', api_version="2.29") + self.assert_called('POST', '/servers/1234/action', + {'evacuate': {'host': 'new_host', + 'adminPass': 'NewAdminPass'}}) + self.run_command('evacuate --force sample-server new_host', + api_version="2.29") + self.assert_called('POST', '/servers/1234/action', + {'evacuate': {'host': 'new_host', + 'force': True}}) + def test_evacuate_with_no_target_host(self): self.run_command('evacuate sample-server') self.assert_called('POST', '/servers/1234/action', diff --git a/novaclient/v2/contrib/host_evacuate.py b/novaclient/v2/contrib/host_evacuate.py index 2a0335d30..22055656b 100644 --- a/novaclient/v2/contrib/host_evacuate.py +++ b/novaclient/v2/contrib/host_evacuate.py @@ -27,12 +27,17 @@ def _server_evacuate(cs, server, args): success = True error_message = "" try: - on_shared_storage = getattr(args, 'on_shared_storage', None) - if api_versions.APIVersion("2.14") <= cs.api_version: - # if microversion >= 2.14 + if api_versions.APIVersion("2.29") <= cs.api_version: + # if microversion >= 2.29 + force = getattr(args, 'force', None) + cs.servers.evacuate(server=server['uuid'], host=args.target_host, + force=force) + elif api_versions.APIVersion("2.14") <= cs.api_version: + # if microversion 2.14 - 2.28 cs.servers.evacuate(server=server['uuid'], host=args.target_host) else: # else microversion 2.0 - 2.13 + on_shared_storage = getattr(args, 'on_shared_storage', None) cs.servers.evacuate(server=server['uuid'], host=args.target_host, on_shared_storage=on_shared_storage) @@ -60,6 +65,13 @@ def _server_evacuate(cs, server, args): help=_('Specifies whether all instances files are on shared storage'), start_version='2.0', end_version='2.13') +@utils.arg( + '--force', + dest='force', + action='store_true', + default=False, + help=_('Force to not verify the scheduler if a host is provided.'), + start_version='2.29') def do_host_evacuate(cs, args): """Evacuate all instances from failed host.""" hypervisors = cs.hypervisors.search(args.host, servers=True) diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index 39adeaba4..fd672af20 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -499,7 +499,7 @@ def evacuate(self, host=None, on_shared_storage=True, password=None): """ return self.manager.evacuate(self, host, on_shared_storage, password) - @api_versions.wraps("2.14") + @api_versions.wraps("2.14", "2.28") def evacuate(self, host=None, password=None): """ Evacuate an instance from failed host to specified host. @@ -511,6 +511,19 @@ def evacuate(self, host=None, password=None): """ return self.manager.evacuate(self, host, password) + @api_versions.wraps("2.29") + def evacuate(self, host=None, password=None, force=None): + """ + Evacuate an instance from failed host to specified host. + + :param host: Name of the target host + :param password: string to set as admin password on the evacuated + server. + :param force: forces to bypass the scheduler if host is provided. + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self.manager.evacuate(self, host, password, force) + def interface_list(self): """ List interfaces attached to an instance. @@ -1658,7 +1671,7 @@ def evacuate(self, server, host=None, on_shared_storage=True, body) return base.TupleWithMeta((resp, body), resp) - @api_versions.wraps("2.14") + @api_versions.wraps("2.14", "2.28") def evacuate(self, server, host=None, password=None): """ Evacuate a server instance. @@ -1680,6 +1693,32 @@ def evacuate(self, server, host=None, password=None): body) return base.TupleWithMeta((resp, body), resp) + @api_versions.wraps("2.29") + def evacuate(self, server, host=None, password=None, force=None): + """ + Evacuate a server instance. + + :param server: The :class:`Server` (or its ID) to share onto. + :param host: Name of the target host. + :param password: string to set as password on the evacuated server. + :param force: forces to bypass the scheduler if host is provided. + :returns: An instance of novaclient.base.TupleWithMeta + """ + + body = {} + if host is not None: + body['host'] = host + + if password is not None: + body['adminPass'] = password + + if force: + body['force'] = force + + resp, body = self._action_return_resp_and_body('evacuate', server, + body) + return base.TupleWithMeta((resp, body), resp) + def interface_list(self, server): """ List attached network interfaces diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 042b35435..7e1241640 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -4688,12 +4688,26 @@ def do_quota_class_update(cs, args): help=_('Specifies whether server files are located on shared storage.'), start_version='2.0', end_version='2.13') +@utils.arg( + '--force', + dest='force', + action='store_true', + default=False, + help=_('Force to not verify the scheduler if a host is provided.'), + start_version='2.29') def do_evacuate(cs, args): """Evacuate server from failed host.""" server = _find_server(cs, args.server) on_shared_storage = getattr(args, 'on_shared_storage', None) - res = server.evacuate(args.host, on_shared_storage, args.password)[1] + force = getattr(args, 'force', None) + update_kwargs = {} + if on_shared_storage is not None: + update_kwargs['on_shared_storage'] = on_shared_storage + if force: + update_kwargs['force'] = force + res = server.evacuate(host=args.host, password=args.password, + **update_kwargs)[1] if isinstance(res, dict): utils.print_dict(res) From c6496789914e2d88984a65350df1d70e75a4d759 Mon Sep 17 00:00:00 2001 From: Sylvain Bauza Date: Tue, 21 Jun 2016 18:08:06 +0200 Subject: [PATCH 1072/1705] Add support for microversion 2.30 Now the os-migrateLive API supports a new body argument called 'force' which helps the operators to bypass the scheduler call in case they provide a host. Also modifies the host_evacuate_live helper method in the contrib tree to make sure operators also have the force flag in case they need it for a global call. Change-Id: Id7fcd88ad060390ac6d1a21510d84363ed643957 Implements: blueprint check-destination-on-migrations-newton --- novaclient/__init__.py | 2 +- novaclient/tests/unit/v2/fakes.py | 13 ++++---- novaclient/tests/unit/v2/test_servers.py | 22 +++++++++++++ novaclient/tests/unit/v2/test_shell.py | 26 +++++++++++++++ novaclient/v2/contrib/host_evacuate_live.py | 19 ++++++++--- novaclient/v2/servers.py | 36 +++++++++++++++++++-- novaclient/v2/shell.py | 20 ++++++++---- 7 files changed, 118 insertions(+), 20 deletions(-) diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 727328f47..56b62db24 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.29") +API_MAX_VERSION = api_versions.APIVersion("2.30") diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 13681e02a..baf345236 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -715,13 +715,14 @@ def post_servers_1234_action(self, body, **kw): # raise AssertionError if we didn't find 'action' at all. pass elif action == 'os-migrateLive': + expected = set(['host', 'block_migration']) + if self.api_version >= api_versions.APIVersion("2.30"): + if 'force' in body[action].keys(): + # force can be optional + expected.add('force') if self.api_version < api_versions.APIVersion("2.25"): - assert set(body[action].keys()) == set(['host', - 'block_migration', - 'disk_over_commit']) - else: - assert set(body[action].keys()) == set(['host', - 'block_migration']) + expected.add('disk_over_commit') + assert set(body[action].keys()) == expected elif action == 'rebuild': body = body[action] adminPass = body.get('adminPass', 'randompassword') diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index 70b06f220..38bf1a4ba 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -1195,3 +1195,25 @@ def test_evacuate(self): self.assert_called('POST', '/servers/1234/action', {'evacuate': {'host': 'fake_target_host', 'force': True}}) + + +class ServersV230Test(ServersV229Test): + def setUp(self): + super(ServersV230Test, self).setUp() + self.cs.api_version = api_versions.APIVersion("2.30") + + def test_live_migrate_server(self): + s = self.cs.servers.get(1234) + ret = s.live_migrate(host='hostname', block_migration='auto') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action', + {'os-migrateLive': {'host': 'hostname', + 'block_migration': 'auto'}}) + ret = self.cs.servers.live_migrate(s, host='hostname', + block_migration='auto', + force=True) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action', + {'os-migrateLive': {'host': 'hostname', + 'block_migration': 'auto', + 'force': True}}) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index d4a552698..8790b2a2b 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -1758,6 +1758,19 @@ def test_live_migration_v225(self): {'os-migrateLive': {'host': None, 'block_migration': 'auto'}}) + def test_live_migration_v2_30(self): + self.run_command('live-migration sample-server hostname', + api_version='2.30') + self.assert_called('POST', '/servers/1234/action', + {'os-migrateLive': {'host': 'hostname', + 'block_migration': 'auto'}}) + self.run_command('live-migration --force sample-server hostname', + api_version='2.30') + self.assert_called('POST', '/servers/1234/action', + {'os-migrateLive': {'host': 'hostname', + 'block_migration': 'auto', + 'force': True}}) + def test_live_migration_force_complete(self): self.run_command('live-migration-force-complete sample-server 1', api_version='2.22') @@ -1811,6 +1824,19 @@ def test_host_evacuate_live_with_target_host(self): self.assert_called('POST', '/servers/uuid3/action', body, pos=3) self.assert_called('POST', '/servers/uuid4/action', body, pos=4) + def test_host_evacuate_live_2_30(self): + self.run_command('host-evacuate-live --force hyper ' + '--target-host hostname', + api_version='2.30') + self.assert_called('GET', '/os-hypervisors/hyper/servers', pos=0) + body = {'os-migrateLive': {'host': 'hostname', + 'block_migration': 'auto', + 'force': True}} + self.assert_called('POST', '/servers/uuid1/action', body, pos=1) + self.assert_called('POST', '/servers/uuid2/action', body, pos=2) + self.assert_called('POST', '/servers/uuid3/action', body, pos=3) + self.assert_called('POST', '/servers/uuid4/action', body, pos=4) + def test_host_evacuate_live_with_block_migration(self): self.run_command('host-evacuate-live --block-migrate hyper') self.assert_called('GET', '/os-hypervisors/hyper/servers', pos=0) diff --git a/novaclient/v2/contrib/host_evacuate_live.py b/novaclient/v2/contrib/host_evacuate_live.py index badc4d1b3..f55dda1a4 100644 --- a/novaclient/v2/contrib/host_evacuate_live.py +++ b/novaclient/v2/contrib/host_evacuate_live.py @@ -26,14 +26,16 @@ def __init__(self, server_uuid, live_migration_accepted, self.error_message = error_message success = True error_message = "" + update_kwargs = {} try: + # API >= 2.30 + if 'force' in args and args.force: + update_kwargs['force'] = args.force # API 2.0->2.24 if 'disk_over_commit' in args: - cs.servers.live_migrate(server['uuid'], args.target_host, - args.block_migrate, args.disk_over_commit) - else: # API 2.25+ - cs.servers.live_migrate(server['uuid'], args.target_host, - args.block_migrate) + update_kwargs['disk_over_commit'] = args.disk_over_commit + cs.servers.live_migrate(server['uuid'], args.target_host, + args.block_migrate, **update_kwargs) except Exception as e: success = False error_message = _("Error while live migrating instance: %s") % e @@ -72,6 +74,13 @@ def __init__(self, server_uuid, live_migration_accepted, dest='max_servers', metavar='', help='Maximum number of servers to live migrate simultaneously') +@utils.arg( + '--force', + dest='force', + action='store_true', + default=False, + help=_('Force to not verify the scheduler if a host is provided.'), + start_version='2.30') def do_host_evacuate_live(cs, args): """Live migrate all instances of the specified host to other available hosts. diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index fd672af20..1ddbc721b 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -432,7 +432,7 @@ def live_migrate(self, host=None, block_migration, disk_over_commit) - @api_versions.wraps("2.25") + @api_versions.wraps("2.25", "2.29") def live_migrate(self, host=None, block_migration=None): """ Migrates a running instance to a new machine. @@ -446,6 +446,21 @@ def live_migrate(self, host=None, block_migration=None): block_migration = "auto" return self.manager.live_migrate(self, host, block_migration) + @api_versions.wraps("2.30") + def live_migrate(self, host=None, block_migration=None, force=None): + """ + Migrates a running instance to a new machine. + + :param host: destination host name. + :param block_migration: if True, do block_migration, the default + value is None which is mapped to 'auto'. + :param force: force to bypass the scheduler if host is provided. + :returns: An instance of novaclient.base.TupleWithMeta + """ + if block_migration is None: + block_migration = "auto" + return self.manager.live_migrate(self, host, block_migration, force) + def reset_state(self, state='error'): """ Reset the state of an instance to active or error. @@ -1578,7 +1593,7 @@ def live_migrate(self, server, host, block_migration, disk_over_commit): 'block_migration': block_migration, 'disk_over_commit': disk_over_commit}) - @api_versions.wraps('2.25') + @api_versions.wraps('2.25', '2.29') def live_migrate(self, server, host, block_migration): """ Migrates a running instance to a new machine. @@ -1593,6 +1608,23 @@ def live_migrate(self, server, host, block_migration): {'host': host, 'block_migration': block_migration}) + @api_versions.wraps('2.30') + def live_migrate(self, server, host, block_migration, force=None): + """ + Migrates a running instance to a new machine. + + :param server: instance id which comes from nova list. + :param host: destination host name. + :param block_migration: if True, do block_migration, can be set as + 'auto' + :param force: forces to bypass the scheduler if host is provided. + :returns: An instance of novaclient.base.TupleWithMeta + """ + body = {'host': host, 'block_migration': block_migration} + if force: + body['force'] = force + return self._action('os-migrateLive', server, body) + def reset_state(self, server, state='error'): """ Reset the state of an instance to active or error. diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 7e1241640..d2ed16e7f 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -3768,16 +3768,24 @@ def parser_hosts(fields): 'novaclient 3.3.0.') % '--disk-over-commit', help=argparse.SUPPRESS, start_version="2.0", end_version="2.24") +@utils.arg( + '--force', + dest='force', + action='store_true', + default=False, + help=_('Force to not verify the scheduler if a host is provided.'), + start_version='2.30') def do_live_migration(cs, args): """Migrate running server to a new machine.""" + update_kwargs = {} if 'disk_over_commit' in args: - _find_server(cs, args.server).live_migrate(args.host, - args.block_migrate, - args.disk_over_commit) - else: - _find_server(cs, args.server).live_migrate(args.host, - args.block_migrate) + update_kwargs['disk_over_commit'] = args.disk_over_commit + if 'force' in args and args.force: + update_kwargs['force'] = args.force + + _find_server(cs, args.server).live_migrate(args.host, args.block_migrate, + **update_kwargs) @api_versions.wraps("2.22") From 7a24b0f7badd9fe00422c98afcb7771bdbc45cfd Mon Sep 17 00:00:00 2001 From: Radoslav Gerganov Date: Wed, 22 Jun 2016 13:28:04 +0300 Subject: [PATCH 1073/1705] Fix the help message for 'get-mks-console' MKS is not a serial console but the standard type of VM console for VMware instances. Change-Id: I9a9a03e259f16ba2b6b6fd0751ad47b95f837a78 Closes-Bug: #1595131 --- novaclient/v2/shell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 042b35435..268e449e4 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -2509,7 +2509,7 @@ def do_get_serial_console(cs, args): @api_versions.wraps('2.8') @utils.arg('server', metavar='', help=_('Name or ID of server.')) def do_get_mks_console(cs, args): - """Get a serial console to a server.""" + """Get an MKS console to a server.""" server = _find_server(cs, args.server) data = server.get_mks_console() From e54d3a760336acffc0dba79e7cc1073bf160bb4d Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 24 Jun 2016 03:17:36 +0000 Subject: [PATCH 1074/1705] Updated from global requirements Change-Id: I5c37af21c76f67d7fcea7615c1d47147d1e6ebdf --- test-requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test-requirements.txt b/test-requirements.txt index 9818c31ec..c1b5dbd1a 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -11,7 +11,7 @@ keyring>=5.5.1 # MIT/PSF mock>=2.0 # BSD python-keystoneclient!=1.8.0,!=2.1.0,>=1.7.0 # Apache-2.0 python-cinderclient!=1.7.0,!=1.7.1,>=1.6.0 # Apache-2.0 -requests-mock>=0.7.0 # Apache-2.0 +requests-mock>=1.0 # Apache-2.0 sphinx!=1.3b1,<1.3,>=1.2.1 # BSD os-client-config>=1.13.1 # Apache-2.0 oslosphinx!=3.4.0,>=2.5.0 # Apache-2.0 @@ -21,4 +21,4 @@ testtools>=1.4.0 # MIT tempest>=11.0.0 # Apache-2.0 # releasenotes -reno>=1.6.2 # Apache2 +reno>=1.8.0 # Apache2 From 0c64947c6fda6855490958d2b95b74b1e4c1dfd7 Mon Sep 17 00:00:00 2001 From: zhengyao1 Date: Fri, 24 Jun 2016 17:04:56 +0800 Subject: [PATCH 1075/1705] make string.letters python3 compatible The string.letters in python2 was supported. But in python3, string.letters was removed. In python3, recommend using string.ascii_letters instead. This patch fix it. Change-Id: I7bfe2b94c0c740d9143ddddc655f8086a537f24b Closes-Bug: #1595786 --- novaclient/tests/functional/v2/test_servers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/novaclient/tests/functional/v2/test_servers.py b/novaclient/tests/functional/v2/test_servers.py index 1eadf066a..cad3e1b17 100644 --- a/novaclient/tests/functional/v2/test_servers.py +++ b/novaclient/tests/functional/v2/test_servers.py @@ -121,7 +121,8 @@ def test_add_remove_description_on_existing_server(self): def test_update_with_description_longer_than_255_symbols(self): # Negative case for description longer than 255 characters server = self._create_server() - descr = ''.join(random.choice(string.letters) for i in range(256)) + descr = ''.join(random.choice(string.ascii_letters) + for i in range(256)) output = self.nova("update %s --description '%s'" % (server.id, descr), fail_ok=True, merge_stderr=True) self.assertIn("\nERROR (BadRequest): Invalid input for field/attribute" From d6ca9e2d7473f6d15a3c76bcd5a7c3cc65b49296 Mon Sep 17 00:00:00 2001 From: Andreas Jaeger Date: Tue, 28 Jun 2016 14:26:12 +0200 Subject: [PATCH 1076/1705] List system dependencies for running common tests Add an other-requirements.txt file containing a cross-platform list of dependencies needed for running included tox-based tests. Also include a tox environment for convenience calling the bindep[*] utility to list any missing system requirements. This change is self-testing. For other-requirements.txt see also http://docs.openstack.org/infra/manual/drivers.html#package-requirements [*] http://docs.openstack.org/infra/bindep/ Change-Id: I3c62f6924d2d70e23e6787b8da848fbeb0d52ddc --- other-requirements.txt | 23 +++++++++++++++++++++++ tox.ini | 8 ++++++++ 2 files changed, 31 insertions(+) create mode 100644 other-requirements.txt diff --git a/other-requirements.txt b/other-requirements.txt new file mode 100644 index 000000000..da3ab7f54 --- /dev/null +++ b/other-requirements.txt @@ -0,0 +1,23 @@ +# This is a cross-platform list tracking distribution packages needed by tests; +# see http://docs.openstack.org/infra/bindep/ for additional information. + +build-essential [platform:dpkg] +dbus-devel [platform:rpm] +dbus-glib-devel [platform:rpm] +gettext +language-pack-en [platform:ubuntu] +libdbus-1-dev [platform:dpkg] +libdbus-glib-1-dev [platform:dpkg] +libffi-dev [platform:dpkg] +libffi-devel [platform:rpm] +libuuid-devel [platform:rpm] +locales [platform:debian] +python-dev [platform:dpkg] +python-devel [platform:rpm] +python3-all-dev [platform:ubuntu !platform:ubuntu-precise] +python3-dev [platform:dpkg] +python3-devel [platform:fedora] +python3.4 [platform:ubuntu-trusty] +python3.5 [platform:ubuntu-xenial] +python34-devel [platform:centos] +uuid-dev [platform:dpkg] diff --git a/tox.ini b/tox.ini index 6dc6de411..92212f754 100644 --- a/tox.ini +++ b/tox.ini @@ -69,3 +69,11 @@ exclude=.venv,.git,.tox,dist,*lib/python*,*egg,build,doc/source/conf.py,releasen [hacking] import_exceptions = novaclient.i18n + +[testenv:bindep] +# Do not install any requirements. We want this to be fast and work even if +# system dependencies are missing, since it's used to tell you what system +# dependencies are missing! This also means that bindep must be installed +# separately, outside of the requirements files. +deps = bindep +commands = bindep test From 0b2de530053d93cdd9dc4ea08c482325321f7ff7 Mon Sep 17 00:00:00 2001 From: Roman Podoliaka Date: Wed, 29 Jun 2016 14:53:18 +0300 Subject: [PATCH 1077/1705] Make it possible to list quotas with details Allow requests to /os-quota-sets/{tenant_id}/detail for listing of detailed informantion on quotas, which also includes reserved and in-use values in addition to limit ones provided by /os-quota-sets/{tenant_id} Change-Id: I31e939c8eedee1b3b375e1d02cc21ca15dd9932a Closes-Bug: #1596891 --- novaclient/tests/unit/fixture_data/quotas.py | 2 +- novaclient/tests/unit/v2/fakes.py | 76 ++++++++++++++++++++ novaclient/tests/unit/v2/test_quotas.py | 14 ++++ novaclient/tests/unit/v2/test_shell.py | 17 +++++ novaclient/v2/quotas.py | 14 ++-- novaclient/v2/shell.py | 8 ++- 6 files changed, 125 insertions(+), 6 deletions(-) diff --git a/novaclient/tests/unit/fixture_data/quotas.py b/novaclient/tests/unit/fixture_data/quotas.py index 487cfa003..5051bf3ed 100644 --- a/novaclient/tests/unit/fixture_data/quotas.py +++ b/novaclient/tests/unit/fixture_data/quotas.py @@ -26,7 +26,7 @@ def setUp(self): self.headers = self.json_headers for u in ('test', 'tenant-id', 'tenant-id/defaults', - '%s/defaults' % uuid2): + '%s/defaults' % uuid2, 'test/detail'): self.requests.register_uri('GET', self.url(u), json=test_json, headers=self.headers) diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index baf345236..edaeafb23 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -1259,6 +1259,82 @@ def get_os_quota_sets_97f4c221bff44578b0300df4ef119353(self, **kw): 'security_groups': 1, 'security_group_rules': 1}}) + def get_os_quota_sets_97f4c221bff44578b0300df4ef119353_detail(self, **kw): + return (200, {}, { + 'quota_set': { + 'tenant_id': '97f4c221bff44578b0300df4ef119353', + 'cores': { + 'in_use': 0, + 'limit': 20, + 'reserved': 0 + }, + 'fixed_ips': { + 'in_use': 0, + 'limit': -1, + 'reserved': 0 + }, + 'floating_ips': { + 'in_use': 0, + 'limit': 10, + 'reserved': 0 + }, + 'injected_file_content_bytes': { + 'in_use': 0, + 'limit': 10240, + 'reserved': 0 + }, + 'injected_file_path_bytes': { + 'in_use': 0, + 'limit': 255, + 'reserved': 0 + }, + 'injected_files': { + 'in_use': 0, + 'limit': 5, + 'reserved': 0 + }, + 'instances': { + 'in_use': 0, + 'limit': 10, + 'reserved': 0 + }, + 'key_pairs': { + 'in_use': 0, + 'limit': 100, + 'reserved': 0 + }, + 'metadata_items': { + 'in_use': 0, + 'limit': 128, + 'reserved': 0 + }, + 'ram': { + 'in_use': 0, + 'limit': 51200, + 'reserved': 0 + }, + 'security_group_rules': { + 'in_use': 0, + 'limit': 20, + 'reserved': 0 + }, + 'security_groups': { + 'in_use': 0, + 'limit': 10, + 'reserved': 0 + }, + 'server_group_members': { + 'in_use': 0, + 'limit': 10, + 'reserved': 0 + }, + 'server_groups': { + 'in_use': 0, + 'limit': 10, + 'reserved': 0 + } + }}) + def get_os_quota_sets_97f4c221bff44578b0300df4ef119353_defaults(self): return (200, {}, { 'quota_set': { diff --git a/novaclient/tests/unit/v2/test_quotas.py b/novaclient/tests/unit/v2/test_quotas.py index d8144a55c..0ed766a03 100644 --- a/novaclient/tests/unit/v2/test_quotas.py +++ b/novaclient/tests/unit/v2/test_quotas.py @@ -38,6 +38,20 @@ def test_user_quotas_get(self): url = '/os-quota-sets/%s?user_id=%s' % (tenant_id, user_id) self.assert_called('GET', url) + def test_tenant_quotas_get_detail(self): + tenant_id = 'test' + q = self.cs.quotas.get(tenant_id, detail=True) + self.assert_request_id(q, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('GET', '/os-quota-sets/%s/detail' % tenant_id) + + def test_user_quotas_get_detail(self): + tenant_id = 'test' + user_id = 'fake_user' + q = self.cs.quotas.get(tenant_id, user_id=user_id, detail=True) + self.assert_request_id(q, fakes.FAKE_REQUEST_ID_LIST) + url = '/os-quota-sets/%s/detail?user_id=%s' % (tenant_id, user_id) + self.assert_called('GET', url) + def test_tenant_quotas_defaults(self): tenant_id = '97f4c221bff44578b0300df4ef119353' q = self.cs.quotas.defaults(tenant_id) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 8790b2a2b..d68ecc90b 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2141,6 +2141,14 @@ def test_quota_show(self): 'GET', '/os-quota-sets/97f4c221bff44578b0300df4ef119353') + def test_quota_show_detail(self): + self.run_command( + 'quota-show --tenant ' + '97f4c221bff44578b0300df4ef119353 --detail') + self.assert_called( + 'GET', + '/os-quota-sets/97f4c221bff44578b0300df4ef119353/detail') + def test_user_quota_show(self): self.run_command( 'quota-show --tenant ' @@ -2149,6 +2157,15 @@ def test_user_quota_show(self): 'GET', '/os-quota-sets/97f4c221bff44578b0300df4ef119353?user_id=u1') + def test_user_quota_show_detail(self): + self.run_command( + 'quota-show --tenant ' + '97f4c221bff44578b0300df4ef119353 --user u1 --detail') + self.assert_called( + 'GET', + '/os-quota-sets/97f4c221bff44578b0300df4ef119353/detail' + '?user_id=u1') + def test_quota_show_no_tenant(self): self.run_command('quota-show') self.assert_called('GET', '/os-quota-sets/tenant_id') diff --git a/novaclient/v2/quotas.py b/novaclient/v2/quotas.py index e05435b35..1aee5b17e 100644 --- a/novaclient/v2/quotas.py +++ b/novaclient/v2/quotas.py @@ -32,14 +32,20 @@ def update(self, *args, **kwargs): class QuotaSetManager(base.Manager): resource_class = QuotaSet - def get(self, tenant_id, user_id=None): + def get(self, tenant_id, user_id=None, detail=False): + url = '/os-quota-sets/%(tenant_id)s' + if detail: + url += '/detail' + if hasattr(tenant_id, 'tenant_id'): tenant_id = tenant_id.tenant_id if user_id: - url = '/os-quota-sets/%s?user_id=%s' % (tenant_id, user_id) + params = {'tenant_id': tenant_id, 'user_id': user_id} + url += '?user_id=%(user_id)s' else: - url = '/os-quota-sets/%s' % tenant_id - return self._get(url, "quota_set") + params = {'tenant_id': tenant_id} + + return self._get(url % params, "quota_set") def update(self, tenant_id, **kwargs): diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 1aeba676f..ceb6dfe92 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -4367,6 +4367,11 @@ def _quota_update(manager, identifier, args): metavar='', default=None, help=_('ID of user to list the quotas for.')) +@utils.arg( + '--detail', + action='store_true', + default=False, + help=_('Show detailed info (limit, reserved, in-use).')) def do_quota_show(cs, args): """List the quotas for a tenant/user.""" @@ -4378,7 +4383,8 @@ def do_quota_show(cs, args): else: project_id = cs.client.tenant_id - _quota_show(cs.quotas.get(project_id, user_id=args.user)) + _quota_show(cs.quotas.get(project_id, user_id=args.user, + detail=args.detail)) @utils.arg( From 74c3da93f5bfb019ae310ef811ea3d8137fcff44 Mon Sep 17 00:00:00 2001 From: Kristi Nikolla Date: Fri, 15 Apr 2016 10:35:59 -0400 Subject: [PATCH 1078/1705] Deprecated the `--volume-service-name` option The `--volume-service-name` name option needs to be deprecated since the volume proxy API and CLI have been deprecated. Change-Id: I8af9b62e56c5ab7bffa79b2700b3c9213aaad65d Closes-Bug: 1570593 --- novaclient/shell.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/novaclient/shell.py b/novaclient/shell.py index df2f581b2..664998e58 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -523,9 +523,11 @@ def get_base_parser(self, argv): parser.add_argument( '--volume-service-name', + action=DeprecatedAction, metavar='', default=utils.env('NOVA_VOLUME_SERVICE_NAME'), - help=_('Defaults to env[NOVA_VOLUME_SERVICE_NAME].')) + use=_('This option will be removed in novaclient 4.3.0.'), + help=argparse.SUPPRESS) parser.add_argument( '--volume_service_name', action=DeprecatedAction, From 431e3e95e4f261340db7b75ddfcd76135752e008 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Thu, 30 Jun 2016 18:03:47 +0300 Subject: [PATCH 1079/1705] [functional] Fix wrong message in server description test By unknown reasons(at least for me), output of `nova update` command for description field started fail. Test name: TestServersDescription.test_update_with_description_longer_than_255_symbols The error message doesn't include new line at the start of message anymore. Also, I don't know why new line was there in the past. The main behavior and error message are not changed, so let's remove '\n' from assertation and unblock gates. Change-Id: I8442fe837985600250ccdf0d1398424f1fca2185 --- novaclient/tests/functional/v2/test_servers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/tests/functional/v2/test_servers.py b/novaclient/tests/functional/v2/test_servers.py index cad3e1b17..6d03b7914 100644 --- a/novaclient/tests/functional/v2/test_servers.py +++ b/novaclient/tests/functional/v2/test_servers.py @@ -125,7 +125,7 @@ def test_update_with_description_longer_than_255_symbols(self): for i in range(256)) output = self.nova("update %s --description '%s'" % (server.id, descr), fail_ok=True, merge_stderr=True) - self.assertIn("\nERROR (BadRequest): Invalid input for field/attribute" + self.assertIn("ERROR (BadRequest): Invalid input for field/attribute" " description. Value: %s. u\'%s\' is too long (HTTP 400)" % (descr, descr), output) From efc6ec599f9e6ed1168fd23ff597dcb7cb7c6c38 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Thu, 30 Jun 2016 18:49:38 +0000 Subject: [PATCH 1080/1705] Updated from global requirements Change-Id: Ib5a7b908d22439f700b9a9130cdc629a1421c88b --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 47e21e6b6..01b324bb7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ keystoneauth1>=2.7.0 # Apache-2.0 iso8601>=0.1.11 # MIT oslo.i18n>=2.1.0 # Apache-2.0 oslo.serialization>=1.10.0 # Apache-2.0 -oslo.utils>=3.11.0 # Apache-2.0 +oslo.utils>=3.14.0 # Apache-2.0 PrettyTable<0.8,>=0.7 # BSD requests>=2.10.0 # Apache-2.0 simplejson>=2.2.0 # MIT From 52be6ef9982d2c9b20f8237b19fee799631dac5d Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 1 Jul 2016 04:24:36 +0000 Subject: [PATCH 1081/1705] Updated from global requirements Change-Id: I0cd85b92ae7e105d74e930ed255f6d1e14686a64 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index c1b5dbd1a..b63d28f61 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -18,7 +18,7 @@ oslosphinx!=3.4.0,>=2.5.0 # Apache-2.0 testrepository>=0.0.18 # Apache-2.0/BSD testscenarios>=0.4 # Apache-2.0/BSD testtools>=1.4.0 # MIT -tempest>=11.0.0 # Apache-2.0 +tempest>=12.1.0 # Apache-2.0 # releasenotes reno>=1.8.0 # Apache2 From 5b28eddb2ec8c66c3bc756856e765b7d53da3142 Mon Sep 17 00:00:00 2001 From: Radoslav Gerganov Date: Mon, 27 Jun 2016 13:08:31 +0300 Subject: [PATCH 1082/1705] Add support for microversion 2.31 Microversion 2.31 fixes a bug in os-console-auth-tokens API and doesn't require any changes in the novaclient. Change-Id: I275903aeb3ff048397ab592ecbe0cdd570350c41 Implements: blueprint fix-console-auth-tokens --- novaclient/__init__.py | 2 +- novaclient/tests/unit/v2/test_shell.py | 1 + releasenotes/notes/microversion-v2_31-3e1a16eb5eb53f59.yaml | 4 ++++ 3 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/microversion-v2_31-3e1a16eb5eb53f59.yaml diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 56b62db24..55c0acec8 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.30") +API_MAX_VERSION = api_versions.APIVersion("2.31") diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 8790b2a2b..5f763e8c0 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2851,6 +2851,7 @@ def test_versions(self): # headers, and is tested in test_api_versions, but is # not explicitly tested via wraps and _SUBSTITUTIONS. 28, # doesn't require any changes in novaclient + 31, # doesn't require any changes in novaclient ]) versions_supported = set(range(0, novaclient.API_MAX_VERSION.ver_minor + 1)) diff --git a/releasenotes/notes/microversion-v2_31-3e1a16eb5eb53f59.yaml b/releasenotes/notes/microversion-v2_31-3e1a16eb5eb53f59.yaml new file mode 100644 index 000000000..4ad0b4be5 --- /dev/null +++ b/releasenotes/notes/microversion-v2_31-3e1a16eb5eb53f59.yaml @@ -0,0 +1,4 @@ +--- +upgrade: + - Support for microversion 2.31 which fixes a bug in the + os-console-auth-tokens API From 84f4e1e10c4f18ff61425018a09940581747ec78 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Wed, 22 Jun 2016 16:35:21 +0300 Subject: [PATCH 1083/1705] Raise an exception in v2.client for direct_use The direct use of novaclient.v2.client.Client was deprecated long time ago (2.29.0 release of novaclient, which relates to Liberty release of OpenStack). It is time to move from warning message to an exception. It will allow us to change novaclient.v2.client.Client interface and keep compatible stuff to novaclient.client.Client. Change-Id: I6e6c6eebcf7992d553a2f85f57b8c2ede0cc8311 Related-Bug: #1493576 --- novaclient/v2/client.py | 19 ++++++++++--------- ...rect-use-of-v2client-c8e1ee2afefec5a1.yaml | 5 +++++ 2 files changed, 15 insertions(+), 9 deletions(-) create mode 100644 releasenotes/notes/restrict-direct-use-of-v2client-c8e1ee2afefec5a1.yaml diff --git a/novaclient/v2/client.py b/novaclient/v2/client.py index c202b754e..f5dee15dc 100644 --- a/novaclient/v2/client.py +++ b/novaclient/v2/client.py @@ -17,7 +17,8 @@ from novaclient import api_versions from novaclient import client -from novaclient.i18n import _LW +from novaclient import exceptions +from novaclient.i18n import _LE from novaclient.v2 import agents from novaclient.v2 import aggregates from novaclient.v2 import availability_zones @@ -101,18 +102,18 @@ def __init__(self, username=None, api_key=None, project_id=None, :param str session: Session :param str auth: Auth :param api_version: Compute API version - :param direct_use: Direct use + :param direct_use: Inner variable of novaclient. Do not use it outside + novaclient. It's restricted. :param logger: Logger :type api_version: novaclient.api_versions.APIVersion """ if direct_use: - import warnings - - warnings.warn( - _LW("'novaclient.v2.client.Client' is not designed to be " - "initialized directly. It is inner class of novaclient. " - "Please, use 'novaclient.client.Client' instead. " - "Related lp bug-report: 1493576")) + raise exceptions.Forbidden( + 403, _LE("'novaclient.v2.client.Client' is not designed to be " + "initialized directly. It is inner class of " + "novaclient. You should use " + "'novaclient.client.Client' instead. Related lp " + "bug-report: 1493576")) # FIXME(comstud): Rename the api_key argument above when we # know it's not being used as keyword argument diff --git a/releasenotes/notes/restrict-direct-use-of-v2client-c8e1ee2afefec5a1.yaml b/releasenotes/notes/restrict-direct-use-of-v2client-c8e1ee2afefec5a1.yaml new file mode 100644 index 000000000..0e90cc810 --- /dev/null +++ b/releasenotes/notes/restrict-direct-use-of-v2client-c8e1ee2afefec5a1.yaml @@ -0,0 +1,5 @@ +--- +upgrade: + - novaclient.v2.client.Client raises an exception in case of direct usage + instead of warning message. novaclient.client.Client is a primary + interface to initialize the python client for Nova. From f1b068345595e04b0e74eefa0db5b79a2329b2e8 Mon Sep 17 00:00:00 2001 From: Roman Podoliaka Date: Fri, 1 Jul 2016 14:36:03 +0300 Subject: [PATCH 1084/1705] functional: skip test_auth_via_keystone_vX if X is not available The v2 test case currently fails in jobs where only Identity v3 API is enabled (e.g. gate-novaclient-dsvm-functional-identity-v3-only-nv). Change-Id: I5ee84a7c93b044d11eb94e855ab0cfe56c294db7 --- novaclient/tests/functional/base.py | 14 ++++++++++++++ novaclient/tests/functional/test_auth.py | 10 ++++++++++ 2 files changed, 24 insertions(+) diff --git a/novaclient/tests/functional/base.py b/novaclient/tests/functional/base.py index f14e46fa4..eafa2bf6e 100644 --- a/novaclient/tests/functional/base.py +++ b/novaclient/tests/functional/base.py @@ -16,9 +16,11 @@ from cinderclient.v2 import client as cinderclient import fixtures +from keystoneauth1.exceptions import discovery as discovery_exc from keystoneauth1 import identity from keystoneauth1 import session as ksession from keystoneclient import client as keystoneclient +from keystoneclient import discover as keystone_discover import os_client_config import six import tempest.lib.cli.base @@ -33,6 +35,18 @@ "'cubswin:)'. use 'sudo' for root.") +def is_keystone_version_available(session, version): + """Given a (major, minor) pair, check if the API version is enabled.""" + + d = keystone_discover.Discover(session) + try: + d.create_client(version) + except (discovery_exc.DiscoveryFailure, discovery_exc.VersionNotAvailable): + return False + else: + return True + + # The following are simple filter functions that filter our available # image / flavor list so that they can be used in standard testing. def pick_flavor(flavors): diff --git a/novaclient/tests/functional/test_auth.py b/novaclient/tests/functional/test_auth.py index 58e303bcb..16ca5977e 100644 --- a/novaclient/tests/functional/test_auth.py +++ b/novaclient/tests/functional/test_auth.py @@ -36,7 +36,17 @@ def nova(self, action, identity_api_version): "nova", action, flags, cli_dir=self.cli_clients.cli_dir) def test_auth_via_keystone_v2(self): + session = self.keystone.session + version = (2, 0) + if not base.is_keystone_version_available(session, version): + self.skip("Identity API version 2.0 is not available.") + self.nova("list", identity_api_version="2.0") def test_auth_via_keystone_v3(self): + session = self.keystone.session + version = (3, 0) + if not base.is_keystone_version_available(session, version): + self.skip("Identity API version 3.0 is not available.") + self.nova("list", identity_api_version="3") From 2ba578d505e494272203b51ed655a6a7b1cac11a Mon Sep 17 00:00:00 2001 From: Roman Podoliaka Date: Fri, 1 Jul 2016 14:39:01 +0300 Subject: [PATCH 1085/1705] functional: fix a deprecation warning in test_auth.TestAuthentication Jobs logs currently contain a warning that --endpoint-type is deprecated in favor of --os-endpoint-type. Fix that. Change-Id: I05260e5550fb750067d01f77ee5d603d0a6154e5 --- novaclient/tests/functional/test_auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/tests/functional/test_auth.py b/novaclient/tests/functional/test_auth.py index 16ca5977e..760e62e92 100644 --- a/novaclient/tests/functional/test_auth.py +++ b/novaclient/tests/functional/test_auth.py @@ -24,7 +24,7 @@ def nova(self, action, identity_api_version): url.params, url.query, url.fragment)) flags = ('--os-username %s --os-tenant-name %s --os-password %s ' - '--os-auth-url %s --endpoint-type publicURL' % ( + '--os-auth-url %s --os-endpoint-type publicURL' % ( self.cli_clients.username, self.cli_clients.tenant_name, self.cli_clients.password, From 6f8667990cdb3cd662d51be2403cca5859559f04 Mon Sep 17 00:00:00 2001 From: Kevin_Zheng Date: Wed, 18 May 2016 09:29:37 +0800 Subject: [PATCH 1086/1705] Clean up deprecated CLI options Clean up all deprecated CLI options that are marked to be removed in version 3.3.0 as we are already in version 4.0.0 Change-Id: Ia9c5602df1be987e7c12f625d4f8359590678a19 --- novaclient/shell.py | 104 ---------- novaclient/tests/unit/test_shell.py | 2 +- novaclient/tests/unit/v2/test_shell.py | 23 +-- novaclient/v2/shell.py | 185 ------------------ ...ated-option-in-3.3.0-82a413157838570d.yaml | 53 +++++ 5 files changed, 63 insertions(+), 304 deletions(-) create mode 100644 releasenotes/notes/remove-deprecated-option-in-3.3.0-82a413157838570d.yaml diff --git a/novaclient/shell.py b/novaclient/shell.py index 42c182d7f..36b12f7e1 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -449,80 +449,28 @@ def get_base_parser(self, argv): action='store_true', help=_("Print call timing info.")) - parser.add_argument( - '--os_username', - action=DeprecatedAction, - use=_('use "%s"; this option will be removed ' - 'in novaclient 3.3.0.') % '--os-username', - help=argparse.SUPPRESS) - - parser.add_argument( - '--os_password', - action=DeprecatedAction, - use=_('use "%s"; this option will be removed ' - 'in novaclient 3.3.0.') % '--os-password', - help=argparse.SUPPRESS) - - parser.add_argument( - '--os_tenant_name', - action=DeprecatedAction, - use=_('use "%s"; this option will be removed ' - 'in novaclient 3.3.0.') % '--os-tenant-name', - help=argparse.SUPPRESS) - - parser.add_argument( - '--os_auth_url', - action=DeprecatedAction, - use=_('use "%s"; this option will be removed ' - 'in novaclient 3.3.0.') % '--os-auth-url', - help=argparse.SUPPRESS) - parser.add_argument( '--os-region-name', metavar='', default=utils.env('OS_REGION_NAME', 'NOVA_REGION_NAME'), help=_('Defaults to env[OS_REGION_NAME].')) - parser.add_argument( - '--os_region_name', - action=DeprecatedAction, - use=_('use "%s"; this option will be removed ' - 'in novaclient 3.3.0.') % '--os-region-name', - help=argparse.SUPPRESS) parser.add_argument( '--os-auth-system', metavar='', default=utils.env('OS_AUTH_SYSTEM'), help=argparse.SUPPRESS) - parser.add_argument( - '--os_auth_system', - action=DeprecatedAction, - use=_('use "%s"; this option will be removed ' - 'in novaclient 3.3.0.') % '--os-auth-system', - help=argparse.SUPPRESS) parser.add_argument( '--service-type', metavar='', help=_('Defaults to compute for most actions.')) - parser.add_argument( - '--service_type', - action=DeprecatedAction, - use=_('use "%s"; this option will be removed ' - 'in novaclient 3.3.0.') % '--service-type', - help=argparse.SUPPRESS) parser.add_argument( '--service-name', metavar='', default=utils.env('NOVA_SERVICE_NAME'), help=_('Defaults to env[NOVA_SERVICE_NAME].')) - parser.add_argument( - '--service_name', - action=DeprecatedAction, - use=_('use "%s"; this option will be removed ' - 'in novaclient 3.3.0.') % '--service-name', - help=argparse.SUPPRESS) parser.add_argument( '--volume-service-name', @@ -531,12 +479,6 @@ def get_base_parser(self, argv): default=utils.env('NOVA_VOLUME_SERVICE_NAME'), use=_('This option will be removed in novaclient 4.3.0.'), help=argparse.SUPPRESS) - parser.add_argument( - '--volume_service_name', - action=DeprecatedAction, - use=_('use "%s"; this option will be removed ' - 'in novaclient 3.3.0.') % '--volume-service-name', - help=argparse.SUPPRESS) parser.add_argument( '--os-endpoint-type', @@ -550,19 +492,6 @@ def get_base_parser(self, argv): 'env[OS_ENDPOINT_TYPE] or ') + DEFAULT_NOVA_ENDPOINT_TYPE + '.') - parser.add_argument( - '--endpoint-type', - action=DeprecatedAction, - use=_('use "%s"; this option will be removed ' - 'in novaclient 3.3.0.') % '--os-endpoint-type', - help=argparse.SUPPRESS) - # NOTE(dtroyer): We can't add --endpoint_type here due to argparse - # thinking usage-list --end is ambiguous; but it - # works fine with only --endpoint-type present - # Go figure. I'm leaving this here for doc purposes. - # parser.add_argument('--endpoint_type', - # help=argparse.SUPPRESS) - parser.add_argument( '--os-compute-api-version', metavar='', @@ -570,12 +499,6 @@ def get_base_parser(self, argv): default=DEFAULT_OS_COMPUTE_API_VERSION), help=_('Accepts X, X.Y (where X is major and Y is minor part) or ' '"X.latest", defaults to env[OS_COMPUTE_API_VERSION].')) - parser.add_argument( - '--os_compute_api_version', - action=DeprecatedAction, - use=_('use "%s"; this option will be removed ' - 'in novaclient 3.3.0.') % '--os-compute-api-version', - help=argparse.SUPPRESS) parser.add_argument( '--bypass-url', @@ -584,12 +507,6 @@ def get_base_parser(self, argv): default=utils.env('NOVACLIENT_BYPASS_URL'), help=_("Use this API endpoint instead of the Service Catalog. " "Defaults to env[NOVACLIENT_BYPASS_URL].")) - parser.add_argument( - '--bypass_url', - action=DeprecatedAction, - use=_('use "%s"; this option will be removed ' - 'in novaclient 3.3.0.') % '--bypass-url', - help=argparse.SUPPRESS) # The auth-system-plugins might require some extra options novaclient.auth_plugin.load_auth_system_opts(parser) @@ -710,27 +627,6 @@ def setup_debugging(self, debug): def main(self, argv): # Parse args once to find version and debug settings parser = self.get_base_parser(argv) - - # NOTE(dtroyer): Hackery to handle --endpoint_type due to argparse - # thinking usage-list --end is ambiguous; but it - # works fine with only --endpoint-type present - # Go figure. - if '--endpoint_type' in argv: - spot = argv.index('--endpoint_type') - argv[spot] = '--endpoint-type' - # NOTE(Vek): Not emitting a warning here, as that will - # occur when "--endpoint-type" is processed - - # For backwards compat with old os-auth-token parameter - if '--os-auth-token' in argv: - spot = argv.index('--os-auth-token') - argv[spot] = '--os-token' - print(_('WARNING: Option "%(option)s" is deprecated; %(use)s') % { - 'option': '--os-auth-token', - 'use': _('use "%s"; this option will be removed in ' - 'novaclient 3.3.0.') % '--os-token', - }, file=sys.stderr) - (args, args_list) = parser.parse_known_args(argv) self.setup_debugging(args.debug) diff --git a/novaclient/tests/unit/test_shell.py b/novaclient/tests/unit/test_shell.py index b8899b27e..bfb889c2a 100644 --- a/novaclient/tests/unit/test_shell.py +++ b/novaclient/tests/unit/test_shell.py @@ -607,7 +607,7 @@ def _test_service_type(self, version, service_type, mock_client): if version is None: cmd = 'list' else: - cmd = ('--service_type %s --os-compute-api-version %s list' % + cmd = ('--service-type %s --os-compute-api-version %s list' % (service_type, version)) self.make_env() self.shell(cmd) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 4211cd756..f0593e443 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -158,7 +158,7 @@ def test_boot_image_with(self): ) def test_boot_key(self): - self.run_command('boot --flavor 1 --image 1 --key_name 1 some-server') + self.run_command('boot --flavor 1 --image 1 --key-name 1 some-server') self.assert_called_anytime( 'POST', '/servers', {'server': { @@ -177,7 +177,7 @@ def test_boot_user_data(self): data = testfile_fd.read().encode('utf-8') expected_file_data = base64.b64encode(data).decode('utf-8') self.run_command( - 'boot --flavor 1 --image 1 --user_data %s some-server' % testfile) + 'boot --flavor 1 --image 1 --user-data %s some-server' % testfile) self.assert_called_anytime( 'POST', '/servers', {'server': { @@ -274,7 +274,7 @@ def test_boot_invalid_user_data(self): invalid_file = os.path.join(os.path.dirname(__file__), 'no_such_file') cmd = ('boot some-server --flavor 1 --image 1' - ' --user_data %s' % invalid_file) + ' --user-data %s' % invalid_file) self.assertRaises(exceptions.CommandError, self.run_command, cmd) def test_boot_no_image_no_bdms(self): @@ -287,7 +287,7 @@ def test_boot_no_flavor(self): def test_boot_no_image_bdms(self): self.run_command( - 'boot --flavor 1 --block_device_mapping vda=blah:::0 some-server' + 'boot --flavor 1 --block-device-mapping vda=blah:::0 some-server' ) self.assert_called_anytime( 'POST', '/os-volumes_boot', @@ -669,8 +669,9 @@ def test_boot_invalid_files(self): ' --file /foo=%s' % invalid_file) self.assertRaises(exceptions.CommandError, self.run_command, cmd) - def test_boot_num_instances(self): - self.run_command('boot --image 1 --flavor 1 --num-instances 3 server') + def test_boot_max_min_count(self): + self.run_command('boot --image 1 --flavor 1 --min-count 1' + ' --max-count 3 server') self.assert_called_anytime( 'POST', '/servers', { @@ -683,14 +684,8 @@ def test_boot_num_instances(self): } }) - def test_boot_invalid_num_instances(self): - cmd = 'boot --image 1 --flavor 1 --num-instances 0 server' - self.assertRaises(exceptions.CommandError, self.run_command, cmd) - - def test_boot_num_instances_and_count(self): - cmd = 'boot --image 1 --flavor 1 --num-instances 3 --min-count 3 serv' - self.assertRaises(exceptions.CommandError, self.run_command, cmd) - cmd = 'boot --image 1 --flavor 1 --num-instances 3 --max-count 3 serv' + def test_boot_invalid_min_count(self): + cmd = 'boot --image 1 --flavor 1 --min-count 0 server' self.assertRaises(exceptions.CommandError, self.run_command, cmd) def test_boot_min_max_count(self): diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index ceb6dfe92..b6ce4e7b2 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -181,17 +181,6 @@ def _boot(cs, args): min_count = 1 max_count = 1 - # Don't let user mix num_instances and max_count/min_count. - if (args.num_instances is not None and - args.min_count is None and - args.max_count is None): - if args.num_instances < 1: - raise exceptions.CommandError(_("num_instances should be >= 1")) - max_count = args.num_instances - elif (args.num_instances is not None and - (args.min_count is not None or args.max_count is not None)): - raise exceptions.CommandError(_("Don't mix num-instances and " - "max/min-count")) if args.min_count is not None: if args.min_count < 1: raise exceptions.CommandError(_("min_count should be >= 1")) @@ -388,15 +377,6 @@ def _boot(cs, args): default=None, metavar="", help=_("Snapshot ID to boot from (will create a volume).")) -@utils.arg( - '--num-instances', - default=None, - type=int, - metavar='', - action=shell.DeprecatedAction, - use=_('use "--min-count" and "--max-count"; this option will be removed ' - 'in novaclient 3.3.0.'), - help=argparse.SUPPRESS) @utils.arg( '--min-count', default=None, @@ -430,46 +410,22 @@ def _boot(cs, args): metavar='', help=_("Key name of keypair that should be created earlier with \ the command keypair-add.")) -@utils.arg( - '--key_name', - action=shell.DeprecatedAction, - use=_('use "%s"; this option will be removed in ' - 'novaclient 3.3.0.') % '--key-name', - help=argparse.SUPPRESS) @utils.arg('name', metavar='', help=_('Name for the new server.')) @utils.arg( '--user-data', default=None, metavar='', help=_("user data file to pass to be exposed by the metadata server.")) -@utils.arg( - '--user_data', - action=shell.DeprecatedAction, - use=_('use "%s"; this option will be removed in ' - 'novaclient 3.3.0.') % '--user-data', - help=argparse.SUPPRESS) @utils.arg( '--availability-zone', default=None, metavar='', help=_("The availability zone for server placement.")) -@utils.arg( - '--availability_zone', - action=shell.DeprecatedAction, - use=_('use "%s"; this option will be removed in ' - 'novaclient 3.3.0.') % '--availability-zone', - help=argparse.SUPPRESS) @utils.arg( '--security-groups', default=None, metavar='', help=_("Comma separated list of security group names.")) -@utils.arg( - '--security_groups', - action=shell.DeprecatedAction, - use=_('use "%s"; this option will be removed in ' - 'novaclient 3.3.0.') % '--security-groups', - help=argparse.SUPPRESS) @utils.arg( '--block-device-mapping', metavar="", @@ -477,13 +433,6 @@ def _boot(cs, args): default=[], help=_("Block device mapping in the format " "=:::.")) -@utils.arg( - '--block_device_mapping', - real_action='append', - action=shell.DeprecatedAction, - use=_('use "%s"; this option will be removed in ' - 'novaclient 3.3.0.') % '--block-device-mapping', - help=argparse.SUPPRESS) @utils.arg( '--block-device', metavar="key1=value1[,key2=value2...]", @@ -1317,12 +1266,6 @@ def do_image_delete(cs, args): metavar='', default=None, help=_('Only return servers that match reservation-id.')) -@utils.arg( - '--reservation_id', - action=shell.DeprecatedAction, - use=_('use "%s"; this option will be removed in ' - 'novaclient 3.3.0.') % '--reservation-id', - help=argparse.SUPPRESS) @utils.arg( '--ip', dest='ip', @@ -1347,12 +1290,6 @@ def do_image_delete(cs, args): metavar='', default=None, help=_('Search with regular expression match by server name.')) -@utils.arg( - '--instance_name', - action=shell.DeprecatedAction, - use=_('use "%s"; this option will be removed in ' - 'novaclient 3.3.0.') % '--instance-name', - help=argparse.SUPPRESS) @utils.arg( '--status', dest='status', @@ -1388,15 +1325,6 @@ def do_image_delete(cs, args): default=int(strutils.bool_from_string( os.environ.get("ALL_TENANTS", 'false'), True)), help=_('Display information from all tenants (Admin only).')) -@utils.arg( - '--all_tenants', - nargs='?', - type=int, - const=1, - action=shell.DeprecatedAction, - use=_('use "%s"; this option will be removed in ' - 'novaclient 3.3.0.') % '--all-tenants', - help=argparse.SUPPRESS) @utils.arg( '--tenant', # nova db searches by project_id @@ -1717,12 +1645,6 @@ def do_reboot(cs, args): metavar='', default=False, help=_("Set the provided admin password on the rebuilt server.")) -@utils.arg( - '--rebuild_password', - action=shell.DeprecatedAction, - use=_('use "%s"; this option will be removed in ' - 'novaclient 3.3.0.') % '--rebuild-password', - help=argparse.SUPPRESS) @utils.arg( '--poll', dest='poll', @@ -2486,13 +2408,6 @@ def do_get_rdp_console(cs, args): '--console-type', default='serial', help=_('Type of serial console, default="serial".')) -@utils.arg( - '--console_type', - default='serial', - action=shell.DeprecatedAction, - use=_('use "%s"; this option will be removed in ' - 'novaclient 3.3.0.') % '--console-type', - help=argparse.SUPPRESS) def do_get_serial_console(cs, args): """Get a serial console to a server.""" if args.console_type not in ('serial',): @@ -2773,12 +2688,6 @@ def do_dns_delete_domain(cs, args): default=None, help=_('Limit access to this domain to servers ' 'in the specified availability zone.')) -@utils.arg( - '--availability_zone', - action=shell.DeprecatedAction, - use=_('use "%s"; this option will be removed in ' - 'novaclient 3.3.0.') % '--availability-zone', - help=argparse.SUPPRESS) def do_dns_create_private_domain(cs, args): """Create the specified DNS domain.""" cs.dns_domains.create_private(args.domain, @@ -2957,15 +2866,6 @@ def do_secgroup_delete(cs, args): default=int(strutils.bool_from_string( os.environ.get("ALL_TENANTS", 'false'), True)), help=_('Display information from all tenants (Admin only).')) -@utils.arg( - '--all_tenants', - nargs='?', - type=int, - const=1, - action=shell.DeprecatedAction, - use=_('use "%s"; this option will be removed in ' - 'novaclient 3.3.0.') % '--all-tenants', - help=argparse.SUPPRESS) def do_secgroup_list(cs, args): """List security groups for the current tenant.""" search_opts = {'all_tenants': args.all_tenants} @@ -3092,12 +2992,6 @@ def _keypair_create(cs, args, name, pub_key): metavar='', default=None, help=_('Path to a public ssh key.')) -@utils.arg( - '--pub_key', - action=shell.DeprecatedAction, - use=_('use "%s"; this option will be removed in ' - 'novaclient 3.3.0.') % '--pub-key', - help=argparse.SUPPRESS) @utils.arg( '--key-type', metavar='', @@ -3746,13 +3640,6 @@ def parser_hosts(fields): default="auto", help=_('True in case of block_migration. (Default=auto:live_migration)'), start_version="2.25") -@utils.arg( - '--block_migrate', - real_action='store_true', - action=shell.DeprecatedAction, - use=_('use "%s"; this option will be removed in ' - 'novaclient 3.3.0.') % '--block-migrate', - help=argparse.SUPPRESS) @utils.arg( '--disk-over-commit', action='store_true', @@ -3760,14 +3647,6 @@ def parser_hosts(fields): default=False, help=_('Allow overcommit. (Default=False)'), start_version="2.0", end_version="2.24") -@utils.arg( - '--disk_over_commit', - real_action='store_true', - action=shell.DeprecatedAction, - use=_('use "%s"; this option will be removed in ' - 'novaclient 3.3.0.') % '--disk-over-commit', - help=argparse.SUPPRESS, - start_version="2.0", end_version="2.24") @utils.arg( '--force', dest='force', @@ -4436,13 +4315,6 @@ def do_quota_defaults(cs, args): type=int, default=None, help=_('New value for the "floating-ips" quota.')) -@utils.arg( - '--floating_ips', - type=int, - action=shell.DeprecatedAction, - use=_('use "%s"; this option will be removed in ' - 'novaclient 3.3.0.') % '--floating-ips', - help=argparse.SUPPRESS) @utils.arg( '--fixed-ips', metavar='', @@ -4455,39 +4327,18 @@ def do_quota_defaults(cs, args): type=int, default=None, help=_('New value for the "metadata-items" quota.')) -@utils.arg( - '--metadata_items', - type=int, - action=shell.DeprecatedAction, - use=_('use "%s"; this option will be removed in ' - 'novaclient 3.3.0.') % '--metadata-items', - help=argparse.SUPPRESS) @utils.arg( '--injected-files', metavar='', type=int, default=None, help=_('New value for the "injected-files" quota.')) -@utils.arg( - '--injected_files', - type=int, - action=shell.DeprecatedAction, - use=_('use "%s"; this option will be removed in ' - 'novaclient 3.3.0.') % '--injected-files', - help=argparse.SUPPRESS) @utils.arg( '--injected-file-content-bytes', metavar='', type=int, default=None, help=_('New value for the "injected-file-content-bytes" quota.')) -@utils.arg( - '--injected_file_content_bytes', - type=int, - action=shell.DeprecatedAction, - use=_('use "%s"; this option will be removed in ' - 'novaclient 3.3.0.') % '--injected-file-content-bytes', - help=argparse.SUPPRESS) @utils.arg( '--injected-file-path-bytes', metavar='', @@ -4589,13 +4440,6 @@ def do_quota_class_show(cs, args): type=int, default=None, help=_('New value for the "floating-ips" quota.')) -@utils.arg( - '--floating_ips', - type=int, - action=shell.DeprecatedAction, - use=_('use "%s"; this option will be removed in ' - 'novaclient 3.3.0.') % '--floating-ips', - help=argparse.SUPPRESS) @utils.arg( '--fixed-ips', metavar='', @@ -4608,39 +4452,18 @@ def do_quota_class_show(cs, args): type=int, default=None, help=_('New value for the "metadata-items" quota.')) -@utils.arg( - '--metadata_items', - type=int, - action=shell.DeprecatedAction, - use=_('use "%s"; this option will be removed in ' - 'novaclient 3.3.0.') % '--metadata-items', - help=argparse.SUPPRESS) @utils.arg( '--injected-files', metavar='', type=int, default=None, help=_('New value for the "injected-files" quota.')) -@utils.arg( - '--injected_files', - type=int, - action=shell.DeprecatedAction, - use=_('use "%s"; this option will be removed in ' - 'novaclient 3.3.0.') % '--injected-files', - help=argparse.SUPPRESS) @utils.arg( '--injected-file-content-bytes', metavar='', type=int, default=None, help=_('New value for the "injected-file-content-bytes" quota.')) -@utils.arg( - '--injected_file_content_bytes', - type=int, - action=shell.DeprecatedAction, - use=_('use "%s"; this option will be removed in ' - 'novaclient 3.3.0.') % '--injected-file-content-bytes', - help=argparse.SUPPRESS) @utils.arg( '--injected-file-path-bytes', metavar='', @@ -4961,14 +4784,6 @@ def do_secgroup_delete_default_rule(cs, args): default=argparse.SUPPRESS, nargs='*', help=_('Policies for the server groups.')) -@utils.arg( - '--policy', - default=[], - real_action='append', - action=shell.DeprecatedAction, - use=_('use positional parameters; this option will be removed in ' - 'novaclient 3.3.0.'), - help=argparse.SUPPRESS) def do_server_group_create(cs, args): """Create a new server group with the specified details.""" if not args.policy: diff --git a/releasenotes/notes/remove-deprecated-option-in-3.3.0-82a413157838570d.yaml b/releasenotes/notes/remove-deprecated-option-in-3.3.0-82a413157838570d.yaml new file mode 100644 index 000000000..f78208551 --- /dev/null +++ b/releasenotes/notes/remove-deprecated-option-in-3.3.0-82a413157838570d.yaml @@ -0,0 +1,53 @@ +--- +update: + - The following deprecated options have been removed: + - create instances: + - --num-instance replaced by --min-count and --max-count + - --key_name replaced by --key-name + - --user_data replaced by --user-data + - --availability_zone replaced by -- availability-zone + - --security_groups replaced by --sercurity-groups + - --block_device_mapping replaced by --block-device-mapping + - list servers: + - --reservation_id replaced by --reservation-id + - --instance_name replaced by --instance-name + - --all_tenants replaced by --all-tenants + - rebuild instance: + - --rebuild_password replaced by --rebuild-password + - get serial console: + - --console_type replaced by --console-type + - create dns private domain: + - --availability_zone replaced by --availability-zone + - list security groups: + - --all_tenants replaced by --all-tenants + - add key pairs: + - --pub_key replaced by --pub-key + - live-migrate servers: + - --block_migrate replaced by --block-migrate + - --disk_over_commit replaced by --disk-over-commit + - update quotas: + - --floating_ips replaced by --floating-ips + - --metadata_items replaced by --metadata-items + - --injected_files replaced by --injected-files + - --injected_file_content_bytes replaced by --injected-file-content-bytes + - update quota classes: + - --floating_ips replaced by --floating-ips + - --metadata_items replaced by --metadata-items + - --injected_files replaced by --injected-files + - --injected_file_content_bytes replaced by --injected-file-content-bytes + - create server groups: + - --policy + - Authentication Options: + - --os_username replaced by --os-username + - --os_password replaced by --os-password + - --os_tenant_name replaced by --os-tenant-name + - --os_auth_url replaced by --os-auth-url + - --os_region_name replaced by --os-region-name + - --os_auth_system replaced by --os-auth-system + - --endpoint-type replaced by --os-endpoint-type + - Optional arguments: + - --service_type replaced by --service-type + - --service_name replaced by --service-name + - --volume_service_name replaced by --volume-service-name + - --os_compute_api_version replaced by --os-compute-api-version + - --bypass_url replaced by --bypass-url From 58c46d66353b85d16710561bf6091110f1b184c5 Mon Sep 17 00:00:00 2001 From: yuyafei Date: Tue, 5 Jul 2016 16:21:47 +0800 Subject: [PATCH 1087/1705] Remove white space between print and () TrivialFix Change-Id: I65546f177b6d1014b34be3c6a28cf52c9d39ab70 --- novaclient/v2/shell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index ceb6dfe92..b322eefa6 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -5026,7 +5026,7 @@ def do_version_list(cs, args): print(_("Maximum version %(v)s") % {'v': novaclient.API_MAX_VERSION.get_string()}) - print (_("\nServer supported API versions:")) + print(_("\nServer supported API versions:")) utils.print_list(result, columns) From 55201c55a8d74fc49fe8e6359d54d5b4c2303e1c Mon Sep 17 00:00:00 2001 From: yuyafei Date: Mon, 4 Jul 2016 15:30:56 +0800 Subject: [PATCH 1088/1705] base.Resource not define __ne__() built-in function Class base.Resource defines __eq__() built-in function, but does not define __ne__() built-in function, so self.assertEqual works but self.assertNotEqual does not work at all in this test case in python2. This patch fixes it by defining __ne__() built-in function of class base.Resource. Also fixes spelling errors:resoruces. Change-Id: I39e6f6b94e9490afc14143208e6f20b0ef960991 Closes-Bug: #1586268 --- novaclient/base.py | 3 +++ novaclient/tests/unit/test_base.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/novaclient/base.py b/novaclient/base.py index 0200332d6..6ef271684 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -209,6 +209,9 @@ def __eq__(self, other): return self.id == other.id return self._info == other._info + def __ne__(self, other): + return not self.__eq__(other) + def is_loaded(self): return self._loaded diff --git a/novaclient/tests/unit/test_base.py b/novaclient/tests/unit/test_base.py index 9fa75364c..a24636ca5 100644 --- a/novaclient/tests/unit/test_base.py +++ b/novaclient/tests/unit/test_base.py @@ -63,7 +63,7 @@ def test_eq(self): r2 = base.Resource(None, {'id': 1, 'name': 'hello'}) self.assertEqual(r1, r2) - # Two resoruces of different types: never equal + # Two resources of different types: never equal r1 = base.Resource(None, {'id': 1}) r2 = flavors.Flavor(None, {'id': 1}) self.assertNotEqual(r1, r2) From 423239c9754b573bc54f0d04efb0807c17ae0d8d Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Sat, 9 Jul 2016 19:27:02 +0000 Subject: [PATCH 1089/1705] Updated from global requirements Change-Id: I3ca9064c717a6ecd16347bf23edccfa32820a6f5 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 01b324bb7..c2b23f281 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ keystoneauth1>=2.7.0 # Apache-2.0 iso8601>=0.1.11 # MIT oslo.i18n>=2.1.0 # Apache-2.0 oslo.serialization>=1.10.0 # Apache-2.0 -oslo.utils>=3.14.0 # Apache-2.0 +oslo.utils>=3.15.0 # Apache-2.0 PrettyTable<0.8,>=0.7 # BSD requests>=2.10.0 # Apache-2.0 simplejson>=2.2.0 # MIT From e7f1bdb7e416d68100a58c46b33a0043c792de26 Mon Sep 17 00:00:00 2001 From: Artom Lifshitz Date: Mon, 11 Jul 2016 15:52:09 -0400 Subject: [PATCH 1090/1705] Update clouds.yaml.sample Update clouds.yaml.sample to: * Use v3 Keystone URL * Include user_domain_id and project_domain_id Those are necessary to run functional tests against a local devstack. This change also changes the password to 'change_me' to make it clearer that it needs to be changed. Change-Id: I060648ae63c57619351d6f23f25ae75320d08321 --- novaclient/tests/functional/clouds.yaml.sample | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/novaclient/tests/functional/clouds.yaml.sample b/novaclient/tests/functional/clouds.yaml.sample index 4d65bd9bc..baf23f695 100644 --- a/novaclient/tests/functional/clouds.yaml.sample +++ b/novaclient/tests/functional/clouds.yaml.sample @@ -2,6 +2,8 @@ clouds: devstack: auth: username: admin - password: a + password: change_me project_name: admin - auth_url: http://localhost:5000/v2.0 + auth_url: http://localhost:5000/v3 + user_domain_id: default + project_domain_id: default From 7fb0bd4f357a7f9e24c864584b398a25171580cf Mon Sep 17 00:00:00 2001 From: Artom Lifshitz Date: Wed, 30 Mar 2016 04:58:50 -0400 Subject: [PATCH 1091/1705] Virtual device tagging client support In order to support virtual device role tagging that was introduced in microversion 2.32, this patch adds: * The 'tag' key to the --nic flag when booting an instance. * The 'tag' key to the --block-device flag when booting an instance. Change-Id: I1866c670994254bc2849b494e318f4ff8cc44eb7 Implements: blueprint virt-device-role-tagging --- novaclient/__init__.py | 2 +- .../functional/v2/test_device_tagging.py | 36 +++++++++++ novaclient/tests/unit/v2/test_servers.py | 50 ++++++++++++++++ novaclient/tests/unit/v2/test_shell.py | 60 +++++++++++++++++++ novaclient/v2/servers.py | 18 ++++++ novaclient/v2/shell.py | 58 +++++++++++++++++- .../microversion-v2_32-7947430cc2415597.yaml | 25 ++++++++ 7 files changed, 245 insertions(+), 4 deletions(-) create mode 100644 novaclient/tests/functional/v2/test_device_tagging.py create mode 100644 releasenotes/notes/microversion-v2_32-7947430cc2415597.yaml diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 55c0acec8..e13263741 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.31") +API_MAX_VERSION = api_versions.APIVersion("2.32") diff --git a/novaclient/tests/functional/v2/test_device_tagging.py b/novaclient/tests/functional/v2/test_device_tagging.py new file mode 100644 index 000000000..d9a5850d2 --- /dev/null +++ b/novaclient/tests/functional/v2/test_device_tagging.py @@ -0,0 +1,36 @@ +# Copyright (C) 2016, Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import uuid + +from novaclient.tests.functional import base + + +class TestDeviceTaggingCLI(base.ClientTestBase): + + COMPUTE_API_VERSION = "2.32" + + def test_boot_server_with_tagged_devices(self): + server_info = self.nova('boot', params=( + '%(name)s --flavor %(flavor)s --poll ' + '--nic net-id=%(net-uuid)s,tag=foo ' + '--block-device ' + 'source=image,dest=volume,id=%(image)s,size=1,' + 'bootindex=0,tag=bar' % {'name': str(uuid.uuid4()), + 'flavor': self.flavor.id, + 'net-uuid': self.network.id, + 'image': self.image.id})) + server_id = self._get_value_from_the_table(server_info, 'id') + self.client.servers.delete(server_id) + self.wait_for_resource_delete(server_id, self.client.servers) diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index 38bf1a4ba..38a6bd367 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -1217,3 +1217,53 @@ def test_live_migrate_server(self): {'os-migrateLive': {'host': 'hostname', 'block_migration': 'auto', 'force': True}}) + + +class ServersV232Test(ServersV226Test): + def setUp(self): + super(ServersV232Test, self).setUp() + self.cs.api_version = api_versions.APIVersion("2.32") + + def test_create_server_boot_with_tagged_nics(self): + nics = [{'net-id': '11111111-1111-1111-1111-111111111111', + 'tag': 'one'}, + {'net-id': '22222222-2222-2222-2222-222222222222', + 'tag': 'two'}] + self.cs.servers.create(name="Server with tagged nics", + image=1, + flavor=1, + nics=nics) + self.assert_called('POST', '/servers') + + def test_create_server_boot_with_tagged_nics_pre232(self): + self.cs.api_version = api_versions.APIVersion("2.31") + nics = [{'net-id': '11111111-1111-1111-1111-111111111111', + 'tag': 'one'}, + {'net-id': '22222222-2222-2222-2222-222222222222', + 'tag': 'two'}] + self.assertRaises(ValueError, self.cs.servers.create, + name="Server with tagged nics", image=1, flavor=1, + nics=nics) + + def test_create_server_boot_from_volume_tagged_bdm_v2(self): + bdm = [{"volume_size": "1", + "volume_id": "11111111-1111-1111-1111-111111111111", + "delete_on_termination": "0", + "device_name": "vda", "tag": "foo"}] + s = self.cs.servers.create(name="My server", image=1, flavor=1, + meta={'foo': 'bar'}, userdata="hello moto", + key_name="fakekey", + block_device_mapping_v2=bdm) + self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/os-volumes_boot') + + def test_create_server_boot_from_volume_tagged_bdm_v2_pre232(self): + self.cs.api_version = api_versions.APIVersion("2.31") + bdm = [{"volume_size": "1", + "volume_id": "11111111-1111-1111-1111-111111111111", + "delete_on_termination": "0", + "device_name": "vda", "tag": "foo"}] + self.assertRaises(ValueError, self.cs.servers.create, name="My server", + image=1, flavor=1, meta={'foo': 'bar'}, + userdata="hello moto", key_name="fakekey", + block_device_mapping_v2=bdm) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index f0593e443..587b6338f 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -343,6 +343,44 @@ def test_boot_image_bdms_v2(self): }}, ) + def test_boot_image_bdms_v2_with_tag(self): + self.run_command( + 'boot --flavor 1 --image 1 --block-device id=fake-id,' + 'source=volume,dest=volume,device=vda,size=1,format=ext4,' + 'type=disk,shutdown=preserve,tag=foo some-server', + api_version='2.32' + ) + self.assert_called_anytime( + 'POST', '/os-volumes_boot', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'block_device_mapping_v2': [ + { + 'uuid': 1, + 'source_type': 'image', + 'destination_type': 'local', + 'boot_index': 0, + 'delete_on_termination': True, + }, + { + 'uuid': 'fake-id', + 'source_type': 'volume', + 'destination_type': 'volume', + 'device_name': 'vda', + 'volume_size': '1', + 'guest_format': 'ext4', + 'device_type': 'disk', + 'delete_on_termination': False, + 'tag': 'foo', + }, + ], + 'imageRef': '1', + 'min_count': 1, + 'max_count': 1, + }}, + ) + def test_boot_no_image_bdms_v2(self): self.run_command( 'boot --flavor 1 --block-device id=fake-id,source=volume,' @@ -523,6 +561,26 @@ def test_boot_nics(self): }, ) + def test_boot_nics_with_tag(self): + cmd = ('boot --image 1 --flavor 1 ' + '--nic net-id=a=c,v4-fixed-ip=10.0.0.1,tag=foo some-server') + self.run_command(cmd, api_version='2.32') + self.assert_called_anytime( + 'POST', '/servers', + { + 'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': '1', + 'min_count': 1, + 'max_count': 1, + 'networks': [ + {'uuid': 'a=c', 'fixed_ip': '10.0.0.1', 'tag': 'foo'}, + ], + }, + }, + ) + def test_boot_nics_ipv6(self): cmd = ('boot --image 1 --flavor 1 ' '--nic net-id=a=c,v6-fixed-ip=2001:db9:0:1::10 some-server') @@ -2864,6 +2922,8 @@ def test_versions(self): # not explicitly tested via wraps and _SUBSTITUTIONS. 28, # doesn't require any changes in novaclient 31, # doesn't require any changes in novaclient + 32, # doesn't require separate version-wrapped methods in + # novaclient ]) versions_supported = set(range(0, novaclient.API_MAX_VERSION.ver_minor + 1)) diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index 1ddbc721b..ebeaaee30 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -727,6 +727,8 @@ def _boot(self, resource_url, response_key, name, image, flavor, net_data['fixed_ip'] = nic_info['v6-fixed-ip'] if nic_info.get('port-id'): net_data['port'] = nic_info['port-id'] + if nic_info.get('tag'): + net_data['tag'] = nic_info['tag'] all_net_data.append(net_data) body['server']['networks'] = all_net_data @@ -1287,6 +1289,22 @@ def create(self, name, image, flavor, meta=None, files=None, if "description" in kwargs and self.api_version < descr_microversion: raise exceptions.UnsupportedAttribute("description", "2.19") + tags_microversion = api_versions.APIVersion("2.32") + if self.api_version < tags_microversion: + if nics: + for nic_info in nics: + if nic_info.get("tag"): + raise ValueError("Setting interface tags is " + "unsupported before microversion " + "2.32") + + if block_device_mapping_v2: + for bdm in block_device_mapping_v2: + if bdm.get("tag"): + raise ValueError("Setting block device tags is " + "unsupported before microversion " + "2.32") + boot_kwargs = dict( meta=meta, files=files, userdata=userdata, reservation_id=reservation_id, min_count=min_count, diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 7394316cd..6252aa3bc 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -63,6 +63,7 @@ 'bootindex': 'boot_index', 'type': 'device_type', 'shutdown': 'delete_on_termination', + 'tag': 'tag', } @@ -269,10 +270,10 @@ def _boot(cs, args): err_msg = (_("Invalid nic argument '%s'. Nic arguments must be of " "the form --nic , with only one of net-id, net-name " - "or port-id specified.") % nic_str) + "port-id=port-uuid,tag=tag>, with only one of net-id, " + "net-name or port-id specified.") % nic_str) nic_info = {"net-id": "", "v4-fixed-ip": "", "v6-fixed-ip": "", - "port-id": "", "net-name": ""} + "port-id": "", "net-name": "", "tag": ""} for kv_str in nic_str.split(","): try: @@ -438,6 +439,36 @@ def _boot(cs, args): metavar="key1=value1[,key2=value2...]", action='append', default=[], + start_version='2.0', + end_version='2.31', + help=_("Block device mapping with the keys: " + "id=UUID (image_id, snapshot_id or volume_id only if using source " + "image, snapshot or volume) " + "source=source type (image, snapshot, volume or blank), " + "dest=destination type of the block device (volume or local), " + "bus=device's bus (e.g. uml, lxc, virtio, ...; if omitted, " + "hypervisor driver chooses a suitable default, " + "honoured only if device type is supplied) " + "type=device type (e.g. disk, cdrom, ...; defaults to 'disk') " + "device=name of the device (e.g. vda, xda, ...; " + "if omitted, hypervisor driver chooses suitable device " + "depending on selected bus; note the libvirt driver always " + "uses default device names), " + "size=size of the block device in MB(for swap) and in " + "GB(for other formats) " + "(if omitted, hypervisor driver calculates size), " + "format=device will be formatted (e.g. swap, ntfs, ...; optional), " + "bootindex=integer used for ordering the boot disks " + "(for image backed instances it is equal to 0, " + "for others need to be specified) and " + "shutdown=shutdown behaviour (either preserve or remove, " + "for local destination set to remove).")) +@utils.arg( + '--block-device', + metavar="key1=value1[,key2=value2...]", + action='append', + default=[], + start_version='2.32', help=_("Block device mapping with the keys: " "id=UUID (image_id, snapshot_id or volume_id only if using source " "image, snapshot or volume) " @@ -448,6 +479,7 @@ def _boot(cs, args): "honoured only if device type is supplied) " "type=device type (e.g. disk, cdrom, ...; defaults to 'disk') " "device=name of the device (e.g. vda, xda, ...; " + "tag=device metadata tag (optional) " "if omitted, hypervisor driver chooses suitable device " "depending on selected bus; note the libvirt driver always " "uses default device names), " @@ -487,6 +519,8 @@ def _boot(cs, args): action='append', dest='nics', default=[], + start_version='2.0', + end_version='2.31', help=_("Create a NIC on the server. " "Specify option multiple times to create multiple NICs. " "net-id: attach NIC to network with this UUID " @@ -496,6 +530,24 @@ def _boot(cs, args): "v6-fixed-ip: IPv6 fixed address for NIC (optional), " "port-id: attach NIC to port with this UUID " "(either port-id or net-id must be provided).")) +@utils.arg( + '--nic', + metavar="", + action='append', + dest='nics', + default=[], + start_version='2.32', + help=_("Create a NIC on the server. " + "Specify option multiple times to create multiple nics. " + "net-id: attach NIC to network with this UUID " + "net-name: attach NIC to network with this name " + "(either port-id or net-id or net-name must be provided), " + "v4-fixed-ip: IPv4 fixed address for NIC (optional), " + "v6-fixed-ip: IPv6 fixed address for NIC (optional), " + "port-id: attach NIC to port with this UUID " + "tag: interface metadata tag (optional) " + "(either port-id or net-id must be provided).")) @utils.arg( '--config-drive', metavar="", diff --git a/releasenotes/notes/microversion-v2_32-7947430cc2415597.yaml b/releasenotes/notes/microversion-v2_32-7947430cc2415597.yaml new file mode 100644 index 000000000..69d6f5e1f --- /dev/null +++ b/releasenotes/notes/microversion-v2_32-7947430cc2415597.yaml @@ -0,0 +1,25 @@ +--- +features: + - | + The 2.32 microverison adds support for virtual device + role tagging. Device role tagging is an answer to the + question 'Which device is which?' from inside the guest. + When booting an instance, an optional arbitrary 'tag' + parameter can be set on virtual network interfaces + and/or block device mappings. This tag is exposed to the + instance through the metadata API and on the config + drive. Each tagged virtual network interface is listed + along with information about the virtual hardware, such + as bus type (ex: PCI), bus address (ex: 0000:00:02.0), + and MAC address. For tagged block devices, the exposed + hardware metadata includes the bus (ex: SCSI), bus + address (ex: 1:0:2:0) and serial number. + + In the client, device tagging is exposed with the 'tag' + key in the --block-device and --nic boot arguments. +issues: + - | + While not an issue with the client itself, it should be + noted that if a tagged network interface or volume is + detached from a guest, its metadata will continue to + appear in the config drive, even after instance reboot. From 8c1d8fbe816f7650e9b8d201c30cfcbc6c0c2f4e Mon Sep 17 00:00:00 2001 From: Kevin_Zheng Date: Tue, 12 Jul 2016 11:29:53 +0800 Subject: [PATCH 1092/1705] Correctly handle NOVACLIENT_INESCURE Parameter insecure is read using env parameter NOVACLIENT_INSECURE, this could be string, and string is handled as True in the rest code, which is not correct, we should transform it into boolean to handle it correctly. Change-Id: I536763e45b8399b394c7dfa92d327bb4bfaa509f Closes-bug: #1602076 --- novaclient/shell.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/novaclient/shell.py b/novaclient/shell.py index 36b12f7e1..c740ea702 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -395,8 +395,8 @@ def _append_global_identity_args(self, parser, argv): loading.register_auth_argparse_arguments( parser, argv, default=default_auth_plugin) - parser.set_defaults(insecure=utils.env('NOVACLIENT_INSECURE', - default=False)) + parser.set_defaults(insecure=strutils.bool_from_string( + utils.env('NOVACLIENT_INSECURE', default=False))) parser.set_defaults(os_auth_url=utils.env('OS_AUTH_URL', 'NOVA_URL')) parser.set_defaults(os_username=utils.env('OS_USERNAME', From 598df4e67424faaad0f1ab54949191efec0e0ece Mon Sep 17 00:00:00 2001 From: Huanxuan Ao Date: Wed, 13 Jul 2016 10:52:58 +0800 Subject: [PATCH 1093/1705] Add a missing i18n support Add a missing i18n support in v2/shell.py, and also add single quotation marks for "%s" and "key=value" in the message. Change-Id: Iaf642e0b39c2c774803d722a078a9faaa35cefc6 --- novaclient/v2/shell.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 6252aa3bc..d2aca4620 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -42,6 +42,7 @@ from novaclient import client from novaclient import exceptions from novaclient.i18n import _ +from novaclient.i18n import _LE from novaclient import shell from novaclient import utils from novaclient.v2 import availability_zones @@ -80,7 +81,7 @@ def _key_value_pairing(text): (k, v) = text.split('=', 1) return (k, v) except ValueError: - msg = "%r is not in the format of key=value" % text + msg = _LE("'%s' is not in the format of 'key=value'") % text raise argparse.ArgumentTypeError(msg) From 87c1b5311b2e641564305a4753b989b6498775b1 Mon Sep 17 00:00:00 2001 From: Sean Dague Date: Fri, 24 Jun 2016 15:20:16 -0400 Subject: [PATCH 1094/1705] Deprecate baremetal API and CLI interfaces Nova has a very bad read only baremetal proxy, which we will be deprecating and removing. This signals this in our client. Co-Authored-By: Jim Rollenhagen Change-Id: Icf479c29c0c43d91871f49fe4b71266954b0c3f7 --- .../tests/unit/v2/contrib/test_baremetal.py | 13 ++++-- novaclient/v2/contrib/baremetal.py | 40 +++++++++++++++---- .../deprecate-baremetal-d67f58a2986b3565.yaml | 13 ++++++ 3 files changed, 55 insertions(+), 11 deletions(-) create mode 100644 releasenotes/notes/deprecate-baremetal-d67f58a2986b3565.yaml diff --git a/novaclient/tests/unit/v2/contrib/test_baremetal.py b/novaclient/tests/unit/v2/contrib/test_baremetal.py index fdcd4f5fb..c892acb99 100644 --- a/novaclient/tests/unit/v2/contrib/test_baremetal.py +++ b/novaclient/tests/unit/v2/contrib/test_baremetal.py @@ -14,12 +14,16 @@ # under the License. +import mock +import warnings + from novaclient import extension from novaclient.tests.unit import utils from novaclient.tests.unit.v2.contrib import fakes from novaclient.v2.contrib import baremetal +@mock.patch.object(warnings, 'warn') class BaremetalExtensionTest(utils.TestCase): def setUp(self): super(BaremetalExtensionTest, self).setUp() @@ -28,20 +32,23 @@ def setUp(self): ] self.cs = fakes.FakeClient(extensions=extensions) - def test_list_nodes(self): + def test_list_nodes(self, mock_warn): nl = self.cs.baremetal.list() self.assert_request_id(nl, fakes.FAKE_REQUEST_ID_LIST) self.cs.assert_called('GET', '/os-baremetal-nodes') for n in nl: self.assertIsInstance(n, baremetal.BareMetalNode) + self.assertEqual(1, mock_warn.call_count) - def test_get_node(self): + def test_get_node(self, mock_warn): n = self.cs.baremetal.get(1) self.assert_request_id(n, fakes.FAKE_REQUEST_ID_LIST) self.cs.assert_called('GET', '/os-baremetal-nodes/1') self.assertIsInstance(n, baremetal.BareMetalNode) + self.assertEqual(1, mock_warn.call_count) - def test_node_list_interfaces(self): + def test_node_list_interfaces(self, mock_warn): il = self.cs.baremetal.list_interfaces(1) self.assert_request_id(il, fakes.FAKE_REQUEST_ID_LIST) self.cs.assert_called('GET', '/os-baremetal-nodes/1') + self.assertEqual(1, mock_warn.call_count) diff --git a/novaclient/v2/contrib/baremetal.py b/novaclient/v2/contrib/baremetal.py index 4307d6cc1..7f0138816 100644 --- a/novaclient/v2/contrib/baremetal.py +++ b/novaclient/v2/contrib/baremetal.py @@ -17,14 +17,32 @@ Baremetal interface (v2 extension). """ +from __future__ import print_function + +import sys +import warnings + from novaclient import base from novaclient.i18n import _ from novaclient import utils +DEPRECATION_WARNING = ( + 'The novaclient.v2.contrib.baremetal module is deprecated and ' + 'will be removed after Nova 15.0.0 is released. Use ' + 'python-ironicclient or openstacksdk instead.') + + +def _emit_deprecation_warning(command_name): + print('WARNING: Command %s is deprecated and will be removed after Nova ' + '15.0.0 is released. Use python-ironicclient or ' + 'python-openstackclient instead.' % command_name, file=sys.stderr) + + class BareMetalNode(base.Resource): """ - A baremetal node (typically a physical server or an empty VM). + DEPRECATED: A baremetal node (typically a physical server or an + empty VM). """ def __repr__(self): @@ -42,34 +60,37 @@ def __repr__(self): class BareMetalNodeManager(base.ManagerWithFind): """ - Manage :class:`BareMetalNode` resources. + DEPRECATED: Manage :class:`BareMetalNode` resources. """ resource_class = BareMetalNode def get(self, node_id): """ - Get a baremetal node. + DEPRECATED: Get a baremetal node. :param node_id: The ID of the node to delete. :rtype: :class:`BareMetalNode` """ + warnings.warn(DEPRECATION_WARNING, DeprecationWarning) return self._get("/os-baremetal-nodes/%s" % node_id, 'node') def list(self): """ - Get a list of all baremetal nodes. + DEPRECATED: Get a list of all baremetal nodes. :rtype: list of :class:`BareMetalNode` """ + warnings.warn(DEPRECATION_WARNING, DeprecationWarning) return self._list('/os-baremetal-nodes', 'nodes') def list_interfaces(self, node_id): """ - List the interfaces on a baremetal node. + DEPRECATED: List the interfaces on a baremetal node. :param node_id: The ID of the node to list. :rtype: novaclient.base.ListWithMeta """ + warnings.warn(DEPRECATION_WARNING, DeprecationWarning) interfaces = base.ListWithMeta([], None) node = self._get("/os-baremetal-nodes/%s" % node_id, 'node') interfaces.append_request_ids(node.request_ids) @@ -125,7 +146,8 @@ def _parse_address(fields): def do_baremetal_node_list(cs, _args): - """Print list of available baremetal nodes.""" + """DEPRECATED: Print list of available baremetal nodes.""" + _emit_deprecation_warning('baremetal-node-list') nodes = cs.baremetal.list() _print_baremetal_nodes_list(nodes) @@ -156,13 +178,15 @@ def _print_baremetal_node_interfaces(interfaces): metavar='', help=_("ID of node")) def do_baremetal_node_show(cs, args): - """Show information about a baremetal node.""" + """DEPRECATED: Show information about a baremetal node.""" + _emit_deprecation_warning('baremetal-node-show') node = _find_baremetal_node(cs, args.node) _print_baremetal_resource(node) @utils.arg('node', metavar='', help=_("ID of node")) def do_baremetal_interface_list(cs, args): - """List network interfaces associated with a baremetal node.""" + """DEPRECATED: List network interfaces associated with a baremetal node.""" + _emit_deprecation_warning('baremetal-interface-list') interfaces = cs.baremetal.list_interfaces(args.node) _print_baremetal_node_interfaces(interfaces) diff --git a/releasenotes/notes/deprecate-baremetal-d67f58a2986b3565.yaml b/releasenotes/notes/deprecate-baremetal-d67f58a2986b3565.yaml new file mode 100644 index 000000000..123334679 --- /dev/null +++ b/releasenotes/notes/deprecate-baremetal-d67f58a2986b3565.yaml @@ -0,0 +1,13 @@ +--- +deprecations: + - | + The following CLIs and python API bindings are now deprecated for removal: + + * nova baremetal-node-list + * nova baremetal-node-show + * nova baremetal-interface-list + + These will be removed in the first major python-novaclient release after + the Nova 15.0.0 Ocata release. Use python-ironicclient or + python-openstackclient for CLI and python-ironicclient or openstacksdk + for python API bindings. From 6edb7517e3c4123c25e1a3e32bc6621fcfbaa496 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Mon, 18 Jul 2016 16:45:48 -0400 Subject: [PATCH 1095/1705] Fix deprecation message for --volume-service-name We just released novaclient 5.0.0 so this fixes the deprecation message for the --volume-service-name option, not only for the version but also the timeline for removal (to be consistent with other things we'll remove like image and baremetal proxies). Change-Id: I138f0ff05861a18b74cff650ea72f38d80a247fb --- novaclient/shell.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/novaclient/shell.py b/novaclient/shell.py index c740ea702..1f277485d 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -477,7 +477,8 @@ def get_base_parser(self, argv): action=DeprecatedAction, metavar='', default=utils.env('NOVA_VOLUME_SERVICE_NAME'), - use=_('This option will be removed in novaclient 4.3.0.'), + use=_('This option will be removed after Nova 15.0.0 is ' + 'released.'), help=argparse.SUPPRESS) parser.add_argument( From 85ef6f46fa6e56ef6fdbe052f6c429ef0e0493b3 Mon Sep 17 00:00:00 2001 From: EdLeafe Date: Thu, 10 Mar 2016 18:12:23 +0000 Subject: [PATCH 1096/1705] Change all test URLs to use example.com The tests in novaclient/tests/unit/test_client.py use several domain names other than example.com, which should always be used to avoid conflict with actual domains. These include foo.com, blah.com, and nooooooooo.com, all of which are legitimate owned domain names. These have been replaced with example.com usages. Closes-bug 1555682 Change-Id: I6573e118364300cd09653b87df28ba1a95870d3c --- novaclient/tests/unit/test_client.py | 47 ++++++++++++++++------------ 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/novaclient/tests/unit/test_client.py b/novaclient/tests/unit/test_client.py index 65b421b0f..bf89a00e6 100644 --- a/novaclient/tests/unit/test_client.py +++ b/novaclient/tests/unit/test_client.py @@ -42,17 +42,18 @@ def test_get(self, mock_http_adapter): class ClientTest(utils.TestCase): def test_client_with_timeout(self): + auth_url = "http://example.com" instance = novaclient.client.HTTPClient(user='user', password='password', projectid='project', timeout=2, - auth_url="http://www.blah.com") + auth_url=auth_url) self.assertEqual(2, instance.timeout) mock_request = mock.Mock() mock_request.return_value = requests.Response() mock_request.return_value.status_code = 200 mock_request.return_value.headers = { - 'x-server-management-url': 'blah.com', + 'x-server-management-url': 'example.com', 'x-auth-token': 'blah', } with mock.patch('requests.request', mock_request): @@ -62,14 +63,16 @@ def test_client_with_timeout(self): verify=mock.ANY) def test_client_reauth(self): + auth_url = "http://www.example.com" instance = novaclient.client.HTTPClient(user='user', password='password', projectid='project', timeout=2, - auth_url="http://www.blah.com") + auth_url=auth_url) instance.auth_token = 'foobar' - instance.management_url = 'http://example.com' - instance.get_service_url = mock.Mock(return_value='http://example.com') + mgmt_url = "http://mgmt.example.com" + instance.management_url = mgmt_url + instance.get_service_url = mock.Mock(return_value=mgmt_url) instance.version = 'v2.0' mock_request = mock.Mock() mock_request.side_effect = novaclient.exceptions.Unauthorized(401) @@ -96,11 +99,11 @@ def test_client_reauth(self): } expected = [mock.call('GET', - 'http://example.com/servers/detail', + 'http://mgmt.example.com/servers/detail', timeout=mock.ANY, headers=get_headers, verify=mock.ANY), - mock.call('POST', 'http://www.blah.com/tokens', + mock.call('POST', 'http://www.example.com/tokens', timeout=mock.ANY, headers=reauth_headers, allow_redirects=mock.ANY, @@ -114,10 +117,11 @@ def test_client_reauth(self): return_value=(200, "{'versions':[]}")) def _check_version_url(self, management_url, version_url, mock_request): projectid = '25e469aa1848471b875e68cde6531bc5' + auth_url = "http://example.com" instance = novaclient.client.HTTPClient(user='user', password='password', projectid=projectid, - auth_url="http://www.blah.com") + auth_url=auth_url) instance.auth_token = 'foobar' instance.management_url = management_url % projectid mock_get_service_url = mock.Mock(return_value=instance.management_url) @@ -137,17 +141,20 @@ def _check_version_url(self, management_url, version_url, mock_request): mock_request.assert_called_once_with(url, 'GET', headers=mock.ANY) def test_client_version_url(self): - self._check_version_url('http://foo.com/v2/%s', 'http://foo.com/') - self._check_version_url('http://foo.com/v2.1/%s', 'http://foo.com/') - self._check_version_url('http://foo.com/v3.785/%s', 'http://foo.com/') + self._check_version_url('http://example.com/v2/%s', + 'http://example.com/') + self._check_version_url('http://example.com/v2.1/%s', + 'http://example.com/') + self._check_version_url('http://example.com/v3.785/%s', + 'http://example.com/') def test_client_version_url_with_project_name(self): - self._check_version_url('http://foo.com/nova/v2/%s', - 'http://foo.com/nova/') - self._check_version_url('http://foo.com/nova/v2.1/%s', - 'http://foo.com/nova/') - self._check_version_url('http://foo.com/nova/v3.785/%s', - 'http://foo.com/nova/') + self._check_version_url('http://example.com/nova/v2/%s', + 'http://example.com/nova/') + self._check_version_url('http://example.com/nova/v2.1/%s', + 'http://example.com/nova/') + self._check_version_url('http://example.com/nova/v3.785/%s', + 'http://example.com/nova/') def test_get_client_class_v2(self): output = novaclient.client.get_client_class('2') @@ -315,7 +322,7 @@ def test_session_connection_pool(self): def test_get_session(self): cs = novaclient.client.HTTPClient("user", None, "", "") - self.assertIsNone(cs._get_session("http://nooooooooo.com")) + self.assertIsNone(cs._get_session("http://example.com")) @mock.patch("novaclient.client.requests.Session") def test_get_session_open_session(self, mock_session): @@ -328,14 +335,14 @@ def test_get_session_open_session(self, mock_session): @mock.patch("novaclient.client.requests.Session") @mock.patch("novaclient.client._ClientConnectionPool") def test_get_session_connection_pool(self, mock_pool, mock_session): - service_url = "http://example.com" + service_url = "http://service.example.com" pool = mock.MagicMock() pool.get.return_value = "http_adapter" mock_pool.return_value = pool cs = novaclient.client.HTTPClient("user", None, "", "", connection_pool=True) - cs._current_url = "http://another.com" + cs._current_url = "http://current.example.com" session = cs._get_session(service_url) self.assertEqual(session, mock_session.return_value) From 6b11a1c46b46208cdeab35d1b7b91e3286fc7fd5 Mon Sep 17 00:00:00 2001 From: "Swapnil Kulkarni (coolsvap)" Date: Fri, 22 Jul 2016 04:07:56 +0000 Subject: [PATCH 1097/1705] Remove discover from test-requirements It's only needed for python < 2.7 which is not supported Change-Id: Ia292b0757a9ea5ad329657a3c9cdc90dfc2d4bfe --- test-requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index b63d28f61..e606e457a 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -5,7 +5,6 @@ hacking<0.11,>=0.10.0 bandit>=1.0.1 # Apache-2.0 coverage>=3.6 # Apache-2.0 -discover # BSD fixtures>=3.0.0 # Apache-2.0/BSD keyring>=5.5.1 # MIT/PSF mock>=2.0 # BSD From 2bdccb16c5256b34c3580215d4bb18dc3c59a389 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 29 Jul 2016 02:34:54 +0000 Subject: [PATCH 1098/1705] Updated from global requirements Change-Id: Id0640bd34bd7c792b72c8c837c6a3fd8926eea13 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c2b23f281..894f2525b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ keystoneauth1>=2.7.0 # Apache-2.0 iso8601>=0.1.11 # MIT oslo.i18n>=2.1.0 # Apache-2.0 oslo.serialization>=1.10.0 # Apache-2.0 -oslo.utils>=3.15.0 # Apache-2.0 +oslo.utils>=3.16.0 # Apache-2.0 PrettyTable<0.8,>=0.7 # BSD requests>=2.10.0 # Apache-2.0 simplejson>=2.2.0 # MIT From 48da89e2a297e44125cb88aa284cf99bf87c2b1f Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Mon, 1 Aug 2016 18:47:30 +0000 Subject: [PATCH 1099/1705] Updated from global requirements Change-Id: I6146320ea4e325b92c03f3822846bb0e4beae675 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 894f2525b..e9fd28fb9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. pbr>=1.6 # Apache-2.0 -keystoneauth1>=2.7.0 # Apache-2.0 +keystoneauth1>=2.10.0 # Apache-2.0 iso8601>=0.1.11 # MIT oslo.i18n>=2.1.0 # Apache-2.0 oslo.serialization>=1.10.0 # Apache-2.0 From adbfdd0aa266696e7b80dfc482430fe2d4563e58 Mon Sep 17 00:00:00 2001 From: Clenimar Filemon Date: Tue, 26 Jul 2016 18:11:59 -0300 Subject: [PATCH 1100/1705] Refactor test_servers APIVersion setup Set APIVersion in the base test class instead of in every derived test class. Change-Id: Ifbc6af6f1e42033c9ad8923cb063d345e520fb00 Co-Authored-By: Andrey Kurilin --- novaclient/tests/unit/v2/test_servers.py | 53 ++++++++++-------------- 1 file changed, 23 insertions(+), 30 deletions(-) diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index 38a6bd367..06d993092 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -34,10 +34,13 @@ class ServersTest(utils.FixturedTestCase): client_fixture_class = client.V1 data_fixture_class = data.V1 + api_version = None def setUp(self): super(ServersTest, self).setUp() self.useFixture(floatingips.FloatingFixture(self.requests)) + if self.api_version: + self.cs.api_version = api_versions.APIVersion(self.api_version) def test_list_servers(self): sl = self.cs.servers.list() @@ -975,9 +978,8 @@ def test_create_server_with_description(self): class ServersV26Test(ServersTest): - def setUp(self): - super(ServersV26Test, self).setUp() - self.cs.api_version = api_versions.APIVersion("2.6") + + api_version = "2.6" def test_get_vnc_console(self): s = self.cs.servers.get(1234) @@ -1021,9 +1023,8 @@ def test_get_rdp_console(self): class ServersV28Test(ServersV26Test): - def setUp(self): - super(ServersV28Test, self).setUp() - self.cs.api_version = api_versions.APIVersion("2.8") + + api_version = "2.8" def test_get_mks_console(self): s = self.cs.servers.get(1234) @@ -1037,9 +1038,8 @@ def test_get_mks_console(self): class ServersV214Test(ServersV28Test): - def setUp(self): - super(ServersV214Test, self).setUp() - self.cs.api_version = api_versions.APIVersion("2.14") + + api_version = "2.14" def test_evacuate(self): s = self.cs.servers.get(1234) @@ -1051,9 +1051,8 @@ def test_evacuate(self): class ServersV217Test(ServersV214Test): - def setUp(self): - super(ServersV217Test, self).setUp() - self.cs.api_version = api_versions.APIVersion("2.17") + + api_version = "2.17" def test_trigger_crash_dump(self): s = self.cs.servers.get(1234) @@ -1064,9 +1063,8 @@ def test_trigger_crash_dump(self): class ServersV219Test(ServersV217Test): - def setUp(self): - super(ServersV219Test, self).setUp() - self.cs.api_version = api_versions.APIVersion("2.19") + + api_version = "2.19" def test_create_server_with_description(self): self.cs.servers.create( @@ -1096,9 +1094,8 @@ def test_rebuild_with_description(self): class ServersV225Test(ServersV219Test): - def setUp(self): - super(ServersV219Test, self).setUp() - self.cs.api_version = api_versions.APIVersion("2.25") + + api_version = "2.25" def test_live_migrate_server(self): s = self.cs.servers.get(1234) @@ -1146,9 +1143,8 @@ def test_live_migrate_server_block_migration_none(self): class ServersV226Test(ServersV225Test): - def setUp(self): - super(ServersV219Test, self).setUp() - self.cs.api_version = api_versions.APIVersion("2.26") + + api_version = "2.26" def test_tag_list(self): s = self.cs.servers.get(1234) @@ -1182,9 +1178,8 @@ def test_tags_set(self): class ServersV229Test(ServersV226Test): - def setUp(self): - super(ServersV229Test, self).setUp() - self.cs.api_version = api_versions.APIVersion("2.29") + + api_version = "2.29" def test_evacuate(self): s = self.cs.servers.get(1234) @@ -1198,9 +1193,8 @@ def test_evacuate(self): class ServersV230Test(ServersV229Test): - def setUp(self): - super(ServersV230Test, self).setUp() - self.cs.api_version = api_versions.APIVersion("2.30") + + api_version = "2.30" def test_live_migrate_server(self): s = self.cs.servers.get(1234) @@ -1220,9 +1214,8 @@ def test_live_migrate_server(self): class ServersV232Test(ServersV226Test): - def setUp(self): - super(ServersV232Test, self).setUp() - self.cs.api_version = api_versions.APIVersion("2.32") + + api_version = "2.32" def test_create_server_boot_with_tagged_nics(self): nics = [{'net-id': '11111111-1111-1111-1111-111111111111', From 454350ff6d4351c573addb93377f9794d26c7c79 Mon Sep 17 00:00:00 2001 From: Jens Rosenboom Date: Tue, 19 Jul 2016 12:10:26 +0200 Subject: [PATCH 1101/1705] Fix python35 job failures - Installation of the cryptography module fails on Ubuntu Xenial because the libssl-dev package needs to be installed first, so we add this to other-requirements.txt - inspect.getargspec()` was deprecated in Python 3.0 and will be removed in 3.6 (ETA late 2016). From Python 3.5 it started throwing a deprecation warning which leds some tests failures. Co-Authored-By: Andrey Kurilin Change-Id: Ic094ca5c636af9ac1e212914df910a020d92702d --- novaclient/base.py | 10 +++++----- other-requirements.txt | 1 + 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/novaclient/base.py b/novaclient/base.py index 6ef271684..14485e5e1 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -23,10 +23,10 @@ import contextlib import copy import hashlib -import inspect import os import threading +from oslo_utils import reflection from oslo_utils import strutils from requests import Response import six @@ -407,14 +407,14 @@ def findall(self, **kwargs): detailed = True list_kwargs = {} - list_argspec = inspect.getargspec(self.list) - if 'detailed' in list_argspec.args: + list_argspec = reflection.get_callable_args(self.list) + if 'detailed' in list_argspec: detailed = ("human_id" not in kwargs and "name" not in kwargs and "display_name" not in kwargs) list_kwargs['detailed'] = detailed - if 'is_public' in list_argspec.args and 'is_public' in kwargs: + if 'is_public' in list_argspec and 'is_public' in kwargs: is_public = kwargs['is_public'] list_kwargs['is_public'] = is_public if is_public is None: @@ -422,7 +422,7 @@ def findall(self, **kwargs): del tmp_kwargs['is_public'] searches = tmp_kwargs.items() - if 'search_opts' in list_argspec.args: + if 'search_opts' in list_argspec: # pass search_opts in to do server side based filtering. # TODO(jogo) not all search_opts support regex, find way to # identify when to use regex and when to use string matching. diff --git a/other-requirements.txt b/other-requirements.txt index da3ab7f54..5c2a93afa 100644 --- a/other-requirements.txt +++ b/other-requirements.txt @@ -10,6 +10,7 @@ libdbus-1-dev [platform:dpkg] libdbus-glib-1-dev [platform:dpkg] libffi-dev [platform:dpkg] libffi-devel [platform:rpm] +libssl-dev [platform:ubuntu-xenial] libuuid-devel [platform:rpm] locales [platform:debian] python-dev [platform:dpkg] From 6bbcedb0001df2312cbf39ba8090e780f9e0c884 Mon Sep 17 00:00:00 2001 From: Andrey Volkov Date: Mon, 1 Aug 2016 17:38:14 +0300 Subject: [PATCH 1102/1705] Add support for microversion 2.33 This change allows novaclient to get several hypervisors with the help of new optional parameters 'limit' and 'marker' which were added to hipervisor-list command. Change-Id: Ib723fb1dc1cd57b6f796bb93e0dd8ddf2da4b2a0 --- novaclient/__init__.py | 2 +- novaclient/tests/unit/test_utils.py | 20 +++++++ novaclient/tests/unit/v2/test_hypervisors.py | 14 +++++ novaclient/tests/unit/v2/test_shell.py | 5 ++ novaclient/utils.py | 7 +++ novaclient/v2/hypervisors.py | 31 +++++++++-- novaclient/v2/shell.py | 54 ++++++++++++++++--- .../microversion-v2_33-10d12ea3b25839e8.yaml | 5 ++ 8 files changed, 126 insertions(+), 12 deletions(-) create mode 100644 releasenotes/notes/microversion-v2_33-10d12ea3b25839e8.yaml diff --git a/novaclient/__init__.py b/novaclient/__init__.py index e13263741..ad6d401ec 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.32") +API_MAX_VERSION = api_versions.APIVersion("2.33") diff --git a/novaclient/tests/unit/test_utils.py b/novaclient/tests/unit/test_utils.py index f3d88c21e..64a947ea5 100644 --- a/novaclient/tests/unit/test_utils.py +++ b/novaclient/tests/unit/test_utils.py @@ -16,6 +16,7 @@ import mock from oslo_utils import encodeutils import six +from six.moves.urllib import parse from novaclient import base from novaclient import exceptions @@ -437,3 +438,22 @@ def test_record_time(self): with utils.record_time(times, False, 'x'): pass self.assertEqual(0, len(times)) + + +class PrepareQueryStringTestCase(test_utils.TestCase): + def test_convert_dict_to_string(self): + ustr = b'?\xd0\xbf=1&\xd1\x80=2' + if six.PY3: + # in py3 real unicode symbols will be urlencoded + ustr = ustr.decode('utf8') + cases = ( + ({}, ''), + ({'2': 2, '10': 1}, '?10=1&2=2'), + ({'abc': 1, 'abc1': 2}, '?abc=1&abc1=2'), + ({b'\xd0\xbf': 1, b'\xd1\x80': 2}, ustr), + ({(1, 2): '1', (3, 4): '2'}, '?(1, 2)=1&(3, 4)=2') + ) + for case in cases: + self.assertEqual( + case[1], + parse.unquote_plus(utils.prepare_query_string(case[0]))) diff --git a/novaclient/tests/unit/v2/test_hypervisors.py b/novaclient/tests/unit/v2/test_hypervisors.py index a522b8537..d395eee85 100644 --- a/novaclient/tests/unit/v2/test_hypervisors.py +++ b/novaclient/tests/unit/v2/test_hypervisors.py @@ -13,6 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient import api_versions from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit.fixture_data import hypervisors as data from novaclient.tests.unit import utils @@ -185,3 +186,16 @@ def test_hypervisor_statistics_data_model(self): # Test for Bug #1370415, the line below used to raise AttributeError self.assertEqual("", result.__repr__()) + + +class HypervisorsV233Test(HypervisorsTest): + def setUp(self): + super(HypervisorsV233Test, self).setUp() + self.cs.api_version = api_versions.APIVersion("2.33") + + def test_use_limit_marker_params(self): + params = {'limit': 10, 'marker': 'fake-marker'} + self.cs.hypervisors.list(**params) + for k, v in params.items(): + self.assertIn('%s=%s' % (k, v), + self.requests.last_request.path_url) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 587b6338f..eda8d101c 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2154,6 +2154,11 @@ def test_hypervisor_list_matching(self): self.run_command('hypervisor-list --matching hyper') self.assert_called('GET', '/os-hypervisors/hyper/search') + def test_hypervisor_list_limit_marker(self): + self.run_command('hypervisor-list --limit 10 --marker hyper1', + api_version='2.33') + self.assert_called('GET', '/os-hypervisors?limit=10&marker=hyper1') + def test_hypervisor_servers(self): self.run_command('hypervisor-servers hyper') self.assert_called('GET', '/os-hypervisors/hyper/servers') diff --git a/novaclient/utils.py b/novaclient/utils.py index 0170c9524..d7573758a 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -24,6 +24,7 @@ import pkg_resources import prettytable import six +from six.moves.urllib import parse from novaclient import exceptions from novaclient.i18n import _ @@ -465,3 +466,9 @@ def record_time(times, enabled, *args): yield end = time.time() times.append((' '.join(args), start, end)) + + +def prepare_query_string(params): + """Convert dict params to query string""" + params = sorted(params.items(), key=lambda x: x[0]) + return '?%s' % parse.urlencode(params) if params else '' diff --git a/novaclient/v2/hypervisors.py b/novaclient/v2/hypervisors.py index 684a7fb34..8169b7613 100644 --- a/novaclient/v2/hypervisors.py +++ b/novaclient/v2/hypervisors.py @@ -19,7 +19,9 @@ from six.moves.urllib import parse +from novaclient import api_versions from novaclient import base +from novaclient import utils class Hypervisor(base.Resource): @@ -33,14 +35,35 @@ class HypervisorManager(base.ManagerWithFind): resource_class = Hypervisor is_alphanum_id_allowed = True + def _list_base(self, detailed=True, marker=None, limit=None): + path = '/os-hypervisors' + if detailed: + path += '/detail' + params = {} + if limit is not None: + params['limit'] = int(limit) + if marker is not None: + params['marker'] = str(marker) + path += utils.prepare_query_string(params) + return self._list(path, 'hypervisors') + + @api_versions.wraps("2.0", "2.32") def list(self, detailed=True): """ Get a list of hypervisors. """ - detail = "" - if detailed: - detail = "/detail" - return self._list('/os-hypervisors%s' % detail, 'hypervisors') + return self._list_base(detailed=detailed) + + @api_versions.wraps("2.33") + def list(self, detailed=True, marker=None, limit=None): + """ + Get a list of hypervisors. + :param marker: Begin returning hypervisor that appear later in the + keypair list than that represented by this keypair name + (optional). + :param limit: maximum number of keypairs to return (optional). + """ + return self._list_base(detailed=detailed, marker=marker, limit=limit) def search(self, hypervisor_match, servers=False): """ diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index d2aca4620..5d6a09023 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -3980,6 +3980,22 @@ def _find_hypervisor(cs, hypervisor): return utils.find_resource(cs.hypervisors, hypervisor) +def _do_hypervisor_list(cs, matching=None, limit=None, marker=None): + columns = ['ID', 'Hypervisor hostname', 'State', 'Status'] + if matching: + utils.print_list(cs.hypervisors.search(matching), columns) + else: + params = {} + if limit is not None: + params['limit'] = limit + if marker is not None: + params['marker'] = marker + # Since we're not outputting detail data, choose + # detailed=False for server-side efficiency + utils.print_list(cs.hypervisors.list(False, **params), columns) + + +@api_versions.wraps("2.0", "2.32") @utils.arg( '--matching', metavar='', @@ -3987,13 +4003,37 @@ def _find_hypervisor(cs, hypervisor): help=_('List hypervisors matching the given .')) def do_hypervisor_list(cs, args): """List hypervisors.""" - columns = ['ID', 'Hypervisor hostname', 'State', 'Status'] - if args.matching: - utils.print_list(cs.hypervisors.search(args.matching), columns) - else: - # Since we're not outputting detail data, choose - # detailed=False for server-side efficiency - utils.print_list(cs.hypervisors.list(False), columns) + _do_hypervisor_list(cs, matching=args.matching) + + +@api_versions.wraps("2.33") +@utils.arg( + '--matching', + metavar='', + default=None, + help=_('List hypervisors matching the given . ' + 'If matching is used limit and marker options will be ignored.')) +@utils.arg( + '--marker', + dest='marker', + metavar='', + default=None, + help=_('The last hypervisor of the previous page; displays list of ' + 'hypervisors after "marker".')) +@utils.arg( + '--limit', + dest='limit', + metavar='', + type=int, + default=None, + help=_("Maximum number of hypervisors to display. If limit == -1, all " + "hypervisors will be displayed. If limit is bigger than " + "'osapi_max_limit' option of Nova API, limit 'osapi_max_limit' " + "will be used instead.")) +def do_hypervisor_list(cs, args): + """List hypervisors.""" + _do_hypervisor_list( + cs, matching=args.matching, limit=args.limit, marker=args.marker) @utils.arg( diff --git a/releasenotes/notes/microversion-v2_33-10d12ea3b25839e8.yaml b/releasenotes/notes/microversion-v2_33-10d12ea3b25839e8.yaml new file mode 100644 index 000000000..0376b1455 --- /dev/null +++ b/releasenotes/notes/microversion-v2_33-10d12ea3b25839e8.yaml @@ -0,0 +1,5 @@ +--- +features: + - Added microversion v2.33 that adds pagination support for hypervisors with + the help of new optional parameters 'limit' and 'marker' which were added + to hypervisor-list command. \ No newline at end of file From e78cc205143f739d14476d61b2bffa7d78eca30d Mon Sep 17 00:00:00 2001 From: Timofey Durakov Date: Mon, 4 Jul 2016 12:15:12 +0300 Subject: [PATCH 1103/1705] Added support for microversion 2.34 microversion support added Change-Id: I5743ad049c413e3ebe3ef816ee1ad2c26cc5cc2d bp: async-live-migration-rest-check Depends-On: #I9fed3079d0f1b7de3ad1b3ecd309c93785fd11fe --- novaclient/__init__.py | 2 +- novaclient/tests/unit/v2/test_shell.py | 1 + releasenotes/notes/microversion-v2_34-a9c5601811152964.yaml | 3 +++ 3 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/microversion-v2_34-a9c5601811152964.yaml diff --git a/novaclient/__init__.py b/novaclient/__init__.py index ad6d401ec..76a83fa75 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.33") +API_MAX_VERSION = api_versions.APIVersion("2.34") diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index eda8d101c..4fcd295b4 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2929,6 +2929,7 @@ def test_versions(self): 31, # doesn't require any changes in novaclient 32, # doesn't require separate version-wrapped methods in # novaclient + 34, # doesn't require any changes in novaclient ]) versions_supported = set(range(0, novaclient.API_MAX_VERSION.ver_minor + 1)) diff --git a/releasenotes/notes/microversion-v2_34-a9c5601811152964.yaml b/releasenotes/notes/microversion-v2_34-a9c5601811152964.yaml new file mode 100644 index 000000000..284b01eb4 --- /dev/null +++ b/releasenotes/notes/microversion-v2_34-a9c5601811152964.yaml @@ -0,0 +1,3 @@ +--- +upgrade: + - Support for microversion 2.34 added. \ No newline at end of file From 5694a9a0f292d68ac56eb294aae770db789d4e05 Mon Sep 17 00:00:00 2001 From: Pavel Kholkin Date: Thu, 4 Aug 2016 15:48:51 +0300 Subject: [PATCH 1104/1705] remove start_version arg for keypairs v2.10 shell start_version is redundant because of decorator @api_versions.wraps("2.10") on the whole function TrivialFix Change-Id: Ib468214490e817a28640a02fb629c762e14828cb --- novaclient/v2/shell.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 5d6a09023..d873740b4 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -3095,8 +3095,7 @@ def do_keypair_delete(cs, args): '--user', metavar='', default=None, - help=_('ID of key-pair owner (Admin only).'), - start_version="2.10") + help=_('ID of key-pair owner (Admin only).')) def do_keypair_delete(cs, args): """Delete keypair given by its name.""" cs.keypairs.delete(args.name, args.user) @@ -3125,8 +3124,7 @@ def do_keypair_list(cs, args): '--user', metavar='', default=None, - help=_('List key-pairs of specified user ID (Admin only).'), - start_version="2.10") + help=_('List key-pairs of specified user ID (Admin only).')) def do_keypair_list(cs, args): """Print a list of keypairs for a user""" keypairs = cs.keypairs.list(args.user) @@ -3161,8 +3159,7 @@ def do_keypair_show(cs, args): '--user', metavar='', default=None, - help=_('ID of key-pair owner (Admin only).'), - start_version="2.10") + help=_('ID of key-pair owner (Admin only).')) def do_keypair_show(cs, args): """Show details about the given keypair.""" keypair = cs.keypairs.get(args.keypair, args.user) From fae24eee4af6044c712d799ff09442b61cf6dab2 Mon Sep 17 00:00:00 2001 From: Hironori Shiina Date: Fri, 5 Aug 2016 02:08:36 +0900 Subject: [PATCH 1105/1705] Modify flatten method to display an empty dict When a dict is flattened, if a value of an item is empty, e.g. {"cpu_info": {}}, the item is not displayed. This patch fixes _flatten method to display such an item including an empty dict. Change-Id: I2e4ada0de217c5c4ddea10f3f283c6d8e78083b6 Related-Bug: #1594230 --- novaclient/tests/unit/test_utils.py | 6 ++++-- novaclient/utils.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/novaclient/tests/unit/test_utils.py b/novaclient/tests/unit/test_utils.py index 64a947ea5..919d4cb2b 100644 --- a/novaclient/tests/unit/test_utils.py +++ b/novaclient/tests/unit/test_utils.py @@ -340,7 +340,8 @@ def test_flattening(self): 'b4': {'c1': ['l', 'l', ['l']], 'c2': 'string'}}, 'a2': ['l'], - 'a3': ('t',)}) + 'a3': ('t',), + 'a4': {}}) self.assertEqual({'a1_b1': 1234, 'a1_b2': 'string', @@ -348,7 +349,8 @@ def test_flattening(self): 'a1_b4_c1': ['l', 'l', ['l']], 'a1_b4_c2': 'string', 'a2': ['l'], - 'a3': ('t',)}, + 'a3': ('t',), + 'a4': {}}, squashed) def test_pretty_choice_dict(self): diff --git a/novaclient/utils.py b/novaclient/utils.py index d7573758a..093bc9619 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -210,7 +210,7 @@ def _flatten(data, prefix=None): if isinstance(data, dict): for key, value in six.iteritems(data): new_key = '%s_%s' % (prefix, key) if prefix else key - if isinstance(value, (dict, list)): + if isinstance(value, (dict, list)) and value: for item in _flatten(value, new_key): yield item else: From d166968eac2f45a7b769e24ddce603047a9abfb1 Mon Sep 17 00:00:00 2001 From: Pavel Kholkin Date: Mon, 18 Jul 2016 15:26:40 +0300 Subject: [PATCH 1106/1705] Microversion 2.35 adds keypairs pagination support After this microversion novaclient allows to get several keypairs with the help of new optional parameters 'limit' and 'marker' which were added to keypair-list command. Implements blueprint: keypairs-pagination Change-Id: I659d9528cbf64481bd2b3f99008a57287404854e --- novaclient/__init__.py | 2 +- .../tests/functional/v2/test_keypairs.py | 27 ++++++++++++++++ novaclient/tests/unit/v2/fakes.py | 2 +- novaclient/tests/unit/v2/test_keypairs.py | 16 ++++++++++ novaclient/tests/unit/v2/test_shell.py | 15 +++++++++ novaclient/v2/keypairs.py | 25 ++++++++++++++- novaclient/v2/shell.py | 32 ++++++++++++++++++- .../microversion-v2_35-537619a43278fbb5.yaml | 5 +++ 8 files changed, 120 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/microversion-v2_35-537619a43278fbb5.yaml diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 76a83fa75..6b728d1f9 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.34") +API_MAX_VERSION = api_versions.APIVersion("2.35") diff --git a/novaclient/tests/functional/v2/test_keypairs.py b/novaclient/tests/functional/v2/test_keypairs.py index c55333e92..dba132c51 100644 --- a/novaclient/tests/functional/v2/test_keypairs.py +++ b/novaclient/tests/functional/v2/test_keypairs.py @@ -91,3 +91,30 @@ def cleanup(): self.assertRaises( ValueError, self._get_column_value_from_single_row_table, output, "Name") + + +class TestKeypairsNovaClientV235(base.TenantTestBase): + """Keypairs functional tests for v2.35 nova-api microversion.""" + + COMPUTE_API_VERSION = "2.35" + + def test_create_and_list_keypair_with_marker_and_limit(self): + names = [] + for i in range(3): + names.append(self.name_generate("v2_35")) + self.nova("keypair-add %s --user %s" % (names[i], self.user_id)) + self.addCleanup(self.another_nova, "keypair-delete %s" % names[i]) + + # sort keypairs before pagination + names = sorted(names) + + # list only one keypair after the first + output_1 = self.another_nova("keypair-list --limit 1 --marker %s" % + names[0]) + output_2 = self.nova("keypair-list --limit 1 --marker %s --user %s" % + (names[0], self.user_id)) + self.assertEqual(output_1, output_2) + # it should be table with only one second key-pair + self.assertEqual( + names[1], self._get_column_value_from_single_row_table(output_1, + "Name")) diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index edaeafb23..b4b671ffd 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -1190,7 +1190,7 @@ def get_os_keypairs_test(self, *kw): return (200, {}, {'keypair': self.get_os_keypairs()[2]['keypairs'][0]['keypair']}) - def get_os_keypairs(self, *kw): + def get_os_keypairs(self, user_id=None, limit=None, marker=None, *kw): return (200, {}, { "keypairs": [{"keypair": { "public_key": "FAKE_SSH_RSA", diff --git a/novaclient/tests/unit/v2/test_keypairs.py b/novaclient/tests/unit/v2/test_keypairs.py index 19570c76a..cf310b0ef 100644 --- a/novaclient/tests/unit/v2/test_keypairs.py +++ b/novaclient/tests/unit/v2/test_keypairs.py @@ -111,3 +111,19 @@ def test_import_keypair(self): 'public_key': pub_key, 'type': 'ssh'}}) self.assertIsInstance(kp, keypairs.Keypair) + + +class KeypairsV35TestCase(KeypairsTest): + def setUp(self): + super(KeypairsV35TestCase, self).setUp() + self.cs.api_version = api_versions.APIVersion("2.35") + + def test_list_keypairs(self): + kps = self.cs.keypairs.list(user_id='test_user', marker='test_kp', + limit=3) + self.assert_request_id(kps, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('GET', + '/%s?limit=3&marker=test_kp&user_id=test_user' + % self.keypair_prefix) + for kp in kps: + self.assertIsInstance(kp, keypairs.Keypair) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 4fcd295b4..96a26c8ab 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2873,6 +2873,21 @@ def test_keypair_list(self): self.run_command('keypair-list') self.assert_called('GET', '/os-keypairs') + def test_keypair_list_with_user_id(self): + self.run_command('keypair-list --user test_user', api_version='2.10') + self.assert_called('GET', '/os-keypairs?user_id=test_user') + + def test_keypair_list_with_limit_and_marker(self): + self.run_command('keypair-list --marker test_kp --limit 3', + api_version='2.35') + self.assert_called('GET', '/os-keypairs?limit=3&marker=test_kp') + + def test_keypair_list_with_user_id_limit_and_marker(self): + self.run_command('keypair-list --user test_user --marker test_kp ' + '--limit 3', api_version='2.35') + self.assert_called( + 'GET', '/os-keypairs?limit=3&marker=test_kp&user_id=test_user') + def test_keypair_show(self): self.run_command('keypair-show test') self.assert_called('GET', '/os-keypairs/test') diff --git a/novaclient/v2/keypairs.py b/novaclient/v2/keypairs.py index 505435c6c..9ac05eda2 100644 --- a/novaclient/v2/keypairs.py +++ b/novaclient/v2/keypairs.py @@ -19,6 +19,7 @@ from novaclient import api_versions from novaclient import base +from novaclient import utils class Keypair(base.Resource): @@ -162,7 +163,7 @@ def list(self): """ return self._list('/%s' % self.keypair_prefix, 'keypairs') - @api_versions.wraps("2.10") + @api_versions.wraps("2.10", "2.34") def list(self, user_id=None): """ Get a list of keypairs. @@ -172,3 +173,25 @@ def list(self, user_id=None): query_string = "?user_id=%s" % user_id if user_id else "" url = '/%s%s' % (self.keypair_prefix, query_string) return self._list(url, 'keypairs') + + @api_versions.wraps("2.35") + def list(self, user_id=None, marker=None, limit=None): + """ + Get a list of keypairs. + + :param user_id: Id of key-pairs owner (Admin only). + :param marker: Begin returning keypairs that appear later in the + keypair list than that represented by this keypair name + (optional). + :param limit: maximum number of keypairs to return (optional). + """ + params = {} + if user_id: + params['user_id'] = user_id + if limit: + params['limit'] = int(limit) + if marker: + params['marker'] = str(marker) + query_string = utils.prepare_query_string(params) + url = '/%s%s' % (self.keypair_prefix, query_string) + return self._list(url, 'keypairs') diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index d873740b4..09dc47b16 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -3119,7 +3119,7 @@ def do_keypair_list(cs, args): utils.print_list(keypairs, columns) -@api_versions.wraps("2.10") +@api_versions.wraps("2.10", "2.34") @utils.arg( '--user', metavar='', @@ -3132,6 +3132,36 @@ def do_keypair_list(cs, args): utils.print_list(keypairs, columns) +@api_versions.wraps("2.35") +@utils.arg( + '--user', + metavar='', + default=None, + help=_('List key-pairs of specified user ID (Admin only).')) +@utils.arg( + '--marker', + dest='marker', + metavar='', + default=None, + help=_('The last keypair of the previous page; displays list of keypairs ' + 'after "marker".')) +@utils.arg( + '--limit', + dest='limit', + metavar='', + type=int, + default=None, + help=_("Maximum number of keypairs to display. If limit == -1, all " + "keypairs will be displayed. If limit is bigger than " + "'osapi_max_limit' option of Nova API, limit 'osapi_max_limit' " + "will be used instead.")) +def do_keypair_list(cs, args): + """Print a list of keypairs for a user""" + keypairs = cs.keypairs.list(args.user, args.marker, args.limit) + columns = _get_keypairs_list_columns(cs, args) + utils.print_list(keypairs, columns) + + def _print_keypair(keypair): kp = keypair._info.copy() pk = kp.pop('public_key') diff --git a/releasenotes/notes/microversion-v2_35-537619a43278fbb5.yaml b/releasenotes/notes/microversion-v2_35-537619a43278fbb5.yaml new file mode 100644 index 000000000..0b220a4db --- /dev/null +++ b/releasenotes/notes/microversion-v2_35-537619a43278fbb5.yaml @@ -0,0 +1,5 @@ +--- +features: + - Added microversion v2.35 that adds pagination support for keypairs with + the help of new optional parameters 'limit' and 'marker' which were added + to keypair-list command. \ No newline at end of file From baeaf60e601d536b94552d3f477b78895df138f9 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 5 Aug 2016 20:28:02 +0000 Subject: [PATCH 1107/1705] Updated from global requirements Change-Id: I207d86ba6446031ae5072bb89ca6f0327ddf0df1 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index e606e457a..84496216e 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -12,7 +12,7 @@ python-keystoneclient!=1.8.0,!=2.1.0,>=1.7.0 # Apache-2.0 python-cinderclient!=1.7.0,!=1.7.1,>=1.6.0 # Apache-2.0 requests-mock>=1.0 # Apache-2.0 sphinx!=1.3b1,<1.3,>=1.2.1 # BSD -os-client-config>=1.13.1 # Apache-2.0 +os-client-config!=1.19.0,>=1.13.1 # Apache-2.0 oslosphinx!=3.4.0,>=2.5.0 # Apache-2.0 testrepository>=0.0.18 # Apache-2.0/BSD testscenarios>=0.4 # Apache-2.0/BSD From 35057e1a7379bd495f97013978ae9334143f77bd Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Tue, 9 Aug 2016 13:51:23 -0400 Subject: [PATCH 1108/1705] Fix boot --nic error message for v2.32 boundary If the --nic args were incorrect the error message said that nic tags were supported but that's dependent on the microversion used. This fixes the error message to be conditional based on the 2.32 microversion. It also pulls the error message generation out of the loop since it's static. Change-Id: I5d71ba7c490aa96f72094398b80af85fcde21ac6 Closes-Bug: #1611454 --- novaclient/tests/unit/v2/test_shell.py | 16 +++++++++++++++ novaclient/v2/shell.py | 27 +++++++++++++++++--------- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 96a26c8ab..cac85853e 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -581,6 +581,22 @@ def test_boot_nics_with_tag(self): }, ) + def test_boot_invalid_nics_pre_v2_32(self): + # This is a negative test to make sure we fail with the correct message + cmd = ('boot --image 1 --flavor 1 ' + '--nic net-id=1,port-id=2 some-server') + ex = self.assertRaises(exceptions.CommandError, self.run_command, + cmd, api_version='2.1') + self.assertNotIn('tag=tag', six.text_type(ex)) + + def test_boot_invalid_nics_v2_32(self): + # This is a negative test to make sure we fail with the correct message + cmd = ('boot --image 1 --flavor 1 ' + '--nic net-id=1,port-id=2 some-server') + ex = self.assertRaises(exceptions.CommandError, self.run_command, + cmd, api_version='2.32') + self.assertIn('tag=tag', six.text_type(ex)) + def test_boot_nics_ipv6(self): cmd = ('boot --image 1 --flavor 1 ' '--nic net-id=a=c,v6-fixed-ip=2001:db9:0:1::10 some-server') diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 09dc47b16..20427adeb 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -266,13 +266,22 @@ def _boot(cs, args): "with the new ones (--block-device, --boot-volume, --snapshot, " "--ephemeral, --swap)")) + if cs.api_version >= api_versions.APIVersion('2.32'): + err_msg = (_("Invalid nic argument '%s'. Nic arguments must be of " + "the form --nic , " + "with only one of net-id, net-name or port-id " + "specified.")) + else: + err_msg = (_("Invalid nic argument '%s'. Nic arguments must be of " + "the form --nic , " + "with only one of net-id, net-name or port-id " + "specified.")) nics = [] for nic_str in args.nics: - err_msg = (_("Invalid nic argument '%s'. Nic arguments must be of " - "the form --nic , with only one of net-id, " - "net-name or port-id specified.") % nic_str) nic_info = {"net-id": "", "v4-fixed-ip": "", "v6-fixed-ip": "", "port-id": "", "net-name": "", "tag": ""} @@ -280,7 +289,7 @@ def _boot(cs, args): try: k, v = kv_str.split("=", 1) except ValueError: - raise exceptions.CommandError(err_msg) + raise exceptions.CommandError(err_msg % nic_str) if k in nic_info: # if user has given a net-name resolve it to network ID @@ -289,10 +298,10 @@ def _boot(cs, args): v = _find_network_id(cs, v) # if some argument was given multiple times if nic_info[k]: - raise exceptions.CommandError(err_msg) + raise exceptions.CommandError(err_msg % nic_str) nic_info[k] = v else: - raise exceptions.CommandError(err_msg) + raise exceptions.CommandError(err_msg % nic_str) if nic_info['v4-fixed-ip'] and not netutils.is_valid_ipv4( nic_info['v4-fixed-ip']): @@ -303,7 +312,7 @@ def _boot(cs, args): raise exceptions.CommandError(_("Invalid ipv6 address.")) if bool(nic_info['net-id']) == bool(nic_info['port-id']): - raise exceptions.CommandError(err_msg) + raise exceptions.CommandError(err_msg % nic_str) nics.append(nic_info) From 4d971af927256e7e32dcfb5a43d5c4487f9c4af9 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Tue, 9 Aug 2016 13:57:55 -0400 Subject: [PATCH 1109/1705] Split nic parsing out of _boot method The _boot method is getting too big, so this patch splits the nic parsing out into a separate helper method, much like _parse_block_device_mapping_v2. Change-Id: I09353a56a224b0b21eb07d85115daf003ef32e99 --- novaclient/v2/shell.py | 104 ++++++++++++++++++++++------------------- 1 file changed, 55 insertions(+), 49 deletions(-) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 20427adeb..f0f81367a 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -164,6 +164,60 @@ def _parse_block_device_mapping_v2(args, image): return bdm +def _parse_nics(cs, args): + if cs.api_version >= api_versions.APIVersion('2.32'): + err_msg = (_("Invalid nic argument '%s'. Nic arguments must be of " + "the form --nic , " + "with only one of net-id, net-name or port-id " + "specified.")) + else: + err_msg = (_("Invalid nic argument '%s'. Nic arguments must be of " + "the form --nic , " + "with only one of net-id, net-name or port-id " + "specified.")) + nics = [] + for nic_str in args.nics: + nic_info = {"net-id": "", "v4-fixed-ip": "", "v6-fixed-ip": "", + "port-id": "", "net-name": "", "tag": ""} + + for kv_str in nic_str.split(","): + try: + k, v = kv_str.split("=", 1) + except ValueError: + raise exceptions.CommandError(err_msg % nic_str) + + if k in nic_info: + # if user has given a net-name resolve it to network ID + if k == 'net-name': + k = 'net-id' + v = _find_network_id(cs, v) + # if some argument was given multiple times + if nic_info[k]: + raise exceptions.CommandError(err_msg % nic_str) + nic_info[k] = v + else: + raise exceptions.CommandError(err_msg % nic_str) + + if nic_info['v4-fixed-ip'] and not netutils.is_valid_ipv4( + nic_info['v4-fixed-ip']): + raise exceptions.CommandError(_("Invalid ipv4 address.")) + + if nic_info['v6-fixed-ip'] and not netutils.is_valid_ipv6( + nic_info['v6-fixed-ip']): + raise exceptions.CommandError(_("Invalid ipv6 address.")) + + if bool(nic_info['net-id']) == bool(nic_info['port-id']): + raise exceptions.CommandError(err_msg % nic_str) + + nics.append(nic_info) + + return nics + + def _boot(cs, args): """Boot a new server.""" if not args.flavor: @@ -266,55 +320,7 @@ def _boot(cs, args): "with the new ones (--block-device, --boot-volume, --snapshot, " "--ephemeral, --swap)")) - if cs.api_version >= api_versions.APIVersion('2.32'): - err_msg = (_("Invalid nic argument '%s'. Nic arguments must be of " - "the form --nic , " - "with only one of net-id, net-name or port-id " - "specified.")) - else: - err_msg = (_("Invalid nic argument '%s'. Nic arguments must be of " - "the form --nic , " - "with only one of net-id, net-name or port-id " - "specified.")) - nics = [] - for nic_str in args.nics: - nic_info = {"net-id": "", "v4-fixed-ip": "", "v6-fixed-ip": "", - "port-id": "", "net-name": "", "tag": ""} - - for kv_str in nic_str.split(","): - try: - k, v = kv_str.split("=", 1) - except ValueError: - raise exceptions.CommandError(err_msg % nic_str) - - if k in nic_info: - # if user has given a net-name resolve it to network ID - if k == 'net-name': - k = 'net-id' - v = _find_network_id(cs, v) - # if some argument was given multiple times - if nic_info[k]: - raise exceptions.CommandError(err_msg % nic_str) - nic_info[k] = v - else: - raise exceptions.CommandError(err_msg % nic_str) - - if nic_info['v4-fixed-ip'] and not netutils.is_valid_ipv4( - nic_info['v4-fixed-ip']): - raise exceptions.CommandError(_("Invalid ipv4 address.")) - - if nic_info['v6-fixed-ip'] and not netutils.is_valid_ipv6( - nic_info['v6-fixed-ip']): - raise exceptions.CommandError(_("Invalid ipv6 address.")) - - if bool(nic_info['net-id']) == bool(nic_info['port-id']): - raise exceptions.CommandError(err_msg % nic_str) - - nics.append(nic_info) + nics = _parse_nics(cs, args) hints = {} if args.scheduler_hints: From 232711c0ef98baf79bcf4c8bdbae4b84003c9ab9 Mon Sep 17 00:00:00 2001 From: Sarah Ulmer Date: Thu, 11 Aug 2016 09:21:21 -0500 Subject: [PATCH 1110/1705] Added smaller flavors for novaclient functional tests to use Priority is added to pick smaller flavors, m1.nano and m1.micro`, for novaclient functional tests. Functional tests rely on devstack, which should use smaller flavors when available. Closes-Bug: #1611985 Change-Id: Iac75141a3d65b26a3093188ead1b7ed7c65514ad --- novaclient/tests/functional/base.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/novaclient/tests/functional/base.py b/novaclient/tests/functional/base.py index eafa2bf6e..5d3f95e0c 100644 --- a/novaclient/tests/functional/base.py +++ b/novaclient/tests/functional/base.py @@ -51,12 +51,10 @@ def is_keystone_version_available(session, version): # image / flavor list so that they can be used in standard testing. def pick_flavor(flavors): """Given a flavor list pick a reasonable one.""" - for flavor in flavors: - if flavor.name == 'm1.tiny': - return flavor - for flavor in flavors: - if flavor.name == 'm1.small': - return flavor + for flavor_priority in ('m1.nano', 'm1.micro', 'm1.tiny', 'm1.small'): + for flavor in flavors: + if flavor.name == flavor_priority: + return flavor raise NoFlavorException() From 1d2a20d8943de375be9b3382fb29a8e1f4beb327 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Thu, 11 Aug 2016 18:18:08 +0000 Subject: [PATCH 1111/1705] Updated from global requirements Change-Id: I7ca9067b948e3fda61f77cadc041fea4b6be932e --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 84496216e..ba0b0aa11 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -8,7 +8,7 @@ coverage>=3.6 # Apache-2.0 fixtures>=3.0.0 # Apache-2.0/BSD keyring>=5.5.1 # MIT/PSF mock>=2.0 # BSD -python-keystoneclient!=1.8.0,!=2.1.0,>=1.7.0 # Apache-2.0 +python-keystoneclient!=2.1.0,>=2.0.0 # Apache-2.0 python-cinderclient!=1.7.0,!=1.7.1,>=1.6.0 # Apache-2.0 requests-mock>=1.0 # Apache-2.0 sphinx!=1.3b1,<1.3,>=1.2.1 # BSD From 04613efbcf37d9c2d3b99bc83acb9fc806d1895b Mon Sep 17 00:00:00 2001 From: Sean Dague Date: Fri, 12 Aug 2016 06:31:41 -0400 Subject: [PATCH 1112/1705] Make novaclient functional tests use pretty tox This makes local running of the tests much nicer as we can see the progress of successful tests working around the testr issue of showing no output until the end of the run. Change-Id: I4adc965fd95b6e79a3ec26614d1c9f98262593b9 --- tox.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 92212f754..99916ec71 100644 --- a/tox.ini +++ b/tox.ini @@ -42,14 +42,14 @@ basepython = python2.7 passenv = OS_NOVACLIENT_TEST_NETWORK setenv = OS_TEST_PATH = ./novaclient/tests/functional -commands = python setup.py testr --testr-args='--concurrency=1 {posargs}' +commands = bash tools/pretty_tox.sh '--concurrency=1 {posargs}' [testenv:functional-py34] basepython = python3.4 passenv = OS_NOVACLIENT_TEST_NETWORK setenv = OS_TEST_PATH = ./novaclient/tests/functional -commands = python setup.py testr --testr-args='--concurrency=1 {posargs}' +commands = bash tools/pretty_tox.sh '--concurrency=1 {posargs}' [testenv:cover] commands = python setup.py testr --coverage --testr-args='{posargs}' From 1adabce8bf83950daca136dbd54d2b0b80868d1a Mon Sep 17 00:00:00 2001 From: Andreas Jaeger Date: Fri, 12 Aug 2016 21:14:16 +0200 Subject: [PATCH 1113/1705] Move other-requirements.txt to bindep.txt The default filename for documenting binary dependencies has been changed from "other-requirements.txt" to "bindep.txt" with the release of bindep 2.1.0. While the previous name is still supported, it will be deprecated. Move the file around to follow this change. Note that this change is self-testing, the OpenStack CI infrastructure will use a "bindep.txt" file to setup nodes for testing. For more information about bindep, see also: http://docs.openstack.org/infra/manual/drivers.html#package-requirements http://docs.openstack.org/infra/bindep/ As well as this announcement: http://lists.openstack.org/pipermail/openstack-dev/2016-August/101590.html Change-Id: Ida04b2a6277f141701d20fe55ac327a1e8513adf --- other-requirements.txt => bindep.txt | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename other-requirements.txt => bindep.txt (100%) diff --git a/other-requirements.txt b/bindep.txt similarity index 100% rename from other-requirements.txt rename to bindep.txt From f839cf1625714c2e159631c56560d9d92eb9964f Mon Sep 17 00:00:00 2001 From: Sean Dague Date: Thu, 11 Aug 2016 15:20:53 -0400 Subject: [PATCH 1114/1705] Look up image names directly in glance This does a lookup directly to the image service for name => id mapping of images. This is required to move on to supporting the 2.36 microversion since that microversion makes the compute image API proxy return a 404, so before the client can support that microversion it has to first drop it's usage of the proxy API. Because of the way the FakeHTTPClient was stubbing the image API proxy and the tests are not passing uuids for image IDs, there is a lot of cleaning up of the tests to make this work with a fake glance v2 API backend. The tests were basically false though since you can't do 'nova image-show 1', but because the stubs mask that it's just been a mountain of lies that has to be cleaned up here. As a side effect of fixing a bunch of the tests, this also makes debugging assert_called less terrible with a better error message. Co-Authored-By: Matt Riedemann Related to blueprint deprecate-api-proxies Change-Id: Iaff3999eafb7d746e5c6032f07ce0756f7b5e868 --- novaclient/base.py | 18 +- novaclient/tests/unit/fakes.py | 10 +- novaclient/tests/unit/v2/fakes.py | 65 ++-- novaclient/tests/unit/v2/test_shell.py | 289 ++++++++++-------- novaclient/v2/client.py | 1 + novaclient/v2/images.py | 43 +++ novaclient/v2/shell.py | 16 +- .../no-glance-proxy-5c13001a4b13e8ce.yaml | 10 + 8 files changed, 278 insertions(+), 174 deletions(-) create mode 100644 releasenotes/notes/no-glance-proxy-5c13001a4b13e8ce.yaml diff --git a/novaclient/base.py b/novaclient/base.py index 14485e5e1..6e9fdcdff 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -266,6 +266,18 @@ def _list(self, url, response_key, obj_class=None, body=None): for res in data if res] return ListWithMeta(items, resp) + @contextlib.contextmanager + def alternate_service_type(self, default, allowed_types=()): + original_service_type = self.api.client.service_type + if original_service_type in allowed_types: + yield + else: + self.api.client.service_type = default + try: + yield + finally: + self.api.client.service_type = original_service_type + @contextlib.contextmanager def completion_cache(self, cache_type, obj_class, mode): """The completion cache for bash autocompletion. @@ -332,7 +344,11 @@ def write_to_completion_cache(self, cache_type, val): def _get(self, url, response_key): resp, body = self.api.client.get(url) - return self.resource_class(self, body[response_key], loaded=True, + if response_key is not None: + content = body[response_key] + else: + content = body + return self.resource_class(self, content, loaded=True, resp=resp) def _create(self, url, body, response_key, return_raw=False, **kwargs): diff --git a/novaclient/tests/unit/fakes.py b/novaclient/tests/unit/fakes.py index c69648657..e89370bc6 100644 --- a/novaclient/tests/unit/fakes.py +++ b/novaclient/tests/unit/fakes.py @@ -78,13 +78,19 @@ def assert_called(self, method, url, body=None, pos=-1): }, pos=4) """ expected = (method, url) - called = self.client.callstack[pos][0:2] assert self.client.callstack, \ "Expected %s %s but no calls were made." % expected + called = self.client.callstack[pos][0:2] + assert expected == called, \ - 'Expected %s %s; got %s %s' % (expected + called) + ('\nExpected: %(expected)s' + '\nActual: %(called)s' + '\nCall position: %(pos)s' + '\nCalls:\n%(calls)s' % + {'expected': expected, 'called': called, 'pos': pos, + 'calls': '\n'.join(str(c) for c in self.client.callstack)}) if body is not None: if self.client.callstack[pos][2] != body: diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index b4b671ffd..59e5e8581 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -45,6 +45,8 @@ # fake image uuids FAKE_IMAGE_UUID_1 = 'c99d7632-bd66-4be9-aed5-3dd14b223a76' FAKE_IMAGE_UUID_2 = 'f27f479a-ddda-419a-9bbc-d6b56b210161' +FAKE_IMAGE_UUID_SNAPSHOT = '555cae93-fb41-4145-9c52-f5b923538a26' +FAKE_IMAGE_UUID_SNAP_DEL = '55bb23af-97a4-4068-bdf8-f10c62880ddf' # fake request id FAKE_REQUEST_ID = fakes.FAKE_REQUEST_ID @@ -128,6 +130,12 @@ def _cs_request(self, url, method, **kwargs): # more like we'd expect when making REST calls. raise exceptions.NotFound('404') + # Handle fake glance v2 requests + v2_image = False + if callback.startswith('get_v2_images'): + v2_image = True + callback = callback.replace('get_v2_', 'get_') + if not hasattr(self, callback): raise AssertionError('Called unknown API method: %s %s, ' 'expected fakes method name: %s' % @@ -137,6 +145,12 @@ def _cs_request(self, url, method, **kwargs): self.callstack.append((method, url, kwargs.get('body'))) status, headers, body = getattr(self, callback)(**kwargs) + + # If we're dealing with a glance v2 image response, the response + # isn't wrapped like the compute images API proxy is, so handle that. + if body and v2_image and 'image' in body: + body = body['image'] + r = utils.TestResponse({ "status_code": status, "text": body, @@ -417,7 +431,7 @@ def get_servers_detail(self, **kw): "id": 5678, "name": "sample-server2", "image": { - "id": 2, + "id": FAKE_IMAGE_UUID_1, "name": "sample image", }, "flavor": { @@ -742,9 +756,11 @@ def post_servers_1234_action(self, body, **kw): _body = {'adminPass': 'RescuePassword'} elif action == 'createImage': assert set(body[action].keys()) == set(['name', 'metadata']) - _headers = dict(location="http://blah/images/456") + _headers = dict(location="http://blah/images/%s" % + FAKE_IMAGE_UUID_SNAPSHOT) if body[action]['name'] == 'mysnapshot_deleted': - _headers = dict(location="http://blah/images/457") + _headers = dict(location="http://blah/images/%s" % + FAKE_IMAGE_UUID_SNAP_DEL) elif action == 'os-getConsoleOutput': assert list(body[action]) == ['length'] return (202, {}, {'output': 'foo'}) @@ -1079,8 +1095,6 @@ def put_os_floating_ips_bulk_delete(self, **kw): # def get_images(self, **kw): return (200, {}, {'images': [ - {'id': 1, 'name': 'CentOS 5.2'}, - {'id': 2, 'name': 'My Server Backup'}, {'id': FAKE_IMAGE_UUID_1, 'name': 'CentOS 5.2'}, {'id': FAKE_IMAGE_UUID_2, 'name': 'My Server Backup'} ]}) @@ -1088,18 +1102,7 @@ def get_images(self, **kw): def get_images_detail(self, **kw): return (200, {}, {'images': [ { - 'id': 1, - 'name': 'CentOS 5.2', - "updated": "2010-10-10T12:00:00Z", - "created": "2010-08-10T12:00:00Z", - "status": "ACTIVE", - "metadata": { - "test_key": "test_value", - }, - "links": {}, - }, - { - "id": 2, + "id": FAKE_IMAGE_UUID_SNAPSHOT, "name": "My Server Backup", "serverId": 1234, "updated": "2010-10-10T12:00:00Z", @@ -1109,7 +1112,7 @@ def get_images_detail(self, **kw): "links": {}, }, { - "id": 3, + "id": FAKE_IMAGE_UUID_SNAP_DEL, "name": "My Server Backup Deleted", "serverId": 1234, "updated": "2010-10-10T12:00:00Z", @@ -1141,38 +1144,31 @@ def get_images_detail(self, **kw): } ]}) - def get_images_1(self, **kw): + def get_images_555cae93_fb41_4145_9c52_f5b923538a26(self, **kw): return (200, {}, {'image': self.get_images_detail()[2]['images'][0]}) - def get_images_2(self, **kw): - return (200, {}, {'image': self.get_images_detail()[2]['images'][1]}) - - def get_images_456(self, **kw): + def get_images_55bb23af_97a4_4068_bdf8_f10c62880ddf(self, **kw): return (200, {}, {'image': self.get_images_detail()[2]['images'][1]}) - def get_images_457(self, **kw): - return (200, {}, {'image': self.get_images_detail()[2]['images'][2]}) - def get_images_c99d7632_bd66_4be9_aed5_3dd14b223a76(self, **kw): - return (200, {}, {'image': self.get_images_detail()[2]['images'][3]}) + return (200, {}, {'image': self.get_images_detail()[2]['images'][2]}) def get_images_f27f479a_ddda_419a_9bbc_d6b56b210161(self, **kw): - return (200, {}, {'image': self.get_images_detail()[2]['images'][4]}) + return (200, {}, {'image': self.get_images_detail()[2]['images'][3]}) def get_images_3e861307_73a6_4d1f_8d68_f68b03223032(self): raise exceptions.NotFound('404') - def post_images_1_metadata(self, body, **kw): + def post_images_c99d7632_bd66_4be9_aed5_3dd14b223a76_metadata( + self, body, **kw): assert list(body) == ['metadata'] fakes.assert_has_keys(body['metadata'], required=['test_key']) + get_image = self.get_images_c99d7632_bd66_4be9_aed5_3dd14b223a76 return ( 200, {}, - {'metadata': self.get_images_1()[2]['image']['metadata']}) - - def delete_images_1(self, **kw): - return (204, {}, None) + {'metadata': get_image()[2]['image']['metadata']}) def delete_images_c99d7632_bd66_4be9_aed5_3dd14b223a76(self, **kw): return (204, {}, None) @@ -1180,7 +1176,8 @@ def delete_images_c99d7632_bd66_4be9_aed5_3dd14b223a76(self, **kw): def delete_images_f27f479a_ddda_419a_9bbc_d6b56b210161(self, **kw): return (204, {}, None) - def delete_images_1_metadata_test_key(self, **kw): + def delete_images_c99d7632_bd66_4be9_aed5_3dd14b223a76_metadata_test_key( + self, **kw): return (204, {}, None) # diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index cac85853e..90469ea1c 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -131,13 +131,14 @@ def test_agents_modify(self): "md5hash": "add6bb58e139be103324d04d82d8f546"}}) def test_boot(self): - self.run_command('boot --flavor 1 --image 1 some-server') + self.run_command('boot --flavor 1 --image %s ' + 'some-server' % FAKE_UUID_1) self.assert_called_anytime( 'POST', '/servers', {'server': { 'flavorRef': '1', 'name': 'some-server', - 'imageRef': '1', + 'imageRef': FAKE_UUID_1, 'min_count': 1, 'max_count': 1, }}, @@ -151,20 +152,21 @@ def test_boot_image_with(self): {'server': { 'flavorRef': '1', 'name': 'some-server', - 'imageRef': '1', + 'imageRef': FAKE_UUID_1, 'min_count': 1, 'max_count': 1, }}, ) def test_boot_key(self): - self.run_command('boot --flavor 1 --image 1 --key-name 1 some-server') + self.run_command('boot --flavor 1 --image %s --key-name 1 some-server' + % FAKE_UUID_1) self.assert_called_anytime( 'POST', '/servers', {'server': { 'flavorRef': '1', 'name': 'some-server', - 'imageRef': '1', + 'imageRef': FAKE_UUID_1, 'key_name': '1', 'min_count': 1, 'max_count': 1, @@ -177,13 +179,14 @@ def test_boot_user_data(self): data = testfile_fd.read().encode('utf-8') expected_file_data = base64.b64encode(data).decode('utf-8') self.run_command( - 'boot --flavor 1 --image 1 --user-data %s some-server' % testfile) + 'boot --flavor 1 --image %s --user-data %s some-server' % + (FAKE_UUID_1, testfile)) self.assert_called_anytime( 'POST', '/servers', {'server': { 'flavorRef': '1', 'name': 'some-server', - 'imageRef': '1', + 'imageRef': FAKE_UUID_1, 'min_count': 1, 'max_count': 1, 'user_data': expected_file_data @@ -192,14 +195,14 @@ def test_boot_user_data(self): def test_boot_avzone(self): self.run_command( - 'boot --flavor 1 --image 1 --availability-zone avzone ' - 'some-server') + 'boot --flavor 1 --image %s --availability-zone avzone ' + 'some-server' % FAKE_UUID_1) self.assert_called_anytime( 'POST', '/servers', {'server': { 'flavorRef': '1', 'name': 'some-server', - 'imageRef': '1', + 'imageRef': FAKE_UUID_1, 'availability_zone': 'avzone', 'min_count': 1, 'max_count': 1 @@ -208,8 +211,8 @@ def test_boot_avzone(self): def test_boot_secgroup(self): self.run_command( - 'boot --flavor 1 --image 1 --security-groups secgroup1,' - 'secgroup2 some-server') + 'boot --flavor 1 --image %s --security-groups secgroup1,' + 'secgroup2 some-server' % FAKE_UUID_1) self.assert_called_anytime( 'POST', '/servers', {'server': { @@ -217,7 +220,7 @@ def test_boot_secgroup(self): {'name': 'secgroup2'}], 'flavorRef': '1', 'name': 'some-server', - 'imageRef': '1', + 'imageRef': FAKE_UUID_1, 'min_count': 1, 'max_count': 1, }}, @@ -225,13 +228,14 @@ def test_boot_secgroup(self): def test_boot_config_drive(self): self.run_command( - 'boot --flavor 1 --image 1 --config-drive 1 some-server') + 'boot --flavor 1 --image %s --config-drive 1 some-server' % + FAKE_UUID_1) self.assert_called_anytime( 'POST', '/servers', {'server': { 'flavorRef': '1', 'name': 'some-server', - 'imageRef': '1', + 'imageRef': FAKE_UUID_1, 'min_count': 1, 'max_count': 1, 'config_drive': True @@ -240,14 +244,14 @@ def test_boot_config_drive(self): def test_boot_access_ip(self): self.run_command( - 'boot --flavor 1 --image 1 --access-ip-v4 10.10.10.10 ' - '--access-ip-v6 ::1 some-server') + 'boot --flavor 1 --image %s --access-ip-v4 10.10.10.10 ' + '--access-ip-v6 ::1 some-server' % FAKE_UUID_1) self.assert_called_anytime( 'POST', '/servers', {'server': { 'flavorRef': '1', 'name': 'some-server', - 'imageRef': '1', + 'imageRef': FAKE_UUID_1, 'accessIPv4': '10.10.10.10', 'accessIPv6': '::1', 'max_count': 1, @@ -257,13 +261,14 @@ def test_boot_access_ip(self): def test_boot_config_drive_custom(self): self.run_command( - 'boot --flavor 1 --image 1 --config-drive /dev/hda some-server') + 'boot --flavor 1 --image %s --config-drive /dev/hda some-server' % + FAKE_UUID_1) self.assert_called_anytime( 'POST', '/servers', {'server': { 'flavorRef': '1', 'name': 'some-server', - 'imageRef': '1', + 'imageRef': FAKE_UUID_1, 'min_count': 1, 'max_count': 1, 'config_drive': '/dev/hda' @@ -273,8 +278,8 @@ def test_boot_config_drive_custom(self): def test_boot_invalid_user_data(self): invalid_file = os.path.join(os.path.dirname(__file__), 'no_such_file') - cmd = ('boot some-server --flavor 1 --image 1' - ' --user-data %s' % invalid_file) + cmd = ('boot some-server --flavor 1 --image %s' + ' --user-data %s' % (FAKE_UUID_1, invalid_file)) self.assertRaises(exceptions.CommandError, self.run_command, cmd) def test_boot_no_image_no_bdms(self): @@ -282,7 +287,7 @@ def test_boot_no_image_no_bdms(self): self.assertRaises(exceptions.CommandError, self.run_command, cmd) def test_boot_no_flavor(self): - cmd = 'boot --image 1 some-server' + cmd = 'boot --image %s some-server' % FAKE_UUID_1 self.assertRaises(exceptions.CommandError, self.run_command, cmd) def test_boot_no_image_bdms(self): @@ -309,9 +314,9 @@ def test_boot_no_image_bdms(self): def test_boot_image_bdms_v2(self): self.run_command( - 'boot --flavor 1 --image 1 --block-device id=fake-id,' + 'boot --flavor 1 --image %s --block-device id=fake-id,' 'source=volume,dest=volume,device=vda,size=1,format=ext4,' - 'type=disk,shutdown=preserve some-server' + 'type=disk,shutdown=preserve some-server' % FAKE_UUID_1 ) self.assert_called_anytime( 'POST', '/os-volumes_boot', @@ -320,7 +325,7 @@ def test_boot_image_bdms_v2(self): 'name': 'some-server', 'block_device_mapping_v2': [ { - 'uuid': 1, + 'uuid': FAKE_UUID_1, 'source_type': 'image', 'destination_type': 'local', 'boot_index': 0, @@ -337,7 +342,7 @@ def test_boot_image_bdms_v2(self): 'delete_on_termination': False, }, ], - 'imageRef': '1', + 'imageRef': FAKE_UUID_1, 'min_count': 1, 'max_count': 1, }}, @@ -345,9 +350,9 @@ def test_boot_image_bdms_v2(self): def test_boot_image_bdms_v2_with_tag(self): self.run_command( - 'boot --flavor 1 --image 1 --block-device id=fake-id,' + 'boot --flavor 1 --image %s --block-device id=fake-id,' 'source=volume,dest=volume,device=vda,size=1,format=ext4,' - 'type=disk,shutdown=preserve,tag=foo some-server', + 'type=disk,shutdown=preserve,tag=foo some-server' % FAKE_UUID_1, api_version='2.32' ) self.assert_called_anytime( @@ -357,7 +362,7 @@ def test_boot_image_bdms_v2_with_tag(self): 'name': 'some-server', 'block_device_mapping_v2': [ { - 'uuid': 1, + 'uuid': FAKE_UUID_1, 'source_type': 'image', 'destination_type': 'local', 'boot_index': 0, @@ -375,7 +380,7 @@ def test_boot_image_bdms_v2_with_tag(self): 'tag': 'foo', }, ], - 'imageRef': '1', + 'imageRef': FAKE_UUID_1, 'min_count': 1, 'max_count': 1, }}, @@ -504,20 +509,20 @@ def test_boot_no_image_bdms_v2(self): def test_boot_bdms_v2_invalid_shutdown_value(self): self.assertRaises(exceptions.CommandError, self.run_command, - ('boot --flavor 1 --image 1 --block-device ' + ('boot --flavor 1 --image %s --block-device ' 'id=fake-id,source=volume,dest=volume,device=vda,' 'size=1,format=ext4,type=disk,shutdown=foobar ' - 'some-server')) + 'some-server' % FAKE_UUID_1)) def test_boot_metadata(self): - self.run_command('boot --image 1 --flavor 1 --meta foo=bar=pants' - ' --meta spam=eggs some-server ') + self.run_command('boot --image %s --flavor 1 --meta foo=bar=pants' + ' --meta spam=eggs some-server ' % FAKE_UUID_1) self.assert_called_anytime( 'POST', '/servers', {'server': { 'flavorRef': '1', 'name': 'some-server', - 'imageRef': '1', + 'imageRef': FAKE_UUID_1, 'metadata': {'foo': 'bar=pants', 'spam': 'eggs'}, 'min_count': 1, 'max_count': 1, @@ -525,15 +530,16 @@ def test_boot_metadata(self): ) def test_boot_hints(self): - self.run_command('boot --image 1 --flavor 1 ' - '--hint a=b0=c0 --hint a=b1=c1 some-server ') + self.run_command('boot --image %s --flavor 1 ' + '--hint a=b0=c0 --hint a=b1=c1 some-server ' % + FAKE_UUID_1) self.assert_called_anytime( 'POST', '/servers', { 'server': { 'flavorRef': '1', 'name': 'some-server', - 'imageRef': '1', + 'imageRef': FAKE_UUID_1, 'min_count': 1, 'max_count': 1, }, @@ -542,8 +548,9 @@ def test_boot_hints(self): ) def test_boot_nics(self): - cmd = ('boot --image 1 --flavor 1 ' - '--nic net-id=a=c,v4-fixed-ip=10.0.0.1 some-server') + cmd = ('boot --image %s --flavor 1 ' + '--nic net-id=a=c,v4-fixed-ip=10.0.0.1 some-server' % + FAKE_UUID_1) self.run_command(cmd) self.assert_called_anytime( 'POST', '/servers', @@ -551,7 +558,7 @@ def test_boot_nics(self): 'server': { 'flavorRef': '1', 'name': 'some-server', - 'imageRef': '1', + 'imageRef': FAKE_UUID_1, 'min_count': 1, 'max_count': 1, 'networks': [ @@ -562,8 +569,9 @@ def test_boot_nics(self): ) def test_boot_nics_with_tag(self): - cmd = ('boot --image 1 --flavor 1 ' - '--nic net-id=a=c,v4-fixed-ip=10.0.0.1,tag=foo some-server') + cmd = ('boot --image %s --flavor 1 ' + '--nic net-id=a=c,v4-fixed-ip=10.0.0.1,tag=foo some-server' % + FAKE_UUID_1) self.run_command(cmd, api_version='2.32') self.assert_called_anytime( 'POST', '/servers', @@ -571,7 +579,7 @@ def test_boot_nics_with_tag(self): 'server': { 'flavorRef': '1', 'name': 'some-server', - 'imageRef': '1', + 'imageRef': FAKE_UUID_1, 'min_count': 1, 'max_count': 1, 'networks': [ @@ -583,23 +591,24 @@ def test_boot_nics_with_tag(self): def test_boot_invalid_nics_pre_v2_32(self): # This is a negative test to make sure we fail with the correct message - cmd = ('boot --image 1 --flavor 1 ' - '--nic net-id=1,port-id=2 some-server') + cmd = ('boot --image %s --flavor 1 ' + '--nic net-id=1,port-id=2 some-server' % FAKE_UUID_1) ex = self.assertRaises(exceptions.CommandError, self.run_command, cmd, api_version='2.1') self.assertNotIn('tag=tag', six.text_type(ex)) def test_boot_invalid_nics_v2_32(self): # This is a negative test to make sure we fail with the correct message - cmd = ('boot --image 1 --flavor 1 ' - '--nic net-id=1,port-id=2 some-server') + cmd = ('boot --image %s --flavor 1 ' + '--nic net-id=1,port-id=2 some-server' % FAKE_UUID_1) ex = self.assertRaises(exceptions.CommandError, self.run_command, cmd, api_version='2.32') self.assertIn('tag=tag', six.text_type(ex)) def test_boot_nics_ipv6(self): - cmd = ('boot --image 1 --flavor 1 ' - '--nic net-id=a=c,v6-fixed-ip=2001:db9:0:1::10 some-server') + cmd = ('boot --image %s --flavor 1 ' + '--nic net-id=a=c,v6-fixed-ip=2001:db9:0:1::10 some-server' % + FAKE_UUID_1) self.run_command(cmd) self.assert_called_anytime( 'POST', '/servers', @@ -607,7 +616,7 @@ def test_boot_nics_ipv6(self): 'server': { 'flavorRef': '1', 'name': 'some-server', - 'imageRef': '1', + 'imageRef': FAKE_UUID_1, 'min_count': 1, 'max_count': 1, 'networks': [ @@ -618,44 +627,48 @@ def test_boot_nics_ipv6(self): ) def test_boot_nics_both_ipv4_and_ipv6(self): - cmd = ('boot --image 1 --flavor 1 ' + cmd = ('boot --image %s --flavor 1 ' '--nic net-id=a=c,v4-fixed-ip=10.0.0.1,' - 'v6-fixed-ip=2001:db9:0:1::10 some-server') + 'v6-fixed-ip=2001:db9:0:1::10 some-server' % FAKE_UUID_1) self.assertRaises(exceptions.CommandError, self.run_command, cmd) def test_boot_nics_no_value(self): - cmd = ('boot --image 1 --flavor 1 ' - '--nic net-id some-server') + cmd = ('boot --image %s --flavor 1 ' + '--nic net-id some-server' % FAKE_UUID_1) self.assertRaises(exceptions.CommandError, self.run_command, cmd) def test_boot_nics_random_key(self): - cmd = ('boot --image 1 --flavor 1 ' - '--nic net-id=a=c,v4-fixed-ip=10.0.0.1,foo=bar some-server') + cmd = ('boot --image %s --flavor 1 ' + '--nic net-id=a=c,v4-fixed-ip=10.0.0.1,foo=bar some-server' % + FAKE_UUID_1) self.assertRaises(exceptions.CommandError, self.run_command, cmd) def test_boot_nics_no_netid_or_portid(self): - cmd = ('boot --image 1 --flavor 1 ' - '--nic v4-fixed-ip=10.0.0.1 some-server') + cmd = ('boot --image %s --flavor 1 ' + '--nic v4-fixed-ip=10.0.0.1 some-server' % FAKE_UUID_1) self.assertRaises(exceptions.CommandError, self.run_command, cmd) def test_boot_nics_netid_and_portid(self): - cmd = ('boot --image 1 --flavor 1 ' - '--nic port-id=some=port,net-id=some=net some-server') + cmd = ('boot --image %s --flavor 1 ' + '--nic port-id=some=port,net-id=some=net some-server' % + FAKE_UUID_1) self.assertRaises(exceptions.CommandError, self.run_command, cmd) def test_boot_nics_invalid_ipv4(self): - cmd = ('boot --image 1 --flavor 1 ' - '--nic net-id=a=c,v4-fixed-ip=2001:db9:0:1::10 some-server') + cmd = ('boot --image %s --flavor 1 ' + '--nic net-id=a=c,v4-fixed-ip=2001:db9:0:1::10 some-server' % + FAKE_UUID_1) self.assertRaises(exceptions.CommandError, self.run_command, cmd) def test_boot_nics_invalid_ipv6(self): - cmd = ('boot --image 1 --flavor 1 ' - '--nic net-id=a=c,v6-fixed-ip=10.0.0.1 some-server') + cmd = ('boot --image %s --flavor 1 ' + '--nic net-id=a=c,v6-fixed-ip=10.0.0.1 some-server' % + FAKE_UUID_1) self.assertRaises(exceptions.CommandError, self.run_command, cmd) def test_boot_nics_net_id_twice(self): - cmd = ('boot --image 1 --flavor 1 ' - '--nic net-id=net-id1,net-id=net-id2 some-server') + cmd = ('boot --image %s --flavor 1 ' + '--nic net-id=net-id1,net-id=net-id2 some-server' % FAKE_UUID_1) self.assertRaises(exceptions.CommandError, self.run_command, cmd) @mock.patch( @@ -664,8 +677,8 @@ def test_boot_nics_net_name(self, mock_networks_list): mock_networks_list.return_value = (200, {}, { 'networks': [{"label": "some-net", 'id': '1'}]}) - cmd = ('boot --image 1 --flavor 1 ' - '--nic net-name=some-net some-server') + cmd = ('boot --image %s --flavor 1 ' + '--nic net-name=some-net some-server' % FAKE_UUID_1) self.run_command(cmd) self.assert_called_anytime( 'POST', '/servers', @@ -673,7 +686,7 @@ def test_boot_nics_net_name(self, mock_networks_list): 'server': { 'flavorRef': '1', 'name': 'some-server', - 'imageRef': '1', + 'imageRef': FAKE_UUID_1, 'min_count': 1, 'max_count': 1, 'networks': [ @@ -684,8 +697,8 @@ def test_boot_nics_net_name(self, mock_networks_list): ) def test_boot_nics_net_name_not_found(self): - cmd = ('boot --image 1 --flavor 1 ' - '--nic net-name=some-net some-server') + cmd = ('boot --image %s --flavor 1 ' + '--nic net-name=some-net some-server' % FAKE_UUID_1) self.assertRaises(exceptions.ResourceNotFound, self.run_command, cmd) @mock.patch( @@ -695,20 +708,22 @@ def test_boot_nics_net_name_multiple_matches(self, mock_networks_list): 'networks': [{"label": "some-net", 'id': '1'}, {"label": "some-net", 'id': '2'}]}) - cmd = ('boot --image 1 --flavor 1 ' - '--nic net-name=some-net some-server') + cmd = ('boot --image %s --flavor 1 ' + '--nic net-name=some-net some-server' % FAKE_UUID_1) self.assertRaises(exceptions.NoUniqueMatch, self.run_command, cmd) @mock.patch('novaclient.v2.shell._find_network_id', return_value='net-id') def test_boot_nics_net_name_and_net_id(self, mock_find_network_id): - cmd = ('boot --image 1 --flavor 1 ' - '--nic net-name=some-net,net-id=some-id some-server') + cmd = ('boot --image %s --flavor 1 ' + '--nic net-name=some-net,net-id=some-id some-server' % + FAKE_UUID_1) self.assertRaises(exceptions.CommandError, self.run_command, cmd) @mock.patch('novaclient.v2.shell._find_network_id', return_value='net-id') def test_boot_nics_net_name_and_port_id(self, mock_find_network_id): - cmd = ('boot --image 1 --flavor 1 ' - '--nic net-name=some-net,port-id=some-id some-server') + cmd = ('boot --image %s --flavor 1 ' + '--nic net-name=some-net,port-id=some-id some-server' % + FAKE_UUID_1) self.assertRaises(exceptions.CommandError, self.run_command, cmd) def test_boot_files(self): @@ -717,16 +732,16 @@ def test_boot_files(self): data = testfile_fd.read() expected = base64.b64encode(data.encode('utf-8')).decode('utf-8') - cmd = ('boot some-server --flavor 1 --image 1' + cmd = ('boot some-server --flavor 1 --image %s' ' --file /tmp/foo=%s --file /tmp/bar=%s') - self.run_command(cmd % (testfile, testfile)) + self.run_command(cmd % (FAKE_UUID_1, testfile, testfile)) self.assert_called_anytime( 'POST', '/servers', {'server': { 'flavorRef': '1', 'name': 'some-server', - 'imageRef': '1', + 'imageRef': FAKE_UUID_1, 'min_count': 1, 'max_count': 1, 'personality': [ @@ -739,92 +754,96 @@ def test_boot_files(self): def test_boot_invalid_files(self): invalid_file = os.path.join(os.path.dirname(__file__), 'asdfasdfasdfasdf') - cmd = ('boot some-server --flavor 1 --image 1' - ' --file /foo=%s' % invalid_file) + cmd = ('boot some-server --flavor 1 --image %s' + ' --file /foo=%s' % (FAKE_UUID_1, invalid_file)) self.assertRaises(exceptions.CommandError, self.run_command, cmd) def test_boot_max_min_count(self): - self.run_command('boot --image 1 --flavor 1 --min-count 1' - ' --max-count 3 server') + self.run_command('boot --image %s --flavor 1 --min-count 1' + ' --max-count 3 server' % FAKE_UUID_1) self.assert_called_anytime( 'POST', '/servers', { 'server': { 'flavorRef': '1', 'name': 'server', - 'imageRef': '1', + 'imageRef': FAKE_UUID_1, 'min_count': 1, 'max_count': 3, } }) def test_boot_invalid_min_count(self): - cmd = 'boot --image 1 --flavor 1 --min-count 0 server' + cmd = 'boot --image %s --flavor 1 --min-count 0 server' % FAKE_UUID_1 self.assertRaises(exceptions.CommandError, self.run_command, cmd) def test_boot_min_max_count(self): - self.run_command('boot --image 1 --flavor 1 --max-count 3 server') + self.run_command('boot --image %s --flavor 1 --max-count 3 server' % + FAKE_UUID_1) self.assert_called_anytime( 'POST', '/servers', { 'server': { 'flavorRef': '1', 'name': 'server', - 'imageRef': '1', + 'imageRef': FAKE_UUID_1, 'min_count': 1, 'max_count': 3, } }) - self.run_command('boot --image 1 --flavor 1 --min-count 3 server') + self.run_command('boot --image %s --flavor 1 --min-count 3 server' % + FAKE_UUID_1) self.assert_called_anytime( 'POST', '/servers', { 'server': { 'flavorRef': '1', 'name': 'server', - 'imageRef': '1', + 'imageRef': FAKE_UUID_1, 'min_count': 3, 'max_count': 3, } }) - self.run_command('boot --image 1 --flavor 1 ' - '--min-count 3 --max-count 3 server') + self.run_command('boot --image %s --flavor 1 ' + '--min-count 3 --max-count 3 server' % FAKE_UUID_1) self.assert_called_anytime( 'POST', '/servers', { 'server': { 'flavorRef': '1', 'name': 'server', - 'imageRef': '1', + 'imageRef': FAKE_UUID_1, 'min_count': 3, 'max_count': 3, } }) - self.run_command('boot --image 1 --flavor 1 ' - '--min-count 3 --max-count 5 server') + self.run_command('boot --image %s --flavor 1 ' + '--min-count 3 --max-count 5 server' % FAKE_UUID_1) self.assert_called_anytime( 'POST', '/servers', { 'server': { 'flavorRef': '1', 'name': 'server', - 'imageRef': '1', + 'imageRef': FAKE_UUID_1, 'min_count': 3, 'max_count': 5, } }) - cmd = 'boot --image 1 --flavor 1 --min-count 3 --max-count 1 serv' + cmd = ('boot --image %s --flavor 1 --min-count 3 --max-count 1 serv' % + FAKE_UUID_1) self.assertRaises(exceptions.CommandError, self.run_command, cmd) @mock.patch('novaclient.v2.shell._poll_for_status') def test_boot_with_poll(self, poll_method): - self.run_command('boot --flavor 1 --image 1 some-server --poll') + self.run_command('boot --flavor 1 --image %s some-server --poll' % + FAKE_UUID_1) self.assert_called_anytime( 'POST', '/servers', {'server': { 'flavorRef': '1', 'name': 'some-server', - 'imageRef': '1', + 'imageRef': FAKE_UUID_1, 'min_count': 1, 'max_count': 1, }}, @@ -836,13 +855,14 @@ def test_boot_with_poll(self, poll_method): def test_boot_with_poll_to_check_VM_state_error(self): self.assertRaises(exceptions.ResourceInErrorState, self.run_command, - 'boot --flavor 1 --image 1 some-bad-server --poll') + 'boot --flavor 1 --image %s some-bad-server --poll' % + FAKE_UUID_1) def test_boot_named_flavor(self): self.run_command(["boot", "--image", FAKE_UUID_1, "--flavor", "512 MB Server", "--max-count", "3", "server"]) - self.assert_called('GET', '/images/' + FAKE_UUID_1, pos=0) + self.assert_called('GET', '/v2/images/' + FAKE_UUID_1, pos=0) self.assert_called('GET', '/flavors/512 MB Server', pos=1) self.assert_called('GET', '/flavors?is_public=None', pos=2) self.assert_called('GET', '/flavors/2', pos=3) @@ -859,7 +879,8 @@ def test_boot_named_flavor(self): }, pos=4) def test_boot_invalid_ephemeral_data_format(self): - cmd = 'boot --flavor 1 --image 1 --ephemeral 1 some-server' + cmd = ('boot --flavor 1 --image %s --ephemeral 1 some-server' % + FAKE_UUID_1) self.assertRaises(argparse.ArgumentTypeError, self.run_command, cmd) def test_flavor_list(self): @@ -949,19 +970,22 @@ def test_flavor_access_remove_by_name(self): {'removeTenantAccess': {'tenant': 'proj2'}}) def test_image_show(self): - _, err = self.run_command('image-show 1') + _, err = self.run_command('image-show %s' % FAKE_UUID_1) self.assertIn('Command image-show is deprecated', err) - self.assert_called('GET', '/images/1') + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1) def test_image_meta_set(self): - _, err = self.run_command('image-meta 1 set test_key=test_value') + _, err = self.run_command('image-meta %s set test_key=test_value' % + FAKE_UUID_1) self.assertIn('Command image-meta is deprecated', err) - self.assert_called('POST', '/images/1/metadata', + self.assert_called('POST', '/images/%s/metadata' % FAKE_UUID_1, {'metadata': {'test_key': 'test_value'}}) def test_image_meta_del(self): - self.run_command('image-meta 1 delete test_key=test_value') - self.assert_called('DELETE', '/images/1/metadata/test_key') + self.run_command('image-meta %s delete test_key=test_value' % + FAKE_UUID_1) + self.assert_called('DELETE', '/images/%s/metadata/test_key' % + FAKE_UUID_1) @mock.patch('sys.stdout', six.StringIO()) @mock.patch('sys.stderr', six.StringIO()) @@ -1009,7 +1033,8 @@ def test_create_image_with_poll(self, poll_method): ) self.assertEqual(1, poll_method.call_count) poll_method.assert_has_calls( - [mock.call(self.shell.cs.images.get, '456', 'snapshotting', + [mock.call(self.shell.cs.glance.find_image, + fakes.FAKE_IMAGE_UUID_SNAPSHOT, 'snapshotting', ['active'])]) def test_create_image_with_poll_to_check_image_state_deleted(self): @@ -1018,15 +1043,15 @@ def test_create_image_with_poll_to_check_image_state_deleted(self): 'image-create sample-server mysnapshot_deleted --poll') def test_image_delete(self): - _, err = self.run_command('image-delete 1') + _, err = self.run_command('image-delete %s' % FAKE_UUID_1) self.assertIn('Command image-delete is deprecated', err) - self.assert_called('DELETE', '/images/1') + self.assert_called('DELETE', '/images/%s' % FAKE_UUID_1) def test_image_delete_multiple(self): self.run_command('image-delete %s %s' % (FAKE_UUID_1, FAKE_UUID_2)) - self.assert_called('GET', '/images/' + FAKE_UUID_1, pos=0) + self.assert_called('GET', '/v2/images/' + FAKE_UUID_1, pos=0) self.assert_called('DELETE', '/images/' + FAKE_UUID_1, pos=1) - self.assert_called('GET', '/images/' + FAKE_UUID_2, pos=2) + self.assert_called('GET', '/v2/images/' + FAKE_UUID_2, pos=2) self.assert_called('DELETE', '/images/' + FAKE_UUID_2, pos=3) def test_list(self): @@ -1042,8 +1067,8 @@ def test_list_deleted(self): self.assert_called('GET', '/servers/detail?deleted=True') def test_list_with_images(self): - self.run_command('list --image 1') - self.assert_called('GET', '/servers/detail?image=1') + self.run_command('list --image %s' % FAKE_UUID_1) + self.assert_called('GET', '/servers/detail?image=%s' % FAKE_UUID_1) def test_list_with_flavors(self): self.run_command('list --flavor 1') @@ -1180,11 +1205,11 @@ def test_rebuild(self): output, _ = self.run_command('rebuild sample-server %s' % FAKE_UUID_1) self.assert_called('GET', '/servers?name=sample-server', pos=0) self.assert_called('GET', '/servers/1234', pos=1) - self.assert_called('GET', '/images/%s' % FAKE_UUID_1, pos=2) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=2) self.assert_called('POST', '/servers/1234/action', {'rebuild': {'imageRef': FAKE_UUID_1}}, pos=3) self.assert_called('GET', '/flavors/1', pos=4) - self.assert_called('GET', '/images/%s' % FAKE_UUID_2, pos=5) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=5) self.assertIn('adminPass', output) def test_rebuild_password(self): @@ -1193,12 +1218,12 @@ def test_rebuild_password(self): % FAKE_UUID_1) self.assert_called('GET', '/servers?name=sample-server', pos=0) self.assert_called('GET', '/servers/1234', pos=1) - self.assert_called('GET', '/images/%s' % FAKE_UUID_1, pos=2) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=2) self.assert_called('POST', '/servers/1234/action', {'rebuild': {'imageRef': FAKE_UUID_1, 'adminPass': 'asdf'}}, pos=3) self.assert_called('GET', '/flavors/1', pos=4) - self.assert_called('GET', '/images/%s' % FAKE_UUID_2, pos=5) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=5) self.assertIn('adminPass', output) def test_rebuild_preserve_ephemeral(self): @@ -1206,25 +1231,25 @@ def test_rebuild_preserve_ephemeral(self): % FAKE_UUID_1) self.assert_called('GET', '/servers?name=sample-server', pos=0) self.assert_called('GET', '/servers/1234', pos=1) - self.assert_called('GET', '/images/%s' % FAKE_UUID_1, pos=2) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=2) self.assert_called('POST', '/servers/1234/action', {'rebuild': {'imageRef': FAKE_UUID_1, 'preserve_ephemeral': True}}, pos=3) self.assert_called('GET', '/flavors/1', pos=4) - self.assert_called('GET', '/images/%s' % FAKE_UUID_2, pos=5) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=5) def test_rebuild_name_meta(self): self.run_command('rebuild sample-server %s --name asdf --meta ' 'foo=bar' % FAKE_UUID_1) self.assert_called('GET', '/servers?name=sample-server', pos=0) self.assert_called('GET', '/servers/1234', pos=1) - self.assert_called('GET', '/images/%s' % FAKE_UUID_1, pos=2) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=2) self.assert_called('POST', '/servers/1234/action', {'rebuild': {'imageRef': FAKE_UUID_1, 'name': 'asdf', 'metadata': {'foo': 'bar'}}}, pos=3) self.assert_called('GET', '/flavors/1', pos=4) - self.assert_called('GET', '/images/%s' % FAKE_UUID_2, pos=5) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=5) def test_start(self): self.run_command('start sample-server') @@ -1282,9 +1307,9 @@ def test_rescue_password(self): {'rescue': {'adminPass': 'asdf'}}) def test_rescue_image(self): - self.run_command('rescue sample-server --image 1') + self.run_command('rescue sample-server --image %s' % FAKE_UUID_1) self.assert_called('POST', '/servers/1234/action', - {'rescue': {'rescue_image_ref': 1}}) + {'rescue': {'rescue_image_ref': FAKE_UUID_1}}) def test_unrescue(self): self.run_command('unrescue sample-server') @@ -1356,7 +1381,7 @@ def test_show(self): self.assert_called('GET', '/servers?name=1234', pos=1) self.assert_called('GET', '/servers/1234', pos=2) self.assert_called('GET', '/flavors/1', pos=3) - self.assert_called('GET', '/images/%s' % FAKE_UUID_2, pos=4) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=4) def test_show_no_image(self): self.run_command('show 9012') @@ -1369,13 +1394,13 @@ def test_show_bad_id(self): def test_show_unavailable_image_and_flavor(self): output, _ = self.run_command('show 9013') - self.assert_called('GET', '/servers/9013', pos=-8) + self.assert_called('GET', '/servers/9013', pos=-6) self.assert_called('GET', '/flavors/80645cf4-6ad3-410a-bbc8-6f3e1e291f51', - pos=-7) + pos=-5) self.assert_called('GET', - '/images/3e861307-73a6-4d1f-8d68-f68b03223032', - pos=-3) + '/v2/images/3e861307-73a6-4d1f-8d68-f68b03223032', + pos=-1) self.assertIn('Image not found', output) self.assertIn('Flavor not found', output) diff --git a/novaclient/v2/client.py b/novaclient/v2/client.py index f5dee15dc..044653f52 100644 --- a/novaclient/v2/client.py +++ b/novaclient/v2/client.py @@ -131,6 +131,7 @@ def __init__(self, username=None, api_key=None, project_id=None, self.flavors = flavors.FlavorManager(self) self.flavor_access = flavor_access.FlavorAccessManager(self) self.images = images.ImageManager(self) + self.glance = images.GlanceManager(self) self.limits = limits.LimitsManager(self) self.servers = servers.ServerManager(self) self.versions = versions.VersionManager(self) diff --git a/novaclient/v2/images.py b/novaclient/v2/images.py index dbbc60ce0..d00abba1a 100644 --- a/novaclient/v2/images.py +++ b/novaclient/v2/images.py @@ -18,9 +18,12 @@ import warnings +from oslo_utils import uuidutils from six.moves.urllib import parse from novaclient import base +from novaclient import exceptions +from novaclient.i18n import _ class Image(base.Resource): @@ -42,6 +45,46 @@ def delete(self): return self.manager.delete(self) +class GlanceManager(base.Manager): + """Use glance directly from service catalog. + + This is used to do name to id lookups for images. Do not use it + for anything else besides that. You have been warned. + + """ + + resource_class = Image + + def find_image(self, name_or_id): + """Find an image by name or id (user provided input).""" + + with self.alternate_service_type( + 'image', allowed_types=('image',)): + # glance catalog entries are the unversioned endpoint, so + # we need to jam a version bit in here. + if uuidutils.is_uuid_like(name_or_id): + # if it's a uuid, then just go collect the info and be + # done with it. + return self._get('/v2/images/%s' % name_or_id, None) + else: + matches = self._list('/v2/images?name=%s' % name_or_id, + "images") + num_matches = len(matches) + if num_matches == 0: + msg = "No %s matching %s." % ( + self.resource_class.__name__, name_or_id) + raise exceptions.NotFound(404, msg) + elif num_matches > 1: + msg = (_("Multiple %(class)s matches found for " + "'%(name)s', use an ID to be more specific.") % + {'class': self.resource_class.__name__.lower(), + 'name': name_or_id}) + raise exceptions.NoUniqueMatch(msg) + else: + matches[0].append_request_ids(matches.request_ids) + return matches[0] + + class ImageManager(base.ManagerWithFind): """ DEPRECATED: Manage :class:`Image` resources. diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index f0f81367a..f3a4fcb2f 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -1274,7 +1274,7 @@ def _print_image(image): info = image._info.copy() # ignore links, we don't need to present those - info.pop('links') + info.pop('links', None) # try to replace a server entity to just an id server = info.pop('server', None) @@ -1322,7 +1322,10 @@ def do_image_delete(cs, args): emit_image_deprecation_warning('image-delete') for image in args.image: try: - _find_image(cs, image).delete() + # _find_image is using the GlanceManager which doesn't implement + # the delete() method so use the ImagesManager for that. + image = _find_image(cs, image) + cs.images.delete(image) except Exception as e: print(_("Delete for image %(image)s failed: %(e)s") % {'image': image, 'e': e}) @@ -2080,7 +2083,7 @@ def do_image_create(cs, args): image_uuid = cs.servers.create_image(server, args.name, meta) if args.poll: - _poll_for_status(cs.images.get, image_uuid, 'snapshotting', + _poll_for_status(cs.glance.find_image, image_uuid, 'snapshotting', ['active']) # NOTE(sirp): A race-condition exists between when the image finishes @@ -2099,7 +2102,7 @@ def do_image_create(cs, args): show_progress=False, silent=True) if args.show: - _print_image(cs.images.get(image_uuid)) + _print_image(_find_image(cs, image_uuid)) @utils.arg('server', metavar='', help=_('Name or ID of server.')) @@ -2253,7 +2256,10 @@ def _find_server(cs, server, raise_if_notfound=True, **find_args): def _find_image(cs, image): """Get an image by name or ID.""" - return utils.find_resource(cs.images, image) + try: + return cs.glance.find_image(image) + except (exceptions.NotFound, exceptions.NoUniqueMatch) as e: + raise exceptions.CommandError(six.text_type(e)) def _find_flavor(cs, flavor): diff --git a/releasenotes/notes/no-glance-proxy-5c13001a4b13e8ce.yaml b/releasenotes/notes/no-glance-proxy-5c13001a4b13e8ce.yaml new file mode 100644 index 000000000..01b65bb76 --- /dev/null +++ b/releasenotes/notes/no-glance-proxy-5c13001a4b13e8ce.yaml @@ -0,0 +1,10 @@ +--- +upgrade: + - | + The 2.36 microversion deprecated the image proxy API. As such, CLI calls + now directly call the image service to get image details, for example, + as a convenience to boot a server with an image name rather than the image + id. To do this the following is assumed: + + #. There is an **image** entry in the service catalog. + #. The image v2 API is available. From f5b19b37382ff5c5f54abf480349c069ef285674 Mon Sep 17 00:00:00 2001 From: Sean Dague Date: Fri, 12 Aug 2016 15:02:36 -0400 Subject: [PATCH 1115/1705] Use neutron for network name -> id resolution The 2.36 microversion deprecates the nova proxy APIs for looking up a network from neutron. The CLI will lookup a network by name for booting a server as a convenience. Over 90% of clouds now have neutron. We use a service catalog lookup to assume that neutron is available, and if not fall back to our proxy. This should mostly shield people from the 2.36 transition. To test this we add support in our fake client for the neutron network lookup. Comments were also left about some cleanups we should do in other parts of the shell testing for net lookup. Related to blueprint deprecate-api-proxies Change-Id: I4f809de4f7efb913c814f132f5b3482731c588c5 --- novaclient/tests/unit/v2/fakes.py | 46 +++++++++++++- novaclient/tests/unit/v2/test_shell.py | 62 ++++++++++++++++--- novaclient/v2/client.py | 20 ++++++ novaclient/v2/networks.py | 33 ++++++++++ novaclient/v2/shell.py | 24 +++++++ .../no-neutron-proxy-18fd54febe939a6b.yaml | 12 ++++ 6 files changed, 187 insertions(+), 10 deletions(-) create mode 100644 releasenotes/notes/no-neutron-proxy-18fd54febe939a6b.yaml diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 59e5e8581..15c44783e 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -158,7 +158,7 @@ def _cs_request(self, url, method, **kwargs): }) return r, body - def get_endpoint(self): + def get_endpoint(self, **kwargs): # check if endpoint matches expected format (eg, v2.1) if (hasattr(self, 'endpoint_type') and ENDPOINT_TYPE_RE.search(self.endpoint_type)): @@ -1909,6 +1909,50 @@ def get_os_hypervisors_region_child_1_uptime(self, **kw): 'hypervisor_hostname': "hyper1", 'uptime': "fake uptime"}}) + def get_v2_0_networks(self, **kw): + """Return neutron proxied networks. + + We establish a few different possible networks that we can get + by name, which we can then call in tests. The only usage of + this API should be for name -> id translation, however a full + valid neutron block is provided for the private network to see + the kinds of things that will be in that payload. + """ + + name = kw.get('name', "blank") + + networks_by_name = { + 'private': [ + {"status": "ACTIVE", + "router:external": False, + "availability_zone_hints": [], + "availability_zones": ["nova"], + "description": "", + "name": "private", + "subnets": ["64706c26-336c-4048-ab3c-23e3283bca2c", + "18512740-c760-4d5f-921f-668105c9ee44"], + "shared": False, + "tenant_id": "abd42f270bca43ea80fe4a166bc3536c", + "created_at": "2016-08-15T17:34:49", + "tags": [], + "ipv6_address_scope": None, + "updated_at": "2016-08-15T17:34:49", + "admin_state_up": True, + "ipv4_address_scope": None, + "port_security_enabled": True, + "mtu": 1450, + "id": "e43a56c7-11d4-45c9-8681-ddc8171b5850", + "revision": 2}], + 'duplicate': [ + {"status": "ACTIVE", + "id": "e43a56c7-11d4-45c9-8681-ddc8171b5850"}, + {"status": "ACTIVE", + "id": "f43a56c7-11d4-45c9-8681-ddc8171b5850"}], + 'blank': [] + } + + return (200, {}, {"networks": networks_by_name[name]}) + def get_os_networks(self, **kw): return (200, {}, {'networks': [{"label": "1", "cidr": "10.0.0.0/24", 'project_id': diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 90469ea1c..821f91081 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -26,6 +26,7 @@ from oslo_utils import timeutils import six from six.moves import builtins +import testtools import novaclient from novaclient import api_versions @@ -671,14 +672,10 @@ def test_boot_nics_net_id_twice(self): '--nic net-id=net-id1,net-id=net-id2 some-server' % FAKE_UUID_1) self.assertRaises(exceptions.CommandError, self.run_command, cmd) - @mock.patch( - 'novaclient.tests.unit.v2.fakes.FakeHTTPClient.get_os_networks') - def test_boot_nics_net_name(self, mock_networks_list): - mock_networks_list.return_value = (200, {}, { - 'networks': [{"label": "some-net", 'id': '1'}]}) - + @mock.patch('novaclient.v2.client.Client.has_neutron', return_value=False) + def test_boot_nics_net_name(self, has_neutron): cmd = ('boot --image %s --flavor 1 ' - '--nic net-name=some-net some-server' % FAKE_UUID_1) + '--nic net-name=1 some-server' % FAKE_UUID_1) self.run_command(cmd) self.assert_called_anytime( 'POST', '/servers', @@ -696,14 +693,61 @@ def test_boot_nics_net_name(self, mock_networks_list): }, ) - def test_boot_nics_net_name_not_found(self): + @mock.patch('novaclient.v2.client.Client.has_neutron', return_value=True) + def test_boot_nics_net_name_neutron(self, has_neutron): + cmd = ('boot --image %s --flavor 1 ' + '--nic net-name=private some-server' % FAKE_UUID_1) + self.run_command(cmd) + self.assert_called_anytime( + 'POST', '/servers', + { + 'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': FAKE_UUID_1, + 'min_count': 1, + 'max_count': 1, + 'networks': [ + {'uuid': 'e43a56c7-11d4-45c9-8681-ddc8171b5850'}, + ], + }, + }, + ) + + @mock.patch('novaclient.v2.client.Client.has_neutron', return_value=True) + def test_boot_nics_net_name_neutron_dup(self, has_neutron): + cmd = ('boot --image %s --flavor 1 ' + '--nic net-name=duplicate some-server' % FAKE_UUID_1) + # this should raise a multiple matches error + msg = ("Multiple network matches found for 'duplicate', " + "use an ID to be more specific.") + with testtools.ExpectedException(exceptions.CommandError, msg): + self.run_command(cmd) + + @mock.patch('novaclient.v2.client.Client.has_neutron', return_value=True) + def test_boot_nics_net_name_neutron_blank(self, has_neutron): + cmd = ('boot --image %s --flavor 1 ' + '--nic net-name=blank some-server' % FAKE_UUID_1) + # this should raise a multiple matches error + msg = 'No Network matching blank\..*' + with testtools.ExpectedException(exceptions.CommandError, msg): + self.run_command(cmd) + + # TODO(sdague): the following tests should really avoid mocking + # out other tests, and they should check the string in the + # CommandError, because it's not really enough to distinguish + # between various errors. + @mock.patch('novaclient.v2.client.Client.has_neutron', return_value=False) + def test_boot_nics_net_name_not_found(self, has_neutron): cmd = ('boot --image %s --flavor 1 ' '--nic net-name=some-net some-server' % FAKE_UUID_1) self.assertRaises(exceptions.ResourceNotFound, self.run_command, cmd) + @mock.patch('novaclient.v2.client.Client.has_neutron', return_value=False) @mock.patch( 'novaclient.tests.unit.v2.fakes.FakeHTTPClient.get_os_networks') - def test_boot_nics_net_name_multiple_matches(self, mock_networks_list): + def test_boot_nics_net_name_multiple_matches(self, mock_networks_list, + has_neutron): mock_networks_list.return_value = (200, {}, { 'networks': [{"label": "some-net", 'id': '1'}, {"label": "some-net", 'id': '2'}]}) diff --git a/novaclient/v2/client.py b/novaclient/v2/client.py index 044653f52..c6d985e25 100644 --- a/novaclient/v2/client.py +++ b/novaclient/v2/client.py @@ -15,6 +15,8 @@ import logging +from keystoneauth1.exceptions import catalog as key_ex + from novaclient import api_versions from novaclient import client from novaclient import exceptions @@ -149,6 +151,7 @@ def __init__(self, username=None, api_key=None, project_id=None, self.volumes = volumes.VolumeManager(self) self.keypairs = keypairs.KeypairManager(self) self.networks = networks.NetworkManager(self) + self.neutron = networks.NeutronManager(self) self.quota_classes = quota_classes.QuotaClassSetManager(self) self.quotas = quotas.QuotaSetManager(self) self.security_groups = security_groups.SecurityGroupManager(self) @@ -233,6 +236,23 @@ def get_timings(self): def reset_timings(self): self.client.reset_timings() + def has_neutron(self): + """Check the service catalog to figure out if we have neutron. + + This is an intermediary solution for the window of time where + we still have nova-network support in the client, but we + expect most people have neutron. This ensures that if they + have neutron we understand, we talk to it, if they don't, we + fail back to nova proxies. + """ + try: + endpoint = self.client.get_endpoint(service_type='network') + if endpoint: + return True + return False + except key_ex.EndpointNotFound: + return False + @client._original_only def authenticate(self): """Authenticate against the server. diff --git a/novaclient/v2/networks.py b/novaclient/v2/networks.py index 22f3d537c..4f545403f 100644 --- a/novaclient/v2/networks.py +++ b/novaclient/v2/networks.py @@ -41,6 +41,39 @@ def delete(self): return self.manager.delete(self) +class NeutronManager(base.Manager): + """A manager for name -> id lookups for neutron networks. + + This uses neutron directly from service catalog. Do not use it + for anything else besides that. You have been warned. + """ + + resource_class = Network + + def find_network(self, name): + """Find a network by name (user provided input).""" + + with self.alternate_service_type( + 'network', allowed_types=('network',)): + + matches = self._list('/v2.0/networks?name=%s' % name, 'networks') + + num_matches = len(matches) + if num_matches == 0: + msg = "No %s matching %s." % ( + self.resource_class.__name__, name) + raise exceptions.NotFound(404, msg) + elif num_matches > 1: + msg = (_("Multiple %(class)s matches found for " + "'%(name)s', use an ID to be more specific.") % + {'class': self.resource_class.__name__.lower(), + 'name': name}) + raise exceptions.NoUniqueMatch(msg) + else: + matches[0].append_request_ids(matches.request_ids) + return matches[0] + + class NetworkManager(base.ManagerWithFind): """ Manage :class:`Network` resources. diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index f3a4fcb2f..971e9879d 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -2270,7 +2270,31 @@ def _find_flavor(cs, flavor): return cs.flavors.find(ram=flavor) +def _find_network_id_neutron(cs, net_name): + """Get unique network ID from network name from neutron""" + try: + return cs.neutron.find_network(net_name).id + except (exceptions.NotFound, exceptions.NoUniqueMatch) as e: + raise exceptions.CommandError(six.text_type(e)) + + def _find_network_id(cs, net_name): + """Find the network id for a network name. + + If we have access to neutron in the service catalog, use neutron + for this lookup, otherwise use nova. This ensures that we do the + right thing in the future. + + Once nova network support is deleted, we can delete this check and + the has_neutron function. + """ + if cs.has_neutron(): + return _find_network_id_neutron(cs, net_name) + else: + return _find_network_id_novanet(cs, net_name) + + +def _find_network_id_novanet(cs, net_name): """Get unique network ID from network name.""" network_id = None for net_info in cs.networks.list(): diff --git a/releasenotes/notes/no-neutron-proxy-18fd54febe939a6b.yaml b/releasenotes/notes/no-neutron-proxy-18fd54febe939a6b.yaml new file mode 100644 index 000000000..21007cd93 --- /dev/null +++ b/releasenotes/notes/no-neutron-proxy-18fd54febe939a6b.yaml @@ -0,0 +1,12 @@ +--- +upgrade: + - | + The 2.36 microversion deprecated the network proxy APIs in + Nova. Because of this we now go directly to neutron for name to + net-id lookups. For nova-net deployements the old proxies will + continue to be used. + + To do this the following is assumed: + + #. There is a **network** entry in the service catalog. + #. The network v2 API is available. From 05bfe1fce8e79a299e5410b59d8058775bc7653c Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Mon, 15 Aug 2016 16:44:59 -0400 Subject: [PATCH 1116/1705] Handle successful response in console functional tests The console functional tests were written under the assumption of how the gate jobs work in our CI system, but with a local devstack you might have a different configuration. For example, we don't run the nova-novnc service in the gate but it's enabled by default in devstack, so functional console tests for vnc will fail in a local default devstack setup. This change fixes the console functional tests to handle the case that a valid response is returned and adds a check for how the response looks so we're at least validating something. Change-Id: I3505c9955d032301b34764f67466c467a79ed9fb Closes-Bug: #1613435 --- .../functional/v2/legacy/test_consoles.py | 20 +++++++++++-------- .../tests/functional/v2/test_consoles.py | 2 +- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/novaclient/tests/functional/v2/legacy/test_consoles.py b/novaclient/tests/functional/v2/legacy/test_consoles.py index 5afec203e..5595ab144 100644 --- a/novaclient/tests/functional/v2/legacy/test_consoles.py +++ b/novaclient/tests/functional/v2/legacy/test_consoles.py @@ -21,28 +21,32 @@ class TestConsolesNovaClient(base.ClientTestBase): COMPUTE_API_VERSION = "2.1" - def _test_console_get(self, command): + def _test_console_get(self, command, expected_response_type): server = self._create_server() completed_command = command % server.id - self.assertRaises(exceptions.CommandFailed, - self.nova, completed_command) try: - self.nova(completed_command) + output = self.nova(completed_command) + # if we didn't fail, check that the expected response type is in + # the output + console_type = self._get_column_value_from_single_row_table( + output, 'Type') + self.assertEqual(expected_response_type, console_type, output) except exceptions.CommandFailed as cf: self.assertTrue('HTTP 400' in str(cf.stderr)) def _test_vnc_console_get(self): - self._test_console_get('get-vnc-console %s novnc') + self._test_console_get('get-vnc-console %s novnc', 'novnc') def _test_spice_console_get(self): - self._test_console_get('get-spice-console %s spice-html5') + self._test_console_get('get-spice-console %s spice-html5', + 'spice-html5') def _test_rdp_console_get(self): - self._test_console_get('get-rdp-console %s rdp-html5') + self._test_console_get('get-rdp-console %s rdp-html5', 'rdp-html5') def _test_serial_console_get(self): - self._test_console_get('get-serial-console %s') + self._test_console_get('get-serial-console %s', 'serial') def test_vnc_console_get(self): self._test_vnc_console_get() diff --git a/novaclient/tests/functional/v2/test_consoles.py b/novaclient/tests/functional/v2/test_consoles.py index 084277b0a..eaad45d49 100644 --- a/novaclient/tests/functional/v2/test_consoles.py +++ b/novaclient/tests/functional/v2/test_consoles.py @@ -38,4 +38,4 @@ class TestConsolesNovaClientV28(test_consoles.TestConsolesNovaClient): COMPUTE_API_VERSION = "2.8" def test_webmks_console_get(self): - self._test_console_get('get-mks-console %s ') + self._test_console_get('get-mks-console %s ', 'webmks') From 56ee58403411652eba066f14c61f917e4d51a02c Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Mon, 15 Aug 2016 17:53:31 -0400 Subject: [PATCH 1117/1705] Make wait_for_server_os_boot wait longer When running the trigger crash dump functional tests on a local devstack VM running with 4VCPUs and 8GB RAM, I'm getting timeouts after 60s waiting for the guest OS to boot. The gate jobs use VMs with 8VCPUs and 8GB RAM, which is fine, but we shouldn't require people to have a VM that size just to run novaclient functional tests reliably. While debugging, I verified that the guest OS is booting, it's just not done in 60 seconds. This change bumps the timeout to 300 seconds, which is the default that Tempest uses for waiting for a server to complete building. It also adds the last console output from the guest to the error message for debugging if/when this fails. Change-Id: I36d43c6e87839856740b2317ee57858ce6357051 Closes-Bug: #1613454 --- novaclient/tests/functional/base.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/novaclient/tests/functional/base.py b/novaclient/tests/functional/base.py index 5d3f95e0c..09a710fdd 100644 --- a/novaclient/tests/functional/base.py +++ b/novaclient/tests/functional/base.py @@ -258,7 +258,7 @@ def wait_for_volume_status(self, volume, status, timeout=60, self.fail("Volume %s did not reach status %s after %d s" % (volume.id, status, timeout)) - def wait_for_server_os_boot(self, server_id, timeout=60, + def wait_for_server_os_boot(self, server_id, timeout=300, poll_interval=1): """Wait until instance's operating system is completely booted. @@ -267,13 +267,15 @@ def wait_for_server_os_boot(self, server_id, timeout=60, :param poll_interval: poll interval in seconds """ start_time = time.time() + console = None while time.time() - start_time < timeout: - if BOOT_IS_COMPLETE in self.nova('console-log %s ' % server_id): + console = self.nova('console-log %s ' % server_id) + if BOOT_IS_COMPLETE in console: break time.sleep(poll_interval) else: - self.fail("Server %s did not boot after %d s" - % (server_id, timeout)) + self.fail("Server %s did not boot after %d s.\nConsole:\n%s" + % (server_id, timeout, console)) def wait_for_resource_delete(self, resource, manager, timeout=60, poll_interval=1): From 0f6f3694fcc6d274a56bd4a1383b9409c7fc5913 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Sat, 13 Aug 2016 22:26:02 -0400 Subject: [PATCH 1118/1705] Skip nova-network-only tests if using Neutron Adds a check in the base class for functional tests to see if we're using neutron and if so, skips tests that only work for nova-network. Change-Id: Iae49145bb2f09ab1752fdec224c60936c4850c64 Closes-Bug: #1612410 --- novaclient/tests/functional/base.py | 17 +++++++++++++++++ .../tests/functional/v2/legacy/test_fixedips.py | 2 ++ .../functional/v2/legacy/test_readonly_nova.py | 2 ++ .../v2/legacy/test_virtual_interface.py | 2 ++ 4 files changed, 23 insertions(+) diff --git a/novaclient/tests/functional/base.py b/novaclient/tests/functional/base.py index 5d3f95e0c..af7e00991 100644 --- a/novaclient/tests/functional/base.py +++ b/novaclient/tests/functional/base.py @@ -95,6 +95,9 @@ class NoCloudConfigException(Exception): pass +USE_NEUTRON = None + + class ClientTestBase(testtools.TestCase): """Base test class for read only python-novaclient commands. @@ -232,6 +235,16 @@ def setUp(self): password=passwd) self.cinder = cinderclient.Client(auth=auth, session=session) + global USE_NEUTRON + if USE_NEUTRON is None: + # check to see if we're running with neutron or not + for service in self.keystone.services.list(): + if service.type == 'network': + USE_NEUTRON = True + break + else: + USE_NEUTRON = False + def nova(self, action, flags='', params='', fail_ok=False, endpoint_type='publicURL', merge_stderr=False): if self.COMPUTE_API_VERSION: @@ -415,6 +428,10 @@ def _get_project_id(self, name): project = self.keystone.tenants.find(name=name) return project.id + def skip_if_neutron(self): + if USE_NEUTRON: + self.skipTest('nova-network is not available') + class TenantTestBase(ClientTestBase): """Base test class for additional tenant and user creation which diff --git a/novaclient/tests/functional/v2/legacy/test_fixedips.py b/novaclient/tests/functional/v2/legacy/test_fixedips.py index 283faa5a4..b752ce26a 100644 --- a/novaclient/tests/functional/v2/legacy/test_fixedips.py +++ b/novaclient/tests/functional/v2/legacy/test_fixedips.py @@ -22,6 +22,8 @@ class TestFixedIPsNovaClient(base.ClientTestBase): COMPUTE_API_VERSION = '2.1' def _test_fixedip_get(self, expect_reserved=False): + # os-fixed-ips does not proxy to neutron + self.skip_if_neutron() server = self._create_server(with_network=False) networks = server.networks self.assertIn('private', networks) diff --git a/novaclient/tests/functional/v2/legacy/test_readonly_nova.py b/novaclient/tests/functional/v2/legacy/test_readonly_nova.py index d8ac5d0e5..09eecdd5b 100644 --- a/novaclient/tests/functional/v2/legacy/test_readonly_nova.py +++ b/novaclient/tests/functional/v2/legacy/test_readonly_nova.py @@ -50,10 +50,12 @@ def test_admin_credentials(self): # "Neutron does not provide this feature" def test_admin_dns_domains(self): + self.skip_if_neutron() self.nova('dns-domains') @decorators.skip_because(bug="1157349") def test_admin_dns_list(self): + self.skip_if_neutron() self.nova('dns-list') def test_admin_endpoints(self): diff --git a/novaclient/tests/functional/v2/legacy/test_virtual_interface.py b/novaclient/tests/functional/v2/legacy/test_virtual_interface.py index f37b19be2..8e934f650 100644 --- a/novaclient/tests/functional/v2/legacy/test_virtual_interface.py +++ b/novaclient/tests/functional/v2/legacy/test_virtual_interface.py @@ -20,6 +20,8 @@ class TestVirtualInterfacesNovaClient(base.ClientTestBase): COMPUTE_API_VERSION = "2.1" def test_virtual_interface_list(self): + # os-virtual-interfaces does not proxy to neutron + self.skip_if_neutron() server = self._create_server() output = self.nova('virtual-interface-list %s' % server.id) self.assertTrue(len(output.split("\n")) > 5, From 95ad95e280d34996a49bf8ab4edc6398b290f25b Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Wed, 17 Aug 2016 12:07:40 -0400 Subject: [PATCH 1119/1705] Use glanceclient for functional tests With the 2.36 microversion in nova the images proxy API is deprecated and returns a 404. This change makes the functional tests use glanceclient directly for listing images. Change-Id: If02bc97a10f1551a707e8e1cdb548fc27ef22ce1 --- novaclient/tests/functional/base.py | 5 ++++- test-requirements.txt | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/novaclient/tests/functional/base.py b/novaclient/tests/functional/base.py index af7e00991..08b6b0daa 100644 --- a/novaclient/tests/functional/base.py +++ b/novaclient/tests/functional/base.py @@ -16,6 +16,7 @@ from cinderclient.v2 import client as cinderclient import fixtures +from glanceclient import client as glanceclient from keystoneauth1.exceptions import discovery as discovery_exc from keystoneauth1 import identity from keystoneauth1 import session as ksession @@ -208,9 +209,11 @@ def setUp(self): self.client = novaclient.client.Client(version, session=session) + self.glance = glanceclient.Client('2', session=session) + # pick some reasonable flavor / image combo self.flavor = pick_flavor(self.client.flavors.list()) - self.image = pick_image(self.client.images.list()) + self.image = pick_image(self.glance.images.list()) self.network = pick_network(self.client.networks.list()) # create a CLI client in case we'd like to do CLI diff --git a/test-requirements.txt b/test-requirements.txt index ba0b0aa11..34c9e20d4 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -10,6 +10,7 @@ keyring>=5.5.1 # MIT/PSF mock>=2.0 # BSD python-keystoneclient!=2.1.0,>=2.0.0 # Apache-2.0 python-cinderclient!=1.7.0,!=1.7.1,>=1.6.0 # Apache-2.0 +python-glanceclient>=2.0.0 # Apache-2.0 requests-mock>=1.0 # Apache-2.0 sphinx!=1.3b1,<1.3,>=1.2.1 # BSD os-client-config!=1.19.0,>=1.13.1 # Apache-2.0 From 4f0871fe2cd1f6ae9dd8683a556d15a9d941687f Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Thu, 18 Aug 2016 15:02:14 +0300 Subject: [PATCH 1120/1705] Add eggs to gitignore list Change-Id: I41bb26723d8b5cb9abaabc3d417842d875b3cc92 --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index a0bd6f33a..372efb948 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ ChangeLog novaclient/versioninfo *.egg *egg-info +.eggs # Files created by releasenotes build releasenotes/build From 65c1fbae702fe10db22c70e148381389d64f1f67 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Thu, 18 Aug 2016 18:34:08 +0300 Subject: [PATCH 1121/1705] Store api_version object in one place Currently we store api_version in two places: client and HTTPClient/SessionClient instances. It would be nice to store it in one place, so it would be easier to change api_version without recreating novaclient object. Change-Id: I36369a126c20de5922141e8725834ca93d6b0a1a --- novaclient/tests/unit/test_base.py | 7 ++--- .../unit/v2/contrib/test_list_extensions.py | 4 ++- .../tests/unit/v2/contrib/test_migrations.py | 3 +- novaclient/tests/unit/v2/fakes.py | 9 +++--- .../tests/unit/v2/test_flavor_access.py | 29 ++++++++--------- novaclient/tests/unit/v2/test_flavors.py | 6 ++-- .../tests/unit/v2/test_quota_classes.py | 19 ++++++------ novaclient/tests/unit/v2/test_services.py | 11 +++---- novaclient/tests/unit/v2/test_usage.py | 6 ++-- novaclient/tests/unit/v2/test_versions.py | 9 ++++-- novaclient/tests/unit/v2/test_volumes.py | 31 +++++++++++-------- novaclient/v2/client.py | 10 ++++-- 12 files changed, 77 insertions(+), 67 deletions(-) diff --git a/novaclient/tests/unit/test_base.py b/novaclient/tests/unit/test_base.py index a24636ca5..74ceca397 100644 --- a/novaclient/tests/unit/test_base.py +++ b/novaclient/tests/unit/test_base.py @@ -14,6 +14,7 @@ from requests import Response import six +from novaclient import api_versions from novaclient import base from novaclient import exceptions from novaclient.tests.unit import utils @@ -21,9 +22,6 @@ from novaclient.v2 import flavors -cs = fakes.FakeClient() - - def create_response_obj_with_header(): resp = Response() resp.headers['x-openstack-request-id'] = fakes.FAKE_REQUEST_ID @@ -37,7 +35,6 @@ def create_response_obj_with_compute_header(): class BaseTest(utils.TestCase): - def test_resource_repr(self): r = base.Resource(None, dict(foo="bar", baz="spam")) self.assertEqual("", repr(r)) @@ -50,6 +47,7 @@ class TmpObject(object): self.assertEqual(4, base.getid(TmpObject)) def test_resource_lazy_getattr(self): + cs = fakes.FakeClient(api_versions.APIVersion("2.0")) f = flavors.Flavor(cs.flavors, {'id': 1}) self.assertEqual('256 MB Server', f.name) cs.assert_called('GET', '/flavors/1') @@ -74,6 +72,7 @@ def test_eq(self): self.assertEqual(r1, r2) def test_findall_invalid_attribute(self): + cs = fakes.FakeClient(api_versions.APIVersion("2.0")) # Make sure findall with an invalid attribute doesn't cause errors. # The following should not raise an exception. cs.flavors.findall(vegetable='carrot') diff --git a/novaclient/tests/unit/v2/contrib/test_list_extensions.py b/novaclient/tests/unit/v2/contrib/test_list_extensions.py index 5c6d369d2..02fe296ca 100644 --- a/novaclient/tests/unit/v2/contrib/test_list_extensions.py +++ b/novaclient/tests/unit/v2/contrib/test_list_extensions.py @@ -11,6 +11,7 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient import api_versions from novaclient import extension from novaclient.tests.unit import utils from novaclient.tests.unit.v2 import fakes @@ -24,7 +25,8 @@ def setUp(self): extension.Extension(list_extensions.__name__.split(".")[-1], list_extensions), ] - self.cs = fakes.FakeClient(extensions=extensions) + self.cs = fakes.FakeClient(api_versions.APIVersion("2.0"), + extensions=extensions) def test_list_extensions(self): all_exts = self.cs.list_extensions.show_all() diff --git a/novaclient/tests/unit/v2/contrib/test_migrations.py b/novaclient/tests/unit/v2/contrib/test_migrations.py index d203e64ca..4cf5d45eb 100644 --- a/novaclient/tests/unit/v2/contrib/test_migrations.py +++ b/novaclient/tests/unit/v2/contrib/test_migrations.py @@ -24,7 +24,8 @@ def setUp(self): extension.Extension(migrations.__name__.split(".")[-1], migrations), ] - self.cs = fakes.FakeClient(extensions=self.extensions) + self.cs = fakes.FakeClient(api_versions.APIVersion("2.0"), + extensions=self.extensions) def test_list_migrations(self): ml = self.cs.migrations.list() diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 15c44783e..58d5b39ed 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -56,13 +56,12 @@ class FakeClient(fakes.FakeClient, client.Client): - def __init__(self, api_version=None, *args, **kwargs): + def __init__(self, api_version, *args, **kwargs): client.Client.__init__(self, 'username', 'password', 'project_id', 'auth_url', extensions=kwargs.get('extensions'), direct_use=False) - self.api_version = api_version or api_versions.APIVersion("2.1") - kwargs["api_version"] = self.api_version + kwargs["api_version"] = api_version self.client = FakeHTTPClient(**kwargs) @@ -2368,8 +2367,8 @@ def __init__(self, api_version, *args, **kwargs): client.Client.__init__(self, 'username', 'password', 'project_id', 'auth_url', extensions=kwargs.get('extensions'), - api_version=api_version, direct_use=False) - kwargs['api_version'] = api_version + direct_use=False) + kwargs["api_version"] = api_version self.client = FakeSessionMockClient(**kwargs) diff --git a/novaclient/tests/unit/v2/test_flavor_access.py b/novaclient/tests/unit/v2/test_flavor_access.py index c6cafdf86..1c604f91e 100644 --- a/novaclient/tests/unit/v2/test_flavor_access.py +++ b/novaclient/tests/unit/v2/test_flavor_access.py @@ -13,28 +13,29 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient import api_versions from novaclient.tests.unit import utils from novaclient.tests.unit.v2 import fakes from novaclient.v2 import flavor_access -cs = fakes.FakeClient() - - class FlavorAccessTest(utils.TestCase): + def setUp(self): + super(FlavorAccessTest, self).setUp() + self.cs = fakes.FakeClient(api_versions.APIVersion("2.0")) def test_list_access_by_flavor_private(self): - kwargs = {'flavor': cs.flavors.get(2)} - r = cs.flavor_access.list(**kwargs) + kwargs = {'flavor': self.cs.flavors.get(2)} + r = self.cs.flavor_access.list(**kwargs) self.assert_request_id(r, fakes.FAKE_REQUEST_ID_LIST) - cs.assert_called('GET', '/flavors/2/os-flavor-access') + self.cs.assert_called('GET', '/flavors/2/os-flavor-access') for a in r: self.assertIsInstance(a, flavor_access.FlavorAccess) def test_add_tenant_access(self): - flavor = cs.flavors.get(2) + flavor = self.cs.flavors.get(2) tenant = 'proj2' - r = cs.flavor_access.add_tenant_access(flavor, tenant) + r = self.cs.flavor_access.add_tenant_access(flavor, tenant) self.assert_request_id(r, fakes.FAKE_REQUEST_ID_LIST) body = { @@ -43,14 +44,14 @@ def test_add_tenant_access(self): } } - cs.assert_called('POST', '/flavors/2/action', body) + self.cs.assert_called('POST', '/flavors/2/action', body) for a in r: self.assertIsInstance(a, flavor_access.FlavorAccess) def test_remove_tenant_access(self): - flavor = cs.flavors.get(2) + flavor = self.cs.flavors.get(2) tenant = 'proj2' - r = cs.flavor_access.remove_tenant_access(flavor, tenant) + r = self.cs.flavor_access.remove_tenant_access(flavor, tenant) self.assert_request_id(r, fakes.FAKE_REQUEST_ID_LIST) body = { @@ -59,14 +60,14 @@ def test_remove_tenant_access(self): } } - cs.assert_called('POST', '/flavors/2/action', body) + self.cs.assert_called('POST', '/flavors/2/action', body) for a in r: self.assertIsInstance(a, flavor_access.FlavorAccess) def test_repr_flavor_access(self): - flavor = cs.flavors.get(2) + flavor = self.cs.flavors.get(2) tenant = 'proj3' - r = cs.flavor_access.add_tenant_access(flavor, tenant) + r = self.cs.flavor_access.add_tenant_access(flavor, tenant) def get_expected(flavor_access): return ("" % diff --git a/novaclient/tests/unit/v2/test_flavors.py b/novaclient/tests/unit/v2/test_flavors.py index 70eb8e40a..14bf78b3d 100644 --- a/novaclient/tests/unit/v2/test_flavors.py +++ b/novaclient/tests/unit/v2/test_flavors.py @@ -15,6 +15,7 @@ import mock +from novaclient import api_versions from novaclient import base from novaclient import exceptions from novaclient.tests.unit import utils @@ -25,12 +26,9 @@ class FlavorsTest(utils.TestCase): def setUp(self): super(FlavorsTest, self).setUp() - self.cs = self._get_fake_client() + self.cs = fakes.FakeClient(api_versions.APIVersion("2.0")) self.flavor_type = self._get_flavor_type() - def _get_fake_client(self): - return fakes.FakeClient() - def _get_flavor_type(self): return flavors.Flavor diff --git a/novaclient/tests/unit/v2/test_quota_classes.py b/novaclient/tests/unit/v2/test_quota_classes.py index c45c731ef..a1da954d1 100644 --- a/novaclient/tests/unit/v2/test_quota_classes.py +++ b/novaclient/tests/unit/v2/test_quota_classes.py @@ -13,30 +13,31 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient import api_versions from novaclient.tests.unit import utils from novaclient.tests.unit.v2 import fakes -cs = fakes.FakeClient() - - class QuotaClassSetsTest(utils.TestCase): + def setUp(self): + super(QuotaClassSetsTest, self).setUp() + self.cs = fakes.FakeClient(api_versions.APIVersion("2.0")) def test_class_quotas_get(self): class_name = 'test' - q = cs.quota_classes.get(class_name) + q = self.cs.quota_classes.get(class_name) self.assert_request_id(q, fakes.FAKE_REQUEST_ID_LIST) - cs.assert_called('GET', '/os-quota-class-sets/%s' % class_name) + self.cs.assert_called('GET', '/os-quota-class-sets/%s' % class_name) def test_update_quota(self): - q = cs.quota_classes.get('test') + q = self.cs.quota_classes.get('test') self.assert_request_id(q, fakes.FAKE_REQUEST_ID_LIST) q.update(cores=2) - cs.assert_called('PUT', '/os-quota-class-sets/test') + self.cs.assert_called('PUT', '/os-quota-class-sets/test') def test_refresh_quota(self): - q = cs.quota_classes.get('test') - q2 = cs.quota_classes.get('test') + q = self.cs.quota_classes.get('test') + q2 = self.cs.quota_classes.get('test') self.assertEqual(q.cores, q2.cores) q2.cores = 0 self.assertNotEqual(q.cores, q2.cores) diff --git a/novaclient/tests/unit/v2/test_services.py b/novaclient/tests/unit/v2/test_services.py index 16f068e7b..320f839ac 100644 --- a/novaclient/tests/unit/v2/test_services.py +++ b/novaclient/tests/unit/v2/test_services.py @@ -20,14 +20,13 @@ class ServicesTest(utils.TestCase): + api_version = "2.0" + def setUp(self): super(ServicesTest, self).setUp() - self.cs = self._get_fake_client() + self.cs = fakes.FakeClient(api_versions.APIVersion(self.api_version)) self.service_type = self._get_service_type() - def _get_fake_client(self): - return fakes.FakeClient() - def _get_service_type(self): return services.Service @@ -109,9 +108,7 @@ def test_services_disable_log_reason(self): class ServicesV211TestCase(ServicesTest): - def setUp(self): - super(ServicesV211TestCase, self).setUp() - self.cs.api_version = api_versions.APIVersion("2.11") + api_version = "2.11" def _update_body(self, host, binary, disabled_reason=None, force_down=None): diff --git a/novaclient/tests/unit/v2/test_usage.py b/novaclient/tests/unit/v2/test_usage.py index 3109e77f3..e67178b41 100644 --- a/novaclient/tests/unit/v2/test_usage.py +++ b/novaclient/tests/unit/v2/test_usage.py @@ -15,6 +15,7 @@ import six +from novaclient import api_versions from novaclient.tests.unit import utils from novaclient.tests.unit.v2 import fakes from novaclient.v2 import usage @@ -23,12 +24,9 @@ class UsageTest(utils.TestCase): def setUp(self): super(UsageTest, self).setUp() - self.cs = self._get_fake_client() + self.cs = fakes.FakeClient(api_versions.APIVersion("2.0")) self.usage_type = self._get_usage_type() - def _get_fake_client(self): - return fakes.FakeClient() - def _get_usage_type(self): return usage.Usage diff --git a/novaclient/tests/unit/v2/test_versions.py b/novaclient/tests/unit/v2/test_versions.py index f88f534b7..30496a7ea 100644 --- a/novaclient/tests/unit/v2/test_versions.py +++ b/novaclient/tests/unit/v2/test_versions.py @@ -14,6 +14,7 @@ import mock +from novaclient import api_versions from novaclient import base from novaclient import exceptions as exc from novaclient.tests.unit import utils @@ -24,7 +25,7 @@ class VersionsTest(utils.TestCase): def setUp(self): super(VersionsTest, self).setUp() - self.cs = fakes.FakeClient() + self.cs = fakes.FakeClient(api_versions.APIVersion("2.0")) self.service_type = versions.Version @mock.patch.object(versions.VersionManager, '_is_session_client', @@ -98,7 +99,8 @@ def test_get_endpoint_without_project_id(self, mock_is_session_client): # doesn't return uuid in url endpoint_type = 'v2.1' expected_endpoint = 'http://nova-api:8774/v2.1/' - cs_2_1 = fakes.FakeClient(endpoint_type=endpoint_type) + cs_2_1 = fakes.FakeClient(api_versions.APIVersion("2.0"), + endpoint_type=endpoint_type) result = cs_2_1.versions.get_current() self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) @@ -117,7 +119,8 @@ def test_v2_get_endpoint_without_project_id(self, mock_is_session_client): # doesn't return uuid in url endpoint_type = 'v2' expected_endpoint = 'http://nova-api:8774/v2/' - cs_2 = fakes.FakeClient(endpoint_type=endpoint_type) + cs_2 = fakes.FakeClient(api_versions.APIVersion("2.0"), + endpoint_type=endpoint_type) result = cs_2.versions.get_current() self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) diff --git a/novaclient/tests/unit/v2/test_volumes.py b/novaclient/tests/unit/v2/test_volumes.py index 6ff7367e2..17c4df5d0 100644 --- a/novaclient/tests/unit/v2/test_volumes.py +++ b/novaclient/tests/unit/v2/test_volumes.py @@ -13,51 +13,56 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient import api_versions from novaclient.tests.unit import utils from novaclient.tests.unit.v2 import fakes from novaclient.v2 import volumes -cs = fakes.FakeClient() - - class VolumesTest(utils.TestCase): + def setUp(self): + super(VolumesTest, self).setUp() + self.cs = fakes.FakeClient(api_versions.APIVersion("2.0")) def test_create_server_volume(self): - v = cs.volumes.create_server_volume( + v = self.cs.volumes.create_server_volume( server_id=1234, volume_id='15e59938-07d5-11e1-90e3-e3dffe0c5983', device='/dev/vdb' ) self.assert_request_id(v, fakes.FAKE_REQUEST_ID_LIST) - cs.assert_called('POST', '/servers/1234/os-volume_attachments') + self.cs.assert_called('POST', '/servers/1234/os-volume_attachments') self.assertIsInstance(v, volumes.Volume) def test_update_server_volume(self): vol_id = '15e59938-07d5-11e1-90e3-e3dffe0c5983' - v = cs.volumes.update_server_volume( + v = self.cs.volumes.update_server_volume( server_id=1234, attachment_id='Work', new_volume_id=vol_id ) self.assert_request_id(v, fakes.FAKE_REQUEST_ID_LIST) - cs.assert_called('PUT', '/servers/1234/os-volume_attachments/Work') + self.cs.assert_called('PUT', + '/servers/1234/os-volume_attachments/Work') self.assertIsInstance(v, volumes.Volume) def test_get_server_volume(self): - v = cs.volumes.get_server_volume(1234, 'Work') + v = self.cs.volumes.get_server_volume(1234, 'Work') self.assert_request_id(v, fakes.FAKE_REQUEST_ID_LIST) - cs.assert_called('GET', '/servers/1234/os-volume_attachments/Work') + self.cs.assert_called('GET', + '/servers/1234/os-volume_attachments/Work') self.assertIsInstance(v, volumes.Volume) def test_list_server_volumes(self): - vl = cs.volumes.get_server_volumes(1234) + vl = self.cs.volumes.get_server_volumes(1234) self.assert_request_id(vl, fakes.FAKE_REQUEST_ID_LIST) - cs.assert_called('GET', '/servers/1234/os-volume_attachments') + self.cs.assert_called('GET', + '/servers/1234/os-volume_attachments') for v in vl: self.assertIsInstance(v, volumes.Volume) def test_delete_server_volume(self): - ret = cs.volumes.delete_server_volume(1234, 'Work') + ret = self.cs.volumes.delete_server_volume(1234, 'Work') self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) - cs.assert_called('DELETE', '/servers/1234/os-volume_attachments/Work') + self.cs.assert_called('DELETE', + '/servers/1234/os-volume_attachments/Work') diff --git a/novaclient/v2/client.py b/novaclient/v2/client.py index c6d985e25..a43a38254 100644 --- a/novaclient/v2/client.py +++ b/novaclient/v2/client.py @@ -17,7 +17,6 @@ from keystoneauth1.exceptions import catalog as key_ex -from novaclient import api_versions from novaclient import client from novaclient import exceptions from novaclient.i18n import _LE @@ -137,7 +136,6 @@ def __init__(self, username=None, api_key=None, project_id=None, self.limits = limits.LimitsManager(self) self.servers = servers.ServerManager(self) self.versions = versions.VersionManager(self) - self.api_version = api_version or api_versions.APIVersion("2.1") # extensions self.agents = agents.AgentsManager(self) @@ -217,6 +215,14 @@ def __init__(self, username=None, api_key=None, project_id=None, logger=logger, **kwargs) + @property + def api_version(self): + return self.client.api_version + + @api_version.setter + def api_version(self, value): + self.client.api_version = value + @client._original_only def __enter__(self): self.client.open_session() From c3b5365cdf2500a5bd1d4c2bb940d2b4e394fc02 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 19 Aug 2016 08:58:29 +0000 Subject: [PATCH 1122/1705] Updated from global requirements Change-Id: Ic0dfb60b5ca2baa03da08de9dcb3f7f4a15b7e56 --- test-requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test-requirements.txt b/test-requirements.txt index 34c9e20d4..702e21bff 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -3,17 +3,17 @@ # process, which may cause wedges in the gate later. hacking<0.11,>=0.10.0 -bandit>=1.0.1 # Apache-2.0 +bandit>=1.1.0 # Apache-2.0 coverage>=3.6 # Apache-2.0 fixtures>=3.0.0 # Apache-2.0/BSD keyring>=5.5.1 # MIT/PSF mock>=2.0 # BSD python-keystoneclient!=2.1.0,>=2.0.0 # Apache-2.0 python-cinderclient!=1.7.0,!=1.7.1,>=1.6.0 # Apache-2.0 -python-glanceclient>=2.0.0 # Apache-2.0 +python-glanceclient>=2.0.0 # Apache-2.0 requests-mock>=1.0 # Apache-2.0 sphinx!=1.3b1,<1.3,>=1.2.1 # BSD -os-client-config!=1.19.0,>=1.13.1 # Apache-2.0 +os-client-config!=1.19.0,!=1.19.1,!=1.20.0,>=1.13.1 # Apache-2.0 oslosphinx!=3.4.0,>=2.5.0 # Apache-2.0 testrepository>=0.0.18 # Apache-2.0/BSD testscenarios>=0.4 # Apache-2.0/BSD From 9c5c8a685be9ca1ca1e2f7bad93e339e12c66a37 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Thu, 18 Aug 2016 15:49:10 +0300 Subject: [PATCH 1123/1705] [functional] Skip tests if API doesn't support microversion To allow launching functional tests of latest novaclient with previous OpenStack releases, we should skip those tests which check unsupported microversions. Change-Id: I24f671371d603c9b2753529c913143648325eda3 --- novaclient/tests/functional/base.py | 47 +++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/novaclient/tests/functional/base.py b/novaclient/tests/functional/base.py index 494d8aab6..a73033306 100644 --- a/novaclient/tests/functional/base.py +++ b/novaclient/tests/functional/base.py @@ -97,6 +97,7 @@ class NoCloudConfigException(Exception): USE_NEUTRON = None +SERVER_API_VERSIONS = None class ClientTestBase(testtools.TestCase): @@ -194,11 +195,6 @@ def setUp(self): else: self.insecure = False - if self.COMPUTE_API_VERSION == "2.latest": - version = novaclient.API_MAX_VERSION.get_string() - else: - version = self.COMPUTE_API_VERSION or "2" - auth = identity.Password(username=user, password=passwd, project_name=tenant, @@ -207,7 +203,7 @@ def setUp(self): user_domain_id=user_domain_id) session = ksession.Session(auth=auth, verify=(not self.insecure)) - self.client = novaclient.client.Client(version, session=session) + self.client = self._get_novaclient(session) self.glance = glanceclient.Client('2', session=session) @@ -248,6 +244,45 @@ def setUp(self): else: USE_NEUTRON = False + def _get_novaclient(self, session): + nc = novaclient.client.Client("2", session=session) + + if self.COMPUTE_API_VERSION: + global SERVER_API_VERSIONS + if SERVER_API_VERSIONS is None: + # Obtain supported versions by API side + v = nc.versions.get_current() + if not hasattr(v, 'version') or not v.version: + # API doesn't support microversions + SERVER_API_VERSIONS = ( + novaclient.api_versions.APIVersion("2.0"), + novaclient.api_versions.APIVersion("2.0")) + else: + SERVER_API_VERSIONS = ( + novaclient.api_versions.APIVersion(v.min_version), + novaclient.api_versions.APIVersion(v.version)) + + if self.COMPUTE_API_VERSION == "2.latest": + requested_version = min(novaclient.API_MAX_VERSION, + SERVER_API_VERSIONS[1]) + else: + requested_version = novaclient.api_versions.APIVersion( + self.COMPUTE_API_VERSION) + + if not requested_version.matches(*SERVER_API_VERSIONS): + msg = ("%s is not supported by Nova-API. Supported version" % + self.COMPUTE_API_VERSION) + if SERVER_API_VERSIONS[0] == SERVER_API_VERSIONS[1]: + msg += ": %s" % SERVER_API_VERSIONS[0].get_string() + else: + msg += "s: %s - %s" % ( + SERVER_API_VERSIONS[0].get_string(), + SERVER_API_VERSIONS[1].get_string()) + self.skipTest(msg) + + nc.api_version = requested_version + return nc + def nova(self, action, flags='', params='', fail_ok=False, endpoint_type='publicURL', merge_stderr=False): if self.COMPUTE_API_VERSION: From 578c39865d41158de185b90ca4cecc7fbb8f59ae Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Thu, 18 Aug 2016 10:03:11 -0400 Subject: [PATCH 1124/1705] Deprecate network-* commands and clamp to microversion 2.35 This introduces a helper to clamp the client microversion to 2.35, which is the last version to support the network proxy. We print a deprecation warning if those commands are used, and mark them as deprecated in the help text. This is a network-specific user-friendly bit of sugar to make sure that nova-network users aren't cut out before we actually drop the support for it on the server side. Note that quotas and limits are special because only the network related resources in those are not returned with 2.36. So this change handles 2.36 separately for quota-update and quota-class-update, and deprecates the network resource quota update arguments for <2.35 as an indication those are going away. As expected, several of the functional tests have to be updated to work with the new world that is microversion 2.36. Related to blueprint deprecate-api-proxies Co-Authored-By: Matt Riedemann Change-Id: Id68c2dbef29b201aa7c8ef9417432feb5596529a --- novaclient/__init__.py | 2 +- novaclient/tests/functional/base.py | 11 +- .../functional/v2/legacy/test_servers.py | 3 +- .../tests/functional/v2/test_networks.py | 46 ++++ novaclient/tests/functional/v2/test_quotas.py | 18 +- .../tests/functional/v2/test_readonly_nova.py | 14 ++ .../functional/v2/test_virtual_interface.py | 3 +- novaclient/tests/unit/v2/test_shell.py | 68 +++++ novaclient/v2/contrib/tenant_networks.py | 5 + novaclient/v2/shell.py | 233 +++++++++++++++++- ...eprecate-network-cli-f0a539528be594d3.yaml | 67 +++++ 11 files changed, 461 insertions(+), 9 deletions(-) create mode 100644 novaclient/tests/functional/v2/test_networks.py create mode 100644 releasenotes/notes/deprecate-network-cli-f0a539528be594d3.yaml diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 6b728d1f9..942f43cad 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.35") +API_MAX_VERSION = api_versions.APIVersion("2.36") diff --git a/novaclient/tests/functional/base.py b/novaclient/tests/functional/base.py index 494d8aab6..0b2190350 100644 --- a/novaclient/tests/functional/base.py +++ b/novaclient/tests/functional/base.py @@ -214,7 +214,16 @@ def setUp(self): # pick some reasonable flavor / image combo self.flavor = pick_flavor(self.client.flavors.list()) self.image = pick_image(self.glance.images.list()) - self.network = pick_network(self.client.networks.list()) + + tested_api_version = self.client.api_version + proxy_api_version = novaclient.api_versions.APIVersion('2.35') + if tested_api_version > proxy_api_version: + self.client.api_version = proxy_api_version + try: + # TODO(mriedem): Get the networks from neutron if using neutron. + self.network = pick_network(self.client.networks.list()) + finally: + self.client.api_version = tested_api_version # create a CLI client in case we'd like to do CLI # testing. tempest.lib does this really weird thing where it diff --git a/novaclient/tests/functional/v2/legacy/test_servers.py b/novaclient/tests/functional/v2/legacy/test_servers.py index a06b3037a..d55fed21c 100644 --- a/novaclient/tests/functional/v2/legacy/test_servers.py +++ b/novaclient/tests/functional/v2/legacy/test_servers.py @@ -56,13 +56,12 @@ def test_boot_server_with_legacy_bdm_volume_id_only(self): self._boot_server_with_legacy_bdm() def test_boot_server_with_net_name(self): - network = self.client.networks.list()[0] server_info = self.nova("boot", params=( "%(name)s --flavor %(flavor)s --image %(image)s --poll " "--nic net-name=%(net-name)s" % {"name": str(uuid.uuid4()), "image": self.image.id, "flavor": self.flavor.id, - "net-name": network.label})) + "net-name": self.network.label})) server_id = self._get_value_from_the_table(server_info, "id") self.client.servers.delete(server_id) diff --git a/novaclient/tests/functional/v2/test_networks.py b/novaclient/tests/functional/v2/test_networks.py new file mode 100644 index 000000000..6fa3467f6 --- /dev/null +++ b/novaclient/tests/functional/v2/test_networks.py @@ -0,0 +1,46 @@ +# Copyright 2016 Red Hat, Inc. +# 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 novaclient.tests.functional import base + + +class TestNetworkCommandsV2_36(base.ClientTestBase): + """Deprecated network command functional tests.""" + + # Proxy APIs were deprecated in 2.36 but the CLI should fallback to 2.35 + # and emit a warning. + COMPUTE_API_VERSION = "2.36" + + def test_command_deprecation(self): + output = self.nova('network-list', merge_stderr=True) + self.assertIn( + 'is deprecated', output, + 'network-list command did not print deprecation warning') + + def test_limits(self): + """Tests that 2.36 won't return network-related resource limits and + the CLI output won't show them. + """ + output = self.nova('limits') + # assert that SecurityGroups isn't in the table output + self.assertRaises(ValueError, self._get_value_from_the_table, + output, 'SecurityGroups') + + def test_quota_show(self): + """Tests that 2.36 won't return network-related resource quotas and + the CLI output won't show them. + """ + output = self.nova('quota-show') + # assert that security_groups isn't in the table output + self.assertRaises(ValueError, self._get_value_from_the_table, + output, 'security_groups') diff --git a/novaclient/tests/functional/v2/test_quotas.py b/novaclient/tests/functional/v2/test_quotas.py index fdfb06ac3..1d4e6af31 100644 --- a/novaclient/tests/functional/v2/test_quotas.py +++ b/novaclient/tests/functional/v2/test_quotas.py @@ -13,10 +13,10 @@ from novaclient.tests.functional.v2.legacy import test_quotas -class TestQuotasNovaClient(test_quotas.TestQuotasNovaClient): +class TestQuotasNovaClient2_35(test_quotas.TestQuotasNovaClient): """Nova quotas functional tests.""" - COMPUTE_API_VERSION = "2.latest" + COMPUTE_API_VERSION = "2.35" _quota_resources = ['instances', 'cores', 'ram', 'floating_ips', 'fixed_ips', 'metadata_items', @@ -47,3 +47,17 @@ def test_quotas_update(self): for quota_name in self._quota_resources: self.assertEqual(getattr(original_quotas, quota_name), getattr(updated_quotas, quota_name) - difference) + + +class TestQuotasNovaClient2_36(TestQuotasNovaClient2_35): + """Nova quotas functional tests.""" + + COMPUTE_API_VERSION = "2.latest" + + # The 2.36 microversion stops proxying network quota resources like + # floating/fixed IPs and security groups/rules. + _quota_resources = ['instances', 'cores', 'ram', + 'metadata_items', 'injected_files', + 'injected_file_content_bytes', + 'injected_file_path_bytes', 'key_pairs', + 'server_groups', 'server_group_members'] diff --git a/novaclient/tests/functional/v2/test_readonly_nova.py b/novaclient/tests/functional/v2/test_readonly_nova.py index 7ef948eea..aee408c9c 100644 --- a/novaclient/tests/functional/v2/test_readonly_nova.py +++ b/novaclient/tests/functional/v2/test_readonly_nova.py @@ -10,6 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. +import six +from tempest.lib import exceptions + +from novaclient import api_versions from novaclient.tests.functional.v2.legacy import test_readonly_nova @@ -22,3 +26,13 @@ class SimpleReadOnlyNovaClientTest( """ COMPUTE_API_VERSION = "2.latest" + + def test_admin_image_list(self): + # The nova images proxy API returns a 404 after 2.35. + if self.client.api_version > api_versions.APIVersion('2.35'): + ex = self.assertRaises(exceptions.CommandFailed, + super(SimpleReadOnlyNovaClientTest, self). + test_admin_image_list) + self.assertIn('NotFound', six.text_type(ex)) + else: + super(SimpleReadOnlyNovaClientTest, self).test_admin_image_list() diff --git a/novaclient/tests/functional/v2/test_virtual_interface.py b/novaclient/tests/functional/v2/test_virtual_interface.py index 46f682485..cef930266 100644 --- a/novaclient/tests/functional/v2/test_virtual_interface.py +++ b/novaclient/tests/functional/v2/test_virtual_interface.py @@ -22,7 +22,6 @@ class TestVirtualInterfacesNovaClient( def test_virtual_interface_list(self): output = super(TestVirtualInterfacesNovaClient, self).test_virtual_interface_list() - network = self.client.networks.list()[0] - self.assertEqual(network.id, + self.assertEqual(self.network.id, self._get_column_value_from_single_row_table( output, "Network ID")) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 821f91081..db8f1abf4 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -693,6 +693,38 @@ def test_boot_nics_net_name(self, has_neutron): }, ) + @mock.patch('novaclient.v2.client.Client.has_neutron', return_value=False) + def test_boot_nics_net_name_nova_net_2_36(self, has_neutron): + orig_find_network = novaclient.v2.shell._find_network_id_novanet + + def stubbed_find_network(cs, net_name): + # assert that we dropped back to 2.35 + self.assertEqual(api_versions.APIVersion('2.35'), + cs.client.api_version) + return orig_find_network(cs, net_name) + + cmd = ('boot --image %s --flavor 1 ' + '--nic net-name=1 some-server' % FAKE_UUID_1) + with mock.patch.object(novaclient.v2.shell, '_find_network_id_novanet', + side_effect=stubbed_find_network) as find_net: + self.run_command(cmd, api_version='2.36') + find_net.assert_called_once_with(self.shell.cs, '1') + self.assert_called_anytime( + 'POST', '/servers', + { + 'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': FAKE_UUID_1, + 'min_count': 1, + 'max_count': 1, + 'networks': [ + {'uuid': '1'}, + ], + }, + }, + ) + @mock.patch('novaclient.v2.client.Client.has_neutron', return_value=True) def test_boot_nics_net_name_neutron(self, has_neutron): cmd = ('boot --image %s --flavor 1 ' @@ -3271,3 +3303,39 @@ def test_error_state(self, mock_time): action=action, show_progress=True, silent=False) + + +class ShellUtilTest(utils.TestCase): + def test_deprecated_network_newer(self): + @novaclient.v2.shell.deprecated_network + def tester(cs): + 'foo' + self.assertEqual(api_versions.APIVersion('2.35'), + cs.api_version) + + cs = mock.MagicMock() + cs.api_version = api_versions.APIVersion('2.9999') + tester(cs) + self.assertEqual('DEPRECATED: foo', tester.__doc__) + + def test_deprecated_network_older(self): + @novaclient.v2.shell.deprecated_network + def tester(cs): + 'foo' + # since we didn't need to adjust the api_version the mock won't + # have cs.client.api_version set on it + self.assertFalse(hasattr(cs, 'client')) + # we have to set the attribute back on cs so the decorator can + # set the value on it when we return from this wrapped function + setattr(cs, 'client', mock.MagicMock()) + + cs = mock.MagicMock() + cs.api_version = api_versions.APIVersion('2.1') + # we have to delete the cs.client attribute so hasattr won't return a + # false positive in the wrapped function + del cs.client + tester(cs) + self.assertEqual('DEPRECATED: foo', tester.__doc__) + # the deprecated_network decorator will set cs.client.api_version + # after calling the wrapped function + self.assertEqual(cs.api_version, cs.api_version) diff --git a/novaclient/v2/contrib/tenant_networks.py b/novaclient/v2/contrib/tenant_networks.py index 56c989abc..c088f777c 100644 --- a/novaclient/v2/contrib/tenant_networks.py +++ b/novaclient/v2/contrib/tenant_networks.py @@ -15,6 +15,7 @@ from novaclient import base from novaclient.i18n import _ from novaclient import utils +from novaclient.v2 import shell class TenantNetwork(base.Resource): @@ -60,6 +61,7 @@ def do_net(cs, args): @utils.arg('network_id', metavar='', help='ID of network') +@shell.deprecated_network def do_tenant_network_show(cs, args): """ Show a tenant network. @@ -75,6 +77,7 @@ def do_net_list(cs, args): do_tenant_network_list(cs, args) +@shell.deprecated_network def do_tenant_network_list(cs, args): """ List tenant networks. @@ -106,6 +109,7 @@ def do_net_create(cs, args): 'cidr', metavar='', help=_('IP block to allocate from (ex. 172.16.0.0/24 or 2001:DB8::/64)')) +@shell.deprecated_network def do_tenant_network_create(cs, args): """ Create a tenant network. @@ -123,6 +127,7 @@ def do_net_delete(cs, args): @utils.arg('network_id', metavar='', help='ID of network') +@shell.deprecated_network def do_tenant_network_delete(cs, args): """ Delete a tenant network. diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 971e9879d..b49656a55 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -21,6 +21,7 @@ import argparse import copy import datetime +import functools import getpass import locale import logging @@ -76,6 +77,28 @@ def emit_image_deprecation_warning(command_name): 'instead.' % command_name, file=sys.stderr) +def deprecated_network(fn): + @functools.wraps(fn) + def wrapped(cs, *args, **kwargs): + command_name = '-'.join(fn.__name__.split('_')[1:]) + print('WARNING: Command %s is deprecated and will be removed ' + 'after Nova 15.0.0 is released. Use python-neutronclient ' + 'or python-openstackclient instead.' % command_name, + file=sys.stderr) + # The network proxy API methods were deprecated in 2.36 and will return + # a 404 so we fallback to 2.35 to maintain a transition for CLI users. + want_version = api_versions.APIVersion('2.35') + cur_version = cs.api_version + if cs.api_version > want_version: + cs.api_version = want_version + try: + return fn(cs, *args, **kwargs) + finally: + cs.api_version = cur_version + wrapped.__doc__ = 'DEPRECATED: ' + fn.__doc__ + return wrapped + + def _key_value_pairing(text): try: (k, v) = text.split('=', 1) @@ -954,6 +977,7 @@ def do_flavor_access_remove(cs, args): @utils.arg( 'project_id', metavar='', help=_('The ID of the project.')) +@deprecated_network def do_scrub(cs, args): """Delete networks and security groups associated with a project.""" networks_list = cs.networks.list() @@ -975,6 +999,7 @@ def do_scrub(cs, args): metavar='', help=_('Comma-separated list of fields to display. ' 'Use the show command to see which fields are available.')) +@deprecated_network def do_network_list(cs, args): """Print a list of available networks.""" network_list = cs.networks.list() @@ -989,6 +1014,7 @@ def do_network_list(cs, args): 'network', metavar='', help=_("UUID or label of network.")) +@deprecated_network def do_network_show(cs, args): """Show details about the given network.""" network = utils.find_resource(cs.networks, args.network) @@ -999,6 +1025,7 @@ def do_network_show(cs, args): 'network', metavar='', help=_("UUID or label of network.")) +@deprecated_network def do_network_delete(cs, args): """Delete network by label or id.""" network = utils.find_resource(cs.networks, args.network) @@ -1025,6 +1052,7 @@ def do_network_delete(cs, args): 'network', metavar='', help=_("UUID of network.")) +@deprecated_network def do_network_disassociate(cs, args): """Disassociate host and/or project from the given network.""" if args.host_only: @@ -1043,6 +1071,7 @@ def do_network_disassociate(cs, args): 'host', metavar='', help=_("Name of host")) +@deprecated_network def do_network_associate_host(cs, args): """Associate host with network.""" cs.networks.associate_host(args.network, args.host) @@ -1052,6 +1081,7 @@ def do_network_associate_host(cs, args): 'network', metavar='', help=_("UUID of network.")) +@deprecated_network def do_network_associate_project(cs, args): """Associate project with network.""" cs.networks.associate_project(args.network) @@ -1182,6 +1212,7 @@ def _filter_network_create_options(args): '--allowed-end', dest="allowed_end", help=_('End of allowed addresses for instances.')) +@deprecated_network def do_network_create(cs, args): """Create a network.""" @@ -2291,7 +2322,16 @@ def _find_network_id(cs, net_name): if cs.has_neutron(): return _find_network_id_neutron(cs, net_name) else: - return _find_network_id_novanet(cs, net_name) + # The network proxy API methods were deprecated in 2.36 and will return + # a 404 so we fallback to 2.35 to maintain a transition for CLI users. + want_version = api_versions.APIVersion('2.35') + cur_version = cs.api_version + if cs.api_version > want_version: + cs.api_version = want_version + try: + return _find_network_id_novanet(cs, net_name) + finally: + cs.api_version = cur_version def _find_network_id_novanet(cs, net_name): @@ -2663,12 +2703,14 @@ def do_list_secgroup(cs, args): help=_('Name of Floating IP Pool. (Optional)'), nargs='?', default=None) +@deprecated_network def do_floating_ip_create(cs, args): """Allocate a floating IP for the current tenant.""" _print_floating_ip_list([cs.floating_ips.create(pool=args.pool)]) @utils.arg('address', metavar='
', help=_('IP of Floating IP.')) +@deprecated_network def do_floating_ip_delete(cs, args): """De-allocate a floating IP.""" floating_ips = cs.floating_ips.list() @@ -2679,11 +2721,13 @@ def do_floating_ip_delete(cs, args): args.address) +@deprecated_network def do_floating_ip_list(cs, _args): """List floating IPs.""" _print_floating_ip_list(cs.floating_ips.list()) +@deprecated_network def do_floating_ip_pool_list(cs, _args): """List all floating IP pools.""" utils.print_list(cs.floating_ip_pools.list(), ['name']) @@ -2692,6 +2736,7 @@ def do_floating_ip_pool_list(cs, _args): @utils.arg( '--host', dest='host', metavar='', default=None, help=_('Filter by host.')) +@deprecated_network def do_floating_ip_bulk_list(cs, args): """List all floating IPs (nova-network only).""" utils.print_list(cs.floating_ips_bulk.list(args.host), ['project_id', @@ -2709,6 +2754,7 @@ def do_floating_ip_bulk_list(cs, args): @utils.arg( '--interface', metavar='', default=None, help=_('Interface for new Floating IPs.')) +@deprecated_network def do_floating_ip_bulk_create(cs, args): """Bulk create floating IPs by range (nova-network only).""" cs.floating_ips_bulk.create(args.ip_range, args.pool, args.interface) @@ -2716,6 +2762,7 @@ def do_floating_ip_bulk_create(cs, args): @utils.arg('ip_range', metavar='', help=_('Address range to delete.')) +@deprecated_network def do_floating_ip_bulk_delete(cs, args): """Bulk delete floating IPs by range (nova-network only).""" cs.floating_ips_bulk.delete(args.ip_range) @@ -2730,6 +2777,7 @@ def _print_domain_list(domain_entries): 'project', 'availability_zone']) +@deprecated_network def do_dns_domains(cs, args): """Print a list of available dns domains.""" domains = cs.dns_domains.domains() @@ -2739,6 +2787,7 @@ def do_dns_domains(cs, args): @utils.arg('domain', metavar='', help=_('DNS domain.')) @utils.arg('--ip', metavar='', help=_('IP address.'), default=None) @utils.arg('--name', metavar='', help=_('DNS name.'), default=None) +@deprecated_network def do_dns_list(cs, args): """List current DNS entries for domain and IP or domain and name.""" if not (args.ip or args.name): @@ -2761,6 +2810,7 @@ def do_dns_list(cs, args): metavar='', help=_('DNS type (e.g. "A")'), default='A') +@deprecated_network def do_dns_create(cs, args): """Create a DNS entry for domain, name, and IP.""" cs.dns_entries.create(args.domain, args.name, args.ip, args.type) @@ -2768,12 +2818,14 @@ def do_dns_create(cs, args): @utils.arg('domain', metavar='', help=_('DNS domain.')) @utils.arg('name', metavar='', help=_('DNS name.')) +@deprecated_network def do_dns_delete(cs, args): """Delete the specified DNS entry.""" cs.dns_entries.delete(args.domain, args.name) @utils.arg('domain', metavar='', help=_('DNS domain.')) +@deprecated_network def do_dns_delete_domain(cs, args): """Delete the specified DNS domain.""" cs.dns_domains.delete(args.domain) @@ -2786,6 +2838,7 @@ def do_dns_delete_domain(cs, args): default=None, help=_('Limit access to this domain to servers ' 'in the specified availability zone.')) +@deprecated_network def do_dns_create_private_domain(cs, args): """Create the specified DNS domain.""" cs.dns_domains.create_private(args.domain, @@ -2798,6 +2851,7 @@ def do_dns_create_private_domain(cs, args): help=_('Limit access to this domain to users ' 'of the specified project.'), default=None) +@deprecated_network def do_dns_create_public_domain(cs, args): """Create the specified DNS domain.""" cs.dns_domains.create_public(args.domain, @@ -2875,6 +2929,7 @@ def _get_secgroup(cs, secgroup): metavar='', help=_('Port at end of range.')) @utils.arg('cidr', metavar='', help=_('CIDR for address range.')) +@deprecated_network def do_secgroup_add_rule(cs, args): """Add a rule to a security group.""" secgroup = _get_secgroup(cs, args.secgroup) @@ -2903,6 +2958,7 @@ def do_secgroup_add_rule(cs, args): metavar='', help=_('Port at end of range.')) @utils.arg('cidr', metavar='', help=_('CIDR for address range.')) +@deprecated_network def do_secgroup_delete_rule(cs, args): """Delete a rule from a security group.""" secgroup = _get_secgroup(cs, args.secgroup) @@ -2922,6 +2978,7 @@ def do_secgroup_delete_rule(cs, args): @utils.arg( 'description', metavar='', help=_('Description of security group.')) +@deprecated_network def do_secgroup_create(cs, args): """Create a security group.""" secgroup = cs.security_groups.create(args.name, args.description) @@ -2936,6 +2993,7 @@ def do_secgroup_create(cs, args): @utils.arg( 'description', metavar='', help=_('Description of security group.')) +@deprecated_network def do_secgroup_update(cs, args): """Update a security group.""" sg = _get_secgroup(cs, args.secgroup) @@ -2947,6 +3005,7 @@ def do_secgroup_update(cs, args): 'secgroup', metavar='', help=_('ID or name of security group.')) +@deprecated_network def do_secgroup_delete(cs, args): """Delete a security group.""" secgroup = _get_secgroup(cs, args.secgroup) @@ -2964,6 +3023,7 @@ def do_secgroup_delete(cs, args): default=int(strutils.bool_from_string( os.environ.get("ALL_TENANTS", 'false'), True)), help=_('Display information from all tenants (Admin only).')) +@deprecated_network def do_secgroup_list(cs, args): """List security groups for the current tenant.""" search_opts = {'all_tenants': args.all_tenants} @@ -2978,6 +3038,7 @@ def do_secgroup_list(cs, args): 'secgroup', metavar='', help=_('ID or name of security group.')) +@deprecated_network def do_secgroup_list_rules(cs, args): """List rules for a security group.""" secgroup = _get_secgroup(cs, args.secgroup) @@ -3004,6 +3065,7 @@ def do_secgroup_list_rules(cs, args): 'to_port', metavar='', help=_('Port at end of range.')) +@deprecated_network def do_secgroup_add_group_rule(cs, args): """Add a source group rule to a security group.""" secgroup = _get_secgroup(cs, args.secgroup) @@ -3042,6 +3104,7 @@ def do_secgroup_add_group_rule(cs, args): 'to_port', metavar='', help=_('Port at end of range.')) +@deprecated_network def do_secgroup_delete_group_rule(cs, args): """Delete a source group rule from a security group.""" secgroup = _get_secgroup(cs, args.secgroup) @@ -3973,6 +4036,7 @@ def _print_fixed_ip(cs, fixed_ip): @utils.arg('fixed_ip', metavar='', help=_('Fixed IP Address.')) +@deprecated_network def do_fixed_ip_get(cs, args): """Retrieve info on a fixed IP.""" result = cs.fixed_ips.get(args.fixed_ip) @@ -3980,12 +4044,14 @@ def do_fixed_ip_get(cs, args): @utils.arg('fixed_ip', metavar='', help=_('Fixed IP Address.')) +@deprecated_network def do_fixed_ip_reserve(cs, args): """Reserve a fixed IP.""" cs.fixed_ips.reserve(args.fixed_ip) @utils.arg('fixed_ip', metavar='', help=_('Fixed IP Address.')) +@deprecated_network def do_fixed_ip_unreserve(cs, args): """Unreserve a fixed IP.""" cs.fixed_ips.unreserve(args.fixed_ip) @@ -4450,6 +4516,7 @@ def do_quota_defaults(cs, args): _quota_show(cs.quotas.defaults(project_id)) +@api_versions.wraps("2.0", "2.35") @utils.arg( 'tenant', metavar='', @@ -4479,12 +4546,14 @@ def do_quota_defaults(cs, args): metavar='', type=int, default=None, + action=shell.DeprecatedAction, help=_('New value for the "floating-ips" quota.')) @utils.arg( '--fixed-ips', metavar='', type=int, default=None, + action=shell.DeprecatedAction, help=_('New value for the "fixed-ips" quota.')) @utils.arg( '--metadata-items', @@ -4521,12 +4590,14 @@ def do_quota_defaults(cs, args): metavar='', type=int, default=None, + action=shell.DeprecatedAction, help=_('New value for the "security-groups" quota.')) @utils.arg( '--security-group-rules', metavar='', type=int, default=None, + action=shell.DeprecatedAction, help=_('New value for the "security-group-rules" quota.')) @utils.arg( '--server-groups', @@ -4553,6 +4624,88 @@ def do_quota_update(cs, args): _quota_update(cs.quotas, args.tenant, args) +# 2.36 does not support updating quota for floating IPs, fixed IPs, security +# groups or security group rules. +@api_versions.wraps("2.36") +@utils.arg( + 'tenant', + metavar='', + help=_('ID of tenant to set the quotas for.')) +@utils.arg( + '--user', + metavar='', + default=None, + help=_('ID of user to set the quotas for.')) +@utils.arg( + '--instances', + metavar='', + type=int, default=None, + help=_('New value for the "instances" quota.')) +@utils.arg( + '--cores', + metavar='', + type=int, default=None, + help=_('New value for the "cores" quota.')) +@utils.arg( + '--ram', + metavar='', + type=int, default=None, + help=_('New value for the "ram" quota.')) +@utils.arg( + '--metadata-items', + metavar='', + type=int, + default=None, + help=_('New value for the "metadata-items" quota.')) +@utils.arg( + '--injected-files', + metavar='', + type=int, + default=None, + help=_('New value for the "injected-files" quota.')) +@utils.arg( + '--injected-file-content-bytes', + metavar='', + type=int, + default=None, + help=_('New value for the "injected-file-content-bytes" quota.')) +@utils.arg( + '--injected-file-path-bytes', + metavar='', + type=int, + default=None, + help=_('New value for the "injected-file-path-bytes" quota.')) +@utils.arg( + '--key-pairs', + metavar='', + type=int, + default=None, + help=_('New value for the "key-pairs" quota.')) +@utils.arg( + '--server-groups', + metavar='', + type=int, + default=None, + help=_('New value for the "server-groups" quota.')) +@utils.arg( + '--server-group-members', + metavar='', + type=int, + default=None, + help=_('New value for the "server-group-members" quota.')) +@utils.arg( + '--force', + dest='force', + action="store_true", + default=None, + help=_('Whether force update the quota even if the already used and ' + 'reserved exceeds the new quota.')) +def do_quota_update(cs, args): + """Update the quotas for a tenant/user.""" + + _quota_update(cs.quotas, args.tenant, args) + + @utils.arg( '--tenant', metavar='', @@ -4580,6 +4733,7 @@ def do_quota_class_show(cs, args): _quota_show(cs.quota_classes.get(args.class_name)) +@api_versions.wraps("2.0", "2.35") @utils.arg( 'class_name', metavar='', @@ -4604,12 +4758,14 @@ def do_quota_class_show(cs, args): metavar='', type=int, default=None, + action=shell.DeprecatedAction, help=_('New value for the "floating-ips" quota.')) @utils.arg( '--fixed-ips', metavar='', type=int, default=None, + action=shell.DeprecatedAction, help=_('New value for the "fixed-ips" quota.')) @utils.arg( '--metadata-items', @@ -4646,12 +4802,14 @@ def do_quota_class_show(cs, args): metavar='', type=int, default=None, + action=shell.DeprecatedAction, help=_('New value for the "security-groups" quota.')) @utils.arg( '--security-group-rules', metavar='', type=int, default=None, + action=shell.DeprecatedAction, help=_('New value for the "security-group-rules" quota.')) @utils.arg( '--server-groups', @@ -4671,6 +4829,76 @@ def do_quota_class_update(cs, args): _quota_update(cs.quota_classes, args.class_name, args) +# 2.36 does not support updating quota for floating IPs, fixed IPs, security +# groups or security group rules. +@api_versions.wraps("2.36") +@utils.arg( + 'class_name', + metavar='', + help=_('Name of quota class to set the quotas for.')) +@utils.arg( + '--instances', + metavar='', + type=int, default=None, + help=_('New value for the "instances" quota.')) +@utils.arg( + '--cores', + metavar='', + type=int, default=None, + help=_('New value for the "cores" quota.')) +@utils.arg( + '--ram', + metavar='', + type=int, default=None, + help=_('New value for the "ram" quota.')) +@utils.arg( + '--metadata-items', + metavar='', + type=int, + default=None, + help=_('New value for the "metadata-items" quota.')) +@utils.arg( + '--injected-files', + metavar='', + type=int, + default=None, + help=_('New value for the "injected-files" quota.')) +@utils.arg( + '--injected-file-content-bytes', + metavar='', + type=int, + default=None, + help=_('New value for the "injected-file-content-bytes" quota.')) +@utils.arg( + '--injected-file-path-bytes', + metavar='', + type=int, + default=None, + help=_('New value for the "injected-file-path-bytes" quota.')) +@utils.arg( + '--key-pairs', + metavar='', + type=int, + default=None, + help=_('New value for the "key-pairs" quota.')) +@utils.arg( + '--server-groups', + metavar='', + type=int, + default=None, + help=_('New value for the "server-groups" quota.')) +@utils.arg( + '--server-group-members', + metavar='', + type=int, + default=None, + help=_('New value for the "server-group-members" quota.')) +def do_quota_class_update(cs, args): + """Update the quotas for a quota class.""" + + _quota_update(cs.quota_classes, args.class_name, args) + + @utils.arg('server', metavar='', help=_('Name or ID of server.')) @utils.arg( 'host', metavar='', nargs='?', @@ -4871,6 +5099,7 @@ def do_server_group_list(cs, args): _print_server_group_details(cs, server_groups) +@deprecated_network def do_secgroup_list_default_rules(cs, args): """List rules that will be added to the 'default' security group for new tenants. @@ -4892,6 +5121,7 @@ def do_secgroup_list_default_rules(cs, args): metavar='', help=_('Port at end of range.')) @utils.arg('cidr', metavar='', help=_('CIDR for address range.')) +@deprecated_network def do_secgroup_add_default_rule(cs, args): """Add a rule to the set of rules that will be added to the 'default' security group for new tenants (nova-network only). @@ -4916,6 +5146,7 @@ def do_secgroup_add_default_rule(cs, args): metavar='', help=_('Port at end of range.')) @utils.arg('cidr', metavar='', help=_('CIDR for address range.')) +@deprecated_network def do_secgroup_delete_default_rule(cs, args): """Delete a rule from the set of rules that will be added to the 'default' security group for new tenants (nova-network only). diff --git a/releasenotes/notes/deprecate-network-cli-f0a539528be594d3.yaml b/releasenotes/notes/deprecate-network-cli-f0a539528be594d3.yaml new file mode 100644 index 000000000..2d8c86a6d --- /dev/null +++ b/releasenotes/notes/deprecate-network-cli-f0a539528be594d3.yaml @@ -0,0 +1,67 @@ +--- +upgrade: + - | + The ability to update the following network-related resources via the + ``nova quota-update`` and ``nova quota-class-update`` commands is now + deprecated: + + * Fixed IPs + * Floating IPs + * Security Groups + * Security Group Rules + + By default the quota and limits CLIs will not update or show those + resources using microversion >= 2.36. You can still use them, however, by + specifying ``--os-compute-api-version 2.35``. Quota information for network + resources should be retrieved from python-neutronclient or + python-openstackclient. +deprecations: + - | + The following commands are now deprecated: + + * dns-create + * dns-create-private-domain + * dns-create-public-domain + * dns-delete + * dns-delete-domain + * dns-domains + * dns-list + * fixed-ip-get + * fixed-ip-reserve + * fixed-ip-unreserve + * floating-ip-create + * floating-ip-delete + * floating-ip-list + * floating-ip-pool-list + * floating-ip-bulk-create + * floating-ip-bulk-delete + * floating-ip-bulk-list + * network-create + * network-delete + * network-disassociate + * network-associate-host + * network-associate-project + * network-list + * network-show + * scrub + * secgroup-create + * secgroup-delete + * secgroup-list + * secgroup-update + * secgroup-add-group-rule + * secgroup-delete-group-rule + * secgroup-add-rule + * secgroup-delete-rule + * secgroup-list-rules + * secgroup-list-default-rules + * secgroup-add-default-rule + * secgroup-delete-default-rule + * tenant-network-create + * tenant-network-delete + * tenant-network-list + * tenant-network-show + + With the 2.36 microversion these will fail in the API. The CLI will + fallback to passing the 2.35 microversion to ease the transition. Network + resource information should be retrieved from python-neutronclient or + python-openstackclient. From aaebeb05a03e34281a091dc6dfc4672b01cdfbbb Mon Sep 17 00:00:00 2001 From: Dan Smith Date: Thu, 28 Jul 2016 11:12:50 -0700 Subject: [PATCH 1125/1705] Deprecate all the nova-network functions in the python API Per the plan, this marks all nova-network-related functions as deprecated for removal. Change-Id: I511793cd9a01669e77e1ae5ecb391ce937477309 --- novaclient/api_versions.py | 19 +++++++ novaclient/tests/unit/test_api_versions.py | 17 ++++++ novaclient/v2/contrib/tenant_networks.py | 13 ++++- novaclient/v2/fixed_ips.py | 12 +++-- novaclient/v2/floating_ip_dns.py | 52 ++++++++++++++----- novaclient/v2/floating_ip_pools.py | 7 ++- novaclient/v2/floating_ips.py | 18 +++++-- novaclient/v2/floating_ips_bulk.py | 14 +++-- novaclient/v2/fping.py | 11 ++-- novaclient/v2/networks.py | 32 +++++++----- novaclient/v2/security_group_default_rules.py | 15 ++++-- novaclient/v2/security_group_rules.py | 14 +++-- novaclient/v2/security_groups.py | 24 ++++++--- 13 files changed, 188 insertions(+), 60 deletions(-) diff --git a/novaclient/api_versions.py b/novaclient/api_versions.py index 0161b589f..4abea01a3 100644 --- a/novaclient/api_versions.py +++ b/novaclient/api_versions.py @@ -17,6 +17,7 @@ import pkgutil import re import traceback +import warnings from oslo_utils import strutils @@ -421,3 +422,21 @@ def _warn_missing_microversion_header(header_name): "Your request was processed by a Nova API which does not support " "microversions (%s header is missing from response). " "Warning: Response may be incorrect."), header_name) + + +def deprecated_after(version): + decorator = wraps('2.0', version) + + def wrapper(fn): + @functools.wraps(fn) + def wrapped(*a, **k): + decorated = decorator(fn) + if hasattr(fn, '__module__'): + mod = fn.__module__ + else: + mod = a[0].__module__ + warnings.warn('The %s module is deprecated ' + 'and will be removed.' % mod, DeprecationWarning) + return decorated(*a, **k) + return wrapped + return wrapper diff --git a/novaclient/tests/unit/test_api_versions.py b/novaclient/tests/unit/test_api_versions.py index 44ac27915..00eb2b6f8 100644 --- a/novaclient/tests/unit/test_api_versions.py +++ b/novaclient/tests/unit/test_api_versions.py @@ -438,3 +438,20 @@ def test_server_without_microversion_rax_workaround(self): api_versions.discover_version( fake_client, api_versions.APIVersion('2.latest')).get_string()) + + +class DecoratedAfterTestCase(utils.TestCase): + def test_decorated_after(self): + + class Fake(object): + api_version = api_versions.APIVersion('2.123') + + @api_versions.deprecated_after('2.123') + def foo(self): + pass + + with mock.patch('warnings.warn') as mock_warn: + Fake().foo() + msg = ('The novaclient.tests.unit.test_api_versions module ' + 'is deprecated and will be removed.') + mock_warn.assert_called_once_with(msg, mock.ANY) diff --git a/novaclient/v2/contrib/tenant_networks.py b/novaclient/v2/contrib/tenant_networks.py index c088f777c..16d69f387 100644 --- a/novaclient/v2/contrib/tenant_networks.py +++ b/novaclient/v2/contrib/tenant_networks.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from novaclient import api_versions from novaclient import base from novaclient.i18n import _ from novaclient import utils @@ -21,7 +22,7 @@ class TenantNetwork(base.Resource): def delete(self): """ - Delete this project network. + DEPRECATED: Delete this project network. :returns: An instance of novaclient.base.TupleWithMeta """ @@ -29,25 +30,33 @@ def delete(self): class TenantNetworkManager(base.ManagerWithFind): + """DEPRECATED""" resource_class = base.Resource + @api_versions.deprecated_after('2.35') def list(self): + """DEPRECATED""" return self._list('/os-tenant-networks', 'networks') + @api_versions.deprecated_after('2.35') def get(self, network): + """DEPRECATED""" return self._get('/os-tenant-networks/%s' % base.getid(network), 'network') + @api_versions.deprecated_after('2.35') def delete(self, network): """ - Delete a specified project network. + DEPRECATED: Delete a specified project network. :param network: a project network to delete :returns: An instance of novaclient.base.TupleWithMeta """ return self._delete('/os-tenant-networks/%s' % base.getid(network)) + @api_versions.deprecated_after('2.35') def create(self, label, cidr): + """DEPRECATED""" body = {'network': {'label': label, 'cidr': cidr}} return self._create('/os-tenant-networks', body, 'network') diff --git a/novaclient/v2/fixed_ips.py b/novaclient/v2/fixed_ips.py index 232ac6fd2..16604690c 100644 --- a/novaclient/v2/fixed_ips.py +++ b/novaclient/v2/fixed_ips.py @@ -17,27 +17,32 @@ Fixed IPs interface. """ +from novaclient import api_versions from novaclient import base class FixedIP(base.Resource): + """DEPRECATED""" def __repr__(self): return "" % self.address class FixedIPsManager(base.Manager): + """DEPRECATED""" resource_class = FixedIP + @api_versions.deprecated_after('2.35') def get(self, fixed_ip): - """Show information for a Fixed IP. + """DEPRECATED: Show information for a Fixed IP. :param fixed_ip: Fixed IP address to get info for """ return self._get('/os-fixed-ips/%s' % base.getid(fixed_ip), "fixed_ip") + @api_versions.deprecated_after('2.35') def reserve(self, fixed_ip): - """Reserve a Fixed IP. + """DEPRECATED: Reserve a Fixed IP. :param fixed_ip: Fixed IP address to reserve :returns: An instance of novaclient.base.TupleWithMeta @@ -47,8 +52,9 @@ def reserve(self, fixed_ip): base.getid(fixed_ip), body=body) return self.convert_into_with_meta(body, resp) + @api_versions.deprecated_after('2.35') def unreserve(self, fixed_ip): - """Unreserve a Fixed IP. + """DEPRECATED: Unreserve a Fixed IP. :param fixed_ip: Fixed IP address to unreserve :returns: An instance of novaclient.base.TupleWithMeta diff --git a/novaclient/v2/floating_ip_dns.py b/novaclient/v2/floating_ip_dns.py index d6ccb8d5c..64b3a65d9 100644 --- a/novaclient/v2/floating_ip_dns.py +++ b/novaclient/v2/floating_ip_dns.py @@ -15,6 +15,7 @@ from six.moves.urllib import parse +from novaclient import api_versions from novaclient import base @@ -29,9 +30,11 @@ def _quote_domain(domain): class FloatingIPDNSDomain(base.Resource): + """DEPRECATED""" + def delete(self): """ - Delete the own Floating IP DNS domain. + DEPRECATED: Delete the own Floating IP DNS domain. :returns: An instance of novaclient.base.TupleWithMeta """ @@ -39,7 +42,7 @@ def delete(self): def create(self): """ - Create a Floating IP DNS domain. + DEPRECATED: Create a Floating IP DNS domain. :returns: An instance of novaclient.base.DictWithMeta """ @@ -51,7 +54,7 @@ def create(self): def get(self): """ - Get the own Floating IP DNS domain. + DEPRECATED: Get the own Floating IP DNS domain. :returns: An instance of novaclient.base.TupleWithMeta or novaclient.base.ListWithMeta @@ -65,30 +68,36 @@ def get(self): class FloatingIPDNSDomainManager(base.Manager): + """DEPRECATED""" + resource_class = FloatingIPDNSDomain + @api_versions.deprecated_after('2.35') def domains(self): - """Return the list of available dns domains.""" + """DEPRECATED: Return the list of available dns domains.""" return self._list("/os-floating-ip-dns", "domain_entries") + @api_versions.deprecated_after('2.35') def create_private(self, fqdomain, availability_zone): - """Add or modify a private DNS domain.""" + """DEPRECATED: Add or modify a private DNS domain.""" body = {'domain_entry': {'scope': 'private', 'availability_zone': availability_zone}} return self._update('/os-floating-ip-dns/%s' % _quote_domain(fqdomain), body, 'domain_entry') + @api_versions.deprecated_after('2.35') def create_public(self, fqdomain, project): - """Add or modify a public DNS domain.""" + """DEPRECATED: Add or modify a public DNS domain.""" body = {'domain_entry': {'scope': 'public', 'project': project}} return self._update('/os-floating-ip-dns/%s' % _quote_domain(fqdomain), body, 'domain_entry') + @api_versions.deprecated_after('2.35') def delete(self, fqdomain): """ - Delete the specified domain. + DEPRECATED: Delete the specified domain. :param fqdomain: The domain to delete :returns: An instance of novaclient.base.TupleWithMeta @@ -97,9 +106,11 @@ def delete(self, fqdomain): class FloatingIPDNSEntry(base.Resource): + """DEPRECATED""" + def delete(self): """ - Delete the own Floating IP DNS entry. + DEPRECATED: Delete the own Floating IP DNS entry. :returns: An instance of novaclient.base.TupleWithMeta """ @@ -107,7 +118,7 @@ def delete(self): def create(self): """ - Create a Floating IP DNS entry. + DEPRECATED: Create a Floating IP DNS entry. :returns: :class:`FloatingIPDNSEntry` """ @@ -115,42 +126,55 @@ def create(self): self.dns_type) def get(self): + """DEPRECATED""" return self.manager.get(self.domain, self.name) class FloatingIPDNSEntryManager(base.Manager): + """DEPRECATED""" resource_class = FloatingIPDNSEntry + @api_versions.deprecated_after('2.35') def get(self, domain, name): - """Return a list of entries for the given domain and IP or name.""" + """ + DEPRECATED: Return a list of entries for the given domain and IP or + name. + """ return self._get("/os-floating-ip-dns/%s/entries/%s" % (_quote_domain(domain), name), "dns_entry") + @api_versions.deprecated_after('2.35') def get_for_ip(self, domain, ip): - """Return a list of entries for the given domain and IP or name.""" + """ + DEPRECATED: Return a list of entries for the given domain and IP or + name. + """ qparams = {'ip': ip} params = "?%s" % parse.urlencode(qparams) return self._list("/os-floating-ip-dns/%s/entries%s" % (_quote_domain(domain), params), "dns_entries") + @api_versions.deprecated_after('2.35') def create(self, domain, name, ip, dns_type): - """Add a new DNS entry.""" + """DEPRECATED: Add a new DNS entry.""" body = {'dns_entry': {'ip': ip, 'dns_type': dns_type}} return self._update("/os-floating-ip-dns/%s/entries/%s" % (_quote_domain(domain), name), body, "dns_entry") + @api_versions.deprecated_after('2.35') def modify_ip(self, domain, name, ip): - """Add a new DNS entry.""" + """DEPRECATED: Add a new DNS entry.""" body = {'dns_entry': {'ip': ip, 'dns_type': 'A'}} return self._update("/os-floating-ip-dns/%s/entries/%s" % (_quote_domain(domain), name), body, "dns_entry") + @api_versions.deprecated_after('2.35') def delete(self, domain, name): """ - Delete entry specified by name and domain. + DEPRECATED: Delete entry specified by name and domain. :returns: An instance of novaclient.base.TupleWithMeta """ diff --git a/novaclient/v2/floating_ip_pools.py b/novaclient/v2/floating_ip_pools.py index 9ea94a273..70a43ce23 100644 --- a/novaclient/v2/floating_ip_pools.py +++ b/novaclient/v2/floating_ip_pools.py @@ -14,17 +14,22 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient import api_versions from novaclient import base class FloatingIPPool(base.Resource): + """DEPRECATED""" + def __repr__(self): return "" % self.name class FloatingIPPoolManager(base.ManagerWithFind): + """DEPRECATED""" resource_class = FloatingIPPool + @api_versions.deprecated_after('2.35') def list(self): - """Retrieve a list of all floating ip pools.""" + """DEPRECATED: Retrieve a list of all floating ip pools.""" return self._list('/os-floating-ip-pools', 'floating_ip_pools') diff --git a/novaclient/v2/floating_ips.py b/novaclient/v2/floating_ips.py index b25bb33f4..2aea6a3b6 100644 --- a/novaclient/v2/floating_ips.py +++ b/novaclient/v2/floating_ips.py @@ -14,13 +14,16 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient import api_versions from novaclient import base class FloatingIP(base.Resource): + """DEPRECATED""" + def delete(self): """ - Delete this floating IP + DEPRECATED: Delete this floating IP :returns: An instance of novaclient.base.TupleWithMeta """ @@ -28,25 +31,30 @@ def delete(self): class FloatingIPManager(base.ManagerWithFind): + """DEPRECATED""" resource_class = FloatingIP + @api_versions.deprecated_after('2.35') def list(self): - """List floating IPs""" + """DEPRECATED: List floating IPs""" return self._list("/os-floating-ips", "floating_ips") + @api_versions.deprecated_after('2.35') def create(self, pool=None): - """Create (allocate) a floating IP for a tenant""" + """DEPRECATED: Create (allocate) a floating IP for a tenant""" return self._create("/os-floating-ips", {'pool': pool}, "floating_ip") + @api_versions.deprecated_after('2.35') def delete(self, floating_ip): - """Delete (deallocate) a floating IP for a tenant + """DEPRECATED: Delete (deallocate) a floating IP for a tenant :param floating_ip: The floating IP address to delete. :returns: An instance of novaclient.base.TupleWithMeta """ return self._delete("/os-floating-ips/%s" % base.getid(floating_ip)) + @api_versions.deprecated_after('2.35') def get(self, floating_ip): - """Retrieve a floating IP""" + """DEPRECATED: Retrieve a floating IP""" return self._get("/os-floating-ips/%s" % base.getid(floating_ip), "floating_ip") diff --git a/novaclient/v2/floating_ips_bulk.py b/novaclient/v2/floating_ips_bulk.py index 4249af60d..c0c8a8e10 100644 --- a/novaclient/v2/floating_ips_bulk.py +++ b/novaclient/v2/floating_ips_bulk.py @@ -16,20 +16,26 @@ """ Bulk Floating IPs interface """ +from novaclient import api_versions from novaclient import base from novaclient.v2 import floating_ips class FloatingIPRange(base.Resource): + """DEPRECATED""" + def __repr__(self): return "" % self.ip_range class FloatingIPBulkManager(base.ManagerWithFind): + """DEPRECATED""" + resource_class = FloatingIPRange + @api_versions.deprecated_after('2.35') def list(self, host=None): - """List all floating IPs.""" + """DEPRECATED: List all floating IPs.""" if host is None: return self._list('/os-floating-ips-bulk', 'floating_ip_info', @@ -39,8 +45,9 @@ def list(self, host=None): 'floating_ip_info', obj_class=floating_ips.FloatingIP) + @api_versions.deprecated_after('2.35') def create(self, ip_range, pool=None, interface=None): - """Create floating IPs by range.""" + """DEPRECATED: Create floating IPs by range.""" body = {"floating_ips_bulk_create": {'ip_range': ip_range}} if pool is not None: body['floating_ips_bulk_create']['pool'] = pool @@ -50,7 +57,8 @@ def create(self, ip_range, pool=None, interface=None): return self._create('/os-floating-ips-bulk', body, 'floating_ips_bulk_create') + @api_versions.deprecated_after('2.35') def delete(self, ip_range): - """Delete floating IPs by range.""" + """DEPRECATED: Delete floating IPs by range.""" body = {"ip_range": ip_range} return self._update('/os-floating-ips-bulk/delete', body) diff --git a/novaclient/v2/fping.py b/novaclient/v2/fping.py index ad2bfb99d..cee13226a 100644 --- a/novaclient/v2/fping.py +++ b/novaclient/v2/fping.py @@ -18,11 +18,12 @@ """ from six.moves import urllib +from novaclient import api_versions from novaclient import base class Fping(base.Resource): - """A server to fping.""" + """DEPRECATED: A server to fping.""" HUMAN_ID = True def __repr__(self): @@ -30,11 +31,12 @@ def __repr__(self): class FpingManager(base.ManagerWithFind): - """Manage :class:`Fping` resources.""" + """DEPRECATED: Manage :class:`Fping` resources.""" resource_class = Fping + @api_versions.deprecated_after('2.35') def list(self, all_tenants=False, include=None, exclude=None): - """Fping all servers. + """DEPRECATED: Fping all servers. :returns: list of :class:`Fping`. """ @@ -52,8 +54,9 @@ def list(self, all_tenants=False, include=None, exclude=None): uri = "%s?%s" % (uri, urllib.parse.urlencode(params)) return self._list(uri, "servers") + @api_versions.deprecated_after('2.35') def get(self, server): - """Fping a specific server. + """DEPRECATED: Fping a specific server. :param server: ID of the server to fping. :returns: :class:`Fping` diff --git a/novaclient/v2/networks.py b/novaclient/v2/networks.py index 4f545403f..36a694232 100644 --- a/novaclient/v2/networks.py +++ b/novaclient/v2/networks.py @@ -16,7 +16,7 @@ """ Network interface. """ - +from novaclient import api_versions from novaclient import base from novaclient import exceptions from novaclient.i18n import _ @@ -34,7 +34,7 @@ def __repr__(self): def delete(self): """ - Delete this network. + DEPRECATED: Delete this network. :returns: An instance of novaclient.base.TupleWithMeta """ @@ -76,21 +76,23 @@ def find_network(self, name): class NetworkManager(base.ManagerWithFind): """ - Manage :class:`Network` resources. + DEPRECATED: Manage :class:`Network` resources. """ resource_class = Network + @api_versions.deprecated_after('2.35') def list(self): """ - Get a list of all networks. + DEPRECATED: Get a list of all networks. :rtype: list of :class:`Network`. """ return self._list("/os-networks", "networks") + @api_versions.deprecated_after('2.35') def get(self, network): """ - Get a specific network. + DEPRECATED: Get a specific network. :param network: The ID of the :class:`Network` to get. :rtype: :class:`Network` @@ -98,18 +100,20 @@ def get(self, network): return self._get("/os-networks/%s" % base.getid(network), "network") + @api_versions.deprecated_after('2.35') def delete(self, network): """ - Delete a specific network. + DEPRECATED: Delete a specific network. :param network: The ID of the :class:`Network` to delete. :returns: An instance of novaclient.base.TupleWithMeta """ return self._delete("/os-networks/%s" % base.getid(network)) + @api_versions.deprecated_after('2.35') def create(self, **kwargs): """ - Create (allocate) a network. The following parameters are + DEPRECATED: Create (allocate) a network. The following parameters are optional except for label; cidr or cidr_v6 must be specified, too. :param label: str @@ -140,10 +144,11 @@ def create(self, **kwargs): body = {"network": kwargs} return self._create('/os-networks', body, 'network') + @api_versions.deprecated_after('2.35') def disassociate(self, network, disassociate_host=True, disassociate_project=True): """ - Disassociate a specific network from project and/or host. + DEPRECATED: Disassociate a specific network from project and/or host. :param network: The ID of the :class:`Network`. :param disassociate_host: Whether to disassociate the host @@ -165,9 +170,10 @@ def disassociate(self, network, disassociate_host=True, return self.convert_into_with_meta(body, resp) + @api_versions.deprecated_after('2.35') def associate_host(self, network, host): """ - Associate a specific network with a host. + DEPRECATED: Associate a specific network with a host. :param network: The ID of the :class:`Network`. :param host: The name of the host to associate the network with @@ -179,9 +185,10 @@ def associate_host(self, network, host): return self.convert_into_with_meta(body, resp) + @api_versions.deprecated_after('2.35') def associate_project(self, network): """ - Associate a specific network with a project. + DEPRECATED: Associate a specific network with a project. The project is defined by the project authenticated against @@ -193,10 +200,11 @@ def associate_project(self, network): return self.convert_into_with_meta(body, resp) + @api_versions.deprecated_after('2.35') def add(self, network=None): """ - Associates the current project with a network. Network can be chosen - automatically or provided explicitly. + DEPRECATED: Associates the current project with a network. Network can + be chosen automatically or provided explicitly. :param network: The ID of the :class:`Network` to associate (optional). :returns: An instance of novaclient.base.TupleWithMeta diff --git a/novaclient/v2/security_group_default_rules.py b/novaclient/v2/security_group_default_rules.py index 6516a460d..58aef144a 100644 --- a/novaclient/v2/security_group_default_rules.py +++ b/novaclient/v2/security_group_default_rules.py @@ -13,19 +13,20 @@ """ Security group default rules interface. """ - +from novaclient import api_versions from novaclient import base from novaclient import exceptions from novaclient.i18n import _ class SecurityGroupDefaultRule(base.Resource): + """DEPRECATED""" def __str__(self): return str(self.id) def delete(self): """ - Delete this security group default rule. + DEPRECATED: Delete this security group default rule. :returns: An instance of novaclient.base.TupleWithMeta """ @@ -33,12 +34,14 @@ def delete(self): class SecurityGroupDefaultRuleManager(base.Manager): + """DEPRECATED""" resource_class = SecurityGroupDefaultRule + @api_versions.deprecated_after('2.35') def create(self, ip_protocol=None, from_port=None, to_port=None, cidr=None): """ - Create a security group default rule + DEPRECATED: Create a security group default rule :param ip_protocol: IP protocol, one of 'tcp', 'udp' or 'icmp' :param from_port: Source port @@ -67,9 +70,10 @@ def create(self, ip_protocol=None, from_port=None, to_port=None, return self._create('/os-security-group-default-rules', body, 'security_group_default_rule') + @api_versions.deprecated_after('2.35') def delete(self, rule): """ - Delete a security group default rule + DEPRECATED: Delete a security group default rule :param rule: The security group default rule to delete (ID or Class) :returns: An instance of novaclient.base.TupleWithMeta @@ -77,9 +81,10 @@ def delete(self, rule): return self._delete('/os-security-group-default-rules/%s' % base.getid(rule)) + @api_versions.deprecated_after('2.35') def list(self): """ - Get a list of all security group default rules + DEPRECATED: Get a list of all security group default rules :rtype: list of :class:`SecurityGroupDefaultRule` """ diff --git a/novaclient/v2/security_group_rules.py b/novaclient/v2/security_group_rules.py index c10603721..750fba419 100644 --- a/novaclient/v2/security_group_rules.py +++ b/novaclient/v2/security_group_rules.py @@ -16,19 +16,21 @@ """ Security group rules interface (1.1 extension). """ - +from novaclient import api_versions from novaclient import base from novaclient import exceptions from novaclient.i18n import _ class SecurityGroupRule(base.Resource): + """DEPRECATED""" + def __str__(self): return str(self.id) def delete(self): """ - Delete this security group rule. + DEPRECATED: Delete this security group rule. :returns: An instance of novaclient.base.TupleWithMeta """ @@ -36,12 +38,15 @@ def delete(self): class SecurityGroupRuleManager(base.Manager): + """DEPRECATED""" + resource_class = SecurityGroupRule + @api_versions.deprecated_after('2.35') def create(self, parent_group_id, ip_protocol=None, from_port=None, to_port=None, cidr=None, group_id=None): """ - Create a security group rule + DEPRECATED: Create a security group rule :param ip_protocol: IP protocol, one of 'tcp', 'udp' or 'icmp' :param from_port: Source port @@ -74,9 +79,10 @@ def create(self, parent_group_id, ip_protocol=None, from_port=None, return self._create('/os-security-group-rules', body, 'security_group_rule') + @api_versions.deprecated_after('2.35') def delete(self, rule): """ - Delete a security group rule + DEPRECATED: Delete a security group rule :param rule: The security group rule to delete (ID or Class) :returns: An instance of novaclient.base.TupleWithMeta diff --git a/novaclient/v2/security_groups.py b/novaclient/v2/security_groups.py index a6bf63794..88198ec75 100644 --- a/novaclient/v2/security_groups.py +++ b/novaclient/v2/security_groups.py @@ -20,16 +20,19 @@ import six from six.moves.urllib import parse +from novaclient import api_versions from novaclient import base class SecurityGroup(base.Resource): + """DEPRECATED""" + def __str__(self): return str(self.id) def delete(self): """ - Delete this security group. + DEPRECATED: Delete this security group. :returns: An instance of novaclient.base.TupleWithMeta """ @@ -37,7 +40,7 @@ def delete(self): def update(self): """ - Update this security group. + DEPRECATED: Update this security group. :returns: :class:`SecurityGroup` """ @@ -45,11 +48,14 @@ def update(self): class SecurityGroupManager(base.ManagerWithFind): + """DEPRECATED""" + resource_class = SecurityGroup + @api_versions.deprecated_after('2.35') def create(self, name, description): """ - Create a security group + DEPRECATED: Create a security group :param name: name for the security group to create :param description: description of the security group @@ -58,9 +64,10 @@ def create(self, name, description): body = {"security_group": {"name": name, 'description': description}} return self._create('/os-security-groups', body, 'security_group') + @api_versions.deprecated_after('2.35') def update(self, group, name, description): """ - Update a security group + DEPRECATED: Update a security group :param group: The security group to update (group or ID) :param name: name for the security group to update @@ -71,18 +78,20 @@ def update(self, group, name, description): return self._update('/os-security-groups/%s' % base.getid(group), body, 'security_group') + @api_versions.deprecated_after('2.35') def delete(self, group): """ - Delete a security group + DEPRECATED: Delete a security group :param group: The security group to delete (group or ID) :returns: An instance of novaclient.base.TupleWithMeta """ return self._delete('/os-security-groups/%s' % base.getid(group)) + @api_versions.deprecated_after('2.35') def get(self, group_id): """ - Get a security group + DEPRECATED: Get a security group :param group_id: The security group to get by ID :rtype: :class:`SecurityGroup` @@ -90,9 +99,10 @@ def get(self, group_id): return self._get('/os-security-groups/%s' % group_id, 'security_group') + @api_versions.deprecated_after('2.35') def list(self, search_opts=None): """ - Get a list of all security_groups + DEPRECATED: Get a list of all security_groups :rtype: list of :class:`SecurityGroup` """ From 4215e3fd2f0a32e5f6b5f05aa4f4656efee384d2 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Fri, 19 Aug 2016 14:33:11 -0400 Subject: [PATCH 1126/1705] Cap baremetal python APIs at 2.35 The 2.36 microversion makes the baremetal APIs return a 404 so rather than get that back, this change wraps the baremetal python API methods so they are capped at 2.35. Using them after that version will raise VersionNotFoundForAPIMethod. Related to blueprint deprecate-api-proxies Change-Id: Icc9fc7ce2e2115ce101d95c37024223d1d650fa2 --- novaclient/v2/contrib/baremetal.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/novaclient/v2/contrib/baremetal.py b/novaclient/v2/contrib/baremetal.py index 7f0138816..57933891d 100644 --- a/novaclient/v2/contrib/baremetal.py +++ b/novaclient/v2/contrib/baremetal.py @@ -22,6 +22,7 @@ import sys import warnings +from novaclient import api_versions from novaclient import base from novaclient.i18n import _ from novaclient import utils @@ -64,6 +65,7 @@ class BareMetalNodeManager(base.ManagerWithFind): """ resource_class = BareMetalNode + @api_versions.wraps('2.0', '2.35') def get(self, node_id): """ DEPRECATED: Get a baremetal node. @@ -74,6 +76,7 @@ def get(self, node_id): warnings.warn(DEPRECATION_WARNING, DeprecationWarning) return self._get("/os-baremetal-nodes/%s" % node_id, 'node') + @api_versions.wraps('2.0', '2.35') def list(self): """ DEPRECATED: Get a list of all baremetal nodes. @@ -83,6 +86,7 @@ def list(self): warnings.warn(DEPRECATION_WARNING, DeprecationWarning) return self._list('/os-baremetal-nodes', 'nodes') + @api_versions.wraps('2.0', '2.35') def list_interfaces(self, node_id): """ DEPRECATED: List the interfaces on a baremetal node. From 20b721e1ad434cdebe1a4aa34cca8d1e2772fdcf Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Fri, 19 Aug 2016 15:00:40 -0400 Subject: [PATCH 1127/1705] Cap image API deprecated methods at 2.35 The image proxy API GET/DELETE methods are deprecated at microversion 2.36 and will return a 404 after that. This adds the wraps decorator to those python API methods so set the cap at 2.35 so rather than get a 404 you'll get a more uesful VersionNotFoundForAPIMethod. Note that set_meta and delete_meta are not decorated because we missed deprecating the image-metadata proxy API with the 2.36 microversion. That will be fixed in Ocata. Related to blueprint deprecate-api-proxies Change-Id: Ie65efadb5c65e8a624ffd0315a634accd49f1c30 --- novaclient/api_versions.py | 3 +++ novaclient/tests/unit/v2/test_images.py | 9 +++++++++ novaclient/v2/images.py | 11 +++++++++++ 3 files changed, 23 insertions(+) diff --git a/novaclient/api_versions.py b/novaclient/api_versions.py index 4abea01a3..65e510b5c 100644 --- a/novaclient/api_versions.py +++ b/novaclient/api_versions.py @@ -374,6 +374,9 @@ def get_substitutions(func_name, api_version=None): return sorted(substitutions, key=lambda m: m.start_version) +# FIXME(mriedem): This breaks any ManagerWithFind.list method that has a +# 'detailed' kwarg since the ManagerWithFind.findall won't find the correct +# argspec from the wrapped list method. def wraps(start_version, end_version=None): start_version = APIVersion(start_version) if end_version: diff --git a/novaclient/tests/unit/v2/test_images.py b/novaclient/tests/unit/v2/test_images.py index bdafdf2cb..7b8b4c840 100644 --- a/novaclient/tests/unit/v2/test_images.py +++ b/novaclient/tests/unit/v2/test_images.py @@ -15,6 +15,8 @@ import mock +from novaclient import api_versions +from novaclient import exceptions from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit.fixture_data import images as data from novaclient.tests.unit import utils @@ -96,3 +98,10 @@ def test_find(self, mock_warn): self.assert_request_id(iml, fakes.FAKE_REQUEST_ID_LIST) self.assertEqual(1, len(iml)) self.assertEqual('My Server Backup', iml[0].name) + + def test_find_2_36(self): + """Tests that using the find method fails after microversion 2.35. + """ + self.cs.api_version = api_versions.APIVersion('2.36') + self.assertRaises(exceptions.VersionNotFoundForAPIMethod, + self.cs.images.find, name="CentOS 5.2") diff --git a/novaclient/v2/images.py b/novaclient/v2/images.py index d00abba1a..7371d4bea 100644 --- a/novaclient/v2/images.py +++ b/novaclient/v2/images.py @@ -21,6 +21,7 @@ from oslo_utils import uuidutils from six.moves.urllib import parse +from novaclient import api_versions from novaclient import base from novaclient import exceptions from novaclient.i18n import _ @@ -91,6 +92,7 @@ class ImageManager(base.ManagerWithFind): """ resource_class = Image + @api_versions.wraps('2.0', '2.35') def get(self, image): """ DEPRECATED: Get an image. @@ -113,6 +115,14 @@ def list(self, detailed=True, limit=None, marker=None): :param marker: Begin returning images that appear later in the image list than that represented by this image id (optional). """ + # FIXME(mriedem): Should use the api_versions.wraps decorator but that + # breaks the ManagerWithFind.findall method which checks the argspec + # on this function looking for the 'detailed' arg, and it's getting + # tripped up if you use the wraps decorator. This is all deprecated for + # removal anyway so we probably don't care too much about this. + if self.api.api_version > api_versions.APIVersion('2.35'): + raise exceptions.VersionNotFoundForAPIMethod( + self.api.api_version, 'list') warnings.warn( 'The novaclient.v2.images module is deprecated and will be ' 'removed after Nova 15.0.0 is released. Use python-glanceclient ' @@ -129,6 +139,7 @@ def list(self, detailed=True, limit=None, marker=None): query = '?%s' % parse.urlencode(params) if params else '' return self._list('/images%s%s' % (detail, query), 'images') + @api_versions.wraps('2.0', '2.35') def delete(self, image): """ DEPRECATED: Delete an image. From 030ce53d4ea6a6d80bad97a9f16a96a4a22bfc9e Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Tue, 9 Aug 2016 13:08:09 -0400 Subject: [PATCH 1128/1705] Add support for v2.37 and auto-allocated networking This adds support for the v2.37 microversion. The networks part of the server create request is required in this microversion so if nothing is specified for --nic arguments on the command line we default to 'auto' for backward compatibility in the CLI. Part of blueprint get-me-a-network Change-Id: I6636ddcd3be7bf393d2d69cc6c1ba5c7d65ff674 --- novaclient/__init__.py | 2 +- .../tests/functional/v2/test_servers.py | 56 ++++++++++++++ novaclient/tests/unit/v2/test_servers.py | 73 +++++++++++++++++-- novaclient/tests/unit/v2/test_shell.py | 59 +++++++++++++++ novaclient/v2/servers.py | 71 ++++++++++++------ novaclient/v2/shell.py | 59 ++++++++++++++- .../microversion-2.37-d03da96406a45e67.yaml | 27 +++++++ 7 files changed, 315 insertions(+), 32 deletions(-) create mode 100644 releasenotes/notes/microversion-2.37-d03da96406a45e67.yaml diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 942f43cad..f9270766f 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.36") +API_MAX_VERSION = api_versions.APIVersion("2.37") diff --git a/novaclient/tests/functional/v2/test_servers.py b/novaclient/tests/functional/v2/test_servers.py index 6d03b7914..3d8c5128e 100644 --- a/novaclient/tests/functional/v2/test_servers.py +++ b/novaclient/tests/functional/v2/test_servers.py @@ -172,3 +172,59 @@ def test_delete_all(self): uuid = self._boot_server_with_tags() self.nova("server-tag-delete-all %s" % uuid) self.assertEqual([], self.client.servers.tag_list(uuid)) + + +class TestServersAutoAllocateNetworkCLI(base.ClientTestBase): + + COMPUTE_API_VERSION = '2.37' + + def _find_network_in_table(self, table): + # Example: + # +-----------------+-----------------------------------+ + # | Property | Value | + # +-----------------+-----------------------------------+ + # | private network | 192.168.154.128 | + # +-----------------+-----------------------------------+ + for line in table.split('\n'): + if '|' in line: + l_property, l_value = line.split('|')[1:3] + if ' network' in l_property.strip(): + return ' '.join(l_property.strip().split()[:-1]) + + def test_boot_server_with_auto_network(self): + """Tests that the CLI defaults to 'auto' when --nic isn't specified. + """ + server_info = self.nova('boot', params=( + '%(name)s --flavor %(flavor)s --poll ' + '--image %(image)s ' % {'name': self.name_generate('server'), + 'flavor': self.flavor.id, + 'image': self.image.id})) + server_id = self._get_value_from_the_table(server_info, 'id') + self.addCleanup(self.wait_for_resource_delete, + server_id, self.client.servers) + self.addCleanup(self.client.servers.delete, server_id) + # get the server details to verify there is a network, we don't care + # what the network name is, we just want to see an entry show up + server_info = self.nova('show', params=server_id) + network = self._find_network_in_table(server_info) + self.assertIsNotNone( + network, 'Auto-allocated network not found: %s' % server_info) + + def test_boot_server_with_no_network(self): + """Tests that '--nic none' is honored. + """ + server_info = self.nova('boot', params=( + '%(name)s --flavor %(flavor)s --poll ' + '--image %(image)s --nic none' % + {'name': self.name_generate('server'), + 'flavor': self.flavor.id, + 'image': self.image.id})) + server_id = self._get_value_from_the_table(server_info, 'id') + self.addCleanup(self.wait_for_resource_delete, + server_id, self.client.servers) + self.addCleanup(self.client.servers.delete, server_id) + # get the server details to verify there is not a network + server_info = self.nova('show', params=server_id) + network = self._find_network_in_table(server_info) + self.assertIsNone( + network, 'Unexpected network allocation: %s' % server_info) diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index 06d993092..2e057b03a 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -42,6 +42,11 @@ def setUp(self): if self.api_version: self.cs.api_version = api_versions.APIVersion(self.api_version) + def _get_server_create_default_nics(self): + """Callback for default nics kwarg when creating a server. + """ + return None + def test_list_servers(self): sl = self.cs.servers.list() self.assert_request_id(sl, fakes.FAKE_REQUEST_ID_LIST) @@ -132,7 +137,8 @@ def test_create_server(self): files={ '/etc/passwd': 'some data', # a file '/tmp/foo.txt': six.StringIO('data'), # a stream - } + }, + nics=self._get_server_create_default_nics() ) self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers') @@ -191,7 +197,8 @@ def wrapped_boot(url, key, *boot_args, **boot_kwargs): meta={'foo': 'bar'}, userdata="hello moto", key_name="fakekey", - block_device_mapping_v2=bdm + block_device_mapping_v2=bdm, + nics=self._get_server_create_default_nics() ) self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/os-volumes_boot') @@ -239,7 +246,8 @@ def wrapped_boot(url, key, *boot_args, **boot_kwargs): userdata="hello moto", key_name="fakekey", access_ip_v6=access_ip_v6, - access_ip_v4=access_ip_v4 + access_ip_v4=access_ip_v4, + nics=self._get_server_create_default_nics() ) self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers') @@ -256,6 +264,7 @@ def test_create_server_userdata_file_object(self): '/etc/passwd': 'some data', # a file '/tmp/foo.txt': six.StringIO('data'), # a stream }, + nics=self._get_server_create_default_nics(), ) self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers') @@ -273,6 +282,7 @@ def test_create_server_userdata_unicode(self): '/etc/passwd': 'some data', # a file '/tmp/foo.txt': six.StringIO('data'), # a stream }, + nics=self._get_server_create_default_nics(), ) self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers') @@ -290,6 +300,7 @@ def test_create_server_userdata_utf8(self): '/etc/passwd': 'some data', # a file '/tmp/foo.txt': six.StringIO('data'), # a stream }, + nics=self._get_server_create_default_nics(), ) self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers') @@ -303,7 +314,8 @@ def test_create_server_admin_pass(self): image=1, flavor=1, admin_pass=test_password, - key_name=test_key + key_name=test_key, + nics=self._get_server_create_default_nics() ) self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers') @@ -328,6 +340,7 @@ def test_create_server_userdata_bin(self): '/etc/passwd': 'some data', # a file '/tmp/foo.txt': six.StringIO('data'), # a stream }, + nics=self._get_server_create_default_nics(), ) self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers') @@ -343,7 +356,8 @@ def _create_disk_config(self, disk_config): name="My server", image=1, flavor=1, - disk_config=disk_config + disk_config=disk_config, + nics=self._get_server_create_default_nics(), ) self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers') @@ -976,6 +990,16 @@ def test_create_server_with_description(self): key_name="fakekey" ) + def test_create_server_with_nics_auto(self): + """Negative test for specifying nics='auto' before 2.37 + """ + self.assertRaises(ValueError, + self.cs.servers.create, + name='test', + image='d9d8d53c-4b4a-4144-a5e5-b30d9f1fe46a', + flavor='1', + nics='auto') + class ServersV26Test(ServersTest): @@ -1074,7 +1098,8 @@ def test_create_server_with_description(self): flavor=1, meta={'foo': 'bar'}, userdata="hello moto", - key_name="fakekey" + key_name="fakekey", + nics=self._get_server_create_default_nics() ) self.assert_called('POST', '/servers') @@ -1260,3 +1285,39 @@ def test_create_server_boot_from_volume_tagged_bdm_v2_pre232(self): image=1, flavor=1, meta={'foo': 'bar'}, userdata="hello moto", key_name="fakekey", block_device_mapping_v2=bdm) + + +class ServersV2_37Test(ServersV226Test): + + api_version = "2.37" + + def _get_server_create_default_nics(self): + return 'auto' + + def test_create_server_no_nics(self): + """Tests that nics are required in microversion 2.37+ + """ + self.assertRaises(ValueError, self.cs.servers.create, + name='test', + image='d9d8d53c-4b4a-4144-a5e5-b30d9f1fe46a', + flavor='1') + + def test_create_server_with_nics_auto(self): + s = self.cs.servers.create( + name='test', image='d9d8d53c-4b4a-4144-a5e5-b30d9f1fe46a', + flavor='1', nics=self._get_server_create_default_nics()) + self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers') + self.assertIsInstance(s, servers.Server) + + def test_add_floating_ip(self): + # self.cs.floating_ips.list() is not available after 2.35 + pass + + def test_add_floating_ip_to_fixed(self): + # self.cs.floating_ips.list() is not available after 2.35 + pass + + def test_remove_floating_ip(self): + # self.cs.floating_ips.list() is not available after 2.35 + pass diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index db8f1abf4..34eec493e 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -606,6 +606,64 @@ def test_boot_invalid_nics_v2_32(self): cmd, api_version='2.32') self.assertIn('tag=tag', six.text_type(ex)) + def test_boot_invalid_nics_v2_36_auto(self): + """This is a negative test to make sure we fail with the correct + message. --nic auto isn't allowed before 2.37. + """ + cmd = ('boot --image %s --flavor 1 --nic auto test' % FAKE_UUID_1) + ex = self.assertRaises(exceptions.CommandError, self.run_command, + cmd, api_version='2.36') + self.assertNotIn('auto,none', six.text_type(ex)) + + def test_boot_invalid_nics_v2_37(self): + """This is a negative test to make sure we fail with the correct + message. + """ + cmd = ('boot --image %s --flavor 1 ' + '--nic net-id=1 --nic auto some-server' % FAKE_UUID_1) + ex = self.assertRaises(exceptions.CommandError, self.run_command, + cmd, api_version='2.37') + self.assertIn('auto,none', six.text_type(ex)) + + def test_boot_nics_auto_allocate_default(self): + """Tests that if microversion>=2.37 is specified and no --nics are + specified that a single --nic with net-id=auto is used. + """ + cmd = 'boot --image %s --flavor 1 some-server' % FAKE_UUID_1 + self.run_command(cmd, api_version='2.37') + self.assert_called_anytime( + 'POST', '/servers', + { + 'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': FAKE_UUID_1, + 'min_count': 1, + 'max_count': 1, + 'networks': 'auto', + }, + }, + ) + + def test_boot_nics_auto_allocate_none(self): + """Tests specifying '--nic none' with microversion 2.37 + """ + cmd = 'boot --image %s --flavor 1 --nic none some-server' % FAKE_UUID_1 + self.run_command(cmd, api_version='2.37') + self.assert_called_anytime( + 'POST', '/servers', + { + 'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': FAKE_UUID_1, + 'min_count': 1, + 'max_count': 1, + 'networks': 'none', + }, + }, + ) + def test_boot_nics_ipv6(self): cmd = ('boot --image %s --flavor 1 ' '--nic net-id=a=c,v6-fixed-ip=2001:db9:0:1::10 some-server' % @@ -3062,6 +3120,7 @@ def test_versions(self): 32, # doesn't require separate version-wrapped methods in # novaclient 34, # doesn't require any changes in novaclient + 37, # There are no versioned wrapped shell method changes for this ]) versions_supported = set(range(0, novaclient.API_MAX_VERSION.ver_minor + 1)) diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index ebeaaee30..e24fa43a5 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -709,27 +709,32 @@ def _boot(self, resource_url, response_key, name, image, flavor, body['server']['block_device_mapping_v2'] = block_device_mapping_v2 if nics is not None: - # NOTE(tr3buchet): nics can be an empty list - all_net_data = [] - for nic_info in nics: - net_data = {} - # if value is empty string, do not send value in body - if nic_info.get('net-id'): - net_data['uuid'] = nic_info['net-id'] - if (nic_info.get('v4-fixed-ip') and - nic_info.get('v6-fixed-ip')): - raise base.exceptions.CommandError(_( - "Only one of 'v4-fixed-ip' and 'v6-fixed-ip' may be" - " provided.")) - elif nic_info.get('v4-fixed-ip'): - net_data['fixed_ip'] = nic_info['v4-fixed-ip'] - elif nic_info.get('v6-fixed-ip'): - net_data['fixed_ip'] = nic_info['v6-fixed-ip'] - if nic_info.get('port-id'): - net_data['port'] = nic_info['port-id'] - if nic_info.get('tag'): - net_data['tag'] = nic_info['tag'] - all_net_data.append(net_data) + # With microversion 2.37+ nics can be an enum of 'auto' or 'none' + # or a list of dicts. + if isinstance(nics, six.string_types): + all_net_data = nics + else: + # NOTE(tr3buchet): nics can be an empty list + all_net_data = [] + for nic_info in nics: + net_data = {} + # if value is empty string, do not send value in body + if nic_info.get('net-id'): + net_data['uuid'] = nic_info['net-id'] + if (nic_info.get('v4-fixed-ip') and + nic_info.get('v6-fixed-ip')): + raise base.exceptions.CommandError(_( + "Only one of 'v4-fixed-ip' and 'v6-fixed-ip' " + "may be provided.")) + elif nic_info.get('v4-fixed-ip'): + net_data['fixed_ip'] = nic_info['v4-fixed-ip'] + elif nic_info.get('v6-fixed-ip'): + net_data['fixed_ip'] = nic_info['v6-fixed-ip'] + if nic_info.get('port-id'): + net_data['port'] = nic_info['port-id'] + if nic_info.get('tag'): + net_data['tag'] = nic_info['tag'] + all_net_data.append(net_data) body['server']['networks'] = all_net_data if disk_config is not None: @@ -1219,6 +1224,14 @@ def diagnostics(self, server): base.getid(server)) return base.TupleWithMeta((resp, body), resp) + def _validate_create_nics(self, nics): + # nics are required with microversion 2.37+ and can be a string or list + if self.api_version > api_versions.APIVersion('2.36'): + if not nics: + raise ValueError('nics are required after microversion 2.36') + elif nics and not isinstance(nics, list): + raise ValueError('nics must be a list') + def create(self, name, image, flavor, meta=None, files=None, reservation_id=None, min_count=None, max_count=None, security_groups=None, userdata=None, @@ -1259,9 +1272,17 @@ def create(self, name, image, flavor, meta=None, files=None, device mappings for this server. :param block_device_mapping_v2: (optional extension) A dict of block device mappings for this server. - :param nics: (optional extension) an ordered list of nics to be - added to this server, with information about - connected networks, fixed IPs, port etc. + :param nics: An ordered list of nics (dicts) to be added to this + server, with information about connected networks, + fixed IPs, port etc. + Beginning in microversion 2.37 this field is required and + also supports a single string value of 'auto' or 'none'. + The 'auto' value means the Compute service will + automatically allocate a network for the project if one + is not available. This is the same behavior as not + passing anything for nics before microversion 2.37. The + 'none' value tells the Compute service to not allocate + any networking for the server. :param scheduler_hints: (optional extension) arbitrary key-value pairs specified by the client to help boot an instance :param config_drive: (optional extension) value for config drive @@ -1289,6 +1310,8 @@ def create(self, name, image, flavor, meta=None, files=None, if "description" in kwargs and self.api_version < descr_microversion: raise exceptions.UnsupportedAttribute("description", "2.19") + self._validate_create_nics(nics) + tags_microversion = api_versions.APIVersion("2.32") if self.api_version < tags_microversion: if nics: diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index b49656a55..a823cd4a9 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -188,7 +188,16 @@ def _parse_block_device_mapping_v2(args, image): def _parse_nics(cs, args): - if cs.api_version >= api_versions.APIVersion('2.32'): + supports_auto_alloc = cs.api_version >= api_versions.APIVersion('2.37') + if supports_auto_alloc: + err_msg = (_("Invalid nic argument '%s'. Nic arguments must be of " + "the form --nic , " + "with only one of net-id, net-name or port-id " + "specified. Specifying a --nic of auto or none cannot " + "be used with any other --nic value.")) + elif cs.api_version >= api_versions.APIVersion('2.32'): err_msg = (_("Invalid nic argument '%s'. Nic arguments must be of " "the form --nic , " "with only one of net-id, net-name or port-id " "specified.")) + auto_or_none = False nics = [] for nic_str in args.nics: nic_info = {"net-id": "", "v4-fixed-ip": "", "v6-fixed-ip": "", @@ -209,6 +219,13 @@ def _parse_nics(cs, args): for kv_str in nic_str.split(","): try: + # handle the special auto/none cases + if kv_str in ('auto', 'none'): + if not supports_auto_alloc: + raise exceptions.CommandError(err_msg % nic_str) + nics.append(kv_str) + auto_or_none = True + continue k, v = kv_str.split("=", 1) except ValueError: raise exceptions.CommandError(err_msg % nic_str) @@ -225,6 +242,9 @@ def _parse_nics(cs, args): else: raise exceptions.CommandError(err_msg % nic_str) + if auto_or_none: + continue + if nic_info['v4-fixed-ip'] and not netutils.is_valid_ipv4( nic_info['v4-fixed-ip']): raise exceptions.CommandError(_("Invalid ipv4 address.")) @@ -238,6 +258,17 @@ def _parse_nics(cs, args): nics.append(nic_info) + if nics: + if auto_or_none: + if len(nics) > 1: + raise exceptions.CommandError(err_msg % nic_str) + # change the single list entry to a string + nics = nics[0] + else: + # Default to 'auto' if API version >= 2.37 and nothing was specified + if supports_auto_alloc: + nics = 'auto' + return nics @@ -577,6 +608,7 @@ def _boot(cs, args): dest='nics', default=[], start_version='2.32', + end_version='2.36', help=_("Create a NIC on the server. " "Specify option multiple times to create multiple nics. " "net-id: attach NIC to network with this UUID " @@ -587,6 +619,31 @@ def _boot(cs, args): "port-id: attach NIC to port with this UUID " "tag: interface metadata tag (optional) " "(either port-id or net-id must be provided).")) +@utils.arg( + '--nic', + metavar="", + action='append', + dest='nics', + default=[], + start_version='2.37', + help=_("Create a NIC on the server. " + "Specify option multiple times to create multiple nics unless " + "using the special 'auto' or 'none' values. " + "auto: automatically allocate network resources if none are " + "available. This cannot be specified with any other nic value and " + "cannot be specified multiple times. " + "none: do not attach a NIC at all. This cannot be specified " + "with any other nic value and cannot be specified multiple times. " + "net-id: attach NIC to network with a specific UUID. " + "net-name: attach NIC to network with this name " + "(either port-id or net-id or net-name must be provided), " + "v4-fixed-ip: IPv4 fixed address for NIC (optional), " + "v6-fixed-ip: IPv6 fixed address for NIC (optional), " + "port-id: attach NIC to port with this UUID " + "tag: interface metadata tag (optional) " + "(either port-id or net-id must be provided).")) @utils.arg( '--config-drive', metavar="", diff --git a/releasenotes/notes/microversion-2.37-d03da96406a45e67.yaml b/releasenotes/notes/microversion-2.37-d03da96406a45e67.yaml new file mode 100644 index 000000000..211a4f9ce --- /dev/null +++ b/releasenotes/notes/microversion-2.37-d03da96406a45e67.yaml @@ -0,0 +1,27 @@ +--- +features: + - | + The 2.37 microversion is now supported. This introduces the following + changes: + + * CLI: The **--nic** value for the **nova boot** command now takes two + special values, 'auto' and 'none'. If --nic is not specified, the + CLI defaults to 'auto'. + * Python API: The **nics** kwarg is required when creating a server using + the *novaclient.v2.servers.ServerManager.create* API. The **nics** + value can be a list of dicts or a string with value 'auto' or + 'none'. + +upgrade: + - | + With the 2.37 microversion, the **nics** kwarg is required when creating + a server using the *novaclient.v2.servers.ServerManager.create* API. The + **nics** value can be a list of dicts or an enum string with one of the + following values: + + * **auto**: This tells the Compute service to automatically allocate a + network for the project if one is not available and then associate + an IP from that network with the server. This is the same behavior as + passing nics=None before the 2.37 microversion. + * **none**: This tells the Compute service to not allocate any networking + for the server. From 01de5a9f68ad474e9bfa213ffeb3501066a52ebd Mon Sep 17 00:00:00 2001 From: Ukesh Kumar Vasudevan Date: Fri, 19 Aug 2016 18:10:19 +0530 Subject: [PATCH 1129/1705] Pick first image if can't find the specific image In pick_image method to just pick the first image it finds if it can't find the specific cirros-*uec image. Also look for the -disk.img since devstack is changing the default image in: Id65ebae73b28da7185cb349b714b659af51ef77f Change-Id: I0e4c38a9e0ae25a922f6b0e09f3f2531f49c88c9 Closes-Bug: 1614118 --- novaclient/tests/functional/base.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/novaclient/tests/functional/base.py b/novaclient/tests/functional/base.py index 494d8aab6..78761bd11 100644 --- a/novaclient/tests/functional/base.py +++ b/novaclient/tests/functional/base.py @@ -61,8 +61,16 @@ def pick_flavor(flavors): def pick_image(images): for image in images: - if image.name.startswith('cirros') and image.name.endswith('-uec'): + if image.name.startswith('cirros') and ( + image.name.endswith('-uec') or + image.name.endswith('-disk.img')): return image + + # We didn't find the specific cirros image we'd like to use, so just use + # the first available. + if images: + return images[0] + raise NoImageException() From 7a2c412dd378a27b5b6510c50b2b589c7dd25b5f Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Sun, 21 Aug 2016 00:10:05 +0000 Subject: [PATCH 1130/1705] Updated from global requirements Change-Id: Iee9ace66d3e1c0199c0746d68b2d16c8d102361c --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 702e21bff..0734af816 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -10,7 +10,7 @@ keyring>=5.5.1 # MIT/PSF mock>=2.0 # BSD python-keystoneclient!=2.1.0,>=2.0.0 # Apache-2.0 python-cinderclient!=1.7.0,!=1.7.1,>=1.6.0 # Apache-2.0 -python-glanceclient>=2.0.0 # Apache-2.0 +python-glanceclient!=2.4.0,>=2.0.0 # Apache-2.0 requests-mock>=1.0 # Apache-2.0 sphinx!=1.3b1,<1.3,>=1.2.1 # BSD os-client-config!=1.19.0,!=1.19.1,!=1.20.0,>=1.13.1 # Apache-2.0 From 3b834f25c1d31ef482116344f97b0a1059d9a836 Mon Sep 17 00:00:00 2001 From: Ukesh Kumar Vasudevan Date: Mon, 22 Aug 2016 17:13:00 +0530 Subject: [PATCH 1131/1705] functional tests fail if cirros image not exist The functional tests are looking for a cirros-*uec image and if that isn't found they fail: In the function novaclient/tests/functional/base.py:pick_image, images variable is a generator object which doesn't have '__getitem__' attribute and so the functional test is failing. Change-Id: I32b05ff6f2c7501eff04fa9f180eecf3099389ab Closes-Bug: 1615594 --- novaclient/tests/functional/base.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/novaclient/tests/functional/base.py b/novaclient/tests/functional/base.py index 1bd2e809a..febe1669a 100644 --- a/novaclient/tests/functional/base.py +++ b/novaclient/tests/functional/base.py @@ -60,7 +60,9 @@ def pick_flavor(flavors): def pick_image(images): + firstImage = None for image in images: + firstImage = firstImage or image if image.name.startswith('cirros') and ( image.name.endswith('-uec') or image.name.endswith('-disk.img')): @@ -68,8 +70,8 @@ def pick_image(images): # We didn't find the specific cirros image we'd like to use, so just use # the first available. - if images: - return images[0] + if firstImage: + return firstImage raise NoImageException() From a14e30e0ecf9b464b2cfe62c4bd1fd29e015c961 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Mon, 22 Aug 2016 15:12:19 +0300 Subject: [PATCH 1132/1705] [functional] Do not discover same resources for every test Let's discover images, flavors and networks once per test process and store it in global variable Change-Id: Ife5133e541a4355f308a21658c7fcf4266f78091 --- novaclient/tests/functional/base.py | 68 ++++++++++++++++------------- 1 file changed, 37 insertions(+), 31 deletions(-) diff --git a/novaclient/tests/functional/base.py b/novaclient/tests/functional/base.py index 1bd2e809a..17810064f 100644 --- a/novaclient/tests/functional/base.py +++ b/novaclient/tests/functional/base.py @@ -104,8 +104,7 @@ class NoCloudConfigException(Exception): pass -USE_NEUTRON = None -SERVER_API_VERSIONS = None +CACHE = {} class ClientTestBase(testtools.TestCase): @@ -216,18 +215,24 @@ def setUp(self): self.glance = glanceclient.Client('2', session=session) # pick some reasonable flavor / image combo - self.flavor = pick_flavor(self.client.flavors.list()) - self.image = pick_image(self.glance.images.list()) - - tested_api_version = self.client.api_version - proxy_api_version = novaclient.api_versions.APIVersion('2.35') - if tested_api_version > proxy_api_version: - self.client.api_version = proxy_api_version - try: - # TODO(mriedem): Get the networks from neutron if using neutron. - self.network = pick_network(self.client.networks.list()) - finally: - self.client.api_version = tested_api_version + if "flavor" not in CACHE: + CACHE["flavor"] = pick_flavor(self.client.flavors.list()) + if "image" not in CACHE: + CACHE["image"] = pick_image(self.glance.images.list()) + self.flavor = CACHE["flavor"] + self.image = CACHE["image"] + + if "network" not in CACHE: + tested_api_version = self.client.api_version + proxy_api_version = novaclient.api_versions.APIVersion('2.35') + if tested_api_version > proxy_api_version: + self.client.api_version = proxy_api_version + try: + # TODO(mriedem): Get the networks from neutron if using neutron + CACHE["network"] = pick_network(self.client.networks.list()) + finally: + self.client.api_version = tested_api_version + self.network = CACHE["network"] # create a CLI client in case we'd like to do CLI # testing. tempest.lib does this really weird thing where it @@ -251,50 +256,51 @@ def setUp(self): password=passwd) self.cinder = cinderclient.Client(auth=auth, session=session) - global USE_NEUTRON - if USE_NEUTRON is None: + if "use_neutron" not in CACHE: # check to see if we're running with neutron or not for service in self.keystone.services.list(): if service.type == 'network': - USE_NEUTRON = True + CACHE["use_neutron"] = True break else: - USE_NEUTRON = False + CACHE["use_neutron"] = False def _get_novaclient(self, session): nc = novaclient.client.Client("2", session=session) if self.COMPUTE_API_VERSION: - global SERVER_API_VERSIONS - if SERVER_API_VERSIONS is None: + if "min_api_version" not in CACHE: # Obtain supported versions by API side v = nc.versions.get_current() if not hasattr(v, 'version') or not v.version: # API doesn't support microversions - SERVER_API_VERSIONS = ( - novaclient.api_versions.APIVersion("2.0"), + CACHE["min_api_version"] = ( + novaclient.api_versions.APIVersion("2.0")) + CACHE["max_api_version"] = ( novaclient.api_versions.APIVersion("2.0")) else: - SERVER_API_VERSIONS = ( - novaclient.api_versions.APIVersion(v.min_version), + CACHE["min_api_version"] = ( + novaclient.api_versions.APIVersion(v.min_version)) + CACHE["max_api_version"] = ( novaclient.api_versions.APIVersion(v.version)) if self.COMPUTE_API_VERSION == "2.latest": requested_version = min(novaclient.API_MAX_VERSION, - SERVER_API_VERSIONS[1]) + CACHE["max_api_version"]) else: requested_version = novaclient.api_versions.APIVersion( self.COMPUTE_API_VERSION) - if not requested_version.matches(*SERVER_API_VERSIONS): + if not requested_version.matches(CACHE["min_api_version"], + CACHE["max_api_version"]): msg = ("%s is not supported by Nova-API. Supported version" % self.COMPUTE_API_VERSION) - if SERVER_API_VERSIONS[0] == SERVER_API_VERSIONS[1]: - msg += ": %s" % SERVER_API_VERSIONS[0].get_string() + if CACHE["min_api_version"] == CACHE["max_api_version"]: + msg += ": %s" % CACHE["min_api_version"].get_string() else: msg += "s: %s - %s" % ( - SERVER_API_VERSIONS[0].get_string(), - SERVER_API_VERSIONS[1].get_string()) + CACHE["min_api_version"].get_string(), + CACHE["max_api_version"].get_string()) self.skipTest(msg) nc.api_version = requested_version @@ -486,7 +492,7 @@ def _get_project_id(self, name): return project.id def skip_if_neutron(self): - if USE_NEUTRON: + if CACHE["use_neutron"]: self.skipTest('nova-network is not available') From 8751475a25d7bd1971241023fa6519a24d9ca91b Mon Sep 17 00:00:00 2001 From: bhagyashris Date: Tue, 21 Jun 2016 20:49:36 +0530 Subject: [PATCH 1133/1705] Removed unused 'install_venv' module TrivialFix Change-Id: I022ca5831e9e4d66b1cc2d6f895a1ad709fedefb --- tools/install_venv.py | 74 ------------------------------------------- 1 file changed, 74 deletions(-) delete mode 100644 tools/install_venv.py diff --git a/tools/install_venv.py b/tools/install_venv.py deleted file mode 100644 index d51c8d90d..000000000 --- a/tools/install_venv.py +++ /dev/null @@ -1,74 +0,0 @@ -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Copyright 2010 OpenStack Foundation -# Copyright 2013 IBM Corp. -# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import ConfigParser -import os -import sys - -import install_venv_common as install_venv - - -def print_help(project, venv, root): - help = """ - %(project)s development environment setup is complete. - - %(project)s development uses virtualenv to track and manage Python - dependencies while in development and testing. - - To activate the %(project)s virtualenv for the extent of your current - shell session you can run: - - $ source %(venv)s/bin/activate - - Or, if you prefer, you can run commands in the virtualenv on a case by - case basis by running: - - $ %(root)s/tools/with_venv.sh - """ - print(help % dict(project=project, venv=venv, root=root)) - - -def main(argv): - root = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) - - if os.environ.get('tools_path'): - root = os.environ['tools_path'] - venv = os.path.join(root, '.venv') - if os.environ.get('venv'): - venv = os.environ['venv'] - - pip_requires = os.path.join(root, 'requirements.txt') - test_requires = os.path.join(root, 'test-requirements.txt') - py_version = "python%s.%s" % (sys.version_info[0], sys.version_info[1]) - setup_cfg = ConfigParser.ConfigParser() - setup_cfg.read('setup.cfg') - project = setup_cfg.get('metadata', 'name') - - install = install_venv.InstallVenv( - root, venv, pip_requires, test_requires, py_version, project) - options = install.parse_args(argv) - install.check_python_version() - install.check_dependencies() - install.create_virtualenv(no_site_packages=options.no_site_packages) - install.install_dependencies() - print_help(project, venv, root) - -if __name__ == '__main__': - main(sys.argv) From 744c9b6adaf51d689ad589e0c727bf800d00628a Mon Sep 17 00:00:00 2001 From: bhagyashris Date: Mon, 22 Aug 2016 12:36:56 +0530 Subject: [PATCH 1134/1705] Replace functions 'Dict.get' and 'del' with 'Dict.pop' Refactoring code: Making 'kwargs' dict to use single instruction: pop() rather than two instructions: get() and del, giving the codes a format that carries through. TrivialFix Change-Id: Icaf4a57314a8fe48f51993d7b32a1671fec40f31 --- novaclient/client.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index 1668af460..2e5f125a6 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -377,8 +377,7 @@ def request(self, url, method, **kwargs): kwargs['headers']['Accept'] = 'application/json' if 'body' in kwargs: kwargs['headers']['Content-Type'] = 'application/json' - kwargs['data'] = json.dumps(kwargs['body']) - del kwargs['body'] + kwargs['data'] = json.dumps(kwargs.pop('body')) api_versions.update_headers(kwargs["headers"], self.api_version) if self.timeout is not None: kwargs.setdefault('timeout', self.timeout) From 7d72ed2d23e5816c492e968bf40af65bed78961b Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Wed, 24 Aug 2016 13:35:52 +0000 Subject: [PATCH 1135/1705] Updated from global requirements Change-Id: I999506490e98f447503340a383c7ee73da6083f1 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 0734af816..76439388e 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -13,7 +13,7 @@ python-cinderclient!=1.7.0,!=1.7.1,>=1.6.0 # Apache-2.0 python-glanceclient!=2.4.0,>=2.0.0 # Apache-2.0 requests-mock>=1.0 # Apache-2.0 sphinx!=1.3b1,<1.3,>=1.2.1 # BSD -os-client-config!=1.19.0,!=1.19.1,!=1.20.0,>=1.13.1 # Apache-2.0 +os-client-config!=1.19.0,!=1.19.1,!=1.20.0,!=1.20.1,>=1.13.1 # Apache-2.0 oslosphinx!=3.4.0,>=2.5.0 # Apache-2.0 testrepository>=0.0.18 # Apache-2.0/BSD testscenarios>=0.4 # Apache-2.0/BSD From 8ebf85922fd3c388ba7791fcd00ac33e3e37ff41 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Thu, 25 Aug 2016 05:05:16 +0000 Subject: [PATCH 1136/1705] Updated from global requirements Change-Id: Iac1df9df01d8ad6a60facf5a4bde2fb040b3ba0d --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 76439388e..b7bb0e9ff 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -10,7 +10,7 @@ keyring>=5.5.1 # MIT/PSF mock>=2.0 # BSD python-keystoneclient!=2.1.0,>=2.0.0 # Apache-2.0 python-cinderclient!=1.7.0,!=1.7.1,>=1.6.0 # Apache-2.0 -python-glanceclient!=2.4.0,>=2.0.0 # Apache-2.0 +python-glanceclient!=2.4.0,>=2.3.0 # Apache-2.0 requests-mock>=1.0 # Apache-2.0 sphinx!=1.3b1,<1.3,>=1.2.1 # BSD os-client-config!=1.19.0,!=1.19.1,!=1.20.0,!=1.20.1,>=1.13.1 # Apache-2.0 From f7dc91dc110f6526886ba9bb27215f6078c94455 Mon Sep 17 00:00:00 2001 From: Thiago Paiva Date: Thu, 29 Oct 2015 12:45:48 -0300 Subject: [PATCH 1137/1705] Fixes TypeError on Client instantiation When instantiating novaclient with kwargs or named attributes, the 'password' field is duplicated due to an overwrite on the __init__ method of Client v2. This patch pops out the password field when it's passed and no api_key is provided. Co-Authored-By: Pavel Kholkin Closes-bug: #1511417 Change-Id: Id8310eccef5f0f9a2983e25dd35541d1eb0efe86 --- novaclient/tests/unit/test_client.py | 6 ++++++ novaclient/v2/client.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/novaclient/tests/unit/test_client.py b/novaclient/tests/unit/test_client.py index bf89a00e6..b256f8f7c 100644 --- a/novaclient/tests/unit/test_client.py +++ b/novaclient/tests/unit/test_client.py @@ -229,6 +229,12 @@ def test_contextmanager_v1_1(self, mock_http_client): self.assertTrue(fake_client.open_session.called) self.assertTrue(fake_client.close_session.called) + def test_client_with_password_in_args_and_kwargs(self): + # check that TypeError is not raised during instantiation of Client + cs = novaclient.client.Client("2", "user", "password", "project_id", + password='pass') + self.assertEqual('pass', cs.client.password) + def test_get_password_simple(self): cs = novaclient.client.HTTPClient("user", "password", "", "") cs.password_func = mock.Mock() diff --git a/novaclient/v2/client.py b/novaclient/v2/client.py index a43a38254..d37ebb627 100644 --- a/novaclient/v2/client.py +++ b/novaclient/v2/client.py @@ -125,7 +125,7 @@ def __init__(self, username=None, api_key=None, project_id=None, # tenant name) and tenant_id is a UUID (what the Nova API # often refers to as a project_id or tenant_id). - password = api_key + password = kwargs.pop('password', api_key) self.projectid = project_id self.tenant_id = tenant_id self.user_id = user_id From d8c07006dd85ad64a4a4a4cb47558c0a19e42e24 Mon Sep 17 00:00:00 2001 From: bhagyashris Date: Tue, 5 Jul 2016 19:24:21 +0530 Subject: [PATCH 1138/1705] Fix 'UnicodeEncodeError' for unicode values Used '%' instead of format() because "%" formatting operation returns a unicode string for both unicode and non-unicode format strings. Encoded the exception error message in novaclient.shell.main and novaclient.utils.do_action_on_many method to avoid "UnicodeEncodeError" and print valid message. NOTE: Not used u'{0}.format() because in python 3 everything is unicode so no need to add explicit 'u' to format string. Closes-Bug: #1403379 Change-Id: I13f1ef2e1b41299b417cad3759a5cda241890df7 --- novaclient/shell.py | 8 ++++++-- novaclient/utils.py | 2 +- novaclient/v2/hosts.py | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/novaclient/shell.py b/novaclient/shell.py index 1f277485d..3a3f28529 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -982,9 +982,13 @@ def main(): OpenStackComputeShell().main(argv) except Exception as exc: logger.debug(exc, exc_info=1) - print(_("ERROR (%(type)s): %(msg)s") % { + if six.PY2: + message = encodeutils.safe_encode(six.text_type(exc)) + else: + message = encodeutils.exception_to_unicode(exc) + print("ERROR (%(type)s): %(msg)s" % { 'type': exc.__class__.__name__, - 'msg': encodeutils.exception_to_unicode(exc)}, + 'msg': message}, file=sys.stderr) sys.exit(1) except KeyboardInterrupt: diff --git a/novaclient/utils.py b/novaclient/utils.py index 093bc9619..e6f3fd48c 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -407,7 +407,7 @@ def do_action_on_many(action, resources, success_msg, error_msg): print(success_msg % resource) except Exception as e: failure_flag = True - print(e) + print(encodeutils.safe_encode(six.text_type(e))) if failure_flag: raise exceptions.CommandError(error_msg) diff --git a/novaclient/v2/hosts.py b/novaclient/v2/hosts.py index 88789e242..f97818a0d 100644 --- a/novaclient/v2/hosts.py +++ b/novaclient/v2/hosts.py @@ -76,7 +76,7 @@ def host_action(self, host, action): :param action: The action to perform returns: An instance of novaclient.base.TupleWithMeta """ - url = '/os-hosts/{0}/{1}'.format(host, action) + url = '/os-hosts/%s/%s' % (host, action) resp, body = self.api.client.get(url) return base.TupleWithMeta((resp, body), resp) From c915a0267d5da2502edfb6639c9f21395b689a5a Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Tue, 30 Aug 2016 13:33:02 -0400 Subject: [PATCH 1139/1705] Fix test_trigger_crash_dump_in_locked_state_nonadmin test Patch 2a70e9688151588347664badbac5d0a59ef0f95c in the server changed the error message that comes back in the case of a locked server which breaks this test. The test is unnecessarily rigid in it's assertion, we should just be checking that we get a 409 back, not what the actual error message is, which is prone to change. Change-Id: I1fd64953e671c6c7fe1068a9c30f5a077712a9fb Closes-Bug: #1618561 --- novaclient/tests/functional/v2/test_trigger_crash_dump.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/novaclient/tests/functional/v2/test_trigger_crash_dump.py b/novaclient/tests/functional/v2/test_trigger_crash_dump.py index 4b6b06731..27180ac5a 100644 --- a/novaclient/tests/functional/v2/test_trigger_crash_dump.py +++ b/novaclient/tests/functional/v2/test_trigger_crash_dump.py @@ -127,6 +127,7 @@ def test_trigger_crash_dump_in_locked_state_nonadmin(self): self.addCleanup(self.another_nova, 'unlock', params=name) output = self.another_nova('trigger-crash-dump %s ' % server_id, fail_ok=True, merge_stderr=True) - self.assertIn("ERROR (Conflict): Instance %s is in an invalid " - "state for 'trigger_crash_dump' (HTTP 409) " % - server_id, output) + # NOTE(mriedem): Depending on the version of the server you can get + # different error messages back from this, so just assert that it's a + # 409 either way. + self.assertIn("ERROR (Conflict)", output) From f628849d385f781a7cac8d794dff8053165ee23f Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Wed, 31 Aug 2016 03:09:56 +0000 Subject: [PATCH 1140/1705] Updated from global requirements Change-Id: Idb0290aa6bee28e258966824da0e8751c5b09318 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index b7bb0e9ff..34d84684b 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -13,7 +13,7 @@ python-cinderclient!=1.7.0,!=1.7.1,>=1.6.0 # Apache-2.0 python-glanceclient!=2.4.0,>=2.3.0 # Apache-2.0 requests-mock>=1.0 # Apache-2.0 sphinx!=1.3b1,<1.3,>=1.2.1 # BSD -os-client-config!=1.19.0,!=1.19.1,!=1.20.0,!=1.20.1,>=1.13.1 # Apache-2.0 +os-client-config!=1.19.0,!=1.19.1,!=1.20.0,!=1.20.1,!=1.21.0,>=1.13.1 # Apache-2.0 oslosphinx!=3.4.0,>=2.5.0 # Apache-2.0 testrepository>=0.0.18 # Apache-2.0/BSD testscenarios>=0.4 # Apache-2.0/BSD From f3fbebdba6989bad29aed8cf84b247ce83c3440b Mon Sep 17 00:00:00 2001 From: Bin Zhou Date: Fri, 2 Sep 2016 12:01:27 +0800 Subject: [PATCH 1141/1705] Modify use of assertTrue(A in B) Developers should use assertIn(A, B) instead of assertTrue(A in B ). TrivialFix Change-Id: Iae5f990574a4198178de238138731df90d27063b --- novaclient/tests/functional/v2/legacy/test_consoles.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/tests/functional/v2/legacy/test_consoles.py b/novaclient/tests/functional/v2/legacy/test_consoles.py index 5595ab144..094531e42 100644 --- a/novaclient/tests/functional/v2/legacy/test_consoles.py +++ b/novaclient/tests/functional/v2/legacy/test_consoles.py @@ -33,7 +33,7 @@ def _test_console_get(self, command, expected_response_type): output, 'Type') self.assertEqual(expected_response_type, console_type, output) except exceptions.CommandFailed as cf: - self.assertTrue('HTTP 400' in str(cf.stderr)) + self.assertIn('HTTP 400', str(cf.stderr)) def _test_vnc_console_get(self): self._test_console_get('get-vnc-console %s novnc', 'novnc') From 42f217042403badf1baa1679509fd2bd0d343a17 Mon Sep 17 00:00:00 2001 From: vnathan Date: Sun, 4 Sep 2016 12:32:16 +0530 Subject: [PATCH 1142/1705] Fix incorrect output of "nova show" for long user data New argument "--wrap" is added for "nova show" command to wrap the output. The default wrap length is set to 0 i.e. no wrap. Usage: nova show --wrap 80 closes-bug: 1616415 Change-Id: I7f4869d743f7bf9ff653183a5767134c796c440a --- novaclient/v2/shell.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index a823cd4a9..ecf5a5963 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -2236,7 +2236,7 @@ def do_meta(cs, args): cs.servers.delete_meta(server, sorted(metadata.keys(), reverse=True)) -def _print_server(cs, args, server=None): +def _print_server(cs, args, server=None, wrap=0): # By default when searching via name we will do a # findall(name=blah) and due a REST /details which is not the same # as a .get() and doesn't get the information about flavors and @@ -2287,7 +2287,7 @@ def _print_server(cs, args, server=None): info.pop('links', None) info.pop('addresses', None) - utils.print_dict(info) + utils.print_dict(info, wrap=wrap) @utils.arg( @@ -2297,9 +2297,12 @@ def _print_server(cs, args, server=None): default=False, help=_('Skips flavor/image lookups when showing servers.')) @utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg( + '--wrap', dest='wrap', metavar='', type=int, default=0, + help=_('Wrap the output to a specified length, or 0 to disable.')) def do_show(cs, args): """Show details about the given server.""" - _print_server(cs, args) + _print_server(cs, args, wrap=args.wrap) @utils.arg( From e02c08dd4f6dde582877b69993351d277a7f11d3 Mon Sep 17 00:00:00 2001 From: Doug Hellmann Date: Fri, 2 Sep 2016 09:43:46 -0400 Subject: [PATCH 1143/1705] Update reno for stable/newton In order to support automatically updating the release notes when we create stable branches, we want the pages to be in a standard order. This patch updates the order to be reverse chronological, so the most recent notes appear at the top. Change-Id: I9a79d95953acdb94b676020a91627b8da4964608 Signed-off-by: Doug Hellmann --- releasenotes/source/index.rst | 6 +++--- releasenotes/source/newton.rst | 6 ++++++ 2 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 releasenotes/source/newton.rst diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index b054c42fe..896ac5545 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -7,10 +7,10 @@ Contents .. toctree:: :maxdepth: 2 - liberty - mitaka unreleased - + newton + mitaka + liberty Indices and tables ================== diff --git a/releasenotes/source/newton.rst b/releasenotes/source/newton.rst new file mode 100644 index 000000000..97036ed25 --- /dev/null +++ b/releasenotes/source/newton.rst @@ -0,0 +1,6 @@ +=================================== + Newton Series Release Notes +=================================== + +.. release-notes:: + :branch: origin/stable/newton From c0e7785427660ff26bdf7d013b46769699110327 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Wed, 21 Sep 2016 06:15:23 +0000 Subject: [PATCH 1144/1705] Updated from global requirements Change-Id: Ia1790ce87fec388aa594394e0867c82e62af3e4e --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 34d84684b..fff5e24cf 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -10,7 +10,7 @@ keyring>=5.5.1 # MIT/PSF mock>=2.0 # BSD python-keystoneclient!=2.1.0,>=2.0.0 # Apache-2.0 python-cinderclient!=1.7.0,!=1.7.1,>=1.6.0 # Apache-2.0 -python-glanceclient!=2.4.0,>=2.3.0 # Apache-2.0 +python-glanceclient>=2.5.0 # Apache-2.0 requests-mock>=1.0 # Apache-2.0 sphinx!=1.3b1,<1.3,>=1.2.1 # BSD os-client-config!=1.19.0,!=1.19.1,!=1.20.0,!=1.20.1,!=1.21.0,>=1.13.1 # Apache-2.0 From 7592a6e4d1fa8345183e33e620a340d761946191 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Mon, 26 Sep 2016 04:27:06 +0000 Subject: [PATCH 1145/1705] Updated from global requirements Change-Id: Ieeafa78a52caf6d91ade096853a430761488ebb5 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index fff5e24cf..d51bbdf14 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -11,7 +11,7 @@ mock>=2.0 # BSD python-keystoneclient!=2.1.0,>=2.0.0 # Apache-2.0 python-cinderclient!=1.7.0,!=1.7.1,>=1.6.0 # Apache-2.0 python-glanceclient>=2.5.0 # Apache-2.0 -requests-mock>=1.0 # Apache-2.0 +requests-mock>=1.1 # Apache-2.0 sphinx!=1.3b1,<1.3,>=1.2.1 # BSD os-client-config!=1.19.0,!=1.19.1,!=1.20.0,!=1.20.1,!=1.21.0,>=1.13.1 # Apache-2.0 oslosphinx!=3.4.0,>=2.5.0 # Apache-2.0 From c26f0f58615565d648746c3fc5eb6eda318b3830 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Tue, 27 Sep 2016 10:07:32 +0000 Subject: [PATCH 1146/1705] Updated from global requirements Change-Id: Iec0c4fbf6e2be419dc52fbd9144555050e0a25df --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index d51bbdf14..56bb156d7 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -14,7 +14,7 @@ python-glanceclient>=2.5.0 # Apache-2.0 requests-mock>=1.1 # Apache-2.0 sphinx!=1.3b1,<1.3,>=1.2.1 # BSD os-client-config!=1.19.0,!=1.19.1,!=1.20.0,!=1.20.1,!=1.21.0,>=1.13.1 # Apache-2.0 -oslosphinx!=3.4.0,>=2.5.0 # Apache-2.0 +oslosphinx>=4.7.0 # Apache-2.0 testrepository>=0.0.18 # Apache-2.0/BSD testscenarios>=0.4 # Apache-2.0/BSD testtools>=1.4.0 # MIT From 8e28309859e417098392b7ac3967273eab57196a Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Wed, 28 Sep 2016 17:00:49 +0000 Subject: [PATCH 1147/1705] Updated from global requirements Change-Id: I895c335fecaae34cda7cbb105a4a6907a0d7de47 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 56bb156d7..fc54eb723 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -12,7 +12,7 @@ python-keystoneclient!=2.1.0,>=2.0.0 # Apache-2.0 python-cinderclient!=1.7.0,!=1.7.1,>=1.6.0 # Apache-2.0 python-glanceclient>=2.5.0 # Apache-2.0 requests-mock>=1.1 # Apache-2.0 -sphinx!=1.3b1,<1.3,>=1.2.1 # BSD +sphinx!=1.3b1,<1.4,>=1.2.1 # BSD os-client-config!=1.19.0,!=1.19.1,!=1.20.0,!=1.20.1,!=1.21.0,>=1.13.1 # Apache-2.0 oslosphinx>=4.7.0 # Apache-2.0 testrepository>=0.0.18 # Apache-2.0/BSD From cad8f803bf50b28c2fcb42ee01c98b40dcd68112 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 30 Sep 2016 20:06:05 +0000 Subject: [PATCH 1148/1705] Updated from global requirements Change-Id: I124d355c4c98369ecdc75b825f7ef72d7cd249c7 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e9fd28fb9..641155c2a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ iso8601>=0.1.11 # MIT oslo.i18n>=2.1.0 # Apache-2.0 oslo.serialization>=1.10.0 # Apache-2.0 oslo.utils>=3.16.0 # Apache-2.0 -PrettyTable<0.8,>=0.7 # BSD +PrettyTable<0.8,>=0.7.1 # BSD requests>=2.10.0 # Apache-2.0 simplejson>=2.2.0 # MIT six>=1.9.0 # MIT From 749fee334604d0c06dc398c38cb19c51dedb8214 Mon Sep 17 00:00:00 2001 From: Jamie Lennox Date: Tue, 4 Oct 2016 18:59:28 +1100 Subject: [PATCH 1149/1705] Replace requests mocking with requests-mock The novaclient tests are broken with the master (unreleased) branch of requests. This is because most of the mocking that novaclient does with requests in insufficient. Requests-mock is for exactly this purpose and is already widely used in novaclient so replace using mock with requests-mock where appropriate. Closes-Bug: #1630157 Change-Id: Ie91daeb5f0c241eb83b77c04ac1b12e431d6671c --- novaclient/tests/unit/test_client.py | 156 +++++++++++++-------------- novaclient/tests/unit/utils.py | 7 +- 2 files changed, 82 insertions(+), 81 deletions(-) diff --git a/novaclient/tests/unit/test_client.py b/novaclient/tests/unit/test_client.py index b256f8f7c..9a0616c25 100644 --- a/novaclient/tests/unit/test_client.py +++ b/novaclient/tests/unit/test_client.py @@ -14,13 +14,11 @@ # under the License. -import json import logging import fixtures -from keystoneauth1 import adapter +from keystoneauth1 import session import mock -import requests import novaclient.api_versions import novaclient.client @@ -49,18 +47,17 @@ def test_client_with_timeout(self): timeout=2, auth_url=auth_url) self.assertEqual(2, instance.timeout) - mock_request = mock.Mock() - mock_request.return_value = requests.Response() - mock_request.return_value.status_code = 200 - mock_request.return_value.headers = { + + headers = { 'x-server-management-url': 'example.com', 'x-auth-token': 'blah', } - with mock.patch('requests.request', mock_request): - instance.authenticate() - requests.request.assert_called_with( - mock.ANY, mock.ANY, timeout=2, headers=mock.ANY, - verify=mock.ANY) + + self.requests_mock.get(auth_url, headers=headers) + + instance.authenticate() + + self.assertEqual(2, self.requests_mock.last_request.timeout) def test_client_reauth(self): auth_url = "http://www.example.com" @@ -74,48 +71,43 @@ def test_client_reauth(self): instance.management_url = mgmt_url instance.get_service_url = mock.Mock(return_value=mgmt_url) instance.version = 'v2.0' - mock_request = mock.Mock() - mock_request.side_effect = novaclient.exceptions.Unauthorized(401) - with mock.patch('requests.request', mock_request): - try: - instance.get('/servers/detail') - except Exception: - pass - get_headers = {'X-Auth-Project-Id': 'project', - 'X-Auth-Token': 'foobar', - 'User-Agent': 'python-novaclient', - 'Accept': 'application/json'} - reauth_headers = {'Content-Type': 'application/json', - 'Accept': 'application/json', - 'User-Agent': 'python-novaclient'} - data = { - "auth": { - "tenantName": "project", - "passwordCredentials": { - "username": "user", - "password": "password" - } + + auth = self.requests_mock.post(auth_url + '/tokens', status_code=401) + detail = self.requests_mock.get(mgmt_url + '/servers/detail', + status_code=401) + + self.assertRaises(novaclient.exceptions.Unauthorized, + instance.get, + '/servers/detail') + + self.assertEqual(2, self.requests_mock.call_count) + self.assertTrue(detail.called_once) + self.assertTrue(auth.called_once) + + detail_headers = detail.last_request.headers + self.assertEqual('project', detail_headers['X-Auth-Project-Id']) + self.assertEqual('foobar', detail_headers['X-Auth-Token']) + self.assertEqual('python-novaclient', detail_headers['User-Agent']) + self.assertEqual('application/json', detail_headers['Accept']) + + reauth_headers = auth.last_request.headers + self.assertEqual('application/json', reauth_headers['Content-Type']) + self.assertEqual('application/json', reauth_headers['Accept']) + self.assertEqual('python-novaclient', reauth_headers['User-Agent']) + + data = { + "auth": { + "tenantName": "project", + "passwordCredentials": { + "username": "user", + "password": "password" } } + } + + self.assertEqual(data, auth.last_request.json()) - expected = [mock.call('GET', - 'http://mgmt.example.com/servers/detail', - timeout=mock.ANY, - headers=get_headers, - verify=mock.ANY), - mock.call('POST', 'http://www.example.com/tokens', - timeout=mock.ANY, - headers=reauth_headers, - allow_redirects=mock.ANY, - data=mock.ANY, - verify=mock.ANY)] - self.assertEqual(expected, mock_request.call_args_list) - token_post_call = mock_request.call_args_list[1] - self.assertEqual(data, json.loads(token_post_call[1]['data'])) - - @mock.patch.object(novaclient.client.HTTPClient, 'request', - return_value=(200, "{'versions':[]}")) - def _check_version_url(self, management_url, version_url, mock_request): + def _check_version_url(self, management_url, version_url): projectid = '25e469aa1848471b875e68cde6531bc5' auth_url = "http://example.com" instance = novaclient.client.HTTPClient(user='user', @@ -128,17 +120,19 @@ def _check_version_url(self, management_url, version_url, mock_request): instance.get_service_url = mock_get_service_url instance.version = 'v2.0' + versions = self.requests_mock.get(version_url, json={'versions': []}) + servers = self.requests_mock.get(instance.management_url + 'servers') + # If passing None as the part of url, a client accesses the url which # doesn't include "v2/" for getting API version info. instance.get(None) - mock_request.assert_called_once_with(version_url, 'GET', - headers=mock.ANY) - mock_request.reset_mock() + + self.assertTrue(versions.called_once) # Otherwise, a client accesses the url which includes "v2/". + self.assertFalse(servers.called_once) instance.get('servers') - url = instance.management_url + 'servers' - mock_request.assert_called_once_with(url, 'GET', headers=mock.ANY) + self.assertTrue(servers.called_once) def test_client_version_url(self): self._check_version_url('http://example.com/v2/%s', @@ -275,10 +269,13 @@ def test_service_url_lookup(self): auth_url='foo/v2', service_type=service_type) - @mock.patch.object(cs, 'get_service_url', return_value='compute/v5') - @mock.patch.object(cs, 'request', return_value=(200, '{}')) + self.requests_mock.get('http://mgmt.example.com/compute/v5/servers') + + @mock.patch.object(cs, + 'get_service_url', + return_value='http://mgmt.example.com/compute/v5') @mock.patch.object(cs, 'authenticate') - def do_test(mock_auth, mock_request, mock_get): + def do_test(mock_auth, mock_get): def set_service_catalog(): cs.service_catalog = 'catalog' @@ -286,27 +283,30 @@ def set_service_catalog(): mock_auth.side_effect = set_service_catalog cs.get('/servers') mock_get.assert_called_once_with(service_type) - mock_request.assert_called_once_with('compute/v5/servers', - 'GET', headers=mock.ANY) mock_auth.assert_called_once_with() do_test() + self.assertEqual(1, self.requests_mock.call_count) + + self.assertEqual('/compute/v5/servers', + self.requests_mock.last_request.path) + def test_bypass_url_no_service_url_lookup(self): - bypass_url = 'compute/v100' + bypass_url = 'http://mgmt.compute.com/v100' cs = novaclient.client.HTTPClient(None, None, None, auth_url='foo/v2', bypass_url=bypass_url) + get = self.requests_mock.get('http://mgmt.compute.com/v100/servers') + @mock.patch.object(cs, 'get_service_url') - @mock.patch.object(cs, 'request', return_value=(200, '{}')) - def do_test(mock_request, mock_get): + def do_test(mock_get): cs.get('/servers') self.assertFalse(mock_get.called) - mock_request.assert_called_once_with(bypass_url + '/servers', - 'GET', headers=mock.ANY) do_test() + self.assertTrue(get.called_once) @mock.patch("novaclient.client.requests.Session") def test_session(self, mock_session): @@ -438,9 +438,8 @@ def test_log_resp(self): ' "{SHA1}4fc49c6a671ce889078ff6b250f7066cf6d2ada2"}}}', output) - @mock.patch.object(novaclient.client.HTTPClient, 'request') - def test_timings(self, m_request): - m_request.return_value = (None, None) + def test_timings(self): + self.requests_mock.get('http://no.where') client = novaclient.client.HTTPClient(user='zqfan', password='') client._time_request("http://no.where", 'GET') @@ -455,30 +454,27 @@ def test_timings(self, m_request): class SessionClientTest(utils.TestCase): - @mock.patch.object(adapter.LegacyJsonAdapter, 'request') @mock.patch.object(novaclient.client, '_log_request_id') - def test_timings(self, mock_log_request_id, m_request): - m_request.return_value = (mock.MagicMock(status_code=200), None) + def test_timings(self, mock_log_request_id): + self.requests_mock.get('http://no.where') - client = novaclient.client.SessionClient(session=mock.MagicMock()) + client = novaclient.client.SessionClient(session=session.Session()) client.request("http://no.where", 'GET') self.assertEqual(0, len(client.times)) - client = novaclient.client.SessionClient(session=mock.MagicMock(), + client = novaclient.client.SessionClient(session=session.Session(), timings=True) client.request("http://no.where", 'GET') self.assertEqual(1, len(client.times)) self.assertEqual('GET http://no.where', client.times[0][0]) - @mock.patch.object(adapter.LegacyJsonAdapter, 'request') @mock.patch.object(novaclient.client, '_log_request_id') - def test_log_request_id(self, mock_log_request_id, mock_request): - response = mock.MagicMock(status_code=200) - mock_request.return_value = (response, None) - client = novaclient.client.SessionClient(session=mock.MagicMock(), + def test_log_request_id(self, mock_log_request_id): + self.requests_mock.get('http://no.where') + client = novaclient.client.SessionClient(session=session.Session(), service_name='compute') client.request("http://no.where", 'GET') - mock_log_request_id.assert_called_once_with(client.logger, response, + mock_log_request_id.assert_called_once_with(client.logger, mock.ANY, 'compute') diff --git a/novaclient/tests/unit/utils.py b/novaclient/tests/unit/utils.py index c4fd70f56..d3cb5da3a 100644 --- a/novaclient/tests/unit/utils.py +++ b/novaclient/tests/unit/utils.py @@ -55,6 +55,10 @@ class TestCase(testtools.TestCase): 'verify': True, } + @property + def requests(self): + return self.requests_mock + def setUp(self): super(TestCase, self).setUp() if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or @@ -66,6 +70,8 @@ def setUp(self): stderr = self.useFixture(fixtures.StringStream('stderr')).stream self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr)) + self.requests_mock = self.useFixture(requests_mock_fixture.Fixture()) + def assert_request_id(self, request_id_mixin, request_id_list): self.assertEqual(request_id_list, request_id_mixin.request_ids) @@ -78,7 +84,6 @@ class FixturedTestCase(testscenarios.TestWithScenarios, TestCase): def setUp(self): super(FixturedTestCase, self).setUp() - self.requests = self.useFixture(requests_mock_fixture.Fixture()) self.data_fixture = None self.client_fixture = None self.cs = None From e88afc046b2ab833a307869d775b820252341612 Mon Sep 17 00:00:00 2001 From: Jamie Lennox Date: Tue, 4 Oct 2016 18:10:06 +1100 Subject: [PATCH 1150/1705] Clean up requests-mock usage In line with other clients testing we've found that having the requests-mock fixture at self.requests can be misleading for new people as it looks like you're calling requests itself. Also make use of some of the new features of requests-mock like query parsing, json handling, method names. Change-Id: Id61b88c53478d49f91c3f880ed5b90d638051ba0 --- novaclient/tests/unit/fixture_data/agents.py | 18 +- .../tests/unit/fixture_data/aggregates.py | 28 +-- .../unit/fixture_data/availability_zones.py | 12 +- novaclient/tests/unit/fixture_data/base.py | 4 +- novaclient/tests/unit/fixture_data/certs.py | 12 +- novaclient/tests/unit/fixture_data/client.py | 16 +- .../tests/unit/fixture_data/cloudpipe.py | 22 +- .../tests/unit/fixture_data/fixedips.py | 15 +- .../tests/unit/fixture_data/floatingips.py | 118 +++++---- novaclient/tests/unit/fixture_data/fping.py | 12 +- novaclient/tests/unit/fixture_data/hosts.py | 73 +++--- .../tests/unit/fixture_data/hypervisors.py | 42 ++-- novaclient/tests/unit/fixture_data/images.py | 32 ++- .../tests/unit/fixture_data/keypairs.py | 27 +-- novaclient/tests/unit/fixture_data/limits.py | 6 +- .../tests/unit/fixture_data/networks.py | 35 ++- novaclient/tests/unit/fixture_data/quotas.py | 34 +-- .../unit/fixture_data/security_group_rules.py | 23 +- .../unit/fixture_data/security_groups.py | 39 ++- .../tests/unit/fixture_data/server_groups.py | 20 +- .../unit/fixture_data/server_migrations.py | 26 +- novaclient/tests/unit/fixture_data/servers.py | 225 ++++++++---------- novaclient/tests/unit/utils.py | 14 +- novaclient/tests/unit/v2/test_agents.py | 6 +- novaclient/tests/unit/v2/test_hypervisors.py | 5 +- novaclient/tests/unit/v2/test_servers.py | 24 +- 26 files changed, 418 insertions(+), 470 deletions(-) diff --git a/novaclient/tests/unit/fixture_data/agents.py b/novaclient/tests/unit/fixture_data/agents.py index 46a0bb67b..44cbd9343 100644 --- a/novaclient/tests/unit/fixture_data/agents.py +++ b/novaclient/tests/unit/fixture_data/agents.py @@ -32,9 +32,9 @@ def setUp(self): } } - self.requests.register_uri('POST', self.url(), - json=post_os_agents, - headers=self.json_headers) + self.requests_mock.post(self.url(), + json=post_os_agents, + headers=self.json_headers) put_os_agents_1 = { "agent": { @@ -45,10 +45,10 @@ def setUp(self): } } - self.requests.register_uri('PUT', self.url(1), - json=put_os_agents_1, - headers=self.json_headers) + self.requests_mock.put(self.url(1), + json=put_os_agents_1, + headers=self.json_headers) - self.requests.register_uri('DELETE', self.url(1), - headers=self.json_headers, - status_code=202) + self.requests_mock.delete(self.url(1), + headers=self.json_headers, + status_code=202) diff --git a/novaclient/tests/unit/fixture_data/aggregates.py b/novaclient/tests/unit/fixture_data/aggregates.py index 96c3d2072..3ea64b8b7 100644 --- a/novaclient/tests/unit/fixture_data/aggregates.py +++ b/novaclient/tests/unit/fixture_data/aggregates.py @@ -29,25 +29,25 @@ def setUp(self): 'availability_zone': 'nova1'}, ]} - self.requests.register_uri('GET', self.url(), - json=get_os_aggregates, - headers=self.json_headers) + self.requests_mock.get(self.url(), + json=get_os_aggregates, + headers=self.json_headers) get_aggregates_1 = {'aggregate': get_os_aggregates['aggregates'][0]} - self.requests.register_uri('POST', self.url(), - json=get_aggregates_1, - headers=self.json_headers) + self.requests_mock.post(self.url(), + json=get_aggregates_1, + headers=self.json_headers) for agg_id in (1, 2): for method in ('GET', 'PUT'): - self.requests.register_uri(method, self.url(agg_id), - json=get_aggregates_1, - headers=self.json_headers) + self.requests_mock.register_uri(method, self.url(agg_id), + json=get_aggregates_1, + headers=self.json_headers) - self.requests.register_uri('POST', self.url(agg_id, 'action'), - json=get_aggregates_1, - headers=self.json_headers) + self.requests_mock.post(self.url(agg_id, 'action'), + json=get_aggregates_1, + headers=self.json_headers) - self.requests.register_uri('DELETE', self.url(1), status_code=202, - headers=self.json_headers) + self.requests_mock.delete(self.url(1), status_code=202, + headers=self.json_headers) diff --git a/novaclient/tests/unit/fixture_data/availability_zones.py b/novaclient/tests/unit/fixture_data/availability_zones.py index 36659d70a..da1619019 100644 --- a/novaclient/tests/unit/fixture_data/availability_zones.py +++ b/novaclient/tests/unit/fixture_data/availability_zones.py @@ -39,9 +39,9 @@ def setUp(self): ] } - self.requests.register_uri('GET', self.url(), - json=get_os_availability_zone, - headers=self.json_headers) + self.requests_mock.get(self.url(), + json=get_os_availability_zone, + headers=self.json_headers) get_os_zone_detail = { self.zone_info_key: [ @@ -86,6 +86,6 @@ def setUp(self): ] } - self.requests.register_uri('GET', self.url('detail'), - json=get_os_zone_detail, - headers=self.json_headers) + self.requests_mock.get(self.url('detail'), + json=get_os_zone_detail, + headers=self.json_headers) diff --git a/novaclient/tests/unit/fixture_data/base.py b/novaclient/tests/unit/fixture_data/base.py index 899a60929..6580787a9 100644 --- a/novaclient/tests/unit/fixture_data/base.py +++ b/novaclient/tests/unit/fixture_data/base.py @@ -24,9 +24,9 @@ class Fixture(fixtures.Fixture): json_headers = {'Content-Type': 'application/json', 'x-openstack-request-id': fakes.FAKE_REQUEST_ID} - def __init__(self, requests, compute_url=COMPUTE_URL): + def __init__(self, requests_mock, compute_url=COMPUTE_URL): super(Fixture, self).__init__() - self.requests = requests + self.requests_mock = requests_mock self.compute_url = compute_url def url(self, *args, **kwargs): diff --git a/novaclient/tests/unit/fixture_data/certs.py b/novaclient/tests/unit/fixture_data/certs.py index 1dedaa4a3..5bc419e56 100644 --- a/novaclient/tests/unit/fixture_data/certs.py +++ b/novaclient/tests/unit/fixture_data/certs.py @@ -40,9 +40,9 @@ def setUp(self): 'data': 'foo' } } - self.requests.register_uri('GET', self.url('root'), - json=get_os_certificate, - headers=self.json_headers) + self.requests_mock.get(self.url('root'), + json=get_os_certificate, + headers=self.json_headers) post_os_certificates = { 'certificate': { @@ -50,6 +50,6 @@ def setUp(self): 'data': 'bar' } } - self.requests.register_uri('POST', self.url(), - json=post_os_certificates, - headers=self.json_headers) + self.requests_mock.post(self.url(), + json=post_os_certificates, + headers=self.json_headers) diff --git a/novaclient/tests/unit/fixture_data/client.py b/novaclient/tests/unit/fixture_data/client.py index d4d602073..7bfaf4cc3 100644 --- a/novaclient/tests/unit/fixture_data/client.py +++ b/novaclient/tests/unit/fixture_data/client.py @@ -23,13 +23,13 @@ class V1(fixtures.Fixture): - def __init__(self, requests, + def __init__(self, requests_mock, compute_url=COMPUTE_URL, identity_url=IDENTITY_URL): super(V1, self).__init__() self.identity_url = identity_url self.compute_url = compute_url self.client = None - self.requests = requests + self.requests_mock = requests_mock self.token = fixture.V2Token() self.token.set_scope() @@ -46,12 +46,12 @@ def setUp(self): auth_url = '%s/tokens' % self.identity_url headers = {'X-Content-Type': 'application/json'} - self.requests.register_uri('POST', auth_url, - json=self.token, - headers=headers) - self.requests.register_uri('GET', self.identity_url, - json=self.discovery, - headers=headers) + self.requests_mock.post(auth_url, + json=self.token, + headers=headers) + self.requests_mock.get(self.identity_url, + json=self.discovery, + headers=headers) self.client = self.new_client() def new_client(self): diff --git a/novaclient/tests/unit/fixture_data/cloudpipe.py b/novaclient/tests/unit/fixture_data/cloudpipe.py index abe590563..b19e24342 100644 --- a/novaclient/tests/unit/fixture_data/cloudpipe.py +++ b/novaclient/tests/unit/fixture_data/cloudpipe.py @@ -21,17 +21,17 @@ def setUp(self): super(Fixture, self).setUp() get_os_cloudpipe = {'cloudpipes': [{'project_id': 1}]} - self.requests.register_uri('GET', self.url(), - json=get_os_cloudpipe, - headers=self.json_headers) + self.requests_mock.get(self.url(), + json=get_os_cloudpipe, + headers=self.json_headers) instance_id = '9d5824aa-20e6-4b9f-b967-76a699fc51fd' post_os_cloudpipe = {'instance_id': instance_id} - self.requests.register_uri('POST', self.url(), - json=post_os_cloudpipe, - headers=self.json_headers, - status_code=202) - - self.requests.register_uri('PUT', self.url('configure-project'), - headers=self.json_headers, - status_code=202) + self.requests_mock.post(self.url(), + json=post_os_cloudpipe, + headers=self.json_headers, + status_code=202) + + self.requests_mock.put(self.url('configure-project'), + headers=self.json_headers, + status_code=202) diff --git a/novaclient/tests/unit/fixture_data/fixedips.py b/novaclient/tests/unit/fixture_data/fixedips.py index fb677e91c..cf50666bb 100644 --- a/novaclient/tests/unit/fixture_data/fixedips.py +++ b/novaclient/tests/unit/fixture_data/fixedips.py @@ -29,11 +29,10 @@ def setUp(self): } } - self.requests.register_uri('GET', self.url('192.168.1.1'), - json=get_os_fixed_ips, - headers=self.json_headers) - - self.requests.register_uri('POST', - self.url('192.168.1.1', 'action'), - headers=self.json_headers, - status_code=202) + self.requests_mock.get(self.url('192.168.1.1'), + json=get_os_fixed_ips, + headers=self.json_headers) + + self.requests_mock.post(self.url('192.168.1.1', 'action'), + headers=self.json_headers, + status_code=202) diff --git a/novaclient/tests/unit/fixture_data/floatingips.py b/novaclient/tests/unit/fixture_data/floatingips.py index 6ddc9e360..75364cdf8 100644 --- a/novaclient/tests/unit/fixture_data/floatingips.py +++ b/novaclient/tests/unit/fixture_data/floatingips.py @@ -10,8 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from oslo_serialization import jsonutils - from novaclient.tests.unit import fakes from novaclient.tests.unit.fixture_data import base @@ -27,28 +25,27 @@ def setUp(self): {'id': 2, 'fixed_ip': '10.0.0.2', 'ip': '11.0.0.2'}] get_os_floating_ips = {'floating_ips': floating_ips} - self.requests.register_uri('GET', self.url(), - json=get_os_floating_ips, - headers=self.json_headers) + self.requests_mock.get(self.url(), + json=get_os_floating_ips, + headers=self.json_headers) for ip in floating_ips: get_os_floating_ip = {'floating_ip': ip} - self.requests.register_uri('GET', self.url(ip['id']), - json=get_os_floating_ip, - headers=self.json_headers) + self.requests_mock.get(self.url(ip['id']), + json=get_os_floating_ip, + headers=self.json_headers) - self.requests.register_uri('DELETE', self.url(ip['id']), - headers=self.json_headers, - status_code=204) + self.requests_mock.delete(self.url(ip['id']), + headers=self.json_headers, + status_code=204) def post_os_floating_ips(request, context): - body = jsonutils.loads(request.body) ip = floating_ips[0].copy() - ip['pool'] = body.get('pool') + ip['pool'] = request.json().get('pool') return {'floating_ip': ip} - self.requests.register_uri('POST', self.url(), - json=post_os_floating_ips, - headers=self.json_headers) + self.requests_mock.post(self.url(), + json=post_os_floating_ips, + headers=self.json_headers) class DNSFixture(base.Fixture): @@ -64,10 +61,10 @@ def setUp(self): {'domain': 'example.com'} ] } - self.requests.register_uri('GET', self.url(), - json=get_os_floating_ip_dns, - headers=self.json_headers, - status_code=205) + self.requests_mock.get(self.url(), + json=get_os_floating_ip_dns, + headers=self.json_headers, + status_code=205) get_dns_testdomain_entries_testname = { 'dns_entry': { @@ -77,30 +74,29 @@ def setUp(self): 'domain': 'testdomain' } } - url = self.url('testdomain', 'entries', 'testname') - self.requests.register_uri('GET', url, - json=get_dns_testdomain_entries_testname, - headers=self.json_headers, - status_code=205) - self.requests.register_uri('DELETE', self.url('testdomain'), - headers=self.json_headers) + self.requests_mock.get(self.url('testdomain', 'entries', 'testname'), + json=get_dns_testdomain_entries_testname, + headers=self.json_headers, + status_code=205) + + self.requests_mock.delete(self.url('testdomain'), + headers=self.json_headers) url = self.url('testdomain', 'entries', 'testname') - self.requests.register_uri('DELETE', url, headers=self.json_headers) + self.requests_mock.delete(url, headers=self.json_headers) def put_dns_testdomain_entries_testname(request, context): - body = jsonutils.loads(request.body) - fakes.assert_has_keys(body['dns_entry'], + fakes.assert_has_keys(request.json()['dns_entry'], required=['ip', 'dns_type']) context.status_code = 205 return request.body - self.requests.register_uri('PUT', url, - text=put_dns_testdomain_entries_testname, - headers=self.json_headers) + self.requests_mock.put(url, + text=put_dns_testdomain_entries_testname, + headers=self.json_headers) url = self.url('testdomain', 'entries') - self.requests.register_uri('GET', url, status_code=404) + self.requests_mock.get(url, status_code=404) get_os_floating_ip_dns_testdomain = { 'dns_entries': [ @@ -122,13 +118,13 @@ def put_dns_testdomain_entries_testname(request, context): }, ] } - self.requests.register_uri('GET', url + '?ip=1.2.3.4', - json=get_os_floating_ip_dns_testdomain, - status_code=205, - headers=self.json_headers) + self.requests_mock.get(url + '?ip=1.2.3.4', + json=get_os_floating_ip_dns_testdomain, + status_code=205, + headers=self.json_headers) def put_os_floating_ip_dns_testdomain(request, context): - body = jsonutils.loads(request.body) + body = request.json() if body['domain_entry']['scope'] == 'private': fakes.assert_has_keys(body['domain_entry'], required=['availability_zone', 'scope']) @@ -141,10 +137,10 @@ def put_os_floating_ip_dns_testdomain(request, context): return request.body - self.requests.register_uri('PUT', self.url('testdomain'), - text=put_os_floating_ip_dns_testdomain, - status_code=205, - headers=self.json_headers) + self.requests_mock.put(self.url('testdomain'), + text=put_os_floating_ip_dns_testdomain, + status_code=205, + headers=self.json_headers) class BulkFixture(base.Fixture): @@ -160,25 +156,23 @@ def setUp(self): {'id': 2, 'fixed_ip': '10.0.0.2', 'ip': '11.0.0.2'}, ] } - self.requests.register_uri('GET', self.url(), - json=get_os_floating_ips_bulk, - headers=self.json_headers) - self.requests.register_uri('GET', self.url('testHost'), - json=get_os_floating_ips_bulk, - headers=self.json_headers) + self.requests_mock.get(self.url(), + json=get_os_floating_ips_bulk, + headers=self.json_headers) + self.requests_mock.get(self.url('testHost'), + json=get_os_floating_ips_bulk, + headers=self.json_headers) def put_os_floating_ips_bulk_delete(request, context): - body = jsonutils.loads(request.body) - ip_range = body.get('ip_range') + ip_range = request.json().get('ip_range') return {'floating_ips_bulk_delete': ip_range} - self.requests.register_uri('PUT', self.url('delete'), - json=put_os_floating_ips_bulk_delete, - headers=self.json_headers) + self.requests_mock.put(self.url('delete'), + json=put_os_floating_ips_bulk_delete, + headers=self.json_headers) def post_os_floating_ips_bulk(request, context): - body = jsonutils.loads(request.body) - params = body.get('floating_ips_bulk_create') + params = request.json().get('floating_ips_bulk_create') pool = params.get('pool', 'defaultPool') interface = params.get('interface', 'defaultInterface') return { @@ -189,9 +183,9 @@ def post_os_floating_ips_bulk(request, context): } } - self.requests.register_uri('POST', self.url(), - json=post_os_floating_ips_bulk, - headers=self.json_headers) + self.requests_mock.post(self.url(), + json=post_os_floating_ips_bulk, + headers=self.json_headers) class PoolsFixture(base.Fixture): @@ -207,6 +201,6 @@ def setUp(self): {'name': 'bar'} ] } - self.requests.register_uri('GET', self.url(), - json=get_os_floating_ip_pools, - headers=self.json_headers) + self.requests_mock.get(self.url(), + json=get_os_floating_ip_pools, + headers=self.json_headers) diff --git a/novaclient/tests/unit/fixture_data/fping.py b/novaclient/tests/unit/fixture_data/fping.py index 6c6cd4f89..087be44be 100644 --- a/novaclient/tests/unit/fixture_data/fping.py +++ b/novaclient/tests/unit/fixture_data/fping.py @@ -27,9 +27,9 @@ def setUp(self): "alive": True, } } - self.requests.register_uri('GET', self.url(1), - json=get_os_fping_1, - headers=self.json_headers) + self.requests_mock.get(self.url(1), + json=get_os_fping_1, + headers=self.json_headers) get_os_fping = { 'servers': [ @@ -41,6 +41,6 @@ def setUp(self): }, ] } - self.requests.register_uri('GET', self.url(), - json=get_os_fping, - headers=self.json_headers) + self.requests_mock.get(self.url(), + json=get_os_fping, + headers=self.json_headers) diff --git a/novaclient/tests/unit/fixture_data/hosts.py b/novaclient/tests/unit/fixture_data/hosts.py index 7c9b3be80..c95cddeba 100644 --- a/novaclient/tests/unit/fixture_data/hosts.py +++ b/novaclient/tests/unit/fixture_data/hosts.py @@ -10,9 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from oslo_serialization import jsonutils -from six.moves.urllib import parse - from novaclient.tests.unit.fixture_data import base @@ -38,24 +35,22 @@ def setUp(self): headers = self.json_headers - self.requests.register_uri('GET', self.url('host'), - json=get_os_hosts_host, - headers=headers) + self.requests_mock.get(self.url('host'), + json=get_os_hosts_host, + headers=headers) def get_os_hosts(request, context): - host, query = parse.splitquery(request.url) zone = 'nova1' service = None - if query: - qs = parse.parse_qs(query) + if request.query: try: - zone = qs['zone'][0] + zone = request.qs['zone'][0] except Exception: pass try: - service = qs['service'][0] + service = request.qs['service'][0] except Exception: pass @@ -74,51 +69,51 @@ def get_os_hosts(request, context): ] } - self.requests.register_uri('GET', self.url(), - json=get_os_hosts, - headers=headers) + self.requests_mock.get(self.url(), + json=get_os_hosts, + headers=headers) get_os_hosts_sample_host = { 'host': [ {'resource': {'host': 'sample_host'}} ], } - self.requests.register_uri('GET', self.url('sample_host'), - json=get_os_hosts_sample_host, - headers=headers) + self.requests_mock.get(self.url('sample_host'), + json=get_os_hosts_sample_host, + headers=headers) - self.requests.register_uri('PUT', self.url('sample_host', 1), - json=self.put_host_1(), - headers=headers) + self.requests_mock.put(self.url('sample_host', 1), + json=self.put_host_1(), + headers=headers) - self.requests.register_uri('PUT', self.url('sample_host', 2), - json=self.put_host_2(), - headers=headers) + self.requests_mock.put(self.url('sample_host', 2), + json=self.put_host_2(), + headers=headers) - self.requests.register_uri('PUT', self.url('sample_host', 3), - json=self.put_host_3(), - headers=headers) + self.requests_mock.put(self.url('sample_host', 3), + json=self.put_host_3(), + headers=headers) - self.requests.register_uri('GET', self.url('sample_host', 'reboot'), - json=self.get_host_reboot(), - headers=headers) + self.requests_mock.get(self.url('sample_host', 'reboot'), + json=self.get_host_reboot(), + headers=headers) - self.requests.register_uri('GET', self.url('sample_host', 'startup'), - json=self.get_host_startup(), - headers=headers) + self.requests_mock.get(self.url('sample_host', 'startup'), + json=self.get_host_startup(), + headers=headers) - self.requests.register_uri('GET', self.url('sample_host', 'shutdown'), - json=self.get_host_shutdown(), - headers=headers) + self.requests_mock.get(self.url('sample_host', 'shutdown'), + json=self.get_host_shutdown(), + headers=headers) def put_os_hosts_sample_host(request, context): result = {'host': 'dummy'} - result.update(jsonutils.loads(request.body)) + result.update(request.json()) return result - self.requests.register_uri('PUT', self.url('sample_host'), - json=put_os_hosts_sample_host, - headers=headers) + self.requests_mock.put(self.url('sample_host'), + json=put_os_hosts_sample_host, + headers=headers) class V1(BaseFixture): diff --git a/novaclient/tests/unit/fixture_data/hypervisors.py b/novaclient/tests/unit/fixture_data/hypervisors.py index 229dfd754..43b3438b6 100644 --- a/novaclient/tests/unit/fixture_data/hypervisors.py +++ b/novaclient/tests/unit/fixture_data/hypervisors.py @@ -29,9 +29,9 @@ def setUp(self): self.headers = self.json_headers - self.requests.register_uri('GET', self.url(), - json=get_os_hypervisors, - headers=self.headers) + self.requests_mock.get(self.url(), + json=get_os_hypervisors, + headers=self.headers) get_os_hypervisors_detail = { 'hypervisors': [ @@ -82,9 +82,9 @@ def setUp(self): ] } - self.requests.register_uri('GET', self.url('detail'), - json=get_os_hypervisors_detail, - headers=self.headers) + self.requests_mock.get(self.url('detail'), + json=get_os_hypervisors_detail, + headers=self.headers) get_os_hypervisors_stats = { 'hypervisor_statistics': { @@ -103,9 +103,9 @@ def setUp(self): } } - self.requests.register_uri('GET', self.url('statistics'), - json=get_os_hypervisors_stats, - headers=self.headers) + self.requests_mock.get(self.url('statistics'), + json=get_os_hypervisors_stats, + headers=self.headers) get_os_hypervisors_search = { 'hypervisors': [ @@ -114,9 +114,9 @@ def setUp(self): ] } - self.requests.register_uri('GET', self.url('hyper', 'search'), - json=get_os_hypervisors_search, - headers=self.headers) + self.requests_mock.get(self.url('hyper', 'search'), + json=get_os_hypervisors_search, + headers=self.headers) get_hyper_server = { 'hypervisors': [ @@ -139,9 +139,9 @@ def setUp(self): ] } - self.requests.register_uri('GET', self.url('hyper', 'servers'), - json=get_hyper_server, - headers=self.headers) + self.requests_mock.get(self.url('hyper', 'servers'), + json=get_hyper_server, + headers=self.headers) get_os_hypervisors_1234 = { 'hypervisor': { @@ -165,9 +165,9 @@ def setUp(self): } } - self.requests.register_uri('GET', self.url(1234), - json=get_os_hypervisors_1234, - headers=self.headers) + self.requests_mock.get(self.url(1234), + json=get_os_hypervisors_1234, + headers=self.headers) get_os_hypervisors_uptime = { 'hypervisor': { @@ -177,6 +177,6 @@ def setUp(self): } } - self.requests.register_uri('GET', self.url(1234, 'uptime'), - json=get_os_hypervisors_uptime, - headers=self.headers) + self.requests_mock.get(self.url(1234, 'uptime'), + json=get_os_hypervisors_uptime, + headers=self.headers) diff --git a/novaclient/tests/unit/fixture_data/images.py b/novaclient/tests/unit/fixture_data/images.py index 9498b8e32..7c5e0925f 100644 --- a/novaclient/tests/unit/fixture_data/images.py +++ b/novaclient/tests/unit/fixture_data/images.py @@ -10,8 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from oslo_serialization import jsonutils - from novaclient.tests.unit import fakes from novaclient.tests.unit.fixture_data import base @@ -32,9 +30,9 @@ def setUp(self): headers = self.json_headers - self.requests.register_uri('GET', self.url(), - json=get_images, - headers=headers) + self.requests_mock.get(self.url(), + json=get_images, + headers=headers) image_1 = { 'id': 1, @@ -59,27 +57,27 @@ def setUp(self): "links": {}, } - self.requests.register_uri('GET', self.url('detail'), - json={'images': [image_1, image_2]}, - headers=headers) + self.requests_mock.get(self.url('detail'), + json={'images': [image_1, image_2]}, + headers=headers) - self.requests.register_uri('GET', self.url(1), - json={'image': image_1}, - headers=headers) + self.requests_mock.get(self.url(1), + json={'image': image_1}, + headers=headers) def post_images_1_metadata(request, context): - body = jsonutils.loads(request.body) + body = request.json() assert list(body) == ['metadata'] fakes.assert_has_keys(body['metadata'], required=['test_key']) return {'metadata': image_1['metadata']} - self.requests.register_uri('POST', self.url(1, 'metadata'), - json=post_images_1_metadata, - headers=headers) + self.requests_mock.post(self.url(1, 'metadata'), + json=post_images_1_metadata, + headers=headers) for u in (1, '1/metadata/test_key'): - self.requests.register_uri('DELETE', self.url(u), status_code=204, - headers=headers) + self.requests_mock.delete(self.url(u), status_code=204, + headers=headers) class V3(V1): diff --git a/novaclient/tests/unit/fixture_data/keypairs.py b/novaclient/tests/unit/fixture_data/keypairs.py index ee60e7aac..1d73d2528 100644 --- a/novaclient/tests/unit/fixture_data/keypairs.py +++ b/novaclient/tests/unit/fixture_data/keypairs.py @@ -10,8 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from oslo_serialization import jsonutils - from novaclient.tests.unit import fakes from novaclient.tests.unit.fixture_data import base @@ -26,23 +24,24 @@ def setUp(self): headers = self.json_headers - self.requests.register_uri('GET', self.url(), - json={'keypairs': [keypair]}, - headers=headers) + self.requests_mock.get(self.url(), + json={'keypairs': [keypair]}, + headers=headers) - self.requests.register_uri('GET', self.url('test'), - json={'keypair': keypair}, - headers=headers) + self.requests_mock.get(self.url('test'), + json={'keypair': keypair}, + headers=headers) - self.requests.register_uri('DELETE', self.url('test'), status_code=202, - headers=headers) + self.requests_mock.delete(self.url('test'), + status_code=202, + headers=headers) def post_os_keypairs(request, context): - body = jsonutils.loads(request.body) + body = request.json() assert list(body) == ['keypair'] fakes.assert_has_keys(body['keypair'], required=['name']) return {'keypair': keypair} - self.requests.register_uri('POST', self.url(), - json=post_os_keypairs, - headers=headers) + self.requests_mock.post(self.url(), + json=post_os_keypairs, + headers=headers) diff --git a/novaclient/tests/unit/fixture_data/limits.py b/novaclient/tests/unit/fixture_data/limits.py index 6c1d306cd..f9a86ff79 100644 --- a/novaclient/tests/unit/fixture_data/limits.py +++ b/novaclient/tests/unit/fixture_data/limits.py @@ -75,6 +75,6 @@ def setUp(self): } headers = self.json_headers - self.requests.register_uri('GET', self.url(), - json=get_limits, - headers=headers) + self.requests_mock.get(self.url(), + json=get_limits, + headers=headers) diff --git a/novaclient/tests/unit/fixture_data/networks.py b/novaclient/tests/unit/fixture_data/networks.py index 9471605c6..51a4676fe 100644 --- a/novaclient/tests/unit/fixture_data/networks.py +++ b/novaclient/tests/unit/fixture_data/networks.py @@ -10,8 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from oslo_serialization import jsonutils - from novaclient.tests.unit.fixture_data import base @@ -35,30 +33,29 @@ def setUp(self): headers = self.json_headers - self.requests.register_uri('GET', self.url(), - json=get_os_networks, - headers=headers) + self.requests_mock.get(self.url(), + json=get_os_networks, + headers=headers) def post_os_networks(request, context): - body = jsonutils.loads(request.body) - return {'network': body} + return {'network': request.json()} - self.requests.register_uri("POST", self.url(), - json=post_os_networks, - headers=headers) + self.requests_mock.post(self.url(), + json=post_os_networks, + headers=headers) get_os_networks_1 = {'network': {"label": "1", "cidr": "10.0.0.0/24"}} - self.requests.register_uri('GET', self.url(1), - json=get_os_networks_1, - headers=headers) + self.requests_mock.get(self.url(1), + json=get_os_networks_1, + headers=headers) - self.requests.register_uri('DELETE', - self.url('networkdelete'), - status_code=202, - headers=headers) + self.requests_mock.delete(self.url('networkdelete'), + status_code=202, + headers=headers) for u in ('add', 'networkdisassociate/action', 'networktest/action', '1/action', '2/action'): - self.requests.register_uri('POST', self.url(u), status_code=202, - headers=headers) + self.requests_mock.post(self.url(u), + status_code=202, + headers=headers) diff --git a/novaclient/tests/unit/fixture_data/quotas.py b/novaclient/tests/unit/fixture_data/quotas.py index 5051bf3ed..a6931fcc6 100644 --- a/novaclient/tests/unit/fixture_data/quotas.py +++ b/novaclient/tests/unit/fixture_data/quotas.py @@ -27,28 +27,28 @@ def setUp(self): for u in ('test', 'tenant-id', 'tenant-id/defaults', '%s/defaults' % uuid2, 'test/detail'): - self.requests.register_uri('GET', self.url(u), - json=test_json, - headers=self.headers) - - self.requests.register_uri('PUT', self.url(uuid), - json={'quota_set': self.test_quota(uuid)}, + self.requests_mock.get(self.url(u), + json=test_json, headers=self.headers) - self.requests.register_uri('GET', self.url(uuid), - json={'quota_set': self.test_quota(uuid)}, - headers=self.headers) + self.requests_mock.put(self.url(uuid), + json={'quota_set': self.test_quota(uuid)}, + headers=self.headers) - self.requests.register_uri('PUT', self.url(uuid2), - json={'quota_set': self.test_quota(uuid2)}, - headers=self.headers) - self.requests.register_uri('GET', self.url(uuid2), - json={'quota_set': self.test_quota(uuid2)}, - headers=self.headers) + self.requests_mock.get(self.url(uuid), + json={'quota_set': self.test_quota(uuid)}, + headers=self.headers) + + self.requests_mock.put(self.url(uuid2), + json={'quota_set': self.test_quota(uuid2)}, + headers=self.headers) + self.requests_mock.get(self.url(uuid2), + json={'quota_set': self.test_quota(uuid2)}, + headers=self.headers) for u in ('test', uuid2): - self.requests.register_uri('DELETE', self.url(u), status_code=202, - headers=self.headers) + self.requests_mock.delete(self.url(u), status_code=202, + headers=self.headers) def test_quota(self, tenant_id='test'): return { diff --git a/novaclient/tests/unit/fixture_data/security_group_rules.py b/novaclient/tests/unit/fixture_data/security_group_rules.py index e4285fe70..4fda1fe40 100644 --- a/novaclient/tests/unit/fixture_data/security_group_rules.py +++ b/novaclient/tests/unit/fixture_data/security_group_rules.py @@ -10,8 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from oslo_serialization import jsonutils - from novaclient.tests.unit import fakes from novaclient.tests.unit.fixture_data import base @@ -35,16 +33,17 @@ def setUp(self): headers = self.json_headers - self.requests.register_uri('GET', self.url(), - json={'security_group_rules': [rule]}, - headers=headers) + self.requests_mock.get(self.url(), + json={'security_group_rules': [rule]}, + headers=headers) for u in (1, 11, 12): - self.requests.register_uri('DELETE', self.url(u), status_code=202, - headers=headers) + self.requests_mock.delete(self.url(u), + status_code=202, + headers=headers) def post_rules(request, context): - body = jsonutils.loads(request.body) + body = request.json() assert list(body) == ['security_group_rule'] fakes.assert_has_keys(body['security_group_rule'], required=['parent_group_id'], @@ -53,7 +52,7 @@ def post_rules(request, context): return {'security_group_rule': rule} - self.requests.register_uri('POST', self.url(), - json=post_rules, - headers=headers, - status_code=202) + self.requests_mock.post(self.url(), + json=post_rules, + headers=headers, + status_code=202) diff --git a/novaclient/tests/unit/fixture_data/security_groups.py b/novaclient/tests/unit/fixture_data/security_groups.py index 28023093f..b0df1f9ed 100644 --- a/novaclient/tests/unit/fixture_data/security_groups.py +++ b/novaclient/tests/unit/fixture_data/security_groups.py @@ -10,8 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from oslo_serialization import jsonutils - from novaclient.tests.unit import fakes from novaclient.tests.unit.fixture_data import base @@ -64,38 +62,39 @@ def setUp(self): get_groups = {'security_groups': [security_group_1, security_group_2]} headers = self.json_headers - self.requests.register_uri('GET', self.url(), - json=get_groups, - headers=headers) + self.requests_mock.get(self.url(), + json=get_groups, + headers=headers) get_group_1 = {'security_group': security_group_1} - self.requests.register_uri('GET', self.url(1), - json=get_group_1, - headers=headers) + self.requests_mock.get(self.url(1), + json=get_group_1, + headers=headers) - self.requests.register_uri('DELETE', self.url(1), status_code=202, - headers=headers) + self.requests_mock.delete(self.url(1), + status_code=202, + headers=headers) def post_os_security_groups(request, context): - body = jsonutils.loads(request.body) + body = request.json() assert list(body) == ['security_group'] fakes.assert_has_keys(body['security_group'], required=['name', 'description']) return {'security_group': security_group_1} - self.requests.register_uri('POST', self.url(), - json=post_os_security_groups, - headers=headers, - status_code=202) + self.requests_mock.post(self.url(), + json=post_os_security_groups, + headers=headers, + status_code=202) def put_os_security_groups_1(request, context): - body = jsonutils.loads(request.body) + body = request.json() assert list(body) == ['security_group'] fakes.assert_has_keys(body['security_group'], required=['name', 'description']) return body - self.requests.register_uri('PUT', self.url(1), - json=put_os_security_groups_1, - headers=headers, - status_code=205) + self.requests_mock.put(self.url(1), + json=put_os_security_groups_1, + headers=headers, + status_code=205) diff --git a/novaclient/tests/unit/fixture_data/server_groups.py b/novaclient/tests/unit/fixture_data/server_groups.py index 64a398810..afbe2612f 100644 --- a/novaclient/tests/unit/fixture_data/server_groups.py +++ b/novaclient/tests/unit/fixture_data/server_groups.py @@ -10,8 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from oslo_serialization import jsonutils - from novaclient.tests.unit.fixture_data import base @@ -54,16 +52,17 @@ def setUp(self): headers = self.json_headers - self.requests.register_uri('GET', self.url(), - json={'server_groups': server_groups}, - headers=headers) + self.requests_mock.get(self.url(), + json={'server_groups': server_groups}, + headers=headers) server = server_groups[0] - server_j = jsonutils.dumps({'server_group': server}) def _register(method, *args): - self.requests.register_uri(method, self.url(*args), text=server_j, - headers=headers) + self.requests_mock.register_uri(method, + self.url(*args), + json={'server_group': server}, + headers=headers) _register('POST') _register('POST', server['id']) @@ -71,5 +70,6 @@ def _register(method, *args): _register('PUT', server['id']) _register('POST', server['id'], '/action') - self.requests.register_uri('DELETE', self.url(server['id']), - status_code=202, headers=headers) + self.requests_mock.delete(self.url(server['id']), + status_code=202, + headers=headers) diff --git a/novaclient/tests/unit/fixture_data/server_migrations.py b/novaclient/tests/unit/fixture_data/server_migrations.py index 5e946eef5..4fe68e130 100644 --- a/novaclient/tests/unit/fixture_data/server_migrations.py +++ b/novaclient/tests/unit/fixture_data/server_migrations.py @@ -22,9 +22,9 @@ class Fixture(base.Fixture): def setUp(self): super(Fixture, self).setUp() url = self.url('1234', 'migrations', '1', 'action') - self.requests.register_uri('POST', url, - status_code=202, - headers=self.json_headers) + self.requests_mock.post(url, + status_code=202, + headers=self.json_headers) get_migrations = {'migrations': [ { @@ -47,10 +47,9 @@ def setUp(self): }]} url = self.url('1234', 'migrations') - self.requests.register_uri('GET', url, - status_code=200, - json=get_migrations, - headers=self.json_headers) + self.requests_mock.get(url, + json=get_migrations, + headers=self.json_headers) get_migration = {'migration': { "created_at": "2016-01-29T13:42:02.000000", @@ -72,11 +71,10 @@ def setUp(self): }} url = self.url('1234', 'migrations', '1') - self.requests.register_uri('GET', url, - status_code=200, - json=get_migration, - headers=self.json_headers) + self.requests_mock.get(url, + json=get_migration, + headers=self.json_headers) url = self.url('1234', 'migrations', '1') - self.requests.register_uri('DELETE', url, - status_code=202, - headers=self.json_headers) + self.requests_mock.delete(url, + status_code=202, + headers=self.json_headers) diff --git a/novaclient/tests/unit/fixture_data/servers.py b/novaclient/tests/unit/fixture_data/servers.py index bf0dac922..891814b2f 100644 --- a/novaclient/tests/unit/fixture_data/servers.py +++ b/novaclient/tests/unit/fixture_data/servers.py @@ -10,8 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from oslo_serialization import jsonutils - from novaclient.tests.unit import fakes from novaclient.tests.unit.fixture_data import base from novaclient.tests.unit.v2 import fakes as v2_fakes @@ -31,9 +29,9 @@ def setUp(self): ] } - self.requests.register_uri('GET', self.url(), - json=get_servers, - headers=self.json_headers) + self.requests_mock.get(self.url(), + json=get_servers, + headers=self.json_headers) self.server_1234 = { "id": 1234, @@ -155,17 +153,17 @@ def setUp(self): servers = [self.server_1234, self.server_5678, self.server_9012] get_servers_detail = {"servers": servers} - self.requests.register_uri('GET', self.url('detail'), - json=get_servers_detail, - headers=self.json_headers) + self.requests_mock.get(self.url('detail'), + json=get_servers_detail, + headers=self.json_headers) - self.requests.register_uri( - 'GET', self.url('detail', marker=self.server_1234["id"]), + self.requests_mock.get( + self.url('detail', marker=self.server_1234["id"]), json={"servers": [self.server_1234, self.server_5678]}, headers=self.json_headers, complete_qs=True) - self.requests.register_uri( - 'GET', self.url('detail', marker=self.server_5678["id"]), + self.requests_mock.get( + self.url('detail', marker=self.server_5678["id"]), json={"servers": []}, headers=self.json_headers, complete_qs=True) @@ -175,39 +173,37 @@ def setUp(self): self.server_1235['fault'] = {'message': 'something went wrong!'} for s in servers + [self.server_1235]: - self.requests.register_uri('GET', self.url(s['id']), - json={'server': s}, - headers=self.json_headers) + self.requests_mock.get(self.url(s['id']), + json={'server': s}, + headers=self.json_headers) for s in (1234, 5678): - self.requests.register_uri('DELETE', self.url(s), status_code=202, - headers=self.json_headers) + self.requests_mock.delete(self.url(s), + status_code=202, + headers=self.json_headers) for k in ('test_key', 'key1', 'key2'): - self.requests.register_uri('DELETE', - self.url(1234, 'metadata', k), - status_code=204, - headers=self.json_headers) + self.requests_mock.delete(self.url(1234, 'metadata', k), + status_code=204, + headers=self.json_headers) metadata1 = {'metadata': {'test_key': 'test_value'}} - self.requests.register_uri('POST', self.url(1234, 'metadata'), - json=metadata1, - headers=self.json_headers) - self.requests.register_uri('PUT', - self.url(1234, 'metadata', 'test_key'), - json=metadata1, - headers=self.json_headers) + self.requests_mock.post(self.url(1234, 'metadata'), + json=metadata1, + headers=self.json_headers) + self.requests_mock.put(self.url(1234, 'metadata', 'test_key'), + json=metadata1, + headers=self.json_headers) self.diagnostic = {'data': 'Fake diagnostics'} metadata2 = {'metadata': {'key1': 'val1'}} for u in ('uuid1', 'uuid2', 'uuid3', 'uuid4'): - self.requests.register_uri('POST', self.url(u, 'metadata'), - json=metadata2, status_code=204) - self.requests.register_uri('DELETE', - self.url(u, 'metadata', 'key1'), - json=self.diagnostic, - headers=self.json_headers) + self.requests_mock.post(self.url(u, 'metadata'), + json=metadata2, status_code=204) + self.requests_mock.delete(self.url(u, 'metadata', 'key1'), + json=self.diagnostic, + headers=self.json_headers) get_security_groups = { "security_groups": [{ @@ -218,22 +214,21 @@ def setUp(self): 'rules': []}] } - self.requests.register_uri('GET', - self.url('1234', 'os-security-groups'), - json=get_security_groups, - headers=self.json_headers) + self.requests_mock.get(self.url('1234', 'os-security-groups'), + json=get_security_groups, + headers=self.json_headers) - self.requests.register_uri('POST', self.url(), - json=self.post_servers, - headers=self.json_headers) + self.requests_mock.post(self.url(), + json=self.post_servers, + headers=self.json_headers) - self.requests.register_uri('POST', self.url('1234', 'remote-consoles'), - json=self.post_servers_1234_remote_consoles, - headers=self.json_headers) + self.requests_mock.post(self.url('1234', 'remote-consoles'), + json=self.post_servers_1234_remote_consoles, + headers=self.json_headers) - self.requests.register_uri('POST', self.url('1234', 'action'), - json=self.post_servers_1234_action, - headers=self.json_headers) + self.requests_mock.post(self.url('1234', 'action'), + json=self.post_servers_1234_action, + headers=self.json_headers) get_os_interface = { "interfaceAttachments": [ @@ -254,31 +249,29 @@ def setUp(self): ] } - self.requests.register_uri('GET', - self.url('1234', 'os-interface'), - json=get_os_interface, - headers=self.json_headers) + self.requests_mock.get(self.url('1234', 'os-interface'), + json=get_os_interface, + headers=self.json_headers) interface_data = {'interfaceAttachment': {}} - self.requests.register_uri('POST', - self.url('1234', 'os-interface'), - json=interface_data, - headers=self.json_headers) + self.requests_mock.post(self.url('1234', 'os-interface'), + json=interface_data, + headers=self.json_headers) def put_servers_1234(request, context): - body = jsonutils.loads(request.body) + body = request.json() assert list(body) == ['server'] fakes.assert_has_keys(body['server'], optional=['name', 'adminPass']) return request.body - self.requests.register_uri('PUT', self.url(1234), - text=put_servers_1234, - status_code=204, - headers=self.json_headers) + self.requests_mock.put(self.url(1234), + text=put_servers_1234, + status_code=204, + headers=self.json_headers) def post_os_volumes_boot(request, context): - body = jsonutils.loads(request.body) + body = request.json() assert (set(body.keys()) <= set(['server', 'os:scheduler_hints'])) @@ -302,64 +295,56 @@ def post_os_volumes_boot(request, context): # NOTE(jamielennox): hack to make os_volumes mock go to the right place base_url = self.base_url self.base_url = None - self.requests.register_uri('POST', self.url('os-volumes_boot'), - json=post_os_volumes_boot, - status_code=202, - headers=self.json_headers) + self.requests_mock.post(self.url('os-volumes_boot'), + json=post_os_volumes_boot, + status_code=202, + headers=self.json_headers) self.base_url = base_url # # Server password # - self.requests.register_uri('DELETE', - self.url(1234, 'os-server-password'), - status_code=202, - headers=self.json_headers) + self.requests_mock.delete(self.url(1234, 'os-server-password'), + status_code=202, + headers=self.json_headers) # # Server tags # - self.requests.register_uri('GET', - self.url(1234, 'tags'), - json={'tags': ['tag1', 'tag2']}, - headers=self.json_headers) + self.requests_mock.get(self.url(1234, 'tags'), + json={'tags': ['tag1', 'tag2']}, + headers=self.json_headers) - self.requests.register_uri('GET', - self.url(1234, 'tags', 'tag'), - status_code=204, - headers=self.json_headers) + self.requests_mock.get(self.url(1234, 'tags', 'tag'), + status_code=204, + headers=self.json_headers) - self.requests.register_uri('DELETE', - self.url(1234, 'tags', 'tag'), - status_code=204, - headers=self.json_headers) + self.requests_mock.delete(self.url(1234, 'tags', 'tag'), + status_code=204, + headers=self.json_headers) - self.requests.register_uri('DELETE', - self.url(1234, 'tags'), - status_code=204, - headers=self.json_headers) + self.requests_mock.delete(self.url(1234, 'tags'), + status_code=204, + headers=self.json_headers) def put_server_tag(request, context): - body = jsonutils.loads(request.body) - assert body is None + assert request.json() is None context.status_code = 201 return None - self.requests.register_uri('PUT', - self.url(1234, 'tags', 'tag'), - json=put_server_tag, - headers=self.json_headers) + self.requests_mock.put(self.url(1234, 'tags', 'tag'), + json=put_server_tag, + headers=self.json_headers) def put_server_tags(request, context): - body = jsonutils.loads(request.body) + body = request.json() assert list(body) == ['tags'] return body - self.requests.register_uri('PUT', - self.url(1234, 'tags'), - json=put_server_tags, - headers=self.json_headers) + self.requests_mock.put(self.url(1234, 'tags'), + json=put_server_tags, + headers=self.json_headers) class V1(Base): @@ -372,30 +357,27 @@ def setUp(self): # add = self.server_1234['addresses'] - self.requests.register_uri('GET', self.url(1234, 'ips'), - json={'addresses': add}, - headers=self.json_headers) + self.requests_mock.get(self.url(1234, 'ips'), + json={'addresses': add}, + headers=self.json_headers) - self.requests.register_uri('GET', self.url(1234, 'ips', 'public'), - json={'public': add['public']}, - headers=self.json_headers) + self.requests_mock.get(self.url(1234, 'ips', 'public'), + json={'public': add['public']}, + headers=self.json_headers) - self.requests.register_uri('GET', self.url(1234, 'ips', 'private'), - json={'private': add['private']}, - headers=self.json_headers) + self.requests_mock.get(self.url(1234, 'ips', 'private'), + json={'private': add['private']}, + headers=self.json_headers) - self.requests.register_uri('DELETE', - self.url(1234, 'ips', 'public', '1.2.3.4'), - status_code=202) + self.requests_mock.delete(self.url(1234, 'ips', 'public', '1.2.3.4'), + status_code=202) - self.requests.register_uri('GET', - self.url('1234', 'diagnostics'), - json=self.diagnostic, - headers=self.json_headers) + self.requests_mock.get(self.url('1234', 'diagnostics'), + json=self.diagnostic, + headers=self.json_headers) - self.requests.register_uri('DELETE', - self.url('1234', 'os-interface', 'port-id'), - headers=self.json_headers) + self.requests_mock.delete(self.url('1234', 'os-interface', 'port-id'), + headers=self.json_headers) # Testing with the following password and key # @@ -419,13 +401,12 @@ def setUp(self): '/y5a6Z3/AoJZYGG7IH5WN88UROU3B9JZGFB2qtPLQTOvDMZLUhoPRIJeHiVSlo1N' 'tI2/++UsXVg3ow6ItqCJGgdNuGG5JB+bslDHWPxROpesEIHdczk46HCpHQN8f1sk' 'Hi/fmZZNQQqj1Ijq0caOIw=='} - self.requests.register_uri('GET', - self.url(1234, 'os-server-password'), - json=get_server_password, - headers=self.json_headers) + self.requests_mock.get(self.url(1234, 'os-server-password'), + json=get_server_password, + headers=self.json_headers) def post_servers(self, request, context): - body = jsonutils.loads(request.body) + body = request.json() context.status_code = 202 assert (set(body.keys()) <= set(['server', 'os:scheduler_hints'])) @@ -444,7 +425,7 @@ def post_servers(self, request, context): def post_servers_1234_remote_consoles(self, request, context): _body = '' - body = jsonutils.loads(request.body) + body = request.json() context.status_code = 202 assert len(body.keys()) == 1 assert 'remote_console' in body.keys() @@ -458,7 +439,7 @@ def post_servers_1234_remote_consoles(self, request, context): def post_servers_1234_action(self, request, context): _body = '' - body = jsonutils.loads(request.body) + body = request.json() context.status_code = 202 assert len(body.keys()) == 1 action = list(body)[0] diff --git a/novaclient/tests/unit/utils.py b/novaclient/tests/unit/utils.py index d3cb5da3a..49d3631f3 100644 --- a/novaclient/tests/unit/utils.py +++ b/novaclient/tests/unit/utils.py @@ -55,10 +55,6 @@ class TestCase(testtools.TestCase): 'verify': True, } - @property - def requests(self): - return self.requests_mock - def setUp(self): super(TestCase, self).setUp() if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or @@ -89,20 +85,20 @@ def setUp(self): self.cs = None if self.client_fixture_class: - fix = self.client_fixture_class(self.requests) + fix = self.client_fixture_class(self.requests_mock) self.client_fixture = self.useFixture(fix) self.cs = self.client_fixture.client if self.data_fixture_class: - fix = self.data_fixture_class(self.requests) + fix = self.data_fixture_class(self.requests_mock) self.data_fixture = self.useFixture(fix) def assert_called(self, method, path, body=None): - self.assertEqual(self.requests.last_request.method, method) - self.assertEqual(self.requests.last_request.path_url, path) + self.assertEqual(self.requests_mock.last_request.method, method) + self.assertEqual(self.requests_mock.last_request.path_url, path) if body: - req_data = self.requests.last_request.body + req_data = self.requests_mock.last_request.body if isinstance(req_data, six.binary_type): req_data = req_data.decode('utf-8') if not isinstance(body, six.string_types): diff --git a/novaclient/tests/unit/v2/test_agents.py b/novaclient/tests/unit/v2/test_agents.py index 8cf67c1a6..2e749e07a 100644 --- a/novaclient/tests/unit/v2/test_agents.py +++ b/novaclient/tests/unit/v2/test_agents.py @@ -53,9 +53,9 @@ def stub_hypervisors(self, hypervisor='kvm'): headers = {'Content-Type': 'application/json', 'x-openstack-request-id': fakes.FAKE_REQUEST_ID} - self.requests.register_uri('GET', self.data_fixture.url(), - json=get_os_agents, - headers=headers) + self.requests_mock.get(self.data_fixture.url(), + json=get_os_agents, + headers=headers) def test_list_agents(self): self.stub_hypervisors() diff --git a/novaclient/tests/unit/v2/test_hypervisors.py b/novaclient/tests/unit/v2/test_hypervisors.py index d395eee85..d3a578e69 100644 --- a/novaclient/tests/unit/v2/test_hypervisors.py +++ b/novaclient/tests/unit/v2/test_hypervisors.py @@ -194,8 +194,7 @@ def setUp(self): self.cs.api_version = api_versions.APIVersion("2.33") def test_use_limit_marker_params(self): - params = {'limit': 10, 'marker': 'fake-marker'} + params = {'limit': '10', 'marker': 'fake-marker'} self.cs.hypervisors.list(**params) for k, v in params.items(): - self.assertIn('%s=%s' % (k, v), - self.requests.last_request.path_url) + self.assertEqual([v], self.requests_mock.last_request.qs[k]) diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index 2e057b03a..1d8a36fe8 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -17,7 +17,6 @@ import tempfile import mock -from oslo_serialization import jsonutils import six from novaclient import api_versions @@ -38,7 +37,7 @@ class ServersTest(utils.FixturedTestCase): def setUp(self): super(ServersTest, self).setUp() - self.useFixture(floatingips.FloatingFixture(self.requests)) + self.useFixture(floatingips.FloatingFixture(self.requests_mock)) if self.api_version: self.cs.api_version = api_versions.APIVersion(self.api_version) @@ -68,8 +67,8 @@ def test_list_all_servers(self): self.assertEqual(2, len(sl)) - self.assertEqual(self.requests.request_history[-2].method, 'GET') - self.assertEqual(self.requests.request_history[-2].path_url, + self.assertEqual(self.requests_mock.request_history[-2].method, 'GET') + self.assertEqual(self.requests_mock.request_history[-2].path_url, '/servers/detail?marker=1234') self.assert_called('GET', '/servers/detail?marker=5678') @@ -320,7 +319,7 @@ def test_create_server_admin_pass(self): self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers') self.assertIsInstance(s, servers.Server) - body = jsonutils.loads(self.requests.last_request.body) + body = self.requests_mock.last_request.json() self.assertEqual(test_password, body['server']['adminPass']) def test_create_server_userdata_bin(self): @@ -346,7 +345,7 @@ def test_create_server_userdata_bin(self): self.assert_called('POST', '/servers') self.assertIsInstance(s, servers.Server) # verify userdata matches original - body = jsonutils.loads(self.requests.last_request.body) + body = self.requests_mock.last_request.json() transferred_data = body['server']['user_data'] transferred_data = base64.b64decode(transferred_data) self.assertEqual(original_data, transferred_data) @@ -364,8 +363,7 @@ def _create_disk_config(self, disk_config): self.assertIsInstance(s, servers.Server) # verify disk config param was used in the request: - body = jsonutils.loads(self.requests.last_request.body) - server = body['server'] + server = self.requests_mock.last_request.json()['server'] self.assertIn('OS-DCF:diskConfig', server) self.assertEqual(disk_config, server['OS-DCF:diskConfig']) @@ -471,9 +469,7 @@ def _rebuild_resize_disk_config(self, disk_config, operation="rebuild"): self.assert_called('POST', '/servers/1234/action') # verify disk config param was used in the request: - body = jsonutils.loads(self.requests.last_request.body) - - d = body[operation] + d = self.requests_mock.last_request.json()[operation] self.assertIn('OS-DCF:diskConfig', d) self.assertEqual(disk_config, d['OS-DCF:diskConfig']) @@ -488,8 +484,7 @@ def test_rebuild_server_preserve_ephemeral(self): ret = s.rebuild(image=1, preserve_ephemeral=True) self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') - body = jsonutils.loads(self.requests.last_request.body) - d = body['rebuild'] + d = self.requests_mock.last_request.json()['rebuild'] self.assertIn('preserve_ephemeral', d) self.assertTrue(d['preserve_ephemeral']) @@ -498,8 +493,7 @@ def test_rebuild_server_name_meta_files(self): s = self.cs.servers.get(1234) ret = s.rebuild(image=1, name='new', meta={'foo': 'bar'}, files=files) self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) - body = jsonutils.loads(self.requests.last_request.body) - d = body['rebuild'] + d = self.requests_mock.last_request.json()['rebuild'] self.assertEqual('new', d['name']) self.assertEqual({'foo': 'bar'}, d['metadata']) self.assertEqual('/etc/passwd', From 4c1dab99e3b1e3815d9dc3e646f8133e5cf443d5 Mon Sep 17 00:00:00 2001 From: Andreas Jaeger Date: Thu, 6 Oct 2016 20:51:41 +0200 Subject: [PATCH 1151/1705] Enable release notes translation Releasenote translation publishing is being prepared. 'locale_dirs' needs to be defined in conf.py to generate translated version of the release notes. Note that this repository might not get translated release notes - or no translations at all - but we add the entry here nevertheless to prepare for it. Change-Id: Ic73a5d161244396297c9d4f72ae4c586e7f81cf8 --- releasenotes/source/conf.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/releasenotes/source/conf.py b/releasenotes/source/conf.py index 996aff6d6..876063986 100644 --- a/releasenotes/source/conf.py +++ b/releasenotes/source/conf.py @@ -258,3 +258,6 @@ # If true, do not generate a @detailmenu in the "Top" node's menu. #texinfo_no_detailmenu = False + +# -- Options for Internationalization output ------------------------------ +locale_dirs = ['locale/'] From ef3856804d17c1db1769d2f59c9a1ac02f99e9b2 Mon Sep 17 00:00:00 2001 From: jeremyfreudberg Date: Sat, 8 Oct 2016 12:45:56 -0400 Subject: [PATCH 1152/1705] Add timezone disclaimer to docstring Novice users may be tempted to pass in values such as datetime.datetime.now() as a parameter when querying for usage. Adding a disclaimer to the type hints that inputs are interpreted as UTC may help avoid this mistake. Change-Id: Iad34c15f98538fd12215397cb2d9391189f99f79 --- novaclient/v2/usage.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/novaclient/v2/usage.py b/novaclient/v2/usage.py index 8151df5d3..dacf87b55 100644 --- a/novaclient/v2/usage.py +++ b/novaclient/v2/usage.py @@ -51,8 +51,8 @@ def list(self, start, end, detailed=False): """ Get usage for all tenants - :param start: :class:`datetime.datetime` Start date - :param end: :class:`datetime.datetime` End date + :param start: :class:`datetime.datetime` Start date in UTC + :param end: :class:`datetime.datetime` End date in UTC :param detailed: Whether to include information about each instance whose usage is part of the report :rtype: list of :class:`Usage`. @@ -67,8 +67,8 @@ def get(self, tenant_id, start, end): Get usage for a specific tenant. :param tenant_id: Tenant ID to fetch usage for - :param start: :class:`datetime.datetime` Start date - :param end: :class:`datetime.datetime` End date + :param start: :class:`datetime.datetime` Start date in UTC + :param end: :class:`datetime.datetime` End date in UTC :rtype: :class:`Usage` """ return self._get("/os-simple-tenant-usage/%s?start=%s&end=%s" % From fdc5aa5d5ca88df2676f561203c910c3855a6712 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Wed, 12 Oct 2016 17:36:23 -0400 Subject: [PATCH 1153/1705] Remove deprecated commands There are several commands and options which have been deprecated, it's now time to clean those up. flavor-access-list and aggregate-update had options deprecated in newton, but that's still too new to remove those yet. Here is the table of affected commands and the deprecation commit and initial release with the deprecation. Command Commit Release ------- ------ ------- rename 1d1e43957 3.3.0 root-password 3bf6f8fe9 2.27.0 add-floating-ip 4f92f7ba0 2.16.0 remove-floating-ip 4f92f7ba0 2.16.0 absolute-limits bf6fbdb8d 2.25.0 rate-limits bf6fbdb8d 2.25.0 aggregate-details bf68a0cf1 3.4.0 endpoints 1f11840dd 3.1.0 credentials 1f11840dd 3.1.0 Note that network resource quota deprecations are not part of this change as those are impacted by the 2.36 microversion and will be cleaned up later separately. Change-Id: Id649d16ec2cdeb04bbaf2239a5e813abcca9c65d --- .../v2/legacy/test_readonly_nova.py | 13 -- novaclient/tests/unit/v2/test_shell.py | 79 ---------- novaclient/v2/shell.py | 148 ------------------ ...mmands-options-ocata-00f249810e5bdf97.yaml | 17 ++ 4 files changed, 17 insertions(+), 240 deletions(-) create mode 100644 releasenotes/notes/rm-deprecated-commands-options-ocata-00f249810e5bdf97.yaml diff --git a/novaclient/tests/functional/v2/legacy/test_readonly_nova.py b/novaclient/tests/functional/v2/legacy/test_readonly_nova.py index 09eecdd5b..3c8feb874 100644 --- a/novaclient/tests/functional/v2/legacy/test_readonly_nova.py +++ b/novaclient/tests/functional/v2/legacy/test_readonly_nova.py @@ -32,10 +32,6 @@ def test_admin_fake_action(self): # NOTE(jogo): Commands in order listed in 'nova help' - def test_admin_absolute_limites(self): - self.nova('absolute-limits') - self.nova('absolute-limits', params='--reserved') - def test_admin_aggregate_list(self): self.nova('aggregate-list') @@ -45,9 +41,6 @@ def test_admin_availability_zone_list(self): def test_admin_cloudpipe_list(self): self.nova('cloudpipe-list') - def test_admin_credentials(self): - self.nova('credentials') - # "Neutron does not provide this feature" def test_admin_dns_domains(self): self.skip_if_neutron() @@ -58,9 +51,6 @@ def test_admin_dns_list(self): self.skip_if_neutron() self.nova('dns-list') - def test_admin_endpoints(self): - self.nova('endpoints') - def test_admin_flavor_acces_list(self): self.assertRaises(exceptions.CommandFailed, self.nova, @@ -112,9 +102,6 @@ def test_admin_list(self): def test_admin_network_list(self): self.nova('network-list') - def test_admin_rate_limits(self): - self.nova('rate-limits') - def test_admin_secgroup_list(self): self.nova('secgroup-list') diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 34eec493e..5eb19b04f 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -1466,11 +1466,6 @@ def test_migrate(self): self.run_command('migrate sample-server') self.assert_called('POST', '/servers/1234/action', {'migrate': None}) - def test_rename(self): - self.run_command('rename sample-server newname') - self.assert_called('PUT', '/servers/1234', - {'server': {'name': 'newname'}}) - def test_resize(self): self.run_command('resize sample-server 1') self.assert_called('POST', '/servers/1234/action', @@ -1492,14 +1487,6 @@ def test_set_password(self): self.assert_called('POST', '/servers/1234/action', {'changePassword': {'adminPass': 'p'}}) - # root-password is deprecated, keeping this arond until it's removed - # entirely - penick - @mock.patch('getpass.getpass', mock.Mock(return_value='p')) - def test_root_password(self): - self.run_command('root-password sample-server') - self.assert_called('POST', '/servers/1234/action', - {'changePassword': {'adminPass': 'p'}}) - def test_scrub(self): self.run_command('scrub 4ffc664c198e435e9853f2538fbcd7a7') self.assert_called('GET', '/os-networks', pos=-4) @@ -1752,16 +1739,6 @@ def test_floating_ip_bulk_delete(self): self.assert_called('PUT', '/os-floating-ips-bulk/delete', {'ip_range': '10.0.0.1/24'}) - def test_server_floating_ip_add(self): - self.run_command('add-floating-ip sample-server 11.0.0.1') - self.assert_called('POST', '/servers/1234/action', - {'addFloatingIp': {'address': '11.0.0.1'}}) - - def test_server_floating_ip_remove(self): - self.run_command('remove-floating-ip sample-server 11.0.0.1') - self.assert_called('POST', '/servers/1234/action', - {'removeFloatingIp': {'address': '11.0.0.1'}}) - def test_server_floating_ip_associate(self): self.run_command('floating-ip-associate sample-server 11.0.0.1') self.assert_called('POST', '/servers/1234/action', @@ -1935,14 +1912,6 @@ def test_aggregate_remove_host_by_name(self): self.assert_called('POST', '/os-aggregates/1/action', body, pos=-2) self.assert_called('GET', '/os-aggregates/1', pos=-1) - def test_aggregate_details_by_id(self): - self.run_command('aggregate-details 1') - self.assert_called('GET', '/os-aggregates/1') - - def test_aggregate_details_by_name(self): - self.run_command('aggregate-details test') - self.assert_called('GET', '/os-aggregates') - def test_aggregate_show_by_id(self): self.run_command('aggregate-show 1') self.assert_called('GET', '/os-aggregates/1') @@ -2662,16 +2631,6 @@ def test_backup(self): 'backup_type': 'daily', 'rotation': '1'}}) - def test_absolute_limits(self): - self.run_command('absolute-limits') - self.assert_called('GET', '/limits') - - self.run_command('absolute-limits --reserved') - self.assert_called('GET', '/limits?reserved=1') - - self.run_command('absolute-limits --tenant 1234') - self.assert_called('GET', '/limits?tenant_id=1234') - def test_limits(self): self.run_command('limits') self.assert_called('GET', '/limits') @@ -3239,44 +3198,6 @@ def test_with_non_unique_name(self): 'group_one') -class GetFirstEndpointTest(utils.TestCase): - def test_only_one_endpoint(self): - # If there is only one endpoint, it is returned. - endpoint = {"url": "test"} - result = novaclient.v2.shell._get_first_endpoint([endpoint], "XYZ") - self.assertEqual(endpoint, result) - - def test_multiple_endpoints(self): - # If there are multiple endpoints, the first one of the appropriate - # region is returned. - endpoints = [ - {"region": "XYZ"}, - {"region": "ORD", "number": 1}, - {"region": "ORD", "number": 2} - ] - result = novaclient.v2.shell._get_first_endpoint(endpoints, "ORD") - self.assertEqual(endpoints[1], result) - - def test_multiple_endpoints_but_none_suitable(self): - # If there are multiple endpoints but none of them are suitable, an - # exception is raised. - - endpoints = [ - {"region": "XYZ"}, - {"region": "PQR"}, - {"region": "STU"} - ] - self.assertRaises(LookupError, - novaclient.v2.shell._get_first_endpoint, - endpoints, "ORD") - - def test_no_endpoints(self): - # If there are no endpoints available, an exception is raised. - self.assertRaises(LookupError, - novaclient.v2.shell._get_first_endpoint, - [], "ORD") - - class PollForStatusTestCase(utils.TestCase): @mock.patch("novaclient.v2.shell.time") def test_simple_usage(self, mock_time): diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index ecf5a5963..7f609b76f 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -28,7 +28,6 @@ import os import sys import time -import warnings from oslo_utils import encodeutils from oslo_utils import netutils @@ -1888,15 +1887,6 @@ def do_rebuild(cs, args): _poll_for_status(cs.servers.get, server.id, 'rebuilding', ['active']) -@utils.arg( - 'server', metavar='', - help=_('Name (old name) or ID of server.')) -@utils.arg('name', metavar='', help=_('New name for the server.')) -def do_rename(cs, args): - """DEPRECATED, use update instead.""" - do_update(cs, args) - - @utils.arg( 'server', metavar='', help=_('Name (old name) or ID of server.')) @@ -2123,12 +2113,6 @@ def do_refresh_network(cs, args): 'name': 'network-changed'}]) -@utils.arg('server', metavar='', help=_('Name or ID of server.')) -def do_root_password(cs, args): - """DEPRECATED, use set-password instead.""" - do_set_password(cs, args) - - @utils.arg('server', metavar='', help=_('Name or ID of server.')) def do_set_password(cs, args): """ @@ -2679,18 +2663,6 @@ def do_console_log(cs, args): print(data) -@utils.arg('server', metavar='', help=_('Name or ID of server.')) -@utils.arg('address', metavar='
', help=_('IP Address.')) -@utils.arg( - '--fixed-address', - metavar='', - default=None, - help=_('Fixed IP Address to associate with.')) -def do_add_floating_ip(cs, args): - """DEPRECATED, use floating-ip-associate instead.""" - _associate_floating_ip(cs, args) - - @utils.arg('server', metavar='', help=_('Name or ID of server.')) @utils.arg('address', metavar='
', help=_('IP Address.')) @utils.arg( @@ -2708,13 +2680,6 @@ def _associate_floating_ip(cs, args): server.add_floating_ip(args.address, args.fixed_address) -@utils.arg('server', metavar='', help=_('Name or ID of server.')) -@utils.arg('address', metavar='
', help=_('IP Address.')) -def do_remove_floating_ip(cs, args): - """DEPRECATED, use floating-ip-disassociate instead.""" - _disassociate_floating_ip(cs, args) - - @utils.arg('server', metavar='', help=_('Name or ID of server.')) @utils.arg('address', metavar='
', help=_('IP Address.')) def do_floating_ip_disassociate(cs, args): @@ -3369,25 +3334,6 @@ def _find_keypair(cs, keypair): return utils.find_resource(cs.keypairs, keypair) -@utils.arg( - '--tenant', - # nova db searches by project_id - dest='tenant', - metavar='', - nargs='?', - help=_('Display information from single tenant (Admin only).')) -@utils.arg( - '--reserved', - dest='reserved', - action='store_true', - default=False, - help=_('Include reservations count.')) -def do_absolute_limits(cs, args): - """DEPRECATED, use limits instead.""" - limits = cs.limits.get(args.reserved, args.tenant).absolute - _print_absolute_limits(limits) - - def _print_absolute_limits(limits): """Prints absolute limits.""" class Limit(object): @@ -3450,12 +3396,6 @@ def __init__(self, name, used, max, other): utils.print_list(limit_list, columns) -def do_rate_limits(cs, args): - """DEPRECATED, use limits instead.""" - limits = cs.limits.get().rate - _print_rate_limits(limits) - - def _print_rate_limits(limits): """print rate limits.""" columns = ['Verb', 'URI', 'Value', 'Remain', 'Unit', 'Next_Available'] @@ -3837,14 +3777,6 @@ def do_aggregate_remove_host(cs, args): _print_aggregate_details(aggregate) -@utils.arg( - 'aggregate', metavar='', - help=_('Name or ID of aggregate.')) -def do_aggregate_details(cs, args): - """DEPRECATED, use aggregate-show instead.""" - do_aggregate_show(cs, args) - - @utils.arg( 'aggregate', metavar='', help=_('Name or ID of aggregate.')) @@ -4296,86 +4228,6 @@ def do_hypervisor_stats(cs, args): utils.print_dict(stats._info.copy()) -def ensure_service_catalog_present(cs): - if not hasattr(cs.client, 'service_catalog'): - # Turn off token caching and re-auth - cs.client.unauthenticate() - cs.client.use_token_cache(False) - cs.client.authenticate() - - -def do_endpoints(cs, _args): - """Discover endpoints that get returned from the authenticate services.""" - warnings.warn( - "nova endpoints is deprecated, use openstack catalog list instead") - if isinstance(cs.client, client.SessionClient): - access = cs.client.auth.get_access(cs.client.session) - for service in access.service_catalog.catalog: - _print_endpoints(service, cs.client.region_name) - else: - ensure_service_catalog_present(cs) - - catalog = cs.client.service_catalog.catalog - region = cs.client.region_name - for service in catalog['access']['serviceCatalog']: - _print_endpoints(service, region) - - -def _print_endpoints(service, region): - name, endpoints = service["name"], service["endpoints"] - - try: - endpoint = _get_first_endpoint(endpoints, region) - utils.print_dict(endpoint, name) - except LookupError: - print(_("WARNING: %(service)s has no endpoint in %(region)s! " - "Available endpoints for this service:") % - {'service': name, 'region': region}) - for other_endpoint in endpoints: - utils.print_dict(other_endpoint, name) - - -def _get_first_endpoint(endpoints, region): - """Find the first suitable endpoint in endpoints. - - If there is only one endpoint, return it. If there is more than - one endpoint, return the first one with the given region. If there - are no endpoints, or there is more than one endpoint but none of - them match the given region, raise KeyError. - - """ - if len(endpoints) == 1: - return endpoints[0] - else: - for candidate_endpoint in endpoints: - if candidate_endpoint["region"] == region: - return candidate_endpoint - - raise LookupError("No suitable endpoint found") - - -@utils.arg( - '--wrap', dest='wrap', metavar='', default=64, - help=_('Wrap PKI tokens to a specified length, or 0 to disable.')) -def do_credentials(cs, _args): - """Show user credentials returned from auth.""" - warnings.warn( - "nova credentials is deprecated, use openstack client instead") - if isinstance(cs.client, client.SessionClient): - access = cs.client.auth.get_access(cs.client.session) - utils.print_dict(access._user, 'User Credentials', - wrap=int(_args.wrap)) - if hasattr(access, '_token'): - utils.print_dict(access._token, 'Token', wrap=int(_args.wrap)) - else: - ensure_service_catalog_present(cs) - catalog = cs.client.service_catalog.catalog - utils.print_dict(catalog['access']['user'], "User Credentials", - wrap=int(_args.wrap)) - utils.print_dict(catalog['access']['token'], "Token", - wrap=int(_args.wrap)) - - @utils.arg('server', metavar='', help=_('Name or ID of server.')) @utils.arg( '--port', diff --git a/releasenotes/notes/rm-deprecated-commands-options-ocata-00f249810e5bdf97.yaml b/releasenotes/notes/rm-deprecated-commands-options-ocata-00f249810e5bdf97.yaml new file mode 100644 index 000000000..d710ffa76 --- /dev/null +++ b/releasenotes/notes/rm-deprecated-commands-options-ocata-00f249810e5bdf97.yaml @@ -0,0 +1,17 @@ +--- +prelude: > + Several deprecated commands have been removed. See the upgrade section for + details. +upgrade: + - | + The following deprecated commands have been removed: + + * absolute-limits + * add-floating-ip + * aggregate-details + * credentials + * endpoints + * rate-limits + * remove-floating-ip + * rename + * root-password From 199fc67b46e8a52f47b74200de2f91d6f761a22b Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Wed, 12 Oct 2016 18:19:31 -0400 Subject: [PATCH 1154/1705] Remove unused helper volume methods in v2 shell These were probably missed when we removed the volume proxy code in Newton. Change-Id: I5b1413ad61404b31d8d3ffb392a314f52381164a --- novaclient/v2/shell.py | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 7f609b76f..a7d6ab585 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -2416,36 +2416,10 @@ def do_remove_fixed_ip(cs, args): server.remove_fixed_ip(args.address) -def _find_volume(cs, volume): - """Get a volume by name or ID.""" - return utils.find_resource(cs.volumes, volume) - - -def _find_volume_snapshot(cs, snapshot): - """Get a volume snapshot by name or ID.""" - return utils.find_resource(cs.volume_snapshots, snapshot) - - def _print_volume(volume): utils.print_dict(volume._info) -def _print_volume_snapshot(snapshot): - utils.print_dict(snapshot._info) - - -def _translate_volume_keys(collection): - _translate_keys(collection, - [('displayName', 'display_name'), - ('volumeType', 'volume_type')]) - - -def _translate_volume_snapshot_keys(collection): - _translate_keys(collection, - [('displayName', 'display_name'), - ('volumeId', 'volume_id')]) - - def _translate_availability_zone_keys(collection): _translate_keys(collection, [('zoneName', 'name'), ('zoneState', 'status')]) From 2a622fff471d64736106359487bb56e50bf3afa4 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Wed, 12 Oct 2016 18:24:01 -0400 Subject: [PATCH 1155/1705] Update docs for instructions on deprecating commands The instructions for deprecating commands predated reno so this change updates them a bit to be current with our processes now. Also adds a reminder to update the help text/description of a command to mark it deprecated. Change-Id: I530399ba9cf6c50bd0dbb06f231aee61466eebd6 --- doc/source/index.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/doc/source/index.rst b/doc/source/index.rst index bdbce90c4..2e40292a0 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -59,8 +59,10 @@ The process for command deprecation is: deprecated command is used. - That warning message should include a rough timeline for when the command will be removed and what should be used instead, if anything. - - The commit message on the change should include a DocImpact tag so it - gets in the release notes. + - The description in the help text for the deprecated command should mark + that it is deprecated. + - The change should include a release note with the ``deprecations`` section + filled out. - The deprecation cycle is typically the first client release *after* the next *full* Nova server release so that there is at least six months of deprecation. From 74222a6ddc1b24f46018f18afe73c7d270462822 Mon Sep 17 00:00:00 2001 From: Nicolas Simonds Date: Wed, 12 Oct 2016 16:01:41 -0700 Subject: [PATCH 1156/1705] Make "policy" a mandatory argument for server-group-create Commit 6f8667990cdb3cd662d51be2403cca5859559f04 removed the deprecated "--policy" switch, which caused the check for the missing parameter to quietly stop working correctly. Make "policy" a mandatory positional parameter and let argparse figure it out; it has a much better/more helpful error message anyways. Closes-Bug: 1632866 Change-Id: I3234c08b1bc8ba263537bb6232280c471457f069 --- novaclient/v2/shell.py | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index ecf5a5963..5942f10ae 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -5224,27 +5224,13 @@ def do_secgroup_delete_default_rule(cs, args): @utils.arg('name', metavar='', help=_('Server group name.')) -# NOTE(wingwj): The '--policy' way is still reserved here for preserving -# the backwards compatibility of CLI, even if a user won't get this usage -# in '--help' description. It will be deprecated after a suitable deprecation -# period(probably 2 coordinated releases or so). -# -# Moreover, we imagine that a given user will use only positional parameters or -# only the "--policy" option. So we don't need to properly handle -# the possibility that they might mix them here. That usage is unsupported. -# The related discussion can be found in -# https://review.openstack.org/#/c/96382/2/. @utils.arg( 'policy', metavar='', - default=argparse.SUPPRESS, - nargs='*', + nargs='+', help=_('Policies for the server groups.')) def do_server_group_create(cs, args): """Create a new server group with the specified details.""" - if not args.policy: - raise exceptions.CommandError(_("at least one policy must be " - "specified")) kwargs = {'name': args.name, 'policies': args.policy} server_group = cs.server_groups.create(**kwargs) From d161152a2390b2122884c5c617b40dc642f082bb Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 14 Oct 2016 05:44:00 +0000 Subject: [PATCH 1157/1705] Updated from global requirements Change-Id: Iffa460b6af104991dfc218e09bfa02beda9f43cb --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 641155c2a..c784e2ac6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. pbr>=1.6 # Apache-2.0 -keystoneauth1>=2.10.0 # Apache-2.0 +keystoneauth1>=2.13.0 # Apache-2.0 iso8601>=0.1.11 # MIT oslo.i18n>=2.1.0 # Apache-2.0 oslo.serialization>=1.10.0 # Apache-2.0 From 33d8a6702af82cb768a0b4884ca7395307dd5a7c Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Sat, 15 Oct 2016 00:12:23 +0000 Subject: [PATCH 1158/1705] Updated from global requirements Change-Id: I37cc22c7500883ef44e027aa46dc7a7e4270c597 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c784e2ac6..85f98d60e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. pbr>=1.6 # Apache-2.0 -keystoneauth1>=2.13.0 # Apache-2.0 +keystoneauth1>=2.14.0 # Apache-2.0 iso8601>=0.1.11 # MIT oslo.i18n>=2.1.0 # Apache-2.0 oslo.serialization>=1.10.0 # Apache-2.0 From 0a16d416527d8cdad189323b57254838350c57d4 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Wed, 19 Oct 2016 03:58:30 +0000 Subject: [PATCH 1159/1705] Updated from global requirements Change-Id: I34f668657bd92584432a34797c2e119718070a21 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index fc54eb723..386334059 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -8,7 +8,7 @@ coverage>=3.6 # Apache-2.0 fixtures>=3.0.0 # Apache-2.0/BSD keyring>=5.5.1 # MIT/PSF mock>=2.0 # BSD -python-keystoneclient!=2.1.0,>=2.0.0 # Apache-2.0 +python-keystoneclient>=3.6.0 # Apache-2.0 python-cinderclient!=1.7.0,!=1.7.1,>=1.6.0 # Apache-2.0 python-glanceclient>=2.5.0 # Apache-2.0 requests-mock>=1.1 # Apache-2.0 From a972977a4698fbde7f2ad2e66d7925d00453c7d1 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Wed, 19 Oct 2016 17:45:55 +0000 Subject: [PATCH 1160/1705] Updated from global requirements Change-Id: Ie9dea84731248b2d80cbb275548758c67edadacc --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 386334059..d6a4cdf8a 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -13,7 +13,7 @@ python-cinderclient!=1.7.0,!=1.7.1,>=1.6.0 # Apache-2.0 python-glanceclient>=2.5.0 # Apache-2.0 requests-mock>=1.1 # Apache-2.0 sphinx!=1.3b1,<1.4,>=1.2.1 # BSD -os-client-config!=1.19.0,!=1.19.1,!=1.20.0,!=1.20.1,!=1.21.0,>=1.13.1 # Apache-2.0 +os-client-config>=1.22.0 # Apache-2.0 oslosphinx>=4.7.0 # Apache-2.0 testrepository>=0.0.18 # Apache-2.0/BSD testscenarios>=0.4 # Apache-2.0/BSD From daa9bdc82335b37b33d21264a770e10ee452f22c Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Wed, 19 Oct 2016 16:01:42 -0400 Subject: [PATCH 1161/1705] Remove support for non-keystone auth systems The support for non-keystone auth systems and plugins was deprecated with 1f11840dd84f3570330d1fcd53d1e8eea5ff7922 back in the 3.1.0 release in mitaka. Now that we're working on shoring up the support for keystone auth sessions in the client and eventually moving to remove support for the non-session HTTPClient, we should also remove the support to load non-keystone auth plugins. The whole concept of non-keystone auth systems/plugins is a legacy artifact and is a barrier to interoperability which is a goal of nova and OpenStack in general. Change-Id: Ia649db257c416ca054977812ecb3f1a8044fa584 --- novaclient/auth_plugin.py | 150 -------- novaclient/client.py | 23 +- novaclient/exceptions.py | 9 - novaclient/shell.py | 69 +--- novaclient/tests/unit/test_auth_plugins.py | 358 ------------------ novaclient/tests/unit/test_shell.py | 27 +- novaclient/v2/client.py | 6 +- .../remove-auth-system-b2cd247b8a312b72.yaml | 9 + 8 files changed, 29 insertions(+), 622 deletions(-) delete mode 100644 novaclient/auth_plugin.py delete mode 100644 novaclient/tests/unit/test_auth_plugins.py create mode 100644 releasenotes/notes/remove-auth-system-b2cd247b8a312b72.yaml diff --git a/novaclient/auth_plugin.py b/novaclient/auth_plugin.py deleted file mode 100644 index 603bf3a29..000000000 --- a/novaclient/auth_plugin.py +++ /dev/null @@ -1,150 +0,0 @@ -# Copyright 2013 OpenStack Foundation -# Copyright 2013 Spanish National Research Council. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import logging - -import pkg_resources -import six - -from novaclient import exceptions -from novaclient import utils - - -logger = logging.getLogger(__name__) - - -_discovered_plugins = {} - - -def discover_auth_systems(): - """Discover the available auth-systems. - - This won't take into account the old style auth-systems. - """ - ep_name = 'openstack.client.auth_plugin' - for ep in pkg_resources.iter_entry_points(ep_name): - try: - # FIXME(dhellmann): It would be better to use stevedore - # here, since it abstracts this difference in behavior - # between versions of setuptools, but this seemed like a - # more expedient fix. - if hasattr(ep, 'resolve') and hasattr(ep, 'require'): - auth_plugin = ep.resolve() - else: - auth_plugin = ep.load(require=False) - except (ImportError, pkg_resources.UnknownExtra, AttributeError) as e: - logger.debug("ERROR: Cannot load auth plugin %s" % ep.name) - logger.debug(e, exc_info=1) - else: - _discovered_plugins[ep.name] = auth_plugin - - -def load_auth_system_opts(parser): - """Load options needed by the available auth-systems into a parser. - - This function will try to populate the parser with options from the - available plugins. - """ - for name, auth_plugin in six.iteritems(_discovered_plugins): - add_opts_fn = getattr(auth_plugin, "add_opts", None) - if add_opts_fn: - group = parser.add_argument_group("Auth-system '%s' options" % - name) - add_opts_fn(group) - - -def load_plugin(auth_system): - if auth_system in _discovered_plugins: - return _discovered_plugins[auth_system]() - - # NOTE(aloga): If we arrive here, the plugin will be an old-style one, - # so we have to create a fake AuthPlugin for it. - return DeprecatedAuthPlugin(auth_system) - - -class BaseAuthPlugin(object): - """Base class for authentication plugins. - - An authentication plugin needs to override at least the authenticate - method to be a valid plugin. - """ - def __init__(self): - self.opts = {} - - def get_auth_url(self): - """Return the auth url for the plugin (if any).""" - return None - - @staticmethod - def add_opts(parser): - """Populate and return the parser with the options for this plugin. - - If the plugin does not need any options, it should return the same - parser untouched. - """ - return parser - - def parse_opts(self, args): - """Parse the actual auth-system options if any. - - This method is expected to populate the attribute self.opts with a - dict containing the options and values needed to make authentication. - If the dict is empty, the client should assume that it needs the same - options as the 'keystone' auth system (i.e. os_username and - os_password). - - Returns the self.opts dict. - """ - return self.opts - - def authenticate(self, cls, auth_url): - """Authenticate using plugin defined method.""" - raise exceptions.AuthSystemNotFound(self.auth_system) - - -class DeprecatedAuthPlugin(object): - """Class to mimic the AuthPlugin class for deprecated auth systems. - - Old auth systems only define two entry points: openstack.client.auth_url - and openstack.client.authenticate. This class will load those entry points - into a class similar to a valid AuthPlugin. - """ - def __init__(self, auth_system): - self.auth_system = auth_system - - def authenticate(cls, auth_url): - raise exceptions.AuthSystemNotFound(self.auth_system) - - self.opts = {} - - self.get_auth_url = lambda: None - self.authenticate = authenticate - - self._load_endpoints() - - def _load_endpoints(self): - ep_name = 'openstack.client.auth_url' - fn = utils.load_entry_point(ep_name, name=self.auth_system) - if fn: - self.get_auth_url = fn - - ep_name = 'openstack.client.authenticate' - fn = utils.load_entry_point(ep_name, name=self.auth_system) - if fn: - self.authenticate = fn - - def parse_opts(self, args): - return self.opts diff --git a/novaclient/client.py b/novaclient/client.py index 2e5f125a6..f9fecaa4f 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -156,8 +156,7 @@ def __init__(self, user, password, projectid=None, auth_url=None, service_name=None, volume_service_name=None, timings=False, bypass_url=None, os_cache=False, no_cache=True, - http_log_debug=False, auth_system='keystone', - auth_plugin=None, auth_token=None, + http_log_debug=False, auth_token=None, cacert=None, tenant_id=None, user_id=None, connection_pool=False, api_version=None, logger=None): @@ -177,13 +176,6 @@ def __init__(self, user, password, projectid=None, auth_url=None, # been proven invalid self.password_func = None - if auth_system and auth_system != 'keystone' and not auth_plugin: - raise exceptions.AuthSystemNotFound(auth_system) - - if not auth_url and auth_system and auth_system != 'keystone': - auth_url = auth_plugin.get_auth_url() - if not auth_url: - raise exceptions.EndpointNotFound() self.auth_url = auth_url.rstrip('/') if auth_url else auth_url self.version = 'v1.1' self.region_name = region_name @@ -217,8 +209,6 @@ def __init__(self, user, password, projectid=None, auth_url=None, else: self.verify_cert = True - self.auth_system = auth_system - self.auth_plugin = auth_plugin self._session = None self._current_url = None self._logger = logger or logging.getLogger(__name__) @@ -589,10 +579,7 @@ def authenticate(self): auth_url = self.auth_url if self.version == "v2.0": # FIXME(chris): This should be better. while auth_url: - if not self.auth_system or self.auth_system == 'keystone': - auth_url = self._v2_auth(auth_url) - else: - auth_url = self._plugin_auth(auth_url) + auth_url = self._v2_auth(auth_url) # Are we acting on behalf of another user via an # existing token? If so, our actual endpoints may @@ -659,9 +646,6 @@ def _v1_auth(self, url): else: raise exceptions.from_response(resp, body, url) - def _plugin_auth(self, auth_url): - return self.auth_plugin.authenticate(self, auth_url) - def _v2_auth(self, url): """Authenticate against a v2.0 auth service.""" if self.auth_token: @@ -707,7 +691,6 @@ def _construct_http_client(username=None, password=None, project_id=None, service_name=None, volume_service_name=None, timings=False, bypass_url=None, os_cache=False, no_cache=True, http_log_debug=False, - auth_system='keystone', auth_plugin=None, auth_token=None, cacert=None, tenant_id=None, user_id=None, connection_pool=False, session=None, auth=None, user_agent='python-novaclient', @@ -738,8 +721,6 @@ def _construct_http_client(username=None, password=None, project_id=None, auth_token=auth_token, insecure=insecure, timeout=timeout, - auth_system=auth_system, - auth_plugin=auth_plugin, proxy_token=proxy_token, proxy_tenant_id=proxy_tenant_id, region_name=region_name, diff --git a/novaclient/exceptions.py b/novaclient/exceptions.py index 31dd48715..d23df8ab2 100644 --- a/novaclient/exceptions.py +++ b/novaclient/exceptions.py @@ -54,15 +54,6 @@ class NoUniqueMatch(Exception): pass -class AuthSystemNotFound(Exception): - """When the user specify a AuthSystem but not installed.""" - def __init__(self, auth_system): - self.auth_system = auth_system - - def __str__(self): - return "AuthSystemNotFound: %s" % repr(self.auth_system) - - class NoTokenLookupException(Exception): """This form of authentication does not support looking up endpoints from an existing token. diff --git a/novaclient/shell.py b/novaclient/shell.py index 3a3f28529..eb802fdb9 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -23,7 +23,6 @@ import getpass import logging import sys -import warnings from keystoneauth1 import loading from oslo_utils import encodeutils @@ -41,7 +40,6 @@ import novaclient from novaclient import api_versions -import novaclient.auth_plugin from novaclient import client from novaclient import exceptions as exc import novaclient.extension @@ -455,12 +453,6 @@ def get_base_parser(self, argv): default=utils.env('OS_REGION_NAME', 'NOVA_REGION_NAME'), help=_('Defaults to env[OS_REGION_NAME].')) - parser.add_argument( - '--os-auth-system', - metavar='', - default=utils.env('OS_AUTH_SYSTEM'), - help=argparse.SUPPRESS) - parser.add_argument( '--service-type', metavar='', @@ -509,9 +501,6 @@ def get_base_parser(self, argv): help=_("Use this API endpoint instead of the Service Catalog. " "Defaults to env[NOVACLIENT_BYPASS_URL].")) - # The auth-system-plugins might require some extra options - novaclient.auth_plugin.load_auth_system_opts(parser) - self._append_global_identity_args(parser, argv) return parser @@ -639,9 +628,6 @@ def main(self, argv): skip_auth = do_help or ( 'bash-completion' in argv) - # Discover available auth plugins - novaclient.auth_plugin.discover_auth_systems() - if not args.os_compute_api_version: api_version = api_versions.get_api_version( DEFAULT_MAJOR_OS_COMPUTE_API_VERSION) @@ -658,7 +644,6 @@ def main(self, argv): args, 'os_project_id', getattr(args, 'os_tenant_id', None)) os_auth_url = args.os_auth_url os_region_name = args.os_region_name - os_auth_system = args.os_auth_system if "v2.0" not in os_auth_url: # NOTE(andreykurilin): assume that keystone V3 is used and try to @@ -691,15 +676,6 @@ def main(self, argv): auth_token = getattr(args, 'os_token', None) management_url = bypass_url if bypass_url else None - if os_auth_system and os_auth_system != "keystone": - warnings.warn(_( - 'novaclient auth plugins that are not keystone are deprecated.' - ' Auth plugins should now be done as plugins to keystoneauth' - ' and selected with --os-auth-type or OS_AUTH_TYPE')) - auth_plugin = novaclient.auth_plugin.load_plugin(os_auth_system) - else: - auth_plugin = None - if not endpoint_type: endpoint_type = DEFAULT_NOVA_ENDPOINT_TYPE @@ -717,25 +693,20 @@ def main(self, argv): # Expired tokens are handled by client.py:_cs_request must_auth = not (auth_token and management_url) - # Do not use Keystone session for cases with no session support. The - # presence of auth_plugin means os_auth_system is present and is not - # keystone. + # Do not use Keystone session for cases with no session support. use_session = True - if auth_plugin or bypass_url or os_cache or volume_service_name: + if bypass_url or os_cache or volume_service_name: use_session = False # FIXME(usrleon): Here should be restrict for project id same as # for os_username or os_password but for compatibility it is not. if must_auth and not skip_auth: - if auth_plugin: - auth_plugin.parse_opts(args) - if not auth_plugin or not auth_plugin.opts: - if not os_username and not os_user_id: - raise exc.CommandError( - _("You must provide a username " - "or user ID via --os-username, --os-user-id, " - "env[OS_USERNAME] or env[OS_USER_ID]")) + if not os_username and not os_user_id: + raise exc.CommandError( + _("You must provide a username " + "or user ID via --os-username, --os-user-id, " + "env[OS_USERNAME] or env[OS_USER_ID]")) if not any([os_project_name, os_project_id]): raise exc.CommandError(_("You must provide a project name or" @@ -746,16 +717,9 @@ def main(self, argv): " interchangeably.")) if not os_auth_url: - if os_auth_system and os_auth_system != 'keystone': - os_auth_url = auth_plugin.get_auth_url() - - if not os_auth_url: - raise exc.CommandError( - _("You must provide an auth url " - "via either --os-auth-url or env[OS_AUTH_URL] " - "or specify an auth_system which defines a " - "default url with --os-auth-system " - "or env[OS_AUTH_SYSTEM]")) + raise exc.CommandError( + _("You must provide an auth url " + "via either --os-auth-url or env[OS_AUTH_URL].")) if use_session: # Not using Nova auth plugin, so use keystone @@ -792,8 +756,7 @@ def main(self, argv): auth_url=os_auth_url, insecure=insecure, region_name=os_region_name, endpoint_type=endpoint_type, extensions=self.extensions, service_type=service_type, - service_name=service_name, auth_system=os_auth_system, - auth_plugin=auth_plugin, auth_token=auth_token, + service_name=service_name, auth_token=auth_token, volume_service_name=volume_service_name, timings=args.timings, bypass_url=bypass_url, os_cache=os_cache, http_log_debug=args.debug, @@ -857,8 +820,7 @@ def main(self, argv): auth_url=os_auth_url, insecure=insecure, region_name=os_region_name, endpoint_type=endpoint_type, extensions=self.extensions, service_type=service_type, - service_name=service_name, auth_system=os_auth_system, - auth_plugin=auth_plugin, auth_token=auth_token, + service_name=service_name, auth_token=auth_token, volume_service_name=volume_service_name, timings=args.timings, bypass_url=bypass_url, os_cache=os_cache, http_log_debug=args.debug, @@ -870,11 +832,6 @@ def main(self, argv): if must_auth: helper = SecretsHelper(args, self.cs.client) self.cs.client.keyring_saver = helper - if (auth_plugin and auth_plugin.opts and - "os_password" not in auth_plugin.opts): - use_pw = False - else: - use_pw = True tenant_id = helper.tenant_id # Allow commandline to override cache @@ -887,7 +844,7 @@ def main(self, argv): self.cs.client.auth_token = auth_token self.cs.client.management_url = management_url self.cs.client.password_func = lambda: helper.password - elif use_pw: + else: # We're missing something, so auth with user/pass and save # the result in our helper. self.cs.client.password = helper.password diff --git a/novaclient/tests/unit/test_auth_plugins.py b/novaclient/tests/unit/test_auth_plugins.py deleted file mode 100644 index 78dd393da..000000000 --- a/novaclient/tests/unit/test_auth_plugins.py +++ /dev/null @@ -1,358 +0,0 @@ -# Copyright 2012 OpenStack Foundation -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import argparse - -from keystoneauth1 import fixture -import mock -import pkg_resources -import requests - -try: - import json -except ImportError: - import simplejson as json - -from novaclient import auth_plugin -from novaclient import client -from novaclient import exceptions -from novaclient.tests.unit import utils - - -def mock_http_request(resp=None): - """Mock an HTTP Request.""" - if not resp: - resp = fixture.V2Token() - resp.set_scope() - s = resp.add_service('compute') - s.add_endpoint("http://localhost:8774/v1.1", region='RegionOne') - - auth_response = utils.TestResponse({ - "status_code": 200, - "text": json.dumps(resp), - }) - return mock.Mock(return_value=(auth_response)) - - -def requested_headers(cs): - """Return requested passed headers.""" - return { - 'User-Agent': cs.client.USER_AGENT, - 'Content-Type': 'application/json', - 'Accept': 'application/json', - } - - -class DeprecatedAuthPluginTest(utils.TestCase): - def test_auth_system_success(self): - class MockEntrypoint(pkg_resources.EntryPoint): - def load(self, require=False): - return self.authenticate - - def resolve(self): - return self.authenticate - - def authenticate(self, cls, auth_url): - cls._authenticate(auth_url, {"fake": "me"}) - - def mock_iter_entry_points(_type, name): - if _type == 'openstack.client.authenticate': - return [MockEntrypoint("fake", "fake", ["fake"])] - else: - return [] - - mock_request = mock_http_request() - - @mock.patch.object(pkg_resources, "iter_entry_points", - mock_iter_entry_points) - @mock.patch.object(requests, "request", mock_request) - def test_auth_call(): - plugin = auth_plugin.DeprecatedAuthPlugin("fake") - cs = client.Client("2", "username", "password", "project_id", - utils.AUTH_URL_V2, auth_system="fake", - auth_plugin=plugin) - cs.client.authenticate() - - headers = requested_headers(cs) - token_url = cs.client.auth_url + "/tokens" - - mock_request.assert_called_with( - "POST", - token_url, - headers=headers, - data='{"fake": "me"}', - allow_redirects=True, - **self.TEST_REQUEST_BASE) - - test_auth_call() - - def test_auth_system_not_exists(self): - def mock_iter_entry_points(_t, name=None): - return [pkg_resources.EntryPoint("fake", "fake", ["fake"])] - - mock_request = mock_http_request() - - @mock.patch.object(pkg_resources, "iter_entry_points", - mock_iter_entry_points) - @mock.patch.object(requests, "request", mock_request) - def test_auth_call(): - auth_plugin.discover_auth_systems() - plugin = auth_plugin.DeprecatedAuthPlugin("notexists") - cs = client.Client("2", "username", "password", "project_id", - utils.AUTH_URL_V2, auth_system="notexists", - auth_plugin=plugin) - self.assertRaises(exceptions.AuthSystemNotFound, - cs.client.authenticate) - - test_auth_call() - - def test_auth_system_defining_auth_url(self): - class MockAuthUrlEntrypoint(pkg_resources.EntryPoint): - def load(self, require=False): - return self.auth_url - - def resolve(self): - return self.auth_url - - def auth_url(self): - return "http://faked/v2.0" - - class MockAuthenticateEntrypoint(pkg_resources.EntryPoint): - def load(self, require=False): - return self.authenticate - - def resolve(self): - return self.authenticate - - def authenticate(self, cls, auth_url): - cls._authenticate(auth_url, {"fake": "me"}) - - def mock_iter_entry_points(_type, name): - if _type == 'openstack.client.auth_url': - return [MockAuthUrlEntrypoint("fakewithauthurl", - "fakewithauthurl", - ["auth_url"])] - elif _type == 'openstack.client.authenticate': - return [MockAuthenticateEntrypoint("fakewithauthurl", - "fakewithauthurl", - ["authenticate"])] - else: - return [] - - mock_request = mock_http_request() - - @mock.patch.object(pkg_resources, "iter_entry_points", - mock_iter_entry_points) - @mock.patch.object(requests, "request", mock_request) - def test_auth_call(): - plugin = auth_plugin.DeprecatedAuthPlugin("fakewithauthurl") - cs = client.Client("2", "username", "password", "project_id", - auth_system="fakewithauthurl", - auth_plugin=plugin) - cs.client.authenticate() - self.assertEqual("http://faked/v2.0", cs.client.auth_url) - - test_auth_call() - - @mock.patch.object(pkg_resources, "iter_entry_points") - def test_client_raises_exc_without_auth_url(self, mock_iter_entry_points): - class MockAuthUrlEntrypoint(pkg_resources.EntryPoint): - def load(self, require=False): - return self.auth_url - - def resolve(self): - return self.auth_url - - def auth_url(self): - return None - - mock_iter_entry_points.side_effect = lambda _t, name: [ - MockAuthUrlEntrypoint("fakewithauthurl", - "fakewithauthurl", - ["auth_url"])] - - plugin = auth_plugin.DeprecatedAuthPlugin("fakewithauthurl") - self.assertRaises( - exceptions.EndpointNotFound, - client.Client, "2", "username", "password", "project_id", - auth_system="fakewithauthurl", auth_plugin=plugin) - - -class AuthPluginTest(utils.TestCase): - @mock.patch.object(requests, "request") - @mock.patch.object(pkg_resources, "iter_entry_points") - def test_auth_system_success(self, mock_iter_entry_points, mock_request): - """Test that we can authenticate using the auth system.""" - class MockEntrypoint(pkg_resources.EntryPoint): - def load(self, require=False): - return FakePlugin - - def resolve(self): - return FakePlugin - - class FakePlugin(auth_plugin.BaseAuthPlugin): - def authenticate(self, cls, auth_url): - cls._authenticate(auth_url, {"fake": "me"}) - - mock_iter_entry_points.side_effect = lambda _t, name=None: [ - MockEntrypoint("fake", "fake", ["FakePlugin"])] - - mock_request.side_effect = mock_http_request() - - auth_plugin.discover_auth_systems() - plugin = auth_plugin.load_plugin("fake") - cs = client.Client("2", "username", "password", "project_id", - utils.AUTH_URL_V2, auth_system="fake", - auth_plugin=plugin) - cs.client.authenticate() - - headers = requested_headers(cs) - token_url = cs.client.auth_url + "/tokens" - - mock_request.assert_called_with( - "POST", - token_url, - headers=headers, - data='{"fake": "me"}', - allow_redirects=True, - **self.TEST_REQUEST_BASE) - - @mock.patch.object(pkg_resources, "iter_entry_points") - def test_discover_auth_system_options(self, mock_iter_entry_points): - """Test that we can load the auth system options.""" - class FakePlugin(auth_plugin.BaseAuthPlugin): - @staticmethod - def add_opts(parser): - parser.add_argument('--auth_system_opt', - default=False, - action='store_true', - help="Fake option") - return parser - - class MockEntrypoint(pkg_resources.EntryPoint): - def load(self, require=False): - return FakePlugin - - def resolve(self): - return FakePlugin - - mock_iter_entry_points.side_effect = lambda _t, name=None: [ - MockEntrypoint("fake", "fake", ["FakePlugin"])] - - parser = argparse.ArgumentParser() - auth_plugin.discover_auth_systems() - auth_plugin.load_auth_system_opts(parser) - opts, args = parser.parse_known_args(['--auth_system_opt']) - - self.assertTrue(opts.auth_system_opt) - - @mock.patch.object(pkg_resources, "iter_entry_points") - def test_parse_auth_system_options(self, mock_iter_entry_points): - """Test that we can parse the auth system options.""" - class MockEntrypoint(pkg_resources.EntryPoint): - def load(self, require=False): - return FakePlugin - - def resolve(self): - return FakePlugin - - class FakePlugin(auth_plugin.BaseAuthPlugin): - def __init__(self): - self.opts = {"fake_argument": True} - - def parse_opts(self, args): - return self.opts - - mock_iter_entry_points.side_effect = lambda _t, name=None: [ - MockEntrypoint("fake", "fake", ["FakePlugin"])] - - auth_plugin.discover_auth_systems() - plugin = auth_plugin.load_plugin("fake") - - plugin.parse_opts([]) - self.assertIn("fake_argument", plugin.opts) - - @mock.patch.object(pkg_resources, "iter_entry_points") - def test_auth_system_defining_url(self, mock_iter_entry_points): - """Test the auth_system defining an url.""" - class MockEntrypoint(pkg_resources.EntryPoint): - def load(self, require=False): - return FakePlugin - - def resolve(self): - return FakePlugin - - class FakePlugin(auth_plugin.BaseAuthPlugin): - def get_auth_url(self): - return "http://faked/v2.0" - - mock_iter_entry_points.side_effect = lambda _t, name=None: [ - MockEntrypoint("fake", "fake", ["FakePlugin"])] - - auth_plugin.discover_auth_systems() - plugin = auth_plugin.load_plugin("fake") - - cs = client.Client("2", "username", "password", "project_id", - auth_system="fakewithauthurl", - auth_plugin=plugin) - self.assertEqual("http://faked/v2.0", cs.client.auth_url) - - @mock.patch.object(pkg_resources, "iter_entry_points") - def test_exception_if_no_authenticate(self, mock_iter_entry_points): - """Test that no authenticate raises a proper exception.""" - class MockEntrypoint(pkg_resources.EntryPoint): - def load(self, require=False): - return FakePlugin - - def resolve(self): - return FakePlugin - - class FakePlugin(auth_plugin.BaseAuthPlugin): - pass - - mock_iter_entry_points.side_effect = lambda _t, name=None: [ - MockEntrypoint("fake", "fake", ["FakePlugin"])] - - auth_plugin.discover_auth_systems() - plugin = auth_plugin.load_plugin("fake") - - self.assertRaises( - exceptions.EndpointNotFound, - client.Client, "2", "username", "password", "project_id", - auth_system="fake", auth_plugin=plugin) - - @mock.patch.object(pkg_resources, "iter_entry_points") - def test_exception_if_no_url(self, mock_iter_entry_points): - """Test that no auth_url at all raises exception.""" - class MockEntrypoint(pkg_resources.EntryPoint): - def load(self, require=False): - return FakePlugin - - def resolve(self): - return FakePlugin - - class FakePlugin(auth_plugin.BaseAuthPlugin): - pass - - mock_iter_entry_points.side_effect = lambda _t, name=None: [ - MockEntrypoint("fake", "fake", ["FakePlugin"])] - - auth_plugin.discover_auth_systems() - plugin = auth_plugin.load_plugin("fake") - - self.assertRaises( - exceptions.EndpointNotFound, - client.Client, "2", "username", "password", "project_id", - auth_system="fake", auth_plugin=plugin) diff --git a/novaclient/tests/unit/test_shell.py b/novaclient/tests/unit/test_shell.py index bfb889c2a..0a74a93dc 100644 --- a/novaclient/tests/unit/test_shell.py +++ b/novaclient/tests/unit/test_shell.py @@ -62,15 +62,7 @@ FAKE_ENV5 = {'OS_USERNAME': 'username', 'OS_PASSWORD': 'password', 'OS_TENANT_NAME': 'tenant_name', - 'OS_AUTH_URL': 'http://no.where/v2.0', - 'OS_COMPUTE_API_VERSION': '2', - 'OS_AUTH_SYSTEM': 'rackspace'} - -FAKE_ENV6 = {'OS_USERNAME': 'username', - 'OS_PASSWORD': 'password', - 'OS_TENANT_NAME': 'tenant_name', - 'OS_AUTH_URL': 'http://no.where/v2.0', - 'OS_AUTH_SYSTEM': 'rackspace'} + 'OS_AUTH_URL': 'http://no.where/v2.0'} def _create_ver_list(versions): @@ -519,9 +511,7 @@ def test_no_tenant_id(self): def test_no_auth_url(self): required = ('You must provide an auth url' - ' via either --os-auth-url or env[OS_AUTH_URL] or' - ' specify an auth_system which defines a default url' - ' with --os-auth-system or env[OS_AUTH_SYSTEM]',) + ' via either --os-auth-url or env[OS_AUTH_URL].',) self.make_env(exclude='OS_AUTH_URL') try: self.shell('list') @@ -687,7 +677,7 @@ def test_keyring_saver_helper(self, mock_client, @mock.patch('novaclient.client.Client') def test_microversion_with_default_behaviour(self, mock_client): - self.make_env(fake_env=FAKE_ENV6) + self.make_env(fake_env=FAKE_ENV5) self.mock_server_version_range.return_value = ( api_versions.APIVersion("2.1"), api_versions.APIVersion("2.3")) self.shell('list') @@ -697,7 +687,7 @@ def test_microversion_with_default_behaviour(self, mock_client): @mock.patch('novaclient.client.Client') def test_microversion_with_default_behaviour_with_legacy_server( self, mock_client): - self.make_env(fake_env=FAKE_ENV6) + self.make_env(fake_env=FAKE_ENV5) self.mock_server_version_range.return_value = ( api_versions.APIVersion(), api_versions.APIVersion()) self.shell('list') @@ -777,15 +767,6 @@ def test_microversion_with_specific_version_without_microversions(self): self.shell, '--os-compute-api-version 2.3 list') - @mock.patch('novaclient.client.Client') - def test_custom_auth_plugin(self, mock_client): - self.make_env(fake_env=FAKE_ENV5) - self.shell('list') - password = mock_client.call_args_list[0][0][2] - client_kwargs = mock_client.call_args_list[0][1] - self.assertEqual(password, 'password') - self.assertIs(client_kwargs['session'], None) - @mock.patch.object(novaclient.shell.OpenStackComputeShell, 'main') def test_main_error_handling(self, mock_compute_shell): class MyException(Exception): diff --git a/novaclient/v2/client.py b/novaclient/v2/client.py index d37ebb627..93c3f4069 100644 --- a/novaclient/v2/client.py +++ b/novaclient/v2/client.py @@ -68,7 +68,7 @@ def __init__(self, username=None, api_key=None, project_id=None, service_type='compute', service_name=None, volume_service_name=None, timings=False, bypass_url=None, os_cache=False, no_cache=True, http_log_debug=False, - auth_system='keystone', auth_plugin=None, auth_token=None, + auth_token=None, cacert=None, tenant_id=None, user_id=None, connection_pool=False, session=None, auth=None, api_version=None, direct_use=True, logger=None, **kwargs): @@ -93,8 +93,6 @@ def __init__(self, username=None, api_key=None, project_id=None, :param bool os_cache: OS cache :param bool no_cache: No cache :param bool http_log_debug: Enable debugging for HTTP connections - :param str auth_system: Auth system - :param str auth_plugin: Auth plugin :param str auth_token: Auth token :param str cacert: cacert :param str tenant_id: Tenant ID @@ -194,8 +192,6 @@ def __init__(self, username=None, api_key=None, project_id=None, auth_token=auth_token, insecure=insecure, timeout=timeout, - auth_system=auth_system, - auth_plugin=auth_plugin, proxy_token=proxy_token, proxy_tenant_id=proxy_tenant_id, region_name=region_name, diff --git a/releasenotes/notes/remove-auth-system-b2cd247b8a312b72.yaml b/releasenotes/notes/remove-auth-system-b2cd247b8a312b72.yaml new file mode 100644 index 000000000..ce3fb4d0c --- /dev/null +++ b/releasenotes/notes/remove-auth-system-b2cd247b8a312b72.yaml @@ -0,0 +1,9 @@ +--- +prelude: > + The ability to use non-Keystone authentication systems has been removed. +upgrade: + - The ``--os-auth-system`` CLI option and ``OS_AUTH_SYSTEM`` environment + variable usage was deprecated in the 3.1.0 release during the Mitaka + development series. This release drops the support for using those options + to load non-Keystone authentication systems via the + ``openstack.client.auth_plugin`` extension point. From 2ba65d00ca84fd471bf1cef43c332a4b11b99ffe Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Sat, 22 Oct 2016 01:27:20 +0000 Subject: [PATCH 1162/1705] Updated from global requirements Change-Id: I24e51d74ca70dfa86c949be8d4341cc2882061ac --- requirements.txt | 2 +- test-requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 85f98d60e..a6863d6c5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ keystoneauth1>=2.14.0 # Apache-2.0 iso8601>=0.1.11 # MIT oslo.i18n>=2.1.0 # Apache-2.0 oslo.serialization>=1.10.0 # Apache-2.0 -oslo.utils>=3.16.0 # Apache-2.0 +oslo.utils>=3.17.0 # Apache-2.0 PrettyTable<0.8,>=0.7.1 # BSD requests>=2.10.0 # Apache-2.0 simplejson>=2.2.0 # MIT diff --git a/test-requirements.txt b/test-requirements.txt index d6a4cdf8a..c0e245867 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -4,7 +4,7 @@ hacking<0.11,>=0.10.0 bandit>=1.1.0 # Apache-2.0 -coverage>=3.6 # Apache-2.0 +coverage>=4.0 # Apache-2.0 fixtures>=3.0.0 # Apache-2.0/BSD keyring>=5.5.1 # MIT/PSF mock>=2.0 # BSD From 8ae2e4c8a1fddc0ae063feae1dde629d353cfbe5 Mon Sep 17 00:00:00 2001 From: Tony Xu Date: Wed, 26 Oct 2016 10:26:27 +0800 Subject: [PATCH 1163/1705] Add Python 3.5 classifier and venv Now that there is a passing gate job, we can claim support for Python 3.5 in the classifier. This patch also adds the convenience py35 venv. Change-Id: Ic80a40692726b05f0f9d174f6ba7e7040f30db38 --- setup.cfg | 1 + tox.ini | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 0ce97cbd2..0e0dd0a9f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -20,6 +20,7 @@ classifier = Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 Programming Language :: Python :: 3.4 + Programming Language :: Python :: 3.5 [files] packages = diff --git a/tox.ini b/tox.ini index 99916ec71..573194eaf 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ # noted to use py34 you need virtualenv >= 1.11.4 [tox] -envlist = py34,py27,pypy,pep8,docs +envlist = py35,py34,py27,pypy,pep8,docs minversion = 1.6 skipsdist = True From c62981fd0a131ef52f60b6b41a66fd6a4cac76ca Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Wed, 2 Nov 2016 15:40:34 +0000 Subject: [PATCH 1164/1705] Updated from global requirements Change-Id: Ifbcf7f937c33be7a3315bf7719681b0c43faf15c --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index c0e245867..9fa36a6f0 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -21,4 +21,4 @@ testtools>=1.4.0 # MIT tempest>=12.1.0 # Apache-2.0 # releasenotes -reno>=1.8.0 # Apache2 +reno>=1.8.0 # Apache-2.0 From 09e82e8a240bfd74aca698ce190ecad06f25d0ad Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Sun, 6 Nov 2016 02:07:09 +0000 Subject: [PATCH 1165/1705] Updated from global requirements Change-Id: Ifc0a312ee6cf7532bd7f3df68b833755bbf01da3 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a6863d6c5..874ed45c0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ keystoneauth1>=2.14.0 # Apache-2.0 iso8601>=0.1.11 # MIT oslo.i18n>=2.1.0 # Apache-2.0 oslo.serialization>=1.10.0 # Apache-2.0 -oslo.utils>=3.17.0 # Apache-2.0 +oslo.utils>=3.18.0 # Apache-2.0 PrettyTable<0.8,>=0.7.1 # BSD requests>=2.10.0 # Apache-2.0 simplejson>=2.2.0 # MIT From f1b62deaedac530eeba7c7d9829e60fbd24b5888 Mon Sep 17 00:00:00 2001 From: Luong Anh Tuan Date: Mon, 7 Nov 2016 09:07:48 +0700 Subject: [PATCH 1166/1705] Replace oslo_utils.timeutils.isotime Function 'oslo_utils.timeutils.isotime()' is deprecated in version '1.6' and will be removed in a future version. We use datetime.datetime.isoformat() instead. For more informations: http://docs.openstack.org/developer/oslo.utils/api/timeutils.html#oslo_utils.timeutils.isotime Change-Id: If41888cfc820c0d081bad67fec28df97c23ddc9d Closes-Bug: #1514331 --- novaclient/tests/functional/v2/legacy/test_servers.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/novaclient/tests/functional/v2/legacy/test_servers.py b/novaclient/tests/functional/v2/legacy/test_servers.py index d55fed21c..01fa95850 100644 --- a/novaclient/tests/functional/v2/legacy/test_servers.py +++ b/novaclient/tests/functional/v2/legacy/test_servers.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import datetime import uuid from novaclient.tests.functional import base @@ -85,12 +86,12 @@ def test_list_with_limit(self): self.assertEqual(1, len(servers), output) def test_list_with_changes_since(self): - now = timeutils.isotime() + now = datetime.datetime.isoformat(timeutils.utcnow()) name = str(uuid.uuid4()) self._create_servers(name, 1) output = self.nova("list", params="--changes-since %s" % now) self.assertIn(name, output, output) - now = timeutils.isotime() + now = datetime.datetime.isoformat(timeutils.utcnow()) output = self.nova("list", params="--changes-since %s" % now) self.assertNotIn(name, output, output) From c5cb80b2789b4650ce47aa890691ad0efdf4abe3 Mon Sep 17 00:00:00 2001 From: Diana Clarke Date: Tue, 8 Nov 2016 13:50:50 -0500 Subject: [PATCH 1167/1705] Bump client microversion to 2.38 Commit 984d00919ffe5ac5d41edb194740f6f33ca3e78f in nova added microversion 2.38: "Return HTTP 400 on list for invalid status". This microversion doesn't otherwise require changes in python-novaclient. Change-Id: Ia1afe22130258b0efc7f869230ce45a43f8dc60e --- novaclient/__init__.py | 2 +- novaclient/tests/unit/v2/test_shell.py | 1 + releasenotes/notes/microversion-v2_38-0618fe2b3c7f96f9.yaml | 5 +++++ 3 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/microversion-v2_38-0618fe2b3c7f96f9.yaml diff --git a/novaclient/__init__.py b/novaclient/__init__.py index f9270766f..99d9f4a74 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.37") +API_MAX_VERSION = api_versions.APIVersion("2.38") diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 5eb19b04f..e71ba0e00 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -3080,6 +3080,7 @@ def test_versions(self): # novaclient 34, # doesn't require any changes in novaclient 37, # There are no versioned wrapped shell method changes for this + 38, # doesn't require any changes in novaclient ]) versions_supported = set(range(0, novaclient.API_MAX_VERSION.ver_minor + 1)) diff --git a/releasenotes/notes/microversion-v2_38-0618fe2b3c7f96f9.yaml b/releasenotes/notes/microversion-v2_38-0618fe2b3c7f96f9.yaml new file mode 100644 index 000000000..3f8bc84c3 --- /dev/null +++ b/releasenotes/notes/microversion-v2_38-0618fe2b3c7f96f9.yaml @@ -0,0 +1,5 @@ +--- +upgrade: + - Support for microversion 2.38 added. As of microversion 2.38, invalid + statuses passed to 'nova list --status invalid_status' will result in a + HTTP 400 Bad Request error response. \ No newline at end of file From 6b32b65a6be0bab34c2e9c9524fb09486663778e Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Wed, 9 Nov 2016 04:24:12 +0000 Subject: [PATCH 1168/1705] Updated from global requirements Change-Id: I31564c0c90a7d0445264fa0af80e657a17a57db3 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 874ed45c0..312ad2490 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ # 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. -pbr>=1.6 # Apache-2.0 +pbr>=1.8 # Apache-2.0 keystoneauth1>=2.14.0 # Apache-2.0 iso8601>=0.1.11 # MIT oslo.i18n>=2.1.0 # Apache-2.0 From b6f63f953e5b89fa1bcaa724ae25c4a90576e2b3 Mon Sep 17 00:00:00 2001 From: jichenjc Date: Wed, 2 Nov 2016 18:15:29 +0800 Subject: [PATCH 1169/1705] Add version pin for image related function image functions deprecated after v2.35 but we didn't pin the version for them, so when use 2.37 by default won't work. Change-Id: I450917f7fcbfe0a3ea7921c82af9863e80cb40a1 Closes-Bug: 1638506 --- novaclient/tests/unit/v2/test_shell.py | 38 ++++++++++++++++++++++- novaclient/v2/shell.py | 43 ++++++++++++++++---------- 2 files changed, 64 insertions(+), 17 deletions(-) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 5eb19b04f..4c6f4987d 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -3285,7 +3285,7 @@ def test_error_state(self, mock_time): silent=False) -class ShellUtilTest(utils.TestCase): +class ShellNetworkUtilTest(utils.TestCase): def test_deprecated_network_newer(self): @novaclient.v2.shell.deprecated_network def tester(cs): @@ -3319,3 +3319,39 @@ def tester(cs): # the deprecated_network decorator will set cs.client.api_version # after calling the wrapped function self.assertEqual(cs.api_version, cs.api_version) + + +class ShellImageUtilTest(utils.TestCase): + def test_deprecated_image_newer(self): + @novaclient.v2.shell.deprecated_image + def tester(cs): + 'foo' + self.assertEqual(api_versions.APIVersion('2.35'), + cs.api_version) + + cs = mock.MagicMock() + cs.api_version = api_versions.APIVersion('2.9999') + tester(cs) + self.assertEqual('DEPRECATED: foo', tester.__doc__) + + def test_deprecated_image_older(self): + @novaclient.v2.shell.deprecated_image + def tester(cs): + 'foo' + # since we didn't need to adjust the api_version the mock won't + # have cs.client.api_version set on it + self.assertFalse(hasattr(cs, 'client')) + # we have to set the attribute back on cs so the decorator can + # set the value on it when we return from this wrapped function + setattr(cs, 'client', mock.MagicMock()) + + cs = mock.MagicMock() + cs.api_version = api_versions.APIVersion('2.1') + # we have to delete the cs.client attribute so hasattr won't return a + # false positive in the wrapped function + del cs.client + tester(cs) + self.assertEqual('DEPRECATED: foo', tester.__doc__) + # the deprecated_network decorator will set cs.client.api_version + # after calling the wrapped function + self.assertEqual(cs.api_version, cs.api_version) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index d61eae398..7bd32ffb4 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -67,25 +67,29 @@ 'tag': 'tag', } +msg_deprecate_net = ('WARNING: Command %s is deprecated and will be removed ' + 'after Nova 15.0.0 is released. Use python-neutronclient ' + 'or openstackclient instead.') + +msg_deprecate_img = ('WARNING: Command %s is deprecated and will be removed ' + 'after Nova 15.0.0 is released. Use python-glanceclient ' + 'or openstackclient instead') + # NOTE(mriedem): Remove this along with the deprecated commands in the first # python-novaclient release AFTER the nova server 15.0.0 'O' release. def emit_image_deprecation_warning(command_name): - print('WARNING: Command %s is deprecated and will be removed after Nova ' - '15.0.0 is released. Use python-glanceclient or openstackclient ' - 'instead.' % command_name, file=sys.stderr) + print(msg_deprecate_img % command_name, file=sys.stderr) -def deprecated_network(fn): +def deprecated_proxy(fn, msg_format): @functools.wraps(fn) def wrapped(cs, *args, **kwargs): command_name = '-'.join(fn.__name__.split('_')[1:]) - print('WARNING: Command %s is deprecated and will be removed ' - 'after Nova 15.0.0 is released. Use python-neutronclient ' - 'or python-openstackclient instead.' % command_name, - file=sys.stderr) - # The network proxy API methods were deprecated in 2.36 and will return - # a 404 so we fallback to 2.35 to maintain a transition for CLI users. + print(msg_format % command_name, file=sys.stderr) + # The network or image proxy API methods were deprecated in 2.36 and + # will return a 404 so we fallback to 2.35 to maintain a transition + # for CLI users. want_version = api_versions.APIVersion('2.35') cur_version = cs.api_version if cs.api_version > want_version: @@ -98,6 +102,13 @@ def wrapped(cs, *args, **kwargs): return wrapped +deprecated_network = functools.partial(deprecated_proxy, + msg_format=msg_deprecate_net) + +deprecated_image = functools.partial(deprecated_proxy, + msg_format=msg_deprecate_img) + + def _key_value_pairing(text): try: (k, v) = text.split('=', 1) @@ -1330,9 +1341,9 @@ def parse_server_name(image): default=[], help=_('Metadata to add/update or delete (only key is necessary on ' 'delete).')) +@deprecated_image def do_image_meta(cs, args): - """DEPRECATED: Set or delete metadata on an image.""" - emit_image_deprecation_warning('image-meta') + """Set or delete metadata on an image.""" image = _find_image(cs, args.image) metadata = _extract_metadata(args) @@ -1394,9 +1405,9 @@ def _print_flavor(flavor): 'image', metavar='', help=_("Name or ID of image.")) +@deprecated_image def do_image_show(cs, args): - """DEPRECATED: Show details about the given image.""" - emit_image_deprecation_warning('image-show') + """Show details about the given image.""" image = _find_image(cs, args.image) _print_image(image) @@ -1404,9 +1415,9 @@ def do_image_show(cs, args): @utils.arg( 'image', metavar='', nargs='+', help=_('Name or ID of image(s).')) +@deprecated_image def do_image_delete(cs, args): - """DEPRECATED: Delete specified image(s).""" - emit_image_deprecation_warning('image-delete') + """Delete specified image(s).""" for image in args.image: try: # _find_image is using the GlanceManager which doesn't implement From dd2a9b26fab199c5b5bc1cd7abc5439d7f5604b9 Mon Sep 17 00:00:00 2001 From: licanwei Date: Tue, 15 Nov 2016 16:12:39 +0800 Subject: [PATCH 1170/1705] modified the description of service.list The description of service.list is unprecise. The service.list gets a list of services, not to get cpu/memory/hdd info for host. Change-Id: If5994be2f9e67766f7e2a9d786c32f0569b16b3c --- novaclient/v2/services.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/v2/services.py b/novaclient/v2/services.py index 4358be1c3..46563e92e 100644 --- a/novaclient/v2/services.py +++ b/novaclient/v2/services.py @@ -37,7 +37,7 @@ class ServiceManager(base.ManagerWithFind): def list(self, host=None, binary=None): """ - Describes cpu/memory/hdd info for host. + Get a list of services. :param host: destination host name. """ From 5c1efa52737900fda853379ca897377b8c21bde5 Mon Sep 17 00:00:00 2001 From: int32bit Date: Wed, 16 Nov 2016 10:40:43 +0800 Subject: [PATCH 1171/1705] Change fake server id as str to fit real server id type Server id should be always str type not int, but currently in our unit test, we still use int type. It may harmless in current test cases, but **there's a potential danger** in future. For example, we may compare server id in our code, in our test will be 1234 == "1234" which certainly return false but it's not our expected result and yields a false positive. it trouble me a lot to run unit test to solve 'bp show-server-all-tenants'. I re-check many times and completely certain work well but fail to unit test. Honestly I almost couldn't find any advantage to use int type. So I think the id should be change to str in fake servers to fit real server uuid type. Change-Id: I6b27d9c1629656dcc9b18497187c79ca96f6cd67 Closes-Bug: #1642113 --- novaclient/tests/unit/v2/fakes.py | 22 +++++++++++----------- novaclient/tests/unit/v2/test_shell.py | 4 ++-- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 58d5b39ed..ddc96e876 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -379,14 +379,14 @@ def get_limits(self, **kw): def get_servers(self, **kw): return (200, {}, {"servers": [ - {'id': 1234, 'name': 'sample-server'}, - {'id': 5678, 'name': 'sample-server2'} + {'id': '1234', 'name': 'sample-server'}, + {'id': '5678', 'name': 'sample-server2'} ]}) def get_servers_detail(self, **kw): return (200, {}, {"servers": [ { - "id": 1234, + "id": '1234', "name": "sample-server", "image": { "id": FAKE_IMAGE_UUID_2, @@ -427,7 +427,7 @@ def get_servers_detail(self, **kw): "OS-EXT-MOD:some_thing": "mod_some_thing_value", }, { - "id": 5678, + "id": '5678', "name": "sample-server2", "image": { "id": FAKE_IMAGE_UUID_1, @@ -471,7 +471,7 @@ def get_servers_detail(self, **kw): }], }, { - "id": 9012, + "id": '9012', "name": "sample-server3", "image": "", "flavor": { @@ -500,7 +500,7 @@ def get_servers_detail(self, **kw): } }, { - "id": 9013, + "id": '9013', "name": "sample-server4", "flavor": { "id": '80645cf4-6ad3-410a-bbc8-6f3e1e291f51', @@ -551,7 +551,7 @@ def get_servers_1234(self, **kw): def get_servers_1235(self, **kw): r = {'server': self.get_servers_detail()[2]['servers'][0]} - r['server']['id'] = 1235 + r['server']['id'] = '1235' r['server']['status'] = 'error' r['server']['fault'] = {'message': 'something went wrong!'} return (200, {}, r) @@ -1103,7 +1103,7 @@ def get_images_detail(self, **kw): { "id": FAKE_IMAGE_UUID_SNAPSHOT, "name": "My Server Backup", - "serverId": 1234, + "serverId": '1234', "updated": "2010-10-10T12:00:00Z", "created": "2010-08-10T12:00:00Z", "status": "SAVING", @@ -1113,7 +1113,7 @@ def get_images_detail(self, **kw): { "id": FAKE_IMAGE_UUID_SNAP_DEL, "name": "My Server Backup Deleted", - "serverId": 1234, + "serverId": '1234', "updated": "2010-10-10T12:00:00Z", "created": "2010-08-10T12:00:00Z", "status": "DELETED", @@ -1134,7 +1134,7 @@ def get_images_detail(self, **kw): { "id": FAKE_IMAGE_UUID_2, "name": "My Server Backup", - "serverId": 1234, + "serverId": '1234', "updated": "2010-10-10T12:00:00Z", "created": "2010-08-10T12:00:00Z", "status": "SAVING", @@ -2240,7 +2240,7 @@ def get_os_migrations(self, **kw): "dest_compute": "compute2", "dest_host": "1.2.3.4", "dest_node": "node2", - "id": 1234, + "id": '1234', "instance_uuid": "instance_id_123", "new_instance_type_id": 2, "old_instance_type_id": 1, diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 5eb19b04f..c36e22d84 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -984,7 +984,7 @@ def test_boot_with_poll(self, poll_method): ) self.assertEqual(1, poll_method.call_count) poll_method.assert_has_calls( - [mock.call(self.shell.cs.servers.get, 1234, 'building', + [mock.call(self.shell.cs.servers.get, '1234', 'building', ['active'])]) def test_boot_with_poll_to_check_VM_state_error(self): @@ -1618,7 +1618,7 @@ def test_refresh_network(self): self.run_command('refresh-network 1234') self.assert_called('POST', '/os-server-external-events', {'events': [{'name': 'network-changed', - 'server_uuid': 1234}]}) + 'server_uuid': '1234'}]}) def test_set_meta_set(self): self.run_command('meta 1234 set key1=val1 key2=val2') From d93ea9eb75c7534b4d075583dbab859f19c01b9b Mon Sep 17 00:00:00 2001 From: licanwei Date: Fri, 18 Nov 2016 14:41:02 +0800 Subject: [PATCH 1172/1705] Fix the description of hypervisors.list trivial docstring fix http://docs.openstack.org/developer/python-novaclient /ref/v2/hypervisors.html Change-Id: I1b7a3eed1e809fde80dd49fd617931ca4e2fc072 --- novaclient/v2/hypervisors.py | 1 + 1 file changed, 1 insertion(+) diff --git a/novaclient/v2/hypervisors.py b/novaclient/v2/hypervisors.py index 8169b7613..1910a582a 100644 --- a/novaclient/v2/hypervisors.py +++ b/novaclient/v2/hypervisors.py @@ -58,6 +58,7 @@ def list(self, detailed=True): def list(self, detailed=True, marker=None, limit=None): """ Get a list of hypervisors. + :param marker: Begin returning hypervisor that appear later in the keypair list than that represented by this keypair name (optional). From 89a1f7c077b7f3ecdecf6879a45006095b03b087 Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Tue, 22 Nov 2016 17:39:38 +0900 Subject: [PATCH 1173/1705] Remove unused code The 'load_entry_point' method has not been used since Ia649db257c416ca054977812ecb3f1a8044fa584. TrivialFix Change-Id: If477b0bb753c1a8bed2ebb440ac43f55805a0744 --- novaclient/utils.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/novaclient/utils.py b/novaclient/utils.py index e6f3fd48c..a3c4e6f06 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -21,7 +21,6 @@ from oslo_serialization import jsonutils from oslo_utils import encodeutils -import pkg_resources import prettytable import six from six.moves.urllib import parse @@ -413,22 +412,6 @@ def do_action_on_many(action, resources, success_msg, error_msg): raise exceptions.CommandError(error_msg) -def load_entry_point(ep_name, name=None): - """Try to load the entry point ep_name that matches name.""" - for ep in pkg_resources.iter_entry_points(ep_name, name=name): - try: - # FIXME(dhellmann): It would be better to use stevedore - # here, since it abstracts this difference in behavior - # between versions of setuptools, but this seemed like a - # more expedient fix. - if hasattr(ep, 'resolve') and hasattr(ep, 'require'): - return ep.resolve() - else: - return ep.load(require=False) - except (ImportError, pkg_resources.UnknownExtra, AttributeError): - continue - - def is_integer_like(val): """Returns validation of a value as an integer.""" try: From c255efbd977a74c4383c749cdb811479e6db0079 Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Tue, 22 Nov 2016 19:35:54 +0900 Subject: [PATCH 1174/1705] Remove unused test code TrivialFix Change-Id: Ieb6f6e1400de2846ee3c333feb465a2b5e2bc9cf --- novaclient/tests/unit/v2/contrib/fakes.py | 42 -------- novaclient/tests/unit/v2/fakes.py | 118 ---------------------- 2 files changed, 160 deletions(-) diff --git a/novaclient/tests/unit/v2/contrib/fakes.py b/novaclient/tests/unit/v2/contrib/fakes.py index 921d87ebd..04bd341cf 100644 --- a/novaclient/tests/unit/v2/contrib/fakes.py +++ b/novaclient/tests/unit/v2/contrib/fakes.py @@ -94,48 +94,6 @@ def get_os_baremetal_nodes_1(self, **kw): } ) - def post_os_baremetal_nodes(self, **kw): - return ( - 200, FAKE_RESPONSE_HEADERS, { - 'node': { - "id": 1, - "instance_uuid": None, - "cpus": 2, - "local_gb": 10, - "memory_mb": 5, - "pm_address": "2.3.4.5", - "pm_user": "pmuser", - "pm_password": "pmpass", - "prov_mac_address": "aa:bb:cc:dd:ee:ff", - "prov_vlan_id": 1, - "service_host": "somehost", - "terminal_port": 8080, - } - } - ) - - def delete_os_baremetal_nodes_1(self, **kw): - return (202, FAKE_RESPONSE_HEADERS, {}) - - def post_os_baremetal_nodes_1_action(self, **kw): - body = kw['body'] - action = list(body)[0] - if action == "add_interface": - return ( - 200, FAKE_RESPONSE_HEADERS, { - 'interface': { - "id": 2, - "address": "bb:cc:dd:ee:ff:aa", - "datapath_id": 1, - "port_no": 2, - } - } - ) - elif action == "remove_interface": - return (202, FAKE_RESPONSE_HEADERS, {}) - else: - return (500, {}, {}) - def post_os_assisted_volume_snapshots(self, **kw): return (202, FAKE_RESPONSE_HEADERS, {'snapshot': {'id': 'blah', 'volumeId': '1'}}) diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index ddc96e876..e6e1aad43 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -568,11 +568,6 @@ def get_servers_9013(self, **kw): r = {'server': self.get_servers_detail()[2]['servers'][3]} return (200, {}, r) - def put_servers_1234(self, body, **kw): - assert list(body) == ['server'] - fakes.assert_has_keys(body['server'], optional=['name', 'adminPass']) - return (204, {}, body) - def delete_os_server_groups_12345(self, **kw): return (202, {}, None) @@ -585,9 +580,6 @@ def delete_servers_1234(self, **kw): def delete_servers_5678(self, **kw): return (202, {}, None) - def delete_servers_1234_metadata_test_key(self, **kw): - return (204, {}, None) - def delete_servers_1234_metadata_key1(self, **kw): return (204, {}, None) @@ -597,9 +589,6 @@ def delete_servers_1234_metadata_key2(self, **kw): def post_servers_1234_metadata(self, **kw): return (204, {}, {'metadata': {'test_key': 'test_value'}}) - def put_servers_1234_metadata_test_key(self, **kw): - return (200, {}, {'meta': {'test_key': 'test_value'}}) - def get_servers_1234_diagnostics(self, **kw): return (200, {}, {'data': 'Fake diagnostics'}) @@ -1092,12 +1081,6 @@ def put_os_floating_ips_bulk_delete(self, **kw): # # Images # - def get_images(self, **kw): - return (200, {}, {'images': [ - {'id': FAKE_IMAGE_UUID_1, 'name': 'CentOS 5.2'}, - {'id': FAKE_IMAGE_UUID_2, 'name': 'My Server Backup'} - ]}) - def get_images_detail(self, **kw): return (200, {}, {'images': [ { @@ -1536,15 +1519,6 @@ def post_os_security_group_rules(self, body, **kw): self.get_os_security_group_rules()[2]['security_group_rules'][0]} return (202, {}, r) - # - # Security Group Default Rules - # - def get_os_security_group_default_rules(self, **kw): - return (200, {}, {"security_group_default_rules": [ - {'id': 1, 'ip_protocol': 'TCP', 'from_port': 22, - 'to_port': 22, 'cidr': '10.0.0.0/8'} - ]}) - # # Tenant Usage # @@ -2044,95 +2018,6 @@ def post_servers_1234_os_interface(self, **kw): def delete_servers_1234_os_interface_port_id(self, **kw): return (200, {}, None) - # NOTE (vkhomenko): - # Volume responses was taken from: - # https://wiki.openstack.org/wiki/CreateVolumeFromImage - # http://jorgew.github.com/block-storage-api/content/ - # GET_listDetailVolumes_v1__tenantId__volumes_detail_.html - # I suppose they are outdated and should be updated after Cinder released - - def get_volumes_detail(self, **kw): - return (200, FAKE_RESPONSE_HEADERS, {"volumes": [ - { - "display_name": "Work", - "display_description": "volume for work", - "status": "ATTACHED", - "id": "15e59938-07d5-11e1-90e3-e3dffe0c5983", - "created_at": "2011-09-09T00:00:00Z", - "attached": "2011-11-11T00:00:00Z", - "size": 1024, - "attachments": [ - {"id": "3333", - "links": ''}], - "metadata": {}}, - { - "display_name": "Work2", - "display_description": "volume for work2", - "status": "ATTACHED", - "id": "15e59938-07d5-11e1-90e3-ee32ba30feaa", - "created_at": "2011-09-09T00:00:00Z", - "attached": "2011-11-11T00:00:00Z", - "size": 1024, - "attachments": [ - {"id": "2222", - "links": ''}], - "metadata": {}}]}) - - def get_volumes(self, **kw): - return (200, FAKE_RESPONSE_HEADERS, {"volumes": [ - { - "display_name": "Work", - "display_description": "volume for work", - "status": "ATTACHED", - "id": "15e59938-07d5-11e1-90e3-e3dffe0c5983", - "created_at": "2011-09-09T00:00:00Z", - "attached": "2011-11-11T00:00:00Z", - "size": 1024, - "attachments": [ - {"id": "3333", - "links": ''}], - "metadata": {}}, - { - "display_name": "Work2", - "display_description": "volume for work2", - "status": "ATTACHED", - "id": "15e59938-07d5-11e1-90e3-ee32ba30feaa", - "created_at": "2011-09-09T00:00:00Z", - "attached": "2011-11-11T00:00:00Z", - "size": 1024, - "attachments": [ - {"id": "2222", - "links": ''}], - "metadata": {}}]}) - - def get_volumes_15e59938_07d5_11e1_90e3_e3dffe0c5983(self, **kw): - return (200, FAKE_RESPONSE_HEADERS, { - "volume": self.get_volumes_detail()[2]['volumes'][0]}) - - def get_volumes_15e59938_07d5_11e1_90e3_ee32ba30feaa(self, **kw): - return (200, {}, { - "volume": self.get_volumes_detail()[2]['volumes'][1]}) - - def post_volumes(self, **kw): - return (200, FAKE_RESPONSE_HEADERS, {"volume": - {"status": "creating", - "display_name": "vol-007", - "attachments": [(0)], - "availability_zone": "cinder", - "created_at": "2012-08-13T10:57:17.000000", - "display_description": "create volume from image", - "image_id": "f4cf905f-7c58-4d7b-8314-8dd8a2d1d483", - "volume_type": "None", - "metadata": {}, - "id": "5cb239f6-1baf-4fe1-bd78-c852cf00fa39", - "size": 1}}) - - def delete_volumes_15e59938_07d5_11e1_90e3_e3dffe0c5983(self, **kw): - return (200, FAKE_RESPONSE_HEADERS, {}) - - def delete_volumes_15e59938_07d5_11e1_90e3_ee32ba30feaa(self, **kw): - return (200, {}, {}) - def post_servers_1234_os_volume_attachments(self, **kw): return (200, FAKE_RESPONSE_HEADERS, { "volumeAttachment": @@ -2357,9 +2242,6 @@ def delete_servers_1234_tags_tag(self, **kw): def delete_servers_1234_tags(self, **kw): return (204, {}, None) - def get_servers_1234_tags_tag(self, **kw): - return (204, {}, None) - class FakeSessionClient(fakes.FakeClient, client.Client): From 917690ea45b715f1b07bc616e589d2cfa2943ae5 Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Thu, 24 Nov 2016 14:52:18 +0900 Subject: [PATCH 1175/1705] Fix import statement order Fix import statement order to comply with OpenStack Style Guidelines(*1). *1: http://docs.openstack.org/developer/hacking/#import-order-template TrivialFix Change-Id: Ifca8ff3326bf90713a38da34850d5bbf19c5af7b --- novaclient/tests/functional/v2/legacy/test_servers.py | 3 ++- novaclient/tests/unit/v2/contrib/test_baremetal.py | 3 ++- novaclient/tests/unit/v2/fakes.py | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/novaclient/tests/functional/v2/legacy/test_servers.py b/novaclient/tests/functional/v2/legacy/test_servers.py index 01fa95850..474fbb95b 100644 --- a/novaclient/tests/functional/v2/legacy/test_servers.py +++ b/novaclient/tests/functional/v2/legacy/test_servers.py @@ -13,9 +13,10 @@ import datetime import uuid -from novaclient.tests.functional import base from oslo_utils import timeutils +from novaclient.tests.functional import base + class TestServersBootNovaClient(base.ClientTestBase): """Servers boot functional tests.""" diff --git a/novaclient/tests/unit/v2/contrib/test_baremetal.py b/novaclient/tests/unit/v2/contrib/test_baremetal.py index c892acb99..0f4326a87 100644 --- a/novaclient/tests/unit/v2/contrib/test_baremetal.py +++ b/novaclient/tests/unit/v2/contrib/test_baremetal.py @@ -14,9 +14,10 @@ # under the License. -import mock import warnings +import mock + from novaclient import extension from novaclient.tests.unit import utils from novaclient.tests.unit.v2.contrib import fakes diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index e6e1aad43..39775e28c 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -15,10 +15,10 @@ # limitations under the License. import datetime +import re import mock from oslo_utils import strutils -import re import six from six.moves.urllib import parse From 00d5f56a33c41195e5339c02f7fa4e91758b4379 Mon Sep 17 00:00:00 2001 From: Flavio Percoco Date: Thu, 24 Nov 2016 13:07:31 +0100 Subject: [PATCH 1176/1705] Show team and repo badges on README This patch adds the team's and repository's badges to the README file. The motivation behind this is to communicate the project status and features at first glance. For more information about this effort, please read this email thread: http://lists.openstack.org/pipermail/openstack-dev/2016-October/105562.html To see an example of how this would look like check: b'https://gist.github.com/a9926a344e9bc5cc3a3ba3a98b2483b4\n' Change-Id: I4e71d4cbe0c505cf4b4bb0aeefdc161cb805b614 --- README.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.rst b/README.rst index 524cbb109..0be65d77b 100644 --- a/README.rst +++ b/README.rst @@ -1,3 +1,12 @@ +======================== +Team and repository tags +======================== + +.. image:: http://governance.openstack.org/badges/python-novaclient.svg + :target: http://governance.openstack.org/reference/tags/index.html + +.. Change things from this point on + Python bindings to the OpenStack Nova API ========================================= From 83106a4442c2cb2f2617576e2e5539b409130ed3 Mon Sep 17 00:00:00 2001 From: Diana Clarke Date: Mon, 28 Nov 2016 16:31:31 -0500 Subject: [PATCH 1177/1705] Correct copy/paste errors in help The keypair, hypervisor, and flavor CLIs all incorrectly say that they support a special -1 case for the 'limit' parameter. I suspect this was just copied from the servers help text by mistake. Change-Id: I57fb3444d54232f0718424c00c07a28b29473d19 Closes-Bug: #1645489 --- novaclient/v2/shell.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 7bd32ffb4..db72b6bc3 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -868,10 +868,9 @@ def _print_flavor_list(flavors, show_extra_specs=False): metavar='', type=int, default=None, - help=_("Maximum number of flavors to display. If limit == -1, all flavors " - "will be displayed. If limit is bigger than 'osapi_max_limit' " - "option of Nova API, limit 'osapi_max_limit' will be used " - "instead.")) + help=_("Maximum number of flavors to display. If limit is bigger than " + "'osapi_max_limit' option of Nova API, limit 'osapi_max_limit' " + "will be used instead.")) def do_flavor_list(cs, args): """Print a list of available 'flavors' (sizes of servers).""" if args.all: @@ -3269,8 +3268,7 @@ def do_keypair_list(cs, args): metavar='', type=int, default=None, - help=_("Maximum number of keypairs to display. If limit == -1, all " - "keypairs will be displayed. If limit is bigger than " + help=_("Maximum number of keypairs to display. If limit is bigger than " "'osapi_max_limit' option of Nova API, limit 'osapi_max_limit' " "will be used instead.")) def do_keypair_list(cs, args): @@ -4141,8 +4139,7 @@ def do_hypervisor_list(cs, args): metavar='', type=int, default=None, - help=_("Maximum number of hypervisors to display. If limit == -1, all " - "hypervisors will be displayed. If limit is bigger than " + help=_("Maximum number of hypervisors to display. If limit is bigger than " "'osapi_max_limit' option of Nova API, limit 'osapi_max_limit' " "will be used instead.")) def do_hypervisor_list(cs, args): From c7162854f7ddfc1d31a9356ae040f10e9e9bbec3 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Tue, 29 Nov 2016 11:07:37 -0500 Subject: [PATCH 1178/1705] Use upper-constraints when running tox This is basically a copy of the template used in oslo.messaging under change 78f113780510b741bc974c69eb9b0718cd657c1d. Change-Id: I8be883215f27abb58d15b85e8542cbdf32000bac --- tools/tox_install.sh | 30 ++++++++++++++++++++++++++++++ tox.ini | 13 +++++++++---- 2 files changed, 39 insertions(+), 4 deletions(-) create mode 100755 tools/tox_install.sh diff --git a/tools/tox_install.sh b/tools/tox_install.sh new file mode 100755 index 000000000..43468e450 --- /dev/null +++ b/tools/tox_install.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +# Client constraint file contains this client version pin that is in conflict +# with installing the client from source. We should remove the version pin in +# the constraints file before applying it for from-source installation. + +CONSTRAINTS_FILE=$1 +shift 1 + +set -e + +# NOTE(tonyb): Place this in the tox enviroment's log dir so it will get +# published to logs.openstack.org for easy debugging. +localfile="$VIRTUAL_ENV/log/upper-constraints.txt" + +if [[ $CONSTRAINTS_FILE != http* ]]; then + CONSTRAINTS_FILE=file://$CONSTRAINTS_FILE +fi +# NOTE(tonyb): need to add curl to bindep.txt if the project supports bindep +curl $CONSTRAINTS_FILE --insecure --progress-bar --output $localfile + +pip install -c$localfile openstack-requirements + +# This is the main purpose of the script: Allow local installation of +# the current repo. It is listed in constraints file and thus any +# install will be constrained and we need to unconstrain it. +edit-constraints $localfile -- $CLIENT_NAME + +pip install -c$localfile -U $* +exit $? diff --git a/tox.ini b/tox.ini index 573194eaf..8add2572d 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ # noted to use py34 you need virtualenv >= 1.11.4 [tox] envlist = py35,py34,py27,pypy,pep8,docs -minversion = 1.6 +minversion = 2.0 skipsdist = True [testenv] @@ -9,11 +9,14 @@ usedevelop = True # tox is silly... these need to be separated by a newline.... whitelist_externals = find bash -install_command = pip install -U {opts} {packages} +passenv = ZUUL_CACHE_DIR + REQUIREMENTS_PIP_LOCATION +install_command = {toxinidir}/tools/tox_install.sh {env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} {opts} {packages} setenv = VIRTUAL_ENV={envdir} + BRANCH_NAME=master + CLIENT_NAME=python-novaclient -deps = -r{toxinidir}/requirements.txt - -r{toxinidir}/test-requirements.txt +deps = -r{toxinidir}/test-requirements.txt commands = find . -type f -name "*.pyc" -delete bash tools/pretty_tox.sh '{posargs}' @@ -41,6 +44,7 @@ commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasen basepython = python2.7 passenv = OS_NOVACLIENT_TEST_NETWORK setenv = + {[testenv]setenv} OS_TEST_PATH = ./novaclient/tests/functional commands = bash tools/pretty_tox.sh '--concurrency=1 {posargs}' @@ -48,6 +52,7 @@ commands = bash tools/pretty_tox.sh '--concurrency=1 {posargs}' basepython = python3.4 passenv = OS_NOVACLIENT_TEST_NETWORK setenv = + {[testenv]setenv} OS_TEST_PATH = ./novaclient/tests/functional commands = bash tools/pretty_tox.sh '--concurrency=1 {posargs}' From f834711d2f4a6052a054ffc79918f615e400bdbe Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Thu, 24 Nov 2016 19:59:13 +0200 Subject: [PATCH 1179/1705] Move all extensions from contrib dir All extensions from novaclient.v2.contrib should be not be extensions in case of api version >=2.0;<=3.0 (historically, they are turned on by default for cli layer), so let's move it from contrib dir and turn on by default. Change-Id: I4ef4e44cf970947dad33110ce658a133e4f2893e --- novaclient/client.py | 21 +- novaclient/tests/unit/v2/contrib/fakes.py | 114 ------ .../tests/unit/v2/contrib/test_baremetal.py | 6 +- .../unit/v2/contrib/test_tenant_networks.py | 6 +- novaclient/tests/unit/v2/fakes.py | 99 ++++- .../test_assisted_volume_snapshots.py | 11 +- novaclient/tests/unit/v2/test_auth.py | 50 ++- .../tests/unit/v2/{contrib => }/test_cells.py | 11 +- novaclient/tests/unit/v2/test_client.py | 3 + .../v2/{contrib => }/test_instance_actions.py | 11 +- .../v2/{contrib => }/test_list_extensions.py | 9 +- .../unit/v2/{contrib => }/test_migrations.py | 13 +- .../test_server_external_events.py | 11 +- novaclient/tests/unit/v2/test_shell.py | 7 +- novaclient/v2/assisted_volume_snapshots.py | 54 +++ novaclient/v2/cells.py | 44 +++ novaclient/v2/client.py | 35 +- novaclient/v2/contrib/__init__.py | 51 +++ .../v2/contrib/assisted_volume_snapshots.py | 40 +- novaclient/v2/contrib/cells.py | 60 +-- novaclient/v2/contrib/deferred_delete.py | 14 +- novaclient/v2/contrib/host_evacuate.py | 71 +--- novaclient/v2/contrib/host_evacuate_live.py | 85 +---- novaclient/v2/contrib/host_servers_migrate.py | 38 +- novaclient/v2/contrib/instance_action.py | 79 +--- novaclient/v2/contrib/list_extensions.py | 33 +- novaclient/v2/contrib/metadata_extensions.py | 32 +- novaclient/v2/contrib/migrations.py | 85 +---- .../v2/contrib/server_external_events.py | 26 +- novaclient/v2/instance_action.py | 40 ++ novaclient/v2/list_extensions.py | 36 ++ novaclient/v2/migrations.py | 51 +++ novaclient/v2/server_external_events.py | 39 ++ novaclient/v2/shell.py | 357 ++++++++++++++++++ ...e_contrib_extensions-0ec70c070b09eedb.yaml | 21 ++ 35 files changed, 916 insertions(+), 747 deletions(-) delete mode 100644 novaclient/tests/unit/v2/contrib/fakes.py rename novaclient/tests/unit/v2/{contrib => }/test_assisted_volume_snapshots.py (78%) rename novaclient/tests/unit/v2/{contrib => }/test_cells.py (82%) rename novaclient/tests/unit/v2/{contrib => }/test_instance_actions.py (80%) rename novaclient/tests/unit/v2/{contrib => }/test_list_extensions.py (75%) rename novaclient/tests/unit/v2/{contrib => }/test_migrations.py (79%) rename novaclient/tests/unit/v2/{contrib => }/test_server_external_events.py (80%) create mode 100644 novaclient/v2/assisted_volume_snapshots.py create mode 100644 novaclient/v2/cells.py create mode 100644 novaclient/v2/instance_action.py create mode 100644 novaclient/v2/list_extensions.py create mode 100644 novaclient/v2/migrations.py create mode 100644 novaclient/v2/server_external_events.py create mode 100644 releasenotes/notes/deprecate_contrib_extensions-0ec70c070b09eedb.yaml diff --git a/novaclient/client.py b/novaclient/client.py index f9fecaa4f..79f36342f 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -22,12 +22,9 @@ import copy import functools -import glob import hashlib -import imp import itertools import logging -import os import pkgutil import re import warnings @@ -772,18 +769,14 @@ def _discover_via_python_path(): def _discover_via_contrib_path(version): - module_path = os.path.dirname(os.path.abspath(__file__)) - ext_path = os.path.join(module_path, "v%s" % version.ver_major, 'contrib') - ext_glob = os.path.join(ext_path, "*.py") + if version.ver_major == 2: + modules = {"baremetal": "novaclient.v2.contrib.baremetal", + "tenant_networks": "novaclient.v2.contrib.tenant_networks"} - for ext_path in glob.iglob(ext_glob): - name = os.path.basename(ext_path)[:-3] - - if name in extensions_ignored_name: - continue - - module = imp.load_source(name, ext_path) - yield name, module + for name, module_name in modules.items(): + module_loader = pkgutil.get_loader(module_name) + module = module_loader.load_module(module_name) + yield name, module def _discover_via_entry_points(): diff --git a/novaclient/tests/unit/v2/contrib/fakes.py b/novaclient/tests/unit/v2/contrib/fakes.py deleted file mode 100644 index 04bd341cf..000000000 --- a/novaclient/tests/unit/v2/contrib/fakes.py +++ /dev/null @@ -1,114 +0,0 @@ -# Copyright 2012 OpenStack Foundation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from novaclient.tests.unit.v2 import fakes -from novaclient.v2 import client - -FAKE_REQUEST_ID_LIST = fakes.FAKE_REQUEST_ID_LIST -FAKE_RESPONSE_HEADERS = fakes.FAKE_RESPONSE_HEADERS - - -class FakeClient(fakes.FakeClient): - def __init__(self, *args, **kwargs): - client.Client.__init__(self, 'username', 'password', - 'project_id', 'auth_url', - extensions=kwargs.get('extensions'), - direct_use=False) - self.client = FakeHTTPClient(**kwargs) - - -class FakeHTTPClient(fakes.FakeHTTPClient): - def get_os_tenant_networks(self): - return (200, FAKE_RESPONSE_HEADERS, { - 'networks': [{"label": "1", "cidr": "10.0.0.0/24", - 'project_id': '4ffc664c198e435e9853f2538fbcd7a7', - 'id': '1'}]}) - - def get_os_tenant_networks_1(self, **kw): - return (200, FAKE_RESPONSE_HEADERS, { - 'network': {"label": "1", "cidr": "10.0.0.0/24", - 'project_id': '4ffc664c198e435e9853f2538fbcd7a7', - 'id': '1'}}) - - def post_os_tenant_networks(self, **kw): - return (201, FAKE_RESPONSE_HEADERS, { - 'network': {"label": "1", "cidr": "10.0.0.0/24", - 'project_id': '4ffc664c198e435e9853f2538fbcd7a7', - 'id': '1'}}) - - def delete_os_tenant_networks_1(self, **kw): - return (204, FAKE_RESPONSE_HEADERS, None) - - def get_os_baremetal_nodes(self, **kw): - return ( - 200, FAKE_RESPONSE_HEADERS, { - 'nodes': [ - { - "id": 1, - "instance_uuid": None, - "interfaces": [], - "cpus": 2, - "local_gb": 10, - "memory_mb": 5, - "pm_address": "2.3.4.5", - "pm_user": "pmuser", - "pm_password": "pmpass", - "prov_mac_address": "aa:bb:cc:dd:ee:ff", - "prov_vlan_id": 1, - "service_host": "somehost", - "terminal_port": 8080, - } - ] - } - ) - - def get_os_baremetal_nodes_1(self, **kw): - return ( - 200, FAKE_RESPONSE_HEADERS, { - 'node': { - "id": 1, - "instance_uuid": None, - "pm_address": "1.2.3.4", - "interfaces": [], - "cpus": 2, - "local_gb": 10, - "memory_mb": 5, - "pm_user": "pmuser", - "pm_password": "pmpass", - "prov_mac_address": "aa:bb:cc:dd:ee:ff", - "prov_vlan_id": 1, - "service_host": "somehost", - "terminal_port": 8080, - } - } - ) - - def post_os_assisted_volume_snapshots(self, **kw): - return (202, FAKE_RESPONSE_HEADERS, - {'snapshot': {'id': 'blah', 'volumeId': '1'}}) - - def delete_os_assisted_volume_snapshots_x(self, **kw): - return (202, FAKE_RESPONSE_HEADERS, {}) - - def post_os_server_external_events(self, **kw): - return (200, FAKE_RESPONSE_HEADERS, { - 'events': [ - {'name': 'test-event', - 'status': 'completed', - 'tag': 'tag', - 'server_uuid': 'fake-uuid1'}, - {'name': 'test-event', - 'status': 'completed', - 'tag': 'tag', - 'server_uuid': 'fake-uuid2'}]}) diff --git a/novaclient/tests/unit/v2/contrib/test_baremetal.py b/novaclient/tests/unit/v2/contrib/test_baremetal.py index 0f4326a87..512245402 100644 --- a/novaclient/tests/unit/v2/contrib/test_baremetal.py +++ b/novaclient/tests/unit/v2/contrib/test_baremetal.py @@ -18,9 +18,10 @@ import mock +from novaclient import api_versions from novaclient import extension from novaclient.tests.unit import utils -from novaclient.tests.unit.v2.contrib import fakes +from novaclient.tests.unit.v2 import fakes from novaclient.v2.contrib import baremetal @@ -31,7 +32,8 @@ def setUp(self): extensions = [ extension.Extension(baremetal.__name__.split(".")[-1], baremetal), ] - self.cs = fakes.FakeClient(extensions=extensions) + self.cs = fakes.FakeClient(api_versions.APIVersion("2.0"), + extensions=extensions) def test_list_nodes(self, mock_warn): nl = self.cs.baremetal.list() diff --git a/novaclient/tests/unit/v2/contrib/test_tenant_networks.py b/novaclient/tests/unit/v2/contrib/test_tenant_networks.py index e19942729..99e0b2704 100644 --- a/novaclient/tests/unit/v2/contrib/test_tenant_networks.py +++ b/novaclient/tests/unit/v2/contrib/test_tenant_networks.py @@ -13,9 +13,10 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient import api_versions from novaclient import extension from novaclient.tests.unit import utils -from novaclient.tests.unit.v2.contrib import fakes +from novaclient.tests.unit.v2 import fakes from novaclient.v2.contrib import tenant_networks @@ -27,7 +28,8 @@ def setUp(self): extension.Extension(tenant_networks.__name__.split(".")[-1], tenant_networks), ] - self.cs = fakes.FakeClient(extensions=extensions) + self.cs = fakes.FakeClient(api_versions.APIVersion("2.0"), + extensions=extensions) def test_list_tenant_networks(self): nets = self.cs.tenant_networks.list() diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 39775e28c..646cc0961 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -60,9 +60,8 @@ def __init__(self, api_version, *args, **kwargs): client.Client.__init__(self, 'username', 'password', 'project_id', 'auth_url', extensions=kwargs.get('extensions'), - direct_use=False) - kwargs["api_version"] = api_version - self.client = FakeHTTPClient(**kwargs) + direct_use=False, api_version=api_version) + self.client = FakeHTTPClient(api_version=api_version, **kwargs) class FakeHTTPClient(base_client.HTTPClient): @@ -2142,11 +2141,6 @@ def get_os_migrations(self, **kw): return (200, FAKE_RESPONSE_HEADERS, migrations) - def post_os_server_external_events(self, **kw): - return (200, {}, {'events': [ - {'name': 'network-changed', - 'server_uuid': '1234'}]}) - # # Server Groups # @@ -2242,6 +2236,90 @@ def delete_servers_1234_tags_tag(self, **kw): def delete_servers_1234_tags(self, **kw): return (204, {}, None) + def get_os_tenant_networks(self): + return (200, FAKE_RESPONSE_HEADERS, { + 'networks': [{"label": "1", "cidr": "10.0.0.0/24", + 'project_id': '4ffc664c198e435e9853f2538fbcd7a7', + 'id': '1'}]}) + + def get_os_tenant_networks_1(self, **kw): + return (200, FAKE_RESPONSE_HEADERS, { + 'network': {"label": "1", "cidr": "10.0.0.0/24", + 'project_id': '4ffc664c198e435e9853f2538fbcd7a7', + 'id': '1'}}) + + def post_os_tenant_networks(self, **kw): + return (201, FAKE_RESPONSE_HEADERS, { + 'network': {"label": "1", "cidr": "10.0.0.0/24", + 'project_id': '4ffc664c198e435e9853f2538fbcd7a7', + 'id': '1'}}) + + def delete_os_tenant_networks_1(self, **kw): + return (204, FAKE_RESPONSE_HEADERS, None) + + def get_os_baremetal_nodes(self, **kw): + return ( + 200, FAKE_RESPONSE_HEADERS, { + 'nodes': [ + { + "id": 1, + "instance_uuid": None, + "interfaces": [], + "cpus": 2, + "local_gb": 10, + "memory_mb": 5, + "pm_address": "2.3.4.5", + "pm_user": "pmuser", + "pm_password": "pmpass", + "prov_mac_address": "aa:bb:cc:dd:ee:ff", + "prov_vlan_id": 1, + "service_host": "somehost", + "terminal_port": 8080, + } + ] + } + ) + + def get_os_baremetal_nodes_1(self, **kw): + return ( + 200, FAKE_RESPONSE_HEADERS, { + 'node': { + "id": 1, + "instance_uuid": None, + "pm_address": "1.2.3.4", + "interfaces": [], + "cpus": 2, + "local_gb": 10, + "memory_mb": 5, + "pm_user": "pmuser", + "pm_password": "pmpass", + "prov_mac_address": "aa:bb:cc:dd:ee:ff", + "prov_vlan_id": 1, + "service_host": "somehost", + "terminal_port": 8080, + } + } + ) + + def post_os_assisted_volume_snapshots(self, **kw): + return (202, FAKE_RESPONSE_HEADERS, + {'snapshot': {'id': 'blah', 'volumeId': '1'}}) + + def delete_os_assisted_volume_snapshots_x(self, **kw): + return (202, FAKE_RESPONSE_HEADERS, {}) + + def post_os_server_external_events(self, **kw): + return (200, FAKE_RESPONSE_HEADERS, { + 'events': [ + {'name': 'test-event', + 'status': 'completed', + 'tag': 'tag', + 'server_uuid': 'fake-uuid1'}, + {'name': 'test-event', + 'status': 'completed', + 'tag': 'tag', + 'server_uuid': 'fake-uuid2'}]}) + class FakeSessionClient(fakes.FakeClient, client.Client): @@ -2249,9 +2327,8 @@ def __init__(self, api_version, *args, **kwargs): client.Client.__init__(self, 'username', 'password', 'project_id', 'auth_url', extensions=kwargs.get('extensions'), - direct_use=False) - kwargs["api_version"] = api_version - self.client = FakeSessionMockClient(**kwargs) + direct_use=False, api_version=api_version) + self.client = FakeSessionMockClient(api_version=api_version, **kwargs) class FakeSessionMockClient(base_client.SessionClient, FakeHTTPClient): diff --git a/novaclient/tests/unit/v2/contrib/test_assisted_volume_snapshots.py b/novaclient/tests/unit/v2/test_assisted_volume_snapshots.py similarity index 78% rename from novaclient/tests/unit/v2/contrib/test_assisted_volume_snapshots.py rename to novaclient/tests/unit/v2/test_assisted_volume_snapshots.py index 374d2d847..8fa4cb598 100644 --- a/novaclient/tests/unit/v2/contrib/test_assisted_volume_snapshots.py +++ b/novaclient/tests/unit/v2/test_assisted_volume_snapshots.py @@ -16,20 +16,15 @@ Assisted volume snapshots - to be used by Cinder and not end users. """ -from novaclient import extension +from novaclient import api_versions from novaclient.tests.unit import utils -from novaclient.tests.unit.v2.contrib import fakes -from novaclient.v2.contrib import assisted_volume_snapshots as assisted_snaps +from novaclient.tests.unit.v2 import fakes class AssistedVolumeSnapshotsTestCase(utils.TestCase): def setUp(self): super(AssistedVolumeSnapshotsTestCase, self).setUp() - extensions = [ - extension.Extension(assisted_snaps.__name__.split(".")[-1], - assisted_snaps), - ] - self.cs = fakes.FakeClient(extensions=extensions) + self.cs = fakes.FakeClient(api_versions.APIVersion("2.1")) def test_create_snap(self): vs = self.cs.assisted_volume_snapshots.create('1', {}) diff --git a/novaclient/tests/unit/v2/test_auth.py b/novaclient/tests/unit/v2/test_auth.py index 70d9467a3..1fdbf3ad2 100644 --- a/novaclient/tests/unit/v2/test_auth.py +++ b/novaclient/tests/unit/v2/test_auth.py @@ -18,9 +18,13 @@ import mock import requests +from novaclient import client from novaclient import exceptions from novaclient.tests.unit import utils -from novaclient.v2 import client + + +def Client(*args, **kwargs): + return client.Client("2", *args, **kwargs) class AuthenticateAgainstKeystoneTests(utils.TestCase): @@ -35,9 +39,8 @@ def get_token(self, **kwargs): return resp def test_authenticate_success(self): - cs = client.Client("username", "password", "project_id", - utils.AUTH_URL_V2, service_type='compute', - direct_use=False) + cs = Client("username", "password", "project_id", utils.AUTH_URL_V2, + service_type='compute') resp = self.get_token() auth_response = utils.TestResponse({ @@ -83,8 +86,7 @@ def test_auth_call(): test_auth_call() def test_authenticate_failure(self): - cs = client.Client("username", "password", "project_id", - utils.AUTH_URL_V2, direct_use=False) + cs = Client("username", "password", "project_id", utils.AUTH_URL_V2) resp = {"unauthorized": {"message": "Unauthorized", "code": "401"}} auth_response = utils.TestResponse({ "status_code": 401, @@ -100,9 +102,8 @@ def test_auth_call(): test_auth_call() def test_v1_auth_redirect(self): - cs = client.Client("username", "password", "project_id", - utils.AUTH_URL_V1, service_type='compute', - direct_use=False) + cs = Client("username", "password", "project_id", utils.AUTH_URL_V1, + service_type='compute') dict_correct_response = self.get_token() correct_response = json.dumps(dict_correct_response) dict_responses = [ @@ -166,9 +167,8 @@ def test_auth_call(): test_auth_call() def test_v2_auth_redirect(self): - cs = client.Client("username", "password", "project_id", - utils.AUTH_URL_V2, service_type='compute', - direct_use=False) + cs = Client("username", "password", "project_id", utils.AUTH_URL_V2, + service_type='compute') dict_correct_response = self.get_token() correct_response = json.dumps(dict_correct_response) dict_responses = [ @@ -232,9 +232,8 @@ def test_auth_call(): test_auth_call() def test_ambiguous_endpoints(self): - cs = client.Client("username", "password", "project_id", - utils.AUTH_URL_V2, service_type='compute', - direct_use=False) + cs = Client("username", "password", "project_id", utils.AUTH_URL_V2, + service_type='compute') resp = self.get_token() # duplicate existing service @@ -256,9 +255,8 @@ def test_auth_call(): test_auth_call() def test_authenticate_with_token_success(self): - cs = client.Client("username", None, "project_id", - utils.AUTH_URL_V2, service_type='compute', - direct_use=False) + cs = Client("username", None, "project_id", utils.AUTH_URL_V2, + service_type='compute') cs.client.auth_token = "FAKE_ID" resp = self.get_token(token_id="FAKE_ID") auth_response = utils.TestResponse({ @@ -300,8 +298,7 @@ def test_authenticate_with_token_success(self): self.assertEqual(cs.client.auth_token, token_id) def test_authenticate_with_token_failure(self): - cs = client.Client("username", None, "project_id", utils.AUTH_URL_V2, - direct_use=False) + cs = Client("username", None, "project_id", utils.AUTH_URL_V2) cs.client.auth_token = "FAKE_ID" resp = {"unauthorized": {"message": "Unauthorized", "code": "401"}} auth_response = utils.TestResponse({ @@ -317,8 +314,7 @@ def test_authenticate_with_token_failure(self): class AuthenticationTests(utils.TestCase): def test_authenticate_success(self): - cs = client.Client("username", "password", - "project_id", utils.AUTH_URL, direct_use=False) + cs = Client("username", "password", "project_id", utils.AUTH_URL) management_url = 'https://localhost/v1.1/443470' auth_response = utils.TestResponse({ 'status_code': 204, @@ -353,8 +349,7 @@ def test_auth_call(): test_auth_call() def test_authenticate_failure(self): - cs = client.Client("username", "password", - "project_id", utils.AUTH_URL, direct_use=False) + cs = Client("username", "password", "project_id", utils.AUTH_URL) auth_response = utils.TestResponse({'status_code': 401}) mock_request = mock.Mock(return_value=(auth_response)) @@ -365,8 +360,8 @@ def test_auth_call(): test_auth_call() def test_auth_automatic(self): - cs = client.Client("username", "password", - "project_id", utils.AUTH_URL, direct_use=False) + cs = Client("username", "password", "project_id", utils.AUTH_URL, + direct_use=False) http_client = cs.client http_client.management_url = '' http_client.get_service_url = mock.Mock(return_value='') @@ -382,8 +377,7 @@ def test_auth_call(m): test_auth_call() def test_auth_manual(self): - cs = client.Client("username", "password", - "project_id", utils.AUTH_URL, direct_use=False) + cs = Client("username", "password", "project_id", utils.AUTH_URL) @mock.patch.object(cs.client, 'authenticate') def test_auth_call(m): diff --git a/novaclient/tests/unit/v2/contrib/test_cells.py b/novaclient/tests/unit/v2/test_cells.py similarity index 82% rename from novaclient/tests/unit/v2/contrib/test_cells.py rename to novaclient/tests/unit/v2/test_cells.py index 95a5c4b4e..5a47e1c6b 100644 --- a/novaclient/tests/unit/v2/contrib/test_cells.py +++ b/novaclient/tests/unit/v2/test_cells.py @@ -13,20 +13,15 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient import extension +from novaclient import api_versions from novaclient.tests.unit import utils -from novaclient.tests.unit.v2.contrib import fakes -from novaclient.v2.contrib import cells +from novaclient.tests.unit.v2 import fakes class CellsExtensionTests(utils.TestCase): def setUp(self): super(CellsExtensionTests, self).setUp() - extensions = [ - extension.Extension(cells.__name__.split(".")[-1], - cells), - ] - self.cs = fakes.FakeClient(extensions=extensions) + self.cs = fakes.FakeClient(api_versions.APIVersion("2.1")) def test_get_cells(self): cell_name = 'child_cell' diff --git a/novaclient/tests/unit/v2/test_client.py b/novaclient/tests/unit/v2/test_client.py index dc92c9327..94ba44cee 100644 --- a/novaclient/tests/unit/v2/test_client.py +++ b/novaclient/tests/unit/v2/test_client.py @@ -14,6 +14,7 @@ from keystoneauth1 import session +from novaclient import api_versions from novaclient.tests.unit import utils from novaclient.v2 import client @@ -27,6 +28,7 @@ def test_adapter_properties(self): s = session.Session() c = client.Client(session=s, + api_version=api_versions.APIVersion("2.0"), user_agent=user_agent, endpoint_override=endpoint_override, direct_use=False) @@ -40,6 +42,7 @@ def test_passing_interface(self): s = session.Session() c = client.Client(session=s, + api_version=api_versions.APIVersion("2.0"), interface=interface, endpoint_type=endpoint_type, direct_use=False) diff --git a/novaclient/tests/unit/v2/contrib/test_instance_actions.py b/novaclient/tests/unit/v2/test_instance_actions.py similarity index 80% rename from novaclient/tests/unit/v2/contrib/test_instance_actions.py rename to novaclient/tests/unit/v2/test_instance_actions.py index 394b37e40..8eed17197 100644 --- a/novaclient/tests/unit/v2/contrib/test_instance_actions.py +++ b/novaclient/tests/unit/v2/test_instance_actions.py @@ -13,20 +13,15 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient import extension +from novaclient import api_versions from novaclient.tests.unit import utils -from novaclient.tests.unit.v2.contrib import fakes -from novaclient.v2.contrib import instance_action +from novaclient.tests.unit.v2 import fakes class InstanceActionExtensionTests(utils.TestCase): def setUp(self): super(InstanceActionExtensionTests, self).setUp() - extensions = [ - extension.Extension(instance_action.__name__.split(".")[-1], - instance_action), - ] - self.cs = fakes.FakeClient(extensions=extensions) + self.cs = fakes.FakeClient(api_versions.APIVersion("2.1")) def test_list_instance_actions(self): server_uuid = '1234' diff --git a/novaclient/tests/unit/v2/contrib/test_list_extensions.py b/novaclient/tests/unit/v2/test_list_extensions.py similarity index 75% rename from novaclient/tests/unit/v2/contrib/test_list_extensions.py rename to novaclient/tests/unit/v2/test_list_extensions.py index 02fe296ca..f473ed753 100644 --- a/novaclient/tests/unit/v2/contrib/test_list_extensions.py +++ b/novaclient/tests/unit/v2/test_list_extensions.py @@ -12,21 +12,14 @@ # under the License. from novaclient import api_versions -from novaclient import extension from novaclient.tests.unit import utils from novaclient.tests.unit.v2 import fakes -from novaclient.v2.contrib import list_extensions class ListExtensionsTests(utils.TestCase): def setUp(self): super(ListExtensionsTests, self).setUp() - extensions = [ - extension.Extension(list_extensions.__name__.split(".")[-1], - list_extensions), - ] - self.cs = fakes.FakeClient(api_versions.APIVersion("2.0"), - extensions=extensions) + self.cs = fakes.FakeClient(api_versions.APIVersion("2.1")) def test_list_extensions(self): all_exts = self.cs.list_extensions.show_all() diff --git a/novaclient/tests/unit/v2/contrib/test_migrations.py b/novaclient/tests/unit/v2/test_migrations.py similarity index 79% rename from novaclient/tests/unit/v2/contrib/test_migrations.py rename to novaclient/tests/unit/v2/test_migrations.py index 4cf5d45eb..b270f9c3c 100644 --- a/novaclient/tests/unit/v2/contrib/test_migrations.py +++ b/novaclient/tests/unit/v2/test_migrations.py @@ -11,21 +11,15 @@ # under the License. from novaclient import api_versions -from novaclient import extension from novaclient.tests.unit import utils from novaclient.tests.unit.v2 import fakes -from novaclient.v2.contrib import migrations +from novaclient.v2 import migrations class MigrationsTest(utils.TestCase): def setUp(self): super(MigrationsTest, self).setUp() - self.extensions = [ - extension.Extension(migrations.__name__.split(".")[-1], - migrations), - ] - self.cs = fakes.FakeClient(api_versions.APIVersion("2.0"), - extensions=self.extensions) + self.cs = fakes.FakeClient(api_versions.APIVersion("2.1")) def test_list_migrations(self): ml = self.cs.migrations.list() @@ -36,8 +30,7 @@ def test_list_migrations(self): self.assertRaises(AttributeError, getattr, m, "migration_type") def test_list_migrations_v223(self): - cs = fakes.FakeClient(extensions=self.extensions, - api_version=api_versions.APIVersion("2.23")) + cs = fakes.FakeClient(api_versions.APIVersion("2.23")) ml = cs.migrations.list() self.assert_request_id(ml, fakes.FAKE_REQUEST_ID_LIST) cs.assert_called('GET', '/os-migrations') diff --git a/novaclient/tests/unit/v2/contrib/test_server_external_events.py b/novaclient/tests/unit/v2/test_server_external_events.py similarity index 80% rename from novaclient/tests/unit/v2/contrib/test_server_external_events.py rename to novaclient/tests/unit/v2/test_server_external_events.py index 009a0d83a..4b640750c 100644 --- a/novaclient/tests/unit/v2/contrib/test_server_external_events.py +++ b/novaclient/tests/unit/v2/test_server_external_events.py @@ -16,20 +16,15 @@ External event triggering for servers, not to be used by users. """ -from novaclient import extension +from novaclient import api_versions from novaclient.tests.unit import utils -from novaclient.tests.unit.v2.contrib import fakes -from novaclient.v2.contrib import server_external_events as ext_events +from novaclient.tests.unit.v2 import fakes class ServerExternalEventsTestCase(utils.TestCase): def setUp(self): super(ServerExternalEventsTestCase, self).setUp() - extensions = [ - extension.Extension(ext_events.__name__.split(".")[-1], - ext_events), - ] - self.cs = fakes.FakeClient(extensions=extensions) + self.cs = fakes.FakeClient(api_versions.APIVersion("2.1")) def test_external_event(self): events = [{'server_uuid': 'fake-uuid1', diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 9e4494439..1b3af35b6 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -77,8 +77,7 @@ def setUp(self): self.shell = self.useFixture(ShellFixture()).shell self.useFixture(fixtures.MonkeyPatch( - 'novaclient.client.Client', - lambda *args, **kwargs: fakes.FakeClient(*args, **kwargs))) + 'novaclient.client.Client', fakes.FakeClient)) @mock.patch('sys.stdout', new_callable=six.StringIO) @mock.patch('sys.stderr', new_callable=six.StringIO) @@ -3150,9 +3149,9 @@ class ShellWithSessionClientTest(ShellTest): def setUp(self): """Run before each test.""" super(ShellWithSessionClientTest, self).setUp() + self.useFixture(fixtures.MonkeyPatch( - 'novaclient.client.Client', - lambda *args, **kwargs: fakes.FakeSessionClient(*args, **kwargs))) + 'novaclient.client.Client', fakes.FakeSessionClient)) class GetSecgroupTest(utils.TestCase): diff --git a/novaclient/v2/assisted_volume_snapshots.py b/novaclient/v2/assisted_volume_snapshots.py new file mode 100644 index 000000000..79807897d --- /dev/null +++ b/novaclient/v2/assisted_volume_snapshots.py @@ -0,0 +1,54 @@ +# Copyright (C) 2013, Red Hat, Inc. +# +# 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. + +""" +Assisted volume snapshots - to be used by Cinder and not end users. +""" + +import json + +from novaclient import base + + +class Snapshot(base.Resource): + def __repr__(self): + return "" % self.id + + def delete(self): + """ + Delete this snapshot. + + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self.manager.delete(self) + + +class AssistedSnapshotManager(base.Manager): + resource_class = Snapshot + + def create(self, volume_id, create_info): + body = {'snapshot': {'volume_id': volume_id, + 'create_info': create_info}} + return self._create('/os-assisted-volume-snapshots', body, 'snapshot') + + def delete(self, snapshot, delete_info): + """ + Delete a specified assisted volume snapshot. + + :param snapshot: an assisted volume snapshot to delete + :param delete_info: Information for snapshot deletion + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self._delete("/os-assisted-volume-snapshots/%s?delete_info=%s" % + (base.getid(snapshot), json.dumps(delete_info))) diff --git a/novaclient/v2/cells.py b/novaclient/v2/cells.py new file mode 100644 index 000000000..e6a166671 --- /dev/null +++ b/novaclient/v2/cells.py @@ -0,0 +1,44 @@ +# Copyright 2013 Rackspace Hosting +# All Rights Reserved. +# +# 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 novaclient import base + + +class Cell(base.Resource): + def __repr__(self): + return "" % self.name + + +class CellsManager(base.Manager): + resource_class = Cell + + def get(self, cell_name): + """ + Get a cell. + + :param cell_name: Name of the :class:`Cell` to get. + :rtype: :class:`Cell` + """ + return self._get("/os-cells/%s" % cell_name, "cell") + + def capacities(self, cell_name=None): + """ + Get capacities for a cell. + + :param cell_name: Name of the :class:`Cell` to get capacities for. + :rtype: :class:`Cell` + """ + path = ["%s/capacities" % cell_name, "capacities"][cell_name is None] + return self._get("/os-cells/%s" % path, "cell") diff --git a/novaclient/v2/client.py b/novaclient/v2/client.py index 93c3f4069..961e805c0 100644 --- a/novaclient/v2/client.py +++ b/novaclient/v2/client.py @@ -22,9 +22,12 @@ from novaclient.i18n import _LE from novaclient.v2 import agents from novaclient.v2 import aggregates +from novaclient.v2 import assisted_volume_snapshots from novaclient.v2 import availability_zones +from novaclient.v2 import cells from novaclient.v2 import certs from novaclient.v2 import cloudpipe +from novaclient.v2 import contrib from novaclient.v2 import fixed_ips from novaclient.v2 import flavor_access from novaclient.v2 import flavors @@ -36,14 +39,18 @@ from novaclient.v2 import hosts from novaclient.v2 import hypervisors from novaclient.v2 import images +from novaclient.v2 import instance_action from novaclient.v2 import keypairs from novaclient.v2 import limits +from novaclient.v2 import list_extensions +from novaclient.v2 import migrations from novaclient.v2 import networks from novaclient.v2 import quota_classes from novaclient.v2 import quotas from novaclient.v2 import security_group_default_rules from novaclient.v2 import security_group_rules from novaclient.v2 import security_groups +from novaclient.v2 import server_external_events from novaclient.v2 import server_groups from novaclient.v2 import server_migrations from novaclient.v2 import servers @@ -172,16 +179,36 @@ def __init__(self, username=None, api_key=None, project_id=None, self.server_migrations = \ server_migrations.ServerMigrationsManager(self) + # V2.0 extensions: + # NOTE(andreykurilin): baremetal and tenant_networks extensions are + # deprecated now, which is why they are not initialized by default. + self.assisted_volume_snapshots = \ + assisted_volume_snapshots.AssistedSnapshotManager(self) + self.cells = cells.CellsManager(self) + self.instance_action = instance_action.InstanceActionManager(self) + self.list_extensions = list_extensions.ListExtManager(self) + self.migrations = migrations.MigrationManager(self) + self.server_external_events = \ + server_external_events.ServerExternalEventManager(self) + + self.logger = logger or logging.getLogger(__name__) + # Add in any extensions... if extensions: for extension in extensions: + # do not import extensions from contrib directory twice. + if extension.name in contrib.V2_0_EXTENSIONS: + # NOTE(andreykurilin): this message looks more like + # warning or note, but it is not critical, so let's do + # not flood "warning" logging level and use just debug.. + self.logger.debug("Nova 2.0 extenstion '%s' is auto-loaded" + " by default. You do not need to specify" + " it manually.", extension.name) + continue if extension.manager_class: setattr(self, extension.name, extension.manager_class(self)) - if not logger: - logger = logging.getLogger(__name__) - self.client = client._construct_http_client( username=username, password=password, @@ -208,7 +235,7 @@ def __init__(self, username=None, api_key=None, project_id=None, session=session, auth=auth, api_version=api_version, - logger=logger, + logger=self.logger, **kwargs) @property diff --git a/novaclient/v2/contrib/__init__.py b/novaclient/v2/contrib/__init__.py index e69de29bb..b419ae0c2 100644 --- a/novaclient/v2/contrib/__init__.py +++ b/novaclient/v2/contrib/__init__.py @@ -0,0 +1,51 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import inspect +import warnings + +from novaclient.i18n import _LW + +# NOTE(andreykurilin): "baremetal" and "tenant_networks" extensions excluded +# here deliberately. They were deprecated separately from deprecation +# extension mechanism and I prefer to not auto-load them by default +# (V2_0_EXTENSIONS is designed for such behaviour). +V2_0_EXTENSIONS = { + 'assisted_volume_snapshots': + 'novaclient.v2.assisted_volume_snapshots', + 'cells': 'novaclient.v2.cells', + 'instance_action': 'novaclient.v2.instance_action', + 'list_extensions': 'novaclient.v2.list_extensions', + 'migrations': 'novaclient.v2.migrations', + 'server_external_events': 'novaclient.v2.server_external_events', +} + + +def warn(alternative=True): + """Prints warning msg for contrib modules.""" + frm = inspect.stack()[1] + module_name = inspect.getmodule(frm[0]).__name__ + if module_name.startswith("novaclient.v2.contrib."): + msg = (_LW("Module `%s` is deprecated as of OpenStack Ocata") % + module_name) + if alternative: + new_module_name = module_name.replace("contrib.", "") + msg += _LW(" in favor of `%s`") % new_module_name + + msg += (_LW(" and will be removed after OpenStack Pike.")) + + if not alternative: + msg += _LW(" All shell commands were moved to " + "`novaclient.v2.shell` and will be automatically " + "loaded.") + + warnings.warn(msg) diff --git a/novaclient/v2/contrib/assisted_volume_snapshots.py b/novaclient/v2/contrib/assisted_volume_snapshots.py index a6c6a163d..95b5073a2 100644 --- a/novaclient/v2/contrib/assisted_volume_snapshots.py +++ b/novaclient/v2/contrib/assisted_volume_snapshots.py @@ -16,42 +16,14 @@ Assisted volume snapshots - to be used by Cinder and not end users. """ -import json +from novaclient.v2 import assisted_volume_snapshots +from novaclient.v2 import contrib -from novaclient import base - -class Snapshot(base.Resource): - def __repr__(self): - return "" % self.id - - def delete(self): - """ - Delete this snapshot. - - :returns: An instance of novaclient.base.TupleWithMeta - """ - return self.manager.delete(self) - - -class AssistedSnapshotManager(base.Manager): - resource_class = Snapshot - - def create(self, volume_id, create_info): - body = {'snapshot': {'volume_id': volume_id, - 'create_info': create_info}} - return self._create('/os-assisted-volume-snapshots', body, 'snapshot') - - def delete(self, snapshot, delete_info): - """ - Delete a specified assisted volume snapshot. - - :param snapshot: an assisted volume snapshot to delete - :param delete_info: Information for snapshot deletion - :returns: An instance of novaclient.base.TupleWithMeta - """ - return self._delete("/os-assisted-volume-snapshots/%s?delete_info=%s" % - (base.getid(snapshot), json.dumps(delete_info))) +AssistedSnapshotManager = assisted_volume_snapshots.AssistedSnapshotManager +Snapshot = assisted_volume_snapshots.Snapshot manager_class = AssistedSnapshotManager name = 'assisted_volume_snapshots' + +contrib.warn() diff --git a/novaclient/v2/contrib/cells.py b/novaclient/v2/contrib/cells.py index e5de8e5ee..a689d00a9 100644 --- a/novaclient/v2/contrib/cells.py +++ b/novaclient/v2/contrib/cells.py @@ -13,61 +13,11 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient import base -from novaclient.i18n import _ -from novaclient import utils +from novaclient.v2 import cells +from novaclient.v2 import contrib -class Cell(base.Resource): - def __repr__(self): - return "" % self.name +Cell = cells.Cell +CellsManager = cells.CellsManager - -class CellsManager(base.Manager): - resource_class = Cell - - def get(self, cell_name): - """ - Get a cell. - - :param cell_name: Name of the :class:`Cell` to get. - :rtype: :class:`Cell` - """ - return self._get("/os-cells/%s" % cell_name, "cell") - - def capacities(self, cell_name=None): - """ - Get capacities for a cell. - - :param cell_name: Name of the :class:`Cell` to get capacities for. - :rtype: :class:`Cell` - """ - path = ["%s/capacities" % cell_name, "capacities"][cell_name is None] - return self._get("/os-cells/%s" % path, "cell") - - -@utils.arg( - 'cell', - metavar='', - help=_('Name of the cell.')) -def do_cell_show(cs, args): - """Show details of a given cell.""" - cell = cs.cells.get(args.cell) - utils.print_dict(cell._info) - - -@utils.arg( - '--cell', - metavar='', - help=_("Name of the cell to get the capacities."), - default=None) -def do_cell_capacities(cs, args): - """Get cell capacities for all cells or a given cell.""" - cell = cs.cells.capacities(args.cell) - print(_("Ram Available: %s MB") % cell.capacities['ram_free']['total_mb']) - utils.print_dict(cell.capacities['ram_free']['units_by_mb'], - dict_property='Ram(MB)', dict_value="Units") - print(_("\nDisk Available: %s MB") % - cell.capacities['disk_free']['total_mb']) - utils.print_dict(cell.capacities['disk_free']['units_by_mb'], - dict_property='Disk(MB)', dict_value="Units") +contrib.warn() diff --git a/novaclient/v2/contrib/deferred_delete.py b/novaclient/v2/contrib/deferred_delete.py index 3d49104aa..bd5798ac7 100644 --- a/novaclient/v2/contrib/deferred_delete.py +++ b/novaclient/v2/contrib/deferred_delete.py @@ -12,16 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -from novaclient import utils +from novaclient.v2 import contrib - -@utils.arg('server', metavar='', help='Name or ID of server.') -def do_force_delete(cs, args): - """Force delete a server.""" - utils.find_resource(cs.servers, args.server).force_delete() - - -@utils.arg('server', metavar='', help='Name or ID of server.') -def do_restore(cs, args): - """Restore a soft-deleted server.""" - utils.find_resource(cs.servers, args.server, deleted=True).restore() +contrib.warn(alternative=False) diff --git a/novaclient/v2/contrib/host_evacuate.py b/novaclient/v2/contrib/host_evacuate.py index 22055656b..b5305bcb7 100644 --- a/novaclient/v2/contrib/host_evacuate.py +++ b/novaclient/v2/contrib/host_evacuate.py @@ -13,73 +13,10 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient import api_versions -from novaclient import base -from novaclient.i18n import _ -from novaclient import utils +from novaclient.v2 import contrib +from novaclient.v2 import shell -class EvacuateHostResponse(base.Resource): - pass +EvacuateHostResponse = shell.EvacuateHostResponse - -def _server_evacuate(cs, server, args): - success = True - error_message = "" - try: - if api_versions.APIVersion("2.29") <= cs.api_version: - # if microversion >= 2.29 - force = getattr(args, 'force', None) - cs.servers.evacuate(server=server['uuid'], host=args.target_host, - force=force) - elif api_versions.APIVersion("2.14") <= cs.api_version: - # if microversion 2.14 - 2.28 - cs.servers.evacuate(server=server['uuid'], host=args.target_host) - else: - # else microversion 2.0 - 2.13 - on_shared_storage = getattr(args, 'on_shared_storage', None) - cs.servers.evacuate(server=server['uuid'], - host=args.target_host, - on_shared_storage=on_shared_storage) - except Exception as e: - success = False - error_message = _("Error while evacuating instance: %s") % e - return EvacuateHostResponse(base.Manager, - {"server_uuid": server['uuid'], - "evacuate_accepted": success, - "error_message": error_message}) - - -@utils.arg('host', metavar='', help='Name of host.') -@utils.arg( - '--target_host', - metavar='', - default=None, - help=_('Name of target host. If no host is specified the scheduler will ' - 'select a target.')) -@utils.arg( - '--on-shared-storage', - dest='on_shared_storage', - action="store_true", - default=False, - help=_('Specifies whether all instances files are on shared storage'), - start_version='2.0', - end_version='2.13') -@utils.arg( - '--force', - dest='force', - action='store_true', - default=False, - help=_('Force to not verify the scheduler if a host is provided.'), - start_version='2.29') -def do_host_evacuate(cs, args): - """Evacuate all instances from failed host.""" - hypervisors = cs.hypervisors.search(args.host, servers=True) - response = [] - for hyper in hypervisors: - if hasattr(hyper, 'servers'): - for server in hyper.servers: - response.append(_server_evacuate(cs, server, args)) - - utils.print_list(response, - ["Server UUID", "Evacuate Accepted", "Error Message"]) +contrib.warn(alternative=False) diff --git a/novaclient/v2/contrib/host_evacuate_live.py b/novaclient/v2/contrib/host_evacuate_live.py index f55dda1a4..7e27d7359 100644 --- a/novaclient/v2/contrib/host_evacuate_live.py +++ b/novaclient/v2/contrib/host_evacuate_live.py @@ -13,87 +13,6 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.i18n import _ -from novaclient import utils +from novaclient.v2 import contrib - -def _server_live_migrate(cs, server, args): - class HostEvacuateLiveResponse(object): - def __init__(self, server_uuid, live_migration_accepted, - error_message): - self.server_uuid = server_uuid - self.live_migration_accepted = live_migration_accepted - self.error_message = error_message - success = True - error_message = "" - update_kwargs = {} - try: - # API >= 2.30 - if 'force' in args and args.force: - update_kwargs['force'] = args.force - # API 2.0->2.24 - if 'disk_over_commit' in args: - update_kwargs['disk_over_commit'] = args.disk_over_commit - cs.servers.live_migrate(server['uuid'], args.target_host, - args.block_migrate, **update_kwargs) - except Exception as e: - success = False - error_message = _("Error while live migrating instance: %s") % e - return HostEvacuateLiveResponse(server['uuid'], - success, - error_message) - - -@utils.arg('host', metavar='', help='Name of host.') -@utils.arg( - '--target-host', - metavar='', - default=None, - help=_('Name of target host.')) -@utils.arg( - '--block-migrate', - action='store_true', - default=False, - help=_('Enable block migration. (Default=False)'), - start_version="2.0", end_version="2.24") -@utils.arg( - '--block-migrate', - action='store_true', - default="auto", - help=_('Enable block migration. (Default=auto)'), - start_version="2.25") -@utils.arg( - '--disk-over-commit', - action='store_true', - default=False, - help=_('Enable disk overcommit.'), - start_version="2.0", end_version="2.24") -@utils.arg( - '--max-servers', - type=int, - dest='max_servers', - metavar='', - help='Maximum number of servers to live migrate simultaneously') -@utils.arg( - '--force', - dest='force', - action='store_true', - default=False, - help=_('Force to not verify the scheduler if a host is provided.'), - start_version='2.30') -def do_host_evacuate_live(cs, args): - """Live migrate all instances of the specified host - to other available hosts. - """ - hypervisors = cs.hypervisors.search(args.host, servers=True) - response = [] - migrating = 0 - for hyper in hypervisors: - for server in getattr(hyper, 'servers', []): - response.append(_server_live_migrate(cs, server, args)) - migrating = migrating + 1 - if args.max_servers is not None and migrating >= args.max_servers: - break - - utils.print_list(response, ["Server UUID", "Live Migration Accepted", - "Error Message"]) +contrib.warn(alternative=False) diff --git a/novaclient/v2/contrib/host_servers_migrate.py b/novaclient/v2/contrib/host_servers_migrate.py index 96e606526..e88da876b 100644 --- a/novaclient/v2/contrib/host_servers_migrate.py +++ b/novaclient/v2/contrib/host_servers_migrate.py @@ -13,40 +13,10 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient import base -from novaclient.i18n import _ -from novaclient import utils +from novaclient.v2 import contrib +from novaclient.v2 import shell -class HostServersMigrateResponse(base.Resource): - pass +HostServersMigrateResponse = shell.HostServersMigrateResponse - -def _server_migrate(cs, server): - success = True - error_message = "" - try: - cs.servers.migrate(server['uuid']) - except Exception as e: - success = False - error_message = _("Error while migrating instance: %s") % e - return HostServersMigrateResponse(base.Manager, - {"server_uuid": server['uuid'], - "migration_accepted": success, - "error_message": error_message}) - - -@utils.arg('host', metavar='', help='Name of host.') -def do_host_servers_migrate(cs, args): - """Cold migrate all instances off the specified host to other available - hosts. - """ - hypervisors = cs.hypervisors.search(args.host, servers=True) - response = [] - for hyper in hypervisors: - if hasattr(hyper, 'servers'): - for server in hyper.servers: - response.append(_server_migrate(cs, server)) - - utils.print_list(response, - ["Server UUID", "Migration Accepted", "Error Message"]) +contrib.warn(alternative=False) diff --git a/novaclient/v2/contrib/instance_action.py b/novaclient/v2/contrib/instance_action.py index 05b847c2b..a90b99bd5 100644 --- a/novaclient/v2/contrib/instance_action.py +++ b/novaclient/v2/contrib/instance_action.py @@ -13,81 +13,10 @@ # License for the specific language governing permissions and limitations # under the License. -import pprint +from novaclient.v2 import contrib +from novaclient.v2 import instance_action -from novaclient import api_versions -from novaclient import base -from novaclient.i18n import _ -from novaclient import utils -from novaclient.v2 import shell +InstanceActionManager = instance_action.InstanceActionManager -class InstanceActionManager(base.ManagerWithFind): - resource_class = base.Resource - - def get(self, server, request_id): - """ - Get details of an action performed on an instance. - - :param request_id: The request_id of the action to get. - """ - return self._get("/servers/%s/os-instance-actions/%s" % - (base.getid(server), request_id), 'instanceAction') - - def list(self, server): - """ - Get a list of actions performed on a server. - """ - return self._list('/servers/%s/os-instance-actions' % - base.getid(server), 'instanceActions') - - -@utils.arg( - 'server', - metavar='', - help=_('Name or UUID of the server to show actions for.'), - start_version="2.0", end_version="2.20") -@utils.arg( - 'server', - metavar='', - help=_('Name or UUID of the server to show actions for. Only UUID can be ' - 'used to show actions for a deleted server.'), - start_version="2.21") -@utils.arg( - 'request_id', - metavar='', - help=_('Request ID of the action to get.')) -def do_instance_action(cs, args): - """Show an action.""" - if cs.api_version < api_versions.APIVersion("2.21"): - server = shell._find_server(cs, args.server) - else: - server = shell._find_server(cs, args.server, raise_if_notfound=False) - action_resource = cs.instance_action.get(server, args.request_id) - action = action_resource._info - if 'events' in action: - action['events'] = pprint.pformat(action['events']) - utils.print_dict(action) - - -@utils.arg( - 'server', - metavar='', - help=_('Name or UUID of the server to list actions for.'), - start_version="2.0", end_version="2.20") -@utils.arg( - 'server', - metavar='', - help=_('Name or UUID of the server to list actions for. Only UUID can be ' - 'used to list actions on a deleted server.'), - start_version="2.21") -def do_instance_action_list(cs, args): - """List actions on a server.""" - if cs.api_version < api_versions.APIVersion("2.21"): - server = shell._find_server(cs, args.server) - else: - server = shell._find_server(cs, args.server, raise_if_notfound=False) - actions = cs.instance_action.list(server) - utils.print_list(actions, - ['Action', 'Request_ID', 'Message', 'Start_Time'], - sortby_index=3) +contrib.warn() diff --git a/novaclient/v2/contrib/list_extensions.py b/novaclient/v2/contrib/list_extensions.py index 7eb9f16c8..9177fa0cd 100644 --- a/novaclient/v2/contrib/list_extensions.py +++ b/novaclient/v2/contrib/list_extensions.py @@ -13,34 +13,11 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient import base -from novaclient import utils +from novaclient.v2 import contrib +from novaclient.v2 import list_extensions -class ListExtResource(base.Resource): - @property - def summary(self): - descr = self.description.strip() - if not descr: - return '??' - lines = descr.split("\n") - if len(lines) == 1: - return lines[0] - else: - return lines[0] + "..." +ListExtResource = list_extensions.ListExtResource +ListExtManager = list_extensions.ListExtResource - -class ListExtManager(base.Manager): - resource_class = ListExtResource - - def show_all(self): - return self._list("/extensions", 'extensions') - - -def do_list_extensions(client, _args): - """ - List all the os-api extensions that are available. - """ - extensions = client.list_extensions.show_all() - fields = ["Name", "Summary", "Alias", "Updated"] - utils.print_list(extensions, fields) +contrib.warn() diff --git a/novaclient/v2/contrib/metadata_extensions.py b/novaclient/v2/contrib/metadata_extensions.py index 8d2d22f89..eb6fbd224 100644 --- a/novaclient/v2/contrib/metadata_extensions.py +++ b/novaclient/v2/contrib/metadata_extensions.py @@ -13,35 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.i18n import _ -from novaclient import utils -from novaclient.v2 import shell +from novaclient.v2 import contrib -@utils.arg( - 'host', - metavar='', - help=_('Name of host.')) -@utils.arg( - 'action', - metavar='', - choices=['set', 'delete'], - help=_("Actions: 'set' or 'delete'")) -@utils.arg( - 'metadata', - metavar='', - nargs='+', - action='append', - default=[], - help=_('Metadata to set or delete (only key is necessary on delete)')) -def do_host_meta(cs, args): - """Set or Delete metadata on all instances of a host.""" - hypervisors = cs.hypervisors.search(args.host, servers=True) - for hyper in hypervisors: - metadata = shell._extract_metadata(args) - if hasattr(hyper, 'servers'): - for server in hyper.servers: - if args.action == 'set': - cs.servers.set_meta(server['uuid'], metadata) - elif args.action == 'delete': - cs.servers.delete_meta(server['uuid'], metadata.keys()) +contrib.warn(alternative=False) diff --git a/novaclient/v2/contrib/migrations.py b/novaclient/v2/contrib/migrations.py index 47856444d..909b569c6 100644 --- a/novaclient/v2/contrib/migrations.py +++ b/novaclient/v2/contrib/migrations.py @@ -14,86 +14,11 @@ migration interface """ -from six.moves.urllib import parse +from novaclient.v2 import contrib +from novaclient.v2 import migrations -from novaclient import api_versions -from novaclient import base -from novaclient.i18n import _ -from novaclient import utils +Migration = migrations.Migration +MigrationManager = migrations.MigrationManager -class Migration(base.Resource): - def __repr__(self): - return "" % self.id - - -class MigrationManager(base.ManagerWithFind): - resource_class = Migration - - def list(self, host=None, status=None, cell_name=None): - """ - Get a list of migrations. - :param host: (optional) filter migrations by host name. - :param status: (optional) filter migrations by status. - :param cell_name: (optional) filter migrations for a cell. - """ - opts = {} - if host: - opts['host'] = host - if status: - opts['status'] = status - if cell_name: - opts['cell_name'] = cell_name - - # Transform the dict to a sequence of two-element tuples in fixed - # order, then the encoded string will be consistent in Python 2&3. - new_opts = sorted(opts.items(), key=lambda x: x[0]) - - query_string = "?%s" % parse.urlencode(new_opts) if new_opts else "" - - return self._list("/os-migrations%s" % query_string, "migrations") - - -@utils.arg( - '--host', - dest='host', - metavar='', - help=_('Fetch migrations for the given host.')) -@utils.arg( - '--status', - dest='status', - metavar='', - help=_('Fetch migrations for the given status.')) -@utils.arg( - '--cell_name', - dest='cell_name', - metavar='', - help=_('Fetch migrations for the given cell_name.')) -def do_migration_list(cs, args): - """Print a list of migrations.""" - migrations = cs.migrations.list(args.host, args.status, args.cell_name) - _print_migrations(cs, migrations) - - -def _print_migrations(cs, migrations): - fields = ['Source Node', 'Dest Node', 'Source Compute', 'Dest Compute', - 'Dest Host', 'Status', 'Instance UUID', 'Old Flavor', - 'New Flavor', 'Created At', 'Updated At'] - - def old_flavor(migration): - return migration.old_instance_type_id - - def new_flavor(migration): - return migration.new_instance_type_id - - def migration_type(migration): - return migration.migration_type - - formatters = {'Old Flavor': old_flavor, 'New Flavor': new_flavor} - - if cs.api_version >= api_versions.APIVersion("2.23"): - fields.insert(0, "Id") - fields.append("Type") - formatters.update({"Type": migration_type}) - - utils.print_list(migrations, fields, formatters) +contrib.warn() diff --git a/novaclient/v2/contrib/server_external_events.py b/novaclient/v2/contrib/server_external_events.py index a45914b55..bbd1b032f 100644 --- a/novaclient/v2/contrib/server_external_events.py +++ b/novaclient/v2/contrib/server_external_events.py @@ -16,28 +16,14 @@ External event triggering for servers, not to be used by users. """ -from novaclient import base +from novaclient.v2 import contrib +from novaclient.v2 import server_external_events -class Event(base.Resource): - def __repr__(self): - return "" % self.name - - -class ServerExternalEventManager(base.Manager): - resource_class = Event - - def create(self, events): - """Create one or more server events. - - :param:events: A list of dictionaries containing 'server_uuid', 'name', - 'status', and 'tag' (which may be absent) - """ - - body = {'events': events} - return self._create('/os-server-external-events', body, 'events', - return_raw=True) - +Event = server_external_events.Event +ServerExternalEventManager = server_external_events.ServerExternalEventManager manager_class = ServerExternalEventManager name = 'server_external_events' + +contrib.warn() diff --git a/novaclient/v2/instance_action.py b/novaclient/v2/instance_action.py new file mode 100644 index 000000000..5531d35fc --- /dev/null +++ b/novaclient/v2/instance_action.py @@ -0,0 +1,40 @@ +# Copyright 2013 Rackspace Hosting +# All Rights Reserved. +# +# 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 novaclient import base + + +class InstanceAction(base.Resource): + pass + + +class InstanceActionManager(base.ManagerWithFind): + resource_class = InstanceAction + + def get(self, server, request_id): + """ + Get details of an action performed on an instance. + + :param request_id: The request_id of the action to get. + """ + return self._get("/servers/%s/os-instance-actions/%s" % + (base.getid(server), request_id), 'instanceAction') + + def list(self, server): + """ + Get a list of actions performed on a server. + """ + return self._list('/servers/%s/os-instance-actions' % + base.getid(server), 'instanceActions') diff --git a/novaclient/v2/list_extensions.py b/novaclient/v2/list_extensions.py new file mode 100644 index 000000000..faeead601 --- /dev/null +++ b/novaclient/v2/list_extensions.py @@ -0,0 +1,36 @@ +# Copyright 2011 OpenStack Foundation +# All Rights Reserved. +# +# 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 novaclient import base + + +class ListExtResource(base.Resource): + @property + def summary(self): + descr = self.description.strip() + if not descr: + return '??' + lines = descr.split("\n") + if len(lines) == 1: + return lines[0] + else: + return lines[0] + "..." + + +class ListExtManager(base.Manager): + resource_class = ListExtResource + + def show_all(self): + return self._list("/extensions", 'extensions') diff --git a/novaclient/v2/migrations.py b/novaclient/v2/migrations.py new file mode 100644 index 000000000..51b5988b9 --- /dev/null +++ b/novaclient/v2/migrations.py @@ -0,0 +1,51 @@ +# 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. + +""" +migration interface +""" + +from six.moves.urllib import parse + +from novaclient import base + + +class Migration(base.Resource): + def __repr__(self): + return "" % self.id + + +class MigrationManager(base.ManagerWithFind): + resource_class = Migration + + def list(self, host=None, status=None, cell_name=None): + """ + Get a list of migrations. + :param host: (optional) filter migrations by host name. + :param status: (optional) filter migrations by status. + :param cell_name: (optional) filter migrations for a cell. + """ + opts = {} + if host: + opts['host'] = host + if status: + opts['status'] = status + if cell_name: + opts['cell_name'] = cell_name + + # Transform the dict to a sequence of two-element tuples in fixed + # order, then the encoded string will be consistent in Python 2&3. + new_opts = sorted(opts.items(), key=lambda x: x[0]) + + query_string = "?%s" % parse.urlencode(new_opts) if new_opts else "" + + return self._list("/os-migrations%s" % query_string, "migrations") diff --git a/novaclient/v2/server_external_events.py b/novaclient/v2/server_external_events.py new file mode 100644 index 000000000..7c2506a2c --- /dev/null +++ b/novaclient/v2/server_external_events.py @@ -0,0 +1,39 @@ +# Copyright (C) 2014, Red Hat, Inc. +# +# 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. + +""" +External event triggering for servers, not to be used by users. +""" + +from novaclient import base + + +class Event(base.Resource): + def __repr__(self): + return "" % self.name + + +class ServerExternalEventManager(base.Manager): + resource_class = Event + + def create(self, events): + """Create one or more server events. + + :param:events: A list of dictionaries containing 'server_uuid', 'name', + 'status', and 'tag' (which may be absent) + """ + + body = {'events': events} + return self._create('/os-server-external-events', body, 'events', + return_raw=True) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index db72b6bc3..b621025a2 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -26,6 +26,7 @@ import locale import logging import os +import pprint import sys import time @@ -5185,3 +5186,359 @@ def do_server_tag_delete_all(cs, args): """Delete all tags from a server.""" server = _find_server(cs, args.server) server.delete_all_tags() + + +@utils.arg( + 'cell', + metavar='', + help=_('Name of the cell.')) +def do_cell_show(cs, args): + """Show details of a given cell.""" + cell = cs.cells.get(args.cell) + utils.print_dict(cell._info) + + +@utils.arg( + '--cell', + metavar='', + help=_("Name of the cell to get the capacities."), + default=None) +def do_cell_capacities(cs, args): + """Get cell capacities for all cells or a given cell.""" + cell = cs.cells.capacities(args.cell) + print(_("Ram Available: %s MB") % cell.capacities['ram_free']['total_mb']) + utils.print_dict(cell.capacities['ram_free']['units_by_mb'], + dict_property='Ram(MB)', dict_value="Units") + print(_("\nDisk Available: %s MB") % + cell.capacities['disk_free']['total_mb']) + utils.print_dict(cell.capacities['disk_free']['units_by_mb'], + dict_property='Disk(MB)', dict_value="Units") + + +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_force_delete(cs, args): + """Force delete a server.""" + utils.find_resource(cs.servers, args.server).force_delete() + + +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_restore(cs, args): + """Restore a soft-deleted server.""" + utils.find_resource(cs.servers, args.server, deleted=True).restore() + + +class EvacuateHostResponse(base.Resource): + pass + + +def _server_evacuate(cs, server, args): + success = True + error_message = "" + try: + if api_versions.APIVersion("2.29") <= cs.api_version: + # if microversion >= 2.29 + force = getattr(args, 'force', None) + cs.servers.evacuate(server=server['uuid'], host=args.target_host, + force=force) + elif api_versions.APIVersion("2.14") <= cs.api_version: + # if microversion 2.14 - 2.28 + cs.servers.evacuate(server=server['uuid'], host=args.target_host) + else: + # else microversion 2.0 - 2.13 + on_shared_storage = getattr(args, 'on_shared_storage', None) + cs.servers.evacuate(server=server['uuid'], + host=args.target_host, + on_shared_storage=on_shared_storage) + except Exception as e: + success = False + error_message = _("Error while evacuating instance: %s") % e + return EvacuateHostResponse(base.Manager, {"server_uuid": server['uuid'], + "evacuate_accepted": success, + "error_message": error_message}) + + +@utils.arg('host', metavar='', help='Name of host.') +@utils.arg( + '--target_host', + metavar='', + default=None, + help=_('Name of target host. If no host is specified the scheduler will ' + 'select a target.')) +@utils.arg( + '--on-shared-storage', + dest='on_shared_storage', + action="store_true", + default=False, + help=_('Specifies whether all instances files are on shared storage'), + start_version='2.0', + end_version='2.13') +@utils.arg( + '--force', + dest='force', + action='store_true', + default=False, + help=_('Force to not verify the scheduler if a host is provided.'), + start_version='2.29') +def do_host_evacuate(cs, args): + """Evacuate all instances from failed host.""" + + hypervisors = cs.hypervisors.search(args.host, servers=True) + response = [] + for hyper in hypervisors: + if hasattr(hyper, 'servers'): + for server in hyper.servers: + response.append(_server_evacuate(cs, server, args)) + + utils.print_list(response, + ["Server UUID", "Evacuate Accepted", "Error Message"]) + + +def _server_live_migrate(cs, server, args): + class HostEvacuateLiveResponse(object): + def __init__(self, server_uuid, live_migration_accepted, + error_message): + self.server_uuid = server_uuid + self.live_migration_accepted = live_migration_accepted + self.error_message = error_message + success = True + error_message = "" + update_kwargs = {} + try: + # API >= 2.30 + if 'force' in args and args.force: + update_kwargs['force'] = args.force + # API 2.0->2.24 + if 'disk_over_commit' in args: + update_kwargs['disk_over_commit'] = args.disk_over_commit + cs.servers.live_migrate(server['uuid'], args.target_host, + args.block_migrate, **update_kwargs) + except Exception as e: + success = False + error_message = _("Error while live migrating instance: %s") % e + return HostEvacuateLiveResponse(server['uuid'], + success, + error_message) + + +@utils.arg('host', metavar='', help='Name of host.') +@utils.arg( + '--target-host', + metavar='', + default=None, + help=_('Name of target host.')) +@utils.arg( + '--block-migrate', + action='store_true', + default=False, + help=_('Enable block migration. (Default=False)'), + start_version="2.0", end_version="2.24") +@utils.arg( + '--block-migrate', + action='store_true', + default="auto", + help=_('Enable block migration. (Default=auto)'), + start_version="2.25") +@utils.arg( + '--disk-over-commit', + action='store_true', + default=False, + help=_('Enable disk overcommit.'), + start_version="2.0", end_version="2.24") +@utils.arg( + '--max-servers', + type=int, + dest='max_servers', + metavar='', + help='Maximum number of servers to live migrate simultaneously') +@utils.arg( + '--force', + dest='force', + action='store_true', + default=False, + help=_('Force to not verify the scheduler if a host is provided.'), + start_version='2.30') +def do_host_evacuate_live(cs, args): + """Live migrate all instances of the specified host + to other available hosts. + """ + hypervisors = cs.hypervisors.search(args.host, servers=True) + response = [] + migrating = 0 + for hyper in hypervisors: + for server in getattr(hyper, 'servers', []): + response.append(_server_live_migrate(cs, server, args)) + migrating += 1 + if args.max_servers is not None and migrating >= args.max_servers: + break + + utils.print_list(response, ["Server UUID", "Live Migration Accepted", + "Error Message"]) + + +class HostServersMigrateResponse(base.Resource): + pass + + +def _server_migrate(cs, server): + success = True + error_message = "" + try: + cs.servers.migrate(server['uuid']) + except Exception as e: + success = False + error_message = _("Error while migrating instance: %s") % e + return HostServersMigrateResponse(base.Manager, + {"server_uuid": server['uuid'], + "migration_accepted": success, + "error_message": error_message}) + + +@utils.arg('host', metavar='', help='Name of host.') +def do_host_servers_migrate(cs, args): + """Cold migrate all instances off the specified host to other available + hosts. + """ + + hypervisors = cs.hypervisors.search(args.host, servers=True) + response = [] + for hyper in hypervisors: + if hasattr(hyper, 'servers'): + for server in hyper.servers: + response.append(_server_migrate(cs, server)) + + utils.print_list(response, + ["Server UUID", "Migration Accepted", "Error Message"]) + + +@utils.arg( + 'server', + metavar='', + help=_('Name or UUID of the server to show actions for.'), + start_version="2.0", end_version="2.20") +@utils.arg( + 'server', + metavar='', + help=_('Name or UUID of the server to show actions for. Only UUID can be ' + 'used to show actions for a deleted server.'), + start_version="2.21") +@utils.arg( + 'request_id', + metavar='', + help=_('Request ID of the action to get.')) +def do_instance_action(cs, args): + """Show an action.""" + if cs.api_version < api_versions.APIVersion("2.21"): + server = _find_server(cs, args.server) + else: + server = _find_server(cs, args.server, raise_if_notfound=False) + action_resource = cs.instance_action.get(server, args.request_id) + action = action_resource._info + if 'events' in action: + action['events'] = pprint.pformat(action['events']) + utils.print_dict(action) + + +@utils.arg( + 'server', + metavar='', + help=_('Name or UUID of the server to list actions for.'), + start_version="2.0", end_version="2.20") +@utils.arg( + 'server', + metavar='', + help=_('Name or UUID of the server to list actions for. Only UUID can be ' + 'used to list actions on a deleted server.'), + start_version="2.21") +def do_instance_action_list(cs, args): + """List actions on a server.""" + if cs.api_version < api_versions.APIVersion("2.21"): + server = _find_server(cs, args.server) + else: + server = _find_server(cs, args.server, raise_if_notfound=False) + actions = cs.instance_action.list(server) + utils.print_list(actions, + ['Action', 'Request_ID', 'Message', 'Start_Time'], + sortby_index=3) + + +def do_list_extensions(cs, _args): + """ + List all the os-api extensions that are available. + """ + extensions = cs.list_extensions.show_all() + fields = ["Name", "Summary", "Alias", "Updated"] + utils.print_list(extensions, fields) + + +@utils.arg( + 'host', + metavar='', + help=_('Name of host.')) +@utils.arg( + 'action', + metavar='', + choices=['set', 'delete'], + help=_("Actions: 'set' or 'delete'")) +@utils.arg( + 'metadata', + metavar='', + nargs='+', + action='append', + default=[], + help=_('Metadata to set or delete (only key is necessary on delete)')) +def do_host_meta(cs, args): + """Set or Delete metadata on all instances of a host.""" + hypervisors = cs.hypervisors.search(args.host, servers=True) + for hyper in hypervisors: + metadata = _extract_metadata(args) + if hasattr(hyper, 'servers'): + for server in hyper.servers: + if args.action == 'set': + cs.servers.set_meta(server['uuid'], metadata) + elif args.action == 'delete': + cs.servers.delete_meta(server['uuid'], metadata.keys()) + + +def _print_migrations(cs, migrations): + fields = ['Source Node', 'Dest Node', 'Source Compute', 'Dest Compute', + 'Dest Host', 'Status', 'Instance UUID', 'Old Flavor', + 'New Flavor', 'Created At', 'Updated At'] + + def old_flavor(migration): + return migration.old_instance_type_id + + def new_flavor(migration): + return migration.new_instance_type_id + + def migration_type(migration): + return migration.migration_type + + formatters = {'Old Flavor': old_flavor, 'New Flavor': new_flavor} + + if cs.api_version >= api_versions.APIVersion("2.23"): + fields.insert(0, "Id") + fields.append("Type") + formatters.update({"Type": migration_type}) + + utils.print_list(migrations, fields, formatters) + + +@utils.arg( + '--host', + dest='host', + metavar='', + help=_('Fetch migrations for the given host.')) +@utils.arg( + '--status', + dest='status', + metavar='', + help=_('Fetch migrations for the given status.')) +@utils.arg( + '--cell_name', + dest='cell_name', + metavar='', + help=_('Fetch migrations for the given cell_name.')) +def do_migration_list(cs, args): + """Print a list of migrations.""" + migrations = cs.migrations.list(args.host, args.status, args.cell_name) + _print_migrations(cs, migrations) diff --git a/releasenotes/notes/deprecate_contrib_extensions-0ec70c070b09eedb.yaml b/releasenotes/notes/deprecate_contrib_extensions-0ec70c070b09eedb.yaml new file mode 100644 index 000000000..66810756f --- /dev/null +++ b/releasenotes/notes/deprecate_contrib_extensions-0ec70c070b09eedb.yaml @@ -0,0 +1,21 @@ +--- +prelude: > + All extensions of API V2.0 were merged to 2.1, but NovaClient continued + to store them as a separate entities. +upgrade: + - All managers and resources from novaclient.v2.contrib submodules are moved + to appropriate submodules of novaclient.v2 (except barametal and + tenant_networks, which were deprecated previously) + - All shell commands from novaclient.v2.contrib submodules are moved to + novaclient.v2.shell module. + - novaclient.v2.client.Client imports all modules (which were located in + submodules of novaclient.v2.contrib) by-default for api version v2 + - Method novaclient.client.discover_extensions returns only barametal and + tenant_networks extensions, since they are not included by default. + - There are no modules and extensions for "deferred_delete", "host_evacuate", + "host_evacuate_live" and "metadata_extensions" anymore. Previously, they + contained only shell commands and shell module auto loads them (there is + no ability to not do it). +deprecations: + - All modules of novaclient.v2.contrib are deprecated now and will be + removed after OpenStack Pike. From f47393dbbfbf36616a8d28348d93e1368763b8c3 Mon Sep 17 00:00:00 2001 From: Diana Clarke Date: Mon, 7 Nov 2016 16:52:35 -0500 Subject: [PATCH 1180/1705] Microversion 2.39 - Simple tenant usage pagination Optional parameters 'limit' and 'marker' were added to the GET /os-simple-tenant-usage and GET os-simple-tenant-usage/{tenant_id} endpoints. /os-simple-tenant-usage?limit={limit}&marker={instance_uuid} /os-simple-tenant-usage/{tenant_id}?limit={limit}&marker={instance_uuid} Implements blueprint paginate-simple-tenant-usage Change-Id: I9e96d31ede323f1c2aed4271a4fcab720cc9507b --- novaclient/__init__.py | 2 +- novaclient/tests/unit/v2/fakes.py | 75 +++++++++++++++++++ novaclient/tests/unit/v2/test_shell.py | 54 ++++++++++++- novaclient/tests/unit/v2/test_usage.py | 49 ++++++++++++ novaclient/v2/shell.py | 73 ++++++++++++++++-- novaclient/v2/usage.py | 66 ++++++++++++++-- .../microversion-v2_39-484adba0806b08bf.yaml | 4 + 7 files changed, 306 insertions(+), 17 deletions(-) create mode 100644 releasenotes/notes/microversion-v2_39-484adba0806b08bf.yaml diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 99d9f4a74..85cadcf62 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.38") +API_MAX_VERSION = api_versions.APIVersion("2.39") diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 58d5b39ed..28e0ce353 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -73,6 +73,7 @@ def __init__(self, **kwargs): self.auth_url = 'auth_url' self.tenant_id = 'tenant_id' self.callstack = [] + self.visited = [] self.projectid = 'projectid' self.user = 'user' self.region_name = 'region_name' @@ -135,12 +136,21 @@ def _cs_request(self, url, method, **kwargs): v2_image = True callback = callback.replace('get_v2_', 'get_') + simulate_pagination_next_links = [ + 'get_os_simple_tenant_usage', + 'get_os_simple_tenant_usage_tenant_id', + ] + if callback in simulate_pagination_next_links: + while callback in self.visited: + callback += '_next' + if not hasattr(self, callback): raise AssertionError('Called unknown API method: %s %s, ' 'expected fakes method name: %s' % (method, url, callback)) # Note the call + self.visited.append(callback) self.callstack.append((method, url, kwargs.get('body'))) status, headers, body = getattr(self, callback)(**kwargs) @@ -1565,6 +1575,36 @@ def get_os_simple_tenant_usage(self, **kw): six.u('name'): six.u('f15image1'), six.u('tenant_id'): six.u('7b0a1d73f8fb41718f3343c207597869'), + six.u('instance_id'): + six.u('f079e394-1111-457b-b350-bb5ecc685cdd'), + six.u('vcpus'): 1, + six.u('memory_mb'): 512, + six.u('state'): six.u('active'), + six.u('flavor'): six.u('m1.tiny'), + six.u('started_at'): + six.u('2012-01-20 18:06:06.479998')}], + six.u('start'): six.u('2011-12-25 19:48:41.750687'), + six.u('total_local_gb_usage'): 0.0}]}) + + def get_os_simple_tenant_usage_next(self, **kw): + return (200, FAKE_RESPONSE_HEADERS, + {six.u('tenant_usages'): [{ + six.u('total_memory_mb_usage'): 25451.762807466665, + six.u('total_vcpus_usage'): 49.71047423333333, + six.u('total_hours'): 49.71047423333333, + six.u('tenant_id'): + six.u('7b0a1d73f8fb41718f3343c207597869'), + six.u('stop'): six.u('2012-01-22 19:48:41.750722'), + six.u('server_usages'): [{ + six.u('hours'): 49.71047423333333, + six.u('uptime'): 27035, + six.u('local_gb'): 0, + six.u('ended_at'): None, + six.u('name'): six.u('f15image1'), + six.u('tenant_id'): + six.u('7b0a1d73f8fb41718f3343c207597869'), + six.u('instance_id'): + six.u('f079e394-2222-457b-b350-bb5ecc685cdd'), six.u('vcpus'): 1, six.u('memory_mb'): 512, six.u('state'): six.u('active'), @@ -1574,6 +1614,9 @@ def get_os_simple_tenant_usage(self, **kw): six.u('start'): six.u('2011-12-25 19:48:41.750687'), six.u('total_local_gb_usage'): 0.0}]}) + def get_os_simple_tenant_usage_next_next(self, **kw): + return (200, FAKE_RESPONSE_HEADERS, {six.u('tenant_usages'): []}) + def get_os_simple_tenant_usage_tenantfoo(self, **kw): return (200, FAKE_RESPONSE_HEADERS, {six.u('tenant_usage'): { @@ -1590,6 +1633,8 @@ def get_os_simple_tenant_usage_tenantfoo(self, **kw): six.u('name'): six.u('f15image1'), six.u('tenant_id'): six.u('7b0a1d73f8fb41718f3343c207597869'), + six.u('instance_id'): + six.u('f079e394-1111-457b-b350-bb5ecc685cdd'), six.u('vcpus'): 1, six.u('memory_mb'): 512, six.u('state'): six.u('active'), six.u('flavor'): six.u('m1.tiny'), @@ -1611,6 +1656,8 @@ def get_os_simple_tenant_usage_test(self, **kw): six.u('ended_at'): None, six.u('name'): six.u('f15image1'), six.u('tenant_id'): six.u('7b0a1d73f8fb41718f3343c207597869'), + six.u('instance_id'): + six.u('f079e394-1111-457b-b350-bb5ecc685cdd'), six.u('vcpus'): 1, six.u('memory_mb'): 512, six.u('state'): six.u('active'), six.u('flavor'): six.u('m1.tiny'), @@ -1631,6 +1678,30 @@ def get_os_simple_tenant_usage_tenant_id(self, **kw): six.u('ended_at'): None, six.u('name'): six.u('f15image1'), six.u('tenant_id'): six.u('7b0a1d73f8fb41718f3343c207597869'), + six.u('instance_id'): + six.u('f079e394-1111-457b-b350-bb5ecc685cdd'), + six.u('vcpus'): 1, six.u('memory_mb'): 512, + six.u('state'): six.u('active'), + six.u('flavor'): six.u('m1.tiny'), + six.u('started_at'): six.u('2012-01-20 18:06:06.479998')}], + six.u('start'): six.u('2011-12-25 19:48:41.750687'), + six.u('total_local_gb_usage'): 0.0}}) + + def get_os_simple_tenant_usage_tenant_id_next(self, **kw): + return (200, {}, {six.u('tenant_usage'): { + six.u('total_memory_mb_usage'): 25451.762807466665, + six.u('total_vcpus_usage'): 49.71047423333333, + six.u('total_hours'): 49.71047423333333, + six.u('tenant_id'): six.u('7b0a1d73f8fb41718f3343c207597869'), + six.u('stop'): six.u('2012-01-22 19:48:41.750722'), + six.u('server_usages'): [{ + six.u('hours'): 49.71047423333333, + six.u('uptime'): 27035, six.u('local_gb'): 0, + six.u('ended_at'): None, + six.u('name'): six.u('f15image1'), + six.u('tenant_id'): six.u('7b0a1d73f8fb41718f3343c207597869'), + six.u('instance_id'): + six.u('f079e394-2222-457b-b350-bb5ecc685cdd'), six.u('vcpus'): 1, six.u('memory_mb'): 512, six.u('state'): six.u('active'), six.u('flavor'): six.u('m1.tiny'), @@ -1638,6 +1709,9 @@ def get_os_simple_tenant_usage_tenant_id(self, **kw): six.u('start'): six.u('2011-12-25 19:48:41.750687'), six.u('total_local_gb_usage'): 0.0}}) + def get_os_simple_tenant_usage_tenant_id_next_next(self, **kw): + return (200, {}, {six.u('tenant_usage'): {}}) + # # Aggregates # @@ -2377,6 +2451,7 @@ class FakeSessionMockClient(base_client.SessionClient, FakeHTTPClient): def __init__(self, *args, **kwargs): self.callstack = [] + self.visited = [] self.auth = mock.Mock() self.session = mock.Mock() self.session.get_endpoint.return_value = FakeHTTPClient.get_endpoint( diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index e71ba0e00..67663b9a7 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -50,7 +50,7 @@ def setUp(self): def tearDown(self): # For some method like test_image_meta_bad_action we are # testing a SystemExit to be thrown and object self.shell has - # no time to get instantatiated which is OK in this case, so + # no time to get instantiated which is OK in this case, so # we make sure the method is there before launching it. if hasattr(self.shell, 'cs'): self.shell.cs.clear_callstack() @@ -1750,12 +1750,36 @@ def test_server_floating_ip_disassociate(self): {'removeFloatingIp': {'address': '11.0.0.1'}}) def test_usage_list(self): - self.run_command('usage-list --start 2000-01-20 --end 2005-02-01') + cmd = 'usage-list --start 2000-01-20 --end 2005-02-01' + stdout, _ = self.run_command(cmd) self.assert_called('GET', '/os-simple-tenant-usage?' + 'start=2000-01-20T00:00:00&' + 'end=2005-02-01T00:00:00&' + 'detailed=1') + # Servers, RAM MB-Hours, CPU Hours, Disk GB-Hours + self.assertIn('1 | 25451.76 | 49.71 | 0.00', stdout) + + def test_usage_list_stitch_together_next_results(self): + cmd = 'usage-list --start 2000-01-20 --end 2005-02-01' + stdout, _ = self.run_command(cmd, api_version='2.39') + self.assert_called('GET', + '/os-simple-tenant-usage?' + 'start=2000-01-20T00:00:00&' + 'end=2005-02-01T00:00:00&' + 'detailed=1', pos=0) + markers = [ + 'f079e394-1111-457b-b350-bb5ecc685cdd', + 'f079e394-2222-457b-b350-bb5ecc685cdd', + ] + for pos, marker in enumerate(markers): + self.assert_called('GET', + '/os-simple-tenant-usage?' + 'start=2000-01-20T00:00:00&' + 'end=2005-02-01T00:00:00&' + 'marker=%s&detailed=1' % (marker), pos=pos + 1) + # Servers, RAM MB-Hours, CPU Hours, Disk GB-Hours + self.assertIn('2 | 50903.53 | 99.42 | 0.00', stdout) def test_usage_list_no_args(self): timeutils.set_time_override(datetime.datetime(2005, 2, 1, 0, 0)) @@ -1768,12 +1792,34 @@ def test_usage_list_no_args(self): 'detailed=1') def test_usage(self): - self.run_command('usage --start 2000-01-20 --end 2005-02-01 ' - '--tenant test') + cmd = 'usage --start 2000-01-20 --end 2005-02-01 --tenant test' + stdout, _ = self.run_command(cmd) self.assert_called('GET', '/os-simple-tenant-usage/test?' + 'start=2000-01-20T00:00:00&' + 'end=2005-02-01T00:00:00') + # Servers, RAM MB-Hours, CPU Hours, Disk GB-Hours + self.assertIn('1 | 25451.76 | 49.71 | 0.00', stdout) + + def test_usage_stitch_together_next_results(self): + cmd = 'usage --start 2000-01-20 --end 2005-02-01' + stdout, _ = self.run_command(cmd, api_version='2.39') + self.assert_called('GET', + '/os-simple-tenant-usage/tenant_id?' + 'start=2000-01-20T00:00:00&' + 'end=2005-02-01T00:00:00', pos=0) + markers = [ + 'f079e394-1111-457b-b350-bb5ecc685cdd', + 'f079e394-2222-457b-b350-bb5ecc685cdd', + ] + for pos, marker in enumerate(markers): + self.assert_called('GET', + '/os-simple-tenant-usage/tenant_id?' + 'start=2000-01-20T00:00:00&' + 'end=2005-02-01T00:00:00&' + 'marker=%s' % (marker), pos=pos + 1) + # Servers, RAM MB-Hours, CPU Hours, Disk GB-Hours + self.assertIn('2 | 50903.53 | 99.42 | 0.00', stdout) def test_usage_no_tenant(self): self.run_command('usage --start 2000-01-20 --end 2005-02-01') diff --git a/novaclient/tests/unit/v2/test_usage.py b/novaclient/tests/unit/v2/test_usage.py index e67178b41..8c18e1526 100644 --- a/novaclient/tests/unit/v2/test_usage.py +++ b/novaclient/tests/unit/v2/test_usage.py @@ -73,3 +73,52 @@ def test_usage_class_get(self): 'GET', "/os-simple-tenant-usage/tenantfoo?start=%s&end=%s" % (start, stop)) + + +class UsageV39Test(UsageTest): + def setUp(self): + super(UsageV39Test, self).setUp() + self.cs.api_version = api_versions.APIVersion('2.39') + + def test_usage_list_with_paging(self): + now = datetime.datetime.now() + usages = self.cs.usage.list(now, now, marker='some-uuid', limit=3) + self.assert_request_id(usages, fakes.FAKE_REQUEST_ID_LIST) + + self.cs.assert_called( + 'GET', + '/os-simple-tenant-usage?' + + ('start=%s&' % now.isoformat()) + + ('end=%s&' % now.isoformat()) + + ('limit=3&marker=some-uuid&detailed=0')) + for u in usages: + self.assertIsInstance(u, usage.Usage) + + def test_usage_list_detailed_with_paging(self): + now = datetime.datetime.now() + usages = self.cs.usage.list( + now, now, detailed=True, marker='some-uuid', limit=3) + self.assert_request_id(usages, fakes.FAKE_REQUEST_ID_LIST) + + self.cs.assert_called( + 'GET', + '/os-simple-tenant-usage?' + + ('start=%s&' % now.isoformat()) + + ('end=%s&' % now.isoformat()) + + ('limit=3&marker=some-uuid&detailed=1')) + for u in usages: + self.assertIsInstance(u, usage.Usage) + + def test_usage_get_with_paging(self): + now = datetime.datetime.now() + u = self.cs.usage.get( + 'tenantfoo', now, now, marker='some-uuid', limit=3) + self.assert_request_id(u, fakes.FAKE_REQUEST_ID_LIST) + + self.cs.assert_called( + 'GET', + '/os-simple-tenant-usage/tenantfoo?' + + ('start=%s&' % now.isoformat()) + + ('end=%s&' % now.isoformat()) + + ('limit=3&marker=some-uuid')) + self.assertIsInstance(u, usage.Usage) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index d61eae398..b3e68df18 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -19,6 +19,7 @@ from __future__ import print_function import argparse +import collections import copy import datetime import functools @@ -3433,7 +3434,43 @@ def simplify_usage(u): setattr(u, simplerows[3], "%.2f" % u.total_vcpus_usage) setattr(u, simplerows[4], "%.2f" % u.total_local_gb_usage) - usage_list = cs.usage.list(start, end, detailed=True) + def get_marker(usage_list): + marker = None + if usage_list: + usage = usage_list[-1] + if hasattr(usage, 'server_usages') and usage.server_usages: + marker = usage.server_usages[-1]['instance_id'] + return marker + + def merge_usage(usages, next_usage_list): + for next_usage in next_usage_list: + if next_usage.tenant_id in usages: + usage = usages[next_usage.tenant_id] + usage.server_usages.extend(next_usage.server_usages) + usage.total_hours += next_usage.total_hours + usage.total_memory_mb_usage += next_usage.total_memory_mb_usage + usage.total_vcpus_usage += next_usage.total_vcpus_usage + usage.total_local_gb_usage += next_usage.total_local_gb_usage + else: + usages[next_usage.tenant_id] = next_usage + + if cs.api_version < api_versions.APIVersion('2.39'): + usage_list = cs.usage.list(start, end, detailed=True) + else: + # If the number of instances used to calculate the usage is greater + # than osapi_max_limit, the usage will be split across multiple + # requests and the responses will need to be merged back together. + usages = collections.OrderedDict() + usage_list = cs.usage.list(start, end, detailed=True) + merge_usage(usages, usage_list) + marker = get_marker(usage_list) + while marker: + next_usage_list = cs.usage.list( + start, end, detailed=True, marker=marker) + marker = get_marker(next_usage_list) + if marker: + merge_usage(usages, next_usage_list) + usage_list = usages.values() print(_("Usage from %(start)s to %(end)s:") % {'start': start.strftime(dateformat), @@ -3484,15 +3521,41 @@ def simplify_usage(u): setattr(u, simplerows[2], "%.2f" % u.total_vcpus_usage) setattr(u, simplerows[3], "%.2f" % u.total_local_gb_usage) + def get_marker(usage): + marker = None + if hasattr(usage, 'server_usages') and usage.server_usages: + marker = usage.server_usages[-1]['instance_id'] + return marker + + def merge_usage(usage, next_usage): + usage.server_usages.extend(next_usage.server_usages) + usage.total_hours += next_usage.total_hours + usage.total_memory_mb_usage += next_usage.total_memory_mb_usage + usage.total_vcpus_usage += next_usage.total_vcpus_usage + usage.total_local_gb_usage += next_usage.total_local_gb_usage + if args.tenant: - usage = cs.usage.get(args.tenant, start, end) + tenant_id = args.tenant else: if isinstance(cs.client, client.SessionClient): auth = cs.client.auth - project_id = auth.get_auth_ref(cs.client.session).project_id - usage = cs.usage.get(project_id, start, end) + tenant_id = auth.get_auth_ref(cs.client.session).project_id else: - usage = cs.usage.get(cs.client.tenant_id, start, end) + tenant_id = cs.client.tenant_id + + if cs.api_version < api_versions.APIVersion('2.39'): + usage = cs.usage.get(tenant_id, start, end) + else: + # If the number of instances used to calculate the usage is greater + # than osapi_max_limit, the usage will be split across multiple + # requests and the responses will need to be merged back together. + usage = cs.usage.get(tenant_id, start, end) + marker = get_marker(usage) + while marker: + next_usage = cs.usage.get(tenant_id, start, end, marker=marker) + marker = get_marker(next_usage) + if marker: + merge_usage(usage, next_usage) print(_("Usage from %(start)s to %(end)s:") % {'start': start.strftime(dateformat), diff --git a/novaclient/v2/usage.py b/novaclient/v2/usage.py index dacf87b55..199e3111a 100644 --- a/novaclient/v2/usage.py +++ b/novaclient/v2/usage.py @@ -17,6 +17,7 @@ import oslo_utils +from novaclient import api_versions from novaclient import base @@ -46,7 +47,19 @@ class UsageManager(base.ManagerWithFind): Manage :class:`Usage` resources. """ resource_class = Usage + usage_prefix = 'os-simple-tenant-usage' + def _usage_query(self, start, end, marker=None, limit=None, detailed=None): + query = "?start=%s&end=%s" % (start.isoformat(), end.isoformat()) + if limit: + query = "%s&limit=%s" % (query, int(limit)) + if marker: + query = "%s&marker=%s" % (query, marker) + if detailed is not None: + query = "%s&detailed=%s" % (query, int(bool(detailed))) + return query + + @api_versions.wraps("2.0", "2.38") def list(self, start, end, detailed=False): """ Get usage for all tenants @@ -57,11 +70,31 @@ def list(self, start, end, detailed=False): instance whose usage is part of the report :rtype: list of :class:`Usage`. """ - return self._list( - "/os-simple-tenant-usage?start=%s&end=%s&detailed=%s" % - (start.isoformat(), end.isoformat(), int(bool(detailed))), - "tenant_usages") + query_string = self._usage_query(start, end, detailed=detailed) + url = '/%s%s' % (self.usage_prefix, query_string) + return self._list(url, 'tenant_usages') + + @api_versions.wraps("2.39") + def list(self, start, end, detailed=False, marker=None, limit=None): + """ + Get usage for all tenants + + :param start: :class:`datetime.datetime` Start date in UTC + :param end: :class:`datetime.datetime` End date in UTC + :param detailed: Whether to include information about each + instance whose usage is part of the report + :param marker: Begin returning usage data for instances that appear + later in the instance list than that represented by + this instance UUID (optional). + :param limit: Maximum number of instances to include in the usage + (optional). + :rtype: list of :class:`Usage`. + """ + query_string = self._usage_query(start, end, marker, limit, detailed) + url = '/%s%s' % (self.usage_prefix, query_string) + return self._list(url, 'tenant_usages') + @api_versions.wraps("2.0", "2.38") def get(self, tenant_id, start, end): """ Get usage for a specific tenant. @@ -71,6 +104,25 @@ def get(self, tenant_id, start, end): :param end: :class:`datetime.datetime` End date in UTC :rtype: :class:`Usage` """ - return self._get("/os-simple-tenant-usage/%s?start=%s&end=%s" % - (tenant_id, start.isoformat(), end.isoformat()), - "tenant_usage") + query_string = self._usage_query(start, end) + url = '/%s/%s%s' % (self.usage_prefix, tenant_id, query_string) + return self._get(url, 'tenant_usage') + + @api_versions.wraps("2.39") + def get(self, tenant_id, start, end, marker=None, limit=None): + """ + Get usage for a specific tenant. + + :param tenant_id: Tenant ID to fetch usage for + :param start: :class:`datetime.datetime` Start date in UTC + :param end: :class:`datetime.datetime` End date in UTC + :param marker: Begin returning usage data for instances that appear + later in the instance list than that represented by + this instance UUID (optional). + :param limit: Maximum number of instances to include in the usage + (optional). + :rtype: :class:`Usage` + """ + query_string = self._usage_query(start, end, marker, limit) + url = '/%s/%s%s' % (self.usage_prefix, tenant_id, query_string) + return self._get(url, 'tenant_usage') diff --git a/releasenotes/notes/microversion-v2_39-484adba0806b08bf.yaml b/releasenotes/notes/microversion-v2_39-484adba0806b08bf.yaml new file mode 100644 index 000000000..f81827188 --- /dev/null +++ b/releasenotes/notes/microversion-v2_39-484adba0806b08bf.yaml @@ -0,0 +1,4 @@ +--- +features: + - Added microversion v2.39 which introduces pagination support for usage + with the help of new optional parameters 'limit' and 'marker'. From b76dd8528e9238b247fbaea92860f69128bc24ca Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 2 Dec 2016 14:00:55 +0000 Subject: [PATCH 1181/1705] Updated from global requirements Change-Id: I13cc3175ff88750e8e0785fd1b01cfdeb1111410 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 312ad2490..e64027894 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ oslo.i18n>=2.1.0 # Apache-2.0 oslo.serialization>=1.10.0 # Apache-2.0 oslo.utils>=3.18.0 # Apache-2.0 PrettyTable<0.8,>=0.7.1 # BSD -requests>=2.10.0 # Apache-2.0 +requests!=2.12.2,>=2.10.0 # Apache-2.0 simplejson>=2.2.0 # MIT six>=1.9.0 # MIT Babel>=2.3.4 # BSD From 5c4d436d216e462054d7e0e714b2e8667fb0bc4c Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Fri, 2 Dec 2016 21:05:22 +0000 Subject: [PATCH 1182/1705] Revert "Microversion 2.39 - Simple tenant usage pagination" This reverts commit f47393dbbfbf36616a8d28348d93e1368763b8c3. The 2.39 microversion has not been merged in Nova, and this change is missing a functional test to show the support added here, which if present would have required a depends-on to the Nova change for the 2.39 support. So this needs to come back out until it's ready. Change-Id: I6b6990b9a4e1e7cfea1d2aabe5f2464614440360 --- novaclient/__init__.py | 2 +- novaclient/tests/unit/v2/fakes.py | 75 ------------------- novaclient/tests/unit/v2/test_shell.py | 54 +------------ novaclient/tests/unit/v2/test_usage.py | 49 ------------ novaclient/v2/shell.py | 73 ++---------------- novaclient/v2/usage.py | 66 ++-------------- .../microversion-v2_39-484adba0806b08bf.yaml | 4 - 7 files changed, 17 insertions(+), 306 deletions(-) delete mode 100644 releasenotes/notes/microversion-v2_39-484adba0806b08bf.yaml diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 85cadcf62..99d9f4a74 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.39") +API_MAX_VERSION = api_versions.APIVersion("2.38") diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 28e0ce353..58d5b39ed 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -73,7 +73,6 @@ def __init__(self, **kwargs): self.auth_url = 'auth_url' self.tenant_id = 'tenant_id' self.callstack = [] - self.visited = [] self.projectid = 'projectid' self.user = 'user' self.region_name = 'region_name' @@ -136,21 +135,12 @@ def _cs_request(self, url, method, **kwargs): v2_image = True callback = callback.replace('get_v2_', 'get_') - simulate_pagination_next_links = [ - 'get_os_simple_tenant_usage', - 'get_os_simple_tenant_usage_tenant_id', - ] - if callback in simulate_pagination_next_links: - while callback in self.visited: - callback += '_next' - if not hasattr(self, callback): raise AssertionError('Called unknown API method: %s %s, ' 'expected fakes method name: %s' % (method, url, callback)) # Note the call - self.visited.append(callback) self.callstack.append((method, url, kwargs.get('body'))) status, headers, body = getattr(self, callback)(**kwargs) @@ -1575,36 +1565,6 @@ def get_os_simple_tenant_usage(self, **kw): six.u('name'): six.u('f15image1'), six.u('tenant_id'): six.u('7b0a1d73f8fb41718f3343c207597869'), - six.u('instance_id'): - six.u('f079e394-1111-457b-b350-bb5ecc685cdd'), - six.u('vcpus'): 1, - six.u('memory_mb'): 512, - six.u('state'): six.u('active'), - six.u('flavor'): six.u('m1.tiny'), - six.u('started_at'): - six.u('2012-01-20 18:06:06.479998')}], - six.u('start'): six.u('2011-12-25 19:48:41.750687'), - six.u('total_local_gb_usage'): 0.0}]}) - - def get_os_simple_tenant_usage_next(self, **kw): - return (200, FAKE_RESPONSE_HEADERS, - {six.u('tenant_usages'): [{ - six.u('total_memory_mb_usage'): 25451.762807466665, - six.u('total_vcpus_usage'): 49.71047423333333, - six.u('total_hours'): 49.71047423333333, - six.u('tenant_id'): - six.u('7b0a1d73f8fb41718f3343c207597869'), - six.u('stop'): six.u('2012-01-22 19:48:41.750722'), - six.u('server_usages'): [{ - six.u('hours'): 49.71047423333333, - six.u('uptime'): 27035, - six.u('local_gb'): 0, - six.u('ended_at'): None, - six.u('name'): six.u('f15image1'), - six.u('tenant_id'): - six.u('7b0a1d73f8fb41718f3343c207597869'), - six.u('instance_id'): - six.u('f079e394-2222-457b-b350-bb5ecc685cdd'), six.u('vcpus'): 1, six.u('memory_mb'): 512, six.u('state'): six.u('active'), @@ -1614,9 +1574,6 @@ def get_os_simple_tenant_usage_next(self, **kw): six.u('start'): six.u('2011-12-25 19:48:41.750687'), six.u('total_local_gb_usage'): 0.0}]}) - def get_os_simple_tenant_usage_next_next(self, **kw): - return (200, FAKE_RESPONSE_HEADERS, {six.u('tenant_usages'): []}) - def get_os_simple_tenant_usage_tenantfoo(self, **kw): return (200, FAKE_RESPONSE_HEADERS, {six.u('tenant_usage'): { @@ -1633,8 +1590,6 @@ def get_os_simple_tenant_usage_tenantfoo(self, **kw): six.u('name'): six.u('f15image1'), six.u('tenant_id'): six.u('7b0a1d73f8fb41718f3343c207597869'), - six.u('instance_id'): - six.u('f079e394-1111-457b-b350-bb5ecc685cdd'), six.u('vcpus'): 1, six.u('memory_mb'): 512, six.u('state'): six.u('active'), six.u('flavor'): six.u('m1.tiny'), @@ -1656,8 +1611,6 @@ def get_os_simple_tenant_usage_test(self, **kw): six.u('ended_at'): None, six.u('name'): six.u('f15image1'), six.u('tenant_id'): six.u('7b0a1d73f8fb41718f3343c207597869'), - six.u('instance_id'): - six.u('f079e394-1111-457b-b350-bb5ecc685cdd'), six.u('vcpus'): 1, six.u('memory_mb'): 512, six.u('state'): six.u('active'), six.u('flavor'): six.u('m1.tiny'), @@ -1678,30 +1631,6 @@ def get_os_simple_tenant_usage_tenant_id(self, **kw): six.u('ended_at'): None, six.u('name'): six.u('f15image1'), six.u('tenant_id'): six.u('7b0a1d73f8fb41718f3343c207597869'), - six.u('instance_id'): - six.u('f079e394-1111-457b-b350-bb5ecc685cdd'), - six.u('vcpus'): 1, six.u('memory_mb'): 512, - six.u('state'): six.u('active'), - six.u('flavor'): six.u('m1.tiny'), - six.u('started_at'): six.u('2012-01-20 18:06:06.479998')}], - six.u('start'): six.u('2011-12-25 19:48:41.750687'), - six.u('total_local_gb_usage'): 0.0}}) - - def get_os_simple_tenant_usage_tenant_id_next(self, **kw): - return (200, {}, {six.u('tenant_usage'): { - six.u('total_memory_mb_usage'): 25451.762807466665, - six.u('total_vcpus_usage'): 49.71047423333333, - six.u('total_hours'): 49.71047423333333, - six.u('tenant_id'): six.u('7b0a1d73f8fb41718f3343c207597869'), - six.u('stop'): six.u('2012-01-22 19:48:41.750722'), - six.u('server_usages'): [{ - six.u('hours'): 49.71047423333333, - six.u('uptime'): 27035, six.u('local_gb'): 0, - six.u('ended_at'): None, - six.u('name'): six.u('f15image1'), - six.u('tenant_id'): six.u('7b0a1d73f8fb41718f3343c207597869'), - six.u('instance_id'): - six.u('f079e394-2222-457b-b350-bb5ecc685cdd'), six.u('vcpus'): 1, six.u('memory_mb'): 512, six.u('state'): six.u('active'), six.u('flavor'): six.u('m1.tiny'), @@ -1709,9 +1638,6 @@ def get_os_simple_tenant_usage_tenant_id_next(self, **kw): six.u('start'): six.u('2011-12-25 19:48:41.750687'), six.u('total_local_gb_usage'): 0.0}}) - def get_os_simple_tenant_usage_tenant_id_next_next(self, **kw): - return (200, {}, {six.u('tenant_usage'): {}}) - # # Aggregates # @@ -2451,7 +2377,6 @@ class FakeSessionMockClient(base_client.SessionClient, FakeHTTPClient): def __init__(self, *args, **kwargs): self.callstack = [] - self.visited = [] self.auth = mock.Mock() self.session = mock.Mock() self.session.get_endpoint.return_value = FakeHTTPClient.get_endpoint( diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 67663b9a7..e71ba0e00 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -50,7 +50,7 @@ def setUp(self): def tearDown(self): # For some method like test_image_meta_bad_action we are # testing a SystemExit to be thrown and object self.shell has - # no time to get instantiated which is OK in this case, so + # no time to get instantatiated which is OK in this case, so # we make sure the method is there before launching it. if hasattr(self.shell, 'cs'): self.shell.cs.clear_callstack() @@ -1750,36 +1750,12 @@ def test_server_floating_ip_disassociate(self): {'removeFloatingIp': {'address': '11.0.0.1'}}) def test_usage_list(self): - cmd = 'usage-list --start 2000-01-20 --end 2005-02-01' - stdout, _ = self.run_command(cmd) + self.run_command('usage-list --start 2000-01-20 --end 2005-02-01') self.assert_called('GET', '/os-simple-tenant-usage?' + 'start=2000-01-20T00:00:00&' + 'end=2005-02-01T00:00:00&' + 'detailed=1') - # Servers, RAM MB-Hours, CPU Hours, Disk GB-Hours - self.assertIn('1 | 25451.76 | 49.71 | 0.00', stdout) - - def test_usage_list_stitch_together_next_results(self): - cmd = 'usage-list --start 2000-01-20 --end 2005-02-01' - stdout, _ = self.run_command(cmd, api_version='2.39') - self.assert_called('GET', - '/os-simple-tenant-usage?' - 'start=2000-01-20T00:00:00&' - 'end=2005-02-01T00:00:00&' - 'detailed=1', pos=0) - markers = [ - 'f079e394-1111-457b-b350-bb5ecc685cdd', - 'f079e394-2222-457b-b350-bb5ecc685cdd', - ] - for pos, marker in enumerate(markers): - self.assert_called('GET', - '/os-simple-tenant-usage?' - 'start=2000-01-20T00:00:00&' - 'end=2005-02-01T00:00:00&' - 'marker=%s&detailed=1' % (marker), pos=pos + 1) - # Servers, RAM MB-Hours, CPU Hours, Disk GB-Hours - self.assertIn('2 | 50903.53 | 99.42 | 0.00', stdout) def test_usage_list_no_args(self): timeutils.set_time_override(datetime.datetime(2005, 2, 1, 0, 0)) @@ -1792,34 +1768,12 @@ def test_usage_list_no_args(self): 'detailed=1') def test_usage(self): - cmd = 'usage --start 2000-01-20 --end 2005-02-01 --tenant test' - stdout, _ = self.run_command(cmd) + self.run_command('usage --start 2000-01-20 --end 2005-02-01 ' + '--tenant test') self.assert_called('GET', '/os-simple-tenant-usage/test?' + 'start=2000-01-20T00:00:00&' + 'end=2005-02-01T00:00:00') - # Servers, RAM MB-Hours, CPU Hours, Disk GB-Hours - self.assertIn('1 | 25451.76 | 49.71 | 0.00', stdout) - - def test_usage_stitch_together_next_results(self): - cmd = 'usage --start 2000-01-20 --end 2005-02-01' - stdout, _ = self.run_command(cmd, api_version='2.39') - self.assert_called('GET', - '/os-simple-tenant-usage/tenant_id?' - 'start=2000-01-20T00:00:00&' - 'end=2005-02-01T00:00:00', pos=0) - markers = [ - 'f079e394-1111-457b-b350-bb5ecc685cdd', - 'f079e394-2222-457b-b350-bb5ecc685cdd', - ] - for pos, marker in enumerate(markers): - self.assert_called('GET', - '/os-simple-tenant-usage/tenant_id?' - 'start=2000-01-20T00:00:00&' - 'end=2005-02-01T00:00:00&' - 'marker=%s' % (marker), pos=pos + 1) - # Servers, RAM MB-Hours, CPU Hours, Disk GB-Hours - self.assertIn('2 | 50903.53 | 99.42 | 0.00', stdout) def test_usage_no_tenant(self): self.run_command('usage --start 2000-01-20 --end 2005-02-01') diff --git a/novaclient/tests/unit/v2/test_usage.py b/novaclient/tests/unit/v2/test_usage.py index 8c18e1526..e67178b41 100644 --- a/novaclient/tests/unit/v2/test_usage.py +++ b/novaclient/tests/unit/v2/test_usage.py @@ -73,52 +73,3 @@ def test_usage_class_get(self): 'GET', "/os-simple-tenant-usage/tenantfoo?start=%s&end=%s" % (start, stop)) - - -class UsageV39Test(UsageTest): - def setUp(self): - super(UsageV39Test, self).setUp() - self.cs.api_version = api_versions.APIVersion('2.39') - - def test_usage_list_with_paging(self): - now = datetime.datetime.now() - usages = self.cs.usage.list(now, now, marker='some-uuid', limit=3) - self.assert_request_id(usages, fakes.FAKE_REQUEST_ID_LIST) - - self.cs.assert_called( - 'GET', - '/os-simple-tenant-usage?' + - ('start=%s&' % now.isoformat()) + - ('end=%s&' % now.isoformat()) + - ('limit=3&marker=some-uuid&detailed=0')) - for u in usages: - self.assertIsInstance(u, usage.Usage) - - def test_usage_list_detailed_with_paging(self): - now = datetime.datetime.now() - usages = self.cs.usage.list( - now, now, detailed=True, marker='some-uuid', limit=3) - self.assert_request_id(usages, fakes.FAKE_REQUEST_ID_LIST) - - self.cs.assert_called( - 'GET', - '/os-simple-tenant-usage?' + - ('start=%s&' % now.isoformat()) + - ('end=%s&' % now.isoformat()) + - ('limit=3&marker=some-uuid&detailed=1')) - for u in usages: - self.assertIsInstance(u, usage.Usage) - - def test_usage_get_with_paging(self): - now = datetime.datetime.now() - u = self.cs.usage.get( - 'tenantfoo', now, now, marker='some-uuid', limit=3) - self.assert_request_id(u, fakes.FAKE_REQUEST_ID_LIST) - - self.cs.assert_called( - 'GET', - '/os-simple-tenant-usage/tenantfoo?' + - ('start=%s&' % now.isoformat()) + - ('end=%s&' % now.isoformat()) + - ('limit=3&marker=some-uuid')) - self.assertIsInstance(u, usage.Usage) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index b3e68df18..d61eae398 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -19,7 +19,6 @@ from __future__ import print_function import argparse -import collections import copy import datetime import functools @@ -3434,43 +3433,7 @@ def simplify_usage(u): setattr(u, simplerows[3], "%.2f" % u.total_vcpus_usage) setattr(u, simplerows[4], "%.2f" % u.total_local_gb_usage) - def get_marker(usage_list): - marker = None - if usage_list: - usage = usage_list[-1] - if hasattr(usage, 'server_usages') and usage.server_usages: - marker = usage.server_usages[-1]['instance_id'] - return marker - - def merge_usage(usages, next_usage_list): - for next_usage in next_usage_list: - if next_usage.tenant_id in usages: - usage = usages[next_usage.tenant_id] - usage.server_usages.extend(next_usage.server_usages) - usage.total_hours += next_usage.total_hours - usage.total_memory_mb_usage += next_usage.total_memory_mb_usage - usage.total_vcpus_usage += next_usage.total_vcpus_usage - usage.total_local_gb_usage += next_usage.total_local_gb_usage - else: - usages[next_usage.tenant_id] = next_usage - - if cs.api_version < api_versions.APIVersion('2.39'): - usage_list = cs.usage.list(start, end, detailed=True) - else: - # If the number of instances used to calculate the usage is greater - # than osapi_max_limit, the usage will be split across multiple - # requests and the responses will need to be merged back together. - usages = collections.OrderedDict() - usage_list = cs.usage.list(start, end, detailed=True) - merge_usage(usages, usage_list) - marker = get_marker(usage_list) - while marker: - next_usage_list = cs.usage.list( - start, end, detailed=True, marker=marker) - marker = get_marker(next_usage_list) - if marker: - merge_usage(usages, next_usage_list) - usage_list = usages.values() + usage_list = cs.usage.list(start, end, detailed=True) print(_("Usage from %(start)s to %(end)s:") % {'start': start.strftime(dateformat), @@ -3521,41 +3484,15 @@ def simplify_usage(u): setattr(u, simplerows[2], "%.2f" % u.total_vcpus_usage) setattr(u, simplerows[3], "%.2f" % u.total_local_gb_usage) - def get_marker(usage): - marker = None - if hasattr(usage, 'server_usages') and usage.server_usages: - marker = usage.server_usages[-1]['instance_id'] - return marker - - def merge_usage(usage, next_usage): - usage.server_usages.extend(next_usage.server_usages) - usage.total_hours += next_usage.total_hours - usage.total_memory_mb_usage += next_usage.total_memory_mb_usage - usage.total_vcpus_usage += next_usage.total_vcpus_usage - usage.total_local_gb_usage += next_usage.total_local_gb_usage - if args.tenant: - tenant_id = args.tenant + usage = cs.usage.get(args.tenant, start, end) else: if isinstance(cs.client, client.SessionClient): auth = cs.client.auth - tenant_id = auth.get_auth_ref(cs.client.session).project_id + project_id = auth.get_auth_ref(cs.client.session).project_id + usage = cs.usage.get(project_id, start, end) else: - tenant_id = cs.client.tenant_id - - if cs.api_version < api_versions.APIVersion('2.39'): - usage = cs.usage.get(tenant_id, start, end) - else: - # If the number of instances used to calculate the usage is greater - # than osapi_max_limit, the usage will be split across multiple - # requests and the responses will need to be merged back together. - usage = cs.usage.get(tenant_id, start, end) - marker = get_marker(usage) - while marker: - next_usage = cs.usage.get(tenant_id, start, end, marker=marker) - marker = get_marker(next_usage) - if marker: - merge_usage(usage, next_usage) + usage = cs.usage.get(cs.client.tenant_id, start, end) print(_("Usage from %(start)s to %(end)s:") % {'start': start.strftime(dateformat), diff --git a/novaclient/v2/usage.py b/novaclient/v2/usage.py index 199e3111a..dacf87b55 100644 --- a/novaclient/v2/usage.py +++ b/novaclient/v2/usage.py @@ -17,7 +17,6 @@ import oslo_utils -from novaclient import api_versions from novaclient import base @@ -47,19 +46,7 @@ class UsageManager(base.ManagerWithFind): Manage :class:`Usage` resources. """ resource_class = Usage - usage_prefix = 'os-simple-tenant-usage' - def _usage_query(self, start, end, marker=None, limit=None, detailed=None): - query = "?start=%s&end=%s" % (start.isoformat(), end.isoformat()) - if limit: - query = "%s&limit=%s" % (query, int(limit)) - if marker: - query = "%s&marker=%s" % (query, marker) - if detailed is not None: - query = "%s&detailed=%s" % (query, int(bool(detailed))) - return query - - @api_versions.wraps("2.0", "2.38") def list(self, start, end, detailed=False): """ Get usage for all tenants @@ -70,31 +57,11 @@ def list(self, start, end, detailed=False): instance whose usage is part of the report :rtype: list of :class:`Usage`. """ - query_string = self._usage_query(start, end, detailed=detailed) - url = '/%s%s' % (self.usage_prefix, query_string) - return self._list(url, 'tenant_usages') - - @api_versions.wraps("2.39") - def list(self, start, end, detailed=False, marker=None, limit=None): - """ - Get usage for all tenants - - :param start: :class:`datetime.datetime` Start date in UTC - :param end: :class:`datetime.datetime` End date in UTC - :param detailed: Whether to include information about each - instance whose usage is part of the report - :param marker: Begin returning usage data for instances that appear - later in the instance list than that represented by - this instance UUID (optional). - :param limit: Maximum number of instances to include in the usage - (optional). - :rtype: list of :class:`Usage`. - """ - query_string = self._usage_query(start, end, marker, limit, detailed) - url = '/%s%s' % (self.usage_prefix, query_string) - return self._list(url, 'tenant_usages') + return self._list( + "/os-simple-tenant-usage?start=%s&end=%s&detailed=%s" % + (start.isoformat(), end.isoformat(), int(bool(detailed))), + "tenant_usages") - @api_versions.wraps("2.0", "2.38") def get(self, tenant_id, start, end): """ Get usage for a specific tenant. @@ -104,25 +71,6 @@ def get(self, tenant_id, start, end): :param end: :class:`datetime.datetime` End date in UTC :rtype: :class:`Usage` """ - query_string = self._usage_query(start, end) - url = '/%s/%s%s' % (self.usage_prefix, tenant_id, query_string) - return self._get(url, 'tenant_usage') - - @api_versions.wraps("2.39") - def get(self, tenant_id, start, end, marker=None, limit=None): - """ - Get usage for a specific tenant. - - :param tenant_id: Tenant ID to fetch usage for - :param start: :class:`datetime.datetime` Start date in UTC - :param end: :class:`datetime.datetime` End date in UTC - :param marker: Begin returning usage data for instances that appear - later in the instance list than that represented by - this instance UUID (optional). - :param limit: Maximum number of instances to include in the usage - (optional). - :rtype: :class:`Usage` - """ - query_string = self._usage_query(start, end, marker, limit) - url = '/%s/%s%s' % (self.usage_prefix, tenant_id, query_string) - return self._get(url, 'tenant_usage') + return self._get("/os-simple-tenant-usage/%s?start=%s&end=%s" % + (tenant_id, start.isoformat(), end.isoformat()), + "tenant_usage") diff --git a/releasenotes/notes/microversion-v2_39-484adba0806b08bf.yaml b/releasenotes/notes/microversion-v2_39-484adba0806b08bf.yaml deleted file mode 100644 index f81827188..000000000 --- a/releasenotes/notes/microversion-v2_39-484adba0806b08bf.yaml +++ /dev/null @@ -1,4 +0,0 @@ ---- -features: - - Added microversion v2.39 which introduces pagination support for usage - with the help of new optional parameters 'limit' and 'marker'. From ce184797f07675d403245a541de42cd48450d320 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Antal?= Date: Mon, 5 Sep 2016 16:08:04 +0200 Subject: [PATCH 1183/1705] Use more specific asserts in tests I changed asserts with more specific assert methods. e.g.: from assertTrue(sth == None) to assertIsNone(*) or assertTrue(isinstance(inst, type)) to assertIsInstace(inst, type) or assertTrue(not sth) to assertFalse(sth). The code gets more readable, and a better description will be shown on fail. Change-Id: I8be876e0d2a1ea5d6f8454ca973e2d874d10283d --- novaclient/tests/unit/test_api_versions.py | 12 ++++++------ novaclient/tests/unit/test_shell.py | 4 ++-- .../tests/unit/v2/contrib/test_tenant_networks.py | 2 +- novaclient/tests/unit/v2/test_list_extensions.py | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/novaclient/tests/unit/test_api_versions.py b/novaclient/tests/unit/test_api_versions.py index 00eb2b6f8..89d806072 100644 --- a/novaclient/tests/unit/test_api_versions.py +++ b/novaclient/tests/unit/test_api_versions.py @@ -85,12 +85,12 @@ def test_version_comparisons(self): v4 = api_versions.APIVersion("2.0") v_null = api_versions.APIVersion() - self.assertTrue(v1 < v2) - self.assertTrue(v3 > v2) - self.assertTrue(v1 != v2) - self.assertTrue(v1 == v4) - self.assertTrue(v1 != v_null) - self.assertTrue(v_null == v_null) + self.assertTrue(v1.__lt__(v2)) + self.assertTrue(v3.__gt__(v2)) + self.assertTrue(v1.__ne__(v2)) + self.assertTrue(v1.__eq__(v4)) + self.assertTrue(v1.__ne__(v_null)) + self.assertTrue(v_null.__eq__(v_null)) self.assertRaises(TypeError, v1.__le__, "2.1") def test_version_matches(self): diff --git a/novaclient/tests/unit/test_shell.py b/novaclient/tests/unit/test_shell.py index 0a74a93dc..7447857c5 100644 --- a/novaclient/tests/unit/test_shell.py +++ b/novaclient/tests/unit/test_shell.py @@ -173,7 +173,7 @@ def test_init_action_nothing(self, mock_init): self.assertEqual(result.emitted, set()) self.assertIsNone(result.use) - self.assertEqual(result.real_action_args, False) + self.assertIs(result.real_action_args, False) self.assertIsNone(result.real_action) mock_init.assert_called_once_with( 'option_strings', 'dest', help='Deprecated', a=1, b=2, c=3) @@ -200,7 +200,7 @@ def test_init_action_other(self, mock_init): self.assertEqual(result.emitted, set()) self.assertIsNone(result.use) - self.assertEqual(result.real_action_args, False) + self.assertIs(result.real_action_args, False) self.assertEqual(result.real_action, action.return_value) mock_init.assert_called_once_with( 'option_strings', 'dest', help='Deprecated', a=1, b=2, c=3) diff --git a/novaclient/tests/unit/v2/contrib/test_tenant_networks.py b/novaclient/tests/unit/v2/contrib/test_tenant_networks.py index 99e0b2704..ba3998beb 100644 --- a/novaclient/tests/unit/v2/contrib/test_tenant_networks.py +++ b/novaclient/tests/unit/v2/contrib/test_tenant_networks.py @@ -35,7 +35,7 @@ def test_list_tenant_networks(self): nets = self.cs.tenant_networks.list() self.assert_request_id(nets, fakes.FAKE_REQUEST_ID_LIST) self.cs.assert_called('GET', '/os-tenant-networks') - self.assertTrue(len(nets) > 0) + self.assertGreater(len(nets), 0) def test_get_tenant_network(self): net = self.cs.tenant_networks.get(1) diff --git a/novaclient/tests/unit/v2/test_list_extensions.py b/novaclient/tests/unit/v2/test_list_extensions.py index f473ed753..60783b31a 100644 --- a/novaclient/tests/unit/v2/test_list_extensions.py +++ b/novaclient/tests/unit/v2/test_list_extensions.py @@ -25,6 +25,6 @@ def test_list_extensions(self): all_exts = self.cs.list_extensions.show_all() self.assert_request_id(all_exts, fakes.FAKE_REQUEST_ID_LIST) self.cs.assert_called('GET', '/extensions') - self.assertTrue(len(all_exts) > 0) + self.assertGreater(len(all_exts), 0) for r in all_exts: - self.assertTrue(len(r.summary) > 0) + self.assertGreater(len(r.summary), 0) From 244675560673fefb80131d66552d2f58d4610fed Mon Sep 17 00:00:00 2001 From: int32bit Date: Sat, 26 Nov 2016 21:49:50 +0800 Subject: [PATCH 1184/1705] Fix can't process the resource with name 'help' novaclient can't process the resource if the name is 'help', like 'nova show help', 'nova flavor-show help', 'nova delete help', etc. We should encourage user to use 'nova help xxxx' to show subcommand usage, rather than 'nova xxxx help`. Change-Id: I65204a908dc2378b6264297ddd87fcb8912a5770 Closes-Bug: #1641048 --- novaclient/shell.py | 3 +-- novaclient/tests/unit/v2/fakes.py | 19 ++++++++++++++++++- novaclient/tests/unit/v2/test_shell.py | 4 ++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/novaclient/shell.py b/novaclient/shell.py index eb802fdb9..d4fa0a574 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -621,8 +621,7 @@ def main(self, argv): self.setup_debugging(args.debug) self.extensions = [] - do_help = ('help' in argv) or ( - '--help' in argv) or ('-h' in argv) or not argv + do_help = args.help or not args_list or args_list[0] == 'help' # bash-completion should not require authentication skip_auth = do_help or ( diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 0e9819951..1288b059b 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -389,7 +389,8 @@ def get_limits(self, **kw): def get_servers(self, **kw): return (200, {}, {"servers": [ {'id': '1234', 'name': 'sample-server'}, - {'id': '5678', 'name': 'sample-server2'} + {'id': '5678', 'name': 'sample-server2'}, + {'id': '9014', 'name': 'help'} ]}) def get_servers_detail(self, **kw): @@ -520,6 +521,18 @@ def get_servers_detail(self, **kw): "hostId": "9e107d9d372bb6826bd81d3542a419d6", "status": "ACTIVE", }, + { + "id": '9014', + "name": "help", + "flavor": { + "id": '80645cf4-6ad3-410a-bbc8-6f3e1e291f51', + }, + "image": { + "id": '3e861307-73a6-4d1f-8d68-f68b03223032', + }, + "hostId": "9e107d9d372bb6826bd81d3542a419d6", + "status": "ACTIVE", + }, ]}) def post_servers(self, body, **kw): @@ -577,6 +590,10 @@ def get_servers_9013(self, **kw): r = {'server': self.get_servers_detail()[2]['servers'][3]} return (200, {}, r) + def get_servers_9014(self, **kw): + r = {'server': self.get_servers_detail()[2]['servers'][4]} + return (200, {}, r) + def delete_os_server_groups_12345(self, **kw): return (202, {}, None) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 6f5069116..5ae820e97 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -1524,6 +1524,10 @@ def test_show_unavailable_image_and_flavor(self): self.assertIn('Image not found', output) self.assertIn('Flavor not found', output) + def test_show_with_name_help(self): + output, _ = self.run_command('show help') + self.assert_called('GET', '/servers/9014', pos=-6) + @mock.patch('novaclient.v2.shell.utils.print_dict') def test_print_server(self, mock_print_dict): self.run_command('show 5678') From 29c880ceb668d3bddda3dac6fa190d3040597d7e Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Wed, 7 Dec 2016 03:16:54 +0000 Subject: [PATCH 1185/1705] Updated from global requirements Change-Id: I68b5b9cb461037a301eb1b8c698363f67e6a51aa --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 9fa36a6f0..97a350fab 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -8,7 +8,7 @@ coverage>=4.0 # Apache-2.0 fixtures>=3.0.0 # Apache-2.0/BSD keyring>=5.5.1 # MIT/PSF mock>=2.0 # BSD -python-keystoneclient>=3.6.0 # Apache-2.0 +python-keystoneclient>=3.8.0 # Apache-2.0 python-cinderclient!=1.7.0,!=1.7.1,>=1.6.0 # Apache-2.0 python-glanceclient>=2.5.0 # Apache-2.0 requests-mock>=1.1 # Apache-2.0 From a58959dd9ec3962cbb6d273f136286809678b0c4 Mon Sep 17 00:00:00 2001 From: Kevin_Zheng Date: Fri, 25 Nov 2016 16:55:18 +0800 Subject: [PATCH 1186/1705] Check source_type and destination_type when booting with bdm provided When booting instance with block_device_mapping provided, in the current implementation, the "destination_type" is allowed to be None, and this lead to un-sync between Nova and Cinder: Nova will have a bdm in its record while the volume is still in "available" status in cinder. Also we should check source_type here This patch adds a check for source_type and destination_type, source_type shoud be within 'volume', 'image', 'snapshot' or 'blank'; When empty destination_type is provided, it will be assigned to an default value according to source_type, if it is provided with a wrong value, command error will raise and information will shown. Change-Id: If6c7d8ed58a94d262ac6c300e75aca4cdf99ff1d partial-bug: #1644725 --- novaclient/tests/unit/v2/test_shell.py | 86 ++++++++++++++++++++++++++ novaclient/v2/shell.py | 25 ++++++++ 2 files changed, 111 insertions(+) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 9e4494439..642f2df47 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -349,6 +349,92 @@ def test_boot_image_bdms_v2(self): }}, ) + def test_boot_image_bdms_v2_wrong_source_type(self): + self.assertRaises( + exceptions.CommandError, self.run_command, + 'boot --flavor 1 --image %s --block-device id=fake-id,' + 'source=fake,device=vda,size=1,format=ext4,' + 'type=disk,shutdown=preserve some-server' % FAKE_UUID_1) + + def test_boot_image_bdms_v2_no_source_type_no_destination_type(self): + self.run_command( + 'boot --flavor 1 --image %s --block-device id=fake-id,' + 'device=vda,size=1,format=ext4,' + 'type=disk,shutdown=preserve some-server' % FAKE_UUID_1 + ) + self.assert_called_anytime( + 'POST', '/os-volumes_boot', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'block_device_mapping_v2': [ + { + 'uuid': FAKE_UUID_1, + 'source_type': 'image', + 'destination_type': 'local', + 'boot_index': 0, + 'delete_on_termination': True, + }, + { + 'uuid': 'fake-id', + 'source_type': 'blank', + 'destination_type': 'local', + 'device_name': 'vda', + 'volume_size': '1', + 'guest_format': 'ext4', + 'device_type': 'disk', + 'delete_on_termination': False, + }, + ], + 'imageRef': FAKE_UUID_1, + 'min_count': 1, + 'max_count': 1, + }}, + ) + + def test_boot_image_bdms_v2_no_destination_type(self): + self.run_command( + 'boot --flavor 1 --image %s --block-device id=fake-id,' + 'source=volume,device=vda,size=1,format=ext4,' + 'type=disk,shutdown=preserve some-server' % FAKE_UUID_1 + ) + self.assert_called_anytime( + 'POST', '/os-volumes_boot', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'block_device_mapping_v2': [ + { + 'uuid': FAKE_UUID_1, + 'source_type': 'image', + 'destination_type': 'local', + 'boot_index': 0, + 'delete_on_termination': True, + }, + { + 'uuid': 'fake-id', + 'source_type': 'volume', + 'destination_type': 'volume', + 'device_name': 'vda', + 'volume_size': '1', + 'guest_format': 'ext4', + 'device_type': 'disk', + 'delete_on_termination': False, + }, + ], + 'imageRef': FAKE_UUID_1, + 'min_count': 1, + 'max_count': 1, + }}, + ) + + def test_boot_image_bdms_v2_wrong_destination_type(self): + self.assertRaises( + exceptions.CommandError, self.run_command, + 'boot --flavor 1 --image %s --block-device id=fake-id,' + 'source=volume,dest=dest1,device=vda,size=1,format=ext4,' + 'type=disk,shutdown=preserve some-server' % FAKE_UUID_1) + def test_boot_image_bdms_v2_with_tag(self): self.run_command( 'boot --flavor 1 --image %s --block-device id=fake-id,' diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 7bd32ffb4..41bd76ea2 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -157,6 +157,31 @@ def _parse_block_device_mapping_v2(args, image): for key, value in six.iteritems(spec_dict): bdm_dict[CLIENT_BDM2_KEYS[key]] = value + source_type = bdm_dict.get('source_type') + if not source_type: + bdm_dict['source_type'] = 'blank' + elif source_type not in ( + 'volume', 'image', 'snapshot', 'blank'): + raise exceptions.CommandError( + _("The value of source_type key of --block-device " + "should be one of 'volume', 'image', 'snapshot' " + "or 'blank' but it was '%(action)s'") + % {'action': source_type}) + + destination_type = bdm_dict.get('destination_type') + if not destination_type: + source_type = bdm_dict['source_type'] + if source_type in ('image', 'blank'): + bdm_dict['destination_type'] = 'local' + if source_type in ('snapshot', 'volume'): + bdm_dict['destination_type'] = 'volume' + elif destination_type not in ('local', 'volume'): + raise exceptions.CommandError( + _("The value of destination_type key of --block-device " + "should be either 'local' or 'volume' but it " + "was '%(action)s'") + % {'action': destination_type}) + # Convert the delete_on_termination to a boolean or set it to true by # default for local block devices when not specified. if 'delete_on_termination' in bdm_dict: From c1c69f6d3ee74ca8e08a8e19bd7e14197e8e3348 Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Wed, 7 Dec 2016 18:28:18 +0900 Subject: [PATCH 1187/1705] Fix doc generation errors The following files have been removed in I09a6501603667350f49b1b1fa130353a6d5272a2. * novaclient/v2/volume_types.py * novaclient/v2/volume_snapshots.py But doc/source/conf.py has not been fixed. So fix it. And return parameter format in novaclient/v2/hosts.py is fixed. Change-Id: Icd3c5aed776df291141b8c3df4d64aa234e4ed88 Closes-Bug: #1648007 --- doc/source/conf.py | 2 +- novaclient/v2/hosts.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index 1e11705bf..a6e988e65 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -77,7 +77,7 @@ def gen_ref(ver, title, names): "floating_ip_pools", "floating_ips", "hypervisors", "keypairs", "limits", "networks", "quota_classes", "quotas", "security_group_rules", "security_groups", "services", - "virtual_interfaces", "volume_snapshots", "volumes", "volume_types"]) + "virtual_interfaces", "volumes"]) # -- General configuration ---------------------------------------------------- diff --git a/novaclient/v2/hosts.py b/novaclient/v2/hosts.py index f97818a0d..2eceafdf6 100644 --- a/novaclient/v2/hosts.py +++ b/novaclient/v2/hosts.py @@ -74,7 +74,7 @@ def host_action(self, host, action): :param host: The host to perform an action :param action: The action to perform - returns: An instance of novaclient.base.TupleWithMeta + :returns: An instance of novaclient.base.TupleWithMeta """ url = '/os-hosts/%s/%s' % (host, action) resp, body = self.api.client.get(url) From b7edc92d81b0a0ffc3d7e32af7f7433eea4dcbc7 Mon Sep 17 00:00:00 2001 From: int32bit Date: Wed, 7 Dec 2016 13:37:50 +0800 Subject: [PATCH 1188/1705] Add min-disk and min-ram filters to list flavors The Nova API allows user filter flavors by minDisk and minRam, but that is not implemented in the current novaclient yet. This patch introduce '--min-disk' and '--min-ram' options to support it. In addition, We also append '--sort-key' and '--sort-dir' to the flavor-list subcommand to support sort the result list. Change-Id: I3849cc2d73057170dc224fa86b724f5141ca9a23 Closes-Bug: #1647867 --- novaclient/tests/unit/v2/test_flavors.py | 10 ++++++++ novaclient/tests/unit/v2/test_shell.py | 12 +++++++++ novaclient/v2/flavors.py | 10 ++++++-- novaclient/v2/shell.py | 32 ++++++++++++++++++++++-- 4 files changed, 60 insertions(+), 4 deletions(-) diff --git a/novaclient/tests/unit/v2/test_flavors.py b/novaclient/tests/unit/v2/test_flavors.py index 14bf78b3d..c882ddb38 100644 --- a/novaclient/tests/unit/v2/test_flavors.py +++ b/novaclient/tests/unit/v2/test_flavors.py @@ -51,6 +51,16 @@ def test_list_flavors_with_marker_limit(self): self.assert_request_id(fl, fakes.FAKE_REQUEST_ID_LIST) self.cs.assert_called('GET', '/flavors/detail?limit=4&marker=1234') + def test_list_flavors_with_min_disk(self): + fl = self.cs.flavors.list(min_disk=20) + self.assert_request_id(fl, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called('GET', '/flavors/detail?minDisk=20') + + def test_list_flavors_with_min_ram(self): + fl = self.cs.flavors.list(min_ram=512) + self.assert_request_id(fl, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called('GET', '/flavors/detail?minRam=512') + def test_list_flavors_with_sort_key_dir(self): fl = self.cs.flavors.list(sort_key='id', sort_dir='asc') self.assert_request_id(fl, fakes.FAKE_REQUEST_ID_LIST) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 6f5069116..57fd59013 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -1033,6 +1033,18 @@ def test_flavor_list_with_limit_and_marker(self): self.run_command('flavor-list --marker 1 --limit 2') self.assert_called('GET', '/flavors/detail?limit=2&marker=1') + def test_flavor_list_with_min_disk(self): + self.run_command('flavor-list --min-disk 20') + self.assert_called('GET', '/flavors/detail?minDisk=20') + + def test_flavor_list_with_min_ram(self): + self.run_command('flavor-list --min-ram 512') + self.assert_called('GET', '/flavors/detail?minRam=512') + + def test_flavor_list_with_sort_key_dir(self): + self.run_command('flavor-list --sort-key id --sort-dir asc') + self.assert_called('GET', '/flavors/detail?sort_dir=asc&sort_key=id') + def test_flavor_show(self): self.run_command('flavor-show 1') self.assert_called_anytime('GET', '/flavors/1') diff --git a/novaclient/v2/flavors.py b/novaclient/v2/flavors.py index 43801d17d..a0a7b6f95 100644 --- a/novaclient/v2/flavors.py +++ b/novaclient/v2/flavors.py @@ -92,8 +92,8 @@ class FlavorManager(base.ManagerWithFind): resource_class = Flavor is_alphanum_id_allowed = True - def list(self, detailed=True, is_public=True, marker=None, limit=None, - sort_key=None, sort_dir=None): + def list(self, detailed=True, is_public=True, marker=None, min_disk=None, + min_ram=None, limit=None, sort_key=None, sort_dir=None): """Get a list of all flavors. :param detailed: Whether flavor needs to be return with details @@ -103,6 +103,8 @@ def list(self, detailed=True, is_public=True, marker=None, limit=None, access to all flavor types. :param marker: Begin returning flavors that appear later in the flavor list than that represented by this flavor id (optional). + :param min_disk: Filters the flavors by a minimum disk space, in GiB. + :param min_ram: Filters the flavors by a minimum RAM, in MB. :param limit: maximum number of flavors to return (optional). :param sort_key: Flavors list sort key (optional). :param sort_dir: Flavors list sort direction (optional). @@ -114,6 +116,10 @@ def list(self, detailed=True, is_public=True, marker=None, limit=None, # and flavors from their own projects only. if marker: qparams['marker'] = str(marker) + if min_disk: + qparams['minDisk'] = int(min_disk) + if min_ram: + qparams['minRam'] = int(min_ram) if limit: qparams['limit'] = int(limit) if sort_key: diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 08e6f7055..4e8160b39 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -864,6 +864,18 @@ def _print_flavor_list(flavors, show_extra_specs=False): default=None, help=_('The last flavor ID of the previous page; displays list of flavors' ' after "marker".')) +@utils.arg( + '--min-disk', + dest='min_disk', + metavar='', + default=None, + help=_('Filters the flavors by a minimum disk space, in GiB.')) +@utils.arg( + '--min-ram', + dest='min_ram', + metavar='', + default=None, + help=_('Filters the flavors by a minimum RAM, in MB.')) @utils.arg( '--limit', dest='limit', @@ -873,12 +885,28 @@ def _print_flavor_list(flavors, show_extra_specs=False): help=_("Maximum number of flavors to display. If limit is bigger than " "'osapi_max_limit' option of Nova API, limit 'osapi_max_limit' " "will be used instead.")) +@utils.arg( + '--sort-key', + dest='sort_key', + metavar='', + default=None, + help=_('Flavors list sort key.')) +@utils.arg( + '--sort-dir', + dest='sort_dir', + metavar='', + default=None, + help=_('Flavors list sort direction.')) def do_flavor_list(cs, args): """Print a list of available 'flavors' (sizes of servers).""" if args.all: - flavors = cs.flavors.list(is_public=None) + flavors = cs.flavors.list(is_public=None, min_disk=args.min_disk, + min_ram=args.min_ram, sort_key=args.sort_key, + sort_dir=args.sort_dir) else: - flavors = cs.flavors.list(marker=args.marker, limit=args.limit) + flavors = cs.flavors.list(marker=args.marker, min_disk=args.min_disk, + min_ram=args.min_ram, sort_key=args.sort_key, + sort_dir=args.sort_dir, limit=args.limit) _print_flavor_list(flavors, args.extra_specs) From 8a8540303f56f08cd28105f39c27f07ecffd7127 Mon Sep 17 00:00:00 2001 From: Pavel Kholkin Date: Thu, 8 Dec 2016 10:49:56 +0300 Subject: [PATCH 1189/1705] remove variable '_' from tests TrivialFix Change-Id: I9fa3b55d8d5a281b9b17becae1818e2def5b877c --- novaclient/tests/unit/test_shell.py | 2 +- novaclient/tests/unit/v2/test_shell.py | 31 +++++++++++++------------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/novaclient/tests/unit/test_shell.py b/novaclient/tests/unit/test_shell.py index 0a74a93dc..b42ddeb61 100644 --- a/novaclient/tests/unit/test_shell.py +++ b/novaclient/tests/unit/test_shell.py @@ -601,7 +601,7 @@ def _test_service_type(self, version, service_type, mock_client): (service_type, version)) self.make_env() self.shell(cmd) - _, client_kwargs = mock_client.call_args_list[0] + _client_args, client_kwargs = mock_client.call_args_list[0] self.assertEqual(service_type, client_kwargs['service_type']) @mock.patch('novaclient.client.Client') diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 1b3af35b6..1a6be78ff 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -1070,7 +1070,7 @@ def test_flavor_access_list_flavor(self): def test_flavor_access_list_bad_filter(self): cmd = 'flavor-access-list --flavor 2 --tenant proj2' - _, err = self.run_command(cmd) + _out, err = self.run_command(cmd) # assert the deprecation warning for using --tenant self.assertIn('WARNING: Option "--tenant" is deprecated', err) @@ -1103,13 +1103,13 @@ def test_flavor_access_remove_by_name(self): {'removeTenantAccess': {'tenant': 'proj2'}}) def test_image_show(self): - _, err = self.run_command('image-show %s' % FAKE_UUID_1) + _out, err = self.run_command('image-show %s' % FAKE_UUID_1) self.assertIn('Command image-show is deprecated', err) self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1) def test_image_meta_set(self): - _, err = self.run_command('image-meta %s set test_key=test_value' % - FAKE_UUID_1) + _out, err = self.run_command('image-meta %s set test_key=test_value' % + FAKE_UUID_1) self.assertIn('Command image-meta is deprecated', err) self.assert_called('POST', '/images/%s/metadata' % FAKE_UUID_1, {'metadata': {'test_key': 'test_value'}}) @@ -1147,7 +1147,7 @@ def test_create_image_with_metadata(self): ) def test_create_image_show(self): - output, _ = self.run_command( + output, _err = self.run_command( 'image-create sample-server mysnapshot --show') self.assert_called_anytime( 'POST', '/servers/1234/action', @@ -1176,7 +1176,7 @@ def test_create_image_with_poll_to_check_image_state_deleted(self): 'image-create sample-server mysnapshot_deleted --poll') def test_image_delete(self): - _, err = self.run_command('image-delete %s' % FAKE_UUID_1) + _out, err = self.run_command('image-delete %s' % FAKE_UUID_1) self.assertIn('Command image-delete is deprecated', err) self.assert_called('DELETE', '/images/%s' % FAKE_UUID_1) @@ -1274,7 +1274,7 @@ def test_list_sortby_index_without_sort(self): mock.ANY, mock.ANY, mock.ANY, sortby_index=1) def test_list_fields(self): - output, _ = self.run_command( + output, _err = self.run_command( 'list --fields ' 'host,security_groups,OS-EXT-MOD:some_thing') self.assert_called('GET', '/servers/detail') @@ -1307,7 +1307,7 @@ def test_list_with_changes_since_invalid_value(self): self.run_command, 'list --changes-since 0123456789') def test_list_fields_redundant(self): - output, __ = self.run_command('list --fields id,status,status') + output, _err = self.run_command('list --fields id,status,status') header = output.splitlines()[1] self.assertEqual(1, header.count('ID')) self.assertEqual(0, header.count('Id')) @@ -1335,7 +1335,8 @@ def test_reboot_many(self): {'reboot': {'type': 'SOFT'}}, pos=-1) def test_rebuild(self): - output, _ = self.run_command('rebuild sample-server %s' % FAKE_UUID_1) + output, _err = self.run_command('rebuild sample-server %s' + % FAKE_UUID_1) self.assert_called('GET', '/servers?name=sample-server', pos=0) self.assert_called('GET', '/servers/1234', pos=1) self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=2) @@ -1346,9 +1347,9 @@ def test_rebuild(self): self.assertIn('adminPass', output) def test_rebuild_password(self): - output, _ = self.run_command('rebuild sample-server %s' - ' --rebuild-password asdf' - % FAKE_UUID_1) + output, _err = self.run_command('rebuild sample-server %s' + ' --rebuild-password asdf' + % FAKE_UUID_1) self.assert_called('GET', '/servers?name=sample-server', pos=0) self.assert_called('GET', '/servers/1234', pos=1) self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=2) @@ -2461,7 +2462,7 @@ def test_network_list(self): self.assert_called('GET', '/os-networks') def test_network_list_fields(self): - output, _ = self.run_command( + output, _err = self.run_command( 'network-list --fields ' 'vlan,project_id') self.assert_called('GET', '/os-networks') @@ -2474,7 +2475,7 @@ def test_network_list_invalid_fields(self): 'network-list --fields vlan,project_id,invalid') def test_network_list_redundant_fields(self): - output, __ = self.run_command( + output, _err = self.run_command( 'network-list --fields label,project_id,project_id') header = output.splitlines()[1] self.assertEqual(1, header.count('Label')) @@ -2640,7 +2641,7 @@ def test_limits(self): self.run_command('limits --tenant 1234') self.assert_called('GET', '/limits?tenant_id=1234') - stdout, _ = self.run_command('limits --tenant 1234') + stdout, _err = self.run_command('limits --tenant 1234') self.assertIn('Verb', stdout) self.assertIn('Name', stdout) From 4b63110ff7add44a6097f2a5d032161c393519d5 Mon Sep 17 00:00:00 2001 From: Diana Clarke Date: Wed, 7 Dec 2016 12:28:04 -0500 Subject: [PATCH 1190/1705] Usage missing from generated docs Simple Tenant Usage is missing from the generated novaclient Python API reference docs [1]. [1] http://docs.openstack.org/developer/python-novaclient/api.html#reference Change-Id: I7f020cb1e6f42bdb559f5ac3d79b6f96bb927eb2 --- doc/source/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index a6e988e65..ee5c65258 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -76,7 +76,7 @@ def gen_ref(ver, title, names): "aggregates", "availability_zones", "certs", "fixed_ips", "floating_ip_pools", "floating_ips", "hypervisors", "keypairs", "limits", "networks", "quota_classes", "quotas", - "security_group_rules", "security_groups", "services", + "security_group_rules", "security_groups", "services", "usage", "virtual_interfaces", "volumes"]) # -- General configuration ---------------------------------------------------- From 56bb3dae5ff2a3a51e54d8fa42277602e7b50710 Mon Sep 17 00:00:00 2001 From: gengchc2 Date: Fri, 9 Dec 2016 11:04:16 +0800 Subject: [PATCH 1191/1705] Replace six.iteritems() with .items() 1.As mentioned in [1], we should avoid using six.iteritems to achieve iterators. We can use dict.items instead, as it will return iterators in PY3 as well. And dict.items/keys will more readable. 2.In py2, the performance about list should be negligible, see the link [2]. [1] https://wiki.openstack.org/wiki/Python3 [2] http://lists.openstack.org/pipermail/openstack-dev/2015-June/066391.html Change-Id: I6289f46344876d19d9a6793875f896cedc85a01c --- novaclient/base.py | 4 ++-- novaclient/utils.py | 4 ++-- novaclient/v2/security_groups.py | 3 +-- novaclient/v2/servers.py | 2 +- novaclient/v2/shell.py | 2 +- 5 files changed, 7 insertions(+), 8 deletions(-) diff --git a/novaclient/base.py b/novaclient/base.py index 6e9fdcdff..a091f0b66 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -162,7 +162,7 @@ def human_id(self): return None def _add_details(self, info): - for (k, v) in six.iteritems(info): + for (k, v) in info.items(): try: setattr(self, k, v) self._info[k] = v @@ -488,7 +488,7 @@ def _parse_block_device_mapping(self, block_device_mapping): bdm = [] - for device_name, mapping in six.iteritems(block_device_mapping): + for device_name, mapping in block_device_mapping.items(): # # The mapping is in the format: # :[]:[]:[] diff --git a/novaclient/utils.py b/novaclient/utils.py index a3c4e6f06..7486ab29c 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -207,7 +207,7 @@ def _flatten(data, prefix=None): """ if isinstance(data, dict): - for key, value in six.iteritems(data): + for key, value in data.items(): new_key = '%s_%s' % (prefix, key) if prefix else key if isinstance(value, (dict, list)) and value: for item in _flatten(value, new_key): @@ -230,7 +230,7 @@ def flatten_dict(data): """ data = data.copy() # Try and decode any nested JSON structures. - for key, value in six.iteritems(data): + for key, value in data.items(): if isinstance(value, six.string_types): try: data[key] = json.loads(value) diff --git a/novaclient/v2/security_groups.py b/novaclient/v2/security_groups.py index 88198ec75..5a87244e4 100644 --- a/novaclient/v2/security_groups.py +++ b/novaclient/v2/security_groups.py @@ -17,7 +17,6 @@ Security group interface (1.1 extension). """ -import six from six.moves.urllib import parse from novaclient import api_versions @@ -108,7 +107,7 @@ def list(self, search_opts=None): """ search_opts = search_opts or {} - qparams = dict((k, v) for (k, v) in six.iteritems(search_opts) if v) + qparams = dict((k, v) for (k, v) in search_opts.items() if v) query_string = '?%s' % parse.urlencode(qparams) if qparams else '' diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index e24fa43a5..ee828fdcf 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -795,7 +795,7 @@ def list(self, detailed=True, search_opts=None, marker=None, limit=None, qparams = {} - for opt, val in six.iteritems(search_opts): + for opt, val in search_opts.items(): if val: if isinstance(val, six.text_type): val = val.encode('utf-8') diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 1d471bb53..22aa1c170 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -155,7 +155,7 @@ def _parse_block_device_mapping_v2(args, image): spec_dict = dict(v.split('=') for v in device_spec.split(',')) bdm_dict = {} - for key, value in six.iteritems(spec_dict): + for key, value in spec_dict.items(): bdm_dict[CLIENT_BDM2_KEYS[key]] = value # Convert the delete_on_termination to a boolean or set it to true by From 9a0484e2b8ef76af0876d904af84b3ff2b8d622f Mon Sep 17 00:00:00 2001 From: Diana Clarke Date: Wed, 7 Dec 2016 12:19:27 -0500 Subject: [PATCH 1192/1705] CONF.osapi_max_limit -> CONF.api.max_limit This max limit config option was moved to the api group in Ida4ee57d6e1822e35e3198f6d3a89410e211d57d. Update the command line help to reflect those changes. Change-Id: I171940443918329b2c987b507f354d33b7e7db0c --- novaclient/v2/shell.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 1d471bb53..117a499ef 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -882,8 +882,8 @@ def _print_flavor_list(flavors, show_extra_specs=False): type=int, default=None, help=_("Maximum number of flavors to display. If limit is bigger than " - "'osapi_max_limit' option of Nova API, limit 'osapi_max_limit' " - "will be used instead.")) + "'CONF.api.max_limit' option of Nova API, limit " + "'CONF.api.max_limit' will be used instead.")) @utils.arg( '--sort-key', dest='sort_key', @@ -1574,8 +1574,8 @@ def do_image_delete(cs, args): type=int, default=None, help=_("Maximum number of servers to display. If limit == -1, all servers " - "will be displayed. If limit is bigger than 'osapi_max_limit' " - "option of Nova API, limit 'osapi_max_limit' will be used " + "will be displayed. If limit is bigger than 'CONF.api.max_limit' " + "option of Nova API, limit 'CONF.api.max_limit' will be used " "instead.")) @utils.arg( '--changes-since', @@ -3298,8 +3298,8 @@ def do_keypair_list(cs, args): type=int, default=None, help=_("Maximum number of keypairs to display. If limit is bigger than " - "'osapi_max_limit' option of Nova API, limit 'osapi_max_limit' " - "will be used instead.")) + "'CONF.api.max_limit' option of Nova API, limit " + "'CONF.api.max_limit' will be used instead.")) def do_keypair_list(cs, args): """Print a list of keypairs for a user""" keypairs = cs.keypairs.list(args.user, args.marker, args.limit) @@ -4169,8 +4169,8 @@ def do_hypervisor_list(cs, args): type=int, default=None, help=_("Maximum number of hypervisors to display. If limit is bigger than " - "'osapi_max_limit' option of Nova API, limit 'osapi_max_limit' " - "will be used instead.")) + "'CONF.api.max_limit' option of Nova API, limit " + "'CONF.api.max_limit' will be used instead.")) def do_hypervisor_list(cs, args): """List hypervisors.""" _do_hypervisor_list( From ca2fc77b43ea3fef20e9a15e09b4fec6a95b2573 Mon Sep 17 00:00:00 2001 From: jichenjc Date: Sun, 11 Dec 2016 21:18:28 +0800 Subject: [PATCH 1193/1705] Add version pin for image list function image functions deprecated after v2.35 but we didn't pin the version for them, so when use 2.37 by default won't work. https://review.openstack.org/#/c/392523/ fixed others but unfortunately list function is missing test_readonly_nova.py was removed as it's only for 2.35 check while through this patch, >2.35 can be used Change-Id: Ia1157dcb68971c5f64f7ab068fc647f25cd265e4 Related-Bug: 1638506 Closes-Bug: 1650617 --- .../tests/functional/v2/test_readonly_nova.py | 38 ------------------- novaclient/v2/shell.py | 10 +---- 2 files changed, 2 insertions(+), 46 deletions(-) delete mode 100644 novaclient/tests/functional/v2/test_readonly_nova.py diff --git a/novaclient/tests/functional/v2/test_readonly_nova.py b/novaclient/tests/functional/v2/test_readonly_nova.py deleted file mode 100644 index aee408c9c..000000000 --- a/novaclient/tests/functional/v2/test_readonly_nova.py +++ /dev/null @@ -1,38 +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. - -import six -from tempest.lib import exceptions - -from novaclient import api_versions -from novaclient.tests.functional.v2.legacy import test_readonly_nova - - -class SimpleReadOnlyNovaClientTest( - test_readonly_nova.SimpleReadOnlyNovaClientTest): - - """Read only functional python-novaclient tests. - - This only exercises client commands that are read only. - """ - - COMPUTE_API_VERSION = "2.latest" - - def test_admin_image_list(self): - # The nova images proxy API returns a 404 after 2.35. - if self.client.api_version > api_versions.APIVersion('2.35'): - ex = self.assertRaises(exceptions.CommandFailed, - super(SimpleReadOnlyNovaClientTest, self). - test_admin_image_list) - self.assertIn('NotFound', six.text_type(ex)) - else: - super(SimpleReadOnlyNovaClientTest, self).test_admin_image_list() diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 49a5c29b1..b104ff8a1 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -76,12 +76,6 @@ 'or openstackclient instead') -# NOTE(mriedem): Remove this along with the deprecated commands in the first -# python-novaclient release AFTER the nova server 15.0.0 'O' release. -def emit_image_deprecation_warning(command_name): - print(msg_deprecate_img % command_name, file=sys.stderr) - - def deprecated_proxy(fn, msg_format): @functools.wraps(fn) def wrapped(cs, *args, **kwargs): @@ -1360,9 +1354,9 @@ def do_network_create(cs, args): dest="limit", metavar="", help=_('Number of images to return per request.')) +@deprecated_image def do_image_list(cs, _args): - """DEPRECATED: Print a list of available images to boot from.""" - emit_image_deprecation_warning('image-list') + """Print a list of available images to boot from.""" limit = _args.limit image_list = cs.images.list(limit=limit) From 4ff6c617ecd09b2aafae93bf90711ecd9891d384 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Fri, 2 Dec 2016 18:40:18 +0200 Subject: [PATCH 1194/1705] Restict usage *args for novaclient.client.Client Possitional arguments doen't allow to provide proper compatibility and deprecation workflows. *args was deprecated long time ago and a proper message was added. Change-Id: Ib4a8f4db52a0f4cb8e8cf5e8c8e778eef8831e91 --- novaclient/client.py | 10 ++-------- ...strict-args-for-novaclient-ep-491098c3634365be.yaml | 5 +++++ 2 files changed, 7 insertions(+), 8 deletions(-) create mode 100644 releasenotes/notes/restrict-args-for-novaclient-ep-491098c3634365be.yaml diff --git a/novaclient/client.py b/novaclient/client.py index 79f36342f..2e02be3fa 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -808,7 +808,7 @@ def get_client_class(version): def Client(version, username=None, api_key=None, project_id=None, - auth_url=None, *args, **kwargs): + auth_url=None, **kwargs): """Initialize client object based on given version. HOW-TO: @@ -829,15 +829,9 @@ def Client(version, username=None, api_key=None, project_id=None, session API. See "The novaclient Python API" page at python-novaclient's doc. """ - if args: - warnings.warn("Only VERSION, USERNAME, PASSWORD, PROJECT_ID and " - "AUTH_URL arguments can be specified as positional " - "arguments. All other variables should be keyword " - "arguments. Note that this will become an error in " - "Ocata.") api_version, client_class = _get_client_class_and_version(version) kwargs.pop("direct_use", None) return client_class(username=username, api_key=api_key, project_id=project_id, auth_url=auth_url, api_version=api_version, direct_use=False, - *args, **kwargs) + **kwargs) diff --git a/releasenotes/notes/restrict-args-for-novaclient-ep-491098c3634365be.yaml b/releasenotes/notes/restrict-args-for-novaclient-ep-491098c3634365be.yaml new file mode 100644 index 000000000..15a651f62 --- /dev/null +++ b/releasenotes/notes/restrict-args-for-novaclient-ep-491098c3634365be.yaml @@ -0,0 +1,5 @@ +--- +upgrade: + - novaclient.client.Client entry-point accepts only 5 positional arguments( + version, username, api_key, project_id, auth_url). Using positional + arguments for all other options become an error. From 50d83a4de2eaa4150d42ccfa713071ef70eb0272 Mon Sep 17 00:00:00 2001 From: int32bit Date: Sat, 10 Dec 2016 01:29:28 +0800 Subject: [PATCH 1195/1705] Add limit and offset to server-groups list Via `limit` and `offset` arguments, Nova API now support pagination to display server groups. But our novaclient hasn't implemented yet in current version. This patch introduce `--limit` and `--offset` options to `nova server-group-list` subcommand, which can make up for the above issue. Change-Id: I371d4c2e74a8e6bfc8a7529de35668490cb0e44d Closes-Bug: #1648835 --- .../tests/unit/fixture_data/server_groups.py | 39 ++++++++++++++ novaclient/tests/unit/v2/fakes.py | 53 +++++++++++++------ .../tests/unit/v2/test_server_groups.py | 15 +++++- novaclient/tests/unit/v2/test_shell.py | 6 ++- novaclient/v2/server_groups.py | 24 +++++++-- novaclient/v2/shell.py | 21 +++++++- 6 files changed, 135 insertions(+), 23 deletions(-) diff --git a/novaclient/tests/unit/fixture_data/server_groups.py b/novaclient/tests/unit/fixture_data/server_groups.py index afbe2612f..5c4c88c6f 100644 --- a/novaclient/tests/unit/fixture_data/server_groups.py +++ b/novaclient/tests/unit/fixture_data/server_groups.py @@ -50,12 +50,51 @@ def setUp(self): } ] + other_project_server_groups = [ + { + "members": [], + "metadata": {}, + "id": "11111111-1111-1111-1111-111111111111", + "policies": [], + "name": "ig4" + }, + { + "members": [], + "metadata": {}, + "id": "22222222-2222-2222-2222-222222222222", + "policies": ["anti-affinity"], + "name": "ig5" + }, + { + "members": [], + "metadata": {"key": "value"}, + "id": "33333333-3333-3333-3333-333333333333", + "policies": [], "name": "ig6" + }, + { + "members": ["2dccb4a1-02b9-482a-aa23-5799490d6f5d"], + "metadata": {}, + "id": "44444444-4444-4444-4444-444444444444", + "policies": ["anti-affinity"], + "name": "ig5" + } + ] + headers = self.json_headers self.requests_mock.get(self.url(), json={'server_groups': server_groups}, headers=headers) + self.requests_mock.get(self.url(all_projects=True), + json={'server_groups': server_groups + + other_project_server_groups}, + headers=headers) + + self.requests_mock.get(self.url(limit=2, offset=1), + json={'server_groups': server_groups[1:3]}, + headers=headers) + server = server_groups[0] def _register(method, *args): diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 5cfeb71de..6d9fc1cf2 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -2162,22 +2162,43 @@ def get_os_migrations(self, **kw): # Server Groups # - def get_os_server_groups(self, *kw): - return (200, {}, - {"server_groups": [ - {"members": [], "metadata": {}, - "id": "2cbd51f4-fafe-4cdb-801b-cf913a6f288b", - "policies": [], "name": "ig1"}, - {"members": [], "metadata": {}, - "id": "4473bb03-4370-4bfb-80d3-dc8cffc47d94", - "policies": ["anti-affinity"], "name": "ig2"}, - {"members": [], "metadata": {"key": "value"}, - "id": "31ab9bdb-55e1-4ac3-b094-97eeb1b65cc4", - "policies": [], "name": "ig3"}, - {"members": ["2dccb4a1-02b9-482a-aa23-5799490d6f5d"], - "metadata": {}, - "id": "4890bb03-7070-45fb-8453-d34556c87d94", - "policies": ["anti-affinity"], "name": "ig2"}]}) + def get_os_server_groups(self, **kw): + server_groups = [ + {"members": [], "metadata": {}, + "id": "2cbd51f4-fafe-4cdb-801b-cf913a6f288b", + "policies": [], "name": "ig1"}, + {"members": [], "metadata": {}, + "id": "4473bb03-4370-4bfb-80d3-dc8cffc47d94", + "policies": ["anti-affinity"], "name": "ig2"}, + {"members": [], "metadata": {"key": "value"}, + "id": "31ab9bdb-55e1-4ac3-b094-97eeb1b65cc4", + "policies": [], "name": "ig3"}, + {"members": ["2dccb4a1-02b9-482a-aa23-5799490d6f5d"], + "metadata": {}, + "id": "4890bb03-7070-45fb-8453-d34556c87d94", + "policies": ["anti-affinity"], "name": "ig2"}] + + other_project_server_groups = [ + {"members": [], "metadata": {}, + "id": "11111111-1111-1111-1111-111111111111", + "policies": [], "name": "ig4"}, + {"members": [], "metadata": {}, + "id": "22222222-2222-2222-2222-222222222222", + "policies": ["anti-affinity"], "name": "ig5"}, + {"members": [], "metadata": {"key": "value"}, + "id": "31ab9bdb-55e1-4ac3-b094-97eeb1b65cc4", + "policies": [], "name": "ig6"}, + {"members": ["33333333-3333-3333-3333-333333333333"], + "metadata": {}, + "id": "44444444-4444-4444-4444-444444444444", + "policies": ["anti-affinity"], "name": "ig5"}] + + if kw.get("all_projects", False): + server_groups.extend(other_project_server_groups) + limit = int(kw.get("limit", 1000)) + offset = int(kw.get("offset", 0)) + server_groups = server_groups[offset:limit + 1] + return (200, {}, {"server_groups": server_groups}) def _return_server_group(self): r = {'server_group': diff --git a/novaclient/tests/unit/v2/test_server_groups.py b/novaclient/tests/unit/v2/test_server_groups.py index f942d4b0b..9881b20aa 100644 --- a/novaclient/tests/unit/v2/test_server_groups.py +++ b/novaclient/tests/unit/v2/test_server_groups.py @@ -30,6 +30,7 @@ def test_list_server_groups(self): result = self.cs.server_groups.list() self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/os-server-groups') + self.assertEqual(4, len(result)) for server_group in result: self.assertIsInstance(server_group, server_groups.ServerGroup) @@ -37,11 +38,23 @@ def test_list_server_groups(self): def test_list_server_groups_with_all_projects(self): result = self.cs.server_groups.list(all_projects=True) self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('GET', '/os-server-groups?all_projects') + self.assert_called('GET', '/os-server-groups?all_projects=True') + self.assertEqual(8, len(result)) for server_group in result: self.assertIsInstance(server_group, server_groups.ServerGroup) + def test_list_server_groups_with_limit_and_offset(self): + all_groups = self.cs.server_groups.list() + result = self.cs.server_groups.list(limit=2, offset=1) + self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('GET', '/os-server-groups?limit=2&offset=1') + self.assertEqual(2, len(result)) + for server_group in result: + self.assertIsInstance(server_group, + server_groups.ServerGroup) + self.assertEqual(all_groups[1:3], result) + def test_create_server_group(self): kwargs = {'name': 'ig1', 'policies': ['anti-affinity']} diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index dafcb24e9..db81cb404 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -3149,7 +3149,11 @@ def test_list_server_group(self): def test_list_server_group_with_all_projects(self): self.run_command('server-group-list --all-projects') - self.assert_called('GET', '/os-server-groups?all_projects') + self.assert_called('GET', '/os-server-groups?all_projects=True') + + def test_list_server_group_with_limit_and_offset(self): + self.run_command('server-group-list --limit 20 --offset 5') + self.assert_called('GET', '/os-server-groups?limit=20&offset=5') def test_list_server_os_virtual_interfaces(self): self.run_command('virtual-interface-list 1234') diff --git a/novaclient/v2/server_groups.py b/novaclient/v2/server_groups.py index 77ee84f5b..62758c34b 100644 --- a/novaclient/v2/server_groups.py +++ b/novaclient/v2/server_groups.py @@ -17,6 +17,8 @@ Server group interface. """ +from six.moves.urllib import parse + from novaclient import base @@ -43,13 +45,27 @@ class ServerGroupsManager(base.ManagerWithFind): """ resource_class = ServerGroup - def list(self, all_projects=False): + def list(self, all_projects=False, limit=None, offset=None): """Get a list of all server groups. - :rtype: list of :class:`ServerGroup`. + :param all_projects: Lists server groups for all projects. (optional) + :param limit: Maximum number of server groups to return. (optional) + :param offset: Use with `limit` to return a slice of server + groups. `offset` is where to start in the groups + list. (optional) + :returns: list of :class:`ServerGroup`. """ - all = '?all_projects' if all_projects else '' - return self._list('/os-server-groups%s' % all, 'server_groups') + qparams = {} + if all_projects: + qparams['all_projects'] = bool(all_projects) + if limit: + qparams['limit'] = int(limit) + if offset: + qparams['offset'] = int(offset) + qparams = sorted(qparams.items(), key=lambda x: x[0]) + query_string = "?%s" % parse.urlencode(qparams) if qparams else "" + return self._list('/os-server-groups%s' % query_string, + 'server_groups') def get(self, id): """Get a specific server group. diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 0288b5e7c..65de908f6 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -5035,6 +5035,23 @@ def _print_server_group_details(cs, server_group): # noqa utils.print_list(server_group, columns) +@utils.arg( + '--limit', + dest='limit', + metavar='', + type=int, + default=None, + help=_("Maximum number of server groups to display. If limit is bigger" + "than 'CONF.api.max_limit' option of Nova API, limit" + "'CONF.api.max_limit' will be used instead.")) +@utils.arg( + '--offset', + dest='offset', + metavar='', + type=int, + default=None, + help=_('The offset of groups list to display; use with limit to ' + 'return a slice of server groups.')) @utils.arg( '--all-projects', dest='all_projects', @@ -5043,7 +5060,9 @@ def _print_server_group_details(cs, server_group): # noqa help=_('Display server groups from all projects (Admin only).')) def do_server_group_list(cs, args): """Print a list of all server groups.""" - server_groups = cs.server_groups.list(args.all_projects) + server_groups = cs.server_groups.list(all_projects=args.all_projects, + limit=args.limit, + offset=args.offset) _print_server_group_details(cs, server_groups) From 332b24678b9fc8a39276acce991db9ed5080152d Mon Sep 17 00:00:00 2001 From: int32bit Date: Thu, 15 Dec 2016 00:27:02 +0800 Subject: [PATCH 1196/1705] Remove internal attribute access from shell The attribute _info is an internal private field in Resource class, to read this attribute in external class is a bad practice. Change-Id: I8c705d6d08418d18973eb4a2d7858297a12a4d9d --- novaclient/base.py | 3 +++ novaclient/v2/shell.py | 56 ++++++++++++++++++++---------------------- 2 files changed, 30 insertions(+), 29 deletions(-) diff --git a/novaclient/base.py b/novaclient/base.py index a091f0b66..2d08cf277 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -218,6 +218,9 @@ def is_loaded(self): def set_loaded(self, val): self._loaded = val + def set_info(self, key, value): + self._info[key] = value + def to_dict(self): return copy.deepcopy(self._info) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 0288b5e7c..16bf2b441 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -19,7 +19,6 @@ from __future__ import print_function import argparse -import copy import datetime import functools import getpass @@ -805,9 +804,10 @@ def print_progress(progress): def _translate_keys(collection, convert): for item in collection: keys = item.__dict__.keys() + item_dict = item.to_dict() for from_key, to_key in convert: if from_key in keys and to_key not in keys: - setattr(item, to_key, item._info[from_key]) + setattr(item, to_key, item_dict[from_key]) def _translate_extended_states(collection): @@ -1138,7 +1138,7 @@ def do_network_list(cs, args): def do_network_show(cs, args): """Show details about the given network.""" network = utils.find_resource(cs.networks, args.network) - utils.print_dict(network._info) + utils.print_dict(network.to_dict()) @utils.arg( @@ -1422,7 +1422,7 @@ def _extract_metadata(args): def _print_image(image): - info = image._info.copy() + info = image.to_dict() # ignore links, we don't need to present those info.pop('links', None) @@ -1447,7 +1447,7 @@ def _print_image(image): def _print_flavor(flavor): - info = flavor._info.copy() + info = flavor.to_dict() # ignore links, we don't need to present those info.pop('links') info.update({"extra_specs": _print_flavor_extra_specs(flavor)}) @@ -2296,7 +2296,7 @@ def _print_server(cs, args, server=None, wrap=0): minimal = getattr(args, "minimal", False) networks = server.networks - info = server._info.copy() + info = server.to_dict() for network_label, address_list in networks.items(): info['%s network' % network_label] = ', '.join(address_list) @@ -2481,7 +2481,7 @@ def do_remove_fixed_ip(cs, args): def _print_volume(volume): - utils.print_dict(volume._info) + utils.print_dict(volume.to_dict()) def _translate_availability_zone_keys(collection): @@ -2924,7 +2924,7 @@ def do_dns_create_public_domain(cs, args): def _print_secgroup_rules(rules, show_source_group=True): class FormattedRule(object): def __init__(self, obj): - items = (obj if isinstance(obj, dict) else obj._info).items() + items = (obj if isinstance(obj, dict) else obj.to_dict()).items() for k, v in items: if k == 'ip_range': v = v.get('cidr') @@ -3333,7 +3333,7 @@ def do_keypair_list(cs, args): def _print_keypair(keypair): - kp = keypair._info.copy() + kp = keypair.to_dict() pk = kp.pop('public_key') utils.print_dict(kp) print(_("Public key: %s") % pk) @@ -3654,7 +3654,7 @@ def do_agent_create(cs, args): result = cs.agents.create(args.os, args.architecture, args.version, args.url, args.md5hash, args.hypervisor) - utils.print_dict(result._info.copy()) + utils.print_dict(result.to_dict()) @utils.arg('id', metavar='', help=_('ID of the agent-build.')) @@ -3671,7 +3671,7 @@ def do_agent_modify(cs, args): """Modify existing agent build.""" result = cs.agents.update(args.id, args.version, args.url, args.md5hash) - utils.print_dict(result._info) + utils.print_dict(result.to_dict()) def _find_aggregate(cs, aggregate): @@ -3926,7 +3926,7 @@ def do_server_migration_show(cs, args): """Get the migration of specified server.""" server = _find_server(cs, args.server) migration = cs.server_migrations.get(server, args.migration) - utils.print_dict(migration._info) + utils.print_dict(migration.to_dict()) @api_versions.wraps("2.24") @@ -4242,7 +4242,7 @@ def __init__(self, **kwargs): def do_hypervisor_show(cs, args): """Display the details of the specified hypervisor.""" hyper = _find_hypervisor(cs, args.hypervisor) - utils.print_dict(utils.flatten_dict(hyper._info), wrap=int(args.wrap)) + utils.print_dict(utils.flatten_dict(hyper.to_dict()), wrap=int(args.wrap)) @utils.arg( @@ -4255,13 +4255,13 @@ def do_hypervisor_uptime(cs, args): hyper = cs.hypervisors.uptime(hyper) # Output the uptime information - utils.print_dict(hyper._info.copy()) + utils.print_dict(hyper.to_dict()) def do_hypervisor_stats(cs, args): """Get hypervisor statistics over all compute nodes.""" stats = cs.hypervisor_stats.statistics() - utils.print_dict(stats._info.copy()) + utils.print_dict(stats.to_dict()) @utils.arg('server', metavar='', help=_('Name or ID of server.')) @@ -4964,41 +4964,39 @@ def _treeizeAvailabilityZone(zone): """Build a tree view for availability zones.""" AvailabilityZone = availability_zones.AvailabilityZone - az = AvailabilityZone(zone.manager, - copy.deepcopy(zone._info), zone._loaded) + az = AvailabilityZone(zone.manager, zone.to_dict(), zone._loaded) result = [] # Zone tree view item az.zoneName = zone.zoneName az.zoneState = ('available' if zone.zoneState['available'] else 'not available') - az._info['zoneName'] = az.zoneName - az._info['zoneState'] = az.zoneState + az.set_info('zoneName', az.zoneName) + az.set_info('zoneState', az.zoneState) result.append(az) if zone.hosts is not None: zone_hosts = sorted(zone.hosts.items(), key=lambda x: x[0]) for (host, services) in zone_hosts: # Host tree view item - az = AvailabilityZone(zone.manager, - copy.deepcopy(zone._info), zone._loaded) + az = AvailabilityZone(zone.manager, zone.to_dict(), zone._loaded) az.zoneName = '|- %s' % host az.zoneState = '' - az._info['zoneName'] = az.zoneName - az._info['zoneState'] = az.zoneState + az.set_info('zoneName', az.zoneName) + az.set_info('zoneState', az.zoneState) result.append(az) for (svc, state) in services.items(): # Service tree view item - az = AvailabilityZone(zone.manager, - copy.deepcopy(zone._info), zone._loaded) + az = AvailabilityZone(zone.manager, zone.to_dict(), + zone._loaded) az.zoneName = '| |- %s' % svc az.zoneState = '%s %s %s' % ( 'enabled' if state['active'] else 'disabled', ':-)' if state['available'] else 'XXX', state['updated_at']) - az._info['zoneName'] = az.zoneName - az._info['zoneState'] = az.zoneState + az.set_info('zoneName', az.zoneName) + az.set_info('zoneState', az.zoneState) result.append(az) return result @@ -5248,7 +5246,7 @@ def do_server_tag_delete_all(cs, args): def do_cell_show(cs, args): """Show details of a given cell.""" cell = cs.cells.get(args.cell) - utils.print_dict(cell._info) + utils.print_dict(cell.to_dict()) @utils.arg( @@ -5485,7 +5483,7 @@ def do_instance_action(cs, args): else: server = _find_server(cs, args.server, raise_if_notfound=False) action_resource = cs.instance_action.get(server, args.request_id) - action = action_resource._info + action = action_resource.to_dict() if 'events' in action: action['events'] = pprint.pformat(action['events']) utils.print_dict(action) From 5b169ce12d60dfc447e52a90bb56a68d2a65c026 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Thu, 15 Dec 2016 03:55:26 +0000 Subject: [PATCH 1197/1705] Updated from global requirements Change-Id: I5e09541f692d9b4123e74afb92c32ab6f76c5263 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e64027894..b634f78a4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. pbr>=1.8 # Apache-2.0 -keystoneauth1>=2.14.0 # Apache-2.0 +keystoneauth1>=2.16.0 # Apache-2.0 iso8601>=0.1.11 # MIT oslo.i18n>=2.1.0 # Apache-2.0 oslo.serialization>=1.10.0 # Apache-2.0 From 330516543b052b29c9663139086d635c7a27b301 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Fri, 2 Dec 2016 18:38:06 +0200 Subject: [PATCH 1198/1705] Sort arguments for client's methods We have a lot of arguments in several Clients methods. Reviewing their changes is a hard task, so let's sort them to simplify reviews. Change-Id: Ie78773fd92017fbc4631d0fc6ae9da2e15935537 --- novaclient/client.py | 42 +++++++--- novaclient/tests/unit/v2/fakes.py | 8 +- novaclient/v2/client.py | 123 +++++++++++++++++------------- 3 files changed, 105 insertions(+), 68 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index 2e02be3fa..3d95f57b8 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -680,18 +680,36 @@ def _authenticate(self, url, body, **kwargs): return self._extract_service_catalog(url, resp, respbody) -def _construct_http_client(username=None, password=None, project_id=None, - auth_url=None, insecure=False, timeout=None, - proxy_tenant_id=None, proxy_token=None, - region_name=None, endpoint_type='publicURL', - extensions=None, service_type='compute', - service_name=None, volume_service_name=None, - timings=False, bypass_url=None, os_cache=False, - no_cache=True, http_log_debug=False, - auth_token=None, cacert=None, tenant_id=None, - user_id=None, connection_pool=False, session=None, - auth=None, user_agent='python-novaclient', - interface=None, api_version=None, **kwargs): +def _construct_http_client(api_version=None, + auth=None, + auth_token=None, + auth_url=None, + bypass_url=None, + cacert=None, + connection_pool=False, + endpoint_type='publicURL', + extensions=None, + http_log_debug=False, + insecure=False, + interface=None, + no_cache=True, + os_cache=False, + password=None, + project_id=None, + proxy_tenant_id=None, + proxy_token=None, + region_name=None, + service_name=None, + service_type='compute', + session=None, + tenant_id=None, + timeout=None, + timings=False, + user_agent='python-novaclient', + user_id=None, + username=None, + volume_service_name=None, + **kwargs): # TODO(mordred): If not session, just make a Session, then return # SessionClient always if session: diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 5cfeb71de..bfda1ae07 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -57,8 +57,8 @@ class FakeClient(fakes.FakeClient, client.Client): def __init__(self, api_version, *args, **kwargs): - client.Client.__init__(self, 'username', 'password', - 'project_id', 'auth_url', + client.Client.__init__(self, username='username', password='password', + project_id='project_id', auth_url='auth_url', extensions=kwargs.get('extensions'), direct_use=False, api_version=api_version) self.client = FakeHTTPClient(api_version=api_version, **kwargs) @@ -2341,8 +2341,8 @@ def post_os_server_external_events(self, **kw): class FakeSessionClient(fakes.FakeClient, client.Client): def __init__(self, api_version, *args, **kwargs): - client.Client.__init__(self, 'username', 'password', - 'project_id', 'auth_url', + client.Client.__init__(self, username='username', password='password', + project_id='project_id', auth_url='auth_url', extensions=kwargs.get('extensions'), direct_use=False, api_version=api_version) self.client = FakeSessionMockClient(api_version=api_version, **kwargs) diff --git a/novaclient/v2/client.py b/novaclient/v2/client.py index 961e805c0..717727f46 100644 --- a/novaclient/v2/client.py +++ b/novaclient/v2/client.py @@ -68,50 +68,69 @@ class Client(object): directly. It should be done via `novaclient.client.Client` interface. """ - def __init__(self, username=None, api_key=None, project_id=None, - auth_url=None, insecure=False, timeout=None, - proxy_tenant_id=None, proxy_token=None, region_name=None, - endpoint_type='publicURL', extensions=None, - service_type='compute', service_name=None, - volume_service_name=None, timings=False, bypass_url=None, - os_cache=False, no_cache=True, http_log_debug=False, + def __init__(self, api_key=None, + api_version=None, + auth=None, auth_token=None, - cacert=None, tenant_id=None, user_id=None, - connection_pool=False, session=None, auth=None, - api_version=None, direct_use=True, logger=None, **kwargs): + auth_url=None, + bypass_url=None, + cacert=None, + connection_pool=False, + direct_use=True, + endpoint_type='publicURL', + extensions=None, + http_log_debug=False, + insecure=False, + logger=None, + no_cache=True, + os_cache=False, + project_id=None, + proxy_tenant_id=None, + proxy_token=None, + region_name=None, + service_name=None, + service_type='compute', + session=None, + tenant_id=None, + timeout=None, + timings=False, + user_id=None, + username=None, + volume_service_name=None, + **kwargs): """Initialization of Client object. - :param str username: Username :param str api_key: API Key - :param str project_id: Project ID + :param api_version: Compute API version + :type api_version: novaclient.api_versions.APIVersion + :param str auth: Auth + :param str auth_token: Auth token :param str auth_url: Auth URL + :param str bypass_url: Bypass URL + :param str cacert: cacert + :param bool direct_use: Inner variable of novaclient. Do not use it + outside novaclient. It's restricted. + :param str endpoint_type: Endpoint Type + :param str extensions: Extensions + :param bool http_log_debug: Enable debugging for HTTP connections :param bool insecure: Allow insecure - :param float timeout: API timeout, None or 0 disables + :param logging.Logger logger: Logger instance to be used for all + logging stuff + :param bool no_cache: No cache + :param bool os_cache: OS cache + :param str project_id: Project ID :param str proxy_tenant_id: Tenant ID :param str proxy_token: Proxy Token :param str region_name: Region Name - :param str endpoint_type: Endpoint Type - :param str extensions: Extensions - :param str service_type: Service Type :param str service_name: Service Name - :param str volume_service_name: Volume Service Name - :param bool timings: Timings - :param str bypass_url: Bypass URL - :param bool os_cache: OS cache - :param bool no_cache: No cache - :param bool http_log_debug: Enable debugging for HTTP connections - :param str auth_token: Auth token - :param str cacert: cacert + :param str service_type: Service Type + :param str session: Session :param str tenant_id: Tenant ID + :param float timeout: API timeout, None or 0 disables + :param bool timings: Timings :param str user_id: User ID - :param bool connection_pool: Use a connection pool - :param str session: Session - :param str auth: Auth - :param api_version: Compute API version - :param direct_use: Inner variable of novaclient. Do not use it outside - novaclient. It's restricted. - :param logger: Logger - :type api_version: novaclient.api_versions.APIVersion + :param str username: Username + :param str volume_service_name: Volume Service Name """ if direct_use: raise exceptions.Forbidden( @@ -210,32 +229,32 @@ def __init__(self, username=None, api_key=None, project_id=None, extension.manager_class(self)) self.client = client._construct_http_client( - username=username, - password=password, - user_id=user_id, - project_id=project_id, - tenant_id=tenant_id, - auth_url=auth_url, + api_version=api_version, + auth=auth, auth_token=auth_token, + auth_url=auth_url, + bypass_url=bypass_url, + cacert=cacert, + connection_pool=connection_pool, + endpoint_type=endpoint_type, + http_log_debug=http_log_debug, insecure=insecure, - timeout=timeout, - proxy_token=proxy_token, + logger=self.logger, + os_cache=self.os_cache, + password=password, + project_id=project_id, proxy_tenant_id=proxy_tenant_id, + proxy_token=proxy_token, region_name=region_name, - endpoint_type=endpoint_type, - service_type=service_type, service_name=service_name, - volume_service_name=volume_service_name, - timings=timings, - bypass_url=bypass_url, - os_cache=self.os_cache, - http_log_debug=http_log_debug, - cacert=cacert, - connection_pool=connection_pool, + service_type=service_type, session=session, - auth=auth, - api_version=api_version, - logger=self.logger, + tenant_id=tenant_id, + timeout=timeout, + timings=timings, + user_id=user_id, + username=username, + volume_service_name=volume_service_name, **kwargs) @property From bf09ad844e7ccfc724ff502b34389e832a7ab7a2 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Fri, 2 Dec 2016 19:07:03 +0200 Subject: [PATCH 1199/1705] Introduce helper for checking args deprecation This patch introduces `_check_arguments` methods for processing deprecation workflow of novaclient.client.Client entry-point. Also, this patch adds a proper warning messages for 'auth_plugin', 'auth_system' arguments, which were removed previously Change-Id: I7ee210c08f4126b267572c5428511451aeb17260 --- novaclient/client.py | 30 +++++++++++++++ novaclient/tests/unit/test_client.py | 55 +++++++++++++++++++++++----- 2 files changed, 75 insertions(+), 10 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index 3d95f57b8..441114185 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -825,6 +825,32 @@ def get_client_class(version): return client_class +def _check_arguments(kwargs, release, deprecated_name, right_name=None): + """Process deprecation of arguments. + + Checks presence of deprecated argument in kwargs, prints proper warning + message, renames key to right one it needed. + """ + if deprecated_name in kwargs: + msg = _LW("The '%(old)s' argument is deprecated in %(release)s and " + "its use may result in errors in future releases.") % { + "old": deprecated_name, "release": release} + if right_name: + if right_name in kwargs: + msg += _LW(" As '%(new)s' is provided, the '%(old)s' argument " + "will be ignored.") % {"old": deprecated_name, + "new": right_name} + kwargs.pop(deprecated_name) + else: + msg += _LW(" Use '%s' instead.") % right_name + kwargs[right_name] = kwargs.pop(deprecated_name) + else: + # just ignore it + kwargs.pop(deprecated_name) + + warnings.warn(msg) + + def Client(version, username=None, api_key=None, project_id=None, auth_url=None, **kwargs): """Initialize client object based on given version. @@ -847,6 +873,10 @@ def Client(version, username=None, api_key=None, project_id=None, session API. See "The novaclient Python API" page at python-novaclient's doc. """ + + _check_arguments(kwargs, "Ocata", "auth_plugin") + _check_arguments(kwargs, "Ocata", "auth_system") + api_version, client_class = _get_client_class_and_version(version) kwargs.pop("direct_use", None) return client_class(username=username, api_key=api_key, diff --git a/novaclient/tests/unit/test_client.py b/novaclient/tests/unit/test_client.py index 9a0616c25..b559882fe 100644 --- a/novaclient/tests/unit/test_client.py +++ b/novaclient/tests/unit/test_client.py @@ -13,7 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. - +import copy import logging import fixtures @@ -478,16 +478,16 @@ def test_log_request_id(self, mock_log_request_id): 'compute') -class DiscoverExtensionTest(utils.TestCase): +class ClientsUtilsTest(utils.TestCase): @mock.patch("novaclient.client._discover_via_entry_points") @mock.patch("novaclient.client._discover_via_contrib_path") @mock.patch("novaclient.client._discover_via_python_path") @mock.patch("novaclient.extension.Extension") - def test_discover_all(self, mock_extension, - mock_discover_via_python_path, - mock_discover_via_contrib_path, - mock_discover_via_entry_points): + def test_discover_extensions_all(self, mock_extension, + mock_discover_via_python_path, + mock_discover_via_contrib_path, + mock_discover_via_entry_points): def make_gen(start, end): def f(*args, **kwargs): for i in range(start, end): @@ -513,10 +513,9 @@ def f(*args, **kwargs): @mock.patch("novaclient.client._discover_via_contrib_path") @mock.patch("novaclient.client._discover_via_python_path") @mock.patch("novaclient.extension.Extension") - def test_discover_only_contrib(self, mock_extension, - mock_discover_via_python_path, - mock_discover_via_contrib_path, - mock_discover_via_entry_points): + def test_discover_extensions_only_contrib( + self, mock_extension, mock_discover_via_python_path, + mock_discover_via_contrib_path, mock_discover_via_entry_points): mock_discover_via_contrib_path.return_value = [("name", "module")] version = novaclient.api_versions.APIVersion("2.0") @@ -526,3 +525,39 @@ def test_discover_only_contrib(self, mock_extension, self.assertFalse(mock_discover_via_python_path.called) self.assertFalse(mock_discover_via_entry_points.called) mock_extension.assert_called_once_with("name", "module") + + @mock.patch("novaclient.client.warnings") + def test__check_arguments(self, mock_warnings): + release = "Coolest" + + # no reference + novaclient.client._check_arguments({}, release=release, + deprecated_name="foo") + self.assertFalse(mock_warnings.warn.called) + novaclient.client._check_arguments({}, release=release, + deprecated_name="foo", + right_name="bar") + self.assertFalse(mock_warnings.warn.called) + + # with alternative + original_kwargs = {"foo": "text"} + actual_kwargs = copy.copy(original_kwargs) + self.assertEqual(original_kwargs, actual_kwargs) + novaclient.client._check_arguments(actual_kwargs, release=release, + deprecated_name="foo", + right_name="bar") + self.assertNotEqual(original_kwargs, actual_kwargs) + self.assertEqual({"bar": original_kwargs["foo"]}, actual_kwargs) + self.assertTrue(mock_warnings.warn.called) + + mock_warnings.warn.reset_mock() + + # without alternative + original_kwargs = {"foo": "text"} + actual_kwargs = copy.copy(original_kwargs) + self.assertEqual(original_kwargs, actual_kwargs) + novaclient.client._check_arguments(actual_kwargs, release=release, + deprecated_name="foo") + self.assertNotEqual(original_kwargs, actual_kwargs) + self.assertEqual({}, actual_kwargs) + self.assertTrue(mock_warnings.warn.called) From 73196fd3c1b938386cc162969a03a9bf1286b03d Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Fri, 2 Dec 2016 19:10:34 +0200 Subject: [PATCH 1200/1705] Remove redundant args of _construct_http_client Arguments 'extenstions' and 'no_cache' were not transmitted to '_construct_http_client' method. They are redundant Change-Id: Ibe0f874e510eef7cc28fcee87b0083c7febd5161 --- novaclient/client.py | 7 +++++-- novaclient/tests/unit/test_client.py | 12 ------------ novaclient/v2/client.py | 4 +--- .../deprecate-no-cache-arg-7814806b4f79c1b9.yaml | 5 +++++ 4 files changed, 11 insertions(+), 17 deletions(-) create mode 100644 releasenotes/notes/deprecate-no-cache-arg-7814806b4f79c1b9.yaml diff --git a/novaclient/client.py b/novaclient/client.py index 441114185..c5590e7c8 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -688,11 +688,9 @@ def _construct_http_client(api_version=None, cacert=None, connection_pool=False, endpoint_type='publicURL', - extensions=None, http_log_debug=False, insecure=False, interface=None, - no_cache=True, os_cache=False, password=None, project_id=None, @@ -876,6 +874,11 @@ def Client(version, username=None, api_key=None, project_id=None, _check_arguments(kwargs, "Ocata", "auth_plugin") _check_arguments(kwargs, "Ocata", "auth_system") + if "no_cache" in kwargs: + _check_arguments(kwargs, "Ocata", "no_cache", "os_cache") + # os_cache is not a fully compatible with no_cache, so we need to + # apply this custom processing + kwargs["os_cache"] = not kwargs["os_cache"] api_version, client_class = _get_client_class_and_version(version) kwargs.pop("direct_use", None) diff --git a/novaclient/tests/unit/test_client.py b/novaclient/tests/unit/test_client.py index b559882fe..945749d54 100644 --- a/novaclient/tests/unit/test_client.py +++ b/novaclient/tests/unit/test_client.py @@ -184,18 +184,6 @@ def test_client_with_os_cache_disabled(self): self.assertFalse(cs.os_cache) self.assertFalse(cs.client.os_cache) - def test_client_with_no_cache_enabled(self): - cs = novaclient.client.Client("2", "user", "password", "project_id", - auth_url="foo/v2", no_cache=True) - self.assertFalse(cs.os_cache) - self.assertFalse(cs.client.os_cache) - - def test_client_with_no_cache_disabled(self): - cs = novaclient.client.Client("2", "user", "password", "project_id", - auth_url="foo/v2", no_cache=False) - self.assertTrue(cs.os_cache) - self.assertTrue(cs.client.os_cache) - def test_client_set_management_url_v1_1(self): cs = novaclient.client.Client("2", "user", "password", "project_id", auth_url="foo/v2") diff --git a/novaclient/v2/client.py b/novaclient/v2/client.py index 717727f46..e86d8cf02 100644 --- a/novaclient/v2/client.py +++ b/novaclient/v2/client.py @@ -82,7 +82,6 @@ def __init__(self, api_key=None, http_log_debug=False, insecure=False, logger=None, - no_cache=True, os_cache=False, project_id=None, proxy_tenant_id=None, @@ -116,7 +115,6 @@ def __init__(self, api_key=None, :param bool insecure: Allow insecure :param logging.Logger logger: Logger instance to be used for all logging stuff - :param bool no_cache: No cache :param bool os_cache: OS cache :param str project_id: Project ID :param str proxy_tenant_id: Tenant ID @@ -191,7 +189,7 @@ def __init__(self, api_key=None, self.services = services.ServiceManager(self) self.fixed_ips = fixed_ips.FixedIPsManager(self) self.floating_ips_bulk = floating_ips_bulk.FloatingIPBulkManager(self) - self.os_cache = os_cache or not no_cache + self.os_cache = os_cache self.availability_zones = \ availability_zones.AvailabilityZoneManager(self) self.server_groups = server_groups.ServerGroupsManager(self) diff --git a/releasenotes/notes/deprecate-no-cache-arg-7814806b4f79c1b9.yaml b/releasenotes/notes/deprecate-no-cache-arg-7814806b4f79c1b9.yaml new file mode 100644 index 000000000..2fc9e4bfc --- /dev/null +++ b/releasenotes/notes/deprecate-no-cache-arg-7814806b4f79c1b9.yaml @@ -0,0 +1,5 @@ +--- +deprecations: + - novaclient.client.Client entry-point accepted two arguments with same + meaning (**no_cache** and **os_cache**). Since **os_cache** is more widely + used in our code, **no_cache** was deprecated. From 1ce917ef255696424ad299d0c611f7cf8bf137ab Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Fri, 2 Dec 2016 19:23:41 +0200 Subject: [PATCH 1201/1705] Rename bypass_url to endpoint_override * there are two terms in openstack which describes the same thing: bypass_url and endpoint_override * endpoint_override is more wide term. It is used in Keystone) * since not all openstack folks knows that bypass_url and endpoint_override are the same, bypass_url (variable which was used in novaclient) was not transmitted to SessionClient NOTE: name of variable in inner class HTTPClient was not changed, since this class will be abandoned soon. Change-Id: I2d45820c01851b271de8ea66e0d59ec494040c04 --- novaclient/client.py | 14 ++++--- novaclient/shell.py | 38 +++++++++++-------- .../v2/legacy/test_readonly_nova.py | 2 +- novaclient/v2/client.py | 6 +-- .../rename-bypass-url-42cd96956a6bc6dc.yaml | 10 +++++ 5 files changed, 45 insertions(+), 25 deletions(-) create mode 100644 releasenotes/notes/rename-bypass-url-42cd96956a6bc6dc.yaml diff --git a/novaclient/client.py b/novaclient/client.py index c5590e7c8..6897fe334 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -684,9 +684,9 @@ def _construct_http_client(api_version=None, auth=None, auth_token=None, auth_url=None, - bypass_url=None, cacert=None, connection_pool=False, + endpoint_override=None, endpoint_type='publicURL', http_log_debug=False, insecure=False, @@ -711,15 +711,16 @@ def _construct_http_client(api_version=None, # TODO(mordred): If not session, just make a Session, then return # SessionClient always if session: - return SessionClient(session=session, + return SessionClient(api_version=api_version, auth=auth, + endpoint_override=endpoint_override, interface=interface or endpoint_type, - service_type=service_type, region_name=region_name, service_name=service_name, - user_agent=user_agent, + service_type=service_type, + session=session, timings=timings, - api_version=api_version, + user_agent=user_agent, **kwargs) else: # FIXME(jamielennox): username and password are now optional. Need @@ -742,7 +743,7 @@ def _construct_http_client(api_version=None, service_name=service_name, volume_service_name=volume_service_name, timings=timings, - bypass_url=bypass_url, + bypass_url=endpoint_override, os_cache=os_cache, http_log_debug=http_log_debug, cacert=cacert, @@ -879,6 +880,7 @@ def Client(version, username=None, api_key=None, project_id=None, # os_cache is not a fully compatible with no_cache, so we need to # apply this custom processing kwargs["os_cache"] = not kwargs["os_cache"] + _check_arguments(kwargs, "Ocata", "bypass_url", "endpoint_override") api_version, client_class = _get_client_class_and_version(version) kwargs.pop("direct_use", None) diff --git a/novaclient/shell.py b/novaclient/shell.py index d4fa0a574..d35691144 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -494,12 +494,21 @@ def get_base_parser(self, argv): '"X.latest", defaults to env[OS_COMPUTE_API_VERSION].')) parser.add_argument( - '--bypass-url', + '--endpoint-override', metavar='', - dest='bypass_url', - default=utils.env('NOVACLIENT_BYPASS_URL'), + dest='endpoint_override', + default=utils.env('NOVACLIENT_ENDPOINT_OVERRIDE', + 'NOVACLIENT_BYPASS_URL'), help=_("Use this API endpoint instead of the Service Catalog. " - "Defaults to env[NOVACLIENT_BYPASS_URL].")) + "Defaults to env[NOVACLIENT_ENDPOINT_OVERRIDE].")) + + parser.add_argument( + '--bypass-url', + action=DeprecatedAction, + use=_('use "%s"; this option will be removed after Pike OpenStack ' + 'release.') % '--os-endpoint-override', + dest='endpoint_override', + help=argparse.SUPPRESS) self._append_global_identity_args(parser, argv) @@ -658,7 +667,7 @@ def main(self, argv): service_type = args.service_type service_name = args.service_name volume_service_name = args.volume_service_name - bypass_url = args.bypass_url + endpoint_override = args.endpoint_override os_cache = args.os_cache cacert = args.os_cacert timeout = args.timeout @@ -673,7 +682,6 @@ def main(self, argv): # Note if we don't auth we probably don't have a tenant ID so we can't # cache the token. auth_token = getattr(args, 'os_token', None) - management_url = bypass_url if bypass_url else None if not endpoint_type: endpoint_type = DEFAULT_NOVA_ENDPOINT_TYPE @@ -688,13 +696,14 @@ def main(self, argv): # service type specified, we use default nova service type. service_type = DEFAULT_NOVA_SERVICE_TYPE - # If we have an auth token but no management_url, we must auth anyway. + # We should always auth unless we have a token and we're passing a + # specific endpoint # Expired tokens are handled by client.py:_cs_request - must_auth = not (auth_token and management_url) + must_auth = not (auth_token and endpoint_override) # Do not use Keystone session for cases with no session support. use_session = True - if bypass_url or os_cache or volume_service_name: + if endpoint_override or os_cache or volume_service_name: use_session = False # FIXME(usrleon): Here should be restrict for project id same as @@ -757,7 +766,7 @@ def main(self, argv): extensions=self.extensions, service_type=service_type, service_name=service_name, auth_token=auth_token, volume_service_name=volume_service_name, - timings=args.timings, bypass_url=bypass_url, + timings=args.timings, endpoint_override=endpoint_override, os_cache=os_cache, http_log_debug=args.debug, cacert=cacert, timeout=timeout, session=keystone_session, auth=keystone_auth, @@ -821,7 +830,7 @@ def main(self, argv): extensions=self.extensions, service_type=service_type, service_name=service_name, auth_token=auth_token, volume_service_name=volume_service_name, - timings=args.timings, bypass_url=bypass_url, + timings=args.timings, endpoint_override=endpoint_override, os_cache=os_cache, http_log_debug=args.debug, cacert=cacert, timeout=timeout, session=keystone_session, auth=keystone_auth) @@ -836,12 +845,11 @@ def main(self, argv): # Allow commandline to override cache if not auth_token: auth_token = helper.auth_token - if not management_url: - management_url = helper.management_url - if tenant_id and auth_token and management_url: + endpoint_override = endpoint_override or helper.management_url + if tenant_id and auth_token and endpoint_override: self.cs.client.tenant_id = tenant_id self.cs.client.auth_token = auth_token - self.cs.client.management_url = management_url + self.cs.client.management_url = endpoint_override self.cs.client.password_func = lambda: helper.password else: # We're missing something, so auth with user/pass and save diff --git a/novaclient/tests/functional/v2/legacy/test_readonly_nova.py b/novaclient/tests/functional/v2/legacy/test_readonly_nova.py index 3c8feb874..bf0575c90 100644 --- a/novaclient/tests/functional/v2/legacy/test_readonly_nova.py +++ b/novaclient/tests/functional/v2/legacy/test_readonly_nova.py @@ -166,4 +166,4 @@ def test_admin_invalid_bypass_url(self): self.assertRaises(exceptions.CommandFailed, self.nova, 'list', - flags='--bypass-url badurl') + flags='--endpoint-override badurl') diff --git a/novaclient/v2/client.py b/novaclient/v2/client.py index e86d8cf02..1cb72570d 100644 --- a/novaclient/v2/client.py +++ b/novaclient/v2/client.py @@ -73,10 +73,10 @@ def __init__(self, api_key=None, auth=None, auth_token=None, auth_url=None, - bypass_url=None, cacert=None, connection_pool=False, direct_use=True, + endpoint_override=None, endpoint_type='publicURL', extensions=None, http_log_debug=False, @@ -105,10 +105,10 @@ def __init__(self, api_key=None, :param str auth: Auth :param str auth_token: Auth token :param str auth_url: Auth URL - :param str bypass_url: Bypass URL :param str cacert: cacert :param bool direct_use: Inner variable of novaclient. Do not use it outside novaclient. It's restricted. + :param str endpoint_override: Bypass URL :param str endpoint_type: Endpoint Type :param str extensions: Extensions :param bool http_log_debug: Enable debugging for HTTP connections @@ -231,9 +231,9 @@ def __init__(self, api_key=None, auth=auth, auth_token=auth_token, auth_url=auth_url, - bypass_url=bypass_url, cacert=cacert, connection_pool=connection_pool, + endpoint_override=endpoint_override, endpoint_type=endpoint_type, http_log_debug=http_log_debug, insecure=insecure, diff --git a/releasenotes/notes/rename-bypass-url-42cd96956a6bc6dc.yaml b/releasenotes/notes/rename-bypass-url-42cd96956a6bc6dc.yaml new file mode 100644 index 000000000..9dc78f8b5 --- /dev/null +++ b/releasenotes/notes/rename-bypass-url-42cd96956a6bc6dc.yaml @@ -0,0 +1,10 @@ +--- +deprecations: + - The **--bypass-url** CLI argument was deprecated in favor of + **--endpoint-override** + - The **bypass_url** argument of novaclient.client.Client entry-point was + deprecated in favor of **endpoint_override** +fixes: + - Inability to use bypass-url with keystone session +features: + - You can use *bypass-url*/**endpoint-override** with Keystone V3 From f98b8470de2e0225befc5a39ef20ed5e4f7880c1 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Fri, 2 Dec 2016 19:51:57 +0200 Subject: [PATCH 1202/1705] Rename api_key to password "password" term is more clear. Resolves FIXME from the code: FIXME(comstud): Rename the api_key argument above when we know it's not being used as keyword argument Change-Id: I39f810a181b119591cf017eded71356a437a8c0a --- novaclient/client.py | 19 ++++++++++++------- novaclient/tests/unit/fixture_data/client.py | 2 +- novaclient/tests/unit/test_client.py | 6 ------ novaclient/v2/client.py | 9 +++------ ...e-apikey-to-password-735588d841efa49e.yaml | 5 +++++ 5 files changed, 21 insertions(+), 20 deletions(-) create mode 100644 releasenotes/notes/rename-apikey-to-password-735588d841efa49e.yaml diff --git a/novaclient/client.py b/novaclient/client.py index 6897fe334..c7dbd2189 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -850,7 +850,7 @@ def _check_arguments(kwargs, release, deprecated_name, right_name=None): warnings.warn(msg) -def Client(version, username=None, api_key=None, project_id=None, +def Client(version, username=None, password=None, project_id=None, auth_url=None, **kwargs): """Initialize client object based on given version. @@ -872,19 +872,24 @@ def Client(version, username=None, api_key=None, project_id=None, session API. See "The novaclient Python API" page at python-novaclient's doc. """ - + if password: + kwargs["password"] = password _check_arguments(kwargs, "Ocata", "auth_plugin") _check_arguments(kwargs, "Ocata", "auth_system") if "no_cache" in kwargs: - _check_arguments(kwargs, "Ocata", "no_cache", "os_cache") + _check_arguments(kwargs, "Ocata", "no_cache", right_name="os_cache") # os_cache is not a fully compatible with no_cache, so we need to # apply this custom processing kwargs["os_cache"] = not kwargs["os_cache"] - _check_arguments(kwargs, "Ocata", "bypass_url", "endpoint_override") + _check_arguments(kwargs, "Ocata", "bypass_url", + right_name="endpoint_override") + _check_arguments(kwargs, "Ocata", "api_key", right_name="password") api_version, client_class = _get_client_class_and_version(version) kwargs.pop("direct_use", None) - return client_class(username=username, api_key=api_key, - project_id=project_id, auth_url=auth_url, - api_version=api_version, direct_use=False, + return client_class(api_version=api_version, + auth_url=auth_url, + direct_use=False, + project_id=project_id, + username=username, **kwargs) diff --git a/novaclient/tests/unit/fixture_data/client.py b/novaclient/tests/unit/fixture_data/client.py index 7bfaf4cc3..c37141095 100644 --- a/novaclient/tests/unit/fixture_data/client.py +++ b/novaclient/tests/unit/fixture_data/client.py @@ -56,7 +56,7 @@ def setUp(self): def new_client(self): return client.Client("2", username='xx', - api_key='xx', + password='xx', project_id='xx', auth_url=self.identity_url) diff --git a/novaclient/tests/unit/test_client.py b/novaclient/tests/unit/test_client.py index 945749d54..ab25fe0b4 100644 --- a/novaclient/tests/unit/test_client.py +++ b/novaclient/tests/unit/test_client.py @@ -211,12 +211,6 @@ def test_contextmanager_v1_1(self, mock_http_client): self.assertTrue(fake_client.open_session.called) self.assertTrue(fake_client.close_session.called) - def test_client_with_password_in_args_and_kwargs(self): - # check that TypeError is not raised during instantiation of Client - cs = novaclient.client.Client("2", "user", "password", "project_id", - password='pass') - self.assertEqual('pass', cs.client.password) - def test_get_password_simple(self): cs = novaclient.client.HTTPClient("user", "password", "", "") cs.password_func = mock.Mock() diff --git a/novaclient/v2/client.py b/novaclient/v2/client.py index 1cb72570d..e903c1a69 100644 --- a/novaclient/v2/client.py +++ b/novaclient/v2/client.py @@ -68,7 +68,7 @@ class Client(object): directly. It should be done via `novaclient.client.Client` interface. """ - def __init__(self, api_key=None, + def __init__(self, api_version=None, auth=None, auth_token=None, @@ -83,6 +83,7 @@ def __init__(self, api_key=None, insecure=False, logger=None, os_cache=False, + password=None, project_id=None, proxy_tenant_id=None, proxy_token=None, @@ -99,7 +100,6 @@ def __init__(self, api_key=None, **kwargs): """Initialization of Client object. - :param str api_key: API Key :param api_version: Compute API version :type api_version: novaclient.api_versions.APIVersion :param str auth: Auth @@ -115,6 +115,7 @@ def __init__(self, api_key=None, :param bool insecure: Allow insecure :param logging.Logger logger: Logger instance to be used for all logging stuff + :param str password: User password :param bool os_cache: OS cache :param str project_id: Project ID :param str proxy_tenant_id: Tenant ID @@ -138,16 +139,12 @@ def __init__(self, api_key=None, "'novaclient.client.Client' instead. Related lp " "bug-report: 1493576")) - # FIXME(comstud): Rename the api_key argument above when we - # know it's not being used as keyword argument - # NOTE(cyeoh): In the novaclient context (unlike Nova) the # project_id is not the same as the tenant_id. Here project_id # is a name (what the Nova API often refers to as a project or # tenant name) and tenant_id is a UUID (what the Nova API # often refers to as a project_id or tenant_id). - password = kwargs.pop('password', api_key) self.projectid = project_id self.tenant_id = tenant_id self.user_id = user_id diff --git a/releasenotes/notes/rename-apikey-to-password-735588d841efa49e.yaml b/releasenotes/notes/rename-apikey-to-password-735588d841efa49e.yaml new file mode 100644 index 000000000..36578342a --- /dev/null +++ b/releasenotes/notes/rename-apikey-to-password-735588d841efa49e.yaml @@ -0,0 +1,5 @@ +--- +deprecations: + - The **api_key** variable of novaclient.client.Client entry-point was + deprecated in favor of **password**. Nothing has changed in the case + of positional argument usage. From 9df9aff6fc921e7d4b5439f6bde76384f1d31d2f Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Fri, 2 Dec 2016 20:26:32 +0200 Subject: [PATCH 1203/1705] Rename interface to endpoint_type 'interface' argument was not described anywhere in novaclient's docs, but since we transmit all additional (unexpected) arguments to SessionClient implementation, keystoneclient was able to use it. Change-Id: Id7200a6df416694173b9a02206c53cef984f0fec --- novaclient/client.py | 19 +++++++++++++++++-- novaclient/tests/unit/v2/test_client.py | 7 ++----- ...-interface-parameter-e5fe166f39ba0935.yaml | 4 ++++ 3 files changed, 23 insertions(+), 7 deletions(-) create mode 100644 releasenotes/notes/restrict-interface-parameter-e5fe166f39ba0935.yaml diff --git a/novaclient/client.py b/novaclient/client.py index c7dbd2189..4cc5db2eb 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -690,7 +690,6 @@ def _construct_http_client(api_version=None, endpoint_type='publicURL', http_log_debug=False, insecure=False, - interface=None, os_cache=False, password=None, project_id=None, @@ -714,7 +713,7 @@ def _construct_http_client(api_version=None, return SessionClient(api_version=api_version, auth=auth, endpoint_override=endpoint_override, - interface=interface or endpoint_type, + interface=endpoint_type, region_name=region_name, service_name=service_name, service_type=service_type, @@ -884,6 +883,22 @@ def Client(version, username=None, password=None, project_id=None, _check_arguments(kwargs, "Ocata", "bypass_url", right_name="endpoint_override") _check_arguments(kwargs, "Ocata", "api_key", right_name="password") + # NOTE(andreykurilin): OpenStack projects use two variables with one + # meaning: 'endpoint_type' and 'interface'. 'endpoint_type' is an old + # name which was used by most OpenStack clients. Later it was replaced by + # 'interface' in keystone and later some other clients switched to new + # variable name too. In case of novaclient, there is no need to switch to + # 'interface' variable name due too several reasons: + # - novaclient uses 'endpoint_type' variable name long time ago and + # there is no real reasons to switch to new name; + # - 'interface' argument is used in several shell subcommands + # (for example in `nova floating-ip-bulk-create`), so we will need to + # modify these subcommands to not conflict with global flag + # 'interface' + # Actually, novaclient did not accept 'interface' before, but since we + # allow additional arguments(kwargs), someone can use this variable name + # and face issue about unexpected behavior. + _check_arguments(kwargs, "Ocata", "interface", right_name="endpoint_type") api_version, client_class = _get_client_class_and_version(version) kwargs.pop("direct_use", None) diff --git a/novaclient/tests/unit/v2/test_client.py b/novaclient/tests/unit/v2/test_client.py index 94ba44cee..02a9e4d45 100644 --- a/novaclient/tests/unit/v2/test_client.py +++ b/novaclient/tests/unit/v2/test_client.py @@ -36,15 +36,12 @@ def test_adapter_properties(self): self.assertEqual(user_agent, c.client.user_agent) self.assertEqual(endpoint_override, c.client.endpoint_override) - def test_passing_interface(self): + def test_passing_endpoint_type(self): endpoint_type = uuid.uuid4().hex - interface = uuid.uuid4().hex s = session.Session() c = client.Client(session=s, - api_version=api_versions.APIVersion("2.0"), - interface=interface, endpoint_type=endpoint_type, direct_use=False) - self.assertEqual(interface, c.client.interface) + self.assertEqual(endpoint_type, c.client.interface) diff --git a/releasenotes/notes/restrict-interface-parameter-e5fe166f39ba0935.yaml b/releasenotes/notes/restrict-interface-parameter-e5fe166f39ba0935.yaml new file mode 100644 index 000000000..1ac4a1e76 --- /dev/null +++ b/releasenotes/notes/restrict-interface-parameter-e5fe166f39ba0935.yaml @@ -0,0 +1,4 @@ +--- +deprecations: + - keyword argument **interface** of novaclient.client.Client entry-point was + deprecated in favor of **endpoint_type**; From 9bbe5a87b7df62a7962debba5db7c96555da6761 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Fri, 2 Dec 2016 20:39:38 +0200 Subject: [PATCH 1204/1705] Clarify meaning of project_id var Meaning of project_id (argument of novaclient.client) was not clear. In different cases, it can be tenant/project id or name (in terms of Keystone). * tenant_id - this variable is deprecated now and project_id should be used instead; * project_id - meaning of this variable is clarified now and it expects Project ID (in terms of keystone) now. Change-Id: Ie3fc6b50260203c0debe8664bb87a7d72fb645d7 --- novaclient/client.py | 16 ++++++-- novaclient/shell.py | 4 +- novaclient/tests/unit/v2/test_auth.py | 39 +++++++++++-------- novaclient/v2/client.py | 26 +++++++++---- ...-project-id-variable-5832698315000438.yaml | 13 +++++++ 5 files changed, 68 insertions(+), 30 deletions(-) create mode 100644 releasenotes/notes/clarify-project-id-variable-5832698315000438.yaml diff --git a/novaclient/client.py b/novaclient/client.py index 4cc5db2eb..8c85d4951 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -693,13 +693,13 @@ def _construct_http_client(api_version=None, os_cache=False, password=None, project_id=None, + project_name=None, proxy_tenant_id=None, proxy_token=None, region_name=None, service_name=None, service_type='compute', session=None, - tenant_id=None, timeout=None, timings=False, user_agent='python-novaclient', @@ -728,8 +728,12 @@ def _construct_http_client(api_version=None, return HTTPClient(username, password, user_id=user_id, - projectid=project_id, - tenant_id=tenant_id, + # NOTE(andreykurilin): HTTPClient will be replaced + # fully by SessionClient soon, so there are no + # reasons to spend time renaming projectid variable + # to tenant_name/project_name. + projectid=project_name, + tenant_id=project_id, auth_url=auth_url, auth_token=auth_token, insecure=insecure, @@ -873,6 +877,9 @@ def Client(version, username=None, password=None, project_id=None, """ if password: kwargs["password"] = password + if project_id: + kwargs["project_id"] = project_id + _check_arguments(kwargs, "Ocata", "auth_plugin") _check_arguments(kwargs, "Ocata", "auth_system") if "no_cache" in kwargs: @@ -899,12 +906,13 @@ def Client(version, username=None, password=None, project_id=None, # allow additional arguments(kwargs), someone can use this variable name # and face issue about unexpected behavior. _check_arguments(kwargs, "Ocata", "interface", right_name="endpoint_type") + _check_arguments(kwargs, "Ocata", "tenant_name", right_name="project_name") + _check_arguments(kwargs, "Ocata", "tenant_id", right_name="project_id") api_version, client_class = _get_client_class_and_version(version) kwargs.pop("direct_use", None) return client_class(api_version=api_version, auth_url=auth_url, direct_use=False, - project_id=project_id, username=username, **kwargs) diff --git a/novaclient/shell.py b/novaclient/shell.py index d35691144..95ee12eac 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -759,8 +759,8 @@ def main(self, argv): # microversion, so we just pass version 2 at here. self.cs = client.Client( api_versions.APIVersion("2.0"), - os_username, os_password, os_project_name, - tenant_id=os_project_id, user_id=os_user_id, + os_username, os_password, project_id=os_project_id, + project_name=os_project_name, user_id=os_user_id, auth_url=os_auth_url, insecure=insecure, region_name=os_region_name, endpoint_type=endpoint_type, extensions=self.extensions, service_type=service_type, diff --git a/novaclient/tests/unit/v2/test_auth.py b/novaclient/tests/unit/v2/test_auth.py index 1fdbf3ad2..ec9ba3e91 100644 --- a/novaclient/tests/unit/v2/test_auth.py +++ b/novaclient/tests/unit/v2/test_auth.py @@ -39,8 +39,8 @@ def get_token(self, **kwargs): return resp def test_authenticate_success(self): - cs = Client("username", "password", "project_id", utils.AUTH_URL_V2, - service_type='compute') + cs = Client("username", "password", project_name="project_id", + auth_url=utils.AUTH_URL_V2, service_type='compute') resp = self.get_token() auth_response = utils.TestResponse({ @@ -86,7 +86,8 @@ def test_auth_call(): test_auth_call() def test_authenticate_failure(self): - cs = Client("username", "password", "project_id", utils.AUTH_URL_V2) + cs = Client("username", "password", project_name="project_id", + auth_url=utils.AUTH_URL_V2) resp = {"unauthorized": {"message": "Unauthorized", "code": "401"}} auth_response = utils.TestResponse({ "status_code": 401, @@ -102,8 +103,8 @@ def test_auth_call(): test_auth_call() def test_v1_auth_redirect(self): - cs = Client("username", "password", "project_id", utils.AUTH_URL_V1, - service_type='compute') + cs = Client("username", "password", project_name="project_id", + auth_url=utils.AUTH_URL_V1, service_type='compute') dict_correct_response = self.get_token() correct_response = json.dumps(dict_correct_response) dict_responses = [ @@ -167,8 +168,8 @@ def test_auth_call(): test_auth_call() def test_v2_auth_redirect(self): - cs = Client("username", "password", "project_id", utils.AUTH_URL_V2, - service_type='compute') + cs = Client("username", "password", project_name="project_id", + auth_url=utils.AUTH_URL_V2, service_type='compute') dict_correct_response = self.get_token() correct_response = json.dumps(dict_correct_response) dict_responses = [ @@ -232,8 +233,8 @@ def test_auth_call(): test_auth_call() def test_ambiguous_endpoints(self): - cs = Client("username", "password", "project_id", utils.AUTH_URL_V2, - service_type='compute') + cs = Client("username", "password", project_name="project_id", + auth_url=utils.AUTH_URL_V2, service_type='compute') resp = self.get_token() # duplicate existing service @@ -255,8 +256,8 @@ def test_auth_call(): test_auth_call() def test_authenticate_with_token_success(self): - cs = Client("username", None, "project_id", utils.AUTH_URL_V2, - service_type='compute') + cs = Client("username", None, project_name="project_id", + auth_url=utils.AUTH_URL_V2, service_type='compute') cs.client.auth_token = "FAKE_ID" resp = self.get_token(token_id="FAKE_ID") auth_response = utils.TestResponse({ @@ -298,7 +299,8 @@ def test_authenticate_with_token_success(self): self.assertEqual(cs.client.auth_token, token_id) def test_authenticate_with_token_failure(self): - cs = Client("username", None, "project_id", utils.AUTH_URL_V2) + cs = Client("username", None, project_name="project_id", + auth_url=utils.AUTH_URL_V2) cs.client.auth_token = "FAKE_ID" resp = {"unauthorized": {"message": "Unauthorized", "code": "401"}} auth_response = utils.TestResponse({ @@ -314,7 +316,8 @@ def test_authenticate_with_token_failure(self): class AuthenticationTests(utils.TestCase): def test_authenticate_success(self): - cs = Client("username", "password", "project_id", utils.AUTH_URL) + cs = Client("username", "password", project_name="project_id", + auth_url=utils.AUTH_URL) management_url = 'https://localhost/v1.1/443470' auth_response = utils.TestResponse({ 'status_code': 204, @@ -349,7 +352,8 @@ def test_auth_call(): test_auth_call() def test_authenticate_failure(self): - cs = Client("username", "password", "project_id", utils.AUTH_URL) + cs = Client("username", "password", project_name="project_id", + auth_url=utils.AUTH_URL) auth_response = utils.TestResponse({'status_code': 401}) mock_request = mock.Mock(return_value=(auth_response)) @@ -360,8 +364,8 @@ def test_auth_call(): test_auth_call() def test_auth_automatic(self): - cs = Client("username", "password", "project_id", utils.AUTH_URL, - direct_use=False) + cs = Client("username", "password", project_name="project_id", + auth_url=utils.AUTH_URL, direct_use=False) http_client = cs.client http_client.management_url = '' http_client.get_service_url = mock.Mock(return_value='') @@ -377,7 +381,8 @@ def test_auth_call(m): test_auth_call() def test_auth_manual(self): - cs = Client("username", "password", "project_id", utils.AUTH_URL) + cs = Client("username", "password", project_name="project_id", + auth_url=utils.AUTH_URL) @mock.patch.object(cs.client, 'authenticate') def test_auth_call(m): diff --git a/novaclient/v2/client.py b/novaclient/v2/client.py index e903c1a69..7dcc2b681 100644 --- a/novaclient/v2/client.py +++ b/novaclient/v2/client.py @@ -19,7 +19,7 @@ from novaclient import client from novaclient import exceptions -from novaclient.i18n import _LE +from novaclient.i18n import _LE, _LW from novaclient.v2 import agents from novaclient.v2 import aggregates from novaclient.v2 import assisted_volume_snapshots @@ -85,13 +85,13 @@ def __init__(self, os_cache=False, password=None, project_id=None, + project_name=None, proxy_tenant_id=None, proxy_token=None, region_name=None, service_name=None, service_type='compute', session=None, - tenant_id=None, timeout=None, timings=False, user_id=None, @@ -117,14 +117,14 @@ def __init__(self, logging stuff :param str password: User password :param bool os_cache: OS cache - :param str project_id: Project ID + :param str project_id: Project/Tenant ID + :param str project_name: Project/Tenant name :param str proxy_tenant_id: Tenant ID :param str proxy_token: Proxy Token :param str region_name: Region Name :param str service_name: Service Name :param str service_type: Service Type :param str session: Session - :param str tenant_id: Tenant ID :param float timeout: API timeout, None or 0 disables :param bool timings: Timings :param str user_id: User ID @@ -145,8 +145,8 @@ def __init__(self, # tenant name) and tenant_id is a UUID (what the Nova API # often refers to as a project_id or tenant_id). - self.projectid = project_id - self.tenant_id = tenant_id + self.project_id = project_id + self.project_name = project_name self.user_id = user_id self.flavors = flavors.FlavorManager(self) self.flavor_access = flavor_access.FlavorAccessManager(self) @@ -238,13 +238,13 @@ def __init__(self, os_cache=self.os_cache, password=password, project_id=project_id, + project_name=project_name, proxy_tenant_id=proxy_tenant_id, proxy_token=proxy_token, region_name=region_name, service_name=service_name, service_type=service_type, session=session, - tenant_id=tenant_id, timeout=timeout, timings=timings, user_id=user_id, @@ -260,6 +260,18 @@ def api_version(self): def api_version(self, value): self.client.api_version = value + @property + def projectid(self): + self.logger.warning(_LW("Property 'projectid' is deprecated since " + "Ocata. Use 'project_name' instead.")) + return self.project_name + + @property + def tenant_id(self): + self.logger.warning(_LW("Property 'tenant_id' is deprecated since " + "Ocata. Use 'project_id' instead.")) + return self.project_id + @client._original_only def __enter__(self): self.client.open_session() diff --git a/releasenotes/notes/clarify-project-id-variable-5832698315000438.yaml b/releasenotes/notes/clarify-project-id-variable-5832698315000438.yaml new file mode 100644 index 000000000..16b5b5de4 --- /dev/null +++ b/releasenotes/notes/clarify-project-id-variable-5832698315000438.yaml @@ -0,0 +1,13 @@ +--- +deprecations: + - keyword argument **tenant_id** of novaclient.client.Client entry-point was + deprecated in favor of **project_id**; + - keyword argument **tenant_name** of novaclient.client.Client entry-point + was deprecated in favor of **project_name**; +other: + - The meaning of 'project_id' variable of novaclient.client.Client + entry-point was not clear. In different cases it was used as ID or Name of + project(in terms of Keystone). The time to identify meaning is come, so now + project_id/tenant_id variables specifically mean about Project ID(in terms + of Keystone) and project_name/tenant_name variables take care about Project + Name(in terms of Keystone). From 77048cc27eb16ae2facd369e59f20c975ce2959c Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Thu, 8 Dec 2016 17:21:55 +0200 Subject: [PATCH 1205/1705] Deprecate proxy_token and proxy_tenant_id args http://lists.openstack.org/pipermail/openstack-dev/2016-October/106098.html Change-Id: I1ab6d8d36fd2b39c78c5111cbe8191f20e0cda3e --- novaclient/client.py | 6 ++---- novaclient/v2/client.py | 6 ------ .../notes/deprecate-proxy-args-a3f4e224f7664ff8.yaml | 5 +++++ 3 files changed, 7 insertions(+), 10 deletions(-) create mode 100644 releasenotes/notes/deprecate-proxy-args-a3f4e224f7664ff8.yaml diff --git a/novaclient/client.py b/novaclient/client.py index 8c85d4951..d37d6d6f7 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -694,8 +694,6 @@ def _construct_http_client(api_version=None, password=None, project_id=None, project_name=None, - proxy_tenant_id=None, - proxy_token=None, region_name=None, service_name=None, service_type='compute', @@ -738,8 +736,6 @@ def _construct_http_client(api_version=None, auth_token=auth_token, insecure=insecure, timeout=timeout, - proxy_token=proxy_token, - proxy_tenant_id=proxy_tenant_id, region_name=region_name, endpoint_type=endpoint_type, service_type=service_type, @@ -908,6 +904,8 @@ def Client(version, username=None, password=None, project_id=None, _check_arguments(kwargs, "Ocata", "interface", right_name="endpoint_type") _check_arguments(kwargs, "Ocata", "tenant_name", right_name="project_name") _check_arguments(kwargs, "Ocata", "tenant_id", right_name="project_id") + _check_arguments(kwargs, "Ocata", "proxy_tenant_id") + _check_arguments(kwargs, "Ocata", "proxy_token") api_version, client_class = _get_client_class_and_version(version) kwargs.pop("direct_use", None) diff --git a/novaclient/v2/client.py b/novaclient/v2/client.py index 7dcc2b681..6b46aed0f 100644 --- a/novaclient/v2/client.py +++ b/novaclient/v2/client.py @@ -86,8 +86,6 @@ def __init__(self, password=None, project_id=None, project_name=None, - proxy_tenant_id=None, - proxy_token=None, region_name=None, service_name=None, service_type='compute', @@ -119,8 +117,6 @@ def __init__(self, :param bool os_cache: OS cache :param str project_id: Project/Tenant ID :param str project_name: Project/Tenant name - :param str proxy_tenant_id: Tenant ID - :param str proxy_token: Proxy Token :param str region_name: Region Name :param str service_name: Service Name :param str service_type: Service Type @@ -239,8 +235,6 @@ def __init__(self, password=password, project_id=project_id, project_name=project_name, - proxy_tenant_id=proxy_tenant_id, - proxy_token=proxy_token, region_name=region_name, service_name=service_name, service_type=service_type, diff --git a/releasenotes/notes/deprecate-proxy-args-a3f4e224f7664ff8.yaml b/releasenotes/notes/deprecate-proxy-args-a3f4e224f7664ff8.yaml new file mode 100644 index 000000000..887692a67 --- /dev/null +++ b/releasenotes/notes/deprecate-proxy-args-a3f4e224f7664ff8.yaml @@ -0,0 +1,5 @@ +--- +deprecations: + - There are two arguments for proxying stuff (proxy_tenant_id and + proxy_token) in novaclient.client.Client entry-point. They were never + documented and tested. From 2163e6661aa6bfc499e5b215630ecfc6aaec66f5 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Thu, 8 Dec 2016 17:37:02 +0200 Subject: [PATCH 1206/1705] Transmit all auth related vars from cli to inner methods We need cert, project_domain_id, project_domain_name, user_domain_id, user_domain_name arguments to constract SessionClient in proper way. These variables will be used in further patches Change-Id: I44416de7f4dc18916aecb6884158b416e696227e --- novaclient/client.py | 5 +++++ novaclient/shell.py | 22 ++++++++++++++++++---- novaclient/v2/client.py | 17 ++++++++++++++++- 3 files changed, 39 insertions(+), 5 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index d37d6d6f7..0d325dd1d 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -685,6 +685,7 @@ def _construct_http_client(api_version=None, auth_token=None, auth_url=None, cacert=None, + cert=None, connection_pool=False, endpoint_override=None, endpoint_type='publicURL', @@ -692,6 +693,8 @@ def _construct_http_client(api_version=None, insecure=False, os_cache=False, password=None, + project_domain_id=None, + project_domain_name=None, project_id=None, project_name=None, region_name=None, @@ -701,6 +704,8 @@ def _construct_http_client(api_version=None, timeout=None, timings=False, user_agent='python-novaclient', + user_domain_id=None, + user_domain_name=None, user_id=None, username=None, volume_service_name=None, diff --git a/novaclient/shell.py b/novaclient/shell.py index 95ee12eac..87ca7778b 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -662,6 +662,11 @@ def main(self, argv): if not args.os_user_domain_id and not args.os_user_domain_name: setattr(args, "os_user_domain_id", "default") + os_project_domain_id = args.os_project_domain_id + os_project_domain_name = args.os_project_domain_name + os_user_domain_id = args.os_project_domain_id + os_user_domain_name = args.os_project_domain_name + endpoint_type = args.endpoint_type insecure = args.insecure service_type = args.service_type @@ -670,6 +675,7 @@ def main(self, argv): endpoint_override = args.endpoint_override os_cache = args.os_cache cacert = args.os_cacert + cert = args.os_cert timeout = args.timeout keystone_session = None @@ -768,9 +774,13 @@ def main(self, argv): volume_service_name=volume_service_name, timings=args.timings, endpoint_override=endpoint_override, os_cache=os_cache, http_log_debug=args.debug, - cacert=cacert, timeout=timeout, + cacert=cacert, cert=cert, timeout=timeout, session=keystone_session, auth=keystone_auth, - logger=self.client_logger) + logger=self.client_logger, + project_domain_id=os_project_domain_id, + project_domain_name=os_project_domain_name, + user_domain_id=os_user_domain_id, + user_domain_name=os_user_domain_name) if not skip_auth: if not api_version.is_latest(): @@ -832,8 +842,12 @@ def main(self, argv): volume_service_name=volume_service_name, timings=args.timings, endpoint_override=endpoint_override, os_cache=os_cache, http_log_debug=args.debug, - cacert=cacert, timeout=timeout, - session=keystone_session, auth=keystone_auth) + cacert=cacert, cert=cert, timeout=timeout, + session=keystone_session, auth=keystone_auth, + project_domain_id=os_project_domain_id, + project_domain_name=os_project_domain_name, + user_domain_id=os_user_domain_id, + user_domain_name=os_user_domain_name) # Now check for the password/token of which pieces of the # identifying keyring key can come from the underlying client diff --git a/novaclient/v2/client.py b/novaclient/v2/client.py index 6b46aed0f..24da3e4f5 100644 --- a/novaclient/v2/client.py +++ b/novaclient/v2/client.py @@ -74,6 +74,7 @@ def __init__(self, auth_token=None, auth_url=None, cacert=None, + cert=None, connection_pool=False, direct_use=True, endpoint_override=None, @@ -84,6 +85,8 @@ def __init__(self, logger=None, os_cache=False, password=None, + project_domain_id=None, + project_domain_name=None, project_id=None, project_name=None, region_name=None, @@ -92,6 +95,8 @@ def __init__(self, session=None, timeout=None, timings=False, + user_domain_id=None, + user_domain_name=None, user_id=None, username=None, volume_service_name=None, @@ -103,7 +108,8 @@ def __init__(self, :param str auth: Auth :param str auth_token: Auth token :param str auth_url: Auth URL - :param str cacert: cacert + :param str cacert: ca-certificate + :param str cert: certificate :param bool direct_use: Inner variable of novaclient. Do not use it outside novaclient. It's restricted. :param str endpoint_override: Bypass URL @@ -115,6 +121,8 @@ def __init__(self, logging stuff :param str password: User password :param bool os_cache: OS cache + :param str project_domain_id: ID of project domain + :param str project_domain_name: Name of project domain :param str project_id: Project/Tenant ID :param str project_name: Project/Tenant name :param str region_name: Region Name @@ -123,6 +131,8 @@ def __init__(self, :param str session: Session :param float timeout: API timeout, None or 0 disables :param bool timings: Timings + :param str user_domain_id: ID of user domain + :param str user_domain_name: Name of user domain :param str user_id: User ID :param str username: Username :param str volume_service_name: Volume Service Name @@ -225,6 +235,7 @@ def __init__(self, auth_token=auth_token, auth_url=auth_url, cacert=cacert, + cert=cert, connection_pool=connection_pool, endpoint_override=endpoint_override, endpoint_type=endpoint_type, @@ -233,6 +244,8 @@ def __init__(self, logger=self.logger, os_cache=self.os_cache, password=password, + project_domain_id=project_domain_id, + project_domain_name=project_domain_name, project_id=project_id, project_name=project_name, region_name=region_name, @@ -241,6 +254,8 @@ def __init__(self, session=session, timeout=timeout, timings=timings, + user_domain_id=user_domain_id, + user_domain_name=user_domain_name, user_id=user_id, username=username, volume_service_name=volume_service_name, From b4bd07e5a7b9c3a64a696477ca414249a82ee65c Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Thu, 8 Dec 2016 17:46:14 +0200 Subject: [PATCH 1207/1705] Deprecate connection_pool variable This is the last variable which is redundant in case of SessionClient. After this patch we will be able to use SessionClient in 100% usecases. Change-Id: I102ba25eee7434cb66ea6efbf9ec4ab310d0957a --- novaclient/client.py | 3 +-- novaclient/v2/client.py | 2 -- .../notes/deprecate-connection-pool-arg-cef35346d5ebf40c.yaml | 3 +++ 3 files changed, 4 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/deprecate-connection-pool-arg-cef35346d5ebf40c.yaml diff --git a/novaclient/client.py b/novaclient/client.py index 0d325dd1d..7951c7e18 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -686,7 +686,6 @@ def _construct_http_client(api_version=None, auth_url=None, cacert=None, cert=None, - connection_pool=False, endpoint_override=None, endpoint_type='publicURL', http_log_debug=False, @@ -751,7 +750,6 @@ def _construct_http_client(api_version=None, os_cache=os_cache, http_log_debug=http_log_debug, cacert=cacert, - connection_pool=connection_pool, api_version=api_version, logger=logger) @@ -911,6 +909,7 @@ def Client(version, username=None, password=None, project_id=None, _check_arguments(kwargs, "Ocata", "tenant_id", right_name="project_id") _check_arguments(kwargs, "Ocata", "proxy_tenant_id") _check_arguments(kwargs, "Ocata", "proxy_token") + _check_arguments(kwargs, "Ocata", "connection_pool") api_version, client_class = _get_client_class_and_version(version) kwargs.pop("direct_use", None) diff --git a/novaclient/v2/client.py b/novaclient/v2/client.py index 24da3e4f5..d2a7c5d3d 100644 --- a/novaclient/v2/client.py +++ b/novaclient/v2/client.py @@ -75,7 +75,6 @@ def __init__(self, auth_url=None, cacert=None, cert=None, - connection_pool=False, direct_use=True, endpoint_override=None, endpoint_type='publicURL', @@ -236,7 +235,6 @@ def __init__(self, auth_url=auth_url, cacert=cacert, cert=cert, - connection_pool=connection_pool, endpoint_override=endpoint_override, endpoint_type=endpoint_type, http_log_debug=http_log_debug, diff --git a/releasenotes/notes/deprecate-connection-pool-arg-cef35346d5ebf40c.yaml b/releasenotes/notes/deprecate-connection-pool-arg-cef35346d5ebf40c.yaml new file mode 100644 index 000000000..e12e0d2f1 --- /dev/null +++ b/releasenotes/notes/deprecate-connection-pool-arg-cef35346d5ebf40c.yaml @@ -0,0 +1,3 @@ +--- +deprecations: + - The **connection_pool** variable is deprecated now and will be ignored. From e4dc84e0f6ebaefeed114d4ac1663d35730629a4 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Tue, 2 Aug 2016 18:50:34 +0300 Subject: [PATCH 1208/1705] Make SessionClient interface similar to HTTPClient HttpClient will be deprecated soon and SessionClient will be used by default. To not break integration novaclient with other project at the step of moving from HTTPClient, SessionClient should have similar interface. Change-Id: I855ccc5160dc7628f4550e93bf133adf8263aace --- novaclient/client.py | 15 +++++++++++++++ novaclient/tests/unit/test_client.py | 10 ---------- novaclient/tests/unit/v2/test_auth.py | 11 ----------- novaclient/v2/client.py | 19 ++++++++++++------- 4 files changed, 27 insertions(+), 28 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index 7951c7e18..8a840afcf 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -121,6 +121,21 @@ def get_timings(self): def reset_timings(self): self.times = [] + @property + def management_url(self): + self.logger.warning( + _LW("Property `management_url` is deprecated for SessionClient. " + "Use `endpoint_override` instead.")) + return self.endpoint_override + + @management_url.setter + def management_url(self, value): + self.logger.warning( + _LW("Property `management_url` is deprecated for SessionClient. " + "It should be set via `endpoint_override` variable while class" + " initialization.")) + self.endpoint_override = value + def _original_only(f): """Decorator to indicate and enforce original HTTPClient object. diff --git a/novaclient/tests/unit/test_client.py b/novaclient/tests/unit/test_client.py index ab25fe0b4..5ff6133ee 100644 --- a/novaclient/tests/unit/test_client.py +++ b/novaclient/tests/unit/test_client.py @@ -201,16 +201,6 @@ def test_client_get_reset_timings_v1_1(self): cs.reset_timings() self.assertEqual(0, len(cs.get_timings())) - @mock.patch('novaclient.client.HTTPClient') - def test_contextmanager_v1_1(self, mock_http_client): - fake_client = mock.Mock() - mock_http_client.return_value = fake_client - with novaclient.client.Client("2", "user", "password", "project_id", - auth_url="foo/v2"): - pass - self.assertTrue(fake_client.open_session.called) - self.assertTrue(fake_client.close_session.called) - def test_get_password_simple(self): cs = novaclient.client.HTTPClient("user", "password", "", "") cs.password_func = mock.Mock() diff --git a/novaclient/tests/unit/v2/test_auth.py b/novaclient/tests/unit/v2/test_auth.py index ec9ba3e91..512e6743d 100644 --- a/novaclient/tests/unit/v2/test_auth.py +++ b/novaclient/tests/unit/v2/test_auth.py @@ -379,14 +379,3 @@ def test_auth_call(m): self.assertTrue(mock_request.called) test_auth_call() - - def test_auth_manual(self): - cs = Client("username", "password", project_name="project_id", - auth_url=utils.AUTH_URL) - - @mock.patch.object(cs.client, 'authenticate') - def test_auth_call(m): - cs.authenticate() - self.assertTrue(m.called) - - test_auth_call() diff --git a/novaclient/v2/client.py b/novaclient/v2/client.py index d2a7c5d3d..35158fc9a 100644 --- a/novaclient/v2/client.py +++ b/novaclient/v2/client.py @@ -279,17 +279,22 @@ def tenant_id(self): "Ocata. Use 'project_id' instead.")) return self.project_id - @client._original_only def __enter__(self): - self.client.open_session() + self.logger.warning(_LW("NovaClient instance can't be used as a " + "context manager since Ocata (deprecated " + "behaviour) since it is redundant in case of " + "SessionClient.")) return self - @client._original_only def __exit__(self, t, v, tb): - self.client.close_session() + # do not do anything + pass - @client._original_only def set_management_url(self, url): + self.logger.warning( + _LW("Method `set_management_url` is deprecated since Ocata. " + "Use `endpoint_override` argument instead while initializing " + "novaclient's instance.")) self.client.set_management_url(url) def get_timings(self): @@ -315,7 +320,6 @@ def has_neutron(self): except key_ex.EndpointNotFound: return False - @client._original_only def authenticate(self): """Authenticate against the server. @@ -325,4 +329,5 @@ def authenticate(self): Returns on success; raises :exc:`exceptions.Unauthorized` if the credentials are wrong. """ - self.client.authenticate() + self.logger.warning(_LW( + "Method 'authenticate' is deprecated since Ocata.")) From 8409e006c5f362922baae9470f14c12e0443dd70 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Fri, 17 Jun 2016 14:45:55 +0300 Subject: [PATCH 1209/1705] Create keystone session instance if not present Since all authentication plugins should be stored in Keystone, there is no need to share responsibility for authentication stuff with Keystone team. Let's use convinient and the right way to use keystone - keystone sessions. We have own adapter for it - novaclient.client.Session which can be used in 100% cases of novaclient usage. Let's create session object if it is not transmitted via arguments and get rid of novaclient.client.HTTPClient implementation. NOTE: novaclient.client.HTTPClient and all related code will be removed separately. Co-Authored-By: Clenimar Filemon Change-Id: Ibb99fdafbf02b3e92f1a5d04d168151b3400b901 --- novaclient/client.py | 82 +++++++++---------- novaclient/shell.py | 37 ++------- novaclient/tests/unit/fixture_data/servers.py | 2 +- novaclient/tests/unit/test_client.py | 20 +---- novaclient/tests/unit/v2/test_auth.py | 11 +++ ...tch-to-sessionclient-aa49d16599fea570.yaml | 10 +++ 6 files changed, 67 insertions(+), 95 deletions(-) create mode 100644 releasenotes/notes/switch-to-sessionclient-aa49d16599fea570.yaml diff --git a/novaclient/client.py b/novaclient/client.py index 8a840afcf..7780c9a6b 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -30,7 +30,8 @@ import warnings from keystoneauth1 import adapter -from keystoneauth1 import session +from keystoneauth1 import identity +from keystoneauth1 import session as ksession from oslo_utils import importutils from oslo_utils import netutils import pkg_resources @@ -65,7 +66,7 @@ def __init__(self): def get(self, url): """Store and reuse HTTP adapters per Service URL.""" if url not in self._adapters: - self._adapters[url] = session.TCPKeepAliveAdapter() + self._adapters[url] = ksession.TCPKeepAliveAdapter() return self._adapters[url] @@ -705,6 +706,7 @@ def _construct_http_client(api_version=None, endpoint_type='publicURL', http_log_debug=False, insecure=False, + logger=None, os_cache=False, password=None, project_domain_id=None, @@ -724,49 +726,39 @@ def _construct_http_client(api_version=None, username=None, volume_service_name=None, **kwargs): - # TODO(mordred): If not session, just make a Session, then return - # SessionClient always - if session: - return SessionClient(api_version=api_version, - auth=auth, - endpoint_override=endpoint_override, - interface=endpoint_type, - region_name=region_name, - service_name=service_name, - service_type=service_type, - session=session, - timings=timings, - user_agent=user_agent, - **kwargs) - else: - # FIXME(jamielennox): username and password are now optional. Need - # to test that they were provided in this mode. - logger = kwargs.get('logger') - return HTTPClient(username, - password, - user_id=user_id, - # NOTE(andreykurilin): HTTPClient will be replaced - # fully by SessionClient soon, so there are no - # reasons to spend time renaming projectid variable - # to tenant_name/project_name. - projectid=project_name, - tenant_id=project_id, - auth_url=auth_url, - auth_token=auth_token, - insecure=insecure, - timeout=timeout, - region_name=region_name, - endpoint_type=endpoint_type, - service_type=service_type, - service_name=service_name, - volume_service_name=volume_service_name, - timings=timings, - bypass_url=endpoint_override, - os_cache=os_cache, - http_log_debug=http_log_debug, - cacert=cacert, - api_version=api_version, - logger=logger) + if not session: + if not auth and auth_token: + auth = identity.Token(auth_url=auth_url, + token=auth_token) + elif not auth: + auth = identity.Password(username=username, + user_id=user_id, + password=password, + project_id=project_id, + project_name=project_name, + auth_url=auth_url, + project_domain_id=project_domain_id, + project_domain_name=project_domain_name, + user_domain_id=user_domain_id, + user_domain_name=user_domain_name) + session = ksession.Session(auth=auth, + verify=(cacert or not insecure), + timeout=timeout, + cert=cert, + user_agent=user_agent) + + return SessionClient(api_version=api_version, + auth=auth, + endpoint_override=endpoint_override, + interface=endpoint_type, + logger=logger, + region_name=region_name, + service_name=service_name, + service_type=service_type, + session=session, + timings=timings, + user_agent=user_agent, + **kwargs) def discover_extensions(version, only_contrib=False): diff --git a/novaclient/shell.py b/novaclient/shell.py index 87ca7778b..e6c6bcc01 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -674,6 +674,7 @@ def main(self, argv): volume_service_name = args.volume_service_name endpoint_override = args.endpoint_override os_cache = args.os_cache + cert = args.os_cert cacert = args.os_cacert cert = args.os_cert timeout = args.timeout @@ -707,11 +708,6 @@ def main(self, argv): # Expired tokens are handled by client.py:_cs_request must_auth = not (auth_token and endpoint_override) - # Do not use Keystone session for cases with no session support. - use_session = True - if endpoint_override or os_cache or volume_service_name: - use_session = False - # FIXME(usrleon): Here should be restrict for project id same as # for os_username or os_password but for compatibility it is not. if must_auth and not skip_auth: @@ -735,17 +731,12 @@ def main(self, argv): _("You must provide an auth url " "via either --os-auth-url or env[OS_AUTH_URL].")) - if use_session: - # Not using Nova auth plugin, so use keystone - with utils.record_time(self.times, args.timings, - 'auth_url', args.os_auth_url): - keystone_session = ( - loading.load_session_from_argparse_arguments(args)) - keystone_auth = ( - loading.load_auth_from_argparse_arguments(args)) - else: - # set password for auth plugins - os_password = args.os_password + with utils.record_time(self.times, args.timings, + 'auth_url', args.os_auth_url): + keystone_session = ( + loading.load_session_from_argparse_arguments(args)) + keystone_auth = ( + loading.load_auth_from_argparse_arguments(args)) if (not skip_auth and not any([os_project_name, os_project_id])): @@ -870,20 +861,6 @@ def main(self, argv): # the result in our helper. self.cs.client.password = helper.password - try: - # This does a couple of bits which are useful even if we've - # got the token + service URL already. It exits fast in that case. - if not utils.isunauthenticated(args.func): - if not use_session: - # Only call authenticate() if Nova auth plugin is used. - # If keystone is used, authentication is handled as part - # of session. - self.cs.authenticate() - except exc.Unauthorized: - raise exc.CommandError(_("Invalid OpenStack Nova credentials.")) - except exc.AuthorizationFailure: - raise exc.CommandError(_("Unable to authorize user")) - args.func(self.cs, args) if args.timings: diff --git a/novaclient/tests/unit/fixture_data/servers.py b/novaclient/tests/unit/fixture_data/servers.py index 891814b2f..30bdae58e 100644 --- a/novaclient/tests/unit/fixture_data/servers.py +++ b/novaclient/tests/unit/fixture_data/servers.py @@ -329,7 +329,7 @@ def post_os_volumes_boot(request, context): headers=self.json_headers) def put_server_tag(request, context): - assert request.json() is None + assert request.text is None context.status_code = 201 return None diff --git a/novaclient/tests/unit/test_client.py b/novaclient/tests/unit/test_client.py index 5ff6133ee..8ebdb7f1b 100644 --- a/novaclient/tests/unit/test_client.py +++ b/novaclient/tests/unit/test_client.py @@ -172,25 +172,7 @@ def test_get_client_class_latest(self): self.assertRaises(novaclient.exceptions.UnsupportedVersion, novaclient.client.get_client_class, '2.latest') - def test_client_with_os_cache_enabled(self): - cs = novaclient.client.Client("2", "user", "password", "project_id", - auth_url="foo/v2", os_cache=True) - self.assertTrue(cs.os_cache) - self.assertTrue(cs.client.os_cache) - - def test_client_with_os_cache_disabled(self): - cs = novaclient.client.Client("2", "user", "password", "project_id", - auth_url="foo/v2", os_cache=False) - self.assertFalse(cs.os_cache) - self.assertFalse(cs.client.os_cache) - - def test_client_set_management_url_v1_1(self): - cs = novaclient.client.Client("2", "user", "password", "project_id", - auth_url="foo/v2") - cs.set_management_url("blabla") - self.assertEqual("blabla", cs.client.management_url) - - def test_client_get_reset_timings_v1_1(self): + def test_client_get_reset_timings_v2(self): cs = novaclient.client.Client("2", "user", "password", "project_id", auth_url="foo/v2") self.assertEqual(0, len(cs.get_timings())) diff --git a/novaclient/tests/unit/v2/test_auth.py b/novaclient/tests/unit/v2/test_auth.py index 512e6743d..f99b233b3 100644 --- a/novaclient/tests/unit/v2/test_auth.py +++ b/novaclient/tests/unit/v2/test_auth.py @@ -29,6 +29,11 @@ def Client(*args, **kwargs): class AuthenticateAgainstKeystoneTests(utils.TestCase): + def setUp(self): + super(AuthenticateAgainstKeystoneTests, self).setUp() + self.skipTest("This TestCase checks deprecated authentication " + "methods, which will be removed in separate patch.") + def get_token(self, **kwargs): resp = fixture.V2Token(**kwargs) resp.set_scope() @@ -315,6 +320,12 @@ def test_authenticate_with_token_failure(self): class AuthenticationTests(utils.TestCase): + + def setUp(self): + super(AuthenticationTests, self).setUp() + self.skipTest("This TestCase checks deprecated authentication " + "methods, which will be removed in separate patch.") + def test_authenticate_success(self): cs = Client("username", "password", project_name="project_id", auth_url=utils.AUTH_URL) diff --git a/releasenotes/notes/switch-to-sessionclient-aa49d16599fea570.yaml b/releasenotes/notes/switch-to-sessionclient-aa49d16599fea570.yaml new file mode 100644 index 000000000..4a60f7ec4 --- /dev/null +++ b/releasenotes/notes/switch-to-sessionclient-aa49d16599fea570.yaml @@ -0,0 +1,10 @@ +--- +upgrade: + - | + When using novaclient as a library (via novaclient.client.Client + entry-point), an internal implementation of HTTPClient was used if a + session object is not specified. For better user experience we switched + to using SessionClient which uses keystoneauth (Keystone folks maintain + this library) for all auth stuff. + The SessionClient interface is similar to HTTPClient, but there is a + small possibility that you will notice a difference. From 2e5576dafc21906a95196054f85c80b57c48663a Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Thu, 8 Dec 2016 19:09:48 +0200 Subject: [PATCH 1210/1705] Remove all code related to HTTPClient In previous patch we switched to use SessionClient in all cases. It means that all HTTPClient code is redundant now. Change-Id: I5a0da970fd82c79db3d88f1b49279133bbdba639 --- novaclient/client.py | 586 ------------------ novaclient/service_catalog.py | 89 --- novaclient/tests/unit/fixture_data/servers.py | 2 +- novaclient/tests/unit/test_client.py | 367 +---------- novaclient/tests/unit/test_http.py | 220 ------- novaclient/tests/unit/test_service_catalog.py | 60 -- novaclient/tests/unit/v2/fakes.py | 88 +-- novaclient/tests/unit/v2/test_auth.py | 392 ------------ novaclient/tests/unit/v2/test_shell.py | 13 +- novaclient/tests/unit/v2/test_versions.py | 70 +-- novaclient/v2/versions.py | 74 +-- 11 files changed, 74 insertions(+), 1887 deletions(-) delete mode 100644 novaclient/service_catalog.py delete mode 100644 novaclient/tests/unit/test_http.py delete mode 100644 novaclient/tests/unit/test_service_catalog.py delete mode 100644 novaclient/tests/unit/v2/test_auth.py diff --git a/novaclient/client.py b/novaclient/client.py index 7780c9a6b..ec07c38f5 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -20,35 +20,20 @@ OpenStack Client interface. Handles the REST calls and responses. """ -import copy -import functools -import hashlib import itertools -import logging import pkgutil -import re import warnings from keystoneauth1 import adapter from keystoneauth1 import identity from keystoneauth1 import session as ksession from oslo_utils import importutils -from oslo_utils import netutils import pkg_resources -import requests - -try: - import json -except ImportError: - import simplejson as json - -from six.moves.urllib import parse from novaclient import api_versions from novaclient import exceptions from novaclient import extension as ext from novaclient.i18n import _, _LW -from novaclient import service_catalog from novaclient import utils @@ -58,19 +43,6 @@ extensions_ignored_name = ["__init__"] -class _ClientConnectionPool(object): - - def __init__(self): - self._adapters = {} - - def get(self, url): - """Store and reuse HTTP adapters per Service URL.""" - if url not in self._adapters: - self._adapters[url] = ksession.TCPKeepAliveAdapter() - - return self._adapters[url] - - def _log_request_id(logger, resp, service_name): request_id = (resp.headers.get('x-openstack-request-id') or resp.headers.get('x-compute-request-id')) @@ -138,564 +110,6 @@ def management_url(self, value): self.endpoint_override = value -def _original_only(f): - """Decorator to indicate and enforce original HTTPClient object. - - Indicates and enforces that this function can only be used if we are - using the original HTTPClient object. - We use this to specify that if you use the newer Session HTTP client then - you are aware that the way you use your client has been updated and certain - functions are no longer allowed to be used. - """ - @functools.wraps(f) - def wrapper(self, *args, **kwargs): - if isinstance(self.client, SessionClient): - msg = ('This call is no longer available. The operation should ' - 'be performed on the session object instead.') - raise exceptions.InvalidUsage(msg) - - return f(self, *args, **kwargs) - - return wrapper - - -class HTTPClient(object): - USER_AGENT = 'python-novaclient' - - def __init__(self, user, password, projectid=None, auth_url=None, - insecure=False, timeout=None, proxy_tenant_id=None, - proxy_token=None, region_name=None, - endpoint_type='publicURL', service_type=None, - service_name=None, volume_service_name=None, - timings=False, bypass_url=None, - os_cache=False, no_cache=True, - http_log_debug=False, auth_token=None, - cacert=None, tenant_id=None, user_id=None, - connection_pool=False, api_version=None, - logger=None): - self.user = user - self.user_id = user_id - self.password = password - self.projectid = projectid - self.tenant_id = tenant_id - self.api_version = api_version or api_versions.APIVersion() - - self._connection_pool = (_ClientConnectionPool() - if connection_pool else None) - - # This will be called by #_get_password if self.password is None. - # EG if a password can only be obtained by prompting the user, but a - # token is available, you don't want to prompt until the token has - # been proven invalid - self.password_func = None - - self.auth_url = auth_url.rstrip('/') if auth_url else auth_url - self.version = 'v1.1' - self.region_name = region_name - self.endpoint_type = endpoint_type - self.service_type = service_type - self.service_name = service_name - self.volume_service_name = volume_service_name - self.timings = timings - self.bypass_url = bypass_url.rstrip('/') if bypass_url else bypass_url - self.os_cache = os_cache or not no_cache - self.http_log_debug = http_log_debug - if timeout is not None: - self.timeout = float(timeout) - else: - self.timeout = None - - self.times = [] # [("item", starttime, endtime), ...] - - self.management_url = self.bypass_url or None - self.auth_token = auth_token - self.proxy_token = proxy_token - self.proxy_tenant_id = proxy_tenant_id - self.keyring_saver = None - self.keyring_saved = False - - if insecure: - self.verify_cert = False - else: - if cacert: - self.verify_cert = cacert - else: - self.verify_cert = True - - self._session = None - self._current_url = None - self._logger = logger or logging.getLogger(__name__) - - if (self.http_log_debug and logger is None and - not self._logger.handlers): - # Logging level is already set on the root logger - ch = logging.StreamHandler() - self._logger.addHandler(ch) - self._logger.propagate = False - if hasattr(requests, 'logging'): - rql = requests.logging.getLogger(requests.__name__) - rql.addHandler(ch) - # Since we have already setup the root logger on debug, we - # have to set it up here on WARNING (its original level) - # otherwise we will get all the requests logging messages - rql.setLevel(logging.WARNING) - - self.service_catalog = None - self.services_url = {} - self.last_request_id = None - - def use_token_cache(self, use_it): - self.os_cache = use_it - - def unauthenticate(self): - """Forget all of our authentication information.""" - self.management_url = None - self.auth_token = None - - def set_management_url(self, url): - self.management_url = url - - def get_timings(self): - return self.times - - def reset_timings(self): - self.times = [] - - def _redact(self, target, path, text=None): - """Replace the value of a key in `target`. - - The key can be at the top level by specifying a list with a single - key as the path. Nested dictionaries are also supported by passing a - list of keys to be navigated to find the one that should be replaced. - In this case the last one is the one that will be replaced. - - :param dict target: the dictionary that may have a key to be redacted; - modified in place - :param list path: a list representing the nested structure in `target` - that should be redacted; modified in place - :param string text: optional text to use as a replacement for the - redacted key. if text is not specified, the - default text will be sha1 hash of the value being - redacted - """ - - key = path.pop() - - # move to the most nested dict - for p in path: - try: - target = target[p] - except KeyError: - return - - if key in target: - if text: - target[key] = text - elif target[key] is not None: - # because in python3 byte string handling is ... ug - value = target[key].encode('utf-8') - sha1sum = hashlib.sha1(value) - target[key] = "{SHA1}%s" % sha1sum.hexdigest() - - def http_log_req(self, method, url, kwargs): - if not self.http_log_debug: - return - - string_parts = ['curl -g -i'] - - if not kwargs.get('verify', True): - string_parts.append(' --insecure') - - string_parts.append(" '%s'" % url) - string_parts.append(' -X %s' % method) - - headers = copy.deepcopy(kwargs['headers']) - self._redact(headers, ['X-Auth-Token']) - # because dict ordering changes from 2 to 3 - keys = sorted(headers.keys()) - for name in keys: - value = headers[name] - header = ' -H "%s: %s"' % (name, value) - string_parts.append(header) - - if 'data' in kwargs: - data = json.loads(kwargs['data']) - self._redact(data, ['auth', 'passwordCredentials', 'password']) - string_parts.append(" -d '%s'" % json.dumps(data)) - self._logger.debug("REQ: %s" % "".join(string_parts)) - - def http_log_resp(self, resp): - if not self.http_log_debug: - return - - if resp.text and resp.status_code != 400: - try: - body = json.loads(resp.text) - self._redact(body, ['access', 'token', 'id']) - except ValueError: - body = None - else: - body = None - - self._logger.debug("RESP: [%(status)s] %(headers)s\nRESP BODY: " - "%(text)s\n", {'status': resp.status_code, - 'headers': resp.headers, - 'text': json.dumps(body)}) - - # if service name is None then use service_type for logging - service = self.service_name or self.service_type - _log_request_id(self._logger, resp, service) - - def open_session(self): - if not self._connection_pool: - self._session = requests.Session() - - def close_session(self): - if self._session and not self._connection_pool: - self._session.close() - self._session = None - - def _get_session(self, url): - if self._connection_pool: - magic_tuple = parse.urlsplit(url) - scheme, netloc, path, query, frag = magic_tuple - service_url = '%s://%s' % (scheme, netloc) - if self._current_url != service_url: - # Invalidate Session object in case the url is somehow changed - if self._session: - self._session.close() - self._current_url = service_url - self._logger.debug( - "New session created for: (%s)" % service_url) - self._session = requests.Session() - self._session.mount(service_url, - self._connection_pool.get(service_url)) - return self._session - elif self._session: - return self._session - - def request(self, url, method, **kwargs): - kwargs.setdefault('headers', kwargs.get('headers', {})) - kwargs['headers']['User-Agent'] = self.USER_AGENT - kwargs['headers']['Accept'] = 'application/json' - if 'body' in kwargs: - kwargs['headers']['Content-Type'] = 'application/json' - kwargs['data'] = json.dumps(kwargs.pop('body')) - api_versions.update_headers(kwargs["headers"], self.api_version) - if self.timeout is not None: - kwargs.setdefault('timeout', self.timeout) - kwargs['verify'] = self.verify_cert - - self.http_log_req(method, url, kwargs) - - request_func = requests.request - session = self._get_session(url) - if session: - request_func = session.request - - resp = request_func( - method, - url, - **kwargs) - - # TODO(andreykurilin): uncomment this line, when we will be able to - # check only nova-related calls - # api_versions.check_headers(resp, self.api_version) - - self.http_log_resp(resp) - - if resp.text: - # TODO(dtroyer): verify the note below in a requests context - # NOTE(alaski): Because force_exceptions_to_status_code=True - # httplib2 returns a connection refused event as a 400 response. - # To determine if it is a bad request or refused connection we need - # to check the body. httplib2 tests check for 'Connection refused' - # or 'actively refused' in the body, so that's what we'll do. - if resp.status_code == 400: - if ('Connection refused' in resp.text or - 'actively refused' in resp.text): - raise exceptions.ConnectionRefused(resp.text) - try: - body = json.loads(resp.text) - except ValueError: - body = None - else: - body = None - - self.last_request_id = (resp.headers.get('x-openstack-request-id') - if resp.headers else None) - if resp.status_code >= 400: - raise exceptions.from_response(resp, body, url, method) - - return resp, body - - def _time_request(self, url, method, **kwargs): - with utils.record_time(self.times, self.timings, method, url): - resp, body = self.request(url, method, **kwargs) - return resp, body - - def _cs_request(self, url, method, **kwargs): - if not self.management_url: - self.authenticate() - if url is None: - # To get API version information, it is necessary to GET - # a nova endpoint directly without "v2/". - magic_tuple = parse.urlsplit(self.management_url) - scheme, netloc, path, query, frag = magic_tuple - path = re.sub(r'v[1-9](\.[1-9][0-9]*)?/[a-z0-9]+$', '', path) - url = parse.urlunsplit((scheme, netloc, path, None, None)) - else: - if self.service_catalog and not self.bypass_url: - url = self.get_service_url(self.service_type) + url - else: - url = self.management_url + url - - # Perform the request once. If we get a 401 back then it - # might be because the auth token expired, so try to - # re-authenticate and try again. If it still fails, bail. - try: - kwargs.setdefault('headers', {})['X-Auth-Token'] = self.auth_token - if self.projectid: - kwargs['headers']['X-Auth-Project-Id'] = self.projectid - - resp, body = self._time_request(url, method, **kwargs) - return resp, body - except exceptions.Unauthorized as e: - try: - # first discard auth token, to avoid the possibly expired - # token being re-used in the re-authentication attempt - self.unauthenticate() - # overwrite bad token - self.keyring_saved = False - self.authenticate() - kwargs['headers']['X-Auth-Token'] = self.auth_token - resp, body = self._time_request(url, method, **kwargs) - return resp, body - except exceptions.Unauthorized: - raise e - - def _get_password(self): - if not self.password and self.password_func: - self.password = self.password_func() - return self.password - - def get(self, url, **kwargs): - return self._cs_request(url, 'GET', **kwargs) - - def post(self, url, **kwargs): - return self._cs_request(url, 'POST', **kwargs) - - def put(self, url, **kwargs): - return self._cs_request(url, 'PUT', **kwargs) - - def delete(self, url, **kwargs): - return self._cs_request(url, 'DELETE', **kwargs) - - def get_service_url(self, service_type): - if service_type not in self.services_url: - url = self.service_catalog.url_for( - attr='region', - filter_value=self.region_name, - endpoint_type=self.endpoint_type, - service_type=service_type, - service_name=self.service_name, - volume_service_name=self.volume_service_name,) - url = url.rstrip('/') - self.services_url[service_type] = url - return self.services_url[service_type] - - def _extract_service_catalog(self, url, resp, body, extract_token=True): - """Extract service catalog from input resource body. - - See what the auth service told us and process the response. - We may get redirected to another site, fail or actually get - back a service catalog with a token and our endpoints. - """ - - # content must always present - if resp.status_code == 200 or resp.status_code == 201: - try: - self.auth_url = url - self.service_catalog = \ - service_catalog.ServiceCatalog(body) - if extract_token: - self.auth_token = self.service_catalog.get_token() - self.tenant_id = self.service_catalog.get_tenant_id() - - self.management_url = self.get_service_url(self.service_type) - return None - except exceptions.AmbiguousEndpoints: - print(_("Found more than one valid endpoint. Use a more " - "restrictive filter")) - raise - except KeyError: - raise exceptions.AuthorizationFailure() - except exceptions.EndpointNotFound: - print(_("Could not find any suitable endpoint. Correct " - "region?")) - raise - - elif resp.status_code == 305: - return resp.headers['location'] - else: - raise exceptions.from_response(resp, body, url) - - def _fetch_endpoints_from_auth(self, url): - """Fetch endpoint using token. - - We have a token, but don't know the final endpoint for - the region. We have to go back to the auth service and - ask again. This request requires an admin-level token - to work. The proxy token supplied could be from a low-level enduser. - - We can't get this from the keystone service endpoint, we have to use - the admin endpoint. - - This will overwrite our admin token with the user token. - """ - - # GET ...:5001/v2.0/tokens/#####/endpoints - url = '/'.join([url, 'tokens', '%s?belongsTo=%s' - % (self.proxy_token, self.proxy_tenant_id)]) - self._logger.debug("Using Endpoint URL: %s" % url) - resp, body = self._time_request( - url, "GET", headers={'X-Auth-Token': self.auth_token}) - return self._extract_service_catalog(url, resp, body, - extract_token=False) - - def authenticate(self): - if not self.auth_url: - msg = _("Authentication requires 'auth_url', which should be " - "specified in '%s'") % self.__class__.__name__ - raise exceptions.AuthorizationFailure(msg) - magic_tuple = netutils.urlsplit(self.auth_url) - scheme, netloc, path, query, frag = magic_tuple - port = magic_tuple.port - if port is None: - port = 80 - path_parts = path.split('/') - for part in path_parts: - if len(part) > 0 and part[0] == 'v': - self.version = part - break - - if self.auth_token and self.management_url: - self._save_keys() - return - - # TODO(sandy): Assume admin endpoint is 35357 for now. - # Ideally this is going to have to be provided by the service catalog. - new_netloc = netloc.replace(':%d' % port, ':%d' % (35357,)) - admin_url = parse.urlunsplit( - (scheme, new_netloc, path, query, frag)) - - auth_url = self.auth_url - if self.version == "v2.0": # FIXME(chris): This should be better. - while auth_url: - auth_url = self._v2_auth(auth_url) - - # Are we acting on behalf of another user via an - # existing token? If so, our actual endpoints may - # be different than that of the admin token. - if self.proxy_token: - if self.bypass_url: - self.set_management_url(self.bypass_url) - else: - self._fetch_endpoints_from_auth(admin_url) - # Since keystone no longer returns the user token - # with the endpoints any more, we need to replace - # our service account token with the user token. - self.auth_token = self.proxy_token - else: - try: - while auth_url: - auth_url = self._v1_auth(auth_url) - # In some configurations nova makes redirection to - # v2.0 keystone endpoint. Also, new location does not contain - # real endpoint, only hostname and port. - except exceptions.AuthorizationFailure: - if auth_url.find('v2.0') < 0: - auth_url = auth_url + '/v2.0' - self._v2_auth(auth_url) - - if self.bypass_url: - self.set_management_url(self.bypass_url) - elif not self.management_url: - raise exceptions.Unauthorized('Nova Client') - - self._save_keys() - - def _save_keys(self): - # Store the token/mgmt url in the keyring for later requests. - if (self.keyring_saver and self.os_cache and not self.keyring_saved and - self.auth_token and self.management_url and - self.tenant_id): - self.keyring_saver.save(self.auth_token, - self.management_url, - self.tenant_id) - # Don't save it again - self.keyring_saved = True - - def _v1_auth(self, url): - if self.proxy_token: - raise exceptions.NoTokenLookupException() - - headers = {'X-Auth-User': self.user, - 'X-Auth-Key': self._get_password()} - if self.projectid: - headers['X-Auth-Project-Id'] = self.projectid - - resp, body = self._time_request(url, 'GET', headers=headers) - if resp.status_code in (200, 204): # in some cases we get No Content - try: - mgmt_header = 'x-server-management-url' - self.management_url = resp.headers[mgmt_header].rstrip('/') - self.auth_token = resp.headers['x-auth-token'] - self.auth_url = url - except (KeyError, TypeError): - raise exceptions.AuthorizationFailure() - elif resp.status_code == 305: - return resp.headers['location'] - else: - raise exceptions.from_response(resp, body, url) - - def _v2_auth(self, url): - """Authenticate against a v2.0 auth service.""" - if self.auth_token: - body = {"auth": { - "token": {"id": self.auth_token}}} - elif self.user_id: - body = {"auth": { - "passwordCredentials": {"userId": self.user_id, - "password": self._get_password()}}} - else: - body = {"auth": { - "passwordCredentials": {"username": self.user, - "password": self._get_password()}}} - - if self.tenant_id: - body['auth']['tenantId'] = self.tenant_id - elif self.projectid: - body['auth']['tenantName'] = self.projectid - - return self._authenticate(url, body) - - def _authenticate(self, url, body, **kwargs): - """Authenticate and extract the service catalog.""" - method = "POST" - token_url = url + "/tokens" - - # Make sure we follow redirects when trying to reach Keystone - resp, respbody = self._time_request( - token_url, - method, - body=body, - allow_redirects=True, - **kwargs) - - return self._extract_service_catalog(url, resp, respbody) - - def _construct_http_client(api_version=None, auth=None, auth_token=None, diff --git a/novaclient/service_catalog.py b/novaclient/service_catalog.py deleted file mode 100644 index 6883f537b..000000000 --- a/novaclient/service_catalog.py +++ /dev/null @@ -1,89 +0,0 @@ -# Copyright 2011 OpenStack Foundation -# Copyright 2011, Piston Cloud Computing, Inc. -# -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -import novaclient.exceptions - - -class ServiceCatalog(object): - """Helper methods for dealing with a Keystone Service Catalog.""" - - def __init__(self, resource_dict): - self.catalog = resource_dict - - def get_token(self): - return self.catalog['access']['token']['id'] - - def get_tenant_id(self): - return self.catalog['access']['token']['tenant']['id'] - - def url_for(self, attr=None, filter_value=None, - service_type=None, endpoint_type='publicURL', - service_name=None, volume_service_name=None): - """Fetch the public URL from the Compute service for - a particular endpoint attribute. If none given, return - the first. See tests for sample service catalog. - """ - matching_endpoints = [] - if 'endpoints' in self.catalog: - # We have a bastardized service catalog. Treat it special. :/ - for endpoint in self.catalog['endpoints']: - if not filter_value or endpoint[attr] == filter_value: - # Ignore 1.0 compute endpoints - if endpoint.get("type") == 'compute' and \ - endpoint.get('versionId') in (None, '1.1', '2'): - matching_endpoints.append(endpoint) - if not matching_endpoints: - raise novaclient.exceptions.EndpointNotFound() - - # We don't always get a service catalog back ... - if 'serviceCatalog' not in self.catalog['access']: - return None - - # Full catalog ... - catalog = self.catalog['access']['serviceCatalog'] - - for service in catalog: - if service.get("type") != service_type: - continue - - if (service_name and service_type == 'compute' and - service.get('name') != service_name): - continue - - if (volume_service_name and service_type == 'volume' and - service.get('name') != volume_service_name): - continue - - endpoints = service['endpoints'] - for endpoint in endpoints: - # Ignore 1.0 compute endpoints - if (service.get("type") == 'compute' and - endpoint.get('versionId', '2') not in ('1.1', '2')): - continue - if (not filter_value or - endpoint.get(attr).lower() == filter_value.lower()): - endpoint["serviceName"] = service.get("name") - matching_endpoints.append(endpoint) - - if not matching_endpoints: - raise novaclient.exceptions.EndpointNotFound() - elif len(matching_endpoints) > 1: - raise novaclient.exceptions.AmbiguousEndpoints( - endpoints=matching_endpoints) - else: - return matching_endpoints[0][endpoint_type] diff --git a/novaclient/tests/unit/fixture_data/servers.py b/novaclient/tests/unit/fixture_data/servers.py index 30bdae58e..7161f9da3 100644 --- a/novaclient/tests/unit/fixture_data/servers.py +++ b/novaclient/tests/unit/fixture_data/servers.py @@ -444,7 +444,7 @@ def post_servers_1234_action(self, request, context): assert len(body.keys()) == 1 action = list(body)[0] - if v2_fakes.FakeHTTPClient.check_server_actions(body): + if v2_fakes.FakeSessionClient.check_server_actions(body): # NOTE(snikitin): No need to do any operations here. This 'pass' # is needed to avoid AssertionError in the last 'else' statement # if we found 'action' in method check_server_actions and diff --git a/novaclient/tests/unit/test_client.py b/novaclient/tests/unit/test_client.py index 8ebdb7f1b..ad2477632 100644 --- a/novaclient/tests/unit/test_client.py +++ b/novaclient/tests/unit/test_client.py @@ -14,9 +14,7 @@ # under the License. import copy -import logging -import fixtures from keystoneauth1 import session import mock @@ -27,129 +25,7 @@ import novaclient.v2.client -class ClientConnectionPoolTest(utils.TestCase): - - @mock.patch("keystoneauth1.session.TCPKeepAliveAdapter") - def test_get(self, mock_http_adapter): - mock_http_adapter.side_effect = lambda: mock.Mock() - pool = novaclient.client._ClientConnectionPool() - self.assertEqual(pool.get("abc"), pool.get("abc")) - self.assertNotEqual(pool.get("abc"), pool.get("def")) - - class ClientTest(utils.TestCase): - - def test_client_with_timeout(self): - auth_url = "http://example.com" - instance = novaclient.client.HTTPClient(user='user', - password='password', - projectid='project', - timeout=2, - auth_url=auth_url) - self.assertEqual(2, instance.timeout) - - headers = { - 'x-server-management-url': 'example.com', - 'x-auth-token': 'blah', - } - - self.requests_mock.get(auth_url, headers=headers) - - instance.authenticate() - - self.assertEqual(2, self.requests_mock.last_request.timeout) - - def test_client_reauth(self): - auth_url = "http://www.example.com" - instance = novaclient.client.HTTPClient(user='user', - password='password', - projectid='project', - timeout=2, - auth_url=auth_url) - instance.auth_token = 'foobar' - mgmt_url = "http://mgmt.example.com" - instance.management_url = mgmt_url - instance.get_service_url = mock.Mock(return_value=mgmt_url) - instance.version = 'v2.0' - - auth = self.requests_mock.post(auth_url + '/tokens', status_code=401) - detail = self.requests_mock.get(mgmt_url + '/servers/detail', - status_code=401) - - self.assertRaises(novaclient.exceptions.Unauthorized, - instance.get, - '/servers/detail') - - self.assertEqual(2, self.requests_mock.call_count) - self.assertTrue(detail.called_once) - self.assertTrue(auth.called_once) - - detail_headers = detail.last_request.headers - self.assertEqual('project', detail_headers['X-Auth-Project-Id']) - self.assertEqual('foobar', detail_headers['X-Auth-Token']) - self.assertEqual('python-novaclient', detail_headers['User-Agent']) - self.assertEqual('application/json', detail_headers['Accept']) - - reauth_headers = auth.last_request.headers - self.assertEqual('application/json', reauth_headers['Content-Type']) - self.assertEqual('application/json', reauth_headers['Accept']) - self.assertEqual('python-novaclient', reauth_headers['User-Agent']) - - data = { - "auth": { - "tenantName": "project", - "passwordCredentials": { - "username": "user", - "password": "password" - } - } - } - - self.assertEqual(data, auth.last_request.json()) - - def _check_version_url(self, management_url, version_url): - projectid = '25e469aa1848471b875e68cde6531bc5' - auth_url = "http://example.com" - instance = novaclient.client.HTTPClient(user='user', - password='password', - projectid=projectid, - auth_url=auth_url) - instance.auth_token = 'foobar' - instance.management_url = management_url % projectid - mock_get_service_url = mock.Mock(return_value=instance.management_url) - instance.get_service_url = mock_get_service_url - instance.version = 'v2.0' - - versions = self.requests_mock.get(version_url, json={'versions': []}) - servers = self.requests_mock.get(instance.management_url + 'servers') - - # If passing None as the part of url, a client accesses the url which - # doesn't include "v2/" for getting API version info. - instance.get(None) - - self.assertTrue(versions.called_once) - - # Otherwise, a client accesses the url which includes "v2/". - self.assertFalse(servers.called_once) - instance.get('servers') - self.assertTrue(servers.called_once) - - def test_client_version_url(self): - self._check_version_url('http://example.com/v2/%s', - 'http://example.com/') - self._check_version_url('http://example.com/v2.1/%s', - 'http://example.com/') - self._check_version_url('http://example.com/v3.785/%s', - 'http://example.com/') - - def test_client_version_url_with_project_name(self): - self._check_version_url('http://example.com/nova/v2/%s', - 'http://example.com/nova/') - self._check_version_url('http://example.com/nova/v2.1/%s', - 'http://example.com/nova/') - self._check_version_url('http://example.com/nova/v3.785/%s', - 'http://example.com/nova/') - def test_get_client_class_v2(self): output = novaclient.client.get_client_class('2') self.assertEqual(output, novaclient.v2.client.Client) @@ -172,239 +48,6 @@ def test_get_client_class_latest(self): self.assertRaises(novaclient.exceptions.UnsupportedVersion, novaclient.client.get_client_class, '2.latest') - def test_client_get_reset_timings_v2(self): - cs = novaclient.client.Client("2", "user", "password", "project_id", - auth_url="foo/v2") - self.assertEqual(0, len(cs.get_timings())) - cs.client.times.append("somevalue") - self.assertEqual(1, len(cs.get_timings())) - self.assertEqual("somevalue", cs.get_timings()[0]) - - cs.reset_timings() - self.assertEqual(0, len(cs.get_timings())) - - def test_get_password_simple(self): - cs = novaclient.client.HTTPClient("user", "password", "", "") - cs.password_func = mock.Mock() - self.assertEqual("password", cs._get_password()) - self.assertFalse(cs.password_func.called) - - def test_get_password_none(self): - cs = novaclient.client.HTTPClient("user", None, "", "") - self.assertIsNone(cs._get_password()) - - def test_get_password_func(self): - cs = novaclient.client.HTTPClient("user", None, "", "") - cs.password_func = mock.Mock(return_value="password") - self.assertEqual("password", cs._get_password()) - cs.password_func.assert_called_once_with() - - cs.password_func = mock.Mock() - self.assertEqual("password", cs._get_password()) - self.assertFalse(cs.password_func.called) - - def test_auth_url_rstrip_slash(self): - cs = novaclient.client.HTTPClient("user", "password", "project_id", - auth_url="foo/v2/") - self.assertEqual("foo/v2", cs.auth_url) - - def test_token_and_bypass_url(self): - cs = novaclient.client.HTTPClient(None, None, None, - auth_token="12345", - bypass_url="compute/v100/") - self.assertIsNone(cs.auth_url) - self.assertEqual("12345", cs.auth_token) - self.assertEqual("compute/v100", cs.bypass_url) - self.assertEqual("compute/v100", cs.management_url) - - def test_service_url_lookup(self): - service_type = 'compute' - cs = novaclient.client.HTTPClient(None, None, None, - auth_url='foo/v2', - service_type=service_type) - - self.requests_mock.get('http://mgmt.example.com/compute/v5/servers') - - @mock.patch.object(cs, - 'get_service_url', - return_value='http://mgmt.example.com/compute/v5') - @mock.patch.object(cs, 'authenticate') - def do_test(mock_auth, mock_get): - - def set_service_catalog(): - cs.service_catalog = 'catalog' - - mock_auth.side_effect = set_service_catalog - cs.get('/servers') - mock_get.assert_called_once_with(service_type) - mock_auth.assert_called_once_with() - - do_test() - - self.assertEqual(1, self.requests_mock.call_count) - - self.assertEqual('/compute/v5/servers', - self.requests_mock.last_request.path) - - def test_bypass_url_no_service_url_lookup(self): - bypass_url = 'http://mgmt.compute.com/v100' - cs = novaclient.client.HTTPClient(None, None, None, - auth_url='foo/v2', - bypass_url=bypass_url) - - get = self.requests_mock.get('http://mgmt.compute.com/v100/servers') - - @mock.patch.object(cs, 'get_service_url') - def do_test(mock_get): - cs.get('/servers') - self.assertFalse(mock_get.called) - - do_test() - self.assertTrue(get.called_once) - - @mock.patch("novaclient.client.requests.Session") - def test_session(self, mock_session): - fake_session = mock.Mock() - mock_session.return_value = fake_session - cs = novaclient.client.HTTPClient("user", None, "", "") - cs.open_session() - self.assertEqual(cs._session, fake_session) - cs.close_session() - self.assertIsNone(cs._session) - - def test_session_connection_pool(self): - cs = novaclient.client.HTTPClient("user", None, "", - "", connection_pool=True) - cs.open_session() - self.assertIsNone(cs._session) - cs.close_session() - self.assertIsNone(cs._session) - - def test_get_session(self): - cs = novaclient.client.HTTPClient("user", None, "", "") - self.assertIsNone(cs._get_session("http://example.com")) - - @mock.patch("novaclient.client.requests.Session") - def test_get_session_open_session(self, mock_session): - fake_session = mock.Mock() - mock_session.return_value = fake_session - cs = novaclient.client.HTTPClient("user", None, "", "") - cs.open_session() - self.assertEqual(fake_session, cs._get_session("http://example.com")) - - @mock.patch("novaclient.client.requests.Session") - @mock.patch("novaclient.client._ClientConnectionPool") - def test_get_session_connection_pool(self, mock_pool, mock_session): - service_url = "http://service.example.com" - - pool = mock.MagicMock() - pool.get.return_value = "http_adapter" - mock_pool.return_value = pool - cs = novaclient.client.HTTPClient("user", None, "", - "", connection_pool=True) - cs._current_url = "http://current.example.com" - - session = cs._get_session(service_url) - self.assertEqual(session, mock_session.return_value) - pool.get.assert_called_once_with(service_url) - mock_session().mount.assert_called_once_with(service_url, - 'http_adapter') - - def test_init_without_connection_pool(self): - cs = novaclient.client.HTTPClient("user", None, "", "") - self.assertIsNone(cs._connection_pool) - - @mock.patch("novaclient.client._ClientConnectionPool") - def test_init_with_proper_connection_pool(self, mock_pool): - fake_pool = mock.Mock() - mock_pool.return_value = fake_pool - cs = novaclient.client.HTTPClient("user", None, "", - connection_pool=True) - self.assertEqual(cs._connection_pool, fake_pool) - - def test_log_req(self): - self.logger = self.useFixture( - fixtures.FakeLogger( - format="%(message)s", - level=logging.DEBUG, - nuke_handlers=True - ) - ) - cs = novaclient.client.HTTPClient("user", None, "", - connection_pool=True) - cs.http_log_debug = True - cs.http_log_req('GET', '/foo', {'headers': {}}) - cs.http_log_req('GET', '/foo', {'headers': - {'X-Auth-Token': None}}) - cs.http_log_req('GET', '/foo', {'headers': - {'X-Auth-Token': 'totally_bogus'}}) - cs.http_log_req('GET', '/foo', {'headers': - {'X-Foo': 'bar', - 'X-Auth-Token': 'totally_bogus'}}) - cs.http_log_req('GET', '/foo', {'headers': {}, - 'data': - '{"auth": {"passwordCredentials": ' - '{"password": "zhaoqin"}}}'}) - - output = self.logger.output.split('\n') - - self.assertIn("REQ: curl -g -i '/foo' -X GET", output) - self.assertIn( - "REQ: curl -g -i '/foo' -X GET -H " - '"X-Auth-Token: None"', - output) - self.assertIn( - "REQ: curl -g -i '/foo' -X GET -H " - '"X-Auth-Token: {SHA1}b42162b6ffdbd7c3c37b7c95b7ba9f51dda0236d"', - output) - self.assertIn( - "REQ: curl -g -i '/foo' -X GET -H " - '"X-Auth-Token: {SHA1}b42162b6ffdbd7c3c37b7c95b7ba9f51dda0236d"' - ' -H "X-Foo: bar"', - output) - self.assertIn( - "REQ: curl -g -i '/foo' -X GET -d " - '\'{"auth": {"passwordCredentials": {"password":' - ' "{SHA1}4fc49c6a671ce889078ff6b250f7066cf6d2ada2"}}}\'', - output) - - def test_log_resp(self): - self.logger = self.useFixture( - fixtures.FakeLogger( - format="%(message)s", - level=logging.DEBUG, - nuke_handlers=True - ) - ) - - cs = novaclient.client.HTTPClient("user", None, "", - connection_pool=True) - cs.http_log_debug = True - text = ('{"access": {"token": {"id": "zhaoqin"}}}') - resp = utils.TestResponse({'status_code': 200, 'headers': {}, - 'text': text}) - - cs.http_log_resp(resp) - output = self.logger.output.split('\n') - - self.assertIn('RESP: [200] {}', output) - self.assertIn('RESP BODY: {"access": {"token": {"id":' - ' "{SHA1}4fc49c6a671ce889078ff6b250f7066cf6d2ada2"}}}', - output) - - def test_timings(self): - self.requests_mock.get('http://no.where') - - client = novaclient.client.HTTPClient(user='zqfan', password='') - client._time_request("http://no.where", 'GET') - self.assertEqual(0, len(client.times)) - - client = novaclient.client.HTTPClient(user='zqfan', password='', - timings=True) - client._time_request("http://no.where", 'GET') - self.assertEqual(1, len(client.times)) - self.assertEqual('GET http://no.where', client.times[0][0]) - class SessionClientTest(utils.TestCase): @@ -422,6 +65,16 @@ def test_timings(self, mock_log_request_id): self.assertEqual(1, len(client.times)) self.assertEqual('GET http://no.where', client.times[0][0]) + def test_client_get_reset_timings_v2(self): + cs = novaclient.client.SessionClient(session=session.Session()) + self.assertEqual(0, len(cs.get_timings())) + cs.times.append("somevalue") + self.assertEqual(1, len(cs.get_timings())) + self.assertEqual("somevalue", cs.get_timings()[0]) + + cs.reset_timings() + self.assertEqual(0, len(cs.get_timings())) + @mock.patch.object(novaclient.client, '_log_request_id') def test_log_request_id(self, mock_log_request_id): self.requests_mock.get('http://no.where') diff --git a/novaclient/tests/unit/test_http.py b/novaclient/tests/unit/test_http.py deleted file mode 100644 index 588842e49..000000000 --- a/novaclient/tests/unit/test_http.py +++ /dev/null @@ -1,220 +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. - -import mock -import requests -import six - -from novaclient import client -from novaclient import exceptions -from novaclient.tests.unit import utils - - -fake_response = utils.TestResponse({ - "status_code": 200, - "text": '{"hi": "there"}', -}) -mock_request = mock.Mock(return_value=(fake_response)) - -refused_response = utils.TestResponse({ - "status_code": 400, - "text": '[Errno 111] Connection refused', -}) -refused_mock_request = mock.Mock(return_value=(refused_response)) - -bad_req_response = utils.TestResponse({ - "status_code": 400, - "text": '', -}) -bad_req_mock_request = mock.Mock(return_value=(bad_req_response)) - -unknown_error_response = utils.TestResponse({ - "status_code": 503, - "text": '', -}) -unknown_error_mock_request = mock.Mock(return_value=unknown_error_response) - -retry_after_response = utils.TestResponse({ - "status_code": 413, - "text": '', - "headers": { - "retry-after": "5" - }, -}) -retry_after_mock_request = mock.Mock(return_value=retry_after_response) - -retry_after_no_headers_response = utils.TestResponse({ - "status_code": 413, - "text": '', -}) -retry_after_no_headers_mock_request = mock.Mock( - return_value=retry_after_no_headers_response) - -retry_after_non_supporting_response = utils.TestResponse({ - "status_code": 403, - "text": '', - "headers": { - "retry-after": "5" - }, -}) -retry_after_non_supporting_mock_request = mock.Mock( - return_value=retry_after_non_supporting_response) - - -def get_client(**kwargs): - cl = client.HTTPClient("username", "password", - "project_id", - utils.AUTH_URL_V2, - **kwargs) - return cl - - -def get_authed_client(**kwargs): - cl = get_client(**kwargs) - cl.management_url = "http://example.com" - cl.auth_token = "token" - cl.get_service_url = mock.Mock(return_value="http://example.com") - return cl - - -class ClientTest(utils.TestCase): - - def test_get(self): - cl = get_authed_client() - - @mock.patch.object(requests, "request", mock_request) - @mock.patch('time.time', mock.Mock(return_value=1234)) - def test_get_call(): - resp, body = cl.get("/hi") - headers = {"X-Auth-Token": "token", - "X-Auth-Project-Id": "project_id", - "User-Agent": cl.USER_AGENT, - 'Accept': 'application/json'} - mock_request.assert_called_with( - "GET", - "http://example.com/hi", - headers=headers, - **self.TEST_REQUEST_BASE) - # Automatic JSON parsing - self.assertEqual({"hi": "there"}, body) - - test_get_call() - - def test_post(self): - cl = get_authed_client() - - @mock.patch.object(requests, "request", mock_request) - def test_post_call(): - cl.post("/hi", body=[1, 2, 3]) - headers = { - "X-Auth-Token": "token", - "X-Auth-Project-Id": "project_id", - "Content-Type": "application/json", - 'Accept': 'application/json', - "User-Agent": cl.USER_AGENT - } - mock_request.assert_called_with( - "POST", - "http://example.com/hi", - headers=headers, - data='[1, 2, 3]', - **self.TEST_REQUEST_BASE) - - test_post_call() - - def test_auth_failure(self): - cl = get_client() - - # response must not have x-server-management-url header - @mock.patch.object(requests.Session, "request", mock_request) - def test_auth_call(): - self.assertRaises(exceptions.AuthorizationFailure, cl.authenticate) - - test_auth_call() - - def test_auth_failure_due_to_miss_of_auth_url(self): - cl = client.HTTPClient("username", "password") - - self.assertRaises(exceptions.AuthorizationFailure, cl.authenticate) - - def test_connection_refused(self): - cl = get_client() - - @mock.patch.object(requests, "request", refused_mock_request) - def test_refused_call(): - self.assertRaises(exceptions.ConnectionRefused, cl.get, "/hi") - - test_refused_call() - - def test_bad_request(self): - cl = get_client() - - @mock.patch.object(requests, "request", bad_req_mock_request) - def test_refused_call(): - self.assertRaises(exceptions.BadRequest, cl.get, "/hi") - - test_refused_call() - - @mock.patch.object(requests, "request", mock_request) - @mock.patch.object(client, '_log_request_id') - @mock.patch.object(client.HTTPClient, 'http_log_req') - def test_client_logger(self, mock_http_log_req, mock_log_request_id): - cl = get_authed_client(service_name='compute', http_log_debug=True) - self.assertIsNotNone(cl._logger) - - cl.post("/hi", body=[1, 2, 3]) - mock_log_request_id.assert_called_once_with(cl._logger, fake_response, - 'compute') - - @mock.patch.object(requests, 'request', unknown_error_mock_request) - def test_unknown_server_error(self): - cl = get_client() - # This would be cleaner with the context manager version of - # assertRaises or assertRaisesRegexp, but both only appeared in - # Python 2.7 and testtools doesn't match that implementation yet - try: - cl.get('/hi') - except exceptions.ClientException as exc: - self.assertIn('Unknown Error', six.text_type(exc)) - else: - self.fail('Expected exceptions.ClientException') - - @mock.patch.object(requests, "request", retry_after_mock_request) - def test_retry_after_request(self): - cl = get_client() - - try: - cl.get("/hi") - except exceptions.OverLimit as exc: - self.assertEqual(5, exc.retry_after) - else: - self.fail('Expected exceptions.OverLimit') - - @mock.patch.object(requests, "request", - retry_after_no_headers_mock_request) - def test_retry_after_request_no_headers(self): - cl = get_client() - - try: - cl.get("/hi") - except exceptions.OverLimit as exc: - self.assertEqual(0, exc.retry_after) - else: - self.fail('Expected exceptions.OverLimit') - - @mock.patch.object(requests, "request", - retry_after_non_supporting_mock_request) - def test_retry_after_request_non_supporting_exc(self): - cl = get_client() - - self.assertRaises(exceptions.Forbidden, cl.get, "/hi") diff --git a/novaclient/tests/unit/test_service_catalog.py b/novaclient/tests/unit/test_service_catalog.py deleted file mode 100644 index bd3e6cb22..000000000 --- a/novaclient/tests/unit/test_service_catalog.py +++ /dev/null @@ -1,60 +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 keystoneauth1 import fixture - -from novaclient import exceptions -from novaclient import service_catalog -from novaclient.tests.unit import utils - - -SERVICE_CATALOG = fixture.V2Token() -SERVICE_CATALOG.set_scope() - -_s = SERVICE_CATALOG.add_service('compute') -_e = _s.add_endpoint("https://compute1.host/v1/1") -_e["tenantId"] = "1" -_e["versionId"] = "1.0" -_e = _s.add_endpoint("https://compute1.host/v1.1/2", region="North") -_e["tenantId"] = "2" -_e["versionId"] = "1.1" -_e = _s.add_endpoint("https://compute1.host/v2/1", region="North") -_e["tenantId"] = "1" -_e["versionId"] = "2" - -_s = SERVICE_CATALOG.add_service('volume') -_e = _s.add_endpoint("https://volume1.host/v1/1", region="South") -_e["tenantId"] = "1" -_e = _s.add_endpoint("https://volume1.host/v1.1/2", region="South") -_e["tenantId"] = "2" - - -class ServiceCatalogTest(utils.TestCase): - def test_building_a_service_catalog(self): - sc = service_catalog.ServiceCatalog(SERVICE_CATALOG) - - self.assertRaises(exceptions.AmbiguousEndpoints, sc.url_for, - service_type='compute') - self.assertEqual("https://compute1.host/v2/1", - sc.url_for('tenantId', '1', service_type='compute')) - self.assertEqual("https://compute1.host/v1.1/2", - sc.url_for('tenantId', '2', service_type='compute')) - - self.assertRaises(exceptions.EndpointNotFound, sc.url_for, - "region", "South", service_type='compute') - - def test_building_a_service_catalog_insensitive_case(self): - sc = service_catalog.ServiceCatalog(SERVICE_CATALOG) - # Matching south (and catalog has South). - self.assertRaises(exceptions.AmbiguousEndpoints, sc.url_for, - 'region', 'south', service_type='volume') diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 522352f58..8fbe3fdc4 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -61,21 +61,24 @@ def __init__(self, api_version, *args, **kwargs): project_id='project_id', auth_url='auth_url', extensions=kwargs.get('extensions'), direct_use=False, api_version=api_version) - self.client = FakeHTTPClient(api_version=api_version, **kwargs) + self.client = FakeSessionClient(api_version=api_version, **kwargs) -class FakeHTTPClient(base_client.HTTPClient): +class FakeSessionClient(base_client.SessionClient): - def __init__(self, **kwargs): - self.username = 'username' - self.password = 'password' - self.auth_url = 'auth_url' - self.tenant_id = 'tenant_id' - self.callstack = [] - self.projectid = 'projectid' - self.user = 'user' - self.region_name = 'region_name' + def __init__(self, *args, **kwargs): + self.callstack = [] + self.auth = mock.Mock() + self.session = mock.Mock() + self.service_type = 'service_type' + self.service_name = None + self.endpoint_override = None + self.interface = None + self.region_name = None + self.version = None + self.api_version = kwargs.get('api_version') + self.auth.get_auth_ref.return_value.project_id = 'tenant_id' # determines which endpoint to return in get_endpoint() # NOTE(augustina): this is a hacky workaround, ultimately # we need to fix our whole mocking architecture (fixtures?) @@ -83,17 +86,19 @@ def __init__(self, **kwargs): self.endpoint_type = kwargs['endpoint_type'] else: self.endpoint_type = 'endpoint_type' + self.logger = mock.MagicMock() - self.service_type = 'service_type' - self.service_name = 'service_name' - self.volume_service_name = 'volume_service_name' - self.timings = 'timings' - self.bypass_url = 'bypass_url' - self.os_cache = 'os_cache' - self.http_log_debug = 'http_log_debug' - self.last_request_id = None - self.management_url = self.get_endpoint() - self.api_version = kwargs.get("api_version") + def get_endpoint(self, **kwargs): + # check if endpoint matches expected format (eg, v2.1) + if (hasattr(self, 'endpoint_type') and + ENDPOINT_TYPE_RE.search(self.endpoint_type)): + return "http://nova-api:8774/%s/" % self.endpoint_type + else: + return ( + "http://nova-api:8774/v2.1/190a755eef2e4aac9f06aa6be9786385") + + def request(self, url, method, **kwargs): + return self._cs_request(url, method, **kwargs) def _cs_request(self, url, method, **kwargs): # Check that certain things are called correctly @@ -156,15 +161,6 @@ def _cs_request(self, url, method, **kwargs): }) return r, body - def get_endpoint(self, **kwargs): - # check if endpoint matches expected format (eg, v2.1) - if (hasattr(self, 'endpoint_type') and - ENDPOINT_TYPE_RE.search(self.endpoint_type)): - return "http://nova-api:8774/%s/" % self.endpoint_type - else: - return ( - "http://nova-api:8774/v2.1/190a755eef2e4aac9f06aa6be9786385") - def get_versions(self): return (200, FAKE_RESPONSE_HEADERS, { "versions": [ @@ -2357,35 +2353,3 @@ def post_os_server_external_events(self, **kw): 'status': 'completed', 'tag': 'tag', 'server_uuid': 'fake-uuid2'}]}) - - -class FakeSessionClient(fakes.FakeClient, client.Client): - - def __init__(self, api_version, *args, **kwargs): - client.Client.__init__(self, username='username', password='password', - project_id='project_id', auth_url='auth_url', - extensions=kwargs.get('extensions'), - direct_use=False, api_version=api_version) - self.client = FakeSessionMockClient(api_version=api_version, **kwargs) - - -class FakeSessionMockClient(base_client.SessionClient, FakeHTTPClient): - - def __init__(self, *args, **kwargs): - - self.callstack = [] - self.auth = mock.Mock() - self.session = mock.Mock() - self.session.get_endpoint.return_value = FakeHTTPClient.get_endpoint( - self) - self.service_type = 'service_type' - self.service_name = None - self.endpoint_override = None - self.interface = None - self.region_name = None - self.version = None - self.api_version = kwargs.get('api_version') - self.auth.get_auth_ref.return_value.project_id = 'tenant_id' - - def request(self, url, method, **kwargs): - return self._cs_request(url, method, **kwargs) diff --git a/novaclient/tests/unit/v2/test_auth.py b/novaclient/tests/unit/v2/test_auth.py deleted file mode 100644 index f99b233b3..000000000 --- a/novaclient/tests/unit/v2/test_auth.py +++ /dev/null @@ -1,392 +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. - -import copy -import json - -from keystoneauth1 import fixture -import mock -import requests - -from novaclient import client -from novaclient import exceptions -from novaclient.tests.unit import utils - - -def Client(*args, **kwargs): - return client.Client("2", *args, **kwargs) - - -class AuthenticateAgainstKeystoneTests(utils.TestCase): - - def setUp(self): - super(AuthenticateAgainstKeystoneTests, self).setUp() - self.skipTest("This TestCase checks deprecated authentication " - "methods, which will be removed in separate patch.") - - def get_token(self, **kwargs): - resp = fixture.V2Token(**kwargs) - resp.set_scope() - - s = resp.add_service('compute') - s.add_endpoint('http://localhost:8774/v1.1', region='RegionOne') - - return resp - - def test_authenticate_success(self): - cs = Client("username", "password", project_name="project_id", - auth_url=utils.AUTH_URL_V2, service_type='compute') - resp = self.get_token() - - auth_response = utils.TestResponse({ - "status_code": 200, - "text": json.dumps(resp), - }) - - mock_request = mock.Mock(return_value=(auth_response)) - - @mock.patch.object(requests, "request", mock_request) - def test_auth_call(): - cs.client.authenticate() - headers = { - 'User-Agent': cs.client.USER_AGENT, - 'Content-Type': 'application/json', - 'Accept': 'application/json', - } - body = { - 'auth': { - 'passwordCredentials': { - 'username': cs.client.user, - 'password': cs.client.password, - }, - 'tenantName': cs.client.projectid, - }, - } - - token_url = cs.client.auth_url + "/tokens" - mock_request.assert_called_with( - "POST", - token_url, - headers=headers, - data=json.dumps(body), - allow_redirects=True, - **self.TEST_REQUEST_BASE) - - endpoints = resp["access"]["serviceCatalog"][0]['endpoints'] - public_url = endpoints[0]["publicURL"].rstrip('/') - self.assertEqual(cs.client.management_url, public_url) - token_id = resp["access"]["token"]["id"] - self.assertEqual(cs.client.auth_token, token_id) - - test_auth_call() - - def test_authenticate_failure(self): - cs = Client("username", "password", project_name="project_id", - auth_url=utils.AUTH_URL_V2) - resp = {"unauthorized": {"message": "Unauthorized", "code": "401"}} - auth_response = utils.TestResponse({ - "status_code": 401, - "text": json.dumps(resp), - }) - - mock_request = mock.Mock(return_value=(auth_response)) - - @mock.patch.object(requests.Session, "request", mock_request) - def test_auth_call(): - self.assertRaises(exceptions.Unauthorized, cs.client.authenticate) - - test_auth_call() - - def test_v1_auth_redirect(self): - cs = Client("username", "password", project_name="project_id", - auth_url=utils.AUTH_URL_V1, service_type='compute') - dict_correct_response = self.get_token() - correct_response = json.dumps(dict_correct_response) - dict_responses = [ - {"headers": {'location': 'http://127.0.0.1:5001'}, - "status_code": 305, - "text": "Use proxy"}, - # Configured on admin port, nova redirects to v2.0 port. - # When trying to connect on it, keystone auth succeed by v1.0 - # protocol (through headers) but tokens are being returned in - # body (looks like keystone bug). Leaved for compatibility. - {"headers": {}, - "status_code": 200, - "text": correct_response}, - {"headers": {}, - "status_code": 200, - "text": correct_response} - ] - - responses = [(utils.TestResponse(resp)) for resp in dict_responses] - - def side_effect(*args, **kwargs): - return responses.pop(0) - - mock_request = mock.Mock(side_effect=side_effect) - - @mock.patch.object(requests, "request", mock_request) - def test_auth_call(): - cs.client.authenticate() - headers = { - 'User-Agent': cs.client.USER_AGENT, - 'Content-Type': 'application/json', - 'Accept': 'application/json', - } - body = { - 'auth': { - 'passwordCredentials': { - 'username': cs.client.user, - 'password': cs.client.password, - }, - 'tenantName': cs.client.projectid, - }, - } - - token_url = cs.client.auth_url + "/tokens" - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = headers - kwargs['data'] = json.dumps(body) - mock_request.assert_called_with( - "POST", - token_url, - allow_redirects=True, - **kwargs) - - resp = dict_correct_response - endpoints = resp["access"]["serviceCatalog"][0]['endpoints'] - public_url = endpoints[0]["publicURL"].rstrip('/') - self.assertEqual(cs.client.management_url, public_url) - token_id = resp["access"]["token"]["id"] - self.assertEqual(cs.client.auth_token, token_id) - - test_auth_call() - - def test_v2_auth_redirect(self): - cs = Client("username", "password", project_name="project_id", - auth_url=utils.AUTH_URL_V2, service_type='compute') - dict_correct_response = self.get_token() - correct_response = json.dumps(dict_correct_response) - dict_responses = [ - {"headers": {'location': 'http://127.0.0.1:5001'}, - "status_code": 305, - "text": "Use proxy"}, - # Configured on admin port, nova redirects to v2.0 port. - # When trying to connect on it, keystone auth succeed by v1.0 - # protocol (through headers) but tokens are being returned in - # body (looks like keystone bug). Leaved for compatibility. - {"headers": {}, - "status_code": 200, - "text": correct_response}, - {"headers": {}, - "status_code": 200, - "text": correct_response} - ] - - responses = [(utils.TestResponse(resp)) for resp in dict_responses] - - def side_effect(*args, **kwargs): - return responses.pop(0) - - mock_request = mock.Mock(side_effect=side_effect) - - @mock.patch.object(requests, "request", mock_request) - def test_auth_call(): - cs.client.authenticate() - headers = { - 'User-Agent': cs.client.USER_AGENT, - 'Content-Type': 'application/json', - 'Accept': 'application/json', - } - body = { - 'auth': { - 'passwordCredentials': { - 'username': cs.client.user, - 'password': cs.client.password, - }, - 'tenantName': cs.client.projectid, - }, - } - - token_url = cs.client.auth_url + "/tokens" - kwargs = copy.copy(self.TEST_REQUEST_BASE) - kwargs['headers'] = headers - kwargs['data'] = json.dumps(body) - mock_request.assert_called_with( - "POST", - token_url, - allow_redirects=True, - **kwargs) - - resp = dict_correct_response - endpoints = resp["access"]["serviceCatalog"][0]['endpoints'] - public_url = endpoints[0]["publicURL"].rstrip('/') - self.assertEqual(cs.client.management_url, public_url) - token_id = resp["access"]["token"]["id"] - self.assertEqual(cs.client.auth_token, token_id) - - test_auth_call() - - def test_ambiguous_endpoints(self): - cs = Client("username", "password", project_name="project_id", - auth_url=utils.AUTH_URL_V2, service_type='compute') - resp = self.get_token() - - # duplicate existing service - s = resp.add_service('compute') - s.add_endpoint('http://localhost:8774/v1.1', region='RegionOne') - - auth_response = utils.TestResponse({ - "status_code": 200, - "text": json.dumps(resp), - }) - - mock_request = mock.Mock(return_value=(auth_response)) - - @mock.patch.object(requests.Session, "request", mock_request) - def test_auth_call(): - self.assertRaises(exceptions.AmbiguousEndpoints, - cs.client.authenticate) - - test_auth_call() - - def test_authenticate_with_token_success(self): - cs = Client("username", None, project_name="project_id", - auth_url=utils.AUTH_URL_V2, service_type='compute') - cs.client.auth_token = "FAKE_ID" - resp = self.get_token(token_id="FAKE_ID") - auth_response = utils.TestResponse({ - "status_code": 200, - "text": json.dumps(resp), - }) - - mock_request = mock.Mock(return_value=(auth_response)) - - with mock.patch.object(requests, "request", mock_request): - cs.client.authenticate() - headers = { - 'User-Agent': cs.client.USER_AGENT, - 'Content-Type': 'application/json', - 'Accept': 'application/json', - } - body = { - 'auth': { - 'token': { - 'id': cs.client.auth_token, - }, - 'tenantName': cs.client.projectid, - }, - } - - token_url = cs.client.auth_url + "/tokens" - mock_request.assert_called_with( - "POST", - token_url, - headers=headers, - data=json.dumps(body), - allow_redirects=True, - **self.TEST_REQUEST_BASE) - - endpoints = resp["access"]["serviceCatalog"][0]['endpoints'] - public_url = endpoints[0]["publicURL"].rstrip('/') - self.assertEqual(cs.client.management_url, public_url) - token_id = resp["access"]["token"]["id"] - self.assertEqual(cs.client.auth_token, token_id) - - def test_authenticate_with_token_failure(self): - cs = Client("username", None, project_name="project_id", - auth_url=utils.AUTH_URL_V2) - cs.client.auth_token = "FAKE_ID" - resp = {"unauthorized": {"message": "Unauthorized", "code": "401"}} - auth_response = utils.TestResponse({ - "status_code": 401, - "text": json.dumps(resp), - }) - - mock_request = mock.Mock(return_value=(auth_response)) - - with mock.patch.object(requests.Session, "request", mock_request): - self.assertRaises(exceptions.Unauthorized, cs.client.authenticate) - - -class AuthenticationTests(utils.TestCase): - - def setUp(self): - super(AuthenticationTests, self).setUp() - self.skipTest("This TestCase checks deprecated authentication " - "methods, which will be removed in separate patch.") - - def test_authenticate_success(self): - cs = Client("username", "password", project_name="project_id", - auth_url=utils.AUTH_URL) - management_url = 'https://localhost/v1.1/443470' - auth_response = utils.TestResponse({ - 'status_code': 204, - 'headers': { - 'x-server-management-url': management_url, - 'x-auth-token': '1b751d74-de0c-46ae-84f0-915744b582d1', - }, - }) - mock_request = mock.Mock(return_value=(auth_response)) - - @mock.patch.object(requests, "request", mock_request) - def test_auth_call(): - cs.client.authenticate() - headers = { - 'Accept': 'application/json', - 'X-Auth-User': 'username', - 'X-Auth-Key': 'password', - 'X-Auth-Project-Id': 'project_id', - 'User-Agent': cs.client.USER_AGENT - } - mock_request.assert_called_with( - "GET", - cs.client.auth_url, - headers=headers, - **self.TEST_REQUEST_BASE) - - self.assertEqual(cs.client.management_url, - auth_response.headers['x-server-management-url']) - self.assertEqual(cs.client.auth_token, - auth_response.headers['x-auth-token']) - - test_auth_call() - - def test_authenticate_failure(self): - cs = Client("username", "password", project_name="project_id", - auth_url=utils.AUTH_URL) - auth_response = utils.TestResponse({'status_code': 401}) - mock_request = mock.Mock(return_value=(auth_response)) - - @mock.patch.object(requests, "request", mock_request) - def test_auth_call(): - self.assertRaises(exceptions.Unauthorized, cs.client.authenticate) - - test_auth_call() - - def test_auth_automatic(self): - cs = Client("username", "password", project_name="project_id", - auth_url=utils.AUTH_URL, direct_use=False) - http_client = cs.client - http_client.management_url = '' - http_client.get_service_url = mock.Mock(return_value='') - mock_request = mock.Mock(return_value=(None, None)) - - @mock.patch.object(http_client, 'request', mock_request) - @mock.patch.object(http_client, 'authenticate') - def test_auth_call(m): - http_client.get('/') - self.assertTrue(m.called) - self.assertTrue(mock_request.called) - - test_auth_call() diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index db81cb404..558edda52 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -75,7 +75,6 @@ def setUp(self): self.useFixture(fixtures.EnvironmentVariable(var, self.FAKE_ENV[var])) self.shell = self.useFixture(ShellFixture()).shell - self.useFixture(fixtures.MonkeyPatch( 'novaclient.client.Client', fakes.FakeClient)) @@ -920,7 +919,7 @@ def test_boot_nics_net_name_not_found(self, has_neutron): @mock.patch('novaclient.v2.client.Client.has_neutron', return_value=False) @mock.patch( - 'novaclient.tests.unit.v2.fakes.FakeHTTPClient.get_os_networks') + 'novaclient.tests.unit.v2.fakes.FakeSessionClient.get_os_networks') def test_boot_nics_net_name_multiple_matches(self, mock_networks_list, has_neutron): mock_networks_list.return_value = (200, {}, { @@ -3251,16 +3250,6 @@ def test_list_v2_26_not_tags_any(self): self.assert_called('GET', '/servers/detail?not-tags-any=tag1%2Ctag2') -class ShellWithSessionClientTest(ShellTest): - - def setUp(self): - """Run before each test.""" - super(ShellWithSessionClientTest, self).setUp() - - self.useFixture(fixtures.MonkeyPatch( - 'novaclient.client.Client', fakes.FakeSessionClient)) - - class GetSecgroupTest(utils.TestCase): def test_with_integer(self): cs = mock.Mock(**{ diff --git a/novaclient/tests/unit/v2/test_versions.py b/novaclient/tests/unit/v2/test_versions.py index 30496a7ea..6b20cd0b3 100644 --- a/novaclient/tests/unit/v2/test_versions.py +++ b/novaclient/tests/unit/v2/test_versions.py @@ -15,7 +15,6 @@ import mock from novaclient import api_versions -from novaclient import base from novaclient import exceptions as exc from novaclient.tests.unit import utils from novaclient.tests.unit.v2 import fakes @@ -28,73 +27,24 @@ def setUp(self): self.cs = fakes.FakeClient(api_versions.APIVersion("2.0")) self.service_type = versions.Version - @mock.patch.object(versions.VersionManager, '_is_session_client', - return_value=False) - def test_list_services_with_http_client(self, mock_is_session_client): - vl = self.cs.versions.list() - self.assert_request_id(vl, fakes.FAKE_REQUEST_ID_LIST) - self.cs.assert_called('GET', None) - - @mock.patch.object(versions.VersionManager, '_is_session_client', - return_value=True) - def test_list_services_with_session_client(self, mock_is_session_client): + def test_list_services(self): vl = self.cs.versions.list() self.assert_request_id(vl, fakes.FAKE_REQUEST_ID_LIST) self.cs.assert_called('GET', 'http://nova-api:8774/') - @mock.patch.object(versions.VersionManager, '_is_session_client', - return_value=False) - @mock.patch.object(versions.VersionManager, 'list') - def test_get_current_with_http_client(self, mock_list, - mock_is_session_client): - headers = {'x-openstack-request-id': fakes.FAKE_REQUEST_ID} - resp = utils.TestResponse({"headers": headers}) - current_version = versions.Version( - None, {"links": [{"href": "http://nova-api:8774/v2.1"}]}, - loaded=True) - - all_versions = [ - versions.Version( - None, {"links": [{"href": "http://url/v1"}]}, loaded=True), - versions.Version( - None, {"links": [{"href": "http://url/v2"}]}, loaded=True), - versions.Version( - None, {"links": [{"href": "http://url/v3"}]}, loaded=True), - current_version, - versions.Version( - None, {"links": [{"href": "http://url/v21"}]}, loaded=True)] - mock_list.return_value = base.ListWithMeta(all_versions, resp) - v = self.cs.versions.get_current() - self.assert_request_id(v, fakes.FAKE_REQUEST_ID_LIST) - self.assertEqual(current_version, v) - - @mock.patch.object(versions.VersionManager, '_is_session_client', - return_value=True) - def test_get_current_with_session_client(self, mock_is_session_client): + def test_get_current(self): self.cs.callback = [] v = self.cs.versions.get_current() self.assert_request_id(v, fakes.FAKE_REQUEST_ID_LIST) self.cs.assert_called('GET', 'http://nova-api:8774/v2.1/') - @mock.patch.object(versions.VersionManager, '_is_session_client', - return_value=True) @mock.patch.object(versions.VersionManager, '_get', side_effect=exc.Unauthorized("401 RAX")) - def test_get_current_with_rax_workaround(self, session, get): - self.cs.callback = [] - self.assertIsNone(self.cs.versions.get_current()) - - @mock.patch.object(versions.VersionManager, '_is_session_client', - return_value=False) - @mock.patch.object(versions.VersionManager, '_list', - side_effect=exc.Unauthorized("401 RAX")) - def test_get_current_with_rax_auth_plugin_workaround(self, session, _list): + def test_get_current_with_rax_workaround(self, get): self.cs.callback = [] self.assertIsNone(self.cs.versions.get_current()) - @mock.patch.object(versions.VersionManager, '_is_session_client', - return_value=True) - def test_get_endpoint_without_project_id(self, mock_is_session_client): + def test_get_endpoint_without_project_id(self): # create a fake client such that get_endpoint() # doesn't return uuid in url endpoint_type = 'v2.1' @@ -106,15 +56,11 @@ def test_get_endpoint_without_project_id(self, mock_is_session_client): self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) self.assertEqual(result.manager.api.client.endpoint_type, endpoint_type, "Check endpoint_type was set") - self.assertEqual(result.manager.api.client.management_url, - expected_endpoint, "Check endpoint without uuid") # check that the full request works as expected - cs_2_1.assert_called('GET', 'http://nova-api:8774/v2.1/') + cs_2_1.assert_called('GET', expected_endpoint) - @mock.patch.object(versions.VersionManager, '_is_session_client', - return_value=True) - def test_v2_get_endpoint_without_project_id(self, mock_is_session_client): + def test_v2_get_endpoint_without_project_id(self): # create a fake client such that get_endpoint() # doesn't return uuid in url endpoint_type = 'v2' @@ -126,8 +72,6 @@ def test_v2_get_endpoint_without_project_id(self, mock_is_session_client): self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) self.assertEqual(result.manager.api.client.endpoint_type, endpoint_type, "Check v2 endpoint_type was set") - self.assertEqual(result.manager.api.client.management_url, - expected_endpoint, "Check v2 endpoint without uuid") # check that the full request works as expected - cs_2.assert_called('GET', 'http://nova-api:8774/v2/') + cs_2.assert_called('GET', expected_endpoint) diff --git a/novaclient/v2/versions.py b/novaclient/v2/versions.py index 905da0731..48cc52cd6 100644 --- a/novaclient/v2/versions.py +++ b/novaclient/v2/versions.py @@ -19,7 +19,6 @@ from six.moves import urllib from novaclient import base -from novaclient import client from novaclient import exceptions as exc @@ -34,50 +33,37 @@ def __repr__(self): class VersionManager(base.ManagerWithFind): resource_class = Version - def _is_session_client(self): - return isinstance(self.api.client, client.SessionClient) - def _get_current(self): """Returns info about current version.""" + # TODO(sdague): we've now got to make up to 3 HTTP requests to # determine what version we are running, due to differences in # deployments and versions. We really need to cache the # results of this per endpoint and keep the results of it for # some reasonable TTL (like 24 hours) to reduce our round trip # traffic. - if self._is_session_client(): - try: - # Assume that the value of get_endpoint() is something - # we can get the version of. This is a 404 for Nova < - # Mitaka if the service catalog contains project_id. - # - # TODO(sdague): add microversion for when this will - # change - url = "%s" % self.api.client.get_endpoint() - return self._get(url, "version") - except exc.NotFound: - # If that's a 404, we can instead try hacking together - # an endpoint root url by chopping off the last 2 /s. - # This is kind of gross, but we've had this baked in - # so long people got used to this hard coding. - # - # NOTE(sdague): many service providers don't really - # implement GET / in the expected way, if we do a GET - # /v2 that's actually a 300 redirect to - # /v2/... because of how paste works. So adding the - # end slash is really important. - url = "%s/" % url.rsplit("/", 1)[0] - return self._get(url, "version") - else: - # NOTE(andreykurilin): HTTPClient doesn't have ability to send get - # request without token in the url, so `self._get` doesn't work. - all_versions = self.list() - url = self.client.management_url.rsplit("/", 1)[0] - for version in all_versions: - for link in version.links: - if link["href"].rstrip('/') == url: - version.append_request_ids(all_versions.request_ids) - return version + try: + # Assume that the value of get_endpoint() is something + # we can get the version of. This is a 404 for Nova < + # Mitaka if the service catalog contains project_id. + # + # TODO(sdague): add microversion for when this will + # change + url = "%s" % self.api.client.get_endpoint() + return self._get(url, "version") + except exc.NotFound: + # If that's a 404, we can instead try hacking together + # an endpoint root url by chopping off the last 2 /s. + # This is kind of gross, but we've had this baked in + # so long people got used to this hard coding. + # + # NOTE(sdague): many service providers don't really + # implement GET / in the expected way, if we do a GET + # /v2 that's actually a 300 redirect to + # /v2/... because of how paste works. So adding the + # end slash is really important. + url = "%s/" % url.rsplit("/", 1)[0] + return self._get(url, "version") def get_current(self): try: @@ -92,13 +78,11 @@ def get_current(self): def list(self): """List all versions.""" - version_url = None - if self._is_session_client(): - # NOTE: "list versions" API needs to be accessed without base - # URI (like "v2/{project-id}"), so here should be a scheme("http", - # etc.) and a hostname. - endpoint = self.api.client.get_endpoint() - url = urllib.parse.urlparse(endpoint) - version_url = '%s://%s/' % (url.scheme, url.netloc) + # NOTE: "list versions" API needs to be accessed without base + # URI (like "v2/{project-id}"), so here should be a scheme("http", + # etc.) and a hostname. + endpoint = self.api.client.get_endpoint() + url = urllib.parse.urlparse(endpoint) + version_url = '%s://%s/' % (url.scheme, url.netloc) return self._list(version_url, "versions") From 75052a7afc56d5e737f4e51c1035fe7793f956e6 Mon Sep 17 00:00:00 2001 From: Pavel Kholkin Date: Wed, 7 Dec 2016 19:10:25 +0300 Subject: [PATCH 1211/1705] [proxy-api] microversion 2.39 deprecates image-metadata proxy API Almost all proxy APIs were deprecated in microversion 2.36. But the sub-resource image-metadata of image was forgotten to deprecate. This patch deprecates the image-metdata API from 2.39. Image commands were already deprecated since 2.36 in I450917f7fcbfe0a3ea7921c82af9863e80cb40a1 Implements blueprint deprecate-image-meta-proxy-api Change-Id: Idd4cd1d1fec13f9e5f89dc419b57e094c9ad4b3b --- novaclient/__init__.py | 2 +- .../tests/functional/v2/test_image_meta.py | 40 +++++++++++++++++++ novaclient/tests/unit/v2/test_images.py | 16 ++++++++ novaclient/tests/unit/v2/test_shell.py | 6 ++- novaclient/v2/images.py | 2 + ...image-meta-proxy-api-1483b75cf73b021e.yaml | 6 +++ 6 files changed, 69 insertions(+), 3 deletions(-) create mode 100644 novaclient/tests/functional/v2/test_image_meta.py create mode 100644 releasenotes/notes/bp-deprecate-image-meta-proxy-api-1483b75cf73b021e.yaml diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 99d9f4a74..85cadcf62 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.38") +API_MAX_VERSION = api_versions.APIVersion("2.39") diff --git a/novaclient/tests/functional/v2/test_image_meta.py b/novaclient/tests/functional/v2/test_image_meta.py new file mode 100644 index 000000000..4f0a41a54 --- /dev/null +++ b/novaclient/tests/functional/v2/test_image_meta.py @@ -0,0 +1,40 @@ +# Copyright 2016 Mirantis, Inc. +# 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 novaclient.tests.functional import base + + +class TestImageMetaV239(base.ClientTestBase): + """Functional tests for image-meta proxy API.""" + + # 'image-metadata' proxy API was deprecated in 2.39 but the CLI should + # fallback to 2.35 and emit a warning. + COMPUTE_API_VERSION = "2.39" + + def test_command_deprecation(self): + output = self.nova('image-meta %s set test_key=test_value' % + self.image.id, merge_stderr=True) + self.assertIn('is deprecated', output) + + output = self.nova('image-meta %s delete test_key' % + self.image.id, merge_stderr=True) + self.assertIn('is deprecated', output) + + def test_limits(self): + """Tests that 2.39 won't return 'maxImageMeta' resource limit and + the CLI output won't show it. + """ + output = self.nova('limits') + # assert that MaxImageMeta isn't in the table output + self.assertRaises(ValueError, self._get_value_from_the_table, + output, 'maxImageMeta') diff --git a/novaclient/tests/unit/v2/test_images.py b/novaclient/tests/unit/v2/test_images.py index 7b8b4c840..00116a1a6 100644 --- a/novaclient/tests/unit/v2/test_images.py +++ b/novaclient/tests/unit/v2/test_images.py @@ -105,3 +105,19 @@ def test_find_2_36(self): self.cs.api_version = api_versions.APIVersion('2.36') self.assertRaises(exceptions.VersionNotFoundForAPIMethod, self.cs.images.find, name="CentOS 5.2") + + def test_delete_meta_2_39(self): + """Tests that 'delete_meta' method fails after microversion 2.39. + """ + self.cs.api_version = api_versions.APIVersion('2.39') + self.assertRaises(exceptions.VersionNotFoundForAPIMethod, + self.cs.images.delete_meta, 1, + {'test_key': 'test_value'}) + + def test_set_meta_2_39(self): + """Tests that 'set_meta' method fails after microversion 2.39. + """ + self.cs.api_version = api_versions.APIVersion('2.39') + self.assertRaises(exceptions.VersionNotFoundForAPIMethod, + self.cs.images.set_meta, 1, + {'test_key': 'test_value'}) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index b11f6218a..d825dce75 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -1127,8 +1127,9 @@ def test_image_meta_set(self): {'metadata': {'test_key': 'test_value'}}) def test_image_meta_del(self): - self.run_command('image-meta %s delete test_key=test_value' % - FAKE_UUID_1) + _out, err = self.run_command('image-meta %s delete test_key' % + FAKE_UUID_1) + self.assertIn('Command image-meta is deprecated', err) self.assert_called('DELETE', '/images/%s/metadata/test_key' % FAKE_UUID_1) @@ -3097,6 +3098,7 @@ def test_versions(self): 34, # doesn't require any changes in novaclient 37, # There are no versioned wrapped shell method changes for this 38, # doesn't require any changes in novaclient + 39, # There are no versioned wrapped shell method changes for this ]) versions_supported = set(range(0, novaclient.API_MAX_VERSION.ver_minor + 1)) diff --git a/novaclient/v2/images.py b/novaclient/v2/images.py index 7371d4bea..8d17e6bca 100644 --- a/novaclient/v2/images.py +++ b/novaclient/v2/images.py @@ -156,6 +156,7 @@ def delete(self, image): 'or python-openstacksdk instead.', DeprecationWarning) return self._delete("/images/%s" % base.getid(image)) + @api_versions.wraps('2.0', '2.38') def set_meta(self, image, metadata): """ DEPRECATED: Set an images metadata @@ -171,6 +172,7 @@ def set_meta(self, image, metadata): return self._create("/images/%s/metadata" % base.getid(image), body, "metadata") + @api_versions.wraps('2.0', '2.38') def delete_meta(self, image, keys): """ DEPRECATED: Delete metadata from an image diff --git a/releasenotes/notes/bp-deprecate-image-meta-proxy-api-1483b75cf73b021e.yaml b/releasenotes/notes/bp-deprecate-image-meta-proxy-api-1483b75cf73b021e.yaml new file mode 100644 index 000000000..6d8e76c11 --- /dev/null +++ b/releasenotes/notes/bp-deprecate-image-meta-proxy-api-1483b75cf73b021e.yaml @@ -0,0 +1,6 @@ +--- +upgrade: + - Starting from microversion 2.39 'image-metadata' proxy API in Nova is + deprecated and python API bindings will not work, although 'image-meta' + CLI commands will still work, because of the fallback on the CLI to + version 2.35. From 0bdd24518e95279b77514a1e16443f9941a8bb38 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Thu, 8 Dec 2016 19:44:01 +0200 Subject: [PATCH 1212/1705] Deprecate volume_service_name argument We forgot about it while removing all methods related to Volume API. Change-Id: I465b91a17e2633ae876c0011bc8af6ec475c8d48 --- novaclient/client.py | 2 +- novaclient/shell.py | 5 ----- novaclient/v2/client.py | 3 --- ...deprecate-volume-service-name-arg-4c65e8866f9624dd.yaml | 7 +++++++ 4 files changed, 8 insertions(+), 9 deletions(-) create mode 100644 releasenotes/notes/deprecate-volume-service-name-arg-4c65e8866f9624dd.yaml diff --git a/novaclient/client.py b/novaclient/client.py index ec07c38f5..282054f70 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -138,7 +138,6 @@ def _construct_http_client(api_version=None, user_domain_name=None, user_id=None, username=None, - volume_service_name=None, **kwargs): if not session: if not auth and auth_token: @@ -331,6 +330,7 @@ def Client(version, username=None, password=None, project_id=None, _check_arguments(kwargs, "Ocata", "proxy_tenant_id") _check_arguments(kwargs, "Ocata", "proxy_token") _check_arguments(kwargs, "Ocata", "connection_pool") + _check_arguments(kwargs, "Ocata", "volume_service_name") api_version, client_class = _get_client_class_and_version(version) kwargs.pop("direct_use", None) diff --git a/novaclient/shell.py b/novaclient/shell.py index e6c6bcc01..43953f960 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -231,7 +231,6 @@ def _make_key(self): self.client.endpoint_type, self.client.service_type, self.client.service_name, - self.client.volume_service_name, ] for (index, key) in enumerate(keys): if key is None: @@ -671,10 +670,8 @@ def main(self, argv): insecure = args.insecure service_type = args.service_type service_name = args.service_name - volume_service_name = args.volume_service_name endpoint_override = args.endpoint_override os_cache = args.os_cache - cert = args.os_cert cacert = args.os_cacert cert = args.os_cert timeout = args.timeout @@ -762,7 +759,6 @@ def main(self, argv): region_name=os_region_name, endpoint_type=endpoint_type, extensions=self.extensions, service_type=service_type, service_name=service_name, auth_token=auth_token, - volume_service_name=volume_service_name, timings=args.timings, endpoint_override=endpoint_override, os_cache=os_cache, http_log_debug=args.debug, cacert=cacert, cert=cert, timeout=timeout, @@ -830,7 +826,6 @@ def main(self, argv): region_name=os_region_name, endpoint_type=endpoint_type, extensions=self.extensions, service_type=service_type, service_name=service_name, auth_token=auth_token, - volume_service_name=volume_service_name, timings=args.timings, endpoint_override=endpoint_override, os_cache=os_cache, http_log_debug=args.debug, cacert=cacert, cert=cert, timeout=timeout, diff --git a/novaclient/v2/client.py b/novaclient/v2/client.py index 35158fc9a..514fb5e4a 100644 --- a/novaclient/v2/client.py +++ b/novaclient/v2/client.py @@ -98,7 +98,6 @@ def __init__(self, user_domain_name=None, user_id=None, username=None, - volume_service_name=None, **kwargs): """Initialization of Client object. @@ -134,7 +133,6 @@ def __init__(self, :param str user_domain_name: Name of user domain :param str user_id: User ID :param str username: Username - :param str volume_service_name: Volume Service Name """ if direct_use: raise exceptions.Forbidden( @@ -256,7 +254,6 @@ def __init__(self, user_domain_name=user_domain_name, user_id=user_id, username=username, - volume_service_name=volume_service_name, **kwargs) @property diff --git a/releasenotes/notes/deprecate-volume-service-name-arg-4c65e8866f9624dd.yaml b/releasenotes/notes/deprecate-volume-service-name-arg-4c65e8866f9624dd.yaml new file mode 100644 index 000000000..26de99b53 --- /dev/null +++ b/releasenotes/notes/deprecate-volume-service-name-arg-4c65e8866f9624dd.yaml @@ -0,0 +1,7 @@ +--- +deprecations: + - CLI argument for volume_service_name was deprecated long time ago. All + novaclient's methods for communication with Volume API were deprecated and + removed. There is no need to leave **volume_service_name** argument of + novaclient.client.Client entry-point since it is not used anywhere, + so it is removed now. From 2e04cb29d70524a3f11bcd3c785e1aabae7b9a69 Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Mon, 19 Dec 2016 15:44:26 +0900 Subject: [PATCH 1213/1705] Add some missing modules in API reference In doc/source/conf.py, there are some missing files (modules) to generate API references. So add them. Change-Id: I39689a31084e0cdbfdcad66c723a87de009a08f7 Closes-Bug: #1651043 --- doc/source/conf.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index ee5c65258..935a45f9c 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -72,12 +72,16 @@ def gen_ref(ver, title, names): gen_ref(None, "Exceptions", ["exceptions"]) gen_ref("v2", "Version 1.1, Version 2 API", - ["client", "flavors", "images", "servers", "hosts", "agents", - "aggregates", "availability_zones", "certs", "fixed_ips", - "floating_ip_pools", "floating_ips", "hypervisors", "keypairs", - "limits", "networks", "quota_classes", "quotas", - "security_group_rules", "security_groups", "services", "usage", - "virtual_interfaces", "volumes"]) + ["agents", "aggregates", "assisted_volume_snapshots", + "availability_zones", "cells", "certs", "client", "cloudpipe", + "fixed_ips", "flavor_access", "flavors", "floating_ip_dns", + "floating_ip_pools", "floating_ips", "floating_ips_bulk", "fping", + "hosts", "hypervisors", "images", "instance_action", "keypairs", + "limits", "list_extensions", "migrations", "networks", + "quota_classes", "quotas", "security_group_default_rules", + "security_group_rules", "security_groups", "server_external_events", + "server_groups", "server_migrations", "servers", "services", "usage", + "versions", "virtual_interfaces", "volumes"]) # -- General configuration ---------------------------------------------------- From 78e621faf757e3bf4bd8475c29090bb1c83bc0cd Mon Sep 17 00:00:00 2001 From: Akshil Verma Date: Sun, 11 Dec 2016 13:37:25 -0600 Subject: [PATCH 1214/1705] Fixed the __ne__ implementation in base.Resource Any object of Resource class or its child class do not compare with None as expected. For example if a server has been found and is clearly not None, the test "server!=None" will be False. This was occuring because the __eq__ implementation was returning the 'NotImplemented' keyword and the __ne__ implementation was returning the not of __eq__, which in this case will return False as the expected python behavior for a the not of NotImplemented is False. Changed the __ne__ implementation to return the correct boolean value and added the test case that fails with the older implementation and passes in the current fix. Change-Id: I6bf5a6e9c9eed4bbcf6678467df19dfea560b4de Closes-Bug: #1648207 --- novaclient/base.py | 4 +++- novaclient/tests/unit/test_base.py | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/novaclient/base.py b/novaclient/base.py index a091f0b66..0ab0e8ca1 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -210,7 +210,9 @@ def __eq__(self, other): return self._info == other._info def __ne__(self, other): - return not self.__eq__(other) + # Using not of '==' implementation because the not of + # __eq__, when it returns NotImplemented, is returning False. + return not self == other def is_loaded(self): return self._loaded diff --git a/novaclient/tests/unit/test_base.py b/novaclient/tests/unit/test_base.py index 74ceca397..eb70dff99 100644 --- a/novaclient/tests/unit/test_base.py +++ b/novaclient/tests/unit/test_base.py @@ -71,6 +71,12 @@ def test_eq(self): r2 = base.Resource(None, {'name': 'joe', 'age': 12}) self.assertEqual(r1, r2) + def test_ne(self): + # Two resources of different types: never equal + r1 = base.Resource(None, {'id': 1, 'name': 'test'}) + r2 = object() + self.assertNotEqual(r1, r2) + def test_findall_invalid_attribute(self): cs = fakes.FakeClient(api_versions.APIVersion("2.0")) # Make sure findall with an invalid attribute doesn't cause errors. From 53b23d52177ee561ef587f6db33722157d18763e Mon Sep 17 00:00:00 2001 From: Diana Clarke Date: Wed, 7 Dec 2016 03:11:46 +0000 Subject: [PATCH 1215/1705] Microversion 2.40 - Simple tenant usage pagination Add optional parameters 'limit' and 'marker' to the os-simple-tenant-usage endpoints for pagaination. /os-simple-tenant-usage?limit={limit}&marker={instance_uuid} /os-simple-tenant-usage/{tenant}?limit={limit}&marker={instance_uuid} Implements blueprint paginate-simple-tenant-usage Depends-on: Ic8e9f869f1b855f968967bedbf77542f287f26c0 Change-Id: If99db6933de012b71cf2c982075f08b3e664361e --- novaclient/__init__.py | 2 +- .../tests/functional/v2/legacy/test_usage.py | 74 +++++++++++++++++++ novaclient/tests/functional/v2/test_usage.py | 38 ++++++++++ novaclient/tests/unit/v2/fakes.py | 74 +++++++++++++++++++ novaclient/tests/unit/v2/test_shell.py | 54 +++++++++++++- novaclient/tests/unit/v2/test_usage.py | 49 ++++++++++++ novaclient/v2/shell.py | 70 ++++++++++++++++-- novaclient/v2/usage.py | 66 +++++++++++++++-- .../microversion-v2_40-484adba0806b08bf.yaml | 4 + 9 files changed, 414 insertions(+), 17 deletions(-) create mode 100644 novaclient/tests/functional/v2/legacy/test_usage.py create mode 100644 novaclient/tests/functional/v2/test_usage.py create mode 100644 releasenotes/notes/microversion-v2_40-484adba0806b08bf.yaml diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 85cadcf62..956e139e6 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.39") +API_MAX_VERSION = api_versions.APIVersion("2.40") diff --git a/novaclient/tests/functional/v2/legacy/test_usage.py b/novaclient/tests/functional/v2/legacy/test_usage.py new file mode 100644 index 000000000..e37693d1b --- /dev/null +++ b/novaclient/tests/functional/v2/legacy/test_usage.py @@ -0,0 +1,74 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import datetime + +from novaclient.tests.functional import base + + +class TestUsageCLI(base.ClientTestBase): + + COMPUTE_API_VERSION = '2.1' + + def _get_num_servers_from_usage_output(self): + output = self.nova('usage') + servers = self._get_column_value_from_single_row_table( + output, 'Servers') + return int(servers) + + def _get_num_servers_by_tenant_from_usage_output(self): + tenant_id = self._get_project_id(self.cli_clients.tenant_name) + output = self.nova('usage --tenant=%s' % tenant_id) + servers = self._get_column_value_from_single_row_table( + output, 'Servers') + return int(servers) + + def test_usage(self): + before = self._get_num_servers_from_usage_output() + self._create_server('some-server') + after = self._get_num_servers_from_usage_output() + self.assertGreater(after, before) + + def test_usage_tenant(self): + before = self._get_num_servers_by_tenant_from_usage_output() + self._create_server('some-server') + after = self._get_num_servers_by_tenant_from_usage_output() + self.assertGreater(after, before) + + +class TestUsageClient(base.ClientTestBase): + + COMPUTE_API_VERSION = '2.1' + + def _create_servers_in_time_window(self): + start = datetime.datetime.now() + self._create_server('some-server') + self._create_server('another-server') + end = datetime.datetime.now() + return start, end + + def test_get(self): + start, end = self._create_servers_in_time_window() + tenant_id = self._get_project_id(self.cli_clients.tenant_name) + usage = self.client.usage.get(tenant_id, start=start, end=end) + self.assertEqual(tenant_id, usage.tenant_id) + self.assertGreaterEqual(len(usage.server_usages), 2) + + def test_list(self): + start, end = self._create_servers_in_time_window() + tenant_id = self._get_project_id(self.cli_clients.tenant_name) + usages = self.client.usage.list(start=start, end=end, detailed=True) + tenant_ids = [usage.tenant_id for usage in usages] + self.assertIn(tenant_id, tenant_ids) + for usage in usages: + if usage.tenant_id == tenant_id: + self.assertGreaterEqual(len(usage.server_usages), 2) diff --git a/novaclient/tests/functional/v2/test_usage.py b/novaclient/tests/functional/v2/test_usage.py new file mode 100644 index 000000000..1a19ad935 --- /dev/null +++ b/novaclient/tests/functional/v2/test_usage.py @@ -0,0 +1,38 @@ +# 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 novaclient.tests.functional.v2.legacy import test_usage + + +class TestUsageCLI_V240(test_usage.TestUsageCLI): + + COMPUTE_API_VERSION = '2.40' + + +class TestUsageClient_V240(test_usage.TestUsageClient): + + COMPUTE_API_VERSION = '2.40' + + def test_get(self): + start, end = self._create_servers_in_time_window() + tenant_id = self._get_project_id(self.cli_clients.tenant_name) + usage = self.client.usage.get( + tenant_id, start=start, end=end, limit=1) + self.assertEqual(tenant_id, usage.tenant_id) + self.assertEqual(1, len(usage.server_usages)) + + def test_list(self): + start, end = self._create_servers_in_time_window() + usages = self.client.usage.list( + start=start, end=end, detailed=True, limit=1) + self.assertEqual(1, len(usages)) + self.assertEqual(1, len(usages[0].server_usages)) diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 8fbe3fdc4..e6f749a51 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -69,6 +69,7 @@ class FakeSessionClient(base_client.SessionClient): def __init__(self, *args, **kwargs): self.callstack = [] + self.visited = [] self.auth = mock.Mock() self.session = mock.Mock() self.service_type = 'service_type' @@ -139,12 +140,21 @@ def _cs_request(self, url, method, **kwargs): v2_image = True callback = callback.replace('get_v2_', 'get_') + simulate_pagination_next_links = [ + 'get_os_simple_tenant_usage', + 'get_os_simple_tenant_usage_tenant_id', + ] + if callback in simulate_pagination_next_links: + while callback in self.visited: + callback += '_next' + if not hasattr(self, callback): raise AssertionError('Called unknown API method: %s %s, ' 'expected fakes method name: %s' % (method, url, callback)) # Note the call + self.visited.append(callback) self.callstack.append((method, url, kwargs.get('body'))) status, headers, body = getattr(self, callback)(**kwargs) @@ -1551,6 +1561,36 @@ def get_os_simple_tenant_usage(self, **kw): six.u('name'): six.u('f15image1'), six.u('tenant_id'): six.u('7b0a1d73f8fb41718f3343c207597869'), + six.u('instance_id'): + six.u('f079e394-1111-457b-b350-bb5ecc685cdd'), + six.u('vcpus'): 1, + six.u('memory_mb'): 512, + six.u('state'): six.u('active'), + six.u('flavor'): six.u('m1.tiny'), + six.u('started_at'): + six.u('2012-01-20 18:06:06.479998')}], + six.u('start'): six.u('2011-12-25 19:48:41.750687'), + six.u('total_local_gb_usage'): 0.0}]}) + + def get_os_simple_tenant_usage_next(self, **kw): + return (200, FAKE_RESPONSE_HEADERS, + {six.u('tenant_usages'): [{ + six.u('total_memory_mb_usage'): 25451.762807466665, + six.u('total_vcpus_usage'): 49.71047423333333, + six.u('total_hours'): 49.71047423333333, + six.u('tenant_id'): + six.u('7b0a1d73f8fb41718f3343c207597869'), + six.u('stop'): six.u('2012-01-22 19:48:41.750722'), + six.u('server_usages'): [{ + six.u('hours'): 49.71047423333333, + six.u('uptime'): 27035, + six.u('local_gb'): 0, + six.u('ended_at'): None, + six.u('name'): six.u('f15image1'), + six.u('tenant_id'): + six.u('7b0a1d73f8fb41718f3343c207597869'), + six.u('instance_id'): + six.u('f079e394-2222-457b-b350-bb5ecc685cdd'), six.u('vcpus'): 1, six.u('memory_mb'): 512, six.u('state'): six.u('active'), @@ -1560,6 +1600,9 @@ def get_os_simple_tenant_usage(self, **kw): six.u('start'): six.u('2011-12-25 19:48:41.750687'), six.u('total_local_gb_usage'): 0.0}]}) + def get_os_simple_tenant_usage_next_next(self, **kw): + return (200, FAKE_RESPONSE_HEADERS, {six.u('tenant_usages'): []}) + def get_os_simple_tenant_usage_tenantfoo(self, **kw): return (200, FAKE_RESPONSE_HEADERS, {six.u('tenant_usage'): { @@ -1576,6 +1619,8 @@ def get_os_simple_tenant_usage_tenantfoo(self, **kw): six.u('name'): six.u('f15image1'), six.u('tenant_id'): six.u('7b0a1d73f8fb41718f3343c207597869'), + six.u('instance_id'): + six.u('f079e394-1111-457b-b350-bb5ecc685cdd'), six.u('vcpus'): 1, six.u('memory_mb'): 512, six.u('state'): six.u('active'), six.u('flavor'): six.u('m1.tiny'), @@ -1597,6 +1642,8 @@ def get_os_simple_tenant_usage_test(self, **kw): six.u('ended_at'): None, six.u('name'): six.u('f15image1'), six.u('tenant_id'): six.u('7b0a1d73f8fb41718f3343c207597869'), + six.u('instance_id'): + six.u('f079e394-1111-457b-b350-bb5ecc685cdd'), six.u('vcpus'): 1, six.u('memory_mb'): 512, six.u('state'): six.u('active'), six.u('flavor'): six.u('m1.tiny'), @@ -1617,6 +1664,30 @@ def get_os_simple_tenant_usage_tenant_id(self, **kw): six.u('ended_at'): None, six.u('name'): six.u('f15image1'), six.u('tenant_id'): six.u('7b0a1d73f8fb41718f3343c207597869'), + six.u('instance_id'): + six.u('f079e394-1111-457b-b350-bb5ecc685cdd'), + six.u('vcpus'): 1, six.u('memory_mb'): 512, + six.u('state'): six.u('active'), + six.u('flavor'): six.u('m1.tiny'), + six.u('started_at'): six.u('2012-01-20 18:06:06.479998')}], + six.u('start'): six.u('2011-12-25 19:48:41.750687'), + six.u('total_local_gb_usage'): 0.0}}) + + def get_os_simple_tenant_usage_tenant_id_next(self, **kw): + return (200, {}, {six.u('tenant_usage'): { + six.u('total_memory_mb_usage'): 25451.762807466665, + six.u('total_vcpus_usage'): 49.71047423333333, + six.u('total_hours'): 49.71047423333333, + six.u('tenant_id'): six.u('7b0a1d73f8fb41718f3343c207597869'), + six.u('stop'): six.u('2012-01-22 19:48:41.750722'), + six.u('server_usages'): [{ + six.u('hours'): 49.71047423333333, + six.u('uptime'): 27035, six.u('local_gb'): 0, + six.u('ended_at'): None, + six.u('name'): six.u('f15image1'), + six.u('tenant_id'): six.u('7b0a1d73f8fb41718f3343c207597869'), + six.u('instance_id'): + six.u('f079e394-2222-457b-b350-bb5ecc685cdd'), six.u('vcpus'): 1, six.u('memory_mb'): 512, six.u('state'): six.u('active'), six.u('flavor'): six.u('m1.tiny'), @@ -1624,6 +1695,9 @@ def get_os_simple_tenant_usage_tenant_id(self, **kw): six.u('start'): six.u('2011-12-25 19:48:41.750687'), six.u('total_local_gb_usage'): 0.0}}) + def get_os_simple_tenant_usage_tenant_id_next_next(self, **kw): + return (200, {}, {six.u('tenant_usage'): {}}) + # # Aggregates # diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index bcfee1e9a..6a27175da 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -50,7 +50,7 @@ def setUp(self): def tearDown(self): # For some method like test_image_meta_bad_action we are # testing a SystemExit to be thrown and object self.shell has - # no time to get instantatiated which is OK in this case, so + # no time to get instantiated which is OK in this case, so # we make sure the method is there before launching it. if hasattr(self.shell, 'cs'): self.shell.cs.clear_callstack() @@ -1852,12 +1852,36 @@ def test_server_floating_ip_disassociate(self): {'removeFloatingIp': {'address': '11.0.0.1'}}) def test_usage_list(self): - self.run_command('usage-list --start 2000-01-20 --end 2005-02-01') + cmd = 'usage-list --start 2000-01-20 --end 2005-02-01' + stdout, _stderr = self.run_command(cmd) self.assert_called('GET', '/os-simple-tenant-usage?' + 'start=2000-01-20T00:00:00&' + 'end=2005-02-01T00:00:00&' + 'detailed=1') + # Servers, RAM MB-Hours, CPU Hours, Disk GB-Hours + self.assertIn('1 | 25451.76 | 49.71 | 0.00', stdout) + + def test_usage_list_stitch_together_next_results(self): + cmd = 'usage-list --start 2000-01-20 --end 2005-02-01' + stdout, _stderr = self.run_command(cmd, api_version='2.40') + self.assert_called('GET', + '/os-simple-tenant-usage?' + 'start=2000-01-20T00:00:00&' + 'end=2005-02-01T00:00:00&' + 'detailed=1', pos=0) + markers = [ + 'f079e394-1111-457b-b350-bb5ecc685cdd', + 'f079e394-2222-457b-b350-bb5ecc685cdd', + ] + for pos, marker in enumerate(markers): + self.assert_called('GET', + '/os-simple-tenant-usage?' + 'start=2000-01-20T00:00:00&' + 'end=2005-02-01T00:00:00&' + 'marker=%s&detailed=1' % (marker), pos=pos + 1) + # Servers, RAM MB-Hours, CPU Hours, Disk GB-Hours + self.assertIn('2 | 50903.53 | 99.42 | 0.00', stdout) def test_usage_list_no_args(self): timeutils.set_time_override(datetime.datetime(2005, 2, 1, 0, 0)) @@ -1870,12 +1894,34 @@ def test_usage_list_no_args(self): 'detailed=1') def test_usage(self): - self.run_command('usage --start 2000-01-20 --end 2005-02-01 ' - '--tenant test') + cmd = 'usage --start 2000-01-20 --end 2005-02-01 --tenant test' + stdout, _stderr = self.run_command(cmd) self.assert_called('GET', '/os-simple-tenant-usage/test?' + 'start=2000-01-20T00:00:00&' + 'end=2005-02-01T00:00:00') + # Servers, RAM MB-Hours, CPU Hours, Disk GB-Hours + self.assertIn('1 | 25451.76 | 49.71 | 0.00', stdout) + + def test_usage_stitch_together_next_results(self): + cmd = 'usage --start 2000-01-20 --end 2005-02-01' + stdout, _stderr = self.run_command(cmd, api_version='2.40') + self.assert_called('GET', + '/os-simple-tenant-usage/tenant_id?' + 'start=2000-01-20T00:00:00&' + 'end=2005-02-01T00:00:00', pos=0) + markers = [ + 'f079e394-1111-457b-b350-bb5ecc685cdd', + 'f079e394-2222-457b-b350-bb5ecc685cdd', + ] + for pos, marker in enumerate(markers): + self.assert_called('GET', + '/os-simple-tenant-usage/tenant_id?' + 'start=2000-01-20T00:00:00&' + 'end=2005-02-01T00:00:00&' + 'marker=%s' % (marker), pos=pos + 1) + # Servers, RAM MB-Hours, CPU Hours, Disk GB-Hours + self.assertIn('2 | 50903.53 | 99.42 | 0.00', stdout) def test_usage_no_tenant(self): self.run_command('usage --start 2000-01-20 --end 2005-02-01') diff --git a/novaclient/tests/unit/v2/test_usage.py b/novaclient/tests/unit/v2/test_usage.py index e67178b41..900842334 100644 --- a/novaclient/tests/unit/v2/test_usage.py +++ b/novaclient/tests/unit/v2/test_usage.py @@ -73,3 +73,52 @@ def test_usage_class_get(self): 'GET', "/os-simple-tenant-usage/tenantfoo?start=%s&end=%s" % (start, stop)) + + +class UsageV40Test(UsageTest): + def setUp(self): + super(UsageV40Test, self).setUp() + self.cs.api_version = api_versions.APIVersion('2.40') + + def test_usage_list_with_paging(self): + now = datetime.datetime.now() + usages = self.cs.usage.list(now, now, marker='some-uuid', limit=3) + self.assert_request_id(usages, fakes.FAKE_REQUEST_ID_LIST) + + self.cs.assert_called( + 'GET', + '/os-simple-tenant-usage?' + + ('start=%s&' % now.isoformat()) + + ('end=%s&' % now.isoformat()) + + ('limit=3&marker=some-uuid&detailed=0')) + for u in usages: + self.assertIsInstance(u, usage.Usage) + + def test_usage_list_detailed_with_paging(self): + now = datetime.datetime.now() + usages = self.cs.usage.list( + now, now, detailed=True, marker='some-uuid', limit=3) + self.assert_request_id(usages, fakes.FAKE_REQUEST_ID_LIST) + + self.cs.assert_called( + 'GET', + '/os-simple-tenant-usage?' + + ('start=%s&' % now.isoformat()) + + ('end=%s&' % now.isoformat()) + + ('limit=3&marker=some-uuid&detailed=1')) + for u in usages: + self.assertIsInstance(u, usage.Usage) + + def test_usage_get_with_paging(self): + now = datetime.datetime.now() + u = self.cs.usage.get( + 'tenantfoo', now, now, marker='some-uuid', limit=3) + self.assert_request_id(u, fakes.FAKE_REQUEST_ID_LIST) + + self.cs.assert_called( + 'GET', + '/os-simple-tenant-usage/tenantfoo?' + + ('start=%s&' % now.isoformat()) + + ('end=%s&' % now.isoformat()) + + ('limit=3&marker=some-uuid')) + self.assertIsInstance(u, usage.Usage) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index b104ff8a1..704e96705 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -19,6 +19,7 @@ from __future__ import print_function import argparse +import collections import datetime import functools import getpass @@ -3453,6 +3454,36 @@ def do_limits(cs, args): _print_absolute_limits(limits.absolute) +def _get_usage_marker(usage): + marker = None + if hasattr(usage, 'server_usages') and usage.server_usages: + marker = usage.server_usages[-1]['instance_id'] + return marker + + +def _get_usage_list_marker(usage_list): + marker = None + if usage_list: + marker = _get_usage_marker(usage_list[-1]) + return marker + + +def _merge_usage(usage, next_usage): + usage.server_usages.extend(next_usage.server_usages) + usage.total_hours += next_usage.total_hours + usage.total_memory_mb_usage += next_usage.total_memory_mb_usage + usage.total_vcpus_usage += next_usage.total_vcpus_usage + usage.total_local_gb_usage += next_usage.total_local_gb_usage + + +def _merge_usage_list(usages, next_usage_list): + for next_usage in next_usage_list: + if next_usage.tenant_id in usages: + _merge_usage(usages[next_usage.tenant_id], next_usage) + else: + usages[next_usage.tenant_id] = next_usage + + @utils.arg( '--start', metavar='', @@ -3490,7 +3521,23 @@ def simplify_usage(u): setattr(u, simplerows[3], "%.2f" % u.total_vcpus_usage) setattr(u, simplerows[4], "%.2f" % u.total_local_gb_usage) - usage_list = cs.usage.list(start, end, detailed=True) + if cs.api_version < api_versions.APIVersion('2.40'): + usage_list = cs.usage.list(start, end, detailed=True) + else: + # If the number of instances used to calculate the usage is greater + # than CONF.api.max_limit, the usage will be split across multiple + # requests and the responses will need to be merged back together. + usages = collections.OrderedDict() + usage_list = cs.usage.list(start, end, detailed=True) + _merge_usage_list(usages, usage_list) + marker = _get_usage_list_marker(usage_list) + while marker: + next_usage_list = cs.usage.list( + start, end, detailed=True, marker=marker) + marker = _get_usage_list_marker(next_usage_list) + if marker: + _merge_usage_list(usages, next_usage_list) + usage_list = list(usages.values()) print(_("Usage from %(start)s to %(end)s:") % {'start': start.strftime(dateformat), @@ -3542,14 +3589,27 @@ def simplify_usage(u): setattr(u, simplerows[3], "%.2f" % u.total_local_gb_usage) if args.tenant: - usage = cs.usage.get(args.tenant, start, end) + tenant_id = args.tenant else: if isinstance(cs.client, client.SessionClient): auth = cs.client.auth - project_id = auth.get_auth_ref(cs.client.session).project_id - usage = cs.usage.get(project_id, start, end) + tenant_id = auth.get_auth_ref(cs.client.session).project_id else: - usage = cs.usage.get(cs.client.tenant_id, start, end) + tenant_id = cs.client.tenant_id + + if cs.api_version < api_versions.APIVersion('2.40'): + usage = cs.usage.get(tenant_id, start, end) + else: + # If the number of instances used to calculate the usage is greater + # than CONF.api.max_limit, the usage will be split across multiple + # requests and the responses will need to be merged back together. + usage = cs.usage.get(tenant_id, start, end) + marker = _get_usage_marker(usage) + while marker: + next_usage = cs.usage.get(tenant_id, start, end, marker=marker) + marker = _get_usage_marker(next_usage) + if marker: + _merge_usage(usage, next_usage) print(_("Usage from %(start)s to %(end)s:") % {'start': start.strftime(dateformat), diff --git a/novaclient/v2/usage.py b/novaclient/v2/usage.py index dacf87b55..abbeca692 100644 --- a/novaclient/v2/usage.py +++ b/novaclient/v2/usage.py @@ -17,6 +17,7 @@ import oslo_utils +from novaclient import api_versions from novaclient import base @@ -46,7 +47,19 @@ class UsageManager(base.ManagerWithFind): Manage :class:`Usage` resources. """ resource_class = Usage + usage_prefix = 'os-simple-tenant-usage' + def _usage_query(self, start, end, marker=None, limit=None, detailed=None): + query = "?start=%s&end=%s" % (start.isoformat(), end.isoformat()) + if limit: + query = "%s&limit=%s" % (query, int(limit)) + if marker: + query = "%s&marker=%s" % (query, marker) + if detailed is not None: + query = "%s&detailed=%s" % (query, int(bool(detailed))) + return query + + @api_versions.wraps("2.0", "2.39") def list(self, start, end, detailed=False): """ Get usage for all tenants @@ -57,11 +70,31 @@ def list(self, start, end, detailed=False): instance whose usage is part of the report :rtype: list of :class:`Usage`. """ - return self._list( - "/os-simple-tenant-usage?start=%s&end=%s&detailed=%s" % - (start.isoformat(), end.isoformat(), int(bool(detailed))), - "tenant_usages") + query_string = self._usage_query(start, end, detailed=detailed) + url = '/%s%s' % (self.usage_prefix, query_string) + return self._list(url, 'tenant_usages') + + @api_versions.wraps("2.40") + def list(self, start, end, detailed=False, marker=None, limit=None): + """ + Get usage for all tenants + + :param start: :class:`datetime.datetime` Start date in UTC + :param end: :class:`datetime.datetime` End date in UTC + :param detailed: Whether to include information about each + instance whose usage is part of the report + :param marker: Begin returning usage data for instances that appear + later in the instance list than that represented by + this instance UUID (optional). + :param limit: Maximum number of instances to include in the usage + (optional). + :rtype: list of :class:`Usage`. + """ + query_string = self._usage_query(start, end, marker, limit, detailed) + url = '/%s%s' % (self.usage_prefix, query_string) + return self._list(url, 'tenant_usages') + @api_versions.wraps("2.0", "2.39") def get(self, tenant_id, start, end): """ Get usage for a specific tenant. @@ -71,6 +104,25 @@ def get(self, tenant_id, start, end): :param end: :class:`datetime.datetime` End date in UTC :rtype: :class:`Usage` """ - return self._get("/os-simple-tenant-usage/%s?start=%s&end=%s" % - (tenant_id, start.isoformat(), end.isoformat()), - "tenant_usage") + query_string = self._usage_query(start, end) + url = '/%s/%s%s' % (self.usage_prefix, tenant_id, query_string) + return self._get(url, 'tenant_usage') + + @api_versions.wraps("2.40") + def get(self, tenant_id, start, end, marker=None, limit=None): + """ + Get usage for a specific tenant. + + :param tenant_id: Tenant ID to fetch usage for + :param start: :class:`datetime.datetime` Start date in UTC + :param end: :class:`datetime.datetime` End date in UTC + :param marker: Begin returning usage data for instances that appear + later in the instance list than that represented by + this instance UUID (optional). + :param limit: Maximum number of instances to include in the usage + (optional). + :rtype: :class:`Usage` + """ + query_string = self._usage_query(start, end, marker, limit) + url = '/%s/%s%s' % (self.usage_prefix, tenant_id, query_string) + return self._get(url, 'tenant_usage') diff --git a/releasenotes/notes/microversion-v2_40-484adba0806b08bf.yaml b/releasenotes/notes/microversion-v2_40-484adba0806b08bf.yaml new file mode 100644 index 000000000..8b856ac65 --- /dev/null +++ b/releasenotes/notes/microversion-v2_40-484adba0806b08bf.yaml @@ -0,0 +1,4 @@ +--- +features: + - Added microversion v2.40 which introduces pagination support for usage + with the help of new optional parameters 'limit' and 'marker'. From d77aa05049ae63718ec142d0ff6474e2886ee2c2 Mon Sep 17 00:00:00 2001 From: Luong Anh Tuan Date: Mon, 5 Dec 2016 16:20:23 +0700 Subject: [PATCH 1216/1705] Replaces uuid.uuid4 with uuidutils.generate_uuid() Openstack common has a wrapper for generating uuids. We should use that function when generating uuids for consistency. Change-Id: Ic6045a3b8dabedf3ecaa14d94707614fc4d454e4 Closes-Bug: #1082248 --- novaclient/tests/functional/base.py | 5 ++--- .../tests/functional/v2/legacy/test_keypairs.py | 4 ++-- .../functional/v2/legacy/test_server_groups.py | 4 ++-- .../tests/functional/v2/legacy/test_servers.py | 14 +++++++------- .../tests/functional/v2/test_device_tagging.py | 4 ++-- .../tests/functional/v2/test_instance_action.py | 9 ++++----- novaclient/tests/unit/v2/test_client.py | 9 ++++----- 7 files changed, 23 insertions(+), 26 deletions(-) diff --git a/novaclient/tests/functional/base.py b/novaclient/tests/functional/base.py index 673955b17..b1b2bf9ff 100644 --- a/novaclient/tests/functional/base.py +++ b/novaclient/tests/functional/base.py @@ -12,7 +12,6 @@ import os import time -import uuid from cinderclient.v2 import client as cinderclient import fixtures @@ -23,7 +22,7 @@ from keystoneclient import client as keystoneclient from keystoneclient import discover as keystone_discover import os_client_config -import six +from oslo_utils import uuidutils import tempest.lib.cli.base import testtools @@ -380,7 +379,7 @@ def name_generate(self, prefix='Entity'): :param prefix: string prefix """ - name = "%s-%s" % (prefix, six.text_type(uuid.uuid4())) + name = "%s-%s" % (prefix, uuidutils.generate_uuid()) return name def _get_value_from_the_table(self, table, key): diff --git a/novaclient/tests/functional/v2/legacy/test_keypairs.py b/novaclient/tests/functional/v2/legacy/test_keypairs.py index 85a1a3a77..73e77b9cf 100644 --- a/novaclient/tests/functional/v2/legacy/test_keypairs.py +++ b/novaclient/tests/functional/v2/legacy/test_keypairs.py @@ -11,8 +11,8 @@ # under the License. import tempfile -import uuid +from oslo_utils import uuidutils from tempest.lib import exceptions from novaclient.tests.functional import base @@ -36,7 +36,7 @@ def _create_keypair(self, **kwargs): return key_name def _raw_create_keypair(self, **kwargs): - key_name = 'keypair-' + str(uuid.uuid4()) + key_name = 'keypair-' + uuidutils.generate_uuid() kwargs_str = self._serialize_kwargs(kwargs) self.nova('keypair-add %s %s' % (kwargs_str, key_name)) return key_name diff --git a/novaclient/tests/functional/v2/legacy/test_server_groups.py b/novaclient/tests/functional/v2/legacy/test_server_groups.py index ef7ff9ea1..5518ea979 100644 --- a/novaclient/tests/functional/v2/legacy/test_server_groups.py +++ b/novaclient/tests/functional/v2/legacy/test_server_groups.py @@ -11,7 +11,7 @@ # License for the specific language governing permissions and limitations # under the License. -import uuid +from oslo_utils import uuidutils from novaclient.tests.functional import base @@ -22,7 +22,7 @@ class TestServerGroupClient(base.ClientTestBase): COMPUTE_API_VERSION = "2.1" def _create_sg(self, policy): - sg_name = 'server_group-' + str(uuid.uuid4()) + sg_name = 'server_group-' + uuidutils.generate_uuid() output = self.nova('server-group-create %s %s' % (sg_name, policy)) sg_id = self._get_column_value_from_single_row_table(output, "Id") return sg_id diff --git a/novaclient/tests/functional/v2/legacy/test_servers.py b/novaclient/tests/functional/v2/legacy/test_servers.py index 474fbb95b..cfc3939a0 100644 --- a/novaclient/tests/functional/v2/legacy/test_servers.py +++ b/novaclient/tests/functional/v2/legacy/test_servers.py @@ -11,9 +11,9 @@ # under the License. import datetime -import uuid from oslo_utils import timeutils +from oslo_utils import uuidutils from novaclient.tests.functional import base @@ -25,7 +25,7 @@ class TestServersBootNovaClient(base.ClientTestBase): def _boot_server_with_legacy_bdm(self, bdm_params=()): volume_size = 1 - volume_name = str(uuid.uuid4()) + volume_name = uuidutils.generate_uuid() volume = self.cinder.volumes.create(size=volume_size, name=volume_name, imageRef=self.image.id) @@ -38,7 +38,7 @@ def _boot_server_with_legacy_bdm(self, bdm_params=()): server_info = self.nova("boot", params=( "%(name)s --flavor %(flavor)s --poll " "--block-device-mapping vda=%(volume_id)s%(bdm_params)s" % { - "name": str(uuid.uuid4()), "flavor": + "name": uuidutils.generate_uuid(), "flavor": self.flavor.id, "volume_id": volume.id, "bdm_params": bdm_params})) @@ -60,7 +60,7 @@ def test_boot_server_with_legacy_bdm_volume_id_only(self): def test_boot_server_with_net_name(self): server_info = self.nova("boot", params=( "%(name)s --flavor %(flavor)s --image %(image)s --poll " - "--nic net-name=%(net-name)s" % {"name": str(uuid.uuid4()), + "--nic net-name=%(net-name)s" % {"name": uuidutils.generate_uuid(), "image": self.image.id, "flavor": self.flavor.id, "net-name": self.network.label})) @@ -79,7 +79,7 @@ def _create_servers(self, name, number): return [self._create_server(name) for i in range(number)] def test_list_with_limit(self): - name = str(uuid.uuid4()) + name = uuidutils.generate_uuid() self._create_servers(name, 2) output = self.nova("list", params="--limit 1 --name %s" % name) # Cut header and footer of the table @@ -88,7 +88,7 @@ def test_list_with_limit(self): def test_list_with_changes_since(self): now = datetime.datetime.isoformat(timeutils.utcnow()) - name = str(uuid.uuid4()) + name = uuidutils.generate_uuid() self._create_servers(name, 1) output = self.nova("list", params="--changes-since %s" % now) self.assertIn(name, output, output) @@ -97,7 +97,7 @@ def test_list_with_changes_since(self): self.assertNotIn(name, output, output) def test_list_all_servers(self): - name = str(uuid.uuid4()) + name = uuidutils.generate_uuid() precreated_servers = self._create_servers(name, 3) # there are no possibility to exceed the limit on API side, so just # check that "-1" limit processes by novaclient side diff --git a/novaclient/tests/functional/v2/test_device_tagging.py b/novaclient/tests/functional/v2/test_device_tagging.py index d9a5850d2..033b43739 100644 --- a/novaclient/tests/functional/v2/test_device_tagging.py +++ b/novaclient/tests/functional/v2/test_device_tagging.py @@ -12,7 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. -import uuid +from oslo_utils import uuidutils from novaclient.tests.functional import base @@ -27,7 +27,7 @@ def test_boot_server_with_tagged_devices(self): '--nic net-id=%(net-uuid)s,tag=foo ' '--block-device ' 'source=image,dest=volume,id=%(image)s,size=1,' - 'bootindex=0,tag=bar' % {'name': str(uuid.uuid4()), + 'bootindex=0,tag=bar' % {'name': uuidutils.generate_uuid(), 'flavor': self.flavor.id, 'net-uuid': self.network.id, 'image': self.image.id})) diff --git a/novaclient/tests/functional/v2/test_instance_action.py b/novaclient/tests/functional/v2/test_instance_action.py index db95b9b70..728dbb0bc 100644 --- a/novaclient/tests/functional/v2/test_instance_action.py +++ b/novaclient/tests/functional/v2/test_instance_action.py @@ -10,8 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import uuid - +from oslo_utils import uuidutils import six from tempest.lib import exceptions @@ -31,13 +30,13 @@ def _test_cmd_with_not_existing_instance(self, cmd, args): self.fail("%s is not failed on non existing instance." % cmd) def test_show_action_with_not_existing_instance(self): - name_or_uuid = str(uuid.uuid4()) - request_id = str(uuid.uuid4()) + name_or_uuid = uuidutils.generate_uuid() + request_id = uuidutils.generate_uuid() self._test_cmd_with_not_existing_instance( "instance-action", "%s %s" % (name_or_uuid, request_id)) def test_list_actions_with_not_existing_instance(self): - name_or_uuid = str(uuid.uuid4()) + name_or_uuid = uuidutils.generate_uuid() self._test_cmd_with_not_existing_instance("instance-action-list", name_or_uuid) diff --git a/novaclient/tests/unit/v2/test_client.py b/novaclient/tests/unit/v2/test_client.py index 02a9e4d45..65de2d564 100644 --- a/novaclient/tests/unit/v2/test_client.py +++ b/novaclient/tests/unit/v2/test_client.py @@ -10,9 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. -import uuid - from keystoneauth1 import session +from oslo_utils import uuidutils from novaclient import api_versions from novaclient.tests.unit import utils @@ -23,8 +22,8 @@ class ClientTest(utils.TestCase): def test_adapter_properties(self): # sample of properties, there are many more - user_agent = uuid.uuid4().hex - endpoint_override = uuid.uuid4().hex + user_agent = uuidutils.generate_uuid(dashed=False) + endpoint_override = uuidutils.generate_uuid(dashed=False) s = session.Session() c = client.Client(session=s, @@ -37,7 +36,7 @@ def test_adapter_properties(self): self.assertEqual(endpoint_override, c.client.endpoint_override) def test_passing_endpoint_type(self): - endpoint_type = uuid.uuid4().hex + endpoint_type = uuidutils.generate_uuid(dashed=False) s = session.Session() c = client.Client(session=s, From 69d05c6774b9244ee1c6b7f8283f87422bb51b34 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Tue, 20 Dec 2016 09:54:28 -0500 Subject: [PATCH 1217/1705] Clarify some release notes prior to the 7.0.0 release Change-Id: I55dc81659973ab5afc60b030b44f30e62b73dbd9 --- ...ify-project-id-variable-5832698315000438.yaml | 16 ++++++++-------- .../deprecate-proxy-args-a3f4e224f7664ff8.yaml | 6 +++--- .../microversion-v2_38-0618fe2b3c7f96f9.yaml | 2 +- .../rename-bypass-url-42cd96956a6bc6dc.yaml | 6 +++--- ...-args-for-novaclient-ep-491098c3634365be.yaml | 9 ++++++--- 5 files changed, 21 insertions(+), 18 deletions(-) diff --git a/releasenotes/notes/clarify-project-id-variable-5832698315000438.yaml b/releasenotes/notes/clarify-project-id-variable-5832698315000438.yaml index 16b5b5de4..83182d1ce 100644 --- a/releasenotes/notes/clarify-project-id-variable-5832698315000438.yaml +++ b/releasenotes/notes/clarify-project-id-variable-5832698315000438.yaml @@ -1,13 +1,13 @@ --- deprecations: - - keyword argument **tenant_id** of novaclient.client.Client entry-point was - deprecated in favor of **project_id**; - - keyword argument **tenant_name** of novaclient.client.Client entry-point - was deprecated in favor of **project_name**; + - Keyword argument **tenant_id** of novaclient.client.Client entry-point was + deprecated in favor of **project_id**. + - Keyword argument **tenant_name** of novaclient.client.Client entry-point + was deprecated in favor of **project_name**. other: - The meaning of 'project_id' variable of novaclient.client.Client entry-point was not clear. In different cases it was used as ID or Name of - project(in terms of Keystone). The time to identify meaning is come, so now - project_id/tenant_id variables specifically mean about Project ID(in terms - of Keystone) and project_name/tenant_name variables take care about Project - Name(in terms of Keystone). + project (in terms of Keystone). The time to identify meaning is come, so + now project_id/tenant_id variables specifically mean Project ID (in terms + of Keystone) and project_name/tenant_name variables mean Project Name (in + terms of Keystone). diff --git a/releasenotes/notes/deprecate-proxy-args-a3f4e224f7664ff8.yaml b/releasenotes/notes/deprecate-proxy-args-a3f4e224f7664ff8.yaml index 887692a67..c513b6ff8 100644 --- a/releasenotes/notes/deprecate-proxy-args-a3f4e224f7664ff8.yaml +++ b/releasenotes/notes/deprecate-proxy-args-a3f4e224f7664ff8.yaml @@ -1,5 +1,5 @@ --- deprecations: - - There are two arguments for proxying stuff (proxy_tenant_id and - proxy_token) in novaclient.client.Client entry-point. They were never - documented and tested. + - The **proxy_tenant_id** and **proxy_token** arguments to the + novaclient.client.Client entry-point were never documented nor tested and + are now deprecated for removal in a future release. diff --git a/releasenotes/notes/microversion-v2_38-0618fe2b3c7f96f9.yaml b/releasenotes/notes/microversion-v2_38-0618fe2b3c7f96f9.yaml index 3f8bc84c3..a9944a72c 100644 --- a/releasenotes/notes/microversion-v2_38-0618fe2b3c7f96f9.yaml +++ b/releasenotes/notes/microversion-v2_38-0618fe2b3c7f96f9.yaml @@ -1,5 +1,5 @@ --- upgrade: - Support for microversion 2.38 added. As of microversion 2.38, invalid - statuses passed to 'nova list --status invalid_status' will result in a + statuses passed to ``nova list --status invalid_status`` will result in a HTTP 400 Bad Request error response. \ No newline at end of file diff --git a/releasenotes/notes/rename-bypass-url-42cd96956a6bc6dc.yaml b/releasenotes/notes/rename-bypass-url-42cd96956a6bc6dc.yaml index 9dc78f8b5..dc6374e15 100644 --- a/releasenotes/notes/rename-bypass-url-42cd96956a6bc6dc.yaml +++ b/releasenotes/notes/rename-bypass-url-42cd96956a6bc6dc.yaml @@ -3,8 +3,8 @@ deprecations: - The **--bypass-url** CLI argument was deprecated in favor of **--endpoint-override** - The **bypass_url** argument of novaclient.client.Client entry-point was - deprecated in favor of **endpoint_override** + deprecated in favor of **endpoint_override**. fixes: - - Inability to use bypass-url with keystone session + - Inability to use bypass-url with keystone session is fixed. features: - - You can use *bypass-url*/**endpoint-override** with Keystone V3 + - You can use **bypass-url** / **endpoint-override** with Keystone V3. diff --git a/releasenotes/notes/restrict-args-for-novaclient-ep-491098c3634365be.yaml b/releasenotes/notes/restrict-args-for-novaclient-ep-491098c3634365be.yaml index 15a651f62..11dda395a 100644 --- a/releasenotes/notes/restrict-args-for-novaclient-ep-491098c3634365be.yaml +++ b/releasenotes/notes/restrict-args-for-novaclient-ep-491098c3634365be.yaml @@ -1,5 +1,8 @@ --- upgrade: - - novaclient.client.Client entry-point accepts only 5 positional arguments( - version, username, api_key, project_id, auth_url). Using positional - arguments for all other options become an error. + - | + novaclient.client.Client entry-point accepts only 5 positional arguments:: + + version, username, api_key, project_id, auth_url + + Using positional arguments for all other options is now an error. From e8e39ed343622350e114c84ecd5fcc0c94bfec6b Mon Sep 17 00:00:00 2001 From: Diana Clarke Date: Thu, 29 Dec 2016 14:33:37 -0600 Subject: [PATCH 1218/1705] Blacklist rather than whitelist autodoc modules Autogenerate the nova client python API module list rather than having to remember to add new modules; use exclude to blacklist modules that shouldn't be included in autodoc. Change-Id: I051a7094f3b536f1e9a939af87e3fc89554fb375 --- doc/source/conf.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index 935a45f9c..237a363ae 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -70,18 +70,17 @@ def gen_ref(ver, title, names): "signs": "=" * len(name), "pkg": pkg, "name": name}) + +def get_module_names(): + names = os.listdir(os.path.join(ROOT, 'novaclient', 'v2')) + exclude = ['shell.py', '__init__.py'] + for name in names: + if name.endswith('.py') and name not in exclude: + yield name.strip('.py') + + gen_ref(None, "Exceptions", ["exceptions"]) -gen_ref("v2", "Version 1.1, Version 2 API", - ["agents", "aggregates", "assisted_volume_snapshots", - "availability_zones", "cells", "certs", "client", "cloudpipe", - "fixed_ips", "flavor_access", "flavors", "floating_ip_dns", - "floating_ip_pools", "floating_ips", "floating_ips_bulk", "fping", - "hosts", "hypervisors", "images", "instance_action", "keypairs", - "limits", "list_extensions", "migrations", "networks", - "quota_classes", "quotas", "security_group_default_rules", - "security_group_rules", "security_groups", "server_external_events", - "server_groups", "server_migrations", "servers", "services", "usage", - "versions", "virtual_interfaces", "volumes"]) +gen_ref("v2", "Version 2 API", sorted(get_module_names())) # -- General configuration ---------------------------------------------------- From 75ffbaa3e61a8d3264ca048f17a405a1a498abe8 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Thu, 5 Jan 2017 22:03:34 -0500 Subject: [PATCH 1219/1705] Add support for showing aggregate UUIDs (v2.41) This adds support for the v2.41 microversion which returns the aggregate uuid in os-aggregates responses. With this change, CLI requests with microversion >= 2.41 will show the aggregate UUID in the table output. Depends-On: I4112ccd508eb85403933fec8b52efd468e866772 Part of blueprint return-uuid-from-os-aggregates-api Change-Id: I71aeec07c7c442fe82e2bb3f75f49de6ced22e77 --- novaclient/__init__.py | 2 +- novaclient/tests/unit/v2/fakes.py | 9 +- novaclient/tests/unit/v2/test_shell.py | 82 +++++++++++++++++-- novaclient/v2/shell.py | 18 ++-- .../microversion-v2_41-6df7a5a66a9ded35.yaml | 6 ++ 5 files changed, 101 insertions(+), 16 deletions(-) create mode 100644 releasenotes/notes/microversion-v2_41-6df7a5a66a9ded35.yaml diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 956e139e6..0816b4f61 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.40") +API_MAX_VERSION = api_versions.APIVersion("2.41") diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index e6f749a51..f9da820c4 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -1703,7 +1703,7 @@ def get_os_simple_tenant_usage_tenant_id_next_next(self, **kw): # def get_os_aggregates(self, *kw): - return (200, {}, {"aggregates": [ + response = (200, {}, {"aggregates": [ {'id': '1', 'name': 'test', 'availability_zone': 'nova1'}, @@ -1714,6 +1714,13 @@ def get_os_aggregates(self, *kw): 'name': 'test3', 'metadata': {'test': "dup", "none_key": "Nine"}}, ]}) + # microversion >= 2.41 returns the uuid in the response + if self.api_version >= api_versions.APIVersion('2.41'): + aggregates = response[2]['aggregates'] + aggregates[0]['uuid'] = '80785864-087b-45a5-a433-b20eac9b58aa' + aggregates[1]['uuid'] = '30827713-5957-4b68-8fd3-ccaddb568c24' + aggregates[2]['uuid'] = '9a651b22-ce3f-4a87-acd7-98446ef591c4' + return response def _return_aggregate(self): r = {'aggregate': self.get_os_aggregates()[2]['aggregates'][0]} diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 6a27175da..bb01344aa 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -1942,15 +1942,35 @@ def test_flavor_create(self): self.assert_called('GET', '/flavors/1', pos=-1) def test_aggregate_list(self): - self.run_command('aggregate-list') + out, err = self.run_command('aggregate-list') self.assert_called('GET', '/os-aggregates') + self.assertNotIn('UUID', out) + + def test_aggregate_list_v2_41(self): + out, err = self.run_command('aggregate-list', api_version='2.41') + self.assert_called('GET', '/os-aggregates') + self.assertIn('UUID', out) + self.assertIn('80785864-087b-45a5-a433-b20eac9b58aa', out) + self.assertIn('30827713-5957-4b68-8fd3-ccaddb568c24', out) + self.assertIn('9a651b22-ce3f-4a87-acd7-98446ef591c4', out) def test_aggregate_create(self): - self.run_command('aggregate-create test_name nova1') + out, err = self.run_command('aggregate-create test_name nova1') + body = {"aggregate": {"name": "test_name", + "availability_zone": "nova1"}} + self.assert_called('POST', '/os-aggregates', body, pos=-2) + self.assert_called('GET', '/os-aggregates/1', pos=-1) + self.assertNotIn('UUID', out) + + def test_aggregate_create_v2_41(self): + out, err = self.run_command('aggregate-create test_name nova1', + api_version='2.41') body = {"aggregate": {"name": "test_name", "availability_zone": "nova1"}} self.assert_called('POST', '/os-aggregates', body, pos=-2) self.assert_called('GET', '/os-aggregates/1', pos=-1) + self.assertIn('UUID', out) + self.assertIn('80785864-087b-45a5-a433-b20eac9b58aa', out) def test_aggregate_delete_by_id(self): self.run_command('aggregate-delete 1') @@ -1961,10 +1981,20 @@ def test_aggregate_delete_by_name(self): self.assert_called('DELETE', '/os-aggregates/1') def test_aggregate_update_by_id(self): - self.run_command('aggregate-update 1 --name new_name') + out, err = self.run_command('aggregate-update 1 --name new_name') body = {"aggregate": {"name": "new_name"}} self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) self.assert_called('GET', '/os-aggregates/1', pos=-1) + self.assertNotIn('UUID', out) + + def test_aggregate_update_by_id_v2_41(self): + out, err = self.run_command('aggregate-update 1 --name new_name', + api_version='2.41') + body = {"aggregate": {"name": "new_name"}} + self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) + self.assert_called('GET', '/os-aggregates/1', pos=-1) + self.assertIn('UUID', out) + self.assertIn('80785864-087b-45a5-a433-b20eac9b58aa', out) def test_aggregate_update_by_name(self): self.run_command('aggregate-update test --name new_name ') @@ -2011,10 +2041,20 @@ def test_aggregate_update_with_availability_zone_by_name_legacy(self): self.assert_called('GET', '/os-aggregates/1', pos=-1) def test_aggregate_set_metadata_add_by_id(self): - self.run_command('aggregate-set-metadata 3 foo=bar') + out, err = self.run_command('aggregate-set-metadata 3 foo=bar') body = {"set_metadata": {"metadata": {"foo": "bar"}}} self.assert_called('POST', '/os-aggregates/3/action', body, pos=-2) self.assert_called('GET', '/os-aggregates/3', pos=-1) + self.assertNotIn('UUID', out) + + def test_aggregate_set_metadata_add_by_id_v2_41(self): + out, err = self.run_command('aggregate-set-metadata 3 foo=bar', + api_version='2.41') + body = {"set_metadata": {"metadata": {"foo": "bar"}}} + self.assert_called('POST', '/os-aggregates/3/action', body, pos=-2) + self.assert_called('GET', '/os-aggregates/3', pos=-1) + self.assertIn('UUID', out) + self.assertIn('9a651b22-ce3f-4a87-acd7-98446ef591c4', out) def test_aggregate_set_metadata_add_duplicate_by_id(self): cmd = 'aggregate-set-metadata 3 test=dup' @@ -2037,10 +2077,20 @@ def test_aggregate_set_metadata_by_name(self): self.assert_called('GET', '/os-aggregates/1', pos=-1) def test_aggregate_add_host_by_id(self): - self.run_command('aggregate-add-host 1 host1') + out, err = self.run_command('aggregate-add-host 1 host1') + body = {"add_host": {"host": "host1"}} + self.assert_called('POST', '/os-aggregates/1/action', body, pos=-2) + self.assert_called('GET', '/os-aggregates/1', pos=-1) + self.assertNotIn('UUID', out) + + def test_aggregate_add_host_by_id_v2_41(self): + out, err = self.run_command('aggregate-add-host 1 host1', + api_version='2.41') body = {"add_host": {"host": "host1"}} self.assert_called('POST', '/os-aggregates/1/action', body, pos=-2) self.assert_called('GET', '/os-aggregates/1', pos=-1) + self.assertIn('UUID', out) + self.assertIn('80785864-087b-45a5-a433-b20eac9b58aa', out) def test_aggregate_add_host_by_name(self): self.run_command('aggregate-add-host test host1') @@ -2049,10 +2099,20 @@ def test_aggregate_add_host_by_name(self): self.assert_called('GET', '/os-aggregates/1', pos=-1) def test_aggregate_remove_host_by_id(self): - self.run_command('aggregate-remove-host 1 host1') + out, err = self.run_command('aggregate-remove-host 1 host1') body = {"remove_host": {"host": "host1"}} self.assert_called('POST', '/os-aggregates/1/action', body, pos=-2) self.assert_called('GET', '/os-aggregates/1', pos=-1) + self.assertNotIn('UUID', out) + + def test_aggregate_remove_host_by_id_v2_41(self): + out, err = self.run_command('aggregate-remove-host 1 host1', + api_version='2.41') + body = {"remove_host": {"host": "host1"}} + self.assert_called('POST', '/os-aggregates/1/action', body, pos=-2) + self.assert_called('GET', '/os-aggregates/1', pos=-1) + self.assertIn('UUID', out) + self.assertIn('80785864-087b-45a5-a433-b20eac9b58aa', out) def test_aggregate_remove_host_by_name(self): self.run_command('aggregate-remove-host test host1') @@ -2061,8 +2121,15 @@ def test_aggregate_remove_host_by_name(self): self.assert_called('GET', '/os-aggregates/1', pos=-1) def test_aggregate_show_by_id(self): - self.run_command('aggregate-show 1') + out, err = self.run_command('aggregate-show 1') + self.assert_called('GET', '/os-aggregates/1') + self.assertNotIn('UUID', out) + + def test_aggregate_show_by_id_v2_41(self): + out, err = self.run_command('aggregate-show 1', api_version='2.41') self.assert_called('GET', '/os-aggregates/1') + self.assertIn('UUID', out) + self.assertIn('80785864-087b-45a5-a433-b20eac9b58aa', out) def test_aggregate_show_by_name(self): self.run_command('aggregate-show test') @@ -3234,6 +3301,7 @@ def test_versions(self): 37, # There are no versioned wrapped shell method changes for this 38, # doesn't require any changes in novaclient 39, # There are no versioned wrapped shell method changes for this + 41, # There are no version-wrapped shell method changes for this. ]) versions_supported = set(range(0, novaclient.API_MAX_VERSION.ver_minor + 1)) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 704e96705..055559a44 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -3737,6 +3737,8 @@ def do_aggregate_list(cs, args): """Print a list of all aggregates.""" aggregates = cs.aggregates.list() columns = ['Id', 'Name', 'Availability Zone'] + if cs.api_version >= api_versions.APIVersion('2.41'): + columns.append('UUID') utils.print_list(aggregates, columns) @@ -3750,7 +3752,7 @@ def do_aggregate_list(cs, args): def do_aggregate_create(cs, args): """Create a new aggregate with the specified details.""" aggregate = cs.aggregates.create(args.name, args.availability_zone) - _print_aggregate_details(aggregate) + _print_aggregate_details(cs, aggregate) @utils.arg( @@ -3804,7 +3806,7 @@ def do_aggregate_update(cs, args): aggregate = cs.aggregates.update(aggregate.id, updates) print(_("Aggregate %s has been successfully updated.") % aggregate.id) - _print_aggregate_details(aggregate) + _print_aggregate_details(cs, aggregate) @utils.arg( @@ -3833,7 +3835,7 @@ def do_aggregate_set_metadata(cs, args): aggregate = cs.aggregates.set_metadata(aggregate.id, metadata) print(_("Metadata has been successfully updated for aggregate %s.") % aggregate.id) - _print_aggregate_details(aggregate) + _print_aggregate_details(cs, aggregate) @utils.arg( @@ -3849,7 +3851,7 @@ def do_aggregate_add_host(cs, args): print(_("Host %(host)s has been successfully added for aggregate " "%(aggregate_id)s ") % {'host': args.host, 'aggregate_id': aggregate.id}) - _print_aggregate_details(aggregate) + _print_aggregate_details(cs, aggregate) @utils.arg( @@ -3865,7 +3867,7 @@ def do_aggregate_remove_host(cs, args): print(_("Host %(host)s has been successfully removed from aggregate " "%(aggregate_id)s ") % {'host': args.host, 'aggregate_id': aggregate.id}) - _print_aggregate_details(aggregate) + _print_aggregate_details(cs, aggregate) @utils.arg( @@ -3874,11 +3876,13 @@ def do_aggregate_remove_host(cs, args): def do_aggregate_show(cs, args): """Show details of the specified aggregate.""" aggregate = _find_aggregate(cs, args.aggregate) - _print_aggregate_details(aggregate) + _print_aggregate_details(cs, aggregate) -def _print_aggregate_details(aggregate): +def _print_aggregate_details(cs, aggregate): columns = ['Id', 'Name', 'Availability Zone', 'Hosts', 'Metadata'] + if cs.api_version >= api_versions.APIVersion('2.41'): + columns.append('UUID') def parser_metadata(fields): return utils.pretty_choice_dict(getattr(fields, 'metadata', {}) or {}) diff --git a/releasenotes/notes/microversion-v2_41-6df7a5a66a9ded35.yaml b/releasenotes/notes/microversion-v2_41-6df7a5a66a9ded35.yaml new file mode 100644 index 000000000..34d442d9e --- /dev/null +++ b/releasenotes/notes/microversion-v2_41-6df7a5a66a9ded35.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Added support for microversion 2.41 which shows the aggregate UUID in CLI + output when listing, creating, showing, updating, setting metadata, and + adding or removing hosts from an aggregate. From aeec4d11b2dae769bfe020dcd95ec607afaeed55 Mon Sep 17 00:00:00 2001 From: Kevin_Zheng Date: Fri, 23 Dec 2016 17:38:57 +0800 Subject: [PATCH 1220/1705] Fix "Message object do not support addition". In the 7.0.0 Novaclient release we added some warning log about user should using "endpoint_type" rather than "interface" when init novaclient: https://github.com/openstack/python-novaclient/blob/master/novaclient/client.py#L312-L327 https://github.com/openstack/python-novaclient/blob/master/novaclient/client.py#L249-L272 This is now causing a lot jenkins failures acroos projects that is using novaclient with enable_lazy set to False. As in this kind of scenario the warning message is a message object instead of unicode sting and it cannot be added: http://git.openstack.org/cgit/openstack/oslo.i18n/tree/oslo_i18n/_message.py#n227 and "Message object do not support addition" will raise. Related Jenkins Error: Ceilometer: http://logs.openstack.org/29/333129/3/check/gate-ceilometer-python27-ubuntu-xenial/bd125e7/ Closes-bug: #1652414 Change-Id: I11a490f759fdac9707c1321c9659da2605196a94 --- novaclient/client.py | 23 ++++++++++++++++------- novaclient/v2/contrib/__init__.py | 16 ++++++++-------- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index 282054f70..06f68e366 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -253,19 +253,28 @@ def _check_arguments(kwargs, release, deprecated_name, right_name=None): message, renames key to right one it needed. """ if deprecated_name in kwargs: - msg = _LW("The '%(old)s' argument is deprecated in %(release)s and " - "its use may result in errors in future releases.") % { - "old": deprecated_name, "release": release} if right_name: if right_name in kwargs: - msg += _LW(" As '%(new)s' is provided, the '%(old)s' argument " - "will be ignored.") % {"old": deprecated_name, - "new": right_name} + msg = _LW("The '%(old)s' argument is deprecated in " + "%(release)s and its use may result in errors " + "in future releases. As '%(new)s' is provided, " + "the '%(old)s' argument will be ignored.") % { + "old": deprecated_name, "release": release, + "new": right_name} kwargs.pop(deprecated_name) else: - msg += _LW(" Use '%s' instead.") % right_name + msg = _LW("The '%(old)s' argument is deprecated in " + "%(release)s and its use may result in errors in " + "future releases. Use '%(right)s' instead.") % { + "old": deprecated_name, "release": release, + "right": right_name} kwargs[right_name] = kwargs.pop(deprecated_name) + else: + msg = _LW("The '%(old)s' argument is deprecated in %(release)s " + "and its use may result in errors in future " + "releases.") % { + "old": deprecated_name, "release": release} # just ignore it kwargs.pop(deprecated_name) diff --git a/novaclient/v2/contrib/__init__.py b/novaclient/v2/contrib/__init__.py index b419ae0c2..80acf65f5 100644 --- a/novaclient/v2/contrib/__init__.py +++ b/novaclient/v2/contrib/__init__.py @@ -35,17 +35,17 @@ def warn(alternative=True): frm = inspect.stack()[1] module_name = inspect.getmodule(frm[0]).__name__ if module_name.startswith("novaclient.v2.contrib."): - msg = (_LW("Module `%s` is deprecated as of OpenStack Ocata") % - module_name) if alternative: new_module_name = module_name.replace("contrib.", "") - msg += _LW(" in favor of `%s`") % new_module_name - - msg += (_LW(" and will be removed after OpenStack Pike.")) + msg = _LW("Module `%(module)s` is deprecated as of OpenStack " + "Ocata in favor of `%(new_module)s` and will be " + "removed after OpenStack Pike.") % { + "module": module_name, "new_module": new_module_name} if not alternative: - msg += _LW(" All shell commands were moved to " - "`novaclient.v2.shell` and will be automatically " - "loaded.") + msg = _LW("Module `%s` is deprecated as of OpenStack Ocata " + "All shell commands were moved to " + "`novaclient.v2.shell` and will be automatically " + "loaded.") % module_name warnings.warn(msg) From e81b81a027565d91e20ad55fc7c985b7e6da7e98 Mon Sep 17 00:00:00 2001 From: Jeremy Liu Date: Fri, 13 Jan 2017 10:44:46 +0800 Subject: [PATCH 1221/1705] Enable coverage report in console output Currently, the coverage test job doesn't provide any test result to developer, we could enable coverage report in console output to let the developer know the coverage status when running coverage test job. Change-Id: I12764de112e948129e21732faae03562649522fc --- tox.ini | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 8add2572d..fdc89d520 100644 --- a/tox.ini +++ b/tox.ini @@ -57,7 +57,9 @@ setenv = commands = bash tools/pretty_tox.sh '--concurrency=1 {posargs}' [testenv:cover] -commands = python setup.py testr --coverage --testr-args='{posargs}' +commands = + python setup.py testr --coverage --testr-args='{posargs}' + coverage report [flake8] # Following checks should be enabled in the future. From ae7b2c1edb1d2bee0c27a201a9333d1f1e5acba0 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Mon, 16 Jan 2017 17:28:02 +0000 Subject: [PATCH 1222/1705] Updated from global requirements Change-Id: I291eb851e3d77afd4586b2dfab33cd5302a4b66d --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b634f78a4..8b01acc15 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. pbr>=1.8 # Apache-2.0 -keystoneauth1>=2.16.0 # Apache-2.0 +keystoneauth1>=2.17.0 # Apache-2.0 iso8601>=0.1.11 # MIT oslo.i18n>=2.1.0 # Apache-2.0 oslo.serialization>=1.10.0 # Apache-2.0 From 70a3957cae435c792972bccf8f0f4eddd61eefdc Mon Sep 17 00:00:00 2001 From: int32bit Date: Sat, 24 Dec 2016 01:10:13 +0800 Subject: [PATCH 1223/1705] Allow multiple tag add/delete from cli Our users always want to add/remove multiple tags easily, but currently we can only add/remove a single tag to a server via Nova CLI. This patch allows user to add or delete server tags in one hit. Change-Id: I4dc6e5cb30b99857970965cd180e116afba1fc7d --- .../tests/functional/v2/test_servers.py | 11 ++++++++++ novaclient/tests/unit/v2/fakes.py | 18 +++++++++++++++++ novaclient/tests/unit/v2/test_shell.py | 14 +++++++++++++ novaclient/v2/shell.py | 20 +++++++++++++------ 4 files changed, 57 insertions(+), 6 deletions(-) diff --git a/novaclient/tests/functional/v2/test_servers.py b/novaclient/tests/functional/v2/test_servers.py index 3d8c5128e..31b2ae06d 100644 --- a/novaclient/tests/functional/v2/test_servers.py +++ b/novaclient/tests/functional/v2/test_servers.py @@ -158,6 +158,12 @@ def test_add(self): self.assertEqual(["t1", "t2", "t3"], self.client.servers.tag_list(uuid)) + def test_add_many(self): + uuid = self._boot_server_with_tags() + self.nova("server-tag-add %s t3 t4" % uuid) + self.assertEqual(["t1", "t2", "t3", "t4"], + self.client.servers.tag_list(uuid)) + def test_set(self): uuid = self._boot_server_with_tags() self.nova("server-tag-set %s t3 t4" % uuid) @@ -168,6 +174,11 @@ def test_delete(self): self.nova("server-tag-delete %s t2" % uuid) self.assertEqual(["t1"], self.client.servers.tag_list(uuid)) + def test_delete_many(self): + uuid = self._boot_server_with_tags() + self.nova("server-tag-delete %s t1 t2" % uuid) + self.assertEqual([], self.client.servers.tag_list(uuid)) + def test_delete_all(self): uuid = self._boot_server_with_tags() self.nova("server-tag-delete-all %s" % uuid) diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index f9da820c4..3590acdfd 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -2339,6 +2339,15 @@ def delete_servers_1234_migrations_1(self): def put_servers_1234_tags_tag(self, **kw): return (201, {}, None) + def put_servers_1234_tags_tag1(self, **kw): + return (201, {}, None) + + def put_servers_1234_tags_tag2(self, **kw): + return (201, {}, None) + + def put_servers_1234_tags_tag3(self, **kw): + return (201, {}, None) + def put_servers_1234_tags(self, **kw): return (201, {}, None) @@ -2348,6 +2357,15 @@ def get_servers_1234_tags(self, **kw): def delete_servers_1234_tags_tag(self, **kw): return (204, {}, None) + def delete_servers_1234_tags_tag1(self, **kw): + return (204, {}, None) + + def delete_servers_1234_tags_tag2(self, **kw): + return (204, {}, None) + + def delete_servers_1234_tags_tag3(self, **kw): + return (204, {}, None) + def delete_servers_1234_tags(self, **kw): return (204, {}, None) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index bb01344aa..45b135f5e 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -3329,6 +3329,13 @@ def test_server_tag_add(self): api_version='2.26') self.assert_called('PUT', '/servers/1234/tags/tag', None) + def test_server_tag_add_many(self): + self.run_command('server-tag-add sample-server tag1 tag2 tag3', + api_version='2.26') + self.assert_called('PUT', '/servers/1234/tags/tag1', None, pos=-3) + self.assert_called('PUT', '/servers/1234/tags/tag2', None, pos=-2) + self.assert_called('PUT', '/servers/1234/tags/tag3', None, pos=-1) + def test_server_tag_set(self): self.run_command('server-tag-set sample-server tag1 tag2', api_version='2.26') @@ -3344,6 +3351,13 @@ def test_server_tag_delete(self): api_version='2.26') self.assert_called('DELETE', '/servers/1234/tags/tag') + def test_server_tag_delete_many(self): + self.run_command('server-tag-delete sample-server tag1 tag2 tag3', + api_version='2.26') + self.assert_called('DELETE', '/servers/1234/tags/tag1', pos=-3) + self.assert_called('DELETE', '/servers/1234/tags/tag2', pos=-2) + self.assert_called('DELETE', '/servers/1234/tags/tag3', pos=-1) + def test_server_tag_delete_all(self): self.run_command('server-tag-delete-all sample-server', api_version='2.26') diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 055559a44..1acb4dce7 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -5283,11 +5283,15 @@ def do_server_tag_list(cs, args): @api_versions.wraps("2.26") @utils.arg('server', metavar='', help=_('Name or ID of server.')) -@utils.arg('tag', metavar='', help=_('Tag to add.')) +@utils.arg('tag', metavar='', nargs='+', help=_('Tag(s) to add.')) def do_server_tag_add(cs, args): - """Add single tag to a server.""" + """Add one or more tags to a server.""" server = _find_server(cs, args.server) - server.add_tag(args.tag) + utils.do_action_on_many( + lambda t: server.add_tag(t), + args.tag, + _("Request to add tag %s to specified server has been accepted."), + _("Unable to add tag %s to the specified server.")) @api_versions.wraps("2.26") @@ -5301,11 +5305,15 @@ def do_server_tag_set(cs, args): @api_versions.wraps("2.26") @utils.arg('server', metavar='', help=_('Name or ID of server.')) -@utils.arg('tag', metavar='', help=_('Tag to delete.')) +@utils.arg('tag', metavar='', nargs='+', help=_('Tag(s) to delete.')) def do_server_tag_delete(cs, args): - """Delete single tag from a server.""" + """Delete one or more tags from a server.""" server = _find_server(cs, args.server) - server.delete_tag(args.tag) + utils.do_action_on_many( + lambda t: server.delete_tag(t), + args.tag, + _("Request to delete tag %s from specified server has been accepted."), + _("Unable to delete tag %s from specified server.")) @api_versions.wraps("2.26") From 2512a28dc1f9a017b996c648ecd2551162ef1fc3 Mon Sep 17 00:00:00 2001 From: huangtianhua Date: Thu, 22 Dec 2016 10:31:29 +0800 Subject: [PATCH 1224/1705] Make _console() public Heat wants to use 'novaclient.v2.servers. ServerManager._console' method to simplify the implementation of retrieving server console. It's better to change the method to public for public use. The patch changes: 1. add public method named get_console_url() for class 'novaclient.v2.servers.Server' 2. rename _console() to get_console_url() for class 'novaclient.v2.servers.ServerManager' Change-Id: I3d1485468d1d0a79d4002981ebe05b6cdf2332e7 Closes-Bug: #1651677 --- novaclient/exceptions.py | 8 ++ novaclient/tests/unit/v2/test_servers.py | 101 ++++++++++++++--- novaclient/v2/servers.py | 106 +++++++++++++----- .../make-console-public-0c776bfda240cd9d.yaml | 7 ++ 4 files changed, 180 insertions(+), 42 deletions(-) create mode 100644 releasenotes/notes/make-console-public-0c776bfda240cd9d.yaml diff --git a/novaclient/exceptions.py b/novaclient/exceptions.py index d23df8ab2..707aa889e 100644 --- a/novaclient/exceptions.py +++ b/novaclient/exceptions.py @@ -24,6 +24,14 @@ class UnsupportedVersion(Exception): pass +class UnsupportedConsoleType(Exception): + """Indicates that the user is trying to use an unsupported + console type when retrieving console urls of servers. + """ + def __init__(self, console_type): + self.message = 'Unsupported console_type "%s"' % console_type + + class UnsupportedAttribute(AttributeError): """Indicates that the user is trying to transmit the argument to a method, which is not supported by selected version. diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index 1d8a36fe8..a309d558a 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -794,44 +794,59 @@ def test_get_server_diagnostics(self): def test_get_vnc_console(self): s = self.cs.servers.get(1234) - vc = s.get_vnc_console('fake') + vc = s.get_vnc_console('novnc') self.assert_request_id(vc, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') - vc = self.cs.servers.get_vnc_console(s, 'fake') + vc = self.cs.servers.get_vnc_console(s, 'novnc') self.assert_request_id(vc, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') def test_get_spice_console(self): s = self.cs.servers.get(1234) - sc = s.get_spice_console('fake') + sc = s.get_spice_console('spice-html5') self.assert_request_id(sc, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') - sc = self.cs.servers.get_spice_console(s, 'fake') + sc = self.cs.servers.get_spice_console(s, 'spice-html5') self.assert_request_id(sc, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') def test_get_serial_console(self): s = self.cs.servers.get(1234) - sc = s.get_serial_console('fake') + sc = s.get_serial_console('serial') self.assert_request_id(sc, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') - sc = self.cs.servers.get_serial_console(s, 'fake') + sc = self.cs.servers.get_serial_console(s, 'serial') self.assert_request_id(sc, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') def test_get_rdp_console(self): s = self.cs.servers.get(1234) - rc = s.get_rdp_console('fake') + rc = s.get_rdp_console('rdp-html5') self.assert_request_id(rc, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') - rc = self.cs.servers.get_rdp_console(s, 'fake') + rc = self.cs.servers.get_rdp_console(s, 'rdp-html5') self.assert_request_id(rc, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') + def test_get_console_url(self): + s = self.cs.servers.get(1234) + rc = s.get_console_url('novnc') + self.assert_request_id(rc, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + + rc = self.cs.servers.get_console_url(s, 'novnc') + self.assert_request_id(rc, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + + # test the case with invalid console type + self.assertRaises(exceptions.UnsupportedConsoleType, + s.get_console_url, + 'invalid') + def test_create_image(self): s = self.cs.servers.get(1234) im = s.create_image('123') @@ -1001,44 +1016,83 @@ class ServersV26Test(ServersTest): def test_get_vnc_console(self): s = self.cs.servers.get(1234) - vc = s.get_vnc_console('fake') + vc = s.get_vnc_console('novnc') self.assert_request_id(vc, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/remote-consoles') - vc = self.cs.servers.get_vnc_console(s, 'fake') + vc = self.cs.servers.get_vnc_console(s, 'novnc') self.assert_request_id(vc, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/remote-consoles') + # test the case with invalid console type + self.assertRaises(exceptions.UnsupportedConsoleType, + s.get_vnc_console, + 'invalid') + def test_get_spice_console(self): s = self.cs.servers.get(1234) - sc = s.get_spice_console('fake') + sc = s.get_spice_console('spice-html5') self.assert_request_id(sc, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/remote-consoles') - sc = self.cs.servers.get_spice_console(s, 'fake') + sc = self.cs.servers.get_spice_console(s, 'spice-html5') self.assert_request_id(sc, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/remote-consoles') + # test the case with invalid console type + self.assertRaises(exceptions.UnsupportedConsoleType, + s.get_spice_console, + 'invalid') + def test_get_serial_console(self): s = self.cs.servers.get(1234) - sc = s.get_serial_console('fake') + sc = s.get_serial_console('serial') self.assert_request_id(sc, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/remote-consoles') - sc = self.cs.servers.get_serial_console(s, 'fake') + sc = self.cs.servers.get_serial_console(s, 'serial') self.assert_request_id(sc, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/remote-consoles') + # test the case with invalid console type + self.assertRaises(exceptions.UnsupportedConsoleType, + s.get_serial_console, + 'invalid') + def test_get_rdp_console(self): s = self.cs.servers.get(1234) - rc = s.get_rdp_console('fake') + rc = s.get_rdp_console('rdp-html5') self.assert_request_id(rc, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/remote-consoles') - rc = self.cs.servers.get_rdp_console(s, 'fake') + rc = self.cs.servers.get_rdp_console(s, 'rdp-html5') self.assert_request_id(rc, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/remote-consoles') + # test the case with invalid console type + self.assertRaises(exceptions.UnsupportedConsoleType, + s.get_rdp_console, + 'invalid') + + def test_get_console_url(self): + s = self.cs.servers.get(1234) + vc = s.get_console_url('novnc') + self.assert_request_id(vc, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/remote-consoles') + + vc = self.cs.servers.get_console_url(s, 'novnc') + self.assert_request_id(vc, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/remote-consoles') + + # test the case with invalid console type + self.assertRaises(exceptions.UnsupportedConsoleType, + s.get_console_url, + 'invalid') + # console type webmks is supported since api version 2.8 + self.assertRaises(exceptions.UnsupportedConsoleType, + s.get_console_url, + 'webmks') + class ServersV28Test(ServersV26Test): @@ -1054,6 +1108,21 @@ def test_get_mks_console(self): self.assert_request_id(mksc, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/remote-consoles') + def test_get_console_url(self): + s = self.cs.servers.get(1234) + mksc = s.get_console_url('novnc') + self.assert_request_id(mksc, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/remote-consoles') + + mksc = self.cs.servers.get_console_url(s, 'novnc') + self.assert_request_id(mksc, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/remote-consoles') + + # test the case with invalid console type + self.assertRaises(exceptions.UnsupportedConsoleType, + s.get_console_url, + 'invalid') + class ServersV214Test(ServersV28Test): diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index ee828fdcf..460355630 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -35,6 +35,23 @@ REBOOT_SOFT, REBOOT_HARD = 'SOFT', 'HARD' +CONSOLE_TYPE_ACTION_MAPPING = { + 'novnc': 'os-getVNCConsole', + 'xvpvnc': 'os-getVNCConsole', + 'spice-html5': 'os-getSPICEConsole', + 'rdp-html5': 'os-getRDPConsole', + 'serial': 'os-getSerialConsole' +} + +CONSOLE_TYPE_PROTOCOL_MAPPING = { + 'novnc': 'vnc', + 'xvpvnc': 'vnc', + 'spice-html5': 'spice', + 'rdp-html5': 'rdp', + 'serial': 'serial', + 'webmks': 'mks' +} + class Server(base.Resource): HUMAN_ID = True @@ -121,6 +138,15 @@ def get_mks_console(self): """ return self.manager.get_mks_console(self) + def get_console_url(self, console_type): + """ + Retrieve a console of a particular protocol and console_type + + :param console_type: Type of console + """ + + return self.manager.get_console_url(self, console_type) + def get_password(self, private_key=None): """ Get password for a Server. @@ -904,7 +930,7 @@ def get_vnc_console(self, server, console_type): :returns: An instance of novaclient.base.DictWithMeta """ - return self._action('os-getVNCConsole', server, {'type': console_type}) + return self.get_console_url(server, console_type) @api_versions.wraps('2.0', '2.5') def get_spice_console(self, server, console_type): @@ -916,8 +942,7 @@ def get_spice_console(self, server, console_type): :returns: An instance of novaclient.base.DictWithMeta """ - return self._action('os-getSPICEConsole', server, - {'type': console_type}) + return self.get_console_url(server, console_type) @api_versions.wraps('2.0', '2.5') def get_rdp_console(self, server, console_type): @@ -929,8 +954,7 @@ def get_rdp_console(self, server, console_type): :returns: An instance of novaclient.base.DictWithMeta """ - return self._action('os-getRDPConsole', server, - {'type': console_type}) + return self.get_console_url(server, console_type) @api_versions.wraps('2.0', '2.5') def get_serial_console(self, server, console_type): @@ -942,8 +966,29 @@ def get_serial_console(self, server, console_type): :returns: An instance of novaclient.base.DictWithMeta """ - return self._action('os-getSerialConsole', server, - {'type': console_type}) + return self.get_console_url(server, console_type) + + def _get_protocol(self, console_type): + protocol = CONSOLE_TYPE_PROTOCOL_MAPPING.get(console_type) + if not protocol: + raise exceptions.UnsupportedConsoleType(console_type) + + return protocol + + @api_versions.wraps('2.0', '2.5') + def get_console_url(self, server, console_type): + """ + Retrieve a console url of a server. + + :param server: server to get console url for + :param console_type: type can be novnc, xvpvnc, spice-html5, + rdp-html5 and serial. + """ + + action = CONSOLE_TYPE_ACTION_MAPPING.get(console_type) + if not action: + raise exceptions.UnsupportedConsoleType(console_type) + return self._action(action, server, {'type': console_type}) @api_versions.wraps('2.6') def get_vnc_console(self, server, console_type): @@ -955,8 +1000,7 @@ def get_vnc_console(self, server, console_type): :returns: An instance of novaclient.base.DictWithMeta """ - return self._console(server, - {'protocol': 'vnc', 'type': console_type}) + return self.get_console_url(server, console_type) @api_versions.wraps('2.6') def get_spice_console(self, server, console_type): @@ -968,8 +1012,7 @@ def get_spice_console(self, server, console_type): :returns: An instance of novaclient.base.DictWithMeta """ - return self._console(server, - {'protocol': 'spice', 'type': console_type}) + return self.get_console_url(server, console_type) @api_versions.wraps('2.6') def get_rdp_console(self, server, console_type): @@ -981,8 +1024,7 @@ def get_rdp_console(self, server, console_type): :returns: An instance of novaclient.base.DictWithMeta """ - return self._console(server, - {'protocol': 'rdp', 'type': console_type}) + return self.get_console_url(server, console_type) @api_versions.wraps('2.6') def get_serial_console(self, server, console_type): @@ -994,8 +1036,7 @@ def get_serial_console(self, server, console_type): :returns: An instance of novaclient.base.DictWithMeta """ - return self._console(server, - {'protocol': 'serial', 'type': console_type}) + return self.get_console_url(server, console_type) @api_versions.wraps('2.8') def get_mks_console(self, server): @@ -1006,8 +1047,30 @@ def get_mks_console(self, server): :returns: An instance of novaclient.base.DictWithMeta """ - return self._console(server, - {'protocol': 'mks', 'type': 'webmks'}) + return self.get_console_url(server, 'webmks') + + @api_versions.wraps('2.6') + def get_console_url(self, server, console_type): + """ + Retrieve a console url of a server. + + :param server: server to get console url for + :param console_type: type can be novnc/xvpvnc for protocol vnc; + spice-html5 for protocol spice; rdp-html5 for + protocol rdp; serial for protocol serial. + webmks for protocol mks (since version 2.8). + """ + + if self.api_version < api_versions.APIVersion('2.8'): + if console_type == 'webmks': + raise exceptions.UnsupportedConsoleType(console_type) + + protocol = self._get_protocol(console_type) + body = {'remote_console': {'protocol': protocol, + 'type': console_type}} + url = '/servers/%s/remote-consoles' % base.getid(server) + resp, body = self.api.client.post(url, body=body) + return self.convert_into_with_meta(body, resp) def get_password(self, server, private_key=None): """ @@ -1855,15 +1918,6 @@ def _action_return_resp_and_body(self, action, server, info=None, url = '/servers/%s/action' % base.getid(server) return self.api.client.post(url, body=body) - def _console(self, server, info=None, **kwargs): - """ - Retrieve a console of a particular protocol -- vnc/spice/rdp/serial - """ - body = {'remote_console': info} - url = '/servers/%s/remote-consoles' % base.getid(server) - resp, body = self.api.client.post(url, body=body) - return self.convert_into_with_meta(body, resp) - @api_versions.wraps('2.26') def tag_list(self, server): """ diff --git a/releasenotes/notes/make-console-public-0c776bfda240cd9d.yaml b/releasenotes/notes/make-console-public-0c776bfda240cd9d.yaml new file mode 100644 index 000000000..e8743f67a --- /dev/null +++ b/releasenotes/notes/make-console-public-0c776bfda240cd9d.yaml @@ -0,0 +1,7 @@ +--- +features: + - Provides a public unified interface 'get_console_url' for classes + 'novaclient.v2.servers.Server' and 'novaclient.v2.servers.ServerManager'. + Users (Heat, OpenStack-client and etc.) can call this public interface + instead of calling the individual methods to retrieve a console url + of a particular protocol. From 6ba28821bc78270c2c704e20589a1070596ff688 Mon Sep 17 00:00:00 2001 From: Andreas Jaeger Date: Wed, 18 Jan 2017 13:38:57 +0100 Subject: [PATCH 1225/1705] Fix help strings Some help strings miss spaces and thus get displayed like "willbe" or "biggerthan" - add missing spaces. Change-Id: I560738dd46b3088a6e528a1a24fb2dbea495dcc7 --- novaclient/v2/shell.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 055559a44..ae36e4d65 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -1629,7 +1629,7 @@ def do_image_delete(cs, args): dest='not-tags', metavar='', default=None, - help=_("Only the servers that do not have any of the given tags will" + help=_("Only the servers that do not have any of the given tags will " "be included in the list results. Boolean expression in this case " "is 'NOT(t1 AND t2)'. Tags must be separated by commas: " "--not-tags "), @@ -1639,7 +1639,7 @@ def do_image_delete(cs, args): dest='not-tags-any', metavar='', default=None, - help=_("Only the servers that do not have at least one of the given tags" + help=_("Only the servers that do not have at least one of the given tags " "will be included in the list result. Boolean expression in this " "case is 'NOT(t1 OR t2)'. Tags must be separated by commas: " "--not-tags-any "), @@ -5097,8 +5097,8 @@ def _print_server_group_details(cs, server_group): # noqa metavar='', type=int, default=None, - help=_("Maximum number of server groups to display. If limit is bigger" - "than 'CONF.api.max_limit' option of Nova API, limit" + help=_("Maximum number of server groups to display. If limit is bigger " + "than 'CONF.api.max_limit' option of Nova API, limit " "'CONF.api.max_limit' will be used instead.")) @utils.arg( '--offset', From 0fed79fd8fc84b6a1564fbb70655cdebc543381d Mon Sep 17 00:00:00 2001 From: Boris Pavlovic Date: Tue, 8 Dec 2015 14:17:45 +0300 Subject: [PATCH 1226/1705] Add profiling support to novaclient To be able to create profiling traces for Nova, client should be able to send special HTTP header that contains trace info. This patch is also important to be able to make cross project traces. (Typical case heat calls nova via python client, if profiler is initialized in heat, nova client will add extra header, that will be parsed by special osprofiler middleware in nova api.) Security considerations: trace information is signed by one of the HMAC keys that are set in nova.conf. So only person who knows HMAC key is able to send proper header. oslo-spec: https://review.openstack.org/#/c/103825/ Based on: https://review.openstack.org/#/c/105089/ Co-Authored-By: Dina Belova Co-Authored-By: Roman Podoliaka Co-Authored-By: Tovin Seven Partially implements: blueprint osprofiler-support-in-nova Depends-On: I82d2badc8c1fcec27c3fce7c3c20e0f3b76414f1 Change-Id: I56ce4b547230e475854994c9d2249ef90e5b656c --- novaclient/client.py | 20 ++++++++++++++ novaclient/shell.py | 27 ++++++++++++++++++- novaclient/tests/unit/fixture_data/client.py | 12 ++++++--- novaclient/tests/unit/test_shell.py | 21 +++++++++++++++ ...d-osprofiler-support-cc9dd228242e9919.yaml | 25 +++++++++++++++++ test-requirements.txt | 1 + 6 files changed, 101 insertions(+), 5 deletions(-) create mode 100644 releasenotes/notes/add-osprofiler-support-cc9dd228242e9919.yaml diff --git a/novaclient/client.py b/novaclient/client.py index 06f68e366..70645b5ec 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -30,6 +30,9 @@ from oslo_utils import importutils import pkg_resources +osprofiler_profiler = importutils.try_import("osprofiler.profiler") +osprofiler_web = importutils.try_import("osprofiler.web") + from novaclient import api_versions from novaclient import exceptions from novaclient import extension as ext @@ -67,6 +70,12 @@ def __init__(self, *args, **kwargs): def request(self, url, method, **kwargs): kwargs.setdefault('headers', kwargs.get('headers', {})) api_versions.update_headers(kwargs["headers"], self.api_version) + + # NOTE(dbelova): osprofiler_web.get_trace_id_headers does not add any + # headers in case if osprofiler is not initialized. + if osprofiler_web: + kwargs['headers'].update(osprofiler_web.get_trace_id_headers()) + # NOTE(jamielennox): The standard call raises errors from # keystoneauth1, where we need to raise the novaclient errors. raise_exc = kwargs.pop('raise_exc', True) @@ -343,6 +352,17 @@ def Client(version, username=None, password=None, project_id=None, api_version, client_class = _get_client_class_and_version(version) kwargs.pop("direct_use", None) + + profile = kwargs.pop("profile", None) + if osprofiler_profiler and profile: + # Initialize the root of the future trace: the created trace ID will + # be used as the very first parent to which all related traces will be + # bound to. The given HMAC key must correspond to the one set in + # nova-api nova.conf, otherwise the latter will fail to check the + # request signature and will skip initialization of osprofiler on + # the server side. + osprofiler_profiler.init(profile) + return client_class(api_version=api_version, auth_url=auth_url, direct_use=False, diff --git a/novaclient/shell.py b/novaclient/shell.py index 43953f960..6883c9926 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -30,6 +30,8 @@ from oslo_utils import strutils import six +osprofiler_profiler = importutils.try_import("osprofiler.profiler") + HAS_KEYRING = False all_errors = ValueError try: @@ -509,6 +511,19 @@ def get_base_parser(self, argv): dest='endpoint_override', help=argparse.SUPPRESS) + if osprofiler_profiler: + parser.add_argument('--profile', + metavar='HMAC_KEY', + help='HMAC key to use for encrypting context ' + 'data for performance profiling of operation. ' + 'This key should be the value of the HMAC key ' + 'configured for the OSprofiler middleware in ' + 'nova; it is specified in the Nova ' + 'configuration file at "/etc/nova/nova.conf". ' + 'Without the key, profiling will not be ' + 'triggered even if OSprofiler is enabled on ' + 'the server side.') + self._append_global_identity_args(parser, argv) return parser @@ -749,6 +764,10 @@ def main(self, argv): _("You must provide an auth url " "via either --os-auth-url or env[OS_AUTH_URL]")) + additional_kwargs = {} + if osprofiler_profiler: + additional_kwargs["profile"] = args.profile + # This client is just used to discover api version. Version API needn't # microversion, so we just pass version 2 at here. self.cs = client.Client( @@ -767,7 +786,8 @@ def main(self, argv): project_domain_id=os_project_domain_id, project_domain_name=os_project_domain_name, user_domain_id=os_user_domain_id, - user_domain_name=os_user_domain_name) + user_domain_name=os_user_domain_name, + **additional_kwargs) if not skip_auth: if not api_version.is_latest(): @@ -858,6 +878,11 @@ def main(self, argv): args.func(self.cs, args) + if osprofiler_profiler and args.profile: + trace_id = osprofiler_profiler.get().get_base_id() + print("To display trace use the command:\n\n" + " osprofiler trace show --html %s " % trace_id) + if args.timings: self._dump_timings(self.times + self.cs.get_timings()) diff --git a/novaclient/tests/unit/fixture_data/client.py b/novaclient/tests/unit/fixture_data/client.py index c37141095..d0ce3cc52 100644 --- a/novaclient/tests/unit/fixture_data/client.py +++ b/novaclient/tests/unit/fixture_data/client.py @@ -24,7 +24,8 @@ class V1(fixtures.Fixture): def __init__(self, requests_mock, - compute_url=COMPUTE_URL, identity_url=IDENTITY_URL): + compute_url=COMPUTE_URL, identity_url=IDENTITY_URL, + **client_kwargs): super(V1, self).__init__() self.identity_url = identity_url self.compute_url = compute_url @@ -41,6 +42,8 @@ def __init__(self, requests_mock, s = self.token.add_service('computev3') s.add_endpoint(self.compute_url) + self._client_kwargs = client_kwargs + def setUp(self): super(V1, self).setUp() @@ -52,13 +55,14 @@ def setUp(self): self.requests_mock.get(self.identity_url, json=self.discovery, headers=headers) - self.client = self.new_client() + self.client = self.new_client(**self._client_kwargs) - def new_client(self): + def new_client(self, **client_kwargs): return client.Client("2", username='xx', password='xx', project_id='xx', - auth_url=self.identity_url) + auth_url=self.identity_url, + **client_kwargs) class SessionV1(V1): diff --git a/novaclient/tests/unit/test_shell.py b/novaclient/tests/unit/test_shell.py index 161b45472..5ccf09bb5 100644 --- a/novaclient/tests/unit/test_shell.py +++ b/novaclient/tests/unit/test_shell.py @@ -655,6 +655,27 @@ def test_timing(self, m_times, m_requests): exc = self.assertRaises(RuntimeError, self.shell, '--timings list') self.assertEqual('Boom!', str(exc)) + @requests_mock.Mocker() + def test_osprofiler(self, m_requests): + self.make_env() + + def client(*args, **kwargs): + self.assertEqual('swordfish', kwargs['profile']) + with mock.patch('novaclient.client.Client', client): + # we are only interested in the fact Client is initialized properly + self.shell('list --profile swordfish', (0, 2)) + + @requests_mock.Mocker() + def test_osprofiler_not_installed(self, m_requests): + self.make_env() + + # NOTE(rpodolyaka): osprofiler is in test-requirements, so we have to + # simulate its absence here + with mock.patch('novaclient.shell.osprofiler_profiler', None): + _, stderr = self.shell('list --profile swordfish', (0, 2)) + self.assertIn('unrecognized arguments: --profile swordfish', + stderr) + @mock.patch('novaclient.shell.SecretsHelper.tenant_id', return_value=True) @mock.patch('novaclient.shell.SecretsHelper.auth_token', diff --git a/releasenotes/notes/add-osprofiler-support-cc9dd228242e9919.yaml b/releasenotes/notes/add-osprofiler-support-cc9dd228242e9919.yaml new file mode 100644 index 000000000..a956ae147 --- /dev/null +++ b/releasenotes/notes/add-osprofiler-support-cc9dd228242e9919.yaml @@ -0,0 +1,25 @@ +--- +prelude: > + OSprofiler support was added to the client. That makes + possible to trigger Nova operation trace generation from + the CLI. +features: + - A new ``--profile`` option was added to allow Nova + profiling from the CLI. If the user wishes to trace a + nova boot request he or she needs to type the following + command -- ``nova --profile boot --image + --flavor ``, where ``secret_key`` should match one + of the keys defined in nova.conf. As a result of this operation + additional information regarding ``trace_id`` will be + printed, that can be used to generate human-friendly + html report -- ``osprofiler trace show --html --out + trace.html``. + To enable profiling, user needs to have osprofiler + installed in the local environment via ``pip install osprofiler``. +security: + - OSprofiler support, that was added during the Ocata release cycle, + requires passing of trace information between various + OpenStack services. This information is signed by one of + the HMAC keys defined in nova.conf file. That means that + only someone who knows this key is able to send the proper + header to trigger profiling. diff --git a/test-requirements.txt b/test-requirements.txt index 97a350fab..a9c5a42d4 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -15,6 +15,7 @@ requests-mock>=1.1 # Apache-2.0 sphinx!=1.3b1,<1.4,>=1.2.1 # BSD os-client-config>=1.22.0 # Apache-2.0 oslosphinx>=4.7.0 # Apache-2.0 +osprofiler>=1.4.0 # Apache-2.0 testrepository>=0.0.18 # Apache-2.0/BSD testscenarios>=0.4 # Apache-2.0/BSD testtools>=1.4.0 # MIT From 98976d5ab8bf623e00fb56f5f956c696ecd62f81 Mon Sep 17 00:00:00 2001 From: Abhishek Kekane Date: Wed, 18 Jan 2017 13:52:38 +0530 Subject: [PATCH 1227/1705] x-openstack-request-id logged twice in logs In the recent release of keystoneauth1 2.18.0 provision is made to log x-openstack-request-id for session client. Once this new library is synced in openstack projects, the x-openstack-request-id will be logged twice on the console if session client is used. For example, $ nova --debug list DEBUG (session:640) GET call to compute for http://10.232.48.204:8774/v2.1/servers/detail used request id req-b6aeff56-0408-44ed-80ba-cd7b950a8f21 DEBUG (client:85) GET call to compute for http://10.232.48.204:8774/v2.1/servers/detail used request id req-b6aeff56-0408-44ed-80ba-cd7b950a8f21 Above log will be logged twice on the console. Removed logging of x-openstack-request-id in case of SessionClient as it is already logged in keystoneauth1. Depends-On: I492b331ff3da8d0b91178bf0d5fe1d3702f15bd7 Closes-Bug: #1657351 Change-Id: I9ba6a9e498fbcb027f45499d27ece966c52580ba --- novaclient/client.py | 16 ---------------- novaclient/tests/unit/test_client.py | 12 +----------- 2 files changed, 1 insertion(+), 27 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index 70645b5ec..75ecf3805 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -46,18 +46,6 @@ extensions_ignored_name = ["__init__"] -def _log_request_id(logger, resp, service_name): - request_id = (resp.headers.get('x-openstack-request-id') or - resp.headers.get('x-compute-request-id')) - if request_id: - logger.debug('%(method)s call to %(service_name)s for %(url)s ' - 'used request id %(response_request_id)s', - {'method': resp.request.method, - 'service_name': service_name, - 'url': resp.url, - 'response_request_id': request_id}) - - class SessionClient(adapter.LegacyJsonAdapter): def __init__(self, *args, **kwargs): @@ -85,10 +73,6 @@ def request(self, url, method, **kwargs): raise_exc=False, **kwargs) - # if service name is None then use service_type for logging - service = self.service_name or self.service_type - _log_request_id(self.logger, resp, service) - # TODO(andreykurilin): uncomment this line, when we will be able to # check only nova-related calls # api_versions.check_headers(resp, self.api_version) diff --git a/novaclient/tests/unit/test_client.py b/novaclient/tests/unit/test_client.py index ad2477632..4f739888a 100644 --- a/novaclient/tests/unit/test_client.py +++ b/novaclient/tests/unit/test_client.py @@ -51,8 +51,7 @@ def test_get_client_class_latest(self): class SessionClientTest(utils.TestCase): - @mock.patch.object(novaclient.client, '_log_request_id') - def test_timings(self, mock_log_request_id): + def test_timings(self): self.requests_mock.get('http://no.where') client = novaclient.client.SessionClient(session=session.Session()) @@ -75,15 +74,6 @@ def test_client_get_reset_timings_v2(self): cs.reset_timings() self.assertEqual(0, len(cs.get_timings())) - @mock.patch.object(novaclient.client, '_log_request_id') - def test_log_request_id(self, mock_log_request_id): - self.requests_mock.get('http://no.where') - client = novaclient.client.SessionClient(session=session.Session(), - service_name='compute') - client.request("http://no.where", 'GET') - mock_log_request_id.assert_called_once_with(client.logger, mock.ANY, - 'compute') - class ClientsUtilsTest(utils.TestCase): From dc3a06bc3b84abf720ee3a8e9ff1c70690fcc358 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Sat, 21 Jan 2017 15:56:51 +0000 Subject: [PATCH 1228/1705] Updated from global requirements Change-Id: I226c003ea438edd6d4dec6f0b135847381ad7d07 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 8b01acc15..bd75322c4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. pbr>=1.8 # Apache-2.0 -keystoneauth1>=2.17.0 # Apache-2.0 +keystoneauth1>=2.18.0 # Apache-2.0 iso8601>=0.1.11 # MIT oslo.i18n>=2.1.0 # Apache-2.0 oslo.serialization>=1.10.0 # Apache-2.0 From 9940e3fe0e47ff5f2f6d05c9564d17fc19ca0f5c Mon Sep 17 00:00:00 2001 From: Istvan Imre Date: Thu, 12 Jan 2017 13:18:25 +0100 Subject: [PATCH 1229/1705] Pass relevant parameters to Token based authentication In case of token authentication is used pass relevant parameters to Token authenticator. Co-Authored-By: Andrey Kurilin Change-Id: I9a04d89016a834fe96f1b77e91011f7fa4fdda51 Closes-Bug: #1654183 --- novaclient/client.py | 6 ++- novaclient/tests/functional/base.py | 6 +-- novaclient/tests/functional/test_auth.py | 50 +++++++++++++++++++----- 3 files changed, 48 insertions(+), 14 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index 75ecf3805..9ca9658c7 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -135,7 +135,11 @@ def _construct_http_client(api_version=None, if not session: if not auth and auth_token: auth = identity.Token(auth_url=auth_url, - token=auth_token) + token=auth_token, + project_id=project_id, + project_name=project_name, + project_domain_id=project_domain_id, + project_domain_name=project_domain_name) elif not auth: auth = identity.Password(username=username, user_id=user_id, diff --git a/novaclient/tests/functional/base.py b/novaclient/tests/functional/base.py index b1b2bf9ff..acc110132 100644 --- a/novaclient/tests/functional/base.py +++ b/novaclient/tests/functional/base.py @@ -193,7 +193,7 @@ def setUp(self): user = auth_info['username'] passwd = auth_info['password'] - tenant = auth_info['project_name'] + self.project_name = auth_info['project_name'] auth_url = auth_info['auth_url'] user_domain_id = auth_info['user_domain_id'] self.project_domain_id = auth_info['project_domain_id'] @@ -205,7 +205,7 @@ def setUp(self): auth = identity.Password(username=user, password=passwd, - project_name=tenant, + project_name=self.project_name, auth_url=auth_url, project_domain_id=self.project_domain_id, user_domain_id=user_domain_id) @@ -247,7 +247,7 @@ def setUp(self): self.cli_clients = tempest.lib.cli.base.CLIClient( username=user, password=passwd, - tenant_name=tenant, + tenant_name=self.project_name, uri=auth_url, cli_dir=cli_dir, insecure=self.insecure) diff --git a/novaclient/tests/functional/test_auth.py b/novaclient/tests/functional/test_auth.py index 760e62e92..9f645c334 100644 --- a/novaclient/tests/functional/test_auth.py +++ b/novaclient/tests/functional/test_auth.py @@ -13,40 +13,70 @@ from six.moves.urllib import parse import tempest.lib.cli.base +from novaclient import client from novaclient.tests.functional import base class TestAuthentication(base.ClientTestBase): - def nova(self, action, identity_api_version): + + def _get_url(self, identity_api_version): url = parse.urlparse(self.cli_clients.uri) - url = parse.urlunparse((url.scheme, url.netloc, - '/identity/v%s' % identity_api_version, - url.params, url.query, - url.fragment)) + return parse.urlunparse((url.scheme, url.netloc, + '/identity/v%s' % identity_api_version, + url.params, url.query, + url.fragment)) + + def nova_auth_with_password(self, action, identity_api_version): flags = ('--os-username %s --os-tenant-name %s --os-password %s ' '--os-auth-url %s --os-endpoint-type publicURL' % ( self.cli_clients.username, self.cli_clients.tenant_name, self.cli_clients.password, - url)) + self._get_url(identity_api_version))) if self.cli_clients.insecure: flags += ' --insecure ' return tempest.lib.cli.base.execute( "nova", action, flags, cli_dir=self.cli_clients.cli_dir) + def nova_auth_with_token(self, identity_api_version): + auth_ref = self.client.client.session.auth.get_access( + self.client.client.session) + token = auth_ref.auth_token + auth_url = self._get_url(identity_api_version) + kw = {} + if identity_api_version == "3": + kw["project_domain_id"] = self.project_domain_id + nova = client.Client("2", auth_token=token, auth_url=auth_url, + project_name=self.project_name, **kw) + nova.servers.list() + + # NOTE(andreykurilin): token auth is completely broken in CLI + # flags = ('--os-username %s --os-tenant-name %s --os-auth-token %s ' + # '--os-auth-url %s --os-endpoint-type publicURL' % ( + # self.cli_clients.username, + # self.cli_clients.tenant_name, + # token, auth_url)) + # if self.cli_clients.insecure: + # flags += ' --insecure ' + # + # return tempest.lib.cli.base.execute( + # "nova", action, flags, cli_dir=self.cli_clients.cli_dir) + def test_auth_via_keystone_v2(self): session = self.keystone.session version = (2, 0) if not base.is_keystone_version_available(session, version): - self.skip("Identity API version 2.0 is not available.") + self.skipTest("Identity API version 2.0 is not available.") - self.nova("list", identity_api_version="2.0") + self.nova_auth_with_password("list", identity_api_version="2.0") + self.nova_auth_with_token(identity_api_version="2.0") def test_auth_via_keystone_v3(self): session = self.keystone.session version = (3, 0) if not base.is_keystone_version_available(session, version): - self.skip("Identity API version 3.0 is not available.") + self.skipTest("Identity API version 3.0 is not available.") - self.nova("list", identity_api_version="3") + self.nova_auth_with_password("list", identity_api_version="3") + self.nova_auth_with_token(identity_api_version="3") From 1dcd83012be91a40bab1081d91261a9643b17e63 Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Tue, 24 Jan 2017 11:31:07 +0100 Subject: [PATCH 1230/1705] ListExtResource given in place of ListExtManager This patch makes it use ListExtManager again. Change-Id: I00ef7107cdf027c747806c4e5037c05b60312e21 Closes-bug: #1658963 --- novaclient/v2/contrib/list_extensions.py | 2 +- ...iven-in-place-of-ListExtManager-a759a27079d16a44.yaml | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/fixed-ListExtResource-given-in-place-of-ListExtManager-a759a27079d16a44.yaml diff --git a/novaclient/v2/contrib/list_extensions.py b/novaclient/v2/contrib/list_extensions.py index 9177fa0cd..07f4e3d7f 100644 --- a/novaclient/v2/contrib/list_extensions.py +++ b/novaclient/v2/contrib/list_extensions.py @@ -18,6 +18,6 @@ ListExtResource = list_extensions.ListExtResource -ListExtManager = list_extensions.ListExtResource +ListExtManager = list_extensions.ListExtManager contrib.warn() diff --git a/releasenotes/notes/fixed-ListExtResource-given-in-place-of-ListExtManager-a759a27079d16a44.yaml b/releasenotes/notes/fixed-ListExtResource-given-in-place-of-ListExtManager-a759a27079d16a44.yaml new file mode 100644 index 000000000..6491caecc --- /dev/null +++ b/releasenotes/notes/fixed-ListExtResource-given-in-place-of-ListExtManager-a759a27079d16a44.yaml @@ -0,0 +1,9 @@ +--- +fixes: + - | + The contents of the list_extensions.py file was moved from contrib to v2 + directory in release 7.0.0, and a stub importing the objects from the new + location was left in its place for backward compatibility, together with + a warning informing about the new location. However, the stub incorrectly + assigned the ListExtResource class to the ListExtManager name. This has + now been fixed, and ListExtManager is used instead. From ae699768023bb0a8d60798eca7f0cc8aebfe3355 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Mon, 9 Jan 2017 22:09:22 -0500 Subject: [PATCH 1231/1705] Fix functional tests to deal with multiple networks There was a change in the openstacksdk 0.9.11 which python-openstackclient uses to create networks in a devstack run with Neutron. Because of this change, the admin tenant has access to both the 'public' and 'private' network setup in devstack which before used to just be the 'public' network, which exposed a bug in the novaclient functional testing where the admin user is attempting to create a server but not specify a specific network to use, but multiple networks are available to the admin (it can list multiple networks). In this case the networks are the standard public and private networks that are created in devstack. Since a network isn't specified when creating the server, the nova API fails with a 409 error because it can't determine which network to use. This patch fixes the testing in novaclient by checking to see if there are multiple networks available and if so, specifies one for the legacy BDM tests that weren't specifying a network ID (those tests don't really care about the networking). The auto-network test is skipped if there are multiple networks available because passing in a specific network would defeat the purpose of that test. Change-Id: I22ee148581a94b153cf7e733563cfafaa56b1ffd Closes-Bug: #1654806 --- novaclient/tests/functional/base.py | 9 ++++++++- novaclient/tests/functional/v2/legacy/test_servers.py | 8 ++++++-- novaclient/tests/functional/v2/test_servers.py | 6 ++++++ 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/novaclient/tests/functional/base.py b/novaclient/tests/functional/base.py index acc110132..f87298d4f 100644 --- a/novaclient/tests/functional/base.py +++ b/novaclient/tests/functional/base.py @@ -230,10 +230,17 @@ def setUp(self): self.client.api_version = proxy_api_version try: # TODO(mriedem): Get the networks from neutron if using neutron - CACHE["network"] = pick_network(self.client.networks.list()) + networks = self.client.networks.list() + # Keep track of whether or not there are multiple networks + # available to the given tenant because if so, a specific + # network ID has to be passed in on server create requests + # otherwise the server POST will fail with a 409. + CACHE['multiple_networks'] = len(networks) > 1 + CACHE["network"] = pick_network(networks) finally: self.client.api_version = tested_api_version self.network = CACHE["network"] + self.multiple_networks = CACHE['multiple_networks'] # create a CLI client in case we'd like to do CLI # testing. tempest.lib does this really weird thing where it diff --git a/novaclient/tests/functional/v2/legacy/test_servers.py b/novaclient/tests/functional/v2/legacy/test_servers.py index cfc3939a0..ad773f1aa 100644 --- a/novaclient/tests/functional/v2/legacy/test_servers.py +++ b/novaclient/tests/functional/v2/legacy/test_servers.py @@ -35,13 +35,17 @@ def _boot_server_with_legacy_bdm(self, bdm_params=()): if bdm_params: bdm_params = ''.join((':', bdm_params)) - server_info = self.nova("boot", params=( + params = ( "%(name)s --flavor %(flavor)s --poll " "--block-device-mapping vda=%(volume_id)s%(bdm_params)s" % { "name": uuidutils.generate_uuid(), "flavor": self.flavor.id, "volume_id": volume.id, - "bdm_params": bdm_params})) + "bdm_params": bdm_params}) + # check to see if we have to pass in a network id + if self.multiple_networks: + params += ' --nic net-id=%s' % self.network.id + server_info = self.nova("boot", params=params) server_id = self._get_value_from_the_table(server_info, "id") self.client.servers.delete(server_id) diff --git a/novaclient/tests/functional/v2/test_servers.py b/novaclient/tests/functional/v2/test_servers.py index 31b2ae06d..6adf1e052 100644 --- a/novaclient/tests/functional/v2/test_servers.py +++ b/novaclient/tests/functional/v2/test_servers.py @@ -205,6 +205,12 @@ def _find_network_in_table(self, table): def test_boot_server_with_auto_network(self): """Tests that the CLI defaults to 'auto' when --nic isn't specified. """ + # check to see if multiple networks are available because if so we + # have to skip this test as auto will fail with a 409 conflict as it's + # an ambiguous request and nova won't know which network to pick + if self.multiple_networks: + # we could potentially get around this by extending TenantTestBase + self.skipTest('multiple networks available') server_info = self.nova('boot', params=( '%(name)s --flavor %(flavor)s --poll ' '--image %(image)s ' % {'name': self.name_generate('server'), From 2b2af1c7712b6c3bc96082b1b8f06ee20d39f03a Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Thu, 26 Jan 2017 20:28:22 +0200 Subject: [PATCH 1232/1705] Add release not for fixing token auth method Change-Id: I0fade88d7c58f4688285ce694ab1258f2ff13c9e --- releasenotes/notes/fix-token-auth-6c48c63a759f51d5.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 releasenotes/notes/fix-token-auth-6c48c63a759f51d5.yaml diff --git a/releasenotes/notes/fix-token-auth-6c48c63a759f51d5.yaml b/releasenotes/notes/fix-token-auth-6c48c63a759f51d5.yaml new file mode 100644 index 000000000..29487586f --- /dev/null +++ b/releasenotes/notes/fix-token-auth-6c48c63a759f51d5.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - Fix an ability to authenticate using Keystone Token which was broken with + novaclient 7.0.0 release. + From 059e5b8e8d773094d6e6a56a416224857422ce76 Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Tue, 31 Jan 2017 18:25:53 +0000 Subject: [PATCH 1233/1705] Update reno for stable/ocata Change-Id: Ie163d138f986b6c85e63fae8ee296d81a7d860bc --- releasenotes/source/index.rst | 1 + releasenotes/source/ocata.rst | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 releasenotes/source/ocata.rst diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index 896ac5545..9a3636472 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -8,6 +8,7 @@ Contents :maxdepth: 2 unreleased + ocata newton mitaka liberty diff --git a/releasenotes/source/ocata.rst b/releasenotes/source/ocata.rst new file mode 100644 index 000000000..ebe62f42e --- /dev/null +++ b/releasenotes/source/ocata.rst @@ -0,0 +1,6 @@ +=================================== + Ocata Series Release Notes +=================================== + +.. release-notes:: + :branch: origin/stable/ocata From 814ae2986fa5f5d4ef62b3a2da06aa07c3cf1574 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 10 Feb 2017 05:59:16 +0000 Subject: [PATCH 1234/1705] Updated from global requirements Change-Id: I77e91257dcfbe30f8c9d6937038d0dd718c1dfcd --- test-requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test-requirements.txt b/test-requirements.txt index a9c5a42d4..77d4df890 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -12,14 +12,14 @@ python-keystoneclient>=3.8.0 # Apache-2.0 python-cinderclient!=1.7.0,!=1.7.1,>=1.6.0 # Apache-2.0 python-glanceclient>=2.5.0 # Apache-2.0 requests-mock>=1.1 # Apache-2.0 -sphinx!=1.3b1,<1.4,>=1.2.1 # BSD +sphinx>=1.5.1 # BSD os-client-config>=1.22.0 # Apache-2.0 oslosphinx>=4.7.0 # Apache-2.0 osprofiler>=1.4.0 # Apache-2.0 testrepository>=0.0.18 # Apache-2.0/BSD testscenarios>=0.4 # Apache-2.0/BSD testtools>=1.4.0 # MIT -tempest>=12.1.0 # Apache-2.0 +tempest>=14.0.0 # Apache-2.0 # releasenotes reno>=1.8.0 # Apache-2.0 From 540f9ceb461e4ad55b83e873419f46d5b75bcac8 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Sat, 11 Feb 2017 17:51:53 +0000 Subject: [PATCH 1235/1705] Updated from global requirements Change-Id: I4d6f4532055172f1c5ba5ff47c0a216890a48480 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index bd75322c4..057327712 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ oslo.i18n>=2.1.0 # Apache-2.0 oslo.serialization>=1.10.0 # Apache-2.0 oslo.utils>=3.18.0 # Apache-2.0 PrettyTable<0.8,>=0.7.1 # BSD -requests!=2.12.2,>=2.10.0 # Apache-2.0 +requests!=2.12.2,!=2.13.0,>=2.10.0 # Apache-2.0 simplejson>=2.2.0 # MIT six>=1.9.0 # MIT Babel>=2.3.4 # BSD From e7df023d318ace67b9cd47689fd4851585a6be03 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Tue, 21 Feb 2017 14:25:12 -0500 Subject: [PATCH 1236/1705] Add functional test for "nova boot --image-with" We have no functional testing for creating a server using the --image-with option, which is going to be impacted when we remove our deprecated code for the image proxy API in Nova. This adds a functional test for that flow which creates a server, snapshots the server with a meta key/value, then creates a second server using --image-with and specifying the same meta key/value set on the snapshot image. Change-Id: I30017380d3e8dba6f1a50dfdda5eb4802b498bb4 --- novaclient/tests/functional/base.py | 8 +++- .../functional/v2/legacy/test_servers.py | 41 +++++++++++++++++++ .../tests/functional/v2/test_servers.py | 14 +++++++ 3 files changed, 62 insertions(+), 1 deletion(-) diff --git a/novaclient/tests/functional/base.py b/novaclient/tests/functional/base.py index f87298d4f..864321c30 100644 --- a/novaclient/tests/functional/base.py +++ b/novaclient/tests/functional/base.py @@ -408,7 +408,8 @@ def _get_value_from_the_table(self, table, key): l_property, l_value = line.split("|")[1:3] if l_property.strip() == key: return l_value.strip() - raise ValueError("Property '%s' is missing from the table." % key) + raise ValueError("Property '%s' is missing from the table:\n%s" % + (key, table)) def _get_column_value_from_single_row_table(self, table, column): """Get the value for the column in the single-row table @@ -503,6 +504,11 @@ def skip_if_neutron(self): if CACHE["use_neutron"]: self.skipTest('nova-network is not available') + def _cleanup_server(self, server_id): + """Deletes a server and waits for it to be gone.""" + self.client.servers.delete(server_id) + self.wait_for_resource_delete(server_id, self.client.servers) + class TenantTestBase(ClientTestBase): """Base test class for additional tenant and user creation which diff --git a/novaclient/tests/functional/v2/legacy/test_servers.py b/novaclient/tests/functional/v2/legacy/test_servers.py index ad773f1aa..86280e1b6 100644 --- a/novaclient/tests/functional/v2/legacy/test_servers.py +++ b/novaclient/tests/functional/v2/legacy/test_servers.py @@ -73,6 +73,47 @@ def test_boot_server_with_net_name(self): self.client.servers.delete(server_id) self.wait_for_resource_delete(server_id, self.client.servers) + def test_boot_server_using_image_with(self): + """Scenario test which does the following: + + 1. Create a server. + 2. Create a snapshot image of the server with a special meta key. + 3. Create a second server using the --image-with option using the meta + key stored in the snapshot image created in step 2. + """ + # create the first server and wait for it to be active + server_info = self.nova('boot', params=( + '--flavor %(flavor)s --image %(image)s --poll ' + 'image-with-server-1' % {'image': self.image.id, + 'flavor': self.flavor.id})) + server_id = self._get_value_from_the_table(server_info, 'id') + self.addCleanup(self._cleanup_server, server_id) + + # create a snapshot of the server with an image metadata key + snapshot_info = self.nova('image-create', params=( + '--metadata image_with_meta=%(meta_value)s ' + '--show --poll %(server_id)s image-with-snapshot' % { + 'meta_value': server_id, + 'server_id': server_id})) + + # get the snapshot image id out of the output table for the second + # server create request + snapshot_id = self._get_value_from_the_table(snapshot_info, 'id') + self.addCleanup(self.glance.images.delete, snapshot_id) + + # verify the metadata was set on the snapshot image + meta_value = self._get_value_from_the_table( + snapshot_info, 'image_with_meta') + self.assertEqual(server_id, meta_value) + + # create the second server using --image-with + server_info = self.nova('boot', params=( + '--flavor %(flavor)s --image-with image_with_meta=%(meta_value)s ' + '--poll image-with-server-2' % {'meta_value': server_id, + 'flavor': self.flavor.id})) + server_id = self._get_value_from_the_table(server_info, 'id') + self.addCleanup(self._cleanup_server, server_id) + class TestServersListNovaClient(base.ClientTestBase): """Servers list functional tests.""" diff --git a/novaclient/tests/functional/v2/test_servers.py b/novaclient/tests/functional/v2/test_servers.py index 6adf1e052..1a6e95982 100644 --- a/novaclient/tests/functional/v2/test_servers.py +++ b/novaclient/tests/functional/v2/test_servers.py @@ -13,6 +13,8 @@ import random import string +import novaclient +from novaclient import api_versions from novaclient.tests.functional import base from novaclient.tests.functional.v2.legacy import test_servers from novaclient.v2 import shell @@ -23,6 +25,18 @@ class TestServersBootNovaClient(test_servers.TestServersBootNovaClient): COMPUTE_API_VERSION = "2.latest" + def test_boot_server_using_image_with(self): + # --image-with relies on listing images via the compute image proxy + # API which does not work after 2.35 so we have to cap for this test. + try: + self.COMPUTE_API_VERSION = ( + min(novaclient.API_MAX_VERSION, + api_versions.APIVersion('2.35')).get_string()) + super(TestServersBootNovaClient, + self).test_boot_server_using_image_with() + finally: + self.COMPUTE_API_VERSION = '2.latest' + class TestServersListNovaClient(test_servers.TestServersListNovaClient): """Servers list functional tests.""" From 41f66d15aa83004d5b26f7d510b19cd8c4b39e8b Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Wed, 22 Feb 2017 07:19:07 -0500 Subject: [PATCH 1237/1705] Remove deprecated image commands/API bindings We deprecated the image proxy commands and APIs in Newton due to the 2.36 microversion. We said after Ocata 15.0.0 we would remove these, which we can do now in Pike. Note that the list() method on the ImageManager has to be moved to the GlanceManager since we still need to list images for the --image-with option on the boot command. The _match_image method in the shell has to be updated for a glance v2 response where custom metadata properties are flat in the image body. This needs to be released with a major version bump. Change-Id: I2d9fd0243d42538bd1417a42357c17b09368d2a5 --- .../v2/legacy/test_readonly_nova.py | 4 - .../tests/functional/v2/test_image_meta.py | 9 -- .../tests/functional/v2/test_servers.py | 14 -- novaclient/tests/unit/fixture_data/images.py | 7 +- novaclient/tests/unit/v2/fakes.py | 35 +---- novaclient/tests/unit/v2/test_images.py | 98 +------------ novaclient/tests/unit/v2/test_shell.py | 77 ----------- novaclient/v2/client.py | 1 - novaclient/v2/images.py | 130 +----------------- novaclient/v2/shell.py | 104 ++------------ ...ke-rm-deprecated-img-d58e9ae2d774cbfc.yaml | 13 ++ 11 files changed, 44 insertions(+), 448 deletions(-) create mode 100644 releasenotes/notes/pike-rm-deprecated-img-d58e9ae2d774cbfc.yaml diff --git a/novaclient/tests/functional/v2/legacy/test_readonly_nova.py b/novaclient/tests/functional/v2/legacy/test_readonly_nova.py index bf0575c90..c9156b4d5 100644 --- a/novaclient/tests/functional/v2/legacy/test_readonly_nova.py +++ b/novaclient/tests/functional/v2/legacy/test_readonly_nova.py @@ -79,10 +79,6 @@ def test_admin_host_list(self): def test_admin_hypervisor_list(self): self.nova('hypervisor-list') - def test_admin_image_list(self): - out = self.nova('image-list', merge_stderr=True) - self.assertIn('Command image-list is deprecated', out) - @decorators.skip_because(bug="1157349") def test_admin_interface_list(self): self.nova('interface-list') diff --git a/novaclient/tests/functional/v2/test_image_meta.py b/novaclient/tests/functional/v2/test_image_meta.py index 4f0a41a54..83a285a22 100644 --- a/novaclient/tests/functional/v2/test_image_meta.py +++ b/novaclient/tests/functional/v2/test_image_meta.py @@ -21,15 +21,6 @@ class TestImageMetaV239(base.ClientTestBase): # fallback to 2.35 and emit a warning. COMPUTE_API_VERSION = "2.39" - def test_command_deprecation(self): - output = self.nova('image-meta %s set test_key=test_value' % - self.image.id, merge_stderr=True) - self.assertIn('is deprecated', output) - - output = self.nova('image-meta %s delete test_key' % - self.image.id, merge_stderr=True) - self.assertIn('is deprecated', output) - def test_limits(self): """Tests that 2.39 won't return 'maxImageMeta' resource limit and the CLI output won't show it. diff --git a/novaclient/tests/functional/v2/test_servers.py b/novaclient/tests/functional/v2/test_servers.py index 1a6e95982..6adf1e052 100644 --- a/novaclient/tests/functional/v2/test_servers.py +++ b/novaclient/tests/functional/v2/test_servers.py @@ -13,8 +13,6 @@ import random import string -import novaclient -from novaclient import api_versions from novaclient.tests.functional import base from novaclient.tests.functional.v2.legacy import test_servers from novaclient.v2 import shell @@ -25,18 +23,6 @@ class TestServersBootNovaClient(test_servers.TestServersBootNovaClient): COMPUTE_API_VERSION = "2.latest" - def test_boot_server_using_image_with(self): - # --image-with relies on listing images via the compute image proxy - # API which does not work after 2.35 so we have to cap for this test. - try: - self.COMPUTE_API_VERSION = ( - min(novaclient.API_MAX_VERSION, - api_versions.APIVersion('2.35')).get_string()) - super(TestServersBootNovaClient, - self).test_boot_server_using_image_with() - finally: - self.COMPUTE_API_VERSION = '2.latest' - class TestServersListNovaClient(test_servers.TestServersListNovaClient): """Servers list functional tests.""" diff --git a/novaclient/tests/unit/fixture_data/images.py b/novaclient/tests/unit/fixture_data/images.py index 7c5e0925f..5f8b30df4 100644 --- a/novaclient/tests/unit/fixture_data/images.py +++ b/novaclient/tests/unit/fixture_data/images.py @@ -16,7 +16,7 @@ class V1(base.Fixture): - base_url = 'images' + base_url = 'v2/images' def setUp(self): super(V1, self).setUp() @@ -78,8 +78,3 @@ def post_images_1_metadata(request, context): for u in (1, '1/metadata/test_key'): self.requests_mock.delete(self.url(u), status_code=204, headers=headers) - - -class V3(V1): - - base_url = 'v1/images' diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 3590acdfd..80b2d2bfc 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -1103,7 +1103,7 @@ def put_os_floating_ips_bulk_delete(self, **kw): # # Images # - def get_images_detail(self, **kw): + def get_images(self, **kw): return (200, {}, {'images': [ { "id": FAKE_IMAGE_UUID_SNAPSHOT, @@ -1131,9 +1131,7 @@ def get_images_detail(self, **kw): "updated": "2010-10-10T12:00:00Z", "created": "2010-08-10T12:00:00Z", "status": "ACTIVE", - "metadata": { - "test_key": "test_value", - }, + "test_key": "test_value", "links": {}, }, { @@ -1149,41 +1147,20 @@ def get_images_detail(self, **kw): ]}) def get_images_555cae93_fb41_4145_9c52_f5b923538a26(self, **kw): - return (200, {}, {'image': self.get_images_detail()[2]['images'][0]}) + return (200, {}, {'image': self.get_images()[2]['images'][0]}) def get_images_55bb23af_97a4_4068_bdf8_f10c62880ddf(self, **kw): - return (200, {}, {'image': self.get_images_detail()[2]['images'][1]}) + return (200, {}, {'image': self.get_images()[2]['images'][1]}) def get_images_c99d7632_bd66_4be9_aed5_3dd14b223a76(self, **kw): - return (200, {}, {'image': self.get_images_detail()[2]['images'][2]}) + return (200, {}, {'image': self.get_images()[2]['images'][2]}) def get_images_f27f479a_ddda_419a_9bbc_d6b56b210161(self, **kw): - return (200, {}, {'image': self.get_images_detail()[2]['images'][3]}) + return (200, {}, {'image': self.get_images()[2]['images'][3]}) def get_images_3e861307_73a6_4d1f_8d68_f68b03223032(self): raise exceptions.NotFound('404') - def post_images_c99d7632_bd66_4be9_aed5_3dd14b223a76_metadata( - self, body, **kw): - assert list(body) == ['metadata'] - fakes.assert_has_keys(body['metadata'], - required=['test_key']) - get_image = self.get_images_c99d7632_bd66_4be9_aed5_3dd14b223a76 - return ( - 200, - {}, - {'metadata': get_image()[2]['image']['metadata']}) - - def delete_images_c99d7632_bd66_4be9_aed5_3dd14b223a76(self, **kw): - return (204, {}, None) - - def delete_images_f27f479a_ddda_419a_9bbc_d6b56b210161(self, **kw): - return (204, {}, None) - - def delete_images_c99d7632_bd66_4be9_aed5_3dd14b223a76_metadata_test_key( - self, **kw): - return (204, {}, None) - # # Keypairs # diff --git a/novaclient/tests/unit/v2/test_images.py b/novaclient/tests/unit/v2/test_images.py index 00116a1a6..a3177a9cb 100644 --- a/novaclient/tests/unit/v2/test_images.py +++ b/novaclient/tests/unit/v2/test_images.py @@ -11,12 +11,8 @@ # License for the specific language governing permissions and limitations # under the License. -import warnings - import mock -from novaclient import api_versions -from novaclient import exceptions from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit.fixture_data import images as data from novaclient.tests.unit import utils @@ -29,95 +25,13 @@ class ImagesTest(utils.FixturedTestCase): client_fixture_class = client.V1 data_fixture_class = data.V1 - @mock.patch.object(warnings, 'warn') - def test_list_images(self, mock_warn): - il = self.cs.images.list() + @mock.patch('novaclient.base.Manager.alternate_service_type') + def test_list_images(self, mock_alternate_service_type): + il = self.cs.glance.list() self.assert_request_id(il, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('GET', '/images/detail') + self.assert_called('GET', '/v2/images') for i in il: self.assertIsInstance(i, images.Image) self.assertEqual(2, len(il)) - self.assertEqual(1, mock_warn.call_count) - - def test_list_images_undetailed(self): - il = self.cs.images.list(detailed=False) - self.assert_request_id(il, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('GET', '/images') - for i in il: - self.assertIsInstance(i, images.Image) - - def test_list_images_with_marker_limit(self): - il = self.cs.images.list(marker=1234, limit=4) - self.assert_request_id(il, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('GET', '/images/detail?limit=4&marker=1234') - - @mock.patch.object(warnings, 'warn') - def test_get_image_details(self, mock_warn): - i = self.cs.images.get(1) - self.assert_request_id(i, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('GET', '/images/1') - self.assertIsInstance(i, images.Image) - self.assertEqual(1, i.id) - self.assertEqual('CentOS 5.2', i.name) - self.assertEqual(1, mock_warn.call_count) - - @mock.patch.object(warnings, 'warn') - def test_delete_image(self, mock_warn): - i = self.cs.images.delete(1) - self.assert_request_id(i, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('DELETE', '/images/1') - self.assertEqual(1, mock_warn.call_count) - - @mock.patch.object(warnings, 'warn') - def test_delete_meta(self, mock_warn): - i = self.cs.images.delete_meta(1, {'test_key': 'test_value'}) - self.assert_request_id(i, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('DELETE', '/images/1/metadata/test_key') - self.assertEqual(1, mock_warn.call_count) - - @mock.patch.object(warnings, 'warn') - def test_set_meta(self, mock_warn): - i = self.cs.images.set_meta(1, {'test_key': 'test_value'}) - self.assert_request_id(i, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('POST', '/images/1/metadata', - {"metadata": {'test_key': 'test_value'}}) - self.assertEqual(1, mock_warn.call_count) - - @mock.patch.object(warnings, 'warn') - def test_find(self, mock_warn): - i = self.cs.images.find(name="CentOS 5.2") - self.assert_request_id(i, fakes.FAKE_REQUEST_ID_LIST) - self.assertEqual(1, i.id) - self.assert_called('GET', '/images/1') - # This is two warnings because find calls findall which calls list - # which is the first warning, which finds one results and then calls - # get on that, which is the second warning. - self.assertEqual(2, mock_warn.call_count) - - iml = self.cs.images.findall(status='SAVING') - self.assert_request_id(iml, fakes.FAKE_REQUEST_ID_LIST) - self.assertEqual(1, len(iml)) - self.assertEqual('My Server Backup', iml[0].name) - - def test_find_2_36(self): - """Tests that using the find method fails after microversion 2.35. - """ - self.cs.api_version = api_versions.APIVersion('2.36') - self.assertRaises(exceptions.VersionNotFoundForAPIMethod, - self.cs.images.find, name="CentOS 5.2") - - def test_delete_meta_2_39(self): - """Tests that 'delete_meta' method fails after microversion 2.39. - """ - self.cs.api_version = api_versions.APIVersion('2.39') - self.assertRaises(exceptions.VersionNotFoundForAPIMethod, - self.cs.images.delete_meta, 1, - {'test_key': 'test_value'}) - - def test_set_meta_2_39(self): - """Tests that 'set_meta' method fails after microversion 2.39. - """ - self.cs.api_version = api_versions.APIVersion('2.39') - self.assertRaises(exceptions.VersionNotFoundForAPIMethod, - self.cs.images.set_meta, 1, - {'test_key': 'test_value'}) + mock_alternate_service_type.assert_called_once_with( + 'image', allowed_types=('image',)) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 45b135f5e..d0161f5e3 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -1199,35 +1199,6 @@ def test_flavor_access_remove_by_name(self): self.assert_called('POST', '/flavors/2/action', {'removeTenantAccess': {'tenant': 'proj2'}}) - def test_image_show(self): - _out, err = self.run_command('image-show %s' % FAKE_UUID_1) - self.assertIn('Command image-show is deprecated', err) - self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1) - - def test_image_meta_set(self): - _out, err = self.run_command('image-meta %s set test_key=test_value' % - FAKE_UUID_1) - self.assertIn('Command image-meta is deprecated', err) - self.assert_called('POST', '/images/%s/metadata' % FAKE_UUID_1, - {'metadata': {'test_key': 'test_value'}}) - - def test_image_meta_del(self): - _out, err = self.run_command('image-meta %s delete test_key' % - FAKE_UUID_1) - self.assertIn('Command image-meta is deprecated', err) - self.assert_called('DELETE', '/images/%s/metadata/test_key' % - FAKE_UUID_1) - - @mock.patch('sys.stdout', six.StringIO()) - @mock.patch('sys.stderr', six.StringIO()) - def test_image_meta_bad_action(self): - self.assertRaises(SystemExit, self.run_command, - 'image-meta 1 BAD_ACTION test_key=test_value') - - def test_image_list(self): - self.run_command('image-list') - self.assert_called('GET', '/images/detail') - def test_create_image(self): self.run_command('image-create sample-server mysnapshot') self.assert_called( @@ -1273,18 +1244,6 @@ def test_create_image_with_poll_to_check_image_state_deleted(self): exceptions.InstanceInDeletedState, self.run_command, 'image-create sample-server mysnapshot_deleted --poll') - def test_image_delete(self): - _out, err = self.run_command('image-delete %s' % FAKE_UUID_1) - self.assertIn('Command image-delete is deprecated', err) - self.assert_called('DELETE', '/images/%s' % FAKE_UUID_1) - - def test_image_delete_multiple(self): - self.run_command('image-delete %s %s' % (FAKE_UUID_1, FAKE_UUID_2)) - self.assert_called('GET', '/v2/images/' + FAKE_UUID_1, pos=0) - self.assert_called('DELETE', '/images/' + FAKE_UUID_1, pos=1) - self.assert_called('GET', '/v2/images/' + FAKE_UUID_2, pos=2) - self.assert_called('DELETE', '/images/' + FAKE_UUID_2, pos=3) - def test_list(self): self.run_command('list') self.assert_called('GET', '/servers/detail') @@ -3545,39 +3504,3 @@ def tester(cs): # the deprecated_network decorator will set cs.client.api_version # after calling the wrapped function self.assertEqual(cs.api_version, cs.api_version) - - -class ShellImageUtilTest(utils.TestCase): - def test_deprecated_image_newer(self): - @novaclient.v2.shell.deprecated_image - def tester(cs): - 'foo' - self.assertEqual(api_versions.APIVersion('2.35'), - cs.api_version) - - cs = mock.MagicMock() - cs.api_version = api_versions.APIVersion('2.9999') - tester(cs) - self.assertEqual('DEPRECATED: foo', tester.__doc__) - - def test_deprecated_image_older(self): - @novaclient.v2.shell.deprecated_image - def tester(cs): - 'foo' - # since we didn't need to adjust the api_version the mock won't - # have cs.client.api_version set on it - self.assertFalse(hasattr(cs, 'client')) - # we have to set the attribute back on cs so the decorator can - # set the value on it when we return from this wrapped function - setattr(cs, 'client', mock.MagicMock()) - - cs = mock.MagicMock() - cs.api_version = api_versions.APIVersion('2.1') - # we have to delete the cs.client attribute so hasattr won't return a - # false positive in the wrapped function - del cs.client - tester(cs) - self.assertEqual('DEPRECATED: foo', tester.__doc__) - # the deprecated_network decorator will set cs.client.api_version - # after calling the wrapped function - self.assertEqual(cs.api_version, cs.api_version) diff --git a/novaclient/v2/client.py b/novaclient/v2/client.py index 514fb5e4a..2db2f221e 100644 --- a/novaclient/v2/client.py +++ b/novaclient/v2/client.py @@ -153,7 +153,6 @@ def __init__(self, self.user_id = user_id self.flavors = flavors.FlavorManager(self) self.flavor_access = flavor_access.FlavorAccessManager(self) - self.images = images.ImageManager(self) self.glance = images.GlanceManager(self) self.limits = limits.LimitsManager(self) self.servers = servers.ServerManager(self) diff --git a/novaclient/v2/images.py b/novaclient/v2/images.py index 8d17e6bca..e83d9b2fe 100644 --- a/novaclient/v2/images.py +++ b/novaclient/v2/images.py @@ -12,44 +12,25 @@ # License for the specific language governing permissions and limitations # under the License. -""" -DEPRECATED: Image interface. -""" - -import warnings - from oslo_utils import uuidutils -from six.moves.urllib import parse -from novaclient import api_versions from novaclient import base from novaclient import exceptions from novaclient.i18n import _ class Image(base.Resource): - """ - DEPRECATED: An image is a collection of files used to create or rebuild a - server. - """ HUMAN_ID = True def __repr__(self): return "" % self.name - def delete(self): - """ - DEPRECATED: Delete this image. - - :returns: An instance of novaclient.base.TupleWithMeta - """ - return self.manager.delete(self) - class GlanceManager(base.Manager): """Use glance directly from service catalog. - This is used to do name to id lookups for images. Do not use it + This is used to do name to id lookups for images and listing images for + the --image-with option to the 'boot' command. Do not use it for anything else besides that. You have been warned. """ @@ -85,110 +66,11 @@ def find_image(self, name_or_id): matches[0].append_request_ids(matches.request_ids) return matches[0] - -class ImageManager(base.ManagerWithFind): - """ - DEPRECATED: Manage :class:`Image` resources. - """ - resource_class = Image - - @api_versions.wraps('2.0', '2.35') - def get(self, image): - """ - DEPRECATED: Get an image. - - :param image: The ID of the image to get. - :rtype: :class:`Image` + def list(self): """ - warnings.warn( - 'The novaclient.v2.images module is deprecated and will be ' - 'removed after Nova 15.0.0 is released. Use python-glanceclient ' - 'or python-openstacksdk instead.', DeprecationWarning) - return self._get("/images/%s" % base.getid(image), "image") - - def list(self, detailed=True, limit=None, marker=None): - """ - DEPRECATED: Get a list of all images. + Get a detailed list of all images. :rtype: list of :class:`Image` - :param limit: maximum number of images to return. - :param marker: Begin returning images that appear later in the image - list than that represented by this image id (optional). - """ - # FIXME(mriedem): Should use the api_versions.wraps decorator but that - # breaks the ManagerWithFind.findall method which checks the argspec - # on this function looking for the 'detailed' arg, and it's getting - # tripped up if you use the wraps decorator. This is all deprecated for - # removal anyway so we probably don't care too much about this. - if self.api.api_version > api_versions.APIVersion('2.35'): - raise exceptions.VersionNotFoundForAPIMethod( - self.api.api_version, 'list') - warnings.warn( - 'The novaclient.v2.images module is deprecated and will be ' - 'removed after Nova 15.0.0 is released. Use python-glanceclient ' - 'or python-openstacksdk instead.', DeprecationWarning) - params = {} - detail = '' - if detailed: - detail = '/detail' - if limit: - params['limit'] = int(limit) - if marker: - params['marker'] = str(marker) - params = sorted(params.items(), key=lambda x: x[0]) - query = '?%s' % parse.urlencode(params) if params else '' - return self._list('/images%s%s' % (detail, query), 'images') - - @api_versions.wraps('2.0', '2.35') - def delete(self, image): - """ - DEPRECATED: Delete an image. - - It should go without saying that you can't delete an image - that you didn't create. - - :param image: The :class:`Image` (or its ID) to delete. - :returns: An instance of novaclient.base.TupleWithMeta - """ - warnings.warn( - 'The novaclient.v2.images module is deprecated and will be ' - 'removed after Nova 15.0.0 is released. Use python-glanceclient ' - 'or python-openstacksdk instead.', DeprecationWarning) - return self._delete("/images/%s" % base.getid(image)) - - @api_versions.wraps('2.0', '2.38') - def set_meta(self, image, metadata): """ - DEPRECATED: Set an images metadata - - :param image: The :class:`Image` to add metadata to - :param metadata: A dict of metadata to add to the image - """ - warnings.warn( - 'The novaclient.v2.images module is deprecated and will be ' - 'removed after Nova 15.0.0 is released. Use python-glanceclient ' - 'or python-openstacksdk instead.', DeprecationWarning) - body = {'metadata': metadata} - return self._create("/images/%s/metadata" % base.getid(image), - body, "metadata") - - @api_versions.wraps('2.0', '2.38') - def delete_meta(self, image, keys): - """ - DEPRECATED: Delete metadata from an image - - :param image: The :class:`Image` to delete metadata - :param keys: A list of metadata keys to delete from the image - :returns: An instance of novaclient.base.TupleWithMeta - """ - warnings.warn( - 'The novaclient.v2.images module is deprecated and will be ' - 'removed after Nova 15.0.0 is released. Use python-glanceclient ' - 'or python-openstacksdk instead.', DeprecationWarning) - result = base.TupleWithMeta((), None) - for k in keys: - ret = self._delete("/images/%s/metadata/%s" % - (base.getid(image), k)) - result.append_request_ids(ret.request_ids) - - return result + with self.alternate_service_type('image', allowed_types=('image',)): + return self._list('/v2/images', 'images') diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 810159544..8c030025a 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -72,10 +72,6 @@ 'after Nova 15.0.0 is released. Use python-neutronclient ' 'or openstackclient instead.') -msg_deprecate_img = ('WARNING: Command %s is deprecated and will be removed ' - 'after Nova 15.0.0 is released. Use python-glanceclient ' - 'or openstackclient instead') - def deprecated_proxy(fn, msg_format): @functools.wraps(fn) @@ -100,9 +96,6 @@ def wrapped(cs, *args, **kwargs): deprecated_network = functools.partial(deprecated_proxy, msg_format=msg_deprecate_net) -deprecated_image = functools.partial(deprecated_proxy, - msg_format=msg_deprecate_img) - def _key_value_pairing(text): try: @@ -118,15 +111,21 @@ def _meta_parsing(metadata): def _match_image(cs, wanted_properties): - image_list = cs.images.list() + image_list = cs.glance.list() images_matched = [] match = set(wanted_properties) for img in image_list: - try: - if match == match.intersection(set(img.metadata.items())): - images_matched.append(img) - except AttributeError: - pass + img_dict = {} + # exclude any unhashable entries + for key, value in img.to_dict().items(): + try: + set([key, value]) + except TypeError: + pass + else: + img_dict[key] = value + if match == match.intersection(set(img_dict.items())): + images_matched.append(img) return images_matched @@ -1350,57 +1349,6 @@ def do_network_create(cs, args): cs.networks.create(**kwargs) -@utils.arg( - '--limit', - dest="limit", - metavar="", - help=_('Number of images to return per request.')) -@deprecated_image -def do_image_list(cs, _args): - """Print a list of available images to boot from.""" - limit = _args.limit - image_list = cs.images.list(limit=limit) - - def parse_server_name(image): - try: - return image.server['id'] - except (AttributeError, KeyError): - return '' - - fmts = {'Server': parse_server_name} - utils.print_list(image_list, ['ID', 'Name', 'Status', 'Server'], - fmts, sortby_index=1) - - -@utils.arg( - 'image', - metavar='', - help=_("Name or ID of image.")) -@utils.arg( - 'action', - metavar='', - choices=['set', 'delete'], - help=_("Actions: 'set' or 'delete'.")) -@utils.arg( - 'metadata', - metavar='', - nargs='+', - action='append', - default=[], - help=_('Metadata to add/update or delete (only key is necessary on ' - 'delete).')) -@deprecated_image -def do_image_meta(cs, args): - """Set or delete metadata on an image.""" - image = _find_image(cs, args.image) - metadata = _extract_metadata(args) - - if args.action == 'set': - cs.images.set_meta(image, metadata) - elif args.action == 'delete': - cs.images.delete_meta(image, metadata.keys()) - - def _extract_metadata(args): metadata = {} for metadatum in args.metadata[0]: @@ -1449,34 +1397,6 @@ def _print_flavor(flavor): utils.print_dict(info) -@utils.arg( - 'image', - metavar='', - help=_("Name or ID of image.")) -@deprecated_image -def do_image_show(cs, args): - """Show details about the given image.""" - image = _find_image(cs, args.image) - _print_image(image) - - -@utils.arg( - 'image', metavar='', nargs='+', - help=_('Name or ID of image(s).')) -@deprecated_image -def do_image_delete(cs, args): - """Delete specified image(s).""" - for image in args.image: - try: - # _find_image is using the GlanceManager which doesn't implement - # the delete() method so use the ImagesManager for that. - image = _find_image(cs, image) - cs.images.delete(image) - except Exception as e: - print(_("Delete for image %(image)s failed: %(e)s") % - {'image': image, 'e': e}) - - @utils.arg( '--reservation-id', dest='reservation_id', diff --git a/releasenotes/notes/pike-rm-deprecated-img-d58e9ae2d774cbfc.yaml b/releasenotes/notes/pike-rm-deprecated-img-d58e9ae2d774cbfc.yaml new file mode 100644 index 000000000..0e5d109c4 --- /dev/null +++ b/releasenotes/notes/pike-rm-deprecated-img-d58e9ae2d774cbfc.yaml @@ -0,0 +1,13 @@ +--- +prelude: > + Deprecated image commands and python API bindings have been removed. +upgrade: + - | + The following deprecated image commands have been removed:: + + * nova image-list + * nova image-show + * nova image-meta + * nova image-delete + + Along with the related python API bindings in ``novaclient.v2.images``. From adf7d1e48c619579c2fc430229173264db45033e Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Wed, 22 Feb 2017 15:43:42 -0500 Subject: [PATCH 1238/1705] Remove deprecated network-related resource commands In Newton we deprecated the various network-related resource commands which were either for nova-network or were proxy APIs to Neutron, because of the 2.36 microversion which deprecated those APIs. This removes the deprecated commands. Because of the size of this change, the deprecated python API binding removals will come in a separate change. Change-Id: I6ecca49e7208f9dc0969bf377930a729c4b407b8 --- .../v2/legacy/test_readonly_nova.py | 22 - .../tests/functional/v2/test_networks.py | 6 - novaclient/tests/unit/test_shell.py | 4 +- novaclient/tests/unit/v2/test_shell.py | 395 +-------- novaclient/v2/contrib/tenant_networks.py | 86 -- novaclient/v2/shell.py | 821 ------------------ ...ke-rm-deprecated-net-272aeb62b329a5bc.yaml | 53 ++ 7 files changed, 56 insertions(+), 1331 deletions(-) create mode 100644 releasenotes/notes/pike-rm-deprecated-net-272aeb62b329a5bc.yaml diff --git a/novaclient/tests/functional/v2/legacy/test_readonly_nova.py b/novaclient/tests/functional/v2/legacy/test_readonly_nova.py index c9156b4d5..8fed1588e 100644 --- a/novaclient/tests/functional/v2/legacy/test_readonly_nova.py +++ b/novaclient/tests/functional/v2/legacy/test_readonly_nova.py @@ -64,15 +64,6 @@ def test_admin_flavor_acces_list(self): def test_admin_flavor_list(self): self.assertIn("Memory_MB", self.nova('flavor-list')) - def test_admin_floating_ip_bulk_list(self): - self.nova('floating-ip-bulk-list') - - def test_admin_floating_ip_list(self): - self.nova('floating-ip-list') - - def test_admin_floating_ip_pool_list(self): - self.nova('floating-ip-pool-list') - def test_admin_host_list(self): self.nova('host-list') @@ -95,16 +86,6 @@ def test_admin_list(self): 'list', params='--all-tenants bad') - def test_admin_network_list(self): - self.nova('network-list') - - def test_admin_secgroup_list(self): - self.nova('secgroup-list') - - @decorators.skip_because(bug="1157349") - def test_admin_secgroup_list_rules(self): - self.nova('secgroup-list-rules') - def test_admin_server_group_list(self): self.nova('server-group-list') @@ -123,9 +104,6 @@ def test_admin_help(self): def test_admin_list_extensions(self): self.nova('list-extensions') - def test_admin_net_list(self): - self.nova('net-list') - def test_agent_list(self): self.nova('agent-list') self.nova('agent-list', flags='--debug') diff --git a/novaclient/tests/functional/v2/test_networks.py b/novaclient/tests/functional/v2/test_networks.py index 6fa3467f6..49d99ec96 100644 --- a/novaclient/tests/functional/v2/test_networks.py +++ b/novaclient/tests/functional/v2/test_networks.py @@ -21,12 +21,6 @@ class TestNetworkCommandsV2_36(base.ClientTestBase): # and emit a warning. COMPUTE_API_VERSION = "2.36" - def test_command_deprecation(self): - output = self.nova('network-list', merge_stderr=True) - self.assertIn( - 'is deprecated', output, - 'network-list command did not print deprecation warning') - def test_limits(self): """Tests that 2.36 won't return network-related resource limits and the CLI output won't show them. diff --git a/novaclient/tests/unit/test_shell.py b/novaclient/tests/unit/test_shell.py index 5ccf09bb5..a6b41ae8b 100644 --- a/novaclient/tests/unit/test_shell.py +++ b/novaclient/tests/unit/test_shell.py @@ -459,8 +459,8 @@ def test_bash_completion(self): '.*--matching', '.*--wrap', '.*help', - '.*secgroup-delete-rule', - '.*--priority'] + '.*server-group-delete', + '.*--image-with'] for r in required: self.assertThat((stdout + stderr), matchers.MatchesRegex(r, re.DOTALL | re.MULTILINE)) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index d0161f5e3..8ffcc8d84 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -1544,15 +1544,6 @@ def test_set_password(self): self.assert_called('POST', '/servers/1234/action', {'changePassword': {'adminPass': 'p'}}) - def test_scrub(self): - self.run_command('scrub 4ffc664c198e435e9853f2538fbcd7a7') - self.assert_called('GET', '/os-networks', pos=-4) - self.assert_called('GET', '/os-security-groups?all_tenants=1', - pos=-3) - self.assert_called('POST', '/os-networks/1/action', - {"disassociate": None}, pos=-2) - self.assert_called('DELETE', '/os-security-groups/1') - def test_show(self): self.run_command('show 1234') self.assert_called('GET', '/servers?name=1234', pos=0) @@ -1722,84 +1713,6 @@ def test_delete_host_meta(self): self.assert_called('DELETE', '/servers/uuid1/metadata/key1', pos=1) self.assert_called('DELETE', '/servers/uuid2/metadata/key1', pos=2) - def test_dns_create(self): - self.run_command('dns-create 192.168.1.1 testname testdomain') - self.assert_called('PUT', - '/os-floating-ip-dns/testdomain/entries/testname') - - self.run_command('dns-create 192.168.1.1 testname testdomain --type A') - self.assert_called('PUT', - '/os-floating-ip-dns/testdomain/entries/testname') - - def test_dns_create_public_domain(self): - self.run_command('dns-create-public-domain testdomain ' - '--project test_project') - self.assert_called('PUT', '/os-floating-ip-dns/testdomain') - - def test_dns_create_private_domain(self): - self.run_command('dns-create-private-domain testdomain ' - '--availability-zone av_zone') - self.assert_called('PUT', '/os-floating-ip-dns/testdomain') - - def test_dns_delete(self): - self.run_command('dns-delete testdomain testname') - self.assert_called('DELETE', - '/os-floating-ip-dns/testdomain/entries/testname') - - def test_dns_delete_domain(self): - self.run_command('dns-delete-domain testdomain') - self.assert_called('DELETE', '/os-floating-ip-dns/testdomain') - - def test_dns_list(self): - self.run_command('dns-list testdomain --ip 192.168.1.1') - self.assert_called('GET', - '/os-floating-ip-dns/testdomain/entries?' - 'ip=192.168.1.1') - - self.run_command('dns-list testdomain --name testname') - self.assert_called('GET', - '/os-floating-ip-dns/testdomain/entries/testname') - - def test_dns_domains(self): - self.run_command('dns-domains') - self.assert_called('GET', '/os-floating-ip-dns') - - def test_floating_ip_list(self): - self.run_command('floating-ip-list') - self.assert_called('GET', '/os-floating-ips') - - def test_floating_ip_create(self): - self.run_command('floating-ip-create') - self.assert_called('GET', '/os-floating-ips/1') - - def test_floating_ip_delete(self): - self.run_command('floating-ip-delete 11.0.0.1') - self.assert_called('DELETE', '/os-floating-ips/1') - - def test_floating_ip_bulk_list(self): - self.run_command('floating-ip-bulk-list') - self.assert_called('GET', '/os-floating-ips-bulk') - - def test_floating_ip_bulk_create(self): - self.run_command('floating-ip-bulk-create 10.0.0.1/24') - self.assert_called('POST', '/os-floating-ips-bulk', - {'floating_ips_bulk_create': - {'ip_range': '10.0.0.1/24'}}) - - def test_floating_ip_bulk_create_host_and_interface(self): - self.run_command('floating-ip-bulk-create 10.0.0.1/24 --pool testPool' - ' --interface ethX') - self.assert_called('POST', '/os-floating-ips-bulk', - {'floating_ips_bulk_create': - {'ip_range': '10.0.0.1/24', - 'pool': 'testPool', - 'interface': 'ethX'}}) - - def test_floating_ip_bulk_delete(self): - self.run_command('floating-ip-bulk-delete 10.0.0.1/24') - self.assert_called('PUT', '/os-floating-ips-bulk/delete', - {'ip_range': '10.0.0.1/24'}) - def test_server_floating_ip_associate(self): self.run_command('floating-ip-associate sample-server 11.0.0.1') self.assert_called('POST', '/servers/1234/action', @@ -2324,20 +2237,6 @@ def test_services_delete(self): self.run_command('service-delete 1') self.assert_called('DELETE', '/os-services/1') - def test_fixed_ips_get(self): - self.run_command('fixed-ip-get 192.168.1.1') - self.assert_called('GET', '/os-fixed-ips/192.168.1.1') - - def test_fixed_ips_reserve(self): - self.run_command('fixed-ip-reserve 192.168.1.1') - body = {'reserve': None} - self.assert_called('POST', '/os-fixed-ips/192.168.1.1/action', body) - - def test_fixed_ips_unreserve(self): - self.run_command('fixed-ip-unreserve 192.168.1.1') - body = {'unreserve': None} - self.assert_called('POST', '/os-fixed-ips/192.168.1.1/action', body) - def test_host_list(self): self.run_command('host-list') self.assert_called('GET', '/os-hosts') @@ -2631,34 +2530,6 @@ def test_quota_class_update(self): 'PUT', '/os-quota-class-sets/97f4c221bff44578b0300df4ef119353', body) - def test_network_list(self): - self.run_command('network-list') - self.assert_called('GET', '/os-networks') - - def test_network_list_fields(self): - output, _err = self.run_command( - 'network-list --fields ' - 'vlan,project_id') - self.assert_called('GET', '/os-networks') - self.assertIn('1234', output) - self.assertIn('4ffc664c198e435e9853f2538fbcd7a7', output) - - def test_network_list_invalid_fields(self): - self.assertRaises(exceptions.CommandError, - self.run_command, - 'network-list --fields vlan,project_id,invalid') - - def test_network_list_redundant_fields(self): - output, _err = self.run_command( - 'network-list --fields label,project_id,project_id') - header = output.splitlines()[1] - self.assertEqual(1, header.count('Label')) - self.assertEqual(1, header.count('Project Id')) - - def test_network_show(self): - self.run_command('network-show 1') - self.assert_called('GET', '/os-networks') - def test_cloudpipe_list(self): self.run_command('cloudpipe-list') self.assert_called('GET', '/os-cloudpipe') @@ -2674,115 +2545,6 @@ def test_cloudpipe_configure(self): 'vpn_port': '1234'}} self.assert_called('PUT', '/os-cloudpipe/configure-project', body) - def test_network_associate_host(self): - self.run_command('network-associate-host 1 testHost') - body = {'associate_host': 'testHost'} - self.assert_called('POST', '/os-networks/1/action', body) - - def test_network_associate_project(self): - self.run_command('network-associate-project 1') - body = {'id': "1"} - self.assert_called('POST', '/os-networks/add', body) - - def test_network_disassociate_host(self): - self.run_command('network-disassociate --host-only 1 2') - body = {'disassociate_host': None} - self.assert_called('POST', '/os-networks/2/action', body) - - def test_network_disassociate_project(self): - self.run_command('network-disassociate --project-only 1 2') - body = {'disassociate_project': None} - self.assert_called('POST', '/os-networks/2/action', body) - - def test_network_create_v4(self): - self.run_command('network-create --fixed-range-v4 10.0.1.0/24' - ' --dns1 10.0.1.254 new_network') - body = {'network': {'cidr': '10.0.1.0/24', 'label': 'new_network', - 'dns1': '10.0.1.254'}} - self.assert_called('POST', '/os-networks', body) - - def test_network_create_v6(self): - self.run_command('network-create --fixed-range-v6 2001::/64' - ' new_network') - body = {'network': {'cidr_v6': '2001::/64', 'label': 'new_network'}} - self.assert_called('POST', '/os-networks', body) - - def test_network_create_invalid(self): - cmd = 'network-create 10.0.1.0' - self.assertRaises(exceptions.CommandError, self.run_command, cmd) - - def test_network_create_multi_host(self): - self.run_command('network-create --fixed-range-v4 192.168.0.0/24' - ' --multi-host=T new_network') - body = {'network': {'cidr': '192.168.0.0/24', 'label': 'new_network', - 'multi_host': True}} - self.assert_called('POST', '/os-networks', body) - - self.run_command('network-create --fixed-range-v4 192.168.0.0/24' - ' --multi-host=True new_network') - body = {'network': {'cidr': '192.168.0.0/24', 'label': 'new_network', - 'multi_host': True}} - self.assert_called('POST', '/os-networks', body) - - self.run_command('network-create --fixed-range-v4 192.168.0.0/24' - ' --multi-host=1 new_network') - body = {'network': {'cidr': '192.168.0.0/24', 'label': 'new_network', - 'multi_host': True}} - self.assert_called('POST', '/os-networks', body) - - self.run_command('network-create --fixed-range-v4 192.168.1.0/24' - ' --multi-host=F new_network') - body = {'network': {'cidr': '192.168.1.0/24', 'label': 'new_network', - 'multi_host': False}} - self.assert_called('POST', '/os-networks', body) - - def test_network_create_vlan(self): - self.run_command('network-create --fixed-range-v4 192.168.0.0/24' - ' --vlan=200 new_network') - body = {'network': {'cidr': '192.168.0.0/24', 'label': 'new_network', - 'vlan': 200}} - self.assert_called('POST', '/os-networks', body) - - def test_network_create_vlan_start(self): - self.run_command('network-create --fixed-range-v4 192.168.0.0/24' - ' --vlan-start=100 new_network') - body = {'network': {'cidr': '192.168.0.0/24', 'label': 'new_network', - 'vlan_start': 100}} - self.assert_called('POST', '/os-networks', body) - - def test_network_create_extra_args(self): - self.run_command('network-create --fixed-range-v4 192.168.0.0/24' - ' --enable-dhcp F --dhcp-server 192.168.0.2' - ' --share-address T --allowed-start 192.168.0.10' - ' --allowed-end 192.168.0.20 --mtu 9000 new_network') - body = {'network': {'cidr': '192.168.0.0/24', 'label': 'new_network', - 'enable_dhcp': False, 'dhcp_server': '192.168.0.2', - 'share_address': True, 'mtu': 9000, - 'allowed_start': '192.168.0.10', - 'allowed_end': '192.168.0.20'}} - self.assert_called('POST', '/os-networks', body) - - def test_network_delete(self): - self.run_command('network-delete 1') - self.assert_called('DELETE', '/os-networks/1') - - def test_tenant_network_list(self): - self.run_command('tenant-network-list') - self.assert_called('GET', '/os-tenant-networks') - - def test_tenant_network_show(self): - self.run_command('tenant-network-show 1') - self.assert_called('GET', '/os-tenant-networks/1') - - def test_tenant_network_create(self): - self.run_command('tenant-network-create new_network 10.0.1.0/24') - body = {'network': {'cidr': '10.0.1.0/24', 'label': 'new_network'}} - self.assert_called('POST', '/os-tenant-networks', body) - - def test_tenant_network_delete(self): - self.run_command('tenant-network-delete 1') - self.assert_called('DELETE', '/os-tenant-networks/1') - def test_add_fixed_ip(self): self.run_command('add-fixed-ip sample-server 1') self.assert_called('POST', '/servers/1234/action', @@ -2879,82 +2641,6 @@ def test_availability_zone_list(self): self.run_command('availability-zone-list') self.assert_called('GET', '/os-availability-zone/detail') - def test_security_group_create(self): - self.run_command('secgroup-create test FAKE_SECURITY_GROUP') - self.assert_called('POST', '/os-security-groups', - {'security_group': - {'name': 'test', - 'description': 'FAKE_SECURITY_GROUP'}}) - - def test_security_group_update(self): - self.run_command('secgroup-update test te FAKE_SECURITY_GROUP') - self.assert_called('PUT', '/os-security-groups/1', - {'security_group': - {'name': 'te', - 'description': 'FAKE_SECURITY_GROUP'}}) - - def test_security_group_list(self): - self.run_command('secgroup-list') - self.assert_called('GET', '/os-security-groups') - - def test_security_group_add_rule(self): - self.run_command('secgroup-add-rule test tcp 22 22 10.0.0.0/8') - self.assert_called('POST', '/os-security-group-rules', - {'security_group_rule': - {'from_port': 22, - 'ip_protocol': 'tcp', - 'to_port': 22, - 'parent_group_id': 1, - 'cidr': '10.0.0.0/8', - 'group_id': None}}) - - def test_security_group_delete_rule(self): - self.run_command('secgroup-delete-rule test TCP 22 22 10.0.0.0/8') - self.assert_called('DELETE', '/os-security-group-rules/11') - - def test_security_group_delete_rule_protocol_case(self): - self.run_command('secgroup-delete-rule test tcp 22 22 10.0.0.0/8') - self.assert_called('DELETE', '/os-security-group-rules/11') - - def test_security_group_add_group_rule(self): - self.run_command('secgroup-add-group-rule test test2 tcp 22 22') - self.assert_called('POST', '/os-security-group-rules', - {'security_group_rule': - {'from_port': 22, - 'ip_protocol': 'TCP', - 'to_port': 22, - 'parent_group_id': 1, - 'cidr': None, - 'group_id': 2}}) - - def test_security_group_delete_valid_group_rule(self): - self.run_command('secgroup-delete-group-rule test test2 TCP 222 222') - self.assert_called('DELETE', '/os-security-group-rules/12') - - def test_security_group_delete_valid_group_rule_protocol_case(self): - self.run_command('secgroup-delete-group-rule test test2 tcp 222 222') - self.assert_called('DELETE', '/os-security-group-rules/12') - - def test_security_group_delete_invalid_group_rule(self): - self.run_command('secgroup-delete-group-rule test test4 TCP -1 -1') - self.assert_called('DELETE', '/os-security-group-rules/14') - - def test_security_group_delete_invalid_group_rule_protocol_case(self): - self.run_command('secgroup-delete-group-rule test test4 tcp -1 -1') - self.assert_called('DELETE', '/os-security-group-rules/14') - - def test_security_group_list_rules(self): - self.run_command('secgroup-list-rules test') - self.assert_called('GET', '/os-security-groups') - - def test_security_group_list_all_tenants(self): - self.run_command('secgroup-list --all-tenants 1') - self.assert_called('GET', '/os-security-groups?all_tenants=1') - - def test_security_group_delete(self): - self.run_command('secgroup-delete test') - self.assert_called('DELETE', '/os-security-groups/1') - def test_server_security_group_add(self): self.run_command('add-secgroup sample-server testgroup') self.assert_called('POST', '/servers/1234/action', @@ -3235,6 +2921,7 @@ def test_versions(self): exclusions = set([ 1, # Same as version 2.0 3, # doesn't require any changes in novaclient + 4, # fixed-ip-get command is gone 5, # doesn't require any changes in novaclient 7, # doesn't require any changes in novaclient 9, # doesn't require any changes in novaclient @@ -3339,50 +3026,6 @@ def test_list_v2_26_not_tags_any(self): self.assert_called('GET', '/servers/detail?not-tags-any=tag1%2Ctag2') -class GetSecgroupTest(utils.TestCase): - def test_with_integer(self): - cs = mock.Mock(**{ - 'security_groups.get.return_value': 'sec_group', - 'security_groups.list.return_value': [], - }) - result = novaclient.v2.shell._get_secgroup(cs, '1') - self.assertEqual('sec_group', result) - cs.security_groups.get.assert_called_once_with('1') - - def test_with_uuid(self): - cs = mock.Mock(**{ - 'security_groups.get.return_value': 'sec_group', - 'security_groups.list.return_value': [], - }) - result = novaclient.v2.shell._get_secgroup( - cs, 'c0c32459-dc5f-44dc-9a0a-473b28bac831') - self.assertEqual('sec_group', result) - cs.security_groups.get.assert_called_once_with( - 'c0c32459-dc5f-44dc-9a0a-473b28bac831') - - def test_with_an_nonexisting_name(self): - cs = mock.Mock(**{ - 'security_groups.get.return_value': 'sec_group', - 'security_groups.list.return_value': [], - }) - self.assertRaises(exceptions.CommandError, - novaclient.v2.shell._get_secgroup, - cs, - 'abc') - - def test_with_non_unique_name(self): - group_one = mock.MagicMock() - group_one.name = 'group_one' - cs = mock.Mock(**{ - 'security_groups.get.return_value': 'sec_group', - 'security_groups.list.return_value': [group_one, group_one], - }) - self.assertRaises(exceptions.NoUniqueMatch, - novaclient.v2.shell._get_secgroup, - cs, - 'group_one') - - class PollForStatusTestCase(utils.TestCase): @mock.patch("novaclient.v2.shell.time") def test_simple_usage(self, mock_time): @@ -3468,39 +3111,3 @@ def test_error_state(self, mock_time): action=action, show_progress=True, silent=False) - - -class ShellNetworkUtilTest(utils.TestCase): - def test_deprecated_network_newer(self): - @novaclient.v2.shell.deprecated_network - def tester(cs): - 'foo' - self.assertEqual(api_versions.APIVersion('2.35'), - cs.api_version) - - cs = mock.MagicMock() - cs.api_version = api_versions.APIVersion('2.9999') - tester(cs) - self.assertEqual('DEPRECATED: foo', tester.__doc__) - - def test_deprecated_network_older(self): - @novaclient.v2.shell.deprecated_network - def tester(cs): - 'foo' - # since we didn't need to adjust the api_version the mock won't - # have cs.client.api_version set on it - self.assertFalse(hasattr(cs, 'client')) - # we have to set the attribute back on cs so the decorator can - # set the value on it when we return from this wrapped function - setattr(cs, 'client', mock.MagicMock()) - - cs = mock.MagicMock() - cs.api_version = api_versions.APIVersion('2.1') - # we have to delete the cs.client attribute so hasattr won't return a - # false positive in the wrapped function - del cs.client - tester(cs) - self.assertEqual('DEPRECATED: foo', tester.__doc__) - # the deprecated_network decorator will set cs.client.api_version - # after calling the wrapped function - self.assertEqual(cs.api_version, cs.api_version) diff --git a/novaclient/v2/contrib/tenant_networks.py b/novaclient/v2/contrib/tenant_networks.py index 16d69f387..b7a39b9d6 100644 --- a/novaclient/v2/contrib/tenant_networks.py +++ b/novaclient/v2/contrib/tenant_networks.py @@ -14,9 +14,6 @@ from novaclient import api_versions from novaclient import base -from novaclient.i18n import _ -from novaclient import utils -from novaclient.v2 import shell class TenantNetwork(base.Resource): @@ -59,86 +56,3 @@ def create(self, label, cidr): """DEPRECATED""" body = {'network': {'label': label, 'cidr': cidr}} return self._create('/os-tenant-networks', body, 'network') - - -@utils.arg('network_id', metavar='', help='ID of network') -def do_net(cs, args): - """ - DEPRECATED, use tenant-network-show instead. - """ - do_tenant_network_show(cs, args) - - -@utils.arg('network_id', metavar='', help='ID of network') -@shell.deprecated_network -def do_tenant_network_show(cs, args): - """ - Show a tenant network. - """ - network = cs.tenant_networks.get(args.network_id) - utils.print_dict(network._info) - - -def do_net_list(cs, args): - """ - DEPRECATED, use tenant-network-list instead. - """ - do_tenant_network_list(cs, args) - - -@shell.deprecated_network -def do_tenant_network_list(cs, args): - """ - List tenant networks. - """ - networks = cs.tenant_networks.list() - utils.print_list(networks, ['ID', 'Label', 'CIDR']) - - -@utils.arg( - 'label', - metavar='', - help=_('Network label (ex. my_new_network)')) -@utils.arg( - 'cidr', - metavar='', - help=_('IP block to allocate from (ex. 172.16.0.0/24 or 2001:DB8::/64)')) -def do_net_create(cs, args): - """ - DEPRECATED, use tenant-network-create instead. - """ - do_tenant_network_create(cs, args) - - -@utils.arg( - 'label', - metavar='', - help=_('Network label (ex. my_new_network)')) -@utils.arg( - 'cidr', - metavar='', - help=_('IP block to allocate from (ex. 172.16.0.0/24 or 2001:DB8::/64)')) -@shell.deprecated_network -def do_tenant_network_create(cs, args): - """ - Create a tenant network. - """ - network = cs.tenant_networks.create(args.label, args.cidr) - utils.print_dict(network._info) - - -@utils.arg('network_id', metavar='', help='ID of network') -def do_net_delete(cs, args): - """ - DEPRECATED, use tenant-network-delete instead. - """ - do_tenant_network_delete(cs, args) - - -@utils.arg('network_id', metavar='', help='ID of network') -@shell.deprecated_network -def do_tenant_network_delete(cs, args): - """ - Delete a tenant network. - """ - cs.tenant_networks.delete(args.network_id) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 8c030025a..4d6508fc6 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -21,20 +21,16 @@ import argparse import collections import datetime -import functools import getpass -import locale import logging import os import pprint import sys import time -from oslo_utils import encodeutils from oslo_utils import netutils from oslo_utils import strutils from oslo_utils import timeutils -from oslo_utils import uuidutils import six import novaclient @@ -68,34 +64,6 @@ 'tag': 'tag', } -msg_deprecate_net = ('WARNING: Command %s is deprecated and will be removed ' - 'after Nova 15.0.0 is released. Use python-neutronclient ' - 'or openstackclient instead.') - - -def deprecated_proxy(fn, msg_format): - @functools.wraps(fn) - def wrapped(cs, *args, **kwargs): - command_name = '-'.join(fn.__name__.split('_')[1:]) - print(msg_format % command_name, file=sys.stderr) - # The network or image proxy API methods were deprecated in 2.36 and - # will return a 404 so we fallback to 2.35 to maintain a transition - # for CLI users. - want_version = api_versions.APIVersion('2.35') - cur_version = cs.api_version - if cs.api_version > want_version: - cs.api_version = want_version - try: - return fn(cs, *args, **kwargs) - finally: - cs.api_version = cur_version - wrapped.__doc__ = 'DEPRECATED: ' + fn.__doc__ - return wrapped - - -deprecated_network = functools.partial(deprecated_proxy, - msg_format=msg_deprecate_net) - def _key_value_pairing(text): try: @@ -1088,267 +1056,6 @@ def do_flavor_access_remove(cs, args): utils.print_list(access_list, columns) -@utils.arg( - 'project_id', metavar='', - help=_('The ID of the project.')) -@deprecated_network -def do_scrub(cs, args): - """Delete networks and security groups associated with a project.""" - networks_list = cs.networks.list() - networks_list = [network for network in networks_list - if getattr(network, 'project_id', '') == args.project_id] - search_opts = {'all_tenants': 1} - groups = cs.security_groups.list(search_opts) - groups = [group for group in groups - if group.tenant_id == args.project_id] - for network in networks_list: - cs.networks.disassociate(network) - for group in groups: - cs.security_groups.delete(group) - - -@utils.arg( - '--fields', - default=None, - metavar='', - help=_('Comma-separated list of fields to display. ' - 'Use the show command to see which fields are available.')) -@deprecated_network -def do_network_list(cs, args): - """Print a list of available networks.""" - network_list = cs.networks.list() - columns = ['ID', 'Label', 'Cidr'] - columns += _get_list_table_columns_and_formatters( - args.fields, network_list, - exclude_fields=(c.lower() for c in columns))[0] - utils.print_list(network_list, columns) - - -@utils.arg( - 'network', - metavar='', - help=_("UUID or label of network.")) -@deprecated_network -def do_network_show(cs, args): - """Show details about the given network.""" - network = utils.find_resource(cs.networks, args.network) - utils.print_dict(network.to_dict()) - - -@utils.arg( - 'network', - metavar='', - help=_("UUID or label of network.")) -@deprecated_network -def do_network_delete(cs, args): - """Delete network by label or id.""" - network = utils.find_resource(cs.networks, args.network) - network.delete() - - -@utils.arg( - '--host-only', - dest='host_only', - metavar='<0|1>', - nargs='?', - type=int, - const=1, - default=0) -@utils.arg( - '--project-only', - dest='project_only', - metavar='<0|1>', - nargs='?', - type=int, - const=1, - default=0) -@utils.arg( - 'network', - metavar='', - help=_("UUID of network.")) -@deprecated_network -def do_network_disassociate(cs, args): - """Disassociate host and/or project from the given network.""" - if args.host_only: - cs.networks.disassociate(args.network, True, False) - elif args.project_only: - cs.networks.disassociate(args.network, False, True) - else: - cs.networks.disassociate(args.network, True, True) - - -@utils.arg( - 'network', - metavar='', - help=_("UUID of network.")) -@utils.arg( - 'host', - metavar='', - help=_("Name of host")) -@deprecated_network -def do_network_associate_host(cs, args): - """Associate host with network.""" - cs.networks.associate_host(args.network, args.host) - - -@utils.arg( - 'network', - metavar='', - help=_("UUID of network.")) -@deprecated_network -def do_network_associate_project(cs, args): - """Associate project with network.""" - cs.networks.associate_project(args.network) - - -def _filter_network_create_options(args): - valid_args = ['label', 'cidr', 'vlan_start', 'vpn_start', 'cidr_v6', - 'gateway', 'gateway_v6', 'bridge', 'bridge_interface', - 'multi_host', 'dns1', 'dns2', 'uuid', 'fixed_cidr', - 'project_id', 'priority', 'vlan', 'mtu', 'dhcp_server', - 'allowed_start', 'allowed_end'] - kwargs = {} - for k, v in args.__dict__.items(): - if k in valid_args and v is not None: - kwargs[k] = v - - return kwargs - - -@utils.arg( - 'label', - metavar='', - help=_("Label for network")) -@utils.arg( - '--fixed-range-v4', - dest='cidr', - metavar='', - help=_("IPv4 subnet (ex: 10.0.0.0/8)")) -@utils.arg( - '--fixed-range-v6', - dest="cidr_v6", - help=_('IPv6 subnet (ex: fe80::/64')) -@utils.arg( - '--vlan', - dest='vlan', - type=int, - metavar='', - help=_("The vlan ID to be assigned to the project.")) -@utils.arg( - '--vlan-start', - dest='vlan_start', - type=int, - metavar='', - help=_('First vlan ID to be assigned to the project. Subsequent vlan ' - 'IDs will be assigned incrementally.')) -@utils.arg( - '--vpn', - dest='vpn_start', - type=int, - metavar='', - help=_("vpn start")) -@utils.arg( - '--gateway', - dest="gateway", - help=_('gateway')) -@utils.arg( - '--gateway-v6', - dest="gateway_v6", - help=_('IPv6 gateway')) -@utils.arg( - '--bridge', - dest="bridge", - metavar='', - help=_('VIFs on this network are connected to this bridge.')) -@utils.arg( - '--bridge-interface', - dest="bridge_interface", - metavar='', - help=_('The bridge is connected to this interface.')) -@utils.arg( - '--multi-host', - dest="multi_host", - metavar="<'T'|'F'>", - help=_('Multi host')) -@utils.arg( - '--dns1', - dest="dns1", - metavar="", help=_('First DNS.')) -@utils.arg( - '--dns2', - dest="dns2", - metavar="", - help=_('Second DNS.')) -@utils.arg( - '--uuid', - dest="uuid", - metavar="", - help=_('Network UUID.')) -@utils.arg( - '--fixed-cidr', - dest="fixed_cidr", - metavar='', - help=_('IPv4 subnet for fixed IPs (ex: 10.20.0.0/16).')) -@utils.arg( - '--project-id', - dest="project_id", - metavar="", - help=_('Project ID.')) -@utils.arg( - '--priority', - dest="priority", - metavar="", - help=_('Network interface priority.')) -@utils.arg( - '--mtu', - dest="mtu", - type=int, - help=_('MTU for network.')) -@utils.arg( - '--enable-dhcp', - dest="enable_dhcp", - metavar="<'T'|'F'>", - help=_('Enable DHCP.')) -@utils.arg( - '--dhcp-server', - dest="dhcp_server", - help=_('DHCP-server address (defaults to gateway address)')) -@utils.arg( - '--share-address', - dest="share_address", - metavar="<'T'|'F'>", - help=_('Share address')) -@utils.arg( - '--allowed-start', - dest="allowed_start", - help=_('Start of allowed addresses for instances.')) -@utils.arg( - '--allowed-end', - dest="allowed_end", - help=_('End of allowed addresses for instances.')) -@deprecated_network -def do_network_create(cs, args): - """Create a network.""" - - if not (args.cidr or args.cidr_v6): - raise exceptions.CommandError( - _("Must specify either fixed_range_v4 or fixed_range_v6")) - kwargs = _filter_network_create_options(args) - if args.multi_host is not None: - kwargs['multi_host'] = bool(args.multi_host == 'T' or - strutils.bool_from_string(args.multi_host)) - if args.enable_dhcp is not None: - kwargs['enable_dhcp'] = bool( - args.enable_dhcp == 'T' or - strutils.bool_from_string(args.enable_dhcp)) - if args.share_address is not None: - kwargs['share_address'] = bool( - args.share_address == 'T' or - strutils.bool_from_string(args.share_address)) - - cs.networks.create(**kwargs) - - def _extract_metadata(args): metadata = {} for metadatum in args.metadata[0]: @@ -2675,440 +2382,10 @@ def do_list_secgroup(cs, args): _print_secgroups(groups) -@utils.arg( - 'pool', - metavar='', - help=_('Name of Floating IP Pool. (Optional)'), - nargs='?', - default=None) -@deprecated_network -def do_floating_ip_create(cs, args): - """Allocate a floating IP for the current tenant.""" - _print_floating_ip_list([cs.floating_ips.create(pool=args.pool)]) - - -@utils.arg('address', metavar='
', help=_('IP of Floating IP.')) -@deprecated_network -def do_floating_ip_delete(cs, args): - """De-allocate a floating IP.""" - floating_ips = cs.floating_ips.list() - for floating_ip in floating_ips: - if floating_ip.ip == args.address: - return cs.floating_ips.delete(floating_ip.id) - raise exceptions.CommandError(_("Floating IP %s not found.") % - args.address) - - -@deprecated_network -def do_floating_ip_list(cs, _args): - """List floating IPs.""" - _print_floating_ip_list(cs.floating_ips.list()) - - -@deprecated_network -def do_floating_ip_pool_list(cs, _args): - """List all floating IP pools.""" - utils.print_list(cs.floating_ip_pools.list(), ['name']) - - -@utils.arg( - '--host', dest='host', metavar='', default=None, - help=_('Filter by host.')) -@deprecated_network -def do_floating_ip_bulk_list(cs, args): - """List all floating IPs (nova-network only).""" - utils.print_list(cs.floating_ips_bulk.list(args.host), ['project_id', - 'address', - 'instance_uuid', - 'pool', - 'interface']) - - -@utils.arg('ip_range', metavar='', - help=_('Address range to create.')) -@utils.arg( - '--pool', dest='pool', metavar='', default=None, - help=_('Pool for new Floating IPs.')) -@utils.arg( - '--interface', metavar='', default=None, - help=_('Interface for new Floating IPs.')) -@deprecated_network -def do_floating_ip_bulk_create(cs, args): - """Bulk create floating IPs by range (nova-network only).""" - cs.floating_ips_bulk.create(args.ip_range, args.pool, args.interface) - - -@utils.arg('ip_range', metavar='', - help=_('Address range to delete.')) -@deprecated_network -def do_floating_ip_bulk_delete(cs, args): - """Bulk delete floating IPs by range (nova-network only).""" - cs.floating_ips_bulk.delete(args.ip_range) - - -def _print_dns_list(dns_entries): - utils.print_list(dns_entries, ['ip', 'name', 'domain']) - - -def _print_domain_list(domain_entries): - utils.print_list(domain_entries, ['domain', 'scope', - 'project', 'availability_zone']) - - -@deprecated_network -def do_dns_domains(cs, args): - """Print a list of available dns domains.""" - domains = cs.dns_domains.domains() - _print_domain_list(domains) - - -@utils.arg('domain', metavar='', help=_('DNS domain.')) -@utils.arg('--ip', metavar='', help=_('IP address.'), default=None) -@utils.arg('--name', metavar='', help=_('DNS name.'), default=None) -@deprecated_network -def do_dns_list(cs, args): - """List current DNS entries for domain and IP or domain and name.""" - if not (args.ip or args.name): - raise exceptions.CommandError( - _("You must specify either --ip or --name")) - if args.name: - entry = cs.dns_entries.get(args.domain, args.name) - _print_dns_list([entry]) - else: - entries = cs.dns_entries.get_for_ip(args.domain, - ip=args.ip) - _print_dns_list(entries) - - -@utils.arg('ip', metavar='', help=_('IP address.')) -@utils.arg('name', metavar='', help=_('DNS name.')) -@utils.arg('domain', metavar='', help=_('DNS domain.')) -@utils.arg( - '--type', - metavar='', - help=_('DNS type (e.g. "A")'), - default='A') -@deprecated_network -def do_dns_create(cs, args): - """Create a DNS entry for domain, name, and IP.""" - cs.dns_entries.create(args.domain, args.name, args.ip, args.type) - - -@utils.arg('domain', metavar='', help=_('DNS domain.')) -@utils.arg('name', metavar='', help=_('DNS name.')) -@deprecated_network -def do_dns_delete(cs, args): - """Delete the specified DNS entry.""" - cs.dns_entries.delete(args.domain, args.name) - - -@utils.arg('domain', metavar='', help=_('DNS domain.')) -@deprecated_network -def do_dns_delete_domain(cs, args): - """Delete the specified DNS domain.""" - cs.dns_domains.delete(args.domain) - - -@utils.arg('domain', metavar='', help=_('DNS domain.')) -@utils.arg( - '--availability-zone', - metavar='', - default=None, - help=_('Limit access to this domain to servers ' - 'in the specified availability zone.')) -@deprecated_network -def do_dns_create_private_domain(cs, args): - """Create the specified DNS domain.""" - cs.dns_domains.create_private(args.domain, - args.availability_zone) - - -@utils.arg('domain', metavar='', help=_('DNS domain.')) -@utils.arg( - '--project', metavar='', - help=_('Limit access to this domain to users ' - 'of the specified project.'), - default=None) -@deprecated_network -def do_dns_create_public_domain(cs, args): - """Create the specified DNS domain.""" - cs.dns_domains.create_public(args.domain, - args.project) - - -def _print_secgroup_rules(rules, show_source_group=True): - class FormattedRule(object): - def __init__(self, obj): - items = (obj if isinstance(obj, dict) else obj.to_dict()).items() - for k, v in items: - if k == 'ip_range': - v = v.get('cidr') - elif k == 'group': - k = 'source_group' - v = v.get('name') - if v is None: - v = '' - - setattr(self, k, v) - - rules = [FormattedRule(rule) for rule in rules] - headers = ['IP Protocol', 'From Port', 'To Port', 'IP Range'] - if show_source_group: - headers.append('Source Group') - utils.print_list(rules, headers) - - def _print_secgroups(secgroups): utils.print_list(secgroups, ['Id', 'Name', 'Description']) -def _get_secgroup(cs, secgroup): - # Check secgroup is an ID (nova-network) or UUID (neutron) - if (utils.is_integer_like(encodeutils.safe_encode(secgroup)) or - uuidutils.is_uuid_like(secgroup)): - try: - return cs.security_groups.get(secgroup) - except exceptions.NotFound: - pass - - # Check secgroup as a name - match_found = False - for s in cs.security_groups.list(): - encoding = ( - locale.getpreferredencoding() or sys.stdin.encoding or 'UTF-8') - if not six.PY3: - s.name = s.name.encode(encoding) - if secgroup == s.name: - if match_found is not False: - msg = (_("Multiple security group matches found for name '%s'" - ", use an ID to be more specific.") % secgroup) - raise exceptions.NoUniqueMatch(msg) - match_found = s - if match_found is False: - raise exceptions.CommandError(_("Secgroup ID or name '%s' not found.") - % secgroup) - return match_found - - -@utils.arg( - 'secgroup', - metavar='', - help=_('ID or name of security group.')) -@utils.arg( - 'ip_proto', - metavar='', - help=_('IP protocol (icmp, tcp, udp).')) -@utils.arg( - 'from_port', - metavar='', - help=_('Port at start of range.')) -@utils.arg( - 'to_port', - metavar='', - help=_('Port at end of range.')) -@utils.arg('cidr', metavar='', help=_('CIDR for address range.')) -@deprecated_network -def do_secgroup_add_rule(cs, args): - """Add a rule to a security group.""" - secgroup = _get_secgroup(cs, args.secgroup) - rule = cs.security_group_rules.create(secgroup.id, - args.ip_proto, - args.from_port, - args.to_port, - args.cidr) - _print_secgroup_rules([rule]) - - -@utils.arg( - 'secgroup', - metavar='', - help=_('ID or name of security group.')) -@utils.arg( - 'ip_proto', - metavar='', - help=_('IP protocol (icmp, tcp, udp).')) -@utils.arg( - 'from_port', - metavar='', - help=_('Port at start of range.')) -@utils.arg( - 'to_port', - metavar='', - help=_('Port at end of range.')) -@utils.arg('cidr', metavar='', help=_('CIDR for address range.')) -@deprecated_network -def do_secgroup_delete_rule(cs, args): - """Delete a rule from a security group.""" - secgroup = _get_secgroup(cs, args.secgroup) - for rule in secgroup.rules: - if (rule['ip_protocol'] and - rule['ip_protocol'].upper() == args.ip_proto.upper() and - rule['from_port'] == int(args.from_port) and - rule['to_port'] == int(args.to_port) and - rule['ip_range']['cidr'] == args.cidr): - _print_secgroup_rules([rule]) - return cs.security_group_rules.delete(rule['id']) - - raise exceptions.CommandError(_("Rule not found")) - - -@utils.arg('name', metavar='', help=_('Name of security group.')) -@utils.arg( - 'description', metavar='', - help=_('Description of security group.')) -@deprecated_network -def do_secgroup_create(cs, args): - """Create a security group.""" - secgroup = cs.security_groups.create(args.name, args.description) - _print_secgroups([secgroup]) - - -@utils.arg( - 'secgroup', - metavar='', - help=_('ID or name of security group.')) -@utils.arg('name', metavar='', help=_('Name of security group.')) -@utils.arg( - 'description', metavar='', - help=_('Description of security group.')) -@deprecated_network -def do_secgroup_update(cs, args): - """Update a security group.""" - sg = _get_secgroup(cs, args.secgroup) - secgroup = cs.security_groups.update(sg, args.name, args.description) - _print_secgroups([secgroup]) - - -@utils.arg( - 'secgroup', - metavar='', - help=_('ID or name of security group.')) -@deprecated_network -def do_secgroup_delete(cs, args): - """Delete a security group.""" - secgroup = _get_secgroup(cs, args.secgroup) - cs.security_groups.delete(secgroup) - _print_secgroups([secgroup]) - - -@utils.arg( - '--all-tenants', - dest='all_tenants', - metavar='<0|1>', - nargs='?', - type=int, - const=1, - default=int(strutils.bool_from_string( - os.environ.get("ALL_TENANTS", 'false'), True)), - help=_('Display information from all tenants (Admin only).')) -@deprecated_network -def do_secgroup_list(cs, args): - """List security groups for the current tenant.""" - search_opts = {'all_tenants': args.all_tenants} - columns = ['Id', 'Name', 'Description'] - if args.all_tenants: - columns.append('Tenant_ID') - groups = cs.security_groups.list(search_opts=search_opts) - utils.print_list(groups, columns) - - -@utils.arg( - 'secgroup', - metavar='', - help=_('ID or name of security group.')) -@deprecated_network -def do_secgroup_list_rules(cs, args): - """List rules for a security group.""" - secgroup = _get_secgroup(cs, args.secgroup) - _print_secgroup_rules(secgroup.rules) - - -@utils.arg( - 'secgroup', - metavar='', - help=_('ID or name of security group.')) -@utils.arg( - 'source_group', - metavar='', - help=_('ID or name of source group.')) -@utils.arg( - 'ip_proto', - metavar='', - help=_('IP protocol (icmp, tcp, udp).')) -@utils.arg( - 'from_port', - metavar='', - help=_('Port at start of range.')) -@utils.arg( - 'to_port', - metavar='', - help=_('Port at end of range.')) -@deprecated_network -def do_secgroup_add_group_rule(cs, args): - """Add a source group rule to a security group.""" - secgroup = _get_secgroup(cs, args.secgroup) - source_group = _get_secgroup(cs, args.source_group) - params = {'group_id': source_group.id} - - if args.ip_proto or args.from_port or args.to_port: - if not (args.ip_proto and args.from_port and args.to_port): - raise exceptions.CommandError(_("ip_proto, from_port, and to_port" - " must be specified together")) - params['ip_protocol'] = args.ip_proto.upper() - params['from_port'] = args.from_port - params['to_port'] = args.to_port - - rule = cs.security_group_rules.create(secgroup.id, **params) - _print_secgroup_rules([rule]) - - -@utils.arg( - 'secgroup', - metavar='', - help=_('ID or name of security group.')) -@utils.arg( - 'source_group', - metavar='', - help=_('ID or name of source group.')) -@utils.arg( - 'ip_proto', - metavar='', - help=_('IP protocol (icmp, tcp, udp).')) -@utils.arg( - 'from_port', - metavar='', - help=_('Port at start of range.')) -@utils.arg( - 'to_port', - metavar='', - help=_('Port at end of range.')) -@deprecated_network -def do_secgroup_delete_group_rule(cs, args): - """Delete a source group rule from a security group.""" - secgroup = _get_secgroup(cs, args.secgroup) - source_group = _get_secgroup(cs, args.source_group) - params = {'group_name': source_group.name} - - if args.ip_proto or args.from_port or args.to_port: - if not (args.ip_proto and args.from_port and args.to_port): - raise exceptions.CommandError(_("ip_proto, from_port, and to_port" - " must be specified together")) - params['ip_protocol'] = args.ip_proto.upper() - params['from_port'] = int(args.from_port) - params['to_port'] = int(args.to_port) - - for rule in secgroup.rules: - if (rule.get('ip_protocol') and - rule['ip_protocol'].upper() == params.get( - 'ip_protocol').upper() and - rule.get('from_port') == params.get('from_port') and - rule.get('to_port') == params.get('to_port') and - rule.get('group', {}).get('name') == params.get('group_name')): - return cs.security_group_rules.delete(rule['id']) - - raise exceptions.CommandError(_("Rule not found")) - - @api_versions.wraps("2.0", "2.1") def _keypair_create(cs, args, name, pub_key): return cs.keypairs.create(name, pub_key) @@ -4030,40 +3307,6 @@ def do_service_delete(cs, args): cs.services.delete(args.id) -@api_versions.wraps("2.0", "2.3") -def _print_fixed_ip(cs, fixed_ip): - fields = ['address', 'cidr', 'hostname', 'host'] - utils.print_list([fixed_ip], fields) - - -@api_versions.wraps("2.4") -def _print_fixed_ip(cs, fixed_ip): - fields = ['address', 'cidr', 'hostname', 'host', 'reserved'] - utils.print_list([fixed_ip], fields) - - -@utils.arg('fixed_ip', metavar='', help=_('Fixed IP Address.')) -@deprecated_network -def do_fixed_ip_get(cs, args): - """Retrieve info on a fixed IP.""" - result = cs.fixed_ips.get(args.fixed_ip) - _print_fixed_ip(cs, result) - - -@utils.arg('fixed_ip', metavar='', help=_('Fixed IP Address.')) -@deprecated_network -def do_fixed_ip_reserve(cs, args): - """Reserve a fixed IP.""" - cs.fixed_ips.reserve(args.fixed_ip) - - -@utils.arg('fixed_ip', metavar='', help=_('Fixed IP Address.')) -@deprecated_network -def do_fixed_ip_unreserve(cs, args): - """Unreserve a fixed IP.""" - cs.fixed_ips.unreserve(args.fixed_ip) - - @utils.arg('host', metavar='', help=_('Name of host.')) def do_host_describe(cs, args): """Describe a specific host.""" @@ -5042,70 +4285,6 @@ def do_server_group_list(cs, args): _print_server_group_details(cs, server_groups) -@deprecated_network -def do_secgroup_list_default_rules(cs, args): - """List rules that will be added to the 'default' security group for - new tenants. - """ - _print_secgroup_rules(cs.security_group_default_rules.list(), - show_source_group=False) - - -@utils.arg( - 'ip_proto', - metavar='', - help=_('IP protocol (icmp, tcp, udp).')) -@utils.arg( - 'from_port', - metavar='', - help=_('Port at start of range.')) -@utils.arg( - 'to_port', - metavar='', - help=_('Port at end of range.')) -@utils.arg('cidr', metavar='', help=_('CIDR for address range.')) -@deprecated_network -def do_secgroup_add_default_rule(cs, args): - """Add a rule to the set of rules that will be added to the 'default' - security group for new tenants (nova-network only). - """ - rule = cs.security_group_default_rules.create(args.ip_proto, - args.from_port, - args.to_port, - args.cidr) - _print_secgroup_rules([rule], show_source_group=False) - - -@utils.arg( - 'ip_proto', - metavar='', - help=_('IP protocol (icmp, tcp, udp).')) -@utils.arg( - 'from_port', - metavar='', - help=_('Port at start of range.')) -@utils.arg( - 'to_port', - metavar='', - help=_('Port at end of range.')) -@utils.arg('cidr', metavar='', help=_('CIDR for address range.')) -@deprecated_network -def do_secgroup_delete_default_rule(cs, args): - """Delete a rule from the set of rules that will be added to the - 'default' security group for new tenants (nova-network only). - """ - for rule in cs.security_group_default_rules.list(): - if (rule.ip_protocol and - rule.ip_protocol.upper() == args.ip_proto.upper() and - rule.from_port == int(args.from_port) and - rule.to_port == int(args.to_port) and - rule.ip_range['cidr'] == args.cidr): - _print_secgroup_rules([rule], show_source_group=False) - return cs.security_group_default_rules.delete(rule.id) - - raise exceptions.CommandError(_("Rule not found")) - - @utils.arg('name', metavar='', help=_('Server group name.')) @utils.arg( 'policy', diff --git a/releasenotes/notes/pike-rm-deprecated-net-272aeb62b329a5bc.yaml b/releasenotes/notes/pike-rm-deprecated-net-272aeb62b329a5bc.yaml new file mode 100644 index 000000000..e2d9d7bf1 --- /dev/null +++ b/releasenotes/notes/pike-rm-deprecated-net-272aeb62b329a5bc.yaml @@ -0,0 +1,53 @@ +--- +prelude: > + Deprecated network-related resource commands have been removed. +upgrade: + - | + The following deprecated network-related resource commands have been + removed:: + + * nova dns-create + * nova dns-create-private-domain + * nova dns-create-public-domain + * nova dns-delete + * nova dns-delete-domain + * nova dns-domains + * nova dns-list + * nova fixed-ip-get + * nova fixed-ip-reserve + * nova fixed-ip-unreserve + * nova floating-ip-create + * nova floating-ip-delete + * nova floating-ip-list + * nova floating-ip-bulk-create + * nova floating-ip-bulk-delete + * nova floating-ip-bulk-list + * nova floating-ip-pool-list + * nova net + * nova net-create + * nova net-delete + * nova net-list + * nova network-create + * nova network-delete + * nova network-list + * nova network-show + * nova network-associate-host + * nova-network-associate-project + * nova network-disassociate + * nova scrub + * nova secgroup-create + * nova secgroup-delete + * nova secgroup-list + * nova secgroup-update + * nova secgroup-add-rule + * nova secgroup-delete-rule + * nova secgroup-list-rules + * nova secgroup-add-default-rule + * nova secgroup-delete-default-rule + * nova secgroup-list-default-rules + * nova secgroup-add-group-rule + * nova secgroup-delete-group-rule + * nova tenant-network-create + * nova tenant-network-delete + * nova tenant-network-list + * nova tenant-network-show From 1a9bfc424a59b4f2675d607881a5f0b2605a3f06 Mon Sep 17 00:00:00 2001 From: Diana Clarke Date: Wed, 22 Feb 2017 10:08:13 -0500 Subject: [PATCH 1239/1705] Fix devstack python-novaclient warning The following warning is emitted for every nova command using devstack, stable/ocata. site-packages/novaclient/client.py:278: UserWarning: The 'tenant_id' argument is deprecated in Ocata and its use may result in errors in future releases. As 'project_id' is provided, the 'tenant_id' argument will be ignored. See the following commit for related changes: Clarify meaning of project_id var 9bbe5a87b7df62a7962debba5db7c96555da6761 Change-Id: Ifa5ed16f10a43c9961e98b03fc0535e12d7977ba --- novaclient/shell.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/novaclient/shell.py b/novaclient/shell.py index 6883c9926..68f74d23e 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -840,8 +840,8 @@ def main(self, argv): # Recreate client object with discovered version. self.cs = client.Client( api_version, - os_username, os_password, os_project_name, - tenant_id=os_project_id, user_id=os_user_id, + os_username, os_password, project_id=os_project_id, + project_name=os_project_name, user_id=os_user_id, auth_url=os_auth_url, insecure=insecure, region_name=os_region_name, endpoint_type=endpoint_type, extensions=self.extensions, service_type=service_type, From 0a88bd439f138f3edeea589160cf7f9ce51aa752 Mon Sep 17 00:00:00 2001 From: Jeffrey Guan Date: Thu, 23 Feb 2017 13:47:26 +0000 Subject: [PATCH 1240/1705] Grammar typo in the comments for function set_meta. Change-Id: Ia62c8e37f240f0dfc00950f6ef6e1ecc4d5b9453 Closes-Bug: #1668336 --- novaclient/v2/servers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index 460355630..7a08a43a3 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -1631,9 +1631,9 @@ def backup(self, server, backup_name, backup_type, rotation): def set_meta(self, server, metadata): """ - Set a servers metadata + Set a server's metadata :param server: The :class:`Server` to add metadata to - :param metadata: A dict of metadata to add to the server + :param metadata: A dict of metadata to be added to the server """ body = {'metadata': metadata} return self._create("/servers/%s/metadata" % base.getid(server), From f6b1552780da18add9ab9b11d37c26d10d54ba3a Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Wed, 1 Mar 2017 04:16:08 +0000 Subject: [PATCH 1241/1705] Updated from global requirements Change-Id: Idaf27bd64f035e1cc7c919a99d0f87b466500b14 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 057327712..434f086b5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ keystoneauth1>=2.18.0 # Apache-2.0 iso8601>=0.1.11 # MIT oslo.i18n>=2.1.0 # Apache-2.0 oslo.serialization>=1.10.0 # Apache-2.0 -oslo.utils>=3.18.0 # Apache-2.0 +oslo.utils>=3.20.0 # Apache-2.0 PrettyTable<0.8,>=0.7.1 # BSD requests!=2.12.2,!=2.13.0,>=2.10.0 # Apache-2.0 simplejson>=2.2.0 # MIT From e08823198d143ac8ec3125ed66f6afad1555b6b2 Mon Sep 17 00:00:00 2001 From: Timofey Durakov Date: Wed, 1 Mar 2017 13:35:34 +0400 Subject: [PATCH 1242/1705] Deperecate cell_name cli arg for migration-list This patch adds deprecation warning for usage of cell_name argument in migration_list command. Change-Id: I54468682d5391668a513e708e26bc3c165c95ca1 Related-bug: #1668743 --- novaclient/tests/unit/v2/test_shell.py | 3 +-- novaclient/v2/migrations.py | 4 ++++ novaclient/v2/shell.py | 8 ++++++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 8ffcc8d84..b39ace07a 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2729,8 +2729,7 @@ def test_migration_list_with_filters(self): self.run_command('migration-list --host host1 --cell_name child1 ' '--status finished') self.assert_called('GET', - '/os-migrations?cell_name=child1&host=host1' - '&status=finished') + '/os-migrations?host=host1&status=finished') @mock.patch('novaclient.v2.shell._find_server') @mock.patch('os.system') diff --git a/novaclient/v2/migrations.py b/novaclient/v2/migrations.py index 51b5988b9..9ecc8b38a 100644 --- a/novaclient/v2/migrations.py +++ b/novaclient/v2/migrations.py @@ -17,6 +17,7 @@ from six.moves.urllib import parse from novaclient import base +from novaclient.i18n import _LW class Migration(base.Resource): @@ -40,6 +41,9 @@ def list(self, host=None, status=None, cell_name=None): if status: opts['status'] = status if cell_name: + self.client.logger.warning(_LW("Argument 'cell_name' is " + "deprecated since Pike, and will " + "be removed in a future release.")) opts['cell_name'] = cell_name # Transform the dict to a sequence of two-element tuples in fixed diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 4d6508fc6..d94644552 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -4772,8 +4772,12 @@ def migration_type(migration): '--cell_name', dest='cell_name', metavar='', - help=_('Fetch migrations for the given cell_name.')) + help=_('Fetch migrations for the given cell_name.'), + action=shell.DeprecatedAction, + real_action='nothing', + use=_('this option is not supported, and will be ' + 'removed after version 8.0.0.')) def do_migration_list(cs, args): """Print a list of migrations.""" - migrations = cs.migrations.list(args.host, args.status, args.cell_name) + migrations = cs.migrations.list(args.host, args.status) _print_migrations(cs, migrations) From 371b6b42d43be13ff8c6701bebf638d74e34d3a8 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Wed, 1 Mar 2017 14:32:33 -0500 Subject: [PATCH 1243/1705] Remove functional tests for removed commands Change adf7d1e48c619579c2fc430229173264db45033e removed the deprecated network proxy CLIs but missed a couple of functional tests. These were caught because the mitaka functional job runs nova-network which actually runs these tests, but is non-voting. The functional jobs that run on master are using neutron and skip these tests. Change-Id: I5f66905a315cb36f094e30e673629893cf4bdc49 Closes-Bug: #1669043 --- .../functional/v2/legacy/test_fixedips.py | 45 ------------------- .../v2/legacy/test_readonly_nova.py | 5 --- .../tests/functional/v2/test_fixedips.py | 23 ---------- 3 files changed, 73 deletions(-) delete mode 100644 novaclient/tests/functional/v2/legacy/test_fixedips.py delete mode 100644 novaclient/tests/functional/v2/test_fixedips.py diff --git a/novaclient/tests/functional/v2/legacy/test_fixedips.py b/novaclient/tests/functional/v2/legacy/test_fixedips.py deleted file mode 100644 index b752ce26a..000000000 --- a/novaclient/tests/functional/v2/legacy/test_fixedips.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright 2015 IBM Corp. -# 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 oslo_utils import strutils - -from novaclient.tests.functional import base - - -class TestFixedIPsNovaClient(base.ClientTestBase): - """FixedIPs functional tests.""" - - COMPUTE_API_VERSION = '2.1' - - def _test_fixedip_get(self, expect_reserved=False): - # os-fixed-ips does not proxy to neutron - self.skip_if_neutron() - server = self._create_server(with_network=False) - networks = server.networks - self.assertIn('private', networks) - fixed_ip = networks['private'][0] - table = self.nova('fixed-ip-get %s' % fixed_ip) - addr = self._get_column_value_from_single_row_table(table, 'address') - self.assertEqual(fixed_ip, addr) - if expect_reserved: - reserved = self._get_column_value_from_single_row_table(table, - 'reserved') - # By default the fixed IP should not be reserved. - self.assertFalse(strutils.bool_from_string(reserved, strict=True)) - else: - self.assertRaises(ValueError, - self._get_column_value_from_single_row_table, - table, 'reserved') - - def test_fixedip_get(self): - self._test_fixedip_get() diff --git a/novaclient/tests/functional/v2/legacy/test_readonly_nova.py b/novaclient/tests/functional/v2/legacy/test_readonly_nova.py index 8fed1588e..15ef9e213 100644 --- a/novaclient/tests/functional/v2/legacy/test_readonly_nova.py +++ b/novaclient/tests/functional/v2/legacy/test_readonly_nova.py @@ -41,11 +41,6 @@ def test_admin_availability_zone_list(self): def test_admin_cloudpipe_list(self): self.nova('cloudpipe-list') - # "Neutron does not provide this feature" - def test_admin_dns_domains(self): - self.skip_if_neutron() - self.nova('dns-domains') - @decorators.skip_because(bug="1157349") def test_admin_dns_list(self): self.skip_if_neutron() diff --git a/novaclient/tests/functional/v2/test_fixedips.py b/novaclient/tests/functional/v2/test_fixedips.py deleted file mode 100644 index 15d530d59..000000000 --- a/novaclient/tests/functional/v2/test_fixedips.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright 2015 IBM Corp. -# 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 novaclient.tests.functional.v2.legacy import test_fixedips - - -class TestFixedIPsNovaClientV24(test_fixedips.TestFixedIPsNovaClient): - """FixedIPs functional tests for v2.4 nova-api microversion.""" - - COMPUTE_API_VERSION = '2.4' - - def test_fixedip_get(self): - self._test_fixedip_get(expect_reserved=True) From 6ec83fbfa1050fd21c8cbe4f01d36d743da59900 Mon Sep 17 00:00:00 2001 From: Timofey Durakov Date: Thu, 2 Mar 2017 00:00:31 +0400 Subject: [PATCH 1244/1705] Release note for cell_name deprecation This is a follow up patch, that adds a release note about cell_name argument deprecation for migration-list Change-Id: Ia3c21c7697e2c64025d35c23ed32fecff9c076df Related-bug: #1668743 --- .../notes/deprecate_cell_name_arg-eb34cb7c43cfcb89.yaml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 releasenotes/notes/deprecate_cell_name_arg-eb34cb7c43cfcb89.yaml diff --git a/releasenotes/notes/deprecate_cell_name_arg-eb34cb7c43cfcb89.yaml b/releasenotes/notes/deprecate_cell_name_arg-eb34cb7c43cfcb89.yaml new file mode 100644 index 000000000..dbb8ae7ee --- /dev/null +++ b/releasenotes/notes/deprecate_cell_name_arg-eb34cb7c43cfcb89.yaml @@ -0,0 +1,6 @@ +--- +deprecations: + - CLI argument ``--cell_name`` for ``nova migration-list`` command is + deprecated. Nova API does not have logic for handling cell_name + parameter in **os-migrations**, and while the parameter is passed to Nova + it has never been used. From 7d79428b17dc22849a24390ced92787e2a819dea Mon Sep 17 00:00:00 2001 From: ricolin Date: Thu, 2 Mar 2017 20:28:16 +0800 Subject: [PATCH 1245/1705] [Fix gate]Update test requirement Since pbr already landed and the old version of hacking seems not work very well with pbr>=2, we should update it to match global requirement. Partial-Bug: #1668848 Change-Id: Idc1755ccd8d477ffd1ff0febf586238249f45c60 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 77d4df890..28bc527fe 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,7 +1,7 @@ # 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. -hacking<0.11,>=0.10.0 +hacking>=0.12.0,!=0.13.0,<0.14 # Apache-2.0 bandit>=1.1.0 # Apache-2.0 coverage>=4.0 # Apache-2.0 From 07971152413c3aa94e87b16f562acf686b9c5f48 Mon Sep 17 00:00:00 2001 From: Kevin_Zheng Date: Fri, 3 Mar 2017 14:54:11 +0800 Subject: [PATCH 1246/1705] Tags and Metadata fields with unicode cannot be correctly displayed Currently, Tags(list) and Metadata(dict) fields of instance will firstly transformed to str using jsondump first when display. And cannot be correctly transfomed and displayed afterwards. This patch adds ensure_ascii=False to the transform function thus those fields can be correctly tranformed and displayed afterwards. Change-Id: Ib4e7a34f3b19db89280cc73053acbac8c8816f85 Closes-Bug: #1669683 --- .../tests/functional/v2/test_servers.py | 19 +++++++++++++++++-- novaclient/utils.py | 2 +- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/novaclient/tests/functional/v2/test_servers.py b/novaclient/tests/functional/v2/test_servers.py index 6adf1e052..7027413de 100644 --- a/novaclient/tests/functional/v2/test_servers.py +++ b/novaclient/tests/functional/v2/test_servers.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # 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 @@ -134,9 +135,9 @@ class TestServersTagsV226(base.ClientTestBase): COMPUTE_API_VERSION = "2.26" - def _boot_server_with_tags(self): + def _boot_server_with_tags(self, tags=["t1", "t2"]): uuid = self._create_server().id - self.client.servers.set_tags(uuid, ["t1", "t2"]) + self.client.servers.set_tags(uuid, tags) return uuid def test_show(self): @@ -145,6 +146,20 @@ def test_show(self): self.assertEqual('["t1", "t2"]', self._get_value_from_the_table( output, "tags")) + def test_unicode_tag_correctly_displayed(self): + """Regression test for bug #1669683. + + List and dict fields with unicode cannot be correctly + displayed. + + Ensure that once we fix this it doesn't regress. + """ + # create an instance with chinese tag + uuid = self._boot_server_with_tags(tags=["中文标签"]) + output = self.nova("show %s" % uuid) + self.assertEqual('["中文标签"]', self._get_value_from_the_table( + output, "tags")) + def test_list(self): uuid = self._boot_server_with_tags() output = self.nova("server-tag-list %s" % uuid) diff --git a/novaclient/utils.py b/novaclient/utils.py index 7486ab29c..e93df6999 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -246,7 +246,7 @@ def print_dict(d, dict_property="Property", dict_value="Value", wrap=0): for k, v in sorted(d.items()): # convert dict to str to check length if isinstance(v, (dict, list)): - v = jsonutils.dumps(v) + v = jsonutils.dumps(v, ensure_ascii=False) if wrap > 0: v = textwrap.fill(six.text_type(v), wrap) # if value has a newline, add in multiple rows From 45c501ff515b098894e4a1ffa809200b2df07701 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Mon, 6 Mar 2017 01:18:45 +0000 Subject: [PATCH 1247/1705] Updated from global requirements Change-Id: I8f32fd359720c73d3a4ee8f87a6ce7764b921021 --- requirements.txt | 2 +- setup.py | 2 +- test-requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 434f086b5..76a0ba1f9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ # 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. -pbr>=1.8 # Apache-2.0 +pbr>=2.0.0 # Apache-2.0 keystoneauth1>=2.18.0 # Apache-2.0 iso8601>=0.1.11 # MIT oslo.i18n>=2.1.0 # Apache-2.0 diff --git a/setup.py b/setup.py index 782bb21f0..566d84432 100644 --- a/setup.py +++ b/setup.py @@ -25,5 +25,5 @@ pass setuptools.setup( - setup_requires=['pbr>=1.8'], + setup_requires=['pbr>=2.0.0'], pbr=True) diff --git a/test-requirements.txt b/test-requirements.txt index 28bc527fe..af978c722 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,7 +1,7 @@ # 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. -hacking>=0.12.0,!=0.13.0,<0.14 # Apache-2.0 +hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0 bandit>=1.1.0 # Apache-2.0 coverage>=4.0 # Apache-2.0 From 72a0ec9adcd19a96644638e8324566fcc4af8799 Mon Sep 17 00:00:00 2001 From: Dinesh Bhor Date: Tue, 14 Feb 2017 13:10:33 +0530 Subject: [PATCH 1248/1705] Fix ValueError when incorrect metadata passed If you pass incorrect formatted metadata to the 'boot', 'rebuild' and 'image-create' apis returns following error: ERROR (ValueError): dictionary update sequence element #0 has length 1; 2 is required Caught the ValuError and raised argparse.ArgumentTypeError with proper error message. Closes-Bug: #1668549 Change-Id: I14a30b93f4a916fc04610f9e475c12eb352e38c5 --- novaclient/tests/unit/v2/test_shell.py | 22 ++++++++++++++++++++++ novaclient/v2/shell.py | 6 +++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index b39ace07a..aece4422d 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -614,6 +614,14 @@ def test_boot_metadata(self): }}, ) + def test_boot_with_incorrect_metadata(self): + cmd = ('boot --image %s --flavor 1 --meta foo ' + 'some-server ' % FAKE_UUID_1) + result = self.assertRaises(argparse.ArgumentTypeError, + self.run_command, cmd) + expected = "'['foo']' is not in the format of 'key=value'" + self.assertEqual(expected, result.args[0]) + def test_boot_hints(self): self.run_command('boot --image %s --flavor 1 ' '--hint a=b0=c0 --hint a=b1=c1 some-server ' % @@ -1206,6 +1214,13 @@ def test_create_image(self): {'createImage': {'name': 'mysnapshot', 'metadata': {}}}, ) + def test_create_image_with_incorrect_metadata(self): + cmd = 'image-create sample-server mysnapshot --metadata foo' + result = self.assertRaises(argparse.ArgumentTypeError, + self.run_command, cmd) + expected = "'['foo']' is not in the format of 'key=value'" + self.assertEqual(expected, result.args[0]) + def test_create_image_with_metadata(self): self.run_command( 'image-create sample-server mysnapshot --metadata mykey=123') @@ -1442,6 +1457,13 @@ def test_rebuild_name_meta(self): self.assert_called('GET', '/flavors/1', pos=4) self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=5) + def test_rebuild_with_incorrect_metadata(self): + cmd = 'rebuild sample-server %s --name asdf --meta foo' % FAKE_UUID_1 + result = self.assertRaises(argparse.ArgumentTypeError, + self.run_command, cmd) + expected = "'['foo']' is not in the format of 'key=value'" + self.assertEqual(expected, result.args[0]) + def test_start(self): self.run_command('start sample-server') self.assert_called('POST', '/servers/1234/action', {'os-start': None}) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index d94644552..a1485d9c3 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -75,7 +75,11 @@ def _key_value_pairing(text): def _meta_parsing(metadata): - return dict(v.split('=', 1) for v in metadata) + try: + return dict(v.split('=', 1) for v in metadata) + except ValueError: + msg = _("'%s' is not in the format of 'key=value'") % metadata + raise argparse.ArgumentTypeError(msg) def _match_image(cs, wanted_properties): From 291865587793e4abad30363ae2b436a581da1875 Mon Sep 17 00:00:00 2001 From: dineshbhor Date: Fri, 10 Mar 2017 16:53:08 +0530 Subject: [PATCH 1249/1705] Remove duplicate methods TrivialFix Change-Id: Idb938e09516f24a6827f613d58ee0f1e310c79df --- novaclient/tests/unit/v2/fakes.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 80b2d2bfc..4f75c2a28 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -2017,23 +2017,6 @@ def post_os_networks_1_action(self, **kw): def post_os_networks_2_action(self, **kw): return (202, {}, None) - def get_os_tenant_networks(self, **kw): - return (200, {}, {'networks': [{"label": "1", "cidr": "10.0.0.0/24", - 'project_id': - '4ffc664c198e435e9853f2538fbcd7a7', - 'id': '1', 'vlan': '1234'}]}) - - def get_os_tenant_networks_1(self, **kw): - return (200, {}, {'network': {"label": "1", "cidr": "10.0.0.0/24", - "id": "1"}}) - - def post_os_tenant_networks(self, **kw): - return (202, {}, {'network': {"label": "new_network1", - "cidr1": "10.0.1.0/24"}}) - - def delete_os_tenant_networks_1(self, **kw): - return (202, {}, None) - def get_os_availability_zone_detail(self, **kw): return (200, {}, { "availabilityZoneInfo": [ From f1d2641396a5f31faf972dc8210b41ed14876682 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 16 Mar 2017 16:16:44 +0000 Subject: [PATCH 1250/1705] Use Sphinx 1.5 warning-is-error With pbr 2.0 and Sphinx 1.5, the setting for treat sphinx warnings as errors is setting warning-is-error in build_sphinx section. Migrate the setting from the old warnerrors one. There are only three related fixes required. Change-Id: I84b020c18a8a1c7c11a35bacd61523bbb094abe4 --- doc/source/shell.rst | 12 ++++++------ setup.cfg | 6 ++---- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/doc/source/shell.rst b/doc/source/shell.rst index 368195f7d..6f72c3a38 100644 --- a/doc/source/shell.rst +++ b/doc/source/shell.rst @@ -10,10 +10,10 @@ from the command line. It supports the entirety of the OpenStack Nova API. First, you'll need an OpenStack Nova account and an API key. You get this by using the `nova-manage` command in OpenStack Nova. -You'll need to provide :program:`nova` with your OpenStack username and -API key. You can do this with the :option:`--os-username`, :option:`--os-password` -and :option:`--os-tenant-id` options, but it's easier to just set them as -environment variables by setting two environment variables: +You'll need to provide :program:`nova` with your OpenStack username and API +key. You can do this with the `--os-username`, `--os-password` and +`--os-tenant-id` options, but it's easier to just set them as environment +variables by setting two environment variables: .. envvar:: OS_USERNAME @@ -42,9 +42,9 @@ For example, in Bash you'd use:: export OS_TENANT_NAME=myproject export OS_AUTH_URL=http://... export OS_COMPUTE_API_VERSION=2 - + From there, all shell commands take the form:: - + nova [arguments...] Run :program:`nova help` to get a full list of all possible commands, diff --git a/setup.cfg b/setup.cfg index 0e0dd0a9f..dc0bc305f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -31,9 +31,10 @@ console_scripts = nova = novaclient.shell:main [build_sphinx] +all-files = 1 +warning-is-error = 1 source-dir = doc/source build-dir = doc/build -all_files = 1 [upload_sphinx] upload-dir = doc/build/html @@ -54,6 +55,3 @@ output_file = novaclient/locale/novaclient.pot [wheel] universal = 1 - -[pbr] -warnerrors = true From 20f00553d0d8ed791105d5f7fc8798b9ac6a6a53 Mon Sep 17 00:00:00 2001 From: Dan Smith Date: Fri, 17 Mar 2017 07:11:38 -0700 Subject: [PATCH 1251/1705] Fix aggregate_update name and availability_zone clash The name and availability_zone arguments to aggregate update were replaced by optional parameters in change I778ab7ec54a376c60f19dcc89fe62fcab6e59e42. However, the '--name' and 'name' arguments in the parser would conflict, resulting in only the deprecated argument working. Thus, attempting to update the name on an aggregate using --name would end up doing a PUT with no new name provided. Note that there were unit tests for this, but they were not catching this problem. So, this removes those tests and adds functional tests to poke it. Change-Id: Ifef6fdc1a737dd219712a4525d4e34afd3fbd80c Closes-Bug: #1673789 --- .../tests/functional/v2/test_aggregates.py | 64 +++++++++++++++++++ novaclient/tests/unit/v2/test_shell.py | 24 ------- novaclient/v2/shell.py | 14 ++-- 3 files changed, 72 insertions(+), 30 deletions(-) create mode 100644 novaclient/tests/functional/v2/test_aggregates.py diff --git a/novaclient/tests/functional/v2/test_aggregates.py b/novaclient/tests/functional/v2/test_aggregates.py new file mode 100644 index 000000000..6eb30242e --- /dev/null +++ b/novaclient/tests/functional/v2/test_aggregates.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +# 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 oslo_utils import uuidutils + +from novaclient.tests.functional import base + + +class TestAggregatesNovaClient(base.ClientTestBase): + COMPUTE_API_VERSION = '2.1' + + def setUp(self): + super(TestAggregatesNovaClient, self).setUp() + self.agg1 = 'agg-%s' % uuidutils.generate_uuid() + self.agg2 = 'agg-%s' % uuidutils.generate_uuid() + self.addCleanup(self._clean_aggregates) + + def _clean_aggregates(self): + for a in (self.agg1, self.agg2): + try: + self.nova('aggregate-delete', params=a) + except Exception: + pass + + def test_aggregate_update_name_legacy(self): + self.nova('aggregate-create', params=self.agg1) + self.nova('aggregate-update', params='%s %s' % (self.agg1, self.agg2)) + output = self.nova('aggregate-show', params=self.agg2) + self.assertIn(self.agg2, output) + self.nova('aggregate-delete', params=self.agg2) + + def test_aggregate_update_name(self): + self.nova('aggregate-create', params=self.agg1) + self.nova('aggregate-update', + params='--name=%s %s' % (self.agg2, self.agg1)) + output = self.nova('aggregate-show', params=self.agg2) + self.assertIn(self.agg2, output) + self.nova('aggregate-delete', params=self.agg2) + + def test_aggregate_update_az_legacy(self): + self.nova('aggregate-create', params=self.agg2) + self.nova('aggregate-update', + params='%s %s myaz' % (self.agg2, self.agg2)) + output = self.nova('aggregate-show', params=self.agg2) + self.assertIn('myaz', output) + self.nova('aggregate-delete', params=self.agg2) + + def test_aggregate_update_az(self): + self.nova('aggregate-create', params=self.agg2) + self.nova('aggregate-update', + params='--availability-zone=myaz %s' % self.agg2) + output = self.nova('aggregate-show', params=self.agg2) + self.assertIn('myaz', output) + self.nova('aggregate-delete', params=self.agg2) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index aece4422d..9b1503fa4 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -1910,30 +1910,6 @@ def test_aggregate_update_with_availability_zone_by_name(self): self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) self.assert_called('GET', '/os-aggregates/1', pos=-1) - def test_aggregate_update_by_id_legacy(self): - self.run_command('aggregate-update 1 new_name') - body = {"aggregate": {"name": "new_name"}} - self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) - self.assert_called('GET', '/os-aggregates/1', pos=-1) - - def test_aggregate_update_by_name_legacy(self): - self.run_command('aggregate-update test new_name') - body = {"aggregate": {"name": "new_name"}} - self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) - self.assert_called('GET', '/os-aggregates/1', pos=-1) - - def test_aggregate_update_with_availability_zone_by_id_legacy(self): - self.run_command('aggregate-update 1 foo new_zone') - body = {"aggregate": {"name": "foo", "availability_zone": "new_zone"}} - self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) - self.assert_called('GET', '/os-aggregates/1', pos=-1) - - def test_aggregate_update_with_availability_zone_by_name_legacy(self): - self.run_command('aggregate-update test foo new_zone') - body = {"aggregate": {"name": "foo", "availability_zone": "new_zone"}} - self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) - self.assert_called('GET', '/os-aggregates/1', pos=-1) - def test_aggregate_set_metadata_add_by_id(self): out, err = self.run_command('aggregate-set-metadata 3 foo=bar') body = {"set_metadata": {"metadata": {"foo": "bar"}}} diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index a1485d9c3..6339a6432 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -2972,7 +2972,8 @@ def do_aggregate_delete(cs, args): metavar='', help=_('Name or ID of aggregate to update.')) @utils.arg( - 'name', + 'old_name', + metavar='', nargs='?', action=shell.DeprecatedAction, use=_('use "%s"; this option will be removed in ' @@ -2983,7 +2984,7 @@ def do_aggregate_delete(cs, args): dest='name', help=_('Name of aggregate.')) @utils.arg( - 'availability_zone', + 'old_availability_zone', metavar='', nargs='?', default=None, @@ -3000,10 +3001,11 @@ def do_aggregate_update(cs, args): """Update the aggregate's name and optionally availability zone.""" aggregate = _find_aggregate(cs, args.aggregate) updates = {} - if args.name: - updates["name"] = args.name - if args.availability_zone: - updates["availability_zone"] = args.availability_zone + if args.name or args.old_name: + updates["name"] = args.name or args.old_name + if args.availability_zone or args.old_availability_zone: + updates["availability_zone"] = (args.availability_zone or + args.old_availability_zone) aggregate = cs.aggregates.update(aggregate.id, updates) print(_("Aggregate %s has been successfully updated.") % aggregate.id) From 4746e0bb5850d8fbf5cbf5f0a1d25b53a713bf65 Mon Sep 17 00:00:00 2001 From: Eric Brown Date: Tue, 7 Mar 2017 14:43:01 -0800 Subject: [PATCH 1252/1705] Remove py34 tox env and pypi classifier Currently only py27 and py35 (not py34) is tested in the gate, so py34 should no longer be part of the tox environment or part of the PyPi classifier. Change-Id: I81058c5df47c4dae3c331d0030141896b6a9f1bb --- setup.cfg | 1 - tox.ini | 8 ++++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/setup.cfg b/setup.cfg index dc0bc305f..ef9c9551e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -19,7 +19,6 @@ classifier = Programming Language :: Python :: 2 Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 - Programming Language :: Python :: 3.4 Programming Language :: Python :: 3.5 [files] diff --git a/tox.ini b/tox.ini index fdc89d520..48351b605 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ -# noted to use py34 you need virtualenv >= 1.11.4 +# noted to use py35 you need virtualenv >= 1.11.4 [tox] -envlist = py35,py34,py27,pypy,pep8,docs +envlist = py35,py27,pypy,pep8,docs minversion = 2.0 skipsdist = True @@ -48,8 +48,8 @@ setenv = OS_TEST_PATH = ./novaclient/tests/functional commands = bash tools/pretty_tox.sh '--concurrency=1 {posargs}' -[testenv:functional-py34] -basepython = python3.4 +[testenv:functional-py35] +basepython = python3.5 passenv = OS_NOVACLIENT_TEST_NETWORK setenv = {[testenv]setenv} From dee51a0163dee4dff9aca5da82ca1a6c8a9911b7 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Mon, 20 Mar 2017 15:26:09 -0400 Subject: [PATCH 1253/1705] Remove deprecated baremetal CLIs and APIs These were all deprecated in Newton with change: 87c1b5311b2e641564305a4753b989b6498775b1 So now we can remove them. Change-Id: Ic74a989b8a6565cc52ab82440fab8605a4b78e6b --- novaclient/client.py | 3 +- .../tests/unit/v2/contrib/test_baremetal.py | 57 ----- novaclient/tests/unit/v2/fakes.py | 44 ---- novaclient/v2/client.py | 4 +- novaclient/v2/contrib/__init__.py | 6 +- novaclient/v2/contrib/baremetal.py | 196 ------------------ ...rm-baremetal-cli-api-fbc8c242d48cd2fb.yaml | 7 + 7 files changed, 13 insertions(+), 304 deletions(-) delete mode 100644 novaclient/tests/unit/v2/contrib/test_baremetal.py delete mode 100644 novaclient/v2/contrib/baremetal.py create mode 100644 releasenotes/notes/rm-baremetal-cli-api-fbc8c242d48cd2fb.yaml diff --git a/novaclient/client.py b/novaclient/client.py index 9ca9658c7..aac0e8eb2 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -206,8 +206,7 @@ def _discover_via_python_path(): def _discover_via_contrib_path(version): if version.ver_major == 2: - modules = {"baremetal": "novaclient.v2.contrib.baremetal", - "tenant_networks": "novaclient.v2.contrib.tenant_networks"} + modules = {"tenant_networks": "novaclient.v2.contrib.tenant_networks"} for name, module_name in modules.items(): module_loader = pkgutil.get_loader(module_name) diff --git a/novaclient/tests/unit/v2/contrib/test_baremetal.py b/novaclient/tests/unit/v2/contrib/test_baremetal.py deleted file mode 100644 index 512245402..000000000 --- a/novaclient/tests/unit/v2/contrib/test_baremetal.py +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright 2013 Hewlett-Packard Development Company, L.P. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - - -import warnings - -import mock - -from novaclient import api_versions -from novaclient import extension -from novaclient.tests.unit import utils -from novaclient.tests.unit.v2 import fakes -from novaclient.v2.contrib import baremetal - - -@mock.patch.object(warnings, 'warn') -class BaremetalExtensionTest(utils.TestCase): - def setUp(self): - super(BaremetalExtensionTest, self).setUp() - extensions = [ - extension.Extension(baremetal.__name__.split(".")[-1], baremetal), - ] - self.cs = fakes.FakeClient(api_versions.APIVersion("2.0"), - extensions=extensions) - - def test_list_nodes(self, mock_warn): - nl = self.cs.baremetal.list() - self.assert_request_id(nl, fakes.FAKE_REQUEST_ID_LIST) - self.cs.assert_called('GET', '/os-baremetal-nodes') - for n in nl: - self.assertIsInstance(n, baremetal.BareMetalNode) - self.assertEqual(1, mock_warn.call_count) - - def test_get_node(self, mock_warn): - n = self.cs.baremetal.get(1) - self.assert_request_id(n, fakes.FAKE_REQUEST_ID_LIST) - self.cs.assert_called('GET', '/os-baremetal-nodes/1') - self.assertIsInstance(n, baremetal.BareMetalNode) - self.assertEqual(1, mock_warn.call_count) - - def test_node_list_interfaces(self, mock_warn): - il = self.cs.baremetal.list_interfaces(1) - self.assert_request_id(il, fakes.FAKE_REQUEST_ID_LIST) - self.cs.assert_called('GET', '/os-baremetal-nodes/1') - self.assertEqual(1, mock_warn.call_count) diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 4f75c2a28..ef5c6014b 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -2350,50 +2350,6 @@ def post_os_tenant_networks(self, **kw): def delete_os_tenant_networks_1(self, **kw): return (204, FAKE_RESPONSE_HEADERS, None) - def get_os_baremetal_nodes(self, **kw): - return ( - 200, FAKE_RESPONSE_HEADERS, { - 'nodes': [ - { - "id": 1, - "instance_uuid": None, - "interfaces": [], - "cpus": 2, - "local_gb": 10, - "memory_mb": 5, - "pm_address": "2.3.4.5", - "pm_user": "pmuser", - "pm_password": "pmpass", - "prov_mac_address": "aa:bb:cc:dd:ee:ff", - "prov_vlan_id": 1, - "service_host": "somehost", - "terminal_port": 8080, - } - ] - } - ) - - def get_os_baremetal_nodes_1(self, **kw): - return ( - 200, FAKE_RESPONSE_HEADERS, { - 'node': { - "id": 1, - "instance_uuid": None, - "pm_address": "1.2.3.4", - "interfaces": [], - "cpus": 2, - "local_gb": 10, - "memory_mb": 5, - "pm_user": "pmuser", - "pm_password": "pmpass", - "prov_mac_address": "aa:bb:cc:dd:ee:ff", - "prov_vlan_id": 1, - "service_host": "somehost", - "terminal_port": 8080, - } - } - ) - def post_os_assisted_volume_snapshots(self, **kw): return (202, FAKE_RESPONSE_HEADERS, {'snapshot': {'id': 'blah', 'volumeId': '1'}}) diff --git a/novaclient/v2/client.py b/novaclient/v2/client.py index 2db2f221e..d216a94ae 100644 --- a/novaclient/v2/client.py +++ b/novaclient/v2/client.py @@ -196,8 +196,8 @@ def __init__(self, server_migrations.ServerMigrationsManager(self) # V2.0 extensions: - # NOTE(andreykurilin): baremetal and tenant_networks extensions are - # deprecated now, which is why they are not initialized by default. + # NOTE(andreykurilin): tenant_networks extension is + # deprecated now, which is why it is not initialized by default. self.assisted_volume_snapshots = \ assisted_volume_snapshots.AssistedSnapshotManager(self) self.cells = cells.CellsManager(self) diff --git a/novaclient/v2/contrib/__init__.py b/novaclient/v2/contrib/__init__.py index 80acf65f5..eb225b881 100644 --- a/novaclient/v2/contrib/__init__.py +++ b/novaclient/v2/contrib/__init__.py @@ -15,9 +15,9 @@ from novaclient.i18n import _LW -# NOTE(andreykurilin): "baremetal" and "tenant_networks" extensions excluded -# here deliberately. They were deprecated separately from deprecation -# extension mechanism and I prefer to not auto-load them by default +# NOTE(andreykurilin): "tenant_networks" extension excluded +# here deliberately. It was deprecated separately from deprecation +# extension mechanism and I prefer to not auto-load it by default # (V2_0_EXTENSIONS is designed for such behaviour). V2_0_EXTENSIONS = { 'assisted_volume_snapshots': diff --git a/novaclient/v2/contrib/baremetal.py b/novaclient/v2/contrib/baremetal.py deleted file mode 100644 index 57933891d..000000000 --- a/novaclient/v2/contrib/baremetal.py +++ /dev/null @@ -1,196 +0,0 @@ -# Copyright 2013 Hewlett-Packard Development Company, L.P. -# All Rights Reserved. -# -# 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. - -""" -Baremetal interface (v2 extension). -""" - -from __future__ import print_function - -import sys -import warnings - -from novaclient import api_versions -from novaclient import base -from novaclient.i18n import _ -from novaclient import utils - - -DEPRECATION_WARNING = ( - 'The novaclient.v2.contrib.baremetal module is deprecated and ' - 'will be removed after Nova 15.0.0 is released. Use ' - 'python-ironicclient or openstacksdk instead.') - - -def _emit_deprecation_warning(command_name): - print('WARNING: Command %s is deprecated and will be removed after Nova ' - '15.0.0 is released. Use python-ironicclient or ' - 'python-openstackclient instead.' % command_name, file=sys.stderr) - - -class BareMetalNode(base.Resource): - """ - DEPRECATED: A baremetal node (typically a physical server or an - empty VM). - """ - - def __repr__(self): - return "" % self.id - - -class BareMetalNodeInterface(base.Resource): - """ - An interface belonging to a baremetal node. - """ - - def __repr__(self): - return "" % self.id - - -class BareMetalNodeManager(base.ManagerWithFind): - """ - DEPRECATED: Manage :class:`BareMetalNode` resources. - """ - resource_class = BareMetalNode - - @api_versions.wraps('2.0', '2.35') - def get(self, node_id): - """ - DEPRECATED: Get a baremetal node. - - :param node_id: The ID of the node to delete. - :rtype: :class:`BareMetalNode` - """ - warnings.warn(DEPRECATION_WARNING, DeprecationWarning) - return self._get("/os-baremetal-nodes/%s" % node_id, 'node') - - @api_versions.wraps('2.0', '2.35') - def list(self): - """ - DEPRECATED: Get a list of all baremetal nodes. - - :rtype: list of :class:`BareMetalNode` - """ - warnings.warn(DEPRECATION_WARNING, DeprecationWarning) - return self._list('/os-baremetal-nodes', 'nodes') - - @api_versions.wraps('2.0', '2.35') - def list_interfaces(self, node_id): - """ - DEPRECATED: List the interfaces on a baremetal node. - - :param node_id: The ID of the node to list. - :rtype: novaclient.base.ListWithMeta - """ - warnings.warn(DEPRECATION_WARNING, DeprecationWarning) - interfaces = base.ListWithMeta([], None) - node = self._get("/os-baremetal-nodes/%s" % node_id, 'node') - interfaces.append_request_ids(node.request_ids) - for interface in node.interfaces: - interface_object = BareMetalNodeInterface(self, interface) - interfaces.append(interface_object) - return interfaces - - -def _translate_baremetal_node_keys(collection): - convert = [('service_host', 'host'), - ('local_gb', 'disk_gb'), - ('prov_mac_address', 'mac_address'), - ('pm_address', 'pm_address'), - ('pm_user', 'pm_username'), - ('pm_password', 'pm_password'), - ('terminal_port', 'terminal_port'), - ] - for item in collection: - keys = item.__dict__.keys() - for from_key, to_key in convert: - if from_key in keys and to_key not in keys: - setattr(item, to_key, item._info[from_key]) - - -def _print_baremetal_nodes_list(nodes): - """Print the list of baremetal nodes.""" - - def _parse_address(fields): - macs = [] - for interface in fields.interfaces: - macs.append(interface['address']) - return ', '.join("%s" % i for i in macs) - - formatters = { - 'MAC Address': _parse_address - } - - _translate_baremetal_node_keys(nodes) - utils.print_list(nodes, [ - 'ID', - 'Host', - 'Task State', - 'CPUs', - 'Memory_MB', - 'Disk_GB', - 'MAC Address', - 'PM Address', - 'PM Username', - 'PM Password', - 'Terminal Port', - ], formatters=formatters) - - -def do_baremetal_node_list(cs, _args): - """DEPRECATED: Print list of available baremetal nodes.""" - _emit_deprecation_warning('baremetal-node-list') - nodes = cs.baremetal.list() - _print_baremetal_nodes_list(nodes) - - -def _find_baremetal_node(cs, node): - """Get a node by ID.""" - return utils.find_resource(cs.baremetal, node) - - -def _print_baremetal_resource(resource): - """Print details of a baremetal resource.""" - info = resource._info.copy() - utils.print_dict(info) - - -def _print_baremetal_node_interfaces(interfaces): - """Print interfaces of a baremetal node.""" - utils.print_list(interfaces, [ - 'ID', - 'Datapath_ID', - 'Port_No', - 'Address', - ]) - - -@utils.arg( - 'node', - metavar='', - help=_("ID of node")) -def do_baremetal_node_show(cs, args): - """DEPRECATED: Show information about a baremetal node.""" - _emit_deprecation_warning('baremetal-node-show') - node = _find_baremetal_node(cs, args.node) - _print_baremetal_resource(node) - - -@utils.arg('node', metavar='', help=_("ID of node")) -def do_baremetal_interface_list(cs, args): - """DEPRECATED: List network interfaces associated with a baremetal node.""" - _emit_deprecation_warning('baremetal-interface-list') - interfaces = cs.baremetal.list_interfaces(args.node) - _print_baremetal_node_interfaces(interfaces) diff --git a/releasenotes/notes/rm-baremetal-cli-api-fbc8c242d48cd2fb.yaml b/releasenotes/notes/rm-baremetal-cli-api-fbc8c242d48cd2fb.yaml new file mode 100644 index 000000000..d35a7349c --- /dev/null +++ b/releasenotes/notes/rm-baremetal-cli-api-fbc8c242d48cd2fb.yaml @@ -0,0 +1,7 @@ +--- +upgrade: + - | + The baremetal CLIs and python API bindings were deprecated in the Newton + release and have been removed. Use python-openstackclient or + python-ironicclient for CLIs. Use python-ironicclient or openstacksdk for + python API bindings \ No newline at end of file From ac7c96690f16aafe5fec6cfdb4a7dea60278dd47 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Mon, 20 Mar 2017 15:54:28 -0400 Subject: [PATCH 1254/1705] Remove deprecated tenant network APIs These were deprecated in Newton: aaebeb05a03e34281a091dc6dfc4672b01cdfbbb Since this is the last of the deprecated contrib extensions, we can also deprecate the 'only_contrib' parameter from the novaclient.client.discover_extensions method. Change-Id: Ie2e3fdc4e044f6eb304724d16a7d0f1f7ba705fd --- novaclient/client.py | 39 +++++-------- novaclient/tests/unit/test_client.py | 22 +++---- novaclient/tests/unit/test_discover.py | 10 +--- novaclient/tests/unit/v2/contrib/__init__.py | 0 .../unit/v2/contrib/test_tenant_networks.py | 54 ----------------- novaclient/tests/unit/v2/fakes.py | 21 ------- novaclient/v2/contrib/tenant_networks.py | 58 ------------------- ...ke-rm-deprecated-net-272aeb62b329a5bc.yaml | 11 ++++ 8 files changed, 35 insertions(+), 180 deletions(-) delete mode 100644 novaclient/tests/unit/v2/contrib/__init__.py delete mode 100644 novaclient/tests/unit/v2/contrib/test_tenant_networks.py delete mode 100644 novaclient/v2/contrib/tenant_networks.py diff --git a/novaclient/client.py b/novaclient/client.py index aac0e8eb2..4d3f0f387 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -171,23 +171,20 @@ def _construct_http_client(api_version=None, **kwargs) -def discover_extensions(version, only_contrib=False): - """Returns the list of extensions, which can be discovered by python path, - contrib path and by entry-point 'novaclient.extension'. - - :param version: api version - :type version: str or novaclient.api_versions.APIVersion - :param only_contrib: search only in contrib directory or not - :type only_contrib: bool +def discover_extensions(*args, **kwargs): + """Returns the list of extensions, which can be discovered by python path + and by entry-point 'novaclient.extension'. """ - if not isinstance(version, api_versions.APIVersion): - version = api_versions.get_api_version(version) - if only_contrib: - chain = _discover_via_contrib_path(version) - else: - chain = itertools.chain(_discover_via_python_path(), - _discover_via_contrib_path(version), - _discover_via_entry_points()) + # TODO(mriedem): Remove support for 'only_contrib' in Queens. + if 'only_contrib' in kwargs and kwargs['only_contrib']: + warnings.warn(_LW('Discovering extensions only by contrib path is no ' + 'longer supported since all contrib extensions ' + 'have either been made required or removed. The ' + 'only_contrib argument is deprecated and will be ' + 'removed in a future release.')) + return [] + chain = itertools.chain(_discover_via_python_path(), + _discover_via_entry_points()) return [ext.Extension(name, module) for name, module in chain] @@ -204,16 +201,6 @@ def _discover_via_python_path(): yield name, module -def _discover_via_contrib_path(version): - if version.ver_major == 2: - modules = {"tenant_networks": "novaclient.v2.contrib.tenant_networks"} - - for name, module_name in modules.items(): - module_loader = pkgutil.get_loader(module_name) - module = module_loader.load_module(module_name) - yield name, module - - def _discover_via_entry_points(): for ep in pkg_resources.iter_entry_points('novaclient.extension'): name = ep.name diff --git a/novaclient/tests/unit/test_client.py b/novaclient/tests/unit/test_client.py index 4f739888a..81d6d56e8 100644 --- a/novaclient/tests/unit/test_client.py +++ b/novaclient/tests/unit/test_client.py @@ -78,12 +78,10 @@ def test_client_get_reset_timings_v2(self): class ClientsUtilsTest(utils.TestCase): @mock.patch("novaclient.client._discover_via_entry_points") - @mock.patch("novaclient.client._discover_via_contrib_path") @mock.patch("novaclient.client._discover_via_python_path") @mock.patch("novaclient.extension.Extension") def test_discover_extensions_all(self, mock_extension, mock_discover_via_python_path, - mock_discover_via_contrib_path, mock_discover_via_entry_points): def make_gen(start, end): def f(*args, **kwargs): @@ -92,36 +90,34 @@ def f(*args, **kwargs): return f mock_discover_via_python_path.side_effect = make_gen(0, 3) - mock_discover_via_contrib_path.side_effect = make_gen(3, 5) - mock_discover_via_entry_points.side_effect = make_gen(5, 6) + mock_discover_via_entry_points.side_effect = make_gen(3, 4) version = novaclient.api_versions.APIVersion("2.0") result = novaclient.client.discover_extensions(version) - self.assertEqual([mock.call("name-%s" % i, i) for i in range(0, 6)], + self.assertEqual([mock.call("name-%s" % i, i) for i in range(0, 4)], mock_extension.call_args_list) mock_discover_via_python_path.assert_called_once_with() - mock_discover_via_contrib_path.assert_called_once_with(version) mock_discover_via_entry_points.assert_called_once_with() - self.assertEqual([mock_extension()] * 6, result) + self.assertEqual([mock_extension()] * 4, result) + @mock.patch('novaclient.client.warnings') @mock.patch("novaclient.client._discover_via_entry_points") - @mock.patch("novaclient.client._discover_via_contrib_path") @mock.patch("novaclient.client._discover_via_python_path") @mock.patch("novaclient.extension.Extension") def test_discover_extensions_only_contrib( self, mock_extension, mock_discover_via_python_path, - mock_discover_via_contrib_path, mock_discover_via_entry_points): - mock_discover_via_contrib_path.return_value = [("name", "module")] + mock_discover_via_entry_points, mock_warnings): version = novaclient.api_versions.APIVersion("2.0") - novaclient.client.discover_extensions(version, only_contrib=True) - mock_discover_via_contrib_path.assert_called_once_with(version) + self.assertEqual([], novaclient.client.discover_extensions( + version, only_contrib=True)) self.assertFalse(mock_discover_via_python_path.called) self.assertFalse(mock_discover_via_entry_points.called) - mock_extension.assert_called_once_with("name", "module") + self.assertFalse(mock_extension.called) + self.assertTrue(mock_warnings.warn.called) @mock.patch("novaclient.client.warnings") def test__check_arguments(self, mock_warnings): diff --git a/novaclient/tests/unit/test_discover.py b/novaclient/tests/unit/test_discover.py index 44f3a49ef..c03c1d0fd 100644 --- a/novaclient/tests/unit/test_discover.py +++ b/novaclient/tests/unit/test_discover.py @@ -49,25 +49,19 @@ def test_discover_extensions(self): def mock_discover_via_python_path(): yield 'foo', imp.new_module('foo') - def mock_discover_via_contrib_path(version): - yield 'bar', imp.new_module('bar') - def mock_discover_via_entry_points(): yield 'baz', imp.new_module('baz') @mock.patch.object(client, '_discover_via_python_path', mock_discover_via_python_path) - @mock.patch.object(client, - '_discover_via_contrib_path', - mock_discover_via_contrib_path) @mock.patch.object(client, '_discover_via_entry_points', mock_discover_via_entry_points) def test(): extensions = client.discover_extensions('1.1') - self.assertEqual(3, len(extensions)) - names = sorted(['foo', 'bar', 'baz']) + self.assertEqual(2, len(extensions)) + names = sorted(['foo', 'baz']) sorted_extensions = sorted(extensions, key=lambda ext: ext.name) for i in range(len(names)): ext = sorted_extensions[i] diff --git a/novaclient/tests/unit/v2/contrib/__init__.py b/novaclient/tests/unit/v2/contrib/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/novaclient/tests/unit/v2/contrib/test_tenant_networks.py b/novaclient/tests/unit/v2/contrib/test_tenant_networks.py deleted file mode 100644 index ba3998beb..000000000 --- a/novaclient/tests/unit/v2/contrib/test_tenant_networks.py +++ /dev/null @@ -1,54 +0,0 @@ -# Copyright 2012 OpenStack Foundation -# All Rights Reserved. -# -# 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 novaclient import api_versions -from novaclient import extension -from novaclient.tests.unit import utils -from novaclient.tests.unit.v2 import fakes -from novaclient.v2.contrib import tenant_networks - - -class TenantNetworkExtensionTests(utils.TestCase): - - def setUp(self): - super(TenantNetworkExtensionTests, self).setUp() - extensions = [ - extension.Extension(tenant_networks.__name__.split(".")[-1], - tenant_networks), - ] - self.cs = fakes.FakeClient(api_versions.APIVersion("2.0"), - extensions=extensions) - - def test_list_tenant_networks(self): - nets = self.cs.tenant_networks.list() - self.assert_request_id(nets, fakes.FAKE_REQUEST_ID_LIST) - self.cs.assert_called('GET', '/os-tenant-networks') - self.assertGreater(len(nets), 0) - - def test_get_tenant_network(self): - net = self.cs.tenant_networks.get(1) - self.assert_request_id(net, fakes.FAKE_REQUEST_ID_LIST) - self.cs.assert_called('GET', '/os-tenant-networks/1') - - def test_create_tenant_networks(self): - net = self.cs.tenant_networks.create(label="net", - cidr="10.0.0.0/24") - self.assert_request_id(net, fakes.FAKE_REQUEST_ID_LIST) - self.cs.assert_called('POST', '/os-tenant-networks') - - def test_delete_tenant_networks(self): - ret = self.cs.tenant_networks.delete(1) - self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) - self.cs.assert_called('DELETE', '/os-tenant-networks/1') diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index ef5c6014b..47673d965 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -2329,27 +2329,6 @@ def delete_servers_1234_tags_tag3(self, **kw): def delete_servers_1234_tags(self, **kw): return (204, {}, None) - def get_os_tenant_networks(self): - return (200, FAKE_RESPONSE_HEADERS, { - 'networks': [{"label": "1", "cidr": "10.0.0.0/24", - 'project_id': '4ffc664c198e435e9853f2538fbcd7a7', - 'id': '1'}]}) - - def get_os_tenant_networks_1(self, **kw): - return (200, FAKE_RESPONSE_HEADERS, { - 'network': {"label": "1", "cidr": "10.0.0.0/24", - 'project_id': '4ffc664c198e435e9853f2538fbcd7a7', - 'id': '1'}}) - - def post_os_tenant_networks(self, **kw): - return (201, FAKE_RESPONSE_HEADERS, { - 'network': {"label": "1", "cidr": "10.0.0.0/24", - 'project_id': '4ffc664c198e435e9853f2538fbcd7a7', - 'id': '1'}}) - - def delete_os_tenant_networks_1(self, **kw): - return (204, FAKE_RESPONSE_HEADERS, None) - def post_os_assisted_volume_snapshots(self, **kw): return (202, FAKE_RESPONSE_HEADERS, {'snapshot': {'id': 'blah', 'volumeId': '1'}}) diff --git a/novaclient/v2/contrib/tenant_networks.py b/novaclient/v2/contrib/tenant_networks.py deleted file mode 100644 index b7a39b9d6..000000000 --- a/novaclient/v2/contrib/tenant_networks.py +++ /dev/null @@ -1,58 +0,0 @@ -# Copyright 2013 OpenStack Foundation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from novaclient import api_versions -from novaclient import base - - -class TenantNetwork(base.Resource): - def delete(self): - """ - DEPRECATED: Delete this project network. - - :returns: An instance of novaclient.base.TupleWithMeta - """ - return self.manager.delete(network=self) - - -class TenantNetworkManager(base.ManagerWithFind): - """DEPRECATED""" - resource_class = base.Resource - - @api_versions.deprecated_after('2.35') - def list(self): - """DEPRECATED""" - return self._list('/os-tenant-networks', 'networks') - - @api_versions.deprecated_after('2.35') - def get(self, network): - """DEPRECATED""" - return self._get('/os-tenant-networks/%s' % base.getid(network), - 'network') - - @api_versions.deprecated_after('2.35') - def delete(self, network): - """ - DEPRECATED: Delete a specified project network. - - :param network: a project network to delete - :returns: An instance of novaclient.base.TupleWithMeta - """ - return self._delete('/os-tenant-networks/%s' % base.getid(network)) - - @api_versions.deprecated_after('2.35') - def create(self, label, cidr): - """DEPRECATED""" - body = {'network': {'label': label, 'cidr': cidr}} - return self._create('/os-tenant-networks', body, 'network') diff --git a/releasenotes/notes/pike-rm-deprecated-net-272aeb62b329a5bc.yaml b/releasenotes/notes/pike-rm-deprecated-net-272aeb62b329a5bc.yaml index e2d9d7bf1..2d71a7f6c 100644 --- a/releasenotes/notes/pike-rm-deprecated-net-272aeb62b329a5bc.yaml +++ b/releasenotes/notes/pike-rm-deprecated-net-272aeb62b329a5bc.yaml @@ -51,3 +51,14 @@ upgrade: * nova tenant-network-delete * nova tenant-network-list * nova tenant-network-show + + Along with the following python API bindings:: + + * novaclient.v2.contrib.tenant_networks + +deprecations: + - | + The ``only_contrib`` parameter for the + ``novaclient.client.discover_extensions`` method is deprecated and now + results in an empty list returned since all contrib extensions are either + required or have been removed. \ No newline at end of file From ef74c0a7c81cb5084d1e1a398232facc287237d9 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Mon, 20 Mar 2017 16:58:22 -0400 Subject: [PATCH 1255/1705] Remove deprecated floating IP DNS domain/entry APIs These were deprecated in Newton in change: aaebeb05a03e34281a091dc6dfc4672b01cdfbbb Change-Id: I8888f241b04f075d92d2298c623144809546581f --- .../v2/legacy/test_readonly_nova.py | 5 - .../tests/unit/fixture_data/floatingips.py | 96 --------- novaclient/tests/unit/v2/fakes.py | 51 ----- .../tests/unit/v2/test_floating_ip_dns.py | 100 ---------- novaclient/v2/client.py | 3 - novaclient/v2/floating_ip_dns.py | 182 ------------------ ...ke-rm-deprecated-net-272aeb62b329a5bc.yaml | 1 + 7 files changed, 1 insertion(+), 437 deletions(-) delete mode 100644 novaclient/tests/unit/v2/test_floating_ip_dns.py delete mode 100644 novaclient/v2/floating_ip_dns.py diff --git a/novaclient/tests/functional/v2/legacy/test_readonly_nova.py b/novaclient/tests/functional/v2/legacy/test_readonly_nova.py index 15ef9e213..cdf04a635 100644 --- a/novaclient/tests/functional/v2/legacy/test_readonly_nova.py +++ b/novaclient/tests/functional/v2/legacy/test_readonly_nova.py @@ -41,11 +41,6 @@ def test_admin_availability_zone_list(self): def test_admin_cloudpipe_list(self): self.nova('cloudpipe-list') - @decorators.skip_because(bug="1157349") - def test_admin_dns_list(self): - self.skip_if_neutron() - self.nova('dns-list') - def test_admin_flavor_acces_list(self): self.assertRaises(exceptions.CommandFailed, self.nova, diff --git a/novaclient/tests/unit/fixture_data/floatingips.py b/novaclient/tests/unit/fixture_data/floatingips.py index 75364cdf8..6f59d3aee 100644 --- a/novaclient/tests/unit/fixture_data/floatingips.py +++ b/novaclient/tests/unit/fixture_data/floatingips.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.tests.unit import fakes from novaclient.tests.unit.fixture_data import base @@ -48,101 +47,6 @@ def post_os_floating_ips(request, context): headers=self.json_headers) -class DNSFixture(base.Fixture): - - base_url = 'os-floating-ip-dns' - - def setUp(self): - super(DNSFixture, self).setUp() - - get_os_floating_ip_dns = { - 'domain_entries': [ - {'domain': 'example.org'}, - {'domain': 'example.com'} - ] - } - self.requests_mock.get(self.url(), - json=get_os_floating_ip_dns, - headers=self.json_headers, - status_code=205) - - get_dns_testdomain_entries_testname = { - 'dns_entry': { - 'ip': "10.10.10.10", - 'name': 'testname', - 'type': "A", - 'domain': 'testdomain' - } - } - - self.requests_mock.get(self.url('testdomain', 'entries', 'testname'), - json=get_dns_testdomain_entries_testname, - headers=self.json_headers, - status_code=205) - - self.requests_mock.delete(self.url('testdomain'), - headers=self.json_headers) - - url = self.url('testdomain', 'entries', 'testname') - self.requests_mock.delete(url, headers=self.json_headers) - - def put_dns_testdomain_entries_testname(request, context): - fakes.assert_has_keys(request.json()['dns_entry'], - required=['ip', 'dns_type']) - context.status_code = 205 - return request.body - self.requests_mock.put(url, - text=put_dns_testdomain_entries_testname, - headers=self.json_headers) - - url = self.url('testdomain', 'entries') - self.requests_mock.get(url, status_code=404) - - get_os_floating_ip_dns_testdomain = { - 'dns_entries': [ - { - 'dns_entry': { - 'ip': '1.2.3.4', - 'name': "host1", - 'type': "A", - 'domain': 'testdomain' - } - }, - { - 'dns_entry': { - 'ip': '1.2.3.4', - 'name': "host2", - 'type': "A", - 'domain': 'testdomain' - } - }, - ] - } - self.requests_mock.get(url + '?ip=1.2.3.4', - json=get_os_floating_ip_dns_testdomain, - status_code=205, - headers=self.json_headers) - - def put_os_floating_ip_dns_testdomain(request, context): - body = request.json() - if body['domain_entry']['scope'] == 'private': - fakes.assert_has_keys(body['domain_entry'], - required=['availability_zone', 'scope']) - elif body['domain_entry']['scope'] == 'public': - fakes.assert_has_keys(body['domain_entry'], - required=['project', 'scope']) - else: - fakes.assert_has_keys(body['domain_entry'], - required=['project', 'scope']) - - return request.body - - self.requests_mock.put(self.url('testdomain'), - text=put_os_floating_ip_dns_testdomain, - status_code=205, - headers=self.json_headers) - - class BulkFixture(base.Fixture): base_url = 'os-floating-ips-bulk' diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 47673d965..2ba8bfdbf 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -1030,57 +1030,6 @@ def post_os_floating_ips(self, body): def delete_os_floating_ips_1(self, **kw): return (204, {}, None) - def get_os_floating_ip_dns(self, **kw): - return (205, {}, {'domain_entries': - [{'domain': 'example.org'}, - {'domain': 'example.com'}]}) - - def get_os_floating_ip_dns_testdomain_entries(self, **kw): - if kw.get('ip'): - return (205, {}, { - 'dns_entries': [ - {'dns_entry': {'ip': kw.get('ip'), - 'name': "host1", - 'type': "A", - 'domain': 'testdomain'}}, - {'dns_entry': {'ip': kw.get('ip'), - 'name': "host2", - 'type': "A", - 'domain': 'testdomain'}}]}) - else: - return (404, {}, None) - - def get_os_floating_ip_dns_testdomain_entries_testname(self, **kw): - return (205, {}, { - 'dns_entry': {'ip': "10.10.10.10", - 'name': 'testname', - 'type': "A", - 'domain': 'testdomain'}}) - - def put_os_floating_ip_dns_testdomain(self, body, **kw): - if body['domain_entry']['scope'] == 'private': - fakes.assert_has_keys(body['domain_entry'], - required=['availability_zone', 'scope']) - elif body['domain_entry']['scope'] == 'public': - fakes.assert_has_keys(body['domain_entry'], - required=['project', 'scope']) - - else: - fakes.assert_has_keys(body['domain_entry'], - required=['project', 'scope']) - return (205, {}, body) - - def put_os_floating_ip_dns_testdomain_entries_testname(self, body, **kw): - fakes.assert_has_keys(body['dns_entry'], - required=['ip', 'dns_type']) - return (205, {}, body) - - def delete_os_floating_ip_dns_testdomain(self, **kw): - return (200, {}, None) - - def delete_os_floating_ip_dns_testdomain_entries_testname(self, **kw): - return (200, {}, None) - def get_os_floating_ips_bulk(self, **kw): return (200, {}, {'floating_ip_info': [ {'id': 1, 'fixed_ip': '10.0.0.1', 'ip': '11.0.0.1'}, diff --git a/novaclient/tests/unit/v2/test_floating_ip_dns.py b/novaclient/tests/unit/v2/test_floating_ip_dns.py deleted file mode 100644 index 8d9f912ea..000000000 --- a/novaclient/tests/unit/v2/test_floating_ip_dns.py +++ /dev/null @@ -1,100 +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 novaclient.tests.unit.fixture_data import client -from novaclient.tests.unit.fixture_data import floatingips as data -from novaclient.tests.unit import utils -from novaclient.tests.unit.v2 import fakes -from novaclient.v2 import floating_ip_dns - - -class FloatingIPDNSDomainTest(utils.FixturedTestCase): - - testdomain = "testdomain" - client_fixture_class = client.V1 - data_fixture_class = data.DNSFixture - - def test_dns_domains(self): - domainlist = self.cs.dns_domains.domains() - self.assert_request_id(domainlist, fakes.FAKE_REQUEST_ID_LIST) - self.assertEqual(2, len(domainlist)) - - for entry in domainlist: - self.assertIsInstance(entry, - floating_ip_dns.FloatingIPDNSDomain) - - self.assertEqual('example.com', domainlist[1].domain) - - def test_create_private_domain(self): - pd = self.cs.dns_domains.create_private(self.testdomain, 'test_avzone') - self.assert_request_id(pd, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('PUT', '/os-floating-ip-dns/%s' % - self.testdomain) - - def test_create_public_domain(self): - pd = self.cs.dns_domains.create_public(self.testdomain, 'test_project') - self.assert_request_id(pd, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('PUT', '/os-floating-ip-dns/%s' % - self.testdomain) - - def test_delete_domain(self): - ret = self.cs.dns_domains.delete(self.testdomain) - self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('DELETE', '/os-floating-ip-dns/%s' % - self.testdomain) - - -class FloatingIPDNSEntryTest(utils.FixturedTestCase): - - testname = "testname" - testip = "1.2.3.4" - testdomain = "testdomain" - testtype = "A" - client_fixture_class = client.V1 - data_fixture_class = data.DNSFixture - - def test_get_dns_entries_by_ip(self): - entries = self.cs.dns_entries.get_for_ip(self.testdomain, - ip=self.testip) - self.assert_request_id(entries, fakes.FAKE_REQUEST_ID_LIST) - self.assertEqual(2, len(entries)) - - for entry in entries: - self.assertIsInstance(entry, - floating_ip_dns.FloatingIPDNSEntry) - - self.assertEqual('host2', entries[1].dns_entry['name']) - self.assertEqual(entries[1].dns_entry['ip'], self.testip) - - def test_get_dns_entry_by_name(self): - entry = self.cs.dns_entries.get(self.testdomain, - self.testname) - self.assert_request_id(entry, fakes.FAKE_REQUEST_ID_LIST) - self.assertIsInstance(entry, floating_ip_dns.FloatingIPDNSEntry) - self.assertEqual(entry.name, self.testname) - - def test_create_entry(self): - entry = self.cs.dns_entries.create(self.testdomain, - self.testname, - self.testip, - self.testtype) - self.assert_request_id(entry, fakes.FAKE_REQUEST_ID_LIST) - - self.assert_called('PUT', '/os-floating-ip-dns/%s/entries/%s' % - (self.testdomain, self.testname)) - - def test_delete_entry(self): - ret = self.cs.dns_entries.delete(self.testdomain, self.testname) - self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('DELETE', '/os-floating-ip-dns/%s/entries/%s' % - (self.testdomain, self.testname)) diff --git a/novaclient/v2/client.py b/novaclient/v2/client.py index d216a94ae..a3a7edee5 100644 --- a/novaclient/v2/client.py +++ b/novaclient/v2/client.py @@ -31,7 +31,6 @@ from novaclient.v2 import fixed_ips from novaclient.v2 import flavor_access from novaclient.v2 import flavors -from novaclient.v2 import floating_ip_dns from novaclient.v2 import floating_ip_pools from novaclient.v2 import floating_ips from novaclient.v2 import floating_ips_bulk @@ -160,8 +159,6 @@ def __init__(self, # extensions self.agents = agents.AgentsManager(self) - self.dns_domains = floating_ip_dns.FloatingIPDNSDomainManager(self) - self.dns_entries = floating_ip_dns.FloatingIPDNSEntryManager(self) self.cloudpipe = cloudpipe.CloudpipeManager(self) self.certs = certs.CertificateManager(self) self.floating_ips = floating_ips.FloatingIPManager(self) diff --git a/novaclient/v2/floating_ip_dns.py b/novaclient/v2/floating_ip_dns.py deleted file mode 100644 index 64b3a65d9..000000000 --- a/novaclient/v2/floating_ip_dns.py +++ /dev/null @@ -1,182 +0,0 @@ -# Copyright 2011 Andrew Bogott for The Wikimedia Foundation -# All Rights Reserved. -# -# 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 six.moves.urllib import parse - -from novaclient import api_versions -from novaclient import base - - -def _quote_domain(domain): - """Special quoting rule for placing domain names on a url line. - - Domain names tend to have .'s in them. Urllib doesn't quote dots, - but Routes tends to choke on them, so we need an extra level of - by-hand quoting here. - """ - return parse.quote(domain.replace('.', '%2E')) - - -class FloatingIPDNSDomain(base.Resource): - """DEPRECATED""" - - def delete(self): - """ - DEPRECATED: Delete the own Floating IP DNS domain. - - :returns: An instance of novaclient.base.TupleWithMeta - """ - return self.manager.delete(self.domain) - - def create(self): - """ - DEPRECATED: Create a Floating IP DNS domain. - - :returns: An instance of novaclient.base.DictWithMeta - """ - if self.scope == 'public': - return self.manager.create_public(self.domain, self.project) - else: - return self.manager.create_private(self.domain, - self.availability_zone) - - def get(self): - """ - DEPRECATED: Get the own Floating IP DNS domain. - - :returns: An instance of novaclient.base.TupleWithMeta or - novaclient.base.ListWithMeta - """ - entries = self.manager.domains() - for entry in entries: - if entry.get('domain') == self.domain: - return entry - - return base.TupleWithMeta((), entries.request_ids) - - -class FloatingIPDNSDomainManager(base.Manager): - """DEPRECATED""" - - resource_class = FloatingIPDNSDomain - - @api_versions.deprecated_after('2.35') - def domains(self): - """DEPRECATED: Return the list of available dns domains.""" - return self._list("/os-floating-ip-dns", "domain_entries") - - @api_versions.deprecated_after('2.35') - def create_private(self, fqdomain, availability_zone): - """DEPRECATED: Add or modify a private DNS domain.""" - body = {'domain_entry': {'scope': 'private', - 'availability_zone': availability_zone}} - return self._update('/os-floating-ip-dns/%s' % _quote_domain(fqdomain), - body, - 'domain_entry') - - @api_versions.deprecated_after('2.35') - def create_public(self, fqdomain, project): - """DEPRECATED: Add or modify a public DNS domain.""" - body = {'domain_entry': {'scope': 'public', 'project': project}} - - return self._update('/os-floating-ip-dns/%s' % _quote_domain(fqdomain), - body, 'domain_entry') - - @api_versions.deprecated_after('2.35') - def delete(self, fqdomain): - """ - DEPRECATED: Delete the specified domain. - - :param fqdomain: The domain to delete - :returns: An instance of novaclient.base.TupleWithMeta - """ - return self._delete("/os-floating-ip-dns/%s" % _quote_domain(fqdomain)) - - -class FloatingIPDNSEntry(base.Resource): - """DEPRECATED""" - - def delete(self): - """ - DEPRECATED: Delete the own Floating IP DNS entry. - - :returns: An instance of novaclient.base.TupleWithMeta - """ - return self.manager.delete(self.name, self.domain) - - def create(self): - """ - DEPRECATED: Create a Floating IP DNS entry. - - :returns: :class:`FloatingIPDNSEntry` - """ - return self.manager.create(self.domain, self.name, self.ip, - self.dns_type) - - def get(self): - """DEPRECATED""" - return self.manager.get(self.domain, self.name) - - -class FloatingIPDNSEntryManager(base.Manager): - """DEPRECATED""" - resource_class = FloatingIPDNSEntry - - @api_versions.deprecated_after('2.35') - def get(self, domain, name): - """ - DEPRECATED: Return a list of entries for the given domain and IP or - name. - """ - return self._get("/os-floating-ip-dns/%s/entries/%s" % - (_quote_domain(domain), name), "dns_entry") - - @api_versions.deprecated_after('2.35') - def get_for_ip(self, domain, ip): - """ - DEPRECATED: Return a list of entries for the given domain and IP or - name. - """ - qparams = {'ip': ip} - params = "?%s" % parse.urlencode(qparams) - - return self._list("/os-floating-ip-dns/%s/entries%s" % - (_quote_domain(domain), params), "dns_entries") - - @api_versions.deprecated_after('2.35') - def create(self, domain, name, ip, dns_type): - """DEPRECATED: Add a new DNS entry.""" - body = {'dns_entry': {'ip': ip, 'dns_type': dns_type}} - - return self._update("/os-floating-ip-dns/%s/entries/%s" % - (_quote_domain(domain), name), body, "dns_entry") - - @api_versions.deprecated_after('2.35') - def modify_ip(self, domain, name, ip): - """DEPRECATED: Add a new DNS entry.""" - body = {'dns_entry': {'ip': ip, 'dns_type': 'A'}} - - return self._update("/os-floating-ip-dns/%s/entries/%s" % - (_quote_domain(domain), name), body, "dns_entry") - - @api_versions.deprecated_after('2.35') - def delete(self, domain, name): - """ - DEPRECATED: Delete entry specified by name and domain. - - :returns: An instance of novaclient.base.TupleWithMeta - """ - return self._delete("/os-floating-ip-dns/%s/entries/%s" % - (_quote_domain(domain), name)) diff --git a/releasenotes/notes/pike-rm-deprecated-net-272aeb62b329a5bc.yaml b/releasenotes/notes/pike-rm-deprecated-net-272aeb62b329a5bc.yaml index 2d71a7f6c..8a5fadc11 100644 --- a/releasenotes/notes/pike-rm-deprecated-net-272aeb62b329a5bc.yaml +++ b/releasenotes/notes/pike-rm-deprecated-net-272aeb62b329a5bc.yaml @@ -55,6 +55,7 @@ upgrade: Along with the following python API bindings:: * novaclient.v2.contrib.tenant_networks + * novaclient.v2.floating_ip_dns deprecations: - | From ab3315b46f505f9d0917ba667b0856ccf7faeb6c Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Mon, 20 Mar 2017 17:16:54 -0400 Subject: [PATCH 1256/1705] Remove deprecated floating_ips_bulk API These were deprecated in Newton in change: aaebeb05a03e34281a091dc6dfc4672b01cdfbbb Change-Id: Idee91ca8fc4715cd952f97dafcdd5519c9bec919 --- .../tests/unit/fixture_data/floatingips.py | 45 ----------- novaclient/tests/unit/v2/fakes.py | 19 ----- .../tests/unit/v2/test_floating_ips_bulk.py | 75 ------------------- novaclient/v2/client.py | 2 - novaclient/v2/floating_ips_bulk.py | 64 ---------------- ...ke-rm-deprecated-net-272aeb62b329a5bc.yaml | 1 + 6 files changed, 1 insertion(+), 205 deletions(-) delete mode 100644 novaclient/tests/unit/v2/test_floating_ips_bulk.py delete mode 100644 novaclient/v2/floating_ips_bulk.py diff --git a/novaclient/tests/unit/fixture_data/floatingips.py b/novaclient/tests/unit/fixture_data/floatingips.py index 6f59d3aee..a9097be5e 100644 --- a/novaclient/tests/unit/fixture_data/floatingips.py +++ b/novaclient/tests/unit/fixture_data/floatingips.py @@ -47,51 +47,6 @@ def post_os_floating_ips(request, context): headers=self.json_headers) -class BulkFixture(base.Fixture): - - base_url = 'os-floating-ips-bulk' - - def setUp(self): - super(BulkFixture, self).setUp() - - get_os_floating_ips_bulk = { - 'floating_ip_info': [ - {'id': 1, 'fixed_ip': '10.0.0.1', 'ip': '11.0.0.1'}, - {'id': 2, 'fixed_ip': '10.0.0.2', 'ip': '11.0.0.2'}, - ] - } - self.requests_mock.get(self.url(), - json=get_os_floating_ips_bulk, - headers=self.json_headers) - self.requests_mock.get(self.url('testHost'), - json=get_os_floating_ips_bulk, - headers=self.json_headers) - - def put_os_floating_ips_bulk_delete(request, context): - ip_range = request.json().get('ip_range') - return {'floating_ips_bulk_delete': ip_range} - - self.requests_mock.put(self.url('delete'), - json=put_os_floating_ips_bulk_delete, - headers=self.json_headers) - - def post_os_floating_ips_bulk(request, context): - params = request.json().get('floating_ips_bulk_create') - pool = params.get('pool', 'defaultPool') - interface = params.get('interface', 'defaultInterface') - return { - 'floating_ips_bulk_create': { - 'ip_range': '192.168.1.0/30', - 'pool': pool, - 'interface': interface - } - } - - self.requests_mock.post(self.url(), - json=post_os_floating_ips_bulk, - headers=self.json_headers) - - class PoolsFixture(base.Fixture): base_url = 'os-floating-ip-pools' diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 2ba8bfdbf..953f6cd69 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -1030,25 +1030,6 @@ def post_os_floating_ips(self, body): def delete_os_floating_ips_1(self, **kw): return (204, {}, None) - def get_os_floating_ips_bulk(self, **kw): - return (200, {}, {'floating_ip_info': [ - {'id': 1, 'fixed_ip': '10.0.0.1', 'ip': '11.0.0.1'}, - {'id': 2, 'fixed_ip': '10.0.0.2', 'ip': '11.0.0.2'}, - ]}) - - def post_os_floating_ips_bulk(self, **kw): - params = kw.get('body').get('floating_ips_bulk_create') - pool = params.get('pool', 'defaultPool') - interface = params.get('interface', 'defaultInterface') - return (200, {}, {'floating_ips_bulk_create': - {'ip_range': '192.168.1.0/30', - 'pool': pool, - 'interface': interface}}) - - def put_os_floating_ips_bulk_delete(self, **kw): - ip_range = kw.get('body').get('ip_range') - return (200, {}, {'floating_ips_bulk_delete': ip_range}) - # # Images # diff --git a/novaclient/tests/unit/v2/test_floating_ips_bulk.py b/novaclient/tests/unit/v2/test_floating_ips_bulk.py deleted file mode 100644 index 37eb2fa1d..000000000 --- a/novaclient/tests/unit/v2/test_floating_ips_bulk.py +++ /dev/null @@ -1,75 +0,0 @@ -# Copyright 2012 IBM Corp. -# All Rights Reserved. -# -# 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 novaclient.tests.unit.fixture_data import client -from novaclient.tests.unit.fixture_data import floatingips as data -from novaclient.tests.unit import utils -from novaclient.tests.unit.v2 import fakes -from novaclient.v2 import floating_ips - - -class FloatingIPsBulkTest(utils.FixturedTestCase): - - client_fixture_class = client.V1 - data_fixture_class = data.BulkFixture - - def test_list_floating_ips_bulk(self): - fl = self.cs.floating_ips_bulk.list() - self.assert_request_id(fl, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('GET', '/os-floating-ips-bulk') - for f in fl: - self.assertIsInstance(f, floating_ips.FloatingIP) - - def test_list_floating_ips_bulk_host_filter(self): - fl = self.cs.floating_ips_bulk.list('testHost') - self.assert_request_id(fl, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('GET', '/os-floating-ips-bulk/testHost') - for f in fl: - self.assertIsInstance(f, floating_ips.FloatingIP) - - def test_create_floating_ips_bulk(self): - fl = self.cs.floating_ips_bulk.create('192.168.1.0/30') - self.assert_request_id(fl, fakes.FAKE_REQUEST_ID_LIST) - body = {'floating_ips_bulk_create': {'ip_range': '192.168.1.0/30'}} - self.assert_called('POST', '/os-floating-ips-bulk', body) - self.assertEqual(fl.ip_range, - body['floating_ips_bulk_create']['ip_range']) - - def test_create_floating_ips_bulk_with_pool_and_host(self): - fl = self.cs.floating_ips_bulk.create('192.168.1.0/30', 'poolTest', - 'interfaceTest') - self.assert_request_id(fl, fakes.FAKE_REQUEST_ID_LIST) - body = {'floating_ips_bulk_create': { - 'ip_range': '192.168.1.0/30', 'pool': 'poolTest', - 'interface': 'interfaceTest'}} - self.assert_called('POST', '/os-floating-ips-bulk', body) - self.assertEqual(fl.ip_range, - body['floating_ips_bulk_create']['ip_range']) - self.assertEqual(fl.pool, - body['floating_ips_bulk_create']['pool']) - self.assertEqual(fl.interface, - body['floating_ips_bulk_create']['interface']) - - def test_delete_floating_ips_bulk(self): - fl = self.cs.floating_ips_bulk.delete('192.168.1.0/30') - self.assert_request_id(fl, fakes.FAKE_REQUEST_ID_LIST) - body = {'ip_range': '192.168.1.0/30'} - self.assert_called('PUT', '/os-floating-ips-bulk/delete', body) - self.assertEqual(fl.floating_ips_bulk_delete, body['ip_range']) - - def test_repr(self): - fl = self.cs.floating_ips_bulk.create('192.168.1.0/30', 'poolTest', - 'interfaceTest') - self.assertEqual('', "%s" % fl) diff --git a/novaclient/v2/client.py b/novaclient/v2/client.py index a3a7edee5..9e1fdb94e 100644 --- a/novaclient/v2/client.py +++ b/novaclient/v2/client.py @@ -33,7 +33,6 @@ from novaclient.v2 import flavors from novaclient.v2 import floating_ip_pools from novaclient.v2 import floating_ips -from novaclient.v2 import floating_ips_bulk from novaclient.v2 import fping from novaclient.v2 import hosts from novaclient.v2 import hypervisors @@ -184,7 +183,6 @@ def __init__(self, self.hypervisor_stats = hypervisors.HypervisorStatsManager(self) self.services = services.ServiceManager(self) self.fixed_ips = fixed_ips.FixedIPsManager(self) - self.floating_ips_bulk = floating_ips_bulk.FloatingIPBulkManager(self) self.os_cache = os_cache self.availability_zones = \ availability_zones.AvailabilityZoneManager(self) diff --git a/novaclient/v2/floating_ips_bulk.py b/novaclient/v2/floating_ips_bulk.py deleted file mode 100644 index c0c8a8e10..000000000 --- a/novaclient/v2/floating_ips_bulk.py +++ /dev/null @@ -1,64 +0,0 @@ -# Copyright 2012 IBM Corp. -# All Rights Reserved. -# -# 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. - -""" -Bulk Floating IPs interface -""" -from novaclient import api_versions -from novaclient import base -from novaclient.v2 import floating_ips - - -class FloatingIPRange(base.Resource): - """DEPRECATED""" - - def __repr__(self): - return "" % self.ip_range - - -class FloatingIPBulkManager(base.ManagerWithFind): - """DEPRECATED""" - - resource_class = FloatingIPRange - - @api_versions.deprecated_after('2.35') - def list(self, host=None): - """DEPRECATED: List all floating IPs.""" - if host is None: - return self._list('/os-floating-ips-bulk', - 'floating_ip_info', - obj_class=floating_ips.FloatingIP) - else: - return self._list('/os-floating-ips-bulk/%s' % host, - 'floating_ip_info', - obj_class=floating_ips.FloatingIP) - - @api_versions.deprecated_after('2.35') - def create(self, ip_range, pool=None, interface=None): - """DEPRECATED: Create floating IPs by range.""" - body = {"floating_ips_bulk_create": {'ip_range': ip_range}} - if pool is not None: - body['floating_ips_bulk_create']['pool'] = pool - if interface is not None: - body['floating_ips_bulk_create']['interface'] = interface - - return self._create('/os-floating-ips-bulk', body, - 'floating_ips_bulk_create') - - @api_versions.deprecated_after('2.35') - def delete(self, ip_range): - """DEPRECATED: Delete floating IPs by range.""" - body = {"ip_range": ip_range} - return self._update('/os-floating-ips-bulk/delete', body) diff --git a/releasenotes/notes/pike-rm-deprecated-net-272aeb62b329a5bc.yaml b/releasenotes/notes/pike-rm-deprecated-net-272aeb62b329a5bc.yaml index 8a5fadc11..3b66c5a27 100644 --- a/releasenotes/notes/pike-rm-deprecated-net-272aeb62b329a5bc.yaml +++ b/releasenotes/notes/pike-rm-deprecated-net-272aeb62b329a5bc.yaml @@ -56,6 +56,7 @@ upgrade: * novaclient.v2.contrib.tenant_networks * novaclient.v2.floating_ip_dns + * novaclient.v2.floating_ips_bulk deprecations: - | From 31a12984b35545d41204c656db082e70031e361d Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Mon, 20 Mar 2017 17:28:28 -0400 Subject: [PATCH 1257/1705] Remove deprecated floating_ip_pools API These were deprecated in Newton in change: aaebeb05a03e34281a091dc6dfc4672b01cdfbbb Change-Id: I9ec33231bbf060e7daae8f3567a38c71e52f5c42 --- .../tests/unit/fixture_data/floatingips.py | 18 ---------- .../tests/unit/v2/test_floating_ip_pools.py | 34 ------------------ novaclient/v2/client.py | 2 -- novaclient/v2/floating_ip_pools.py | 35 ------------------- ...ke-rm-deprecated-net-272aeb62b329a5bc.yaml | 1 + 5 files changed, 1 insertion(+), 89 deletions(-) delete mode 100644 novaclient/tests/unit/v2/test_floating_ip_pools.py delete mode 100644 novaclient/v2/floating_ip_pools.py diff --git a/novaclient/tests/unit/fixture_data/floatingips.py b/novaclient/tests/unit/fixture_data/floatingips.py index a9097be5e..eae3be743 100644 --- a/novaclient/tests/unit/fixture_data/floatingips.py +++ b/novaclient/tests/unit/fixture_data/floatingips.py @@ -45,21 +45,3 @@ def post_os_floating_ips(request, context): self.requests_mock.post(self.url(), json=post_os_floating_ips, headers=self.json_headers) - - -class PoolsFixture(base.Fixture): - - base_url = 'os-floating-ip-pools' - - def setUp(self): - super(PoolsFixture, self).setUp() - - get_os_floating_ip_pools = { - 'floating_ip_pools': [ - {'name': 'foo'}, - {'name': 'bar'} - ] - } - self.requests_mock.get(self.url(), - json=get_os_floating_ip_pools, - headers=self.json_headers) diff --git a/novaclient/tests/unit/v2/test_floating_ip_pools.py b/novaclient/tests/unit/v2/test_floating_ip_pools.py deleted file mode 100644 index 7eac3df5b..000000000 --- a/novaclient/tests/unit/v2/test_floating_ip_pools.py +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. -# -# All Rights Reserved. -# -# 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 novaclient.tests.unit.fixture_data import client -from novaclient.tests.unit.fixture_data import floatingips as data -from novaclient.tests.unit import utils -from novaclient.tests.unit.v2 import fakes -from novaclient.v2 import floating_ip_pools - - -class TestFloatingIPPools(utils.FixturedTestCase): - - client_fixture_class = client.V1 - data_fixture_class = data.PoolsFixture - - def test_list_floating_ips(self): - fl = self.cs.floating_ip_pools.list() - self.assert_request_id(fl, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('GET', '/os-floating-ip-pools') - for f in fl: - self.assertIsInstance(f, floating_ip_pools.FloatingIPPool) diff --git a/novaclient/v2/client.py b/novaclient/v2/client.py index 9e1fdb94e..5d8421cb7 100644 --- a/novaclient/v2/client.py +++ b/novaclient/v2/client.py @@ -31,7 +31,6 @@ from novaclient.v2 import fixed_ips from novaclient.v2 import flavor_access from novaclient.v2 import flavors -from novaclient.v2 import floating_ip_pools from novaclient.v2 import floating_ips from novaclient.v2 import fping from novaclient.v2 import hosts @@ -161,7 +160,6 @@ def __init__(self, self.cloudpipe = cloudpipe.CloudpipeManager(self) self.certs = certs.CertificateManager(self) self.floating_ips = floating_ips.FloatingIPManager(self) - self.floating_ip_pools = floating_ip_pools.FloatingIPPoolManager(self) self.fping = fping.FpingManager(self) self.volumes = volumes.VolumeManager(self) self.keypairs = keypairs.KeypairManager(self) diff --git a/novaclient/v2/floating_ip_pools.py b/novaclient/v2/floating_ip_pools.py deleted file mode 100644 index 70a43ce23..000000000 --- a/novaclient/v2/floating_ip_pools.py +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. -# -# All Rights Reserved. -# -# 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 novaclient import api_versions -from novaclient import base - - -class FloatingIPPool(base.Resource): - """DEPRECATED""" - - def __repr__(self): - return "" % self.name - - -class FloatingIPPoolManager(base.ManagerWithFind): - """DEPRECATED""" - resource_class = FloatingIPPool - - @api_versions.deprecated_after('2.35') - def list(self): - """DEPRECATED: Retrieve a list of all floating ip pools.""" - return self._list('/os-floating-ip-pools', 'floating_ip_pools') diff --git a/releasenotes/notes/pike-rm-deprecated-net-272aeb62b329a5bc.yaml b/releasenotes/notes/pike-rm-deprecated-net-272aeb62b329a5bc.yaml index 3b66c5a27..a0637b88d 100644 --- a/releasenotes/notes/pike-rm-deprecated-net-272aeb62b329a5bc.yaml +++ b/releasenotes/notes/pike-rm-deprecated-net-272aeb62b329a5bc.yaml @@ -56,6 +56,7 @@ upgrade: * novaclient.v2.contrib.tenant_networks * novaclient.v2.floating_ip_dns + * novaclient.v2.floating_ip_pools * novaclient.v2.floating_ips_bulk deprecations: From 8030b2604df8069269f539f74daf56231ced2575 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Mon, 20 Mar 2017 17:34:03 -0400 Subject: [PATCH 1258/1705] Remove the deprecated fping API This was deprecated in Newton: aaebeb05a03e34281a091dc6dfc4672b01cdfbbb Change-Id: I8deb3b479ec9fd06ab7f527725f489e63fe970c6 --- novaclient/tests/unit/fixture_data/fping.py | 46 ------------- novaclient/tests/unit/v2/test_fping.py | 69 ------------------- novaclient/v2/client.py | 2 - novaclient/v2/fping.py | 64 ----------------- ...ke-rm-deprecated-net-272aeb62b329a5bc.yaml | 1 + 5 files changed, 1 insertion(+), 181 deletions(-) delete mode 100644 novaclient/tests/unit/fixture_data/fping.py delete mode 100644 novaclient/tests/unit/v2/test_fping.py delete mode 100644 novaclient/v2/fping.py diff --git a/novaclient/tests/unit/fixture_data/fping.py b/novaclient/tests/unit/fixture_data/fping.py deleted file mode 100644 index 087be44be..000000000 --- a/novaclient/tests/unit/fixture_data/fping.py +++ /dev/null @@ -1,46 +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 novaclient.tests.unit.fixture_data import base - - -class Fixture(base.Fixture): - - base_url = 'os-fping' - - def setUp(self): - super(Fixture, self).setUp() - - get_os_fping_1 = { - 'server': { - "id": "1", - "project_id": "fake-project", - "alive": True, - } - } - self.requests_mock.get(self.url(1), - json=get_os_fping_1, - headers=self.json_headers) - - get_os_fping = { - 'servers': [ - get_os_fping_1['server'], - { - "id": "2", - "project_id": "fake-project", - "alive": True, - }, - ] - } - self.requests_mock.get(self.url(), - json=get_os_fping, - headers=self.json_headers) diff --git a/novaclient/tests/unit/v2/test_fping.py b/novaclient/tests/unit/v2/test_fping.py deleted file mode 100644 index ed3416d92..000000000 --- a/novaclient/tests/unit/v2/test_fping.py +++ /dev/null @@ -1,69 +0,0 @@ -# Copyright 2012 OpenStack Foundation -# All Rights Reserved. -# -# 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 novaclient.tests.unit.fixture_data import client -from novaclient.tests.unit.fixture_data import fping as data -from novaclient.tests.unit import utils -from novaclient.tests.unit.v2 import fakes -from novaclient.v2 import fping - - -class FpingTest(utils.FixturedTestCase): - - client_fixture_class = client.V1 - data_fixture_class = data.Fixture - - def test_fping_repr(self): - r = self.cs.fping.get(1) - self.assert_request_id(r, fakes.FAKE_REQUEST_ID_LIST) - self.assertEqual("", repr(r)) - - def test_list_fpings(self): - fl = self.cs.fping.list() - self.assert_request_id(fl, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('GET', '/os-fping') - for f in fl: - self.assertIsInstance(f, fping.Fping) - self.assertEqual("fake-project", f.project_id) - self.assertTrue(f.alive) - - def test_list_fpings_all_tenants(self): - fl = self.cs.fping.list(all_tenants=True) - self.assert_request_id(fl, fakes.FAKE_REQUEST_ID_LIST) - for f in fl: - self.assertIsInstance(f, fping.Fping) - self.assert_called('GET', '/os-fping?all_tenants=1') - - def test_list_fpings_exclude(self): - fl = self.cs.fping.list(exclude=['1']) - self.assert_request_id(fl, fakes.FAKE_REQUEST_ID_LIST) - for f in fl: - self.assertIsInstance(f, fping.Fping) - self.assert_called('GET', '/os-fping?exclude=1') - - def test_list_fpings_include(self): - fl = self.cs.fping.list(include=['1']) - self.assert_request_id(fl, fakes.FAKE_REQUEST_ID_LIST) - for f in fl: - self.assertIsInstance(f, fping.Fping) - self.assert_called('GET', '/os-fping?include=1') - - def test_get_fping(self): - f = self.cs.fping.get(1) - self.assert_request_id(f, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('GET', '/os-fping/1') - self.assertIsInstance(f, fping.Fping) - self.assertEqual("fake-project", f.project_id) - self.assertTrue(f.alive) diff --git a/novaclient/v2/client.py b/novaclient/v2/client.py index 5d8421cb7..9f67c8525 100644 --- a/novaclient/v2/client.py +++ b/novaclient/v2/client.py @@ -32,7 +32,6 @@ from novaclient.v2 import flavor_access from novaclient.v2 import flavors from novaclient.v2 import floating_ips -from novaclient.v2 import fping from novaclient.v2 import hosts from novaclient.v2 import hypervisors from novaclient.v2 import images @@ -160,7 +159,6 @@ def __init__(self, self.cloudpipe = cloudpipe.CloudpipeManager(self) self.certs = certs.CertificateManager(self) self.floating_ips = floating_ips.FloatingIPManager(self) - self.fping = fping.FpingManager(self) self.volumes = volumes.VolumeManager(self) self.keypairs = keypairs.KeypairManager(self) self.networks = networks.NetworkManager(self) diff --git a/novaclient/v2/fping.py b/novaclient/v2/fping.py deleted file mode 100644 index cee13226a..000000000 --- a/novaclient/v2/fping.py +++ /dev/null @@ -1,64 +0,0 @@ -# Copyright 2012 OpenStack Foundation -# All Rights Reserved. -# -# 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. - -""" -Fping interface. -""" -from six.moves import urllib - -from novaclient import api_versions -from novaclient import base - - -class Fping(base.Resource): - """DEPRECATED: A server to fping.""" - HUMAN_ID = True - - def __repr__(self): - return "" % self.id - - -class FpingManager(base.ManagerWithFind): - """DEPRECATED: Manage :class:`Fping` resources.""" - resource_class = Fping - - @api_versions.deprecated_after('2.35') - def list(self, all_tenants=False, include=None, exclude=None): - """DEPRECATED: Fping all servers. - - :returns: list of :class:`Fping`. - """ - include = include or [] - exclude = exclude or [] - params = [] - if all_tenants: - params.append(("all_tenants", 1)) - if include: - params.append(("include", ",".join(include))) - elif exclude: - params.append(("exclude", ",".join(exclude))) - uri = "/os-fping" - if params: - uri = "%s?%s" % (uri, urllib.parse.urlencode(params)) - return self._list(uri, "servers") - - @api_versions.deprecated_after('2.35') - def get(self, server): - """DEPRECATED: Fping a specific server. - - :param server: ID of the server to fping. - :returns: :class:`Fping` - """ - return self._get("/os-fping/%s" % base.getid(server), "server") diff --git a/releasenotes/notes/pike-rm-deprecated-net-272aeb62b329a5bc.yaml b/releasenotes/notes/pike-rm-deprecated-net-272aeb62b329a5bc.yaml index a0637b88d..f87f4c851 100644 --- a/releasenotes/notes/pike-rm-deprecated-net-272aeb62b329a5bc.yaml +++ b/releasenotes/notes/pike-rm-deprecated-net-272aeb62b329a5bc.yaml @@ -58,6 +58,7 @@ upgrade: * novaclient.v2.floating_ip_dns * novaclient.v2.floating_ip_pools * novaclient.v2.floating_ips_bulk + * novaclient.v2.fping deprecations: - | From a52c86c203b9c4391f4af948e6af5198bf5c0642 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Mon, 20 Mar 2017 17:38:00 -0400 Subject: [PATCH 1259/1705] Remove deprecated security_group_default_rules APIs These were deprecated in Newton: aaebeb05a03e34281a091dc6dfc4672b01cdfbbb Apparently we did not have any tests for this code. Change-Id: I33c18433e57941b26a4abd290b3cd03994d26327 --- novaclient/v2/client.py | 3 - novaclient/v2/security_group_default_rules.py | 93 ------------------- ...ke-rm-deprecated-net-272aeb62b329a5bc.yaml | 1 + 3 files changed, 1 insertion(+), 96 deletions(-) delete mode 100644 novaclient/v2/security_group_default_rules.py diff --git a/novaclient/v2/client.py b/novaclient/v2/client.py index 9f67c8525..4b97aad47 100644 --- a/novaclient/v2/client.py +++ b/novaclient/v2/client.py @@ -43,7 +43,6 @@ from novaclient.v2 import networks from novaclient.v2 import quota_classes from novaclient.v2 import quotas -from novaclient.v2 import security_group_default_rules from novaclient.v2 import security_group_rules from novaclient.v2 import security_groups from novaclient.v2 import server_external_events @@ -168,8 +167,6 @@ def __init__(self, self.security_groups = security_groups.SecurityGroupManager(self) self.security_group_rules = \ security_group_rules.SecurityGroupRuleManager(self) - self.security_group_default_rules = \ - security_group_default_rules.SecurityGroupDefaultRuleManager(self) self.usage = usage.UsageManager(self) self.virtual_interfaces = \ virtual_interfaces.VirtualInterfaceManager(self) diff --git a/novaclient/v2/security_group_default_rules.py b/novaclient/v2/security_group_default_rules.py deleted file mode 100644 index 58aef144a..000000000 --- a/novaclient/v2/security_group_default_rules.py +++ /dev/null @@ -1,93 +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. - -""" -Security group default rules interface. -""" -from novaclient import api_versions -from novaclient import base -from novaclient import exceptions -from novaclient.i18n import _ - - -class SecurityGroupDefaultRule(base.Resource): - """DEPRECATED""" - def __str__(self): - return str(self.id) - - def delete(self): - """ - DEPRECATED: Delete this security group default rule. - - :returns: An instance of novaclient.base.TupleWithMeta - """ - return self.manager.delete(self) - - -class SecurityGroupDefaultRuleManager(base.Manager): - """DEPRECATED""" - resource_class = SecurityGroupDefaultRule - - @api_versions.deprecated_after('2.35') - def create(self, ip_protocol=None, from_port=None, to_port=None, - cidr=None): - """ - DEPRECATED: Create a security group default rule - - :param ip_protocol: IP protocol, one of 'tcp', 'udp' or 'icmp' - :param from_port: Source port - :param to_port: Destination port - :param cidr: Destination IP address(es) in CIDR notation - """ - - try: - from_port = int(from_port) - except (TypeError, ValueError): - raise exceptions.CommandError(_("From port must be an integer.")) - try: - to_port = int(to_port) - except (TypeError, ValueError): - raise exceptions.CommandError(_("To port must be an integer.")) - if ip_protocol.upper() not in ['TCP', 'UDP', 'ICMP']: - raise exceptions.CommandError(_("IP protocol must be 'tcp', 'udp'" - ", or 'icmp'.")) - - body = {"security_group_default_rule": { - "ip_protocol": ip_protocol, - "from_port": from_port, - "to_port": to_port, - "cidr": cidr}} - - return self._create('/os-security-group-default-rules', body, - 'security_group_default_rule') - - @api_versions.deprecated_after('2.35') - def delete(self, rule): - """ - DEPRECATED: Delete a security group default rule - - :param rule: The security group default rule to delete (ID or Class) - :returns: An instance of novaclient.base.TupleWithMeta - """ - return self._delete('/os-security-group-default-rules/%s' % - base.getid(rule)) - - @api_versions.deprecated_after('2.35') - def list(self): - """ - DEPRECATED: Get a list of all security group default rules - - :rtype: list of :class:`SecurityGroupDefaultRule` - """ - - return self._list('/os-security-group-default-rules', - 'security_group_default_rules') diff --git a/releasenotes/notes/pike-rm-deprecated-net-272aeb62b329a5bc.yaml b/releasenotes/notes/pike-rm-deprecated-net-272aeb62b329a5bc.yaml index f87f4c851..6da0a4d3f 100644 --- a/releasenotes/notes/pike-rm-deprecated-net-272aeb62b329a5bc.yaml +++ b/releasenotes/notes/pike-rm-deprecated-net-272aeb62b329a5bc.yaml @@ -59,6 +59,7 @@ upgrade: * novaclient.v2.floating_ip_pools * novaclient.v2.floating_ips_bulk * novaclient.v2.fping + * novaclient.v2.security_group_default_rules deprecations: - | From 0896bdc52a307c0b9598da0b6b837a95f0c00b9a Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Mon, 20 Mar 2017 17:43:49 -0400 Subject: [PATCH 1260/1705] Remove deprecated security_group_rules APIs These were deprecated in Newton: aaebeb05a03e34281a091dc6dfc4672b01cdfbbb Change-Id: I2ff1e97c022bc57d468947e90d536a37c65b8a7a --- .../unit/fixture_data/security_group_rules.py | 58 ----------- novaclient/tests/unit/v2/fakes.py | 30 ------ .../unit/v2/test_security_group_rules.py | 95 ------------------- novaclient/v2/client.py | 3 - novaclient/v2/security_group_rules.py | 90 ------------------ ...ke-rm-deprecated-net-272aeb62b329a5bc.yaml | 1 + 6 files changed, 1 insertion(+), 276 deletions(-) delete mode 100644 novaclient/tests/unit/fixture_data/security_group_rules.py delete mode 100644 novaclient/tests/unit/v2/test_security_group_rules.py delete mode 100644 novaclient/v2/security_group_rules.py diff --git a/novaclient/tests/unit/fixture_data/security_group_rules.py b/novaclient/tests/unit/fixture_data/security_group_rules.py deleted file mode 100644 index 4fda1fe40..000000000 --- a/novaclient/tests/unit/fixture_data/security_group_rules.py +++ /dev/null @@ -1,58 +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 novaclient.tests.unit import fakes -from novaclient.tests.unit.fixture_data import base - - -class Fixture(base.Fixture): - - base_url = 'os-security-group-rules' - - def setUp(self): - super(Fixture, self).setUp() - - rule = { - 'id': 1, - 'parent_group_id': 1, - 'group_id': 2, - 'ip_protocol': 'TCP', - 'from_port': '22', - 'to_port': 22, - 'cidr': '10.0.0.0/8' - } - - headers = self.json_headers - - self.requests_mock.get(self.url(), - json={'security_group_rules': [rule]}, - headers=headers) - - for u in (1, 11, 12): - self.requests_mock.delete(self.url(u), - status_code=202, - headers=headers) - - def post_rules(request, context): - body = request.json() - assert list(body) == ['security_group_rule'] - fakes.assert_has_keys(body['security_group_rule'], - required=['parent_group_id'], - optional=['group_id', 'ip_protocol', - 'from_port', 'to_port', 'cidr']) - - return {'security_group_rule': rule} - - self.requests_mock.post(self.url(), - json=post_rules, - headers=headers, - status_code=202) diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 953f6cd69..c2fd61db3 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -1418,36 +1418,6 @@ def put_os_security_groups_1(self, body, **kw): required=['name', 'description']) return (205, {}, body) - # - # Security Group Rules - # - def get_os_security_group_rules(self, **kw): - return (200, {}, {"security_group_rules": [ - {'id': 1, 'parent_group_id': 1, 'group_id': 2, - 'ip_protocol': 'TCP', 'from_port': 22, 'to_port': 22, - 'cidr': '10.0.0.0/8'} - ]}) - - def delete_os_security_group_rules_11(self, **kw): - return (202, {}, None) - - def delete_os_security_group_rules_12(self, **kw): - return (202, {}, None) - - def delete_os_security_group_rules_14(self, **kw): - return (202, {}, None) - - def post_os_security_group_rules(self, body, **kw): - assert list(body) == ['security_group_rule'] - fakes.assert_has_keys( - body['security_group_rule'], - required=['parent_group_id'], - optional=['group_id', 'ip_protocol', 'from_port', - 'to_port', 'cidr']) - r = {'security_group_rule': - self.get_os_security_group_rules()[2]['security_group_rules'][0]} - return (202, {}, r) - # # Tenant Usage # diff --git a/novaclient/tests/unit/v2/test_security_group_rules.py b/novaclient/tests/unit/v2/test_security_group_rules.py deleted file mode 100644 index e56e3f13d..000000000 --- a/novaclient/tests/unit/v2/test_security_group_rules.py +++ /dev/null @@ -1,95 +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 novaclient import exceptions -from novaclient.tests.unit.fixture_data import client -from novaclient.tests.unit.fixture_data import security_group_rules as data -from novaclient.tests.unit import utils -from novaclient.tests.unit.v2 import fakes -from novaclient.v2 import security_group_rules - - -class SecurityGroupRulesTest(utils.FixturedTestCase): - - client_fixture_class = client.V1 - data_fixture_class = data.Fixture - - def test_delete_security_group_rule(self): - ret = self.cs.security_group_rules.delete(1) - self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('DELETE', '/os-security-group-rules/1') - - def test_create_security_group_rule(self): - sg = self.cs.security_group_rules.create(1, "tcp", 1, 65535, - "10.0.0.0/16") - self.assert_request_id(sg, fakes.FAKE_REQUEST_ID_LIST) - - body = { - "security_group_rule": { - "ip_protocol": "tcp", - "from_port": 1, - "to_port": 65535, - "cidr": "10.0.0.0/16", - "group_id": None, - "parent_group_id": 1, - } - } - - self.assert_called('POST', '/os-security-group-rules', body) - self.assertIsInstance(sg, security_group_rules.SecurityGroupRule) - - def test_create_security_group_group_rule(self): - sg = self.cs.security_group_rules.create(1, "tcp", 1, 65535, - "10.0.0.0/16", 101) - self.assert_request_id(sg, fakes.FAKE_REQUEST_ID_LIST) - - body = { - "security_group_rule": { - "ip_protocol": "tcp", - "from_port": 1, - "to_port": 65535, - "cidr": "10.0.0.0/16", - "group_id": 101, - "parent_group_id": 1, - } - } - - self.assert_called('POST', '/os-security-group-rules', body) - self.assertIsInstance(sg, security_group_rules.SecurityGroupRule) - - def test_invalid_parameters_create(self): - self.assertRaises(exceptions.CommandError, - self.cs.security_group_rules.create, - 1, "invalid_ip_protocol", 1, 65535, - "10.0.0.0/16", 101) - self.assertRaises(exceptions.CommandError, - self.cs.security_group_rules.create, - 1, "tcp", "invalid_from_port", 65535, - "10.0.0.0/16", 101) - self.assertRaises(exceptions.CommandError, - self.cs.security_group_rules.create, - 1, "tcp", 1, "invalid_to_port", - "10.0.0.0/16", 101) - - def test_security_group_rule_str(self): - sg = self.cs.security_group_rules.create(1, "tcp", 1, 65535, - "10.0.0.0/16") - self.assert_request_id(sg, fakes.FAKE_REQUEST_ID_LIST) - self.assertEqual('1', str(sg)) - - def test_security_group_rule_del(self): - sg = self.cs.security_group_rules.create(1, "tcp", 1, 65535, - "10.0.0.0/16") - ret = sg.delete() - self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('DELETE', '/os-security-group-rules/1') diff --git a/novaclient/v2/client.py b/novaclient/v2/client.py index 4b97aad47..03b3bb7af 100644 --- a/novaclient/v2/client.py +++ b/novaclient/v2/client.py @@ -43,7 +43,6 @@ from novaclient.v2 import networks from novaclient.v2 import quota_classes from novaclient.v2 import quotas -from novaclient.v2 import security_group_rules from novaclient.v2 import security_groups from novaclient.v2 import server_external_events from novaclient.v2 import server_groups @@ -165,8 +164,6 @@ def __init__(self, self.quota_classes = quota_classes.QuotaClassSetManager(self) self.quotas = quotas.QuotaSetManager(self) self.security_groups = security_groups.SecurityGroupManager(self) - self.security_group_rules = \ - security_group_rules.SecurityGroupRuleManager(self) self.usage = usage.UsageManager(self) self.virtual_interfaces = \ virtual_interfaces.VirtualInterfaceManager(self) diff --git a/novaclient/v2/security_group_rules.py b/novaclient/v2/security_group_rules.py deleted file mode 100644 index 750fba419..000000000 --- a/novaclient/v2/security_group_rules.py +++ /dev/null @@ -1,90 +0,0 @@ -# Copyright 2011 OpenStack Foundation -# All Rights Reserved. -# -# 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. - -""" -Security group rules interface (1.1 extension). -""" -from novaclient import api_versions -from novaclient import base -from novaclient import exceptions -from novaclient.i18n import _ - - -class SecurityGroupRule(base.Resource): - """DEPRECATED""" - - def __str__(self): - return str(self.id) - - def delete(self): - """ - DEPRECATED: Delete this security group rule. - - :returns: An instance of novaclient.base.TupleWithMeta - """ - return self.manager.delete(self) - - -class SecurityGroupRuleManager(base.Manager): - """DEPRECATED""" - - resource_class = SecurityGroupRule - - @api_versions.deprecated_after('2.35') - def create(self, parent_group_id, ip_protocol=None, from_port=None, - to_port=None, cidr=None, group_id=None): - """ - DEPRECATED: Create a security group rule - - :param ip_protocol: IP protocol, one of 'tcp', 'udp' or 'icmp' - :param from_port: Source port - :param to_port: Destination port - :param cidr: Destination IP address(es) in CIDR notation - :param group_id: Security group id (int) - :param parent_group_id: Parent security group id (int) - """ - - try: - from_port = int(from_port) - except (TypeError, ValueError): - raise exceptions.CommandError(_("From port must be an integer.")) - try: - to_port = int(to_port) - except (TypeError, ValueError): - raise exceptions.CommandError(_("To port must be an integer.")) - if ip_protocol.upper() not in ['TCP', 'UDP', 'ICMP']: - raise exceptions.CommandError(_("IP protocol must be 'tcp', 'udp'" - ", or 'icmp'.")) - - body = {"security_group_rule": { - "ip_protocol": ip_protocol, - "from_port": from_port, - "to_port": to_port, - "cidr": cidr, - "group_id": group_id, - "parent_group_id": parent_group_id}} - - return self._create('/os-security-group-rules', body, - 'security_group_rule') - - @api_versions.deprecated_after('2.35') - def delete(self, rule): - """ - DEPRECATED: Delete a security group rule - - :param rule: The security group rule to delete (ID or Class) - :returns: An instance of novaclient.base.TupleWithMeta - """ - return self._delete('/os-security-group-rules/%s' % base.getid(rule)) diff --git a/releasenotes/notes/pike-rm-deprecated-net-272aeb62b329a5bc.yaml b/releasenotes/notes/pike-rm-deprecated-net-272aeb62b329a5bc.yaml index 6da0a4d3f..4856b4eac 100644 --- a/releasenotes/notes/pike-rm-deprecated-net-272aeb62b329a5bc.yaml +++ b/releasenotes/notes/pike-rm-deprecated-net-272aeb62b329a5bc.yaml @@ -60,6 +60,7 @@ upgrade: * novaclient.v2.floating_ips_bulk * novaclient.v2.fping * novaclient.v2.security_group_default_rules + * novaclient.v2.security_group_rules deprecations: - | From a298b29cc7e6b7330945b1890f0a4bd4c9f3fde6 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Mon, 20 Mar 2017 18:42:45 -0400 Subject: [PATCH 1261/1705] Remove deprecated security_groups APIs These were deprecated in Newton: aaebeb05a03e34281a091dc6dfc4672b01cdfbbb The SecurityGroup resource object is left since the list-secgroup command and list_security_group servers API code still uses that object. The /servers/{server_id}/os-security-groups API is a proxy to Neutron but was not deprecated in microversion 2.36 so it's not yet deprecated here in the client. Change-Id: I6fa14f43d48f1e035ef54bd2d0078506f0c6d6e0 --- .../unit/fixture_data/security_groups.py | 100 --------------- novaclient/tests/unit/v2/fakes.py | 69 ----------- .../tests/unit/v2/test_security_groups.py | 84 ------------- novaclient/v2/client.py | 2 - novaclient/v2/security_groups.py | 115 ------------------ novaclient/v2/servers.py | 8 +- ...ke-rm-deprecated-net-272aeb62b329a5bc.yaml | 1 + 7 files changed, 7 insertions(+), 372 deletions(-) delete mode 100644 novaclient/tests/unit/fixture_data/security_groups.py delete mode 100644 novaclient/tests/unit/v2/test_security_groups.py delete mode 100644 novaclient/v2/security_groups.py diff --git a/novaclient/tests/unit/fixture_data/security_groups.py b/novaclient/tests/unit/fixture_data/security_groups.py deleted file mode 100644 index b0df1f9ed..000000000 --- a/novaclient/tests/unit/fixture_data/security_groups.py +++ /dev/null @@ -1,100 +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 novaclient.tests.unit import fakes -from novaclient.tests.unit.fixture_data import base - - -class Fixture(base.Fixture): - - base_url = 'os-security-groups' - - def setUp(self): - super(Fixture, self).setUp() - - security_group_1 = { - "name": "test", - "description": "FAKE_SECURITY_GROUP", - "tenant_id": "4ffc664c198e435e9853f2538fbcd7a7", - "id": 1, - "rules": [ - { - "id": 11, - "group": {}, - "ip_protocol": "TCP", - "from_port": 22, - "to_port": 22, - "parent_group_id": 1, - "ip_range": {"cidr": "10.0.0.0/8"} - }, - { - "id": 12, - "group": { - "tenant_id": "272bee4c1e624cd4a72a6b0ea55b4582", - "name": "test2" - }, - "ip_protocol": "TCP", - "from_port": 222, - "to_port": 222, - "parent_group_id": 1, - "ip_range": {} - } - ] - } - - security_group_2 = { - "name": "test2", - "description": "FAKE_SECURITY_GROUP2", - "tenant_id": "272bee4c1e624cd4a72a6b0ea55b4582", - "id": 2, - "rules": [] - } - - get_groups = {'security_groups': [security_group_1, security_group_2]} - headers = self.json_headers - - self.requests_mock.get(self.url(), - json=get_groups, - headers=headers) - - get_group_1 = {'security_group': security_group_1} - self.requests_mock.get(self.url(1), - json=get_group_1, - headers=headers) - - self.requests_mock.delete(self.url(1), - status_code=202, - headers=headers) - - def post_os_security_groups(request, context): - body = request.json() - assert list(body) == ['security_group'] - fakes.assert_has_keys(body['security_group'], - required=['name', 'description']) - return {'security_group': security_group_1} - - self.requests_mock.post(self.url(), - json=post_os_security_groups, - headers=headers, - status_code=202) - - def put_os_security_groups_1(request, context): - body = request.json() - assert list(body) == ['security_group'] - fakes.assert_has_keys(body['security_group'], - required=['name', 'description']) - return body - - self.requests_mock.put(self.url(1), - json=put_os_security_groups_1, - headers=headers, - status_code=205) diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index c2fd61db3..0016b31d7 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -1349,75 +1349,6 @@ def put_os_quota_class_sets_97f4c221bff44578b0300df4ef119353(self, 'security_groups': 1, 'security_group_rules': 1}}) - # - # Security Groups - # - def get_os_security_groups(self, **kw): - return (200, {}, {"security_groups": [ - {"name": "test", - "description": "FAKE_SECURITY_GROUP", - "tenant_id": "4ffc664c198e435e9853f2538fbcd7a7", - "id": 1, - "rules": [ - {"id": 11, - "group": {}, - "ip_protocol": "TCP", - "from_port": 22, - "to_port": 22, - "parent_group_id": 1, - "ip_range": - {"cidr": "10.0.0.0/8"}}, - {"id": 12, - "group": { - "tenant_id": - "272bee4c1e624cd4a72a6b0ea55b4582", - "name": "test2"}, - - "ip_protocol": "TCP", - "from_port": 222, - "to_port": 222, - "parent_group_id": 1, - "ip_range": {}}, - {"id": 14, - "group": { - "tenant_id": - "272bee4c1e624cd4a72a6b0ea55b4582", - "name": "test4"}, - - "ip_protocol": "TCP", - "from_port": -1, - "to_port": -1, - "parent_group_id": 1, - "ip_range": {}}]}, - {"name": "test2", - "description": "FAKE_SECURITY_GROUP2", - "tenant_id": "272bee4c1e624cd4a72a6b0ea55b4582", - "id": 2, - "rules": []}, - {"name": "test4", - "description": "FAKE_SECURITY_GROUP4", - "tenant_id": "272bee4c1e624cd4a72a6b0ea55b4582", - "id": 4, - "rules": []} - ]}) - - def delete_os_security_groups_1(self, **kw): - return (202, {}, None) - - def post_os_security_groups(self, body, **kw): - assert list(body) == ['security_group'] - fakes.assert_has_keys(body['security_group'], - required=['name', 'description']) - r = {'security_group': - self.get_os_security_groups()[2]['security_groups'][0]} - return (202, {}, r) - - def put_os_security_groups_1(self, body, **kw): - assert list(body) == ['security_group'] - fakes.assert_has_keys(body['security_group'], - required=['name', 'description']) - return (205, {}, body) - # # Tenant Usage # diff --git a/novaclient/tests/unit/v2/test_security_groups.py b/novaclient/tests/unit/v2/test_security_groups.py deleted file mode 100644 index df7c39ae7..000000000 --- a/novaclient/tests/unit/v2/test_security_groups.py +++ /dev/null @@ -1,84 +0,0 @@ -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from novaclient.tests.unit.fixture_data import client -from novaclient.tests.unit.fixture_data import security_groups as data -from novaclient.tests.unit import utils -from novaclient.tests.unit.v2 import fakes -from novaclient.v2 import security_groups - - -class SecurityGroupsTest(utils.FixturedTestCase): - - client_fixture_class = client.V1 - data_fixture_class = data.Fixture - - def _do_test_list_security_groups(self, search_opts, path): - sgs = self.cs.security_groups.list(search_opts=search_opts) - self.assert_request_id(sgs, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('GET', path) - for sg in sgs: - self.assertIsInstance(sg, security_groups.SecurityGroup) - - def test_list_security_groups_all_tenants_on(self): - self._do_test_list_security_groups( - None, '/os-security-groups') - - def test_list_security_groups_all_tenants_on_with_search_opts(self): - self._do_test_list_security_groups( - {'all_tenants': 1}, '/os-security-groups?all_tenants=1') - - def test_list_security_groups_all_tenants_off(self): - self._do_test_list_security_groups( - {'all_tenants': 0}, '/os-security-groups') - - def test_get_security_groups(self): - sg = self.cs.security_groups.get(1) - self.assert_request_id(sg, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('GET', '/os-security-groups/1') - self.assertIsInstance(sg, security_groups.SecurityGroup) - self.assertEqual('1', str(sg)) - - def test_delete_security_group(self): - sg = self.cs.security_groups.list()[0] - ret = sg.delete() - self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('DELETE', '/os-security-groups/1') - ret = self.cs.security_groups.delete(1) - self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('DELETE', '/os-security-groups/1') - ret = self.cs.security_groups.delete(sg) - self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('DELETE', '/os-security-groups/1') - - def test_create_security_group(self): - sg = self.cs.security_groups.create("foo", "foo barr") - self.assert_request_id(sg, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('POST', '/os-security-groups') - self.assertIsInstance(sg, security_groups.SecurityGroup) - - def test_update_security_group(self): - sg = self.cs.security_groups.list()[0] - secgroup = self.cs.security_groups.update(sg, "update", "update") - self.assert_request_id(secgroup, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('PUT', '/os-security-groups/1') - self.assertIsInstance(secgroup, security_groups.SecurityGroup) - - def test_refresh_security_group(self): - sg = self.cs.security_groups.get(1) - sg2 = self.cs.security_groups.get(1) - self.assertEqual(sg.name, sg2.name) - sg2.name = "should be test" - self.assertNotEqual(sg.name, sg2.name) - sg2.get() - self.assertEqual(sg.name, sg2.name) diff --git a/novaclient/v2/client.py b/novaclient/v2/client.py index 03b3bb7af..99e82035d 100644 --- a/novaclient/v2/client.py +++ b/novaclient/v2/client.py @@ -43,7 +43,6 @@ from novaclient.v2 import networks from novaclient.v2 import quota_classes from novaclient.v2 import quotas -from novaclient.v2 import security_groups from novaclient.v2 import server_external_events from novaclient.v2 import server_groups from novaclient.v2 import server_migrations @@ -163,7 +162,6 @@ def __init__(self, self.neutron = networks.NeutronManager(self) self.quota_classes = quota_classes.QuotaClassSetManager(self) self.quotas = quotas.QuotaSetManager(self) - self.security_groups = security_groups.SecurityGroupManager(self) self.usage = usage.UsageManager(self) self.virtual_interfaces = \ virtual_interfaces.VirtualInterfaceManager(self) diff --git a/novaclient/v2/security_groups.py b/novaclient/v2/security_groups.py deleted file mode 100644 index 5a87244e4..000000000 --- a/novaclient/v2/security_groups.py +++ /dev/null @@ -1,115 +0,0 @@ -# Copyright 2011 OpenStack Foundation -# All Rights Reserved. -# -# 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. - -""" -Security group interface (1.1 extension). -""" - -from six.moves.urllib import parse - -from novaclient import api_versions -from novaclient import base - - -class SecurityGroup(base.Resource): - """DEPRECATED""" - - def __str__(self): - return str(self.id) - - def delete(self): - """ - DEPRECATED: Delete this security group. - - :returns: An instance of novaclient.base.TupleWithMeta - """ - return self.manager.delete(self) - - def update(self): - """ - DEPRECATED: Update this security group. - - :returns: :class:`SecurityGroup` - """ - return self.manager.update(self) - - -class SecurityGroupManager(base.ManagerWithFind): - """DEPRECATED""" - - resource_class = SecurityGroup - - @api_versions.deprecated_after('2.35') - def create(self, name, description): - """ - DEPRECATED: Create a security group - - :param name: name for the security group to create - :param description: description of the security group - :rtype: the security group object - """ - body = {"security_group": {"name": name, 'description': description}} - return self._create('/os-security-groups', body, 'security_group') - - @api_versions.deprecated_after('2.35') - def update(self, group, name, description): - """ - DEPRECATED: Update a security group - - :param group: The security group to update (group or ID) - :param name: name for the security group to update - :param description: description for the security group to update - :rtype: the security group object - """ - body = {"security_group": {"name": name, 'description': description}} - return self._update('/os-security-groups/%s' % base.getid(group), - body, 'security_group') - - @api_versions.deprecated_after('2.35') - def delete(self, group): - """ - DEPRECATED: Delete a security group - - :param group: The security group to delete (group or ID) - :returns: An instance of novaclient.base.TupleWithMeta - """ - return self._delete('/os-security-groups/%s' % base.getid(group)) - - @api_versions.deprecated_after('2.35') - def get(self, group_id): - """ - DEPRECATED: Get a security group - - :param group_id: The security group to get by ID - :rtype: :class:`SecurityGroup` - """ - return self._get('/os-security-groups/%s' % group_id, - 'security_group') - - @api_versions.deprecated_after('2.35') - def list(self, search_opts=None): - """ - DEPRECATED: Get a list of all security_groups - - :rtype: list of :class:`SecurityGroup` - """ - search_opts = search_opts or {} - - qparams = dict((k, v) for (k, v) in search_opts.items() if v) - - query_string = '?%s' % parse.urlencode(qparams) if qparams else '' - - return self._list('/os-security-groups%s' % query_string, - 'security_groups') diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index 7a08a43a3..ea05acd59 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -30,7 +30,6 @@ from novaclient import crypto from novaclient import exceptions from novaclient.i18n import _ -from novaclient.v2 import security_groups REBOOT_SOFT, REBOOT_HARD = 'SOFT', 'HARD' @@ -632,6 +631,11 @@ def __repr__(self): return '' % self.id +class SecurityGroup(base.Resource): + def __str__(self): + return str(self.id) + + class ServerManager(base.BootingManagerWithFind): resource_class = Server @@ -1780,7 +1784,7 @@ def list_security_group(self, server): """ return self._list('/servers/%s/os-security-groups' % base.getid(server), 'security_groups', - security_groups.SecurityGroup) + SecurityGroup) @api_versions.wraps("2.0", "2.13") def evacuate(self, server, host=None, on_shared_storage=True, diff --git a/releasenotes/notes/pike-rm-deprecated-net-272aeb62b329a5bc.yaml b/releasenotes/notes/pike-rm-deprecated-net-272aeb62b329a5bc.yaml index 4856b4eac..1d706d001 100644 --- a/releasenotes/notes/pike-rm-deprecated-net-272aeb62b329a5bc.yaml +++ b/releasenotes/notes/pike-rm-deprecated-net-272aeb62b329a5bc.yaml @@ -61,6 +61,7 @@ upgrade: * novaclient.v2.fping * novaclient.v2.security_group_default_rules * novaclient.v2.security_group_rules + * novaclient.v2.security_groups deprecations: - | From 4dbb20e5ac81b73a0a4b7d4151c2ccb37c97d5b2 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Mon, 20 Mar 2017 18:49:19 -0400 Subject: [PATCH 1262/1705] Remove deprecated fixed_ips APIs These were deprecated in Newton: aaebeb05a03e34281a091dc6dfc4672b01cdfbbb Change-Id: Iee5bc26384d2079d6a5a8eded1ce5a003439fc95 --- .../tests/unit/fixture_data/fixedips.py | 38 ----------- novaclient/tests/unit/v2/fakes.py | 12 ---- novaclient/tests/unit/v2/test_fixed_ips.py | 48 -------------- novaclient/v2/client.py | 2 - novaclient/v2/fixed_ips.py | 65 ------------------- ...ke-rm-deprecated-net-272aeb62b329a5bc.yaml | 1 + 6 files changed, 1 insertion(+), 165 deletions(-) delete mode 100644 novaclient/tests/unit/fixture_data/fixedips.py delete mode 100644 novaclient/tests/unit/v2/test_fixed_ips.py delete mode 100644 novaclient/v2/fixed_ips.py diff --git a/novaclient/tests/unit/fixture_data/fixedips.py b/novaclient/tests/unit/fixture_data/fixedips.py deleted file mode 100644 index cf50666bb..000000000 --- a/novaclient/tests/unit/fixture_data/fixedips.py +++ /dev/null @@ -1,38 +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 novaclient.tests.unit.fixture_data import base - - -class Fixture(base.Fixture): - - base_url = 'os-fixed-ips' - - def setUp(self): - super(Fixture, self).setUp() - - get_os_fixed_ips = { - "fixed_ip": { - 'cidr': '192.168.1.0/24', - 'address': '192.168.1.1', - 'hostname': 'foo', - 'host': 'bar' - } - } - - self.requests_mock.get(self.url('192.168.1.1'), - json=get_os_fixed_ips, - headers=self.json_headers) - - self.requests_mock.post(self.url('192.168.1.1', 'action'), - headers=self.json_headers, - status_code=202) diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 0016b31d7..179cb883c 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -1610,18 +1610,6 @@ def put_os_services_force_down(self, body, **kw): 'binary': body['binary'], 'forced_down': False}}) - # - # Fixed IPs - # - def get_os_fixed_ips_192_168_1_1(self, *kw): - return (200, {}, {"fixed_ip": {'cidr': '192.168.1.0/24', - 'address': '192.168.1.1', - 'hostname': 'foo', - 'host': 'bar'}}) - - def post_os_fixed_ips_192_168_1_1_action(self, body, **kw): - return (202, {}, None) - # # Hosts # diff --git a/novaclient/tests/unit/v2/test_fixed_ips.py b/novaclient/tests/unit/v2/test_fixed_ips.py deleted file mode 100644 index d7d74bf26..000000000 --- a/novaclient/tests/unit/v2/test_fixed_ips.py +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright 2012 IBM Corp. -# All Rights Reserved. -# -# 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 novaclient.tests.unit.fixture_data import client -from novaclient.tests.unit.fixture_data import fixedips as data -from novaclient.tests.unit import utils -from novaclient.tests.unit.v2 import fakes - - -class FixedIpsTest(utils.FixturedTestCase): - - data_fixture_class = data.Fixture - - scenarios = [('original', {'client_fixture_class': client.V1}), - ('session', {'client_fixture_class': client.SessionV1})] - - def test_get_fixed_ip(self): - info = self.cs.fixed_ips.get(fixed_ip='192.168.1.1') - self.assert_request_id(info, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('GET', '/os-fixed-ips/192.168.1.1') - self.assertEqual('192.168.1.0/24', info.cidr) - self.assertEqual('192.168.1.1', info.address) - self.assertEqual('foo', info.hostname) - self.assertEqual('bar', info.host) - - def test_reserve_fixed_ip(self): - body = {"reserve": None} - fixedip = self.cs.fixed_ips.reserve(fixed_ip='192.168.1.1') - self.assert_request_id(fixedip, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('POST', '/os-fixed-ips/192.168.1.1/action', body) - - def test_unreserve_fixed_ip(self): - body = {"unreserve": None} - fixedip = self.cs.fixed_ips.unreserve(fixed_ip='192.168.1.1') - self.assert_request_id(fixedip, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('POST', '/os-fixed-ips/192.168.1.1/action', body) diff --git a/novaclient/v2/client.py b/novaclient/v2/client.py index 99e82035d..e00a25552 100644 --- a/novaclient/v2/client.py +++ b/novaclient/v2/client.py @@ -28,7 +28,6 @@ from novaclient.v2 import certs from novaclient.v2 import cloudpipe from novaclient.v2 import contrib -from novaclient.v2 import fixed_ips from novaclient.v2 import flavor_access from novaclient.v2 import flavors from novaclient.v2 import floating_ips @@ -170,7 +169,6 @@ def __init__(self, self.hypervisors = hypervisors.HypervisorManager(self) self.hypervisor_stats = hypervisors.HypervisorStatsManager(self) self.services = services.ServiceManager(self) - self.fixed_ips = fixed_ips.FixedIPsManager(self) self.os_cache = os_cache self.availability_zones = \ availability_zones.AvailabilityZoneManager(self) diff --git a/novaclient/v2/fixed_ips.py b/novaclient/v2/fixed_ips.py deleted file mode 100644 index 16604690c..000000000 --- a/novaclient/v2/fixed_ips.py +++ /dev/null @@ -1,65 +0,0 @@ -# Copyright 2012 IBM Corp. -# All Rights Reserved. -# -# 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. - -""" -Fixed IPs interface. -""" - -from novaclient import api_versions -from novaclient import base - - -class FixedIP(base.Resource): - """DEPRECATED""" - def __repr__(self): - return "" % self.address - - -class FixedIPsManager(base.Manager): - """DEPRECATED""" - resource_class = FixedIP - - @api_versions.deprecated_after('2.35') - def get(self, fixed_ip): - """DEPRECATED: Show information for a Fixed IP. - - :param fixed_ip: Fixed IP address to get info for - """ - return self._get('/os-fixed-ips/%s' % base.getid(fixed_ip), - "fixed_ip") - - @api_versions.deprecated_after('2.35') - def reserve(self, fixed_ip): - """DEPRECATED: Reserve a Fixed IP. - - :param fixed_ip: Fixed IP address to reserve - :returns: An instance of novaclient.base.TupleWithMeta - """ - body = {"reserve": None} - resp, body = self.api.client.post('/os-fixed-ips/%s/action' % - base.getid(fixed_ip), body=body) - return self.convert_into_with_meta(body, resp) - - @api_versions.deprecated_after('2.35') - def unreserve(self, fixed_ip): - """DEPRECATED: Unreserve a Fixed IP. - - :param fixed_ip: Fixed IP address to unreserve - :returns: An instance of novaclient.base.TupleWithMeta - """ - body = {"unreserve": None} - resp, body = self.api.client.post('/os-fixed-ips/%s/action' % - base.getid(fixed_ip), body=body) - return self.convert_into_with_meta(body, resp) diff --git a/releasenotes/notes/pike-rm-deprecated-net-272aeb62b329a5bc.yaml b/releasenotes/notes/pike-rm-deprecated-net-272aeb62b329a5bc.yaml index 1d706d001..8718207f4 100644 --- a/releasenotes/notes/pike-rm-deprecated-net-272aeb62b329a5bc.yaml +++ b/releasenotes/notes/pike-rm-deprecated-net-272aeb62b329a5bc.yaml @@ -55,6 +55,7 @@ upgrade: Along with the following python API bindings:: * novaclient.v2.contrib.tenant_networks + * novaclient.v2.fixed_ips * novaclient.v2.floating_ip_dns * novaclient.v2.floating_ip_pools * novaclient.v2.floating_ips_bulk From 0f46143f3b7134c615d49648ca2a8de21399cea5 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Mon, 20 Mar 2017 19:18:10 -0400 Subject: [PATCH 1263/1705] Remove deprecated floating_ips APIs These were deprecated in Newton: aaebeb05a03e34281a091dc6dfc4672b01cdfbbb The floating IP fixtures and GET API methods in the manager are still used for testing the addFloatingIP and removeFloatingIP server operations, so the manager is moved to the test_servers module. Change-Id: I2138e43160772c7c896bc5e93195a8169ebfe6c3 --- novaclient/tests/unit/v2/fakes.py | 34 ---------- novaclient/tests/unit/v2/test_floating_ips.py | 66 ------------------- novaclient/tests/unit/v2/test_servers.py | 29 ++++++-- novaclient/v2/client.py | 2 - novaclient/v2/floating_ips.py | 60 ----------------- ...ke-rm-deprecated-net-272aeb62b329a5bc.yaml | 1 + 6 files changed, 24 insertions(+), 168 deletions(-) delete mode 100644 novaclient/tests/unit/v2/test_floating_ips.py delete mode 100644 novaclient/v2/floating_ips.py diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 179cb883c..465485548 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -996,40 +996,6 @@ def post_flavors_2_action(self, body, **kw): return (202, FAKE_RESPONSE_HEADERS, self.get_flavors_2_os_flavor_access()[2]) - # - # Floating IPs - # - - def get_os_floating_ips(self, **kw): - return ( - 200, - {}, - {'floating_ips': [ - {'id': 1, 'fixed_ip': '10.0.0.1', 'ip': '11.0.0.1'}, - {'id': 2, 'fixed_ip': '10.0.0.2', 'ip': '11.0.0.2'}, - ]}, - ) - - def get_os_floating_ips_1(self, **kw): - return ( - 200, {}, {'floating_ip': {'id': 1, 'fixed_ip': '10.0.0.1', - 'ip': '11.0.0.1'}}) - - def post_os_floating_ips(self, body): - if body.get('pool'): - return ( - 200, {}, {'floating_ip': {'id': 1, 'fixed_ip': '10.0.0.1', - 'ip': '11.0.0.1', - 'pool': 'nova'}}) - else: - return ( - 200, {}, {'floating_ip': {'id': 1, 'fixed_ip': '10.0.0.1', - 'ip': '11.0.0.1', - 'pool': None}}) - - def delete_os_floating_ips_1(self, **kw): - return (204, {}, None) - # # Images # diff --git a/novaclient/tests/unit/v2/test_floating_ips.py b/novaclient/tests/unit/v2/test_floating_ips.py deleted file mode 100644 index f038ecc95..000000000 --- a/novaclient/tests/unit/v2/test_floating_ips.py +++ /dev/null @@ -1,66 +0,0 @@ -# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. -# -# All Rights Reserved. -# -# 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 novaclient.tests.unit.fixture_data import client -from novaclient.tests.unit.fixture_data import floatingips as data -from novaclient.tests.unit import utils -from novaclient.tests.unit.v2 import fakes -from novaclient.v2 import floating_ips - - -class FloatingIPsTest(utils.FixturedTestCase): - - client_fixture_class = client.V1 - data_fixture_class = data.FloatingFixture - - def test_list_floating_ips(self): - fips = self.cs.floating_ips.list() - self.assert_request_id(fips, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('GET', '/os-floating-ips') - for fip in fips: - self.assertIsInstance(fip, floating_ips.FloatingIP) - - def test_delete_floating_ip(self): - fl = self.cs.floating_ips.list()[0] - ret = fl.delete() - self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('DELETE', '/os-floating-ips/1') - ret = self.cs.floating_ips.delete(1) - self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('DELETE', '/os-floating-ips/1') - ret = self.cs.floating_ips.delete(fl) - self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('DELETE', '/os-floating-ips/1') - - def test_create_floating_ip(self): - fl = self.cs.floating_ips.create() - self.assert_request_id(fl, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('POST', '/os-floating-ips') - self.assertIsNone(fl.pool) - self.assertIsInstance(fl, floating_ips.FloatingIP) - - def test_create_floating_ip_with_pool(self): - fl = self.cs.floating_ips.create('nova') - self.assert_request_id(fl, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('POST', '/os-floating-ips') - self.assertEqual('nova', fl.pool) - self.assertIsInstance(fl, floating_ips.FloatingIP) - - def test_repr(self): - fl = self.cs.floating_ips.list()[0] - string = "%s" % fl - self.assertEqual("", - string) diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index a309d558a..3bb22bfb5 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -20,6 +20,7 @@ import six from novaclient import api_versions +from novaclient import base from novaclient import exceptions from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit.fixture_data import floatingips @@ -29,6 +30,21 @@ from novaclient.v2 import servers +class _FloatingIPManager(base.Manager): + resource_class = base.Resource + + @api_versions.deprecated_after('2.35') + def list(self): + """DEPRECATED: List floating IPs""" + return self._list("/os-floating-ips", "floating_ips") + + @api_versions.deprecated_after('2.35') + def get(self, floating_ip): + """DEPRECATED: Retrieve a floating IP""" + return self._get("/os-floating-ips/%s" % base.getid(floating_ip), + "floating_ip") + + class ServersTest(utils.FixturedTestCase): client_fixture_class = client.V1 @@ -40,6 +56,7 @@ def setUp(self): self.useFixture(floatingips.FloatingFixture(self.requests_mock)) if self.api_version: self.cs.api_version = api_versions.APIVersion(self.api_version) + self.floating_ips = _FloatingIPManager(self.cs) def _get_server_create_default_nics(self): """Callback for default nics kwarg when creating a server. @@ -565,7 +582,7 @@ def test_add_floating_ip(self): fip = self.cs.servers.add_floating_ip(s, '11.0.0.1') self.assert_request_id(fip, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') - f = self.cs.floating_ips.list()[0] + f = self.floating_ips.list()[0] fip = self.cs.servers.add_floating_ip(s, f) self.assert_request_id(fip, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') @@ -582,7 +599,7 @@ def test_add_floating_ip_to_fixed(self): fixed_address='12.0.0.1') self.assert_request_id(fip, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') - f = self.cs.floating_ips.list()[0] + f = self.floating_ips.list()[0] fip = self.cs.servers.add_floating_ip(s, f) self.assert_request_id(fip, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') @@ -598,7 +615,7 @@ def test_remove_floating_ip(self): ret = self.cs.servers.remove_floating_ip(s, '11.0.0.1') self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') - f = self.cs.floating_ips.list()[0] + f = self.floating_ips.list()[0] ret = self.cs.servers.remove_floating_ip(s, f) self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') @@ -1374,13 +1391,13 @@ def test_create_server_with_nics_auto(self): self.assertIsInstance(s, servers.Server) def test_add_floating_ip(self): - # self.cs.floating_ips.list() is not available after 2.35 + # self.floating_ips.list() is not available after 2.35 pass def test_add_floating_ip_to_fixed(self): - # self.cs.floating_ips.list() is not available after 2.35 + # self.floating_ips.list() is not available after 2.35 pass def test_remove_floating_ip(self): - # self.cs.floating_ips.list() is not available after 2.35 + # self.floating_ips.list() is not available after 2.35 pass diff --git a/novaclient/v2/client.py b/novaclient/v2/client.py index e00a25552..2ffa7bd93 100644 --- a/novaclient/v2/client.py +++ b/novaclient/v2/client.py @@ -30,7 +30,6 @@ from novaclient.v2 import contrib from novaclient.v2 import flavor_access from novaclient.v2 import flavors -from novaclient.v2 import floating_ips from novaclient.v2 import hosts from novaclient.v2 import hypervisors from novaclient.v2 import images @@ -154,7 +153,6 @@ def __init__(self, self.agents = agents.AgentsManager(self) self.cloudpipe = cloudpipe.CloudpipeManager(self) self.certs = certs.CertificateManager(self) - self.floating_ips = floating_ips.FloatingIPManager(self) self.volumes = volumes.VolumeManager(self) self.keypairs = keypairs.KeypairManager(self) self.networks = networks.NetworkManager(self) diff --git a/novaclient/v2/floating_ips.py b/novaclient/v2/floating_ips.py deleted file mode 100644 index 2aea6a3b6..000000000 --- a/novaclient/v2/floating_ips.py +++ /dev/null @@ -1,60 +0,0 @@ -# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. -# Copyright 2011 OpenStack Foundation -# All Rights Reserved. -# -# 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 novaclient import api_versions -from novaclient import base - - -class FloatingIP(base.Resource): - """DEPRECATED""" - - def delete(self): - """ - DEPRECATED: Delete this floating IP - - :returns: An instance of novaclient.base.TupleWithMeta - """ - return self.manager.delete(self) - - -class FloatingIPManager(base.ManagerWithFind): - """DEPRECATED""" - resource_class = FloatingIP - - @api_versions.deprecated_after('2.35') - def list(self): - """DEPRECATED: List floating IPs""" - return self._list("/os-floating-ips", "floating_ips") - - @api_versions.deprecated_after('2.35') - def create(self, pool=None): - """DEPRECATED: Create (allocate) a floating IP for a tenant""" - return self._create("/os-floating-ips", {'pool': pool}, "floating_ip") - - @api_versions.deprecated_after('2.35') - def delete(self, floating_ip): - """DEPRECATED: Delete (deallocate) a floating IP for a tenant - - :param floating_ip: The floating IP address to delete. - :returns: An instance of novaclient.base.TupleWithMeta - """ - return self._delete("/os-floating-ips/%s" % base.getid(floating_ip)) - - @api_versions.deprecated_after('2.35') - def get(self, floating_ip): - """DEPRECATED: Retrieve a floating IP""" - return self._get("/os-floating-ips/%s" % base.getid(floating_ip), - "floating_ip") diff --git a/releasenotes/notes/pike-rm-deprecated-net-272aeb62b329a5bc.yaml b/releasenotes/notes/pike-rm-deprecated-net-272aeb62b329a5bc.yaml index 8718207f4..fa2faf379 100644 --- a/releasenotes/notes/pike-rm-deprecated-net-272aeb62b329a5bc.yaml +++ b/releasenotes/notes/pike-rm-deprecated-net-272aeb62b329a5bc.yaml @@ -58,6 +58,7 @@ upgrade: * novaclient.v2.fixed_ips * novaclient.v2.floating_ip_dns * novaclient.v2.floating_ip_pools + * novaclient.v2.floating_ips * novaclient.v2.floating_ips_bulk * novaclient.v2.fping * novaclient.v2.security_group_default_rules From db55f563adbb470d452e6eafe2842447241eb7cf Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Sun, 19 Mar 2017 20:25:07 -0400 Subject: [PATCH 1264/1705] Drop deprecated aggregate-update positional args The name and availability_zone positional arguments were deprecated in Newton as part of change: 8030879330da432d4791f64fad4df24c7fc16f71 This change removes them. Change-Id: I61031eb9eea37891f0be5d67942aaae0859065f0 --- .../tests/functional/v2/test_aggregates.py | 15 ----------- novaclient/v2/shell.py | 26 +++---------------- ...ggregate-update-args-17bd019f4be34b18.yaml | 6 +++++ 3 files changed, 10 insertions(+), 37 deletions(-) create mode 100644 releasenotes/notes/drop-deprecated-aggregate-update-args-17bd019f4be34b18.yaml diff --git a/novaclient/tests/functional/v2/test_aggregates.py b/novaclient/tests/functional/v2/test_aggregates.py index 6eb30242e..b89e35664 100644 --- a/novaclient/tests/functional/v2/test_aggregates.py +++ b/novaclient/tests/functional/v2/test_aggregates.py @@ -32,13 +32,6 @@ def _clean_aggregates(self): except Exception: pass - def test_aggregate_update_name_legacy(self): - self.nova('aggregate-create', params=self.agg1) - self.nova('aggregate-update', params='%s %s' % (self.agg1, self.agg2)) - output = self.nova('aggregate-show', params=self.agg2) - self.assertIn(self.agg2, output) - self.nova('aggregate-delete', params=self.agg2) - def test_aggregate_update_name(self): self.nova('aggregate-create', params=self.agg1) self.nova('aggregate-update', @@ -47,14 +40,6 @@ def test_aggregate_update_name(self): self.assertIn(self.agg2, output) self.nova('aggregate-delete', params=self.agg2) - def test_aggregate_update_az_legacy(self): - self.nova('aggregate-create', params=self.agg2) - self.nova('aggregate-update', - params='%s %s myaz' % (self.agg2, self.agg2)) - output = self.nova('aggregate-show', params=self.agg2) - self.assertIn('myaz', output) - self.nova('aggregate-delete', params=self.agg2) - def test_aggregate_update_az(self): self.nova('aggregate-create', params=self.agg2) self.nova('aggregate-update', diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 6339a6432..87b9e6730 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -2971,27 +2971,10 @@ def do_aggregate_delete(cs, args): 'aggregate', metavar='', help=_('Name or ID of aggregate to update.')) -@utils.arg( - 'old_name', - metavar='', - nargs='?', - action=shell.DeprecatedAction, - use=_('use "%s"; this option will be removed in ' - 'novaclient 5.0.0.') % '--name', - help=argparse.SUPPRESS) @utils.arg( '--name', dest='name', help=_('Name of aggregate.')) -@utils.arg( - 'old_availability_zone', - metavar='', - nargs='?', - default=None, - action=shell.DeprecatedAction, - use=_('use "%s"; this option will be removed in ' - 'novaclient 5.0.0.') % '--availability_zone', - help=argparse.SUPPRESS) @utils.arg( '--availability-zone', metavar='', @@ -3001,11 +2984,10 @@ def do_aggregate_update(cs, args): """Update the aggregate's name and optionally availability zone.""" aggregate = _find_aggregate(cs, args.aggregate) updates = {} - if args.name or args.old_name: - updates["name"] = args.name or args.old_name - if args.availability_zone or args.old_availability_zone: - updates["availability_zone"] = (args.availability_zone or - args.old_availability_zone) + if args.name: + updates["name"] = args.name + if args.availability_zone: + updates["availability_zone"] = args.availability_zone aggregate = cs.aggregates.update(aggregate.id, updates) print(_("Aggregate %s has been successfully updated.") % aggregate.id) diff --git a/releasenotes/notes/drop-deprecated-aggregate-update-args-17bd019f4be34b18.yaml b/releasenotes/notes/drop-deprecated-aggregate-update-args-17bd019f4be34b18.yaml new file mode 100644 index 000000000..f02d0be84 --- /dev/null +++ b/releasenotes/notes/drop-deprecated-aggregate-update-args-17bd019f4be34b18.yaml @@ -0,0 +1,6 @@ +--- +upgrade: + - | + The deprecated `name` and `availability_zone` positional arguments in + the ``nova aggregate-update`` command have been removed. Use the + ``--name`` and ``--availability-zone`` options instead. From 79fd13f0000498d4cf415f7e60e40037689b7276 Mon Sep 17 00:00:00 2001 From: Sean Dague Date: Thu, 23 Mar 2017 12:28:11 -0400 Subject: [PATCH 1265/1705] Set test timout to 300 seconds This prevents tests from hanging indefinitely, which we are seeing in the trigger crash dump tests, which are also skipped in this patch. Partial-Bug: #1675526 Change-Id: If4858040187834ccdebb3f83bfbfa14d5d3251fc --- .testr.conf | 5 ++++- novaclient/tests/functional/v2/test_trigger_crash_dump.py | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.testr.conf b/.testr.conf index 0aae11096..c8fae426b 100644 --- a/.testr.conf +++ b/.testr.conf @@ -1,4 +1,7 @@ [DEFAULT] -test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} ${PYTHON:-python} -m subunit.run discover -t ./ ${OS_TEST_PATH:-./novaclient/tests/unit} $LISTOPT $IDOPTION +test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \ + OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} \ + OS_TEST_TIMEOUT=${OS_TEST_TIMEOUT:-300} \ + ${PYTHON:-python} -m subunit.run discover -t ./ ${OS_TEST_PATH:-./novaclient/tests/unit} $LISTOPT $IDOPTION test_id_option=--load-list $IDFILE test_list_option=--list diff --git a/novaclient/tests/functional/v2/test_trigger_crash_dump.py b/novaclient/tests/functional/v2/test_trigger_crash_dump.py index 27180ac5a..bf556eec8 100644 --- a/novaclient/tests/functional/v2/test_trigger_crash_dump.py +++ b/novaclient/tests/functional/v2/test_trigger_crash_dump.py @@ -12,10 +12,13 @@ import time +from tempest.lib import decorators + from novaclient.tests.functional import base from novaclient.v2 import shell +@decorators.skip_because(bug="1675526") class TestTriggerCrashDumpNovaClientV217(base.TenantTestBase): """Functional tests for trigger crash dump""" From 53381a9f5048190ded5b68e1e08fdba83d6ff9ea Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 24 Mar 2017 10:14:28 +0000 Subject: [PATCH 1266/1705] Imported Translations from Zanata For more information about this automatic import see: http://docs.openstack.org/developer/i18n/reviewing-translation-import.html Change-Id: I8937bb1435d04e67de49eba6af5d6b3a53e5e179 --- .../locale/fr/LC_MESSAGES/releasenotes.po | 255 ++++++++++++++++++ 1 file changed, 255 insertions(+) create mode 100644 releasenotes/source/locale/fr/LC_MESSAGES/releasenotes.po diff --git a/releasenotes/source/locale/fr/LC_MESSAGES/releasenotes.po b/releasenotes/source/locale/fr/LC_MESSAGES/releasenotes.po new file mode 100644 index 000000000..c201a0bf1 --- /dev/null +++ b/releasenotes/source/locale/fr/LC_MESSAGES/releasenotes.po @@ -0,0 +1,255 @@ +# Gérald LONLAS , 2016. #zanata +msgid "" +msgstr "" +"Project-Id-Version: Nova Client Release Notes 7.1.1\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-03-24 05:03+0000\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"PO-Revision-Date: 2016-10-22 06:13+0000\n" +"Last-Translator: Gérald LONLAS \n" +"Language-Team: French\n" +"Language: fr\n" +"X-Generator: Zanata 3.9.6\n" +"Plural-Forms: nplurals=2; plural=(n > 1)\n" + +msgid "3.0.0" +msgstr "3.0.0" + +msgid "3.3.0" +msgstr "3.3.0" + +msgid "4.0.0" +msgstr "4.0.0" + +msgid "4.1.0" +msgstr "4.1.0" + +msgid "5.0.0" +msgstr "5.0.0" + +msgid "5.1.0" +msgstr "5.1.0" + +msgid "6.0.0" +msgstr "6.0.0" + +msgid ":ref:`genindex`" +msgstr ":ref:`genindex`" + +msgid ":ref:`search`" +msgstr ":ref:`search`" + +msgid "Contents" +msgstr "Contenu" + +msgid "Current Series Release Notes" +msgstr "Note de la release actuelle" + +msgid "Deprecation Notes" +msgstr "Notes dépréciées " + +msgid "Fixed IPs" +msgstr "IP fixes" + +msgid "Floating IPs" +msgstr "IP flottantes" + +msgid "Indices and tables" +msgstr "Index et table des matières" + +msgid "Known Issues" +msgstr "Problèmes connus" + +msgid "Liberty Series Release Notes" +msgstr "Note de release pour Liberty" + +msgid "Mitaka Series Release Notes" +msgstr "Note de release pour Mitaka" + +msgid "New Features" +msgstr "Nouvelles fonctionnalités" + +msgid "Newton Series Release Notes" +msgstr "Note de release pour Newton" + +msgid "Security Group Rules" +msgstr "Règles de groupe de sécurité" + +msgid "Security Groups" +msgstr "Groupes de sécurité" + +msgid "Upgrade Notes" +msgstr "Notes de mises à jours" + +msgid "Welcome to Nova Client Release Notes documentation!" +msgstr "Bienvenue dans la documentation de la note de Release du Client Nova" + +msgid "absolute-limits" +msgstr "absolute-limits" + +msgid "add-floating-ip" +msgstr "add-floating-ip" + +msgid "aggregate-details" +msgstr "aggregate-details" + +msgid "credentials" +msgstr "informations d'identification" + +msgid "dns-create" +msgstr "dns-create" + +msgid "dns-create-private-domain" +msgstr "dns-create-private-domain" + +msgid "dns-create-public-domain" +msgstr "dns-create-public-domain" + +msgid "dns-delete" +msgstr "dns-delete" + +msgid "dns-delete-domain" +msgstr "dns-delete-domain" + +msgid "dns-domains" +msgstr "dns-domains" + +msgid "dns-list" +msgstr "dns-list" + +msgid "endpoints" +msgstr "points de terminaison" + +msgid "fixed-ip-get" +msgstr "fixed-ip-get" + +msgid "fixed-ip-reserve" +msgstr "fixed-ip-reserve" + +msgid "fixed-ip-unreserve" +msgstr "fixed-ip-unreserve" + +msgid "floating-ip-bulk-create" +msgstr "floating-ip-bulk-create" + +msgid "floating-ip-bulk-delete" +msgstr "floating-ip-bulk-delete" + +msgid "floating-ip-bulk-list" +msgstr "floating-ip-bulk-list" + +msgid "floating-ip-create" +msgstr "floating-ip-create" + +msgid "floating-ip-delete" +msgstr "floating-ip-delete" + +msgid "floating-ip-list" +msgstr "floating-ip-list" + +msgid "floating-ip-pool-list" +msgstr "floating-ip-pool-list" + +msgid "network-associate-host" +msgstr "network-associate-host" + +msgid "network-associate-project" +msgstr "network-associate-project" + +msgid "network-create" +msgstr "network-create" + +msgid "network-delete" +msgstr "network-delete" + +msgid "network-disassociate" +msgstr "network-disassociate" + +msgid "network-list" +msgstr "network-list" + +msgid "network-show" +msgstr "network-show" + +msgid "nova baremetal-interface-list" +msgstr "nova baremetal-interface-list" + +msgid "nova baremetal-node-list" +msgstr "nova baremetal-node-list" + +msgid "nova baremetal-node-show" +msgstr "nova baremetal-node-show" + +msgid "nova image-delete" +msgstr "nova image-delete" + +msgid "nova image-list" +msgstr "nova image-list" + +msgid "nova image-meta" +msgstr "nova image-meta" + +msgid "nova image-show" +msgstr "nova image-show" + +msgid "rate-limits" +msgstr "rate-limits" + +msgid "remove-floating-ip" +msgstr "remove-floating-ip" + +msgid "rename" +msgstr "rename" + +msgid "root-password" +msgstr "root-password" + +msgid "secgroup-add-default-rule" +msgstr "secgroup-add-default-rule" + +msgid "secgroup-add-group-rule" +msgstr "secgroup-add-group-rule" + +msgid "secgroup-add-rule" +msgstr "secgroup-add-rule" + +msgid "secgroup-create" +msgstr "secgroup-create" + +msgid "secgroup-delete" +msgstr "secgroup-delete" + +msgid "secgroup-delete-default-rule" +msgstr "secgroup-delete-default-rule" + +msgid "secgroup-delete-group-rule" +msgstr "secgroup-delete-group-rule" + +msgid "secgroup-delete-rule" +msgstr "secgroup-delete-rule" + +msgid "secgroup-list" +msgstr "secgroup-list" + +msgid "secgroup-list-default-rules" +msgstr "secgroup-list-default-rules" + +msgid "secgroup-list-rules" +msgstr "secgroup-list-rules" + +msgid "secgroup-update" +msgstr "secgroup-update" + +msgid "tenant-network-create" +msgstr "tenant-network-create" + +msgid "tenant-network-delete" +msgstr "tenant-network-delete" + +msgid "tenant-network-list" +msgstr "tenant-network-list" + +msgid "tenant-network-show" +msgstr "tenant-network-show" From 5e9e42e47cebb8a833536ae4781f6592f3d832ce Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 24 Mar 2017 16:36:14 +0000 Subject: [PATCH 1267/1705] Updated from global requirements Change-Id: I0f4997ce3e4eda167dbeced74525869cc280bb50 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index af978c722..bf0a5f559 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -9,7 +9,7 @@ fixtures>=3.0.0 # Apache-2.0/BSD keyring>=5.5.1 # MIT/PSF mock>=2.0 # BSD python-keystoneclient>=3.8.0 # Apache-2.0 -python-cinderclient!=1.7.0,!=1.7.1,>=1.6.0 # Apache-2.0 +python-cinderclient>=2.0.1 # Apache-2.0 python-glanceclient>=2.5.0 # Apache-2.0 requests-mock>=1.1 # Apache-2.0 sphinx>=1.5.1 # BSD From 736ed310ac924641b12d9440c5d38a19cbaf974e Mon Sep 17 00:00:00 2001 From: libing Date: Wed, 22 Mar 2017 09:53:02 +0800 Subject: [PATCH 1268/1705] Remove log translations Log messages are no longer being translated. This removes all use of the _LE, _LI, and _LW translation markers to simplify logging and to avoid confusion with new contributions. See: http://lists.openstack.org/pipermail/openstack-i18n/2016-November/002574.html http://lists.openstack.org/pipermail/openstack-dev/2017-March/113365.html Change-Id: I4c8c83315897c7e64b05402c233b0fa67eb03a6e --- novaclient/api_versions.py | 8 +++--- novaclient/client.py | 46 +++++++++++++++---------------- novaclient/i18n.py | 10 ------- novaclient/v2/client.py | 36 ++++++++++++------------ novaclient/v2/contrib/__init__.py | 16 +++++------ novaclient/v2/migrations.py | 8 +++--- novaclient/v2/shell.py | 3 +- 7 files changed, 58 insertions(+), 69 deletions(-) diff --git a/novaclient/api_versions.py b/novaclient/api_versions.py index 65e510b5c..796beadc4 100644 --- a/novaclient/api_versions.py +++ b/novaclient/api_versions.py @@ -23,7 +23,7 @@ import novaclient from novaclient import exceptions -from novaclient.i18n import _, _LW +from novaclient.i18n import _ LOG = logging.getLogger(__name__) if not LOG.handlers: @@ -234,8 +234,8 @@ def get_api_version(version_string): version_string = str(version_string) if version_string in DEPRECATED_VERSIONS: LOG.warning( - _LW("Version %(deprecated_version)s is deprecated, using " - "alternative version %(alternative)s instead."), + _("Version %(deprecated_version)s is deprecated, using " + "alternative version %(alternative)s instead."), {"deprecated_version": version_string, "alternative": DEPRECATED_VERSIONS[version_string]}) version_string = DEPRECATED_VERSIONS[version_string] @@ -421,7 +421,7 @@ def substitution(obj, *args, **kwargs): def _warn_missing_microversion_header(header_name): """Log a warning about missing microversion response header.""" - LOG.warning(_LW( + LOG.warning(_( "Your request was processed by a Nova API which does not support " "microversions (%s header is missing from response). " "Warning: Response may be incorrect."), header_name) diff --git a/novaclient/client.py b/novaclient/client.py index 4d3f0f387..09ca56474 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -36,7 +36,7 @@ from novaclient import api_versions from novaclient import exceptions from novaclient import extension as ext -from novaclient.i18n import _, _LW +from novaclient.i18n import _ from novaclient import utils @@ -90,16 +90,16 @@ def reset_timings(self): @property def management_url(self): self.logger.warning( - _LW("Property `management_url` is deprecated for SessionClient. " - "Use `endpoint_override` instead.")) + _("Property `management_url` is deprecated for SessionClient. " + "Use `endpoint_override` instead.")) return self.endpoint_override @management_url.setter def management_url(self, value): self.logger.warning( - _LW("Property `management_url` is deprecated for SessionClient. " - "It should be set via `endpoint_override` variable while class" - " initialization.")) + _("Property `management_url` is deprecated for SessionClient. " + "It should be set via `endpoint_override` variable while class" + " initialization.")) self.endpoint_override = value @@ -177,11 +177,11 @@ def discover_extensions(*args, **kwargs): """ # TODO(mriedem): Remove support for 'only_contrib' in Queens. if 'only_contrib' in kwargs and kwargs['only_contrib']: - warnings.warn(_LW('Discovering extensions only by contrib path is no ' - 'longer supported since all contrib extensions ' - 'have either been made required or removed. The ' - 'only_contrib argument is deprecated and will be ' - 'removed in a future release.')) + warnings.warn(_('Discovering extensions only by contrib path is no ' + 'longer supported since all contrib extensions ' + 'have either been made required or removed. The ' + 'only_contrib argument is deprecated and will be ' + 'removed in a future release.')) return [] chain = itertools.chain(_discover_via_python_path(), _discover_via_entry_points()) @@ -223,8 +223,8 @@ def _get_client_class_and_version(version): def get_client_class(version): """Returns Client class based on given version.""" - warnings.warn(_LW("'get_client_class' is deprecated. " - "Please use `novaclient.client.Client` instead.")) + warnings.warn(_("'get_client_class' is deprecated. " + "Please use `novaclient.client.Client` instead.")) _api_version, client_class = _get_client_class_and_version(version) return client_class @@ -238,25 +238,25 @@ def _check_arguments(kwargs, release, deprecated_name, right_name=None): if deprecated_name in kwargs: if right_name: if right_name in kwargs: - msg = _LW("The '%(old)s' argument is deprecated in " - "%(release)s and its use may result in errors " - "in future releases. As '%(new)s' is provided, " - "the '%(old)s' argument will be ignored.") % { + msg = _("The '%(old)s' argument is deprecated in " + "%(release)s and its use may result in errors " + "in future releases. As '%(new)s' is provided, " + "the '%(old)s' argument will be ignored.") % { "old": deprecated_name, "release": release, "new": right_name} kwargs.pop(deprecated_name) else: - msg = _LW("The '%(old)s' argument is deprecated in " - "%(release)s and its use may result in errors in " - "future releases. Use '%(right)s' instead.") % { + msg = _("The '%(old)s' argument is deprecated in " + "%(release)s and its use may result in errors in " + "future releases. Use '%(right)s' instead.") % { "old": deprecated_name, "release": release, "right": right_name} kwargs[right_name] = kwargs.pop(deprecated_name) else: - msg = _LW("The '%(old)s' argument is deprecated in %(release)s " - "and its use may result in errors in future " - "releases.") % { + msg = _("The '%(old)s' argument is deprecated in %(release)s " + "and its use may result in errors in future " + "releases.") % { "old": deprecated_name, "release": release} # just ignore it kwargs.pop(deprecated_name) diff --git a/novaclient/i18n.py b/novaclient/i18n.py index 6a17312d6..ccf8e583a 100644 --- a/novaclient/i18n.py +++ b/novaclient/i18n.py @@ -23,13 +23,3 @@ # The primary translation function using the well-known name "_" _ = _translators.primary - -# Translators for log levels. -# -# The abbreviated names are meant to reflect the usual use of a short -# name like '_'. The "L" is for "log" and the other letter comes from -# the level. -_LI = _translators.log_info -_LW = _translators.log_warning -_LE = _translators.log_error -_LC = _translators.log_critical diff --git a/novaclient/v2/client.py b/novaclient/v2/client.py index 2ffa7bd93..4b504853d 100644 --- a/novaclient/v2/client.py +++ b/novaclient/v2/client.py @@ -19,7 +19,7 @@ from novaclient import client from novaclient import exceptions -from novaclient.i18n import _LE, _LW +from novaclient.i18n import _ from novaclient.v2 import agents from novaclient.v2 import aggregates from novaclient.v2 import assisted_volume_snapshots @@ -127,11 +127,11 @@ def __init__(self, """ if direct_use: raise exceptions.Forbidden( - 403, _LE("'novaclient.v2.client.Client' is not designed to be " - "initialized directly. It is inner class of " - "novaclient. You should use " - "'novaclient.client.Client' instead. Related lp " - "bug-report: 1493576")) + 403, _("'novaclient.v2.client.Client' is not designed to be " + "initialized directly. It is inner class of " + "novaclient. You should use " + "'novaclient.client.Client' instead. Related lp " + "bug-report: 1493576")) # NOTE(cyeoh): In the novaclient context (unlike Nova) the # project_id is not the same as the tenant_id. Here project_id @@ -244,21 +244,21 @@ def api_version(self, value): @property def projectid(self): - self.logger.warning(_LW("Property 'projectid' is deprecated since " - "Ocata. Use 'project_name' instead.")) + self.logger.warning(_("Property 'projectid' is deprecated since " + "Ocata. Use 'project_name' instead.")) return self.project_name @property def tenant_id(self): - self.logger.warning(_LW("Property 'tenant_id' is deprecated since " - "Ocata. Use 'project_id' instead.")) + self.logger.warning(_("Property 'tenant_id' is deprecated since " + "Ocata. Use 'project_id' instead.")) return self.project_id def __enter__(self): - self.logger.warning(_LW("NovaClient instance can't be used as a " - "context manager since Ocata (deprecated " - "behaviour) since it is redundant in case of " - "SessionClient.")) + self.logger.warning(_("NovaClient instance can't be used as a " + "context manager since Ocata (deprecated " + "behaviour) since it is redundant in case of " + "SessionClient.")) return self def __exit__(self, t, v, tb): @@ -267,9 +267,9 @@ def __exit__(self, t, v, tb): def set_management_url(self, url): self.logger.warning( - _LW("Method `set_management_url` is deprecated since Ocata. " - "Use `endpoint_override` argument instead while initializing " - "novaclient's instance.")) + _("Method `set_management_url` is deprecated since Ocata. " + "Use `endpoint_override` argument instead while initializing " + "novaclient's instance.")) self.client.set_management_url(url) def get_timings(self): @@ -304,5 +304,5 @@ def authenticate(self): Returns on success; raises :exc:`exceptions.Unauthorized` if the credentials are wrong. """ - self.logger.warning(_LW( + self.logger.warning(_( "Method 'authenticate' is deprecated since Ocata.")) diff --git a/novaclient/v2/contrib/__init__.py b/novaclient/v2/contrib/__init__.py index eb225b881..3cbadc17b 100644 --- a/novaclient/v2/contrib/__init__.py +++ b/novaclient/v2/contrib/__init__.py @@ -13,7 +13,7 @@ import inspect import warnings -from novaclient.i18n import _LW +from novaclient.i18n import _ # NOTE(andreykurilin): "tenant_networks" extension excluded # here deliberately. It was deprecated separately from deprecation @@ -37,15 +37,15 @@ def warn(alternative=True): if module_name.startswith("novaclient.v2.contrib."): if alternative: new_module_name = module_name.replace("contrib.", "") - msg = _LW("Module `%(module)s` is deprecated as of OpenStack " - "Ocata in favor of `%(new_module)s` and will be " - "removed after OpenStack Pike.") % { + msg = _("Module `%(module)s` is deprecated as of OpenStack " + "Ocata in favor of `%(new_module)s` and will be " + "removed after OpenStack Pike.") % { "module": module_name, "new_module": new_module_name} if not alternative: - msg = _LW("Module `%s` is deprecated as of OpenStack Ocata " - "All shell commands were moved to " - "`novaclient.v2.shell` and will be automatically " - "loaded.") % module_name + msg = _("Module `%s` is deprecated as of OpenStack Ocata " + "All shell commands were moved to " + "`novaclient.v2.shell` and will be automatically " + "loaded.") % module_name warnings.warn(msg) diff --git a/novaclient/v2/migrations.py b/novaclient/v2/migrations.py index 9ecc8b38a..075e59d7a 100644 --- a/novaclient/v2/migrations.py +++ b/novaclient/v2/migrations.py @@ -17,7 +17,7 @@ from six.moves.urllib import parse from novaclient import base -from novaclient.i18n import _LW +from novaclient.i18n import _ class Migration(base.Resource): @@ -41,9 +41,9 @@ def list(self, host=None, status=None, cell_name=None): if status: opts['status'] = status if cell_name: - self.client.logger.warning(_LW("Argument 'cell_name' is " - "deprecated since Pike, and will " - "be removed in a future release.")) + self.client.logger.warning(_("Argument 'cell_name' is " + "deprecated since Pike, and will " + "be removed in a future release.")) opts['cell_name'] = cell_name # Transform the dict to a sequence of two-element tuples in fixed diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 87b9e6730..48fc4968a 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -39,7 +39,6 @@ from novaclient import client from novaclient import exceptions from novaclient.i18n import _ -from novaclient.i18n import _LE from novaclient import shell from novaclient import utils from novaclient.v2 import availability_zones @@ -70,7 +69,7 @@ def _key_value_pairing(text): (k, v) = text.split('=', 1) return (k, v) except ValueError: - msg = _LE("'%s' is not in the format of 'key=value'") % text + msg = _("'%s' is not in the format of 'key=value'") % text raise argparse.ArgumentTypeError(msg) From 4c18c8bd97305ca27d26d19087c1b1e3b831d073 Mon Sep 17 00:00:00 2001 From: jichenjc Date: Sat, 25 Mar 2017 20:06:43 +0800 Subject: [PATCH 1269/1705] Remove version 1.1 support v2 and v2.1 is supported now and it ask user to use v2.x API should be what we wanted , this already deprecated for a couple of release, so we can remove it now Change-Id: I8de6fdbf21ccc4969297044a49590ffcd9cd27d8 --- novaclient/api_versions.py | 9 --------- novaclient/tests/unit/test_client.py | 4 ---- novaclient/tests/unit/test_shell.py | 4 ---- .../notes/remove_api_v_1_1-88b3f18ce1423b46.yaml | 5 +++++ 4 files changed, 5 insertions(+), 17 deletions(-) create mode 100644 releasenotes/notes/remove_api_v_1_1-88b3f18ce1423b46.yaml diff --git a/novaclient/api_versions.py b/novaclient/api_versions.py index 796beadc4..b0c41b023 100644 --- a/novaclient/api_versions.py +++ b/novaclient/api_versions.py @@ -33,8 +33,6 @@ LEGACY_HEADER_NAME = "X-OpenStack-Nova-API-Version" HEADER_NAME = "OpenStack-API-Version" SERVICE_TYPE = "compute" -# key is a deprecated version and value is an alternative version. -DEPRECATED_VERSIONS = {"1.1": "2"} _SUBSTITUTIONS = {} @@ -232,13 +230,6 @@ def check_major_version(api_version): def get_api_version(version_string): """Returns checked APIVersion object""" version_string = str(version_string) - if version_string in DEPRECATED_VERSIONS: - LOG.warning( - _("Version %(deprecated_version)s is deprecated, using " - "alternative version %(alternative)s instead."), - {"deprecated_version": version_string, - "alternative": DEPRECATED_VERSIONS[version_string]}) - version_string = DEPRECATED_VERSIONS[version_string] if strutils.is_int_like(version_string): version_string = "%s.0" % version_string diff --git a/novaclient/tests/unit/test_client.py b/novaclient/tests/unit/test_client.py index 81d6d56e8..5f7272803 100644 --- a/novaclient/tests/unit/test_client.py +++ b/novaclient/tests/unit/test_client.py @@ -34,10 +34,6 @@ def test_get_client_class_v2_int(self): output = novaclient.client.get_client_class(2) self.assertEqual(output, novaclient.v2.client.Client) - def test_get_client_class_v1_1(self): - output = novaclient.client.get_client_class('1.1') - self.assertEqual(output, novaclient.v2.client.Client) - def test_get_client_class_unknown(self): self.assertRaises(novaclient.exceptions.UnsupportedVersion, novaclient.client.get_client_class, '0') diff --git a/novaclient/tests/unit/test_shell.py b/novaclient/tests/unit/test_shell.py index a6b41ae8b..bb515bfc2 100644 --- a/novaclient/tests/unit/test_shell.py +++ b/novaclient/tests/unit/test_shell.py @@ -608,10 +608,6 @@ def _test_service_type(self, version, service_type, mock_client): def test_default_service_type(self, mock_client): self._test_service_type(None, 'compute', mock_client) - @mock.patch('novaclient.client.Client') - def test_v1_1_service_type(self, mock_client): - self._test_service_type('1.1', 'compute', mock_client) - @mock.patch('novaclient.client.Client') def test_v2_service_type(self, mock_client): self._test_service_type('2', 'compute', mock_client) diff --git a/releasenotes/notes/remove_api_v_1_1-88b3f18ce1423b46.yaml b/releasenotes/notes/remove_api_v_1_1-88b3f18ce1423b46.yaml new file mode 100644 index 000000000..5748c5e67 --- /dev/null +++ b/releasenotes/notes/remove_api_v_1_1-88b3f18ce1423b46.yaml @@ -0,0 +1,5 @@ +--- +upgrade: + - remove version 1.1 API support as we only support v2 and v2.1 + API in nova side now. + From 773aea23b8817aaa077db90ebcecedb33383550e Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Tue, 28 Mar 2017 15:32:01 +0300 Subject: [PATCH 1270/1705] Explicitly specify arguments of server_groups creation Create API call expectes two parameters - name and policies[*]. It is not a complex structure, so there is no reason to support kwargs. It is better to specify explicitly all expected parameters. PS: removing support of kwargs is "backward compatible", since we have validation at API side and usage of additional parameters was an error previously. [*] - https://developer.openstack.org/api-ref/compute/?expanded=create-server-group-detail Change-Id: I8b40b0db450287bbc5ee8d69834aa764353dbd98 --- novaclient/v2/server_groups.py | 9 +++++++-- novaclient/v2/shell.py | 5 ++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/novaclient/v2/server_groups.py b/novaclient/v2/server_groups.py index 62758c34b..7a01ba0db 100644 --- a/novaclient/v2/server_groups.py +++ b/novaclient/v2/server_groups.py @@ -84,10 +84,15 @@ def delete(self, id): """ return self._delete('/os-server-groups/%s' % id) - def create(self, **kwargs): + def create(self, name, policies): """Create (allocate) a server group. + :param name: The name of the server group. + :param policies: Policy name or a list of exactly one policy name to + associate with the server group. :rtype: list of :class:`ServerGroup` """ - body = {'server_group': kwargs} + policies = policies if isinstance(policies, list) else [policies] + body = {'server_group': {'name': name, + 'policies': policies}} return self._create('/os-server-groups', body, 'server_group') diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 48fc4968a..90602c204 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -4280,9 +4280,8 @@ def do_server_group_list(cs, args): help=_('Policies for the server groups.')) def do_server_group_create(cs, args): """Create a new server group with the specified details.""" - kwargs = {'name': args.name, - 'policies': args.policy} - server_group = cs.server_groups.create(**kwargs) + server_group = cs.server_groups.create(name=args.name, + policies=args.policy) _print_server_group_details(cs, [server_group]) From b98a033d1673b8da8ca261b1dfc6cb0bd6996e9d Mon Sep 17 00:00:00 2001 From: Arvinder Singh Date: Thu, 10 Nov 2016 15:47:03 +0530 Subject: [PATCH 1271/1705] Fix 'nova list --fields' error in no instances case Code changes done so that nova list command with filters run without error when no instances are present. It shows a blank table instead of error message. Closes-Bug: #1624978 Change-Id: Iff879a520c607b8205a3ce5db8fc92287253dbe0 Co-Authored-By: Diana Clarke --- novaclient/tests/unit/v2/test_shell.py | 10 ++++++++++ novaclient/v2/shell.py | 7 +++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 9b1503fa4..b3fbacc00 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -1355,6 +1355,16 @@ def test_list_fields(self): self.assertIn('OS-EXT-MOD: Some Thing', output) self.assertIn('mod_some_thing_value', output) + @mock.patch( + 'novaclient.tests.unit.v2.fakes.FakeSessionClient.get_servers_detail') + def test_list_fields_no_instances(self, mock_get_servers_detail): + mock_get_servers_detail.return_value = (200, {}, {"servers": []}) + stdout, _stderr = self.run_command('list --fields metadata,networks') + # Because there are no instances, you just get the default columns + # rather than the ones you actually asked for (Metadata, Networks). + defaults = 'ID | Name | Status | Task State | Power State | Networks' + self.assertIn(defaults, stdout) + def test_list_invalid_fields(self): self.assertRaises(exceptions.CommandError, self.run_command, diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 48fc4968a..bf3bcc96b 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -1347,9 +1347,12 @@ def do_list(cs, args): _translate_extended_states(servers) formatters = {} + cols = [] + fmts = {} - cols, fmts = _get_list_table_columns_and_formatters( - args.fields, servers, exclude_fields=('id',), filters=filters) + if servers: + cols, fmts = _get_list_table_columns_and_formatters( + args.fields, servers, exclude_fields=('id',), filters=filters) if args.minimal: columns = [ From 0010ab79b45cb111634c83bd7415752984a8d767 Mon Sep 17 00:00:00 2001 From: Simon Leinen Date: Sun, 2 Apr 2017 12:55:20 +0200 Subject: [PATCH 1272/1705] Update Compute API Guide pointer Since the description of the individual API calls have been moved to the API Reference, the API Guide is more or less what the "overview" in the original guide was. Therefore I just pointed to the guide, and removed the phrase "-- the overview, at least --". Change-Id: I8b1cb6a7ea4469a91f0dfe69d26f4ffec178ed43 Closes-Bug: 1678591 Signed-off-by: Simon Leinen --- doc/source/index.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/source/index.rst b/doc/source/index.rst index 2e40292a0..4421c910d 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -11,11 +11,11 @@ Compute API, such as TryStack, HP, or Rackspace, in order to use the nova client .. seealso:: - You may want to read the `OpenStack Compute Developer Guide`__ -- the overview, at - least -- to get an idea of the concepts. By understanding the concepts + You may want to read the `OpenStack Compute API Guide`__ + to get an idea of the concepts. By understanding the concepts this library should make more sense. - __ http://docs.openstack.org/api/openstack-compute/2/content/ + __ https://developer.openstack.org/api-guide/compute/index.html Contents: From 3a03a0e10dfc797413536f8c8024c0ddf380f6c3 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Mon, 20 Mar 2017 19:39:35 -0400 Subject: [PATCH 1273/1705] Remove deprecated network APIs These were deprecated in Newton: aaebeb05a03e34281a091dc6dfc4672b01cdfbbb The 'find network' convenience helper when booting a server only works with neutron now, and assumes the 'network' endpoint is in the service catalog. The functional tests are changed to use python-neutronclient for listing and finding the network to use. At this point, we don't have any nova-network CI jobs that will work with novaclient, so the non-voting mitaka nova-network job is also being removed in change I63b36fb8acc5c9a273e6adcb271df16d0f71031e. As noted in the release note, the only remaining pure nova-network CLI/API that will work is for listing virtual interfaces, which is only implemented for nova-network within Nova. The functional tests for this API are removed since we don't have any nova-network CI jobs that will test it besides unit tests. Long-term we'll likely deprecate the os-virtual-interfaces API as well and replace it with the os-interface API. Change-Id: I8c520100a0016eed3959619c71dae037ebd72939 --- .../tests/functional/api/test_servers.py | 2 +- novaclient/tests/functional/base.py | 46 ++--- .../functional/v2/legacy/test_servers.py | 2 +- .../v2/legacy/test_virtual_interface.py | 30 ---- .../functional/v2/test_virtual_interface.py | 27 --- .../tests/unit/fixture_data/networks.py | 61 ------- novaclient/tests/unit/v2/fakes.py | 21 --- novaclient/tests/unit/v2/test_networks.py | 119 ------------- novaclient/tests/unit/v2/test_shell.py | 81 +-------- novaclient/v2/client.py | 20 --- novaclient/v2/networks.py | 157 +----------------- novaclient/v2/shell.py | 46 +---- ...ke-rm-deprecated-net-272aeb62b329a5bc.yaml | 6 +- test-requirements.txt | 1 + 14 files changed, 32 insertions(+), 587 deletions(-) delete mode 100644 novaclient/tests/functional/v2/legacy/test_virtual_interface.py delete mode 100644 novaclient/tests/functional/v2/test_virtual_interface.py delete mode 100644 novaclient/tests/unit/fixture_data/networks.py delete mode 100644 novaclient/tests/unit/v2/test_networks.py diff --git a/novaclient/tests/functional/api/test_servers.py b/novaclient/tests/functional/api/test_servers.py index 936bfeac9..4eca50671 100644 --- a/novaclient/tests/functional/api/test_servers.py +++ b/novaclient/tests/functional/api/test_servers.py @@ -33,4 +33,4 @@ def test_server_ips(self): self.fail("Server %s did not go ACTIVE after 60s" % server) ips = self.client.servers.ips(server) - self.assertIn(self.network.label, ips) + self.assertIn(self.network.name, ips) diff --git a/novaclient/tests/functional/base.py b/novaclient/tests/functional/base.py index 864321c30..a374601f6 100644 --- a/novaclient/tests/functional/base.py +++ b/novaclient/tests/functional/base.py @@ -21,6 +21,7 @@ from keystoneauth1 import session as ksession from keystoneclient import client as keystoneclient from keystoneclient import discover as keystone_discover +from neutronclient.v2_0 import client as neutronclient import os_client_config from oslo_utils import uuidutils import tempest.lib.cli.base @@ -29,6 +30,7 @@ import novaclient import novaclient.api_versions import novaclient.client +from novaclient.v2 import networks import novaclient.v2.shell BOOT_IS_COMPLETE = ("login as 'cirros' user. default password: " @@ -79,7 +81,7 @@ def pick_network(networks): network_name = os.environ.get('OS_NOVACLIENT_NETWORK') if network_name: for network in networks: - if network.label == network_name: + if network.name == network_name: return network raise NoNetworkException() return networks[0] @@ -224,21 +226,20 @@ def setUp(self): self.image = CACHE["image"] if "network" not in CACHE: - tested_api_version = self.client.api_version - proxy_api_version = novaclient.api_versions.APIVersion('2.35') - if tested_api_version > proxy_api_version: - self.client.api_version = proxy_api_version - try: - # TODO(mriedem): Get the networks from neutron if using neutron - networks = self.client.networks.list() - # Keep track of whether or not there are multiple networks - # available to the given tenant because if so, a specific - # network ID has to be passed in on server create requests - # otherwise the server POST will fail with a 409. - CACHE['multiple_networks'] = len(networks) > 1 - CACHE["network"] = pick_network(networks) - finally: - self.client.api_version = tested_api_version + # Get the networks from neutron. + neutron = neutronclient.Client(session=session) + neutron_networks = neutron.list_networks()['networks'] + # Convert the neutron dicts to Network objects. + nets = [] + for network in neutron_networks: + nets.append(networks.Network( + networks.NeutronManager, network)) + # Keep track of whether or not there are multiple networks + # available to the given tenant because if so, a specific + # network ID has to be passed in on server create requests + # otherwise the server POST will fail with a 409. + CACHE['multiple_networks'] = len(nets) > 1 + CACHE["network"] = pick_network(nets) self.network = CACHE["network"] self.multiple_networks = CACHE['multiple_networks'] @@ -264,15 +265,6 @@ def setUp(self): password=passwd) self.cinder = cinderclient.Client(auth=auth, session=session) - if "use_neutron" not in CACHE: - # check to see if we're running with neutron or not - for service in self.keystone.services.list(): - if service.type == 'network': - CACHE["use_neutron"] = True - break - else: - CACHE["use_neutron"] = False - def _get_novaclient(self, session): nc = novaclient.client.Client("2", session=session) @@ -500,10 +492,6 @@ def _get_project_id(self, name): project = self.keystone.tenants.find(name=name) return project.id - def skip_if_neutron(self): - if CACHE["use_neutron"]: - self.skipTest('nova-network is not available') - def _cleanup_server(self, server_id): """Deletes a server and waits for it to be gone.""" self.client.servers.delete(server_id) diff --git a/novaclient/tests/functional/v2/legacy/test_servers.py b/novaclient/tests/functional/v2/legacy/test_servers.py index 86280e1b6..86d035ae6 100644 --- a/novaclient/tests/functional/v2/legacy/test_servers.py +++ b/novaclient/tests/functional/v2/legacy/test_servers.py @@ -67,7 +67,7 @@ def test_boot_server_with_net_name(self): "--nic net-name=%(net-name)s" % {"name": uuidutils.generate_uuid(), "image": self.image.id, "flavor": self.flavor.id, - "net-name": self.network.label})) + "net-name": self.network.name})) server_id = self._get_value_from_the_table(server_info, "id") self.client.servers.delete(server_id) diff --git a/novaclient/tests/functional/v2/legacy/test_virtual_interface.py b/novaclient/tests/functional/v2/legacy/test_virtual_interface.py deleted file mode 100644 index 8e934f650..000000000 --- a/novaclient/tests/functional/v2/legacy/test_virtual_interface.py +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright 2015 IBM Corp. -# 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 novaclient.tests.functional import base - - -class TestVirtualInterfacesNovaClient(base.ClientTestBase): - """Virtual Interfaces functional tests.""" - - COMPUTE_API_VERSION = "2.1" - - def test_virtual_interface_list(self): - # os-virtual-interfaces does not proxy to neutron - self.skip_if_neutron() - server = self._create_server() - output = self.nova('virtual-interface-list %s' % server.id) - self.assertTrue(len(output.split("\n")) > 5, - "Output table of `virtual-interface-list` for the test" - " server should not be empty.") - return output diff --git a/novaclient/tests/functional/v2/test_virtual_interface.py b/novaclient/tests/functional/v2/test_virtual_interface.py deleted file mode 100644 index cef930266..000000000 --- a/novaclient/tests/functional/v2/test_virtual_interface.py +++ /dev/null @@ -1,27 +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 novaclient.tests.functional.v2.legacy import test_virtual_interface - - -class TestVirtualInterfacesNovaClient( - test_virtual_interface.TestVirtualInterfacesNovaClient): - """Virtual Interfaces functional tests.""" - - COMPUTE_API_VERSION = "2.latest" - - def test_virtual_interface_list(self): - output = super(TestVirtualInterfacesNovaClient, - self).test_virtual_interface_list() - self.assertEqual(self.network.id, - self._get_column_value_from_single_row_table( - output, "Network ID")) diff --git a/novaclient/tests/unit/fixture_data/networks.py b/novaclient/tests/unit/fixture_data/networks.py deleted file mode 100644 index 51a4676fe..000000000 --- a/novaclient/tests/unit/fixture_data/networks.py +++ /dev/null @@ -1,61 +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 novaclient.tests.unit.fixture_data import base - - -class Fixture(base.Fixture): - - base_url = 'os-networks' - - def setUp(self): - super(Fixture, self).setUp() - - get_os_networks = { - 'networks': [ - { - "label": "1", - "cidr": "10.0.0.0/24", - 'project_id': '4ffc664c198e435e9853f2538fbcd7a7', - 'id': '1' - } - ] - } - - headers = self.json_headers - - self.requests_mock.get(self.url(), - json=get_os_networks, - headers=headers) - - def post_os_networks(request, context): - return {'network': request.json()} - - self.requests_mock.post(self.url(), - json=post_os_networks, - headers=headers) - - get_os_networks_1 = {'network': {"label": "1", "cidr": "10.0.0.0/24"}} - - self.requests_mock.get(self.url(1), - json=get_os_networks_1, - headers=headers) - - self.requests_mock.delete(self.url('networkdelete'), - status_code=202, - headers=headers) - - for u in ('add', 'networkdisassociate/action', 'networktest/action', - '1/action', '2/action'): - self.requests_mock.post(self.url(u), - status_code=202, - headers=headers) diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 465485548..1aaf76d4f 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -1781,27 +1781,6 @@ def get_v2_0_networks(self, **kw): return (200, {}, {"networks": networks_by_name[name]}) - def get_os_networks(self, **kw): - return (200, {}, {'networks': [{"label": "1", "cidr": "10.0.0.0/24", - 'project_id': - '4ffc664c198e435e9853f2538fbcd7a7', - 'id': '1', 'vlan': '1234'}]}) - - def delete_os_networks_1(self, **kw): - return (202, {}, None) - - def post_os_networks(self, **kw): - return (202, {}, {'network': kw}) - - def post_os_networks_add(self, **kw): - return (202, {}, None) - - def post_os_networks_1_action(self, **kw): - return (202, {}, None) - - def post_os_networks_2_action(self, **kw): - return (202, {}, None) - def get_os_availability_zone_detail(self, **kw): return (200, {}, { "availabilityZoneInfo": [ diff --git a/novaclient/tests/unit/v2/test_networks.py b/novaclient/tests/unit/v2/test_networks.py deleted file mode 100644 index 1b17af660..000000000 --- a/novaclient/tests/unit/v2/test_networks.py +++ /dev/null @@ -1,119 +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 novaclient.tests.unit.fixture_data import client -from novaclient.tests.unit.fixture_data import networks as data -from novaclient.tests.unit import utils -from novaclient.tests.unit.v2 import fakes -from novaclient.v2 import networks - - -class NetworksTest(utils.FixturedTestCase): - - client_fixture_class = client.V1 - data_fixture_class = data.Fixture - - def test_list_networks(self): - fl = self.cs.networks.list() - self.assert_request_id(fl, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('GET', '/os-networks') - for f in fl: - self.assertIsInstance(f, networks.Network) - - def test_get_network(self): - f = self.cs.networks.get(1) - self.assert_request_id(f, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('GET', '/os-networks/1') - self.assertIsInstance(f, networks.Network) - - def test_delete(self): - ret = self.cs.networks.delete('networkdelete') - self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('DELETE', '/os-networks/networkdelete') - - def test_create(self): - f = self.cs.networks.create(label='foo') - self.assert_request_id(f, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('POST', '/os-networks', - {'network': {'label': 'foo'}}) - self.assertIsInstance(f, networks.Network) - - def test_create_allparams(self): - params = { - 'label': 'bar', - 'bridge': 'br0', - 'bridge_interface': 'int0', - 'cidr': '192.0.2.0/24', - 'cidr_v6': '2001:DB8::/32', - 'dns1': '1.1.1.1', - 'dns2': '1.1.1.2', - 'fixed_cidr': '198.51.100.0/24', - 'gateway': '192.0.2.1', - 'gateway_v6': '2001:DB8::1', - 'multi_host': 'T', - 'priority': '1', - 'project_id': '1', - 'vlan': 5, - 'vlan_start': 1, - 'vpn_start': 1, - 'mtu': 1500, - 'enable_dhcp': 'T', - 'dhcp_server': '1920.2.2', - 'share_address': 'T', - 'allowed_start': '192.0.2.10', - 'allowed_end': '192.0.2.20', - } - - f = self.cs.networks.create(**params) - self.assert_request_id(f, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('POST', '/os-networks', {'network': params}) - self.assertIsInstance(f, networks.Network) - - def test_associate_project(self): - f = self.cs.networks.associate_project('networktest') - self.assert_request_id(f, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('POST', '/os-networks/add', - {'id': 'networktest'}) - - def test_associate_host(self): - f = self.cs.networks.associate_host('networktest', 'testHost') - self.assert_request_id(f, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('POST', '/os-networks/networktest/action', - {'associate_host': 'testHost'}) - - def test_disassociate(self): - f = self.cs.networks.disassociate('networkdisassociate') - self.assert_request_id(f, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('POST', - '/os-networks/networkdisassociate/action', - {'disassociate': None}) - - def test_disassociate_host_only(self): - f = self.cs.networks.disassociate('networkdisassociate', True, False) - self.assert_request_id(f, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('POST', - '/os-networks/networkdisassociate/action', - {'disassociate_host': None}) - - def test_disassociate_project(self): - f = self.cs.networks.disassociate('networkdisassociate', False, True) - self.assert_request_id(f, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('POST', - '/os-networks/networkdisassociate/action', - {'disassociate_project': None}) - - def test_add(self): - f = self.cs.networks.add('networkadd') - self.assert_request_id(f, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('POST', '/os-networks/add', - {'id': 'networkadd'}) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index b3fbacc00..266b89a6e 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -822,61 +822,7 @@ def test_boot_nics_net_id_twice(self): '--nic net-id=net-id1,net-id=net-id2 some-server' % FAKE_UUID_1) self.assertRaises(exceptions.CommandError, self.run_command, cmd) - @mock.patch('novaclient.v2.client.Client.has_neutron', return_value=False) - def test_boot_nics_net_name(self, has_neutron): - cmd = ('boot --image %s --flavor 1 ' - '--nic net-name=1 some-server' % FAKE_UUID_1) - self.run_command(cmd) - self.assert_called_anytime( - 'POST', '/servers', - { - 'server': { - 'flavorRef': '1', - 'name': 'some-server', - 'imageRef': FAKE_UUID_1, - 'min_count': 1, - 'max_count': 1, - 'networks': [ - {'uuid': '1'}, - ], - }, - }, - ) - - @mock.patch('novaclient.v2.client.Client.has_neutron', return_value=False) - def test_boot_nics_net_name_nova_net_2_36(self, has_neutron): - orig_find_network = novaclient.v2.shell._find_network_id_novanet - - def stubbed_find_network(cs, net_name): - # assert that we dropped back to 2.35 - self.assertEqual(api_versions.APIVersion('2.35'), - cs.client.api_version) - return orig_find_network(cs, net_name) - - cmd = ('boot --image %s --flavor 1 ' - '--nic net-name=1 some-server' % FAKE_UUID_1) - with mock.patch.object(novaclient.v2.shell, '_find_network_id_novanet', - side_effect=stubbed_find_network) as find_net: - self.run_command(cmd, api_version='2.36') - find_net.assert_called_once_with(self.shell.cs, '1') - self.assert_called_anytime( - 'POST', '/servers', - { - 'server': { - 'flavorRef': '1', - 'name': 'some-server', - 'imageRef': FAKE_UUID_1, - 'min_count': 1, - 'max_count': 1, - 'networks': [ - {'uuid': '1'}, - ], - }, - }, - ) - - @mock.patch('novaclient.v2.client.Client.has_neutron', return_value=True) - def test_boot_nics_net_name_neutron(self, has_neutron): + def test_boot_nics_net_name_neutron(self): cmd = ('boot --image %s --flavor 1 ' '--nic net-name=private some-server' % FAKE_UUID_1) self.run_command(cmd) @@ -896,8 +842,7 @@ def test_boot_nics_net_name_neutron(self, has_neutron): }, ) - @mock.patch('novaclient.v2.client.Client.has_neutron', return_value=True) - def test_boot_nics_net_name_neutron_dup(self, has_neutron): + def test_boot_nics_net_name_neutron_dup(self): cmd = ('boot --image %s --flavor 1 ' '--nic net-name=duplicate some-server' % FAKE_UUID_1) # this should raise a multiple matches error @@ -906,8 +851,7 @@ def test_boot_nics_net_name_neutron_dup(self, has_neutron): with testtools.ExpectedException(exceptions.CommandError, msg): self.run_command(cmd) - @mock.patch('novaclient.v2.client.Client.has_neutron', return_value=True) - def test_boot_nics_net_name_neutron_blank(self, has_neutron): + def test_boot_nics_net_name_neutron_blank(self): cmd = ('boot --image %s --flavor 1 ' '--nic net-name=blank some-server' % FAKE_UUID_1) # this should raise a multiple matches error @@ -919,25 +863,6 @@ def test_boot_nics_net_name_neutron_blank(self, has_neutron): # out other tests, and they should check the string in the # CommandError, because it's not really enough to distinguish # between various errors. - @mock.patch('novaclient.v2.client.Client.has_neutron', return_value=False) - def test_boot_nics_net_name_not_found(self, has_neutron): - cmd = ('boot --image %s --flavor 1 ' - '--nic net-name=some-net some-server' % FAKE_UUID_1) - self.assertRaises(exceptions.ResourceNotFound, self.run_command, cmd) - - @mock.patch('novaclient.v2.client.Client.has_neutron', return_value=False) - @mock.patch( - 'novaclient.tests.unit.v2.fakes.FakeSessionClient.get_os_networks') - def test_boot_nics_net_name_multiple_matches(self, mock_networks_list, - has_neutron): - mock_networks_list.return_value = (200, {}, { - 'networks': [{"label": "some-net", 'id': '1'}, - {"label": "some-net", 'id': '2'}]}) - - cmd = ('boot --image %s --flavor 1 ' - '--nic net-name=some-net some-server' % FAKE_UUID_1) - self.assertRaises(exceptions.NoUniqueMatch, self.run_command, cmd) - @mock.patch('novaclient.v2.shell._find_network_id', return_value='net-id') def test_boot_nics_net_name_and_net_id(self, mock_find_network_id): cmd = ('boot --image %s --flavor 1 ' diff --git a/novaclient/v2/client.py b/novaclient/v2/client.py index 4b504853d..b85077392 100644 --- a/novaclient/v2/client.py +++ b/novaclient/v2/client.py @@ -15,8 +15,6 @@ import logging -from keystoneauth1.exceptions import catalog as key_ex - from novaclient import client from novaclient import exceptions from novaclient.i18n import _ @@ -155,7 +153,6 @@ def __init__(self, self.certs = certs.CertificateManager(self) self.volumes = volumes.VolumeManager(self) self.keypairs = keypairs.KeypairManager(self) - self.networks = networks.NetworkManager(self) self.neutron = networks.NeutronManager(self) self.quota_classes = quota_classes.QuotaClassSetManager(self) self.quotas = quotas.QuotaSetManager(self) @@ -278,23 +275,6 @@ def get_timings(self): def reset_timings(self): self.client.reset_timings() - def has_neutron(self): - """Check the service catalog to figure out if we have neutron. - - This is an intermediary solution for the window of time where - we still have nova-network support in the client, but we - expect most people have neutron. This ensures that if they - have neutron we understand, we talk to it, if they don't, we - fail back to nova proxies. - """ - try: - endpoint = self.client.get_endpoint(service_type='network') - if endpoint: - return True - return False - except key_ex.EndpointNotFound: - return False - def authenticate(self): """Authenticate against the server. diff --git a/novaclient/v2/networks.py b/novaclient/v2/networks.py index 36a694232..8c1b923d5 100644 --- a/novaclient/v2/networks.py +++ b/novaclient/v2/networks.py @@ -16,7 +16,6 @@ """ Network interface. """ -from novaclient import api_versions from novaclient import base from novaclient import exceptions from novaclient.i18n import _ @@ -24,21 +23,13 @@ class Network(base.Resource): """ - A network. + A network as defined in the Networking (Neutron) API. """ HUMAN_ID = True - NAME_ATTR = "label" + NAME_ATTR = "name" def __repr__(self): - return "" % self.label - - def delete(self): - """ - DEPRECATED: Delete this network. - - :returns: An instance of novaclient.base.TupleWithMeta - """ - return self.manager.delete(self) + return "" % self.name class NeutronManager(base.Manager): @@ -72,145 +63,3 @@ def find_network(self, name): else: matches[0].append_request_ids(matches.request_ids) return matches[0] - - -class NetworkManager(base.ManagerWithFind): - """ - DEPRECATED: Manage :class:`Network` resources. - """ - resource_class = Network - - @api_versions.deprecated_after('2.35') - def list(self): - """ - DEPRECATED: Get a list of all networks. - - :rtype: list of :class:`Network`. - """ - return self._list("/os-networks", "networks") - - @api_versions.deprecated_after('2.35') - def get(self, network): - """ - DEPRECATED: Get a specific network. - - :param network: The ID of the :class:`Network` to get. - :rtype: :class:`Network` - """ - return self._get("/os-networks/%s" % base.getid(network), - "network") - - @api_versions.deprecated_after('2.35') - def delete(self, network): - """ - DEPRECATED: Delete a specific network. - - :param network: The ID of the :class:`Network` to delete. - :returns: An instance of novaclient.base.TupleWithMeta - """ - return self._delete("/os-networks/%s" % base.getid(network)) - - @api_versions.deprecated_after('2.35') - def create(self, **kwargs): - """ - DEPRECATED: Create (allocate) a network. The following parameters are - optional except for label; cidr or cidr_v6 must be specified, too. - - :param label: str - :param bridge: str - :param bridge_interface: str - :param cidr: str - :param cidr_v6: str - :param dns1: str - :param dns2: str - :param fixed_cidr: str - :param gateway: str - :param gateway_v6: str - :param multi_host: str - :param priority: str - :param project_id: str - :param vlan: int - :param vlan_start: int - :param vpn_start: int - :param mtu: int - :param enable_dhcp: int - :param dhcp_server: str - :param share_address: int - :param allowed_start: str - :param allowed_end: str - - :rtype: object of :class:`Network` - """ - body = {"network": kwargs} - return self._create('/os-networks', body, 'network') - - @api_versions.deprecated_after('2.35') - def disassociate(self, network, disassociate_host=True, - disassociate_project=True): - """ - DEPRECATED: Disassociate a specific network from project and/or host. - - :param network: The ID of the :class:`Network`. - :param disassociate_host: Whether to disassociate the host - :param disassociate_project: Whether to disassociate the project - :returns: An instance of novaclient.base.TupleWithMeta - """ - if disassociate_host and disassociate_project: - body = {"disassociate": None} - elif disassociate_project: - body = {"disassociate_project": None} - elif disassociate_host: - body = {"disassociate_host": None} - else: - raise exceptions.CommandError( - _("Must disassociate either host or project or both")) - - resp, body = self.api.client.post("/os-networks/%s/action" % - base.getid(network), body=body) - - return self.convert_into_with_meta(body, resp) - - @api_versions.deprecated_after('2.35') - def associate_host(self, network, host): - """ - DEPRECATED: Associate a specific network with a host. - - :param network: The ID of the :class:`Network`. - :param host: The name of the host to associate the network with - :returns: An instance of novaclient.base.TupleWithMeta - """ - resp, body = self.api.client.post("/os-networks/%s/action" % - base.getid(network), - body={"associate_host": host}) - - return self.convert_into_with_meta(body, resp) - - @api_versions.deprecated_after('2.35') - def associate_project(self, network): - """ - DEPRECATED: Associate a specific network with a project. - - The project is defined by the project authenticated against - - :param network: The ID of the :class:`Network`. - :returns: An instance of novaclient.base.TupleWithMeta - """ - resp, body = self.api.client.post("/os-networks/add", - body={"id": network}) - - return self.convert_into_with_meta(body, resp) - - @api_versions.deprecated_after('2.35') - def add(self, network=None): - """ - DEPRECATED: Associates the current project with a network. Network can - be chosen automatically or provided explicitly. - - :param network: The ID of the :class:`Network` to associate (optional). - :returns: An instance of novaclient.base.TupleWithMeta - """ - resp, body = self.api.client.post("/os-networks/add", - body={"id": base.getid(network) - if network else None}) - - return self.convert_into_with_meta(body, resp) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index bf3bcc96b..6b525521d 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -2037,7 +2037,7 @@ def _find_flavor(cs, flavor): return cs.flavors.find(ram=flavor) -def _find_network_id_neutron(cs, net_name): +def _find_network_id(cs, net_name): """Get unique network ID from network name from neutron""" try: return cs.neutron.find_network(net_name).id @@ -2045,50 +2045,6 @@ def _find_network_id_neutron(cs, net_name): raise exceptions.CommandError(six.text_type(e)) -def _find_network_id(cs, net_name): - """Find the network id for a network name. - - If we have access to neutron in the service catalog, use neutron - for this lookup, otherwise use nova. This ensures that we do the - right thing in the future. - - Once nova network support is deleted, we can delete this check and - the has_neutron function. - """ - if cs.has_neutron(): - return _find_network_id_neutron(cs, net_name) - else: - # The network proxy API methods were deprecated in 2.36 and will return - # a 404 so we fallback to 2.35 to maintain a transition for CLI users. - want_version = api_versions.APIVersion('2.35') - cur_version = cs.api_version - if cs.api_version > want_version: - cs.api_version = want_version - try: - return _find_network_id_novanet(cs, net_name) - finally: - cs.api_version = cur_version - - -def _find_network_id_novanet(cs, net_name): - """Get unique network ID from network name.""" - network_id = None - for net_info in cs.networks.list(): - if net_name == net_info.label: - if network_id is not None: - msg = (_("Multiple network name matches found for name '%s', " - "use network ID to be more specific.") % net_name) - raise exceptions.NoUniqueMatch(msg) - else: - network_id = net_info.id - - if network_id is None: - msg = (_("No network name match for name '%s'") % net_name) - raise exceptions.ResourceNotFound(msg % {'network': net_name}) - else: - return network_id - - @utils.arg('server', metavar='', help=_('Name or ID of server.')) @utils.arg( 'network_id', diff --git a/releasenotes/notes/pike-rm-deprecated-net-272aeb62b329a5bc.yaml b/releasenotes/notes/pike-rm-deprecated-net-272aeb62b329a5bc.yaml index fa2faf379..1798debe4 100644 --- a/releasenotes/notes/pike-rm-deprecated-net-272aeb62b329a5bc.yaml +++ b/releasenotes/notes/pike-rm-deprecated-net-272aeb62b329a5bc.yaml @@ -1,6 +1,9 @@ --- prelude: > - Deprecated network-related resource commands have been removed. + Deprecated network-related resource commands and python API bindings + have been removed. From this point on, python-novaclient will no longer + work with nova-network *except* for the ``nova virtual-interface-list`` + command. upgrade: - | The following deprecated network-related resource commands have been @@ -61,6 +64,7 @@ upgrade: * novaclient.v2.floating_ips * novaclient.v2.floating_ips_bulk * novaclient.v2.fping + * novaclient.v2.networks * novaclient.v2.security_group_default_rules * novaclient.v2.security_group_rules * novaclient.v2.security_groups diff --git a/test-requirements.txt b/test-requirements.txt index bf0a5f559..7d4b1391d 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -11,6 +11,7 @@ mock>=2.0 # BSD python-keystoneclient>=3.8.0 # Apache-2.0 python-cinderclient>=2.0.1 # Apache-2.0 python-glanceclient>=2.5.0 # Apache-2.0 +python-neutronclient>=5.1.0 # Apache-2.0 requests-mock>=1.1 # Apache-2.0 sphinx>=1.5.1 # BSD os-client-config>=1.22.0 # Apache-2.0 From 9ca9ae6c1098e9c096aa7a400a8bfb153ef4d630 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Wed, 5 Apr 2017 13:31:32 -0400 Subject: [PATCH 1274/1705] Update reno for removed network CLIs We still support the add/remove fixed IP commands for nova-network (the multinic API which isn't deprecated yet), so this updates the release notes to clarify that. Change-Id: Ib3468b93e2b396fa1372d3df4be4454f8bc64314 --- .../notes/pike-rm-deprecated-net-272aeb62b329a5bc.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/releasenotes/notes/pike-rm-deprecated-net-272aeb62b329a5bc.yaml b/releasenotes/notes/pike-rm-deprecated-net-272aeb62b329a5bc.yaml index 1798debe4..8531b05be 100644 --- a/releasenotes/notes/pike-rm-deprecated-net-272aeb62b329a5bc.yaml +++ b/releasenotes/notes/pike-rm-deprecated-net-272aeb62b329a5bc.yaml @@ -2,8 +2,8 @@ prelude: > Deprecated network-related resource commands and python API bindings have been removed. From this point on, python-novaclient will no longer - work with nova-network *except* for the ``nova virtual-interface-list`` - command. + work with nova-network *except* for the ``nova virtual-interface-list``, + ``nova add-fixed-ip`` and ``nova remove-fixed-ip`` commands. upgrade: - | The following deprecated network-related resource commands have been From 948689042528ec5d083e9a5e6f13249f4458ef09 Mon Sep 17 00:00:00 2001 From: jonnary Date: Wed, 5 Apr 2017 21:27:10 +0800 Subject: [PATCH 1275/1705] Revise `nova list` description The servers which `nova list` showing are not only active servers. This patch revises this description. Change-Id: Ie962f79dd15c1bb0ef34f5c280f5b19b0e5705c2 --- novaclient/v2/shell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index bf3bcc96b..a0d6cb85b 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -1275,7 +1275,7 @@ def _print_flavor(flavor): "--not-tags-any "), start_version="2.26") def do_list(cs, args): - """List active servers.""" + """List servers.""" imageid = None flavorid = None if args.image: From b6aea669630dc88296bdded963051a35cc9f995e Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Thu, 6 Apr 2017 16:07:20 -0400 Subject: [PATCH 1276/1705] Deprecate certs commands and APIs The nova-cert service was deprecated in Newton: 789edad0e811d866551bec18dc7729541105f59d We're going to remove the nova-cert service and os-certificates API from the server in Pike, so we need to also get started on the deprecation in the client. Part of blueprint remove-nova-cert Change-Id: If3e1e40947a8ad3f665f6a96d46de8cef6a2a190 --- novaclient/tests/unit/v2/test_certs.py | 10 ++++++++-- novaclient/v2/certs.py | 19 +++++++++++++++---- novaclient/v2/shell.py | 12 ++++++++++-- .../deprecate-certs-1558d8e3b7888938.yaml | 7 +++++++ 4 files changed, 40 insertions(+), 8 deletions(-) create mode 100644 releasenotes/notes/deprecate-certs-1558d8e3b7888938.yaml diff --git a/novaclient/tests/unit/v2/test_certs.py b/novaclient/tests/unit/v2/test_certs.py index d0e899586..d7a3afa85 100644 --- a/novaclient/tests/unit/v2/test_certs.py +++ b/novaclient/tests/unit/v2/test_certs.py @@ -11,6 +11,8 @@ # License for the specific language governing permissions and limitations # under the License. +import mock + from novaclient.tests.unit.fixture_data import certs as data from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit import utils @@ -26,14 +28,18 @@ class CertsTest(utils.FixturedTestCase): scenarios = [('original', {'client_fixture_class': client.V1}), ('session', {'client_fixture_class': client.SessionV1})] - def test_create_cert(self): + @mock.patch('warnings.warn') + def test_create_cert(self, mock_warn): cert = self.cs.certs.create() self.assert_request_id(cert, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/os-certificates') self.assertIsInstance(cert, self.cert_type) + self.assertEqual(1, mock_warn.call_count) - def test_get_root_cert(self): + @mock.patch('warnings.warn') + def test_get_root_cert(self, mock_warn): cert = self.cs.certs.get() self.assert_request_id(cert, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/os-certificates/root') self.assertIsInstance(cert, self.cert_type) + self.assertEqual(1, mock_warn.call_count) diff --git a/novaclient/v2/certs.py b/novaclient/v2/certs.py index 0ed1e1476..655af54db 100644 --- a/novaclient/v2/certs.py +++ b/novaclient/v2/certs.py @@ -16,13 +16,22 @@ # under the License. """ -Certificate interface. +DEPRECATED Certificate interface. """ +import warnings + from novaclient import base +from novaclient.i18n import _ + +CERT_DEPRECATION_WARNING = ( + _('The nova-cert service is deprecated. This API binding will be removed ' + 'in the first major release after the Nova server 16.0.0 Pike release.') +) class Certificate(base.Resource): + """DEPRECATED""" def __repr__(self): return ("" % (len(self.private_key) if self.private_key else 0, @@ -30,13 +39,15 @@ def __repr__(self): class CertificateManager(base.Manager): - """Manage :class:`Certificate` resources.""" + """DEPRECATED Manage :class:`Certificate` resources.""" resource_class = Certificate def create(self): - """Create a x509 certificate for a user in tenant.""" + """DEPRECATED Create a x509 certificate for a user in tenant.""" + warnings.warn(CERT_DEPRECATION_WARNING, DeprecationWarning) return self._create('/os-certificates', {}, 'certificate') def get(self): - """Get root certificate.""" + """DEPRECATED Get root certificate.""" + warnings.warn(CERT_DEPRECATION_WARNING, DeprecationWarning) return self._get("/os-certificates/root", 'certificate') diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 6b525521d..d5ea8131e 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -27,6 +27,7 @@ import pprint import sys import time +import warnings from oslo_utils import netutils from oslo_utils import strutils @@ -48,6 +49,11 @@ logger = logging.getLogger(__name__) +CERT_DEPRECATION_WARNING = ( + _('The nova-cert service is deprecated. This command will be removed ' + 'in the first major release after the Nova server 16.0.0 Pike release.') +) + CLIENT_BDM2_KEYS = { 'id': 'uuid', @@ -2794,7 +2800,8 @@ def simplify_usage(u): default='cert.pem', help=_('Filename for the X.509 certificate. [Default: cert.pem]')) def do_x509_create_cert(cs, args): - """Create x509 cert for a user in tenant.""" + """DEPRECATED Create x509 cert for a user in tenant.""" + warnings.warn(CERT_DEPRECATION_WARNING, DeprecationWarning) if os.path.exists(args.pk_filename): raise exceptions.CommandError(_("Unable to write privatekey - %s " @@ -2825,7 +2832,8 @@ def do_x509_create_cert(cs, args): default='cacert.pem', help=_('Filename to write the x509 root cert.')) def do_x509_get_root_cert(cs, args): - """Fetch the x509 root cert.""" + """DEPRECATED Fetch the x509 root cert.""" + warnings.warn(CERT_DEPRECATION_WARNING, DeprecationWarning) if os.path.exists(args.filename): raise exceptions.CommandError(_("Unable to write x509 root cert - \ %s exists.") % args.filename) diff --git a/releasenotes/notes/deprecate-certs-1558d8e3b7888938.yaml b/releasenotes/notes/deprecate-certs-1558d8e3b7888938.yaml new file mode 100644 index 000000000..fd7e7599f --- /dev/null +++ b/releasenotes/notes/deprecate-certs-1558d8e3b7888938.yaml @@ -0,0 +1,7 @@ +--- +deprecations: + - | + The ``nova x509-create-cert`` and ``nova x509-get-root-cert`` commands + and ``novaclient.v2.certs`` API binding are now deprecated and will be + removed in the first major release after the Nova server 16.0.0 Pike + release. From 878c77aae282353b3f29e60e3e045279c1e1eb5f Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 7 Apr 2017 06:16:19 +0000 Subject: [PATCH 1277/1705] Updated from global requirements Change-Id: I3d11f4e0fa42fb900f0f7066696de3e0461bea6a --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 76a0ba1f9..ac0fb31d3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,4 +11,4 @@ PrettyTable<0.8,>=0.7.1 # BSD requests!=2.12.2,!=2.13.0,>=2.10.0 # Apache-2.0 simplejson>=2.2.0 # MIT six>=1.9.0 # MIT -Babel>=2.3.4 # BSD +Babel!=2.4.0,>=2.3.4 # BSD From 4d956d06f8c2bd3971cd46fb7ccdab33aaa5709c Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Mon, 10 Apr 2017 16:07:04 +0900 Subject: [PATCH 1278/1705] Fix cinder volume leakage A cinder volume remains after the 'test_boot_server_with_legacy_bdm_volume_id_only' functional test. It is resource leakage. So fix it. Change-Id: I3cacbac6468b5fad1c9666892fc57906af24b169 Partial-Bug: #1613105 --- novaclient/tests/functional/v2/legacy/test_servers.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/novaclient/tests/functional/v2/legacy/test_servers.py b/novaclient/tests/functional/v2/legacy/test_servers.py index 86d035ae6..8976abd53 100644 --- a/novaclient/tests/functional/v2/legacy/test_servers.py +++ b/novaclient/tests/functional/v2/legacy/test_servers.py @@ -31,6 +31,11 @@ def _boot_server_with_legacy_bdm(self, bdm_params=()): imageRef=self.image.id) self.wait_for_volume_status(volume, "available") + if (len(bdm_params) >= 3 and bdm_params[2] == '1'): + delete_volume = False + else: + delete_volume = True + bdm_params = ':'.join(bdm_params) if bdm_params: bdm_params = ''.join((':', bdm_params)) @@ -51,6 +56,10 @@ def _boot_server_with_legacy_bdm(self, bdm_params=()): self.client.servers.delete(server_id) self.wait_for_resource_delete(server_id, self.client.servers) + if delete_volume: + self.cinder.volumes.delete(volume.id) + self.wait_for_resource_delete(volume.id, self.cinder.volumes) + def test_boot_server_with_legacy_bdm(self): # bdm v1 format # ::: From 6182a019983bd040b9f9edaad5c45399944695ff Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Wed, 12 Apr 2017 04:21:44 +0000 Subject: [PATCH 1279/1705] Updated from global requirements Change-Id: Ibb1c0330f5e40624f2a8cf046856b483267c1b65 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ac0fb31d3..fed242824 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ # 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. -pbr>=2.0.0 # Apache-2.0 +pbr!=2.1.0,>=2.0.0 # Apache-2.0 keystoneauth1>=2.18.0 # Apache-2.0 iso8601>=0.1.11 # MIT oslo.i18n>=2.1.0 # Apache-2.0 From 92665dbcd823a28ae14a5ee5ba911513697d17be Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 13 Apr 2017 09:17:08 -0500 Subject: [PATCH 1280/1705] Add novaclient client_name and client_version to user-agent keystoneauth supports setting additional info into the user-agent string about what the client is. Set it so that novaclient shows up in server logs as the client. Change-Id: I084a16f5372ec7df598505e9f925a70d26d66f69 --- doc/source/api.rst | 15 +++++++++++++++ novaclient/client.py | 4 ++++ .../add-user-agent-string-db77210dfd3ec671.yaml | 6 ++++++ 3 files changed, 25 insertions(+) create mode 100644 releasenotes/notes/add-user-agent-string-db77210dfd3ec671.yaml diff --git a/doc/source/api.rst b/doc/source/api.rst index 3166ddf62..450d66d99 100644 --- a/doc/source/api.rst +++ b/doc/source/api.rst @@ -38,6 +38,21 @@ parameter. Similarly, if your cloud uses keystone v3 and you have a DOMAIN_NAME or DOMAIN_ID, provide it as `user_domain_(name|id)` and if you are using a PROJECT_NAME also provide the domain information as `project_domain_(name|id)`. +novaclient adds 'python-novaclient' and its version to the user-agent string +that keystoneauth produces. If you are creating an application using novaclient +and want to register a name and version in the user-agent string, pass those +to the Session:: + + >>> sess = session.Session( + ... auth=auth, app_name'nodepool', app_version'1.2.3') + +If you are making a library that consumes novaclient but is not an end-user +application, you can append a (name, version) tuple to the session's +`additional_user_agent` property:: + + >>> sess = session.Session(auth=auth) + >>> sess.additional_user_agent.append(('shade', '1.2.3')) + For more information on this keystoneauth API, see `Using Sessions`_. .. _Using Sessions: http://docs.openstack.org/developer/keystoneauth/using-sessions.html diff --git a/novaclient/client.py b/novaclient/client.py index 09ca56474..be2fad874 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -33,6 +33,7 @@ osprofiler_profiler = importutils.try_import("osprofiler.profiler") osprofiler_web = importutils.try_import("osprofiler.web") +import novaclient from novaclient import api_versions from novaclient import exceptions from novaclient import extension as ext @@ -48,6 +49,9 @@ class SessionClient(adapter.LegacyJsonAdapter): + client_name = 'python-novaclient' + client_version = novaclient.__version__ + def __init__(self, *args, **kwargs): self.times = [] self.timings = kwargs.pop('timings', False) diff --git a/releasenotes/notes/add-user-agent-string-db77210dfd3ec671.yaml b/releasenotes/notes/add-user-agent-string-db77210dfd3ec671.yaml new file mode 100644 index 000000000..0e1eeb0f5 --- /dev/null +++ b/releasenotes/notes/add-user-agent-string-db77210dfd3ec671.yaml @@ -0,0 +1,6 @@ +--- +features: + - novaclient now adds information about itself to the keystoneauth + user-agent. Adding information about wrapping libraries or consuming + applications can be found at + https://docs.openstack.org/developer/python-novaclient/api.html From 3512fca87b86f3b4806fa261089c1653572d4e29 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 19 Apr 2017 09:53:03 +0100 Subject: [PATCH 1281/1705] Explicitly set 'builders' option An upcoming release of pbr will require explicitly stating which builders are requested, rather than defaulting to html and man. Head off any potential impact this may cause by explicitly setting this configuration now. Change-Id: Idfa27bb6b235f8cc4de6d62baf9e9e9a55bae95b --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index ef9c9551e..5aa68c2bc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,6 +30,7 @@ console_scripts = nova = novaclient.shell:main [build_sphinx] +builders = html,man all-files = 1 warning-is-error = 1 source-dir = doc/source From ac25ae6feedc5cd1a7799136441bd2e70a116741 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 19 Apr 2017 09:53:47 +0100 Subject: [PATCH 1282/1705] doc: Remove cruft from conf.py Change-Id: Ieec1309762b2c120e944ef7afaa2e4ebf6098a65 --- doc/source/conf.py | 168 +++------------------------------------------ 1 file changed, 9 insertions(+), 159 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index 237a363ae..4813effbd 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # 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 @@ -11,25 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. # -# python-novaclient documentation build configuration file, created by -# sphinx-quickstart on Sun Dec 6 14:19:25 2009. -# -# This file is execfile()d with the current directory set to its containing -# dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. +# python-novaclient documentation build configuration file -import sys - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.append(os.path.abspath('.')) import os +import sys BASE_DIR = os.path.dirname(os.path.abspath(__file__)) ROOT = os.path.abspath(os.path.join(BASE_DIR, "..", "..")) @@ -85,8 +69,7 @@ def get_module_names(): # -- General configuration ---------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be -# extensions -# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'oslosphinx'] autoclass_content = 'both' @@ -97,9 +80,6 @@ def get_module_names(): # The suffix of source filenames. source_suffix = '.rst' -# The encoding of source files. -#source_encoding = 'utf-8' - # The master toctree document. master_doc = 'index' @@ -107,36 +87,10 @@ def get_module_names(): project = 'python-novaclient' copyright = 'OpenStack Contributors' -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -#version = 'X.Y' -# The full version, including alpha/beta/rc tags. -#release = 'X.Y.Z' - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -#language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -#today = '' -# Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' - -# List of documents that shouldn't be included in the build. -#unused_docs = [] - # List of directories, relative to source directory, that shouldn't be searched # for source files. exclude_trees = [] -# The reST default role (used for this markup: `text`) to use for all -# documents. -#default_role = None - # If true, '()' will be appended to :func: etc. cross-reference text. add_function_parentheses = True @@ -144,127 +98,23 @@ def get_module_names(): # unit titles (such as .. function::). add_module_names = True -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -#show_authors = False - # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' -# A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] - -# Grouping the document tree for man pages. -# List of tuples 'sourcefile', 'target', u'title', u'Authors name', 'manual' - -man_pages = [ - ('man/nova', 'nova', 'OpenStack Nova command line client', - ['OpenStack Contributors'], 1), -] - # -- Options for HTML output -------------------------------------------------- # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. html_theme = 'default' -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -#html_theme_options = {} - -# Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] - -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -#html_title = None - -# A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -#html_logo = None - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -#html_favicon = None - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -# html_static_path = ['_static'] - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -#html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -#html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -#html_additional_pages = {} - -# If false, no module index is generated. -#html_use_modindex = True - -# If false, no index is generated. -#html_use_index = True - -# If true, the index is split into individual pages for each letter. -#html_split_index = False - -# If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -#html_use_opensearch = '' - -# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = '' - # Output file base name for HTML help builder. htmlhelp_basename = 'python-novaclientdoc' +# -- Options for manual page output ------------------------------------------ -# -- Options for LaTeX output ------------------------------------------------- - -# The paper size ('letter' or 'a4'). -#latex_paper_size = 'letter' - -# The font size ('10pt', '11pt' or '12pt'). -#latex_font_size = '10pt' - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, author, documentclass [howto/manual]) -# . -latex_documents = [ - ('index', 'python-novaclient.tex', 'python-novaclient Documentation', - 'Rackspace - based on work by Jacob Kaplan-Moss', 'manual'), +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('man/nova', 'nova', 'OpenStack Nova command line client', + ['OpenStack Contributors'], 1), ] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -#latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -#latex_use_parts = False - -# Additional stuff for the LaTeX preamble. -#latex_preamble = '' - -# Documents to append as an appendix to all manuals. -#latex_appendices = [] - -# If false, no module index is generated. -#latex_use_modindex = True From 4b8863341a15c224824e272fa5eecbe0de35f925 Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Mon, 6 Feb 2017 14:46:54 +0900 Subject: [PATCH 1283/1705] Microversion 2.42 - Fix tag attribute disappearing The 2.32 microversion added the support for creating a server with a tagged block device mapping and/or tagged nic. Due to a bug, block device tags would only work in the 2.32 microversion but nic tags would still work up until the 2.37 microversion, which regressed that functionality as well. Both block device and nic tags were fixed again in the 2.42 microversion. This change updates the help text for the 'nova boot' CLI and client-side validation in the shell for the various microversion ranges of support for both block device and nic tags. Co-Authored-By: Andrey Kurilin Change-Id: I7492b20b5d2daa0f8391abed07ef861043c3e51e Implements: blueprint fix-tag-attribute-disappearing Closes-Bug: #1658571 --- novaclient/__init__.py | 2 +- .../functional/v2/test_device_tagging.py | 149 +++++++++++++++++- novaclient/tests/unit/v2/test_shell.py | 1 + novaclient/v2/shell.py | 141 +++++++++++++++-- ...tribute-disappearing-25483a80f548ef35.yaml | 13 ++ 5 files changed, 289 insertions(+), 17 deletions(-) create mode 100644 releasenotes/notes/fix-tag-attribute-disappearing-25483a80f548ef35.yaml diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 0816b4f61..7c0d2c937 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.41") +API_MAX_VERSION = api_versions.APIVersion("2.42") diff --git a/novaclient/tests/functional/v2/test_device_tagging.py b/novaclient/tests/functional/v2/test_device_tagging.py index 033b43739..c19c42403 100644 --- a/novaclient/tests/functional/v2/test_device_tagging.py +++ b/novaclient/tests/functional/v2/test_device_tagging.py @@ -13,24 +13,159 @@ # under the License. from oslo_utils import uuidutils +import six +from tempest.lib import exceptions from novaclient.tests.functional import base -class TestDeviceTaggingCLI(base.ClientTestBase): +class TestBlockDeviceTaggingCLIError(base.ClientTestBase): + """Negative test that asserts that creating a server with a tagged + block device with a specific microversion will fail. + """ + + COMPUTE_API_VERSION = "2.31" + + def test_boot_server_with_tagged_block_devices_with_error(self): + try: + output = self.nova('boot', params=( + '%(name)s --flavor %(flavor)s --poll ' + '--nic net-id=%(net-uuid)s ' + '--block-device ' + 'source=image,dest=volume,id=%(image)s,size=1,bootindex=0,' + 'shutdown=remove,tag=bar' % {'name': uuidutils.generate_uuid(), + 'flavor': self.flavor.id, + 'net-uuid': self.network.id, + 'image': self.image.id})) + except exceptions.CommandFailed as e: + self.assertIn("ERROR (CommandError): " + "'tag' in block device mapping is not supported " + "in API version %s." % self.COMPUTE_API_VERSION, + six.text_type(e)) + else: + server_id = self._get_value_from_the_table(output, 'id') + self.client.servers.delete(server_id) + self.wait_for_resource_delete(server_id, self.client.servers) + self.fail("Booting a server with block device tag is not failed.") + + +class TestNICDeviceTaggingCLIError(base.ClientTestBase): + """Negative test that asserts that creating a server with a tagged + nic with a specific microversion will fail. + """ + + COMPUTE_API_VERSION = "2.31" + + def test_boot_server_with_tagged_nic_devices_with_error(self): + try: + output = self.nova('boot', params=( + '%(name)s --flavor %(flavor)s --poll ' + '--nic net-id=%(net-uuid)s,tag=foo ' + '--block-device ' + 'source=image,dest=volume,id=%(image)s,size=1,bootindex=0,' + 'shutdown=remove' % {'name': uuidutils.generate_uuid(), + 'flavor': self.flavor.id, + 'net-uuid': self.network.id, + 'image': self.image.id})) + except exceptions.CommandFailed as e: + self.assertIn("Invalid nic argument", six.text_type(e)) + else: + server_id = self._get_value_from_the_table(output, 'id') + self.client.servers.delete(server_id) + self.wait_for_resource_delete(server_id, self.client.servers) + self.fail("Booting a server with network interface tag " + "is not failed.") + + +class TestBlockDeviceTaggingCLI(base.ClientTestBase): + """Tests that creating a server with a tagged block device will work + with the 2.32 microversion, where the feature was originally added. + """ + + COMPUTE_API_VERSION = "2.32" + + def test_boot_server_with_tagged_block_devices(self): + server_info = self.nova('boot', params=( + '%(name)s --flavor %(flavor)s --poll ' + '--nic net-id=%(net-uuid)s ' + '--block-device ' + 'source=image,dest=volume,id=%(image)s,size=1,bootindex=0,' + 'shutdown=remove,tag=bar' % {'name': uuidutils.generate_uuid(), + 'flavor': self.flavor.id, + 'net-uuid': self.network.id, + 'image': self.image.id})) + server_id = self._get_value_from_the_table(server_info, 'id') + self.client.servers.delete(server_id) + self.wait_for_resource_delete(server_id, self.client.servers) + + +class TestNICDeviceTaggingCLI(base.ClientTestBase): + """Tests that creating a server with a tagged nic will work + with the 2.32 microversion, where the feature was originally added. + """ COMPUTE_API_VERSION = "2.32" - def test_boot_server_with_tagged_devices(self): + def test_boot_server_with_tagged_nic_devices(self): server_info = self.nova('boot', params=( '%(name)s --flavor %(flavor)s --poll ' '--nic net-id=%(net-uuid)s,tag=foo ' '--block-device ' - 'source=image,dest=volume,id=%(image)s,size=1,' - 'bootindex=0,tag=bar' % {'name': uuidutils.generate_uuid(), - 'flavor': self.flavor.id, - 'net-uuid': self.network.id, - 'image': self.image.id})) + 'source=image,dest=volume,id=%(image)s,size=1,bootindex=0,' + 'shutdown=remove' % {'name': uuidutils.generate_uuid(), + 'flavor': self.flavor.id, + 'net-uuid': self.network.id, + 'image': self.image.id})) server_id = self._get_value_from_the_table(server_info, 'id') self.client.servers.delete(server_id) self.wait_for_resource_delete(server_id, self.client.servers) + + +class TestDeviceTaggingCLIV233(TestBlockDeviceTaggingCLIError, + TestNICDeviceTaggingCLI): + """Tests that in microversion 2.33, creating a server with a tagged + block device will fail, but creating a server with a tagged nic will + succeed. + """ + + COMPUTE_API_VERSION = "2.33" + + +class TestDeviceTaggingCLIV236(TestBlockDeviceTaggingCLIError, + TestNICDeviceTaggingCLI): + """Tests that in microversion 2.36, creating a server with a tagged + block device will fail, but creating a server with a tagged nic will + succeed. This is testing the boundary before 2.37 where nic tagging + was broken. + """ + + COMPUTE_API_VERSION = "2.36" + + +class TestDeviceTaggingCLIV237(TestBlockDeviceTaggingCLIError, + TestNICDeviceTaggingCLIError): + """Tests that in microversion 2.37, creating a server with either a + tagged block device or tagged nic would fail. + """ + + COMPUTE_API_VERSION = "2.37" + + +class TestDeviceTaggingCLIV241(TestBlockDeviceTaggingCLIError, + TestNICDeviceTaggingCLIError): + """Tests that in microversion 2.41, creating a server with either a + tagged block device or tagged nic would fail. This is testing the + boundary before 2.42 where block device tags and nic tags were fixed + for server create requests. + """ + + COMPUTE_API_VERSION = "2.41" + + +class TestDeviceTaggingCLIV242(TestBlockDeviceTaggingCLI, + TestNICDeviceTaggingCLI): + """Tests that in microversion 2.42 you could once again create a server + with a tagged block device or a tagged nic. + """ + + COMPUTE_API_VERSION = "2.42" diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 266b89a6e..7e1d28fca 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2880,6 +2880,7 @@ def test_versions(self): 38, # doesn't require any changes in novaclient 39, # There are no versioned wrapped shell method changes for this 41, # There are no version-wrapped shell method changes for this. + 42, # There are no version-wrapped shell method changes for this. ]) versions_supported = set(range(0, novaclient.API_MAX_VERSION.ver_minor + 1)) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index dd027f977..42bf9b7f7 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -106,7 +106,15 @@ def _match_image(cs, wanted_properties): return images_matched -def _parse_block_device_mapping_v2(args, image): +def _supports_block_device_tags(cs): + if (cs.api_version == api_versions.APIVersion('2.32') or + cs.api_version >= api_versions.APIVersion('2.42')): + return True + else: + return False + + +def _parse_block_device_mapping_v2(cs, args, image): bdm = [] if args.boot_volume: @@ -125,6 +133,12 @@ def _parse_block_device_mapping_v2(args, image): spec_dict = dict(v.split('=') for v in device_spec.split(',')) bdm_dict = {} + if ('tag' in spec_dict and not _supports_block_device_tags(cs)): + raise exceptions.CommandError( + _("'tag' in block device mapping is not supported " + "in API version %(version)s.") + % {'version': cs.api_version.get_string()}) + for key, value in spec_dict.items(): bdm_dict[CLIENT_BDM2_KEYS[key]] = value @@ -193,9 +207,25 @@ def _parse_block_device_mapping_v2(args, image): return bdm +def _supports_nic_tags(cs): + if ((cs.api_version >= api_versions.APIVersion('2.32') and + cs.api_version <= api_versions.APIVersion('2.36')) or + cs.api_version >= api_versions.APIVersion('2.42')): + return True + else: + return False + + def _parse_nics(cs, args): supports_auto_alloc = cs.api_version >= api_versions.APIVersion('2.37') - if supports_auto_alloc: + supports_nic_tags = _supports_nic_tags(cs) + + nic_info = {"net-id": "", "v4-fixed-ip": "", "v6-fixed-ip": "", + "port-id": "", "net-name": ""} + + if supports_auto_alloc and supports_nic_tags: + # API version >= 2.42 + nic_info.update({"tag": ""}) err_msg = (_("Invalid nic argument '%s'. Nic arguments must be of " "the form --nic = api_versions.APIVersion('2.32'): + elif supports_auto_alloc and not supports_nic_tags: + # 2.41 >= API version >= 2.37 + err_msg = (_("Invalid nic argument '%s'. Nic arguments must be of " + "the form --nic , " + "with only one of net-id, net-name or port-id " + "specified. Specifying a --nic of auto or none cannot " + "be used with any other --nic value.")) + elif not supports_auto_alloc and supports_nic_tags: + # 2.36 >= API version >= 2.32 + nic_info.update({"tag": ""}) err_msg = (_("Invalid nic argument '%s'. Nic arguments must be of " "the form --nic ", + "v6-fixed-ip=ip-addr,port-id=port-uuid,tag=tag>", action='append', dest='nics', default=[], @@ -629,11 +727,36 @@ def _boot(cs, args): '--nic', metavar="", + "v4-fixed-ip=ip-addr,v6-fixed-ip=ip-addr>", action='append', dest='nics', default=[], start_version='2.37', + end_version='2.41', + help=_("Create a NIC on the server. " + "Specify option multiple times to create multiple nics unless " + "using the special 'auto' or 'none' values. " + "auto: automatically allocate network resources if none are " + "available. This cannot be specified with any other nic value and " + "cannot be specified multiple times. " + "none: do not attach a NIC at all. This cannot be specified " + "with any other nic value and cannot be specified multiple times. " + "net-id: attach NIC to network with a specific UUID. " + "net-name: attach NIC to network with this name " + "(either port-id or net-id or net-name must be provided), " + "v4-fixed-ip: IPv4 fixed address for NIC (optional), " + "v6-fixed-ip: IPv6 fixed address for NIC (optional), " + "port-id: attach NIC to port with this UUID " + "(either port-id or net-id must be provided).")) +@utils.arg( + '--nic', + metavar="", + action='append', + dest='nics', + default=[], + start_version='2.42', help=_("Create a NIC on the server. " "Specify option multiple times to create multiple nics unless " "using the special 'auto' or 'none' values. " diff --git a/releasenotes/notes/fix-tag-attribute-disappearing-25483a80f548ef35.yaml b/releasenotes/notes/fix-tag-attribute-disappearing-25483a80f548ef35.yaml new file mode 100644 index 000000000..7f6ed29ec --- /dev/null +++ b/releasenotes/notes/fix-tag-attribute-disappearing-25483a80f548ef35.yaml @@ -0,0 +1,13 @@ +--- +fixes: + - | + Microversion 2.42 is related to the following bug. + + * https://bugs.launchpad.net/nova/+bug/1658571 + + The following options have been changed as of Microversion 2.42. + + * Remove ``tag`` attribute in ``--block-device`` option + on the server boot (nova boot) between microversion 2.33 and 2.41. + * Remove ``tag`` attribute in ``--nic`` option + on the server boot (nova boot) between microversion 2.37 and 2.41. From ff1eff18d9815ffa2588034769c842615e76507d Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 21 Apr 2017 10:01:30 -0500 Subject: [PATCH 1284/1705] Remove direct dependency on requests keystoneauth is the library that pulls in requests. If we have client libraries also depend directly on requests, we can get into situations for end users in installations where, because of sequencing of client library releases there are conflicting versions of requests needed and pkg_resources/entrypoints break. Remove the direct depend and let keystoneauth pull it in for us. Change-Id: I76fe89f9c166e61971b5c116c7f01fbd5ccb5cf7 --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index fed242824..6a18a4644 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,6 @@ oslo.i18n>=2.1.0 # Apache-2.0 oslo.serialization>=1.10.0 # Apache-2.0 oslo.utils>=3.20.0 # Apache-2.0 PrettyTable<0.8,>=0.7.1 # BSD -requests!=2.12.2,!=2.13.0,>=2.10.0 # Apache-2.0 simplejson>=2.2.0 # MIT six>=1.9.0 # MIT Babel!=2.4.0,>=2.3.4 # BSD From 59f885896afc0ee637e66a77ad7e0f483f81bdcc Mon Sep 17 00:00:00 2001 From: He Jie Xu Date: Fri, 21 Apr 2017 14:11:19 +0800 Subject: [PATCH 1285/1705] Add `instance-uuid` flag to the migration-list The user needs to query the specific server's migration history. The 'os-migrations' support 'instance_uuid' as query filter, but that isn't enabled in the migration-list command. This patch enable that filter. Change-Id: I137b94f4d73836bcc163b66ab20ee2987042a40f --- novaclient/tests/unit/v2/fakes.py | 33 ++++++++++++++++--- novaclient/tests/unit/v2/test_migrations.py | 12 +++++++ novaclient/v2/migrations.py | 4 ++- novaclient/v2/shell.py | 8 ++++- ...ag-in-migration-list-5d2fed7657d3def5.yaml | 7 ++++ 5 files changed, 58 insertions(+), 6 deletions(-) create mode 100644 releasenotes/notes/instance-uuid-flag-in-migration-list-5d2fed7657d3def5.yaml diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 1aaf76d4f..dedbd395c 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -1937,7 +1937,7 @@ def get_os_cells_child_cell_capacities(self, **kw): return self.get_os_cells_capacities() def get_os_migrations(self, **kw): - migration = { + migration1 = { "created_at": "2012-10-29T13:42:02.000000", "dest_compute": "compute2", "dest_host": "1.2.3.4", @@ -1952,10 +1952,35 @@ def get_os_migrations(self, **kw): "updated_at": "2012-10-29T13:42:02.000000" } - if self.api_version >= api_versions.APIVersion("2.23"): - migration.update({"migration_type": "live-migration"}) + migration2 = { + "created_at": "2012-10-29T13:42:02.000000", + "dest_compute": "compute2", + "dest_host": "1.2.3.4", + "dest_node": "node2", + "id": '1234', + "instance_uuid": "instance_id_456", + "new_instance_type_id": 2, + "old_instance_type_id": 1, + "source_compute": "compute1", + "source_node": "node1", + "status": "Done", + "updated_at": "2013-11-50T13:42:02.000000" + } - migrations = {'migrations': [migration]} + if self.api_version >= api_versions.APIVersion("2.23"): + migration1.update({"migration_type": "live-migration"}) + migration2.update({"migration_type": "live-migration"}) + + migration_list = [] + instance_uuid = kw.get('instance_uuid', None) + if instance_uuid == migration1['instance_uuid']: + migration_list.append(migration1) + elif instance_uuid == migration2['instance_uuid']: + migration_list.append(migration2) + elif instance_uuid is None: + migration_list.extend([migration1, migration2]) + + migrations = {'migrations': migration_list} return (200, FAKE_RESPONSE_HEADERS, migrations) diff --git a/novaclient/tests/unit/v2/test_migrations.py b/novaclient/tests/unit/v2/test_migrations.py index b270f9c3c..e69d3327e 100644 --- a/novaclient/tests/unit/v2/test_migrations.py +++ b/novaclient/tests/unit/v2/test_migrations.py @@ -47,3 +47,15 @@ def test_list_migrations_with_filters(self): '/os-migrations?cell_name=child1&host=host1&status=finished') for m in ml: self.assertIsInstance(m, migrations.Migration) + + def test_list_migrations_with_instance_uuid_filter(self): + ml = self.cs.migrations.list('host1', 'finished', 'child1', + 'instance_id_456') + self.assert_request_id(ml, fakes.FAKE_REQUEST_ID_LIST) + + self.cs.assert_called( + 'GET', + ('/os-migrations?cell_name=child1&host=host1&' + 'instance_uuid=instance_id_456&status=finished')) + self.assertEqual(1, len(ml)) + self.assertEqual('instance_id_456', ml[0].instance_uuid) diff --git a/novaclient/v2/migrations.py b/novaclient/v2/migrations.py index 075e59d7a..5a39ed992 100644 --- a/novaclient/v2/migrations.py +++ b/novaclient/v2/migrations.py @@ -28,7 +28,7 @@ def __repr__(self): class MigrationManager(base.ManagerWithFind): resource_class = Migration - def list(self, host=None, status=None, cell_name=None): + def list(self, host=None, status=None, cell_name=None, instance_uuid=None): """ Get a list of migrations. :param host: (optional) filter migrations by host name. @@ -45,6 +45,8 @@ def list(self, host=None, status=None, cell_name=None): "deprecated since Pike, and will " "be removed in a future release.")) opts['cell_name'] = cell_name + if instance_uuid: + opts['instance_uuid'] = instance_uuid # Transform the dict to a sequence of two-element tuples in fixed # order, then the encoded string will be consistent in Python 2&3. diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 42bf9b7f7..0e18d1809 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -4834,6 +4834,11 @@ def migration_type(migration): utils.print_list(migrations, fields, formatters) +@utils.arg( + '--instance-uuid', + dest='instance_uuid', + metavar='', + help=_('Fetch migrations for the given instance.')) @utils.arg( '--host', dest='host', @@ -4855,5 +4860,6 @@ def migration_type(migration): 'removed after version 8.0.0.')) def do_migration_list(cs, args): """Print a list of migrations.""" - migrations = cs.migrations.list(args.host, args.status) + migrations = cs.migrations.list(args.host, args.status, None, + instance_uuid=args.instance_uuid) _print_migrations(cs, migrations) diff --git a/releasenotes/notes/instance-uuid-flag-in-migration-list-5d2fed7657d3def5.yaml b/releasenotes/notes/instance-uuid-flag-in-migration-list-5d2fed7657d3def5.yaml new file mode 100644 index 000000000..239c662ea --- /dev/null +++ b/releasenotes/notes/instance-uuid-flag-in-migration-list-5d2fed7657d3def5.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + A new ``--instance-uuid`` option is added to ``nova migration-list`` + command. This is used to query the migration history of a specific server + by the migration-list command. Please use ``nova server-migration-list`` + command for querying in-progress migrations of a specific server. From ea3b9f7fef28a7f06b51a3b176c85a1fc33cecaa Mon Sep 17 00:00:00 2001 From: jichenjc Date: Wed, 29 Mar 2017 05:28:17 +0800 Subject: [PATCH 1286/1705] 2.43: Deprecate novaclient /os-hosts usage os-hosts is deprecated in the compute REST API with microversion 2.43 via change: Ieb85653b85a1eff38a9fb0c9ff05e4cd39150ecc So nova shell and API bindings should be deprecated as well. Implements blueprint deprecate-os-hosts Change-Id: I79091edf5a2569e49e79deba312456fdcdee09e1 --- novaclient/__init__.py | 2 +- novaclient/tests/unit/v2/test_hosts.py | 46 +++++++++++++++++++ novaclient/tests/unit/v2/test_shell.py | 38 +++++++++++---- novaclient/v2/hosts.py | 31 +++++++++++-- novaclient/v2/shell.py | 41 +++++++++++++++-- .../microversion-v2_43-76db2ac463b431e4.yaml | 14 ++++++ 6 files changed, 155 insertions(+), 17 deletions(-) create mode 100644 releasenotes/notes/microversion-v2_43-76db2ac463b431e4.yaml diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 7c0d2c937..61377ccab 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.42") +API_MAX_VERSION = api_versions.APIVersion("2.43") diff --git a/novaclient/tests/unit/v2/test_hosts.py b/novaclient/tests/unit/v2/test_hosts.py index aa5186546..a44a5e01e 100644 --- a/novaclient/tests/unit/v2/test_hosts.py +++ b/novaclient/tests/unit/v2/test_hosts.py @@ -11,6 +11,10 @@ # License for the specific language governing permissions and limitations # under the License. +import mock + +from novaclient import api_versions +from novaclient import exceptions from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit.fixture_data import hosts as data from novaclient.tests.unit import utils @@ -23,8 +27,14 @@ class HostsTest(utils.FixturedTestCase): client_fixture_class = client.V1 data_fixture_class = data.V1 + def setUp(self): + super(HostsTest, self).setUp() + self.warning_mock = mock.patch('warnings.warn').start() + self.addCleanup(self.warning_mock.stop) + def test_describe_resource(self): hs = self.cs.hosts.get('host') + self.warning_mock.assert_called_once() self.assert_request_id(hs, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/os-hosts/host') for h in hs: @@ -32,6 +42,7 @@ def test_describe_resource(self): def test_list_host(self): hs = self.cs.hosts.list() + self.warning_mock.assert_called_once() self.assert_request_id(hs, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/os-hosts') for h in hs: @@ -50,6 +61,8 @@ def test_update_enable(self): host = self.cs.hosts.get('sample_host')[0] values = {"status": "enabled"} result = host.update(values) + # one warning for the get, one warning for the update + self.assertEqual(2, self.warning_mock.call_count) self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('PUT', '/os-hosts/sample_host', values) self.assertIsInstance(result, hosts.Host) @@ -74,6 +87,8 @@ def test_update_both(self): def test_host_startup(self): host = self.cs.hosts.get('sample_host')[0] result = host.startup() + # one warning for the get, one warning for the action + self.assertEqual(2, self.warning_mock.call_count) self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) self.assert_called( 'GET', '/os-hosts/sample_host/startup') @@ -81,6 +96,8 @@ def test_host_startup(self): def test_host_reboot(self): host = self.cs.hosts.get('sample_host')[0] result = host.reboot() + # one warning for the get, one warning for the action + self.assertEqual(2, self.warning_mock.call_count) self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) self.assert_called( 'GET', '/os-hosts/sample_host/reboot') @@ -88,6 +105,8 @@ def test_host_reboot(self): def test_host_shutdown(self): host = self.cs.hosts.get('sample_host')[0] result = host.shutdown() + # one warning for the get, one warning for the action + self.assertEqual(2, self.warning_mock.call_count) self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) self.assert_called( 'GET', '/os-hosts/sample_host/shutdown') @@ -100,3 +119,30 @@ def test_hosts_list_repr(self): hs = self.cs.hosts.list() for h in hs: self.assertEqual('' % h.host_name, repr(h)) + + +class DeprecatedHostsTestv2_43(utils.FixturedTestCase): + """Tests the os-hosts API bindings at microversion 2.43 to ensure + they fail with a 404 error. + """ + client_fixture_class = client.V1 + + def setUp(self): + super(DeprecatedHostsTestv2_43, self).setUp() + self.cs.api_version = api_versions.APIVersion('2.43') + + def test_get(self): + self.assertRaises(exceptions.VersionNotFoundForAPIMethod, + self.cs.hosts.get, 'host') + + def test_list(self): + self.assertRaises(exceptions.VersionNotFoundForAPIMethod, + self.cs.hosts.list) + + def test_update(self): + self.assertRaises(exceptions.VersionNotFoundForAPIMethod, + self.cs.hosts.update, 'host', {"status": "enabled"}) + + def test_host_action(self): + self.assertRaises(exceptions.VersionNotFoundForAPIMethod, + self.cs.hosts.host_action, 'host', 'reboot') diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 7e1d28fca..dbf7e5cb8 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2171,7 +2171,11 @@ def test_services_delete(self): self.assert_called('DELETE', '/os-services/1') def test_host_list(self): - self.run_command('host-list') + _, err = self.run_command('host-list') + # make sure we said it's deprecated + self.assertIn('WARNING: Command host-list is deprecated', err) + # and replaced with hypervisor-list + self.assertIn('hypervisor-list', err) self.assert_called('GET', '/os-hosts') def test_host_list_with_zone(self): @@ -2179,23 +2183,40 @@ def test_host_list_with_zone(self): self.assert_called('GET', '/os-hosts?zone=nova') def test_host_update_status(self): - self.run_command('host-update sample-host_1 --status enabled') - body = {'status': 'enabled'} + _, err = self.run_command('host-update sample-host_1 --status enable') + # make sure we said it's deprecated + self.assertIn('WARNING: Command host-update is deprecated', err) + # and replaced with service-enable + self.assertIn('service-enable', err) + body = {'status': 'enable'} self.assert_called('PUT', '/os-hosts/sample-host_1', body) def test_host_update_maintenance(self): - self.run_command('host-update sample-host_2 --maintenance enable') + _, err = ( + self.run_command('host-update sample-host_2 --maintenance enable')) + # make sure we said it's deprecated + self.assertIn('WARNING: Command host-update is deprecated', err) + # and there is no replacement + self.assertIn('There is no replacement', err) body = {'maintenance_mode': 'enable'} self.assert_called('PUT', '/os-hosts/sample-host_2', body) def test_host_update_multiple_settings(self): - self.run_command('host-update sample-host_3 ' - '--status disabled --maintenance enable') - body = {'status': 'disabled', 'maintenance_mode': 'enable'} + _, err = self.run_command('host-update sample-host_3 ' + '--status disable --maintenance enable') + # make sure we said it's deprecated + self.assertIn('WARNING: Command host-update is deprecated', err) + # and replaced with service-disable + self.assertIn('service-disable', err) + body = {'status': 'disable', 'maintenance_mode': 'enable'} self.assert_called('PUT', '/os-hosts/sample-host_3', body) def test_host_startup(self): - self.run_command('host-action sample-host --action startup') + _, err = self.run_command('host-action sample-host --action startup') + # make sure we said it's deprecated + self.assertIn('WARNING: Command host-action is deprecated', err) + # and there is no replacement + self.assertIn('There is no replacement', err) self.assert_called( 'GET', '/os-hosts/sample-host/startup') @@ -2881,6 +2902,7 @@ def test_versions(self): 39, # There are no versioned wrapped shell method changes for this 41, # There are no version-wrapped shell method changes for this. 42, # There are no version-wrapped shell method changes for this. + 43, # There are no version-wrapped shell method changes for this. ]) versions_supported = set(range(0, novaclient.API_MAX_VERSION.ver_minor + 1)) diff --git a/novaclient/v2/hosts.py b/novaclient/v2/hosts.py index 2eceafdf6..713c866f3 100644 --- a/novaclient/v2/hosts.py +++ b/novaclient/v2/hosts.py @@ -14,12 +14,23 @@ # under the License. """ -host interface (1.1 extension). +DEPRECATED host interface (1.1 extension). """ +import warnings + +from novaclient import api_versions from novaclient import base +from novaclient.i18n import _ + + +HOSTS_DEPRECATION_WARNING = ( + _('The os-hosts API is deprecated. This API binding will be removed ' + 'in the first major release after the Nova server 16.0.0 Pike release.') +) class Host(base.Resource): + """DEPRECATED""" def __repr__(self): return "" % self.host @@ -28,15 +39,19 @@ def _add_details(self, info): for (k, v) in dico.items(): setattr(self, k, v) + @api_versions.wraps("2.0", "2.42") def update(self, values): return self.manager.update(self.host, values) + @api_versions.wraps("2.0", "2.42") def startup(self): return self.manager.host_action(self.host, 'startup') + @api_versions.wraps("2.0", "2.42") def shutdown(self): return self.manager.host_action(self.host, 'shutdown') + @api_versions.wraps("2.0", "2.42") def reboot(self): return self.manager.host_action(self.host, 'reboot') @@ -56,31 +71,39 @@ def host_name(self, value): class HostManager(base.ManagerWithFind): resource_class = Host + @api_versions.wraps("2.0", "2.42") def get(self, host): """ - Describes cpu/memory/hdd info for host. + DEPRECATED Describes cpu/memory/hdd info for host. :param host: destination host name. """ + warnings.warn(HOSTS_DEPRECATION_WARNING, DeprecationWarning) return self._list("/os-hosts/%s" % host, "host") + @api_versions.wraps("2.0", "2.42") def update(self, host, values): - """Update status or maintenance mode for the host.""" + """DEPRECATED Update status or maintenance mode for the host.""" + warnings.warn(HOSTS_DEPRECATION_WARNING, DeprecationWarning) return self._update("/os-hosts/%s" % host, values) + @api_versions.wraps("2.0", "2.42") def host_action(self, host, action): """ - Perform an action on a host. + DEPRECATED Perform an action on a host. :param host: The host to perform an action :param action: The action to perform :returns: An instance of novaclient.base.TupleWithMeta """ + warnings.warn(HOSTS_DEPRECATION_WARNING, DeprecationWarning) url = '/os-hosts/%s/%s' % (host, action) resp, body = self.api.client.get(url) return base.TupleWithMeta((resp, body), resp) + @api_versions.wraps("2.0", "2.42") def list(self, zone=None): + warnings.warn(HOSTS_DEPRECATION_WARNING, DeprecationWarning) url = '/os-hosts' if zone: url = '/os-hosts?zone=%s' % zone diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 42bf9b7f7..8fa29a304 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -55,6 +55,26 @@ ) +# NOTE(mriedem): Remove this along with the deprecated commands in the first +# major python-novaclient release AFTER the nova server 16.0.0 Pike release. +def emit_hosts_deprecation_warning(command_name, replacement=None): + if replacement is None: + print(_('WARNING: Command %s is deprecated and will be removed ' + 'in the first major release after the Nova server 16.0.0 ' + 'Pike release. There is no replacement or alternative for ' + 'this command. Specify --os-compute-api-version less than ' + '2.43 to continue using this command until it is removed.') % + command_name, file=sys.stderr) + else: + print(_('WARNING: Command %(command)s is deprecated and will be ' + 'removed in the first major release after the Nova server ' + '16.0.0 Pike release. Use %(replacement)s instead. Specify ' + '--os-compute-api-version less than 2.43 to continue using ' + 'this command until it is removed.') % + {'command': command_name, 'replacement': replacement}, + file=sys.stderr) + + CLIENT_BDM2_KEYS = { 'id': 'uuid', 'source': 'source_type', @@ -3386,7 +3406,9 @@ def do_service_delete(cs, args): @utils.arg('host', metavar='', help=_('Name of host.')) def do_host_describe(cs, args): - """Describe a specific host.""" + """DEPRECATED Describe a specific host.""" + emit_hosts_deprecation_warning('host-describe', 'hypervisor-show') + result = cs.hosts.get(args.host) columns = ["HOST", "PROJECT", "cpu", "memory_mb", "disk_gb"] utils.print_list(result, columns) @@ -3399,7 +3421,9 @@ def do_host_describe(cs, args): help=_('Filters the list, returning only those hosts in the availability ' 'zone .')) def do_host_list(cs, args): - """List all hosts by service.""" + """DEPRECATED List all hosts by service.""" + emit_hosts_deprecation_warning('host-list', 'hypervisor-list') + columns = ["host_name", "service", "zone"] result = cs.hosts.list(args.zone) utils.print_list(result, columns) @@ -3416,7 +3440,14 @@ def do_host_list(cs, args): dest='maintenance', help=_('Either put or resume host to/from maintenance.')) def do_host_update(cs, args): - """Update host settings.""" + """DEPRECATED Update host settings.""" + if args.status == 'enable': + emit_hosts_deprecation_warning('host-update', 'service-enable') + elif args.status == 'disable': + emit_hosts_deprecation_warning('host-update', 'service-disable') + else: + emit_hosts_deprecation_warning('host-update') + updates = {} columns = ["HOST"] if args.status: @@ -3435,7 +3466,9 @@ def do_host_update(cs, args): choices=['startup', 'shutdown', 'reboot'], help=_('A power action: startup, reboot, or shutdown.')) def do_host_action(cs, args): - """Perform a power action on a host.""" + """DEPRECATED Perform a power action on a host.""" + emit_hosts_deprecation_warning('host-action') + result = cs.hosts.host_action(args.host, args.action) utils.print_list([result], ['HOST', 'power_action']) diff --git a/releasenotes/notes/microversion-v2_43-76db2ac463b431e4.yaml b/releasenotes/notes/microversion-v2_43-76db2ac463b431e4.yaml new file mode 100644 index 000000000..7f70ce557 --- /dev/null +++ b/releasenotes/notes/microversion-v2_43-76db2ac463b431e4.yaml @@ -0,0 +1,14 @@ +--- +deprecations: + - | + The following CLIs and their backing API bindings are deprecated and capped + at microversion 2.43: + + * ``nova host-describe`` - superseded by ``nova hypervisor-show`` + * ``nova host-list`` - superseded by ``nova hypervisor-list`` + * ``nova host-update`` - superseded by ``nova service-enable`` and + ``nova service-disable`` + * ``nova host-action`` - no alternative by design + + The CLIs and API bindings will be removed in the first major release after + Nova 16.0.0 Pike is released. From e303cf11bf6c8652edf13caa0ddf238103ea8601 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Tue, 25 Apr 2017 16:24:50 -0400 Subject: [PATCH 1287/1705] 2.44: Deprecate multinic/floatingIP actions and os-virtual-interfaces The 2.44 microversion deprecates the addFixedIP, removeFixedIP, addFloatingIP, removeFloatingIP server action APIs and os-virtual-interfaces API. This change deprecates the CLIs and python API bindings in novaclient. Implements blueprint deprecate-multinic-proxy-api Change-Id: Ie76283962c375b735f30ccb3053db07cf2330de2 --- novaclient/__init__.py | 2 +- novaclient/tests/unit/v2/test_servers.py | 16 +++++++--- novaclient/tests/unit/v2/test_shell.py | 21 ++++++++++--- novaclient/v2/servers.py | 29 +++++++++++++---- novaclient/v2/shell.py | 31 ++++++++++++++++--- novaclient/v2/virtual_interfaces.py | 13 +++++++- .../microversion-v2_44-d60c8834e436ad3d.yaml | 16 ++++++++++ 7 files changed, 106 insertions(+), 22 deletions(-) create mode 100644 releasenotes/notes/microversion-v2_44-d60c8834e436ad3d.yaml diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 61377ccab..c8e960980 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.43") +API_MAX_VERSION = api_versions.APIVersion("2.44") diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index 3bb22bfb5..a68adeed5 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -556,27 +556,33 @@ def test_migrate_server(self): self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') - def test_add_fixed_ip(self): + @mock.patch('warnings.warn') + def test_add_fixed_ip(self, mock_warn): s = self.cs.servers.get(1234) fip = s.add_fixed_ip(1) + mock_warn.assert_called_once() self.assert_request_id(fip, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') fip = self.cs.servers.add_fixed_ip(s, 1) self.assert_request_id(fip, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') - def test_remove_fixed_ip(self): + @mock.patch('warnings.warn') + def test_remove_fixed_ip(self, mock_warn): s = self.cs.servers.get(1234) ret = s.remove_fixed_ip('10.0.0.1') + mock_warn.assert_called_once() self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') ret = self.cs.servers.remove_fixed_ip(s, '10.0.0.1') self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') - def test_add_floating_ip(self): + @mock.patch('warnings.warn') + def test_add_floating_ip(self, mock_warn): s = self.cs.servers.get(1234) fip = s.add_floating_ip('11.0.0.1') + mock_warn.assert_called_once() self.assert_request_id(fip, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') fip = self.cs.servers.add_floating_ip(s, '11.0.0.1') @@ -607,9 +613,11 @@ def test_add_floating_ip_to_fixed(self): self.assert_request_id(fip, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') - def test_remove_floating_ip(self): + @mock.patch('warnings.warn') + def test_remove_floating_ip(self, mock_warn): s = self.cs.servers.get(1234) ret = s.remove_floating_ip('11.0.0.1') + mock_warn.assert_called_once() self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') ret = self.cs.servers.remove_floating_ip(s, '11.0.0.1') diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index dbf7e5cb8..c9346e504 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -1671,12 +1671,18 @@ def test_delete_host_meta(self): self.assert_called('DELETE', '/servers/uuid2/metadata/key1', pos=2) def test_server_floating_ip_associate(self): - self.run_command('floating-ip-associate sample-server 11.0.0.1') + _, err = self.run_command( + 'floating-ip-associate sample-server 11.0.0.1') + self.assertIn('WARNING: Command floating-ip-associate is deprecated', + err) self.assert_called('POST', '/servers/1234/action', {'addFloatingIp': {'address': '11.0.0.1'}}) def test_server_floating_ip_disassociate(self): - self.run_command('floating-ip-disassociate sample-server 11.0.0.1') + _, err = self.run_command( + 'floating-ip-disassociate sample-server 11.0.0.1') + self.assertIn( + 'WARNING: Command floating-ip-disassociate is deprecated', err) self.assert_called('POST', '/servers/1234/action', {'removeFloatingIp': {'address': '11.0.0.1'}}) @@ -2500,12 +2506,14 @@ def test_cloudpipe_configure(self): self.assert_called('PUT', '/os-cloudpipe/configure-project', body) def test_add_fixed_ip(self): - self.run_command('add-fixed-ip sample-server 1') + _, err = self.run_command('add-fixed-ip sample-server 1') + self.assertIn('WARNING: Command add-fixed-ip is deprecated', err) self.assert_called('POST', '/servers/1234/action', {'addFixedIp': {'networkId': '1'}}) def test_remove_fixed_ip(self): - self.run_command('remove-fixed-ip sample-server 10.0.0.10') + _, err = self.run_command('remove-fixed-ip sample-server 10.0.0.10') + self.assertIn('WARNING: Command remove-fixed-ip is deprecated', err) self.assert_called('POST', '/servers/1234/action', {'removeFixedIp': {'address': '10.0.0.10'}}) @@ -2867,7 +2875,9 @@ def test_list_server_group_with_limit_and_offset(self): self.assert_called('GET', '/os-server-groups?limit=20&offset=5') def test_list_server_os_virtual_interfaces(self): - self.run_command('virtual-interface-list 1234') + _, err = self.run_command('virtual-interface-list 1234') + self.assertIn('WARNING: Command virtual-interface-list is deprecated', + err) self.assert_called('GET', '/servers/1234/os-virtual-interfaces') def test_versions(self): @@ -2903,6 +2913,7 @@ def test_versions(self): 41, # There are no version-wrapped shell method changes for this. 42, # There are no version-wrapped shell method changes for this. 43, # There are no version-wrapped shell method changes for this. + 44, # There are no version-wrapped shell method changes for this. ]) versions_supported = set(range(0, novaclient.API_MAX_VERSION.ver_minor + 1)) diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index ea05acd59..cd328f9c0 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -20,6 +20,7 @@ """ import base64 +import warnings from oslo_utils import encodeutils import six @@ -51,6 +52,12 @@ 'webmks': 'mks' } +ADD_REMOVE_FIXED_FLOATING_DEPRECATION_WARNING = _( + 'The %s server action API is deprecated as of the 2.44 microversion. This ' + 'API binding will be removed in the first major release after the Nova ' + '16.0.0 Pike release. Use python-neutronclient or openstacksdk instead.' +) + class Server(base.Resource): HUMAN_ID = True @@ -871,29 +878,36 @@ def list(self, detailed=True, search_opts=None, marker=None, limit=None, marker = result[-1].id return result + @api_versions.wraps('2.0', '2.43') def add_fixed_ip(self, server, network_id): """ - Add an IP address on a network. + DEPRECATED Add an IP address on a network. :param server: The :class:`Server` (or its ID) to add an IP to. :param network_id: The ID of the network the IP should be on. :returns: An instance of novaclient.base.TupleWithMeta """ + warnings.warn(ADD_REMOVE_FIXED_FLOATING_DEPRECATION_WARNING % + 'addFixedIP', DeprecationWarning) return self._action('addFixedIp', server, {'networkId': network_id}) + @api_versions.wraps('2.0', '2.43') def remove_fixed_ip(self, server, address): """ - Remove an IP address. + DEPRECATED Remove an IP address. :param server: The :class:`Server` (or its ID) to add an IP to. :param address: The IP address to remove. :returns: An instance of novaclient.base.TupleWithMeta """ + warnings.warn(ADD_REMOVE_FIXED_FLOATING_DEPRECATION_WARNING % + 'removeFixedIP', DeprecationWarning) return self._action('removeFixedIp', server, {'address': address}) + @api_versions.wraps('2.0', '2.43') def add_floating_ip(self, server, address, fixed_address=None): """ - Add a floating IP to an instance + DEPRECATED Add a floating IP to an instance :param server: The :class:`Server` (or its ID) to add an IP to. :param address: The FloatingIP or string floating address to add. @@ -901,7 +915,8 @@ def add_floating_ip(self, server, address, fixed_address=None): associated with (optional) :returns: An instance of novaclient.base.TupleWithMeta """ - + warnings.warn(ADD_REMOVE_FIXED_FLOATING_DEPRECATION_WARNING % + 'addFloatingIP', DeprecationWarning) address = address.ip if hasattr(address, 'ip') else address if fixed_address: if hasattr(fixed_address, 'ip'): @@ -912,15 +927,17 @@ def add_floating_ip(self, server, address, fixed_address=None): else: return self._action('addFloatingIp', server, {'address': address}) + @api_versions.wraps('2.0', '2.43') def remove_floating_ip(self, server, address): """ - Remove a floating IP address. + DEPRECATED Remove a floating IP address. :param server: The :class:`Server` (or its ID) to remove an IP from. :param address: The FloatingIP or string floating address to remove. :returns: An instance of novaclient.base.TupleWithMeta """ - + warnings.warn(ADD_REMOVE_FIXED_FLOATING_DEPRECATION_WARNING % + 'removeFloatingIP', DeprecationWarning) address = address.ip if hasattr(address, 'ip') else address return self._action('removeFloatingIp', server, {'address': address}) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 8fa29a304..4df02f88a 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -75,6 +75,17 @@ def emit_hosts_deprecation_warning(command_name, replacement=None): file=sys.stderr) +# NOTE(mriedem): Remove this along with the deprecated commands in the first +# major python-novaclient release AFTER the nova server 16.0.0 Pike release. +def emit_fixed_floating_deprecation_warning(command_name): + print(_('WARNING: Command %s is deprecated and will be removed ' + 'in the first major release after the Nova server 16.0.0 ' + 'Pike release. Use python-neutronclient or python-openstackclient' + 'instead. Specify --os-compute-api-version less than 2.44 ' + 'to continue using this command until it is removed.') % + command_name, file=sys.stderr) + + CLIENT_BDM2_KEYS = { 'id': 'uuid', 'source': 'source_type', @@ -2200,7 +2211,8 @@ def _find_network_id(cs, net_name): metavar='', help=_('Network ID.')) def do_add_fixed_ip(cs, args): - """Add new IP address on a network to server.""" + """DEPRECATED Add new IP address on a network to server.""" + emit_fixed_floating_deprecation_warning('add-fixed-ip') server = _find_server(cs, args.server) server.add_fixed_ip(args.network_id) @@ -2208,7 +2220,8 @@ def do_add_fixed_ip(cs, args): @utils.arg('server', metavar='', help=_('Name or ID of server.')) @utils.arg('address', metavar='
', help=_('IP Address.')) def do_remove_fixed_ip(cs, args): - """Remove an IP address from a server.""" + """DEPRECATED Remove an IP address from a server.""" + emit_fixed_floating_deprecation_warning('remove-fixed-ip') server = _find_server(cs, args.server) server.remove_fixed_ip(args.address) @@ -2442,7 +2455,8 @@ def do_console_log(cs, args): default=None, help=_('Fixed IP Address to associate with.')) def do_floating_ip_associate(cs, args): - """Associate a floating IP address to a server.""" + """DEPRECATED Associate a floating IP address to a server.""" + emit_fixed_floating_deprecation_warning('floating-ip-associate') _associate_floating_ip(cs, args) @@ -2454,7 +2468,8 @@ def _associate_floating_ip(cs, args): @utils.arg('server', metavar='', help=_('Name or ID of server.')) @utils.arg('address', metavar='
', help=_('IP Address.')) def do_floating_ip_disassociate(cs, args): - """Disassociate a floating IP address from a server.""" + """DEPRECATED Disassociate a floating IP address from a server.""" + emit_fixed_floating_deprecation_warning('floating-ip-disassociate') _disassociate_floating_ip(cs, args) @@ -4473,7 +4488,13 @@ def _print_virtual_interface_list(cs, interface_list): @utils.arg('server', metavar='', help=_('ID of server.')) def do_virtual_interface_list(cs, args): - """Show virtual interface info about the given server.""" + """DEPRECATED Show virtual interface info about the given server.""" + print(_('WARNING: Command virtual-interface-list is deprecated and will ' + 'be removed in the first major release after the Nova server ' + '16.0.0 Pike release. There is no replacement or alternative for ' + 'this command. Specify --os-compute-api-version less than 2.44 ' + 'to continue using this command until it is removed.'), + file=sys.stderr) server = _find_server(cs, args.server) interface_list = cs.virtual_interfaces.list(base.getid(server)) _print_virtual_interface_list(cs, interface_list) diff --git a/novaclient/v2/virtual_interfaces.py b/novaclient/v2/virtual_interfaces.py index c2a0adf4a..88cd001f8 100644 --- a/novaclient/v2/virtual_interfaces.py +++ b/novaclient/v2/virtual_interfaces.py @@ -14,10 +14,14 @@ # under the License. """ -Virtual Interfaces (1.1 extension). +DEPRECATED Virtual Interfaces (1.1 extension). """ +import warnings + +from novaclient import api_versions from novaclient import base +from novaclient.i18n import _ class VirtualInterface(base.Resource): @@ -26,8 +30,15 @@ def __repr__(self): class VirtualInterfaceManager(base.ManagerWithFind): + """DEPRECATED""" resource_class = VirtualInterface + @api_versions.wraps('2.0', '2.43') def list(self, instance_id): + """DEPRECATED""" + warnings.warn(_('The os-virtual-interfaces API is deprecated. This ' + 'API binding will be removed in the first major ' + 'release after the Nova server 16.0.0 Pike release.'), + DeprecationWarning) return self._list('/servers/%s/os-virtual-interfaces' % instance_id, 'virtual_interfaces') diff --git a/releasenotes/notes/microversion-v2_44-d60c8834e436ad3d.yaml b/releasenotes/notes/microversion-v2_44-d60c8834e436ad3d.yaml new file mode 100644 index 000000000..2ac1b3b04 --- /dev/null +++ b/releasenotes/notes/microversion-v2_44-d60c8834e436ad3d.yaml @@ -0,0 +1,16 @@ +--- +deprecations: + - | + The following CLIs and their backing API bindings are deprecated and capped + at microversion 2.44: + + * ``nova add-fixed-ip``: use python-neutronclient or openstacksdk + * ``nova remove-fixed-ip``: use python-neutronclient or openstacksdk + * ``nova floating-ip-associate``: use python-neutronclient or openstacksdk + * ``nova floating-ip-disassociate``: use python-neutronclient or + openstacksdk + * ``nova virtual-interface-list``: there is no replacement as this is + only implemented for nova-network which is deprecated + + The CLIs and API bindings will be removed in the first major release after + Nova 16.0.0 Pike is released. From 603f0eae9f4c77b3e43a8c83eb797e857c75343d Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Fri, 14 Apr 2017 21:07:45 -0400 Subject: [PATCH 1288/1705] 2.45: createImage/createBackup image_id is in response body This adds support for microversion 2.45 which changes the response on the createImage and createBackup server action APIs to return the image_id for the created snapshot image in a json dict in the response body rather than in a Location header (which is gone in microversion >= 2.45). Since the 'nova backup' command was not printing out the created image ID before, that is also added in this microversion. Part of blueprint remove-create-image-location-header-response Change-Id: Id48aa7b14e2d25008287549b04db437ca9c3f548 --- novaclient/__init__.py | 2 +- novaclient/tests/unit/fixture_data/servers.py | 11 +++++ novaclient/tests/unit/v2/fakes.py | 34 ++++++++++--- novaclient/tests/unit/v2/test_servers.py | 48 +++++++++++++++++++ novaclient/tests/unit/v2/test_shell.py | 34 ++++++++++++- novaclient/v2/servers.py | 12 +++-- novaclient/v2/shell.py | 10 ++-- .../microversion-v2_45-1bfcae3914280534.yaml | 13 +++++ 8 files changed, 150 insertions(+), 14 deletions(-) create mode 100644 releasenotes/notes/microversion-v2_45-1bfcae3914280534.yaml diff --git a/novaclient/__init__.py b/novaclient/__init__.py index c8e960980..cead09fa6 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.44") +API_MAX_VERSION = api_versions.APIVersion("2.45") diff --git a/novaclient/tests/unit/fixture_data/servers.py b/novaclient/tests/unit/fixture_data/servers.py index 7161f9da3..7232fb589 100644 --- a/novaclient/tests/unit/fixture_data/servers.py +++ b/novaclient/tests/unit/fixture_data/servers.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient import api_versions from novaclient.tests.unit import fakes from novaclient.tests.unit.fixture_data import base from novaclient.tests.unit.v2 import fakes as v2_fakes @@ -443,6 +444,8 @@ def post_servers_1234_action(self, request, context): context.status_code = 202 assert len(body.keys()) == 1 action = list(body)[0] + api_version = api_versions.APIVersion( + request.headers.get('X-OpenStack-Nova-API-Version', '2.1')) if v2_fakes.FakeSessionClient.check_server_actions(body): # NOTE(snikitin): No need to do any operations here. This 'pass' @@ -475,6 +478,14 @@ def post_servers_1234_action(self, request, context): _body = {'adminPass': 'RescuePassword'} elif action == 'createImage': assert set(body[action].keys()) == set(['name', 'metadata']) + if api_version >= api_versions.APIVersion('2.45'): + return {'image_id': '456'} + context.headers['location'] = "http://blah/images/456" + elif action == 'createBackup': + assert set(body[action].keys()) == set(['name', 'backup_type', + 'rotation']) + if api_version >= api_versions.APIVersion('2.45'): + return {'image_id': '456'} context.headers['location'] = "http://blah/images/456" elif action == 'os-getConsoleOutput': assert list(body[action]) == ['length'] diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 1aaf76d4f..3fe4e8d97 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -47,6 +47,7 @@ FAKE_IMAGE_UUID_2 = 'f27f479a-ddda-419a-9bbc-d6b56b210161' FAKE_IMAGE_UUID_SNAPSHOT = '555cae93-fb41-4145-9c52-f5b923538a26' FAKE_IMAGE_UUID_SNAP_DEL = '55bb23af-97a4-4068-bdf8-f10c62880ddf' +FAKE_IMAGE_UUID_BACKUP = '2f87e889-41a4-4778-8553-83f5eea68c5d' # fake request id FAKE_REQUEST_ID = fakes.FAKE_REQUEST_ID @@ -716,9 +717,6 @@ def check_server_actions(cls, body): assert body[action] is None elif action in ['addSecurityGroup', 'removeSecurityGroup']: assert list(body[action]) == ['name'] - elif action == 'createBackup': - assert set(body[action]) == set(['name', 'backup_type', - 'rotation']) elif action == 'trigger_crash_dump': assert body[action] is None else: @@ -766,11 +764,22 @@ def post_servers_1234_action(self, body, **kw): _body = {'adminPass': 'RescuePassword'} elif action == 'createImage': assert set(body[action].keys()) == set(['name', 'metadata']) - _headers = dict(location="http://blah/images/%s" % - FAKE_IMAGE_UUID_SNAPSHOT) + if self.api_version < api_versions.APIVersion('2.45'): + _headers = dict(location="http://blah/images/%s" % + FAKE_IMAGE_UUID_SNAPSHOT) + else: + _body = {'image_id': FAKE_IMAGE_UUID_SNAPSHOT} if body[action]['name'] == 'mysnapshot_deleted': _headers = dict(location="http://blah/images/%s" % FAKE_IMAGE_UUID_SNAP_DEL) + elif action == 'createBackup': + assert set(body[action].keys()) == set(['name', 'backup_type', + 'rotation']) + if self.api_version < api_versions.APIVersion('2.45'): + _headers = dict(location="http://blah/images/%s" % + FAKE_IMAGE_UUID_BACKUP) + else: + _body = {'image_id': FAKE_IMAGE_UUID_BACKUP} elif action == 'os-getConsoleOutput': assert list(body[action]) == ['length'] return (202, {}, {'output': 'foo'}) @@ -1039,7 +1048,17 @@ def get_images(self, **kw): "status": "SAVING", "progress": 80, "links": {}, - } + }, + { + "id": FAKE_IMAGE_UUID_BACKUP, + "name": "back1", + "serverId": '1234', + "updated": "2010-10-10T12:00:00Z", + "created": "2010-08-10T12:00:00Z", + "status": "SAVING", + "progress": 80, + "links": {}, + }, ]}) def get_images_555cae93_fb41_4145_9c52_f5b923538a26(self, **kw): @@ -1054,6 +1073,9 @@ def get_images_c99d7632_bd66_4be9_aed5_3dd14b223a76(self, **kw): def get_images_f27f479a_ddda_419a_9bbc_d6b56b210161(self, **kw): return (200, {}, {'image': self.get_images()[2]['images'][3]}) + def get_images_2f87e889_41a4_4778_8553_83f5eea68c5d(self, **kw): + return (200, {}, {'image': self.get_images()[2]['images'][4]}) + def get_images_3e861307_73a6_4d1f_8d68_f68b03223032(self): raise exceptions.NotFound('404') diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index a68adeed5..348091d06 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -1409,3 +1409,51 @@ def test_add_floating_ip_to_fixed(self): def test_remove_floating_ip(self): # self.floating_ips.list() is not available after 2.35 pass + + +class ServersCreateImageBackupV2_45Test(utils.FixturedTestCase): + """Tests the 2.45 microversion for createImage and createBackup + server actions. + """ + + client_fixture_class = client.V1 + data_fixture_class = data.V1 + api_version = '2.45' + + def setUp(self): + super(ServersCreateImageBackupV2_45Test, self).setUp() + self.cs.api_version = api_versions.APIVersion(self.api_version) + + def test_create_image(self): + """Tests the createImage API with the 2.45 microversion which + does not return the Location header, it returns a json dict in the + response body with an image_id key. + """ + s = self.cs.servers.get(1234) + im = s.create_image('123') + self.assertEqual('456', im) + self.assert_request_id(im, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + im = s.create_image('123', {}) + self.assert_request_id(im, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + im = self.cs.servers.create_image(s, '123') + self.assert_request_id(im, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + im = self.cs.servers.create_image(s, '123', {}) + self.assert_request_id(im, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + + def test_backup(self): + s = self.cs.servers.get(1234) + # Test backup on the Server object. + sb = s.backup('back1', 'daily', 1) + self.assertIn('image_id', sb) + self.assertEqual('456', sb['image_id']) + self.assert_request_id(sb, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + # Test backup on the ServerManager. + sb = self.cs.servers.backup(s, 'back1', 'daily', 2) + self.assertEqual('456', sb['image_id']) + self.assert_request_id(sb, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index c9346e504..e1dec382a 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -1139,6 +1139,18 @@ def test_create_image(self): {'createImage': {'name': 'mysnapshot', 'metadata': {}}}, ) + def test_create_image_2_45(self): + """Tests the image-create command with microversion 2.45 which + does not change the output of the command, just how the response + from the server is processed. + """ + self.run_command('image-create sample-server mysnapshot', + api_version='2.45') + self.assert_called( + 'POST', '/servers/1234/action', + {'createImage': {'name': 'mysnapshot', 'metadata': {}}}, + ) + def test_create_image_with_incorrect_metadata(self): cmd = 'image-create sample-server mysnapshot --metadata foo' result = self.assertRaises(argparse.ArgumentTypeError, @@ -2518,7 +2530,9 @@ def test_remove_fixed_ip(self): {'removeFixedIp': {'address': '10.0.0.10'}}) def test_backup(self): - self.run_command('backup sample-server back1 daily 1') + out, err = self.run_command('backup sample-server back1 daily 1') + # With microversion < 2.45 there is no output from this command. + self.assertEqual(0, len(out)) self.assert_called('POST', '/servers/1234/action', {'createBackup': {'name': 'back1', 'backup_type': 'daily', @@ -2529,6 +2543,23 @@ def test_backup(self): 'backup_type': 'daily', 'rotation': '1'}}) + def test_backup_2_45(self): + """Tests the backup command with the 2.45 microversion which + handles a different response and prints out the backup snapshot + image details. + """ + out, err = self.run_command( + 'backup sample-server back1 daily 1', + api_version='2.45') + # We should see the backup snapshot image name in the output. + self.assertIn('back1', out) + self.assertIn('SAVING', out) + self.assert_called_anytime( + 'POST', '/servers/1234/action', + {'createBackup': {'name': 'back1', + 'backup_type': 'daily', + 'rotation': '1'}}) + def test_limits(self): self.run_command('limits') self.assert_called('GET', '/limits') @@ -2914,6 +2945,7 @@ def test_versions(self): 42, # There are no version-wrapped shell method changes for this. 43, # There are no version-wrapped shell method changes for this. 44, # There are no version-wrapped shell method changes for this. + 45, # There are no version-wrapped shell method changes for this. ]) versions_supported = set(range(0, novaclient.API_MAX_VERSION.ver_minor + 1)) diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index cd328f9c0..a036bee5c 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -1630,8 +1630,13 @@ def create_image(self, server, image_name, metadata=None): body = {'name': image_name, 'metadata': metadata or {}} resp, body = self._action_return_resp_and_body('createImage', server, body) - location = resp.headers['location'] - image_uuid = location.split('/')[-1] + # The 2.45 microversion returns the image_id in the response body, + # not as a location header. + if self.api_version >= api_versions.APIVersion('2.45'): + image_uuid = body['image_id'] + else: + location = resp.headers['location'] + image_uuid = location.split('/')[-1] return base.StrWithMeta(image_uuid, resp) def backup(self, server, backup_name, backup_type, rotation): @@ -1643,7 +1648,8 @@ def backup(self, server, backup_name, backup_type, rotation): :param backup_type: The backup type, like 'daily' or 'weekly' :param rotation: Int parameter representing how many backups to keep around. - :returns: An instance of novaclient.base.TupleWithMeta + :returns: An instance of novaclient.base.TupleWithMeta if the request + microversion is < 2.45, otherwise novaclient.base.DictWithMeta. """ body = {'name': backup_name, 'backup_type': backup_type, diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 4df02f88a..20a28b9ab 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -2040,9 +2040,13 @@ def do_image_create(cs, args): 'around.')) def do_backup(cs, args): """Backup a server by creating a 'backup' type snapshot.""" - _find_server(cs, args.server).backup(args.name, - args.backup_type, - args.rotation) + result = _find_server(cs, args.server).backup(args.name, + args.backup_type, + args.rotation) + # Microversion >= 2.45 will return a DictWithMeta that has the image_id + # in it for the backup snapshot image. + if cs.api_version >= api_versions.APIVersion('2.45'): + _print_image(_find_image(cs, result['image_id'])) @utils.arg( diff --git a/releasenotes/notes/microversion-v2_45-1bfcae3914280534.yaml b/releasenotes/notes/microversion-v2_45-1bfcae3914280534.yaml new file mode 100644 index 000000000..1c278f3e1 --- /dev/null +++ b/releasenotes/notes/microversion-v2_45-1bfcae3914280534.yaml @@ -0,0 +1,13 @@ +--- +features: + - | + Support was added for microversion 2.45. This changes how the + ``createImage`` and ``createBackup`` server action APIs return + the created snapshot image ID in the response. With microversion + 2.45 and later, the image ID is return in a json dict response body + with an ``image_id`` key and uuid value. The old ``Location`` response + header is no longer returned in microversion 2.45 or later. + + There are no changes to the ``nova image-create`` CLI. However, the + ``nova backup`` CLI will print out the backup snapshot image information + with microversion 2.45 or greater now. From d5ecc50063597f0bfaeb2133ba316193f024988a Mon Sep 17 00:00:00 2001 From: jichenjc Date: Wed, 29 Mar 2017 06:44:50 +0800 Subject: [PATCH 1289/1705] Remove 1.1 extension comment According to recent nova changes, extension is deprecated and 1.1 is also deprecated, so to avoid confusion, remove those comments in the code Change-Id: Idc78af2151a0fff159c9b32595ed72a04808b7ee --- novaclient/v2/availability_zones.py | 2 +- novaclient/v2/hosts.py | 2 +- novaclient/v2/hypervisors.py | 2 +- novaclient/v2/keypairs.py | 2 +- novaclient/v2/virtual_interfaces.py | 2 +- novaclient/v2/volumes.py | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/novaclient/v2/availability_zones.py b/novaclient/v2/availability_zones.py index 41ee1d1e8..5a6639242 100644 --- a/novaclient/v2/availability_zones.py +++ b/novaclient/v2/availability_zones.py @@ -15,7 +15,7 @@ # under the License. """ -Availability Zone interface (1.1 extension). +Availability Zone interface """ from novaclient import base diff --git a/novaclient/v2/hosts.py b/novaclient/v2/hosts.py index 713c866f3..be7ca58e4 100644 --- a/novaclient/v2/hosts.py +++ b/novaclient/v2/hosts.py @@ -14,7 +14,7 @@ # under the License. """ -DEPRECATED host interface (1.1 extension). +DEPRECATED host interface """ import warnings diff --git a/novaclient/v2/hypervisors.py b/novaclient/v2/hypervisors.py index 1910a582a..761bf1c9f 100644 --- a/novaclient/v2/hypervisors.py +++ b/novaclient/v2/hypervisors.py @@ -14,7 +14,7 @@ # under the License. """ -Hypervisors interface (1.1 extension). +Hypervisors interface """ from six.moves.urllib import parse diff --git a/novaclient/v2/keypairs.py b/novaclient/v2/keypairs.py index 9ac05eda2..021cc6273 100644 --- a/novaclient/v2/keypairs.py +++ b/novaclient/v2/keypairs.py @@ -14,7 +14,7 @@ # under the License. """ -Keypair interface (1.1 extension). +Keypair interface """ from novaclient import api_versions diff --git a/novaclient/v2/virtual_interfaces.py b/novaclient/v2/virtual_interfaces.py index 88cd001f8..caf2e3a0e 100644 --- a/novaclient/v2/virtual_interfaces.py +++ b/novaclient/v2/virtual_interfaces.py @@ -14,7 +14,7 @@ # under the License. """ -DEPRECATED Virtual Interfaces (1.1 extension). +DEPRECATED Virtual Interfaces """ import warnings diff --git a/novaclient/v2/volumes.py b/novaclient/v2/volumes.py index f95b59f34..33e1b651b 100644 --- a/novaclient/v2/volumes.py +++ b/novaclient/v2/volumes.py @@ -14,7 +14,7 @@ # under the License. """ -Volume interface (1.1 extension). +Volume interface """ from novaclient import base From 8cc95aa5d5ed4120124b06839d637e674c24c927 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Mon, 1 May 2017 13:40:46 +0000 Subject: [PATCH 1290/1705] Updated from global requirements Change-Id: I0b09a55c3d4d68e274a5d600420f3eca4f8676c4 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 7d4b1391d..499c9021c 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -14,7 +14,7 @@ python-glanceclient>=2.5.0 # Apache-2.0 python-neutronclient>=5.1.0 # Apache-2.0 requests-mock>=1.1 # Apache-2.0 sphinx>=1.5.1 # BSD -os-client-config>=1.22.0 # Apache-2.0 +os-client-config>=1.27.0 # Apache-2.0 oslosphinx>=4.7.0 # Apache-2.0 osprofiler>=1.4.0 # Apache-2.0 testrepository>=0.0.18 # Apache-2.0/BSD From 3e9a98b8387395093efff151151c394eb90a0f27 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Wed, 3 May 2017 12:23:06 +0000 Subject: [PATCH 1291/1705] Updated from global requirements Change-Id: Ie7b5506c4dcdf380bc1eecac76533a58590937b9 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 6a18a4644..d8c97d270 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. pbr!=2.1.0,>=2.0.0 # Apache-2.0 -keystoneauth1>=2.18.0 # Apache-2.0 +keystoneauth1>=2.20.0 # Apache-2.0 iso8601>=0.1.11 # MIT oslo.i18n>=2.1.0 # Apache-2.0 oslo.serialization>=1.10.0 # Apache-2.0 From aa7f35d76b5768ec3fe4d45ad010d53adff7cccd Mon Sep 17 00:00:00 2001 From: chenaidong1 Date: Thu, 4 May 2017 11:37:58 +0800 Subject: [PATCH 1292/1705] Fix a typo Change-Id: Ia1df99568b3cd671ec71f80bce6bd4252cccec01 --- releasenotes/notes/microversion-v2_32-7947430cc2415597.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/releasenotes/notes/microversion-v2_32-7947430cc2415597.yaml b/releasenotes/notes/microversion-v2_32-7947430cc2415597.yaml index 69d6f5e1f..e6352e015 100644 --- a/releasenotes/notes/microversion-v2_32-7947430cc2415597.yaml +++ b/releasenotes/notes/microversion-v2_32-7947430cc2415597.yaml @@ -1,7 +1,7 @@ --- features: - | - The 2.32 microverison adds support for virtual device + The 2.32 microversion adds support for virtual device role tagging. Device role tagging is an answer to the question 'Which device is which?' from inside the guest. When booting an instance, an optional arbitrary 'tag' From 0d92534824105994da151e789df03f4c8f7740f7 Mon Sep 17 00:00:00 2001 From: "Kevin L. Mitchell" Date: Fri, 12 May 2017 09:04:38 -0500 Subject: [PATCH 1293/1705] Mark cloudpipe deprecated in novaclient Functional tests have been failing since a chance went into nova that removed the cloudpipe functionality. This change removes the sole functional test that depended on that API, and ensures that all the cloudpipe interfaces are marked deprecated and emit proper warnings. Change-Id: I329ee0e5fcf068ea7e54b99fbaf94a524647f660 --- .../v2/legacy/test_readonly_nova.py | 3 -- novaclient/tests/unit/v2/test_cloudpipe.py | 13 ++++++-- novaclient/v2/cloudpipe.py | 33 ++++++++++++++++--- novaclient/v2/shell.py | 21 ++++++++++-- .../deprecate-cloudpipe-670202797fdf97b6.yaml | 8 +++++ 5 files changed, 64 insertions(+), 14 deletions(-) create mode 100644 releasenotes/notes/deprecate-cloudpipe-670202797fdf97b6.yaml diff --git a/novaclient/tests/functional/v2/legacy/test_readonly_nova.py b/novaclient/tests/functional/v2/legacy/test_readonly_nova.py index cdf04a635..6b783a408 100644 --- a/novaclient/tests/functional/v2/legacy/test_readonly_nova.py +++ b/novaclient/tests/functional/v2/legacy/test_readonly_nova.py @@ -38,9 +38,6 @@ def test_admin_aggregate_list(self): def test_admin_availability_zone_list(self): self.assertIn("internal", self.nova('availability-zone-list')) - def test_admin_cloudpipe_list(self): - self.nova('cloudpipe-list') - def test_admin_flavor_acces_list(self): self.assertRaises(exceptions.CommandFailed, self.nova, diff --git a/novaclient/tests/unit/v2/test_cloudpipe.py b/novaclient/tests/unit/v2/test_cloudpipe.py index 10a44d7c0..2e48d848e 100644 --- a/novaclient/tests/unit/v2/test_cloudpipe.py +++ b/novaclient/tests/unit/v2/test_cloudpipe.py @@ -11,6 +11,7 @@ # License for the specific language governing permissions and limitations # under the License. +import mock import six from novaclient.tests.unit.fixture_data import client @@ -27,24 +28,30 @@ class CloudpipeTest(utils.FixturedTestCase): scenarios = [('original', {'client_fixture_class': client.V1}), ('session', {'client_fixture_class': client.SessionV1})] - def test_list_cloudpipes(self): + @mock.patch('warnings.warn') + def test_list_cloudpipes(self, mock_warn): cp = self.cs.cloudpipe.list() self.assert_request_id(cp, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/os-cloudpipe') for c in cp: self.assertIsInstance(c, cloudpipe.Cloudpipe) + mock_warn.assert_called_once_with(mock.ANY) - def test_create(self): + @mock.patch('warnings.warn') + def test_create(self, mock_warn): project = "test" cp = self.cs.cloudpipe.create(project) self.assert_request_id(cp, fakes.FAKE_REQUEST_ID_LIST) body = {'cloudpipe': {'project_id': project}} self.assert_called('POST', '/os-cloudpipe', body) self.assertIsInstance(cp, six.string_types) + mock_warn.assert_called_once_with(mock.ANY) - def test_update(self): + @mock.patch('warnings.warn') + def test_update(self, mock_warn): cp = self.cs.cloudpipe.update("192.168.1.1", 2345) self.assert_request_id(cp, fakes.FAKE_REQUEST_ID_LIST) body = {'configure_project': {'vpn_ip': "192.168.1.1", 'vpn_port': 2345}} self.assert_called('PUT', '/os-cloudpipe/configure-project', body) + mock_warn.assert_called_once_with(mock.ANY) diff --git a/novaclient/v2/cloudpipe.py b/novaclient/v2/cloudpipe.py index cf8040b17..b9ac9f65a 100644 --- a/novaclient/v2/cloudpipe.py +++ b/novaclient/v2/cloudpipe.py @@ -13,9 +13,19 @@ # License for the specific language governing permissions and limitations # under the License. -"""Cloudpipe interface.""" +"""DEPRECATED Cloudpipe interface.""" + +import warnings from novaclient import base +from novaclient.i18n import _ + + +DEPRECATION_WARNING = ( + _('The os-cloudpipe Nova API has been removed. This API binding will be ' + 'removed in the first major release after the Nova server 16.0.0 Pike ' + 'release.') +) class Cloudpipe(base.Resource): @@ -26,31 +36,42 @@ def __repr__(self): def delete(self): """ - Delete the own cloudpipe instance + DEPRECATED Delete the own cloudpipe instance :returns: An instance of novaclient.base.TupleWithMeta """ + + warnings.warn(DEPRECATION_WARNING) + return self.manager.delete(self) class CloudpipeManager(base.ManagerWithFind): + """DEPRECATED""" + resource_class = Cloudpipe def create(self, project): - """Launch a cloudpipe instance. + """DEPRECATED Launch a cloudpipe instance. :param project: UUID of the project (tenant) for the cloudpipe """ + + warnings.warn(DEPRECATION_WARNING) + body = {'cloudpipe': {'project_id': project}} return self._create('/os-cloudpipe', body, 'instance_id', return_raw=True) def list(self): - """Get a list of cloudpipe instances.""" + """DEPRECATED Get a list of cloudpipe instances.""" + + warnings.warn(DEPRECATION_WARNING) + return self._list('/os-cloudpipe', 'cloudpipes') def update(self, address, port): - """Configure cloudpipe parameters for the project. + """DEPRECATED Configure cloudpipe parameters for the project. Update VPN address and port for all networks associated with the project defined by authentication @@ -60,6 +81,8 @@ def update(self, address, port): :returns: An instance of novaclient.base.TupleWithMeta """ + warnings.warn(DEPRECATION_WARNING) + body = {'configure_project': {'vpn_ip': address, 'vpn_port': port}} return self._update("/os-cloudpipe/configure-project", body) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 2c3fd34f8..c212262b3 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -54,6 +54,12 @@ 'in the first major release after the Nova server 16.0.0 Pike release.') ) +CLOUDPIPE_DEPRECATION_WARNING = ( + _('The os-cloudpipe Nova API has been removed. This command will be ' + 'removed in the first major release after the Nova server 16.0.0 Pike ' + 'release.') +) + # NOTE(mriedem): Remove this along with the deprecated commands in the first # major python-novaclient release AFTER the nova server 16.0.0 Pike release. @@ -856,7 +862,10 @@ def do_boot(cs, args): def do_cloudpipe_list(cs, _args): - """Print a list of all cloudpipe instances.""" + """DEPRECATED Print a list of all cloudpipe instances.""" + + print(CLOUDPIPE_DEPRECATION_WARNING, file=sys.stderr) + cloudpipes = cs.cloudpipe.list() columns = ['Project Id', "Public IP", "Public Port", "Internal IP"] utils.print_list(cloudpipes, columns) @@ -867,14 +876,20 @@ def do_cloudpipe_list(cs, _args): metavar='', help=_('UUID of the project to create the cloudpipe for.')) def do_cloudpipe_create(cs, args): - """Create a cloudpipe instance for the given project.""" + """DEPRECATED Create a cloudpipe instance for the given project.""" + + print(CLOUDPIPE_DEPRECATION_WARNING, file=sys.stderr) + cs.cloudpipe.create(args.project) @utils.arg('address', metavar='', help=_('New IP Address.')) @utils.arg('port', metavar='', help=_('New Port.')) def do_cloudpipe_configure(cs, args): - """Update the VPN IP/port of a cloudpipe instance.""" + """DEPRECATED Update the VPN IP/port of a cloudpipe instance.""" + + print(CLOUDPIPE_DEPRECATION_WARNING, file=sys.stderr) + cs.cloudpipe.update(args.address, args.port) diff --git a/releasenotes/notes/deprecate-cloudpipe-670202797fdf97b6.yaml b/releasenotes/notes/deprecate-cloudpipe-670202797fdf97b6.yaml new file mode 100644 index 000000000..1c51ab297 --- /dev/null +++ b/releasenotes/notes/deprecate-cloudpipe-670202797fdf97b6.yaml @@ -0,0 +1,8 @@ +--- +deprecations: + - | + The os-cloudpipe API has been removed from Nova. As a result, the + ``nova cloudpipe-list``, ``nova cloudpipe-create``, and ``nova + cloudpipe-configure`` commands and the ``novaclient.v2.cloudpipe`` + API bindings are now deprecated, and will be removed in the first + major release after the Nova server 16.0.0 Pike release. From e5f1f7f8e7f24de8ddecbedc01642c993488d404 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Fri, 12 May 2017 14:16:59 -0400 Subject: [PATCH 1294/1705] Fix warning for deprecated cert commands If3e1e40947a8ad3f665f6a96d46de8cef6a2a190 deprecated the cert CLIs but used the wrong warning mechanism. For the python API bindings we use the warnings module, but for the CLI we print to stderr. Change-Id: I4dfd9e9dddbc0c7eb8c7115d2241955a630749e6 --- novaclient/v2/shell.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 2c3fd34f8..7a93d3b70 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -27,7 +27,6 @@ import pprint import sys import time -import warnings from oslo_utils import netutils from oslo_utils import strutils @@ -2963,7 +2962,7 @@ def simplify_usage(u): help=_('Filename for the X.509 certificate. [Default: cert.pem]')) def do_x509_create_cert(cs, args): """DEPRECATED Create x509 cert for a user in tenant.""" - warnings.warn(CERT_DEPRECATION_WARNING, DeprecationWarning) + print(CERT_DEPRECATION_WARNING, file=sys.stderr) if os.path.exists(args.pk_filename): raise exceptions.CommandError(_("Unable to write privatekey - %s " @@ -2995,7 +2994,7 @@ def do_x509_create_cert(cs, args): help=_('Filename to write the x509 root cert.')) def do_x509_get_root_cert(cs, args): """DEPRECATED Fetch the x509 root cert.""" - warnings.warn(CERT_DEPRECATION_WARNING, DeprecationWarning) + print(CERT_DEPRECATION_WARNING, file=sys.stderr) if os.path.exists(args.filename): raise exceptions.CommandError(_("Unable to write x509 root cert - \ %s exists.") % args.filename) From 428742b404964c71540db765a4fa1134d47e2598 Mon Sep 17 00:00:00 2001 From: john-griffith Date: Fri, 12 May 2017 11:37:07 -0400 Subject: [PATCH 1295/1705] Fix help message and description for volume-update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The help for the `volume-update` command says: update_server_volume(server_id, attachment_id, new_volume_id) Update the volume identified by the attachment ID, that is attached to the given server ID Parameters: server_id – The ID of the server attachment_id – The ID of the attachment new_volume_id – The ID of the new volume to attach Return type: Volume But in reality the command takes an original volume and a destination volume. The update help is also not very clear regarding what the command actually does and the fact that it's primarily a libvirt data migration from src volume to destination volume. This patch modifies the help to show the correct arguments and also adds some more info regarding what the command is doing. Change-Id: Ia267285f85854976545c2a72c3729c1bfcb2c991 Closes-Bug: #1690397 --- novaclient/tests/unit/v2/test_volumes.py | 4 ++-- novaclient/v2/shell.py | 23 ++++++++++++++--------- novaclient/v2/volumes.py | 20 +++++++++++++------- 3 files changed, 29 insertions(+), 18 deletions(-) diff --git a/novaclient/tests/unit/v2/test_volumes.py b/novaclient/tests/unit/v2/test_volumes.py index 17c4df5d0..eb0fe71fb 100644 --- a/novaclient/tests/unit/v2/test_volumes.py +++ b/novaclient/tests/unit/v2/test_volumes.py @@ -38,8 +38,8 @@ def test_update_server_volume(self): vol_id = '15e59938-07d5-11e1-90e3-e3dffe0c5983' v = self.cs.volumes.update_server_volume( server_id=1234, - attachment_id='Work', - new_volume_id=vol_id + src_volid='Work', + dest_volid=vol_id ) self.assert_request_id(v, fakes.FAKE_REQUEST_ID_LIST) self.cs.assert_called('PUT', diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index c212262b3..d5b79ec05 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -2289,18 +2289,23 @@ def do_volume_attach(cs, args): metavar='', help=_('Name or ID of server.')) @utils.arg( - 'attachment_id', - metavar='', - help=_('Attachment ID of the volume.')) + 'src_volume', + metavar='', + help=_('ID of the source (original) volume.')) @utils.arg( - 'new_volume', - metavar='', - help=_('ID of the volume to attach.')) + 'dest_volume', + metavar='', + help=_('ID of the destination volume.')) def do_volume_update(cs, args): - """Update volume attachment.""" + """Update the attachment on the server. + + Migrates the data from an attached volume to the + specified available volume and swaps out the active + attachment to the new volume. + """ cs.volumes.update_server_volume(_find_server(cs, args.server).id, - args.attachment_id, - args.new_volume) + args.src_volume, + args.dest_volume) @utils.arg( diff --git a/novaclient/v2/volumes.py b/novaclient/v2/volumes.py index 33e1b651b..b0f66f32b 100644 --- a/novaclient/v2/volumes.py +++ b/novaclient/v2/volumes.py @@ -52,19 +52,25 @@ def create_server_volume(self, server_id, volume_id, device=None): return self._create("/servers/%s/os-volume_attachments" % server_id, body, "volumeAttachment") - def update_server_volume(self, server_id, attachment_id, new_volume_id): + def update_server_volume(self, server_id, src_volid, dest_volid): """ - Update the volume identified by the attachment ID, that is attached to - the given server ID + Swaps the existing volume attachment to point to a new volume. + + Takes a server, a source (attached) volume and a destination volume and + performs a hypervisor assisted data migration from src to dest volume, + detaches the original (source) volume and attaches the new destination + volume. Note that not all backing hypervisor drivers support this + operation and it may be disabled via policy. + :param server_id: The ID of the server - :param attachment_id: The ID of the attachment - :param new_volume_id: The ID of the new volume to attach + :param source_volume: The ID of the src volume + :param dest_volume: The ID of the destination volume :rtype: :class:`Volume` """ - body = {'volumeAttachment': {'volumeId': new_volume_id}} + body = {'volumeAttachment': {'volumeId': dest_volid}} return self._update("/servers/%s/os-volume_attachments/%s" % - (server_id, attachment_id,), + (server_id, src_volid,), body, "volumeAttachment") def get_server_volume(self, server_id, attachment_id): From 04ee0e16791692f3d2d49f51aece906e75caff31 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Mon, 15 May 2017 00:54:39 +0000 Subject: [PATCH 1296/1705] Updated from global requirements Change-Id: Id9e673fecaa7a9b5165becf51b71bd81d26a2c18 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 499c9021c..1680242a6 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -4,7 +4,7 @@ hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0 bandit>=1.1.0 # Apache-2.0 -coverage>=4.0 # Apache-2.0 +coverage!=4.4,>=4.0 # Apache-2.0 fixtures>=3.0.0 # Apache-2.0/BSD keyring>=5.5.1 # MIT/PSF mock>=2.0 # BSD From 0191fa24607c32dcf8be29d31498977209174b5d Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Wed, 17 May 2017 03:58:17 +0000 Subject: [PATCH 1297/1705] Updated from global requirements Change-Id: Iada875aeb27820e93f08960ffd89d19df5f5b83d --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 1680242a6..58256c5d9 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -13,7 +13,7 @@ python-cinderclient>=2.0.1 # Apache-2.0 python-glanceclient>=2.5.0 # Apache-2.0 python-neutronclient>=5.1.0 # Apache-2.0 requests-mock>=1.1 # Apache-2.0 -sphinx>=1.5.1 # BSD +sphinx!=1.6.1,>=1.5.1 # BSD os-client-config>=1.27.0 # Apache-2.0 oslosphinx>=4.7.0 # Apache-2.0 osprofiler>=1.4.0 # Apache-2.0 From 3a762401245de8c2941ea41d6168c17ddd30aacd Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 17 May 2017 11:57:16 +0100 Subject: [PATCH 1298/1705] Remove various deprecated options These are all deprecated with at least one release and can be removed now. Change-Id: I5d11eda2a6b35de98f0484492f597a87df882013 Related-Bug: 1570593 --- novaclient/shell.py | 9 --------- novaclient/tests/unit/v2/test_shell.py | 9 +-------- novaclient/v2/shell.py | 16 ---------------- ...recated-option-in-9.0.0-bc76629d28f1d4c4.yaml | 8 ++++++++ 4 files changed, 9 insertions(+), 33 deletions(-) create mode 100644 releasenotes/notes/remove-deprecated-option-in-9.0.0-bc76629d28f1d4c4.yaml diff --git a/novaclient/shell.py b/novaclient/shell.py index 68f74d23e..b23a48fe7 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -465,15 +465,6 @@ def get_base_parser(self, argv): default=utils.env('NOVA_SERVICE_NAME'), help=_('Defaults to env[NOVA_SERVICE_NAME].')) - parser.add_argument( - '--volume-service-name', - action=DeprecatedAction, - metavar='', - default=utils.env('NOVA_VOLUME_SERVICE_NAME'), - use=_('This option will be removed after Nova 15.0.0 is ' - 'released.'), - help=argparse.SUPPRESS) - parser.add_argument( '--os-endpoint-type', metavar='', diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index e1dec382a..f5b469abd 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -1098,12 +1098,6 @@ def test_flavor_access_list_flavor(self): self.run_command('flavor-access-list --flavor 2') self.assert_called('GET', '/flavors/2/os-flavor-access') - def test_flavor_access_list_bad_filter(self): - cmd = 'flavor-access-list --flavor 2 --tenant proj2' - _out, err = self.run_command(cmd) - # assert the deprecation warning for using --tenant - self.assertIn('WARNING: Option "--tenant" is deprecated', err) - def test_flavor_access_list_no_filter(self): cmd = 'flavor-access-list' self.assertRaises(exceptions.CommandError, self.run_command, cmd) @@ -2719,8 +2713,7 @@ def test_migration_list_v223(self): self.assert_called('GET', '/os-migrations') def test_migration_list_with_filters(self): - self.run_command('migration-list --host host1 --cell_name child1 ' - '--status finished') + self.run_command('migration-list --host host1 --status finished') self.assert_called('GET', '/os-migrations?host=host1&status=finished') diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 9b11cd3e5..07b4724cc 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -1175,13 +1175,6 @@ def do_flavor_key(cs, args): '--flavor', metavar='', help=_("Filter results by flavor name or ID.")) -@utils.arg( - '--tenant', metavar='', - help=_('Filter results by tenant ID.'), - action=shell.DeprecatedAction, - real_action='nothing', - use=_('this option is not supported, and will be ' - 'removed in version 5.0.0.')) def do_flavor_access_list(cs, args): """Print access information about the given flavor.""" if args.flavor: @@ -4926,15 +4919,6 @@ def migration_type(migration): dest='status', metavar='', help=_('Fetch migrations for the given status.')) -@utils.arg( - '--cell_name', - dest='cell_name', - metavar='', - help=_('Fetch migrations for the given cell_name.'), - action=shell.DeprecatedAction, - real_action='nothing', - use=_('this option is not supported, and will be ' - 'removed after version 8.0.0.')) def do_migration_list(cs, args): """Print a list of migrations.""" migrations = cs.migrations.list(args.host, args.status, None, diff --git a/releasenotes/notes/remove-deprecated-option-in-9.0.0-bc76629d28f1d4c4.yaml b/releasenotes/notes/remove-deprecated-option-in-9.0.0-bc76629d28f1d4c4.yaml new file mode 100644 index 000000000..89d5afabc --- /dev/null +++ b/releasenotes/notes/remove-deprecated-option-in-9.0.0-bc76629d28f1d4c4.yaml @@ -0,0 +1,8 @@ +--- +upgrade: + - | + The following deprecated options have been removed: + + - ``--tenant`` (from ``flavor access list``) + - ``--cell_name`` (from ``migration list``) + - ``--volume-service-name`` (global option) From 8f6c2169310f83753f15cc693e5900ea76a872df Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Tue, 23 May 2017 11:59:38 +0000 Subject: [PATCH 1299/1705] Updated from global requirements Change-Id: I7d0b505582217c89588f6f1d9c606ff68e1f3e3f --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d8c97d270..3dc8ecfde 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ pbr!=2.1.0,>=2.0.0 # Apache-2.0 keystoneauth1>=2.20.0 # Apache-2.0 iso8601>=0.1.11 # MIT -oslo.i18n>=2.1.0 # Apache-2.0 +oslo.i18n!=3.15.2,>=2.1.0 # Apache-2.0 oslo.serialization>=1.10.0 # Apache-2.0 oslo.utils>=3.20.0 # Apache-2.0 PrettyTable<0.8,>=0.7.1 # BSD From d25502cbe2749ed7042e427fe8d4a3678acf4550 Mon Sep 17 00:00:00 2001 From: Sean Dague Date: Thu, 25 May 2017 14:30:59 -0400 Subject: [PATCH 1300/1705] Have python-novaclient support global_request_id As part of the Boston Summit, support the global_request_id effort by making python-novaclient support passing global_request_id during construction. part of bp:oslo-middleware-request-id oslo spec I65de8261746b25d45e105394f4eeb95b9cb3bd42 Change-Id: Ife29b1856c0c278eab1708c3971fef14b9d77e2e --- novaclient/client.py | 6 +++++- novaclient/tests/unit/test_client.py | 11 +++++++++++ .../notes/global_request_id-26f4e4301f84d403.yaml | 6 ++++++ 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/global_request_id-26f4e4301f84d403.yaml diff --git a/novaclient/client.py b/novaclient/client.py index be2fad874..f65f194f7 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -40,7 +40,7 @@ from novaclient.i18n import _ from novaclient import utils - +REQ_ID_HEADER = 'X-OpenStack-Request-ID' # TODO(jichenjc): when an extension in contrib is moved to core extension, # Add the name into the following list, then after last patch merged, # remove the whole function @@ -57,12 +57,16 @@ def __init__(self, *args, **kwargs): self.timings = kwargs.pop('timings', False) self.api_version = kwargs.pop('api_version', None) self.api_version = self.api_version or api_versions.APIVersion() + self.global_request_id = kwargs.pop('global_request_id', None) super(SessionClient, self).__init__(*args, **kwargs) def request(self, url, method, **kwargs): kwargs.setdefault('headers', kwargs.get('headers', {})) api_versions.update_headers(kwargs["headers"], self.api_version) + if self.global_request_id is not None: + kwargs['headers'].setdefault(REQ_ID_HEADER, self.global_request_id) + # NOTE(dbelova): osprofiler_web.get_trace_id_headers does not add any # headers in case if osprofiler is not initialized. if osprofiler_web: diff --git a/novaclient/tests/unit/test_client.py b/novaclient/tests/unit/test_client.py index 5f7272803..f5f677608 100644 --- a/novaclient/tests/unit/test_client.py +++ b/novaclient/tests/unit/test_client.py @@ -14,6 +14,7 @@ # under the License. import copy +import uuid from keystoneauth1 import session import mock @@ -70,6 +71,16 @@ def test_client_get_reset_timings_v2(self): cs.reset_timings() self.assertEqual(0, len(cs.get_timings())) + def test_global_id(self): + global_id = "req-%s" % uuid.uuid4() + self.requests_mock.get('http://no.where') + + client = novaclient.client.SessionClient(session=session.Session(), + global_request_id=global_id) + client.request("http://no.where", 'GET') + headers = self.requests_mock.last_request.headers + self.assertEqual(headers['X-OpenStack-Request-ID'], global_id) + class ClientsUtilsTest(utils.TestCase): diff --git a/releasenotes/notes/global_request_id-26f4e4301f84d403.yaml b/releasenotes/notes/global_request_id-26f4e4301f84d403.yaml new file mode 100644 index 000000000..e15358f77 --- /dev/null +++ b/releasenotes/notes/global_request_id-26f4e4301f84d403.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + A new ``global_request_id`` parameter is accepted on the client + constructor, which will then pass ``X-OpenStack-Request-ID`` on + all requests made. From e55b578dd015ed61b03a7bb1a8c2774431513001 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Tue, 30 May 2017 13:43:19 +0000 Subject: [PATCH 1301/1705] Updated from global requirements Change-Id: I6dbafc7c7117c6bbd6f98c47b53c0cbbb406fd85 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 58256c5d9..3b2a540f0 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -9,7 +9,7 @@ fixtures>=3.0.0 # Apache-2.0/BSD keyring>=5.5.1 # MIT/PSF mock>=2.0 # BSD python-keystoneclient>=3.8.0 # Apache-2.0 -python-cinderclient>=2.0.1 # Apache-2.0 +python-cinderclient>=2.1.0 # Apache-2.0 python-glanceclient>=2.5.0 # Apache-2.0 python-neutronclient>=5.1.0 # Apache-2.0 requests-mock>=1.1 # Apache-2.0 From 2a2920e1314d6198f8bc6dd74912891a14cbfe73 Mon Sep 17 00:00:00 2001 From: Artom Lifshitz Date: Thu, 1 Jun 2017 16:13:48 -0400 Subject: [PATCH 1302/1705] Error out if nic auto or none are not alone Previously, the client would allow a boot command containing, for example, --nic auto,tag=foo, giving the impression that the auto nic would receive a tag. This is not the case. Instead of silently ignoring the tag argument in such a case, this patch makes the client error out with a warning message. Change-Id: Id7ef67435fa7a3f49c9abcb751f5fa9458388f13 Closes-bug: 1695088 --- novaclient/tests/unit/v2/test_shell.py | 24 ++++++++++++++++++++++++ novaclient/v2/shell.py | 19 +++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index f5b469abd..8ecd64158 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -640,6 +640,30 @@ def test_boot_hints(self): }, ) + def test_boot_nic_auto_not_alone_after(self): + cmd = ('boot --image %s --flavor 1 ' + '--nic auto,tag=foo some-server' % + FAKE_UUID_1) + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + def test_boot_nic_auto_not_alone_before(self): + cmd = ('boot --image %s --flavor 1 ' + '--nic tag=foo,auto some-server' % + FAKE_UUID_1) + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + def test_boot_nic_none_not_alone_before(self): + cmd = ('boot --image %s --flavor 1 ' + '--nic none,tag=foo some-server' % + FAKE_UUID_1) + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + def test_boot_nic_none_not_alone_after(self): + cmd = ('boot --image %s --flavor 1 ' + '--nic tag=foo,none some-server' % + FAKE_UUID_1) + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + def test_boot_nics(self): cmd = ('boot --image %s --flavor 1 ' '--nic net-id=a=c,v4-fixed-ip=10.0.0.1 some-server' % diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 07b4724cc..10285d6af 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -298,12 +298,30 @@ def _parse_nics(cs, args): auto_or_none = False nics = [] for nic_str in args.nics: + nic_info_set = False for kv_str in nic_str.split(","): + if auto_or_none: + # Since we start with auto_or_none being False, it being true + # means we've parsed an auto or none argument, then continued + # after the comma to another key=value pair. Since auto or none + # can only be given by themselves, raise. + raise exceptions.CommandError(_("'auto' or 'none' cannot be " + "used with any other nic " + "arguments")) try: # handle the special auto/none cases if kv_str in ('auto', 'none'): if not supports_auto_alloc: raise exceptions.CommandError(err_msg % nic_str) + if nic_info_set: + # Since we start with nic_info_set being False, it + # being true means we've parsed a key=value pair, then + # landed on a auto or none argument after the comma. + # Since auto or none can only be given by themselves, + # raise. + raise exceptions.CommandError( + _("'auto' or 'none' cannot be used with any " + "other nic arguments")) nics.append(kv_str) auto_or_none = True continue @@ -320,6 +338,7 @@ def _parse_nics(cs, args): if nic_info[k]: raise exceptions.CommandError(err_msg % nic_str) nic_info[k] = v + nic_info_set = True else: raise exceptions.CommandError(err_msg % nic_str) From 0b1d59e94860c106593d803d0cc2e07fb8b97140 Mon Sep 17 00:00:00 2001 From: Artom Lifshitz Date: Thu, 1 Jun 2017 15:50:49 -0400 Subject: [PATCH 1303/1705] Better handle key=value pair not being key=value Previously, --block-device and --ephemeral would error out with an insctrutable (to the user) error message if one of the key=value pair it expects wasn't actually key=value, for instance: nova --block-device 995abd81-2626-413c-9af6-51f105fab214,tag=foo \ --image CirrOS --flavor custom my_instance ValueError: dictionary update sequence element #0 has length 1; 2 is required This patch makes the error message clearer. Change-Id: I2dd5dd805df2b0382b015dc625205119d7d0b03b --- novaclient/tests/unit/v2/test_shell.py | 10 ++++++++++ novaclient/v2/shell.py | 16 ++++++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index f5b469abd..96d273d95 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -289,6 +289,16 @@ def test_boot_no_flavor(self): cmd = 'boot --image %s some-server' % FAKE_UUID_1 self.assertRaises(exceptions.CommandError, self.run_command, cmd) + def test_boot_not_key_value_bdm(self): + cmd = ('boot --flavor 1 --image %s --block-device %s,tag=foo ' + 'test-server' % (FAKE_UUID_1, FAKE_UUID_2)) + self.assertRaises(argparse.ArgumentTypeError, self.run_command, cmd) + + def test_boot_not_key_value_ephemeral(self): + cmd = ('boot --flavor 1 --image %s --ephemeral %s,tag=foo ' + 'test-server' % (FAKE_UUID_1, FAKE_UUID_2)) + self.assertRaises(argparse.ArgumentTypeError, self.run_command, cmd) + def test_boot_no_image_bdms(self): self.run_command( 'boot --flavor 1 --block-device-mapping vda=blah:::0 some-server' diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 07b4724cc..f0a4fc2d2 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -150,6 +150,18 @@ def _supports_block_device_tags(cs): return False +def _parse_device_spec(device_spec): + spec_dict = {} + for arg in device_spec.split(','): + if '=' in arg: + spec_dict.update([arg.split('=')]) + else: + raise argparse.ArgumentTypeError( + _("Expected a comma-separated list of key=value pairs. '%s' " + "is not a key=value pair.") % arg) + return spec_dict + + def _parse_block_device_mapping_v2(cs, args, image): bdm = [] @@ -166,7 +178,7 @@ def _parse_block_device_mapping_v2(cs, args, image): bdm.append(bdm_dict) for device_spec in args.block_device: - spec_dict = dict(v.split('=') for v in device_spec.split(',')) + spec_dict = _parse_device_spec(device_spec) bdm_dict = {} if ('tag' in spec_dict and not _supports_block_device_tags(cs)): @@ -223,7 +235,7 @@ def _parse_block_device_mapping_v2(cs, args, image): bdm_dict = {'source_type': 'blank', 'destination_type': 'local', 'boot_index': -1, 'delete_on_termination': True} try: - eph_dict = dict(v.split('=') for v in ephemeral_spec.split(',')) + eph_dict = _parse_device_spec(ephemeral_spec) except ValueError: err_msg = (_("Invalid ephemeral argument '%s'.") % args.ephemeral) raise argparse.ArgumentTypeError(err_msg) From 9a272014167390217351dd5a60f50f518f4ae600 Mon Sep 17 00:00:00 2001 From: nidhimittalhada Date: Tue, 9 May 2017 15:20:49 +0530 Subject: [PATCH 1304/1705] client.logger.warning wrongly used in migrations client.logger.warning if reached, will give this error "No handlers could be found for logger "novaclient.v2.client"". Fixed by using warnings.warn() and corrected root cause by adding appropriate handlers. Change-Id: I60c8a023cff92f8b6f37a4a14b6193c3efaa19a8 Closes-Bug: #1688507 --- novaclient/tests/unit/v2/test_migrations.py | 11 +++++++++++ novaclient/v2/client.py | 6 +++++- novaclient/v2/migrations.py | 8 +++++--- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/novaclient/tests/unit/v2/test_migrations.py b/novaclient/tests/unit/v2/test_migrations.py index e69d3327e..408909cda 100644 --- a/novaclient/tests/unit/v2/test_migrations.py +++ b/novaclient/tests/unit/v2/test_migrations.py @@ -10,6 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. +import mock + from novaclient import api_versions from novaclient.tests.unit import utils from novaclient.tests.unit.v2 import fakes @@ -38,6 +40,15 @@ def test_list_migrations_v223(self): self.assertIsInstance(m, migrations.Migration) self.assertEqual(m.migration_type, 'live-migration') + @mock.patch('novaclient.v2.migrations.warnings.warn') + def test_list_migrations_with_cell_name(self, mock_warn): + ml = self.cs.migrations.list(cell_name="abc") + self.assert_request_id(ml, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called('GET', '/os-migrations?cell_name=abc') + for m in ml: + self.assertIsInstance(m, migrations.Migration) + self.assertTrue(mock_warn.called) + def test_list_migrations_with_filters(self): ml = self.cs.migrations.list('host1', 'finished', 'child1') self.assert_request_id(ml, fakes.FAKE_REQUEST_ID_LIST) diff --git a/novaclient/v2/client.py b/novaclient/v2/client.py index b85077392..73280b59b 100644 --- a/novaclient/v2/client.py +++ b/novaclient/v2/client.py @@ -183,7 +183,11 @@ def __init__(self, self.server_external_events = \ server_external_events.ServerExternalEventManager(self) - self.logger = logger or logging.getLogger(__name__) + if not logger: + logger = logging.getLogger(__name__) + if not logger.handlers: + logger.addHandler(logging.StreamHandler()) + self.logger = logger # Add in any extensions... if extensions: diff --git a/novaclient/v2/migrations.py b/novaclient/v2/migrations.py index 5a39ed992..72e0deda9 100644 --- a/novaclient/v2/migrations.py +++ b/novaclient/v2/migrations.py @@ -19,6 +19,8 @@ from novaclient import base from novaclient.i18n import _ +import warnings + class Migration(base.Resource): def __repr__(self): @@ -41,9 +43,9 @@ def list(self, host=None, status=None, cell_name=None, instance_uuid=None): if status: opts['status'] = status if cell_name: - self.client.logger.warning(_("Argument 'cell_name' is " - "deprecated since Pike, and will " - "be removed in a future release.")) + warnings.warn(_("Argument 'cell_name' is " + "deprecated since Pike, and will " + "be removed in a future release.")) opts['cell_name'] = cell_name if instance_uuid: opts['instance_uuid'] = instance_uuid From d439ec8c5ceb559dfc6f8aaaa8845d8d923f77ac Mon Sep 17 00:00:00 2001 From: nidhimittalhada Date: Thu, 1 Jun 2017 11:27:48 +0530 Subject: [PATCH 1305/1705] Help message for aggregate-update is ambiguous Help message for nova aggregate-update needs to be corrected, to be more clear, by specifying 'name' is the new name desired in the update. Corrected it. Change-Id: I8e616a257c4dd90d85ebac3059683f358a5512dd Closes-Bug: #1560442 --- novaclient/v2/shell.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 07b4724cc..f404f3428 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -3114,12 +3114,12 @@ def do_aggregate_delete(cs, args): @utils.arg( '--name', dest='name', - help=_('Name of aggregate.')) + help=_('New name for aggregate.')) @utils.arg( '--availability-zone', metavar='', dest='availability_zone', - help=_('The availability zone of the aggregate.')) + help=_('New availability zone for aggregate.')) def do_aggregate_update(cs, args): """Update the aggregate's name and optionally availability zone.""" aggregate = _find_aggregate(cs, args.aggregate) From 2cb8d8578132238d54a0ee6442cc27194e3cca92 Mon Sep 17 00:00:00 2001 From: Chris Friesen Date: Fri, 2 Jun 2017 13:10:26 -0600 Subject: [PATCH 1306/1705] 2.46: match nova API version This just bumps API_MAX_VERSION to match nova. In this microversion nova just adds a new header to the response, no client changes are needed. Change-Id: I4c9a37c2a6f79388259032ba90c328d195051f94 --- novaclient/__init__.py | 2 +- novaclient/tests/unit/v2/test_shell.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/novaclient/__init__.py b/novaclient/__init__.py index cead09fa6..4a86d3653 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.45") +API_MAX_VERSION = api_versions.APIVersion("2.46") diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index f5b469abd..aa0ffa18b 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2939,6 +2939,7 @@ def test_versions(self): 43, # There are no version-wrapped shell method changes for this. 44, # There are no version-wrapped shell method changes for this. 45, # There are no version-wrapped shell method changes for this. + 46, # There are no version-wrapped shell method changes for this. ]) versions_supported = set(range(0, novaclient.API_MAX_VERSION.ver_minor + 1)) From e9969067181fc9878c659f3be3ad71742123294c Mon Sep 17 00:00:00 2001 From: Doug Hellmann Date: Mon, 5 Jun 2017 14:14:00 -0400 Subject: [PATCH 1307/1705] strip the remote prefixes from the release note branch specifiers Change-Id: I3f0403497a8a5e06f45f4b01df808c4306085915 Signed-off-by: Doug Hellmann --- releasenotes/source/liberty.rst | 2 +- releasenotes/source/mitaka.rst | 2 +- releasenotes/source/newton.rst | 2 +- releasenotes/source/ocata.rst | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/releasenotes/source/liberty.rst b/releasenotes/source/liberty.rst index 36217be84..68554fb81 100644 --- a/releasenotes/source/liberty.rst +++ b/releasenotes/source/liberty.rst @@ -3,4 +3,4 @@ ============================== .. release-notes:: - :branch: origin/stable/liberty + :branch: stable/liberty diff --git a/releasenotes/source/mitaka.rst b/releasenotes/source/mitaka.rst index e54560965..5fd7ffc4a 100644 --- a/releasenotes/source/mitaka.rst +++ b/releasenotes/source/mitaka.rst @@ -3,4 +3,4 @@ =================================== .. release-notes:: - :branch: origin/stable/mitaka + :branch: stable/mitaka diff --git a/releasenotes/source/newton.rst b/releasenotes/source/newton.rst index 97036ed25..72c7cb4fc 100644 --- a/releasenotes/source/newton.rst +++ b/releasenotes/source/newton.rst @@ -3,4 +3,4 @@ =================================== .. release-notes:: - :branch: origin/stable/newton + :branch: stable/newton diff --git a/releasenotes/source/ocata.rst b/releasenotes/source/ocata.rst index ebe62f42e..53fb86e38 100644 --- a/releasenotes/source/ocata.rst +++ b/releasenotes/source/ocata.rst @@ -3,4 +3,4 @@ =================================== .. release-notes:: - :branch: origin/stable/ocata + :branch: stable/ocata From c055f15133cd03404fc0391b584783fdba6e5b39 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Tue, 6 Jun 2017 21:47:14 -0400 Subject: [PATCH 1308/1705] Skip rebuild functional test due to persistent vif plugging timeout The test_rebuild functional test has been failing at a very high rate lately, above 50%, so it's blocking other changes from getting in. This change skips the test due to the bug until the bug is fixed but we need to unblock other changes. Change-Id: Ib5f6474f642c4fa14d82349c47d51009e9341045 Related-Bug: #1694371 --- novaclient/tests/functional/v2/test_servers.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/novaclient/tests/functional/v2/test_servers.py b/novaclient/tests/functional/v2/test_servers.py index 7027413de..611ace8db 100644 --- a/novaclient/tests/functional/v2/test_servers.py +++ b/novaclient/tests/functional/v2/test_servers.py @@ -14,6 +14,8 @@ import random import string +from tempest.lib import decorators + from novaclient.tests.functional import base from novaclient.tests.functional.v2.legacy import test_servers from novaclient.v2 import shell @@ -85,6 +87,7 @@ def test_list_servers_with_description(self): self._get_column_value_from_single_row_table( output, "Description")) + @decorators.skip_because(bug="1694371") def test_rebuild(self): # Add a description to the tests that rebuild a server server, descr = self._boot_server_with_description() From a8d4cec28e935eb6e602a75d880ec81691f3b4c0 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Tue, 6 Jun 2017 10:27:36 -0400 Subject: [PATCH 1309/1705] Prevent 2.32 release note from showing up in 9.0.0 release notes The release note for the 2.32 microversion support was originally added in the Newton release and tagged on 5.0.0: 7fb0bd4f357a7f9e24c864584b398a25171580cf Then it was modified on master (pike) with change: aa7f35d76b5768ec3fe4d45ad010d53adff7cccd That makes reno think that it's a new unreleased release note, which makes it show up in the unreleased notes which are going to eventually be the release notes for the 9.0.0 release. This is wrong and confusing since 2.32 support happened in 5.0.0 not 9.0.0. Reverting the typo change doesn't help since reno is still picking up the release note from that change in the git history. The only way to remove the release note from being processed on the current branch (master) is to delete it. The note would still be available in stable/ocata and stable/newton branches. Change-Id: Ie6e9b930ff49621244df286d8f1d2cedffa2ff11 --- .../microversion-v2_32-7947430cc2415597.yaml | 25 ------------------- 1 file changed, 25 deletions(-) delete mode 100644 releasenotes/notes/microversion-v2_32-7947430cc2415597.yaml diff --git a/releasenotes/notes/microversion-v2_32-7947430cc2415597.yaml b/releasenotes/notes/microversion-v2_32-7947430cc2415597.yaml deleted file mode 100644 index e6352e015..000000000 --- a/releasenotes/notes/microversion-v2_32-7947430cc2415597.yaml +++ /dev/null @@ -1,25 +0,0 @@ ---- -features: - - | - The 2.32 microversion adds support for virtual device - role tagging. Device role tagging is an answer to the - question 'Which device is which?' from inside the guest. - When booting an instance, an optional arbitrary 'tag' - parameter can be set on virtual network interfaces - and/or block device mappings. This tag is exposed to the - instance through the metadata API and on the config - drive. Each tagged virtual network interface is listed - along with information about the virtual hardware, such - as bus type (ex: PCI), bus address (ex: 0000:00:02.0), - and MAC address. For tagged block devices, the exposed - hardware metadata includes the bus (ex: SCSI), bus - address (ex: 1:0:2:0) and serial number. - - In the client, device tagging is exposed with the 'tag' - key in the --block-device and --nic boot arguments. -issues: - - | - While not an issue with the client itself, it should be - noted that if a tagged network interface or volume is - detached from a guest, its metadata will continue to - appear in the config drive, even after instance reboot. From 4b0c5e4a2852e18390c5ae4c103051bd46805f51 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Wed, 7 Jun 2017 11:59:58 +0000 Subject: [PATCH 1310/1705] Updated from global requirements Change-Id: I6b0a4cbb93ee0ec3a8d69990b2ae261c1786b50e --- test-requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test-requirements.txt b/test-requirements.txt index 3b2a540f0..847b49637 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -10,8 +10,8 @@ keyring>=5.5.1 # MIT/PSF mock>=2.0 # BSD python-keystoneclient>=3.8.0 # Apache-2.0 python-cinderclient>=2.1.0 # Apache-2.0 -python-glanceclient>=2.5.0 # Apache-2.0 -python-neutronclient>=5.1.0 # Apache-2.0 +python-glanceclient>=2.7.0 # Apache-2.0 +python-neutronclient>=6.3.0 # Apache-2.0 requests-mock>=1.1 # Apache-2.0 sphinx!=1.6.1,>=1.5.1 # BSD os-client-config>=1.27.0 # Apache-2.0 @@ -23,4 +23,4 @@ testtools>=1.4.0 # MIT tempest>=14.0.0 # Apache-2.0 # releasenotes -reno>=1.8.0 # Apache-2.0 +reno!=2.3.1,>=1.8.0 # Apache-2.0 From 264c22a9cc0955a1d0f545d5bc5de30bdf7b5072 Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Mon, 12 Jun 2017 15:10:05 +0900 Subject: [PATCH 1311/1705] Fix setting 'global_request_id' in SessionClient In Ic75be3acb8b77aae8da631e3c4cd6f545a9a35cb, 'global_request_id' is set in the constructor of class keystoneauth1.adaptor.Adapter. But 'global_request_id' was not passed to the constructor, 'global_request_id' was cleared. It caused the test failure. It is not necessary to set 'global_request_id' in the constructor of class SessionClient and just passing 'global_request_id' in kwargs is required. Change-Id: Id587e35c221fe2b11889469f88557d254125ea7e Closes-Bug: #1697358 --- novaclient/client.py | 5 ----- requirements.txt | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/novaclient/client.py b/novaclient/client.py index f65f194f7..2d2716310 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -40,7 +40,6 @@ from novaclient.i18n import _ from novaclient import utils -REQ_ID_HEADER = 'X-OpenStack-Request-ID' # TODO(jichenjc): when an extension in contrib is moved to core extension, # Add the name into the following list, then after last patch merged, # remove the whole function @@ -57,16 +56,12 @@ def __init__(self, *args, **kwargs): self.timings = kwargs.pop('timings', False) self.api_version = kwargs.pop('api_version', None) self.api_version = self.api_version or api_versions.APIVersion() - self.global_request_id = kwargs.pop('global_request_id', None) super(SessionClient, self).__init__(*args, **kwargs) def request(self, url, method, **kwargs): kwargs.setdefault('headers', kwargs.get('headers', {})) api_versions.update_headers(kwargs["headers"], self.api_version) - if self.global_request_id is not None: - kwargs['headers'].setdefault(REQ_ID_HEADER, self.global_request_id) - # NOTE(dbelova): osprofiler_web.get_trace_id_headers does not add any # headers in case if osprofiler is not initialized. if osprofiler_web: diff --git a/requirements.txt b/requirements.txt index 3dc8ecfde..d909bbdb9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. pbr!=2.1.0,>=2.0.0 # Apache-2.0 -keystoneauth1>=2.20.0 # Apache-2.0 +keystoneauth1>=2.21.0 # Apache-2.0 iso8601>=0.1.11 # MIT oslo.i18n!=3.15.2,>=2.1.0 # Apache-2.0 oslo.serialization>=1.10.0 # Apache-2.0 From c496531435f7c27ee1ad7f1eff8b05b8064ef9e8 Mon Sep 17 00:00:00 2001 From: Chris Friesen Date: Tue, 13 Jun 2017 10:18:33 -0600 Subject: [PATCH 1312/1705] Clean up ShellTest unit tests Convert all the individual tests to use the common mocked client that is defined in setUp(). This simplifies the individual tests and lays the groundwork for supporting specifying the API version in the mocked client since it'll be needed in a future commit. Change-Id: I4c0722296d3b1997eed878e5b90198f8982157d1 --- novaclient/tests/unit/test_shell.py | 84 ++++++++++++----------------- 1 file changed, 33 insertions(+), 51 deletions(-) diff --git a/novaclient/tests/unit/test_shell.py b/novaclient/tests/unit/test_shell.py index bb515bfc2..01a646c29 100644 --- a/novaclient/tests/unit/test_shell.py +++ b/novaclient/tests/unit/test_shell.py @@ -363,9 +363,9 @@ def make_env(self, exclude=None, fake_env=FAKE_ENV): def setUp(self): super(ShellTest, self).setUp() - self.useFixture(fixtures.MonkeyPatch( - 'novaclient.client.Client', - mock.MagicMock())) + self.mock_client = mock.MagicMock() + self.useFixture(fixtures.MonkeyPatch('novaclient.client.Client', + self.mock_client)) self.nc_util = mock.patch('novaclient.utils.isunauthenticated').start() self.nc_util.return_value = False self.mock_server_version_range = mock.patch( @@ -520,38 +520,34 @@ def test_no_auth_url(self): else: self.fail('CommandError not raised') - @mock.patch('novaclient.client.Client') @requests_mock.Mocker() - def test_nova_endpoint_type(self, mock_client, m_requests): + def test_nova_endpoint_type(self, m_requests): self.make_env(fake_env=FAKE_ENV3) self.register_keystone_discovery_fixture(m_requests) self.shell('list') - client_kwargs = mock_client.call_args_list[0][1] + client_kwargs = self.mock_client.call_args_list[0][1] self.assertEqual(client_kwargs['endpoint_type'], 'novaURL') - @mock.patch('novaclient.client.Client') @requests_mock.Mocker() - def test_endpoint_type_like_other_clients(self, mock_client, m_requests): + def test_endpoint_type_like_other_clients(self, m_requests): self.make_env(fake_env=FAKE_ENV4) self.register_keystone_discovery_fixture(m_requests) self.shell('list') - client_kwargs = mock_client.call_args_list[0][1] + client_kwargs = self.mock_client.call_args_list[0][1] self.assertEqual(client_kwargs['endpoint_type'], 'internalURL') - @mock.patch('novaclient.client.Client') @requests_mock.Mocker() - def test_os_endpoint_type(self, mock_client, m_requests): + def test_os_endpoint_type(self, m_requests): self.make_env(exclude='NOVA_ENDPOINT_TYPE', fake_env=FAKE_ENV3) self.register_keystone_discovery_fixture(m_requests) self.shell('list') - client_kwargs = mock_client.call_args_list[0][1] + client_kwargs = self.mock_client.call_args_list[0][1] self.assertEqual(client_kwargs['endpoint_type'], 'osURL') - @mock.patch('novaclient.client.Client') - def test_default_endpoint_type(self, mock_client): + def test_default_endpoint_type(self): self.make_env() self.shell('list') - client_kwargs = mock_client.call_args_list[0][1] + client_kwargs = self.mock_client.call_args_list[0][1] self.assertEqual(client_kwargs['endpoint_type'], 'publicURL') @mock.patch('sys.stdin', side_effect=mock.MagicMock) @@ -604,19 +600,16 @@ def _test_service_type(self, version, service_type, mock_client): _client_args, client_kwargs = mock_client.call_args_list[0] self.assertEqual(service_type, client_kwargs['service_type']) - @mock.patch('novaclient.client.Client') - def test_default_service_type(self, mock_client): - self._test_service_type(None, 'compute', mock_client) + def test_default_service_type(self): + self._test_service_type(None, 'compute', self.mock_client) - @mock.patch('novaclient.client.Client') - def test_v2_service_type(self, mock_client): - self._test_service_type('2', 'compute', mock_client) + def test_v2_service_type(self): + self._test_service_type('2', 'compute', self.mock_client) - @mock.patch('novaclient.client.Client') - def test_v_unknown_service_type(self, mock_client): + def test_v_unknown_service_type(self): self.assertRaises(exceptions.UnsupportedVersion, self._test_service_type, - 'unknown', 'compute', mock_client) + 'unknown', 'compute', self.mock_client) @mock.patch('sys.argv', ['nova']) @mock.patch('sys.stdout', six.StringIO()) @@ -678,9 +671,8 @@ def test_osprofiler_not_installed(self, m_requests): return_value=True) @mock.patch('novaclient.shell.SecretsHelper.management_url', return_value=True) - @mock.patch('novaclient.client.Client') @requests_mock.Mocker() - def test_keyring_saver_helper(self, mock_client, + def test_keyring_saver_helper(self, sh_management_url_function, sh_auth_token_function, sh_tenant_id_function, @@ -688,82 +680,72 @@ def test_keyring_saver_helper(self, mock_client, self.make_env(fake_env=FAKE_ENV) self.register_keystone_discovery_fixture(m_requests) self.shell('list') - mock_client_instance = mock_client.return_value + mock_client_instance = self.mock_client.return_value keyring_saver = mock_client_instance.client.keyring_saver self.assertIsInstance(keyring_saver, novaclient.shell.SecretsHelper) - @mock.patch('novaclient.client.Client') - def test_microversion_with_default_behaviour(self, mock_client): + def test_microversion_with_default_behaviour(self): self.make_env(fake_env=FAKE_ENV5) self.mock_server_version_range.return_value = ( api_versions.APIVersion("2.1"), api_versions.APIVersion("2.3")) self.shell('list') - client_args = mock_client.call_args_list[1][0] + client_args = self.mock_client.call_args_list[1][0] self.assertEqual(api_versions.APIVersion("2.3"), client_args[0]) - @mock.patch('novaclient.client.Client') - def test_microversion_with_default_behaviour_with_legacy_server( - self, mock_client): + def test_microversion_with_default_behaviour_with_legacy_server(self): self.make_env(fake_env=FAKE_ENV5) self.mock_server_version_range.return_value = ( api_versions.APIVersion(), api_versions.APIVersion()) self.shell('list') - client_args = mock_client.call_args_list[1][0] + client_args = self.mock_client.call_args_list[1][0] self.assertEqual(api_versions.APIVersion("2.0"), client_args[0]) - @mock.patch('novaclient.client.Client') - def test_microversion_with_latest(self, mock_client): + def test_microversion_with_latest(self): self.make_env() novaclient.API_MAX_VERSION = api_versions.APIVersion('2.3') self.mock_server_version_range.return_value = ( api_versions.APIVersion("2.1"), api_versions.APIVersion("2.3")) self.shell('--os-compute-api-version 2.latest list') - client_args = mock_client.call_args_list[1][0] + client_args = self.mock_client.call_args_list[1][0] self.assertEqual(api_versions.APIVersion("2.3"), client_args[0]) - @mock.patch('novaclient.client.Client') - def test_microversion_with_specified_version(self, mock_client): + def test_microversion_with_specified_version(self): self.make_env() self.mock_server_version_range.return_value = ( api_versions.APIVersion("2.10"), api_versions.APIVersion("2.100")) novaclient.API_MAX_VERSION = api_versions.APIVersion("2.100") novaclient.API_MIN_VERSION = api_versions.APIVersion("2.90") self.shell('--os-compute-api-version 2.99 list') - client_args = mock_client.call_args_list[1][0] + client_args = self.mock_client.call_args_list[1][0] self.assertEqual(api_versions.APIVersion("2.99"), client_args[0]) - @mock.patch('novaclient.client.Client') - def test_microversion_with_specified_version_out_of_range(self, - mock_client): + def test_microversion_with_specified_version_out_of_range(self): novaclient.API_MAX_VERSION = api_versions.APIVersion("2.100") novaclient.API_MIN_VERSION = api_versions.APIVersion("2.90") self.assertRaises(exceptions.CommandError, self.shell, '--os-compute-api-version 2.199 list') - @mock.patch('novaclient.client.Client') - def test_microversion_with_v2_and_v2_1_server(self, mock_client): + def test_microversion_with_v2_and_v2_1_server(self): self.make_env() self.mock_server_version_range.return_value = ( api_versions.APIVersion('2.1'), api_versions.APIVersion('2.3')) novaclient.API_MAX_VERSION = api_versions.APIVersion("2.100") novaclient.API_MIN_VERSION = api_versions.APIVersion("2.1") self.shell('--os-compute-api-version 2 list') - client_args = mock_client.call_args_list[1][0] + client_args = self.mock_client.call_args_list[1][0] self.assertEqual(api_versions.APIVersion("2.0"), client_args[0]) - @mock.patch('novaclient.client.Client') - def test_microversion_with_v2_and_v2_server(self, mock_client): + def test_microversion_with_v2_and_v2_server(self): self.make_env() self.mock_server_version_range.return_value = ( api_versions.APIVersion(), api_versions.APIVersion()) novaclient.API_MAX_VERSION = api_versions.APIVersion("2.100") novaclient.API_MIN_VERSION = api_versions.APIVersion("2.1") self.shell('--os-compute-api-version 2 list') - client_args = mock_client.call_args_list[1][0] + client_args = self.mock_client.call_args_list[1][0] self.assertEqual(api_versions.APIVersion("2.0"), client_args[0]) - @mock.patch('novaclient.client.Client') - def test_microversion_with_v2_without_server_compatible(self, mock_client): + def test_microversion_with_v2_without_server_compatible(self): self.make_env() self.mock_server_version_range.return_value = ( api_versions.APIVersion('2.2'), api_versions.APIVersion('2.3')) From ddb386b2dffaf06077c2a0768c56ed5642d216dc Mon Sep 17 00:00:00 2001 From: Sean Dague Date: Thu, 15 Jun 2017 10:26:41 -0400 Subject: [PATCH 1313/1705] Revert "client.logger.warning wrongly used in migrations" This is a partial revert of the previous patch. The inline setup of the logger ends up causing issues when this is consumed by other libraries that want control over the logger. Closes-Bug: #1697452 This reverts commit 9a272014167390217351dd5a60f50f518f4ae600. Change-Id: I1d61d4952aef1039e72a2aac629cb13a6a15d7b2 --- novaclient/v2/client.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/novaclient/v2/client.py b/novaclient/v2/client.py index 73280b59b..b85077392 100644 --- a/novaclient/v2/client.py +++ b/novaclient/v2/client.py @@ -183,11 +183,7 @@ def __init__(self, self.server_external_events = \ server_external_events.ServerExternalEventManager(self) - if not logger: - logger = logging.getLogger(__name__) - if not logger.handlers: - logger.addHandler(logging.StreamHandler()) - self.logger = logger + self.logger = logger or logging.getLogger(__name__) # Add in any extensions... if extensions: From c23324ef4806706bef257e711ac6ffaa8a833ea4 Mon Sep 17 00:00:00 2001 From: Tovin Seven Date: Mon, 19 Jun 2017 10:46:53 +0700 Subject: [PATCH 1314/1705] Make --profile load from environment variables --profile argument can be loaded from OS_PROFILE environment variables to avoid repeating --profile in client commands. Co-Authored-By: Hieu LE Change-Id: I52415354f0c7a7483eddaa48f2acafcdecbb26fd --- novaclient/shell.py | 1 + 1 file changed, 1 insertion(+) diff --git a/novaclient/shell.py b/novaclient/shell.py index b23a48fe7..4bf46aed6 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -505,6 +505,7 @@ def get_base_parser(self, argv): if osprofiler_profiler: parser.add_argument('--profile', metavar='HMAC_KEY', + default=utils.env('OS_PROFILE'), help='HMAC key to use for encrypting context ' 'data for performance profiling of operation. ' 'This key should be the value of the HMAC key ' From c6d58d435678ede3bd5264043bf527c3663e6e86 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Thu, 22 Jun 2017 15:11:29 +0300 Subject: [PATCH 1315/1705] Cleanup duplicated methods Methods `get_vnc_console`, `get_rdp_console`, `get_serial_console` and `get_spice_console` of ServersManager have complitely equal code, args spec and docstrings for 2.0-2.5 and 2.6+ microversions. There is no reason to duplicate those methods, so this patch removes duplicates. Change-Id: I8c0e6f1f9138958bdb5d840cfd89e584c89895b0 --- novaclient/v2/servers.py | 52 ---------------------------------------- 1 file changed, 52 deletions(-) diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index a036bee5c..d16396ff6 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -941,7 +941,6 @@ def remove_floating_ip(self, server, address): address = address.ip if hasattr(address, 'ip') else address return self._action('removeFloatingIp', server, {'address': address}) - @api_versions.wraps('2.0', '2.5') def get_vnc_console(self, server, console_type): """ Get a vnc console for an instance @@ -953,7 +952,6 @@ def get_vnc_console(self, server, console_type): return self.get_console_url(server, console_type) - @api_versions.wraps('2.0', '2.5') def get_spice_console(self, server, console_type): """ Get a spice console for an instance @@ -965,7 +963,6 @@ def get_spice_console(self, server, console_type): return self.get_console_url(server, console_type) - @api_versions.wraps('2.0', '2.5') def get_rdp_console(self, server, console_type): """ Get a rdp console for an instance @@ -977,7 +974,6 @@ def get_rdp_console(self, server, console_type): return self.get_console_url(server, console_type) - @api_versions.wraps('2.0', '2.5') def get_serial_console(self, server, console_type): """ Get a serial console for an instance @@ -1011,54 +1007,6 @@ def get_console_url(self, server, console_type): raise exceptions.UnsupportedConsoleType(console_type) return self._action(action, server, {'type': console_type}) - @api_versions.wraps('2.6') - def get_vnc_console(self, server, console_type): - """ - Get a vnc console for an instance - - :param server: The :class:`Server` (or its ID) to get console for. - :param console_type: Type of vnc console to get ('novnc' or 'xvpvnc') - :returns: An instance of novaclient.base.DictWithMeta - """ - - return self.get_console_url(server, console_type) - - @api_versions.wraps('2.6') - def get_spice_console(self, server, console_type): - """ - Get a spice console for an instance - - :param server: The :class:`Server` (or its ID) to get console for. - :param console_type: Type of spice console to get ('spice-html5') - :returns: An instance of novaclient.base.DictWithMeta - """ - - return self.get_console_url(server, console_type) - - @api_versions.wraps('2.6') - def get_rdp_console(self, server, console_type): - """ - Get a rdp console for an instance - - :param server: The :class:`Server` (or its ID) to get console for. - :param console_type: Type of rdp console to get ('rdp-html5') - :returns: An instance of novaclient.base.DictWithMeta - """ - - return self.get_console_url(server, console_type) - - @api_versions.wraps('2.6') - def get_serial_console(self, server, console_type): - """ - Get a serial console for an instance - - :param server: The :class:`Server` (or its ID) to get console for. - :param console_type: Type of serial console to get ('serial') - :returns: An instance of novaclient.base.DictWithMeta - """ - - return self.get_console_url(server, console_type) - @api_versions.wraps('2.8') def get_mks_console(self, server): """ From 78986dcae2f12f18ae2380111e06e793998b06c2 Mon Sep 17 00:00:00 2001 From: Chris Friesen Date: Thu, 16 Feb 2017 11:28:54 -0600 Subject: [PATCH 1316/1705] 2.47: Show flavor info in server details This adds support for microversion 2.47 which directly embeds the flavor information in the server details. With this change, CLI requests with microversion >= 2.47 will no longer need to do additional queries to get the flavor and flavor extra_specs information. Instead, the flavor information will be output as separate key/value pairs with the keys namespaced with the "flavor:" prefix. As one would expect, these keys can also be specified as output fields when listing servers. Change-Id: Ic00ec95485485dff0fd4dcf8cad6ca56a481d512 Implements: blueprint instance-flavor-api Depends-On: If646149efb7eec8c90bf7d07c39ff4c495349941 --- novaclient/__init__.py | 2 +- .../functional/v2/legacy/test_servers.py | 12 ++++ .../tests/functional/v2/test_servers.py | 63 +++++++++++++++++++ novaclient/tests/unit/test_shell.py | 1 + novaclient/tests/unit/v2/test_shell.py | 2 + novaclient/v2/shell.py | 63 ++++++++++++++++--- .../microversion-v2_47-4aa54fbbd519e421.yaml | 18 ++++++ 7 files changed, 150 insertions(+), 11 deletions(-) create mode 100644 releasenotes/notes/microversion-v2_47-4aa54fbbd519e421.yaml diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 4a86d3653..3b93a4641 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.46") +API_MAX_VERSION = api_versions.APIVersion("2.47") diff --git a/novaclient/tests/functional/v2/legacy/test_servers.py b/novaclient/tests/functional/v2/legacy/test_servers.py index 8976abd53..c0852a1bf 100644 --- a/novaclient/tests/functional/v2/legacy/test_servers.py +++ b/novaclient/tests/functional/v2/legacy/test_servers.py @@ -159,3 +159,15 @@ def test_list_all_servers(self): # Cut header and footer of the table for server in precreated_servers: self.assertIn(server.id, output) + + def test_list_minimal(self): + name = uuidutils.generate_uuid() + uuid = self._create_server(name).id + server_output = self.nova("list --minimal") + # The only fields output are "ID" and "Name" + output_uuid = self._get_column_value_from_single_row_table( + server_output, 'ID') + output_name = self._get_column_value_from_single_row_table( + server_output, 'Name') + self.assertEqual(output_uuid, uuid) + self.assertEqual(output_name, name) diff --git a/novaclient/tests/functional/v2/test_servers.py b/novaclient/tests/functional/v2/test_servers.py index 611ace8db..c2290eb22 100644 --- a/novaclient/tests/functional/v2/test_servers.py +++ b/novaclient/tests/functional/v2/test_servers.py @@ -263,3 +263,66 @@ def test_boot_server_with_no_network(self): network = self._find_network_in_table(server_info) self.assertIsNone( network, 'Unexpected network allocation: %s' % server_info) + + +class TestServersDetailsFlavorInfo(base.ClientTestBase): + + COMPUTE_API_VERSION = '2.47' + + def _validate_flavor_details(self, flavor_details, server_details): + # This is a mapping between the keys used in the flavor GET response + # and the keys used for the flavor information embedded in the server + # details. + flavor_key_mapping = { + "OS-FLV-EXT-DATA:ephemeral": "flavor:ephemeral", + "disk": "flavor:disk", + "extra_specs": "flavor:extra_specs", + "name": "flavor:original_name", + "ram": "flavor:ram", + "swap": "flavor:swap", + "vcpus": "flavor:vcpus", + } + + for key in flavor_key_mapping: + flavor_val = self._get_value_from_the_table( + flavor_details, key) + server_flavor_val = self._get_value_from_the_table( + server_details, flavor_key_mapping[key]) + if key is "swap" and flavor_val is "": + # "flavor-show" displays zero swap as empty string. + flavor_val = '0' + self.assertEqual(flavor_val, server_flavor_val) + + def _setup_extra_specs(self, flavor_id): + extra_spec_key = "dummykey" + self.nova('flavor-key', params=('%(flavor)s set %(key)s=dummyval' % + {'flavor': flavor_id, + 'key': extra_spec_key})) + unset_params = ('%(flavor)s unset %(key)s' % + {'flavor': flavor_id, 'key': extra_spec_key}) + self.addCleanup(self.nova, 'flavor-key', params=unset_params) + + def test_show(self): + self._setup_extra_specs(self.flavor.id) + uuid = self._create_server().id + server_output = self.nova("show %s" % uuid) + flavor_output = self.nova("flavor-show %s" % self.flavor.id) + self._validate_flavor_details(flavor_output, server_output) + + def test_show_minimal(self): + uuid = self._create_server().id + server_output = self.nova("show --minimal %s" % uuid) + server_output_flavor = self._get_value_from_the_table( + server_output, 'flavor') + self.assertEqual(self.flavor.name, server_output_flavor) + + def test_list(self): + self._setup_extra_specs(self.flavor.id) + self._create_server() + server_output = self.nova("list --fields flavor:disk") + # namespaced fields get reformatted slightly as column names + server_flavor_val = self._get_column_value_from_single_row_table( + server_output, 'flavor: Disk') + flavor_output = self.nova("flavor-show %s" % self.flavor.id) + flavor_val = self._get_value_from_the_table(flavor_output, 'disk') + self.assertEqual(flavor_val, server_flavor_val) diff --git a/novaclient/tests/unit/test_shell.py b/novaclient/tests/unit/test_shell.py index 01a646c29..10de208f3 100644 --- a/novaclient/tests/unit/test_shell.py +++ b/novaclient/tests/unit/test_shell.py @@ -364,6 +364,7 @@ def make_env(self, exclude=None, fake_env=FAKE_ENV): def setUp(self): super(ShellTest, self).setUp() self.mock_client = mock.MagicMock() + self.mock_client.return_value.api_version = novaclient.API_MIN_VERSION self.useFixture(fixtures.MonkeyPatch('novaclient.client.Client', self.mock_client)) self.nc_util = mock.patch('novaclient.utils.isunauthenticated').start() diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 15a267eaa..45ee5d523 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2974,6 +2974,8 @@ def test_versions(self): 44, # There are no version-wrapped shell method changes for this. 45, # There are no version-wrapped shell method changes for this. 46, # There are no version-wrapped shell method changes for this. + 47, # NOTE(cfriesen): 47 adds support for flavor details embedded + # within the server details ]) versions_supported = set(range(0, novaclient.API_MAX_VERSION.ver_minor + 1)) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 951f6e9ed..1b59e9572 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -971,6 +971,21 @@ def print_progress(progress): time.sleep(poll_period) +def _expand_dict_attr(collection, attr): + """Expand item attribute whose value is a dict. + + Take a collection of items where the named attribute is known to have a + dictionary value and replace the named attribute with multiple attributes + whose names are the keys of the dictionary namespaced with the original + attribute name. + """ + for item in collection: + field = getattr(item, attr) + delattr(item, attr) + for subkey in field.keys(): + setattr(item, attr + ':' + subkey, field[subkey]) + + def _translate_keys(collection, convert): for item in collection: keys = item.__dict__.keys() @@ -1503,8 +1518,15 @@ def do_list(cs, args): if arg in args: search_opts[arg] = getattr(args, arg) - filters = {'flavor': lambda f: f['id'], - 'security_groups': utils.format_security_groups} + filters = {'security_groups': utils.format_security_groups} + + # In microversion 2.47 we started embedding flavor info in server details. + have_embedded_flavor_info = ( + cs.api_version >= api_versions.APIVersion('2.47')) + # If we don't have embedded flavor info then we only report the flavor id + # rather than looking up the rest of the information. + if not have_embedded_flavor_info: + filters['flavor'] = lambda f: f['id'] id_col = 'ID' @@ -1548,6 +1570,11 @@ def do_list(cs, args): cols = [] fmts = {} + # For detailed lists, if we have embedded flavor information then replace + # the "flavor" attribute with more detailed information. + if detailed and have_embedded_flavor_info: + _expand_dict_attr(servers, 'flavor') + if servers: cols, fmts = _get_list_table_columns_and_formatters( args.fields, servers, exclude_fields=('id',), filters=filters) @@ -2131,15 +2158,31 @@ def _print_server(cs, args, server=None, wrap=0): info['%s network' % network_label] = ', '.join(address_list) flavor = info.get('flavor', {}) - flavor_id = flavor.get('id', '') - if minimal: - info['flavor'] = flavor_id + if cs.api_version >= api_versions.APIVersion('2.47'): + # The "flavor" field is a JSON representation of a dict containing the + # flavor information used at boot. + if minimal: + # To retain something similar to the previous behaviour, keep the + # 'flavor' field name but just output the original name. + info['flavor'] = flavor['original_name'] + else: + # Replace the "flavor" field with individual namespaced fields. + del info['flavor'] + for key in flavor.keys(): + info['flavor:' + key] = flavor[key] else: - try: - info['flavor'] = '%s (%s)' % (_find_flavor(cs, flavor_id).name, - flavor_id) - except Exception: - info['flavor'] = '%s (%s)' % (_("Flavor not found"), flavor_id) + # Prior to microversion 2.47 we just have the ID of the flavor so we + # need to retrieve the flavor information (which may have changed + # since the instance was booted). + flavor_id = flavor.get('id', '') + if minimal: + info['flavor'] = flavor_id + else: + try: + info['flavor'] = '%s (%s)' % (_find_flavor(cs, flavor_id).name, + flavor_id) + except Exception: + info['flavor'] = '%s (%s)' % (_("Flavor not found"), flavor_id) if 'security_groups' in info: # when we have multiple nics the info will include the diff --git a/releasenotes/notes/microversion-v2_47-4aa54fbbd519e421.yaml b/releasenotes/notes/microversion-v2_47-4aa54fbbd519e421.yaml new file mode 100644 index 000000000..c5945adba --- /dev/null +++ b/releasenotes/notes/microversion-v2_47-4aa54fbbd519e421.yaml @@ -0,0 +1,18 @@ +--- +features: + - | + Added support for microversion 2.47 which returns the flavor details + directly embedded in the server details when listing or showing servers. + With this change, CLI requests with microversion >= 2.47 will no longer + need to do additional queries to get the flavor and flavor extra_specs + information. Instead, the flavor information will be output as + separate key/value pairs with the keys namespaced with the + "flavor:" prefix. As one would expect, these keys can also be + specified as output fields when listing servers, like this: + + ``nova list --fields name,flavor:original_name`` + + When displaying details of a single server, the ``--minimal`` option will + display a ``flavor`` field with a value of the ``original_name`` of the + flavor. Prior to this microversion the value was the ``id`` of the flavor. + From a0301ecf597d7ee4e078cd2a3e227f5abe3fcf0c Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Mon, 12 Jun 2017 15:28:48 +0900 Subject: [PATCH 1317/1705] Microversion 2.48: Standardization of VM diagnostics Change-Id: I4b63b0d2d8856e8c8c14375d8b99ae248ba4f10f Implements: blueprint restore-vm-diagnostics --- novaclient/__init__.py | 2 +- novaclient/tests/unit/v2/test_shell.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 3b93a4641..d4aeb9862 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.47") +API_MAX_VERSION = api_versions.APIVersion("2.48") diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 45ee5d523..442a47bd9 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2976,6 +2976,7 @@ def test_versions(self): 46, # There are no version-wrapped shell method changes for this. 47, # NOTE(cfriesen): 47 adds support for flavor details embedded # within the server details + 48, # There are no version-wrapped shell method changes for this. ]) versions_supported = set(range(0, novaclient.API_MAX_VERSION.ver_minor + 1)) From 6ac6c5f8e0ca4f939f7f0426ea91d3aad4f54f3c Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Tue, 27 Jun 2017 12:22:08 +0000 Subject: [PATCH 1318/1705] Updated from global requirements Change-Id: I2c603cc617628c6e2ef8f6b6cbec1087ffbe5165 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 847b49637..4514a4666 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -13,7 +13,7 @@ python-cinderclient>=2.1.0 # Apache-2.0 python-glanceclient>=2.7.0 # Apache-2.0 python-neutronclient>=6.3.0 # Apache-2.0 requests-mock>=1.1 # Apache-2.0 -sphinx!=1.6.1,>=1.5.1 # BSD +sphinx>=1.6.2 # BSD os-client-config>=1.27.0 # Apache-2.0 oslosphinx>=4.7.0 # Apache-2.0 osprofiler>=1.4.0 # Apache-2.0 From e7b84daa8774434f3b1382ce40741bebe248de12 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Sun, 25 Jun 2017 09:43:12 -0400 Subject: [PATCH 1319/1705] Deprecate binary argument in nova service enable/disable/force-down CLIs Change If1e03c9343b8cc9c34bd51c2b4d25acdb21131ff in the API in Pike makes the os-services API look up services records via the host mapping in the API datadabase to determine which cell the service lives in. The host mappings only exist for nova-compute services, which means you can only enable/disable/force-down nova-compute services now, which is realistically the only service that ever made sense for those actions. That change broke some functional tests in novaclient though which loop through all services and disables them or forces them down. This change fixes those tests by only attempting the action on nova-compute services. This change also deprecates the binary argument to the service enable/disable/force-down commands since the only value that ever really worked or does anything is nova-compute, so a future version of the CLI should just hard-code that value. Change-Id: Idd0d2be960ca0ed59097c10c931da47a1a3e66fb Closes-Bug: #1700359 --- .../functional/v2/legacy/test_os_services.py | 16 ++++++++++++++++ .../tests/functional/v2/test_os_services.py | 8 ++++++++ novaclient/tests/unit/v2/test_shell.py | 12 ++++++++++++ novaclient/v2/shell.py | 15 ++++++++++++--- ...cate-service-binary-arg-2d5c446f5a2409a7.yaml | 10 ++++++++++ 5 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/deprecate-service-binary-arg-2d5c446f5a2409a7.yaml diff --git a/novaclient/tests/functional/v2/legacy/test_os_services.py b/novaclient/tests/functional/v2/legacy/test_os_services.py index 3ffc2d81e..8af71611d 100644 --- a/novaclient/tests/functional/v2/legacy/test_os_services.py +++ b/novaclient/tests/functional/v2/legacy/test_os_services.py @@ -31,6 +31,14 @@ def test_os_service_disable_enable(self): # in serial way (https://review.openstack.org/#/c/217768/), but # it's a potential issue for making these tests parallel in the future for serv in self.client.services.list(): + # In Pike the os-services API was made multi-cell aware and it + # looks up services by host, which uses the host mapping record + # in the API DB which is only populated for nova-compute services, + # effectively making it impossible to perform actions like enable + # or disable non-nova-compute services since the API won't be able + # to find them. So filter out anything that's not nova-compute. + if serv.binary != 'nova-compute': + continue host = self._get_column_value_from_single_row_table( self.nova('service-list --binary %s' % serv.binary), 'Host') service = self.nova('service-disable %s %s' % (host, serv.binary)) @@ -46,6 +54,14 @@ def test_os_service_disable_enable(self): def test_os_service_disable_log_reason(self): for serv in self.client.services.list(): + # In Pike the os-services API was made multi-cell aware and it + # looks up services by host, which uses the host mapping record + # in the API DB which is only populated for nova-compute services, + # effectively making it impossible to perform actions like enable + # or disable non-nova-compute services since the API won't be able + # to find them. So filter out anything that's not nova-compute. + if serv.binary != 'nova-compute': + continue host = self._get_column_value_from_single_row_table( self.nova('service-list --binary %s' % serv.binary), 'Host') service = self.nova('service-disable --reason test_disable %s %s' diff --git a/novaclient/tests/functional/v2/test_os_services.py b/novaclient/tests/functional/v2/test_os_services.py index 9fef43397..3da8449c0 100644 --- a/novaclient/tests/functional/v2/test_os_services.py +++ b/novaclient/tests/functional/v2/test_os_services.py @@ -20,6 +20,14 @@ class TestOsServicesNovaClientV211(test_os_services.TestOsServicesNovaClient): def test_os_services_force_down_force_up(self): for serv in self.client.services.list(): + # In Pike the os-services API was made multi-cell aware and it + # looks up services by host, which uses the host mapping record + # in the API DB which is only populated for nova-compute services, + # effectively making it impossible to perform actions like enable + # or disable non-nova-compute services since the API won't be able + # to find them. So filter out anything that's not nova-compute. + if serv.binary != 'nova-compute': + continue host = self._get_column_value_from_single_row_table( self.nova('service-list --binary %s' % serv.binary), 'Host') service = self.nova('service-force-down %s %s' diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 15a267eaa..812efcfc5 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2201,11 +2201,23 @@ def test_services_enable(self): body = {'host': 'host1', 'binary': 'nova-cert'} self.assert_called('PUT', '/os-services/enable', body) + def test_services_enable_default_binary(self): + """Tests that the default binary is nova-compute if not specified.""" + self.run_command('service-enable host1') + body = {'host': 'host1', 'binary': 'nova-compute'} + self.assert_called('PUT', '/os-services/enable', body) + def test_services_disable(self): self.run_command('service-disable host1 nova-cert') body = {'host': 'host1', 'binary': 'nova-cert'} self.assert_called('PUT', '/os-services/disable', body) + def test_services_disable_default_binary(self): + """Tests that the default binary is nova-compute if not specified.""" + self.run_command('service-disable host1') + body = {'host': 'host1', 'binary': 'nova-compute'} + self.assert_called('PUT', '/os-services/disable', body) + def test_services_disable_with_reason(self): self.run_command('service-disable host1 nova-cert --reason no_reason') body = {'host': 'host1', 'binary': 'nova-cert', diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 951f6e9ed..d095baa77 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -3420,7 +3420,10 @@ def do_service_list(cs, args): @utils.arg('host', metavar='', help=_('Name of host.')) -@utils.arg('binary', metavar='', help=_('Service binary.')) +# TODO(mriedem): Eventually just hard-code the binary to "nova-compute". +@utils.arg('binary', metavar='', help=_('Service binary. The only ' + 'meaningful binary is "nova-compute". (Deprecated)'), + default='nova-compute', nargs='?') def do_service_enable(cs, args): """Enable the service.""" result = cs.services.enable(args.host, args.binary) @@ -3428,7 +3431,10 @@ def do_service_enable(cs, args): @utils.arg('host', metavar='', help=_('Name of host.')) -@utils.arg('binary', metavar='', help=_('Service binary.')) +# TODO(mriedem): Eventually just hard-code the binary to "nova-compute". +@utils.arg('binary', metavar='', help=_('Service binary. The only ' + 'meaningful binary is "nova-compute". (Deprecated)'), + default='nova-compute', nargs='?') @utils.arg( '--reason', metavar='', @@ -3447,7 +3453,10 @@ def do_service_disable(cs, args): @api_versions.wraps("2.11") @utils.arg('host', metavar='', help=_('Name of host.')) -@utils.arg('binary', metavar='', help=_('Service binary.')) +# TODO(mriedem): Eventually just hard-code the binary to "nova-compute". +@utils.arg('binary', metavar='', help=_('Service binary. The only ' + 'meaningful binary is "nova-compute". (Deprecated)'), + default='nova-compute', nargs='?') @utils.arg( '--unset', dest='force_down', diff --git a/releasenotes/notes/deprecate-service-binary-arg-2d5c446f5a2409a7.yaml b/releasenotes/notes/deprecate-service-binary-arg-2d5c446f5a2409a7.yaml new file mode 100644 index 000000000..254b42d68 --- /dev/null +++ b/releasenotes/notes/deprecate-service-binary-arg-2d5c446f5a2409a7.yaml @@ -0,0 +1,10 @@ +--- +deprecations: + - | + The ``binary`` argument to the ``nova service-enable``, + ``nova service-disable``, and ``nova service-force-down`` commands has been + deprecated. The only binary that it makes sense to use is ``nova-compute`` + since disabling a service like ``nova-scheduler`` or ``nova-conductor`` + does not actually do anything, and starting in the 16.0.0 Pike release the + compute API will not be able to look up services other than + ``nova-compute`` for these operations. From ed0b2c0f43a880e3f12dc91af08aee5eabbacd50 Mon Sep 17 00:00:00 2001 From: Arundhati Surpur Date: Thu, 6 Jul 2017 16:55:41 +0530 Subject: [PATCH 1320/1705] Removed extra word 'method' from the NOTE Removed extra 'method' word from the file api_versions.py Change-Id: Ie0f89bdab5e3a0bd66f3cd992e03c45bda01ef1d --- novaclient/api_versions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/api_versions.py b/novaclient/api_versions.py index b0c41b023..61cded280 100644 --- a/novaclient/api_versions.py +++ b/novaclient/api_versions.py @@ -400,7 +400,7 @@ def substitution(obj, *args, **kwargs): # NOTE(andreykurilin): The way to obtain function's name in Python 2 # bases on traceback(see _get_function_name for details). Since the - # right versioned method method is used in several places, one object + # right versioned method is used in several places, one object # can have different names. Let's generate name of function one time # and use __id__ property in all other places. substitution.__id__ = name From bd0a2adefe5e89c4269f6589837f871764c6f1f6 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Mon, 3 Jul 2017 16:27:29 +0300 Subject: [PATCH 1321/1705] Fix cropping the endpoint url `nova version-list` crops the endpoint url to exclude API version and project-id(for microversions < 2.18). It is ok, but the code was designed before Nova-API moved under uwsgi, which changed the endpoint url format. The latest endpoint url contains the hostname and port which can be one for several services and it is a wrong case to cut the whole path from the endpoint. Closes-Bug: #1702194 Change-Id: Icba858b496855e2ffd71b35168e8057b28236119 --- .../v2/legacy/test_readonly_nova.py | 2 +- novaclient/tests/unit/v2/test_versions.py | 27 ++++++++++++++++++- novaclient/v2/versions.py | 22 ++++++++++++--- 3 files changed, 45 insertions(+), 6 deletions(-) diff --git a/novaclient/tests/functional/v2/legacy/test_readonly_nova.py b/novaclient/tests/functional/v2/legacy/test_readonly_nova.py index 6b783a408..bf1ebe4c0 100644 --- a/novaclient/tests/functional/v2/legacy/test_readonly_nova.py +++ b/novaclient/tests/functional/v2/legacy/test_readonly_nova.py @@ -100,7 +100,7 @@ def test_migration_list(self): self.nova('migration-list', flags='--debug') def test_version_list(self): - self.nova('version-list') + self.nova('version-list', flags='--debug') def test_quota_defaults(self): self.nova('quota-defaults') diff --git a/novaclient/tests/unit/v2/test_versions.py b/novaclient/tests/unit/v2/test_versions.py index 6b20cd0b3..63d63c1a3 100644 --- a/novaclient/tests/unit/v2/test_versions.py +++ b/novaclient/tests/unit/v2/test_versions.py @@ -30,7 +30,7 @@ def setUp(self): def test_list_services(self): vl = self.cs.versions.list() self.assert_request_id(vl, fakes.FAKE_REQUEST_ID_LIST) - self.cs.assert_called('GET', 'http://nova-api:8774/') + self.cs.assert_called('GET', 'http://nova-api:8774') def test_get_current(self): self.cs.callback = [] @@ -75,3 +75,28 @@ def test_v2_get_endpoint_without_project_id(self): # check that the full request works as expected cs_2.assert_called('GET', expected_endpoint) + + def test_list_versions(self): + fapi = mock.Mock() + version_mgr = versions.VersionManager(fapi) + version_mgr._list = mock.Mock() + data = [ + ("https://example.com:777/v2", "https://example.com:777"), + ("https://example.com/v2", "https://example.com"), + ("http://example.com/compute/v2", "http://example.com/compute"), + ("https://example.com/v2/prrrooojeect-uuid", + "https://example.com"), + ("https://example.com:777/v2.1", "https://example.com:777"), + ("https://example.com/v2.1", "https://example.com"), + ("http://example.com/compute/v2.1", "http://example.com/compute"), + ("https://example.com/v2.1/prrrooojeect-uuid", + "https://example.com"), + ("http://example.com/compute", "http://example.com/compute"), + ("http://compute.example.com", "http://compute.example.com"), + ] + + for endpoint, expected in data: + version_mgr._list.reset_mock() + fapi.client.get_endpoint.return_value = endpoint + version_mgr.list() + version_mgr._list.assert_called_once_with(expected, "versions") diff --git a/novaclient/v2/versions.py b/novaclient/v2/versions.py index 48cc52cd6..dd157d9f2 100644 --- a/novaclient/v2/versions.py +++ b/novaclient/v2/versions.py @@ -78,11 +78,25 @@ def get_current(self): def list(self): """List all versions.""" - # NOTE: "list versions" API needs to be accessed without base - # URI (like "v2/{project-id}"), so here should be a scheme("http", - # etc.) and a hostname. endpoint = self.api.client.get_endpoint() url = urllib.parse.urlparse(endpoint) - version_url = '%s://%s/' % (url.scheme, url.netloc) + # NOTE(andreykurilin): endpoint URL has at least 3 formats: + # 1. the classic (legacy) endpoint: + # http://{host}:{optional_port}/v{2 or 2.1}/{project-id} + # 2. starting from microversion 2.18 project-id is not included: + # http://{host}:{optional_port}/v{2 or 2.1} + # 3. under wsgi: + # http://{host}:{optional_port}/compute/v{2 or 2.1} + if (url.path.endswith("v2") or "/v2/" in url.path or + url.path.endswith("v2.1") or "/v2.1/" in url.path): + # this way should handle all 3 possible formats + path = url.path[:url.path.rfind("/v2")] + version_url = '%s://%s%s' % (url.scheme, url.netloc, path) + else: + # NOTE(andreykurilin): probably, it is one of the next cases: + # * https://compute.example.com/ + # * https://example.com/compute + # leave as is without cropping. + version_url = endpoint return self._list(version_url, "versions") From e11a1266bb9d0b904045575a2c59b894a3d6666f Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Wed, 5 Jul 2017 13:23:33 +0900 Subject: [PATCH 1322/1705] Microversion 2.49 - Virt device tagged attach Change-Id: I65a2d33710f85e9b8c342d6ff4c1e4cc82990b8c Implements: blueprint virt-device-tagged-attach-detach --- novaclient/__init__.py | 2 +- novaclient/tests/unit/v2/test_servers.py | 26 +++++++++++++ novaclient/tests/unit/v2/test_shell.py | 30 ++++++++++++++ novaclient/tests/unit/v2/test_volumes.py | 24 +++++++++++- novaclient/v2/servers.py | 39 +++++++++++++++++++ novaclient/v2/shell.py | 27 ++++++++++++- novaclient/v2/volumes.py | 22 +++++++++++ .../microversion-v2_49-56bde596ee13366d.yaml | 7 ++++ 8 files changed, 173 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/microversion-v2_49-56bde596ee13366d.yaml diff --git a/novaclient/__init__.py b/novaclient/__init__.py index d4aeb9862..768ad119a 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.48") +API_MAX_VERSION = api_versions.APIVersion("2.49") diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index 348091d06..04772306b 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -1457,3 +1457,29 @@ def test_backup(self): self.assertEqual('456', sb['image_id']) self.assert_request_id(sb, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') + + +class ServersV249Test(ServersV2_37Test): + + api_version = "2.49" + + def test_interface_attach_with_tag(self): + s = self.cs.servers.get(1234) + ret = s.interface_attach('7f42712e-63fe-484c-a6df-30ae4867ff66', + None, None, 'test_tag') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called( + 'POST', '/servers/1234/os-interface', + {'interfaceAttachment': + {'port_id': '7f42712e-63fe-484c-a6df-30ae4867ff66', + 'tag': 'test_tag'}}) + + def test_add_fixed_ip(self): + # novaclient.v2.servers.Server.add_fixed_ip() + # is not available after 2.44 + pass + + def test_remove_fixed_ip(self): + # novaclient.v2.servers.Server.remove_fixed_ip() + # is not available after 2.44 + pass diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index fa4140cb5..64b7c931b 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2697,6 +2697,20 @@ def test_interface_attach(self): self.assert_called('POST', '/servers/1234/os-interface', {'interfaceAttachment': {'port_id': 'port_id'}}) + def test_interface_attach_with_tag_pre_v2_49(self): + self.assertRaises( + SystemExit, self.run_command, + 'interface-attach --port-id port_id --tag test_tag 1234', + api_version='2.48') + + def test_interface_attach_with_tag(self): + self.run_command( + 'interface-attach --port-id port_id --tag test_tag 1234', + api_version='2.49') + self.assert_called('POST', '/servers/1234/os-interface', + {'interfaceAttachment': {'port_id': 'port_id', + 'tag': 'test_tag'}}) + def test_interface_detach(self): self.run_command('interface-detach 1234 port_id') self.assert_called('DELETE', '/servers/1234/os-interface/port_id') @@ -2718,6 +2732,22 @@ def test_volume_attach_without_device(self): {'volumeAttachment': {'volumeId': 'Work'}}) + def test_volume_attach_with_tag_pre_v2_49(self): + self.assertRaises( + SystemExit, self.run_command, + 'volume-attach --tag test_tag sample-server Work /dev/vdb', + api_version='2.48') + + def test_volume_attach_with_tag(self): + self.run_command( + 'volume-attach --tag test_tag sample-server Work /dev/vdb', + api_version='2.49') + self.assert_called('POST', '/servers/1234/os-volume_attachments', + {'volumeAttachment': + {'device': '/dev/vdb', + 'volumeId': 'Work', + 'tag': 'test_tag'}}) + def test_volume_update(self): self.run_command('volume-update sample-server Work Work') self.assert_called('PUT', '/servers/1234/os-volume_attachments/Work', diff --git a/novaclient/tests/unit/v2/test_volumes.py b/novaclient/tests/unit/v2/test_volumes.py index eb0fe71fb..60178cb02 100644 --- a/novaclient/tests/unit/v2/test_volumes.py +++ b/novaclient/tests/unit/v2/test_volumes.py @@ -20,9 +20,11 @@ class VolumesTest(utils.TestCase): + api_version = "2.0" + def setUp(self): super(VolumesTest, self).setUp() - self.cs = fakes.FakeClient(api_versions.APIVersion("2.0")) + self.cs = fakes.FakeClient(api_versions.APIVersion(self.api_version)) def test_create_server_volume(self): v = self.cs.volumes.create_server_volume( @@ -66,3 +68,23 @@ def test_delete_server_volume(self): self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.cs.assert_called('DELETE', '/servers/1234/os-volume_attachments/Work') + + +class VolumesV249Test(VolumesTest): + api_version = "2.49" + + def test_create_server_volume_with_tag(self): + v = self.cs.volumes.create_server_volume( + server_id=1234, + volume_id='15e59938-07d5-11e1-90e3-e3dffe0c5983', + device='/dev/vdb', + tag='test_tag' + ) + self.assert_request_id(v, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called( + 'POST', '/servers/1234/os-volume_attachments', + {'volumeAttachment': { + 'volumeId': '15e59938-07d5-11e1-90e3-e3dffe0c5983', + 'device': '/dev/vdb', + 'tag': 'test_tag'}}) + self.assertIsInstance(v, volumes.Volume) diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index d16396ff6..b60b0b6de 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -577,12 +577,21 @@ def interface_list(self): """ return self.manager.interface_list(self) + @api_versions.wraps("2.0", "2.48") def interface_attach(self, port_id, net_id, fixed_ip): """ Attach a network interface to an instance. """ return self.manager.interface_attach(self, port_id, net_id, fixed_ip) + @api_versions.wraps("2.49") + def interface_attach(self, port_id, net_id, fixed_ip, tag=None): + """ + Attach a network interface to an instance with an optional tag. + """ + return self.manager.interface_attach(self, port_id, net_id, fixed_ip, + tag) + def interface_detach(self, port_id): """ Detach a network interface from an instance. @@ -1839,6 +1848,7 @@ def interface_list(self, server): return self._list('/servers/%s/os-interface' % base.getid(server), 'interfaceAttachments', obj_class=NetworkInterface) + @api_versions.wraps("2.0", "2.48") def interface_attach(self, server, port_id, net_id, fixed_ip): """ Attach a network_interface to an instance. @@ -1859,6 +1869,35 @@ def interface_attach(self, server, port_id, net_id, fixed_ip): return self._create('/servers/%s/os-interface' % base.getid(server), body, 'interfaceAttachment') + @api_versions.wraps("2.49") + def interface_attach(self, server, port_id, net_id, fixed_ip, tag=None): + """ + Attach a network_interface to an instance. + + :param server: The :class:`Server` (or its ID) to attach to. + :param port_id: The port to attach. + The port_id and net_id parameters are mutually + exclusive. + :param net_id: The ID of the network to attach. + :param fixed_ip: The fixed IP addresses. If the fixed_ip is specified, + the net_id has to be specified at the same time. + :param tag: The tag. + """ + + body = {'interfaceAttachment': {}} + if port_id: + body['interfaceAttachment']['port_id'] = port_id + if net_id: + body['interfaceAttachment']['net_id'] = net_id + if fixed_ip: + body['interfaceAttachment']['fixed_ips'] = [ + {'ip_address': fixed_ip}] + if tag: + body['interfaceAttachment']['tag'] = tag + + return self._create('/servers/%s/os-interface' % base.getid(server), + body, 'interfaceAttachment') + def interface_detach(self, server, port_id): """ Detach a network_interface from an instance. diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 7896b7c7f..72d71e2b1 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -2339,14 +2339,25 @@ def _translate_volume_attachments_keys(collection): help=_('Name of the device e.g. /dev/vdb. ' 'Use "auto" for autoassign (if supported). ' 'Libvirt driver will use default device name.')) +@utils.arg( + '--tag', + metavar='', + default=None, + help=_('Tag for the attached volume.'), + start_version="2.49") def do_volume_attach(cs, args): """Attach a volume to a server.""" if args.device == 'auto': args.device = None + update_kwargs = {} + if 'tag' in args and args.tag: + update_kwargs['tag'] = args.tag + volume = cs.volumes.create_server_volume(_find_server(cs, args.server).id, args.volume, - args.device) + args.device, + **update_kwargs) _print_volume(volume) @@ -4376,11 +4387,23 @@ def do_interface_list(cs, args): metavar='', help=_('Requested fixed IP.'), default=None, dest="fixed_ip") +@utils.arg( + '--tag', + metavar='', + default=None, + dest="tag", + help=_('Tag for the attached interface.'), + start_version="2.49") def do_interface_attach(cs, args): """Attach a network interface to a server.""" server = _find_server(cs, args.server) - res = server.interface_attach(args.port_id, args.net_id, args.fixed_ip) + update_kwargs = {} + if 'tag' in args and args.tag: + update_kwargs['tag'] = args.tag + + res = server.interface_attach(args.port_id, args.net_id, args.fixed_ip, + **update_kwargs) if isinstance(res, dict): utils.print_dict(res) diff --git a/novaclient/v2/volumes.py b/novaclient/v2/volumes.py index b0f66f32b..ebccf8a09 100644 --- a/novaclient/v2/volumes.py +++ b/novaclient/v2/volumes.py @@ -17,6 +17,7 @@ Volume interface """ +from novaclient import api_versions from novaclient import base @@ -37,6 +38,7 @@ class VolumeManager(base.Manager): """ resource_class = Volume + @api_versions.wraps("2.0", "2.48") def create_server_volume(self, server_id, volume_id, device=None): """ Attach a volume identified by the volume ID to the given server ID @@ -52,6 +54,26 @@ def create_server_volume(self, server_id, volume_id, device=None): return self._create("/servers/%s/os-volume_attachments" % server_id, body, "volumeAttachment") + @api_versions.wraps("2.49") + def create_server_volume(self, server_id, volume_id, device=None, + tag=None): + """ + Attach a volume identified by the volume ID to the given server ID + + :param server_id: The ID of the server + :param volume_id: The ID of the volume to attach. + :param device: The device name (optional) + :param tag: The tag (optional) + :rtype: :class:`Volume` + """ + body = {'volumeAttachment': {'volumeId': volume_id}} + if device is not None: + body['volumeAttachment']['device'] = device + if tag is not None: + body['volumeAttachment']['tag'] = tag + return self._create("/servers/%s/os-volume_attachments" % server_id, + body, "volumeAttachment") + def update_server_volume(self, server_id, src_volid, dest_volid): """ Swaps the existing volume attachment to point to a new volume. diff --git a/releasenotes/notes/microversion-v2_49-56bde596ee13366d.yaml b/releasenotes/notes/microversion-v2_49-56bde596ee13366d.yaml new file mode 100644 index 000000000..86120be48 --- /dev/null +++ b/releasenotes/notes/microversion-v2_49-56bde596ee13366d.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Added support for microversion 2.49 that enables users to + attach tagged interfaces and volumes. + A new ``--tag`` option is added to ``nova volume-attach`` and + ``nova interface-attach`` commands. From e11efd8d762e5d9875afca9bcb76ea9ae69238c5 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 4 Jul 2017 11:06:56 +0100 Subject: [PATCH 1323/1705] doc: Create directory structure for docs migration The spec [1] recommends the following sections: - install - contributor - configuration - cli - admin - user - reference Only a few of these are useful here: - contributor - cli - user - reference Add these and populate them. [1] specs.openstack.org/openstack/docs-specs/specs/pike/os-manuals-migration Change-Id: I8a9ddcc915f25ebfaa1e994dba6c15883bd2715d --- .gitignore | 6 +- README.rst | 102 ++---------------- doc/.gitignore | 2 - doc/source/cli/index.rst | 8 ++ doc/source/{man => cli}/nova.rst | 6 +- doc/source/conf.py | 62 ++++++----- doc/source/contributor/index.rst | 16 +++ doc/source/contributor/testing.rst | 28 +++++ doc/source/index.rst | 96 +++-------------- .../{api.rst => reference/api/index.rst} | 11 +- doc/source/reference/deprecation-policy.rst | 32 ++++++ doc/source/reference/index.rst | 8 ++ doc/source/user/index.rst | 8 ++ doc/source/{ => user}/shell.rst | 39 ++++--- 14 files changed, 195 insertions(+), 229 deletions(-) delete mode 100644 doc/.gitignore create mode 100644 doc/source/cli/index.rst rename doc/source/{man => cli}/nova.rst (98%) create mode 100644 doc/source/contributor/index.rst create mode 100644 doc/source/contributor/testing.rst rename doc/source/{api.rst => reference/api/index.rst} (96%) create mode 100644 doc/source/reference/deprecation-policy.rst create mode 100644 doc/source/reference/index.rst create mode 100644 doc/source/user/index.rst rename doc/source/{ => user}/shell.rst (57%) diff --git a/.gitignore b/.gitignore index 372efb948..82ede6ad7 100644 --- a/.gitignore +++ b/.gitignore @@ -18,5 +18,9 @@ novaclient/versioninfo *egg-info .eggs +# Files created by documentation build +/doc/build/ +/doc/source/reference/api/ + # Files created by releasenotes build -releasenotes/build +/releasenotes/build diff --git a/README.rst b/README.rst index 0be65d77b..00f86f46d 100644 --- a/README.rst +++ b/README.rst @@ -7,8 +7,9 @@ Team and repository tags .. Change things from this point on -Python bindings to the OpenStack Nova API -========================================= +============================================ +Python bindings to the OpenStack Compute API +============================================ .. image:: https://img.shields.io/pypi/v/python-novaclient.svg :target: https://pypi.python.org/pypi/python-novaclient/ @@ -18,23 +19,14 @@ Python bindings to the OpenStack Nova API :target: https://pypi.python.org/pypi/python-novaclient/ :alt: Downloads -This is a client for the OpenStack Nova API. There's a Python API (the -``novaclient`` module), and a command-line script (``nova``). Each -implements 100% of the OpenStack Nova API. - -See the `OpenStack CLI guide`_ for information on how to use the ``nova`` -command-line tool. You may also want to look at the -`OpenStack API documentation`_. - -.. _OpenStack CLI Guide: http://docs.openstack.org/cli-reference/nova.html -.. _OpenStack API documentation: http://developer.openstack.org/api-ref-compute-v2.1.html - -python-novaclient is licensed under the Apache License like the rest of -OpenStack. +This is a client for the OpenStack Compute API. It provides a Python API (the +``novaclient`` module) and a command-line script (``nova``). Each implements +100% of the OpenStack Compute API. * License: Apache License, Version 2.0 * `PyPi`_ - package installation * `Online Documentation`_ +* `Launchpad project`_ - release management * `Blueprints`_ - feature specifications * `Bugs`_ - issue tracking * `Source`_ @@ -42,86 +34,10 @@ OpenStack. * `How to Contribute`_ .. _PyPi: https://pypi.python.org/pypi/python-novaclient -.. _Online Documentation: http://docs.openstack.org/developer/python-novaclient +.. _Online Documentation: http://docs.openstack.org/python-novaclient +.. _Launchpad project: https://launchpad.net/python-novaclient .. _Blueprints: https://blueprints.launchpad.net/python-novaclient .. _Bugs: https://bugs.launchpad.net/python-novaclient .. _Source: https://git.openstack.org/cgit/openstack/python-novaclient .. _How to Contribute: http://docs.openstack.org/infra/manual/developers.html .. _Specs: http://specs.openstack.org/openstack/nova-specs/ - - -.. contents:: Contents: - :local: - -Command-line API ----------------- - -Installing this package gets you a shell command, ``nova``, that you -can use to interact with any OpenStack cloud. - -You'll need to provide your OpenStack username and password. You can do this -with the ``--os-username``, ``--os-password`` and ``--os-project-name`` -params, but it's easier to just set them as environment variables:: - - export OS_USERNAME= - export OS_PASSWORD= - export OS_PROJECT_NAME= - - -You will also need to define the authentication url with ``--os-auth-url`` -and the version of the API with ``--os-compute-api-version``. Or set them as -environment variables as well and set the OS_AUTH_URL to the keystone endpoint:: - - export OS_AUTH_URL=http://:5000/v3/ - export OS_COMPUTE_API_VERSION=2.1 - - -Since Keystone can return multiple regions in the Service Catalog, you -can specify the one you want with ``--os-region-name`` (or -``export OS_REGION_NAME``). It defaults to the first in the list returned. - -You'll find complete documentation on the shell by running -``nova help`` - -Python API ----------- - -There's also a complete Python API, with documentation linked below. - - -To use with keystone as the authentication system:: - - >>> from keystoneauth1.identity import v3 - >>> from keystoneauth1 import session - >>> from novaclient import client - >>> auth = v3.Password(auth_url='http://example.com:5000/v3', - ... username='username', - ... password='password', - ... project_name='project-name', - ... user_domain_id='default', - ... project_domain_id='default') - >>> sess = session.Session(auth=auth) - >>> nova = client.Client("2.1", session=sess) - >>> nova.flavors.list() - [...] - >>> nova.servers.list() - [...] - >>> nova.keypairs.list() - [...] - - -Testing -------- - -There are multiple test targets that can be run to validate the code. - -* tox -e pep8 - style guidelines enforcement -* tox -e py27 - traditional unit testing -* tox -e functional - live functional testing against an existing - openstack - -Functional testing assumes the existence of a `clouds.yaml` file as supported -by `os-client-config` (http://docs.openstack.org/developer/os-client-config) -It assumes the existence of a cloud named `devstack` that behaves like a normal -devstack installation with a demo and an admin user/tenant - or clouds named -`functional_admin` and `functional_nonadmin`. diff --git a/doc/.gitignore b/doc/.gitignore deleted file mode 100644 index 8e0be80f5..000000000 --- a/doc/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -build/ -source/ref/ diff --git a/doc/source/cli/index.rst b/doc/source/cli/index.rst new file mode 100644 index 000000000..ce2917315 --- /dev/null +++ b/doc/source/cli/index.rst @@ -0,0 +1,8 @@ +=============== + CLI Reference +=============== + +.. toctree:: + :maxdepth: 2 + + nova diff --git a/doc/source/man/nova.rst b/doc/source/cli/nova.rst similarity index 98% rename from doc/source/man/nova.rst rename to doc/source/cli/nova.rst index 56e56b59d..c3c6caf42 100644 --- a/doc/source/man/nova.rst +++ b/doc/source/cli/nova.rst @@ -1,6 +1,6 @@ -==== -nova -==== +====== + nova +====== SYNOPSIS diff --git a/doc/source/conf.py b/doc/source/conf.py index 4813effbd..19d10bde6 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -22,37 +22,46 @@ sys.path.insert(0, BASE_DIR) +# TODO(stephenfin): This looks like something that pbr's autodoc integration +# could be doing for us. Investigate. + def gen_ref(ver, title, names): - refdir = os.path.join(BASE_DIR, "ref") + refdir = os.path.join(BASE_DIR, "reference", "api") pkg = "novaclient" if ver: pkg = "%s.%s" % (pkg, ver) refdir = os.path.join(refdir, ver) if not os.path.exists(refdir): os.makedirs(refdir) - idxpath = os.path.join(refdir, "index.rst") - with open(idxpath, "w") as idx: - idx.write(("%(title)s\n" - "%(signs)s\n" - "\n" - ".. toctree::\n" - " :maxdepth: 1\n" - "\n") % {"title": title, "signs": "=" * len(title)}) - for name in names: - idx.write(" %s\n" % name) - rstpath = os.path.join(refdir, "%s.rst" % name) - with open(rstpath, "w") as rst: - rst.write(("%(title)s\n" - "%(signs)s\n" - "\n" - ".. automodule:: %(pkg)s.%(name)s\n" - " :members:\n" - " :undoc-members:\n" - " :show-inheritance:\n" - " :noindex:\n") - % {"title": name.capitalize(), - "signs": "=" * len(name), - "pkg": pkg, "name": name}) + + # we don't want to write index files for top-level directories - only + # sub-directories + if ver: + idxpath = os.path.join(refdir, "index.rst") + with open(idxpath, "w") as idx: + idx.write(("%(title)s\n" + "%(signs)s\n" + "\n" + ".. toctree::\n" + " :maxdepth: 1\n" + "\n") % {"title": title, "signs": "=" * len(title)}) + for name in names: + idx.write(" %s\n" % name) + + for name in names: + rstpath = os.path.join(refdir, "%s.rst" % name) + with open(rstpath, "w") as rst: + rst.write(("%(title)s\n" + "%(signs)s\n" + "\n" + ".. automodule:: %(pkg)s.%(name)s\n" + " :members:\n" + " :undoc-members:\n" + " :show-inheritance:\n" + " :noindex:\n") + % {"title": name.capitalize(), + "signs": "=" * len(name), + "pkg": pkg, "name": name}) def get_module_names(): @@ -107,14 +116,11 @@ def get_module_names(): # Sphinx are currently 'default' and 'sphinxdoc'. html_theme = 'default' -# Output file base name for HTML help builder. -htmlhelp_basename = 'python-novaclientdoc' - # -- Options for manual page output ------------------------------------------ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ('man/nova', 'nova', 'OpenStack Nova command line client', + ('cli/nova', 'nova', 'OpenStack Nova command line client', ['OpenStack Contributors'], 1), ] diff --git a/doc/source/contributor/index.rst b/doc/source/contributor/index.rst new file mode 100644 index 000000000..cf8dccae1 --- /dev/null +++ b/doc/source/contributor/index.rst @@ -0,0 +1,16 @@ +=================== + Contributor Guide +=================== + +Code is hosted at `git.openstack.org`__. Submit bugs to the Nova project on +`Launchpad`__. Submit code to the `openstack/python-novaclient` project using +`Gerrit`__. + +__ https://git.openstack.org/cgit/openstack/python-novaclient +__ https://launchpad.net/nova +__ http://docs.openstack.org/infra/manual/developers.html#development-workflow + +.. toctree:: + :maxdepth: 2 + + testing diff --git a/doc/source/contributor/testing.rst b/doc/source/contributor/testing.rst new file mode 100644 index 000000000..c4e59143f --- /dev/null +++ b/doc/source/contributor/testing.rst @@ -0,0 +1,28 @@ +========= + Testing +========= + +The preferred way to run the unit tests is using ``tox``. There are multiple +test targets that can be run to validate the code. + +``tox -e pep8`` + + Style guidelines enforcement. + +``tox -e py27`` + + Traditional unit testing. + +``tox -e functional`` + + Live functional testing against an existing OpenStack instance. + +Functional testing assumes the existence of a `clouds.yaml` file as supported +by `os-client-config `__ +It assumes the existence of a cloud named `devstack` that behaves like a normal +DevStack installation with a demo and an admin user/tenant - or clouds named +`functional_admin` and `functional_nonadmin`. + +Refer to `Consistent Testing Interface`__ for more details. + +__ http://git.openstack.org/cgit/openstack/governance/tree/reference/project-testing-interface.rst diff --git a/doc/source/index.rst b/doc/source/index.rst index 4421c910d..4ee0122c1 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -1,13 +1,14 @@ -Python bindings to the OpenStack Nova API -========================================= +=========================================== + Python bindings to the OpenStack Nova API +=========================================== This is a client for OpenStack Nova API. There's :doc:`a Python API -` (the :mod:`novaclient` module), and a :doc:`command-line script -` (installed as :program:`nova`). Each implements the entire -OpenStack Nova API. +` (the :mod:`novaclient` module), and a :doc:`command-line +script ` (installed as :program:`nova`). Each implements the +entire OpenStack Nova API. -You'll need credentials for an OpenStack cloud that implements the -Compute API, such as TryStack, HP, or Rackspace, in order to use the nova client. +You'll need credentials for an OpenStack cloud that implements the Compute API, +such as TryStack, HP, or Rackspace, in order to use the nova client. .. seealso:: @@ -17,83 +18,10 @@ Compute API, such as TryStack, HP, or Rackspace, in order to use the nova client __ https://developer.openstack.org/api-guide/compute/index.html -Contents: - .. toctree:: :maxdepth: 2 - shell - api - ref/index - ref/v2/index - -Contributing -============ - -Code is hosted at `git.openstack.org`_. Submit bugs to the Nova project on -`Launchpad`_. Submit code to the openstack/python-novaclient project using -`Gerrit`_. - -.. _git.openstack.org: https://git.openstack.org/cgit/openstack/python-novaclient -.. _Launchpad: https://launchpad.net/nova -.. _Gerrit: http://docs.openstack.org/infra/manual/developers.html#development-workflow - -Testing -------- - -The preferred way to run the unit tests is using ``tox``. - -See `Consistent Testing Interface`_ for more details. - -.. _Consistent Testing Interface: http://git.openstack.org/cgit/openstack/governance/tree/reference/project-testing-interface.rst - -Deprecating commands -==================== - -There are times when commands need to be deprecated due to rename or removal. -The process for command deprecation is: - - 1. Push up a change for review which deprecates the command(s). - - - The change should print a deprecation warning to stderr each time a - deprecated command is used. - - That warning message should include a rough timeline for when the command - will be removed and what should be used instead, if anything. - - The description in the help text for the deprecated command should mark - that it is deprecated. - - The change should include a release note with the ``deprecations`` section - filled out. - - The deprecation cycle is typically the first client release *after* the - next *full* Nova server release so that there is at least six months of - deprecation. - - 2. Once the change is approved, have a member of the `nova-release`_ team - release a new version of python-novaclient. - - .. _nova-release: https://review.openstack.org/#/admin/groups/147,members - - 3. Example: ``_ - - This change was made while the Nova 12.0.0 Liberty release was in - development. The current version of python-novaclient at the time was - 2.25.0. Once the change was merged, python-novaclient 2.26.0 was released. - Since there was less than six months before 12.0.0 would be released, the - deprecation cycle ran through the 13.0.0 Nova server release. - - -Man Page -======== - -.. toctree:: - :maxdepth: 1 - - man/nova - - - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` + user/index + reference/index + cli/index + contributor/index diff --git a/doc/source/api.rst b/doc/source/reference/api/index.rst similarity index 96% rename from doc/source/api.rst rename to doc/source/reference/api/index.rst index 450d66d99..c5ed6d885 100644 --- a/doc/source/api.rst +++ b/doc/source/reference/api/index.rst @@ -1,5 +1,6 @@ -The :mod:`novaclient` Python API -================================ +================================== + The :mod:`novaclient` Python API +================================== .. module:: novaclient :synopsis: A client for the OpenStack Nova API. @@ -18,7 +19,6 @@ Here ``VERSION`` can be a string or ``novaclient.api_versions.APIVersion`` obj. If you prefer string value, you can use ``1.1`` (deprecated now), ``2`` or ``2.X`` (where X is a microversion). - Alternatively, you can create a client instance using the keystoneauth session API:: @@ -98,7 +98,6 @@ Then call methods on its managers:: .. _1493576: https://launchpad.net/bugs/1493576 - Reference --------- @@ -107,5 +106,5 @@ For more information, see the reference: .. toctree:: :maxdepth: 2 - ref/index - ref/v2/index + v2/index + exceptions diff --git a/doc/source/reference/deprecation-policy.rst b/doc/source/reference/deprecation-policy.rst new file mode 100644 index 000000000..0085bca90 --- /dev/null +++ b/doc/source/reference/deprecation-policy.rst @@ -0,0 +1,32 @@ +Deprecating commands +==================== + +There are times when commands need to be deprecated due to rename or removal. +The process for command deprecation is: + +1. Push up a change for review which deprecates the command(s). + + - The change should print a deprecation warning to ``stderr`` each time a + deprecated command is used. + - That warning message should include a rough timeline for when the command + will be removed and what should be used instead, if anything. + - The description in the help text for the deprecated command should mark + that it is deprecated. + - The change should include a release note with the ``deprecations`` section + filled out. + - The deprecation cycle is typically the first client release *after* the + next *full* nova server release so that there is at least six months of + deprecation. + +2. Once the change is approved, have a member of the `nova-release`_ team + release a new version of `python-novaclient`. + + .. _nova-release: https://review.openstack.org/#/admin/groups/147,members + +3. Example: ``_ + + This change was made while the nova 12.0.0 Liberty release was in + development. The current version of `python-novaclient` at the time was + 2.25.0. Once the change was merged, `python-novaclient` 2.26.0 was released. + Since there was less than six months before 12.0.0 would be released, the + deprecation cycle ran through the 13.0.0 nova server release. diff --git a/doc/source/reference/index.rst b/doc/source/reference/index.rst new file mode 100644 index 000000000..92e93855a --- /dev/null +++ b/doc/source/reference/index.rst @@ -0,0 +1,8 @@ +Reference +========= + +.. toctree:: + :maxdepth: 1 + + api/index + deprecation-policy diff --git a/doc/source/user/index.rst b/doc/source/user/index.rst new file mode 100644 index 000000000..3c54920b2 --- /dev/null +++ b/doc/source/user/index.rst @@ -0,0 +1,8 @@ +============ + User Guide +============ + +.. toctree:: + :maxdepth: 2 + + shell diff --git a/doc/source/shell.rst b/doc/source/user/shell.rst similarity index 57% rename from doc/source/shell.rst rename to doc/source/user/shell.rst index 6f72c3a38..b2d7d9ef8 100644 --- a/doc/source/shell.rst +++ b/doc/source/user/shell.rst @@ -1,19 +1,20 @@ -The :program:`nova` shell utility -================================= +=================================== + The :program:`nova` Shell Utility +=================================== .. program:: nova .. highlight:: bash -The :program:`nova` shell utility interacts with OpenStack Nova API -from the command line. It supports the entirety of the OpenStack Nova API. +The :program:`nova` shell utility interacts with OpenStack Nova API from the +command line. It supports the entirety of the OpenStack Nova API. -First, you'll need an OpenStack Nova account and an API key. You get this -by using the `nova-manage` command in OpenStack Nova. +First, you'll need an OpenStack Nova account and an API key. You get this by +using the `nova-manage` command in OpenStack Nova. You'll need to provide :program:`nova` with your OpenStack username and API key. You can do this with the `--os-username`, `--os-password` and `--os-tenant-id` options, but it's easier to just set them as environment -variables by setting two environment variables: +variables by setting some environment variables: .. envvar:: OS_USERNAME @@ -35,18 +36,32 @@ variables by setting two environment variables: The OpenStack API version. +.. envvar:: OS_REGION_NAME + + The Keystone region name. Defaults to the first region if multiple regions + are available. + For example, in Bash you'd use:: export OS_USERNAME=yourname export OS_PASSWORD=yadayadayada export OS_TENANT_NAME=myproject - export OS_AUTH_URL=http://... - export OS_COMPUTE_API_VERSION=2 + export OS_AUTH_URL=http://:5000/v3/ + export OS_COMPUTE_API_VERSION=2.1 From there, all shell commands take the form:: nova [arguments...] -Run :program:`nova help` to get a full list of all possible commands, -and run :program:`nova help ` to get detailed help for that -command. +Run :program:`nova help` to get a full list of all possible commands, and run +:program:`nova help ` to get detailed help for that command. + +Reference +--------- + +For more information, see the reference: + +.. toctree:: + :maxdepth: 2 + + /cli/nova From 945e155f05d3a76fa55c68c77564b136ee3f9ba1 Mon Sep 17 00:00:00 2001 From: chenxing Date: Tue, 27 Jun 2017 05:31:57 +0000 Subject: [PATCH 1324/1705] doc: Switch from oslosphinx to openstackdocstheme Change-Id: I4c242007dedf74821acae926329f4a59cedae6d5 --- doc/source/conf.py | 18 +++++++++++++++--- releasenotes/source/conf.py | 4 ++-- test-requirements.txt | 2 +- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index 19d10bde6..46a7ae337 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -79,7 +79,10 @@ def get_module_names(): # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', 'oslosphinx'] +extensions = [ + 'sphinx.ext.autodoc', + 'openstackdocstheme', +] autoclass_content = 'both' @@ -92,7 +95,10 @@ def get_module_names(): # The master toctree document. master_doc = 'index' -# General information about the project. +# openstackdocstheme options +repository_name = 'openstack/python-novaclient' +bug_project = 'python-novaclient' +bug_tag = 'doc' project = 'python-novaclient' copyright = 'OpenStack Contributors' @@ -114,7 +120,13 @@ def get_module_names(): # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. -html_theme = 'default' +html_theme = 'openstackdocs' + +# -- Options for openstackdocstheme ------------------------------------------- + +repository_name = 'openstack/python-novaclient' +bug_project = 'python-novaclient' +bug_tag = '' # -- Options for manual page output ------------------------------------------ diff --git a/releasenotes/source/conf.py b/releasenotes/source/conf.py index 876063986..7a743acac 100644 --- a/releasenotes/source/conf.py +++ b/releasenotes/source/conf.py @@ -26,8 +26,8 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'oslosphinx', 'reno.sphinxext', + 'openstackdocstheme', ] # Add any paths that contain templates here, relative to this directory. @@ -100,7 +100,7 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'default' +html_theme = 'openstackdocs' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the diff --git a/test-requirements.txt b/test-requirements.txt index 4514a4666..d3cbba2b0 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -15,7 +15,7 @@ python-neutronclient>=6.3.0 # Apache-2.0 requests-mock>=1.1 # Apache-2.0 sphinx>=1.6.2 # BSD os-client-config>=1.27.0 # Apache-2.0 -oslosphinx>=4.7.0 # Apache-2.0 +openstackdocstheme>=1.11.0 # Apache-2.0 osprofiler>=1.4.0 # Apache-2.0 testrepository>=0.0.18 # Apache-2.0/BSD testscenarios>=0.4 # Apache-2.0/BSD From 437b08cf38f836798d51b9736c5af9217f925897 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 4 Jul 2017 17:35:31 +0100 Subject: [PATCH 1325/1705] Remove custom autodoc implementation pbr provides this functionality for us. Make use of it. Change-Id: Ide7b2992ac931395b776d39ed4a1a50bded0a881 --- doc/source/conf.py | 65 +----------------------------- doc/source/reference/api/index.rst | 3 +- novaclient/utils.py | 9 +++-- setup.cfg | 5 +++ 4 files changed, 13 insertions(+), 69 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index 46a7ae337..c252baab1 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -12,69 +12,6 @@ # # python-novaclient documentation build configuration file -import os -import sys - -BASE_DIR = os.path.dirname(os.path.abspath(__file__)) -ROOT = os.path.abspath(os.path.join(BASE_DIR, "..", "..")) - -sys.path.insert(0, ROOT) -sys.path.insert(0, BASE_DIR) - - -# TODO(stephenfin): This looks like something that pbr's autodoc integration -# could be doing for us. Investigate. - -def gen_ref(ver, title, names): - refdir = os.path.join(BASE_DIR, "reference", "api") - pkg = "novaclient" - if ver: - pkg = "%s.%s" % (pkg, ver) - refdir = os.path.join(refdir, ver) - if not os.path.exists(refdir): - os.makedirs(refdir) - - # we don't want to write index files for top-level directories - only - # sub-directories - if ver: - idxpath = os.path.join(refdir, "index.rst") - with open(idxpath, "w") as idx: - idx.write(("%(title)s\n" - "%(signs)s\n" - "\n" - ".. toctree::\n" - " :maxdepth: 1\n" - "\n") % {"title": title, "signs": "=" * len(title)}) - for name in names: - idx.write(" %s\n" % name) - - for name in names: - rstpath = os.path.join(refdir, "%s.rst" % name) - with open(rstpath, "w") as rst: - rst.write(("%(title)s\n" - "%(signs)s\n" - "\n" - ".. automodule:: %(pkg)s.%(name)s\n" - " :members:\n" - " :undoc-members:\n" - " :show-inheritance:\n" - " :noindex:\n") - % {"title": name.capitalize(), - "signs": "=" * len(name), - "pkg": pkg, "name": name}) - - -def get_module_names(): - names = os.listdir(os.path.join(ROOT, 'novaclient', 'v2')) - exclude = ['shell.py', '__init__.py'] - for name in names: - if name.endswith('.py') and name not in exclude: - yield name.strip('.py') - - -gen_ref(None, "Exceptions", ["exceptions"]) -gen_ref("v2", "Version 2 API", sorted(get_module_names())) - # -- General configuration ---------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be @@ -84,6 +21,8 @@ def get_module_names(): 'openstackdocstheme', ] +# The content that will be inserted into the main body of an autoclass +# directive. autoclass_content = 'both' # Add any paths that contain templates here, relative to this directory. diff --git a/doc/source/reference/api/index.rst b/doc/source/reference/api/index.rst index c5ed6d885..e7b11e491 100644 --- a/doc/source/reference/api/index.rst +++ b/doc/source/reference/api/index.rst @@ -106,5 +106,4 @@ For more information, see the reference: .. toctree:: :maxdepth: 2 - v2/index - exceptions + autoindex diff --git a/novaclient/utils.py b/novaclient/utils.py index e93df6999..451b2372d 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -103,8 +103,11 @@ def add_arg(func, *args, **kwargs): def service_type(stype): """Adds 'service_type' attribute to decorated function. + Usage: + .. code-block:: python + @service_type('volume') def mymethod(f): ... @@ -436,11 +439,9 @@ def record_time(times, enabled, *args): """Record the time of a specific action. :param times: A list of tuples holds time data. - :type times: list :param enabled: Whether timing is enabled. - :type enabled: bool - :param *args: Other data to be stored besides time data, these args - will be joined to a string. + :param args: Other data to be stored besides time data, these args + will be joined to a string. """ if not enabled: yield diff --git a/setup.cfg b/setup.cfg index 5aa68c2bc..3a8087ff3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -39,6 +39,11 @@ build-dir = doc/build [upload_sphinx] upload-dir = doc/build/html +[pbr] +autodoc_index_modules = True +autodoc_exclude_modules = novaclient.tests.* novaclient.v2.contrib.* +api_doc_dir = reference/api + [compile_catalog] domain = novaclient directory = novaclient/locale From 77f940c5345d662fd7adef410ea0989e35fa2664 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Thu, 13 Jul 2017 14:24:25 +0000 Subject: [PATCH 1326/1705] Updated from global requirements Change-Id: I4d947576ce0e8bfe1fda9d662faac7d32c1f32f3 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 4514a4666..c86626459 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -20,7 +20,7 @@ osprofiler>=1.4.0 # Apache-2.0 testrepository>=0.0.18 # Apache-2.0/BSD testscenarios>=0.4 # Apache-2.0/BSD testtools>=1.4.0 # MIT -tempest>=14.0.0 # Apache-2.0 +tempest>=16.1.0 # Apache-2.0 # releasenotes reno!=2.3.1,>=1.8.0 # Apache-2.0 From 5bfa57a433175b8bae750125b95a73650aa663b1 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Wed, 12 Jul 2017 14:10:07 -0400 Subject: [PATCH 1327/1705] Microversion 2.50 - fix quota class sets resource usage This adds support for the 2.50 microversion which does the following: * Adds the server_groups and server_groups_members resources to the output for the 'nova quota-class-show' and 'nova quota-class-update' CLIs. * Removes the ability to show or update network-related resource quota class values, specifically floating_ips, fixed_ips, security_groups and security_group_members. * Defines explicit kwargs for the update() method in the python API binding. This also fixes a problem where the 'nova quota-class-update' CLI was incorrectly capped at the 2.35 microversion for updating network-related resources. That was true for the os-quota-sets API which is tenant-specific, but not for the os-quota-class-sets API which is global. Functional tests are added for the 2.1 and 2.50 microversion behavior for both commands. Part of blueprint fix-quota-classes-api Change-Id: I2531f9094d92e1b9ed36ab03bc43ae1be5290790 --- novaclient/__init__.py | 2 +- .../tests/functional/v2/test_quota_classes.py | 133 ++++++++++++++++++ novaclient/tests/unit/v2/fakes.py | 29 ++++ .../tests/unit/v2/test_quota_classes.py | 52 +++++++ novaclient/v2/quota_classes.py | 39 +++++ novaclient/v2/shell.py | 13 +- .../microversion-v2_50-4f484658d66d01aa.yaml | 32 +++++ 7 files changed, 295 insertions(+), 5 deletions(-) create mode 100644 novaclient/tests/functional/v2/test_quota_classes.py create mode 100644 releasenotes/notes/microversion-v2_50-4f484658d66d01aa.yaml diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 768ad119a..0ce0f7c44 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.49") +API_MAX_VERSION = api_versions.APIVersion("2.50") diff --git a/novaclient/tests/functional/v2/test_quota_classes.py b/novaclient/tests/functional/v2/test_quota_classes.py new file mode 100644 index 000000000..399f53968 --- /dev/null +++ b/novaclient/tests/functional/v2/test_quota_classes.py @@ -0,0 +1,133 @@ +# Copyright 2017 Huawei Technologies Co.,LTD. +# +# 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 tempest.lib import exceptions + +from novaclient.tests.functional import base + + +class TestQuotaClassesNovaClient(base.ClientTestBase): + """Nova quota classes functional tests for the v2.1 microversion.""" + + COMPUTE_API_VERSION = '2.1' + + # The list of quota class resources we expect in the output table. + _included_resources = ['instances', 'cores', 'ram', + 'floating_ips', 'fixed_ips', 'metadata_items', + 'injected_files', 'injected_file_content_bytes', + 'injected_file_path_bytes', 'key_pairs', + 'security_groups', 'security_group_rules'] + + # The list of quota class resources we do not expect in the output table. + _excluded_resources = ['server_groups', 'server_group_members'] + + # Any resources that are not shown but can be updated. For example, before + # microversion 2.50 you can update server_groups and server_groups_members + # quota class values but they are not shown in the GET response. + _extra_update_resources = _excluded_resources + + # The list of resources which are blocked from being updated. + _blocked_update_resources = [] + + def _get_quota_class_name(self): + """Returns a fake quota class name specific to this test class.""" + return 'fake-class-%s' % self.COMPUTE_API_VERSION.replace('.', '-') + + def _verify_qouta_class_show_output(self, output, expected_values): + # Assert that the expected key/value pairs are in the output table + for quota_name in self._included_resources: + # First make sure the resource is actually in expected quota. + self.assertIn(quota_name, expected_values) + expected_value = expected_values[quota_name] + actual_value = self._get_value_from_the_table(output, quota_name) + self.assertEqual(expected_value, actual_value) + + # Now make sure anything that we don't expect in the output table is + # actually not showing up. + for quota_name in self._excluded_resources: + # ValueError is raised when the key isn't found in the table. + self.assertRaises(ValueError, + self._get_value_from_the_table, + output, quota_name) + + def test_quota_class_show(self): + """Tests showing quota class values for a fake non-existing quota + class. The API will return the defaults if the quota class does not + actually exist. We use a fake class to avoid any interaction with the + real default quota class values. + """ + default_quota_class_set = self.client.quota_classes.get('default') + default_values = { + quota_name: str(getattr(default_quota_class_set, quota_name)) + for quota_name in self._included_resources + } + output = self.nova('quota-class-show %s' % + self._get_quota_class_name()) + self._verify_qouta_class_show_output(output, default_values) + + def test_quota_class_update(self): + """Tests updating a fake quota class. The way this works in the API + is that if the quota class is not found, it is created. So in this + test we can use a fake quota class with fake values and they will all + get set. We don't use the default quota class because it is global + and we don't want to interfere with other tests. + """ + class_name = self._get_quota_class_name() + params = [class_name] + expected_values = {} + for quota_name in ( + self._included_resources + self._extra_update_resources): + params.append("--%s 99" % quota_name.replace("_", "-")) + expected_values[quota_name] = '99' + + # Note that the quota-class-update CLI doesn't actually output any + # information from the response. + self.nova("quota-class-update", params=" ".join(params)) + # Assert the results using the quota-class-show output. + output = self.nova('quota-class-show %s' % class_name) + self._verify_qouta_class_show_output(output, expected_values) + + # Assert that attempting to update resources that are blocked will + # result in a failure. + for quota_name in self._blocked_update_resources: + self.assertRaises( + exceptions.CommandFailed, + self.nova, "quota-class-update %s --%s 99" % + (class_name, quota_name.replace("_", "-"))) + + +class TestQuotasNovaClient2_50(TestQuotaClassesNovaClient): + """Nova quota classes functional tests for the v2.50 microversion.""" + + COMPUTE_API_VERSION = '2.50' + + # The 2.50 microversion added the server_groups and server_group_members + # to the response, and filtered out floating_ips, fixed_ips, + # security_groups and security_group_members, similar to the 2.36 + # microversion in the os-qouta-sets API. + _included_resources = ['instances', 'cores', 'ram', 'metadata_items', + 'injected_files', 'injected_file_content_bytes', + 'injected_file_path_bytes', 'key_pairs', + 'server_groups', 'server_group_members'] + + # The list of quota class resources we do not expect in the output table. + _excluded_resources = ['floating_ips', 'fixed_ips', + 'security_groups', 'security_group_rules'] + + # In 2.50, server_groups and server_group_members can be both updated + # in a PUT request and shown in a GET response. + _extra_update_resources = [] + + # In 2.50, you can't update the network-related resources. + _blocked_update_resources = _excluded_resources diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index c2eb36bd8..1ebf1e3ea 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -1289,6 +1289,20 @@ def delete_os_quota_sets_97f4c221bff44578b0300df4ef119353(self, **kw): # def get_os_quota_class_sets_test(self, **kw): + if self.api_version >= api_versions.APIVersion('2.50'): + return (200, FAKE_RESPONSE_HEADERS, { + 'quota_class_set': { + 'id': 'test', + 'metadata_items': 1, + 'injected_file_content_bytes': 1, + 'injected_file_path_bytes': 1, + 'ram': 1, + 'instances': 1, + 'injected_files': 1, + 'cores': 1, + 'key_pairs': 1, + 'server_groups': 1, + 'server_group_members': 1}}) return (200, FAKE_RESPONSE_HEADERS, { 'quota_class_set': { 'id': 'test', @@ -1297,6 +1311,7 @@ def get_os_quota_class_sets_test(self, **kw): 'injected_file_path_bytes': 1, 'ram': 1, 'floating_ips': 1, + 'fixed_ips': -1, 'instances': 1, 'injected_files': 1, 'cores': 1, @@ -1306,6 +1321,19 @@ def get_os_quota_class_sets_test(self, **kw): def put_os_quota_class_sets_test(self, body, **kw): assert list(body) == ['quota_class_set'] + if self.api_version >= api_versions.APIVersion('2.50'): + return (200, {}, { + 'quota_class_set': { + 'metadata_items': 1, + 'injected_file_content_bytes': 1, + 'injected_file_path_bytes': 1, + 'ram': 1, + 'instances': 1, + 'injected_files': 1, + 'cores': 1, + 'key_pairs': 1, + 'server_groups': 1, + 'server_group_members': 1}}) return (200, {}, { 'quota_class_set': { 'metadata_items': 1, @@ -1313,6 +1341,7 @@ def put_os_quota_class_sets_test(self, body, **kw): 'injected_file_path_bytes': 1, 'ram': 1, 'floating_ips': 1, + 'fixed_ips': -1, 'instances': 1, 'injected_files': 1, 'cores': 1, diff --git a/novaclient/tests/unit/v2/test_quota_classes.py b/novaclient/tests/unit/v2/test_quota_classes.py index a1da954d1..e9d9ad75a 100644 --- a/novaclient/tests/unit/v2/test_quota_classes.py +++ b/novaclient/tests/unit/v2/test_quota_classes.py @@ -28,12 +28,14 @@ def test_class_quotas_get(self): q = self.cs.quota_classes.get(class_name) self.assert_request_id(q, fakes.FAKE_REQUEST_ID_LIST) self.cs.assert_called('GET', '/os-quota-class-sets/%s' % class_name) + return q def test_update_quota(self): q = self.cs.quota_classes.get('test') self.assert_request_id(q, fakes.FAKE_REQUEST_ID_LIST) q.update(cores=2) self.cs.assert_called('PUT', '/os-quota-class-sets/test') + return q def test_refresh_quota(self): q = self.cs.quota_classes.get('test') @@ -43,3 +45,53 @@ def test_refresh_quota(self): self.assertNotEqual(q.cores, q2.cores) q2.get() self.assertEqual(q.cores, q2.cores) + + +class QuotaClassSetsTest2_50(QuotaClassSetsTest): + """Tests the quota classes API binding using the 2.50 microversion.""" + def setUp(self): + super(QuotaClassSetsTest2_50, self).setUp() + self.cs = fakes.FakeClient(api_versions.APIVersion("2.50")) + + def test_class_quotas_get(self): + """Tests that network-related resources aren't in a 2.50 response + and server group related resources are in the response. + """ + q = super(QuotaClassSetsTest2_50, self).test_class_quotas_get() + for invalid_resource in ('floating_ips', 'fixed_ips', 'networks', + 'security_groups', 'security_group_rules'): + self.assertFalse(hasattr(q, invalid_resource), + '%s should not be in %s' % (invalid_resource, q)) + # Also make sure server_groups and server_group_members are in the + # response. + for valid_resource in ('server_groups', 'server_group_members'): + self.assertTrue(hasattr(q, valid_resource), + '%s should be in %s' % (invalid_resource, q)) + + def test_update_quota(self): + """Tests that network-related resources aren't in a 2.50 response + and server group related resources are in the response. + """ + q = super(QuotaClassSetsTest2_50, self).test_update_quota() + for invalid_resource in ('floating_ips', 'fixed_ips', 'networks', + 'security_groups', 'security_group_rules'): + self.assertFalse(hasattr(q, invalid_resource), + '%s should not be in %s' % (invalid_resource, q)) + # Also make sure server_groups and server_group_members are in the + # response. + for valid_resource in ('server_groups', 'server_group_members'): + self.assertTrue(hasattr(q, valid_resource), + '%s should be in %s' % (invalid_resource, q)) + + def test_update_quota_invalid_resources(self): + """Tests trying to update quota class values for invalid resources. + + This will fail with TypeError because the network-related resource + kwargs aren't defined. + """ + q = self.cs.quota_classes.get('test') + self.assertRaises(TypeError, q.update, floating_ips=1) + self.assertRaises(TypeError, q.update, fixed_ips=1) + self.assertRaises(TypeError, q.update, security_groups=1) + self.assertRaises(TypeError, q.update, security_group_rules=1) + self.assertRaises(TypeError, q.update, networks=1) diff --git a/novaclient/v2/quota_classes.py b/novaclient/v2/quota_classes.py index 4a38a970c..eae5bfdec 100644 --- a/novaclient/v2/quota_classes.py +++ b/novaclient/v2/quota_classes.py @@ -13,6 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient import api_versions from novaclient import base @@ -32,6 +33,10 @@ def get(self, class_name): def _update_body(self, **kwargs): return {'quota_class_set': kwargs} + # NOTE(mriedem): Before 2.50 the resources you could update was just a + # kwargs dict and not validated on the client-side, only on the API server + # side. + @api_versions.wraps("2.0", "2.49") def update(self, class_name, **kwargs): body = self._update_body(**kwargs) @@ -42,3 +47,37 @@ def update(self, class_name, **kwargs): return self._update('/os-quota-class-sets/%s' % (class_name), body, 'quota_class_set') + + # NOTE(mriedem): 2.50 does strict validation of the resources you can + # specify since the network-related resources are blocked in 2.50. + @api_versions.wraps("2.50") + def update(self, class_name, instances=None, cores=None, ram=None, + metadata_items=None, injected_files=None, + injected_file_content_bytes=None, injected_file_path_bytes=None, + key_pairs=None, server_groups=None, server_group_members=None): + resources = {} + if instances is not None: + resources['instances'] = instances + if cores is not None: + resources['cores'] = cores + if ram is not None: + resources['ram'] = ram + if metadata_items is not None: + resources['metadata_items'] = metadata_items + if injected_files is not None: + resources['injected_files'] = injected_files + if injected_file_content_bytes is not None: + resources['injected_file_content_bytes'] = ( + injected_file_content_bytes) + if injected_file_path_bytes is not None: + resources['injected_file_path_bytes'] = injected_file_path_bytes + if key_pairs is not None: + resources['key_pairs'] = key_pairs + if server_groups is not None: + resources['server_groups'] = server_groups + if server_group_members is not None: + resources['server_group_members'] = server_group_members + + body = {'quota_class_set': resources} + return self._update('/os-quota-class-sets/%s' % class_name, body, + 'quota_class_set') diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 72d71e2b1..e390c26e0 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -3827,6 +3827,11 @@ def do_ssh(cs, args): os.system(cmd) +# NOTE(mriedem): In the 2.50 microversion, the os-quota-class-sets API +# will return the server_groups and server_group_members, but no longer +# return floating_ips, fixed_ips, security_groups or security_group_members +# as those are deprecated as networking service proxies and/or because +# nova-network is deprecated. Similar to the 2.36 microversion. _quota_resources = ['instances', 'cores', 'ram', 'floating_ips', 'fixed_ips', 'metadata_items', 'injected_files', 'injected_file_content_bytes', @@ -4137,7 +4142,7 @@ def do_quota_class_show(cs, args): _quota_show(cs.quota_classes.get(args.class_name)) -@api_versions.wraps("2.0", "2.35") +@api_versions.wraps("2.0", "2.49") @utils.arg( 'class_name', metavar='', @@ -4233,9 +4238,9 @@ def do_quota_class_update(cs, args): _quota_update(cs.quota_classes, args.class_name, args) -# 2.36 does not support updating quota for floating IPs, fixed IPs, security -# groups or security group rules. -@api_versions.wraps("2.36") +# 2.50 does not support updating quota class values for floating IPs, +# fixed IPs, security groups or security group rules. +@api_versions.wraps("2.50") @utils.arg( 'class_name', metavar='', diff --git a/releasenotes/notes/microversion-v2_50-4f484658d66d01aa.yaml b/releasenotes/notes/microversion-v2_50-4f484658d66d01aa.yaml new file mode 100644 index 000000000..3ca1908cd --- /dev/null +++ b/releasenotes/notes/microversion-v2_50-4f484658d66d01aa.yaml @@ -0,0 +1,32 @@ +--- +fixes: + - | + Adds support for the ``2.50`` microversion which fixes the + ``nova quota-class-show`` and ``nova quota-class-update`` commands in the + following ways: + + * The ``server_groups`` and ``server_group_members`` quota resources will + now be shown in the output table for ``nova quota-class-show``. + * The ``floating_ips``, ``fixed_ips``, ``security_groups`` and + ``security_group_rules`` quota resources will no longer be able to + be updated using ``nova quota-class-update`` nor will they be shown in + the output of ``nova quota-class-show``. Use python-openstackclient or + python-neutronclient to work with quotas for network resources. + + In addition, the ``nova quota-class-update`` CLI was previously incorrectly + limiting the ability to update quota class values for ``floating_ips``, + ``fixed_ips``, ``security_groups`` and ``security_group_rules`` based on + the 2.36 microversion. That has been changed to limit based on the ``2.50`` + microversion. +upgrade: + - | + The ``novaclient.v2.quota_classes.QuotaClassSetManager.update`` method + now defines specific kwargs starting with microversion ``2.50`` since + updating network-related resource quota class values is not supported on + the server with microversion ``2.50``. The list of excluded resources is: + + - ``fixed_ips`` + - ``floating_ips`` + - ``networks`` + - ``security_groups`` + - ``security_group_rules`` From 66c11374aad1924adc947a3fd94ce56b42013c66 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Wed, 12 Jul 2017 21:29:32 -0400 Subject: [PATCH 1328/1705] Add support for the 2.51 microversion The 2.51 microversion adds the 'volume-extended' server external event to the os-server-external-events API. This is an admin-only API by default and this event is currently only used by Cinder as part of extending the size of an attached volume, and therefore does not have any CLI or python API binding impacts in the client. Part of blueprint nova-support-attached-volume-extend Change-Id: I8293704dbb4f75306fe32d3a0118d5bf42c8457e --- novaclient/__init__.py | 2 +- novaclient/tests/unit/v2/test_shell.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 0ce0f7c44..d6ebb9638 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.50") +API_MAX_VERSION = api_versions.APIVersion("2.51") diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 64b7c931b..dbda0b7d7 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -3019,6 +3019,7 @@ def test_versions(self): 47, # NOTE(cfriesen): 47 adds support for flavor details embedded # within the server details 48, # There are no version-wrapped shell method changes for this. + 51, # There are no version-wrapped shell method changes for this. ]) versions_supported = set(range(0, novaclient.API_MAX_VERSION.ver_minor + 1)) From 8ab54527f8bfd62049dcfc3c77ba8f28c99ee322 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Thu, 13 Jul 2017 17:13:46 -0400 Subject: [PATCH 1329/1705] Add functional test for resize-confirm plus quota validation With the upcoming counting quotas changes in Nova, and the fact the novaclient functional tests are run in serial with the same tenant, it would be useful to validate that "nova resize-confirm" works as expected and that the quota usage before, during and after the resize is confirmed is as expected, that is, when resizing we expect the quota usage to change. Change-Id: Id5be8edfde446b9e714460a606b446a2daf64007 --- novaclient/tests/functional/base.py | 15 +++ novaclient/tests/functional/v2/test_resize.py | 104 ++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 novaclient/tests/functional/v2/test_resize.py diff --git a/novaclient/tests/functional/base.py b/novaclient/tests/functional/base.py index a374601f6..637ae52b9 100644 --- a/novaclient/tests/functional/base.py +++ b/novaclient/tests/functional/base.py @@ -484,6 +484,11 @@ def _create_server(self, name=None, with_network=True, add_cleanup=True, 'building', ['active']) return server + def _wait_for_state_change(self, server_id, status): + novaclient.v2.shell._poll_for_status( + self.client.servers.get, server_id, None, [status], + show_progress=False, poll_period=1, silent=True) + def _get_project_id(self, name): """Obtain project id by project name.""" if self.keystone.version == "v3": @@ -497,6 +502,16 @@ def _cleanup_server(self, server_id): self.client.servers.delete(server_id) self.wait_for_resource_delete(server_id, self.client.servers) + def _get_absolute_limits(self): + """Returns the absolute limits (quota usage) including reserved quota + usage for the given tenant running the test. + + :return: A dict where the key is the limit (or usage) and value. + """ + # The absolute limits are returned in a generator so convert to a dict. + return {limit.name: limit.value + for limit in self.client.limits.get(reserved=True).absolute} + class TenantTestBase(ClientTestBase): """Base test class for additional tenant and user creation which diff --git a/novaclient/tests/functional/v2/test_resize.py b/novaclient/tests/functional/v2/test_resize.py new file mode 100644 index 000000000..6884e4ba5 --- /dev/null +++ b/novaclient/tests/functional/v2/test_resize.py @@ -0,0 +1,104 @@ +# Copyright 2017 Huawei Technologies Co.,LTD. +# +# 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 novaclient.tests.functional import base + + +class TestServersResize(base.ClientTestBase): + """Servers resize functional tests.""" + + COMPUTE_API_VERSION = '2.1' + + def _create_server(self, name, flavor): + """Boots a server with the given name and flavor and waits for it to + be ACTIVE. + """ + params = ( + "%(name)s --flavor %(flavor)s --image %(image)s --poll " % { + "name": self.name_generate(name), + "flavor": flavor, + "image": self.image.id}) + # check to see if we have to pass in a network id + if self.multiple_networks: + params += ' --nic net-id=%s' % self.network.id + server_info = self.nova("boot", params=params) + server_id = self._get_value_from_the_table(server_info, "id") + self.addCleanup(self._cleanup_server, server_id) + return server_id + + def _pick_alternate_flavor(self): + """Given the flavor picked in the base class setup, this finds the + opposite flavor to use for a resize test. For example, if m1.nano is + the flavor, then use m1.micro, but those are only available if Tempest + is configured. If m1.tiny, then use m1.small. + """ + flavor_name = self.flavor.name + if flavor_name == 'm1.nano': + # This is an upsize test. + return 'm1.micro' + if flavor_name == 'm1.micro': + # This is a downsize test. + return 'm1.nano' + if flavor_name == 'm1.tiny': + # This is an upsize test. + return 'm1.small' + if flavor_name == 'm1.small': + # This is a downsize test. + return 'm1.tiny' + self.fail('Unable to find alternate for flavor: %s' % flavor_name) + + def _compare_quota_usage(self, old_usage, new_usage, expect_diff=True): + """Compares the quota usage in the provided AbsoluteLimits.""" + # For a resize, instance usage shouldn't change. + self.assertEqual(old_usage['totalInstancesUsed'], + new_usage['totalInstancesUsed'], + 'totalInstancesUsed does not match') + # For the resize we're doing, those flavors have the same vcpus so we + # don't expect any quota change. + self.assertEqual(old_usage['totalCoresUsed'], + new_usage['totalCoresUsed'], + 'totalCoresUsed does not match') + # RAM is the only thing that will change for these flavors in a resize. + if expect_diff: + self.assertNotEqual(old_usage['totalRAMUsed'], + new_usage['totalRAMUsed'], + 'totalRAMUsed should have changed') + else: + self.assertEqual(old_usage['totalRAMUsed'], + new_usage['totalRAMUsed'], + 'totalRAMUsed does not match') + + def test_resize_up_confirm(self): + """Tests creating a server and resizes up and confirms the resize. + Compares quota before, during and after the resize. + """ + server_id = self._create_server('resize-up-confirm', self.flavor.name) + # get the starting quota now that we've created a server + starting_usage = self._get_absolute_limits() + # now resize up + alternate_flavor = self._pick_alternate_flavor() + self.nova('resize', + params='%s %s --poll' % (server_id, alternate_flavor)) + resize_usage = self._get_absolute_limits() + # compare the starting usage against the resize usage + self._compare_quota_usage(starting_usage, resize_usage) + # now confirm the resize + self.nova('resize-confirm', params='%s' % server_id) + # we have to wait for the server to be ACTIVE before we can check quota + self._wait_for_state_change(server_id, 'active') + # get the final quota usage which should be the same as the resize + # usage before confirm + confirm_usage = self._get_absolute_limits() + self._compare_quota_usage( + resize_usage, confirm_usage, expect_diff=False) From 9c21ad0db3087fae0f1404a88ecabcda928cf765 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Fri, 14 Jul 2017 17:17:10 -0400 Subject: [PATCH 1330/1705] Add resize down test which also verifies quota changes This adds a functional test which creates a server at a larger flavor and then resizes it down to a smaller flavor, and then reverts the resize. It checks quota before, during and after the resize. This is an important wrinkle with the counting quotas changes happening in the server in Pike. Change-Id: If1bb59e513b91a99fae2b6825248a40d8e7ac7b9 --- novaclient/tests/functional/v2/test_resize.py | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/novaclient/tests/functional/v2/test_resize.py b/novaclient/tests/functional/v2/test_resize.py index 6884e4ba5..25526024f 100644 --- a/novaclient/tests/functional/v2/test_resize.py +++ b/novaclient/tests/functional/v2/test_resize.py @@ -102,3 +102,50 @@ def test_resize_up_confirm(self): confirm_usage = self._get_absolute_limits() self._compare_quota_usage( resize_usage, confirm_usage, expect_diff=False) + + def _create_resize_down_flavors(self): + """Creates two flavors with different size ram but same size vcpus + and disk. + + :returns: tuple of (larger_flavor_name, smaller_flavor_name) + """ + self.nova('flavor-create', params='resize-larger-flavor auto 128 0 1') + self.addCleanup( + self.nova, 'flavor-delete', params='resize-larger-flavor') + + self.nova('flavor-create', params='resize-smaller-flavor auto 64 0 1') + self.addCleanup( + self.nova, 'flavor-delete', params='resize-smaller-flavor') + + return 'resize-larger-flavor', 'resize-smaller-flavor' + + def test_resize_down_revert(self): + """Tests creating a server and resizes down and reverts the resize. + Compares quota before, during and after the resize. + """ + # devstack's m1.tiny and m1.small have different size disks so we + # can't use those as you can't resize down the disk. So we have to + # create our own flavors. + larger_flavor, smaller_flavor = self._create_resize_down_flavors() + # Now create the server with the larger flavor. + server_id = self._create_server('resize-down-revert', larger_flavor) + # get the starting quota now that we've created a server + starting_usage = self._get_absolute_limits() + # now resize down + self.nova('resize', + params='%s %s --poll' % (server_id, smaller_flavor)) + resize_usage = self._get_absolute_limits() + # compare the starting usage against the resize usage; in the case of + # a resize down we don't expect usage to change until it's confirmed, + # which doesn't happen in this test since we revert + self._compare_quota_usage( + starting_usage, resize_usage, expect_diff=False) + # now revert the resize + self.nova('resize-revert', params='%s' % server_id) + # we have to wait for the server to be ACTIVE before we can check quota + self._wait_for_state_change(server_id, 'active') + # get the final quota usage which should be the same as the resize + # usage before revert + revert_usage = self._get_absolute_limits() + self._compare_quota_usage( + resize_usage, revert_usage, expect_diff=False) From baa23254ec4cedf21a03c7417553edddbba5cacd Mon Sep 17 00:00:00 2001 From: zhangdaolong Date: Sun, 16 Jul 2017 02:09:00 +0800 Subject: [PATCH 1331/1705] Fix the inappropriate parameter name The argument to the function should be the volume ID, not the attachment ID.So the argument name of the function here is easy to misunderstand. Change-Id: Ie093b6acebe8b9e8dbdfed89856666db25bcf756 --- novaclient/tests/unit/v2/test_volumes.py | 38 +++++++++++++++++++ novaclient/v2/volumes.py | 47 ++++++++++++++++++++---- 2 files changed, 77 insertions(+), 8 deletions(-) diff --git a/novaclient/tests/unit/v2/test_volumes.py b/novaclient/tests/unit/v2/test_volumes.py index 60178cb02..932b71d79 100644 --- a/novaclient/tests/unit/v2/test_volumes.py +++ b/novaclient/tests/unit/v2/test_volumes.py @@ -13,6 +13,8 @@ # License for the specific language governing permissions and limitations # under the License. +import mock + from novaclient import api_versions from novaclient.tests.unit import utils from novaclient.tests.unit.v2 import fakes @@ -55,6 +57,24 @@ def test_get_server_volume(self): '/servers/1234/os-volume_attachments/Work') self.assertIsInstance(v, volumes.Volume) + def test_get_server_volume_with_exception(self): + self.assertRaises(TypeError, + self.cs.volumes.get_server_volume, + "1234") + + self.assertRaises(TypeError, + self.cs.volumes.get_server_volume, + "1234", + volume_id="Work", + attachment_id="123") + + @mock.patch('warnings.warn') + def test_get_server_volume_with_warn(self, mock_warn): + self.cs.volumes.get_server_volume(1234, + volume_id=None, + attachment_id="Work") + mock_warn.assert_called_once() + def test_list_server_volumes(self): vl = self.cs.volumes.get_server_volumes(1234) self.assert_request_id(vl, fakes.FAKE_REQUEST_ID_LIST) @@ -88,3 +108,21 @@ def test_create_server_volume_with_tag(self): 'device': '/dev/vdb', 'tag': 'test_tag'}}) self.assertIsInstance(v, volumes.Volume) + + def test_delete_server_volume_with_exception(self): + self.assertRaises(TypeError, + self.cs.volumes.delete_server_volume, + "1234") + + self.assertRaises(TypeError, + self.cs.volumes.delete_server_volume, + "1234", + volume_id="Work", + attachment_id="123") + + @mock.patch('warnings.warn') + def test_delete_server_volume_with_warn(self, mock_warn): + self.cs.volumes.delete_server_volume(1234, + volume_id=None, + attachment_id="Work") + mock_warn.assert_called_once() diff --git a/novaclient/v2/volumes.py b/novaclient/v2/volumes.py index ebccf8a09..d6208cbd1 100644 --- a/novaclient/v2/volumes.py +++ b/novaclient/v2/volumes.py @@ -16,6 +16,7 @@ """ Volume interface """ +import warnings from novaclient import api_versions from novaclient import base @@ -95,17 +96,32 @@ def update_server_volume(self, server_id, src_volid, dest_volid): (server_id, src_volid,), body, "volumeAttachment") - def get_server_volume(self, server_id, attachment_id): + def get_server_volume(self, server_id, volume_id=None, attachment_id=None): """ - Get the volume identified by the attachment ID, that is attached to + Get the volume identified by the volume ID, that is attached to the given server ID :param server_id: The ID of the server - :param attachment_id: The ID of the attachment + :param volume_id: The ID of the volume to attach :rtype: :class:`Volume` """ + + if attachment_id is not None and volume_id is not None: + raise TypeError("You cannot specify both volume_id " + "and attachment_id arguments.") + + elif attachment_id is not None: + warnings.warn("attachment_id argument " + "of volumes.get_server_volume " + "method is deprecated in favor " + "of volume_id.") + volume_id = attachment_id + + if volume_id is None: + raise TypeError("volume_id is required argument.") + return self._get("/servers/%s/os-volume_attachments/%s" % (server_id, - attachment_id,), "volumeAttachment") + volume_id,), "volumeAttachment") def get_server_volumes(self, server_id): """ @@ -117,13 +133,28 @@ def get_server_volumes(self, server_id): return self._list("/servers/%s/os-volume_attachments" % server_id, "volumeAttachments") - def delete_server_volume(self, server_id, attachment_id): + def delete_server_volume(self, server_id, volume_id=None, + attachment_id=None): """ - Detach a volume identified by the attachment ID from the given server + Detach a volume identified by the volume ID from the given server :param server_id: The ID of the server - :param attachment_id: The ID of the attachment + :param volume_id: The ID of the volume to attach :returns: An instance of novaclient.base.TupleWithMeta """ + if attachment_id is not None and volume_id is not None: + raise TypeError("You cannot specify both volume_id " + "and attachment_id arguments.") + + elif attachment_id is not None: + warnings.warn("attachment_id argument " + "of volumes.delete_server_volume " + "method is deprecated in favor " + "of volume_id.") + volume_id = attachment_id + + if volume_id is None: + raise TypeError("volume_id is required argument.") + return self._delete("/servers/%s/os-volume_attachments/%s" % - (server_id, attachment_id,)) + (server_id, volume_id,)) From 24e4e92c1e9bfc8cfa15816ac42cd2e5e1914146 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Tue, 18 Jul 2017 01:56:34 +0000 Subject: [PATCH 1332/1705] Updated from global requirements Change-Id: I7ec074495426c43bb20f7d3c00ee1a122c351e87 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d909bbdb9..c9a5ac617 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ pbr!=2.1.0,>=2.0.0 # Apache-2.0 keystoneauth1>=2.21.0 # Apache-2.0 iso8601>=0.1.11 # MIT oslo.i18n!=3.15.2,>=2.1.0 # Apache-2.0 -oslo.serialization>=1.10.0 # Apache-2.0 +oslo.serialization!=2.19.1,>=1.10.0 # Apache-2.0 oslo.utils>=3.20.0 # Apache-2.0 PrettyTable<0.8,>=0.7.1 # BSD simplejson>=2.2.0 # MIT From 30c9215da730d13c7b96422621aecbfbadfb517c Mon Sep 17 00:00:00 2001 From: Kevin_Zheng Date: Tue, 18 Jul 2017 11:50:04 +0800 Subject: [PATCH 1333/1705] Microversion 2.52 - Support tag when boot Change-Id: Ib65894f0e128c599db8d3440fe5a8427e62d5782 Depends-On: Ifcaaf285c8f98a1d0e8bbbc87b2f57fbce057346 Implements: blueprint support-tag-instance-when-boot --- novaclient/__init__.py | 2 +- novaclient/tests/unit/v2/test_servers.py | 44 +++++++++++++++++++ novaclient/tests/unit/v2/test_shell.py | 40 +++++++++++++++++ novaclient/v2/servers.py | 11 ++++- novaclient/v2/shell.py | 10 +++++ .../microversion-v2_52-2fe81b3bf2e4b4ea.yaml | 8 ++++ 6 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/microversion-v2_52-2fe81b3bf2e4b4ea.yaml diff --git a/novaclient/__init__.py b/novaclient/__init__.py index d6ebb9638..926bfff6c 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.51") +API_MAX_VERSION = api_versions.APIVersion("2.52") diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index 04772306b..b807a7dca 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -1483,3 +1483,47 @@ def test_remove_fixed_ip(self): # novaclient.v2.servers.Server.remove_fixed_ip() # is not available after 2.44 pass + + +class ServersV252Test(ServersV249Test): + + api_version = "2.52" + + def test_create_server_with_tags(self): + self.cs.servers.create( + name="My server", + image=1, + flavor=1, + meta={'foo': 'bar'}, + userdata="hello moto", + key_name="fakekey", + nics=self._get_server_create_default_nics(), + tags=['tag1', 'tag2'] + ) + self.assert_called('POST', '/servers', + {'server': { + 'flavorRef': '1', + 'imageRef': '1', + 'key_name': 'fakekey', + 'max_count': 1, + 'metadata': {'foo': 'bar'}, + 'min_count': 1, + 'name': 'My server', + 'networks': 'auto', + 'tags': ['tag1', 'tag2'], + 'user_data': 'aGVsbG8gbW90bw==' + }} + ) + + def test_create_server_with_tags_pre_252_fails(self): + self.cs.api_version = api_versions.APIVersion('2.51') + self.assertRaises(exceptions.UnsupportedAttribute, + self.cs.servers.create, + name="My server", + image=1, + flavor=1, + meta={'foo': 'bar'}, + userdata="hello moto", + key_name="fakekey", + nics=self._get_server_create_default_nics(), + tags=['tag1', 'tag2']) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index dbda0b7d7..66c462d94 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -1068,6 +1068,45 @@ def test_boot_invalid_ephemeral_data_format(self): FAKE_UUID_1) self.assertRaises(argparse.ArgumentTypeError, self.run_command, cmd) + def test_boot_with_tags(self): + self.run_command('boot --flavor 1 --image %s --nic auto ' + 'some-server --tags tag1,tag2' % FAKE_UUID_1, + api_version='2.52') + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': FAKE_UUID_1, + 'min_count': 1, + 'max_count': 1, + 'networks': 'auto', + 'tags': ['tag1', 'tag2'] + }}, + ) + + def test_boot_without_tags_v252(self): + self.run_command('boot --flavor 1 --image %s --nic auto ' + 'some-server' % FAKE_UUID_1, + api_version='2.52') + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': FAKE_UUID_1, + 'min_count': 1, + 'max_count': 1, + 'networks': 'auto', + }}, + ) + + def test_boot_with_tags_pre_v2_52(self): + cmd = ('boot --flavor 1 --image %s some-server ' + '--tags tag1,tag2' % FAKE_UUID_1) + self.assertRaises(SystemExit, self.run_command, + cmd, api_version='2.51') + def test_flavor_list(self): self.run_command('flavor-list') self.assert_called_anytime('GET', '/flavors/detail') @@ -3020,6 +3059,7 @@ def test_versions(self): # within the server details 48, # There are no version-wrapped shell method changes for this. 51, # There are no version-wrapped shell method changes for this. + 52, # There are no version-wrapped shell method changes for this. ]) versions_supported = set(range(0, novaclient.API_MAX_VERSION.ver_minor + 1)) diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index b60b0b6de..81a0b96b7 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -663,7 +663,7 @@ def _boot(self, resource_url, response_key, name, image, flavor, block_device_mapping_v2=None, nics=None, scheduler_hints=None, config_drive=None, admin_pass=None, disk_config=None, access_ip_v4=None, access_ip_v6=None, description=None, - **kwargs): + tags=None, **kwargs): """ Create (boot) a new server. """ @@ -795,6 +795,9 @@ def _boot(self, resource_url, response_key, name, image, flavor, if description: body['server']['description'] = description + if tags: + body['server']['tags'] = tags + return self._create(resource_url, body, response_key, return_raw=return_raw, **kwargs) @@ -1337,6 +1340,8 @@ def create(self, name, image, flavor, meta=None, files=None, :param access_ip_v6: (optional extension) add alternative access ip v6 :param description: optional description of the server (allowed since microversion 2.19) + :param tags: A list of arbitrary strings to be added to the + server as tags (allowed since microversion 2.52) """ if not min_count: min_count = 1 @@ -1369,6 +1374,10 @@ def create(self, name, image, flavor, meta=None, files=None, "unsupported before microversion " "2.32") + boot_tags_microversion = api_versions.APIVersion("2.52") + if "tags" in kwargs and self.api_version < boot_tags_microversion: + raise exceptions.UnsupportedAttribute("tags", "2.52") + boot_kwargs = dict( meta=meta, files=files, userdata=userdata, reservation_id=reservation_id, min_count=min_count, diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index e390c26e0..c00f88bea 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -530,6 +530,9 @@ def _boot(cs, args): if 'description' in args: boot_kwargs["description"] = args.description + if 'tags' in args and args.tags: + boot_kwargs["tags"] = args.tags.split(',') + return boot_args, boot_kwargs @@ -877,6 +880,13 @@ def _boot(cs, args): default=None, help=_('Description for the server.'), start_version="2.19") +@utils.arg( + '--tags', + metavar='', + default=None, + help=_('Tags for the server.' + 'Tags must be separated by commas: --tags '), + start_version="2.52") def do_boot(cs, args): """Boot a new server.""" boot_args, boot_kwargs = _boot(cs, args) diff --git a/releasenotes/notes/microversion-v2_52-2fe81b3bf2e4b4ea.yaml b/releasenotes/notes/microversion-v2_52-2fe81b3bf2e4b4ea.yaml new file mode 100644 index 000000000..0f7eeedcd --- /dev/null +++ b/releasenotes/notes/microversion-v2_52-2fe81b3bf2e4b4ea.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + `Microversion 2.52`_ is now supported which adds the ``--tags`` option to + the ``nova boot`` command and a ``tags`` kwarg to the + ``novaclient.v2.servers.ServerManager.create()`` python API binding method. + + .. _Microversion 2.52: https://docs.openstack.org/nova/latest/api_microversion_history.html#id47 From 1945d1c3bc9eda347014c1283f4c7511f6b8ad90 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Wed, 19 Jul 2017 22:30:30 +0000 Subject: [PATCH 1334/1705] Updated from global requirements Change-Id: Ia734b9f1dac610693a75df70ec1ffe0fe45dfcab --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 44c52f1fb..33a960e97 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -15,7 +15,7 @@ python-neutronclient>=6.3.0 # Apache-2.0 requests-mock>=1.1 # Apache-2.0 sphinx>=1.6.2 # BSD os-client-config>=1.27.0 # Apache-2.0 -openstackdocstheme>=1.11.0 # Apache-2.0 +openstackdocstheme>=1.11.0 # Apache-2.0 osprofiler>=1.4.0 # Apache-2.0 testrepository>=0.0.18 # Apache-2.0/BSD testscenarios>=0.4 # Apache-2.0/BSD From 617eb742ab9426a769e9859b569b54b260025a39 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Sat, 15 Jul 2017 18:01:19 -0400 Subject: [PATCH 1335/1705] Adjust test_resize_down_revert to account for counting quotas Change I9269ffa2b80e48db96c622d0dc0817738854f602 in the Nova server removes the need for reservations, which means that once the server is resized in nova-compute and the vcpus and memory_mb are changed on the instances record, the usage count will also change, before the resize is confirmed. This means we need to adjust the usage assertions in the resize down test. Usage changes once the server is in VERIFY_RESIZE state, and the usage will change again if the resize is reverted. Note that if we were getting usage without the 'reserved' query paramter to the 'limits' API, the behavior would have been the same regardless of the change to Nova for counting quotas. Depends-On: I9269ffa2b80e48db96c622d0dc0817738854f602 Related to blueprint cells-count-resources-to-check-quota-in-api Change-Id: I91ca0d204454987dcce1c68d9bf0f532b469df6d --- novaclient/tests/functional/v2/test_resize.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/novaclient/tests/functional/v2/test_resize.py b/novaclient/tests/functional/v2/test_resize.py index 25526024f..9277480ea 100644 --- a/novaclient/tests/functional/v2/test_resize.py +++ b/novaclient/tests/functional/v2/test_resize.py @@ -135,17 +135,16 @@ def test_resize_down_revert(self): self.nova('resize', params='%s %s --poll' % (server_id, smaller_flavor)) resize_usage = self._get_absolute_limits() - # compare the starting usage against the resize usage; in the case of - # a resize down we don't expect usage to change until it's confirmed, - # which doesn't happen in this test since we revert - self._compare_quota_usage( - starting_usage, resize_usage, expect_diff=False) + # compare the starting usage against the resize usage; with counting + # quotas in the server there are no reservations, so the + # usage changes after the resize happens before it's confirmed. + self._compare_quota_usage(starting_usage, resize_usage) # now revert the resize self.nova('resize-revert', params='%s' % server_id) # we have to wait for the server to be ACTIVE before we can check quota self._wait_for_state_change(server_id, 'active') - # get the final quota usage which should be the same as the resize - # usage before revert + # get the final quota usage which will be different from the resize + # usage since we've reverted back *up* to the original flavor; the API + # code checks quota again if we revert up in size revert_usage = self._get_absolute_limits() - self._compare_quota_usage( - resize_usage, revert_usage, expect_diff=False) + self._compare_quota_usage(resize_usage, revert_usage) From 02145d765c4dc5970e5ee5a8621abc44151fcbfb Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 21 Jul 2017 04:46:09 +0000 Subject: [PATCH 1336/1705] Updated from global requirements Change-Id: Ia28ac9c04a663a2ce36950dae9cb9b348af01bc4 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 33a960e97..8e8cd51bc 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -14,7 +14,7 @@ python-glanceclient>=2.7.0 # Apache-2.0 python-neutronclient>=6.3.0 # Apache-2.0 requests-mock>=1.1 # Apache-2.0 sphinx>=1.6.2 # BSD -os-client-config>=1.27.0 # Apache-2.0 +os-client-config>=1.28.0 # Apache-2.0 openstackdocstheme>=1.11.0 # Apache-2.0 osprofiler>=1.4.0 # Apache-2.0 testrepository>=0.0.18 # Apache-2.0/BSD From ed058c46a1b016e53cf8312a69e13a0bc832dc2a Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Wed, 19 Jul 2017 22:34:58 -0400 Subject: [PATCH 1337/1705] Expect id and disabled_reason in GET /os-services response Since we no longer have v2 extensions, the GET /os-services response is going to have an id and disabled_reason field, so we don't need to have conditional checks around those. Change-Id: I5d11ffb48febb53f70bf1193b2cba5ec839a3f0d --- novaclient/v2/shell.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index c00f88bea..947fc31a3 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -3469,17 +3469,8 @@ def do_reset_network(cs, args): def do_service_list(cs, args): """Show a list of all running services. Filter by host & binary.""" result = cs.services.list(host=args.host, binary=args.binary) - columns = ["Binary", "Host", "Zone", "Status", "State", "Updated_at"] - # NOTE(sulo): we check if the response has disabled_reason - # so as not to add the column when the extended ext is not enabled. - if result and hasattr(result[0], 'disabled_reason'): - columns.append("Disabled Reason") - - # NOTE(gtt): After https://review.openstack.org/#/c/39998/ nova will - # show id in response. - if result and hasattr(result[0], 'id'): - columns.insert(0, "Id") - + columns = ["Id", "Binary", "Host", "Zone", "Status", + "State", "Updated_at", "Disabled Reason"] utils.print_list(result, columns) From c3f0864998faf8abdf4ee32b9fe65394b29c3ef4 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Sat, 22 Jul 2017 16:38:55 +0000 Subject: [PATCH 1338/1705] Updated from global requirements Change-Id: I6c7062c45fbc66512e7683552cd5fe477196c42c --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c9a5ac617..7010c4b00 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. pbr!=2.1.0,>=2.0.0 # Apache-2.0 -keystoneauth1>=2.21.0 # Apache-2.0 +keystoneauth1!=3.0.0,>=2.21.0 # Apache-2.0 iso8601>=0.1.11 # MIT oslo.i18n!=3.15.2,>=2.1.0 # Apache-2.0 oslo.serialization!=2.19.1,>=1.10.0 # Apache-2.0 From 6dc996f9ca70b418bd037f328efd8a1ff76df6e8 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Sun, 23 Jul 2017 13:52:24 +0000 Subject: [PATCH 1339/1705] Updated from global requirements Change-Id: Id6714ca11b033b715004f4899b59dd6951320882 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7010c4b00..17b9cf6ad 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. pbr!=2.1.0,>=2.0.0 # Apache-2.0 -keystoneauth1!=3.0.0,>=2.21.0 # Apache-2.0 +keystoneauth1>=3.0.1 # Apache-2.0 iso8601>=0.1.11 # MIT oslo.i18n!=3.15.2,>=2.1.0 # Apache-2.0 oslo.serialization!=2.19.1,>=1.10.0 # Apache-2.0 From 19b3128034512ae4f33df5956e8c93da9c5e047c Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Sun, 23 Jul 2017 19:08:30 +0000 Subject: [PATCH 1340/1705] Updated from global requirements Change-Id: I431f85c07465a184af16a9956f40396c63b05985 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 8e8cd51bc..21c3c6990 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -9,7 +9,7 @@ fixtures>=3.0.0 # Apache-2.0/BSD keyring>=5.5.1 # MIT/PSF mock>=2.0 # BSD python-keystoneclient>=3.8.0 # Apache-2.0 -python-cinderclient>=2.1.0 # Apache-2.0 +python-cinderclient>=3.0.0 # Apache-2.0 python-glanceclient>=2.7.0 # Apache-2.0 python-neutronclient>=6.3.0 # Apache-2.0 requests-mock>=1.1 # Apache-2.0 From 6071f6f1b2a231e3bc5ba5c16e6df7e562e93642 Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Wed, 26 Jul 2017 11:24:47 +0900 Subject: [PATCH 1341/1705] Add 'Forced down' column in serivce-list Starting from microversion 2.11, the 'force_down' parameter is returned in the response of the "List Compute Services" API (GET /os-services). But the service-list command does not show the 'forced_down' parameter in the result table. So add the 'Forced down' column in the table. Change-Id: I79e3ba37bdf054604eab40452628dbf30014f6f1 Closes-Bug: #1706486 --- novaclient/tests/functional/v2/test_os_services.py | 10 ++++++++-- novaclient/v2/shell.py | 3 +++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/novaclient/tests/functional/v2/test_os_services.py b/novaclient/tests/functional/v2/test_os_services.py index 3da8449c0..e12e40ae7 100644 --- a/novaclient/tests/functional/v2/test_os_services.py +++ b/novaclient/tests/functional/v2/test_os_services.py @@ -28,8 +28,14 @@ def test_os_services_force_down_force_up(self): # to find them. So filter out anything that's not nova-compute. if serv.binary != 'nova-compute': continue - host = self._get_column_value_from_single_row_table( - self.nova('service-list --binary %s' % serv.binary), 'Host') + service_list = self.nova('service-list --binary %s' % serv.binary) + # Check the 'service-list' table has the 'Forced down' column + status = self._get_column_value_from_single_row_table( + service_list, 'Forced down') + self.assertEqual('False', status) + + host = self._get_column_value_from_single_row_table(service_list, + 'Host') service = self.nova('service-force-down %s %s' % (host, serv.binary)) self.addCleanup(self.nova, 'service-force-down --unset', diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 947fc31a3..fa74e0b31 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -3471,6 +3471,9 @@ def do_service_list(cs, args): result = cs.services.list(host=args.host, binary=args.binary) columns = ["Id", "Binary", "Host", "Zone", "Status", "State", "Updated_at", "Disabled Reason"] + if cs.api_version >= api_versions.APIVersion('2.11'): + columns.append("Forced down") + utils.print_list(result, columns) From 5745beae5c456aafef82ab7af94b452875409760 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Wed, 19 Jul 2017 23:54:12 -0400 Subject: [PATCH 1342/1705] Microversion 2.53 - services and hypervisors using UUIDs Adds support for the 2.53 microversion which changes the os-services and os-hypervisors APIs to use a UUID for the ID value on the resource. Also, the PUT and GET API methods have changed a bit for both resources in this microversion, so the pythong API bindings and command lines have been adjusted accordingly. Part of blueprint service-hyper-uuid-in-api Change-Id: Ic721143cc154d91e74a8a9dd2c1e991045c94305 --- novaclient/__init__.py | 2 +- .../functional/v2/legacy/test_hypervisors.py | 16 ++- .../tests/functional/v2/test_hypervisors.py | 7 ++ .../tests/functional/v2/test_os_services.py | 99 +++++++++++++++++++ .../tests/unit/fixture_data/hypervisors.py | 64 ++++++++---- novaclient/tests/unit/v2/fakes.py | 28 +++++- novaclient/tests/unit/v2/test_hypervisors.py | 64 ++++++++---- novaclient/tests/unit/v2/test_services.py | 69 ++++++++++++- novaclient/tests/unit/v2/test_shell.py | 43 ++++++++ novaclient/v2/hypervisors.py | 35 +++++-- novaclient/v2/services.py | 64 +++++++++++- novaclient/v2/shell.py | 75 +++++++++++++- .../microversion-v2_53-3463b546a38c5f84.yaml | 35 +++++++ 13 files changed, 544 insertions(+), 57 deletions(-) create mode 100644 releasenotes/notes/microversion-v2_53-3463b546a38c5f84.yaml diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 926bfff6c..3fe7488d0 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.52") +API_MAX_VERSION = api_versions.APIVersion("2.53") diff --git a/novaclient/tests/functional/v2/legacy/test_hypervisors.py b/novaclient/tests/functional/v2/legacy/test_hypervisors.py index 36edd60e1..ecc102dc0 100644 --- a/novaclient/tests/functional/v2/legacy/test_hypervisors.py +++ b/novaclient/tests/functional/v2/legacy/test_hypervisors.py @@ -13,18 +13,32 @@ import six from novaclient.tests.functional import base +from novaclient import utils class TestHypervisors(base.ClientTestBase): COMPUTE_API_VERSION = "2.1" - def _test_list(self, cpu_info_type): + def _test_list(self, cpu_info_type, uuid_as_id=False): hypervisors = self.client.hypervisors.list() if not len(hypervisors): self.fail("No hypervisors detected.") for hypervisor in hypervisors: self.assertIsInstance(hypervisor.cpu_info, cpu_info_type) + if uuid_as_id: + # microversion >= 2.53 returns a uuid for the id + self.assertFalse(utils.is_integer_like(hypervisor.id), + 'Expected hypervisor.id to be a UUID.') + self.assertFalse( + utils.is_integer_like(hypervisor.service['id']), + 'Expected hypervisor.service.id to be a UUID.') + else: + self.assertTrue(utils.is_integer_like(hypervisor.id), + 'Expected hypervisor.id to be an integer.') + self.assertTrue( + utils.is_integer_like(hypervisor.service['id']), + 'Expected hypervisor.service.id to be an integer.') def test_list(self): self._test_list(six.text_type) diff --git a/novaclient/tests/functional/v2/test_hypervisors.py b/novaclient/tests/functional/v2/test_hypervisors.py index 51327a6d9..ca66a44a2 100644 --- a/novaclient/tests/functional/v2/test_hypervisors.py +++ b/novaclient/tests/functional/v2/test_hypervisors.py @@ -19,3 +19,10 @@ class TestHypervisorsV28(test_hypervisors.TestHypervisors): def test_list(self): self._test_list(dict) + + +class TestHypervisorsV2_53(TestHypervisorsV28): + COMPUTE_API_VERSION = "2.53" + + def test_list(self): + self._test_list(cpu_info_type=dict, uuid_as_id=True) diff --git a/novaclient/tests/functional/v2/test_os_services.py b/novaclient/tests/functional/v2/test_os_services.py index 3da8449c0..719ad7330 100644 --- a/novaclient/tests/functional/v2/test_os_services.py +++ b/novaclient/tests/functional/v2/test_os_services.py @@ -10,7 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient.tests.functional import base from novaclient.tests.functional.v2.legacy import test_os_services +from novaclient import utils class TestOsServicesNovaClientV211(test_os_services.TestOsServicesNovaClient): @@ -42,3 +44,100 @@ def test_os_services_force_down_force_up(self): status = self._get_column_value_from_single_row_table( service, 'Forced down') self.assertEqual('False', status) + + +class TestOsServicesNovaClientV2_53(base.ClientTestBase): + """Tests the nova service-* commands using the 2.53 microversion. + + The main difference with the 2.53 microversion in these commands is + the host/binary combination is replaced with the service.id as the + unique identifier for a service. + """ + COMPUTE_API_VERSION = "2.53" + + def test_os_services_list(self): + table = self.nova('service-list') + for serv in self.client.services.list(): + self.assertIn(serv.binary, table) + # the id should not be an integer and should be in the table + self.assertFalse(utils.is_integer_like(serv.id)) + self.assertIn(serv.id, table) + + def test_os_service_disable_enable(self): + # Disable and enable Nova services in accordance with list of nova + # services returned by client + # NOTE(sdague): service disable has the chance in racing + # with other tests. Now functional tests for novaclient are launched + # in serial way (https://review.openstack.org/#/c/217768/), but + # it's a potential issue for making these tests parallel in the future + for serv in self.client.services.list(): + # In Pike the os-services API was made multi-cell aware and it + # looks up services by host, which uses the host mapping record + # in the API DB which is only populated for nova-compute services, + # effectively making it impossible to perform actions like enable + # or disable non-nova-compute services since the API won't be able + # to find them. So filter out anything that's not nova-compute. + if serv.binary != 'nova-compute': + continue + service = self.nova('service-disable %s' % serv.id) + self.addCleanup(self.nova, 'service-enable', params="%s" % serv.id) + service_id = self._get_column_value_from_single_row_table( + service, 'ID') + self.assertEqual(serv.id, service_id) + status = self._get_column_value_from_single_row_table( + service, 'Status') + self.assertEqual('disabled', status) + service = self.nova('service-enable %s' % serv.id) + service_id = self._get_column_value_from_single_row_table( + service, 'ID') + self.assertEqual(serv.id, service_id) + status = self._get_column_value_from_single_row_table( + service, 'Status') + self.assertEqual('enabled', status) + + def test_os_service_disable_log_reason(self): + for serv in self.client.services.list(): + # In Pike the os-services API was made multi-cell aware and it + # looks up services by host, which uses the host mapping record + # in the API DB which is only populated for nova-compute services, + # effectively making it impossible to perform actions like enable + # or disable non-nova-compute services since the API won't be able + # to find them. So filter out anything that's not nova-compute. + if serv.binary != 'nova-compute': + continue + service = self.nova('service-disable --reason test_disable %s' + % serv.id) + self.addCleanup(self.nova, 'service-enable', params="%s" % serv.id) + service_id = self._get_column_value_from_single_row_table( + service, 'ID') + self.assertEqual(serv.id, service_id) + status = self._get_column_value_from_single_row_table( + service, 'Status') + log_reason = self._get_column_value_from_single_row_table( + service, 'Disabled Reason') + self.assertEqual('disabled', status) + self.assertEqual('test_disable', log_reason) + + def test_os_services_force_down_force_up(self): + for serv in self.client.services.list(): + # In Pike the os-services API was made multi-cell aware and it + # looks up services by host, which uses the host mapping record + # in the API DB which is only populated for nova-compute services, + # effectively making it impossible to perform actions like enable + # or disable non-nova-compute services since the API won't be able + # to find them. So filter out anything that's not nova-compute. + if serv.binary != 'nova-compute': + continue + service = self.nova('service-force-down %s' % serv.id) + self.addCleanup(self.nova, 'service-force-down --unset', + params="%s" % serv.id) + service_id = self._get_column_value_from_single_row_table( + service, 'ID') + self.assertEqual(serv.id, service_id) + forced_down = self._get_column_value_from_single_row_table( + service, 'Forced down') + self.assertEqual('True', forced_down) + service = self.nova('service-force-down --unset %s' % serv.id) + forced_down = self._get_column_value_from_single_row_table( + service, 'Forced down') + self.assertEqual('False', forced_down) diff --git a/novaclient/tests/unit/fixture_data/hypervisors.py b/novaclient/tests/unit/fixture_data/hypervisors.py index 43b3438b6..1e706786b 100644 --- a/novaclient/tests/unit/fixture_data/hypervisors.py +++ b/novaclient/tests/unit/fixture_data/hypervisors.py @@ -10,20 +10,28 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient import api_versions from novaclient.tests.unit.fixture_data import base class V1(base.Fixture): base_url = 'os-hypervisors' + api_version = '2.1' + hyper_id_1 = 1234 + hyper_id_2 = 5678 + service_id_1 = 1 + service_id_2 = 2 def setUp(self): super(V1, self).setUp() + uuid_as_id = (api_versions.APIVersion(self.api_version) >= + api_versions.APIVersion('2.53')) get_os_hypervisors = { 'hypervisors': [ - {'id': 1234, 'hypervisor_hostname': 'hyper1'}, - {'id': 5678, 'hypervisor_hostname': 'hyper2'}, + {'id': self.hyper_id_1, 'hypervisor_hostname': 'hyper1'}, + {'id': self.hyper_id_2, 'hypervisor_hostname': 'hyper2'}, ] } @@ -36,9 +44,9 @@ def setUp(self): get_os_hypervisors_detail = { 'hypervisors': [ { - 'id': 1234, + 'id': self.hyper_id_1, 'service': { - 'id': 1, + 'id': self.service_id_1, 'host': 'compute1', }, 'vcpus': 4, @@ -58,9 +66,9 @@ def setUp(self): 'disk_available_least': 100 }, { - 'id': 2, + 'id': self.hyper_id_2, 'service': { - 'id': 2, + 'id': self.service_id_2, 'host': 'compute2', }, 'vcpus': 4, @@ -109,19 +117,23 @@ def setUp(self): get_os_hypervisors_search = { 'hypervisors': [ - {'id': 1234, 'hypervisor_hostname': 'hyper1'}, - {'id': 5678, 'hypervisor_hostname': 'hyper2'} + {'id': self.hyper_id_1, 'hypervisor_hostname': 'hyper1'}, + {'id': self.hyper_id_2, 'hypervisor_hostname': 'hyper2'} ] } - self.requests_mock.get(self.url('hyper', 'search'), + if uuid_as_id: + url = self.url(hypervisor_hostname_pattern='hyper') + else: + url = self.url('hyper', 'search') + self.requests_mock.get(url, json=get_os_hypervisors_search, headers=self.headers) get_hyper_server = { 'hypervisors': [ { - 'id': 1234, + 'id': self.hyper_id_1, 'hypervisor_hostname': 'hyper1', 'servers': [ {'name': 'inst1', 'uuid': 'uuid1'}, @@ -129,7 +141,7 @@ def setUp(self): ] }, { - 'id': 5678, + 'id': self.hyper_id_2, 'hypervisor_hostname': 'hyper2', 'servers': [ {'name': 'inst3', 'uuid': 'uuid3'}, @@ -139,14 +151,19 @@ def setUp(self): ] } - self.requests_mock.get(self.url('hyper', 'servers'), + if uuid_as_id: + url = self.url(hypervisor_hostname_pattern='hyper', + with_servers=True) + else: + url = self.url('hyper', 'servers') + self.requests_mock.get(url, json=get_hyper_server, headers=self.headers) - get_os_hypervisors_1234 = { + get_os_hypervisors_hyper1 = { 'hypervisor': { - 'id': 1234, - 'service': {'id': 1, 'host': 'compute1'}, + 'id': self.hyper_id_1, + 'service': {'id': self.service_id_1, 'host': 'compute1'}, 'vcpus': 4, 'memory_mb': 10 * 1024, 'local_gb': 250, @@ -165,18 +182,27 @@ def setUp(self): } } - self.requests_mock.get(self.url(1234), - json=get_os_hypervisors_1234, + self.requests_mock.get(self.url(self.hyper_id_1), + json=get_os_hypervisors_hyper1, headers=self.headers) get_os_hypervisors_uptime = { 'hypervisor': { - 'id': 1234, + 'id': self.hyper_id_1, 'hypervisor_hostname': 'hyper1', 'uptime': 'fake uptime' } } - self.requests_mock.get(self.url(1234, 'uptime'), + self.requests_mock.get(self.url(self.hyper_id_1, 'uptime'), json=get_os_hypervisors_uptime, headers=self.headers) + + +class V2_53(V1): + """Fixture data for the os-hypervisors 2.53 API.""" + api_version = '2.53' + hyper_id_1 = 'd480b1b6-2255-43c2-b2c2-d60d42c2c074' + hyper_id_2 = '43a8214d-f36a-4fc0-a25c-3cf35c17522d' + service_id_1 = 'a87743ff-9c29-42ff-805d-2444659b5fc0' + service_id_2 = '0486ab8b-1cfc-4ccb-9d94-9f22ec8bbd6b' diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 1ebf1e3ea..4138e8a98 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -54,6 +54,9 @@ FAKE_REQUEST_ID_LIST = fakes.FAKE_REQUEST_ID_LIST FAKE_RESPONSE_HEADERS = {'x-openstack-request-id': FAKE_REQUEST_ID} +FAKE_SERVICE_UUID_1 = '75e9eabc-ed3b-4f11-8bba-add1e7e7e2de' +FAKE_SERVICE_UUID_2 = '1f140183-c914-4ddf-8757-6df73028aa86' + class FakeClient(fakes.FakeClient, client.Client): @@ -1582,6 +1585,12 @@ def delete_os_aggregates_1(self, **kw): def get_os_services(self, **kw): host = kw.get('host', 'host1') binary = kw.get('binary', 'nova-compute') + if self.api_version >= api_versions.APIVersion('2.53'): + service_id_1 = FAKE_SERVICE_UUID_1 + service_id_2 = FAKE_SERVICE_UUID_2 + else: + service_id_1 = 1 + service_id_2 = 2 return (200, FAKE_RESPONSE_HEADERS, {'services': [{'binary': binary, 'host': host, @@ -1589,14 +1598,16 @@ def get_os_services(self, **kw): 'status': 'enabled', 'state': 'up', 'updated_at': datetime.datetime( - 2012, 10, 29, 13, 42, 2)}, + 2012, 10, 29, 13, 42, 2), + 'id': service_id_1}, {'binary': binary, 'host': host, 'zone': 'nova', 'status': 'disabled', 'state': 'down', 'updated_at': datetime.datetime( - 2012, 9, 18, 8, 3, 38)}, + 2012, 9, 18, 8, 3, 38), + 'id': service_id_2}, ]}) def put_os_services_enable(self, body, **kw): @@ -1618,9 +1629,22 @@ def put_os_services_disable_log_reason(self, body, **kw): 'status': 'disabled', 'disabled_reason': body['disabled_reason']}}) + def put_os_services_75e9eabc_ed3b_4f11_8bba_add1e7e7e2de( + self, body, **kw): + """This should only be called with microversion >= 2.53.""" + return (200, FAKE_RESPONSE_HEADERS, {'service': { + 'host': 'host1', + 'binary': 'nova-compute', + 'status': body.get('status', 'enabled'), + 'disabled_reason': body.get('disabled_reason'), + 'forced_down': body.get('forced_down', False)}}) + def delete_os_services_1(self, **kw): return (204, FAKE_RESPONSE_HEADERS, None) + def delete_os_services_75e9eabc_ed3b_4f11_8bba_add1e7e7e2de(self, **kwarg): + return (204, FAKE_RESPONSE_HEADERS, None) + def put_os_services_force_down(self, body, **kw): return (200, FAKE_RESPONSE_HEADERS, {'service': { 'host': body['host'], diff --git a/novaclient/tests/unit/v2/test_hypervisors.py b/novaclient/tests/unit/v2/test_hypervisors.py index d3a578e69..0a3a2514f 100644 --- a/novaclient/tests/unit/v2/test_hypervisors.py +++ b/novaclient/tests/unit/v2/test_hypervisors.py @@ -31,8 +31,10 @@ def compare_to_expected(self, expected, hyper): def test_hypervisor_index(self): expected = [ - dict(id=1234, hypervisor_hostname='hyper1'), - dict(id=5678, hypervisor_hostname='hyper2')] + dict(id=self.data_fixture.hyper_id_1, + hypervisor_hostname='hyper1'), + dict(id=self.data_fixture.hyper_id_2, + hypervisor_hostname='hyper2')] result = self.cs.hypervisors.list(False) self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) @@ -43,8 +45,9 @@ def test_hypervisor_index(self): def test_hypervisor_detail(self): expected = [ - dict(id=1234, - service=dict(id=1, host='compute1'), + dict(id=self.data_fixture.hyper_id_1, + service=dict(id=self.data_fixture.service_id_1, + host='compute1'), vcpus=4, memory_mb=10 * 1024, local_gb=250, @@ -60,8 +63,9 @@ def test_hypervisor_detail(self): running_vms=2, cpu_info='cpu_info', disk_available_least=100), - dict(id=2, - service=dict(id=2, host="compute2"), + dict(id=self.data_fixture.hyper_id_2, + service=dict(id=self.data_fixture.service_id_2, + host="compute2"), vcpus=4, memory_mb=10 * 1024, local_gb=250, @@ -87,24 +91,30 @@ def test_hypervisor_detail(self): def test_hypervisor_search(self): expected = [ - dict(id=1234, hypervisor_hostname='hyper1'), - dict(id=5678, hypervisor_hostname='hyper2')] + dict(id=self.data_fixture.hyper_id_1, + hypervisor_hostname='hyper1'), + dict(id=self.data_fixture.hyper_id_2, + hypervisor_hostname='hyper2')] result = self.cs.hypervisors.search('hyper') self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('GET', '/os-hypervisors/hyper/search') + if self.cs.api_version >= api_versions.APIVersion('2.53'): + self.assert_called( + 'GET', '/os-hypervisors?hypervisor_hostname_pattern=hyper') + else: + self.assert_called('GET', '/os-hypervisors/hyper/search') for idx, hyper in enumerate(result): self.compare_to_expected(expected[idx], hyper) def test_hypervisor_servers(self): expected = [ - dict(id=1234, + dict(id=self.data_fixture.hyper_id_1, hypervisor_hostname='hyper1', servers=[ dict(name='inst1', uuid='uuid1'), dict(name='inst2', uuid='uuid2')]), - dict(id=5678, + dict(id=self.data_fixture.hyper_id_2, hypervisor_hostname='hyper2', servers=[ dict(name='inst3', uuid='uuid3'), @@ -113,15 +123,20 @@ def test_hypervisor_servers(self): result = self.cs.hypervisors.search('hyper', True) self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('GET', '/os-hypervisors/hyper/servers') + if self.cs.api_version >= api_versions.APIVersion('2.53'): + self.assert_called( + 'GET', '/os-hypervisors?hypervisor_hostname_pattern=hyper&' + 'with_servers=True') + else: + self.assert_called('GET', '/os-hypervisors/hyper/servers') for idx, hyper in enumerate(result): self.compare_to_expected(expected[idx], hyper) def test_hypervisor_get(self): expected = dict( - id=1234, - service=dict(id=1, host='compute1'), + id=self.data_fixture.hyper_id_1, + service=dict(id=self.data_fixture.service_id_1, host='compute1'), vcpus=4, memory_mb=10 * 1024, local_gb=250, @@ -138,21 +153,23 @@ def test_hypervisor_get(self): cpu_info='cpu_info', disk_available_least=100) - result = self.cs.hypervisors.get(1234) + result = self.cs.hypervisors.get(self.data_fixture.hyper_id_1) self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('GET', '/os-hypervisors/1234') + self.assert_called( + 'GET', '/os-hypervisors/%s' % self.data_fixture.hyper_id_1) self.compare_to_expected(expected, result) def test_hypervisor_uptime(self): expected = dict( - id=1234, + id=self.data_fixture.hyper_id_1, hypervisor_hostname="hyper1", uptime="fake uptime") - result = self.cs.hypervisors.uptime(1234) + result = self.cs.hypervisors.uptime(self.data_fixture.hyper_id_1) self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('GET', '/os-hypervisors/1234/uptime') + self.assert_called( + 'GET', '/os-hypervisors/%s/uptime' % self.data_fixture.hyper_id_1) self.compare_to_expected(expected, result) @@ -198,3 +215,12 @@ def test_use_limit_marker_params(self): self.cs.hypervisors.list(**params) for k, v in params.items(): self.assertEqual([v], self.requests_mock.last_request.qs[k]) + + +class HypervisorsV2_53Test(HypervisorsV233Test): + """Tests the os-hypervisors 2.53 API bindings.""" + data_fixture_class = data.V2_53 + + def setUp(self): + super(HypervisorsV2_53Test, self).setUp() + self.cs.api_version = api_versions.APIVersion("2.53") diff --git a/novaclient/tests/unit/v2/test_services.py b/novaclient/tests/unit/v2/test_services.py index 320f839ac..0c0434afd 100644 --- a/novaclient/tests/unit/v2/test_services.py +++ b/novaclient/tests/unit/v2/test_services.py @@ -34,11 +34,18 @@ def test_list_services(self): svs = self.cs.services.list() self.assert_request_id(svs, fakes.FAKE_REQUEST_ID_LIST) self.cs.assert_called('GET', '/os-services') + expect_uuid_id = ( + api_versions.APIVersion(self.api_version) >= + api_versions.APIVersion('2.53')) for s in svs: self.assertIsInstance(s, self._get_service_type()) self.assertEqual('nova-compute', s.binary) self.assertEqual('host1', s.host) - self.assertTrue(str(s).startswith('= api_versions.APIVersion('2.53'): + url = ('/os-hypervisors?hypervisor_hostname_pattern=%s' % + parse.quote(hypervisor_match, safe='')) + if servers: + url += '&with_servers=True' + else: + target = 'servers' if servers else 'search' + url = ('/os-hypervisors/%s/%s' % + (parse.quote(hypervisor_match, safe=''), target)) return self._list(url, 'hypervisors') def get(self, hypervisor): """ Get a specific hypervisor. + + :param hypervisor: Either a Hypervisor object or an ID. Starting with + microversion 2.53 the ID must be a UUID value. """ return self._get("/os-hypervisors/%s" % base.getid(hypervisor), "hypervisor") @@ -87,6 +107,9 @@ def get(self, hypervisor): def uptime(self, hypervisor): """ Get the uptime for a specific hypervisor. + + :param hypervisor: Either a Hypervisor object or an ID. Starting with + microversion 2.53 the ID must be a UUID value. """ return self._get("/os-hypervisors/%s/uptime" % base.getid(hypervisor), "hypervisor") diff --git a/novaclient/v2/services.py b/novaclient/v2/services.py index 46563e92e..3fe877d32 100644 --- a/novaclient/v2/services.py +++ b/novaclient/v2/services.py @@ -20,11 +20,16 @@ from novaclient import api_versions from novaclient import base +from novaclient import utils class Service(base.Resource): def __repr__(self): - return "" % self.binary + # If the id is int-like, then represent the service using it's binary + # name, otherwise use the UUID ID. + if utils.is_integer_like(self.id): + return "" % self.binary + return "" % self.id def _add_details(self, info): dico = 'resource' in info and info['resource'] or info @@ -70,27 +75,80 @@ def _update_body(self, host, binary, disabled_reason=None, body["forced_down"] = force_down return body + @api_versions.wraps('2.0', '2.52') def enable(self, host, binary): """Enable the service specified by hostname and binary.""" body = self._update_body(host, binary) return self._update("/os-services/enable", body, "service") + @api_versions.wraps('2.53') + def enable(self, service_uuid): + """Enable the service specified by the service UUID ID. + + :param service_uuid: The UUID ID of the service to enable. + """ + return self._update( + "/os-services/%s" % service_uuid, {'status': 'enabled'}, "service") + + @api_versions.wraps('2.0', '2.52') def disable(self, host, binary): """Disable the service specified by hostname and binary.""" body = self._update_body(host, binary) return self._update("/os-services/disable", body, "service") + @api_versions.wraps('2.53') + def disable(self, service_uuid): + """Disable the service specified by the service UUID ID. + + :param service_uuid: The UUID ID of the service to disable. + """ + return self._update("/os-services/%s" % service_uuid, + {'status': 'disabled'}, "service") + + @api_versions.wraps('2.0', '2.52') def disable_log_reason(self, host, binary, reason): """Disable the service with reason.""" body = self._update_body(host, binary, reason) return self._update("/os-services/disable-log-reason", body, "service") + @api_versions.wraps('2.53') + def disable_log_reason(self, service_uuid, reason): + """Disable the service with a reason. + + :param service_uuid: The UUID ID of the service to disable. + :param reason: The reason for disabling a service. The minimum length + is 1 and the maximum length is 255. + """ + body = { + 'status': 'disabled', + 'disabled_reason': reason + } + return self._update("/os-services/%s" % service_uuid, body, "service") + def delete(self, service_id): - """Delete a service.""" + """Delete a service. + + :param service_id: Before microversion 2.53, this must be an integer id + and may not uniquely the service in a multi-cell deployment. + Starting with microversion 2.53 this must be a UUID. + """ return self._delete("/os-services/%s" % service_id) - @api_versions.wraps("2.11") + @api_versions.wraps("2.11", "2.52") def force_down(self, host, binary, force_down=None): """Force service state to down specified by hostname and binary.""" body = self._update_body(host, binary, force_down=force_down) return self._update("/os-services/force-down", body, "service") + + @api_versions.wraps("2.53") + def force_down(self, service_uuid, force_down): + """Update the service's ``forced_down`` field specified by the + service UUID ID. + + :param service_uuid: The UUID ID of the service. + :param force_down: Whether or not this service was forced down manually + by an administrator. This value is useful to know that some 3rd + party has verified the service should be marked down. + """ + return self._update("/os-services/%s" % service_uuid, + {'forced_down': force_down}, "service") diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 947fc31a3..14c0c4027 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -3474,6 +3474,9 @@ def do_service_list(cs, args): utils.print_list(result, columns) +# Before microversion 2.53, the service was identified using it's host/binary +# values. +@api_versions.wraps('2.0', '2.52') @utils.arg('host', metavar='', help=_('Name of host.')) # TODO(mriedem): Eventually just hard-code the binary to "nova-compute". @utils.arg('binary', metavar='', help=_('Service binary. The only ' @@ -3485,6 +3488,18 @@ def do_service_enable(cs, args): utils.print_list([result], ['Host', 'Binary', 'Status']) +# Starting in microversion 2.53, the service is identified by UUID ID. +@api_versions.wraps('2.53') +@utils.arg('id', metavar='', help=_('ID of the service as a UUID.')) +def do_service_enable(cs, args): + """Enable the service.""" + result = cs.services.enable(args.id) + utils.print_list([result], ['ID', 'Host', 'Binary', 'Status']) + + +# Before microversion 2.53, the service was identified using it's host/binary +# values. +@api_versions.wraps('2.0', '2.52') @utils.arg('host', metavar='', help=_('Name of host.')) # TODO(mriedem): Eventually just hard-code the binary to "nova-compute". @utils.arg('binary', metavar='', help=_('Service binary. The only ' @@ -3506,7 +3521,27 @@ def do_service_disable(cs, args): utils.print_list([result], ['Host', 'Binary', 'Status']) -@api_versions.wraps("2.11") +# Starting in microversion 2.53, the service is identified by UUID ID. +@api_versions.wraps('2.53') +@utils.arg('id', metavar='', help=_('ID of the service as a UUID.')) +@utils.arg( + '--reason', + metavar='', + help=_('Reason for disabling the service.')) +def do_service_disable(cs, args): + """Disable the service.""" + if args.reason: + result = cs.services.disable_log_reason(args.id, args.reason) + utils.print_list( + [result], ['ID', 'Host', 'Binary', 'Status', 'Disabled Reason']) + else: + result = cs.services.disable(args.id) + utils.print_list([result], ['ID', 'Host', 'Binary', 'Status']) + + +# Before microversion 2.53, the service was identified using it's host/binary +# values. +@api_versions.wraps("2.11", "2.52") @utils.arg('host', metavar='', help=_('Name of host.')) # TODO(mriedem): Eventually just hard-code the binary to "nova-compute". @utils.arg('binary', metavar='', help=_('Service binary. The only ' @@ -3524,9 +3559,37 @@ def do_service_force_down(cs, args): utils.print_list([result], ['Host', 'Binary', 'Forced down']) -@utils.arg('id', metavar='', help=_('ID of service.')) +# Starting in microversion 2.53, the service is identified by UUID ID. +@api_versions.wraps('2.53') +@utils.arg('id', metavar='', help=_('ID of the service as a UUID.')) +@utils.arg( + '--unset', + dest='force_down', + help=_("Unset the forced_down state of the service."), + action='store_false', + default=True) +def do_service_force_down(cs, args): + """Force service to down.""" + result = cs.services.force_down(args.id, args.force_down) + utils.print_list([result], ['ID', 'Host', 'Binary', 'Forced down']) + + +# Before microversion 2.53, the service was identified using it's host/binary +# values. +@api_versions.wraps('2.0', '2.52') +@utils.arg('id', metavar='', + help=_('ID of service as an integer. Note that this may not ' + 'uniquely identify a service in a multi-cell deployment.')) +def do_service_delete(cs, args): + """Delete the service by integer ID.""" + cs.services.delete(args.id) + + +# Starting in microversion 2.53, the service is identified by UUID ID. +@api_versions.wraps('2.53') +@utils.arg('id', metavar='', help=_('ID of service as a UUID.')) def do_service_delete(cs, args): - """Delete the service.""" + """Delete the service by UUID ID.""" cs.services.delete(args.id) @@ -3691,7 +3754,8 @@ def __init__(self, **kwargs): @utils.arg( 'hypervisor', metavar='', - help=_('Name or ID of the hypervisor to show the details of.')) + help=_('Name or ID of the hypervisor. Starting with microversion 2.53 ' + 'the ID must be a UUID.')) @utils.arg( '--wrap', dest='wrap', metavar='', default=40, help=_('Wrap the output to a specified length. ' @@ -3705,7 +3769,8 @@ def do_hypervisor_show(cs, args): @utils.arg( 'hypervisor', metavar='', - help=_('Name or ID of the hypervisor to show the uptime of.')) + help=_('Name or ID of the hypervisor. Starting with microversion 2.53 ' + 'the ID must be a UUID.')) def do_hypervisor_uptime(cs, args): """Display the uptime of the specified hypervisor.""" hyper = _find_hypervisor(cs, args.hypervisor) diff --git a/releasenotes/notes/microversion-v2_53-3463b546a38c5f84.yaml b/releasenotes/notes/microversion-v2_53-3463b546a38c5f84.yaml new file mode 100644 index 000000000..0ee4cfcb9 --- /dev/null +++ b/releasenotes/notes/microversion-v2_53-3463b546a38c5f84.yaml @@ -0,0 +1,35 @@ +--- +features: + - | + Added support for `microversion 2.53`_. The following changes were made + for the ``services`` commands and python API bindings: + + - The ``nova service-list`` command and API will have a UUID value for the + ``id`` field in the output and response, respectively. + - The ``nova service-enable`` command and API will require a UUID service + id value to uniquely identify the service rather than a ``host`` and + ``binary`` value. The UUID ``id`` field will also be in the command + output. + - The ``nova service-disable`` command and API will require a UUID service + id value to uniquely identify the service rather than a ``host`` and + ``binary`` value. The UUID ``id`` field will also be in the command + output. + - The ``nova service-force-down`` command and API will require a UUID + service id value to uniquely identify the service rather than a ``host`` + and ``binary`` value. The UUID ``id`` field will also be in the command + output. + - The ``nova service-delete`` command and API will require a UUID + service id value to uniquely identify the service rather than an integer + service id value. + + The following changes were made for the ``hypervisors`` commands and python + API bindings: + + - The ID field in the various ``nova hypervisor-*`` commands and + ``Hypervisor.id`` attribute in the API binding will now be a UUID value. + - If paging over hypervisors using ``nova hypervisor-list``, the + ``--marker`` must be a UUID value. + - The ``nova hypervisor-show`` and ``nova hypervisor-uptime`` commands and + APIs now take a UUID value for the hypervisor ID. + + .. _microversion 2.53: https://docs.openstack.org/nova/latest/api_microversion_history.html#id48 From 2f1668540331beb7a586919ec5f32b8d6b3881dd Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Wed, 26 Jul 2017 12:14:54 -0400 Subject: [PATCH 1343/1705] Change Service repr to use self.id always Before this change, the Service object repr was the binary for microversion < 2.53. With microversion >= 2.53, the Service repr became the id, which is a UUID. Using the binary never really made sense since if you have multiple nova-compute services, the binary is going to be the same for all of them in the repr and nothing is distinguishable. This changes the Service repr to just use the id, which is going to be the integer id value if microversion < 2.53 and the UUID id value if microversion >= 2.53. There is no release note for this change since the repr should not be treated as a contractual API. Change-Id: I3a7de2683e339295022efb279828ab1a91b3b62e --- novaclient/tests/unit/v2/test_services.py | 9 +-------- novaclient/v2/services.py | 5 ----- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/novaclient/tests/unit/v2/test_services.py b/novaclient/tests/unit/v2/test_services.py index 0c0434afd..ddc75eeb1 100644 --- a/novaclient/tests/unit/v2/test_services.py +++ b/novaclient/tests/unit/v2/test_services.py @@ -34,18 +34,11 @@ def test_list_services(self): svs = self.cs.services.list() self.assert_request_id(svs, fakes.FAKE_REQUEST_ID_LIST) self.cs.assert_called('GET', '/os-services') - expect_uuid_id = ( - api_versions.APIVersion(self.api_version) >= - api_versions.APIVersion('2.53')) for s in svs: self.assertIsInstance(s, self._get_service_type()) self.assertEqual('nova-compute', s.binary) self.assertEqual('host1', s.host) - if expect_uuid_id: - stringified = '' % s.id - else: - stringified = '' % s.binary - self.assertEqual(stringified, str(s)) + self.assertEqual('' % s.id, str(s)) def test_list_services_with_hostname(self): svs = self.cs.services.list(host='host2') diff --git a/novaclient/v2/services.py b/novaclient/v2/services.py index 3fe877d32..f3d1255dc 100644 --- a/novaclient/v2/services.py +++ b/novaclient/v2/services.py @@ -20,15 +20,10 @@ from novaclient import api_versions from novaclient import base -from novaclient import utils class Service(base.Resource): def __repr__(self): - # If the id is int-like, then represent the service using it's binary - # name, otherwise use the UUID ID. - if utils.is_integer_like(self.id): - return "" % self.binary return "" % self.id def _add_details(self, info): From 7024d84e258199d51607e176bb4d8ad5e44f6991 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Wed, 26 Jul 2017 12:44:34 -0400 Subject: [PATCH 1344/1705] Be clear about hypevisors.search used in a few CLIs The following CLIs are doing a pattern match on the hypervisor hostname when getting servers: 1. nova host-evacuate 2. nova host-evacuate-live (terrible name) 3. nova host-servers-migrate 4. nova host-meta (terrible name) The fact that the hypervisor host(s) are looked up using a pattern match is not clear in the help string for the host argument. This makes the help more clear and adds a warning about being specific if you only want to target a specific host for these operations. In a later change we may modify the behavior in these CLIs which allow you to find hypervisors via pattern matching, but for now we should at least update the help text so we can backport it. Change-Id: Icec03326bb4d9f898c04e10199038167ce5e3cce Partial-Bug: #1667794 --- novaclient/v2/shell.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 947fc31a3..1740bb1e1 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -4750,7 +4750,10 @@ def _server_evacuate(cs, server, args): "error_message": error_message}) -@utils.arg('host', metavar='', help='Name of host.') +@utils.arg('host', metavar='', + help='The hypervisor hostname (or pattern) to search for. ' + 'WARNING: Use a fully qualified domain name if you only ' + 'want to evacuate from a specific host.') @utils.arg( '--target_host', metavar='', @@ -4813,7 +4816,10 @@ def __init__(self, server_uuid, live_migration_accepted, error_message) -@utils.arg('host', metavar='', help='Name of host.') +@utils.arg('host', metavar='', + help='The hypervisor hostname (or pattern) to search for. ' + 'WARNING: Use a fully qualified domain name if you only ' + 'want to live migrate from a specific host.') @utils.arg( '--target-host', metavar='', @@ -4886,7 +4892,10 @@ def _server_migrate(cs, server): "error_message": error_message}) -@utils.arg('host', metavar='', help='Name of host.') +@utils.arg('host', metavar='', + help='The hypervisor hostname (or pattern) to search for. ' + 'WARNING: Use a fully qualified domain name if you only ' + 'want to cold migrate from a specific host.') def do_host_servers_migrate(cs, args): """Cold migrate all instances off the specified host to other available hosts. @@ -4963,10 +4972,10 @@ def do_list_extensions(cs, _args): utils.print_list(extensions, fields) -@utils.arg( - 'host', - metavar='', - help=_('Name of host.')) +@utils.arg('host', metavar='', + help='The hypervisor hostname (or pattern) to search for. ' + 'WARNING: Use a fully qualified domain name if you only ' + 'want to update metadata for servers on a specific host.') @utils.arg( 'action', metavar='', From ead61d6a2181ecc68f3d748640a8dc077e953278 Mon Sep 17 00:00:00 2001 From: nidhimittalhada Date: Thu, 27 Jul 2017 11:39:11 +0530 Subject: [PATCH 1345/1705] Help text for "--matching" is not clear. The help text for "hypervisor-list --matching" reads, "List hypervisors matching the given ." This is a bit confusing, since it does talk about hypervisors as plural, but it still talks about a singular hostname as opposed to a pattern being matched. Amended help text to make it clear. Change-Id: I56fd449001289732254ea99d9b75030834491703 Closes-Bug: #1706525 --- novaclient/v2/shell.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 14c0c4027..bdaf589de 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -3687,7 +3687,7 @@ def _do_hypervisor_list(cs, matching=None, limit=None, marker=None): '--matching', metavar='', default=None, - help=_('List hypervisors matching the given .')) + help=_('List hypervisors matching the given (or pattern).')) def do_hypervisor_list(cs, args): """List hypervisors.""" _do_hypervisor_list(cs, matching=args.matching) @@ -3698,7 +3698,7 @@ def do_hypervisor_list(cs, args): '--matching', metavar='', default=None, - help=_('List hypervisors matching the given . ' + help=_('List hypervisors matching the given (or pattern). ' 'If matching is used limit and marker options will be ignored.')) @utils.arg( '--marker', From ec15bf30e731058f06b28997b3c9b333a40eab99 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Tue, 20 Jun 2017 17:12:13 +0300 Subject: [PATCH 1346/1705] Allow tuple as for nics value `novaclient.servers.boot` accepts 'nics' argument with information about network interfaces to attach. Originally, it accepts a list or a string. In case if a list value, the proper method iterates over it for processing and constructs a right structure for Nova-API. A tuple type is similar to a list type in the processing method and can look more logical for some developers (a tuple is a sequence of immutable python object, which can be ideal for predefined nics) and doensn't require anything to change in code except one validation check. Also, this patch adds more info in the error message for validation step. Change-Id: Ief9d22fefb0a64afc346c56e832c6c0b2b3d8fcb --- novaclient/tests/unit/v2/test_servers.py | 16 ++++++++++++++-- novaclient/v2/servers.py | 5 +++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index b807a7dca..b750e1264 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -223,7 +223,7 @@ def wrapped_boot(url, key, *boot_args, **boot_kwargs): def test_create_server_boot_with_nics_ipv6(self): old_boot = self.cs.servers._boot nics = [{'net-id': '11111111-1111-1111-1111-111111111111', - 'v6-fixed-ip': '2001:db9:0:1::10'}] + 'v6-fixed-ip': '2001:db9:0:1::10'}] def wrapped_boot(url, key, *boot_args, **boot_kwargs): self.assertEqual(boot_kwargs['nics'], nics) @@ -963,7 +963,7 @@ def test_evacuate(self): self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') ret = self.cs.servers.evacuate(s, 'fake_target_host', - 'False', 'NewAdminPassword') + 'False', 'NewAdminPassword') self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') @@ -1034,6 +1034,18 @@ def test_create_server_with_nics_auto(self): flavor='1', nics='auto') + def test__validate_create_nics(self): + if self.cs.api_version > api_versions.APIVersion('2.36'): + self.assertRaises(ValueError, + self.cs.servers._validate_create_nics, None) + else: + self.cs.servers._validate_create_nics(None) + self.assertRaises(ValueError, + self.cs.servers._validate_create_nics, + mock.Mock()) + self.cs.servers._validate_create_nics(["foo", "bar"]) + self.cs.servers._validate_create_nics(("foo", "bar")) + class ServersV26Test(ServersTest): diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index 81a0b96b7..413774d5a 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -1273,8 +1273,9 @@ def _validate_create_nics(self, nics): if self.api_version > api_versions.APIVersion('2.36'): if not nics: raise ValueError('nics are required after microversion 2.36') - elif nics and not isinstance(nics, list): - raise ValueError('nics must be a list') + elif nics and not isinstance(nics, (list, tuple)): + raise ValueError('nics must be a list or a tuple, not %s' % + type(nics)) def create(self, name, image, flavor, meta=None, files=None, reservation_id=None, min_count=None, From 81b0573b53f9b886d2e74009a6a35715b4f00027 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 4 Jul 2017 10:39:06 +0100 Subject: [PATCH 1347/1705] doc: Remove Makefile Why is this even here? Change-Id: I3886944fa2a76aad31e4408ab4361da0bcf8e99e --- doc/Makefile | 90 ---------------------------------------------------- 1 file changed, 90 deletions(-) delete mode 100644 doc/Makefile diff --git a/doc/Makefile b/doc/Makefile deleted file mode 100644 index 73aeb6edf..000000000 --- a/doc/Makefile +++ /dev/null @@ -1,90 +0,0 @@ -# Makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -SPHINXSOURCE = source -PAPER = -BUILDDIR = build - -# Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) $(SPHINXSOURCE) - -.PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest - -help: - @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" - -clean: - -rm -rf $(BUILDDIR)/* - -html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/python-novaclient.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/python-novaclient.qhc" - -latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ - "run these through (pdf)latex." - -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." - -linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." - -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." From 79ae29aaee7a84eff9f9eeff40a5a5587a6b4493 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Thu, 27 Jul 2017 20:33:04 +0000 Subject: [PATCH 1348/1705] Updated from global requirements Change-Id: Ic4fcb25e7577b671ba41ba6493a2f33450735c64 --- requirements.txt | 2 +- test-requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 17b9cf6ad..bc39486d1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. pbr!=2.1.0,>=2.0.0 # Apache-2.0 -keystoneauth1>=3.0.1 # Apache-2.0 +keystoneauth1>=3.1.0 # Apache-2.0 iso8601>=0.1.11 # MIT oslo.i18n!=3.15.2,>=2.1.0 # Apache-2.0 oslo.serialization!=2.19.1,>=1.10.0 # Apache-2.0 diff --git a/test-requirements.txt b/test-requirements.txt index 21c3c6990..a9bd543ad 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -15,7 +15,7 @@ python-neutronclient>=6.3.0 # Apache-2.0 requests-mock>=1.1 # Apache-2.0 sphinx>=1.6.2 # BSD os-client-config>=1.28.0 # Apache-2.0 -openstackdocstheme>=1.11.0 # Apache-2.0 +openstackdocstheme>=1.16.0 # Apache-2.0 osprofiler>=1.4.0 # Apache-2.0 testrepository>=0.0.18 # Apache-2.0/BSD testscenarios>=0.4 # Apache-2.0/BSD From 561b35a8a813c6457508c284a98392d56c5ee5e7 Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Fri, 28 Jul 2017 11:58:06 +0900 Subject: [PATCH 1349/1705] Update URLs in docs, comments and setup.cfg Some URLs are broken, so fix them. The other URLs are redirect to new URLs, so replace them with new ones. Change-Id: Ida2fd70ad0d7b029fadfe91be14d7180b9b5b8d1 Closes-Bug: #1707104 --- CONTRIBUTING.rst | 4 ++-- HACKING.rst | 2 +- README.rst | 8 ++++---- bindep.txt | 2 +- doc/source/contributor/index.rst | 2 +- doc/source/contributor/testing.rst | 2 +- doc/source/reference/api/index.rst | 2 +- novaclient/i18n.py | 2 +- novaclient/v2/servers.py | 2 +- setup.cfg | 2 +- 10 files changed, 14 insertions(+), 14 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index bdb4ad7eb..20115405c 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -1,13 +1,13 @@ If you would like to contribute to the development of OpenStack, you must follow the steps documented at: - http://docs.openstack.org/infra/manual/developers.html#development-workflow + https://docs.openstack.org/infra/manual/developers.html#development-workflow Once those steps have been completed, changes to OpenStack should be submitted for review via the Gerrit tool, following the workflow documented at: - http://docs.openstack.org/infra/manual/developers.html#development-workflow + https://docs.openstack.org/infra/manual/developers.html#development-workflow Pull requests submitted through GitHub will be ignored. diff --git a/HACKING.rst b/HACKING.rst index 49eba1184..5a5d01d7f 100644 --- a/HACKING.rst +++ b/HACKING.rst @@ -2,7 +2,7 @@ Nova Client Style Commandments ============================== - Step 1: Read the OpenStack Style Commandments - http://docs.openstack.org/developer/hacking + https://docs.openstack.org/hacking/latest - Step 2: Read on diff --git a/README.rst b/README.rst index 00f86f46d..acf6493af 100644 --- a/README.rst +++ b/README.rst @@ -2,8 +2,8 @@ Team and repository tags ======================== -.. image:: http://governance.openstack.org/badges/python-novaclient.svg - :target: http://governance.openstack.org/reference/tags/index.html +.. image:: https://governance.openstack.org/tc/badges/python-novaclient.svg + :target: https://governance.openstack.org/tc/reference/tags/index.html .. Change things from this point on @@ -34,10 +34,10 @@ This is a client for the OpenStack Compute API. It provides a Python API (the * `How to Contribute`_ .. _PyPi: https://pypi.python.org/pypi/python-novaclient -.. _Online Documentation: http://docs.openstack.org/python-novaclient +.. _Online Documentation: https://docs.openstack.org/python-novaclient/latest .. _Launchpad project: https://launchpad.net/python-novaclient .. _Blueprints: https://blueprints.launchpad.net/python-novaclient .. _Bugs: https://bugs.launchpad.net/python-novaclient .. _Source: https://git.openstack.org/cgit/openstack/python-novaclient -.. _How to Contribute: http://docs.openstack.org/infra/manual/developers.html +.. _How to Contribute: https://docs.openstack.org/infra/manual/developers.html .. _Specs: http://specs.openstack.org/openstack/nova-specs/ diff --git a/bindep.txt b/bindep.txt index 5c2a93afa..7aea44026 100644 --- a/bindep.txt +++ b/bindep.txt @@ -1,5 +1,5 @@ # This is a cross-platform list tracking distribution packages needed by tests; -# see http://docs.openstack.org/infra/bindep/ for additional information. +# see https://docs.openstack.org/infra/bindep/ for additional information. build-essential [platform:dpkg] dbus-devel [platform:rpm] diff --git a/doc/source/contributor/index.rst b/doc/source/contributor/index.rst index cf8dccae1..4df1ed3f3 100644 --- a/doc/source/contributor/index.rst +++ b/doc/source/contributor/index.rst @@ -8,7 +8,7 @@ Code is hosted at `git.openstack.org`__. Submit bugs to the Nova project on __ https://git.openstack.org/cgit/openstack/python-novaclient __ https://launchpad.net/nova -__ http://docs.openstack.org/infra/manual/developers.html#development-workflow +__ https://docs.openstack.org/infra/manual/developers.html#development-workflow .. toctree:: :maxdepth: 2 diff --git a/doc/source/contributor/testing.rst b/doc/source/contributor/testing.rst index c4e59143f..b789dc2d2 100644 --- a/doc/source/contributor/testing.rst +++ b/doc/source/contributor/testing.rst @@ -18,7 +18,7 @@ test targets that can be run to validate the code. Live functional testing against an existing OpenStack instance. Functional testing assumes the existence of a `clouds.yaml` file as supported -by `os-client-config `__ +by `os-client-config `__ It assumes the existence of a cloud named `devstack` that behaves like a normal DevStack installation with a demo and an admin user/tenant - or clouds named `functional_admin` and `functional_nonadmin`. diff --git a/doc/source/reference/api/index.rst b/doc/source/reference/api/index.rst index c5ed6d885..9e93cdab2 100644 --- a/doc/source/reference/api/index.rst +++ b/doc/source/reference/api/index.rst @@ -55,7 +55,7 @@ application, you can append a (name, version) tuple to the session's For more information on this keystoneauth API, see `Using Sessions`_. -.. _Using Sessions: http://docs.openstack.org/developer/keystoneauth/using-sessions.html +.. _Using Sessions: https://docs.openstack.org/keystoneauth/latest/using-sessions.html It is also possible to use an instance as a context manager in which case there will be a session kept alive for the duration of the with statement:: diff --git a/novaclient/i18n.py b/novaclient/i18n.py index ccf8e583a..21c0addaa 100644 --- a/novaclient/i18n.py +++ b/novaclient/i18n.py @@ -12,7 +12,7 @@ """oslo_i18n integration module for novaclient. -See http://docs.openstack.org/developer/oslo.i18n/usage.html . +See https://docs.openstack.org/oslo.i18n/latest/user/usage.html . """ diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index 413774d5a..ef94b8c92 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -820,7 +820,7 @@ def list(self, detailed=True, search_opts=None, marker=None, limit=None, match the search_opts (optional). The search opts format is a dictionary of key / value pairs that will be appended to the query string. For a complete list of keys see: - http://developer.openstack.org/api-ref-compute-v2.1.html#listServers + https://developer.openstack.org/api-ref/compute/#list-servers :param marker: Begin returning servers that appear later in the server list than that represented by this server id (optional). :param limit: Maximum number of servers to return (optional). diff --git a/setup.cfg b/setup.cfg index 5aa68c2bc..f1905a5d7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -6,7 +6,7 @@ description-file = license = Apache License, Version 2.0 author = OpenStack author-email = openstack-dev@lists.openstack.org -home-page = http://docs.openstack.org/developer/python-novaclient +home-page = https://docs.openstack.org/python-novaclient/latest classifier = Development Status :: 5 - Production/Stable Environment :: Console From a7aee6481e93c02cf1c9256d610bfd5bd719d7dc Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Fri, 28 Jul 2017 12:23:08 +0900 Subject: [PATCH 1350/1705] Remove run_tests.sh TrivialFix Change-Id: Iee216bd7f28d72aacb9aecb0470ab833edbe5e2b --- .../remove-run_tests.sh-3bdcaee4d388177a.yaml | 4 ++ run_tests.sh | 46 ------------------- 2 files changed, 4 insertions(+), 46 deletions(-) create mode 100644 releasenotes/notes/remove-run_tests.sh-3bdcaee4d388177a.yaml delete mode 100755 run_tests.sh diff --git a/releasenotes/notes/remove-run_tests.sh-3bdcaee4d388177a.yaml b/releasenotes/notes/remove-run_tests.sh-3bdcaee4d388177a.yaml new file mode 100644 index 000000000..524a2c048 --- /dev/null +++ b/releasenotes/notes/remove-run_tests.sh-3bdcaee4d388177a.yaml @@ -0,0 +1,4 @@ +--- +upgrade: + - The ``run_tests.sh`` shell script that was deprecated in Newton has + been removed. diff --git a/run_tests.sh b/run_tests.sh deleted file mode 100755 index 9cee6751b..000000000 --- a/run_tests.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/bin/bash - -set -eu - -cat < Date: Fri, 28 Jul 2017 04:25:45 +0000 Subject: [PATCH 1351/1705] Updated from global requirements Change-Id: I449ac2094e2d03f2bf397dfea84b800ad8aa52dc --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index a9bd543ad..67448b0f5 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -9,7 +9,7 @@ fixtures>=3.0.0 # Apache-2.0/BSD keyring>=5.5.1 # MIT/PSF mock>=2.0 # BSD python-keystoneclient>=3.8.0 # Apache-2.0 -python-cinderclient>=3.0.0 # Apache-2.0 +python-cinderclient>=3.1.0 # Apache-2.0 python-glanceclient>=2.7.0 # Apache-2.0 python-neutronclient>=6.3.0 # Apache-2.0 requests-mock>=1.1 # Apache-2.0 From ed33a53249d9e34c99056d0bbc4b241fd6b70e74 Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Fri, 28 Jul 2017 21:07:47 +0000 Subject: [PATCH 1352/1705] Update reno for stable/pike Change-Id: I3132ef5f22619c61fbdba4ab02991a5f26966176 --- releasenotes/source/index.rst | 1 + releasenotes/source/pike.rst | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 releasenotes/source/pike.rst diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index 9a3636472..cf41b5ca8 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -8,6 +8,7 @@ Contents :maxdepth: 2 unreleased + pike ocata newton mitaka diff --git a/releasenotes/source/pike.rst b/releasenotes/source/pike.rst new file mode 100644 index 000000000..e43bfc0ce --- /dev/null +++ b/releasenotes/source/pike.rst @@ -0,0 +1,6 @@ +=================================== + Pike Series Release Notes +=================================== + +.. release-notes:: + :branch: stable/pike From 0fe136a9e032f47c8d61ec82f1dbe5ffd3d8fedf Mon Sep 17 00:00:00 2001 From: yanghuichan Date: Fri, 4 Aug 2017 15:51:46 +0800 Subject: [PATCH 1353/1705] Replace six.itervalues() with dict.values() in python-novaclient 1.As mentioned in [1], we should avoid using six.itervalues to achieve iterators. We can use dict.values instead, as it will return iterators in PY3 as well. And dict.values will more readable. 2.In py2, the performance about list should be negligible, see the link [2]. [1] https://wiki.openstack.org/wiki/Python3#Common_patterns [2] http://lists.openstack.org/pipermail/openstack-dev/2015-June/066391.html Change-Id: I77df96b09fc0b66449339a474ac725edb890c0bc --- novaclient/v2/shell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 63c665901..670c717a7 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -3864,7 +3864,7 @@ def do_ssh(cs, args): msg = _("Server '%(server)s' is not attached to any network.") raise exceptions.CommandError(msg % {'server': args.server}) else: - network_addresses = list(six.itervalues(addresses))[0] + network_addresses = list(addresses.values())[0] # Select the address in the selected network. # If the extension is not present, we assume the address to be floating. From da44174c54d367e9ad3a2a4578fca1c6ad41c911 Mon Sep 17 00:00:00 2001 From: Jeremy Liu Date: Fri, 26 May 2017 22:54:15 +0800 Subject: [PATCH 1354/1705] Fix reservation_id not supported by Nova API Nova API accepts "return_reservation_id" instead of "reservation_id" when creating instances [1]. This patch fixed that. [1] https://github.com/openstack/nova/blob/master/nova/api/openstack/compute/schemas/multiple_create.py#L19 Change-Id: Ie58248aa1cd981b877c7e592db698faffcd01d9a Closes-bug: #1693818 --- novaclient/tests/unit/fixture_data/servers.py | 3 +++ novaclient/tests/unit/v2/test_servers.py | 11 +++++++++++ novaclient/v2/servers.py | 12 +++++++----- novaclient/v2/shell.py | 16 ++++++++++++++-- 4 files changed, 35 insertions(+), 7 deletions(-) diff --git a/novaclient/tests/unit/fixture_data/servers.py b/novaclient/tests/unit/fixture_data/servers.py index 7232fb589..9822fa5b0 100644 --- a/novaclient/tests/unit/fixture_data/servers.py +++ b/novaclient/tests/unit/fixture_data/servers.py @@ -417,6 +417,9 @@ def post_servers(self, request, context): if 'personality' in body['server']: for pfile in body['server']['personality']: fakes.assert_has_keys(pfile, required=['path', 'contents']) + if ('return_reservation_id' in body['server'].keys() and + body['server']['return_reservation_id']): + return {'reservation_id': 'r-3fhpjulh'} if body['server']['name'] == 'some-bad-server': body = self.server_1235 else: diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index b750e1264..863368574 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -390,6 +390,17 @@ def test_create_server_disk_config_auto(self): def test_create_server_disk_config_manual(self): self._create_disk_config('MANUAL') + def test_create_server_return_reservation_id(self): + s = self.cs.servers.create( + name="My server", + image=1, + flavor=1, + reservation_id=True, + nics=self._get_server_create_default_nics() + ) + self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers') + def test_update_server(self): s = self.cs.servers.get(1234) diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index ef94b8c92..4c38ed700 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -657,7 +657,7 @@ class ServerManager(base.BootingManagerWithFind): def _boot(self, resource_url, response_key, name, image, flavor, meta=None, files=None, userdata=None, - reservation_id=None, return_raw=False, min_count=None, + reservation_id=False, return_raw=False, min_count=None, max_count=None, security_groups=None, key_name=None, availability_zone=None, block_device_mapping=None, block_device_mapping_v2=None, nics=None, scheduler_hints=None, @@ -695,7 +695,8 @@ def _boot(self, resource_url, response_key, name, image, flavor, if meta: body["server"]["metadata"] = meta if reservation_id: - body["server"]["reservation_id"] = reservation_id + body["server"]["return_reservation_id"] = reservation_id + return_raw = True if key_name: body["server"]["key_name"] = key_name if scheduler_hints: @@ -1278,7 +1279,7 @@ def _validate_create_nics(self, nics): type(nics)) def create(self, name, image, flavor, meta=None, files=None, - reservation_id=None, min_count=None, + reservation_id=False, min_count=None, max_count=None, security_groups=None, userdata=None, key_name=None, availability_zone=None, block_device_mapping=None, block_device_mapping_v2=None, @@ -1300,7 +1301,8 @@ def create(self, name, image, flavor, meta=None, files=None, are the file contents (either as a string or as a file-like object). A maximum of five entries is allowed, and each file must be 10k or less. - :param reservation_id: a UUID for the set of servers being requested. + :param reservation_id: return a reservation_id for the set of + servers being requested, boolean. :param min_count: (optional extension) The minimum number of servers to launch. :param max_count: (optional extension) The maximum number of @@ -1399,7 +1401,7 @@ def create(self, name, image, flavor, meta=None, files=None, if nics: boot_kwargs['nics'] = nics - response_key = "server" + response_key = "server" if not reservation_id else "reservation_id" return self._boot(resource_url, response_key, *boot_args, **boot_kwargs) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 670c717a7..708a649bf 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -525,7 +525,8 @@ def _boot(cs, args): config_drive=config_drive, admin_pass=args.admin_pass, access_ip_v4=args.access_ip_v4, - access_ip_v6=args.access_ip_v6) + access_ip_v6=args.access_ip_v6, + reservation_id=args.return_reservation_id) if 'description' in args: boot_kwargs["description"] = args.description @@ -887,6 +888,12 @@ def _boot(cs, args): help=_('Tags for the server.' 'Tags must be separated by commas: --tags '), start_version="2.52") +@utils.arg( + '--return-reservation-id', + dest='return_reservation_id', + action="store_true", + default=False, + help=_("Return a reservation id bound to created servers.")) def do_boot(cs, args): """Boot a new server.""" boot_args, boot_kwargs = _boot(cs, args) @@ -895,7 +902,12 @@ def do_boot(cs, args): boot_kwargs.update(extra_boot_kwargs) server = cs.servers.create(*boot_args, **boot_kwargs) - _print_server(cs, args, server) + if boot_kwargs['reservation_id']: + new_server = {'reservation_id': server} + utils.print_dict(new_server) + return + else: + _print_server(cs, args, server) if args.poll: _poll_for_status(cs.servers.get, server.id, 'building', ['active']) From 74ea37df5e8273c93a485233fd5ca4077cc03e5a Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Mon, 7 Aug 2017 00:53:37 +0000 Subject: [PATCH 1355/1705] Updated from global requirements Change-Id: I73432970608b418e78990388258540e71e6143f1 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 67448b0f5..c6bfd8283 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -10,7 +10,7 @@ keyring>=5.5.1 # MIT/PSF mock>=2.0 # BSD python-keystoneclient>=3.8.0 # Apache-2.0 python-cinderclient>=3.1.0 # Apache-2.0 -python-glanceclient>=2.7.0 # Apache-2.0 +python-glanceclient>=2.8.0 # Apache-2.0 python-neutronclient>=6.3.0 # Apache-2.0 requests-mock>=1.1 # Apache-2.0 sphinx>=1.6.2 # BSD From e77cf002f04eca692d1a69fc5461e06ad283659f Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 4 Jul 2017 11:54:38 +0100 Subject: [PATCH 1356/1705] tools: Remove dead script This is no longer the recommended way to do things. Just remove it. Change-Id: I2056169955b47e08993461012edb554d3311de40 --- tools/with_venv.sh | 4 ---- 1 file changed, 4 deletions(-) delete mode 100755 tools/with_venv.sh diff --git a/tools/with_venv.sh b/tools/with_venv.sh deleted file mode 100755 index c8d2940fc..000000000 --- a/tools/with_venv.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -TOOLS=`dirname $0` -VENV=$TOOLS/../.venv -source $VENV/bin/activate && $@ From 8353b3c897b5f7e5e5575c207a4f3a61beb18d53 Mon Sep 17 00:00:00 2001 From: huangtianhua Date: Fri, 4 Aug 2017 10:12:16 +0800 Subject: [PATCH 1357/1705] Remove substitutions for command error msg For server_tag_add and server_tag_delete commands, the substitution in the command error message doesn't work, this change removes the substitutions, and to tell user the 'specified' tag can not be added or deleted is enough, like other commands do. Change-Id: I7a9c9103d4fc88e1b82458f6ccc9738c6aa0315c Closes-Bug: #1708408 --- novaclient/v2/shell.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 708a649bf..45daaf1f2 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -4728,7 +4728,7 @@ def do_server_tag_add(cs, args): lambda t: server.add_tag(t), args.tag, _("Request to add tag %s to specified server has been accepted."), - _("Unable to add tag %s to the specified server.")) + _("Unable to add the specified tag to the server.")) @api_versions.wraps("2.26") @@ -4750,7 +4750,7 @@ def do_server_tag_delete(cs, args): lambda t: server.delete_tag(t), args.tag, _("Request to delete tag %s from specified server has been accepted."), - _("Unable to delete tag %s from specified server.")) + _("Unable to delete the specified tag from the server.")) @api_versions.wraps("2.26") From 0a3cf89c67a3fb78876137506fe57e645f4fabe4 Mon Sep 17 00:00:00 2001 From: huangtianhua Date: Wed, 9 Aug 2017 14:39:51 +0800 Subject: [PATCH 1358/1705] Allow boot server with multiple nics Trying to boot a server while specifying multiple --nic parameters leads to an error: Invalid nic argument. This patch fixes it. Change-Id: I662fd366df8e79db1966d45a9e090087dbace4b0 Closes-Bug: #1706597 --- novaclient/tests/unit/v2/test_shell.py | 23 +++++++++++++++++++ novaclient/v2/shell.py | 18 +++++++-------- ...g-with-multiple-nics-c6e5885b948d35ba.yaml | 4 ++++ 3 files changed, 36 insertions(+), 9 deletions(-) create mode 100644 releasenotes/notes/fix-booting-with-multiple-nics-c6e5885b948d35ba.yaml diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 2a1514552..7b33d90fd 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -695,6 +695,29 @@ def test_boot_nics(self): }, ) + def test_boot_with_multiple_nics(self): + cmd = ('boot --image %s --flavor 1 ' + '--nic net-id=net_a,v4-fixed-ip=10.0.0.1 ' + '--nic net-id=net_b some-server' % + FAKE_UUID_1) + self.run_command(cmd) + self.assert_called_anytime( + 'POST', '/servers', + { + 'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': FAKE_UUID_1, + 'min_count': 1, + 'max_count': 1, + 'networks': [ + {'uuid': 'net_a', 'fixed_ip': '10.0.0.1'}, + {'uuid': 'net_b'} + ], + }, + }, + ) + def test_boot_nics_with_tag(self): cmd = ('boot --image %s --flavor 1 ' '--nic net-id=a=c,v4-fixed-ip=10.0.0.1,tag=foo some-server' % diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 708a649bf..677e2c02f 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -268,12 +268,11 @@ def _parse_nics(cs, args): supports_auto_alloc = cs.api_version >= api_versions.APIVersion('2.37') supports_nic_tags = _supports_nic_tags(cs) - nic_info = {"net-id": "", "v4-fixed-ip": "", "v6-fixed-ip": "", - "port-id": "", "net-name": ""} + nic_keys = {'net-id', 'v4-fixed-ip', 'v6-fixed-ip', 'port-id', 'net-name'} if supports_auto_alloc and supports_nic_tags: # API version >= 2.42 - nic_info.update({"tag": ""}) + nic_keys.add('tag') err_msg = (_("Invalid nic argument '%s'. Nic arguments must be of " "the form --nic = API version >= 2.32 - nic_info.update({"tag": ""}) + nic_keys.add('tag') err_msg = (_("Invalid nic argument '%s'. Nic arguments must be of " "the form --nic Date: Fri, 18 Aug 2017 11:41:58 +0000 Subject: [PATCH 1359/1705] Updated from global requirements Change-Id: Iadb6ed4585639b21df50533f89d164b76bca2b11 --- test-requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test-requirements.txt b/test-requirements.txt index c6bfd8283..382447d63 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -7,12 +7,12 @@ bandit>=1.1.0 # Apache-2.0 coverage!=4.4,>=4.0 # Apache-2.0 fixtures>=3.0.0 # Apache-2.0/BSD keyring>=5.5.1 # MIT/PSF -mock>=2.0 # BSD +mock>=2.0.0 # BSD python-keystoneclient>=3.8.0 # Apache-2.0 python-cinderclient>=3.1.0 # Apache-2.0 python-glanceclient>=2.8.0 # Apache-2.0 python-neutronclient>=6.3.0 # Apache-2.0 -requests-mock>=1.1 # Apache-2.0 +requests-mock>=1.1.0 # Apache-2.0 sphinx>=1.6.2 # BSD os-client-config>=1.28.0 # Apache-2.0 openstackdocstheme>=1.16.0 # Apache-2.0 @@ -23,4 +23,4 @@ testtools>=1.4.0 # MIT tempest>=16.1.0 # Apache-2.0 # releasenotes -reno!=2.3.1,>=1.8.0 # Apache-2.0 +reno>=2.5.0 # Apache-2.0 From 4c483322fe5454c8ece66cc9c86cbc0702e14368 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Sat, 2 Sep 2017 12:12:46 +0000 Subject: [PATCH 1360/1705] Updated from global requirements Change-Id: Ibc3f4fb199e47a29e721579096413c008724fee9 --- requirements.txt | 2 +- test-requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index bc39486d1..1bf1ae409 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. pbr!=2.1.0,>=2.0.0 # Apache-2.0 -keystoneauth1>=3.1.0 # Apache-2.0 +keystoneauth1>=3.2.0 # Apache-2.0 iso8601>=0.1.11 # MIT oslo.i18n!=3.15.2,>=2.1.0 # Apache-2.0 oslo.serialization!=2.19.1,>=1.10.0 # Apache-2.0 diff --git a/test-requirements.txt b/test-requirements.txt index 382447d63..7c90f6637 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -9,7 +9,7 @@ fixtures>=3.0.0 # Apache-2.0/BSD keyring>=5.5.1 # MIT/PSF mock>=2.0.0 # BSD python-keystoneclient>=3.8.0 # Apache-2.0 -python-cinderclient>=3.1.0 # Apache-2.0 +python-cinderclient>=3.2.0 # Apache-2.0 python-glanceclient>=2.8.0 # Apache-2.0 python-neutronclient>=6.3.0 # Apache-2.0 requests-mock>=1.1.0 # Apache-2.0 From 04371ebc7cec00d87055d35f2fb83f1bbf88d9ed Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Mon, 11 Sep 2017 21:49:08 +0000 Subject: [PATCH 1361/1705] Updated from global requirements Change-Id: If6d224e42f5c13ee8813abe454e4733fb1c7a33f --- requirements.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index 1bf1ae409..1a44faf31 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,10 +4,10 @@ pbr!=2.1.0,>=2.0.0 # Apache-2.0 keystoneauth1>=3.2.0 # Apache-2.0 iso8601>=0.1.11 # MIT -oslo.i18n!=3.15.2,>=2.1.0 # Apache-2.0 -oslo.serialization!=2.19.1,>=1.10.0 # Apache-2.0 -oslo.utils>=3.20.0 # Apache-2.0 +oslo.i18n>=3.15.3 # Apache-2.0 +oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0 +oslo.utils>=3.28.0 # Apache-2.0 PrettyTable<0.8,>=0.7.1 # BSD -simplejson>=2.2.0 # MIT +simplejson>=3.5.1 # MIT six>=1.9.0 # MIT Babel!=2.4.0,>=2.3.4 # BSD From 98ef444974fed3bed7009e5f4fb92002404e5e63 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Wed, 13 Sep 2017 13:02:47 +0000 Subject: [PATCH 1362/1705] Updated from global requirements Change-Id: Ifbc3bdaf404aeb15642de33678f5b1c05b8e93d5 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 7c90f6637..b04c4ffd7 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -15,7 +15,7 @@ python-neutronclient>=6.3.0 # Apache-2.0 requests-mock>=1.1.0 # Apache-2.0 sphinx>=1.6.2 # BSD os-client-config>=1.28.0 # Apache-2.0 -openstackdocstheme>=1.16.0 # Apache-2.0 +openstackdocstheme>=1.17.0 # Apache-2.0 osprofiler>=1.4.0 # Apache-2.0 testrepository>=0.0.18 # Apache-2.0/BSD testscenarios>=0.4 # Apache-2.0/BSD From c0305a1e180cc2a101c94b50b423adea37c5578d Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Mon, 16 Oct 2017 17:53:34 +0900 Subject: [PATCH 1363/1705] Update "The nova Shell Utility" in the user guide Change-Id: I02025f364a11b7f41935fe835407852fc37b4d87 Closes-Bug: #1723895 --- doc/source/user/shell.rst | 49 ++++++++++++++++++++++++++++----------- 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/doc/source/user/shell.rst b/doc/source/user/shell.rst index b2d7d9ef8..bd1fb7e93 100644 --- a/doc/source/user/shell.rst +++ b/doc/source/user/shell.rst @@ -8,33 +8,52 @@ The :program:`nova` shell utility interacts with OpenStack Nova API from the command line. It supports the entirety of the OpenStack Nova API. -First, you'll need an OpenStack Nova account and an API key. You get this by -using the `nova-manage` command in OpenStack Nova. - -You'll need to provide :program:`nova` with your OpenStack username and API -key. You can do this with the `--os-username`, `--os-password` and -`--os-tenant-id` options, but it's easier to just set them as environment -variables by setting some environment variables: +You'll need to provide :program:`nova` with your OpenStack Keystone user +information. You can do this with the `--os-username`, `--os-password`, +`--os-project-name` (`--os-project-id`), `--os-project-domain-name` +(`--os-project-domain-id`) and `--os-user-domain-name` (`--os-user-domain-id`) +options, but it's easier to just set them as environment variables by setting +some environment variables: .. envvar:: OS_USERNAME - Your OpenStack Nova username. + Your OpenStack Keystone user name. .. envvar:: OS_PASSWORD Your password. -.. envvar:: OS_TENANT_NAME +.. envvar:: OS_PROJECT_NAME + + The name of project for work. + +.. envvar:: OS_PROJECT_ID + + The ID of project for work. + +.. envvar:: OS_PROJECT_DOMAIN_NAME + + The name of domain containing the project. + +.. envvar:: OS_PROJECT_DOMAIN_ID + + The ID of domain containing the project. + +.. envvar:: OS_USER_DOMAIN_NAME + + The user's domain name. + +.. envvar:: OS_USER_DOMAIN_ID - Project for work. + The user's domain ID. .. envvar:: OS_AUTH_URL - The OpenStack API server URL. + The OpenStack Keystone endpoint URL. .. envvar:: OS_COMPUTE_API_VERSION - The OpenStack API version. + The OpenStack Nova API version (microversion). .. envvar:: OS_REGION_NAME @@ -45,8 +64,10 @@ For example, in Bash you'd use:: export OS_USERNAME=yourname export OS_PASSWORD=yadayadayada - export OS_TENANT_NAME=myproject - export OS_AUTH_URL=http://:5000/v3/ + export OS_PROJECT_NAME=myproject + export OS_PROJECT_DOMAIN_NAME=default + export OS_USER_DOMAIN_NAME=default + export OS_AUTH_URL=http:///identity export OS_COMPUTE_API_VERSION=2.1 From there, all shell commands take the form:: From 4707422377214829126f1d6352119ea08a7ec104 Mon Sep 17 00:00:00 2001 From: Dai Dang Van Date: Tue, 17 Oct 2017 14:04:01 +0700 Subject: [PATCH 1364/1705] Use generic user for both zuul v2 and v3 Zuul v2 uses 'jenkins' as user, but Zuul v3 uses 'zuul'. Using $USER solves it for both cases. Change-Id: Ia3f2646bd3b5fa87c90c8dac57117fd29d73706b --- novaclient/tests/functional/hooks/post_test_hook.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/novaclient/tests/functional/hooks/post_test_hook.sh b/novaclient/tests/functional/hooks/post_test_hook.sh index 766352933..473393eb6 100755 --- a/novaclient/tests/functional/hooks/post_test_hook.sh +++ b/novaclient/tests/functional/hooks/post_test_hook.sh @@ -21,14 +21,14 @@ function generate_testr_results { sudo /usr/os-testr-env/bin/subunit2html $BASE/logs/testrepository.subunit $BASE/logs/testr_results.html sudo gzip -9 $BASE/logs/testrepository.subunit sudo gzip -9 $BASE/logs/testr_results.html - sudo chown jenkins:jenkins $BASE/logs/testrepository.subunit.gz $BASE/logs/testr_results.html.gz + sudo chown $USER:$USER $BASE/logs/testrepository.subunit.gz $BASE/logs/testr_results.html.gz sudo chmod a+r $BASE/logs/testrepository.subunit.gz $BASE/logs/testr_results.html.gz fi } export NOVACLIENT_DIR="$BASE/new/python-novaclient" -sudo chown -R jenkins:stack $NOVACLIENT_DIR +sudo chown -R $USER:stack $NOVACLIENT_DIR # Go to the novaclient dir cd $NOVACLIENT_DIR @@ -37,7 +37,7 @@ cd $NOVACLIENT_DIR echo "Running novaclient functional test suite" set +e # Preserve env for OS_ credentials -sudo -E -H -u jenkins tox -e ${TOX_ENV:-functional} +sudo -E -H -u $USER tox -e ${TOX_ENV:-functional} EXIT_CODE=$? set -e From 3f803bf0ab3a19f6937c07861d310899436d1e70 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Mon, 13 Nov 2017 10:28:24 +0000 Subject: [PATCH 1365/1705] Updated from global requirements Change-Id: Id0466fb1c6defa71c8b588f731df22c2f009cd19 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1a44faf31..caeb18b91 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ keystoneauth1>=3.2.0 # Apache-2.0 iso8601>=0.1.11 # MIT oslo.i18n>=3.15.3 # Apache-2.0 oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0 -oslo.utils>=3.28.0 # Apache-2.0 +oslo.utils>=3.31.0 # Apache-2.0 PrettyTable<0.8,>=0.7.1 # BSD simplejson>=3.5.1 # MIT six>=1.9.0 # MIT From 2aeccd9527849a96a1c9386e6c7263b279757bf4 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Wed, 15 Nov 2017 12:02:15 +0000 Subject: [PATCH 1366/1705] Updated from global requirements Change-Id: Ie231f3abecb182f46019ad611aefb77fb6fadce6 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index b04c4ffd7..da9857a59 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -20,7 +20,7 @@ osprofiler>=1.4.0 # Apache-2.0 testrepository>=0.0.18 # Apache-2.0/BSD testscenarios>=0.4 # Apache-2.0/BSD testtools>=1.4.0 # MIT -tempest>=16.1.0 # Apache-2.0 +tempest>=17.1.0 # Apache-2.0 # releasenotes reno>=2.5.0 # Apache-2.0 From d9838dfdf5a27c7f195c6e03a8f8c2393ddb2be7 Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Tue, 17 Oct 2017 14:13:04 +0900 Subject: [PATCH 1367/1705] Fix missing metavar in aggregate-update Fix missing metavar in aggregate-update in order to fix inconsistency with other options. If 'metavar' is not added, 'NAME' is used instead of ''. But it should be ''. TrivialFix Change-Id: I22c012bac3381f63a3bfb5a5d0c6f0260d174695 --- novaclient/v2/shell.py | 1 + 1 file changed, 1 insertion(+) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 918984d0a..ea68e1a40 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -3220,6 +3220,7 @@ def do_aggregate_delete(cs, args): help=_('Name or ID of aggregate to update.')) @utils.arg( '--name', + metavar='', dest='name', help=_('New name for aggregate.')) @utils.arg( From aca318c8e3d5918465a4e3502d4d631fa22f2acf Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Thu, 16 Nov 2017 11:25:20 +0000 Subject: [PATCH 1368/1705] Updated from global requirements Change-Id: Iefb2680c35d9d1eab550f914bb46f621162cf1c9 --- requirements.txt | 2 +- test-requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index caeb18b91..5489dedee 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,5 +9,5 @@ oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0 oslo.utils>=3.31.0 # Apache-2.0 PrettyTable<0.8,>=0.7.1 # BSD simplejson>=3.5.1 # MIT -six>=1.9.0 # MIT +six>=1.10.0 # MIT Babel!=2.4.0,>=2.3.4 # BSD diff --git a/test-requirements.txt b/test-requirements.txt index da9857a59..389045fe9 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -19,7 +19,7 @@ openstackdocstheme>=1.17.0 # Apache-2.0 osprofiler>=1.4.0 # Apache-2.0 testrepository>=0.0.18 # Apache-2.0/BSD testscenarios>=0.4 # Apache-2.0/BSD -testtools>=1.4.0 # MIT +testtools>=2.2.0 # MIT tempest>=17.1.0 # Apache-2.0 # releasenotes From e24421b79307cd331eb7ea5ba34d63e3c2f799f6 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 13 Jun 2017 08:08:12 -0500 Subject: [PATCH 1369/1705] Stop posting to os-volumes_boot os-volumes_boot is an old clone of /servers and is not documented in the API docs. POST to /servers. Change-Id: If0161a89877f19f24e91d780cf227fdc48e7e860 --- novaclient/tests/unit/fixture_data/servers.py | 9 --------- novaclient/tests/unit/v2/test_servers.py | 6 +++--- novaclient/tests/unit/v2/test_shell.py | 20 +++++++++---------- novaclient/v2/servers.py | 12 ++++------- 4 files changed, 17 insertions(+), 30 deletions(-) diff --git a/novaclient/tests/unit/fixture_data/servers.py b/novaclient/tests/unit/fixture_data/servers.py index 9822fa5b0..f019a2277 100644 --- a/novaclient/tests/unit/fixture_data/servers.py +++ b/novaclient/tests/unit/fixture_data/servers.py @@ -293,15 +293,6 @@ def post_os_volumes_boot(request, context): return {'server': self.server_9012} - # NOTE(jamielennox): hack to make os_volumes mock go to the right place - base_url = self.base_url - self.base_url = None - self.requests_mock.post(self.url('os-volumes_boot'), - json=post_os_volumes_boot, - status_code=202, - headers=self.json_headers) - self.base_url = base_url - # # Server password # diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index 863368574..0766f1cd6 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -188,7 +188,7 @@ def test_create_server_from_volume(): nics=nics ) self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('POST', '/os-volumes_boot') + self.assert_called('POST', '/servers') self.assertIsInstance(s, servers.Server) test_create_server_from_volume() @@ -217,7 +217,7 @@ def wrapped_boot(url, key, *boot_args, **boot_kwargs): nics=self._get_server_create_default_nics() ) self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('POST', '/os-volumes_boot') + self.assert_called('POST', '/servers') self.assertIsInstance(s, servers.Server) def test_create_server_boot_with_nics_ipv6(self): @@ -1384,7 +1384,7 @@ def test_create_server_boot_from_volume_tagged_bdm_v2(self): key_name="fakekey", block_device_mapping_v2=bdm) self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('POST', '/os-volumes_boot') + self.assert_called('POST', '/servers') def test_create_server_boot_from_volume_tagged_bdm_v2_pre232(self): self.cs.api_version = api_versions.APIVersion("2.31") diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 7b33d90fd..a4580b088 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -304,7 +304,7 @@ def test_boot_no_image_bdms(self): 'boot --flavor 1 --block-device-mapping vda=blah:::0 some-server' ) self.assert_called_anytime( - 'POST', '/os-volumes_boot', + 'POST', '/servers', {'server': { 'flavorRef': '1', 'name': 'some-server', @@ -328,7 +328,7 @@ def test_boot_image_bdms_v2(self): 'type=disk,shutdown=preserve some-server' % FAKE_UUID_1 ) self.assert_called_anytime( - 'POST', '/os-volumes_boot', + 'POST', '/servers', {'server': { 'flavorRef': '1', 'name': 'some-server', @@ -371,7 +371,7 @@ def test_boot_image_bdms_v2_no_source_type_no_destination_type(self): 'type=disk,shutdown=preserve some-server' % FAKE_UUID_1 ) self.assert_called_anytime( - 'POST', '/os-volumes_boot', + 'POST', '/servers', {'server': { 'flavorRef': '1', 'name': 'some-server', @@ -407,7 +407,7 @@ def test_boot_image_bdms_v2_no_destination_type(self): 'type=disk,shutdown=preserve some-server' % FAKE_UUID_1 ) self.assert_called_anytime( - 'POST', '/os-volumes_boot', + 'POST', '/servers', {'server': { 'flavorRef': '1', 'name': 'some-server', @@ -451,7 +451,7 @@ def test_boot_image_bdms_v2_with_tag(self): api_version='2.32' ) self.assert_called_anytime( - 'POST', '/os-volumes_boot', + 'POST', '/servers', {'server': { 'flavorRef': '1', 'name': 'some-server', @@ -488,7 +488,7 @@ def test_boot_no_image_bdms_v2(self): 'type=disk,shutdown=preserve some-server' ) self.assert_called_anytime( - 'POST', '/os-volumes_boot', + 'POST', '/servers', {'server': { 'flavorRef': '1', 'name': 'some-server', @@ -515,7 +515,7 @@ def test_boot_no_image_bdms_v2(self): cmd = 'boot --flavor 1 --boot-volume fake-id some-server' self.run_command(cmd) self.assert_called_anytime( - 'POST', '/os-volumes_boot', + 'POST', '/servers', {'server': { 'flavorRef': '1', 'name': 'some-server', @@ -537,7 +537,7 @@ def test_boot_no_image_bdms_v2(self): cmd = 'boot --flavor 1 --snapshot fake-id some-server' self.run_command(cmd) self.assert_called_anytime( - 'POST', '/os-volumes_boot', + 'POST', '/servers', {'server': { 'flavorRef': '1', 'name': 'some-server', @@ -558,7 +558,7 @@ def test_boot_no_image_bdms_v2(self): self.run_command('boot --flavor 1 --swap 1 some-server') self.assert_called_anytime( - 'POST', '/os-volumes_boot', + 'POST', '/servers', {'server': { 'flavorRef': '1', 'name': 'some-server', @@ -582,7 +582,7 @@ def test_boot_no_image_bdms_v2(self): 'boot --flavor 1 --ephemeral size=1,format=ext4 some-server' ) self.assert_called_anytime( - 'POST', '/os-volumes_boot', + 'POST', '/servers', {'server': { 'flavorRef': '1', 'name': 'some-server', diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index 4c38ed700..2801e2a36 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -655,7 +655,7 @@ def __str__(self): class ServerManager(base.BootingManagerWithFind): resource_class = Server - def _boot(self, resource_url, response_key, name, image, flavor, + def _boot(self, response_key, name, image, flavor, meta=None, files=None, userdata=None, reservation_id=False, return_raw=False, min_count=None, max_count=None, security_groups=None, key_name=None, @@ -799,7 +799,7 @@ def _boot(self, resource_url, response_key, name, image, flavor, if tags: body['server']['tags'] = tags - return self._create(resource_url, body, response_key, + return self._create('/servers', body, response_key, return_raw=return_raw, **kwargs) def get(self, server): @@ -1391,19 +1391,15 @@ def create(self, name, image, flavor, meta=None, files=None, access_ip_v4=access_ip_v4, access_ip_v6=access_ip_v6, **kwargs) if block_device_mapping: - resource_url = "/os-volumes_boot" boot_kwargs['block_device_mapping'] = block_device_mapping elif block_device_mapping_v2: - resource_url = "/os-volumes_boot" boot_kwargs['block_device_mapping_v2'] = block_device_mapping_v2 - else: - resource_url = "/servers" + if nics: boot_kwargs['nics'] = nics response_key = "server" if not reservation_id else "reservation_id" - return self._boot(resource_url, response_key, *boot_args, - **boot_kwargs) + return self._boot(response_key, *boot_args, **boot_kwargs) @api_versions.wraps("2.0", "2.18") def update(self, server, name=None): From 8bfcd2b0e6ff90c7a0e80af4ee14e60a71401cc3 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Thu, 16 Nov 2017 12:45:51 -0500 Subject: [PATCH 1370/1705] Remove SecretsHelper This code hasn't worked for over two years when 9ed9ab68f7d2fb98c505afc3a1079a72f15c959c was added which does API version discovery, which requires auth, which happens before the SecretsHelper is used. That was back in the 2.27.0 release in the Liberty series. Given how long this has been broken without anyone noticing, and it's undocumented and untested (basically), and python-openstackclient properly handles prompting for a password, we should just remove this code rather than try to fix it. Change-Id: I62188e73a48f6878ce920a3b4724dba101564aef Closes-Bug: #1732744 --- novaclient/shell.py | 158 ---------------------------- novaclient/tests/unit/test_shell.py | 33 ------ 2 files changed, 191 deletions(-) diff --git a/novaclient/shell.py b/novaclient/shell.py index 4bf46aed6..4ecae61e1 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -20,7 +20,6 @@ from __future__ import print_function import argparse -import getpass import logging import sys @@ -32,14 +31,6 @@ osprofiler_profiler = importutils.try_import("osprofiler.profiler") -HAS_KEYRING = False -all_errors = ValueError -try: - import keyring - HAS_KEYRING = True -except ImportError: - pass - import novaclient from novaclient import api_versions from novaclient import client @@ -210,133 +201,6 @@ def __call__(self, parser, namespace, values, option_string): action(parser, namespace, values, option_string) -class SecretsHelper(object): - def __init__(self, args, client): - self.args = args - self.client = client - self.key = None - self._password = None - - def _validate_string(self, text): - if text is None or len(text) == 0: - return False - return True - - def _make_key(self): - if self.key is not None: - return self.key - keys = [ - self.client.auth_url, - self.client.projectid, - self.client.user, - self.client.region_name, - self.client.endpoint_type, - self.client.service_type, - self.client.service_name, - ] - for (index, key) in enumerate(keys): - if key is None: - keys[index] = '?' - else: - keys[index] = str(keys[index]) - self.key = "/".join(keys) - return self.key - - def _prompt_password(self, verify=True): - pw = None - if hasattr(sys.stdin, 'isatty') and sys.stdin.isatty(): - # Check for Ctl-D - try: - while True: - pw1 = getpass.getpass('OS Password: ') - if verify: - pw2 = getpass.getpass('Please verify: ') - else: - pw2 = pw1 - if pw1 == pw2 and self._validate_string(pw1): - pw = pw1 - break - except EOFError: - pass - return pw - - def save(self, auth_token, management_url, tenant_id): - if not HAS_KEYRING or not self.args.os_cache: - return - if (auth_token == self.auth_token and - management_url == self.management_url): - # Nothing changed.... - return - if not all([management_url, auth_token, tenant_id]): - raise ValueError(_("Unable to save empty management url/auth " - "token")) - value = "|".join([str(auth_token), - str(management_url), - str(tenant_id)]) - keyring.set_password("novaclient_auth", self._make_key(), value) - - @property - def password(self): - # Cache password so we prompt user at most once - if self._password: - pass - elif self._validate_string(self.args.os_password): - self._password = self.args.os_password - else: - verify_pass = strutils.bool_from_string( - utils.env("OS_VERIFY_PASSWORD", default=False), True) - self._password = self._prompt_password(verify_pass) - if not self._password: - raise exc.CommandError( - 'Expecting a password provided via either ' - '--os-password, env[OS_PASSWORD], or ' - 'prompted response') - return self._password - - @property - def management_url(self): - if not HAS_KEYRING or not self.args.os_cache: - return None - management_url = None - try: - block = keyring.get_password('novaclient_auth', self._make_key()) - if block: - _token, management_url, _tenant_id = block.split('|', 2) - except all_errors: - pass - return management_url - - @property - def auth_token(self): - # Now is where it gets complicated since we - # want to look into the keyring module, if it - # exists and see if anything was provided in that - # file that we can use. - if not HAS_KEYRING or not self.args.os_cache: - return None - token = None - try: - block = keyring.get_password('novaclient_auth', self._make_key()) - if block: - token, _management_url, _tenant_id = block.split('|', 2) - except all_errors: - pass - return token - - @property - def tenant_id(self): - if not HAS_KEYRING or not self.args.os_cache: - return None - tenant_id = None - try: - block = keyring.get_password('novaclient_auth', self._make_key()) - if block: - _token, _management_url, tenant_id = block.split('|', 2) - except all_errors: - pass - return tenant_id - - class NovaClientArgumentParser(argparse.ArgumentParser): def __init__(self, *args, **kwargs): @@ -688,7 +552,6 @@ def main(self, argv): # We may have either, both or none of these. # If we have both, we don't need USERNAME, PASSWORD etc. - # Fill in the blanks from the SecretsHelper if possible. # Finally, authenticate unless we have both. # Note if we don't auth we probably don't have a tenant ID so we can't # cache the token. @@ -847,27 +710,6 @@ def main(self, argv): user_domain_id=os_user_domain_id, user_domain_name=os_user_domain_name) - # Now check for the password/token of which pieces of the - # identifying keyring key can come from the underlying client - if must_auth: - helper = SecretsHelper(args, self.cs.client) - self.cs.client.keyring_saver = helper - - tenant_id = helper.tenant_id - # Allow commandline to override cache - if not auth_token: - auth_token = helper.auth_token - endpoint_override = endpoint_override or helper.management_url - if tenant_id and auth_token and endpoint_override: - self.cs.client.tenant_id = tenant_id - self.cs.client.auth_token = auth_token - self.cs.client.management_url = endpoint_override - self.cs.client.password_func = lambda: helper.password - else: - # We're missing something, so auth with user/pass and save - # the result in our helper. - self.cs.client.password = helper.password - args.func(self.cs, args) if osprofiler_profiler and args.profile: diff --git a/novaclient/tests/unit/test_shell.py b/novaclient/tests/unit/test_shell.py index 10de208f3..8e589a470 100644 --- a/novaclient/tests/unit/test_shell.py +++ b/novaclient/tests/unit/test_shell.py @@ -576,20 +576,6 @@ def test_password(self, mock_getpass, mock_stdin, m_requests): stdout, stderr = self.shell('list') self.assertEqual((stdout + stderr), ex) - @mock.patch('sys.stdin', side_effect=mock.MagicMock) - @mock.patch('getpass.getpass', side_effect=EOFError) - def test_no_password(self, mock_getpass, mock_stdin): - required = ('Expecting a password provided' - ' via either --os-password, env[OS_PASSWORD],' - ' or prompted response',) - self.make_env(exclude='OS_PASSWORD') - try: - self.shell('list') - except exceptions.CommandError as message: - self.assertEqual(required, message.args) - else: - self.fail('CommandError not raised') - def _test_service_type(self, version, service_type, mock_client): if version is None: cmd = 'list' @@ -666,25 +652,6 @@ def test_osprofiler_not_installed(self, m_requests): self.assertIn('unrecognized arguments: --profile swordfish', stderr) - @mock.patch('novaclient.shell.SecretsHelper.tenant_id', - return_value=True) - @mock.patch('novaclient.shell.SecretsHelper.auth_token', - return_value=True) - @mock.patch('novaclient.shell.SecretsHelper.management_url', - return_value=True) - @requests_mock.Mocker() - def test_keyring_saver_helper(self, - sh_management_url_function, - sh_auth_token_function, - sh_tenant_id_function, - m_requests): - self.make_env(fake_env=FAKE_ENV) - self.register_keystone_discovery_fixture(m_requests) - self.shell('list') - mock_client_instance = self.mock_client.return_value - keyring_saver = mock_client_instance.client.keyring_saver - self.assertIsInstance(keyring_saver, novaclient.shell.SecretsHelper) - def test_microversion_with_default_behaviour(self): self.make_env(fake_env=FAKE_ENV5) self.mock_server_version_range.return_value = ( From a4a4f3b905b88438e6c5aa7e635fe80af7043334 Mon Sep 17 00:00:00 2001 From: Andreas Jaeger Date: Thu, 16 Nov 2017 20:46:18 +0100 Subject: [PATCH 1371/1705] Remove setting of version/release from releasenotes Release notes are version independent, so remove version/release values. We've found that projects now require the service package to be installed in order to build release notes, and this is entirely due to the current convention of pulling in the version information. Release notes should not need installation in order to build, so this unnecessary version setting needs to be removed. This is needed for new release notes publishing, see I56909152975f731a9d2c21b2825b972195e48ee8 and the discussion starting at http://lists.openstack.org/pipermail/openstack-dev/2017-November/124480.html . Change-Id: Ie7a8592f29a7d809ae9bcc2577cae563210b4de9 --- releasenotes/source/conf.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/releasenotes/source/conf.py b/releasenotes/source/conf.py index 7a743acac..07b6d77f6 100644 --- a/releasenotes/source/conf.py +++ b/releasenotes/source/conf.py @@ -46,16 +46,11 @@ project = u'Nova Client Release Notes' copyright = u'2015, Nova developers' -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -import pbr.version -nova_version = pbr.version.VersionInfo('python-novaclient') +# Release notes are version independent. # The short X.Y version. -version = nova_version.canonical_version_string() +version = '' # The full version, including alpha/beta/rc tags. -release = nova_version.version_string_with_vcs() +release = '' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. From bef67651042a3ce123222079a41461677f3f9df0 Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Tue, 14 Nov 2017 15:06:56 +0900 Subject: [PATCH 1372/1705] Microversion 2.54 - Enable reset keypair while rebuild Adds support for microversion 2.54 which adds resetting keypair and unsetting keypair in rebuild operation. Adds optional ``--key-name`` and ``--key-unset`` options in the ``nova rebuild`` command. The ``--key-name`` and ``--key-unset`` cannot be specified at the same time. Change-Id: Ie2a39bb29dd59c070adc94e79ea0f6473227a427 Implements: blueprint rebuild-keypair-reset --- novaclient/__init__.py | 2 +- novaclient/tests/unit/v2/test_servers.py | 31 +++++++++++++++++ novaclient/tests/unit/v2/test_shell.py | 33 +++++++++++++++++++ novaclient/v2/servers.py | 11 +++++++ novaclient/v2/shell.py | 24 ++++++++++++++ .../microversion-v2_54-6c7ccb61eff6cb6d.yaml | 7 ++++ 6 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/microversion-v2_54-6c7ccb61eff6cb6d.yaml diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 3fe7488d0..d84695cec 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.53") +API_MAX_VERSION = api_versions.APIVersion("2.54") diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index 0766f1cd6..396e3b763 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -1550,3 +1550,34 @@ def test_create_server_with_tags_pre_252_fails(self): key_name="fakekey", nics=self._get_server_create_default_nics(), tags=['tag1', 'tag2']) + + +class ServersV254Test(ServersV252Test): + + api_version = "2.54" + + def test_rebuild_with_key_name(self): + s = self.cs.servers.get(1234) + ret = s.rebuild(image="1", key_name="test_keypair") + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action', + {'rebuild': { + 'imageRef': '1', + 'key_name': 'test_keypair'}}) + + def test_rebuild_with_key_name_none(self): + s = self.cs.servers.get(1234) + ret = s.rebuild(image="1", key_name=None) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action', + {'rebuild': { + 'key_name': None, + 'imageRef': '1'}}) + + def test_rebuild_with_key_name_pre_254_fails(self): + self.cs.api_version = api_versions.APIVersion('2.53') + ex = self.assertRaises(exceptions.UnsupportedAttribute, + self.cs.servers.rebuild, + '1234', fakes.FAKE_IMAGE_UUID_1, + key_name='test_keypair') + self.assertIn('key_name', six.text_type(ex.message)) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index a4580b088..341f5e51f 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -1494,6 +1494,38 @@ def test_rebuild_name_meta(self): self.assert_called('GET', '/flavors/1', pos=4) self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=5) + def test_rebuild_reset_keypair(self): + self.run_command('rebuild sample-server %s --key-name test_keypair' % + FAKE_UUID_1, api_version='2.54') + self.assert_called('GET', '/servers?name=sample-server', pos=0) + self.assert_called('GET', '/servers/1234', pos=1) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=2) + self.assert_called('POST', '/servers/1234/action', + {'rebuild': {'imageRef': FAKE_UUID_1, + 'key_name': 'test_keypair', + 'description': None}}, pos=3) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=4) + + def test_rebuild_unset_keypair(self): + self.run_command('rebuild sample-server %s --key-unset' % + FAKE_UUID_1, api_version='2.54') + self.assert_called('GET', '/servers?name=sample-server', pos=0) + self.assert_called('GET', '/servers/1234', pos=1) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=2) + self.assert_called('POST', '/servers/1234/action', + {'rebuild': {'imageRef': FAKE_UUID_1, + 'key_name': None, + 'description': None}}, pos=3) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=4) + + def test_rebuild_unset_keypair_with_key_name(self): + ex = self.assertRaises( + exceptions.CommandError, self.run_command, + 'rebuild sample-server %s --key-unset --key-name test_keypair' % + FAKE_UUID_1, api_version='2.54') + self.assertIn("Cannot specify '--key-unset' with '--key-name'.", + six.text_type(ex)) + def test_rebuild_with_incorrect_metadata(self): cmd = 'rebuild sample-server %s --name asdf --meta foo' % FAKE_UUID_1 result = self.assertRaises(argparse.ArgumentTypeError, @@ -3126,6 +3158,7 @@ def test_versions(self): 48, # There are no version-wrapped shell method changes for this. 51, # There are no version-wrapped shell method changes for this. 52, # There are no version-wrapped shell method changes for this. + 54, # There are no version-wrapped shell method changes for this. ]) versions_supported = set(range(0, novaclient.API_MAX_VERSION.ver_minor + 1)) diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index 2801e2a36..ed8e2b55f 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -1496,12 +1496,21 @@ def rebuild(self, server, image, password=None, disk_config=None, and each file must be 10k or less. :param description: optional description of the server (allowed since microversion 2.19) + :param key_name: optional key pair name for rebuild operation; passing + None will unset the key for the server instance + (starting from microversion 2.54) :returns: :class:`Server` """ descr_microversion = api_versions.APIVersion("2.19") if "description" in kwargs and self.api_version < descr_microversion: raise exceptions.UnsupportedAttribute("description", "2.19") + # Starting from microversion 2.54, + # the optional 'key_name' parameter has been added. + if ('key_name' in kwargs and + self.api_version < api_versions.APIVersion('2.54')): + raise exceptions.UnsupportedAttribute('key_name', '2.54') + body = {'imageRef': base.getid(image)} if password is not None: body['adminPass'] = password @@ -1513,6 +1522,8 @@ def rebuild(self, server, image, password=None, disk_config=None, body['name'] = name if "description" in kwargs: body["description"] = kwargs["description"] + if 'key_name' in kwargs: + body['key_name'] = kwargs['key_name'] if meta: body['metadata'] = meta if files: diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index ea68e1a40..837526c41 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -1786,6 +1786,20 @@ def do_reboot(cs, args): default=[], help=_("Store arbitrary files from locally to " "on the new server. You may store up to 5 files.")) +@utils.arg( + '--key-name', + metavar='', + default=None, + help=_("Keypair name to set in the server. " + "Cannot be specified with the '--key-unset' option."), + start_version='2.54') +@utils.arg( + '--key-unset', + action='store_true', + default=False, + help=_("Unset keypair in the server. " + "Cannot be specified with the '--key-name' option."), + start_version='2.54') def do_rebuild(cs, args): """Shutdown, re-image, and re-boot a server.""" server = _find_server(cs, args.server) @@ -1819,6 +1833,16 @@ def do_rebuild(cs, args): "form '--file " "'") % f) kwargs['files'] = files + + if cs.api_version >= api_versions.APIVersion('2.54'): + if args.key_unset: + kwargs['key_name'] = None + if args.key_name: + raise exceptions.CommandError( + _("Cannot specify '--key-unset' with '--key-name'.")) + elif args.key_name: + kwargs['key_name'] = args.key_name + server = server.rebuild(image, _password, **kwargs) _print_server(cs, args, server) diff --git a/releasenotes/notes/microversion-v2_54-6c7ccb61eff6cb6d.yaml b/releasenotes/notes/microversion-v2_54-6c7ccb61eff6cb6d.yaml new file mode 100644 index 000000000..5321c486f --- /dev/null +++ b/releasenotes/notes/microversion-v2_54-6c7ccb61eff6cb6d.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Adds support for microversion 2.54 which adds resetting keypair and + unsetting keypair in rebuild operation. + Adds optional ``--key-name`` and ``--key-unset`` options + in the ``nova rebuild`` command. From a8e4521b673ed3935c096580ad1f14a1be9a4f39 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Fri, 17 Nov 2017 14:17:14 +0200 Subject: [PATCH 1373/1705] [functional] Remove duplication of boot helper The base test class for functionl tests has an unified helper method for booting VM and waiting for active status. This method can be easily extended with one new argumen `flavor` to cover the case required by `TestServersResize` test case. This patch ports `TestServersResize` to use generic helper method. Change-Id: I9a53066dbb8907ed87a70f207b1e41b5b8a66908 --- novaclient/tests/functional/base.py | 7 ++-- novaclient/tests/functional/v2/test_resize.py | 36 +++++++------------ 2 files changed, 17 insertions(+), 26 deletions(-) diff --git a/novaclient/tests/functional/base.py b/novaclient/tests/functional/base.py index 637ae52b9..6b95f40d4 100644 --- a/novaclient/tests/functional/base.py +++ b/novaclient/tests/functional/base.py @@ -468,14 +468,15 @@ def _get_list_of_values_from_single_column_table(self, table, column): values.append(line.split("|")[1].strip()) return values - def _create_server(self, name=None, with_network=True, add_cleanup=True, - **kwargs): + def _create_server(self, name=None, flavor=None, with_network=True, + add_cleanup=True, **kwargs): name = name or self.name_generate(prefix='server') if with_network: nics = [{"net-id": self.network.id}] else: nics = None - server = self.client.servers.create(name, self.image, self.flavor, + flavor = flavor or self.flavor + server = self.client.servers.create(name, self.image, flavor, nics=nics, **kwargs) if add_cleanup: self.addCleanup(server.delete) diff --git a/novaclient/tests/functional/v2/test_resize.py b/novaclient/tests/functional/v2/test_resize.py index 9277480ea..2c14c19f9 100644 --- a/novaclient/tests/functional/v2/test_resize.py +++ b/novaclient/tests/functional/v2/test_resize.py @@ -20,23 +20,6 @@ class TestServersResize(base.ClientTestBase): COMPUTE_API_VERSION = '2.1' - def _create_server(self, name, flavor): - """Boots a server with the given name and flavor and waits for it to - be ACTIVE. - """ - params = ( - "%(name)s --flavor %(flavor)s --image %(image)s --poll " % { - "name": self.name_generate(name), - "flavor": flavor, - "image": self.image.id}) - # check to see if we have to pass in a network id - if self.multiple_networks: - params += ' --nic net-id=%s' % self.network.id - server_info = self.nova("boot", params=params) - server_id = self._get_value_from_the_table(server_info, "id") - self.addCleanup(self._cleanup_server, server_id) - return server_id - def _pick_alternate_flavor(self): """Given the flavor picked in the base class setup, this finds the opposite flavor to use for a resize test. For example, if m1.nano is @@ -83,7 +66,8 @@ def test_resize_up_confirm(self): """Tests creating a server and resizes up and confirms the resize. Compares quota before, during and after the resize. """ - server_id = self._create_server('resize-up-confirm', self.flavor.name) + server_id = self._create_server('resize-up-confirm', + flavor=self.flavor.id).id # get the starting quota now that we've created a server starting_usage = self._get_absolute_limits() # now resize up @@ -107,17 +91,22 @@ def _create_resize_down_flavors(self): """Creates two flavors with different size ram but same size vcpus and disk. - :returns: tuple of (larger_flavor_name, smaller_flavor_name) + :returns: tuple of 2 IDs which represents larger_flavor for resize and + smaller flavor. """ - self.nova('flavor-create', params='resize-larger-flavor auto 128 0 1') + output = self.nova('flavor-create', + params='resize-larger-flavor auto 128 0 1') + larger_id = self._get_column_value_from_single_row_table(output, "ID") self.addCleanup( self.nova, 'flavor-delete', params='resize-larger-flavor') - self.nova('flavor-create', params='resize-smaller-flavor auto 64 0 1') + output = self.nova('flavor-create', + params='resize-smaller-flavor auto 64 0 1') + smaller_id = self._get_column_value_from_single_row_table(output, "ID") self.addCleanup( self.nova, 'flavor-delete', params='resize-smaller-flavor') - return 'resize-larger-flavor', 'resize-smaller-flavor' + return larger_id, smaller_id def test_resize_down_revert(self): """Tests creating a server and resizes down and reverts the resize. @@ -128,7 +117,8 @@ def test_resize_down_revert(self): # create our own flavors. larger_flavor, smaller_flavor = self._create_resize_down_flavors() # Now create the server with the larger flavor. - server_id = self._create_server('resize-down-revert', larger_flavor) + server_id = self._create_server('resize-down-revert', + flavor=larger_flavor).id # get the starting quota now that we've created a server starting_usage = self._get_absolute_limits() # now resize down From 14ee2bcc4ea8296fb906db731139cd948e9e1102 Mon Sep 17 00:00:00 2001 From: ghanshyam Date: Wed, 22 Nov 2017 03:28:21 +0000 Subject: [PATCH 1374/1705] Move zuulv3 jobs to project repo This patch moves the zuulv3 jobs for python novaclient. Needed-By: I1508933ef77669754adf8032fc3d835960f78cb7 Needed-By: I37b02be0aeffc3a0f0516616b5294444012b8dea Change-Id: I43a8435485751748ca6228f67d401945cb32652e --- .zuul.yaml | 33 ++++++++ .../post.yaml | 80 +++++++++++++++++++ .../run.yaml | 59 ++++++++++++++ .../post.yaml | 80 +++++++++++++++++++ .../run.yaml | 58 ++++++++++++++ 5 files changed, 310 insertions(+) create mode 100644 .zuul.yaml create mode 100644 playbooks/legacy/novaclient-dsvm-functional-identity-v3-only/post.yaml create mode 100644 playbooks/legacy/novaclient-dsvm-functional-identity-v3-only/run.yaml create mode 100644 playbooks/legacy/novaclient-dsvm-functional-neutron/post.yaml create mode 100644 playbooks/legacy/novaclient-dsvm-functional-neutron/run.yaml diff --git a/.zuul.yaml b/.zuul.yaml new file mode 100644 index 000000000..94da1e955 --- /dev/null +++ b/.zuul.yaml @@ -0,0 +1,33 @@ +- job: + name: novaclient-dsvm-functional-identity-v3-only + parent: legacy-dsvm-base + run: playbooks/legacy/novaclient-dsvm-functional-identity-v3-only/run.yaml + post-run: playbooks/legacy/novaclient-dsvm-functional-identity-v3-only/post.yaml + timeout: 7200 + voting: false + required-projects: + - openstack-infra/devstack-gate + - openstack/nova + - openstack/python-novaclient + +- job: + name: novaclient-dsvm-functional-neutron + parent: legacy-dsvm-base + run: playbooks/legacy/novaclient-dsvm-functional-neutron/run.yaml + post-run: playbooks/legacy/novaclient-dsvm-functional-neutron/post.yaml + timeout: 7200 + required-projects: + - openstack-infra/devstack-gate + - openstack/neutron + - openstack/nova + - openstack/python-novaclient + +- project: + name: openstack/python-novaclient + check: + jobs: + - novaclient-dsvm-functional-identity-v3-only + - novaclient-dsvm-functional-neutron + gate: + jobs: + - novaclient-dsvm-functional-neutron diff --git a/playbooks/legacy/novaclient-dsvm-functional-identity-v3-only/post.yaml b/playbooks/legacy/novaclient-dsvm-functional-identity-v3-only/post.yaml new file mode 100644 index 000000000..dac875340 --- /dev/null +++ b/playbooks/legacy/novaclient-dsvm-functional-identity-v3-only/post.yaml @@ -0,0 +1,80 @@ +- hosts: primary + tasks: + + - name: Copy files from {{ ansible_user_dir }}/workspace/ on node + synchronize: + src: '{{ ansible_user_dir }}/workspace/' + dest: '{{ zuul.executor.log_root }}' + mode: pull + copy_links: true + verify_host: true + rsync_opts: + - --include=**/*nose_results.html + - --include=*/ + - --exclude=* + - --prune-empty-dirs + + - name: Copy files from {{ ansible_user_dir }}/workspace/ on node + synchronize: + src: '{{ ansible_user_dir }}/workspace/' + dest: '{{ zuul.executor.log_root }}' + mode: pull + copy_links: true + verify_host: true + rsync_opts: + - --include=**/*testr_results.html.gz + - --include=*/ + - --exclude=* + - --prune-empty-dirs + + - name: Copy files from {{ ansible_user_dir }}/workspace/ on node + synchronize: + src: '{{ ansible_user_dir }}/workspace/' + dest: '{{ zuul.executor.log_root }}' + mode: pull + copy_links: true + verify_host: true + rsync_opts: + - --include=/.testrepository/tmp* + - --include=*/ + - --exclude=* + - --prune-empty-dirs + + - name: Copy files from {{ ansible_user_dir }}/workspace/ on node + synchronize: + src: '{{ ansible_user_dir }}/workspace/' + dest: '{{ zuul.executor.log_root }}' + mode: pull + copy_links: true + verify_host: true + rsync_opts: + - --include=**/*testrepository.subunit.gz + - --include=*/ + - --exclude=* + - --prune-empty-dirs + + - name: Copy files from {{ ansible_user_dir }}/workspace/ on node + synchronize: + src: '{{ ansible_user_dir }}/workspace/' + dest: '{{ zuul.executor.log_root }}/tox' + mode: pull + copy_links: true + verify_host: true + rsync_opts: + - --include=/.tox/*/log/* + - --include=*/ + - --exclude=* + - --prune-empty-dirs + + - name: Copy files from {{ ansible_user_dir }}/workspace/ on node + synchronize: + src: '{{ ansible_user_dir }}/workspace/' + dest: '{{ zuul.executor.log_root }}' + mode: pull + copy_links: true + verify_host: true + rsync_opts: + - --include=/logs/** + - --include=*/ + - --exclude=* + - --prune-empty-dirs diff --git a/playbooks/legacy/novaclient-dsvm-functional-identity-v3-only/run.yaml b/playbooks/legacy/novaclient-dsvm-functional-identity-v3-only/run.yaml new file mode 100644 index 000000000..ca45b7365 --- /dev/null +++ b/playbooks/legacy/novaclient-dsvm-functional-identity-v3-only/run.yaml @@ -0,0 +1,59 @@ +- hosts: all + name: Autoconverted job legacy-novaclient-dsvm-functional-identity-v3-only from + old job gate-novaclient-dsvm-functional-identity-v3-only-ubuntu-xenial-nv + tasks: + + - name: Ensure legacy workspace directory + file: + path: '{{ ansible_user_dir }}/workspace' + state: directory + + - shell: + cmd: | + set -e + set -x + cat > clonemap.yaml << EOF + clonemap: + - name: openstack-infra/devstack-gate + dest: devstack-gate + EOF + /usr/zuul-env/bin/zuul-cloner -m clonemap.yaml --cache-dir /opt/git \ + git://git.openstack.org \ + openstack-infra/devstack-gate + executable: /bin/bash + chdir: '{{ ansible_user_dir }}/workspace' + environment: '{{ zuul | zuul_legacy_vars }}' + + - shell: + cmd: | + set -e + set -x + export PYTHONUNBUFFERED=true + export BRANCH_OVERRIDE=default + export DEVSTACK_PROJECT_FROM_GIT=python-novaclient + if [ "$BRANCH_OVERRIDE" != "default" ] ; then + export OVERRIDE_ZUUL_BRANCH=$BRANCH_OVERRIDE + fi + # This ensures that if we set override branch to something + # else, we still take python-novaclient from the zuul branch + # name. So override branch can be 'stable/mitaka' but we can + # test master changes. + uc_project=`echo $DEVSTACK_PROJECT_FROM_GIT | tr [:lower:] [:upper:] | tr '-' '_' | sed 's/[^A-Z_]//'` + export "OVERRIDE_"$uc_project"_PROJECT_BRANCH"=$ZUUL_BRANCH + + function post_test_hook { + # Configure and run functional tests + $BASE/new/python-novaclient/novaclient/tests/functional/hooks/post_test_hook.sh + } + if [ "-identity-v3-only" == "-identity-v3-only" ] ; then + export DEVSTACK_LOCAL_CONFIG="ENABLE_IDENTITY_V2=False" + elif [ "-identity-v3-only" == "-neutron" ] ; then + export DEVSTACK_GATE_NEUTRON=1 + fi + export -f post_test_hook + + cp devstack-gate/devstack-vm-gate-wrap.sh ./safe-devstack-vm-gate-wrap.sh + ./safe-devstack-vm-gate-wrap.sh + executable: /bin/bash + chdir: '{{ ansible_user_dir }}/workspace' + environment: '{{ zuul | zuul_legacy_vars }}' diff --git a/playbooks/legacy/novaclient-dsvm-functional-neutron/post.yaml b/playbooks/legacy/novaclient-dsvm-functional-neutron/post.yaml new file mode 100644 index 000000000..dac875340 --- /dev/null +++ b/playbooks/legacy/novaclient-dsvm-functional-neutron/post.yaml @@ -0,0 +1,80 @@ +- hosts: primary + tasks: + + - name: Copy files from {{ ansible_user_dir }}/workspace/ on node + synchronize: + src: '{{ ansible_user_dir }}/workspace/' + dest: '{{ zuul.executor.log_root }}' + mode: pull + copy_links: true + verify_host: true + rsync_opts: + - --include=**/*nose_results.html + - --include=*/ + - --exclude=* + - --prune-empty-dirs + + - name: Copy files from {{ ansible_user_dir }}/workspace/ on node + synchronize: + src: '{{ ansible_user_dir }}/workspace/' + dest: '{{ zuul.executor.log_root }}' + mode: pull + copy_links: true + verify_host: true + rsync_opts: + - --include=**/*testr_results.html.gz + - --include=*/ + - --exclude=* + - --prune-empty-dirs + + - name: Copy files from {{ ansible_user_dir }}/workspace/ on node + synchronize: + src: '{{ ansible_user_dir }}/workspace/' + dest: '{{ zuul.executor.log_root }}' + mode: pull + copy_links: true + verify_host: true + rsync_opts: + - --include=/.testrepository/tmp* + - --include=*/ + - --exclude=* + - --prune-empty-dirs + + - name: Copy files from {{ ansible_user_dir }}/workspace/ on node + synchronize: + src: '{{ ansible_user_dir }}/workspace/' + dest: '{{ zuul.executor.log_root }}' + mode: pull + copy_links: true + verify_host: true + rsync_opts: + - --include=**/*testrepository.subunit.gz + - --include=*/ + - --exclude=* + - --prune-empty-dirs + + - name: Copy files from {{ ansible_user_dir }}/workspace/ on node + synchronize: + src: '{{ ansible_user_dir }}/workspace/' + dest: '{{ zuul.executor.log_root }}/tox' + mode: pull + copy_links: true + verify_host: true + rsync_opts: + - --include=/.tox/*/log/* + - --include=*/ + - --exclude=* + - --prune-empty-dirs + + - name: Copy files from {{ ansible_user_dir }}/workspace/ on node + synchronize: + src: '{{ ansible_user_dir }}/workspace/' + dest: '{{ zuul.executor.log_root }}' + mode: pull + copy_links: true + verify_host: true + rsync_opts: + - --include=/logs/** + - --include=*/ + - --exclude=* + - --prune-empty-dirs diff --git a/playbooks/legacy/novaclient-dsvm-functional-neutron/run.yaml b/playbooks/legacy/novaclient-dsvm-functional-neutron/run.yaml new file mode 100644 index 000000000..fb13df180 --- /dev/null +++ b/playbooks/legacy/novaclient-dsvm-functional-neutron/run.yaml @@ -0,0 +1,58 @@ +- hosts: all + name: Autoconverted job legacy-novaclient-dsvm-functional-neutron from old job gate-novaclient-dsvm-functional-neutron-ubuntu-xenial + tasks: + + - name: Ensure legacy workspace directory + file: + path: '{{ ansible_user_dir }}/workspace' + state: directory + + - shell: + cmd: | + set -e + set -x + cat > clonemap.yaml << EOF + clonemap: + - name: openstack-infra/devstack-gate + dest: devstack-gate + EOF + /usr/zuul-env/bin/zuul-cloner -m clonemap.yaml --cache-dir /opt/git \ + git://git.openstack.org \ + openstack-infra/devstack-gate + executable: /bin/bash + chdir: '{{ ansible_user_dir }}/workspace' + environment: '{{ zuul | zuul_legacy_vars }}' + + - shell: + cmd: | + set -e + set -x + export PYTHONUNBUFFERED=true + export BRANCH_OVERRIDE=default + export DEVSTACK_PROJECT_FROM_GIT=python-novaclient + if [ "$BRANCH_OVERRIDE" != "default" ] ; then + export OVERRIDE_ZUUL_BRANCH=$BRANCH_OVERRIDE + fi + # This ensures that if we set override branch to something + # else, we still take python-novaclient from the zuul branch + # name. So override branch can be 'stable/mitaka' but we can + # test master changes. + uc_project=`echo $DEVSTACK_PROJECT_FROM_GIT | tr [:lower:] [:upper:] | tr '-' '_' | sed 's/[^A-Z_]//'` + export "OVERRIDE_"$uc_project"_PROJECT_BRANCH"=$ZUUL_BRANCH + + function post_test_hook { + # Configure and run functional tests + $BASE/new/python-novaclient/novaclient/tests/functional/hooks/post_test_hook.sh + } + if [ "-neutron" == "-identity-v3-only" ] ; then + export DEVSTACK_LOCAL_CONFIG="ENABLE_IDENTITY_V2=False" + elif [ "-neutron" == "-neutron" ] ; then + export DEVSTACK_GATE_NEUTRON=1 + fi + export -f post_test_hook + + cp devstack-gate/devstack-vm-gate-wrap.sh ./safe-devstack-vm-gate-wrap.sh + ./safe-devstack-vm-gate-wrap.sh + executable: /bin/bash + chdir: '{{ ansible_user_dir }}/workspace' + environment: '{{ zuul | zuul_legacy_vars }}' From 50460bddfc943ad3bd4ad3c23c0cf05d013b85b3 Mon Sep 17 00:00:00 2001 From: Yikun Jiang Date: Fri, 27 Oct 2017 15:28:15 +0800 Subject: [PATCH 1375/1705] Use utils.prepare_query_string instead of duplicated code There are some duplicated code in nova client for generating query string. The 'prepare_query_string' method can convert dict params to query string(it transforms the dict to a sequence of two-element tuples in fixed order, then the encoded string will be consistent in Python 2&3.) This patch use utils.prepare_query_string instead of these and plus some notes in the 'prepare_query_string' method. Change-Id: Idb3c5e97f8bbcd5ec5446f776c10fa8c84b54d5d Closes-Bug: 1727968 --- novaclient/base.py | 9 +++++++-- novaclient/tests/unit/test_utils.py | 24 ++++++++++++++++++------ novaclient/utils.py | 10 ++++++++++ novaclient/v2/flavors.py | 6 +----- novaclient/v2/keypairs.py | 14 +++++++------- novaclient/v2/limits.py | 6 +----- novaclient/v2/migrations.py | 10 +--------- novaclient/v2/server_groups.py | 8 ++------ 8 files changed, 47 insertions(+), 40 deletions(-) diff --git a/novaclient/base.py b/novaclient/base.py index 6bcb527c4..328b25126 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -247,7 +247,10 @@ def client(self): def api_version(self): return self.api.api_version - def _list(self, url, response_key, obj_class=None, body=None): + def _list(self, url, response_key, obj_class=None, body=None, + filters=None): + if filters: + url = utils.get_url_with_filter(url, filters) if body: resp, body = self.api.client.post(url, body=body) else: @@ -347,7 +350,9 @@ def write_to_completion_cache(self, cache_type, val): if cache: cache.write("%s\n" % val) - def _get(self, url, response_key): + def _get(self, url, response_key, filters=None): + if filters: + url = utils.get_url_with_filter(url, filters) resp, body = self.api.client.get(url) if response_key is not None: content = body[response_key] diff --git a/novaclient/tests/unit/test_utils.py b/novaclient/tests/unit/test_utils.py index 919d4cb2b..372e6f007 100644 --- a/novaclient/tests/unit/test_utils.py +++ b/novaclient/tests/unit/test_utils.py @@ -443,19 +443,31 @@ def test_record_time(self): class PrepareQueryStringTestCase(test_utils.TestCase): - def test_convert_dict_to_string(self): - ustr = b'?\xd0\xbf=1&\xd1\x80=2' + + def setUp(self): + super(PrepareQueryStringTestCase, self).setUp() + self.ustr = b'?\xd0\xbf=1&\xd1\x80=2' if six.PY3: # in py3 real unicode symbols will be urlencoded - ustr = ustr.decode('utf8') - cases = ( + self.ustr = self.ustr.decode('utf8') + self.cases = ( ({}, ''), + (None, ''), ({'2': 2, '10': 1}, '?10=1&2=2'), ({'abc': 1, 'abc1': 2}, '?abc=1&abc1=2'), - ({b'\xd0\xbf': 1, b'\xd1\x80': 2}, ustr), + ({b'\xd0\xbf': 1, b'\xd1\x80': 2}, self.ustr), ({(1, 2): '1', (3, 4): '2'}, '?(1, 2)=1&(3, 4)=2') ) - for case in cases: + + def test_convert_dict_to_string(self): + for case in self.cases: self.assertEqual( case[1], parse.unquote_plus(utils.prepare_query_string(case[0]))) + + def test_get_url_with_filter(self): + url = '/fake' + for case in self.cases: + self.assertEqual( + '%s%s' % (url, case[1]), + parse.unquote_plus(utils.get_url_with_filter(url, case[0]))) diff --git a/novaclient/utils.py b/novaclient/utils.py index 451b2372d..df4464e6c 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -454,5 +454,15 @@ def record_time(times, enabled, *args): def prepare_query_string(params): """Convert dict params to query string""" + # Transform the dict to a sequence of two-element tuples in fixed + # order, then the encoded string will be consistent in Python 2&3. + if not params: + return '' params = sorted(params.items(), key=lambda x: x[0]) return '?%s' % parse.urlencode(params) if params else '' + + +def get_url_with_filter(url, filters): + query_string = prepare_query_string(filters) + url = "%s%s" % (url, query_string) + return url diff --git a/novaclient/v2/flavors.py b/novaclient/v2/flavors.py index a0a7b6f95..4f17eafbf 100644 --- a/novaclient/v2/flavors.py +++ b/novaclient/v2/flavors.py @@ -17,7 +17,6 @@ """ from oslo_utils import strutils -from six.moves.urllib import parse from novaclient import base from novaclient import exceptions @@ -128,14 +127,11 @@ def list(self, detailed=True, is_public=True, marker=None, min_disk=None, qparams['sort_dir'] = str(sort_dir) if not is_public: qparams['is_public'] = is_public - qparams = sorted(qparams.items(), key=lambda x: x[0]) - query_string = "?%s" % parse.urlencode(qparams) if qparams else "" - detail = "" if detailed: detail = "/detail" - return self._list("/flavors%s%s" % (detail, query_string), "flavors") + return self._list("/flavors%s" % detail, "flavors", filters=qparams) def get(self, flavor): """Get a specific flavor. diff --git a/novaclient/v2/keypairs.py b/novaclient/v2/keypairs.py index 021cc6273..08c5ea39e 100644 --- a/novaclient/v2/keypairs.py +++ b/novaclient/v2/keypairs.py @@ -19,7 +19,6 @@ from novaclient import api_versions from novaclient import base -from novaclient import utils class Keypair(base.Resource): @@ -170,9 +169,11 @@ def list(self, user_id=None): :param user_id: Id of key-pairs owner (Admin only). """ - query_string = "?user_id=%s" % user_id if user_id else "" - url = '/%s%s' % (self.keypair_prefix, query_string) - return self._list(url, 'keypairs') + params = {} + if user_id: + params['user_id'] = user_id + return self._list('/%s' % self.keypair_prefix, 'keypairs', + filters=params) @api_versions.wraps("2.35") def list(self, user_id=None, marker=None, limit=None): @@ -192,6 +193,5 @@ def list(self, user_id=None, marker=None, limit=None): params['limit'] = int(limit) if marker: params['marker'] = str(marker) - query_string = utils.prepare_query_string(params) - url = '/%s%s' % (self.keypair_prefix, query_string) - return self._list(url, 'keypairs') + return self._list('/%s' % self.keypair_prefix, 'keypairs', + filters=params) diff --git a/novaclient/v2/limits.py b/novaclient/v2/limits.py index 46d77def8..d90d3e9a4 100644 --- a/novaclient/v2/limits.py +++ b/novaclient/v2/limits.py @@ -12,8 +12,6 @@ # License for the specific language governing permissions and limitations # under the License. -from six.moves.urllib import parse - from novaclient import base @@ -95,6 +93,4 @@ def get(self, reserved=False, tenant_id=None): opts['reserved'] = 1 if tenant_id: opts['tenant_id'] = tenant_id - query_string = "?%s" % parse.urlencode(opts) if opts else "" - - return self._get("/limits%s" % query_string, "limits") + return self._get("/limits", "limits", filters=opts) diff --git a/novaclient/v2/migrations.py b/novaclient/v2/migrations.py index 72e0deda9..d1562b711 100644 --- a/novaclient/v2/migrations.py +++ b/novaclient/v2/migrations.py @@ -14,8 +14,6 @@ migration interface """ -from six.moves.urllib import parse - from novaclient import base from novaclient.i18n import _ @@ -50,10 +48,4 @@ def list(self, host=None, status=None, cell_name=None, instance_uuid=None): if instance_uuid: opts['instance_uuid'] = instance_uuid - # Transform the dict to a sequence of two-element tuples in fixed - # order, then the encoded string will be consistent in Python 2&3. - new_opts = sorted(opts.items(), key=lambda x: x[0]) - - query_string = "?%s" % parse.urlencode(new_opts) if new_opts else "" - - return self._list("/os-migrations%s" % query_string, "migrations") + return self._list("/os-migrations", "migrations", filters=opts) diff --git a/novaclient/v2/server_groups.py b/novaclient/v2/server_groups.py index 7a01ba0db..62a5c9a09 100644 --- a/novaclient/v2/server_groups.py +++ b/novaclient/v2/server_groups.py @@ -17,8 +17,6 @@ Server group interface. """ -from six.moves.urllib import parse - from novaclient import base @@ -62,10 +60,8 @@ def list(self, all_projects=False, limit=None, offset=None): qparams['limit'] = int(limit) if offset: qparams['offset'] = int(offset) - qparams = sorted(qparams.items(), key=lambda x: x[0]) - query_string = "?%s" % parse.urlencode(qparams) if qparams else "" - return self._list('/os-server-groups%s' % query_string, - 'server_groups') + return self._list('/os-server-groups', 'server_groups', + filters=qparams) def get(self, id): """Get a specific server group. From 4f78a4217c7c47e0e9912c1c46160bd6e43a5135 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Thu, 16 Nov 2017 16:00:06 -0500 Subject: [PATCH 1376/1705] Add support for microversion 2.55 - flavor description This adds the support for microversion 2.55 which allows creating a flavor with a description, showing the description in flavor details, and updating the description on an existing flavor. Related python API bindings are added, and the new "nova flavor-update " CLI is added. Implements blueprint flavor-description Change-Id: I0a09c0a63d2e91ef5aa31a8e43e28f8745faae14 --- novaclient/__init__.py | 2 +- novaclient/tests/unit/v2/fakes.py | 34 +++++++++- novaclient/tests/unit/v2/test_flavors.py | 65 ++++++++++++++++++- novaclient/tests/unit/v2/test_shell.py | 53 ++++++++++++++- novaclient/v2/flavors.py | 41 +++++++++++- novaclient/v2/shell.py | 40 ++++++++++-- ...5-flavor-description-a93718b31f1f0f39.yaml | 8 +++ 7 files changed, 232 insertions(+), 11 deletions(-) create mode 100644 releasenotes/notes/microversion-v2_55-flavor-description-a93718b31f1f0f39.yaml diff --git a/novaclient/__init__.py b/novaclient/__init__.py index d84695cec..7fb218c76 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.54") +API_MAX_VERSION = api_versions.APIVersion("2.55") diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 4138e8a98..b59f55496 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -14,6 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import copy import datetime import re @@ -832,9 +833,12 @@ def put_os_cloudpipe_configure_project(self, **kw): def get_flavors(self, **kw): status, header, flavors = self.get_flavors_detail(**kw) + included_fields = ['id', 'name'] + if self.api_version >= api_versions.APIVersion('2.55'): + included_fields.append('description') for flavor in flavors['flavors']: for k in list(flavor): - if k not in ['id', 'name']: + if k not in included_fields: del flavor[k] return (200, FAKE_RESPONSE_HEADERS, flavors) @@ -880,6 +884,18 @@ def get_flavors_detail(self, **kw): if not v['os-flavor-access:is_public'] ] + # Add description in the response for all flavors. + if self.api_version >= api_versions.APIVersion('2.55'): + for flavor in flavors['flavors']: + flavor['description'] = None + # Add a new flavor that is a copy of the first but with a different + # name, flavorid and a description set. + new_flavor = copy.deepcopy(flavors['flavors'][0]) + new_flavor['id'] = 'with-description' + new_flavor['name'] = 'with-description' + new_flavor['description'] = 'test description' + flavors['flavors'].append(new_flavor) + return (200, FAKE_RESPONSE_HEADERS, flavors) def get_flavors_1(self, **kw): @@ -937,6 +953,14 @@ def get_flavors_4(self, **kw): self.get_flavors_detail(is_public='None')[2]['flavors'][2]} ) + def get_flavors_with_description(self, **kw): + return ( + 200, + {}, + {'flavor': + self.get_flavors_detail(is_public='None')[2]['flavors'][-1]} + ) + def delete_flavors_flavordelete(self, **kw): return (202, FAKE_RESPONSE_HEADERS, None) @@ -951,6 +975,14 @@ def post_flavors(self, body, **kw): self.get_flavors_detail(is_public='None')[2]['flavors'][0]} ) + def put_flavors_with_description(self, body, **kw): + assert 'flavor' in body + assert 'description' in body['flavor'] + flavor = self.get_flavors_with_description(**kw)[2] + # Fake out the actual update of the flavor description for the response + flavor['description'] = body['flavor']['description'] + return (200, {}, {'flavor': flavor}) + def get_flavors_1_os_extra_specs(self, **kw): return ( 200, diff --git a/novaclient/tests/unit/v2/test_flavors.py b/novaclient/tests/unit/v2/test_flavors.py index c882ddb38..f05e7cddb 100644 --- a/novaclient/tests/unit/v2/test_flavors.py +++ b/novaclient/tests/unit/v2/test_flavors.py @@ -131,7 +131,8 @@ def test_find(self): self.assertRaises(exceptions.NotFound, self.cs.flavors.find, disk=12345) - def _create_body(self, name, ram, vcpus, disk, ephemeral, id, swap, + @staticmethod + def _create_body(name, ram, vcpus, disk, ephemeral, id, swap, rxtx_factor, is_public): return { "flavor": { @@ -258,3 +259,65 @@ def test_unset_keys(self, mock_delete): mock.call("/flavors/1/os-extra_specs/k1"), mock.call("/flavors/1/os-extra_specs/k2") ]) + + +class FlavorsTest_v2_55(utils.TestCase): + """Tests creating/showing/updating a flavor with a description.""" + def setUp(self): + super(FlavorsTest_v2_55, self).setUp() + self.cs = fakes.FakeClient(api_versions.APIVersion('2.55')) + + def test_list_flavors(self): + fl = self.cs.flavors.list() + self.cs.assert_called('GET', '/flavors/detail') + for flavor in fl: + self.assertTrue(hasattr(flavor, 'description'), + "%s does not have a description set." % flavor) + + def test_list_flavors_undetailed(self): + fl = self.cs.flavors.list(detailed=False) + self.cs.assert_called('GET', '/flavors') + for flavor in fl: + self.assertTrue(hasattr(flavor, 'description'), + "%s does not have a description set." % flavor) + + def test_get_flavor_details(self): + f = self.cs.flavors.get('with-description') + self.cs.assert_called('GET', '/flavors/with-description') + self.assertEqual('test description', f.description) + + def test_create(self): + self.cs.flavors.create( + 'with-description', 512, 1, 10, 'with-description', ephemeral=10, + is_public=False, description='test description') + + body = FlavorsTest._create_body( + "with-description", 512, 1, 10, 10, 'with-description', + 0, 1.0, False) + body['flavor']['description'] = 'test description' + self.cs.assert_called('POST', '/flavors', body) + + def test_create_bad_version(self): + """Tests trying to create a flavor with a description before 2.55.""" + self.cs.api_version = api_versions.APIVersion('2.54') + self.assertRaises(exceptions.UnsupportedAttribute, + self.cs.flavors.create, + 'with-description', 512, 1, 10, 'with-description', + description='test description') + + def test_update(self): + updated_flavor = self.cs.flavors.update( + 'with-description', 'new description') + body = { + 'flavor': { + 'description': 'new description' + } + } + self.cs.assert_called('PUT', '/flavors/with-description', body) + self.assertEqual('new description', updated_flavor.description) + + def test_update_bad_version(self): + """Tests trying to update a flavor with a description before 2.55.""" + self.cs.api_version = api_versions.APIVersion('2.54') + self.assertRaises(exceptions.VersionNotFoundForAPIMethod, + self.cs.flavors.update, 'foo', 'bar') diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 341f5e51f..7198715ca 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -1131,8 +1131,15 @@ def test_boot_with_tags_pre_v2_52(self): cmd, api_version='2.51') def test_flavor_list(self): - self.run_command('flavor-list') + out, _ = self.run_command('flavor-list') self.assert_called_anytime('GET', '/flavors/detail') + self.assertNotIn('Description', out) + + def test_flavor_list_with_description(self): + """Tests that the description column is added for version >= 2.55.""" + out, _ = self.run_command('flavor-list', api_version='2.55') + self.assert_called_anytime('GET', '/flavors/detail') + self.assertIn('Description', out) def test_flavor_list_with_extra_specs(self): self.run_command('flavor-list --extra-specs') @@ -1160,8 +1167,15 @@ def test_flavor_list_with_sort_key_dir(self): self.assert_called('GET', '/flavors/detail?sort_dir=asc&sort_key=id') def test_flavor_show(self): - self.run_command('flavor-show 1') + out, _ = self.run_command('flavor-show 1') + self.assert_called_anytime('GET', '/flavors/1') + self.assertNotIn('description', out) + + def test_flavor_show_with_description(self): + """Tests that the description is shown in version >= 2.55.""" + out, _ = self.run_command('flavor-show 1', api_version='2.55') self.assert_called_anytime('GET', '/flavors/1') + self.assertIn('description', out) def test_flavor_show_with_alphanum_id(self): self.run_command('flavor-show aa1') @@ -1910,6 +1924,41 @@ def test_flavor_create(self): self.assert_called('POST', '/flavors', pos=-2) self.assert_called('GET', '/flavors/1', pos=-1) + def test_flavor_create_with_description(self): + """Tests creating a flavor with a description.""" + self.run_command("flavor-create description " + "1234 512 10 1 --description foo", api_version='2.55') + expected_post_body = { + "flavor": { + "name": "description", + "ram": 512, + "vcpus": 1, + "disk": 10, + "id": "1234", + "swap": 0, + "OS-FLV-EXT-DATA:ephemeral": 0, + "rxtx_factor": 1.0, + "os-flavor-access:is_public": True, + "description": "foo" + } + } + self.assert_called('POST', '/flavors', expected_post_body, pos=-2) + + def test_flavor_update(self): + """Tests creating a flavor with a description.""" + out, _ = self.run_command( + "flavor-update with-description new-description", + api_version='2.55') + expected_put_body = { + "flavor": { + "description": "new-description" + } + } + self.assert_called('GET', '/flavors/with-description', pos=-2) + self.assert_called('PUT', '/flavors/with-description', + expected_put_body, pos=-1) + self.assertIn('new-description', out) + def test_aggregate_list(self): out, err = self.run_command('aggregate-list') self.assert_called('GET', '/os-aggregates') diff --git a/novaclient/v2/flavors.py b/novaclient/v2/flavors.py index a0a7b6f95..30fa2a8e6 100644 --- a/novaclient/v2/flavors.py +++ b/novaclient/v2/flavors.py @@ -19,6 +19,7 @@ from oslo_utils import strutils from six.moves.urllib import parse +from novaclient import api_versions from novaclient import base from novaclient import exceptions from novaclient.i18n import _ @@ -86,6 +87,16 @@ def delete(self): """ return self.manager.delete(self) + @api_versions.wraps('2.55') + def update(self, description=None): + """ + Update the description for this flavor. + + :param description: The description to set on the flavor. + :returns: :class:`Flavor` + """ + return self.manager.update(self, description=description) + class FlavorManager(base.ManagerWithFind): """Manage :class:`Flavor` resources.""" @@ -170,7 +181,8 @@ def _build_body(self, name, ram, vcpus, disk, id, swap, } def create(self, name, ram, vcpus, disk, flavorid="auto", - ephemeral=0, swap=0, rxtx_factor=1.0, is_public=True): + ephemeral=0, swap=0, rxtx_factor=1.0, is_public=True, + description=None): """Create a flavor. :param name: Descriptive name of the flavor @@ -180,8 +192,14 @@ def create(self, name, ram, vcpus, disk, flavorid="auto", :param flavorid: ID for the flavor (optional). You can use the reserved value ``"auto"`` to have Nova generate a UUID for the flavor in cases where you cannot simply pass ``None``. + :param ephemeral: Ephemeral disk space in GB. :param swap: Swap space in MB :param rxtx_factor: RX/TX factor + :param is_public: Whether or not the flavor is public. + :param description: A free form description of the flavor. + Limited to 65535 characters in length. + Only printable characters are allowed. + (Available starting with microversion 2.55) :returns: :class:`Flavor` """ @@ -219,7 +237,28 @@ def create(self, name, ram, vcpus, disk, flavorid="auto", except Exception: raise exceptions.CommandError(_("is_public must be a boolean.")) + supports_description = api_versions.APIVersion('2.55') + if description and self.api_version < supports_description: + raise exceptions.UnsupportedAttribute('description', '2.55') + body = self._build_body(name, ram, vcpus, disk, flavorid, swap, ephemeral, rxtx_factor, is_public) + if description: + body['flavor']['description'] = description return self._create("/flavors", body, "flavor") + + @api_versions.wraps('2.55') + def update(self, flavor, description=None): + """ + Update the description of the flavor. + + :param flavor: The :class:`Flavor` (or its ID) to update. + :param description: The description to set on the flavor. + """ + body = { + 'flavor': { + 'description': description + } + } + return self._update('/flavors/%s' % base.getid(flavor), body, 'flavor') diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 837526c41..c7a366fbe 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -1052,7 +1052,7 @@ def _print_flavor_extra_specs(flavor): return "N/A" -def _print_flavor_list(flavors, show_extra_specs=False): +def _print_flavor_list(cs, flavors, show_extra_specs=False): _translate_flavor_keys(flavors) headers = [ @@ -1073,6 +1073,9 @@ def _print_flavor_list(flavors, show_extra_specs=False): else: formatters = {} + if cs.api_version >= api_versions.APIVersion('2.55'): + headers.append('Description') + utils.print_list(flavors, headers, formatters) @@ -1138,7 +1141,7 @@ def do_flavor_list(cs, args): flavors = cs.flavors.list(marker=args.marker, min_disk=args.min_disk, min_ram=args.min_ram, sort_key=args.sort_key, sort_dir=args.sort_dir, limit=args.limit) - _print_flavor_list(flavors, args.extra_specs) + _print_flavor_list(cs, flavors, args.extra_specs) @utils.arg( @@ -1149,7 +1152,7 @@ def do_flavor_delete(cs, args): """Delete a specific flavor""" flavorid = _find_flavor(cs, args.flavor) cs.flavors.delete(flavorid) - _print_flavor_list([flavorid]) + _print_flavor_list(cs, [flavorid]) @utils.arg( @@ -1204,12 +1207,39 @@ def do_flavor_show(cs, args): help=_("Make flavor accessible to the public (default true)."), type=lambda v: strutils.bool_from_string(v, True), default=True) +@utils.arg( + '--description', + metavar='', + help=_('A free form description of the flavor. Limited to 65535 ' + 'characters in length. Only printable characters are allowed.'), + start_version='2.55') def do_flavor_create(cs, args): """Create a new flavor.""" + if cs.api_version >= api_versions.APIVersion('2.55'): + description = args.description + else: + description = None f = cs.flavors.create(args.name, args.ram, args.vcpus, args.disk, args.id, args.ephemeral, args.swap, args.rxtx_factor, - args.is_public) - _print_flavor_list([f]) + args.is_public, description) + _print_flavor_list(cs, [f]) + + +@api_versions.wraps('2.55') +@utils.arg( + 'flavor', + metavar='', + help=_('Name or ID of the flavor to update.')) +@utils.arg( + 'description', + metavar='', + help=_('A free form description of the flavor. Limited to 65535 ' + 'characters in length. Only printable characters are allowed.')) +def do_flavor_update(cs, args): + """Update the description of an existing flavor.""" + flavorid = _find_flavor(cs, args.flavor) + flavor = cs.flavors.update(flavorid, args.description) + _print_flavor_list(cs, [flavor]) @utils.arg( diff --git a/releasenotes/notes/microversion-v2_55-flavor-description-a93718b31f1f0f39.yaml b/releasenotes/notes/microversion-v2_55-flavor-description-a93718b31f1f0f39.yaml new file mode 100644 index 000000000..9c3e6c00d --- /dev/null +++ b/releasenotes/notes/microversion-v2_55-flavor-description-a93718b31f1f0f39.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Support is added for compute API version 2.55. This adds the ability + to create a flavor with a description, show the description of a flavor, + and update the description on an existing flavor. + + A new ``nova flavor-update `` command is added. From 00ffdef3d549e331ed99e2db40629e62fefede3b Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Mon, 3 Jul 2017 13:26:27 +0300 Subject: [PATCH 1377/1705] [ci] Use pseudo-random names for new resources Random names of resources makes the task impossible to find a test which doesn't clean resources after a launch. This patch changes name_generate method to generate names including a name of the test, which can be really helpful. Also, the simple script that prints resources after test run is added. Change-Id: Id3a743afb624dd4bf8ed3523a1916455fd93655a --- novaclient/tests/functional/base.py | 27 ++++++++++------- .../tests/functional/hooks/check_resources.py | 30 +++++++++++++++++++ .../v2/legacy/test_flavor_access.py | 6 ++-- .../functional/v2/legacy/test_instances.py | 2 +- .../functional/v2/legacy/test_keypairs.py | 3 +- .../v2/legacy/test_server_groups.py | 4 +-- .../functional/v2/legacy/test_servers.py | 20 ++++++------- .../tests/functional/v2/legacy/test_usage.py | 8 ++--- .../tests/functional/v2/test_aggregates.py | 6 ++-- .../functional/v2/test_device_tagging.py | 9 +++--- .../tests/functional/v2/test_flavor_access.py | 2 +- .../tests/functional/v2/test_keypairs.py | 6 ++-- novaclient/tests/functional/v2/test_resize.py | 16 ++++------ .../tests/functional/v2/test_servers.py | 4 +-- .../functional/v2/test_trigger_crash_dump.py | 2 +- tox.ini | 8 +++-- 16 files changed, 91 insertions(+), 62 deletions(-) create mode 100644 novaclient/tests/functional/hooks/check_resources.py diff --git a/novaclient/tests/functional/base.py b/novaclient/tests/functional/base.py index 6b95f40d4..2c83f3027 100644 --- a/novaclient/tests/functional/base.py +++ b/novaclient/tests/functional/base.py @@ -373,13 +373,20 @@ def wait_for_resource_delete(self, resource, manager, else: self.fail("The resource '%s' still exists." % resource.id) - def name_generate(self, prefix='Entity'): - """Generate randomized name for some entity. - - :param prefix: string prefix - """ - name = "%s-%s" % (prefix, uuidutils.generate_uuid()) - return name + def name_generate(self): + """Generate randomized name for some entity.""" + # NOTE(andreykurilin): name_generator method is used for various + # resources (servers, flavors, volumes, keystone users, etc). + # Since the length of name has limits we cannot use the whole UUID, + # so the first 8 chars is taken from it. + # Based on the fact that the new name includes class and method + # names, 8 chars of uuid should be enough to prevent any conflicts, + # even if the single test will be launched in parallel thousand times + return "%(prefix)s-%(test_cls)s-%(test_name)s" % { + "prefix": uuidutils.generate_uuid()[:8], + "test_cls": self.__class__.__name__, + "test_name": self.id().rsplit(".", 1)[-1] + } def _get_value_from_the_table(self, table, key): """Parses table to get desired value. @@ -470,7 +477,7 @@ def _get_list_of_values_from_single_column_table(self, table, column): def _create_server(self, name=None, flavor=None, with_network=True, add_cleanup=True, **kwargs): - name = name or self.name_generate(prefix='server') + name = name or self.name_generate() if with_network: nics = [{"net-id": self.network.id}] else: @@ -521,8 +528,8 @@ class TenantTestBase(ClientTestBase): def setUp(self): super(TenantTestBase, self).setUp() - user_name = self.name_generate('v' + self.COMPUTE_API_VERSION) - project_name = self.name_generate('v' + self.COMPUTE_API_VERSION) + user_name = uuidutils.generate_uuid() + project_name = uuidutils.generate_uuid() password = 'password' if self.keystone.version == "v3": diff --git a/novaclient/tests/functional/hooks/check_resources.py b/novaclient/tests/functional/hooks/check_resources.py new file mode 100644 index 000000000..1d2d1bdba --- /dev/null +++ b/novaclient/tests/functional/hooks/check_resources.py @@ -0,0 +1,30 @@ +# 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 novaclient.tests.functional import base + + +class ResourceChecker(base.ClientTestBase): + + def runTest(self): + pass + + def check(self): + self.setUp() + + print("$ nova list --all-tenants") + print(self.nova("list", params="--all-tenants")) + print("\n") + + +if __name__ == "__main__": + ResourceChecker().check() diff --git a/novaclient/tests/functional/v2/legacy/test_flavor_access.py b/novaclient/tests/functional/v2/legacy/test_flavor_access.py index 54671b286..964e3d610 100644 --- a/novaclient/tests/functional/v2/legacy/test_flavor_access.py +++ b/novaclient/tests/functional/v2/legacy/test_flavor_access.py @@ -29,7 +29,7 @@ def test_non_public_flavor_list(self): # Check that non-public flavor appears in flavor list # only for admin tenant and only with --all attribute # and doesn't appear for non-admin tenant - flv_name = self.name_generate(prefix='flv') + flv_name = self.name_generate() self.nova('flavor-create --is-public false %s auto 512 1 1' % flv_name) self.addCleanup(self.nova, 'flavor-delete %s' % flv_name) flavor_list1 = self.nova('flavor-list') @@ -42,7 +42,7 @@ def test_non_public_flavor_list(self): def test_add_access_non_public_flavor(self): # Check that it's allowed to grant an access to non-public flavor for # the given tenant - flv_name = self.name_generate(prefix='flv') + flv_name = self.name_generate() self.nova('flavor-create --is-public false %s auto 512 1 1' % flv_name) self.addCleanup(self.nova, 'flavor-delete %s' % flv_name) self.nova('flavor-access-add', params="%s %s" % @@ -55,7 +55,7 @@ def test_add_access_public_flavor(self): # successfully for public flavor, but the next operation, # 'flavor-access-list --flavor %(name_of_public_flavor)' returns # a CommandError - flv_name = self.name_generate(prefix='flv') + flv_name = self.name_generate() self.nova('flavor-create %s auto 512 1 1' % flv_name) self.addCleanup(self.nova, 'flavor-delete %s' % flv_name) self.nova('flavor-access-add %s %s' % (flv_name, self.project_id)) diff --git a/novaclient/tests/functional/v2/legacy/test_instances.py b/novaclient/tests/functional/v2/legacy/test_instances.py index 3ff07b44b..131407a77 100644 --- a/novaclient/tests/functional/v2/legacy/test_instances.py +++ b/novaclient/tests/functional/v2/legacy/test_instances.py @@ -37,7 +37,7 @@ def test_attach_volume(self): destroy. """ - name = self.name_generate('Instance') + name = self.name_generate() # Boot via the cli, as we're primarily testing the cli in this test self.nova('boot', diff --git a/novaclient/tests/functional/v2/legacy/test_keypairs.py b/novaclient/tests/functional/v2/legacy/test_keypairs.py index 73e77b9cf..1e04781df 100644 --- a/novaclient/tests/functional/v2/legacy/test_keypairs.py +++ b/novaclient/tests/functional/v2/legacy/test_keypairs.py @@ -12,7 +12,6 @@ import tempfile -from oslo_utils import uuidutils from tempest.lib import exceptions from novaclient.tests.functional import base @@ -36,7 +35,7 @@ def _create_keypair(self, **kwargs): return key_name def _raw_create_keypair(self, **kwargs): - key_name = 'keypair-' + uuidutils.generate_uuid() + key_name = self.name_generate() kwargs_str = self._serialize_kwargs(kwargs) self.nova('keypair-add %s %s' % (kwargs_str, key_name)) return key_name diff --git a/novaclient/tests/functional/v2/legacy/test_server_groups.py b/novaclient/tests/functional/v2/legacy/test_server_groups.py index 5518ea979..a1ab151b4 100644 --- a/novaclient/tests/functional/v2/legacy/test_server_groups.py +++ b/novaclient/tests/functional/v2/legacy/test_server_groups.py @@ -11,8 +11,6 @@ # License for the specific language governing permissions and limitations # under the License. -from oslo_utils import uuidutils - from novaclient.tests.functional import base @@ -22,7 +20,7 @@ class TestServerGroupClient(base.ClientTestBase): COMPUTE_API_VERSION = "2.1" def _create_sg(self, policy): - sg_name = 'server_group-' + uuidutils.generate_uuid() + sg_name = self.name_generate() output = self.nova('server-group-create %s %s' % (sg_name, policy)) sg_id = self._get_column_value_from_single_row_table(output, "Id") return sg_id diff --git a/novaclient/tests/functional/v2/legacy/test_servers.py b/novaclient/tests/functional/v2/legacy/test_servers.py index c0852a1bf..5980e94f5 100644 --- a/novaclient/tests/functional/v2/legacy/test_servers.py +++ b/novaclient/tests/functional/v2/legacy/test_servers.py @@ -13,7 +13,6 @@ import datetime from oslo_utils import timeutils -from oslo_utils import uuidutils from novaclient.tests.functional import base @@ -25,7 +24,7 @@ class TestServersBootNovaClient(base.ClientTestBase): def _boot_server_with_legacy_bdm(self, bdm_params=()): volume_size = 1 - volume_name = uuidutils.generate_uuid() + volume_name = self.name_generate() volume = self.cinder.volumes.create(size=volume_size, name=volume_name, imageRef=self.image.id) @@ -43,7 +42,7 @@ def _boot_server_with_legacy_bdm(self, bdm_params=()): params = ( "%(name)s --flavor %(flavor)s --poll " "--block-device-mapping vda=%(volume_id)s%(bdm_params)s" % { - "name": uuidutils.generate_uuid(), "flavor": + "name": self.name_generate(), "flavor": self.flavor.id, "volume_id": volume.id, "bdm_params": bdm_params}) @@ -73,7 +72,7 @@ def test_boot_server_with_legacy_bdm_volume_id_only(self): def test_boot_server_with_net_name(self): server_info = self.nova("boot", params=( "%(name)s --flavor %(flavor)s --image %(image)s --poll " - "--nic net-name=%(net-name)s" % {"name": uuidutils.generate_uuid(), + "--nic net-name=%(net-name)s" % {"name": self.name_generate(), "image": self.image.id, "flavor": self.flavor.id, "net-name": self.network.name})) @@ -133,7 +132,7 @@ def _create_servers(self, name, number): return [self._create_server(name) for i in range(number)] def test_list_with_limit(self): - name = uuidutils.generate_uuid() + name = self.name_generate() self._create_servers(name, 2) output = self.nova("list", params="--limit 1 --name %s" % name) # Cut header and footer of the table @@ -142,7 +141,7 @@ def test_list_with_limit(self): def test_list_with_changes_since(self): now = datetime.datetime.isoformat(timeutils.utcnow()) - name = uuidutils.generate_uuid() + name = self.name_generate() self._create_servers(name, 1) output = self.nova("list", params="--changes-since %s" % now) self.assertIn(name, output, output) @@ -151,7 +150,7 @@ def test_list_with_changes_since(self): self.assertNotIn(name, output, output) def test_list_all_servers(self): - name = uuidutils.generate_uuid() + name = self.name_generate() precreated_servers = self._create_servers(name, 3) # there are no possibility to exceed the limit on API side, so just # check that "-1" limit processes by novaclient side @@ -161,13 +160,12 @@ def test_list_all_servers(self): self.assertIn(server.id, output) def test_list_minimal(self): - name = uuidutils.generate_uuid() - uuid = self._create_server(name).id + server = self._create_server() server_output = self.nova("list --minimal") # The only fields output are "ID" and "Name" output_uuid = self._get_column_value_from_single_row_table( server_output, 'ID') output_name = self._get_column_value_from_single_row_table( server_output, 'Name') - self.assertEqual(output_uuid, uuid) - self.assertEqual(output_name, name) + self.assertEqual(output_uuid, server.id) + self.assertEqual(output_name, server.name) diff --git a/novaclient/tests/functional/v2/legacy/test_usage.py b/novaclient/tests/functional/v2/legacy/test_usage.py index e37693d1b..11080c135 100644 --- a/novaclient/tests/functional/v2/legacy/test_usage.py +++ b/novaclient/tests/functional/v2/legacy/test_usage.py @@ -34,13 +34,13 @@ def _get_num_servers_by_tenant_from_usage_output(self): def test_usage(self): before = self._get_num_servers_from_usage_output() - self._create_server('some-server') + self._create_server() after = self._get_num_servers_from_usage_output() self.assertGreater(after, before) def test_usage_tenant(self): before = self._get_num_servers_by_tenant_from_usage_output() - self._create_server('some-server') + self._create_server() after = self._get_num_servers_by_tenant_from_usage_output() self.assertGreater(after, before) @@ -51,8 +51,8 @@ class TestUsageClient(base.ClientTestBase): def _create_servers_in_time_window(self): start = datetime.datetime.now() - self._create_server('some-server') - self._create_server('another-server') + self._create_server() + self._create_server() end = datetime.datetime.now() return start, end diff --git a/novaclient/tests/functional/v2/test_aggregates.py b/novaclient/tests/functional/v2/test_aggregates.py index b89e35664..f3f5f032f 100644 --- a/novaclient/tests/functional/v2/test_aggregates.py +++ b/novaclient/tests/functional/v2/test_aggregates.py @@ -11,8 +11,6 @@ # License for the specific language governing permissions and limitations # under the License. -from oslo_utils import uuidutils - from novaclient.tests.functional import base @@ -21,8 +19,8 @@ class TestAggregatesNovaClient(base.ClientTestBase): def setUp(self): super(TestAggregatesNovaClient, self).setUp() - self.agg1 = 'agg-%s' % uuidutils.generate_uuid() - self.agg2 = 'agg-%s' % uuidutils.generate_uuid() + self.agg1 = self.name_generate() + self.agg2 = self.name_generate() self.addCleanup(self._clean_aggregates) def _clean_aggregates(self): diff --git a/novaclient/tests/functional/v2/test_device_tagging.py b/novaclient/tests/functional/v2/test_device_tagging.py index c19c42403..5bb900e74 100644 --- a/novaclient/tests/functional/v2/test_device_tagging.py +++ b/novaclient/tests/functional/v2/test_device_tagging.py @@ -12,7 +12,6 @@ # License for the specific language governing permissions and limitations # under the License. -from oslo_utils import uuidutils import six from tempest.lib import exceptions @@ -33,7 +32,7 @@ def test_boot_server_with_tagged_block_devices_with_error(self): '--nic net-id=%(net-uuid)s ' '--block-device ' 'source=image,dest=volume,id=%(image)s,size=1,bootindex=0,' - 'shutdown=remove,tag=bar' % {'name': uuidutils.generate_uuid(), + 'shutdown=remove,tag=bar' % {'name': self.name_generate(), 'flavor': self.flavor.id, 'net-uuid': self.network.id, 'image': self.image.id})) @@ -63,7 +62,7 @@ def test_boot_server_with_tagged_nic_devices_with_error(self): '--nic net-id=%(net-uuid)s,tag=foo ' '--block-device ' 'source=image,dest=volume,id=%(image)s,size=1,bootindex=0,' - 'shutdown=remove' % {'name': uuidutils.generate_uuid(), + 'shutdown=remove' % {'name': self.name_generate(), 'flavor': self.flavor.id, 'net-uuid': self.network.id, 'image': self.image.id})) @@ -90,7 +89,7 @@ def test_boot_server_with_tagged_block_devices(self): '--nic net-id=%(net-uuid)s ' '--block-device ' 'source=image,dest=volume,id=%(image)s,size=1,bootindex=0,' - 'shutdown=remove,tag=bar' % {'name': uuidutils.generate_uuid(), + 'shutdown=remove,tag=bar' % {'name': self.name_generate(), 'flavor': self.flavor.id, 'net-uuid': self.network.id, 'image': self.image.id})) @@ -112,7 +111,7 @@ def test_boot_server_with_tagged_nic_devices(self): '--nic net-id=%(net-uuid)s,tag=foo ' '--block-device ' 'source=image,dest=volume,id=%(image)s,size=1,bootindex=0,' - 'shutdown=remove' % {'name': uuidutils.generate_uuid(), + 'shutdown=remove' % {'name': self.name_generate(), 'flavor': self.flavor.id, 'net-uuid': self.network.id, 'image': self.image.id})) diff --git a/novaclient/tests/functional/v2/test_flavor_access.py b/novaclient/tests/functional/v2/test_flavor_access.py index 229aa3fc4..23ef030ec 100644 --- a/novaclient/tests/functional/v2/test_flavor_access.py +++ b/novaclient/tests/functional/v2/test_flavor_access.py @@ -22,7 +22,7 @@ class TestFlvAccessNovaClientV27(test_flavor_access.TestFlvAccessNovaClient): COMPUTE_API_VERSION = "2.7" def test_add_access_public_flavor(self): - flv_name = self.name_generate('v' + self.COMPUTE_API_VERSION) + flv_name = self.name_generate() self.nova('flavor-create %s auto 512 1 1' % flv_name) self.addCleanup(self.nova, 'flavor-delete %s' % flv_name) output = self.nova('flavor-access-add %s %s' % diff --git a/novaclient/tests/functional/v2/test_keypairs.py b/novaclient/tests/functional/v2/test_keypairs.py index dba132c51..4f2df58a7 100644 --- a/novaclient/tests/functional/v2/test_keypairs.py +++ b/novaclient/tests/functional/v2/test_keypairs.py @@ -50,7 +50,7 @@ class TestKeypairsNovaClientV210(base.TenantTestBase): COMPUTE_API_VERSION = "2.10" def test_create_and_list_keypair(self): - name = self.name_generate("v2_10") + name = self.name_generate() self.nova("keypair-add %s --user %s" % (name, self.user_id)) self.addCleanup(self.another_nova, "keypair-delete %s" % name) output = self.nova("keypair-list") @@ -71,7 +71,7 @@ def test_create_and_list_keypair(self): self._get_value_from_the_table(output_1, "user_id")) def test_create_and_delete(self): - name = self.name_generate("v2_10") + name = self.name_generate() def cleanup(): # We should check keypair existence and remove it from correct user @@ -101,7 +101,7 @@ class TestKeypairsNovaClientV235(base.TenantTestBase): def test_create_and_list_keypair_with_marker_and_limit(self): names = [] for i in range(3): - names.append(self.name_generate("v2_35")) + names.append(self.name_generate()) self.nova("keypair-add %s --user %s" % (names[i], self.user_id)) self.addCleanup(self.another_nova, "keypair-delete %s" % names[i]) diff --git a/novaclient/tests/functional/v2/test_resize.py b/novaclient/tests/functional/v2/test_resize.py index 2c14c19f9..0354610fd 100644 --- a/novaclient/tests/functional/v2/test_resize.py +++ b/novaclient/tests/functional/v2/test_resize.py @@ -66,8 +66,7 @@ def test_resize_up_confirm(self): """Tests creating a server and resizes up and confirms the resize. Compares quota before, during and after the resize. """ - server_id = self._create_server('resize-up-confirm', - flavor=self.flavor.id).id + server_id = self._create_server(flavor=self.flavor.id).id # get the starting quota now that we've created a server starting_usage = self._get_absolute_limits() # now resize up @@ -95,16 +94,14 @@ def _create_resize_down_flavors(self): smaller flavor. """ output = self.nova('flavor-create', - params='resize-larger-flavor auto 128 0 1') + params='%s auto 128 0 1' % self.name_generate()) larger_id = self._get_column_value_from_single_row_table(output, "ID") - self.addCleanup( - self.nova, 'flavor-delete', params='resize-larger-flavor') + self.addCleanup(self.nova, 'flavor-delete', params=larger_id) output = self.nova('flavor-create', - params='resize-smaller-flavor auto 64 0 1') + params='%s auto 64 0 1' % self.name_generate()) smaller_id = self._get_column_value_from_single_row_table(output, "ID") - self.addCleanup( - self.nova, 'flavor-delete', params='resize-smaller-flavor') + self.addCleanup(self.nova, 'flavor-delete', params=smaller_id) return larger_id, smaller_id @@ -117,8 +114,7 @@ def test_resize_down_revert(self): # create our own flavors. larger_flavor, smaller_flavor = self._create_resize_down_flavors() # Now create the server with the larger flavor. - server_id = self._create_server('resize-down-revert', - flavor=larger_flavor).id + server_id = self._create_server(flavor=larger_flavor).id # get the starting quota now that we've created a server starting_usage = self._get_absolute_limits() # now resize down diff --git a/novaclient/tests/functional/v2/test_servers.py b/novaclient/tests/functional/v2/test_servers.py index c2290eb22..8d5fd02c9 100644 --- a/novaclient/tests/functional/v2/test_servers.py +++ b/novaclient/tests/functional/v2/test_servers.py @@ -231,7 +231,7 @@ def test_boot_server_with_auto_network(self): self.skipTest('multiple networks available') server_info = self.nova('boot', params=( '%(name)s --flavor %(flavor)s --poll ' - '--image %(image)s ' % {'name': self.name_generate('server'), + '--image %(image)s ' % {'name': self.name_generate(), 'flavor': self.flavor.id, 'image': self.image.id})) server_id = self._get_value_from_the_table(server_info, 'id') @@ -251,7 +251,7 @@ def test_boot_server_with_no_network(self): server_info = self.nova('boot', params=( '%(name)s --flavor %(flavor)s --poll ' '--image %(image)s --nic none' % - {'name': self.name_generate('server'), + {'name': self.name_generate(), 'flavor': self.flavor.id, 'image': self.image.id})) server_id = self._get_value_from_the_table(server_info, 'id') diff --git a/novaclient/tests/functional/v2/test_trigger_crash_dump.py b/novaclient/tests/functional/v2/test_trigger_crash_dump.py index bf556eec8..b7df75140 100644 --- a/novaclient/tests/functional/v2/test_trigger_crash_dump.py +++ b/novaclient/tests/functional/v2/test_trigger_crash_dump.py @@ -119,7 +119,7 @@ def test_trigger_crash_dump_in_locked_state_admin(self): self._assert_nmi(server.id) def test_trigger_crash_dump_in_locked_state_nonadmin(self): - name = self.name_generate(prefix='server') + name = self.name_generate() server = self.another_nova('boot --flavor %s --image %s --poll %s' % (self.flavor.name, self.image.name, name)) self.addCleanup(self.another_nova, 'delete', params=name) diff --git a/tox.ini b/tox.ini index 48351b605..c8910f5f6 100644 --- a/tox.ini +++ b/tox.ini @@ -46,7 +46,9 @@ passenv = OS_NOVACLIENT_TEST_NETWORK setenv = {[testenv]setenv} OS_TEST_PATH = ./novaclient/tests/functional -commands = bash tools/pretty_tox.sh '--concurrency=1 {posargs}' +commands = + bash tools/pretty_tox.sh '--concurrency=1 {posargs}' + python novaclient/tests/functional/hooks/check_resources.py [testenv:functional-py35] basepython = python3.5 @@ -54,7 +56,9 @@ passenv = OS_NOVACLIENT_TEST_NETWORK setenv = {[testenv]setenv} OS_TEST_PATH = ./novaclient/tests/functional -commands = bash tools/pretty_tox.sh '--concurrency=1 {posargs}' +commands = + bash tools/pretty_tox.sh '--concurrency=1 {posargs}' + python novaclient/tests/functional/hooks/check_resources.py [testenv:cover] commands = From c363793a68816048ce7363837b36b584421ac0e7 Mon Sep 17 00:00:00 2001 From: Chen Hanxiao Date: Fri, 27 Oct 2017 17:06:37 +0800 Subject: [PATCH 1378/1705] inject file: add description of injecting multiple files. We support inject multiple files by one cmd. This patch adds some description and limitation in help messages. Change-Id: I481d2c83530c82e18661a13d53580501e721754f Signed-off-by: Chen Hanxiao --- novaclient/v2/shell.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index c7a366fbe..a25e90b7f 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -590,7 +590,10 @@ def _boot(cs, args): dest='files', default=[], help=_("Store arbitrary files from locally to " - "on the new server. Limited by the injected_files quota value.")) + "on the new server. More files can be injected using multiple " + "'--file' options. Limited by the 'injected_files' quota value. " + "The default value is 5. You can get the current quota value by " + "'Personality' limit from 'nova limits' command.")) @utils.arg( '--key-name', default=os.environ.get('NOVACLIENT_DEFAULT_KEY_NAME'), @@ -1815,7 +1818,8 @@ def do_reboot(cs, args): dest='files', default=[], help=_("Store arbitrary files from locally to " - "on the new server. You may store up to 5 files.")) + "on the new server. More files can be injected using multiple " + "'--file' options. You may store up to 5 files.")) @utils.arg( '--key-name', metavar='', From aede01a0a8d21d49fc4a811650b5ae5bdcb17adc Mon Sep 17 00:00:00 2001 From: Andreas Jaeger Date: Sat, 2 Dec 2017 09:33:14 +0100 Subject: [PATCH 1379/1705] Avoid tox_install.sh for constraints support We do not need tox_install.sh, pip can handle constraints itself and install the project correctly. Thus update tox.ini and remove the now obsolete tools/tox_install.sh file. This follows https://review.openstack.org/#/c/508061 to remove tools/tox_install.sh. Change-Id: I7f06c0be47f57a9c1ee3cadc7328b1b1b138b3cd --- tools/tox_install.sh | 30 ------------------------------ tox.ini | 13 +++++-------- 2 files changed, 5 insertions(+), 38 deletions(-) delete mode 100755 tools/tox_install.sh diff --git a/tools/tox_install.sh b/tools/tox_install.sh deleted file mode 100755 index 43468e450..000000000 --- a/tools/tox_install.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env bash - -# Client constraint file contains this client version pin that is in conflict -# with installing the client from source. We should remove the version pin in -# the constraints file before applying it for from-source installation. - -CONSTRAINTS_FILE=$1 -shift 1 - -set -e - -# NOTE(tonyb): Place this in the tox enviroment's log dir so it will get -# published to logs.openstack.org for easy debugging. -localfile="$VIRTUAL_ENV/log/upper-constraints.txt" - -if [[ $CONSTRAINTS_FILE != http* ]]; then - CONSTRAINTS_FILE=file://$CONSTRAINTS_FILE -fi -# NOTE(tonyb): need to add curl to bindep.txt if the project supports bindep -curl $CONSTRAINTS_FILE --insecure --progress-bar --output $localfile - -pip install -c$localfile openstack-requirements - -# This is the main purpose of the script: Allow local installation of -# the current repo. It is listed in constraints file and thus any -# install will be constrained and we need to unconstrain it. -edit-constraints $localfile -- $CLIENT_NAME - -pip install -c$localfile -U $* -exit $? diff --git a/tox.ini b/tox.ini index c8910f5f6..2ffc89a57 100644 --- a/tox.ini +++ b/tox.ini @@ -11,12 +11,11 @@ whitelist_externals = find bash passenv = ZUUL_CACHE_DIR REQUIREMENTS_PIP_LOCATION -install_command = {toxinidir}/tools/tox_install.sh {env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} {opts} {packages} -setenv = VIRTUAL_ENV={envdir} - BRANCH_NAME=master - CLIENT_NAME=python-novaclient - -deps = -r{toxinidir}/test-requirements.txt +install_command = pip install {opts} {packages} +deps = + -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} + -r{toxinidir}/test-requirements.txt + -r{toxinidir}/requirements.txt commands = find . -type f -name "*.pyc" -delete bash tools/pretty_tox.sh '{posargs}' @@ -44,7 +43,6 @@ commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasen basepython = python2.7 passenv = OS_NOVACLIENT_TEST_NETWORK setenv = - {[testenv]setenv} OS_TEST_PATH = ./novaclient/tests/functional commands = bash tools/pretty_tox.sh '--concurrency=1 {posargs}' @@ -54,7 +52,6 @@ commands = basepython = python3.5 passenv = OS_NOVACLIENT_TEST_NETWORK setenv = - {[testenv]setenv} OS_TEST_PATH = ./novaclient/tests/functional commands = bash tools/pretty_tox.sh '--concurrency=1 {posargs}' From e5e8cebc817aafbcbda7785d0f9c13cbdbf3a90c Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Sat, 22 Jul 2017 21:41:54 +0900 Subject: [PATCH 1380/1705] Microversion 2.56 - Enable cold migration with target host Change-Id: I4deea811ffae3e7944d5ec10ca0bbf2bfa056a7c Implements: blueprint cold-migration-with-target-queens --- novaclient/__init__.py | 2 +- novaclient/tests/unit/fixture_data/servers.py | 2 ++ novaclient/tests/unit/v2/fakes.py | 11 +++++++- novaclient/tests/unit/v2/test_servers.py | 23 +++++++++++++++ novaclient/tests/unit/v2/test_shell.py | 17 +++++++++++ novaclient/v2/servers.py | 28 +++++++++++++++++++ novaclient/v2/shell.py | 14 ++++++++-- ...n-with-target-queens-e361d4ae977aa396.yaml | 8 ++++++ 8 files changed, 101 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/bp-cold-migration-with-target-queens-e361d4ae977aa396.yaml diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 7fb218c76..435ff9c9f 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.55") +API_MAX_VERSION = api_versions.APIVersion("2.56") diff --git a/novaclient/tests/unit/fixture_data/servers.py b/novaclient/tests/unit/fixture_data/servers.py index f019a2277..2402cd106 100644 --- a/novaclient/tests/unit/fixture_data/servers.py +++ b/novaclient/tests/unit/fixture_data/servers.py @@ -452,6 +452,8 @@ def post_servers_1234_action(self, request, context): # but we can not specify version in data_fixture now and this is # V1 data, so just let it pass pass + elif action == 'migrate': + return None elif action == 'rebuild': body = body[action] adminPass = body.get('adminPass', 'randompassword') diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index b59f55496..bdb8d6735 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -687,7 +687,7 @@ def delete_servers_1234_os_server_password(self, **kw): # Server actions # - none_actions = ['revertResize', 'migrate', 'os-stop', 'os-start', + none_actions = ['revertResize', 'os-stop', 'os-start', 'forceDelete', 'restore', 'pause', 'unpause', 'unlock', 'unrescue', 'resume', 'suspend', 'lock', 'shelve', 'shelveOffload', 'unshelve', 'resetNetwork'] @@ -749,6 +749,15 @@ def post_servers_1234_action(self, body, **kw): if self.api_version < api_versions.APIVersion("2.25"): expected.add('disk_over_commit') assert set(body[action].keys()) == expected + elif action == 'migrate': + if self.api_version < api_versions.APIVersion("2.56"): + assert body[action] is None + else: + expected = set() + if 'host' in body[action].keys(): + # host can be optional + expected.add('host') + assert set(body[action].keys()) == expected elif action == 'rebuild': body = body[action] adminPass = body.get('adminPass', 'randompassword') diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index 396e3b763..fb18261b3 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -1581,3 +1581,26 @@ def test_rebuild_with_key_name_pre_254_fails(self): '1234', fakes.FAKE_IMAGE_UUID_1, key_name='test_keypair') self.assertIn('key_name', six.text_type(ex.message)) + + +class ServersV256Test(ServersV254Test): + + api_version = "2.56" + + def test_migrate_server(self): + s = self.cs.servers.get(1234) + ret = s.migrate() + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action', + {'migrate': {}}) + ret = s.migrate(host='target-host') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action', + {'migrate': {'host': 'target-host'}}) + + def test_migrate_server_pre_256_fails(self): + self.cs.api_version = api_versions.APIVersion('2.55') + s = self.cs.servers.get(1234) + ex = self.assertRaises(TypeError, + s.migrate, host='target-host') + self.assertIn('host', six.text_type(ex)) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 7198715ca..daf6da016 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -1628,6 +1628,23 @@ def test_migrate(self): self.run_command('migrate sample-server') self.assert_called('POST', '/servers/1234/action', {'migrate': None}) + def test_migrate_pre_v256(self): + self.assertRaises(SystemExit, + self.run_command, + 'migrate --host target-host sample-server', + api_version='2.55') + + def test_migrate_v256(self): + self.run_command('migrate sample-server', + api_version='2.56') + self.assert_called('POST', '/servers/1234/action', + {'migrate': {}}) + + self.run_command('migrate --host target-host sample-server', + api_version='2.56') + self.assert_called('POST', '/servers/1234/action', + {'migrate': {'host': 'target-host'}}) + def test_resize(self): self.run_command('resize sample-server 1') self.assert_called('POST', '/servers/1234/action', diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index ed8e2b55f..1e236c894 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -327,6 +327,7 @@ def diagnostics(self): """Diagnostics -- Retrieve server diagnostics.""" return self.manager.diagnostics(self) + @api_versions.wraps("2.0", "2.55") def migrate(self): """ Migrate a server to a new host. @@ -335,6 +336,16 @@ def migrate(self): """ return self.manager.migrate(self) + @api_versions.wraps("2.56") + def migrate(self, host=None): + """ + Migrate a server to a new host. + + :param host: (Optional) The target host. + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self.manager.migrate(self, host=host) + def remove_fixed_ip(self, address): """ Remove an IP address. @@ -1545,6 +1556,7 @@ def rebuild(self, server, image, password=None, disk_config=None, body, **kwargs) return Server(self, body['server'], resp=resp) + @api_versions.wraps("2.0", "2.55") def migrate(self, server): """ Migrate a server to a new host. @@ -1554,6 +1566,22 @@ def migrate(self, server): """ return self._action('migrate', server) + @api_versions.wraps("2.56") + def migrate(self, server, host=None): + """ + Migrate a server to a new host. + + :param server: The :class:`Server` (or its ID). + :param host: (Optional) The target host. + :returns: An instance of novaclient.base.TupleWithMeta + """ + info = {} + + if host: + info['host'] = host + + return self._action('migrate', server, info) + def resize(self, server, flavor, disk_config=None, **kwargs): """ Resize a server's resources. diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index a25e90b7f..f9487a8b9 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -1950,6 +1950,12 @@ def do_resize_revert(cs, args): @utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg( + '--host', + metavar='', + default=None, + help=_('Destination host name.'), + start_version='2.56') @utils.arg( '--poll', dest='poll', @@ -1957,9 +1963,13 @@ def do_resize_revert(cs, args): default=False, help=_('Report the server migration progress until it completes.')) def do_migrate(cs, args): - """Migrate a server. The new host will be selected by the scheduler.""" + """Migrate a server.""" + update_kwargs = {} + if 'host' in args and args.host: + update_kwargs['host'] = args.host + server = _find_server(cs, args.server) - server.migrate() + server.migrate(**update_kwargs) if args.poll: _poll_for_status(cs.servers.get, server.id, 'migrating', diff --git a/releasenotes/notes/bp-cold-migration-with-target-queens-e361d4ae977aa396.yaml b/releasenotes/notes/bp-cold-migration-with-target-queens-e361d4ae977aa396.yaml new file mode 100644 index 000000000..00317ec6a --- /dev/null +++ b/releasenotes/notes/bp-cold-migration-with-target-queens-e361d4ae977aa396.yaml @@ -0,0 +1,8 @@ +--- +features: + - Added a new ``--host`` option to ``nova migrate`` command + in microversion 2.56. It enables administrators to specify + a target host when cold migating a server. The target host will be + validated by the scheduler. The target host cannot be the same as + the current host on which the server is running and must be in the + same cell that the server is currently in. From 63d56cae7b409a2a445acac990cc8a4ee26bd0a4 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Tue, 5 Dec 2017 03:33:38 +0000 Subject: [PATCH 1381/1705] Updated from global requirements Change-Id: I3773f6a1ff19e9e0d051ab77df9653686ec7bd40 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 5489dedee..4bc8e3d96 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. pbr!=2.1.0,>=2.0.0 # Apache-2.0 -keystoneauth1>=3.2.0 # Apache-2.0 +keystoneauth1>=3.3.0 # Apache-2.0 iso8601>=0.1.11 # MIT oslo.i18n>=3.15.3 # Apache-2.0 oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0 From 674e7c8b447a2c197750e7424a9c083a26008d39 Mon Sep 17 00:00:00 2001 From: Chen Hanxiao Date: Fri, 1 Dec 2017 21:20:43 +0800 Subject: [PATCH 1382/1705] inject file: add method of showing quota value of injecting files for 'rebuild' command Adding command of how to get the quota value of injecting multiple files, as we've already done in 'boot' command. Change-Id: Ib5ee9658a9e15b849dce729719f8297d2108de57 Signed-off-by: Chen Hanxiao --- novaclient/v2/shell.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index f9487a8b9..6165dfed7 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -1819,7 +1819,9 @@ def do_reboot(cs, args): default=[], help=_("Store arbitrary files from locally to " "on the new server. More files can be injected using multiple " - "'--file' options. You may store up to 5 files.")) + "'--file' options. You may store up to 5 files by default. " + "The maximum number of files is specified by the 'Personality' " + "limit reported by the 'nova limits' command.")) @utils.arg( '--key-name', metavar='', From 198c0d7327ca41167473f8aeb4c7bbdb4910cb5a Mon Sep 17 00:00:00 2001 From: Theodoros Tsioutsias Date: Mon, 4 Dec 2017 08:56:26 +0000 Subject: [PATCH 1383/1705] CommandError is raised for invalid server fields When listing servers with fields of invalid type, a TypeError was raised. With this change and in order to ensure the validity of the fields, we are whitelisting them using the keys of the dictionary Resource.to_dict(). For all fields not in the whitelist a CommandError is raised. Change-Id: I647fa611d29745f830daadac1c3f9c1c71c2733a Closes-Bug: #1733917 --- novaclient/tests/unit/v2/test_shell.py | 18 ++++++++++++++++++ novaclient/v2/shell.py | 13 ++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index daf6da016..453c8eacc 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -1395,6 +1395,12 @@ def test_list_fields(self): self.assertIn('securitygroup1', output) self.assertIn('OS-EXT-MOD: Some Thing', output) self.assertIn('mod_some_thing_value', output) + # Testing the 'networks' field that is explicitly added to the + # existing fields list. + output, _err = self.run_command('list --fields networks') + self.assertIn('Networks', output) + self.assertIn('10.11.12.13', output) + self.assertIn('5.6.7.8', output) @mock.patch( 'novaclient.tests.unit.v2.fakes.FakeSessionClient.get_servers_detail') @@ -1411,6 +1417,18 @@ def test_list_invalid_fields(self): self.run_command, 'list --fields host,security_groups,' 'OS-EXT-MOD:some_thing,invalid') + self.assertRaises(exceptions.CommandError, + self.run_command, + 'list --fields __dict__') + self.assertRaises(exceptions.CommandError, + self.run_command, + 'list --fields update') + self.assertRaises(exceptions.CommandError, + self.run_command, + 'list --fields __init__') + self.assertRaises(exceptions.CommandError, + self.run_command, + 'list --fields __module__,updated') def test_list_with_marker(self): self.run_command('list --marker some-uuid') diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index f9487a8b9..dc2d93cb5 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -1009,6 +1009,7 @@ def _expand_dict_attr(collection, attr): delattr(item, attr) for subkey in field.keys(): setattr(item, attr + ':' + subkey, field[subkey]) + item.set_info(attr + ':' + subkey, field[subkey]) def _translate_keys(collection, convert): @@ -1018,6 +1019,7 @@ def _translate_keys(collection, convert): for from_key, to_key in convert: if from_key in keys and to_key not in keys: setattr(item, to_key, item_dict[from_key]) + item.set_info(to_key, item_dict[from_key]) def _translate_extended_states(collection): @@ -1042,6 +1044,8 @@ def _translate_extended_states(collection): getattr(item, 'task_state') except AttributeError: setattr(item, 'task_state', "N/A") + item.set_info('power_state', item.power_state) + item.set_info('task_state', item.task_state) def _translate_flavor_keys(collection): @@ -1706,12 +1710,19 @@ def _get_list_table_columns_and_formatters(fields, objs, exclude_fields=(), columns = [] formatters = {} + existing_fields = set() non_existent_fields = [] exclude_fields = set(exclude_fields) + # NOTE(ttsiouts): Bug #1733917. Validating the fields using the keys of + # the Resource.to_dict(). Adding also the 'networks' field. + if obj: + obj_dict = obj.to_dict() + existing_fields = set(['networks']) | set(obj_dict.keys()) + for field in fields.split(','): - if not hasattr(obj, field): + if field not in existing_fields: non_existent_fields.append(field) continue if field in exclude_fields: From e719d4e046df7748bc4cd01210e6aab696060bd4 Mon Sep 17 00:00:00 2001 From: Jiao Pengju Date: Fri, 8 Dec 2017 10:24:05 +0800 Subject: [PATCH 1384/1705] Remove deprecated command in nova.rst nova image-list has been deprecated in python-novaclient, so the documentation should be updated. Change-Id: Iacd9e0e6c99ef0e5c40c716d0aa56ba0f2972fc3 Closes-Bug: #1737074 --- doc/source/cli/nova.rst | 4 ---- 1 file changed, 4 deletions(-) diff --git a/doc/source/cli/nova.rst b/doc/source/cli/nova.rst index c3c6caf42..05308ba49 100644 --- a/doc/source/cli/nova.rst +++ b/doc/source/cli/nova.rst @@ -48,10 +48,6 @@ Get information about boot command:: nova help boot -List available images:: - - nova image-list - List available flavors:: nova flavor-list From ee2221f0526c4a6bed431229e363c740d07b8ee9 Mon Sep 17 00:00:00 2001 From: ghanshyam Date: Wed, 13 Dec 2017 03:35:09 +0300 Subject: [PATCH 1385/1705] Optimize jobs run on novaclient novaclient run 2 jobs for functional tests - novaclient-dsvm-functional-identity-v3-only (non-voting) - novaclient-dsvm-functional-neutron These 2 were added when neturon and identity v3 were not default in devstack. Now both run as default and we do not separate job to run. This commit does below changes: - delete the 'novaclient-dsvm-functional-neutron' - In 'novaclient-dsvm-functional-identity-v3-only' - make 'novaclient-dsvm-functional-identity-v3-only' as voting - cleanup some 'if' condition which are hardcoded true now - rename 'novaclient-dsvm-functional-identity-v3-only' to 'novaclient-dsvm-functional' NOTE: this not going to backport to stable branch as they seems running identity v2 and v3 in those jobs. Let's keep the same setup there. Change-Id: I4bc564e548876ef4d3b30e736c0055f19c062319 --- .zuul.yaml | 24 ++---- .../run.yaml | 59 -------------- .../post.yaml | 80 ------------------- .../post.yaml | 0 .../run.yaml | 7 +- 5 files changed, 6 insertions(+), 164 deletions(-) delete mode 100644 playbooks/legacy/novaclient-dsvm-functional-identity-v3-only/run.yaml delete mode 100644 playbooks/legacy/novaclient-dsvm-functional-neutron/post.yaml rename playbooks/legacy/{novaclient-dsvm-functional-identity-v3-only => novaclient-dsvm-functional}/post.yaml (100%) rename playbooks/legacy/{novaclient-dsvm-functional-neutron => novaclient-dsvm-functional}/run.yaml (83%) diff --git a/.zuul.yaml b/.zuul.yaml index 94da1e955..2d050d1d0 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -1,33 +1,19 @@ - job: - name: novaclient-dsvm-functional-identity-v3-only + name: novaclient-dsvm-functional parent: legacy-dsvm-base - run: playbooks/legacy/novaclient-dsvm-functional-identity-v3-only/run.yaml - post-run: playbooks/legacy/novaclient-dsvm-functional-identity-v3-only/post.yaml + run: playbooks/legacy/novaclient-dsvm-functional/run.yaml + post-run: playbooks/legacy/novaclient-dsvm-functional/post.yaml timeout: 7200 - voting: false required-projects: - openstack-infra/devstack-gate - openstack/nova - openstack/python-novaclient -- job: - name: novaclient-dsvm-functional-neutron - parent: legacy-dsvm-base - run: playbooks/legacy/novaclient-dsvm-functional-neutron/run.yaml - post-run: playbooks/legacy/novaclient-dsvm-functional-neutron/post.yaml - timeout: 7200 - required-projects: - - openstack-infra/devstack-gate - - openstack/neutron - - openstack/nova - - openstack/python-novaclient - - project: name: openstack/python-novaclient check: jobs: - - novaclient-dsvm-functional-identity-v3-only - - novaclient-dsvm-functional-neutron + - novaclient-dsvm-functional gate: jobs: - - novaclient-dsvm-functional-neutron + - novaclient-dsvm-functional diff --git a/playbooks/legacy/novaclient-dsvm-functional-identity-v3-only/run.yaml b/playbooks/legacy/novaclient-dsvm-functional-identity-v3-only/run.yaml deleted file mode 100644 index ca45b7365..000000000 --- a/playbooks/legacy/novaclient-dsvm-functional-identity-v3-only/run.yaml +++ /dev/null @@ -1,59 +0,0 @@ -- hosts: all - name: Autoconverted job legacy-novaclient-dsvm-functional-identity-v3-only from - old job gate-novaclient-dsvm-functional-identity-v3-only-ubuntu-xenial-nv - tasks: - - - name: Ensure legacy workspace directory - file: - path: '{{ ansible_user_dir }}/workspace' - state: directory - - - shell: - cmd: | - set -e - set -x - cat > clonemap.yaml << EOF - clonemap: - - name: openstack-infra/devstack-gate - dest: devstack-gate - EOF - /usr/zuul-env/bin/zuul-cloner -m clonemap.yaml --cache-dir /opt/git \ - git://git.openstack.org \ - openstack-infra/devstack-gate - executable: /bin/bash - chdir: '{{ ansible_user_dir }}/workspace' - environment: '{{ zuul | zuul_legacy_vars }}' - - - shell: - cmd: | - set -e - set -x - export PYTHONUNBUFFERED=true - export BRANCH_OVERRIDE=default - export DEVSTACK_PROJECT_FROM_GIT=python-novaclient - if [ "$BRANCH_OVERRIDE" != "default" ] ; then - export OVERRIDE_ZUUL_BRANCH=$BRANCH_OVERRIDE - fi - # This ensures that if we set override branch to something - # else, we still take python-novaclient from the zuul branch - # name. So override branch can be 'stable/mitaka' but we can - # test master changes. - uc_project=`echo $DEVSTACK_PROJECT_FROM_GIT | tr [:lower:] [:upper:] | tr '-' '_' | sed 's/[^A-Z_]//'` - export "OVERRIDE_"$uc_project"_PROJECT_BRANCH"=$ZUUL_BRANCH - - function post_test_hook { - # Configure and run functional tests - $BASE/new/python-novaclient/novaclient/tests/functional/hooks/post_test_hook.sh - } - if [ "-identity-v3-only" == "-identity-v3-only" ] ; then - export DEVSTACK_LOCAL_CONFIG="ENABLE_IDENTITY_V2=False" - elif [ "-identity-v3-only" == "-neutron" ] ; then - export DEVSTACK_GATE_NEUTRON=1 - fi - export -f post_test_hook - - cp devstack-gate/devstack-vm-gate-wrap.sh ./safe-devstack-vm-gate-wrap.sh - ./safe-devstack-vm-gate-wrap.sh - executable: /bin/bash - chdir: '{{ ansible_user_dir }}/workspace' - environment: '{{ zuul | zuul_legacy_vars }}' diff --git a/playbooks/legacy/novaclient-dsvm-functional-neutron/post.yaml b/playbooks/legacy/novaclient-dsvm-functional-neutron/post.yaml deleted file mode 100644 index dac875340..000000000 --- a/playbooks/legacy/novaclient-dsvm-functional-neutron/post.yaml +++ /dev/null @@ -1,80 +0,0 @@ -- hosts: primary - tasks: - - - name: Copy files from {{ ansible_user_dir }}/workspace/ on node - synchronize: - src: '{{ ansible_user_dir }}/workspace/' - dest: '{{ zuul.executor.log_root }}' - mode: pull - copy_links: true - verify_host: true - rsync_opts: - - --include=**/*nose_results.html - - --include=*/ - - --exclude=* - - --prune-empty-dirs - - - name: Copy files from {{ ansible_user_dir }}/workspace/ on node - synchronize: - src: '{{ ansible_user_dir }}/workspace/' - dest: '{{ zuul.executor.log_root }}' - mode: pull - copy_links: true - verify_host: true - rsync_opts: - - --include=**/*testr_results.html.gz - - --include=*/ - - --exclude=* - - --prune-empty-dirs - - - name: Copy files from {{ ansible_user_dir }}/workspace/ on node - synchronize: - src: '{{ ansible_user_dir }}/workspace/' - dest: '{{ zuul.executor.log_root }}' - mode: pull - copy_links: true - verify_host: true - rsync_opts: - - --include=/.testrepository/tmp* - - --include=*/ - - --exclude=* - - --prune-empty-dirs - - - name: Copy files from {{ ansible_user_dir }}/workspace/ on node - synchronize: - src: '{{ ansible_user_dir }}/workspace/' - dest: '{{ zuul.executor.log_root }}' - mode: pull - copy_links: true - verify_host: true - rsync_opts: - - --include=**/*testrepository.subunit.gz - - --include=*/ - - --exclude=* - - --prune-empty-dirs - - - name: Copy files from {{ ansible_user_dir }}/workspace/ on node - synchronize: - src: '{{ ansible_user_dir }}/workspace/' - dest: '{{ zuul.executor.log_root }}/tox' - mode: pull - copy_links: true - verify_host: true - rsync_opts: - - --include=/.tox/*/log/* - - --include=*/ - - --exclude=* - - --prune-empty-dirs - - - name: Copy files from {{ ansible_user_dir }}/workspace/ on node - synchronize: - src: '{{ ansible_user_dir }}/workspace/' - dest: '{{ zuul.executor.log_root }}' - mode: pull - copy_links: true - verify_host: true - rsync_opts: - - --include=/logs/** - - --include=*/ - - --exclude=* - - --prune-empty-dirs diff --git a/playbooks/legacy/novaclient-dsvm-functional-identity-v3-only/post.yaml b/playbooks/legacy/novaclient-dsvm-functional/post.yaml similarity index 100% rename from playbooks/legacy/novaclient-dsvm-functional-identity-v3-only/post.yaml rename to playbooks/legacy/novaclient-dsvm-functional/post.yaml diff --git a/playbooks/legacy/novaclient-dsvm-functional-neutron/run.yaml b/playbooks/legacy/novaclient-dsvm-functional/run.yaml similarity index 83% rename from playbooks/legacy/novaclient-dsvm-functional-neutron/run.yaml rename to playbooks/legacy/novaclient-dsvm-functional/run.yaml index fb13df180..e4bed30cd 100644 --- a/playbooks/legacy/novaclient-dsvm-functional-neutron/run.yaml +++ b/playbooks/legacy/novaclient-dsvm-functional/run.yaml @@ -1,5 +1,5 @@ - hosts: all - name: Autoconverted job legacy-novaclient-dsvm-functional-neutron from old job gate-novaclient-dsvm-functional-neutron-ubuntu-xenial + name: novaclient-dsvm-functional job with identity v3 and neutron tasks: - name: Ensure legacy workspace directory @@ -44,11 +44,6 @@ # Configure and run functional tests $BASE/new/python-novaclient/novaclient/tests/functional/hooks/post_test_hook.sh } - if [ "-neutron" == "-identity-v3-only" ] ; then - export DEVSTACK_LOCAL_CONFIG="ENABLE_IDENTITY_V2=False" - elif [ "-neutron" == "-neutron" ] ; then - export DEVSTACK_GATE_NEUTRON=1 - fi export -f post_test_hook cp devstack-gate/devstack-vm-gate-wrap.sh ./safe-devstack-vm-gate-wrap.sh From cfc3a1014dc92f9c836808d08c0b226841ba8d85 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Tue, 19 Dec 2017 01:44:34 +0000 Subject: [PATCH 1386/1705] Updated from global requirements Change-Id: Ib2e28ffd160afd28e57531fbe548185aeb4dee45 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4bc8e3d96..d0c324613 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ keystoneauth1>=3.3.0 # Apache-2.0 iso8601>=0.1.11 # MIT oslo.i18n>=3.15.3 # Apache-2.0 oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0 -oslo.utils>=3.31.0 # Apache-2.0 +oslo.utils>=3.33.0 # Apache-2.0 PrettyTable<0.8,>=0.7.1 # BSD simplejson>=3.5.1 # MIT six>=1.10.0 # MIT From 19387b09df466cfa57fdef9949789f435ccd18e7 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Sat, 23 Dec 2017 10:11:36 +0000 Subject: [PATCH 1387/1705] Updated from global requirements Change-Id: If6afc12a2e76adcd3c7ceb085f4c2cb86fa91a7b --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 389045fe9..65dedf368 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -9,7 +9,7 @@ fixtures>=3.0.0 # Apache-2.0/BSD keyring>=5.5.1 # MIT/PSF mock>=2.0.0 # BSD python-keystoneclient>=3.8.0 # Apache-2.0 -python-cinderclient>=3.2.0 # Apache-2.0 +python-cinderclient>=3.3.0 # Apache-2.0 python-glanceclient>=2.8.0 # Apache-2.0 python-neutronclient>=6.3.0 # Apache-2.0 requests-mock>=1.1.0 # Apache-2.0 From ba50955d717e7fed228decd1432ee43180fe4051 Mon Sep 17 00:00:00 2001 From: Chen Hanxiao Date: Wed, 27 Dec 2017 11:09:21 +0800 Subject: [PATCH 1388/1705] flavor create: clarify --swap description --swap will add a additional storage device, which not affect the original swap partition/device. This patch will clarify this misleading description. Change-Id: I58c8157844cb4ece0cc43d7097d75a3ac4fa4d02 Signed-off-by: Chen Hanxiao --- novaclient/v2/shell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index b9c9f6760..ee4355694 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -1201,7 +1201,7 @@ def do_flavor_show(cs, args): @utils.arg( '--swap', metavar='', - help=_("Swap space size in MB (default 0)."), + help=_("Additional swap space size in MB (default 0)."), default=0) @utils.arg( '--rxtx-factor', From 0ed540b0e2c78f5393d59ca77c96a6b094765a28 Mon Sep 17 00:00:00 2001 From: Tatiana Kholkina Date: Tue, 26 Dec 2017 15:17:56 +0300 Subject: [PATCH 1389/1705] Remove irrelevant note Since Id649d16ec2cdeb04bbaf2239a5e813abcca9c65d do_rename method does not exist so we can remove related note. Also, the note is incorrect, so do not change the check. Change-Id: I4bb0ce22b86db1cb8e474f563e50db11a838d411 --- novaclient/v2/shell.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index b9c9f6760..5c9381816 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -1919,10 +1919,6 @@ def do_update(cs, args): update_kwargs = {} if args.name: update_kwargs["name"] = args.name - # NOTE(andreykurilin): `do_update` method is used by `do_rename` method, - # which do not have description argument at all. When `do_rename` will be - # removed after deprecation period, feel free to change the check below to: - # `if args.description:` if "description" in args and args.description is not None: update_kwargs["description"] = args.description _find_server(cs, args.server).update(**update_kwargs) From 80843644134e5eaa2c738ab35e5236ecfbcf8f74 Mon Sep 17 00:00:00 2001 From: Chen Hanxiao Date: Wed, 27 Dec 2017 16:14:25 +0800 Subject: [PATCH 1390/1705] boot: show warning if more than one match when setting --image-with When setting --image-with meta_key=meta_value, we may got more than one results. We selected the first matched one silently. This patch prints a warning message for this scenario. Change-Id: I5be73fb61fb08d3abd0a509f3ac5cb6ea623c85a Signed-off-by: Chen Hanxiao --- novaclient/v2/shell.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index a30579de3..0f211d0a0 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -91,6 +91,15 @@ def emit_fixed_floating_deprecation_warning(command_name): command_name, file=sys.stderr) +def emit_duplicated_image_with_warning(img, image_with): + img_uuid_list = [str(image.id) for image in img] + print(_('WARNING: Multiple matching images: %(img_uuid_list)s\n' + 'Using image: %(chosen_one)s') % + {'img_uuid_list': img_uuid_list, + 'chosen_one': img_uuid_list[0]}, + file=sys.stderr) + + CLIENT_BDM2_KEYS = { 'id': 'uuid', 'source': 'source_type', @@ -396,9 +405,9 @@ def _boot(cs, args): if not image and args.image_with: images = _match_image(cs, args.image_with) + if len(images) > 1: + emit_duplicated_image_with_warning(images, args.image_with) if images: - # TODO(harlowja): log a warning that we - # are selecting the first of many? image = images[0] min_count = 1 From d68f26ab95032b6880d18ad6c04e4d7c09b63629 Mon Sep 17 00:00:00 2001 From: Guoqiang Ding Date: Thu, 28 Dec 2017 23:03:42 +0800 Subject: [PATCH 1391/1705] Update new documentation PTI jobs For compliance with the Project Testing Interface as described in [1]. For more detailed information, please refer to [2]. [1] https://governance.openstack.org/tc/reference/project-testing-interface.html [2] http://lists.openstack.org/pipermail/openstack-dev/2017-December/125710.html Change-Id: Iace251446bafbd8963bc5a073de50ade583f6e46 --- doc/requirements.txt | 3 +++ test-requirements.txt | 5 ----- tox.ini | 14 +++++++++++--- 3 files changed, 14 insertions(+), 8 deletions(-) create mode 100644 doc/requirements.txt diff --git a/doc/requirements.txt b/doc/requirements.txt new file mode 100644 index 000000000..fada2eb09 --- /dev/null +++ b/doc/requirements.txt @@ -0,0 +1,3 @@ +sphinx>=1.6.2 # BSD +openstackdocstheme>=1.17.0 # Apache-2.0 +reno>=2.5.0 # Apache-2.0 diff --git a/test-requirements.txt b/test-requirements.txt index 65dedf368..d66dda685 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -13,14 +13,9 @@ python-cinderclient>=3.3.0 # Apache-2.0 python-glanceclient>=2.8.0 # Apache-2.0 python-neutronclient>=6.3.0 # Apache-2.0 requests-mock>=1.1.0 # Apache-2.0 -sphinx>=1.6.2 # BSD os-client-config>=1.28.0 # Apache-2.0 -openstackdocstheme>=1.17.0 # Apache-2.0 osprofiler>=1.4.0 # Apache-2.0 testrepository>=0.0.18 # Apache-2.0/BSD testscenarios>=0.4 # Apache-2.0/BSD testtools>=2.2.0 # MIT tempest>=17.1.0 # Apache-2.0 - -# releasenotes -reno>=2.5.0 # Apache-2.0 diff --git a/tox.ini b/tox.ini index 2ffc89a57..ba4a4c141 100644 --- a/tox.ini +++ b/tox.ini @@ -32,12 +32,20 @@ commands = bandit -r novaclient -n5 -x tests commands = {posargs} [testenv:docs] +deps = + -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} + -r{toxinidir}/requirements.txt + -r{toxinidir}/doc/requirements.txt commands = - python setup.py build_sphinx + sphinx-build -b html doc/source doc/build/html [testenv:releasenotes] -commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html - +deps = + -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} + -r{toxinidir}/requirements.txt + -r{toxinidir}/doc/requirements.txt +commands = + sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html [testenv:functional] basepython = python2.7 From 852bd46805098e03fdd85a83ccbeec403bfb4836 Mon Sep 17 00:00:00 2001 From: Chen Hanxiao Date: Fri, 29 Dec 2017 10:15:36 +0800 Subject: [PATCH 1392/1705] boot: error out if no images match the property from --image-with We may fail to get a image when setting --image-with meta_key=meta_value. We should error out, rather than sending a request then waiting for the errors from the remote side. Change-Id: I54a645a533e39b069e50fdee6893f8b0cced494c Signed-off-by: Chen Hanxiao --- novaclient/tests/unit/v2/test_shell.py | 5 +++++ novaclient/v2/shell.py | 3 +++ 2 files changed, 8 insertions(+) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 453c8eacc..30b695e88 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -157,6 +157,11 @@ def test_boot_image_with(self): }}, ) + def test_boot_image_with_error_out_no_match(self): + cmd = ("boot --flavor 1" + " --image-with fake_key=fake_value some-server") + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + def test_boot_key(self): self.run_command('boot --flavor 1 --image %s --key-name 1 some-server' % FAKE_UUID_1) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 0f211d0a0..7976adf88 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -409,6 +409,9 @@ def _boot(cs, args): emit_duplicated_image_with_warning(images, args.image_with) if images: image = images[0] + else: + raise exceptions.CommandError(_("No images match the property " + "expected by --image-with")) min_count = 1 max_count = 1 From 6beef56634fecc4fd7bd3bab7f3ebfd1faaf785f Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Tue, 2 Jan 2018 07:15:56 +0000 Subject: [PATCH 1393/1705] Updated from global requirements Change-Id: Ib79f8a8b2e14d1f2c7a1143b2f29ac1571f31299 --- doc/requirements.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/requirements.txt b/doc/requirements.txt index fada2eb09..91ff9ceac 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,3 +1,6 @@ +# 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. sphinx>=1.6.2 # BSD openstackdocstheme>=1.17.0 # Apache-2.0 reno>=2.5.0 # Apache-2.0 From f4009a71a0ee72346be1ad4af8f79b915367abaa Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Tue, 2 Jan 2018 10:15:46 -0500 Subject: [PATCH 1394/1705] Remove incorrect legacy QuotaSet.id property The QuotaSet.id property was added in commit d17505462db3876a781016b16464e8ed967ae50d assertin that the os-quota-set response had no id in it, but it always has, ever since the API was added in commit 6220c4276e30c633ffc4165ce6db0d120c0e88a7 in nova. This change removes the incorrect property and fixture data that returns a tenant_id in the response which the API doesn't actually do. The test_quota fixture data is also updated to fix the name on the key_pairs key and add missing keys in the response for server groups. Change-Id: Ifaf59813e75876334dcc2ac239ed6bdddb495aa7 --- novaclient/tests/unit/fixture_data/quotas.py | 10 ++++++---- novaclient/v2/quotas.py | 11 +---------- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/novaclient/tests/unit/fixture_data/quotas.py b/novaclient/tests/unit/fixture_data/quotas.py index a6931fcc6..e3d179e26 100644 --- a/novaclient/tests/unit/fixture_data/quotas.py +++ b/novaclient/tests/unit/fixture_data/quotas.py @@ -52,8 +52,8 @@ def setUp(self): def test_quota(self, tenant_id='test'): return { - 'tenant_id': tenant_id, - 'metadata_items': [], + 'id': tenant_id, + 'metadata_items': 1, 'injected_file_content_bytes': 1, 'injected_file_path_bytes': 1, 'ram': 1, @@ -61,7 +61,9 @@ def test_quota(self, tenant_id='test'): 'instances': 1, 'injected_files': 1, 'cores': 1, - 'keypairs': 1, + 'key_pairs': 1, 'security_groups': 1, - 'security_group_rules': 1 + 'security_group_rules': 1, + 'server_groups': 1, + 'server_group_members': 1 } diff --git a/novaclient/v2/quotas.py b/novaclient/v2/quotas.py index 1aee5b17e..0e421169b 100644 --- a/novaclient/v2/quotas.py +++ b/novaclient/v2/quotas.py @@ -18,15 +18,8 @@ class QuotaSet(base.Resource): - @property - def id(self): - """QuotaSet does not have a 'id' attribute but base.Resource needs it - to self-refresh and QuotaSet is indexed by tenant_id. - """ - return self.tenant_id - def update(self, *args, **kwargs): - return self.manager.update(self.tenant_id, *args, **kwargs) + return self.manager.update(self.id, *args, **kwargs) class QuotaSetManager(base.Manager): @@ -37,8 +30,6 @@ def get(self, tenant_id, user_id=None, detail=False): if detail: url += '/detail' - if hasattr(tenant_id, 'tenant_id'): - tenant_id = tenant_id.tenant_id if user_id: params = {'tenant_id': tenant_id, 'user_id': user_id} url += '?user_id=%(user_id)s' From b91ca62aea71c43f0a2ecdce3a3a3c9abd2539e0 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Thu, 11 Jan 2018 15:50:54 -0500 Subject: [PATCH 1395/1705] Fix being able to create a reno using tox -e venv Iace251446bafbd8963bc5a073de50ade583f6e46 moved the reno dependency to a new requirements file which broke the ability to create a release note using: tox -e venv -- reno new This fixes it by adding the doc/requirements into the venv tox environment. Change-Id: I243d4d5148964511a83855d2b1b451aa1543b98b --- tox.ini | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tox.ini b/tox.ini index ba4a4c141..6dbff636a 100644 --- a/tox.ini +++ b/tox.ini @@ -29,6 +29,11 @@ commands = flake8 {posargs} commands = bandit -r novaclient -n5 -x tests [testenv:venv] +deps = + -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} + -r{toxinidir}/test-requirements.txt + -r{toxinidir}/requirements.txt + -r{toxinidir}/doc/requirements.txt commands = {posargs} [testenv:docs] From 40bf06023314792f49c931c5a42b3f4e4be81b8e Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Thu, 11 Jan 2018 15:59:16 -0500 Subject: [PATCH 1396/1705] Remove deprecated MigrationManager.list cell_name kwarg The cell_name kwarg was deprecated in Pike: I54468682d5391668a513e708e26bc3c165c95ca1 And the CLI option was removed earlier in Queens (not yet released): I5d11eda2a6b35de98f0484492f597a87df882013 But that change forgot about the python API binding code so this change removes that as well and updates the release note. Change-Id: I0cf808eaf7df80e221b412d2374b81fd402bd037 Closes-Bug: #1668743 --- novaclient/tests/unit/v2/test_migrations.py | 20 ++++--------------- novaclient/v2/migrations.py | 11 +--------- novaclient/v2/shell.py | 2 +- ...ated-option-in-9.0.0-bc76629d28f1d4c4.yaml | 3 +++ 4 files changed, 9 insertions(+), 27 deletions(-) diff --git a/novaclient/tests/unit/v2/test_migrations.py b/novaclient/tests/unit/v2/test_migrations.py index 408909cda..4054012fe 100644 --- a/novaclient/tests/unit/v2/test_migrations.py +++ b/novaclient/tests/unit/v2/test_migrations.py @@ -10,8 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import mock - from novaclient import api_versions from novaclient.tests.unit import utils from novaclient.tests.unit.v2 import fakes @@ -40,33 +38,23 @@ def test_list_migrations_v223(self): self.assertIsInstance(m, migrations.Migration) self.assertEqual(m.migration_type, 'live-migration') - @mock.patch('novaclient.v2.migrations.warnings.warn') - def test_list_migrations_with_cell_name(self, mock_warn): - ml = self.cs.migrations.list(cell_name="abc") - self.assert_request_id(ml, fakes.FAKE_REQUEST_ID_LIST) - self.cs.assert_called('GET', '/os-migrations?cell_name=abc') - for m in ml: - self.assertIsInstance(m, migrations.Migration) - self.assertTrue(mock_warn.called) - def test_list_migrations_with_filters(self): - ml = self.cs.migrations.list('host1', 'finished', 'child1') + ml = self.cs.migrations.list('host1', 'finished') self.assert_request_id(ml, fakes.FAKE_REQUEST_ID_LIST) self.cs.assert_called( 'GET', - '/os-migrations?cell_name=child1&host=host1&status=finished') + '/os-migrations?host=host1&status=finished') for m in ml: self.assertIsInstance(m, migrations.Migration) def test_list_migrations_with_instance_uuid_filter(self): - ml = self.cs.migrations.list('host1', 'finished', 'child1', - 'instance_id_456') + ml = self.cs.migrations.list('host1', 'finished', 'instance_id_456') self.assert_request_id(ml, fakes.FAKE_REQUEST_ID_LIST) self.cs.assert_called( 'GET', - ('/os-migrations?cell_name=child1&host=host1&' + ('/os-migrations?host=host1&' 'instance_uuid=instance_id_456&status=finished')) self.assertEqual(1, len(ml)) self.assertEqual('instance_id_456', ml[0].instance_uuid) diff --git a/novaclient/v2/migrations.py b/novaclient/v2/migrations.py index d1562b711..10e8f37fb 100644 --- a/novaclient/v2/migrations.py +++ b/novaclient/v2/migrations.py @@ -15,9 +15,6 @@ """ from novaclient import base -from novaclient.i18n import _ - -import warnings class Migration(base.Resource): @@ -28,23 +25,17 @@ def __repr__(self): class MigrationManager(base.ManagerWithFind): resource_class = Migration - def list(self, host=None, status=None, cell_name=None, instance_uuid=None): + def list(self, host=None, status=None, instance_uuid=None): """ Get a list of migrations. :param host: (optional) filter migrations by host name. :param status: (optional) filter migrations by status. - :param cell_name: (optional) filter migrations for a cell. """ opts = {} if host: opts['host'] = host if status: opts['status'] = status - if cell_name: - warnings.warn(_("Argument 'cell_name' is " - "deprecated since Pike, and will " - "be removed in a future release.")) - opts['cell_name'] = cell_name if instance_uuid: opts['instance_uuid'] = instance_uuid diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 7976adf88..d7dba3e20 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -5212,6 +5212,6 @@ def migration_type(migration): help=_('Fetch migrations for the given status.')) def do_migration_list(cs, args): """Print a list of migrations.""" - migrations = cs.migrations.list(args.host, args.status, None, + migrations = cs.migrations.list(args.host, args.status, instance_uuid=args.instance_uuid) _print_migrations(cs, migrations) diff --git a/releasenotes/notes/remove-deprecated-option-in-9.0.0-bc76629d28f1d4c4.yaml b/releasenotes/notes/remove-deprecated-option-in-9.0.0-bc76629d28f1d4c4.yaml index 89d5afabc..a98272c80 100644 --- a/releasenotes/notes/remove-deprecated-option-in-9.0.0-bc76629d28f1d4c4.yaml +++ b/releasenotes/notes/remove-deprecated-option-in-9.0.0-bc76629d28f1d4c4.yaml @@ -6,3 +6,6 @@ upgrade: - ``--tenant`` (from ``flavor access list``) - ``--cell_name`` (from ``migration list``) - ``--volume-service-name`` (global option) + + As a result, the ``novaclient.v2.migrations.MigrationManager.list`` + python API binding method no longer takes a ``cell_name`` kwarg. From 4bc4078fcb4f2a47269882cc83e1d637db1f37c2 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Thu, 11 Jan 2018 16:25:11 -0500 Subject: [PATCH 1397/1705] Remove deprecated certs CLIs and python bindings The certs CLIs and python API bindings were deprecated in release 9.0.0 in Pike via change: If3e1e40947a8ad3f665f6a96d46de8cef6a2a190 We can safely remove them now and we'll do a major version release for this. Change-Id: I91a49b03e4d3c661ef6072962fac416f2dc37d0b --- novaclient/tests/unit/fixture_data/certs.py | 55 ---------------- novaclient/tests/unit/v2/test_certs.py | 45 -------------- novaclient/v2/certs.py | 53 ---------------- novaclient/v2/client.py | 2 - novaclient/v2/shell.py | 62 ------------------- .../notes/remove-certs-4333342189200d91.yaml | 6 ++ 6 files changed, 6 insertions(+), 217 deletions(-) delete mode 100644 novaclient/tests/unit/fixture_data/certs.py delete mode 100644 novaclient/tests/unit/v2/test_certs.py delete mode 100644 novaclient/v2/certs.py create mode 100644 releasenotes/notes/remove-certs-4333342189200d91.yaml diff --git a/novaclient/tests/unit/fixture_data/certs.py b/novaclient/tests/unit/fixture_data/certs.py deleted file mode 100644 index 5bc419e56..000000000 --- a/novaclient/tests/unit/fixture_data/certs.py +++ /dev/null @@ -1,55 +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 novaclient.tests.unit.fixture_data import base - - -class Fixture(base.Fixture): - - base_url = 'os-certificates' - - def get_os_certificates_root(self, **kw): - return ( - 200, - {}, - {'certificate': {'private_key': None, 'data': 'foo'}} - ) - - def post_os_certificates(self, **kw): - return ( - 200, - {}, - {'certificate': {'private_key': 'foo', 'data': 'bar'}} - ) - - def setUp(self): - super(Fixture, self).setUp() - - get_os_certificate = { - 'certificate': { - 'private_key': None, - 'data': 'foo' - } - } - self.requests_mock.get(self.url('root'), - json=get_os_certificate, - headers=self.json_headers) - - post_os_certificates = { - 'certificate': { - 'private_key': 'foo', - 'data': 'bar' - } - } - self.requests_mock.post(self.url(), - json=post_os_certificates, - headers=self.json_headers) diff --git a/novaclient/tests/unit/v2/test_certs.py b/novaclient/tests/unit/v2/test_certs.py deleted file mode 100644 index d7a3afa85..000000000 --- a/novaclient/tests/unit/v2/test_certs.py +++ /dev/null @@ -1,45 +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. - -import mock - -from novaclient.tests.unit.fixture_data import certs as data -from novaclient.tests.unit.fixture_data import client -from novaclient.tests.unit import utils -from novaclient.tests.unit.v2 import fakes -from novaclient.v2 import certs - - -class CertsTest(utils.FixturedTestCase): - - data_fixture_class = data.Fixture - cert_type = certs.Certificate - - scenarios = [('original', {'client_fixture_class': client.V1}), - ('session', {'client_fixture_class': client.SessionV1})] - - @mock.patch('warnings.warn') - def test_create_cert(self, mock_warn): - cert = self.cs.certs.create() - self.assert_request_id(cert, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('POST', '/os-certificates') - self.assertIsInstance(cert, self.cert_type) - self.assertEqual(1, mock_warn.call_count) - - @mock.patch('warnings.warn') - def test_get_root_cert(self, mock_warn): - cert = self.cs.certs.get() - self.assert_request_id(cert, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('GET', '/os-certificates/root') - self.assertIsInstance(cert, self.cert_type) - self.assertEqual(1, mock_warn.call_count) diff --git a/novaclient/v2/certs.py b/novaclient/v2/certs.py deleted file mode 100644 index 655af54db..000000000 --- a/novaclient/v2/certs.py +++ /dev/null @@ -1,53 +0,0 @@ -# Copyright 2010 Jacob Kaplan-Moss - -# Copyright 2011 OpenStack Foundation -# All Rights Reserved. -# -# 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. - -""" -DEPRECATED Certificate interface. -""" - -import warnings - -from novaclient import base -from novaclient.i18n import _ - -CERT_DEPRECATION_WARNING = ( - _('The nova-cert service is deprecated. This API binding will be removed ' - 'in the first major release after the Nova server 16.0.0 Pike release.') -) - - -class Certificate(base.Resource): - """DEPRECATED""" - def __repr__(self): - return ("" % - (len(self.private_key) if self.private_key else 0, - len(self.data))) - - -class CertificateManager(base.Manager): - """DEPRECATED Manage :class:`Certificate` resources.""" - resource_class = Certificate - - def create(self): - """DEPRECATED Create a x509 certificate for a user in tenant.""" - warnings.warn(CERT_DEPRECATION_WARNING, DeprecationWarning) - return self._create('/os-certificates', {}, 'certificate') - - def get(self): - """DEPRECATED Get root certificate.""" - warnings.warn(CERT_DEPRECATION_WARNING, DeprecationWarning) - return self._get("/os-certificates/root", 'certificate') diff --git a/novaclient/v2/client.py b/novaclient/v2/client.py index b85077392..54f8bd056 100644 --- a/novaclient/v2/client.py +++ b/novaclient/v2/client.py @@ -23,7 +23,6 @@ from novaclient.v2 import assisted_volume_snapshots from novaclient.v2 import availability_zones from novaclient.v2 import cells -from novaclient.v2 import certs from novaclient.v2 import cloudpipe from novaclient.v2 import contrib from novaclient.v2 import flavor_access @@ -150,7 +149,6 @@ def __init__(self, # extensions self.agents = agents.AgentsManager(self) self.cloudpipe = cloudpipe.CloudpipeManager(self) - self.certs = certs.CertificateManager(self) self.volumes = volumes.VolumeManager(self) self.keypairs = keypairs.KeypairManager(self) self.neutron = networks.NeutronManager(self) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index d7dba3e20..2d50d1164 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -48,11 +48,6 @@ logger = logging.getLogger(__name__) -CERT_DEPRECATION_WARNING = ( - _('The nova-cert service is deprecated. This command will be removed ' - 'in the first major release after the Nova server 16.0.0 Pike release.') -) - CLOUDPIPE_DEPRECATION_WARNING = ( _('The os-cloudpipe Nova API has been removed. This command will be ' 'removed in the first major release after the Nova server 16.0.0 Pike ' @@ -3157,63 +3152,6 @@ def simplify_usage(u): print(_('None')) -@utils.arg( - 'pk_filename', - metavar='', - nargs='?', - default='pk.pem', - help=_('Filename for the private key. [Default: pk.pem]')) -@utils.arg( - 'cert_filename', - metavar='', - nargs='?', - default='cert.pem', - help=_('Filename for the X.509 certificate. [Default: cert.pem]')) -def do_x509_create_cert(cs, args): - """DEPRECATED Create x509 cert for a user in tenant.""" - print(CERT_DEPRECATION_WARNING, file=sys.stderr) - - if os.path.exists(args.pk_filename): - raise exceptions.CommandError(_("Unable to write privatekey - %s " - "exists.") % args.pk_filename) - if os.path.exists(args.cert_filename): - raise exceptions.CommandError(_("Unable to write x509 cert - %s " - "exists.") % args.cert_filename) - - certs = cs.certs.create() - - try: - old_umask = os.umask(0o377) - with open(args.pk_filename, 'w') as private_key: - private_key.write(certs.private_key) - print(_("Wrote private key to %s") % args.pk_filename) - finally: - os.umask(old_umask) - - with open(args.cert_filename, 'w') as cert: - cert.write(certs.data) - print(_("Wrote x509 certificate to %s") % args.cert_filename) - - -@utils.arg( - 'filename', - metavar='', - nargs='?', - default='cacert.pem', - help=_('Filename to write the x509 root cert.')) -def do_x509_get_root_cert(cs, args): - """DEPRECATED Fetch the x509 root cert.""" - print(CERT_DEPRECATION_WARNING, file=sys.stderr) - if os.path.exists(args.filename): - raise exceptions.CommandError(_("Unable to write x509 root cert - \ - %s exists.") % args.filename) - - with open(args.filename, 'w') as cert: - cacert = cs.certs.get() - cert.write(cacert.data) - print(_("Wrote x509 root cert to %s") % args.filename) - - @utils.arg( '--hypervisor', metavar='', diff --git a/releasenotes/notes/remove-certs-4333342189200d91.yaml b/releasenotes/notes/remove-certs-4333342189200d91.yaml new file mode 100644 index 000000000..75b0f680e --- /dev/null +++ b/releasenotes/notes/remove-certs-4333342189200d91.yaml @@ -0,0 +1,6 @@ +--- +upgrade: + - | + The ``nova x509-create-cert`` and ``nova x509-get-root-cert`` commands + and ``novaclient.v2.certs`` API binding were deprecated in the 9.0.0 + release and have now been removed. From 8c73ba44714163fa99ce10ce46ff39deaeb9b9cf Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Thu, 11 Jan 2018 16:31:45 -0500 Subject: [PATCH 1398/1705] Remove deprecated cloudpipe CLIs and python API bindings These were all deprecated in the 9.0.0 release in Pike via change I329ee0e5fcf068ea7e54b99fbaf94a524647f660 and we can remove them now. This will go into 10.0.0. Change-Id: Ia90a49112847e365fcdaf581dc9ee32f9a20fd85 --- .../tests/unit/fixture_data/cloudpipe.py | 37 -------- novaclient/tests/unit/v2/fakes.py | 21 ----- novaclient/tests/unit/v2/test_cloudpipe.py | 57 ------------ novaclient/tests/unit/v2/test_shell.py | 15 ---- novaclient/v2/client.py | 2 - novaclient/v2/cloudpipe.py | 88 ------------------- novaclient/v2/shell.py | 38 -------- .../remove-cloudpipe-6c790c57dc3796eb.yaml | 6 ++ 8 files changed, 6 insertions(+), 258 deletions(-) delete mode 100644 novaclient/tests/unit/fixture_data/cloudpipe.py delete mode 100644 novaclient/tests/unit/v2/test_cloudpipe.py delete mode 100644 novaclient/v2/cloudpipe.py create mode 100644 releasenotes/notes/remove-cloudpipe-6c790c57dc3796eb.yaml diff --git a/novaclient/tests/unit/fixture_data/cloudpipe.py b/novaclient/tests/unit/fixture_data/cloudpipe.py deleted file mode 100644 index b19e24342..000000000 --- a/novaclient/tests/unit/fixture_data/cloudpipe.py +++ /dev/null @@ -1,37 +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 novaclient.tests.unit.fixture_data import base - - -class Fixture(base.Fixture): - - base_url = 'os-cloudpipe' - - def setUp(self): - super(Fixture, self).setUp() - - get_os_cloudpipe = {'cloudpipes': [{'project_id': 1}]} - self.requests_mock.get(self.url(), - json=get_os_cloudpipe, - headers=self.json_headers) - - instance_id = '9d5824aa-20e6-4b9f-b967-76a699fc51fd' - post_os_cloudpipe = {'instance_id': instance_id} - self.requests_mock.post(self.url(), - json=post_os_cloudpipe, - headers=self.json_headers, - status_code=202) - - self.requests_mock.put(self.url('configure-project'), - headers=self.json_headers, - status_code=202) diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index bdb8d6735..81947be31 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -815,27 +815,6 @@ def post_servers_1234_action(self, body, **kw): def post_servers_5678_action(self, body, **kw): return self.post_servers_1234_action(body, **kw) - # - # Cloudpipe - # - - def get_os_cloudpipe(self, **kw): - return ( - 200, - {}, - {'cloudpipes': [{'project_id': 1}]} - ) - - def post_os_cloudpipe(self, **ks): - return ( - 202, - {}, - {'instance_id': '9d5824aa-20e6-4b9f-b967-76a699fc51fd'} - ) - - def put_os_cloudpipe_configure_project(self, **kw): - return (202, {}, None) - # # Flavors # diff --git a/novaclient/tests/unit/v2/test_cloudpipe.py b/novaclient/tests/unit/v2/test_cloudpipe.py deleted file mode 100644 index 2e48d848e..000000000 --- a/novaclient/tests/unit/v2/test_cloudpipe.py +++ /dev/null @@ -1,57 +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. - -import mock -import six - -from novaclient.tests.unit.fixture_data import client -from novaclient.tests.unit.fixture_data import cloudpipe as data -from novaclient.tests.unit import utils -from novaclient.tests.unit.v2 import fakes -from novaclient.v2 import cloudpipe - - -class CloudpipeTest(utils.FixturedTestCase): - - data_fixture_class = data.Fixture - - scenarios = [('original', {'client_fixture_class': client.V1}), - ('session', {'client_fixture_class': client.SessionV1})] - - @mock.patch('warnings.warn') - def test_list_cloudpipes(self, mock_warn): - cp = self.cs.cloudpipe.list() - self.assert_request_id(cp, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('GET', '/os-cloudpipe') - for c in cp: - self.assertIsInstance(c, cloudpipe.Cloudpipe) - mock_warn.assert_called_once_with(mock.ANY) - - @mock.patch('warnings.warn') - def test_create(self, mock_warn): - project = "test" - cp = self.cs.cloudpipe.create(project) - self.assert_request_id(cp, fakes.FAKE_REQUEST_ID_LIST) - body = {'cloudpipe': {'project_id': project}} - self.assert_called('POST', '/os-cloudpipe', body) - self.assertIsInstance(cp, six.string_types) - mock_warn.assert_called_once_with(mock.ANY) - - @mock.patch('warnings.warn') - def test_update(self, mock_warn): - cp = self.cs.cloudpipe.update("192.168.1.1", 2345) - self.assert_request_id(cp, fakes.FAKE_REQUEST_ID_LIST) - body = {'configure_project': {'vpn_ip': "192.168.1.1", - 'vpn_port': 2345}} - self.assert_called('PUT', '/os-cloudpipe/configure-project', body) - mock_warn.assert_called_once_with(mock.ANY) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 30b695e88..f1e5a9cd2 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2768,21 +2768,6 @@ def test_quota_class_update(self): 'PUT', '/os-quota-class-sets/97f4c221bff44578b0300df4ef119353', body) - def test_cloudpipe_list(self): - self.run_command('cloudpipe-list') - self.assert_called('GET', '/os-cloudpipe') - - def test_cloudpipe_create(self): - self.run_command('cloudpipe-create myproject') - body = {'cloudpipe': {'project_id': "myproject"}} - self.assert_called('POST', '/os-cloudpipe', body) - - def test_cloudpipe_configure(self): - self.run_command('cloudpipe-configure 192.168.1.1 1234') - body = {'configure_project': {'vpn_ip': "192.168.1.1", - 'vpn_port': '1234'}} - self.assert_called('PUT', '/os-cloudpipe/configure-project', body) - def test_add_fixed_ip(self): _, err = self.run_command('add-fixed-ip sample-server 1') self.assertIn('WARNING: Command add-fixed-ip is deprecated', err) diff --git a/novaclient/v2/client.py b/novaclient/v2/client.py index 54f8bd056..5a94275e6 100644 --- a/novaclient/v2/client.py +++ b/novaclient/v2/client.py @@ -23,7 +23,6 @@ from novaclient.v2 import assisted_volume_snapshots from novaclient.v2 import availability_zones from novaclient.v2 import cells -from novaclient.v2 import cloudpipe from novaclient.v2 import contrib from novaclient.v2 import flavor_access from novaclient.v2 import flavors @@ -148,7 +147,6 @@ def __init__(self, # extensions self.agents = agents.AgentsManager(self) - self.cloudpipe = cloudpipe.CloudpipeManager(self) self.volumes = volumes.VolumeManager(self) self.keypairs = keypairs.KeypairManager(self) self.neutron = networks.NeutronManager(self) diff --git a/novaclient/v2/cloudpipe.py b/novaclient/v2/cloudpipe.py deleted file mode 100644 index b9ac9f65a..000000000 --- a/novaclient/v2/cloudpipe.py +++ /dev/null @@ -1,88 +0,0 @@ -# Copyright 2012 OpenStack Foundation -# All Rights Reserved. -# -# 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. - -"""DEPRECATED Cloudpipe interface.""" - -import warnings - -from novaclient import base -from novaclient.i18n import _ - - -DEPRECATION_WARNING = ( - _('The os-cloudpipe Nova API has been removed. This API binding will be ' - 'removed in the first major release after the Nova server 16.0.0 Pike ' - 'release.') -) - - -class Cloudpipe(base.Resource): - """A cloudpipe instance is a VPN attached to a project's VLAN.""" - - def __repr__(self): - return "" % self.project_id - - def delete(self): - """ - DEPRECATED Delete the own cloudpipe instance - - :returns: An instance of novaclient.base.TupleWithMeta - """ - - warnings.warn(DEPRECATION_WARNING) - - return self.manager.delete(self) - - -class CloudpipeManager(base.ManagerWithFind): - """DEPRECATED""" - - resource_class = Cloudpipe - - def create(self, project): - """DEPRECATED Launch a cloudpipe instance. - - :param project: UUID of the project (tenant) for the cloudpipe - """ - - warnings.warn(DEPRECATION_WARNING) - - body = {'cloudpipe': {'project_id': project}} - return self._create('/os-cloudpipe', body, 'instance_id', - return_raw=True) - - def list(self): - """DEPRECATED Get a list of cloudpipe instances.""" - - warnings.warn(DEPRECATION_WARNING) - - return self._list('/os-cloudpipe', 'cloudpipes') - - def update(self, address, port): - """DEPRECATED Configure cloudpipe parameters for the project. - - Update VPN address and port for all networks associated - with the project defined by authentication - - :param address: IP address - :param port: Port number - :returns: An instance of novaclient.base.TupleWithMeta - """ - - warnings.warn(DEPRECATION_WARNING) - - body = {'configure_project': {'vpn_ip': address, - 'vpn_port': port}} - return self._update("/os-cloudpipe/configure-project", body) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 2d50d1164..d1f1bfadc 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -48,12 +48,6 @@ logger = logging.getLogger(__name__) -CLOUDPIPE_DEPRECATION_WARNING = ( - _('The os-cloudpipe Nova API has been removed. This command will be ' - 'removed in the first major release after the Nova server 16.0.0 Pike ' - 'release.') -) - # NOTE(mriedem): Remove this along with the deprecated commands in the first # major python-novaclient release AFTER the nova server 16.0.0 Pike release. @@ -923,38 +917,6 @@ def do_boot(cs, args): _poll_for_status(cs.servers.get, server.id, 'building', ['active']) -def do_cloudpipe_list(cs, _args): - """DEPRECATED Print a list of all cloudpipe instances.""" - - print(CLOUDPIPE_DEPRECATION_WARNING, file=sys.stderr) - - cloudpipes = cs.cloudpipe.list() - columns = ['Project Id', "Public IP", "Public Port", "Internal IP"] - utils.print_list(cloudpipes, columns) - - -@utils.arg( - 'project', - metavar='', - help=_('UUID of the project to create the cloudpipe for.')) -def do_cloudpipe_create(cs, args): - """DEPRECATED Create a cloudpipe instance for the given project.""" - - print(CLOUDPIPE_DEPRECATION_WARNING, file=sys.stderr) - - cs.cloudpipe.create(args.project) - - -@utils.arg('address', metavar='', help=_('New IP Address.')) -@utils.arg('port', metavar='', help=_('New Port.')) -def do_cloudpipe_configure(cs, args): - """DEPRECATED Update the VPN IP/port of a cloudpipe instance.""" - - print(CLOUDPIPE_DEPRECATION_WARNING, file=sys.stderr) - - cs.cloudpipe.update(args.address, args.port) - - def _poll_for_status(poll_fn, obj_id, action, final_ok_states, poll_period=5, show_progress=True, status_field="status", silent=False): diff --git a/releasenotes/notes/remove-cloudpipe-6c790c57dc3796eb.yaml b/releasenotes/notes/remove-cloudpipe-6c790c57dc3796eb.yaml new file mode 100644 index 000000000..4ee7e8136 --- /dev/null +++ b/releasenotes/notes/remove-cloudpipe-6c790c57dc3796eb.yaml @@ -0,0 +1,6 @@ +--- +upgrade: + - | + The deprecated ``nova cloudpipe-list``, ``nova cloudpipe-create``, and + ``nova cloudpipe-configure`` commands and the ``novaclient.v2.cloudpipe`` + API bindings have been removed. From 1d88717e751e743ca7edd978346b95b6e7b3f235 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Thu, 11 Jan 2018 16:59:20 -0500 Subject: [PATCH 1399/1705] Remove deprecated os-hosts CLIs and python API bindings These were all deprecated in the 9.0.0 release in Pike via change I79091edf5a2569e49e79deba312456fdcdee09e1 and can now be removed. This will go into the 10.0.0 release. Change-Id: I85a287ff2666c2dcdcbbc8fd6c2e285176f7a67a --- .../v2/legacy/test_readonly_nova.py | 3 - novaclient/tests/unit/fixture_data/hosts.py | 144 ----------------- novaclient/tests/unit/v2/fakes.py | 36 +---- novaclient/tests/unit/v2/test_hosts.py | 148 ------------------ novaclient/tests/unit/v2/test_shell.py | 60 ------- novaclient/v2/client.py | 2 - novaclient/v2/hosts.py | 112 ------------- novaclient/v2/shell.py | 89 ----------- .../notes/remove-hosts-d08855550c40b9c6.yaml | 13 ++ 9 files changed, 14 insertions(+), 593 deletions(-) delete mode 100644 novaclient/tests/unit/fixture_data/hosts.py delete mode 100644 novaclient/tests/unit/v2/test_hosts.py delete mode 100644 novaclient/v2/hosts.py create mode 100644 releasenotes/notes/remove-hosts-d08855550c40b9c6.yaml diff --git a/novaclient/tests/functional/v2/legacy/test_readonly_nova.py b/novaclient/tests/functional/v2/legacy/test_readonly_nova.py index bf1ebe4c0..df88b8c6d 100644 --- a/novaclient/tests/functional/v2/legacy/test_readonly_nova.py +++ b/novaclient/tests/functional/v2/legacy/test_readonly_nova.py @@ -51,9 +51,6 @@ def test_admin_flavor_acces_list(self): def test_admin_flavor_list(self): self.assertIn("Memory_MB", self.nova('flavor-list')) - def test_admin_host_list(self): - self.nova('host-list') - def test_admin_hypervisor_list(self): self.nova('hypervisor-list') diff --git a/novaclient/tests/unit/fixture_data/hosts.py b/novaclient/tests/unit/fixture_data/hosts.py deleted file mode 100644 index c95cddeba..000000000 --- a/novaclient/tests/unit/fixture_data/hosts.py +++ /dev/null @@ -1,144 +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 novaclient.tests.unit.fixture_data import base - - -class BaseFixture(base.Fixture): - - base_url = 'os-hosts' - - def setUp(self): - super(BaseFixture, self).setUp() - - get_os_hosts_host = { - 'host': [ - {'resource': {'project': '(total)', 'host': 'dummy', - 'cpu': 16, 'memory_mb': 32234, 'disk_gb': 128}}, - {'resource': {'project': '(used_now)', 'host': 'dummy', - 'cpu': 1, 'memory_mb': 2075, 'disk_gb': 45}}, - {'resource': {'project': '(used_max)', 'host': 'dummy', - 'cpu': 1, 'memory_mb': 2048, 'disk_gb': 30}}, - {'resource': {'project': 'admin', 'host': 'dummy', - 'cpu': 1, 'memory_mb': 2048, 'disk_gb': 30}} - ] - } - - headers = self.json_headers - - self.requests_mock.get(self.url('host'), - json=get_os_hosts_host, - headers=headers) - - def get_os_hosts(request, context): - zone = 'nova1' - service = None - - if request.query: - try: - zone = request.qs['zone'][0] - except Exception: - pass - - try: - service = request.qs['service'][0] - except Exception: - pass - - return { - 'hosts': [ - { - 'host_name': 'host1', - 'service': service or 'nova-compute', - 'zone': zone - }, - { - 'host_name': 'host1', - 'service': service or 'nova-cert', - 'zone': zone - } - ] - } - - self.requests_mock.get(self.url(), - json=get_os_hosts, - headers=headers) - - get_os_hosts_sample_host = { - 'host': [ - {'resource': {'host': 'sample_host'}} - ], - } - self.requests_mock.get(self.url('sample_host'), - json=get_os_hosts_sample_host, - headers=headers) - - self.requests_mock.put(self.url('sample_host', 1), - json=self.put_host_1(), - headers=headers) - - self.requests_mock.put(self.url('sample_host', 2), - json=self.put_host_2(), - headers=headers) - - self.requests_mock.put(self.url('sample_host', 3), - json=self.put_host_3(), - headers=headers) - - self.requests_mock.get(self.url('sample_host', 'reboot'), - json=self.get_host_reboot(), - headers=headers) - - self.requests_mock.get(self.url('sample_host', 'startup'), - json=self.get_host_startup(), - headers=headers) - - self.requests_mock.get(self.url('sample_host', 'shutdown'), - json=self.get_host_shutdown(), - headers=headers) - - def put_os_hosts_sample_host(request, context): - result = {'host': 'dummy'} - result.update(request.json()) - return result - - self.requests_mock.put(self.url('sample_host'), - json=put_os_hosts_sample_host, - headers=headers) - - -class V1(BaseFixture): - - def put_host_1(self): - return {'host': 'sample-host_1', - 'status': 'enabled'} - - def put_host_2(self): - return {'host': 'sample-host_2', - 'maintenance_mode': 'on_maintenance'} - - def put_host_3(self): - return {'host': 'sample-host_3', - 'status': 'enabled', - 'maintenance_mode': 'on_maintenance'} - - def get_host_reboot(self): - return {'host': 'sample_host', - 'power_action': 'reboot'} - - def get_host_startup(self): - return {'host': 'sample_host', - 'power_action': 'startup'} - - def get_host_shutdown(self): - return {'host': 'sample_host', - 'power_action': 'shutdown'} diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 81947be31..ce36694e9 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -1672,43 +1672,9 @@ def put_os_services_force_down(self, body, **kw): 'forced_down': False}}) # - # Hosts + # Hypervisors # - def get_os_hosts(self, **kw): - zone = kw.get('zone', 'nova1') - return (200, {}, {'hosts': [{'host': 'host1', - 'service': 'nova-compute', - 'zone': zone}, - {'host': 'host1', - 'service': 'nova-cert', - 'zone': zone}]}) - - def put_os_hosts_sample_host_1(self, body, **kw): - return (200, {}, {'host': 'sample-host_1', - 'status': 'enabled'}) - - def put_os_hosts_sample_host_2(self, body, **kw): - return (200, {}, {'host': 'sample-host_2', - 'maintenance_mode': 'on_maintenance'}) - - def put_os_hosts_sample_host_3(self, body, **kw): - return (200, {}, {'host': 'sample-host_3', - 'status': 'enabled', - 'maintenance_mode': 'on_maintenance'}) - - def get_os_hosts_sample_host_reboot(self, **kw): - return (200, {}, {'host': 'sample_host', - 'power_action': 'reboot'}) - - def get_os_hosts_sample_host_startup(self, **kw): - return (200, {}, {'host': 'sample_host', - 'power_action': 'startup'}) - - def get_os_hosts_sample_host_shutdown(self, **kw): - return (200, {}, {'host': 'sample_host', - 'power_action': 'shutdown'}) - def get_os_hypervisors(self, **kw): return (200, {}, { "hypervisors": [ diff --git a/novaclient/tests/unit/v2/test_hosts.py b/novaclient/tests/unit/v2/test_hosts.py deleted file mode 100644 index a44a5e01e..000000000 --- a/novaclient/tests/unit/v2/test_hosts.py +++ /dev/null @@ -1,148 +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. - -import mock - -from novaclient import api_versions -from novaclient import exceptions -from novaclient.tests.unit.fixture_data import client -from novaclient.tests.unit.fixture_data import hosts as data -from novaclient.tests.unit import utils -from novaclient.tests.unit.v2 import fakes -from novaclient.v2 import hosts - - -class HostsTest(utils.FixturedTestCase): - - client_fixture_class = client.V1 - data_fixture_class = data.V1 - - def setUp(self): - super(HostsTest, self).setUp() - self.warning_mock = mock.patch('warnings.warn').start() - self.addCleanup(self.warning_mock.stop) - - def test_describe_resource(self): - hs = self.cs.hosts.get('host') - self.warning_mock.assert_called_once() - self.assert_request_id(hs, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('GET', '/os-hosts/host') - for h in hs: - self.assertIsInstance(h, hosts.Host) - - def test_list_host(self): - hs = self.cs.hosts.list() - self.warning_mock.assert_called_once() - self.assert_request_id(hs, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('GET', '/os-hosts') - for h in hs: - self.assertIsInstance(h, hosts.Host) - self.assertEqual(h.zone, 'nova1') - - def test_list_host_with_zone(self): - hs = self.cs.hosts.list('nova') - self.assert_request_id(hs, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('GET', '/os-hosts?zone=nova') - for h in hs: - self.assertIsInstance(h, hosts.Host) - self.assertEqual(h.zone, 'nova') - - def test_update_enable(self): - host = self.cs.hosts.get('sample_host')[0] - values = {"status": "enabled"} - result = host.update(values) - # one warning for the get, one warning for the update - self.assertEqual(2, self.warning_mock.call_count) - self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('PUT', '/os-hosts/sample_host', values) - self.assertIsInstance(result, hosts.Host) - - def test_update_maintenance(self): - host = self.cs.hosts.get('sample_host')[0] - values = {"maintenance_mode": "enable"} - result = host.update(values) - self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('PUT', '/os-hosts/sample_host', values) - self.assertIsInstance(result, hosts.Host) - - def test_update_both(self): - host = self.cs.hosts.get('sample_host')[0] - values = {"status": "enabled", - "maintenance_mode": "enable"} - result = host.update(values) - self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('PUT', '/os-hosts/sample_host', values) - self.assertIsInstance(result, hosts.Host) - - def test_host_startup(self): - host = self.cs.hosts.get('sample_host')[0] - result = host.startup() - # one warning for the get, one warning for the action - self.assertEqual(2, self.warning_mock.call_count) - self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called( - 'GET', '/os-hosts/sample_host/startup') - - def test_host_reboot(self): - host = self.cs.hosts.get('sample_host')[0] - result = host.reboot() - # one warning for the get, one warning for the action - self.assertEqual(2, self.warning_mock.call_count) - self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called( - 'GET', '/os-hosts/sample_host/reboot') - - def test_host_shutdown(self): - host = self.cs.hosts.get('sample_host')[0] - result = host.shutdown() - # one warning for the get, one warning for the action - self.assertEqual(2, self.warning_mock.call_count) - self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called( - 'GET', '/os-hosts/sample_host/shutdown') - - def test_hosts_repr(self): - hs = self.cs.hosts.get('host') - self.assertEqual('', repr(hs[0])) - - def test_hosts_list_repr(self): - hs = self.cs.hosts.list() - for h in hs: - self.assertEqual('' % h.host_name, repr(h)) - - -class DeprecatedHostsTestv2_43(utils.FixturedTestCase): - """Tests the os-hosts API bindings at microversion 2.43 to ensure - they fail with a 404 error. - """ - client_fixture_class = client.V1 - - def setUp(self): - super(DeprecatedHostsTestv2_43, self).setUp() - self.cs.api_version = api_versions.APIVersion('2.43') - - def test_get(self): - self.assertRaises(exceptions.VersionNotFoundForAPIMethod, - self.cs.hosts.get, 'host') - - def test_list(self): - self.assertRaises(exceptions.VersionNotFoundForAPIMethod, - self.cs.hosts.list) - - def test_update(self): - self.assertRaises(exceptions.VersionNotFoundForAPIMethod, - self.cs.hosts.update, 'host', {"status": "enabled"}) - - def test_host_action(self): - self.assertRaises(exceptions.VersionNotFoundForAPIMethod, - self.cs.hosts.host_action, 'host', 'reboot') diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index f1e5a9cd2..a6c0ae4a9 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2454,66 +2454,6 @@ def test_services_delete_v2_53(self): self.assert_called( 'DELETE', '/os-services/%s' % fakes.FAKE_SERVICE_UUID_1) - def test_host_list(self): - _, err = self.run_command('host-list') - # make sure we said it's deprecated - self.assertIn('WARNING: Command host-list is deprecated', err) - # and replaced with hypervisor-list - self.assertIn('hypervisor-list', err) - self.assert_called('GET', '/os-hosts') - - def test_host_list_with_zone(self): - self.run_command('host-list --zone nova') - self.assert_called('GET', '/os-hosts?zone=nova') - - def test_host_update_status(self): - _, err = self.run_command('host-update sample-host_1 --status enable') - # make sure we said it's deprecated - self.assertIn('WARNING: Command host-update is deprecated', err) - # and replaced with service-enable - self.assertIn('service-enable', err) - body = {'status': 'enable'} - self.assert_called('PUT', '/os-hosts/sample-host_1', body) - - def test_host_update_maintenance(self): - _, err = ( - self.run_command('host-update sample-host_2 --maintenance enable')) - # make sure we said it's deprecated - self.assertIn('WARNING: Command host-update is deprecated', err) - # and there is no replacement - self.assertIn('There is no replacement', err) - body = {'maintenance_mode': 'enable'} - self.assert_called('PUT', '/os-hosts/sample-host_2', body) - - def test_host_update_multiple_settings(self): - _, err = self.run_command('host-update sample-host_3 ' - '--status disable --maintenance enable') - # make sure we said it's deprecated - self.assertIn('WARNING: Command host-update is deprecated', err) - # and replaced with service-disable - self.assertIn('service-disable', err) - body = {'status': 'disable', 'maintenance_mode': 'enable'} - self.assert_called('PUT', '/os-hosts/sample-host_3', body) - - def test_host_startup(self): - _, err = self.run_command('host-action sample-host --action startup') - # make sure we said it's deprecated - self.assertIn('WARNING: Command host-action is deprecated', err) - # and there is no replacement - self.assertIn('There is no replacement', err) - self.assert_called( - 'GET', '/os-hosts/sample-host/startup') - - def test_host_shutdown(self): - self.run_command('host-action sample-host --action shutdown') - self.assert_called( - 'GET', '/os-hosts/sample-host/shutdown') - - def test_host_reboot(self): - self.run_command('host-action sample-host --action reboot') - self.assert_called( - 'GET', '/os-hosts/sample-host/reboot') - def test_host_evacuate_v2_14(self): self.run_command('host-evacuate hyper --target target_hyper', api_version='2.14') diff --git a/novaclient/v2/client.py b/novaclient/v2/client.py index 5a94275e6..4d02b280a 100644 --- a/novaclient/v2/client.py +++ b/novaclient/v2/client.py @@ -26,7 +26,6 @@ from novaclient.v2 import contrib from novaclient.v2 import flavor_access from novaclient.v2 import flavors -from novaclient.v2 import hosts from novaclient.v2 import hypervisors from novaclient.v2 import images from novaclient.v2 import instance_action @@ -156,7 +155,6 @@ def __init__(self, self.virtual_interfaces = \ virtual_interfaces.VirtualInterfaceManager(self) self.aggregates = aggregates.AggregateManager(self) - self.hosts = hosts.HostManager(self) self.hypervisors = hypervisors.HypervisorManager(self) self.hypervisor_stats = hypervisors.HypervisorStatsManager(self) self.services = services.ServiceManager(self) diff --git a/novaclient/v2/hosts.py b/novaclient/v2/hosts.py deleted file mode 100644 index be7ca58e4..000000000 --- a/novaclient/v2/hosts.py +++ /dev/null @@ -1,112 +0,0 @@ -# Copyright 2011 OpenStack Foundation -# All Rights Reserved. -# -# 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. - -""" -DEPRECATED host interface -""" -import warnings - -from novaclient import api_versions -from novaclient import base -from novaclient.i18n import _ - - -HOSTS_DEPRECATION_WARNING = ( - _('The os-hosts API is deprecated. This API binding will be removed ' - 'in the first major release after the Nova server 16.0.0 Pike release.') -) - - -class Host(base.Resource): - """DEPRECATED""" - def __repr__(self): - return "" % self.host - - def _add_details(self, info): - dico = 'resource' in info and info['resource'] or info - for (k, v) in dico.items(): - setattr(self, k, v) - - @api_versions.wraps("2.0", "2.42") - def update(self, values): - return self.manager.update(self.host, values) - - @api_versions.wraps("2.0", "2.42") - def startup(self): - return self.manager.host_action(self.host, 'startup') - - @api_versions.wraps("2.0", "2.42") - def shutdown(self): - return self.manager.host_action(self.host, 'shutdown') - - @api_versions.wraps("2.0", "2.42") - def reboot(self): - return self.manager.host_action(self.host, 'reboot') - - @property - def host_name(self): - return self.host - - @host_name.setter - def host_name(self, value): - # A host from hosts.list() has the attribute "host_name" instead of - # "host." This sets "host" if that's the case. Even though it doesn't - # exactly mirror the response format, it enables users to work with - # host objects from list and non-list operations interchangeably. - self.host = value - - -class HostManager(base.ManagerWithFind): - resource_class = Host - - @api_versions.wraps("2.0", "2.42") - def get(self, host): - """ - DEPRECATED Describes cpu/memory/hdd info for host. - - :param host: destination host name. - """ - warnings.warn(HOSTS_DEPRECATION_WARNING, DeprecationWarning) - return self._list("/os-hosts/%s" % host, "host") - - @api_versions.wraps("2.0", "2.42") - def update(self, host, values): - """DEPRECATED Update status or maintenance mode for the host.""" - warnings.warn(HOSTS_DEPRECATION_WARNING, DeprecationWarning) - return self._update("/os-hosts/%s" % host, values) - - @api_versions.wraps("2.0", "2.42") - def host_action(self, host, action): - """ - DEPRECATED Perform an action on a host. - - :param host: The host to perform an action - :param action: The action to perform - :returns: An instance of novaclient.base.TupleWithMeta - """ - warnings.warn(HOSTS_DEPRECATION_WARNING, DeprecationWarning) - url = '/os-hosts/%s/%s' % (host, action) - resp, body = self.api.client.get(url) - return base.TupleWithMeta((resp, body), resp) - - @api_versions.wraps("2.0", "2.42") - def list(self, zone=None): - warnings.warn(HOSTS_DEPRECATION_WARNING, DeprecationWarning) - url = '/os-hosts' - if zone: - url = '/os-hosts?zone=%s' % zone - return self._list(url, "hosts") - - list_all = list diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index d1f1bfadc..ca6a54e28 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -49,26 +49,6 @@ logger = logging.getLogger(__name__) -# NOTE(mriedem): Remove this along with the deprecated commands in the first -# major python-novaclient release AFTER the nova server 16.0.0 Pike release. -def emit_hosts_deprecation_warning(command_name, replacement=None): - if replacement is None: - print(_('WARNING: Command %s is deprecated and will be removed ' - 'in the first major release after the Nova server 16.0.0 ' - 'Pike release. There is no replacement or alternative for ' - 'this command. Specify --os-compute-api-version less than ' - '2.43 to continue using this command until it is removed.') % - command_name, file=sys.stderr) - else: - print(_('WARNING: Command %(command)s is deprecated and will be ' - 'removed in the first major release after the Nova server ' - '16.0.0 Pike release. Use %(replacement)s instead. Specify ' - '--os-compute-api-version less than 2.43 to continue using ' - 'this command until it is removed.') % - {'command': command_name, 'replacement': replacement}, - file=sys.stderr) - - # NOTE(mriedem): Remove this along with the deprecated commands in the first # major python-novaclient release AFTER the nova server 16.0.0 Pike release. def emit_fixed_floating_deprecation_warning(command_name): @@ -3598,75 +3578,6 @@ def do_service_delete(cs, args): cs.services.delete(args.id) -@utils.arg('host', metavar='', help=_('Name of host.')) -def do_host_describe(cs, args): - """DEPRECATED Describe a specific host.""" - emit_hosts_deprecation_warning('host-describe', 'hypervisor-show') - - result = cs.hosts.get(args.host) - columns = ["HOST", "PROJECT", "cpu", "memory_mb", "disk_gb"] - utils.print_list(result, columns) - - -@utils.arg( - '--zone', - metavar='', - default=None, - help=_('Filters the list, returning only those hosts in the availability ' - 'zone .')) -def do_host_list(cs, args): - """DEPRECATED List all hosts by service.""" - emit_hosts_deprecation_warning('host-list', 'hypervisor-list') - - columns = ["host_name", "service", "zone"] - result = cs.hosts.list(args.zone) - utils.print_list(result, columns) - - -@utils.arg('host', metavar='', help=_('Name of host.')) -@utils.arg( - '--status', metavar='', default=None, dest='status', - help=_('Either enable or disable a host.')) -@utils.arg( - '--maintenance', - metavar='', - default=None, - dest='maintenance', - help=_('Either put or resume host to/from maintenance.')) -def do_host_update(cs, args): - """DEPRECATED Update host settings.""" - if args.status == 'enable': - emit_hosts_deprecation_warning('host-update', 'service-enable') - elif args.status == 'disable': - emit_hosts_deprecation_warning('host-update', 'service-disable') - else: - emit_hosts_deprecation_warning('host-update') - - updates = {} - columns = ["HOST"] - if args.status: - updates['status'] = args.status - columns.append("status") - if args.maintenance: - updates['maintenance_mode'] = args.maintenance - columns.append("maintenance_mode") - result = cs.hosts.update(args.host, updates) - utils.print_list([result], columns) - - -@utils.arg('host', metavar='', help=_('Name of host.')) -@utils.arg( - '--action', metavar='', dest='action', - choices=['startup', 'shutdown', 'reboot'], - help=_('A power action: startup, reboot, or shutdown.')) -def do_host_action(cs, args): - """DEPRECATED Perform a power action on a host.""" - emit_hosts_deprecation_warning('host-action') - - result = cs.hosts.host_action(args.host, args.action) - utils.print_list([result], ['HOST', 'power_action']) - - def _find_hypervisor(cs, hypervisor): """Get a hypervisor by name or ID.""" return utils.find_resource(cs.hypervisors, hypervisor) diff --git a/releasenotes/notes/remove-hosts-d08855550c40b9c6.yaml b/releasenotes/notes/remove-hosts-d08855550c40b9c6.yaml new file mode 100644 index 000000000..06f466d44 --- /dev/null +++ b/releasenotes/notes/remove-hosts-d08855550c40b9c6.yaml @@ -0,0 +1,13 @@ +--- +upgrade: + - | + The following CLIs and their backing API bindings were deprecated and + capped at microversion 2.43: + + * ``nova host-describe`` - superseded by ``nova hypervisor-show`` + * ``nova host-list`` - superseded by ``nova hypervisor-list`` + * ``nova host-update`` - superseded by ``nova service-enable`` and + ``nova service-disable`` + * ``nova host-action`` - no alternative by design + + The CLIs and API bindings have now been removed. From 01fb16533bf562f39fe822bc12b9cc34b8580359 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Thu, 11 Jan 2018 17:19:48 -0500 Subject: [PATCH 1400/1705] Remove deprecated fixedip/floatingip/virtual interface CLIs/APIs The add-fixed-ip, remove-fixed-ip, floating-ip-associate, floating-ip-disassociate, and virtual-interface-list CLIs and related python API bindings were deprecated in the 9.0.0 release in Pike via change Ie76283962c375b735f30ccb3053db07cf2330de2. This removes the CLIs and related python API bindings and will go into the 10.0.0 release. Change-Id: Icb667973c65d26395db660b1c7b919550db66d08 --- novaclient/tests/unit/v2/fakes.py | 15 --- novaclient/tests/unit/v2/test_servers.py | 104 ----------------- novaclient/tests/unit/v2/test_shell.py | 35 +----- novaclient/v2/client.py | 3 - novaclient/v2/servers.py | 108 ------------------ novaclient/v2/shell.py | 90 --------------- novaclient/v2/virtual_interfaces.py | 44 ------- ...dd-rm-fixed-floating-398c905d9c91cca8.yaml | 15 +++ 8 files changed, 16 insertions(+), 398 deletions(-) delete mode 100644 novaclient/v2/virtual_interfaces.py create mode 100644 releasenotes/notes/remove-virt-interfaces-add-rm-fixed-floating-398c905d9c91cca8.yaml diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index ce36694e9..9b059a38a 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -704,13 +704,6 @@ def check_server_actions(cls, body): assert 'flavorRef' in body[action] elif action in cls.none_actions: assert body[action] is None - elif action == 'addFixedIp': - assert list(body[action]) == ['networkId'] - elif action in ['removeFixedIp', 'removeFloatingIp']: - assert list(body[action]) == ['address'] - elif action == 'addFloatingIp': - assert (list(body[action]) == ['address'] or - sorted(list(body[action])) == ['address', 'fixed_address']) elif action == 'changePassword': assert list(body[action]) == ['adminPass'] elif action in cls.type_actions: @@ -1134,14 +1127,6 @@ def post_os_keypairs(self, body, **kw): r = {'keypair': self.get_os_keypairs()[2]['keypairs'][0]['keypair']} return (202, {}, r) - # - # Virtual Interfaces - # - def get_servers_1234_os_virtual_interfaces(self, **kw): - return (200, {}, {"virtual_interfaces": [ - {'id': 'fakeid', 'mac_address': 'fakemac'} - ]}) - # # Quotas # diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index fb18261b3..b09bf6cdc 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -20,7 +20,6 @@ import six from novaclient import api_versions -from novaclient import base from novaclient import exceptions from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit.fixture_data import floatingips @@ -30,21 +29,6 @@ from novaclient.v2 import servers -class _FloatingIPManager(base.Manager): - resource_class = base.Resource - - @api_versions.deprecated_after('2.35') - def list(self): - """DEPRECATED: List floating IPs""" - return self._list("/os-floating-ips", "floating_ips") - - @api_versions.deprecated_after('2.35') - def get(self, floating_ip): - """DEPRECATED: Retrieve a floating IP""" - return self._get("/os-floating-ips/%s" % base.getid(floating_ip), - "floating_ip") - - class ServersTest(utils.FixturedTestCase): client_fixture_class = client.V1 @@ -56,7 +40,6 @@ def setUp(self): self.useFixture(floatingips.FloatingFixture(self.requests_mock)) if self.api_version: self.cs.api_version = api_versions.APIVersion(self.api_version) - self.floating_ips = _FloatingIPManager(self.cs) def _get_server_create_default_nics(self): """Callback for default nics kwarg when creating a server. @@ -567,81 +550,6 @@ def test_migrate_server(self): self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') - @mock.patch('warnings.warn') - def test_add_fixed_ip(self, mock_warn): - s = self.cs.servers.get(1234) - fip = s.add_fixed_ip(1) - mock_warn.assert_called_once() - self.assert_request_id(fip, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('POST', '/servers/1234/action') - fip = self.cs.servers.add_fixed_ip(s, 1) - self.assert_request_id(fip, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('POST', '/servers/1234/action') - - @mock.patch('warnings.warn') - def test_remove_fixed_ip(self, mock_warn): - s = self.cs.servers.get(1234) - ret = s.remove_fixed_ip('10.0.0.1') - mock_warn.assert_called_once() - self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('POST', '/servers/1234/action') - ret = self.cs.servers.remove_fixed_ip(s, '10.0.0.1') - self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('POST', '/servers/1234/action') - - @mock.patch('warnings.warn') - def test_add_floating_ip(self, mock_warn): - s = self.cs.servers.get(1234) - fip = s.add_floating_ip('11.0.0.1') - mock_warn.assert_called_once() - self.assert_request_id(fip, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('POST', '/servers/1234/action') - fip = self.cs.servers.add_floating_ip(s, '11.0.0.1') - self.assert_request_id(fip, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('POST', '/servers/1234/action') - f = self.floating_ips.list()[0] - fip = self.cs.servers.add_floating_ip(s, f) - self.assert_request_id(fip, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('POST', '/servers/1234/action') - fip = s.add_floating_ip(f) - self.assert_request_id(fip, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('POST', '/servers/1234/action') - - def test_add_floating_ip_to_fixed(self): - s = self.cs.servers.get(1234) - fip = s.add_floating_ip('11.0.0.1', fixed_address='12.0.0.1') - self.assert_request_id(fip, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('POST', '/servers/1234/action') - fip = self.cs.servers.add_floating_ip(s, '11.0.0.1', - fixed_address='12.0.0.1') - self.assert_request_id(fip, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('POST', '/servers/1234/action') - f = self.floating_ips.list()[0] - fip = self.cs.servers.add_floating_ip(s, f) - self.assert_request_id(fip, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('POST', '/servers/1234/action') - fip = s.add_floating_ip(f) - self.assert_request_id(fip, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('POST', '/servers/1234/action') - - @mock.patch('warnings.warn') - def test_remove_floating_ip(self, mock_warn): - s = self.cs.servers.get(1234) - ret = s.remove_floating_ip('11.0.0.1') - mock_warn.assert_called_once() - self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('POST', '/servers/1234/action') - ret = self.cs.servers.remove_floating_ip(s, '11.0.0.1') - self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('POST', '/servers/1234/action') - f = self.floating_ips.list()[0] - ret = self.cs.servers.remove_floating_ip(s, f) - self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('POST', '/servers/1234/action') - ret = s.remove_floating_ip(f) - self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('POST', '/servers/1234/action') - def test_stop(self): s = self.cs.servers.get(1234) ret = s.stop() @@ -1421,18 +1329,6 @@ def test_create_server_with_nics_auto(self): self.assert_called('POST', '/servers') self.assertIsInstance(s, servers.Server) - def test_add_floating_ip(self): - # self.floating_ips.list() is not available after 2.35 - pass - - def test_add_floating_ip_to_fixed(self): - # self.floating_ips.list() is not available after 2.35 - pass - - def test_remove_floating_ip(self): - # self.floating_ips.list() is not available after 2.35 - pass - class ServersCreateImageBackupV2_45Test(utils.FixturedTestCase): """Tests the 2.45 microversion for createImage and createBackup diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index a6c0ae4a9..6174feff0 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -1858,22 +1858,6 @@ def test_delete_host_meta(self): self.assert_called('DELETE', '/servers/uuid1/metadata/key1', pos=1) self.assert_called('DELETE', '/servers/uuid2/metadata/key1', pos=2) - def test_server_floating_ip_associate(self): - _, err = self.run_command( - 'floating-ip-associate sample-server 11.0.0.1') - self.assertIn('WARNING: Command floating-ip-associate is deprecated', - err) - self.assert_called('POST', '/servers/1234/action', - {'addFloatingIp': {'address': '11.0.0.1'}}) - - def test_server_floating_ip_disassociate(self): - _, err = self.run_command( - 'floating-ip-disassociate sample-server 11.0.0.1') - self.assertIn( - 'WARNING: Command floating-ip-disassociate is deprecated', err) - self.assert_called('POST', '/servers/1234/action', - {'removeFloatingIp': {'address': '11.0.0.1'}}) - def test_usage_list(self): cmd = 'usage-list --start 2000-01-20 --end 2005-02-01' stdout, _stderr = self.run_command(cmd) @@ -2708,18 +2692,6 @@ def test_quota_class_update(self): 'PUT', '/os-quota-class-sets/97f4c221bff44578b0300df4ef119353', body) - def test_add_fixed_ip(self): - _, err = self.run_command('add-fixed-ip sample-server 1') - self.assertIn('WARNING: Command add-fixed-ip is deprecated', err) - self.assert_called('POST', '/servers/1234/action', - {'addFixedIp': {'networkId': '1'}}) - - def test_remove_fixed_ip(self): - _, err = self.run_command('remove-fixed-ip sample-server 10.0.0.10') - self.assertIn('WARNING: Command remove-fixed-ip is deprecated', err) - self.assert_called('POST', '/servers/1234/action', - {'removeFixedIp': {'address': '10.0.0.10'}}) - def test_backup(self): out, err = self.run_command('backup sample-server back1 daily 1') # With microversion < 2.45 there is no output from this command. @@ -3125,12 +3097,6 @@ def test_list_server_group_with_limit_and_offset(self): self.run_command('server-group-list --limit 20 --offset 5') self.assert_called('GET', '/os-server-groups?limit=20&offset=5') - def test_list_server_os_virtual_interfaces(self): - _, err = self.run_command('virtual-interface-list 1234') - self.assertIn('WARNING: Command virtual-interface-list is deprecated', - err) - self.assert_called('GET', '/servers/1234/os-virtual-interfaces') - def test_versions(self): exclusions = set([ 1, # Same as version 2.0 @@ -3139,6 +3105,7 @@ def test_versions(self): 5, # doesn't require any changes in novaclient 7, # doesn't require any changes in novaclient 9, # doesn't require any changes in novaclient + 12, # no longer supported 15, # doesn't require any changes in novaclient 16, # doesn't require any changes in novaclient 18, # NOTE(andreykurilin): this microversion requires changes in diff --git a/novaclient/v2/client.py b/novaclient/v2/client.py index 4d02b280a..1f543a820 100644 --- a/novaclient/v2/client.py +++ b/novaclient/v2/client.py @@ -43,7 +43,6 @@ from novaclient.v2 import services from novaclient.v2 import usage from novaclient.v2 import versions -from novaclient.v2 import virtual_interfaces from novaclient.v2 import volumes @@ -152,8 +151,6 @@ def __init__(self, self.quota_classes = quota_classes.QuotaClassSetManager(self) self.quotas = quotas.QuotaSetManager(self) self.usage = usage.UsageManager(self) - self.virtual_interfaces = \ - virtual_interfaces.VirtualInterfaceManager(self) self.aggregates = aggregates.AggregateManager(self) self.hypervisors = hypervisors.HypervisorManager(self) self.hypervisor_stats = hypervisors.HypervisorStatsManager(self) diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index 1e236c894..e305253ed 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -20,7 +20,6 @@ """ import base64 -import warnings from oslo_utils import encodeutils import six @@ -52,12 +51,6 @@ 'webmks': 'mks' } -ADD_REMOVE_FIXED_FLOATING_DEPRECATION_WARNING = _( - 'The %s server action API is deprecated as of the 2.44 microversion. This ' - 'API binding will be removed in the first major release after the Nova ' - '16.0.0 Pike release. Use python-neutronclient or openstacksdk instead.' -) - class Server(base.Resource): HUMAN_ID = True @@ -172,35 +165,6 @@ def clear_password(self): """ return self.manager.clear_password(self) - def add_fixed_ip(self, network_id): - """ - Add an IP address on a network. - - :param network_id: The ID of the network the IP should be on. - :returns: An instance of novaclient.base.TupleWithMeta - """ - return self.manager.add_fixed_ip(self, network_id) - - def add_floating_ip(self, address, fixed_address=None): - """ - Add floating IP to an instance - - :param address: The IP address or FloatingIP to add to the instance - :param fixed_address: The fixedIP address the FloatingIP is to be - associated with (optional) - :returns: An instance of novaclient.base.TupleWithMeta - """ - return self.manager.add_floating_ip(self, address, fixed_address) - - def remove_floating_ip(self, address): - """ - Remove floating IP from an instance - - :param address: The IP address or FloatingIP to remove - :returns: An instance of novaclient.base.TupleWithMeta - """ - return self.manager.remove_floating_ip(self, address) - def stop(self): """ Stop -- Stop the running server. @@ -346,15 +310,6 @@ def migrate(self, host=None): """ return self.manager.migrate(self, host=host) - def remove_fixed_ip(self, address): - """ - Remove an IP address. - - :param address: The IP address to remove. - :returns: An instance of novaclient.base.TupleWithMeta - """ - return self.manager.remove_fixed_ip(self, address) - def change_password(self, password): """ Update the admin password for a server. @@ -902,69 +857,6 @@ def list(self, detailed=True, search_opts=None, marker=None, limit=None, marker = result[-1].id return result - @api_versions.wraps('2.0', '2.43') - def add_fixed_ip(self, server, network_id): - """ - DEPRECATED Add an IP address on a network. - - :param server: The :class:`Server` (or its ID) to add an IP to. - :param network_id: The ID of the network the IP should be on. - :returns: An instance of novaclient.base.TupleWithMeta - """ - warnings.warn(ADD_REMOVE_FIXED_FLOATING_DEPRECATION_WARNING % - 'addFixedIP', DeprecationWarning) - return self._action('addFixedIp', server, {'networkId': network_id}) - - @api_versions.wraps('2.0', '2.43') - def remove_fixed_ip(self, server, address): - """ - DEPRECATED Remove an IP address. - - :param server: The :class:`Server` (or its ID) to add an IP to. - :param address: The IP address to remove. - :returns: An instance of novaclient.base.TupleWithMeta - """ - warnings.warn(ADD_REMOVE_FIXED_FLOATING_DEPRECATION_WARNING % - 'removeFixedIP', DeprecationWarning) - return self._action('removeFixedIp', server, {'address': address}) - - @api_versions.wraps('2.0', '2.43') - def add_floating_ip(self, server, address, fixed_address=None): - """ - DEPRECATED Add a floating IP to an instance - - :param server: The :class:`Server` (or its ID) to add an IP to. - :param address: The FloatingIP or string floating address to add. - :param fixed_address: The FixedIP the floatingIP should be - associated with (optional) - :returns: An instance of novaclient.base.TupleWithMeta - """ - warnings.warn(ADD_REMOVE_FIXED_FLOATING_DEPRECATION_WARNING % - 'addFloatingIP', DeprecationWarning) - address = address.ip if hasattr(address, 'ip') else address - if fixed_address: - if hasattr(fixed_address, 'ip'): - fixed_address = fixed_address.ip - return self._action('addFloatingIp', server, - {'address': address, - 'fixed_address': fixed_address}) - else: - return self._action('addFloatingIp', server, {'address': address}) - - @api_versions.wraps('2.0', '2.43') - def remove_floating_ip(self, server, address): - """ - DEPRECATED Remove a floating IP address. - - :param server: The :class:`Server` (or its ID) to remove an IP from. - :param address: The FloatingIP or string floating address to remove. - :returns: An instance of novaclient.base.TupleWithMeta - """ - warnings.warn(ADD_REMOVE_FIXED_FLOATING_DEPRECATION_WARNING % - 'removeFloatingIP', DeprecationWarning) - address = address.ip if hasattr(address, 'ip') else address - return self._action('removeFloatingIp', server, {'address': address}) - def get_vnc_console(self, server, console_type): """ Get a vnc console for an instance diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index ca6a54e28..684f8cf28 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -49,17 +49,6 @@ logger = logging.getLogger(__name__) -# NOTE(mriedem): Remove this along with the deprecated commands in the first -# major python-novaclient release AFTER the nova server 16.0.0 Pike release. -def emit_fixed_floating_deprecation_warning(command_name): - print(_('WARNING: Command %s is deprecated and will be removed ' - 'in the first major release after the Nova server 16.0.0 ' - 'Pike release. Use python-neutronclient or python-openstackclient' - 'instead. Specify --os-compute-api-version less than 2.44 ' - 'to continue using this command until it is removed.') % - command_name, file=sys.stderr) - - def emit_duplicated_image_with_warning(img, image_with): img_uuid_list = [str(image.id) for image in img] print(_('WARNING: Multiple matching images: %(img_uuid_list)s\n' @@ -2338,27 +2327,6 @@ def _find_network_id(cs, net_name): raise exceptions.CommandError(six.text_type(e)) -@utils.arg('server', metavar='', help=_('Name or ID of server.')) -@utils.arg( - 'network_id', - metavar='', - help=_('Network ID.')) -def do_add_fixed_ip(cs, args): - """DEPRECATED Add new IP address on a network to server.""" - emit_fixed_floating_deprecation_warning('add-fixed-ip') - server = _find_server(cs, args.server) - server.add_fixed_ip(args.network_id) - - -@utils.arg('server', metavar='', help=_('Name or ID of server.')) -@utils.arg('address', metavar='
', help=_('IP Address.')) -def do_remove_fixed_ip(cs, args): - """DEPRECATED Remove an IP address from a server.""" - emit_fixed_floating_deprecation_warning('remove-fixed-ip') - server = _find_server(cs, args.server) - server.remove_fixed_ip(args.address) - - def _print_volume(volume): utils.print_dict(volume.to_dict()) @@ -2596,37 +2564,6 @@ def do_console_log(cs, args): print(data) -@utils.arg('server', metavar='', help=_('Name or ID of server.')) -@utils.arg('address', metavar='
', help=_('IP Address.')) -@utils.arg( - '--fixed-address', - metavar='', - default=None, - help=_('Fixed IP Address to associate with.')) -def do_floating_ip_associate(cs, args): - """DEPRECATED Associate a floating IP address to a server.""" - emit_fixed_floating_deprecation_warning('floating-ip-associate') - _associate_floating_ip(cs, args) - - -def _associate_floating_ip(cs, args): - server = _find_server(cs, args.server) - server.add_floating_ip(args.address, args.fixed_address) - - -@utils.arg('server', metavar='', help=_('Name or ID of server.')) -@utils.arg('address', metavar='
', help=_('IP Address.')) -def do_floating_ip_disassociate(cs, args): - """DEPRECATED Disassociate a floating IP address from a server.""" - emit_fixed_floating_deprecation_warning('floating-ip-disassociate') - _disassociate_floating_ip(cs, args) - - -def _disassociate_floating_ip(cs, args): - server = _find_server(cs, args.server) - server.remove_floating_ip(args.address) - - @utils.arg('server', metavar='', help=_('Name or ID of server.')) @utils.arg( 'secgroup', @@ -4582,33 +4519,6 @@ def do_version_list(cs, args): utils.print_list(result, columns) -@api_versions.wraps("2.0", "2.11") -def _print_virtual_interface_list(cs, interface_list): - columns = ['Id', 'Mac address'] - utils.print_list(interface_list, columns) - - -@api_versions.wraps("2.12") -def _print_virtual_interface_list(cs, interface_list): - columns = ['Id', 'Mac address', 'Network ID'] - formatters = {"Network ID": lambda o: o.net_id} - utils.print_list(interface_list, columns, formatters) - - -@utils.arg('server', metavar='', help=_('ID of server.')) -def do_virtual_interface_list(cs, args): - """DEPRECATED Show virtual interface info about the given server.""" - print(_('WARNING: Command virtual-interface-list is deprecated and will ' - 'be removed in the first major release after the Nova server ' - '16.0.0 Pike release. There is no replacement or alternative for ' - 'this command. Specify --os-compute-api-version less than 2.44 ' - 'to continue using this command until it is removed.'), - file=sys.stderr) - server = _find_server(cs, args.server) - interface_list = cs.virtual_interfaces.list(base.getid(server)) - _print_virtual_interface_list(cs, interface_list) - - @api_versions.wraps("2.26") @utils.arg('server', metavar='', help=_('Name or ID of server.')) def do_server_tag_list(cs, args): diff --git a/novaclient/v2/virtual_interfaces.py b/novaclient/v2/virtual_interfaces.py deleted file mode 100644 index caf2e3a0e..000000000 --- a/novaclient/v2/virtual_interfaces.py +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright 2012 OpenStack Foundation -# All Rights Reserved. -# -# 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. - -""" -DEPRECATED Virtual Interfaces -""" - -import warnings - -from novaclient import api_versions -from novaclient import base -from novaclient.i18n import _ - - -class VirtualInterface(base.Resource): - def __repr__(self): - return "" - - -class VirtualInterfaceManager(base.ManagerWithFind): - """DEPRECATED""" - resource_class = VirtualInterface - - @api_versions.wraps('2.0', '2.43') - def list(self, instance_id): - """DEPRECATED""" - warnings.warn(_('The os-virtual-interfaces API is deprecated. This ' - 'API binding will be removed in the first major ' - 'release after the Nova server 16.0.0 Pike release.'), - DeprecationWarning) - return self._list('/servers/%s/os-virtual-interfaces' % instance_id, - 'virtual_interfaces') diff --git a/releasenotes/notes/remove-virt-interfaces-add-rm-fixed-floating-398c905d9c91cca8.yaml b/releasenotes/notes/remove-virt-interfaces-add-rm-fixed-floating-398c905d9c91cca8.yaml new file mode 100644 index 000000000..dddfa7da9 --- /dev/null +++ b/releasenotes/notes/remove-virt-interfaces-add-rm-fixed-floating-398c905d9c91cca8.yaml @@ -0,0 +1,15 @@ +--- +upgrade: + - | + The following CLIs and their backing API bindings were deprecated and + capped at microversion 2.44: + + * ``nova add-fixed-ip``: use python-neutronclient or openstacksdk + * ``nova remove-fixed-ip``: use python-neutronclient or openstacksdk + * ``nova floating-ip-associate``: use python-neutronclient or openstacksdk + * ``nova floating-ip-disassociate``: use python-neutronclient or + openstacksdk + * ``nova virtual-interface-list``: there is no replacement as this is + only implemented for nova-network which is deprecated + + The CLIs and API bindings have now been removed. From fefc3ba723865307f4cc2ca287eb59faa4e8a5b1 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Thu, 11 Jan 2018 17:39:50 -0500 Subject: [PATCH 1401/1705] Remove deprecated services binary CLI arg The services CLI 'binary' arg was deprecated in Pike via change Idd0d2be960ca0ed59097c10c931da47a1a3e66fb because with cells v2 support in the API, the binary argument for the enable/disable CLIs doesn't make sense as the only binary those work with is 'nova-compute'. So this removes the deprecated argument and hard-codes the value to be 'nova-compute'. Change-Id: I60490f3d74212bb172dccc1c7d337198e1021236 --- .../functional/v2/legacy/test_os_services.py | 14 +++++------ .../tests/functional/v2/test_os_services.py | 8 +++---- novaclient/tests/unit/v2/test_shell.py | 24 +++++-------------- novaclient/v2/shell.py | 20 ++++------------ ...e-service-binary-arg-ec2838214c8c7abc.yaml | 6 +++++ 5 files changed, 25 insertions(+), 47 deletions(-) create mode 100644 releasenotes/notes/remove-service-binary-arg-ec2838214c8c7abc.yaml diff --git a/novaclient/tests/functional/v2/legacy/test_os_services.py b/novaclient/tests/functional/v2/legacy/test_os_services.py index 8af71611d..6a12de2c2 100644 --- a/novaclient/tests/functional/v2/legacy/test_os_services.py +++ b/novaclient/tests/functional/v2/legacy/test_os_services.py @@ -41,13 +41,12 @@ def test_os_service_disable_enable(self): continue host = self._get_column_value_from_single_row_table( self.nova('service-list --binary %s' % serv.binary), 'Host') - service = self.nova('service-disable %s %s' % (host, serv.binary)) - self.addCleanup(self.nova, 'service-enable', - params="%s %s" % (host, serv.binary)) + service = self.nova('service-disable %s' % host) + self.addCleanup(self.nova, 'service-enable', params=host) status = self._get_column_value_from_single_row_table( service, 'Status') self.assertEqual('disabled', status) - service = self.nova('service-enable %s %s' % (host, serv.binary)) + service = self.nova('service-enable %s' % host) status = self._get_column_value_from_single_row_table( service, 'Status') self.assertEqual('enabled', status) @@ -64,10 +63,9 @@ def test_os_service_disable_log_reason(self): continue host = self._get_column_value_from_single_row_table( self.nova('service-list --binary %s' % serv.binary), 'Host') - service = self.nova('service-disable --reason test_disable %s %s' - % (host, serv.binary)) - self.addCleanup(self.nova, 'service-enable', - params="%s %s" % (host, serv.binary)) + service = self.nova( + 'service-disable --reason test_disable %s' % host) + self.addCleanup(self.nova, 'service-enable', params=host) status = self._get_column_value_from_single_row_table( service, 'Status') log_reason = self._get_column_value_from_single_row_table( diff --git a/novaclient/tests/functional/v2/test_os_services.py b/novaclient/tests/functional/v2/test_os_services.py index 1dba1ad01..40da9f749 100644 --- a/novaclient/tests/functional/v2/test_os_services.py +++ b/novaclient/tests/functional/v2/test_os_services.py @@ -38,15 +38,13 @@ def test_os_services_force_down_force_up(self): host = self._get_column_value_from_single_row_table(service_list, 'Host') - service = self.nova('service-force-down %s %s' - % (host, serv.binary)) + service = self.nova('service-force-down %s' % host) self.addCleanup(self.nova, 'service-force-down --unset', - params="%s %s" % (host, serv.binary)) + params=host) status = self._get_column_value_from_single_row_table( service, 'Forced down') self.assertEqual('True', status) - service = self.nova('service-force-down --unset %s %s' - % (host, serv.binary)) + service = self.nova('service-force-down --unset %s' % host) status = self._get_column_value_from_single_row_table( service, 'Forced down') self.assertEqual('False', status) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 6174feff0..5163959a5 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2369,8 +2369,8 @@ def test_services_list_with_host_binary(self): self.assert_called('GET', '/os-services?host=host1&binary=nova-cert') def test_services_enable(self): - self.run_command('service-enable host1 nova-cert') - body = {'host': 'host1', 'binary': 'nova-cert'} + self.run_command('service-enable host1') + body = {'host': 'host1', 'binary': 'nova-compute'} self.assert_called('PUT', '/os-services/enable', body) def test_services_enable_v2_53(self): @@ -2381,15 +2381,9 @@ def test_services_enable_v2_53(self): self.assert_called( 'PUT', '/os-services/%s' % fakes.FAKE_SERVICE_UUID_1, body) - def test_services_enable_default_binary(self): - """Tests that the default binary is nova-compute if not specified.""" - self.run_command('service-enable host1') - body = {'host': 'host1', 'binary': 'nova-compute'} - self.assert_called('PUT', '/os-services/enable', body) - def test_services_disable(self): - self.run_command('service-disable host1 nova-cert') - body = {'host': 'host1', 'binary': 'nova-cert'} + self.run_command('service-disable host1') + body = {'host': 'host1', 'binary': 'nova-compute'} self.assert_called('PUT', '/os-services/disable', body) def test_services_disable_v2_53(self): @@ -2400,15 +2394,9 @@ def test_services_disable_v2_53(self): self.assert_called( 'PUT', '/os-services/%s' % fakes.FAKE_SERVICE_UUID_1, body) - def test_services_disable_default_binary(self): - """Tests that the default binary is nova-compute if not specified.""" - self.run_command('service-disable host1') - body = {'host': 'host1', 'binary': 'nova-compute'} - self.assert_called('PUT', '/os-services/disable', body) - def test_services_disable_with_reason(self): - self.run_command('service-disable host1 nova-cert --reason no_reason') - body = {'host': 'host1', 'binary': 'nova-cert', + self.run_command('service-disable host1 --reason no_reason') + body = {'host': 'host1', 'binary': 'nova-compute', 'disabled_reason': 'no_reason'} self.assert_called('PUT', '/os-services/disable-log-reason', body) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 684f8cf28..8ad018c4d 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -3400,13 +3400,9 @@ def do_service_list(cs, args): # values. @api_versions.wraps('2.0', '2.52') @utils.arg('host', metavar='', help=_('Name of host.')) -# TODO(mriedem): Eventually just hard-code the binary to "nova-compute". -@utils.arg('binary', metavar='', help=_('Service binary. The only ' - 'meaningful binary is "nova-compute". (Deprecated)'), - default='nova-compute', nargs='?') def do_service_enable(cs, args): """Enable the service.""" - result = cs.services.enable(args.host, args.binary) + result = cs.services.enable(args.host, 'nova-compute') utils.print_list([result], ['Host', 'Binary', 'Status']) @@ -3423,10 +3419,6 @@ def do_service_enable(cs, args): # values. @api_versions.wraps('2.0', '2.52') @utils.arg('host', metavar='', help=_('Name of host.')) -# TODO(mriedem): Eventually just hard-code the binary to "nova-compute". -@utils.arg('binary', metavar='', help=_('Service binary. The only ' - 'meaningful binary is "nova-compute". (Deprecated)'), - default='nova-compute', nargs='?') @utils.arg( '--reason', metavar='', @@ -3434,12 +3426,12 @@ def do_service_enable(cs, args): def do_service_disable(cs, args): """Disable the service.""" if args.reason: - result = cs.services.disable_log_reason(args.host, args.binary, + result = cs.services.disable_log_reason(args.host, 'nova-compute', args.reason) utils.print_list([result], ['Host', 'Binary', 'Status', 'Disabled Reason']) else: - result = cs.services.disable(args.host, args.binary) + result = cs.services.disable(args.host, 'nova-compute') utils.print_list([result], ['Host', 'Binary', 'Status']) @@ -3465,10 +3457,6 @@ def do_service_disable(cs, args): # values. @api_versions.wraps("2.11", "2.52") @utils.arg('host', metavar='', help=_('Name of host.')) -# TODO(mriedem): Eventually just hard-code the binary to "nova-compute". -@utils.arg('binary', metavar='', help=_('Service binary. The only ' - 'meaningful binary is "nova-compute". (Deprecated)'), - default='nova-compute', nargs='?') @utils.arg( '--unset', dest='force_down', @@ -3477,7 +3465,7 @@ def do_service_disable(cs, args): default=True) def do_service_force_down(cs, args): """Force service to down.""" - result = cs.services.force_down(args.host, args.binary, args.force_down) + result = cs.services.force_down(args.host, 'nova-compute', args.force_down) utils.print_list([result], ['Host', 'Binary', 'Forced down']) diff --git a/releasenotes/notes/remove-service-binary-arg-ec2838214c8c7abc.yaml b/releasenotes/notes/remove-service-binary-arg-ec2838214c8c7abc.yaml new file mode 100644 index 000000000..2a136a23d --- /dev/null +++ b/releasenotes/notes/remove-service-binary-arg-ec2838214c8c7abc.yaml @@ -0,0 +1,6 @@ +--- +upgrade: + - | + The deprecated ``binary`` argument to the ``nova service-enable``, + ``nova service-disable``, and ``nova service-force-down`` commands has been + removed. From 038cfdd5b3395acbf483dc982cae3eba34ddf1c5 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Thu, 14 Dec 2017 18:20:45 -0500 Subject: [PATCH 1402/1705] Add support for the 2.57 microversion With the 2.57 microversion, we: * Deprecate the --file option from the nova boot and nova rebuild CLIs and API bindings. * Add --user-data and --user-data-unset to the nova rebuild CLI and API bindings. * Deprecate the maxPersonality and maxPersonalitySize fields from the nova limits and nova absolute-limits CLIs and API bindings. * Deprecate injected_files, injected_file_content_bytes, and injected_file_path_bytes from the nova quota-show, nova quota-update, nova quota-defaults, nova quota-class-show, and nova quota-class-update CLIs and API bindings. Part of blueprint deprecate-file-injection Change-Id: Id13e3eac3ef87d429454042ac7046e865774ff8e --- novaclient/__init__.py | 2 +- novaclient/exceptions.py | 1 + novaclient/tests/functional/v2/test_quotas.py | 13 +- novaclient/tests/unit/fixture_data/limits.py | 25 ++-- novaclient/tests/unit/fixture_data/quotas.py | 18 +++ novaclient/tests/unit/v2/fakes.py | 41 ++++-- novaclient/tests/unit/v2/test_limits.py | 43 +++++-- .../tests/unit/v2/test_quota_classes.py | 36 +++++- novaclient/tests/unit/v2/test_quotas.py | 40 ++++++ novaclient/tests/unit/v2/test_servers.py | 84 +++++++++---- novaclient/tests/unit/v2/test_shell.py | 100 ++++++++++++++- novaclient/v2/quota_classes.py | 29 ++++- novaclient/v2/quotas.py | 39 ++++++ novaclient/v2/servers.py | 66 +++++++--- novaclient/v2/shell.py | 119 ++++++++++++------ .../microversion-v2_57-acae2ee11ddae4fb.yaml | 31 +++++ 16 files changed, 575 insertions(+), 112 deletions(-) create mode 100644 releasenotes/notes/microversion-v2_57-acae2ee11ddae4fb.yaml diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 435ff9c9f..2103d5cea 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.56") +API_MAX_VERSION = api_versions.APIVersion("2.57") diff --git a/novaclient/exceptions.py b/novaclient/exceptions.py index 707aa889e..a7046d338 100644 --- a/novaclient/exceptions.py +++ b/novaclient/exceptions.py @@ -48,6 +48,7 @@ def __init__(self, argument_name, start_version, end_version=None): self.message = ( "'%(name)s' argument is only allowed since microversion " "%(start)s." % {"name": argument_name, "start": start_version}) + super(UnsupportedAttribute, self).__init__(self.message) class CommandError(Exception): diff --git a/novaclient/tests/functional/v2/test_quotas.py b/novaclient/tests/functional/v2/test_quotas.py index 1d4e6af31..effddf8a6 100644 --- a/novaclient/tests/functional/v2/test_quotas.py +++ b/novaclient/tests/functional/v2/test_quotas.py @@ -52,7 +52,7 @@ def test_quotas_update(self): class TestQuotasNovaClient2_36(TestQuotasNovaClient2_35): """Nova quotas functional tests.""" - COMPUTE_API_VERSION = "2.latest" + COMPUTE_API_VERSION = "2.36" # The 2.36 microversion stops proxying network quota resources like # floating/fixed IPs and security groups/rules. @@ -61,3 +61,14 @@ class TestQuotasNovaClient2_36(TestQuotasNovaClient2_35): 'injected_file_content_bytes', 'injected_file_path_bytes', 'key_pairs', 'server_groups', 'server_group_members'] + + +class TestQuotasNovaClient2_57(TestQuotasNovaClient2_35): + """Nova quotas functional tests.""" + + COMPUTE_API_VERSION = "2.latest" + + # The 2.57 microversion deprecates injected_file* quotas. + _quota_resources = ['instances', 'cores', 'ram', + 'metadata_items', 'key_pairs', + 'server_groups', 'server_group_members'] diff --git a/novaclient/tests/unit/fixture_data/limits.py b/novaclient/tests/unit/fixture_data/limits.py index f9a86ff79..0e7986580 100644 --- a/novaclient/tests/unit/fixture_data/limits.py +++ b/novaclient/tests/unit/fixture_data/limits.py @@ -16,6 +16,13 @@ class Fixture(base.Fixture): base_url = 'limits' + absolute = { + "maxTotalRAMSize": 51200, + "maxServerMeta": 5, + "maxImageMeta": 5, + "maxPersonality": 5, + "maxPersonalitySize": 10240 + } def setUp(self): super(Fixture, self).setUp() @@ -64,13 +71,7 @@ def setUp(self): ] } ], - "absolute": { - "maxTotalRAMSize": 51200, - "maxServerMeta": 5, - "maxImageMeta": 5, - "maxPersonality": 5, - "maxPersonalitySize": 10240 - }, + "absolute": self.absolute, }, } @@ -78,3 +79,13 @@ def setUp(self): self.requests_mock.get(self.url(), json=get_limits, headers=headers) + + +class Fixture2_57(Fixture): + """Fixture data for the 2.57 microversion where personality files are + deprecated. + """ + absolute = { + "maxTotalRAMSize": 51200, + "maxServerMeta": 5 + } diff --git a/novaclient/tests/unit/fixture_data/quotas.py b/novaclient/tests/unit/fixture_data/quotas.py index e3d179e26..1ffa8c265 100644 --- a/novaclient/tests/unit/fixture_data/quotas.py +++ b/novaclient/tests/unit/fixture_data/quotas.py @@ -57,6 +57,7 @@ def test_quota(self, tenant_id='test'): 'injected_file_content_bytes': 1, 'injected_file_path_bytes': 1, 'ram': 1, + 'fixed_ips': -1, 'floating_ips': 1, 'instances': 1, 'injected_files': 1, @@ -67,3 +68,20 @@ def test_quota(self, tenant_id='test'): 'server_groups': 1, 'server_group_members': 1 } + + +class V2_57(V1): + """2.57 fixture data where there are no injected file or network resources + """ + + def test_quota(self, tenant_id='test'): + return { + 'id': tenant_id, + 'metadata_items': 1, + 'ram': 1, + 'instances': 1, + 'cores': 1, + 'key_pairs': 1, + 'server_groups': 1, + 'server_group_members': 1 + } diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 9b059a38a..11789ec19 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -331,6 +331,14 @@ def get_extensions(self, **kw): # def get_limits(self, **kw): + absolute = { + "maxTotalRAMSize": 51200, + "maxServerMeta": 5, + "maxImageMeta": 5 + } + # 2.57 removes injected_file* entries from the response. + if self.api_version < api_versions.APIVersion('2.57'): + absolute.update({"maxPersonality": 5, "maxPersonalitySize": 10240}) return (200, {}, {"limits": { "rate": [ { @@ -374,13 +382,7 @@ def get_limits(self, **kw): ] } ], - "absolute": { - "maxTotalRAMSize": 51200, - "maxServerMeta": 5, - "maxImageMeta": 5, - "maxPersonality": 5, - "maxPersonalitySize": 10240 - }, + "absolute": absolute, }}) # @@ -1297,6 +1299,19 @@ def delete_os_quota_sets_97f4c221bff44578b0300df4ef119353(self, **kw): # def get_os_quota_class_sets_test(self, **kw): + # 2.57 removes injected_file* entries from the response. + if self.api_version >= api_versions.APIVersion('2.57'): + return (200, FAKE_RESPONSE_HEADERS, { + 'quota_class_set': { + 'id': 'test', + 'metadata_items': 1, + 'ram': 1, + 'instances': 1, + 'cores': 1, + 'key_pairs': 1, + 'server_groups': 1, + 'server_group_members': 1}}) + if self.api_version >= api_versions.APIVersion('2.50'): return (200, FAKE_RESPONSE_HEADERS, { 'quota_class_set': { @@ -1329,6 +1344,18 @@ def get_os_quota_class_sets_test(self, **kw): def put_os_quota_class_sets_test(self, body, **kw): assert list(body) == ['quota_class_set'] + # 2.57 removes injected_file* entries from the response. + if self.api_version >= api_versions.APIVersion('2.57'): + return (200, {}, { + 'quota_class_set': { + 'metadata_items': 1, + 'ram': 1, + 'instances': 1, + 'cores': 1, + 'key_pairs': 1, + 'server_groups': 1, + 'server_group_members': 1}}) + if self.api_version >= api_versions.APIVersion('2.50'): return (200, {}, { 'quota_class_set': { diff --git a/novaclient/tests/unit/v2/test_limits.py b/novaclient/tests/unit/v2/test_limits.py index a0b8bcf2a..1abcd1a5d 100644 --- a/novaclient/tests/unit/v2/test_limits.py +++ b/novaclient/tests/unit/v2/test_limits.py @@ -11,6 +11,7 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient import api_versions from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit.fixture_data import limits as data from novaclient.tests.unit import utils @@ -22,6 +23,8 @@ class LimitsTest(utils.FixturedTestCase): client_fixture_class = client.V1 data_fixture_class = data.Fixture + supports_image_meta = True # 2.39 deprecates maxImageMeta + supports_personality = True # 2.57 deprecates maxPersonality* def test_get_limits(self): obj = self.cs.limits.get() @@ -39,13 +42,16 @@ def test_absolute_limits_reserved(self): obj = self.cs.limits.get(reserved=True) self.assert_request_id(obj, fakes.FAKE_REQUEST_ID_LIST) - expected = ( + expected = [ limits.AbsoluteLimit("maxTotalRAMSize", 51200), - limits.AbsoluteLimit("maxServerMeta", 5), - limits.AbsoluteLimit("maxImageMeta", 5), - limits.AbsoluteLimit("maxPersonality", 5), - limits.AbsoluteLimit("maxPersonalitySize", 10240), - ) + limits.AbsoluteLimit("maxServerMeta", 5) + ] + if self.supports_image_meta: + expected.append(limits.AbsoluteLimit("maxImageMeta", 5)) + if self.supports_personality: + expected.extend([ + limits.AbsoluteLimit("maxPersonality", 5), + limits.AbsoluteLimit("maxPersonalitySize", 10240)]) self.assert_called('GET', '/limits?reserved=1') abs_limits = list(obj.absolute) @@ -75,16 +81,29 @@ def test_rate_absolute_limits(self): for limit in rate_limits: self.assertIn(limit, expected) - expected = ( + expected = [ limits.AbsoluteLimit("maxTotalRAMSize", 51200), - limits.AbsoluteLimit("maxServerMeta", 5), - limits.AbsoluteLimit("maxImageMeta", 5), - limits.AbsoluteLimit("maxPersonality", 5), - limits.AbsoluteLimit("maxPersonalitySize", 10240), - ) + limits.AbsoluteLimit("maxServerMeta", 5) + ] + if self.supports_image_meta: + expected.append(limits.AbsoluteLimit("maxImageMeta", 5)) + if self.supports_personality: + expected.extend([ + limits.AbsoluteLimit("maxPersonality", 5), + limits.AbsoluteLimit("maxPersonalitySize", 10240)]) abs_limits = list(obj.absolute) self.assertEqual(len(abs_limits), len(expected)) for limit in abs_limits: self.assertIn(limit, expected) + + +class LimitsTest2_57(LimitsTest): + data_fixture_class = data.Fixture2_57 + supports_image_meta = False + supports_personality = False + + def setUp(self): + super(LimitsTest2_57, self).setUp() + self.cs.api_version = api_versions.APIVersion('2.57') diff --git a/novaclient/tests/unit/v2/test_quota_classes.py b/novaclient/tests/unit/v2/test_quota_classes.py index e9d9ad75a..3becb6323 100644 --- a/novaclient/tests/unit/v2/test_quota_classes.py +++ b/novaclient/tests/unit/v2/test_quota_classes.py @@ -49,17 +49,20 @@ def test_refresh_quota(self): class QuotaClassSetsTest2_50(QuotaClassSetsTest): """Tests the quota classes API binding using the 2.50 microversion.""" + api_version = '2.50' + invalid_resources = ['floating_ips', 'fixed_ips', 'networks', + 'security_groups', 'security_group_rules'] + def setUp(self): super(QuotaClassSetsTest2_50, self).setUp() - self.cs = fakes.FakeClient(api_versions.APIVersion("2.50")) + self.cs = fakes.FakeClient(api_versions.APIVersion(self.api_version)) def test_class_quotas_get(self): """Tests that network-related resources aren't in a 2.50 response and server group related resources are in the response. """ q = super(QuotaClassSetsTest2_50, self).test_class_quotas_get() - for invalid_resource in ('floating_ips', 'fixed_ips', 'networks', - 'security_groups', 'security_group_rules'): + for invalid_resource in self.invalid_resources: self.assertFalse(hasattr(q, invalid_resource), '%s should not be in %s' % (invalid_resource, q)) # Also make sure server_groups and server_group_members are in the @@ -73,8 +76,7 @@ def test_update_quota(self): and server group related resources are in the response. """ q = super(QuotaClassSetsTest2_50, self).test_update_quota() - for invalid_resource in ('floating_ips', 'fixed_ips', 'networks', - 'security_groups', 'security_group_rules'): + for invalid_resource in self.invalid_resources: self.assertFalse(hasattr(q, invalid_resource), '%s should not be in %s' % (invalid_resource, q)) # Also make sure server_groups and server_group_members are in the @@ -95,3 +97,27 @@ def test_update_quota_invalid_resources(self): self.assertRaises(TypeError, q.update, security_groups=1) self.assertRaises(TypeError, q.update, security_group_rules=1) self.assertRaises(TypeError, q.update, networks=1) + return q + + +class QuotaClassSetsTest2_57(QuotaClassSetsTest2_50): + """Tests the quota classes API binding using the 2.57 microversion.""" + api_version = '2.57' + + def setUp(self): + super(QuotaClassSetsTest2_57, self).setUp() + self.invalid_resources.extend(['injected_files', + 'injected_file_content_bytes', + 'injected_file_path_bytes']) + + def test_update_quota_invalid_resources(self): + """Tests trying to update quota class values for invalid resources. + + This will fail with TypeError because the file-related resource + kwargs aren't defined. + """ + q = super( + QuotaClassSetsTest2_57, self).test_update_quota_invalid_resources() + self.assertRaises(TypeError, q.update, injected_files=1) + self.assertRaises(TypeError, q.update, injected_file_content_bytes=1) + self.assertRaises(TypeError, q.update, injected_file_path_bytes=1) diff --git a/novaclient/tests/unit/v2/test_quotas.py b/novaclient/tests/unit/v2/test_quotas.py index 0ed766a03..67a0bc3df 100644 --- a/novaclient/tests/unit/v2/test_quotas.py +++ b/novaclient/tests/unit/v2/test_quotas.py @@ -13,6 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient import api_versions from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit.fixture_data import quotas as data from novaclient.tests.unit import utils @@ -29,6 +30,7 @@ def test_tenant_quotas_get(self): q = self.cs.quotas.get(tenant_id) self.assert_request_id(q, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/os-quota-sets/%s' % tenant_id) + return q def test_user_quotas_get(self): tenant_id = 'test' @@ -65,6 +67,7 @@ def test_force_update_quota(self): self.assert_called( 'PUT', '/os-quota-sets/97f4c221bff44578b0300df4ef119353', {'quota_set': {'force': True, 'cores': 2}}) + return q def test_quotas_delete(self): tenant_id = 'test' @@ -79,3 +82,40 @@ def test_user_quotas_delete(self): self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) url = '/os-quota-sets/%s?user_id=%s' % (tenant_id, user_id) self.assert_called('DELETE', url) + + +class QuotaSetsTest2_57(QuotaSetsTest): + """Tests the quotas API binding using the 2.57 microversion.""" + data_fixture_class = data.V2_57 + invalid_resources = ['floating_ips', 'fixed_ips', 'networks', + 'security_groups', 'security_group_rules', + 'injected_files', 'injected_file_content_bytes', + 'injected_file_path_bytes'] + + def setUp(self): + super(QuotaSetsTest2_57, self).setUp() + self.cs.api_version = api_versions.APIVersion('2.57') + + def test_tenant_quotas_get(self): + q = super(QuotaSetsTest2_57, self).test_tenant_quotas_get() + for invalid_resource in self.invalid_resources: + self.assertFalse(hasattr(q, invalid_resource), + '%s should not be in %s' % (invalid_resource, q)) + + def test_force_update_quota(self): + q = super(QuotaSetsTest2_57, self).test_force_update_quota() + for invalid_resource in self.invalid_resources: + self.assertFalse(hasattr(q, invalid_resource), + '%s should not be in %s' % (invalid_resource, q)) + + def test_update_quota_invalid_resources(self): + """Tests trying to update quota values for invalid resources.""" + q = self.cs.quotas.get('test') + self.assertRaises(TypeError, q.update, floating_ips=1) + self.assertRaises(TypeError, q.update, fixed_ips=1) + self.assertRaises(TypeError, q.update, security_groups=1) + self.assertRaises(TypeError, q.update, security_group_rules=1) + self.assertRaises(TypeError, q.update, networks=1) + self.assertRaises(TypeError, q.update, injected_files=1) + self.assertRaises(TypeError, q.update, injected_file_content_bytes=1) + self.assertRaises(TypeError, q.update, injected_file_path_bytes=1) diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index b09bf6cdc..07ad1497e 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -34,6 +34,7 @@ class ServersTest(utils.FixturedTestCase): client_fixture_class = client.V1 data_fixture_class = data.V1 api_version = None + supports_files = True def setUp(self): super(ServersTest, self).setUp() @@ -126,6 +127,12 @@ def test_get_server_promote_details(self): self.assertEqual(s1._info, s2._info) def test_create_server(self): + kwargs = {} + if self.supports_files: + kwargs['files'] = { + '/etc/passwd': 'some data', # a file + '/tmp/foo.txt': six.StringIO('data'), # a stream + } s = self.cs.servers.create( name="My server", image=1, @@ -133,11 +140,8 @@ def test_create_server(self): meta={'foo': 'bar'}, userdata="hello moto", key_name="fakekey", - files={ - '/etc/passwd': 'some data', # a file - '/tmp/foo.txt': six.StringIO('data'), # a stream - }, - nics=self._get_server_create_default_nics() + nics=self._get_server_create_default_nics(), + **kwargs ) self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers') @@ -253,23 +257,32 @@ def wrapped_boot(url, key, *boot_args, **boot_kwargs): self.assertIsInstance(s, servers.Server) def test_create_server_userdata_file_object(self): + kwargs = {} + if self.supports_files: + kwargs['files'] = { + '/etc/passwd': 'some data', # a file + '/tmp/foo.txt': six.StringIO('data'), # a stream + } s = self.cs.servers.create( name="My server", image=1, flavor=1, meta={'foo': 'bar'}, userdata=six.StringIO('hello moto'), - files={ - '/etc/passwd': 'some data', # a file - '/tmp/foo.txt': six.StringIO('data'), # a stream - }, nics=self._get_server_create_default_nics(), + **kwargs ) self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers') self.assertIsInstance(s, servers.Server) def test_create_server_userdata_unicode(self): + kwargs = {} + if self.supports_files: + kwargs['files'] = { + '/etc/passwd': 'some data', # a file + '/tmp/foo.txt': six.StringIO('data'), # a stream + } s = self.cs.servers.create( name="My server", image=1, @@ -277,17 +290,20 @@ def test_create_server_userdata_unicode(self): meta={'foo': 'bar'}, userdata=six.u('こんにちは'), key_name="fakekey", - files={ - '/etc/passwd': 'some data', # a file - '/tmp/foo.txt': six.StringIO('data'), # a stream - }, nics=self._get_server_create_default_nics(), + **kwargs ) self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers') self.assertIsInstance(s, servers.Server) def test_create_server_userdata_utf8(self): + kwargs = {} + if self.supports_files: + kwargs['files'] = { + '/etc/passwd': 'some data', # a file + '/tmp/foo.txt': six.StringIO('data'), # a stream + } s = self.cs.servers.create( name="My server", image=1, @@ -295,11 +311,8 @@ def test_create_server_userdata_utf8(self): meta={'foo': 'bar'}, userdata='こんにちは', key_name="fakekey", - files={ - '/etc/passwd': 'some data', # a file - '/tmp/foo.txt': six.StringIO('data'), # a stream - }, nics=self._get_server_create_default_nics(), + **kwargs ) self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers') @@ -323,6 +336,12 @@ def test_create_server_admin_pass(self): self.assertEqual(test_password, body['server']['adminPass']) def test_create_server_userdata_bin(self): + kwargs = {} + if self.supports_files: + kwargs['files'] = { + '/etc/passwd': 'some data', # a file + '/tmp/foo.txt': six.StringIO('data'), # a stream + } with tempfile.TemporaryFile(mode='wb+') as bin_file: original_data = os.urandom(1024) bin_file.write(original_data) @@ -335,11 +354,8 @@ def test_create_server_userdata_bin(self): meta={'foo': 'bar'}, userdata=bin_file, key_name="fakekey", - files={ - '/etc/passwd': 'some data', # a file - '/tmp/foo.txt': six.StringIO('data'), # a stream - }, nics=self._get_server_create_default_nics(), + **kwargs ) self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers') @@ -1500,3 +1516,29 @@ def test_migrate_server_pre_256_fails(self): ex = self.assertRaises(TypeError, s.migrate, host='target-host') self.assertIn('host', six.text_type(ex)) + + +class ServersV257Test(ServersV256Test): + """Tests the servers python API bindings with microversion 2.57 where + personality files are deprecated. + """ + api_version = "2.57" + supports_files = False + + def test_create_server_with_files_fails(self): + ex = self.assertRaises( + exceptions.UnsupportedAttribute, self.cs.servers.create, + name="My server", image=1, flavor=1, + files={ + '/etc/passwd': 'some data', # a file + '/tmp/foo.txt': six.StringIO('data'), # a stream + }, nics='auto') + self.assertIn('files', six.text_type(ex)) + + def test_rebuild_server_name_meta_files(self): + files = {'/etc/passwd': 'some data'} + s = self.cs.servers.get(1234) + ex = self.assertRaises( + exceptions.UnsupportedAttribute, s.rebuild, image=1, name='new', + meta={'foo': 'bar'}, files=files) + self.assertIn('files', six.text_type(ex)) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 5163959a5..fb21eb5e7 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -36,6 +36,7 @@ import novaclient.shell from novaclient.tests.unit import utils from novaclient.tests.unit.v2 import fakes +from novaclient.v2 import servers import novaclient.v2.shell FAKE_UUID_1 = fakes.FAKE_IMAGE_UUID_1 @@ -971,6 +972,16 @@ def test_boot_invalid_files(self): ' --file /foo=%s' % (FAKE_UUID_1, invalid_file)) self.assertRaises(exceptions.CommandError, self.run_command, cmd) + def test_boot_files_2_57(self): + """Tests that trying to run the boot command with the --file option + after microversion 2.56 fails. + """ + testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt') + cmd = ('boot some-server --flavor 1 --image %s' + ' --file /tmp/foo=%s') + self.assertRaises(SystemExit, self.run_command, + cmd % (FAKE_UUID_1, testfile), api_version='2.57') + def test_boot_max_min_count(self): self.run_command('boot --image %s --flavor 1 --min-count 1' ' --max-count 3 server' % FAKE_UUID_1) @@ -1570,6 +1581,62 @@ def test_rebuild_with_incorrect_metadata(self): expected = "'['foo']' is not in the format of 'key=value'" self.assertEqual(expected, result.args[0]) + def test_rebuild_user_data_2_56(self): + """Tests that trying to run the rebuild command with the --user-data* + options before microversion 2.57 fails. + """ + cmd = 'rebuild sample-server %s --user-data test' % FAKE_UUID_1 + self.assertRaises(SystemExit, self.run_command, cmd, + api_version='2.56') + cmd = 'rebuild sample-server %s --user-data-unset' % FAKE_UUID_1 + self.assertRaises(SystemExit, self.run_command, cmd, + api_version='2.56') + + def test_rebuild_files_2_57(self): + """Tests that trying to run the rebuild command with the --file option + after microversion 2.56 fails. + """ + testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt') + cmd = 'rebuild sample-server %s --file /tmp/foo=%s' + self.assertRaises(SystemExit, self.run_command, + cmd % (FAKE_UUID_1, testfile), api_version='2.57') + + def test_rebuild_change_user_data(self): + self.run_command('rebuild sample-server %s --user-data test' % + FAKE_UUID_1, api_version='2.57') + user_data = servers.ServerManager.transform_userdata('test') + self.assert_called('GET', '/servers?name=sample-server', pos=0) + self.assert_called('GET', '/servers/1234', pos=1) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=2) + self.assert_called('POST', '/servers/1234/action', + {'rebuild': {'imageRef': FAKE_UUID_1, + 'user_data': user_data, + 'description': None}}, pos=3) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=4) + + def test_rebuild_unset_user_data(self): + self.run_command('rebuild sample-server %s --user-data-unset' % + FAKE_UUID_1, api_version='2.57') + self.assert_called('GET', '/servers?name=sample-server', pos=0) + self.assert_called('GET', '/servers/1234', pos=1) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=2) + self.assert_called('POST', '/servers/1234/action', + {'rebuild': {'imageRef': FAKE_UUID_1, + 'user_data': None, + 'description': None}}, pos=3) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=4) + + def test_rebuild_user_data_and_unset_user_data(self): + """Tests that trying to set --user-data and --unset-user-data in the + same rebuild call fails. + """ + cmd = ('rebuild sample-server %s --user-data x --user-data-unset' % + FAKE_UUID_1) + ex = self.assertRaises(exceptions.CommandError, self.run_command, cmd, + api_version='2.57') + self.assertIn("Cannot specify '--user-data-unset' with " + "'--user-data'.", six.text_type(ex)) + def test_start(self): self.run_command('start sample-server') self.assert_called('POST', '/servers/1234/action', {'os-start': None}) @@ -2643,6 +2710,17 @@ def test_quota_update_fixed_ip(self): 'PUT', '/os-quota-sets/97f4c221bff44578b0300df4ef119353', {'quota_set': {'fixed_ips': 5}}) + def test_quota_update_injected_file_2_57(self): + """Tests that trying to update injected_file* quota with microversion + 2.57 fails. + """ + for quota in ('--injected-files', '--injected-file-content-bytes', + '--injected-file-path-bytes'): + cmd = ('quota-update 97f4c221bff44578b0300df4ef119353 %s=5' % + quota) + self.assertRaises(SystemExit, self.run_command, cmd, + api_version='2.57') + def test_quota_delete(self): self.run_command('quota-delete --tenant ' '97f4c221bff44578b0300df4ef119353') @@ -2680,6 +2758,16 @@ def test_quota_class_update(self): 'PUT', '/os-quota-class-sets/97f4c221bff44578b0300df4ef119353', body) + def test_quota_class_update_injected_file_2_57(self): + """Tests that trying to update injected_file* quota with microversion + 2.57 fails. + """ + for quota in ('--injected-files', '--injected-file-content-bytes', + '--injected-file-path-bytes'): + cmd = 'quota-class-update default %s=5' % quota + self.assertRaises(SystemExit, self.run_command, cmd, + api_version='2.57') + def test_backup(self): out, err = self.run_command('backup sample-server back1 daily 1') # With microversion < 2.45 there is no output from this command. @@ -2712,8 +2800,9 @@ def test_backup_2_45(self): 'rotation': '1'}}) def test_limits(self): - self.run_command('limits') + out = self.run_command('limits')[0] self.assert_called('GET', '/limits') + self.assertIn('Personality', out) self.run_command('limits --reserved') self.assert_called('GET', '/limits?reserved=1') @@ -2725,6 +2814,14 @@ def test_limits(self): self.assertIn('Verb', stdout) self.assertIn('Name', stdout) + def test_limits_2_57(self): + """Tests the limits command at microversion 2.57 where personality + size limits should not be shown. + """ + out = self.run_command('limits', api_version='2.57')[0] + self.assert_called('GET', '/limits') + self.assertNotIn('Personality', out) + def test_evacuate(self): self.run_command('evacuate sample-server new_host') self.assert_called('POST', '/servers/1234/action', @@ -3128,6 +3225,7 @@ def test_versions(self): 51, # There are no version-wrapped shell method changes for this. 52, # There are no version-wrapped shell method changes for this. 54, # There are no version-wrapped shell method changes for this. + 57, # There are no version-wrapped shell method changes for this. ]) versions_supported = set(range(0, novaclient.API_MAX_VERSION.ver_minor + 1)) diff --git a/novaclient/v2/quota_classes.py b/novaclient/v2/quota_classes.py index eae5bfdec..917cc9c43 100644 --- a/novaclient/v2/quota_classes.py +++ b/novaclient/v2/quota_classes.py @@ -50,7 +50,7 @@ def update(self, class_name, **kwargs): # NOTE(mriedem): 2.50 does strict validation of the resources you can # specify since the network-related resources are blocked in 2.50. - @api_versions.wraps("2.50") + @api_versions.wraps("2.50", "2.56") def update(self, class_name, instances=None, cores=None, ram=None, metadata_items=None, injected_files=None, injected_file_content_bytes=None, injected_file_path_bytes=None, @@ -81,3 +81,30 @@ def update(self, class_name, instances=None, cores=None, ram=None, body = {'quota_class_set': resources} return self._update('/os-quota-class-sets/%s' % class_name, body, 'quota_class_set') + + # NOTE(mriedem): 2.57 deprecates the usage of injected_files, + # injected_file_content_bytes and injected_file_path_bytes so those + # kwargs are removed. + @api_versions.wraps("2.57") + def update(self, class_name, instances=None, cores=None, ram=None, + metadata_items=None, key_pairs=None, server_groups=None, + server_group_members=None): + resources = {} + if instances is not None: + resources['instances'] = instances + if cores is not None: + resources['cores'] = cores + if ram is not None: + resources['ram'] = ram + if metadata_items is not None: + resources['metadata_items'] = metadata_items + if key_pairs is not None: + resources['key_pairs'] = key_pairs + if server_groups is not None: + resources['server_groups'] = server_groups + if server_group_members is not None: + resources['server_group_members'] = server_group_members + + body = {'quota_class_set': resources} + return self._update('/os-quota-class-sets/%s' % class_name, body, + 'quota_class_set') diff --git a/novaclient/v2/quotas.py b/novaclient/v2/quotas.py index 0e421169b..82249f25e 100644 --- a/novaclient/v2/quotas.py +++ b/novaclient/v2/quotas.py @@ -13,6 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient import api_versions from novaclient import base @@ -38,6 +39,10 @@ def get(self, tenant_id, user_id=None, detail=False): return self._get(url % params, "quota_set") + # NOTE(mriedem): Before 2.57 the resources you could update was just a + # kwargs dict and not validated on the client-side, only on the API server + # side. + @api_versions.wraps("2.0", "2.56") def update(self, tenant_id, **kwargs): user_id = kwargs.pop('user_id', None) @@ -53,6 +58,40 @@ def update(self, tenant_id, **kwargs): url = '/os-quota-sets/%s' % tenant_id return self._update(url, body, 'quota_set') + # NOTE(mriedem): 2.57 does strict validation of the resources you can + # specify. 2.36 blocks network-related resources and 2.57 blocks + # injected files related quotas. + @api_versions.wraps("2.57") + def update(self, tenant_id, user_id=None, force=False, + instances=None, cores=None, ram=None, + metadata_items=None, key_pairs=None, server_groups=None, + server_group_members=None): + + resources = {} + if force: + resources['force'] = force + if instances is not None: + resources['instances'] = instances + if cores is not None: + resources['cores'] = cores + if ram is not None: + resources['ram'] = ram + if metadata_items is not None: + resources['metadata_items'] = metadata_items + if key_pairs is not None: + resources['key_pairs'] = key_pairs + if server_groups is not None: + resources['server_groups'] = server_groups + if server_group_members is not None: + resources['server_group_members'] = server_group_members + body = {'quota_set': resources} + + if user_id: + url = '/os-quota-sets/%s?user_id=%s' % (tenant_id, user_id) + else: + url = '/os-quota-sets/%s' % tenant_id + return self._update(url, body, 'quota_set') + def defaults(self, tenant_id): return self._get('/os-quota-sets/%s/defaults' % tenant_id, 'quota_set') diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index e305253ed..79ab0cdce 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -621,6 +621,27 @@ def __str__(self): class ServerManager(base.BootingManagerWithFind): resource_class = Server + @staticmethod + def transform_userdata(userdata): + if hasattr(userdata, 'read'): + userdata = userdata.read() + + # NOTE(melwitt): Text file data is converted to bytes prior to + # base64 encoding. The utf-8 encoding will fail for binary files. + if six.PY3: + try: + userdata = userdata.encode("utf-8") + except AttributeError: + # In python 3, 'bytes' object has no attribute 'encode' + pass + else: + try: + userdata = encodeutils.safe_encode(userdata) + except UnicodeDecodeError: + pass + + return base64.b64encode(userdata).decode('utf-8') + def _boot(self, response_key, name, image, flavor, meta=None, files=None, userdata=None, reservation_id=False, return_raw=False, min_count=None, @@ -639,25 +660,7 @@ def _boot(self, response_key, name, image, flavor, "flavorRef": str(base.getid(flavor)), }} if userdata: - if hasattr(userdata, 'read'): - userdata = userdata.read() - - # NOTE(melwitt): Text file data is converted to bytes prior to - # base64 encoding. The utf-8 encoding will fail for binary files. - if six.PY3: - try: - userdata = userdata.encode("utf-8") - except AttributeError: - # In python 3, 'bytes' object has no attribute 'encode' - pass - else: - try: - userdata = encodeutils.safe_encode(userdata) - except UnicodeDecodeError: - pass - - userdata_b64 = base64.b64encode(userdata).decode('utf-8') - body["server"]["user_data"] = userdata_b64 + body["server"]["user_data"] = self.transform_userdata(userdata) if meta: body["server"]["metadata"] = meta if reservation_id: @@ -1204,6 +1207,7 @@ def create(self, name, image, flavor, meta=None, files=None, are the file contents (either as a string or as a file-like object). A maximum of five entries is allowed, and each file must be 10k or less. + (deprecated starting with microversion 2.57) :param reservation_id: return a reservation_id for the set of servers being requested, boolean. :param min_count: (optional extension) The minimum number of @@ -1284,6 +1288,10 @@ def create(self, name, image, flavor, meta=None, files=None, if "tags" in kwargs and self.api_version < boot_tags_microversion: raise exceptions.UnsupportedAttribute("tags", "2.52") + personality_files_deprecation = api_versions.APIVersion('2.57') + if files and self.api_version >= personality_files_deprecation: + raise exceptions.UnsupportedAttribute('files', '2.0', '2.56') + boot_kwargs = dict( meta=meta, files=files, userdata=userdata, reservation_id=reservation_id, min_count=min_count, @@ -1397,11 +1405,17 @@ def rebuild(self, server, image, password=None, disk_config=None, are the file contents (either as a string or as a file-like object). A maximum of five entries is allowed, and each file must be 10k or less. + (deprecated starting with microversion 2.57) :param description: optional description of the server (allowed since microversion 2.19) :param key_name: optional key pair name for rebuild operation; passing None will unset the key for the server instance (starting from microversion 2.54) + :param userdata: optional user data to pass to be exposed by the + metadata server; this can be a file type object as + well or a string. If None is specified, the existing + user_data is unset. + (starting from microversion 2.57) :returns: :class:`Server` """ descr_microversion = api_versions.APIVersion("2.19") @@ -1414,6 +1428,14 @@ def rebuild(self, server, image, password=None, disk_config=None, self.api_version < api_versions.APIVersion('2.54')): raise exceptions.UnsupportedAttribute('key_name', '2.54') + # Microversion 2.57 deprecates personality files and adds support + # for user_data. + files_and_userdata = api_versions.APIVersion('2.57') + if files and self.api_version >= files_and_userdata: + raise exceptions.UnsupportedAttribute('files', '2.0', '2.56') + if 'userdata' in kwargs and self.api_version < files_and_userdata: + raise exceptions.UnsupportedAttribute('userdata', '2.57') + body = {'imageRef': base.getid(image)} if password is not None: body['adminPass'] = password @@ -1443,6 +1465,12 @@ def rebuild(self, server, image, password=None, disk_config=None, 'path': filepath, 'contents': cont, }) + if 'userdata' in kwargs: + # If userdata is specified but None, it means unset the existing + # user_data on the instance. + userdata = kwargs['userdata'] + body['user_data'] = (userdata if userdata is None else + self.transform_userdata(userdata)) resp, body = self._action_return_resp_and_body('rebuild', server, body, **kwargs) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 8ad018c4d..ecd72a260 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -391,19 +391,22 @@ def _boot(cs, args): meta = _meta_parsing(args.meta) - files = {} - for f in args.files: - try: - dst, src = f.split('=', 1) - files[dst] = open(src) - except IOError as e: - raise exceptions.CommandError(_("Can't open '%(src)s': %(exc)s") % - {'src': src, 'exc': e}) - except ValueError: - raise exceptions.CommandError(_("Invalid file argument '%s'. " - "File arguments must be of the " - "form '--file " - "'") % f) + include_files = cs.api_version < api_versions.APIVersion('2.57') + if include_files: + files = {} + for f in args.files: + try: + dst, src = f.split('=', 1) + files[dst] = open(src) + except IOError as e: + raise exceptions.CommandError( + _("Can't open '%(src)s': %(exc)s") % + {'src': src, 'exc': e}) + except ValueError: + raise exceptions.CommandError( + _("Invalid file argument '%s'. " + "File arguments must be of the " + "form '--file '") % f) # use the os-keypair extension key_name = None @@ -481,7 +484,6 @@ def _boot(cs, args): boot_kwargs = dict( meta=meta, - files=files, key_name=key_name, min_count=min_count, max_count=max_count, @@ -504,6 +506,9 @@ def _boot(cs, args): if 'tags' in args and args.tags: boot_kwargs["tags"] = args.tags.split(',') + if include_files: + boot_kwargs['files'] = files + return boot_args, boot_kwargs @@ -563,7 +568,8 @@ def _boot(cs, args): "on the new server. More files can be injected using multiple " "'--file' options. Limited by the 'injected_files' quota value. " "The default value is 5. You can get the current quota value by " - "'Personality' limit from 'nova limits' command.")) + "'Personality' limit from 'nova limits' command."), + start_version='2.0', end_version='2.56') @utils.arg( '--key-name', default=os.environ.get('NOVACLIENT_DEFAULT_KEY_NAME'), @@ -1770,7 +1776,8 @@ def do_reboot(cs, args): "on the new server. More files can be injected using multiple " "'--file' options. You may store up to 5 files by default. " "The maximum number of files is specified by the 'Personality' " - "limit reported by the 'nova limits' command.")) + "limit reported by the 'nova limits' command."), + start_version='2.0', end_version='2.56') @utils.arg( '--key-name', metavar='', @@ -1785,6 +1792,19 @@ def do_reboot(cs, args): help=_("Unset keypair in the server. " "Cannot be specified with the '--key-name' option."), start_version='2.54') +@utils.arg( + '--user-data', + default=None, + metavar='', + help=_("User data file to pass to be exposed by the metadata server."), + start_version='2.57') +@utils.arg( + '--user-data-unset', + action='store_true', + default=False, + help=_("Unset user_data in the server. Cannot be specified with the " + "'--user-data' option."), + start_version='2.57') def do_rebuild(cs, args): """Shutdown, re-image, and re-boot a server.""" server = _find_server(cs, args.server) @@ -1803,21 +1823,34 @@ def do_rebuild(cs, args): meta = _meta_parsing(args.meta) kwargs['meta'] = meta - files = {} - for f in args.files: - try: - dst, src = f.split('=', 1) - with open(src, 'r') as s: - files[dst] = s.read() - except IOError as e: - raise exceptions.CommandError(_("Can't open '%(src)s': %(exc)s") % - {'src': src, 'exc': e}) - except ValueError: - raise exceptions.CommandError(_("Invalid file argument '%s'. " - "File arguments must be of the " - "form '--file " - "'") % f) - kwargs['files'] = files + # 2.57 deprecates the --file option and adds the --user-data and + # --user-data-unset options. + if cs.api_version < api_versions.APIVersion('2.57'): + files = {} + for f in args.files: + try: + dst, src = f.split('=', 1) + with open(src, 'r') as s: + files[dst] = s.read() + except IOError as e: + raise exceptions.CommandError( + _("Can't open '%(src)s': %(exc)s") % + {'src': src, 'exc': e}) + except ValueError: + raise exceptions.CommandError( + _("Invalid file argument '%s'. " + "File arguments must be of the " + "form '--file '") % f) + kwargs['files'] = files + else: + if args.user_data_unset: + kwargs['userdata'] = None + if args.user_data: + raise exceptions.CommandError( + _("Cannot specify '--user-data-unset' with " + "'--user-data'.")) + elif args.user_data: + kwargs['userdata'] = args.user_data if cs.api_version >= api_versions.APIVersion('2.54'): if args.key_unset: @@ -3739,6 +3772,10 @@ def do_ssh(cs, args): # return floating_ips, fixed_ips, security_groups or security_group_members # as those are deprecated as networking service proxies and/or because # nova-network is deprecated. Similar to the 2.36 microversion. +# NOTE(mriedem): In the 2.57 microversion, the os-quota-sets and +# os-quota-class-sets APIs will no longer return injected_files, +# injected_file_content_bytes or injected_file_content_bytes since personality +# files (file injection) is deprecated starting with v2.57. _quota_resources = ['instances', 'cores', 'ram', 'floating_ips', 'fixed_ips', 'metadata_items', 'injected_files', 'injected_file_content_bytes', @@ -3942,6 +3979,7 @@ def do_quota_update(cs, args): # 2.36 does not support updating quota for floating IPs, fixed IPs, security # groups or security group rules. +# 2.57 does not support updating injected_file* quotas. @api_versions.wraps("2.36") @utils.arg( 'tenant', @@ -3978,19 +4016,22 @@ def do_quota_update(cs, args): metavar='', type=int, default=None, - help=_('New value for the "injected-files" quota.')) + help=_('New value for the "injected-files" quota.'), + start_version='2.36', end_version='2.56') @utils.arg( '--injected-file-content-bytes', metavar='', type=int, default=None, - help=_('New value for the "injected-file-content-bytes" quota.')) + help=_('New value for the "injected-file-content-bytes" quota.'), + start_version='2.36', end_version='2.56') @utils.arg( '--injected-file-path-bytes', metavar='', type=int, default=None, - help=_('New value for the "injected-file-path-bytes" quota.')) + help=_('New value for the "injected-file-path-bytes" quota.'), + start_version='2.36', end_version='2.56') @utils.arg( '--key-pairs', metavar='', @@ -4147,6 +4188,7 @@ def do_quota_class_update(cs, args): # 2.50 does not support updating quota class values for floating IPs, # fixed IPs, security groups or security group rules. +# 2.57 does not support updating injected_file* quotas. @api_versions.wraps("2.50") @utils.arg( 'class_name', @@ -4178,19 +4220,22 @@ def do_quota_class_update(cs, args): metavar='', type=int, default=None, - help=_('New value for the "injected-files" quota.')) + help=_('New value for the "injected-files" quota.'), + start_version='2.50', end_version='2.56') @utils.arg( '--injected-file-content-bytes', metavar='', type=int, default=None, - help=_('New value for the "injected-file-content-bytes" quota.')) + help=_('New value for the "injected-file-content-bytes" quota.'), + start_version='2.50', end_version='2.56') @utils.arg( '--injected-file-path-bytes', metavar='', type=int, default=None, - help=_('New value for the "injected-file-path-bytes" quota.')) + help=_('New value for the "injected-file-path-bytes" quota.'), + start_version='2.50', end_version='2.56') @utils.arg( '--key-pairs', metavar='', diff --git a/releasenotes/notes/microversion-v2_57-acae2ee11ddae4fb.yaml b/releasenotes/notes/microversion-v2_57-acae2ee11ddae4fb.yaml new file mode 100644 index 000000000..4625d506e --- /dev/null +++ b/releasenotes/notes/microversion-v2_57-acae2ee11ddae4fb.yaml @@ -0,0 +1,31 @@ +--- +features: + - | + Support is added for the 2.57 microversion: + + * A ``userdata`` keyword argument can be passed to the ``Server.rebuild`` + python API binding. If set to None, it will unset any existing userdata + on the server. + * The ``--user-data`` and ``--user-data-unset`` options are added to the + ``nova rebuild`` CLI. The options are mutually exclusive. Specifying + ``--user-data`` will overwrite the existing userdata in the server, and + ``--user-data-unset`` will unset any existing userdata on the server. +upgrade: + - | + Support is added for the 2.57 microversion: + + * The ``--file`` option for the ``nova boot`` and ``nova rebuild`` CLIs is + capped at the 2.56 microversion. Similarly, the ``file`` parameter to + the ``Server.create`` and ``Server.rebuild`` python API binding methods + is capped at 2.56. Users are recommended to use the ``--user-data`` + option instead. + * The ``--injected-files``, ``--injected-file-content-bytes`` and + ``--injected-file-path-bytes`` options are capped at the 2.56 + microversion in the ``nova quota-update`` and ``nova quota-class-update`` + commands. + * The ``maxPersonality`` and ``maxPersonalitySize`` fields are capped at + the 2.56 microversion in the ``nova limits`` command and API binding. + * The ``injected_files``, ``injected_file_content_bytes`` and + ``injected_file_path_bytes`` entries are capped at version 2.56 from + the output of the ``nova quota-show`` and ``nova quota-class-show`` + commands and related python API bindings. From 8d80a5e0995336488373fff0885e13b1baa50923 Mon Sep 17 00:00:00 2001 From: Yikun Jiang Date: Mon, 18 Dec 2017 10:40:01 +0800 Subject: [PATCH 1403/1705] Microversion 2.58 - Instance actions list pagination Add optional parameters 'limit', 'marker' and 'changes-since' to the os-instance-actions endpoints for pagination. Implement: blueprint pagination-add-changes-since-for-instance-action-list Change-Id: Ie66d9b00c90236fdcc01aed7649dc7f163aa323e --- novaclient/__init__.py | 2 +- .../functional/v2/test_instance_action.py | 56 +++++++++++++++++++ novaclient/tests/unit/v2/fakes.py | 29 ++++++---- .../tests/unit/v2/test_instance_actions.py | 22 ++++++++ novaclient/tests/unit/v2/test_shell.py | 43 ++++++++++++++ novaclient/v2/instance_action.py | 29 ++++++++++ novaclient/v2/shell.py | 51 +++++++++++++++++ .../microversion-v2_58-327c1031ebfe4a3a.yaml | 8 +++ 8 files changed, 227 insertions(+), 13 deletions(-) create mode 100644 releasenotes/notes/microversion-v2_58-327c1031ebfe4a3a.yaml diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 2103d5cea..66520e666 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.57") +API_MAX_VERSION = api_versions.APIVersion("2.58") diff --git a/novaclient/tests/functional/v2/test_instance_action.py b/novaclient/tests/functional/v2/test_instance_action.py index 728dbb0bc..a318b6e7e 100644 --- a/novaclient/tests/functional/v2/test_instance_action.py +++ b/novaclient/tests/functional/v2/test_instance_action.py @@ -10,6 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +import time + +from oslo_utils import timeutils from oslo_utils import uuidutils import six from tempest.lib import exceptions @@ -57,3 +60,56 @@ def test_show_and_list_actions_on_deleted_instance(self): # ensure that obtained action is "create". self.assertEqual("create", self._get_value_from_the_table(output, "action")) + + +class TestInstanceActionCLIV258(TestInstanceActionCLI): + """Instance action functional tests for v2.58 nova-api microversion.""" + + COMPUTE_API_VERSION = "2.58" + + def test_list_instance_action_with_marker_and_limit(self): + server = self._create_server() + server.stop() + # The actions are sorted by created_at in descending order, + # and now we have two actions: create and stop. + output = self.nova("instance-action-list %s --limit 1" % server.id) + marker_req = self._get_column_value_from_single_row_table( + output, "Request_ID") + action = self._get_list_of_values_from_single_column_table( + output, "Action") + # The stop action was most recently created so it's what + # we get back when limit=1. + self.assertEqual(action, ['stop']) + + output = self.nova("instance-action-list %s --limit 1 " + "--marker %s" % (server.id, marker_req)) + action = self._get_list_of_values_from_single_column_table( + output, "Action") + self.assertEqual(action, ['create']) + + def test_list_instance_action_with_changes_since(self): + # Ignore microseconds to make this a deterministic test. + before_create = timeutils.utcnow().replace(microsecond=0).isoformat() + server = self._create_server() + time.sleep(2) + before_stop = timeutils.utcnow().replace(microsecond=0).isoformat() + server.stop() + + create_output = self.nova( + "instance-action-list %s --changes-since %s" % + (server.id, before_create)) + action = self._get_list_of_values_from_single_column_table( + create_output, "Action") + # The actions are sorted by created_at in descending order. + self.assertEqual(action, ['create', 'stop']) + + stop_output = self.nova("instance-action-list %s --changes-since %s" % + (server.id, before_stop)) + action = self._get_list_of_values_from_single_column_table( + stop_output, "Action") + # Provide detailed debug information if this fails. + self.assertEqual(action, ['stop'], + 'Expected to find the stop action with ' + '--changes-since=%s but got: %s\n\n' + 'First instance-action-list output: %s' % + (before_stop, stop_output, create_output)) diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 11789ec19..7c8f95285 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -1948,26 +1948,31 @@ def delete_servers_1234_os_volume_attachments_Work(self, **kw): return (200, FAKE_RESPONSE_HEADERS, {}) def get_servers_1234_os_instance_actions(self, **kw): - return (200, FAKE_RESPONSE_HEADERS, { - "instanceActions": - [{"instance_uuid": "1234", + action = {"instance_uuid": "1234", "user_id": "b968c25e04ab405f9fe4e6ca54cce9a5", "start_time": "2013-03-25T13:45:09.000000", "request_id": "req-abcde12345", "action": "create", "message": None, - "project_id": "04019601fe3648c0abd4f4abfb9e6106"}]}) + "project_id": "04019601fe3648c0abd4f4abfb9e6106"} + if self.api_version >= api_versions.APIVersion('2.58'): + # This is intentionally different from the start_time. + action['updated_at'] = '2013-03-25T13:50:09.000000' + return (200, FAKE_RESPONSE_HEADERS, { + "instanceActions": [action]}) def get_servers_1234_os_instance_actions_req_abcde12345(self, **kw): + action = {"instance_uuid": "1234", + "user_id": "b968c25e04ab405f9fe4e6ca54cce9a5", + "start_time": "2013-03-25T13:45:09.000000", + "request_id": "req-abcde12345", + "action": "create", + "message": None, + "project_id": "04019601fe3648c0abd4f4abfb9e6106"} + if self.api_version >= api_versions.APIVersion('2.58'): + action['updated_at'] = '2013-03-25T13:45:09.000000' return (200, FAKE_RESPONSE_HEADERS, { - "instanceAction": - {"instance_uuid": "1234", - "user_id": "b968c25e04ab405f9fe4e6ca54cce9a5", - "start_time": "2013-03-25T13:45:09.000000", - "request_id": "req-abcde12345", - "action": "create", - "message": None, - "project_id": "04019601fe3648c0abd4f4abfb9e6106"}}) + "instanceAction": action}) def post_servers_uuid1_action(self, **kw): return 202, {}, {} diff --git a/novaclient/tests/unit/v2/test_instance_actions.py b/novaclient/tests/unit/v2/test_instance_actions.py index 8eed17197..e26bf7a50 100644 --- a/novaclient/tests/unit/v2/test_instance_actions.py +++ b/novaclient/tests/unit/v2/test_instance_actions.py @@ -16,6 +16,7 @@ from novaclient import api_versions from novaclient.tests.unit import utils from novaclient.tests.unit.v2 import fakes +from novaclient.v2 import instance_action class InstanceActionExtensionTests(utils.TestCase): @@ -39,3 +40,24 @@ def test_get_instance_action(self): self.cs.assert_called( 'GET', '/servers/%s/os-instance-actions/%s' % (server_uuid, request_id)) + + +class InstanceActionExtensionV258Tests(InstanceActionExtensionTests): + def setUp(self): + super(InstanceActionExtensionV258Tests, self).setUp() + self.cs.api_version = api_versions.APIVersion("2.58") + + def test_list_instance_actions_with_limit_marker_params(self): + server_uuid = '1234' + marker = '12140183-c814-4ddf-8453-6df43028aa67' + + ias = self.cs.instance_action.list( + server_uuid, marker=marker, limit=10, + changes_since='2016-02-29T06:23:22') + self.assert_request_id(ias, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called( + 'GET', + '/servers/%s/os-instance-actions?changes-since=%s&limit=10&' + 'marker=%s' % (server_uuid, '2016-02-29T06%3A23%3A22', marker)) + for ia in ias: + self.assertIsInstance(ia, instance_action.InstanceAction) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index fb21eb5e7..8f416cddd 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2976,6 +2976,49 @@ def test_instance_action_get(self): 'GET', '/servers/1234/os-instance-actions/req-abcde12345') + def test_instance_action_list_marker_pre_v258_not_allowed(self): + cmd = 'instance-action-list sample-server --marker %s' + self.assertRaises(SystemExit, self.run_command, + cmd % FAKE_UUID_1, api_version='2.57') + + def test_instance_action_list_limit_pre_v258_not_allowed(self): + cmd = 'instance-action-list sample-server --limit 10' + self.assertRaises(SystemExit, self.run_command, + cmd, api_version='2.57') + + def test_instance_action_list_changes_since_pre_v258_not_allowed(self): + cmd = 'instance-action-list sample-server --changes-since ' \ + '2016-02-29T06:23:22' + self.assertRaises(SystemExit, self.run_command, + cmd, api_version='2.57') + + def test_instance_action_list_limit_marker_v258(self): + out = self.run_command('instance-action-list sample-server --limit 10 ' + '--marker %s' % FAKE_UUID_1, + api_version='2.58')[0] + # Assert that the updated_at value is in the output. + self.assertIn('2013-03-25T13:50:09.000000', out) + self.assert_called( + 'GET', + '/servers/1234/os-instance-actions?' + 'limit=10&marker=%s' % FAKE_UUID_1) + + def test_instance_action_list_with_changes_since_v258(self): + self.run_command('instance-action-list sample-server ' + '--changes-since 2016-02-29T06:23:22', + api_version='2.58') + self.assert_called( + 'GET', + '/servers/1234/os-instance-actions?' + 'changes-since=2016-02-29T06%3A23%3A22') + + def test_instance_action_list_with_changes_since_invalid_value_v258(self): + ex = self.assertRaises( + exceptions.CommandError, self.run_command, + 'instance-action-list sample-server --changes-since 0123456789', + api_version='2.58') + self.assertIn('Invalid changes-since value', six.text_type(ex)) + def test_cell_show(self): self.run_command('cell-show child_cell') self.assert_called('GET', '/os-cells/child_cell') diff --git a/novaclient/v2/instance_action.py b/novaclient/v2/instance_action.py index 5531d35fc..b8316c2d7 100644 --- a/novaclient/v2/instance_action.py +++ b/novaclient/v2/instance_action.py @@ -13,6 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient import api_versions from novaclient import base @@ -32,9 +33,37 @@ def get(self, server, request_id): return self._get("/servers/%s/os-instance-actions/%s" % (base.getid(server), request_id), 'instanceAction') + @api_versions.wraps("2.0", "2.57") def list(self, server): """ Get a list of actions performed on a server. + + :param server: The :class:`Server` (or its ID) """ return self._list('/servers/%s/os-instance-actions' % base.getid(server), 'instanceActions') + + @api_versions.wraps("2.58") + def list(self, server, marker=None, limit=None, changes_since=None): + """ + Get a list of actions performed on a server. + + :param server: The :class:`Server` (or its ID) + :param marker: Begin returning actions that appear later in the action + list than that represented by this action request id + (optional). + :param limit: Maximum number of actions to return. (optional). + :param changes_since: List only instance actions changed after a + certain point of time. The provided time should + be an ISO 8061 formatted time. ex + 2016-03-04T06:27:59Z . (optional). + """ + opts = {} + if marker: + opts['marker'] = marker + if limit: + opts['limit'] = limit + if changes_since: + opts['changes-since'] = changes_since + return self._list('/servers/%s/os-instance-actions' % + base.getid(server), 'instanceActions', filters=opts) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index ecd72a260..56828e9fb 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -4864,6 +4864,7 @@ def do_instance_action(cs, args): utils.print_dict(action) +@api_versions.wraps("2.0", "2.57") @utils.arg( 'server', metavar='', @@ -4887,6 +4888,56 @@ def do_instance_action_list(cs, args): sortby_index=3) +@api_versions.wraps("2.58") +@utils.arg( + 'server', + metavar='', + help=_('Name or UUID of the server to list actions for. Only UUID can be ' + 'used to list actions on a deleted server.')) +@utils.arg( + '--marker', + dest='marker', + metavar='', + default=None, + help=_('The last instance action of the previous page; displays list of ' + 'actions after "marker".')) +@utils.arg( + '--limit', + dest='limit', + metavar='', + type=int, + default=None, + help=_('Maximum number of instance actions to display. Note that there ' + 'is a configurable max limit on the server, and the limit that is ' + 'used will be the minimum between what is requested here and what ' + 'is configured in the server.')) +@utils.arg( + '--changes-since', + dest='changes_since', + metavar='', + default=None, + help=_('List only instance actions changed after a certain point of ' + 'time. The provided time should be an ISO 8061 formatted time. ' + 'ex 2016-03-04T06:27:59Z.')) +def do_instance_action_list(cs, args): + """List actions on a server.""" + server = _find_server(cs, args.server, raise_if_notfound=False) + if args.changes_since: + try: + timeutils.parse_isotime(args.changes_since) + except ValueError: + raise exceptions.CommandError(_('Invalid changes-since value: %s') + % args.changes_since) + actions = cs.instance_action.list(server, marker=args.marker, + limit=args.limit, + changes_since=args.changes_since) + # TODO(yikun): Output a "Marker" column if there is a next link? + utils.print_list(actions, + ['Action', 'Request_ID', 'Message', 'Start_Time', + 'Updated_At'], + sortby_index=3) + + def do_list_extensions(cs, _args): """ List all the os-api extensions that are available. diff --git a/releasenotes/notes/microversion-v2_58-327c1031ebfe4a3a.yaml b/releasenotes/notes/microversion-v2_58-327c1031ebfe4a3a.yaml new file mode 100644 index 000000000..b2fb6a3da --- /dev/null +++ b/releasenotes/notes/microversion-v2_58-327c1031ebfe4a3a.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Added support for microversion v2.58 which introduces pagination support + for instance actions with the help of new optional parameters ``limit``, + ``marker``, and also adds the new filter ``changes-since``. Users can use + ``changes-since`` filter to filter the results based on the last time the + instance action was updated. From 3e6119becabd4d8b83c138d0d455adfddc33cbfa Mon Sep 17 00:00:00 2001 From: Yikun Jiang Date: Mon, 15 Jan 2018 17:14:32 +0800 Subject: [PATCH 1404/1705] Add missing spaces in `nova list --changes-since` help trivialfix Change-Id: I394bfa9c2232f5cb150be35aa25d7b895fc40046 --- novaclient/v2/shell.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 8ad018c4d..3d8edb633 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -1442,8 +1442,8 @@ def _print_flavor(flavor): dest='changes_since', metavar='', default=None, - help=_("List only servers changed after a certain point of time." - "The provided time should be an ISO 8061 formatted time." + help=_("List only servers changed after a certain point of time. " + "The provided time should be an ISO 8061 formatted time. " "ex 2016-03-04T06:27:59Z .")) @utils.arg( '--tags', From bfd43e6402938a489cfb4b9e4953884e97970508 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Thu, 18 Jan 2018 03:30:06 +0000 Subject: [PATCH 1405/1705] Updated from global requirements Change-Id: Ie5cb08fa1b2724e7065009136b7fb4ce49e35a08 --- doc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index 91ff9ceac..647bb1e38 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,6 +1,6 @@ # 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. -sphinx>=1.6.2 # BSD +sphinx!=1.6.6,>=1.6.2 # BSD openstackdocstheme>=1.17.0 # Apache-2.0 reno>=2.5.0 # Apache-2.0 From 1d9322a2f5d91e6e4d163df7b4a89e3ac1743e5b Mon Sep 17 00:00:00 2001 From: sunjiazz Date: Thu, 18 Jan 2018 15:15:50 +0800 Subject: [PATCH 1406/1705] Update documentation links Update doc links according to OpenStack document migration Change-Id: I189f2b31456203c80234a44cbf87534f8c159ed0 --- doc/source/cli/nova.rst | 2 +- doc/source/contributor/testing.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/cli/nova.rst b/doc/source/cli/nova.rst index 05308ba49..1a35bcf08 100644 --- a/doc/source/cli/nova.rst +++ b/doc/source/cli/nova.rst @@ -72,7 +72,7 @@ Terminate an instance:: SEE ALSO ======== -OpenStack Nova CLI Guide: http://docs.openstack.org/cli-reference/nova.html +OpenStack Nova CLI Guide: https://docs.openstack.org/cli-reference/nova.html BUGS diff --git a/doc/source/contributor/testing.rst b/doc/source/contributor/testing.rst index b789dc2d2..9b01c40ef 100644 --- a/doc/source/contributor/testing.rst +++ b/doc/source/contributor/testing.rst @@ -25,4 +25,4 @@ DevStack installation with a demo and an admin user/tenant - or clouds named Refer to `Consistent Testing Interface`__ for more details. -__ http://git.openstack.org/cgit/openstack/governance/tree/reference/project-testing-interface.rst +__ https://git.openstack.org/cgit/openstack/governance/tree/reference/project-testing-interface.rst From ad0c036fb6102a91e66d4004c78f034a21afd8e6 Mon Sep 17 00:00:00 2001 From: Yikun Jiang Date: Mon, 30 Oct 2017 10:48:58 +0800 Subject: [PATCH 1407/1705] Microversion 2.59 - Migrations list pagination Add optional parameters 'limit', 'marker' and 'changes-since' to the os-migrations endpoints for pagination. /os-migrations?limit={limit}&marker={migrations_uuid} /os-migrations?changes-since={changes-since} Change-Id: I7437a61ee07c339b43d008204d1416044a407b68 Implement: blueprint add-pagination-and-change-since-for-migration-list Depends-on: I7e01f95d7173d9217f76e838b3ea71555151ef56 --- novaclient/__init__.py | 2 +- novaclient/tests/unit/v2/fakes.py | 4 ++ novaclient/tests/unit/v2/test_migrations.py | 55 +++++++++++++--- novaclient/tests/unit/v2/test_shell.py | 43 ++++++++++++- novaclient/v2/migrations.py | 47 ++++++++++++-- novaclient/v2/shell.py | 64 +++++++++++++++++++ .../microversion-v2_59-4160c852d7d8812d.yaml | 8 +++ 7 files changed, 206 insertions(+), 17 deletions(-) create mode 100644 releasenotes/notes/microversion-v2_59-4160c852d7d8812d.yaml diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 66520e666..3392aabc9 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.58") +API_MAX_VERSION = api_versions.APIVersion("2.59") diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 7c8f95285..4647d8055 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -2049,6 +2049,10 @@ def get_os_migrations(self, **kw): migration1.update({"migration_type": "live-migration"}) migration2.update({"migration_type": "live-migration"}) + if self.api_version >= api_versions.APIVersion("2.59"): + migration1.update({"uuid": "11111111-07d5-11e1-90e3-e3dffe0c5983"}) + migration2.update({"uuid": "22222222-07d5-11e1-90e3-e3dffe0c5983"}) + migration_list = [] instance_uuid = kw.get('instance_uuid', None) if instance_uuid == migration1['instance_uuid']: diff --git a/novaclient/tests/unit/v2/test_migrations.py b/novaclient/tests/unit/v2/test_migrations.py index 4054012fe..4efa3cb44 100644 --- a/novaclient/tests/unit/v2/test_migrations.py +++ b/novaclient/tests/unit/v2/test_migrations.py @@ -28,15 +28,7 @@ def test_list_migrations(self): for m in ml: self.assertIsInstance(m, migrations.Migration) self.assertRaises(AttributeError, getattr, m, "migration_type") - - def test_list_migrations_v223(self): - cs = fakes.FakeClient(api_versions.APIVersion("2.23")) - ml = cs.migrations.list() - self.assert_request_id(ml, fakes.FAKE_REQUEST_ID_LIST) - cs.assert_called('GET', '/os-migrations') - for m in ml: - self.assertIsInstance(m, migrations.Migration) - self.assertEqual(m.migration_type, 'live-migration') + self.assertRaises(AttributeError, getattr, m, "uuid") def test_list_migrations_with_filters(self): ml = self.cs.migrations.list('host1', 'finished') @@ -58,3 +50,48 @@ def test_list_migrations_with_instance_uuid_filter(self): 'instance_uuid=instance_id_456&status=finished')) self.assertEqual(1, len(ml)) self.assertEqual('instance_id_456', ml[0].instance_uuid) + + +class MigrationsV223Test(MigrationsTest): + def setUp(self): + super(MigrationsV223Test, self).setUp() + self.cs.api_version = api_versions.APIVersion("2.23") + + def test_list_migrations(self): + ml = self.cs.migrations.list() + self.assert_request_id(ml, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called('GET', '/os-migrations') + for m in ml: + self.assertIsInstance(m, migrations.Migration) + self.assertEqual(m.migration_type, 'live-migration') + self.assertRaises(AttributeError, getattr, m, "uuid") + + +class MigrationsV259Test(MigrationsV223Test): + def setUp(self): + super(MigrationsV259Test, self).setUp() + self.cs.api_version = api_versions.APIVersion("2.59") + + def test_list_migrations(self): + ml = self.cs.migrations.list() + self.assert_request_id(ml, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called('GET', '/os-migrations') + for m in ml: + self.assertIsInstance(m, migrations.Migration) + self.assertEqual(m.migration_type, 'live-migration') + self.assertTrue(hasattr(m, 'uuid')) + + def test_list_migrations_with_limit_marker_params(self): + marker = '12140183-c814-4ddf-8453-6df43028aa67' + params = {'limit': 10, + 'marker': marker, + 'changes_since': '2012-02-29T06:23:22'} + + ms = self.cs.migrations.list(**params) + self.assert_request_id(ms, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called('GET', + '/os-migrations?' + 'changes-since=%s&limit=10&marker=%s' + % ('2012-02-29T06%3A23%3A22', marker)) + for m in ms: + self.assertIsInstance(m, migrations.Migration) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 8f416cddd..bba9f7538 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -3036,14 +3036,55 @@ def test_migration_list(self): self.assert_called('GET', '/os-migrations') def test_migration_list_v223(self): - self.run_command('migration-list', api_version="2.23") + out, _ = self.run_command('migration-list', api_version="2.23") self.assert_called('GET', '/os-migrations') + # Make sure there is no UUID in the output. Uses "| UUID" to + # avoid collisions with the "Instance UUID" column. + self.assertNotIn('| UUID', out) def test_migration_list_with_filters(self): self.run_command('migration-list --host host1 --status finished') self.assert_called('GET', '/os-migrations?host=host1&status=finished') + def test_migration_list_marker_pre_v259_not_allowed(self): + cmd = 'migration-list --marker %s' + self.assertRaises(SystemExit, self.run_command, + cmd % FAKE_UUID_1, api_version='2.58') + + def test_migration_list_limit_pre_v259_not_allowed(self): + cmd = 'migration-list --limit 10' + self.assertRaises(SystemExit, self.run_command, + cmd, api_version='2.58') + + def test_migration_list_changes_since_pre_v259_not_allowed(self): + cmd = 'migration-list --changes-since 2016-02-29T06:23:22' + self.assertRaises(SystemExit, self.run_command, + cmd, api_version='2.58') + + def test_migration_list_limit_marker_v259(self): + out, _ = self.run_command( + 'migration-list --limit 10 --marker %s' % FAKE_UUID_1, + api_version='2.59') + self.assert_called( + 'GET', + '/os-migrations?limit=10&marker=%s' % FAKE_UUID_1) + # Make sure the UUID column is now in the output. Uses "| UUID" to + # avoid collisions with the "Instance UUID" column. + self.assertIn('| UUID', out) + + def test_migration_list_with_changes_since_v259(self): + self.run_command('migration-list --changes-since 2016-02-29T06:23:22', + api_version='2.59') + self.assert_called( + 'GET', '/os-migrations?changes-since=2016-02-29T06%3A23%3A22') + + def test_migration_list_with_changes_since_invalid_value_v259(self): + ex = self.assertRaises(exceptions.CommandError, self.run_command, + 'migration-list --changes-since 0123456789', + api_version='2.59') + self.assertIn('Invalid changes-since value', six.text_type(ex)) + @mock.patch('novaclient.v2.shell._find_server') @mock.patch('os.system') def test_ssh(self, mock_system, mock_find_server): diff --git a/novaclient/v2/migrations.py b/novaclient/v2/migrations.py index 10e8f37fb..00eb6d110 100644 --- a/novaclient/v2/migrations.py +++ b/novaclient/v2/migrations.py @@ -14,6 +14,7 @@ migration interface """ +from novaclient import api_versions from novaclient import base @@ -25,12 +26,8 @@ def __repr__(self): class MigrationManager(base.ManagerWithFind): resource_class = Migration - def list(self, host=None, status=None, instance_uuid=None): - """ - Get a list of migrations. - :param host: (optional) filter migrations by host name. - :param status: (optional) filter migrations by status. - """ + def _list_base(self, host=None, status=None, instance_uuid=None, + marker=None, limit=None, changes_since=None): opts = {} if host: opts['host'] = host @@ -38,5 +35,43 @@ def list(self, host=None, status=None, instance_uuid=None): opts['status'] = status if instance_uuid: opts['instance_uuid'] = instance_uuid + if marker: + opts['marker'] = marker + if limit: + opts['limit'] = limit + if changes_since: + opts['changes-since'] = changes_since return self._list("/os-migrations", "migrations", filters=opts) + + @api_versions.wraps("2.0", "2.58") + def list(self, host=None, status=None, instance_uuid=None): + """ + Get a list of migrations. + :param host: filter migrations by host name (optional). + :param status: filter migrations by status (optional). + :param instance_uuid: filter migrations by instance uuid (optional). + """ + return self._list_base(host=host, status=status, + instance_uuid=instance_uuid) + + @api_versions.wraps("2.59") + def list(self, host=None, status=None, instance_uuid=None, + marker=None, limit=None, changes_since=None): + """ + Get a list of migrations. + :param host: filter migrations by host name (optional). + :param status: filter migrations by status (optional). + :param instance_uuid: filter migrations by instance uuid (optional). + :param marker: Begin returning migrations that appear later in the + migrations list than that represented by this migration UUID + (optional). + :param limit: maximum number of migrations to return (optional). + :param changes_since: only return migrations updated after. The + provided time should be an ISO 8061 formatted time. ex + 2016-03-04T06:27:59Z . (optional). + """ + return self._list_base(host=host, status=status, + instance_uuid=instance_uuid, + marker=marker, limit=limit, + changes_since=changes_since) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 56828e9fb..4b0079a2e 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -4992,6 +4992,10 @@ def migration_type(migration): formatters = {'Old Flavor': old_flavor, 'New Flavor': new_flavor} + # Insert migrations UUID after ID + if cs.api_version >= api_versions.APIVersion("2.59"): + fields.insert(0, "UUID") + if cs.api_version >= api_versions.APIVersion("2.23"): fields.insert(0, "Id") fields.append("Type") @@ -5000,6 +5004,7 @@ def migration_type(migration): utils.print_list(migrations, fields, formatters) +@api_versions.wraps("2.0", "2.58") @utils.arg( '--instance-uuid', dest='instance_uuid', @@ -5020,3 +5025,62 @@ def do_migration_list(cs, args): migrations = cs.migrations.list(args.host, args.status, instance_uuid=args.instance_uuid) _print_migrations(cs, migrations) + + +@api_versions.wraps("2.59") +@utils.arg( + '--instance-uuid', + dest='instance_uuid', + metavar='', + help=_('Fetch migrations for the given instance.')) +@utils.arg( + '--host', + dest='host', + metavar='', + help=_('Fetch migrations for the given host.')) +@utils.arg( + '--status', + dest='status', + metavar='', + help=_('Fetch migrations for the given status.')) +@utils.arg( + '--marker', + dest='marker', + metavar='', + default=None, + help=_('The last migration of the previous page; displays list of ' + 'migrations after "marker". Note that the marker is the ' + 'migration UUID.')) +@utils.arg( + '--limit', + dest='limit', + metavar='', + type=int, + default=None, + help=_('Maximum number of migrations to display. Note that there is a ' + 'configurable max limit on the server, and the limit that is used ' + 'will be the minimum between what is requested here and what ' + 'is configured in the server.')) +@utils.arg( + '--changes-since', + dest='changes_since', + metavar='', + default=None, + help=_('List only migrations changed after a certain point of time. ' + 'The provided time should be an ISO 8061 formatted time. ' + 'ex 2016-03-04T06:27:59Z .')) +def do_migration_list(cs, args): + """Print a list of migrations.""" + if args.changes_since: + try: + timeutils.parse_isotime(args.changes_since) + except ValueError: + raise exceptions.CommandError(_('Invalid changes-since value: %s') + % args.changes_since) + + migrations = cs.migrations.list(args.host, args.status, + instance_uuid=args.instance_uuid, + marker=args.marker, limit=args.limit, + changes_since=args.changes_since) + # TODO(yikun): Output a "Marker" column if there is a next link? + _print_migrations(cs, migrations) diff --git a/releasenotes/notes/microversion-v2_59-4160c852d7d8812d.yaml b/releasenotes/notes/microversion-v2_59-4160c852d7d8812d.yaml new file mode 100644 index 000000000..e90c87b81 --- /dev/null +++ b/releasenotes/notes/microversion-v2_59-4160c852d7d8812d.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Added support for microversion v2.59 which introduces pagination support + for migrations with the help of new optional parameters ``limit``, + ``marker``, and also adds the new filter ``changes-since``. Users can use + ``changes-since`` filter to filter the results based on the last time the + migration was updated. From 4e94fe53618638c37285d61c322b663192678bfb Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Mon, 22 Jan 2018 18:26:21 -0500 Subject: [PATCH 1408/1705] Add support for microversion 2.60 - volume multiattach There are no client-side changes for this, we're just registering the microversion support. Someone would have to use this microversion when trying to attach a multiattach capable volume to a server. Related nova API change: I02120ef8767c3f9c9497bff67101e57e204ed6f4 Part of blueprint multi-attach-volume Change-Id: Iff590954e7e12ee630140024f70c98d3cfa14a31 --- novaclient/__init__.py | 2 +- novaclient/tests/unit/v2/test_shell.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 3392aabc9..c114e5b9c 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.59") +API_MAX_VERSION = api_versions.APIVersion("2.60") diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index bba9f7538..34c6717b5 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -3310,6 +3310,7 @@ def test_versions(self): 52, # There are no version-wrapped shell method changes for this. 54, # There are no version-wrapped shell method changes for this. 57, # There are no version-wrapped shell method changes for this. + 60, # There are no client-side changes for volume multiattach. ]) versions_supported = set(range(0, novaclient.API_MAX_VERSION.ver_minor + 1)) From d703219dea6b7462f40e0a233e300dc1accabc56 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Wed, 24 Jan 2018 01:31:04 +0000 Subject: [PATCH 1409/1705] Updated from global requirements Change-Id: I0aa83fc265f2c274d703fa402c09887b32204aa1 --- doc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index 647bb1e38..fa2038978 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -2,5 +2,5 @@ # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. sphinx!=1.6.6,>=1.6.2 # BSD -openstackdocstheme>=1.17.0 # Apache-2.0 +openstackdocstheme>=1.18.1 # Apache-2.0 reno>=2.5.0 # Apache-2.0 From a9aad884940987579bf141d680714e2a05ab5063 Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Thu, 25 Jan 2018 23:23:13 +0000 Subject: [PATCH 1410/1705] Update reno for stable/queens Change-Id: I241ad34451a8e1e26dd4bc679b80cc02da176d53 --- releasenotes/source/index.rst | 1 + releasenotes/source/queens.rst | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 releasenotes/source/queens.rst diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index cf41b5ca8..fff709d3d 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -8,6 +8,7 @@ Contents :maxdepth: 2 unreleased + queens pike ocata newton diff --git a/releasenotes/source/queens.rst b/releasenotes/source/queens.rst new file mode 100644 index 000000000..36ac6160c --- /dev/null +++ b/releasenotes/source/queens.rst @@ -0,0 +1,6 @@ +=================================== + Queens Series Release Notes +=================================== + +.. release-notes:: + :branch: stable/queens From d418b5f245f4cef4d35b8795aa6af8b98cd60141 Mon Sep 17 00:00:00 2001 From: int32bit Date: Sat, 10 Dec 2016 12:12:41 +0800 Subject: [PATCH 1411/1705] Add CLI to show instance usage audit logs Currently we can get instance usage audit logs via Nova API, and the docs also update for it. It is necessary to add that to our client and CLI. This patch adds the following command. nova instance-usage-audit-log [--before ] Co-Authored-by: Takashi Natsume Change-Id: I4ef8e40c322f1768ee1b5e01e9681cab0e2804bd --- .../v2/test_instance_usage_audit_log.py | 87 +++++++++++++++++++ novaclient/tests/unit/v2/fakes.py | 82 +++++++++++++++++ .../unit/v2/test_instance_usage_audit_log.py | 37 ++++++++ novaclient/tests/unit/v2/test_shell.py | 11 +++ novaclient/v2/client.py | 3 + novaclient/v2/instance_usage_audit_log.py | 40 +++++++++ novaclient/v2/shell.py | 17 ++++ ...nce-usage-audit-logs-7826b411fac1283b.yaml | 8 ++ 8 files changed, 285 insertions(+) create mode 100644 novaclient/tests/functional/v2/test_instance_usage_audit_log.py create mode 100644 novaclient/tests/unit/v2/test_instance_usage_audit_log.py create mode 100644 novaclient/v2/instance_usage_audit_log.py create mode 100644 releasenotes/notes/show-instance-usage-audit-logs-7826b411fac1283b.yaml diff --git a/novaclient/tests/functional/v2/test_instance_usage_audit_log.py b/novaclient/tests/functional/v2/test_instance_usage_audit_log.py new file mode 100644 index 000000000..5e7ce8892 --- /dev/null +++ b/novaclient/tests/functional/v2/test_instance_usage_audit_log.py @@ -0,0 +1,87 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import datetime + +from oslo_utils import timeutils + +from novaclient.tests.functional import base + + +class TestInstanceUsageAuditLogCLI(base.ClientTestBase): + COMPUTE_API_VERSION = '2.1' + + # NOTE(takashin): By default, 'instance_usage_audit' is False in nova. + # So the instance usage audit log is not recoreded. + # Therefore an empty result can be got. + # But it is tested here to call APIs and get responses normally. + + @staticmethod + def _get_begin_end_time(): + current = timeutils.utcnow() + + end = datetime.datetime(day=1, month=current.month, year=current.year) + year = end.year + + if current.month == 1: + year -= 1 + month = 12 + else: + month = current.month - 1 + + begin = datetime.datetime(day=1, month=month, year=year) + + return (begin, end) + + def test_get_os_instance_usage_audit_log(self): + (begin, end) = self._get_begin_end_time() + expected = { + 'hosts_not_run': '[]', + 'log': '{}', + 'num_hosts': '0', + 'num_hosts_done': '0', + 'num_hosts_not_run': '0', + 'num_hosts_running': '0', + 'overall_status': 'ALL hosts done. 0 errors.', + 'total_errors': '0', + 'total_instances': '0', + 'period_beginning': str(begin), + 'period_ending': str(end) + } + + output = self.nova('instance-usage-audit-log') + + for key in expected.keys(): + self.assertEqual(expected[key], + self._get_value_from_the_table(output, key)) + + def test_get_os_instance_usage_audit_log_with_before(self): + expected = { + 'hosts_not_run': '[]', + 'log': '{}', + 'num_hosts': '0', + 'num_hosts_done': '0', + 'num_hosts_not_run': '0', + 'num_hosts_running': '0', + 'overall_status': 'ALL hosts done. 0 errors.', + 'total_errors': '0', + 'total_instances': '0', + 'period_beginning': '2016-11-01 00:00:00', + 'period_ending': '2016-12-01 00:00:00' + } + + output = self.nova( + 'instance-usage-audit-log --before "2016-12-10 13:59:59.999999"') + + for key in expected.keys(): + self.assertEqual(expected[key], + self._get_value_from_the_table(output, key)) diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 4647d8055..24d1784d5 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -124,6 +124,8 @@ def _cs_request(self, url, method, **kwargs): munged_url = munged_url.replace(' ', '_') munged_url = munged_url.replace('!', '_') munged_url = munged_url.replace('@', '_') + munged_url = munged_url.replace('%20', '_') + munged_url = munged_url.replace('%3A', '_') callback = "%s_%s" % (method.lower(), munged_url) if url is None or callback == "get_http:__nova_api:8774": @@ -1974,6 +1976,86 @@ def get_servers_1234_os_instance_actions_req_abcde12345(self, **kw): return (200, FAKE_RESPONSE_HEADERS, { "instanceAction": action}) + def get_os_instance_usage_audit_log(self, **kw): + return (200, FAKE_RESPONSE_HEADERS, { + "instance_usage_audit_logs": { + "hosts_not_run": ["samplehost3"], + "log": { + "samplehost0": { + "errors": 1, + "instances": 1, + "message": ("Instance usage audit ran for host " + "samplehost0, 1 instances in 0.01 " + "seconds."), + "state": "DONE" + }, + "samplehost1": { + "errors": 1, + "instances": 2, + "message": ("Instance usage audit ran for host " + "samplehost1, 2 instances in 0.01 " + "seconds."), + "state": "DONE" + }, + "samplehost2": { + "errors": 1, + "instances": 3, + "message": ("Instance usage audit ran for host " + "samplehost2, 3 instances in 0.01 " + "seconds."), + "state": "DONE" + }, + }, + "num_hosts": 4, + "num_hosts_done": 3, + "num_hosts_not_run": 1, + "num_hosts_running": 0, + "overall_status": "3 of 4 hosts done. 3 errors.", + "period_beginning": "2012-06-01 00:00:00", + "period_ending": "2012-07-01 00:00:00", + "total_errors": 3, + "total_instances": 6}}) + + def get_os_instance_usage_audit_log_2016_12_10_13_59_59_999999(self, **kw): + return (200, FAKE_RESPONSE_HEADERS, { + "instance_usage_audit_log": { + "hosts_not_run": ["samplehost3"], + "log": { + "samplehost0": { + "errors": 1, + "instances": 1, + "message": ("Instance usage audit ran for host " + "samplehost0, 1 instances in 0.01 " + "seconds."), + "state": "DONE" + }, + "samplehost1": { + "errors": 1, + "instances": 2, + "message": ("Instance usage audit ran for host " + "samplehost1, 2 instances in 0.01 " + "seconds."), + "state": "DONE" + }, + "samplehost2": { + "errors": 1, + "instances": 3, + "message": ("Instance usage audit ran for host " + "samplehost2, 3 instances in 0.01 " + "seconds."), + "state": "DONE" + }, + }, + "num_hosts": 4, + "num_hosts_done": 3, + "num_hosts_not_run": 1, + "num_hosts_running": 0, + "overall_status": "3 of 4 hosts done. 3 errors.", + "period_beginning": "2012-06-01 00:00:00", + "period_ending": "2012-07-01 00:00:00", + "total_errors": 3, + "total_instances": 6}}) + def post_servers_uuid1_action(self, **kw): return 202, {}, {} diff --git a/novaclient/tests/unit/v2/test_instance_usage_audit_log.py b/novaclient/tests/unit/v2/test_instance_usage_audit_log.py new file mode 100644 index 000000000..148ebbda2 --- /dev/null +++ b/novaclient/tests/unit/v2/test_instance_usage_audit_log.py @@ -0,0 +1,37 @@ +# Copyright 2013 Rackspace Hosting +# All Rights Reserved. +# +# 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 novaclient import api_versions +from novaclient.tests.unit import utils +from novaclient.tests.unit.v2 import fakes + + +class InstanceUsageAuditLogTests(utils.TestCase): + def setUp(self): + super(InstanceUsageAuditLogTests, self).setUp() + self.cs = fakes.FakeClient(api_versions.APIVersion("2.1")) + + def test_instance_usage_audit_log(self): + audit_log = self.cs.instance_usage_audit_log.get() + self.assert_request_id(audit_log, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called('GET', '/os-instance_usage_audit_log') + + def test_instance_usage_audit_log_with_before(self): + audit_log = self.cs.instance_usage_audit_log.get( + before='2016-12-10 13:59:59.999999') + self.assert_request_id(audit_log, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called( + 'GET', + '/os-instance_usage_audit_log/2016-12-10%2013%3A59%3A59.999999') diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 34c6717b5..d971e6b47 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -3019,6 +3019,17 @@ def test_instance_action_list_with_changes_since_invalid_value_v258(self): api_version='2.58') self.assertIn('Invalid changes-since value', six.text_type(ex)) + def test_instance_usage_audit_log(self): + self.run_command('instance-usage-audit-log') + self.assert_called('GET', '/os-instance_usage_audit_log') + + def test_instance_usage_audit_log_with_before(self): + self.run_command( + ["instance-usage-audit-log", "--before", + "2016-12-10 13:59:59.999999"]) + self.assert_called('GET', '/os-instance_usage_audit_log' + '/2016-12-10%2013%3A59%3A59.999999') + def test_cell_show(self): self.run_command('cell-show child_cell') self.assert_called('GET', '/os-cells/child_cell') diff --git a/novaclient/v2/client.py b/novaclient/v2/client.py index 1f543a820..bc66575f4 100644 --- a/novaclient/v2/client.py +++ b/novaclient/v2/client.py @@ -29,6 +29,7 @@ from novaclient.v2 import hypervisors from novaclient.v2 import images from novaclient.v2 import instance_action +from novaclient.v2 import instance_usage_audit_log from novaclient.v2 import keypairs from novaclient.v2 import limits from novaclient.v2 import list_extensions @@ -169,6 +170,8 @@ def __init__(self, assisted_volume_snapshots.AssistedSnapshotManager(self) self.cells = cells.CellsManager(self) self.instance_action = instance_action.InstanceActionManager(self) + self.instance_usage_audit_log = \ + instance_usage_audit_log.InstanceUsageAuditLogManager(self) self.list_extensions = list_extensions.ListExtManager(self) self.migrations = migrations.MigrationManager(self) self.server_external_events = \ diff --git a/novaclient/v2/instance_usage_audit_log.py b/novaclient/v2/instance_usage_audit_log.py new file mode 100644 index 000000000..19b588e9a --- /dev/null +++ b/novaclient/v2/instance_usage_audit_log.py @@ -0,0 +1,40 @@ +# Copyright 2013 Rackspace Hosting +# All Rights Reserved. +# +# 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 six.moves.urllib import parse + +from novaclient import base + + +class InstanceUsageAuditLog(base.Resource): + pass + + +class InstanceUsageAuditLogManager(base.Manager): + resource_class = InstanceUsageAuditLog + + def get(self, before=None): + """Get server usage audits. + + :param before: Filters the response by the date and time + before which to list usage audits. + """ + if before: + return self._get('/os-instance_usage_audit_log/%s' % + parse.quote(before, safe=''), + 'instance_usage_audit_log') + else: + return self._get('/os-instance_usage_audit_log', + 'instance_usage_audit_logs') diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 7c05d1691..e75ebb564 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -5084,3 +5084,20 @@ def do_migration_list(cs, args): changes_since=args.changes_since) # TODO(yikun): Output a "Marker" column if there is a next link? _print_migrations(cs, migrations) + + +@utils.arg( + '--before', + dest='before', + metavar='', + default=None, + help=_("Filters the response by the date and time before which to list " + "usage audits. The date and time stamp format is as follows: " + "CCYY-MM-DD hh:mm:ss.NNNNNN ex 2015-08-27 09:49:58 or " + "2015-08-27 09:49:58.123456.")) +def do_instance_usage_audit_log(cs, args): + """List/Get server usage audits.""" + audit_log = cs.instance_usage_audit_log.get(before=args.before).to_dict() + if 'hosts_not_run' in audit_log: + audit_log['hosts_not_run'] = pprint.pformat(audit_log['hosts_not_run']) + utils.print_dict(audit_log) diff --git a/releasenotes/notes/show-instance-usage-audit-logs-7826b411fac1283b.yaml b/releasenotes/notes/show-instance-usage-audit-logs-7826b411fac1283b.yaml new file mode 100644 index 000000000..57abbd307 --- /dev/null +++ b/releasenotes/notes/show-instance-usage-audit-logs-7826b411fac1283b.yaml @@ -0,0 +1,8 @@ +--- +features: + - Added new client API and CLI (``nova instance-usage-audit-log``) + to get server usage audit logs. + By default, it lists usage audits for all servers on all + compute hosts where usage auditing is configured. + If you specify the ``--before`` option, the result is filtered + by the date and time before which to list server usage audits. From 9213ec2d32fa173ec9943c28fb6c3ba5c196015d Mon Sep 17 00:00:00 2001 From: Sen Yang Date: Wed, 15 Nov 2017 12:09:04 -0600 Subject: [PATCH 1412/1705] Implement hypervisor hostname exact pattern match When starting cold migration with nova command "nova host- servers-migrate compute-1", the migration started from all compute hosts starting with name "compute-1", not only from compute-1 host. The same thing happens to "nova host-meta", "nova host-evacuate", "nova host-evacuate-live" as well. With the "--strict" option added to these nova commands, the action will be applied to a single compute with the exact hostname string match, but not to the computes with hostname substring match. Error handling is also added to these nova commands such that when specified hostname name does not exist, "NotFound" will be returned. Closes-Bug: #1667794 Change-Id: I5610efa160864b0d91cd67961883a6bec5bb8dd0 --- novaclient/tests/unit/v2/fakes.py | 38 ++++ novaclient/tests/unit/v2/test_shell.py | 171 ++++++++++++++++++ novaclient/v2/shell.py | 107 +++++++---- ...trict_hostname_match-f37243f0520a09a2.yaml | 9 + 4 files changed, 289 insertions(+), 36 deletions(-) create mode 100644 releasenotes/notes/strict_hostname_match-f37243f0520a09a2.yaml diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 7c8f95285..b6ddf7f4c 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -633,6 +633,12 @@ def post_servers_uuid3_metadata(self, **kw): def post_servers_uuid4_metadata(self, **kw): return (204, {}, {'metadata': {'key1': 'val1'}}) + def post_servers_uuid5_metadata(self, **kw): + return (204, {}, {'metadata': {'key1': 'val1'}}) + + def post_servers_uuid6_metadata(self, **kw): + return (204, {}, {'metadata': {'key1': 'val1'}}) + def delete_servers_uuid1_metadata_key1(self, **kw): return (200, {}, {'data': 'Fake diagnostics'}) @@ -645,6 +651,12 @@ def delete_servers_uuid3_metadata_key1(self, **kw): def delete_servers_uuid4_metadata_key1(self, **kw): return (200, {}, {'data': 'Fake diagnostics'}) + def delete_servers_uuid5_metadata_key1(self, **kw): + return (200, {}, {'data': 'Fake diagnostics'}) + + def delete_servers_uuid6_metadata_key1(self, **kw): + return (200, {}, {'data': 'Fake diagnostics'}) + def get_servers_1234_os_security_groups(self, **kw): return (200, {}, { "security_groups": [{ @@ -1773,6 +1785,26 @@ def get_os_hypervisors_hyper_servers(self, **kw): {'name': 'inst4', 'uuid': 'uuid4'}]}] }) + def get_os_hypervisors_hyper1_servers(self, **kw): + return (200, {}, { + 'hypervisors': [ + {'id': 1234, + 'hypervisor_hostname': 'hyper1', + 'servers': [ + {'name': 'inst1', 'uuid': 'uuid1'}, + {'name': 'inst2', 'uuid': 'uuid2'}]}] + }) + + def get_os_hypervisors_hyper2_servers(self, **kw): + return (200, {}, { + 'hypervisors': [ + {'id': 5678, + 'hypervisor_hostname': 'hyper2', + 'servers': [ + {'name': 'inst3', 'uuid': 'uuid3'}, + {'name': 'inst4', 'uuid': 'uuid4'}]}] + }) + def get_os_hypervisors_hyper_no_servers_servers(self, **kw): return (200, {}, {'hypervisors': [{'id': 1234, 'hypervisor_hostname': 'hyper1'}]}) @@ -1986,6 +2018,12 @@ def post_servers_uuid3_action(self, **kw): def post_servers_uuid4_action(self, **kw): return 202, {}, {} + def post_servers_uuid5_action(self, **kw): + return 202, {}, {} + + def post_servers_uuid6_action(self, **kw): + return 202, {}, {} + def get_os_cells_child_cell(self, **kw): cell = {'cell': { 'username': 'cell1_user', diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 8f416cddd..3d9a8ae3c 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -1915,16 +1915,40 @@ def test_set_host_meta(self): {'metadata': {'key1': 'val1', 'key2': 'val2'}}, pos=4) + def test_set_host_meta_strict(self): + self.run_command('host-meta hyper1 --strict set key1=val1 key2=val2') + self.assert_called('GET', '/os-hypervisors/hyper1/servers', pos=0) + self.assert_called('POST', '/servers/uuid1/metadata', + {'metadata': {'key1': 'val1', 'key2': 'val2'}}, + pos=1) + self.assert_called('POST', '/servers/uuid2/metadata', + {'metadata': {'key1': 'val1', 'key2': 'val2'}}, + pos=2) + + def test_set_host_meta_no_match(self): + cmd = 'host-meta hyper --strict set key1=val1 key2=val2' + self.assertRaises(exceptions.NotFound, self.run_command, cmd) + def test_set_host_meta_with_no_servers(self): self.run_command('host-meta hyper_no_servers set key1=val1 key2=val2') self.assert_called('GET', '/os-hypervisors/hyper_no_servers/servers') + def test_set_host_meta_with_no_servers_strict(self): + cmd = 'host-meta hyper_no_servers --strict set key1=val1 key2=val2' + self.assertRaises(exceptions.NotFound, self.run_command, cmd) + def test_delete_host_meta(self): self.run_command('host-meta hyper delete key1') self.assert_called('GET', '/os-hypervisors/hyper/servers', pos=0) self.assert_called('DELETE', '/servers/uuid1/metadata/key1', pos=1) self.assert_called('DELETE', '/servers/uuid2/metadata/key1', pos=2) + def test_delete_host_meta_strict(self): + self.run_command('host-meta hyper1 --strict delete key1') + self.assert_called('GET', '/os-hypervisors/hyper1/servers', pos=0) + self.assert_called('DELETE', '/servers/uuid1/metadata/key1', pos=1) + self.assert_called('DELETE', '/servers/uuid2/metadata/key1', pos=2) + def test_usage_list(self): cmd = 'usage-list --start 2000-01-20 --end 2005-02-01' stdout, _stderr = self.run_command(cmd) @@ -2300,6 +2324,19 @@ def test_host_evacuate_live_with_no_target_host(self): self.assert_called('POST', '/servers/uuid3/action', body, pos=3) self.assert_called('POST', '/servers/uuid4/action', body, pos=4) + def test_host_evacuate_live_with_no_target_host_strict(self): + self.run_command('host-evacuate-live hyper1 --strict') + self.assert_called('GET', '/os-hypervisors/hyper1/servers', pos=0) + body = {'os-migrateLive': {'host': None, + 'block_migration': False, + 'disk_over_commit': False}} + self.assert_called('POST', '/servers/uuid1/action', body, pos=1) + self.assert_called('POST', '/servers/uuid2/action', body, pos=2) + + def test_host_evacuate_live_no_match(self): + cmd = 'host-evacuate-live hyper --strict' + self.assertRaises(exceptions.NotFound, self.run_command, cmd) + def test_host_evacuate_live_2_25(self): self.run_command('host-evacuate-live hyper', api_version='2.25') self.assert_called('GET', '/os-hypervisors/hyper/servers', pos=0) @@ -2309,6 +2346,14 @@ def test_host_evacuate_live_2_25(self): self.assert_called('POST', '/servers/uuid3/action', body, pos=3) self.assert_called('POST', '/servers/uuid4/action', body, pos=4) + def test_host_evacuate_live_2_25_strict(self): + self.run_command('host-evacuate-live hyper1 --strict', + api_version='2.25') + self.assert_called('GET', '/os-hypervisors/hyper1/servers', pos=0) + body = {'os-migrateLive': {'host': None, 'block_migration': 'auto'}} + self.assert_called('POST', '/servers/uuid1/action', body, pos=1) + self.assert_called('POST', '/servers/uuid2/action', body, pos=2) + def test_host_evacuate_live_with_target_host(self): self.run_command('host-evacuate-live hyper ' '--target-host hostname') @@ -2321,6 +2366,16 @@ def test_host_evacuate_live_with_target_host(self): self.assert_called('POST', '/servers/uuid3/action', body, pos=3) self.assert_called('POST', '/servers/uuid4/action', body, pos=4) + def test_host_evacuate_live_with_target_host_strict(self): + self.run_command('host-evacuate-live hyper1 ' + '--target-host hostname --strict') + self.assert_called('GET', '/os-hypervisors/hyper1/servers', pos=0) + body = {'os-migrateLive': {'host': 'hostname', + 'block_migration': False, + 'disk_over_commit': False}} + self.assert_called('POST', '/servers/uuid1/action', body, pos=1) + self.assert_called('POST', '/servers/uuid2/action', body, pos=2) + def test_host_evacuate_live_2_30(self): self.run_command('host-evacuate-live --force hyper ' '--target-host hostname', @@ -2334,6 +2389,17 @@ def test_host_evacuate_live_2_30(self): self.assert_called('POST', '/servers/uuid3/action', body, pos=3) self.assert_called('POST', '/servers/uuid4/action', body, pos=4) + def test_host_evacuate_live_2_30_strict(self): + self.run_command('host-evacuate-live --force hyper1 ' + '--target-host hostname --strict', + api_version='2.30') + self.assert_called('GET', '/os-hypervisors/hyper1/servers', pos=0) + body = {'os-migrateLive': {'host': 'hostname', + 'block_migration': 'auto', + 'force': True}} + self.assert_called('POST', '/servers/uuid1/action', body, pos=1) + self.assert_called('POST', '/servers/uuid2/action', body, pos=2) + def test_host_evacuate_live_with_block_migration(self): self.run_command('host-evacuate-live --block-migrate hyper') self.assert_called('GET', '/os-hypervisors/hyper/servers', pos=0) @@ -2345,6 +2411,15 @@ def test_host_evacuate_live_with_block_migration(self): self.assert_called('POST', '/servers/uuid3/action', body, pos=3) self.assert_called('POST', '/servers/uuid4/action', body, pos=4) + def test_host_evacuate_live_with_block_migration_strict(self): + self.run_command('host-evacuate-live --block-migrate hyper2 --strict') + self.assert_called('GET', '/os-hypervisors/hyper2/servers', pos=0) + body = {'os-migrateLive': {'host': None, + 'block_migration': True, + 'disk_over_commit': False}} + self.assert_called('POST', '/servers/uuid3/action', body, pos=1) + self.assert_called('POST', '/servers/uuid4/action', body, pos=2) + def test_host_evacuate_live_with_block_migration_2_25(self): self.run_command('host-evacuate-live --block-migrate hyper', api_version='2.25') @@ -2355,6 +2430,14 @@ def test_host_evacuate_live_with_block_migration_2_25(self): self.assert_called('POST', '/servers/uuid3/action', body, pos=3) self.assert_called('POST', '/servers/uuid4/action', body, pos=4) + def test_host_evacuate_live_with_block_migration_2_25_strict(self): + self.run_command('host-evacuate-live --block-migrate hyper2 --strict', + api_version='2.25') + self.assert_called('GET', '/os-hypervisors/hyper2/servers', pos=0) + body = {'os-migrateLive': {'host': None, 'block_migration': True}} + self.assert_called('POST', '/servers/uuid3/action', body, pos=1) + self.assert_called('POST', '/servers/uuid4/action', body, pos=2) + def test_host_evacuate_live_with_disk_over_commit(self): self.run_command('host-evacuate-live --disk-over-commit hyper') self.assert_called('GET', '/os-hypervisors/hyper/servers', pos=0) @@ -2366,11 +2449,26 @@ def test_host_evacuate_live_with_disk_over_commit(self): self.assert_called('POST', '/servers/uuid3/action', body, pos=3) self.assert_called('POST', '/servers/uuid4/action', body, pos=4) + def test_host_evacuate_live_with_disk_over_commit_strict(self): + self.run_command('host-evacuate-live --disk-over-commit hyper2 ' + '--strict') + self.assert_called('GET', '/os-hypervisors/hyper2/servers', pos=0) + body = {'os-migrateLive': {'host': None, + 'block_migration': False, + 'disk_over_commit': True}} + self.assert_called('POST', '/servers/uuid3/action', body, pos=1) + self.assert_called('POST', '/servers/uuid4/action', body, pos=2) + def test_host_evacuate_live_with_disk_over_commit_2_25(self): self.assertRaises(SystemExit, self.run_command, 'host-evacuate-live --disk-over-commit hyper', api_version='2.25') + def test_host_evacuate_live_with_disk_over_commit_2_25_strict(self): + self.assertRaises(SystemExit, self.run_command, + 'host-evacuate-live --disk-over-commit hyper2 ' + '--strict', api_version='2.25') + def test_host_evacuate_list_with_max_servers(self): self.run_command('host-evacuate-live --max-servers 1 hyper') self.assert_called('GET', '/os-hypervisors/hyper/servers', pos=0) @@ -2379,6 +2477,14 @@ def test_host_evacuate_list_with_max_servers(self): 'disk_over_commit': False}} self.assert_called('POST', '/servers/uuid1/action', body, pos=1) + def test_host_evacuate_list_with_max_servers_strict(self): + self.run_command('host-evacuate-live --max-servers 1 hyper1 --strict') + self.assert_called('GET', '/os-hypervisors/hyper1/servers', pos=0) + body = {'os-migrateLive': {'host': None, + 'block_migration': False, + 'disk_over_commit': False}} + self.assert_called('POST', '/servers/uuid1/action', body, pos=1) + def test_reset_state(self): self.run_command('reset-state sample-server') self.assert_called('POST', '/servers/1234/action', @@ -2506,6 +2612,15 @@ def test_host_evacuate_v2_14(self): self.assert_called('POST', '/servers/uuid4/action', {'evacuate': {'host': 'target_hyper'}}, pos=4) + def test_host_evacuate_v2_14_strict(self): + self.run_command('host-evacuate hyper1 --target target_hyper --strict', + api_version='2.14') + self.assert_called('GET', '/os-hypervisors/hyper1/servers', pos=0) + self.assert_called('POST', '/servers/uuid1/action', + {'evacuate': {'host': 'target_hyper'}}, pos=1) + self.assert_called('POST', '/servers/uuid2/action', + {'evacuate': {'host': 'target_hyper'}}, pos=2) + def test_host_evacuate(self): self.run_command('host-evacuate hyper --target target_hyper') self.assert_called('GET', '/os-hypervisors/hyper/servers', pos=0) @@ -2522,6 +2637,20 @@ def test_host_evacuate(self): {'evacuate': {'host': 'target_hyper', 'onSharedStorage': False}}, pos=4) + def test_host_evacuate_strict(self): + self.run_command('host-evacuate hyper1 --target target_hyper --strict') + self.assert_called('GET', '/os-hypervisors/hyper1/servers', pos=0) + self.assert_called('POST', '/servers/uuid1/action', + {'evacuate': {'host': 'target_hyper', + 'onSharedStorage': False}}, pos=1) + self.assert_called('POST', '/servers/uuid2/action', + {'evacuate': {'host': 'target_hyper', + 'onSharedStorage': False}}, pos=2) + + def test_host_evacuate_no_match(self): + cmd = 'host-evacuate hyper --target target_hyper --strict' + self.assertRaises(exceptions.NotFound, self.run_command, cmd) + def test_host_evacuate_v2_29(self): self.run_command('host-evacuate hyper --target target_hyper --force', api_version='2.29') @@ -2539,6 +2668,17 @@ def test_host_evacuate_v2_29(self): {'evacuate': {'host': 'target_hyper', 'force': True} }, pos=4) + def test_host_evacuate_v2_29_strict(self): + self.run_command('host-evacuate hyper1 --target target_hyper' + ' --force --strict', api_version='2.29') + self.assert_called('GET', '/os-hypervisors/hyper1/servers', pos=0) + self.assert_called('POST', '/servers/uuid1/action', + {'evacuate': {'host': 'target_hyper', 'force': True} + }, pos=1) + self.assert_called('POST', '/servers/uuid2/action', + {'evacuate': {'host': 'target_hyper', 'force': True} + }, pos=2) + def test_host_evacuate_with_shared_storage(self): self.run_command( 'host-evacuate --on-shared-storage hyper --target target_hyper') @@ -2556,6 +2696,17 @@ def test_host_evacuate_with_shared_storage(self): {'evacuate': {'host': 'target_hyper', 'onSharedStorage': True}}, pos=4) + def test_host_evacuate_with_shared_storage_strict(self): + self.run_command('host-evacuate --on-shared-storage hyper1' + ' --target target_hyper --strict') + self.assert_called('GET', '/os-hypervisors/hyper1/servers', pos=0) + self.assert_called('POST', '/servers/uuid1/action', + {'evacuate': {'host': 'target_hyper', + 'onSharedStorage': True}}, pos=1) + self.assert_called('POST', '/servers/uuid2/action', + {'evacuate': {'host': 'target_hyper', + 'onSharedStorage': True}}, pos=2) + def test_host_evacuate_with_no_target_host(self): self.run_command('host-evacuate --on-shared-storage hyper') self.assert_called('GET', '/os-hypervisors/hyper/servers', pos=0) @@ -2568,6 +2719,14 @@ def test_host_evacuate_with_no_target_host(self): self.assert_called('POST', '/servers/uuid4/action', {'evacuate': {'onSharedStorage': True}}, pos=4) + def test_host_evacuate_with_no_target_host_strict(self): + self.run_command('host-evacuate --on-shared-storage hyper1 --strict') + self.assert_called('GET', '/os-hypervisors/hyper1/servers', pos=0) + self.assert_called('POST', '/servers/uuid1/action', + {'evacuate': {'onSharedStorage': True}}, pos=1) + self.assert_called('POST', '/servers/uuid2/action', + {'evacuate': {'onSharedStorage': True}}, pos=2) + def test_host_servers_migrate(self): self.run_command('host-servers-migrate hyper') self.assert_called('GET', '/os-hypervisors/hyper/servers', pos=0) @@ -2580,6 +2739,18 @@ def test_host_servers_migrate(self): self.assert_called('POST', '/servers/uuid4/action', {'migrate': None}, pos=4) + def test_host_servers_migrate_strict(self): + self.run_command('host-servers-migrate hyper1 --strict') + self.assert_called('GET', '/os-hypervisors/hyper1/servers', pos=0) + self.assert_called('POST', + '/servers/uuid1/action', {'migrate': None}, pos=1) + self.assert_called('POST', + '/servers/uuid2/action', {'migrate': None}, pos=2) + + def test_host_servers_migrate_no_match(self): + cmd = 'host-servers-migrate hyper --strict' + self.assertRaises(exceptions.NotFound, self.run_command, cmd) + def test_hypervisor_list(self): self.run_command('hypervisor-list') self.assert_called('GET', '/os-hypervisors') diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index bcb1dc815..3dabc7ecc 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -4674,6 +4674,23 @@ def _server_evacuate(cs, server, args): "error_message": error_message}) +def _hyper_servers(cs, host, strict): + hypervisors = cs.hypervisors.search(host, servers=True) + for hyper in hypervisors: + if strict and hyper.hypervisor_hostname != host: + continue + if hasattr(hyper, 'servers'): + for server in hyper.servers: + yield server + if strict: + break + else: + if strict: + msg = (_("No hypervisor matching '%s' could be found.") % + host) + raise exceptions.NotFound(404, msg) + + @utils.arg('host', metavar='', help='The hypervisor hostname (or pattern) to search for. ' 'WARNING: Use a fully qualified domain name if you only ' @@ -4699,18 +4716,22 @@ def _server_evacuate(cs, server, args): default=False, help=_('Force to not verify the scheduler if a host is provided.'), start_version='2.29') +@utils.arg( + '--strict', + dest='strict', + action='store_true', + default=False, + help=_('Evacuate host with exact hypervisor hostname match')) def do_host_evacuate(cs, args): """Evacuate all instances from failed host.""" - - hypervisors = cs.hypervisors.search(args.host, servers=True) response = [] - for hyper in hypervisors: - if hasattr(hyper, 'servers'): - for server in hyper.servers: - response.append(_server_evacuate(cs, server, args)) - - utils.print_list(response, - ["Server UUID", "Evacuate Accepted", "Error Message"]) + for server in _hyper_servers(cs, args.host, args.strict): + response.append(_server_evacuate(cs, server, args)) + utils.print_list(response, [ + "Server UUID", + "Evacuate Accepted", + "Error Message", + ]) def _server_live_migrate(cs, server, args): @@ -4780,22 +4801,29 @@ def __init__(self, server_uuid, live_migration_accepted, default=False, help=_('Force to not verify the scheduler if a host is provided.'), start_version='2.30') +@utils.arg( + '--strict', + dest='strict', + action='store_true', + default=False, + help=_('live Evacuate host with exact hypervisor hostname match')) def do_host_evacuate_live(cs, args): """Live migrate all instances of the specified host to other available hosts. """ - hypervisors = cs.hypervisors.search(args.host, servers=True) response = [] migrating = 0 - for hyper in hypervisors: - for server in getattr(hyper, 'servers', []): - response.append(_server_live_migrate(cs, server, args)) - migrating += 1 - if args.max_servers is not None and migrating >= args.max_servers: - break - - utils.print_list(response, ["Server UUID", "Live Migration Accepted", - "Error Message"]) + for server in _hyper_servers(cs, args.host, args.strict): + response.append(_server_live_migrate(cs, server, args)) + migrating = migrating + 1 + if (args.max_servers is not None and + migrating >= args.max_servers): + break + utils.print_list(response, [ + "Server UUID", + "Live Migration Accepted", + "Error Message", + ]) class HostServersMigrateResponse(base.Resource): @@ -4820,20 +4848,24 @@ def _server_migrate(cs, server): help='The hypervisor hostname (or pattern) to search for. ' 'WARNING: Use a fully qualified domain name if you only ' 'want to cold migrate from a specific host.') +@utils.arg( + '--strict', + dest='strict', + action='store_true', + default=False, + help=_('Migrate host with exact hypervisor hostname match')) def do_host_servers_migrate(cs, args): """Cold migrate all instances off the specified host to other available hosts. """ - - hypervisors = cs.hypervisors.search(args.host, servers=True) response = [] - for hyper in hypervisors: - if hasattr(hyper, 'servers'): - for server in hyper.servers: - response.append(_server_migrate(cs, server)) - - utils.print_list(response, - ["Server UUID", "Migration Accepted", "Error Message"]) + for server in _hyper_servers(cs, args.host, args.strict): + response.append(_server_migrate(cs, server)) + utils.print_list(response, [ + "Server UUID", + "Migration Accepted", + "Error Message", + ]) @utils.arg( @@ -4963,17 +4995,20 @@ def do_list_extensions(cs, _args): action='append', default=[], help=_('Metadata to set or delete (only key is necessary on delete)')) +@utils.arg( + '--strict', + dest='strict', + action='store_true', + default=False, + help=_('Set host-meta to the hypervisor with exact hostname match')) def do_host_meta(cs, args): """Set or Delete metadata on all instances of a host.""" - hypervisors = cs.hypervisors.search(args.host, servers=True) - for hyper in hypervisors: + for server in _hyper_servers(cs, args.host, args.strict): metadata = _extract_metadata(args) - if hasattr(hyper, 'servers'): - for server in hyper.servers: - if args.action == 'set': - cs.servers.set_meta(server['uuid'], metadata) - elif args.action == 'delete': - cs.servers.delete_meta(server['uuid'], metadata.keys()) + if args.action == 'set': + cs.servers.set_meta(server['uuid'], metadata) + elif args.action == 'delete': + cs.servers.delete_meta(server['uuid'], metadata.keys()) def _print_migrations(cs, migrations): diff --git a/releasenotes/notes/strict_hostname_match-f37243f0520a09a2.yaml b/releasenotes/notes/strict_hostname_match-f37243f0520a09a2.yaml new file mode 100644 index 000000000..04d77d4aa --- /dev/null +++ b/releasenotes/notes/strict_hostname_match-f37243f0520a09a2.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + Provides "--strict" option for "nova host-servers-migrate", "nova host-evacuate", + "nova host-evacuate-live" and "nova host-meta" commands. When "--strict" option is + used, the action will be applied to a single compute with the exact hypervisor + hostname string match rather than to the computes with hostname substring match. + When the specified hostname does not exist in the system, "NotFound" error code + will be returned. From a2363c42cfa0afd2f316eb0d065b6e578b0bd060 Mon Sep 17 00:00:00 2001 From: "James E. Blair" Date: Wed, 24 Jan 2018 16:56:29 -0800 Subject: [PATCH 1413/1705] Zuul: Remove project name Zuul no longer requires the project-name for in-repo configuration. Omitting it makes forking or renaming projects easier. Change-Id: I9f67465dc14d90d2a79bd3e3964bb4aa14a59c06 --- .zuul.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.zuul.yaml b/.zuul.yaml index 2d050d1d0..8424e55e2 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -10,7 +10,6 @@ - openstack/python-novaclient - project: - name: openstack/python-novaclient check: jobs: - novaclient-dsvm-functional From 43f093623a766b743daca8ba467bb857611a2c64 Mon Sep 17 00:00:00 2001 From: Rahul Date: Thu, 14 Dec 2017 15:55:43 +0530 Subject: [PATCH 1414/1705] nova limits ERROR (Exception): Field names must be unique Running `nova absolute-limits` or `nova limits` can fail when unaccounted for limits are returned This works OK for a standard Vanilla OpenStack deployment today, but it is quite fragile if new limit types are introduced, or if a deployer sends back other custom limits. For instance, my limits include `maxTotalPrivateNetworks` and `totalPrivateNetworksUsed` limits, which do not map accordingly, and thus result in an error. This happens when 'Others' field(custom limit type) is occuring more than once which is not handled in code causing multiple columns having same name. Which throws exception and does not pretty prints absolute limit. This fix should also fix any future custom limits. Closes-bug: #1546767 Change-Id: I1d3cf707722fc71c20cf4ec517b3f4f4875480e0 --- novaclient/tests/unit/v2/test_shell.py | 20 ++++++++++++++++++++ novaclient/v2/shell.py | 3 ++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 34c6717b5..7a8169cbe 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -18,6 +18,7 @@ import argparse import base64 +import collections import datetime import os @@ -43,6 +44,11 @@ FAKE_UUID_2 = fakes.FAKE_IMAGE_UUID_2 +# Converting dictionary to object +TestAbsoluteLimits = collections.namedtuple("TestAbsoluteLimits", + ["name", "value"]) + + class ShellFixture(fixtures.Fixture): def setUp(self): super(ShellFixture, self).setUp() @@ -2814,6 +2820,20 @@ def test_limits(self): self.assertIn('Verb', stdout) self.assertIn('Name', stdout) + def test_print_absolute_limits(self): + # Note: This test is to validate that no exception is + # thrown if in case we pass multiple custom fields + limits = [TestAbsoluteLimits('maxTotalPrivateNetworks', 3), + TestAbsoluteLimits('totalPrivateNetworksUsed', 0), + # Above two fields are custom fields + TestAbsoluteLimits('maxImageMeta', 15), + TestAbsoluteLimits('totalCoresUsed', 10), + TestAbsoluteLimits('totalInstancesUsed', 5), + TestAbsoluteLimits('maxServerMeta', 10), + TestAbsoluteLimits('totalRAMUsed', 10240), + TestAbsoluteLimits('totalFloatingIpsUsed', 10)] + novaclient.v2.shell._print_absolute_limits(limits=limits) + def test_limits_2_57(self): """Tests the limits command at microversion 2.57 where personality size limits should not be shown. diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 7c05d1691..2343c6964 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -2853,7 +2853,8 @@ def __init__(self, name, used, max, other): used[name] = l.value else: other[name] = l.value - columns.append('Other') + if 'Other' not in columns: + columns.append('Other') if name not in limit_names: limit_names.append(name) From eff607ccef91d09052d58f6798f68d67404f51ce Mon Sep 17 00:00:00 2001 From: Jacek Tomasiak Date: Tue, 16 Jan 2018 11:09:26 +0100 Subject: [PATCH 1415/1705] Fix listing of instances above API max_limit Output of `nova list` command contained only `max_limit` entries even when there were more. The list needs to be obtained in multiple requests and the paging loop was terminated prematurely. Change-Id: I4194e6d5e34ecc0ac704851c08bb895e223aa850 Closes-Bug: 1743532 --- novaclient/tests/unit/fixture_data/servers.py | 32 ++- novaclient/tests/unit/utils.py | 12 +- novaclient/tests/unit/v2/fakes.py | 4 + novaclient/tests/unit/v2/test_servers.py | 58 ++-- novaclient/tests/unit/v2/test_shell.py | 265 +++++++++++++----- novaclient/v2/servers.py | 5 +- novaclient/v2/shell.py | 4 +- 7 files changed, 280 insertions(+), 100 deletions(-) diff --git a/novaclient/tests/unit/fixture_data/servers.py b/novaclient/tests/unit/fixture_data/servers.py index 2402cd106..3950158d7 100644 --- a/novaclient/tests/unit/fixture_data/servers.py +++ b/novaclient/tests/unit/fixture_data/servers.py @@ -32,6 +32,16 @@ def setUp(self): self.requests_mock.get(self.url(), json=get_servers, + headers=self.json_headers, + complete_qs=True) + + self.requests_mock.get(self.url(name='sample-server'), + json=get_servers, + headers=self.json_headers, + complete_qs=True) + + self.requests_mock.get(self.url(marker='5678'), + json={"servers": []}, headers=self.json_headers) self.server_1234 = { @@ -160,14 +170,32 @@ def setUp(self): self.requests_mock.get( self.url('detail', marker=self.server_1234["id"]), + json={"servers": [self.server_5678, self.server_9012]}, + headers=self.json_headers, complete_qs=True) + + self.requests_mock.get( + self.url('detail', marker=self.server_1234["id"], limit=2), + json={"servers": [self.server_5678, self.server_9012]}, + headers=self.json_headers, complete_qs=True) + + # simulate max_limit=2 by returning 2 items when limit=3 + # another request should be triggered with limit=1 to get complete + # result + self.requests_mock.get( + self.url('detail', limit=3), json={"servers": [self.server_1234, self.server_5678]}, headers=self.json_headers, complete_qs=True) self.requests_mock.get( - self.url('detail', marker=self.server_5678["id"]), - json={"servers": []}, + self.url('detail', marker=self.server_5678["id"], limit=1), + json={"servers": [self.server_9012]}, headers=self.json_headers, complete_qs=True) + self.requests_mock.get( + self.url('detail', marker=self.server_9012["id"]), + json={"servers": []}, + headers=self.json_headers) + self.server_1235 = self.server_1234.copy() self.server_1235['id'] = 1235 self.server_1235['status'] = 'error' diff --git a/novaclient/tests/unit/utils.py b/novaclient/tests/unit/utils.py index 49d3631f3..f036497a9 100644 --- a/novaclient/tests/unit/utils.py +++ b/novaclient/tests/unit/utils.py @@ -93,12 +93,16 @@ def setUp(self): fix = self.data_fixture_class(self.requests_mock) self.data_fixture = self.useFixture(fix) - def assert_called(self, method, path, body=None): - self.assertEqual(self.requests_mock.last_request.method, method) - self.assertEqual(self.requests_mock.last_request.path_url, path) + def assert_called(self, method, path, body=None, pos=-1): + self.assertEqual( + self.requests_mock.request_history[pos].method, + method) + self.assertEqual( + self.requests_mock.request_history[pos].path_url, + path) if body: - req_data = self.requests_mock.last_request.body + req_data = self.requests_mock.request_history[pos].body if isinstance(req_data, six.binary_type): req_data = req_data.decode('utf-8') if not isinstance(body, six.string_types): diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 803dc41ec..e853ef223 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -392,6 +392,8 @@ def get_limits(self, **kw): # def get_servers(self, **kw): + if kw.get('marker') == '9014': + return (200, {}, {"servers": []}) return (200, {}, {"servers": [ {'id': '1234', 'name': 'sample-server'}, {'id': '5678', 'name': 'sample-server2'}, @@ -399,6 +401,8 @@ def get_servers(self, **kw): ]}) def get_servers_detail(self, **kw): + if kw.get('marker') == '9014': + return (200, {}, {"servers": []}) return (200, {}, {"servers": [ { "id": '1234', diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index 07ad1497e..dfa1b6fd5 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -47,48 +47,64 @@ def _get_server_create_default_nics(self): """ return None - def test_list_servers(self): + def test_list_all_servers(self): sl = self.cs.servers.list() self.assert_request_id(sl, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('GET', '/servers/detail') + self.assert_called('GET', '/servers/detail', pos=-2) + self.assert_called('GET', '/servers/detail?marker=9012') for s in sl: self.assertIsInstance(s, servers.Server) def test_filter_servers_unicode(self): sl = self.cs.servers.list(search_opts={'name': u't€sting'}) self.assert_request_id(sl, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('GET', '/servers/detail?name=t%E2%82%ACsting') + self.assert_called( + 'GET', + '/servers/detail?name=t%E2%82%ACsting', + pos=-2) + self.assert_called( + 'GET', + '/servers/detail?marker=9012&name=t%E2%82%ACsting') for s in sl: self.assertIsInstance(s, servers.Server) - def test_list_all_servers(self): - # use marker just to identify this call in fixtures - sl = self.cs.servers.list(limit=-1, marker=1234) + def test_list_servers_undetailed(self): + sl = self.cs.servers.list(detailed=False) + self.assert_request_id(sl, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('GET', '/servers', pos=-2) + self.assert_called('GET', '/servers?marker=5678') + for s in sl: + self.assertIsInstance(s, servers.Server) + + def test_list_servers_with_marker(self): + sl = self.cs.servers.list(marker=1234) self.assert_request_id(sl, fakes.FAKE_REQUEST_ID_LIST) self.assertEqual(2, len(sl)) - self.assertEqual(self.requests_mock.request_history[-2].method, 'GET') - self.assertEqual(self.requests_mock.request_history[-2].path_url, - '/servers/detail?marker=1234') - self.assert_called('GET', '/servers/detail?marker=5678') + self.assert_called('GET', '/servers/detail?marker=1234', pos=-2) + self.assert_called('GET', '/servers/detail?marker=9012') for s in sl: self.assertIsInstance(s, servers.Server) - def test_list_servers_undetailed(self): - sl = self.cs.servers.list(detailed=False) + def test_list_servers_with_marker_limit(self): + sl = self.cs.servers.list(marker=1234, limit=2) self.assert_request_id(sl, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('GET', '/servers') + self.assert_called('GET', '/servers/detail?limit=2&marker=1234') for s in sl: self.assertIsInstance(s, servers.Server) + self.assertEqual(2, len(sl)) - def test_list_servers_with_marker_limit(self): - sl = self.cs.servers.list(marker=1234, limit=2) + def test_list_servers_with_limit_above_max_limit(self): + # use limit=3 to trigger paging simulation on backend fixture side + sl = self.cs.servers.list(limit=3) self.assert_request_id(sl, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('GET', '/servers/detail?limit=2&marker=1234') + self.assert_called('GET', '/servers/detail?limit=3', pos=-2) + self.assert_called('GET', '/servers/detail?limit=1&marker=5678') for s in sl: self.assertIsInstance(s, servers.Server) + self.assertEqual(3, len(sl)) def test_list_servers_sort_single(self): sl = self.cs.servers.list(sort_keys=['display_name'], @@ -96,7 +112,10 @@ def test_list_servers_sort_single(self): self.assert_request_id(sl, fakes.FAKE_REQUEST_ID_LIST) self.assert_called( 'GET', - '/servers/detail?sort_dir=asc&sort_key=display_name') + '/servers/detail?sort_dir=asc&sort_key=display_name', pos=-2) + self.assert_called( + 'GET', + '/servers/detail?marker=9012&sort_dir=asc&sort_key=display_name') for s in sl: self.assertIsInstance(s, servers.Server) @@ -107,6 +126,11 @@ def test_list_servers_sort_multiple(self): self.assert_called( 'GET', ('/servers/detail?sort_dir=asc&sort_dir=desc&' + 'sort_key=display_name&sort_key=id'), + pos=-2) + self.assert_called( + 'GET', + ('/servers/detail?marker=9012&sort_dir=asc&sort_dir=desc&' 'sort_key=display_name&sort_key=id')) for s in sl: self.assertIsInstance(s, servers.Server) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 785b9683f..dfa798b85 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -1324,63 +1324,99 @@ def test_create_image_with_poll_to_check_image_state_deleted(self): def test_list(self): self.run_command('list') - self.assert_called('GET', '/servers/detail') + self.assert_called('GET', '/servers/detail', pos=0) + self.assert_called('GET', '/servers/detail?marker=9014') def test_list_minimal(self): self.run_command('list --minimal') - self.assert_called('GET', '/servers') + self.assert_called('GET', '/servers', pos=0) + self.assert_called('GET', '/servers?marker=9014') def test_list_deleted(self): self.run_command('list --deleted') - self.assert_called('GET', '/servers/detail?deleted=True') + self.assert_called( + 'GET', + '/servers/detail?deleted=True', + pos=0) + self.assert_called( + 'GET', + '/servers/detail?deleted=True&marker=9014') def test_list_with_images(self): self.run_command('list --image %s' % FAKE_UUID_1) - self.assert_called('GET', '/servers/detail?image=%s' % FAKE_UUID_1) + self.assert_called( + 'GET', + '/servers/detail?image=%s' % FAKE_UUID_1, + pos=1) + self.assert_called( + 'GET', + '/servers/detail?image=%s&marker=9014' % FAKE_UUID_1) def test_list_with_flavors(self): self.run_command('list --flavor 1') - self.assert_called('GET', '/servers/detail?flavor=1') + self.assert_called('GET', '/servers/detail?flavor=1', pos=1) + self.assert_called('GET', '/servers/detail?flavor=1&marker=9014') def test_list_by_tenant(self): self.run_command('list --tenant fake_tenant') self.assert_called( 'GET', - '/servers/detail?all_tenants=1&tenant_id=fake_tenant') + '/servers/detail?all_tenants=1&tenant_id=fake_tenant', pos=0) + self.assert_called( + 'GET', + '/servers/detail?all_tenants=1&marker=9014&tenant_id=fake_tenant') def test_list_by_user(self): self.run_command('list --user fake_user') self.assert_called( 'GET', - '/servers/detail?all_tenants=1&user_id=fake_user') + '/servers/detail?all_tenants=1&user_id=fake_user', pos=0) + self.assert_called( + 'GET', + '/servers/detail?all_tenants=1&marker=9014&user_id=fake_user') def test_list_with_single_sort_key_no_dir(self): self.run_command('list --sort 1') self.assert_called( - 'GET', ('/servers/detail?sort_dir=desc&sort_key=1')) + 'GET', ('/servers/detail?sort_dir=desc&sort_key=1'), pos=0) + self.assert_called( + 'GET', + '/servers/detail?marker=9014&sort_dir=desc&sort_key=1') def test_list_with_single_sort_key_and_dir(self): self.run_command('list --sort 1:asc') self.assert_called( - 'GET', ('/servers/detail?sort_dir=asc&sort_key=1')) + 'GET', ('/servers/detail?sort_dir=asc&sort_key=1'), pos=0) + self.assert_called( + 'GET', + '/servers/detail?marker=9014&sort_dir=asc&sort_key=1') def test_list_with_sort_keys_no_dir(self): self.run_command('list --sort 1,2') self.assert_called( 'GET', ('/servers/detail?sort_dir=desc&sort_dir=desc&' + 'sort_key=1&sort_key=2'), pos=0) + self.assert_called( + 'GET', ('/servers/detail?marker=9014&sort_dir=desc&sort_dir=desc&' 'sort_key=1&sort_key=2')) def test_list_with_sort_keys_and_dirs(self): self.run_command('list --sort 1:asc,2:desc') self.assert_called( 'GET', ('/servers/detail?sort_dir=asc&sort_dir=desc&' + 'sort_key=1&sort_key=2'), pos=0) + self.assert_called( + 'GET', ('/servers/detail?marker=9014&sort_dir=asc&sort_dir=desc&' 'sort_key=1&sort_key=2')) def test_list_with_sort_keys_and_some_dirs(self): self.run_command('list --sort 1,2:asc') self.assert_called( 'GET', ('/servers/detail?sort_dir=desc&sort_dir=asc&' - 'sort_key=1&sort_key=2')) + 'sort_key=1&sort_key=2'), pos=0) + self.assert_called( + 'GET', ('/servers/detail?marker=9014&sort_dir=desc&' + 'sort_dir=asc&sort_key=1&sort_key=2')) def test_list_with_invalid_sort_dir_one(self): cmd = 'list --sort 1:foo' @@ -1412,7 +1448,8 @@ def test_list_fields(self): output, _err = self.run_command( 'list --fields ' 'host,security_groups,OS-EXT-MOD:some_thing') - self.assert_called('GET', '/servers/detail') + self.assert_called('GET', '/servers/detail', pos=0) + self.assert_called('GET', '/servers/detail?marker=9014') self.assertIn('computenode1', output) self.assertIn('securitygroup1', output) self.assertIn('OS-EXT-MOD: Some Thing', output) @@ -1454,7 +1491,8 @@ def test_list_invalid_fields(self): def test_list_with_marker(self): self.run_command('list --marker some-uuid') - self.assert_called('GET', '/servers/detail?marker=some-uuid') + self.assert_called('GET', '/servers/detail?marker=some-uuid', pos=0) + self.assert_called('GET', '/servers/detail?marker=9014') def test_list_with_limit(self): self.run_command('list --limit 3') @@ -1463,7 +1501,13 @@ def test_list_with_limit(self): def test_list_with_changes_since(self): self.run_command('list --changes-since 2016-02-29T06:23:22') self.assert_called( - 'GET', '/servers/detail?changes-since=2016-02-29T06%3A23%3A22') + 'GET', + '/servers/detail?changes-since=2016-02-29T06%3A23%3A22', + pos=0) + self.assert_called( + 'GET', + ('/servers/detail?changes-since=2016-02-29T06%3A23%3A22&' + 'marker=9014')) def test_list_with_changes_since_invalid_value(self): self.assertRaises(exceptions.CommandError, @@ -1501,12 +1545,14 @@ def test_rebuild(self): output, _err = self.run_command('rebuild sample-server %s' % FAKE_UUID_1) self.assert_called('GET', '/servers?name=sample-server', pos=0) - self.assert_called('GET', '/servers/1234', pos=1) - self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=2) + self.assert_called('GET', '/servers?marker=9014&name=sample-server', + pos=1) + self.assert_called('GET', '/servers/1234', pos=2) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=3) self.assert_called('POST', '/servers/1234/action', - {'rebuild': {'imageRef': FAKE_UUID_1}}, pos=3) - self.assert_called('GET', '/flavors/1', pos=4) - self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=5) + {'rebuild': {'imageRef': FAKE_UUID_1}}, pos=4) + self.assert_called('GET', '/flavors/1', pos=5) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=6) self.assertIn('adminPass', output) def test_rebuild_password(self): @@ -1514,63 +1560,73 @@ def test_rebuild_password(self): ' --rebuild-password asdf' % FAKE_UUID_1) self.assert_called('GET', '/servers?name=sample-server', pos=0) - self.assert_called('GET', '/servers/1234', pos=1) - self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=2) + self.assert_called('GET', '/servers?marker=9014&name=sample-server', + pos=1) + self.assert_called('GET', '/servers/1234', pos=2) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=3) self.assert_called('POST', '/servers/1234/action', {'rebuild': {'imageRef': FAKE_UUID_1, - 'adminPass': 'asdf'}}, pos=3) - self.assert_called('GET', '/flavors/1', pos=4) - self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=5) + 'adminPass': 'asdf'}}, pos=4) + self.assert_called('GET', '/flavors/1', pos=5) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=6) self.assertIn('adminPass', output) def test_rebuild_preserve_ephemeral(self): self.run_command('rebuild sample-server %s --preserve-ephemeral' % FAKE_UUID_1) self.assert_called('GET', '/servers?name=sample-server', pos=0) - self.assert_called('GET', '/servers/1234', pos=1) - self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=2) + self.assert_called('GET', '/servers?marker=9014&name=sample-server', + pos=1) + self.assert_called('GET', '/servers/1234', pos=2) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=3) self.assert_called('POST', '/servers/1234/action', {'rebuild': {'imageRef': FAKE_UUID_1, - 'preserve_ephemeral': True}}, pos=3) - self.assert_called('GET', '/flavors/1', pos=4) - self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=5) + 'preserve_ephemeral': True}}, pos=4) + self.assert_called('GET', '/flavors/1', pos=5) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=6) def test_rebuild_name_meta(self): self.run_command('rebuild sample-server %s --name asdf --meta ' 'foo=bar' % FAKE_UUID_1) self.assert_called('GET', '/servers?name=sample-server', pos=0) - self.assert_called('GET', '/servers/1234', pos=1) - self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=2) + self.assert_called('GET', '/servers?marker=9014&name=sample-server', + pos=1) + self.assert_called('GET', '/servers/1234', pos=2) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=3) self.assert_called('POST', '/servers/1234/action', {'rebuild': {'imageRef': FAKE_UUID_1, 'name': 'asdf', - 'metadata': {'foo': 'bar'}}}, pos=3) - self.assert_called('GET', '/flavors/1', pos=4) - self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=5) + 'metadata': {'foo': 'bar'}}}, pos=4) + self.assert_called('GET', '/flavors/1', pos=5) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=6) def test_rebuild_reset_keypair(self): self.run_command('rebuild sample-server %s --key-name test_keypair' % FAKE_UUID_1, api_version='2.54') self.assert_called('GET', '/servers?name=sample-server', pos=0) - self.assert_called('GET', '/servers/1234', pos=1) - self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=2) + self.assert_called('GET', '/servers?marker=9014&name=sample-server', + pos=1) + self.assert_called('GET', '/servers/1234', pos=2) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=3) self.assert_called('POST', '/servers/1234/action', {'rebuild': {'imageRef': FAKE_UUID_1, 'key_name': 'test_keypair', - 'description': None}}, pos=3) - self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=4) + 'description': None}}, pos=4) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=5) def test_rebuild_unset_keypair(self): self.run_command('rebuild sample-server %s --key-unset' % FAKE_UUID_1, api_version='2.54') self.assert_called('GET', '/servers?name=sample-server', pos=0) - self.assert_called('GET', '/servers/1234', pos=1) - self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=2) + self.assert_called('GET', '/servers?marker=9014&name=sample-server', + pos=1) + self.assert_called('GET', '/servers/1234', pos=2) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=3) self.assert_called('POST', '/servers/1234/action', {'rebuild': {'imageRef': FAKE_UUID_1, 'key_name': None, - 'description': None}}, pos=3) - self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=4) + 'description': None}}, pos=4) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=5) def test_rebuild_unset_keypair_with_key_name(self): ex = self.assertRaises( @@ -1612,25 +1668,29 @@ def test_rebuild_change_user_data(self): FAKE_UUID_1, api_version='2.57') user_data = servers.ServerManager.transform_userdata('test') self.assert_called('GET', '/servers?name=sample-server', pos=0) - self.assert_called('GET', '/servers/1234', pos=1) - self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=2) + self.assert_called('GET', '/servers?marker=9014&name=sample-server', + pos=1) + self.assert_called('GET', '/servers/1234', pos=2) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=3) self.assert_called('POST', '/servers/1234/action', {'rebuild': {'imageRef': FAKE_UUID_1, 'user_data': user_data, - 'description': None}}, pos=3) - self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=4) + 'description': None}}, pos=4) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=5) def test_rebuild_unset_user_data(self): self.run_command('rebuild sample-server %s --user-data-unset' % FAKE_UUID_1, api_version='2.57') self.assert_called('GET', '/servers?name=sample-server', pos=0) - self.assert_called('GET', '/servers/1234', pos=1) - self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=2) + self.assert_called('GET', '/servers?marker=9014&name=sample-server', + pos=1) + self.assert_called('GET', '/servers/1234', pos=2) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=3) self.assert_called('POST', '/servers/1234/action', {'rebuild': {'imageRef': FAKE_UUID_1, 'user_data': None, - 'description': None}}, pos=3) - self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=4) + 'description': None}}, pos=4) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=5) def test_rebuild_user_data_and_unset_user_data(self): """Tests that trying to set --user-data and --unset-user-data in the @@ -1651,7 +1711,11 @@ def test_start_with_all_tenants(self): self.run_command('start sample-server --all-tenants') self.assert_called('GET', '/servers?all_tenants=1&name=sample-server', pos=0) - self.assert_called('GET', '/servers/1234', pos=1) + self.assert_called('GET', + ('/servers?all_tenants=1&marker=9014&' + 'name=sample-server'), + pos=1) + self.assert_called('GET', '/servers/1234', pos=2) self.assert_called('POST', '/servers/1234/action', {'os-start': None}) def test_stop(self): @@ -1662,7 +1726,11 @@ def test_stop_with_all_tenants(self): self.run_command('stop sample-server --all-tenants') self.assert_called('GET', '/servers?all_tenants=1&name=sample-server', pos=0) - self.assert_called('GET', '/servers/1234', pos=1) + self.assert_called('GET', + ('/servers?all_tenants=1&marker=9014&' + 'name=sample-server'), + pos=1) + self.assert_called('GET', '/servers/1234', pos=2) self.assert_called('POST', '/servers/1234/action', {'os-stop': None}) def test_pause(self): @@ -1765,10 +1833,12 @@ def test_set_password(self): def test_show(self): self.run_command('show 1234') self.assert_called('GET', '/servers?name=1234', pos=0) - self.assert_called('GET', '/servers?name=1234', pos=1) - self.assert_called('GET', '/servers/1234', pos=2) - self.assert_called('GET', '/flavors/1', pos=3) - self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=4) + self.assert_called('GET', '/servers?marker=9014&name=1234', pos=1) + self.assert_called('GET', '/servers?name=1234', pos=2) + self.assert_called('GET', '/servers?marker=9014&name=1234', pos=3) + self.assert_called('GET', '/servers/1234', pos=4) + self.assert_called('GET', '/flavors/1', pos=5) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=6) def test_show_no_image(self): self.run_command('show 9012') @@ -1827,21 +1897,31 @@ def test_restore_withname(self): self.run_command('restore sample-server') self.assert_called('GET', '/servers?deleted=True&name=sample-server', pos=0) - self.assert_called('GET', '/servers/1234', pos=1) + self.assert_called('GET', + ('/servers?deleted=True&marker=9014&' + 'name=sample-server'), + pos=1) + self.assert_called('GET', '/servers/1234', pos=2) self.assert_called('POST', '/servers/1234/action', {'restore': None}, - pos=2) + pos=3) def test_delete_two_with_two_existent(self): self.run_command('delete 1234 5678') - self.assert_called('DELETE', '/servers/1234', pos=-5) + self.assert_called('DELETE', '/servers/1234', pos=-7) self.assert_called('DELETE', '/servers/5678', pos=-1) self.run_command('delete sample-server sample-server2') self.assert_called('GET', - '/servers?name=sample-server', pos=-6) - self.assert_called('GET', '/servers/1234', pos=-5) - self.assert_called('DELETE', '/servers/1234', pos=-4) + '/servers?name=sample-server', pos=-8) + self.assert_called('GET', + '/servers?marker=9014&name=sample-server', + pos=-7) + self.assert_called('GET', '/servers/1234', pos=-6) + self.assert_called('DELETE', '/servers/1234', pos=-5) self.assert_called('GET', '/servers?name=sample-server2', + pos=-4) + self.assert_called('GET', + '/servers?marker=9014&name=sample-server2', pos=-3) self.assert_called('GET', '/servers/5678', pos=-2) self.assert_called('DELETE', '/servers/5678', pos=-1) @@ -1850,13 +1930,21 @@ def test_delete_two_with_two_existent_all_tenants(self): self.run_command('delete sample-server sample-server2 --all-tenants') self.assert_called('GET', '/servers?all_tenants=1&name=sample-server', pos=0) - self.assert_called('GET', '/servers/1234', pos=1) - self.assert_called('DELETE', '/servers/1234', pos=2) + self.assert_called('GET', + ('/servers?all_tenants=1&marker=9014&' + 'name=sample-server'), + pos=1) + self.assert_called('GET', '/servers/1234', pos=2) + self.assert_called('DELETE', '/servers/1234', pos=3) self.assert_called('GET', '/servers?all_tenants=1&name=sample-server2', - pos=3) - self.assert_called('GET', '/servers/5678', pos=4) - self.assert_called('DELETE', '/servers/5678', pos=5) + pos=4) + self.assert_called('GET', + ('/servers?all_tenants=1&marker=9014&' + 'name=sample-server2'), + pos=5) + self.assert_called('GET', '/servers/5678', pos=6) + self.assert_called('DELETE', '/servers/5678', pos=7) def test_delete_two_with_one_nonexistent(self): cmd = 'delete 1234 123456789' @@ -2503,21 +2591,25 @@ def test_reset_state_with_all_tenants(self): self.run_command('reset-state sample-server --all-tenants') self.assert_called('GET', '/servers?all_tenants=1&name=sample-server', pos=0) - self.assert_called('GET', '/servers/1234', pos=1) + self.assert_called('GET', + ('/servers?all_tenants=1&marker=9014&' + 'name=sample-server'), + pos=1) + self.assert_called('GET', '/servers/1234', pos=2) self.assert_called('POST', '/servers/1234/action', {'os-resetState': {'state': 'error'}}) def test_reset_state_multiple(self): self.run_command('reset-state sample-server sample-server2') self.assert_called('POST', '/servers/1234/action', - {'os-resetState': {'state': 'error'}}, pos=-4) + {'os-resetState': {'state': 'error'}}, pos=-5) self.assert_called('POST', '/servers/5678/action', {'os-resetState': {'state': 'error'}}, pos=-1) def test_reset_state_active_multiple(self): self.run_command('reset-state --active sample-server sample-server2') self.assert_called('POST', '/servers/1234/action', - {'os-resetState': {'state': 'active'}}, pos=-4) + {'os-resetState': {'state': 'active'}}, pos=-5) self.assert_called('POST', '/servers/5678/action', {'os-resetState': {'state': 'active'}}, pos=-1) @@ -3533,7 +3625,8 @@ def test_versions(self): def test_list_v2_10(self): self.run_command('list', api_version='2.10') - self.assert_called('GET', '/servers/detail') + self.assert_called('GET', '/servers/detail', pos=0) + self.assert_called('GET', '/servers/detail?marker=9014') def test_server_tag_add(self): self.run_command('server-tag-add sample-server tag', @@ -3576,19 +3669,43 @@ def test_server_tag_delete_all(self): def test_list_v2_26_tags(self): self.run_command('list --tags tag1,tag2', api_version='2.26') - self.assert_called('GET', '/servers/detail?tags=tag1%2Ctag2') + self.assert_called( + 'GET', + '/servers/detail?tags=tag1%2Ctag2', + pos=0) + self.assert_called( + 'GET', + '/servers/detail?marker=9014&tags=tag1%2Ctag2') def test_list_v2_26_tags_any(self): self.run_command('list --tags-any tag1,tag2', api_version='2.26') - self.assert_called('GET', '/servers/detail?tags-any=tag1%2Ctag2') + self.assert_called( + 'GET', + '/servers/detail?tags-any=tag1%2Ctag2', + pos=0) + self.assert_called( + 'GET', + '/servers/detail?marker=9014&tags-any=tag1%2Ctag2') def test_list_v2_26_not_tags(self): self.run_command('list --not-tags tag1,tag2', api_version='2.26') - self.assert_called('GET', '/servers/detail?not-tags=tag1%2Ctag2') + self.assert_called( + 'GET', + '/servers/detail?not-tags=tag1%2Ctag2', + pos=0) + self.assert_called( + 'GET', + '/servers/detail?marker=9014¬-tags=tag1%2Ctag2') def test_list_v2_26_not_tags_any(self): self.run_command('list --not-tags-any tag1,tag2', api_version='2.26') - self.assert_called('GET', '/servers/detail?not-tags-any=tag1%2Ctag2') + self.assert_called( + 'GET', + '/servers/detail?not-tags-any=tag1%2Ctag2', + pos=0) + self.assert_called( + 'GET', + '/servers/detail?marker=9014¬-tags-any=tag1%2Ctag2') class PollForStatusTestCase(utils.TestCase): diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index 79ab0cdce..805dd4541 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -855,7 +855,10 @@ def list(self, detailed=True, search_opts=None, marker=None, limit=None, result.extend(servers) result.append_request_ids(servers.request_ids) - if not servers or limit != -1: + if limit and limit != -1: + limit = max(limit - len(servers), 0) + + if not servers or limit == 0: break marker = result[-1].id return result diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index e73bd5040..532306454 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -1441,8 +1441,8 @@ def _print_flavor(flavor): default=None, help=_("Maximum number of servers to display. If limit == -1, all servers " "will be displayed. If limit is bigger than 'CONF.api.max_limit' " - "option of Nova API, limit 'CONF.api.max_limit' will be used " - "instead.")) + "option of Nova API, multiple requests will be sent and results " + "will be merged.")) @utils.arg( '--changes-since', dest='changes_since', From d272d6f3df2610a62f81e2ca26798ea8a6674b06 Mon Sep 17 00:00:00 2001 From: Thomas Goirand Date: Wed, 7 Feb 2018 09:17:12 +0100 Subject: [PATCH 1416/1705] Fix crashing console-log Because of encoding issue, the "nova console-log" is prone to a stack dump, as explained in the bug report. This patch sets the encoding output of stdout to utf8 before attempting to print in it. Change-Id: I63bc3dc8807021f5a97f58b0fe13a10d93688c7e Closes-Bug: 1746534 --- novaclient/v2/shell.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index e73bd5040..e919caa10 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -19,6 +19,7 @@ from __future__ import print_function import argparse +import codecs import collections import datetime import getpass @@ -2594,7 +2595,10 @@ def do_console_log(cs, args): """Get console log output of a server.""" server = _find_server(cs, args.server) data = server.get_console_output(length=args.length) - print(data) + + if data and data[-1] != '\n': + data += '\n' + codecs.getwriter('utf-8')(sys.stdout).write(data) @utils.arg('server', metavar='', help=_('Name or ID of server.')) From ca27736810a41080761e604e09c6763aaed07ed7 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Fri, 15 Dec 2017 18:24:53 +0200 Subject: [PATCH 1417/1705] Remove 2 redundant methods I suppose that methods get_resource_manager_extra_kwargs and add_resource_manager_extra_kwargs_hook were designed in those days when Nova API had extensions. Nowdays, Nova API has strict schema validation of requests, so no additional arguments are allowed and these methods look redundant. The fact that get_resource_manager_extra_kwargs lists of the objects added by add_resource_manager_extra_kwargs_hook whereas add_resource_manager_extra_kwargs_hook is not called anywhere doesn't add chances for leaving these methods in our code. Compatibility: both methods are designed for CLI and the CLI behaviour will not change after removing them, so we can do a cleanup without following standard deprecation cycle. Change-Id: Id61457c3db8a17e2294dc579b2519873927fec48 --- novaclient/tests/unit/test_utils.py | 20 ---------- novaclient/utils.py | 37 ------------------- novaclient/v2/shell.py | 14 ++----- ...ff-redundant-methods-47e679c13e88f28a.yaml | 10 +++++ 4 files changed, 14 insertions(+), 67 deletions(-) create mode 100644 releasenotes/notes/get-rid-off-redundant-methods-47e679c13e88f28a.yaml diff --git a/novaclient/tests/unit/test_utils.py b/novaclient/tests/unit/test_utils.py index 372e6f007..46ffbc13a 100644 --- a/novaclient/tests/unit/test_utils.py +++ b/novaclient/tests/unit/test_utils.py @@ -380,26 +380,6 @@ def test_validate_flavor_metadata_keys_with_invalid_keys(self): self.assertIn(key, str(ce)) -class ResourceManagerExtraKwargsHookTestCase(test_utils.TestCase): - def test_get_resource_manager_extra_kwargs_hook_test(self): - do_foo = mock.MagicMock() - - def hook1(args): - return {'kwarg1': 'v_hook1'} - - def hook2(args): - return {'kwarg1': 'v_hook2'} - do_foo.resource_manager_kwargs_hooks = [hook1, hook2] - args = {} - exc = self.assertRaises(exceptions.NoUniqueMatch, - utils.get_resource_manager_extra_kwargs, - do_foo, - args) - except_error = ("Hook 'hook2' is attempting to redefine " - "attributes") - self.assertIn(except_error, six.text_type(exc)) - - class DoActionOnManyTestCase(test_utils.TestCase): def _test_do_action_on_many(self, side_effect, fail): diff --git a/novaclient/utils.py b/novaclient/utils.py index df4464e6c..8869f696b 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -118,43 +118,6 @@ def inner(f): return inner -def add_resource_manager_extra_kwargs_hook(f, hook): - """Add hook to bind CLI arguments to ResourceManager calls. - - The `do_foo` calls in shell.py will receive CLI args and then in turn pass - them through to the ResourceManager. Before passing through the args, the - hooks registered here will be called, giving us a chance to add extra - kwargs (taken from the command-line) to what's passed to the - ResourceManager. - """ - if not hasattr(f, 'resource_manager_kwargs_hooks'): - f.resource_manager_kwargs_hooks = [] - - names = [h.__name__ for h in f.resource_manager_kwargs_hooks] - if hook.__name__ not in names: - f.resource_manager_kwargs_hooks.append(hook) - - -def get_resource_manager_extra_kwargs(f, args, allow_conflicts=False): - """Return extra_kwargs by calling resource manager kwargs hooks.""" - hooks = getattr(f, "resource_manager_kwargs_hooks", []) - extra_kwargs = {} - for hook in hooks: - hook_kwargs = hook(args) - hook_name = hook.__name__ - conflicting_keys = set(hook_kwargs.keys()) & set(extra_kwargs.keys()) - if conflicting_keys and not allow_conflicts: - msg = (_("Hook '%(hook_name)s' is attempting to redefine " - "attributes '%(conflicting_keys)s'") % - {'hook_name': hook_name, - 'conflicting_keys': conflicting_keys}) - raise exceptions.NoUniqueMatch(msg) - - extra_kwargs.update(hook_kwargs) - - return extra_kwargs - - def pretty_choice_list(l): return ', '.join("'%s'" % i for i in l) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 7c05d1691..5fcff2df9 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -877,9 +877,6 @@ def do_boot(cs, args): """Boot a new server.""" boot_args, boot_kwargs = _boot(cs, args) - extra_boot_kwargs = utils.get_resource_manager_extra_kwargs(do_boot, args) - boot_kwargs.update(extra_boot_kwargs) - server = cs.servers.create(*boot_args, **boot_kwargs) if boot_kwargs['reservation_id']: new_server = {'reservation_id': server} @@ -1815,13 +1812,11 @@ def do_rebuild(cs, args): else: _password = None - kwargs = utils.get_resource_manager_extra_kwargs(do_rebuild, args) - kwargs['preserve_ephemeral'] = args.preserve_ephemeral - kwargs['name'] = args.name + kwargs = {'preserve_ephemeral': args.preserve_ephemeral, + 'name': args.name, + 'meta': _meta_parsing(args.meta)} if 'description' in args: kwargs['description'] = args.description - meta = _meta_parsing(args.meta) - kwargs['meta'] = meta # 2.57 deprecates the --file option and adds the --user-data and # --user-data-unset options. @@ -1910,8 +1905,7 @@ def do_resize(cs, args): """Resize a server.""" server = _find_server(cs, args.server) flavor = _find_flavor(cs, args.flavor) - kwargs = utils.get_resource_manager_extra_kwargs(do_resize, args) - server.resize(flavor, **kwargs) + server.resize(flavor) if args.poll: _poll_for_status(cs.servers.get, server.id, 'resizing', ['active', 'verify_resize']) diff --git a/releasenotes/notes/get-rid-off-redundant-methods-47e679c13e88f28a.yaml b/releasenotes/notes/get-rid-off-redundant-methods-47e679c13e88f28a.yaml new file mode 100644 index 000000000..954ae395d --- /dev/null +++ b/releasenotes/notes/get-rid-off-redundant-methods-47e679c13e88f28a.yaml @@ -0,0 +1,10 @@ +--- +deprecations: + - | + ``novaclient.utils.add_resource_manager_extra_kwargs_hook`` and + ``novaclient.utils.get_resource_manager_extra_kwargs`` were designed for + supporting extensions in nova/novaclient. Nowadays, this "extensions" + feature is abandoned and both ``add_resource_manager_extra_kwargs_hook``, + ``add_resource_manager_extra_kwargs_hook`` are not used in novaclient's + code. These methods are not documented, so we are removing them without + standard deprecation cycle. From 0537f3025fe0b1043581ae7174bd44aa5cbc81f4 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Wed, 14 Feb 2018 12:29:58 +0000 Subject: [PATCH 1418/1705] Updated from global requirements Change-Id: Idab1627d1b4d6fecc02d482f931a9ae072b46f09 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index d66dda685..140b64d75 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -11,7 +11,7 @@ mock>=2.0.0 # BSD python-keystoneclient>=3.8.0 # Apache-2.0 python-cinderclient>=3.3.0 # Apache-2.0 python-glanceclient>=2.8.0 # Apache-2.0 -python-neutronclient>=6.3.0 # Apache-2.0 +python-neutronclient>=6.7.0 # Apache-2.0 requests-mock>=1.1.0 # Apache-2.0 os-client-config>=1.28.0 # Apache-2.0 osprofiler>=1.4.0 # Apache-2.0 From 665fe5bd8e8b1b474cf17b75fbd4715f948dc8df Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Sat, 17 Feb 2018 10:16:08 +0000 Subject: [PATCH 1419/1705] Updated from global requirements Change-Id: I7288c55f19aec77faf8d1a16ec1b625f2cc15ce1 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d0c324613..502c9df15 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. pbr!=2.1.0,>=2.0.0 # Apache-2.0 -keystoneauth1>=3.3.0 # Apache-2.0 +keystoneauth1>=3.4.0 # Apache-2.0 iso8601>=0.1.11 # MIT oslo.i18n>=3.15.3 # Apache-2.0 oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0 From 02343c2d8cc51f9818c0b89691f2b0a878c611b7 Mon Sep 17 00:00:00 2001 From: Tatiana Kholkina Date: Mon, 19 Feb 2018 15:27:45 +0300 Subject: [PATCH 1420/1705] Fix the docstring for the update method It is impossible to provide a description to update method in versions 2.0-2.18. But the docstring of the method says that it can update a description. Change-Id: I832e8a752779277a045bd127e2e6c6a3ae88f840 --- novaclient/v2/servers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index 805dd4541..4e7168793 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -69,7 +69,7 @@ def delete(self): @api_versions.wraps("2.0", "2.18") def update(self, name=None): """ - Update the name and the description for this server. + Update the name for this server. :param name: Update the server's name. :returns: :class:`Server` From bc4f9195adf196bf4d0fabc4baa98809cdf9430f Mon Sep 17 00:00:00 2001 From: Nguyen Hung Phuong Date: Wed, 21 Feb 2018 15:49:49 +0700 Subject: [PATCH 1421/1705] Clean imports in code In some part in the code we import objects. In the Openstack style guidelines they recommend to import only modules. [1]: "Do not import objects, only modules". [1] https://docs.openstack.org/hacking/0.10.3/ Change-Id: Id4b47eeae1d3e86a3cb259c32fcb376ffbd3f8c7 --- novaclient/base.py | 4 ++-- novaclient/tests/unit/test_base.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/novaclient/base.py b/novaclient/base.py index 328b25126..1b2c590ce 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -28,7 +28,7 @@ from oslo_utils import reflection from oslo_utils import strutils -from requests import Response +import requests import six from novaclient import exceptions @@ -103,7 +103,7 @@ def append_request_ids(self, resp): self._append_request_id(resp) def _append_request_id(self, resp): - if isinstance(resp, Response): + if isinstance(resp, requests.Response): # Extract 'x-openstack-request-id' from headers if # response is a Response object. request_id = (resp.headers.get('x-openstack-request-id') or diff --git a/novaclient/tests/unit/test_base.py b/novaclient/tests/unit/test_base.py index eb70dff99..0b1ee83aa 100644 --- a/novaclient/tests/unit/test_base.py +++ b/novaclient/tests/unit/test_base.py @@ -11,7 +11,7 @@ # License for the specific language governing permissions and limitations # under the License. -from requests import Response +import requests import six from novaclient import api_versions @@ -23,13 +23,13 @@ def create_response_obj_with_header(): - resp = Response() + resp = requests.Response() resp.headers['x-openstack-request-id'] = fakes.FAKE_REQUEST_ID return resp def create_response_obj_with_compute_header(): - resp = Response() + resp = requests.Response() resp.headers['x-compute-request-id'] = fakes.FAKE_REQUEST_ID return resp From 3b73249729aeeadcd6c4f7b9424a8091f6f8ce2b Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Thu, 22 Feb 2018 16:25:39 +0900 Subject: [PATCH 1422/1705] Fix a comment in novaclient/api_versions.py 'cliutils.arg' has been replaced with 'utils.arg' in the following patches. Iaec234fbcf4d0f8c7e8f2175eae11d3083a62090 I12b03aa0a13c95ae949adf7e876c675ce309bae5 I405044af3912d86da66df05413edfc724bc102d0 I8103adafde7d8b3a101181366639314740f9a25a So replace 'cliutils.arg' with 'utils.arg' in a comment. TrivialFix Change-Id: Id26b941979dd1a94c20c05d4debcd0ed2f1470e0 --- novaclient/api_versions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/api_versions.py b/novaclient/api_versions.py index 61cded280..231765198 100644 --- a/novaclient/api_versions.py +++ b/novaclient/api_versions.py @@ -393,7 +393,7 @@ def substitution(obj, *args, **kwargs): return methods[-1].func(obj, *args, **kwargs) # Let's share "arguments" with original method and substitution to - # allow put cliutils.arg and wraps decorators in any order + # allow put utils.arg and wraps decorators in any order if not hasattr(func, 'arguments'): func.arguments = [] substitution.arguments = func.arguments From a4966ea192833e1f154b89c8b1ad08c2958ffdc0 Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Wed, 7 Mar 2018 13:47:05 +0900 Subject: [PATCH 1423/1705] Add os-testr in test-requirements.txt The py27 and py35 jobs fail because the subunit-trace command is not found. The command is utilized in tools/pretty_tox.sh. But os-testr is not included in test-requirements.txt. So add it to fix the gate job failures. Change-Id: I5ea7269d75413ebcc1311e4d9e6e1d1f60092cc9 Closes-Bug: #1753898 --- test-requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/test-requirements.txt b/test-requirements.txt index 140b64d75..8ae73f870 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -14,6 +14,7 @@ python-glanceclient>=2.8.0 # Apache-2.0 python-neutronclient>=6.7.0 # Apache-2.0 requests-mock>=1.1.0 # Apache-2.0 os-client-config>=1.28.0 # Apache-2.0 +os-testr>=1.0.0 # Apache-2.0 osprofiler>=1.4.0 # Apache-2.0 testrepository>=0.0.18 # Apache-2.0/BSD testscenarios>=0.4 # Apache-2.0/BSD From c15a85ab0fa6bc8e4b415a07f99826c23677a727 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Wed, 7 Mar 2018 18:57:24 +0000 Subject: [PATCH 1424/1705] Updated from global requirements Change-Id: I3329b703944b9f39df1f6b70878abc0c58c2fc12 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 8ae73f870..71db96d28 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -14,7 +14,7 @@ python-glanceclient>=2.8.0 # Apache-2.0 python-neutronclient>=6.7.0 # Apache-2.0 requests-mock>=1.1.0 # Apache-2.0 os-client-config>=1.28.0 # Apache-2.0 -os-testr>=1.0.0 # Apache-2.0 +os-testr>=1.0.0 # Apache-2.0 osprofiler>=1.4.0 # Apache-2.0 testrepository>=0.0.18 # Apache-2.0/BSD testscenarios>=0.4 # Apache-2.0/BSD From adf1f3b997e0ce83a64b069d52ca4089dea8e584 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Sat, 10 Mar 2018 13:49:20 +0000 Subject: [PATCH 1425/1705] Updated from global requirements Change-Id: I34e1019d1a78344bc4d4bec72d7a8639a214933d --- doc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index fa2038978..b1295c78f 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,6 +1,6 @@ # 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. -sphinx!=1.6.6,>=1.6.2 # BSD +sphinx!=1.6.6,<1.7.0,>=1.6.2 # BSD openstackdocstheme>=1.18.1 # Apache-2.0 reno>=2.5.0 # Apache-2.0 From ecf8029152b105f3ab961162a6f528a94679360d Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Tue, 13 Mar 2018 07:27:42 +0000 Subject: [PATCH 1426/1705] Updated from global requirements Change-Id: Ic73c18e76a4200a82d88d5eb63c24eab6b15cac4 --- doc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index b1295c78f..fa2038978 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,6 +1,6 @@ # 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. -sphinx!=1.6.6,<1.7.0,>=1.6.2 # BSD +sphinx!=1.6.6,>=1.6.2 # BSD openstackdocstheme>=1.18.1 # Apache-2.0 reno>=2.5.0 # Apache-2.0 From 52c65fb4d3f995a597c8367d9f75b8efdf643794 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Thu, 15 Mar 2018 08:01:06 +0000 Subject: [PATCH 1427/1705] Updated from global requirements Change-Id: Iee40a8c0d7c6732cddac59e99ee39ebe3baf68fe --- doc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index fa2038978..012efb226 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,6 +1,6 @@ # 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. -sphinx!=1.6.6,>=1.6.2 # BSD +sphinx!=1.6.6,!=1.6.7,>=1.6.2 # BSD openstackdocstheme>=1.18.1 # Apache-2.0 reno>=2.5.0 # Apache-2.0 From c0f2f96d2f787a988b95dec895b48c0d50e36f4a Mon Sep 17 00:00:00 2001 From: Nguyen Hai Date: Thu, 15 Mar 2018 23:54:21 +0900 Subject: [PATCH 1428/1705] Fix local test fails with pypy tox: ERROR: pypy: InterpreterNotFound: pypy Maybe we need to remove pypy-constraints from envlist in tox.ini because there is no effect to review. Change-Id: I1ff1cb103dcaad1396bc2bda487165feae6b0931 Closes-Bug: #1756108 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 6dbff636a..37ece4472 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ # noted to use py35 you need virtualenv >= 1.11.4 [tox] -envlist = py35,py27,pypy,pep8,docs +envlist = py35,py27,pep8,docs minversion = 2.0 skipsdist = True From f36390dbcef95a5afdb9f3d32b579c7ffc861789 Mon Sep 17 00:00:00 2001 From: Doug Hellmann Date: Thu, 22 Mar 2018 17:57:39 -0400 Subject: [PATCH 1429/1705] add lower-constraints job Create a tox environment for running the unit tests against the lower bounds of the dependencies. Create a lower-constraints.txt to be used to enforce the lower bounds in those tests. Add openstack-tox-lower-constraints job to the zuul configuration. See http://lists.openstack.org/pipermail/openstack-dev/2018-March/128352.html for more details. Change-Id: I5c23fc5f4e31b461255865fb8973cc05c448e364 Depends-On: https://review.openstack.org/555034 Signed-off-by: Doug Hellmann --- .zuul.yaml | 2 + lower-constraints.txt | 111 ++++++++++++++++++++++++++++++++++++++++++ tox.ini | 7 +++ 3 files changed, 120 insertions(+) create mode 100644 lower-constraints.txt diff --git a/.zuul.yaml b/.zuul.yaml index 8424e55e2..f8ae1458d 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -13,6 +13,8 @@ check: jobs: - novaclient-dsvm-functional + - openstack-tox-lower-constraints gate: jobs: - novaclient-dsvm-functional + - openstack-tox-lower-constraints diff --git a/lower-constraints.txt b/lower-constraints.txt new file mode 100644 index 000000000..3f7fd32a3 --- /dev/null +++ b/lower-constraints.txt @@ -0,0 +1,111 @@ +amqp==2.1.1 +appdirs==1.3.0 +asn1crypto==0.23.0 +Babel==2.3.4 +bandit==1.4.0 +cachetools==2.0.0 +cffi==1.7.0 +cliff==2.8.0 +cmd2==0.8.0 +contextlib2==0.4.0 +coverage==4.0 +cryptography==2.1 +debtcollector==1.2.0 +decorator==3.4.0 +deprecation==1.0 +dogpile.cache==0.6.2 +eventlet==0.18.2 +extras==1.0.0 +fasteners==0.7.0 +fixtures==3.0.0 +flake8==2.5.5 +future==0.16.0 +futurist==1.2.0 +gitdb==0.6.4 +GitPython==1.0.1 +greenlet==0.4.10 +hacking==0.12.0 +idna==2.6 +iso8601==0.1.11 +Jinja2==2.10 +jmespath==0.9.0 +jsonpatch==1.16 +jsonpointer==1.13 +jsonschema==2.6.0 +keyring==5.5.1 +keystoneauth1==3.4.0 +kombu==4.0.0 +linecache2==1.0.0 +MarkupSafe==1.0 +mccabe==0.2.1 +mock==2.0.0 +monotonic==0.6 +msgpack-python==0.4.0 +munch==2.1.0 +netaddr==0.7.18 +netifaces==0.10.4 +openstacksdk==0.11.2 +os-client-config==1.28.0 +os-service-types==1.2.0 +os-testr==1.0.0 +osc-lib==1.8.0 +oslo.concurrency==3.25.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 +osprofiler==1.4.0 +paramiko==2.0.0 +Paste==2.0.2 +PasteDeploy==1.5.0 +pbr==2.0.0 +pep8==1.5.7 +pika==0.10.0 +pika-pool==0.1.3 +positional==1.2.1 +prettytable==0.7.2 +pyasn1==0.1.8 +pycparser==2.18 +pyflakes==0.8.1 +pyinotify==0.9.6 +pyOpenSSL==17.1.0 +pyparsing==2.1.0 +pyperclip==1.5.27 +python-cinderclient==3.3.0 +python-dateutil==2.5.3 +python-glanceclient==2.8.0 +python-keystoneclient==3.8.0 +python-mimeparse==1.6.0 +python-neutronclient==6.7.0 +python-subunit==1.0.0 +pytz==2013.6 +PyYAML==3.12 +repoze.lru==0.7 +requests==2.14.2 +requests-mock==1.1.0 +requestsexceptions==1.2.0 +rfc3986==0.3.1 +Routes==2.3.1 +simplejson==3.5.1 +six==1.10.0 +smmap==0.9.0 +statsd==3.2.1 +stestr==1.0.0 +stevedore==1.20.0 +tempest==17.1.0 +tenacity==3.2.1 +testrepository==0.0.18 +testscenarios==0.4 +testtools==2.2.0 +traceback2==1.4.0 +unittest2==1.1.0 +urllib3==1.21.1 +vine==1.1.4 +warlock==1.2.0 +WebOb==1.7.1 +wrapt==1.7.0 diff --git a/tox.ini b/tox.ini index 37ece4472..b4dfa8f44 100644 --- a/tox.ini +++ b/tox.ini @@ -98,3 +98,10 @@ import_exceptions = novaclient.i18n # separately, outside of the requirements files. deps = bindep commands = bindep test + +[testenv:lower-constraints] +basepython = python3 +deps = + -c{toxinidir}/lower-constraints.txt + -r{toxinidir}/test-requirements.txt + -r{toxinidir}/requirements.txt From 41dde2a2b2f31d43191103bf1d0c13ab18f18321 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 23 Mar 2018 01:50:05 +0000 Subject: [PATCH 1430/1705] Updated from global requirements Change-Id: I2bbc33f1b8047c28ee257f588f5b33ba0e11dbbd --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 71db96d28..e4062860d 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -12,7 +12,7 @@ python-keystoneclient>=3.8.0 # Apache-2.0 python-cinderclient>=3.3.0 # Apache-2.0 python-glanceclient>=2.8.0 # Apache-2.0 python-neutronclient>=6.7.0 # Apache-2.0 -requests-mock>=1.1.0 # Apache-2.0 +requests-mock>=1.2.0 # Apache-2.0 os-client-config>=1.28.0 # Apache-2.0 os-testr>=1.0.0 # Apache-2.0 osprofiler>=1.4.0 # Apache-2.0 From 026388630eb544af9a0206eb8a5472e1dd4981d8 Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Tue, 3 Apr 2018 09:15:48 +0900 Subject: [PATCH 1431/1705] Fix validation for command arguments In the "Update Aggregate" API, if both an availability zone and a name are not specified, a 400 error is returned. It should be checked in the novaclient side (nova command). So add the validation for it at the 'nova aggregate-update' command. Change-Id: If50579ef3572a10b67e6da32e3258917901e9d9d Closes-Bug: #1696891 --- novaclient/tests/unit/v2/test_shell.py | 7 +++++++ novaclient/v2/shell.py | 5 +++++ 2 files changed, 12 insertions(+) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index dfa798b85..b56eb0033 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2243,6 +2243,13 @@ def test_aggregate_update_with_availability_zone_by_name(self): self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) self.assert_called('GET', '/os-aggregates/1', pos=-1) + def test_aggregate_update_without_availability_zone_and_name(self): + ex = self.assertRaises(exceptions.CommandError, self.run_command, + 'aggregate-update test') + self.assertIn("Either '--name ' or '--availability-zone " + "' must be specified.", + six.text_type(ex)) + def test_aggregate_set_metadata_add_by_id(self): out, err = self.run_command('aggregate-set-metadata 3 foo=bar') body = {"set_metadata": {"metadata": {"foo": "bar"}}} diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 6b21c4880..de5b52bbc 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -3175,6 +3175,11 @@ def do_aggregate_update(cs, args): if args.availability_zone: updates["availability_zone"] = args.availability_zone + if not updates: + raise exceptions.CommandError(_( + "Either '--name ' or '--availability-zone " + "' must be specified.")) + aggregate = cs.aggregates.update(aggregate.id, updates) print(_("Aggregate %s has been successfully updated.") % aggregate.id) _print_aggregate_details(cs, aggregate) From e044b911c2f8a982a4514878762e01f3b668c18c Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Tue, 3 Apr 2018 09:37:51 +0900 Subject: [PATCH 1432/1705] Fix AttributeError in getting a resource ID In the 'wait_for_resource_delete' method of the 'ClientTestBase' class, the following statement is executed when timeout. self.fail("The resource '%s' still exists." % resource.id) The 'resource' variable is passed in the argument of the 'wait_for_resource_delete' method. It is sometimes a string which is a resource ID. When it is a string (a resource ID), AttributeError is raised at the statement. This patch fixes the issue. Change-Id: I558026de54a5cc75359225b50054934a220f3e5c Closes-Bug: #1704132 --- novaclient/tests/functional/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/novaclient/tests/functional/base.py b/novaclient/tests/functional/base.py index 2c83f3027..57f822b2d 100644 --- a/novaclient/tests/functional/base.py +++ b/novaclient/tests/functional/base.py @@ -29,6 +29,7 @@ import novaclient import novaclient.api_versions +from novaclient import base import novaclient.client from novaclient.v2 import networks import novaclient.v2.shell @@ -371,7 +372,7 @@ def wait_for_resource_delete(self, resource, manager, raise time.sleep(poll_interval) else: - self.fail("The resource '%s' still exists." % resource.id) + self.fail("The resource '%s' still exists." % base.getid(resource)) def name_generate(self): """Generate randomized name for some entity.""" From be94318add2b0c89a94b1a6a542ed2ff74ce3b48 Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Tue, 3 Apr 2018 14:26:01 +0900 Subject: [PATCH 1433/1705] Replace GB with GiB In documents and comments, 'GB' (gigabyte) is used. Strictly 'GiB' (gibibyte) should be used. So replace 'GB' with 'GiB'. Change-Id: Ic03f202d4fe357bc6400275abdccd4b37521f4a3 Closes-Bug: #1521791 --- doc/source/reference/api/index.rst | 10 +++++----- novaclient/base.py | 2 +- .../functional/v2/legacy/test_servers.py | 2 +- novaclient/tests/unit/v2/test_shell.py | 8 ++++---- novaclient/v2/flavors.py | 4 ++-- novaclient/v2/shell.py | 20 +++++++++---------- 6 files changed, 23 insertions(+), 23 deletions(-) diff --git a/doc/source/reference/api/index.rst b/doc/source/reference/api/index.rst index 0d2938347..e332c3647 100644 --- a/doc/source/reference/api/index.rst +++ b/doc/source/reference/api/index.rst @@ -82,11 +82,11 @@ Then call methods on its managers:: >>> nova.flavors.list() [, , - , - , - , - , - ] + , + , + , + , + ] >>> fl = nova.flavors.find(ram=512) >>> nova.servers.create("my-server", flavor=fl) diff --git a/novaclient/base.py b/novaclient/base.py index 1b2c590ce..3bf61856b 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -501,7 +501,7 @@ def _parse_block_device_mapping(self, block_device_mapping): for device_name, mapping in block_device_mapping.items(): # # The mapping is in the format: - # :[]:[]:[] + # :[]:[]:[] # bdm_dict = {'device_name': device_name} diff --git a/novaclient/tests/functional/v2/legacy/test_servers.py b/novaclient/tests/functional/v2/legacy/test_servers.py index 5980e94f5..aa812bb20 100644 --- a/novaclient/tests/functional/v2/legacy/test_servers.py +++ b/novaclient/tests/functional/v2/legacy/test_servers.py @@ -61,7 +61,7 @@ def _boot_server_with_legacy_bdm(self, bdm_params=()): def test_boot_server_with_legacy_bdm(self): # bdm v1 format - # ::: + # ::: # params = (type, size, delete-on-terminate) params = ('', '', '1') self._boot_server_with_legacy_bdm(bdm_params=params) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index dfa798b85..37b7f48d4 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2051,7 +2051,7 @@ def test_usage_list(self): 'start=2000-01-20T00:00:00&' + 'end=2005-02-01T00:00:00&' + 'detailed=1') - # Servers, RAM MB-Hours, CPU Hours, Disk GB-Hours + # Servers, RAM MB-Hours, CPU Hours, Disk GiB-Hours self.assertIn('1 | 25451.76 | 49.71 | 0.00', stdout) def test_usage_list_stitch_together_next_results(self): @@ -2072,7 +2072,7 @@ def test_usage_list_stitch_together_next_results(self): 'start=2000-01-20T00:00:00&' 'end=2005-02-01T00:00:00&' 'marker=%s&detailed=1' % (marker), pos=pos + 1) - # Servers, RAM MB-Hours, CPU Hours, Disk GB-Hours + # Servers, RAM MB-Hours, CPU Hours, Disk GiB-Hours self.assertIn('2 | 50903.53 | 99.42 | 0.00', stdout) def test_usage_list_no_args(self): @@ -2092,7 +2092,7 @@ def test_usage(self): '/os-simple-tenant-usage/test?' + 'start=2000-01-20T00:00:00&' + 'end=2005-02-01T00:00:00') - # Servers, RAM MB-Hours, CPU Hours, Disk GB-Hours + # Servers, RAM MB-Hours, CPU Hours, Disk GiB-Hours self.assertIn('1 | 25451.76 | 49.71 | 0.00', stdout) def test_usage_stitch_together_next_results(self): @@ -2112,7 +2112,7 @@ def test_usage_stitch_together_next_results(self): 'start=2000-01-20T00:00:00&' 'end=2005-02-01T00:00:00&' 'marker=%s' % (marker), pos=pos + 1) - # Servers, RAM MB-Hours, CPU Hours, Disk GB-Hours + # Servers, RAM MB-Hours, CPU Hours, Disk GiB-Hours self.assertIn('2 | 50903.53 | 99.42 | 0.00', stdout) def test_usage_no_tenant(self): diff --git a/novaclient/v2/flavors.py b/novaclient/v2/flavors.py index c4b6e40ce..89884bae1 100644 --- a/novaclient/v2/flavors.py +++ b/novaclient/v2/flavors.py @@ -184,11 +184,11 @@ def create(self, name, ram, vcpus, disk, flavorid="auto", :param name: Descriptive name of the flavor :param ram: Memory in MB for the flavor :param vcpus: Number of VCPUs for the flavor - :param disk: Size of local disk in GB + :param disk: Size of local disk in GiB :param flavorid: ID for the flavor (optional). You can use the reserved value ``"auto"`` to have Nova generate a UUID for the flavor in cases where you cannot simply pass ``None``. - :param ephemeral: Ephemeral disk space in GB. + :param ephemeral: Ephemeral disk space in GiB. :param swap: Swap space in MB :param rxtx_factor: RX/TX factor :param is_public: Whether or not the flavor is public. diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 6b21c4880..20298b689 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -599,7 +599,7 @@ def _boot(cs, args): action='append', default=[], help=_("Block device mapping in the format " - "=:::.")) + "=:::.")) @utils.arg( '--block-device', metavar="key1=value1[,key2=value2...]", @@ -621,7 +621,7 @@ def _boot(cs, args): "depending on selected bus; note the libvirt driver always " "uses default device names), " "size=size of the block device in MB(for swap) and in " - "GB(for other formats) " + "GiB(for other formats) " "(if omitted, hypervisor driver calculates size), " "format=device will be formatted (e.g. swap, ntfs, ...; optional), " "bootindex=integer used for ordering the boot disks " @@ -651,7 +651,7 @@ def _boot(cs, args): "depending on selected bus; note the libvirt driver always " "uses default device names), " "size=size of the block device in MB(for swap) and in " - "GB(for other formats) " + "GiB(for other formats) " "(if omitted, hypervisor driver calculates size), " "format=device will be formatted (e.g. swap, ntfs, ...; optional), " "bootindex=integer used for ordering the boot disks " @@ -680,7 +680,7 @@ def _boot(cs, args): "depending on selected bus; note the libvirt driver always " "uses default device names), " "size=size of the block device in MB(for swap) and in " - "GB(for other formats) " + "GiB(for other formats) " "(if omitted, hypervisor driver calculates size), " "format=device will be formatted (e.g. swap, ntfs, ...; optional), " "bootindex=integer used for ordering the boot disks " @@ -708,7 +708,7 @@ def _boot(cs, args): "depending on selected bus; note the libvirt driver always " "uses default device names), " "size=size of the block device in MB(for swap) and in " - "GB(for other formats) " + "GiB(for other formats) " "(if omitted, hypervisor driver calculates size), " "format=device will be formatted (e.g. swap, ntfs, ...; optional), " "bootindex=integer used for ordering the boot disks " @@ -727,7 +727,7 @@ def _boot(cs, args): metavar="size=[,format=]", action='append', default=[], - help=_("Create and attach a local ephemeral block device of GB " + help=_("Create and attach a local ephemeral block device of GiB " "and format it to .")) @utils.arg( '--hint', @@ -1130,11 +1130,11 @@ def do_flavor_show(cs, args): @utils.arg( 'disk', metavar='', - help=_("Disk size in GB.")) + help=_("Disk size in GiB.")) @utils.arg( '--ephemeral', metavar='', - help=_("Ephemeral space size in GB (default 0)."), + help=_("Ephemeral space size in GiB (default 0)."), default=0) @utils.arg( 'vcpus', @@ -2939,7 +2939,7 @@ def do_usage_list(cs, args): """List usage data for all tenants.""" dateformat = "%Y-%m-%d" rows = ["Tenant ID", "Servers", "RAM MB-Hours", "CPU Hours", - "Disk GB-Hours"] + "Disk GiB-Hours"] now = timeutils.utcnow() @@ -3007,7 +3007,7 @@ def simplify_usage(u): def do_usage(cs, args): """Show usage data for a single tenant.""" dateformat = "%Y-%m-%d" - rows = ["Servers", "RAM MB-Hours", "CPU Hours", "Disk GB-Hours"] + rows = ["Servers", "RAM MB-Hours", "CPU Hours", "Disk GiB-Hours"] now = timeutils.utcnow() From bcc7d8f1138ea22207ac0d31c5be132d6f274b34 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Mon, 16 Apr 2018 10:30:32 -0400 Subject: [PATCH 1434/1705] Stop printing flavor details on successful flavor-delete It's weird that the flavor-delete CLI would print out the details of the flavor the user just deleted, rather than give some confirmation message, or simply not print anything out at all, which is arguably more expected behavior when deleting a resource. This change simply removes the output on successful deletion which brings "nova flavor-delete" more inline with other delete CLIs, including "openstack flavor delete" behavior. While in here, the variable and docstring code is updated to reflect reality and a release note is added in case anyone was ever relying on this odd behavior. Change-Id: Ie3e07b45cc70213cde4e6077c604c9f2835c75ad Closes-Bug: #1764420 --- novaclient/v2/flavors.py | 3 ++- novaclient/v2/shell.py | 5 ++--- .../bug-1764420-flavor-delete-output-7b80f73deee5a869.yaml | 5 +++++ 3 files changed, 9 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/bug-1764420-flavor-delete-output-7b80f73deee5a869.yaml diff --git a/novaclient/v2/flavors.py b/novaclient/v2/flavors.py index 89884bae1..f015a06e8 100644 --- a/novaclient/v2/flavors.py +++ b/novaclient/v2/flavors.py @@ -155,7 +155,8 @@ def get(self, flavor): def delete(self, flavor): """Delete a specific flavor. - :param flavor: The ID of the :class:`Flavor` to get. + :param flavor: Instance of :class:`Flavor` to delete or ID of the + flavor to delete. :returns: An instance of novaclient.base.TupleWithMeta """ return self._delete("/flavors/%s" % base.getid(flavor)) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 28d4427d4..4801cfcf8 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -1099,9 +1099,8 @@ def do_flavor_list(cs, args): help=_("Name or ID of the flavor to delete.")) def do_flavor_delete(cs, args): """Delete a specific flavor""" - flavorid = _find_flavor(cs, args.flavor) - cs.flavors.delete(flavorid) - _print_flavor_list(cs, [flavorid]) + flavor = _find_flavor(cs, args.flavor) + cs.flavors.delete(flavor) @utils.arg( diff --git a/releasenotes/notes/bug-1764420-flavor-delete-output-7b80f73deee5a869.yaml b/releasenotes/notes/bug-1764420-flavor-delete-output-7b80f73deee5a869.yaml new file mode 100644 index 000000000..7d95eb391 --- /dev/null +++ b/releasenotes/notes/bug-1764420-flavor-delete-output-7b80f73deee5a869.yaml @@ -0,0 +1,5 @@ +--- +upgrade: + - | + The ``flavor-delete`` command no longer prints out the details of the + deleted flavor. On successful deletion, there is no output. From 936b5a572d43c7694e3534c52331c2658cd04b40 Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Tue, 17 Apr 2018 11:35:20 +0900 Subject: [PATCH 1435/1705] Fix comments in novaclient/tests/unit/fakes.py Replace 'Assert than' with 'Assert that'. TrivialFix Change-Id: I426895467dfa693214f78b495d82e730f4307b9d --- novaclient/tests/unit/fakes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/novaclient/tests/unit/fakes.py b/novaclient/tests/unit/fakes.py index e89370bc6..b2820a48a 100644 --- a/novaclient/tests/unit/fakes.py +++ b/novaclient/tests/unit/fakes.py @@ -42,7 +42,7 @@ def assert_has_keys(dict, required=None, optional=None): class FakeClient(object): def assert_called(self, method, url, body=None, pos=-1): - """Assert than an HTTP method was called at given order/position. + """Assert that an HTTP method was called at given order/position. :param method: HTTP method name which is expected to be called :param url: Expected request url to be called with given method @@ -98,7 +98,7 @@ def assert_called(self, method, url, body=None, pos=-1): (self.client.callstack[pos][2], body)) def assert_called_anytime(self, method, url, body=None): - """Assert than an HTTP method was called anytime in the test. + """Assert that an HTTP method was called anytime in the test. :param method: HTTP method name which is expected to be called :param url: Expected request url to be called with given method From 229d0df752702700dd30ddbe6d94d5efd5477318 Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Mon, 16 Apr 2018 12:35:00 +0900 Subject: [PATCH 1436/1705] Microversion 2.61 - support extra_specs in flavor API Starting from microversion 2.61, the responses of the 'Flavor' APIs include the 'extra_specs' parameter. Therefore 'Flavors extra-specs' (os-extra_specs) API calls have been removed in the following commands since microversion 2.61. * nova flavor-list * nova flavor-show Change-Id: I10d621d9b62764114d55cb368e720d182eaffd11 Implements: blueprint add-extra-specs-to-flavor-list --- novaclient/__init__.py | 2 +- novaclient/tests/unit/fakes.py | 13 ++++++++++ novaclient/tests/unit/v2/fakes.py | 7 ++++++ novaclient/tests/unit/v2/test_shell.py | 24 ++++++++++++++++++- novaclient/v2/shell.py | 13 ++++++---- .../microversion-v2_61-9a8faa02fddf9ed6.yaml | 13 ++++++++++ 6 files changed, 66 insertions(+), 6 deletions(-) create mode 100644 releasenotes/notes/microversion-v2_61-9a8faa02fddf9ed6.yaml diff --git a/novaclient/__init__.py b/novaclient/__init__.py index c114e5b9c..6795d8827 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.60") +API_MAX_VERSION = api_versions.APIVersion("2.61") diff --git a/novaclient/tests/unit/fakes.py b/novaclient/tests/unit/fakes.py index e89370bc6..b380b0ddf 100644 --- a/novaclient/tests/unit/fakes.py +++ b/novaclient/tests/unit/fakes.py @@ -131,6 +131,19 @@ def assert_called_anytime(self, method, url, body=None): self.client.callstack = [] + def assert_not_called(self, method, url, body=None): + """Assert that an HTTP method was not called in the test. + + :param method: HTTP method name which is expected not to be called + :param url: Expected request url not to be called with given method + :param body: Expected request body not to be called with given method + and url. Default is None. + """ + not_expected = (method, url, body) + for entry in self.client.callstack: + assert not_expected != entry[0:3], ( + 'API %s %s body=%s was called.' % not_expected) + def clear_callstack(self): self.client.callstack = [] diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index e853ef223..7b905b9eb 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -837,6 +837,8 @@ def get_flavors(self, **kw): included_fields = ['id', 'name'] if self.api_version >= api_versions.APIVersion('2.55'): included_fields.append('description') + if self.api_version >= api_versions.APIVersion('2.61'): + included_fields.append('extra_specs') for flavor in flavors['flavors']: for k in list(flavor): if k not in included_fields: @@ -897,6 +899,11 @@ def get_flavors_detail(self, **kw): new_flavor['description'] = 'test description' flavors['flavors'].append(new_flavor) + # Add extra_specs in the response for all flavors. + if self.api_version >= api_versions.APIVersion('2.61'): + for flavor in flavors['flavors']: + flavor['extra_specs'] = {'test': 'value'} + return (200, FAKE_RESPONSE_HEADERS, flavors) def get_flavors_1(self, **kw): diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index df646691e..997063416 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -104,6 +104,9 @@ def assert_called(self, method, url, body=None, **kwargs): def assert_called_anytime(self, method, url, body=None): return self.shell.cs.assert_called_anytime(method, url, body) + def assert_not_called(self, method, url, body=None): + return self.shell.cs.assert_not_called(method, url, body) + def test_agents_list_with_hypervisor(self): self.run_command('agent-list --hypervisor xen') self.assert_called('GET', '/os-agents?hypervisor=xen') @@ -1168,6 +1171,16 @@ def test_flavor_list_with_extra_specs(self): self.assert_called('GET', '/flavors/aa1/os-extra_specs') self.assert_called_anytime('GET', '/flavors/detail') + def test_flavor_list_with_extra_specs_2_61_or_later(self): + """Tests that the 'os-extra_specs' API is not called + when the '--extra-specs' option is specified since microversion 2.61. + """ + out, _ = self.run_command('flavor-list --extra-specs', + api_version='2.61') + self.assert_not_called('GET', '/flavors/aa1/os-extra_specs') + self.assert_called_anytime('GET', '/flavors/detail') + self.assertIn('extra_specs', out) + def test_flavor_list_with_all(self): self.run_command('flavor-list --all') self.assert_called('GET', '/flavors/detail?is_public=None') @@ -1196,9 +1209,17 @@ def test_flavor_show(self): def test_flavor_show_with_description(self): """Tests that the description is shown in version >= 2.55.""" out, _ = self.run_command('flavor-show 1', api_version='2.55') - self.assert_called_anytime('GET', '/flavors/1') + self.assert_called('GET', '/flavors/1', pos=-2) + self.assert_called('GET', '/flavors/1/os-extra_specs', pos=-1) self.assertIn('description', out) + def test_flavor_show_2_61_or_later(self): + """Tests that the 'os-extra_specs' is not called in version >= 2.61.""" + out, _ = self.run_command('flavor-show 1', api_version='2.61') + self.assert_not_called('GET', '/flavors/1/os-extra_specs') + self.assert_called_anytime('GET', '/flavors/1') + self.assertIn('extra_specs', out) + def test_flavor_show_with_alphanum_id(self): self.run_command('flavor-show aa1') self.assert_called_anytime('GET', '/flavors/aa1') @@ -3612,6 +3633,7 @@ def test_versions(self): 54, # There are no version-wrapped shell method changes for this. 57, # There are no version-wrapped shell method changes for this. 60, # There are no client-side changes for volume multiattach. + 61, # There are no version-wrapped shell method changes for this. ]) versions_supported = set(range(0, novaclient.API_MAX_VERSION.ver_minor + 1)) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 4801cfcf8..d53fc05bc 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -1016,11 +1016,13 @@ def _print_flavor_list(cs, flavors, show_extra_specs=False): 'Is_Public', ] + formatters = {} if show_extra_specs: - formatters = {'extra_specs': _print_flavor_extra_specs} + # Starting with microversion 2.61, extra specs are included + # in the flavor details response. + if cs.api_version < api_versions.APIVersion('2.61'): + formatters = {'extra_specs': _print_flavor_extra_specs} headers.append('extra_specs') - else: - formatters = {} if cs.api_version >= api_versions.APIVersion('2.55'): headers.append('Description') @@ -1316,7 +1318,10 @@ def _print_flavor(flavor): info = flavor.to_dict() # ignore links, we don't need to present those info.pop('links') - info.update({"extra_specs": _print_flavor_extra_specs(flavor)}) + # Starting with microversion 2.61, extra specs are included + # in the flavor details response. + if 'extra_specs' not in info: + info.update({"extra_specs": _print_flavor_extra_specs(flavor)}) utils.print_dict(info) diff --git a/releasenotes/notes/microversion-v2_61-9a8faa02fddf9ed6.yaml b/releasenotes/notes/microversion-v2_61-9a8faa02fddf9ed6.yaml new file mode 100644 index 000000000..a96b65d3a --- /dev/null +++ b/releasenotes/notes/microversion-v2_61-9a8faa02fddf9ed6.yaml @@ -0,0 +1,13 @@ +--- +other: + - | + Starting from microversion 2.61, the responses of the 'Flavor' APIs + include the 'extra_specs' parameter. Therefore 'Flavors extra-specs' + (os-extra_specs) API calls have been removed in the following commands + since microversion 2.61. + + * ``nova flavor-list`` + * ``nova flavor-show`` + + There are no behavior changes in the CLI. This is just a performance + optimization. From 6049be67c0f66bd2d15c7b52f77dd89ac4ec2e94 Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Mon, 23 Jan 2017 19:22:14 +0200 Subject: [PATCH 1437/1705] [CLI] Fix token auth type There are 2 known issues which breaks token auth method in CLI: * The wrong check of flag (the check should be for --os-token since arguments are not parsed at that moment) is performed in CLI inner method `_append_global_identity_args`. It led to usage of "password" auth type by default[1] even if `--os-token` cli argument is specified. If `--os-auth-type` is specified to token, keystoneauth1 library makes the right decision[2]. * Based on an auth type, keystoneauth library registers different CLI arguments[3]. It means that `--os-username` argument is available only in password auth type, `--os-token` is available only in token auth type, etc. It also affects the way in which the python code should access such arguments. The arguments which are unrelated to the selected auth type are omitted from the parsed arguments object. That sounds reasonable, but unfortunately the code assumes the unrelated arguments are always present which leads to an AttributeError. Combination of these 2 issues made token auth type broken in CLI layer. [1] https://github.com/openstack/python-novaclient/blob/ee2221f0526c4a6bed431229e363c740d07b8ee9/novaclient/shell.py#L255-L257 [2] https://github.com/openstack/keystoneauth/blob/14dd37b34c4821abf145ea24e593eddaa9f607c8/keystoneauth1/loading/cli.py#L51-L52 [3] https://github.com/openstack/keystoneauth/blob/14dd37b34c4821abf145ea24e593eddaa9f607c8/keystoneauth1/loading/cli.py#L65-L73 Closes-Bug: #1659015 Change-Id: Ibc861d396b71fe105288d8336623cc22cf92523e --- novaclient/shell.py | 34 +++++++++++------------- novaclient/tests/functional/test_auth.py | 20 +++++++------- novaclient/tests/unit/test_shell.py | 12 ++++----- 3 files changed, 31 insertions(+), 35 deletions(-) diff --git a/novaclient/shell.py b/novaclient/shell.py index 4ecae61e1..2702fe23b 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -249,11 +249,11 @@ def __init__(self): def _append_global_identity_args(self, parser, argv): # Register the CLI arguments that have moved to the session object. loading.register_session_argparse_arguments(parser) - # Peek into argv to see if os-auth-token or os-token were given, + # Peek into argv to see if os-token was given, # in which case, the token auth plugin is what the user wants # else, we'll default to password default_auth_plugin = 'password' - if 'os-token' in argv: + if "--os-token" in argv: default_auth_plugin = 'token' loading.register_auth_argparse_arguments( parser, argv, default=default_auth_plugin) @@ -513,8 +513,10 @@ def main(self, argv): api_version = api_versions.get_api_version( args.os_compute_api_version) - os_username = args.os_username - os_user_id = args.os_user_id + auth_token = getattr(args, "os_token", None) + + os_username = getattr(args, "os_username", None) + os_user_id = getattr(args, "os_user_id", None) os_password = None # Fetched and set later as needed os_project_name = getattr( args, 'os_project_name', getattr(args, 'os_tenant_name', None)) @@ -529,13 +531,16 @@ def main(self, argv): if (not args.os_project_domain_id and not args.os_project_domain_name): setattr(args, "os_project_domain_id", "default") - if not args.os_user_domain_id and not args.os_user_domain_name: + + # os_user_domain_id is redundant in case of Token auth type + if not auth_token and (not args.os_user_domain_id and + not args.os_user_domain_name): setattr(args, "os_user_domain_id", "default") os_project_domain_id = args.os_project_domain_id os_project_domain_name = args.os_project_domain_name - os_user_domain_id = args.os_project_domain_id - os_user_domain_name = args.os_project_domain_name + os_user_domain_id = getattr(args, "os_user_domain_id", None) + os_user_domain_name = getattr(args, "os_user_domain_name", None) endpoint_type = args.endpoint_type insecure = args.insecure @@ -550,13 +555,6 @@ def main(self, argv): keystone_session = None keystone_auth = None - # We may have either, both or none of these. - # If we have both, we don't need USERNAME, PASSWORD etc. - # Finally, authenticate unless we have both. - # Note if we don't auth we probably don't have a tenant ID so we can't - # cache the token. - auth_token = getattr(args, 'os_token', None) - if not endpoint_type: endpoint_type = DEFAULT_NOVA_ENDPOINT_TYPE @@ -579,11 +577,11 @@ def main(self, argv): # for os_username or os_password but for compatibility it is not. if must_auth and not skip_auth: - if not os_username and not os_user_id: + if not any([auth_token, os_username, os_user_id]): raise exc.CommandError( - _("You must provide a username " - "or user ID via --os-username, --os-user-id, " - "env[OS_USERNAME] or env[OS_USER_ID]")) + _("You must provide a user name/id (via --os-username, " + "--os-user-id, env[OS_USERNAME] or env[OS_USER_ID]) or " + "an auth token (via --os-token).")) if not any([os_project_name, os_project_id]): raise exc.CommandError(_("You must provide a project name or" diff --git a/novaclient/tests/functional/test_auth.py b/novaclient/tests/functional/test_auth.py index 9f645c334..74fa7fb19 100644 --- a/novaclient/tests/functional/test_auth.py +++ b/novaclient/tests/functional/test_auth.py @@ -51,17 +51,15 @@ def nova_auth_with_token(self, identity_api_version): project_name=self.project_name, **kw) nova.servers.list() - # NOTE(andreykurilin): token auth is completely broken in CLI - # flags = ('--os-username %s --os-tenant-name %s --os-auth-token %s ' - # '--os-auth-url %s --os-endpoint-type publicURL' % ( - # self.cli_clients.username, - # self.cli_clients.tenant_name, - # token, auth_url)) - # if self.cli_clients.insecure: - # flags += ' --insecure ' - # - # return tempest.lib.cli.base.execute( - # "nova", action, flags, cli_dir=self.cli_clients.cli_dir) + flags = ('--os-tenant-name %(project_name)s --os-token %(token)s ' + '--os-auth-url %(auth_url)s --os-endpoint-type publicURL' + % {"project_name": self.project_name, + "token": token, "auth_url": auth_url}) + if self.cli_clients.insecure: + flags += ' --insecure ' + + tempest.lib.cli.base.execute( + "nova", "list", flags, cli_dir=self.cli_clients.cli_dir) def test_auth_via_keystone_v2(self): session = self.keystone.session diff --git a/novaclient/tests/unit/test_shell.py b/novaclient/tests/unit/test_shell.py index 8e589a470..86047923e 100644 --- a/novaclient/tests/unit/test_shell.py +++ b/novaclient/tests/unit/test_shell.py @@ -467,9 +467,9 @@ def test_bash_completion(self): matchers.MatchesRegex(r, re.DOTALL | re.MULTILINE)) def test_no_username(self): - required = ('You must provide a username or user ID' - ' via --os-username, --os-user-id,' - ' env[OS_USERNAME] or env[OS_USER_ID]') + required = ('You must provide a user name/id (via --os-username, ' + '--os-user-id, env[OS_USERNAME] or env[OS_USER_ID]) or ' + 'an auth token (via --os-token).') self.make_env(exclude='OS_USERNAME') try: self.shell('list') @@ -479,9 +479,9 @@ def test_no_username(self): self.fail('CommandError not raised') def test_no_user_id(self): - required = ('You must provide a username or user ID' - ' via --os-username, --os-user-id,' - ' env[OS_USERNAME] or env[OS_USER_ID]') + required = ('You must provide a user name/id (via --os-username, ' + '--os-user-id, env[OS_USERNAME] or env[OS_USER_ID]) or ' + 'an auth token (via --os-token).') self.make_env(exclude='OS_USER_ID', fake_env=FAKE_ENV2) try: self.shell('list') From f4d927c358e22e899396004590f381278fde10ba Mon Sep 17 00:00:00 2001 From: Tovin Seven Date: Fri, 20 Apr 2018 17:22:48 +0700 Subject: [PATCH 1438/1705] Trivial: Update pypi url to new url Pypi url changed from [1] to [2] [1] https://pypi.python.org/pypi/ [2] https://pypi.org/project/ Change-Id: I2b4af1f3d89b775657e03b801ec84ef498ca24c8 --- README.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index acf6493af..156555121 100644 --- a/README.rst +++ b/README.rst @@ -12,11 +12,11 @@ Python bindings to the OpenStack Compute API ============================================ .. image:: https://img.shields.io/pypi/v/python-novaclient.svg - :target: https://pypi.python.org/pypi/python-novaclient/ + :target: https://pypi.org/project/python-novaclient/ :alt: Latest Version .. image:: https://img.shields.io/pypi/dm/python-novaclient.svg - :target: https://pypi.python.org/pypi/python-novaclient/ + :target: https://pypi.org/project/python-novaclient/ :alt: Downloads This is a client for the OpenStack Compute API. It provides a Python API (the @@ -33,7 +33,7 @@ This is a client for the OpenStack Compute API. It provides a Python API (the * `Specs`_ * `How to Contribute`_ -.. _PyPi: https://pypi.python.org/pypi/python-novaclient +.. _PyPi: https://pypi.org/project/python-novaclient .. _Online Documentation: https://docs.openstack.org/python-novaclient/latest .. _Launchpad project: https://launchpad.net/python-novaclient .. _Blueprints: https://blueprints.launchpad.net/python-novaclient From 5483be7fe74a90e3a38428cfb436864ffeee4c54 Mon Sep 17 00:00:00 2001 From: Yikun Jiang Date: Fri, 27 Apr 2018 12:15:09 +0800 Subject: [PATCH 1439/1705] Microversion 2.62 - Add host/hostId to instance action event Adds support for microversion 2.62 which adds ``host`` (hostname) and ``hostId`` (an obfuscated hashed host id string) fields to the instance action ``GET /servers/{server_id}/os-instance-actions/{req_id}`` API. The event column is already included in the result of "nova instance-action " command, therefore does not have any CLI or python API binding impacts in the client. Related nova API change: I2f8b4a12a088b9ed96b428eafde2e0c478fb1db5 Part of blueprint: add-host-to-instance-action-events Change-Id: Iee7e1a3a22249c98873aa96694fd4885916cd097 --- novaclient/__init__.py | 2 +- .../functional/v2/test_instance_action.py | 28 +++++++++++++++++++ novaclient/tests/unit/v2/test_shell.py | 1 + .../microversion-v2_62-479a23f0d4307500.yaml | 11 ++++++++ 4 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/microversion-v2_62-479a23f0d4307500.yaml diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 6795d8827..1530b37a7 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.61") +API_MAX_VERSION = api_versions.APIVersion("2.62") diff --git a/novaclient/tests/functional/v2/test_instance_action.py b/novaclient/tests/functional/v2/test_instance_action.py index a318b6e7e..93e8486ef 100644 --- a/novaclient/tests/functional/v2/test_instance_action.py +++ b/novaclient/tests/functional/v2/test_instance_action.py @@ -113,3 +113,31 @@ def test_list_instance_action_with_changes_since(self): '--changes-since=%s but got: %s\n\n' 'First instance-action-list output: %s' % (before_stop, stop_output, create_output)) + + +class TestInstanceActionCLIV262(TestInstanceActionCLIV258, + base.TenantTestBase): + """Instance action functional tests for v2.62 nova-api microversion.""" + + COMPUTE_API_VERSION = "2.62" + + def test_show_actions_with_host(self): + name = self.name_generate() + # Create server with non-admin user + server = self.another_nova('boot --flavor %s --image %s --poll %s' % + (self.flavor.name, self.image.name, name)) + server_id = self._get_value_from_the_table(server, 'id') + output = self.nova("instance-action-list %s" % server_id) + request_id = self._get_column_value_from_single_row_table( + output, "Request_ID") + + # Only the 'hostId' are exposed to non-admin + output = self.another_nova( + "instance-action %s %s" % (server_id, request_id)) + self.assertNotIn("'host'", output) + self.assertIn("'hostId'", output) + + # The 'host' and 'hostId' are exposed to admin + output = self.nova("instance-action %s %s" % (server_id, request_id)) + self.assertIn("'host'", output) + self.assertIn("'hostId'", output) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 997063416..66300c894 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -3634,6 +3634,7 @@ def test_versions(self): 57, # There are no version-wrapped shell method changes for this. 60, # There are no client-side changes for volume multiattach. 61, # There are no version-wrapped shell method changes for this. + 62, # There are no version-wrapped shell method changes for this. ]) versions_supported = set(range(0, novaclient.API_MAX_VERSION.ver_minor + 1)) diff --git a/releasenotes/notes/microversion-v2_62-479a23f0d4307500.yaml b/releasenotes/notes/microversion-v2_62-479a23f0d4307500.yaml new file mode 100644 index 000000000..ed2948b24 --- /dev/null +++ b/releasenotes/notes/microversion-v2_62-479a23f0d4307500.yaml @@ -0,0 +1,11 @@ +--- +features: + - | + Adds support for microversion 2.62 which adds ``host`` (hostname) + and ``hostId`` (an obfuscated hashed host id string) fields to the + instance action ``GET /servers/{server_id}/os-instance-actions/{req_id}`` + API. + + The event columns are already included in the result of + "nova instance-action " command, therefore does not + have any CLI or python API binding impacts in the client. From 460638436ebbdeaf240ee427a2e222c094f231e2 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Fri, 27 Apr 2018 10:19:12 -0400 Subject: [PATCH 1440/1705] Make sure microversion < 2.62 does not show host(Id) for instance actions This adds a simple assertion that when microversion < 2.62, the "nova instance-action" CLI does not show host or hostId output. Related to blueprint add-host-to-instance-action-events Change-Id: I6e0866d9daff75046a5e82f57ff745d494b4b5ed --- novaclient/tests/functional/v2/test_instance_action.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/novaclient/tests/functional/v2/test_instance_action.py b/novaclient/tests/functional/v2/test_instance_action.py index 93e8486ef..9ea15c284 100644 --- a/novaclient/tests/functional/v2/test_instance_action.py +++ b/novaclient/tests/functional/v2/test_instance_action.py @@ -66,6 +66,8 @@ class TestInstanceActionCLIV258(TestInstanceActionCLI): """Instance action functional tests for v2.58 nova-api microversion.""" COMPUTE_API_VERSION = "2.58" + # Does this microversion return a hostId field in the event response? + expect_event_hostId_field = False def test_list_instance_action_with_marker_and_limit(self): server = self._create_server() @@ -86,6 +88,13 @@ def test_list_instance_action_with_marker_and_limit(self): action = self._get_list_of_values_from_single_column_table( output, "Action") self.assertEqual(action, ['create']) + if not self.expect_event_hostId_field: + # Make sure host and hostId are not in the response when + # microversion is less than 2.62. + output = self.nova("instance-action %s %s" % ( + server.id, marker_req)) + self.assertNotIn("'host'", output) + self.assertNotIn("'hostId'", output) def test_list_instance_action_with_changes_since(self): # Ignore microseconds to make this a deterministic test. @@ -120,6 +129,7 @@ class TestInstanceActionCLIV262(TestInstanceActionCLIV258, """Instance action functional tests for v2.62 nova-api microversion.""" COMPUTE_API_VERSION = "2.62" + expect_event_hostId_field = True def test_show_actions_with_host(self): name = self.name_generate() From 57e9a5d34cde8cef319487cb56eca383cff76059 Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Fri, 27 Apr 2018 16:54:09 +0900 Subject: [PATCH 1441/1705] Fix the policy argument in server-group-create In the "server-group-create" command, multiple policies can be specified currently. But only one item is allowed in the nova side. So make the command allow only one policy. Change-Id: Ifd2d084faa2b849d6ee466d9accbad21b6a4e11b Closes-Bug: #1767287 --- novaclient/tests/unit/v2/test_shell.py | 4 ++++ novaclient/v2/shell.py | 3 +-- releasenotes/notes/bug-1767287-cc28d60d9e59f9bd.yaml | 6 ++++++ 3 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/bug-1767287-cc28d60d9e59f9bd.yaml diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 66300c894..504eb39d5 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -3571,6 +3571,10 @@ def test_create_server_group(self): {'server_group': {'name': 'wjsg', 'policies': ['affinity']}}) + def test_create_server_group_with_multiple_policies(self): + self.assertRaises(SystemExit, self.run_command, + 'server-group-create wjsg affinity anti-affinity') + def test_delete_multi_server_groups(self): self.run_command('server-group-delete 12345 56789') self.assert_called('DELETE', '/os-server-groups/56789') diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index d53fc05bc..e4c826b64 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -4501,8 +4501,7 @@ def do_server_group_list(cs, args): @utils.arg( 'policy', metavar='', - nargs='+', - help=_('Policies for the server groups.')) + help=_('Policy for the server group.')) def do_server_group_create(cs, args): """Create a new server group with the specified details.""" server_group = cs.server_groups.create(name=args.name, diff --git a/releasenotes/notes/bug-1767287-cc28d60d9e59f9bd.yaml b/releasenotes/notes/bug-1767287-cc28d60d9e59f9bd.yaml new file mode 100644 index 000000000..76dce988b --- /dev/null +++ b/releasenotes/notes/bug-1767287-cc28d60d9e59f9bd.yaml @@ -0,0 +1,6 @@ +--- +upgrade: + - | + The ``nova server-group-create`` command now only supports specifying + a single policy name when creating the server group. This is to match + the server-side API validation. From 24bb7ea831032bddca8891bae54f7125221b69d0 Mon Sep 17 00:00:00 2001 From: "zhang.lei" Date: Wed, 9 May 2018 09:22:51 +0000 Subject: [PATCH 1442/1705] Fix the incorrect cirros default password Following by https://docs.openstack.org/image-guide/obtain-images.html#cirros-test Change-Id: Iee3b90900e238b717020c4b7f32e5a5f346bef95 --- novaclient/tests/functional/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/tests/functional/base.py b/novaclient/tests/functional/base.py index 57f822b2d..943c44f4e 100644 --- a/novaclient/tests/functional/base.py +++ b/novaclient/tests/functional/base.py @@ -35,7 +35,7 @@ import novaclient.v2.shell BOOT_IS_COMPLETE = ("login as 'cirros' user. default password: " - "'cubswin:)'. use 'sudo' for root.") + "'gocubsgo'. use 'sudo' for root.") def is_keystone_version_available(session, version): From abaa86fd86d4beca0082ff1768d5306e5e86302e Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Tue, 5 Jun 2018 19:57:49 +0000 Subject: [PATCH 1443/1705] Revert "Fix listing of instances above API max_limit" This reverts commit eff607ccef91d09052d58f6798f68d67404f51ce. There was no apparent need for the change being reverted since user can list all servers by specifying --limit=1 when running the nova list command. The change introduced a problem whereby the first pass to list instances from the server would get up to [api]/max_limit (default 1000) results and then call again with a marker. If the last instance in the list (the marker) is corrupted in the instance_mappings table in the API DB by not having an associated cell mapping, listing instances will always fail with a MarkerNotFound error even though the CLI user is not passing a marker nor specifying --limit=-1. The corrupted instance mapping record resulting in the MarkerNotFound error is something else that should be fixed on the server side (and likely result in a 500) but the change in behavior of the CLI makes it always fail if you hit this even if you're not passing a marker. Change-Id: Ibb43f500a74733b85bd3242592d36985bfb45856 Closes-Bug: #1773945 --- novaclient/tests/unit/fixture_data/servers.py | 32 +-- novaclient/tests/unit/utils.py | 12 +- novaclient/tests/unit/v2/fakes.py | 4 - novaclient/tests/unit/v2/test_servers.py | 58 ++-- novaclient/tests/unit/v2/test_shell.py | 265 +++++------------- novaclient/v2/servers.py | 5 +- novaclient/v2/shell.py | 4 +- 7 files changed, 100 insertions(+), 280 deletions(-) diff --git a/novaclient/tests/unit/fixture_data/servers.py b/novaclient/tests/unit/fixture_data/servers.py index 3950158d7..2402cd106 100644 --- a/novaclient/tests/unit/fixture_data/servers.py +++ b/novaclient/tests/unit/fixture_data/servers.py @@ -32,16 +32,6 @@ def setUp(self): self.requests_mock.get(self.url(), json=get_servers, - headers=self.json_headers, - complete_qs=True) - - self.requests_mock.get(self.url(name='sample-server'), - json=get_servers, - headers=self.json_headers, - complete_qs=True) - - self.requests_mock.get(self.url(marker='5678'), - json={"servers": []}, headers=self.json_headers) self.server_1234 = { @@ -170,31 +160,13 @@ def setUp(self): self.requests_mock.get( self.url('detail', marker=self.server_1234["id"]), - json={"servers": [self.server_5678, self.server_9012]}, - headers=self.json_headers, complete_qs=True) - - self.requests_mock.get( - self.url('detail', marker=self.server_1234["id"], limit=2), - json={"servers": [self.server_5678, self.server_9012]}, - headers=self.json_headers, complete_qs=True) - - # simulate max_limit=2 by returning 2 items when limit=3 - # another request should be triggered with limit=1 to get complete - # result - self.requests_mock.get( - self.url('detail', limit=3), json={"servers": [self.server_1234, self.server_5678]}, headers=self.json_headers, complete_qs=True) self.requests_mock.get( - self.url('detail', marker=self.server_5678["id"], limit=1), - json={"servers": [self.server_9012]}, - headers=self.json_headers, complete_qs=True) - - self.requests_mock.get( - self.url('detail', marker=self.server_9012["id"]), + self.url('detail', marker=self.server_5678["id"]), json={"servers": []}, - headers=self.json_headers) + headers=self.json_headers, complete_qs=True) self.server_1235 = self.server_1234.copy() self.server_1235['id'] = 1235 diff --git a/novaclient/tests/unit/utils.py b/novaclient/tests/unit/utils.py index f036497a9..49d3631f3 100644 --- a/novaclient/tests/unit/utils.py +++ b/novaclient/tests/unit/utils.py @@ -93,16 +93,12 @@ def setUp(self): fix = self.data_fixture_class(self.requests_mock) self.data_fixture = self.useFixture(fix) - def assert_called(self, method, path, body=None, pos=-1): - self.assertEqual( - self.requests_mock.request_history[pos].method, - method) - self.assertEqual( - self.requests_mock.request_history[pos].path_url, - path) + def assert_called(self, method, path, body=None): + self.assertEqual(self.requests_mock.last_request.method, method) + self.assertEqual(self.requests_mock.last_request.path_url, path) if body: - req_data = self.requests_mock.request_history[pos].body + req_data = self.requests_mock.last_request.body if isinstance(req_data, six.binary_type): req_data = req_data.decode('utf-8') if not isinstance(body, six.string_types): diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index e853ef223..803dc41ec 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -392,8 +392,6 @@ def get_limits(self, **kw): # def get_servers(self, **kw): - if kw.get('marker') == '9014': - return (200, {}, {"servers": []}) return (200, {}, {"servers": [ {'id': '1234', 'name': 'sample-server'}, {'id': '5678', 'name': 'sample-server2'}, @@ -401,8 +399,6 @@ def get_servers(self, **kw): ]}) def get_servers_detail(self, **kw): - if kw.get('marker') == '9014': - return (200, {}, {"servers": []}) return (200, {}, {"servers": [ { "id": '1234', diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index dfa1b6fd5..07ad1497e 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -47,64 +47,48 @@ def _get_server_create_default_nics(self): """ return None - def test_list_all_servers(self): + def test_list_servers(self): sl = self.cs.servers.list() self.assert_request_id(sl, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('GET', '/servers/detail', pos=-2) - self.assert_called('GET', '/servers/detail?marker=9012') + self.assert_called('GET', '/servers/detail') for s in sl: self.assertIsInstance(s, servers.Server) def test_filter_servers_unicode(self): sl = self.cs.servers.list(search_opts={'name': u't€sting'}) self.assert_request_id(sl, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called( - 'GET', - '/servers/detail?name=t%E2%82%ACsting', - pos=-2) - self.assert_called( - 'GET', - '/servers/detail?marker=9012&name=t%E2%82%ACsting') + self.assert_called('GET', '/servers/detail?name=t%E2%82%ACsting') for s in sl: self.assertIsInstance(s, servers.Server) - def test_list_servers_undetailed(self): - sl = self.cs.servers.list(detailed=False) - self.assert_request_id(sl, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('GET', '/servers', pos=-2) - self.assert_called('GET', '/servers?marker=5678') - for s in sl: - self.assertIsInstance(s, servers.Server) - - def test_list_servers_with_marker(self): - sl = self.cs.servers.list(marker=1234) + def test_list_all_servers(self): + # use marker just to identify this call in fixtures + sl = self.cs.servers.list(limit=-1, marker=1234) self.assert_request_id(sl, fakes.FAKE_REQUEST_ID_LIST) self.assertEqual(2, len(sl)) - self.assert_called('GET', '/servers/detail?marker=1234', pos=-2) - self.assert_called('GET', '/servers/detail?marker=9012') + self.assertEqual(self.requests_mock.request_history[-2].method, 'GET') + self.assertEqual(self.requests_mock.request_history[-2].path_url, + '/servers/detail?marker=1234') + self.assert_called('GET', '/servers/detail?marker=5678') for s in sl: self.assertIsInstance(s, servers.Server) - def test_list_servers_with_marker_limit(self): - sl = self.cs.servers.list(marker=1234, limit=2) + def test_list_servers_undetailed(self): + sl = self.cs.servers.list(detailed=False) self.assert_request_id(sl, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('GET', '/servers/detail?limit=2&marker=1234') + self.assert_called('GET', '/servers') for s in sl: self.assertIsInstance(s, servers.Server) - self.assertEqual(2, len(sl)) - def test_list_servers_with_limit_above_max_limit(self): - # use limit=3 to trigger paging simulation on backend fixture side - sl = self.cs.servers.list(limit=3) + def test_list_servers_with_marker_limit(self): + sl = self.cs.servers.list(marker=1234, limit=2) self.assert_request_id(sl, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('GET', '/servers/detail?limit=3', pos=-2) - self.assert_called('GET', '/servers/detail?limit=1&marker=5678') + self.assert_called('GET', '/servers/detail?limit=2&marker=1234') for s in sl: self.assertIsInstance(s, servers.Server) - self.assertEqual(3, len(sl)) def test_list_servers_sort_single(self): sl = self.cs.servers.list(sort_keys=['display_name'], @@ -112,10 +96,7 @@ def test_list_servers_sort_single(self): self.assert_request_id(sl, fakes.FAKE_REQUEST_ID_LIST) self.assert_called( 'GET', - '/servers/detail?sort_dir=asc&sort_key=display_name', pos=-2) - self.assert_called( - 'GET', - '/servers/detail?marker=9012&sort_dir=asc&sort_key=display_name') + '/servers/detail?sort_dir=asc&sort_key=display_name') for s in sl: self.assertIsInstance(s, servers.Server) @@ -126,11 +107,6 @@ def test_list_servers_sort_multiple(self): self.assert_called( 'GET', ('/servers/detail?sort_dir=asc&sort_dir=desc&' - 'sort_key=display_name&sort_key=id'), - pos=-2) - self.assert_called( - 'GET', - ('/servers/detail?marker=9012&sort_dir=asc&sort_dir=desc&' 'sort_key=display_name&sort_key=id')) for s in sl: self.assertIsInstance(s, servers.Server) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index dfa798b85..785b9683f 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -1324,99 +1324,63 @@ def test_create_image_with_poll_to_check_image_state_deleted(self): def test_list(self): self.run_command('list') - self.assert_called('GET', '/servers/detail', pos=0) - self.assert_called('GET', '/servers/detail?marker=9014') + self.assert_called('GET', '/servers/detail') def test_list_minimal(self): self.run_command('list --minimal') - self.assert_called('GET', '/servers', pos=0) - self.assert_called('GET', '/servers?marker=9014') + self.assert_called('GET', '/servers') def test_list_deleted(self): self.run_command('list --deleted') - self.assert_called( - 'GET', - '/servers/detail?deleted=True', - pos=0) - self.assert_called( - 'GET', - '/servers/detail?deleted=True&marker=9014') + self.assert_called('GET', '/servers/detail?deleted=True') def test_list_with_images(self): self.run_command('list --image %s' % FAKE_UUID_1) - self.assert_called( - 'GET', - '/servers/detail?image=%s' % FAKE_UUID_1, - pos=1) - self.assert_called( - 'GET', - '/servers/detail?image=%s&marker=9014' % FAKE_UUID_1) + self.assert_called('GET', '/servers/detail?image=%s' % FAKE_UUID_1) def test_list_with_flavors(self): self.run_command('list --flavor 1') - self.assert_called('GET', '/servers/detail?flavor=1', pos=1) - self.assert_called('GET', '/servers/detail?flavor=1&marker=9014') + self.assert_called('GET', '/servers/detail?flavor=1') def test_list_by_tenant(self): self.run_command('list --tenant fake_tenant') self.assert_called( 'GET', - '/servers/detail?all_tenants=1&tenant_id=fake_tenant', pos=0) - self.assert_called( - 'GET', - '/servers/detail?all_tenants=1&marker=9014&tenant_id=fake_tenant') + '/servers/detail?all_tenants=1&tenant_id=fake_tenant') def test_list_by_user(self): self.run_command('list --user fake_user') self.assert_called( 'GET', - '/servers/detail?all_tenants=1&user_id=fake_user', pos=0) - self.assert_called( - 'GET', - '/servers/detail?all_tenants=1&marker=9014&user_id=fake_user') + '/servers/detail?all_tenants=1&user_id=fake_user') def test_list_with_single_sort_key_no_dir(self): self.run_command('list --sort 1') self.assert_called( - 'GET', ('/servers/detail?sort_dir=desc&sort_key=1'), pos=0) - self.assert_called( - 'GET', - '/servers/detail?marker=9014&sort_dir=desc&sort_key=1') + 'GET', ('/servers/detail?sort_dir=desc&sort_key=1')) def test_list_with_single_sort_key_and_dir(self): self.run_command('list --sort 1:asc') self.assert_called( - 'GET', ('/servers/detail?sort_dir=asc&sort_key=1'), pos=0) - self.assert_called( - 'GET', - '/servers/detail?marker=9014&sort_dir=asc&sort_key=1') + 'GET', ('/servers/detail?sort_dir=asc&sort_key=1')) def test_list_with_sort_keys_no_dir(self): self.run_command('list --sort 1,2') self.assert_called( 'GET', ('/servers/detail?sort_dir=desc&sort_dir=desc&' - 'sort_key=1&sort_key=2'), pos=0) - self.assert_called( - 'GET', ('/servers/detail?marker=9014&sort_dir=desc&sort_dir=desc&' 'sort_key=1&sort_key=2')) def test_list_with_sort_keys_and_dirs(self): self.run_command('list --sort 1:asc,2:desc') self.assert_called( 'GET', ('/servers/detail?sort_dir=asc&sort_dir=desc&' - 'sort_key=1&sort_key=2'), pos=0) - self.assert_called( - 'GET', ('/servers/detail?marker=9014&sort_dir=asc&sort_dir=desc&' 'sort_key=1&sort_key=2')) def test_list_with_sort_keys_and_some_dirs(self): self.run_command('list --sort 1,2:asc') self.assert_called( 'GET', ('/servers/detail?sort_dir=desc&sort_dir=asc&' - 'sort_key=1&sort_key=2'), pos=0) - self.assert_called( - 'GET', ('/servers/detail?marker=9014&sort_dir=desc&' - 'sort_dir=asc&sort_key=1&sort_key=2')) + 'sort_key=1&sort_key=2')) def test_list_with_invalid_sort_dir_one(self): cmd = 'list --sort 1:foo' @@ -1448,8 +1412,7 @@ def test_list_fields(self): output, _err = self.run_command( 'list --fields ' 'host,security_groups,OS-EXT-MOD:some_thing') - self.assert_called('GET', '/servers/detail', pos=0) - self.assert_called('GET', '/servers/detail?marker=9014') + self.assert_called('GET', '/servers/detail') self.assertIn('computenode1', output) self.assertIn('securitygroup1', output) self.assertIn('OS-EXT-MOD: Some Thing', output) @@ -1491,8 +1454,7 @@ def test_list_invalid_fields(self): def test_list_with_marker(self): self.run_command('list --marker some-uuid') - self.assert_called('GET', '/servers/detail?marker=some-uuid', pos=0) - self.assert_called('GET', '/servers/detail?marker=9014') + self.assert_called('GET', '/servers/detail?marker=some-uuid') def test_list_with_limit(self): self.run_command('list --limit 3') @@ -1501,13 +1463,7 @@ def test_list_with_limit(self): def test_list_with_changes_since(self): self.run_command('list --changes-since 2016-02-29T06:23:22') self.assert_called( - 'GET', - '/servers/detail?changes-since=2016-02-29T06%3A23%3A22', - pos=0) - self.assert_called( - 'GET', - ('/servers/detail?changes-since=2016-02-29T06%3A23%3A22&' - 'marker=9014')) + 'GET', '/servers/detail?changes-since=2016-02-29T06%3A23%3A22') def test_list_with_changes_since_invalid_value(self): self.assertRaises(exceptions.CommandError, @@ -1545,14 +1501,12 @@ def test_rebuild(self): output, _err = self.run_command('rebuild sample-server %s' % FAKE_UUID_1) self.assert_called('GET', '/servers?name=sample-server', pos=0) - self.assert_called('GET', '/servers?marker=9014&name=sample-server', - pos=1) - self.assert_called('GET', '/servers/1234', pos=2) - self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=3) + self.assert_called('GET', '/servers/1234', pos=1) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=2) self.assert_called('POST', '/servers/1234/action', - {'rebuild': {'imageRef': FAKE_UUID_1}}, pos=4) - self.assert_called('GET', '/flavors/1', pos=5) - self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=6) + {'rebuild': {'imageRef': FAKE_UUID_1}}, pos=3) + self.assert_called('GET', '/flavors/1', pos=4) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=5) self.assertIn('adminPass', output) def test_rebuild_password(self): @@ -1560,73 +1514,63 @@ def test_rebuild_password(self): ' --rebuild-password asdf' % FAKE_UUID_1) self.assert_called('GET', '/servers?name=sample-server', pos=0) - self.assert_called('GET', '/servers?marker=9014&name=sample-server', - pos=1) - self.assert_called('GET', '/servers/1234', pos=2) - self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=3) + self.assert_called('GET', '/servers/1234', pos=1) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=2) self.assert_called('POST', '/servers/1234/action', {'rebuild': {'imageRef': FAKE_UUID_1, - 'adminPass': 'asdf'}}, pos=4) - self.assert_called('GET', '/flavors/1', pos=5) - self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=6) + 'adminPass': 'asdf'}}, pos=3) + self.assert_called('GET', '/flavors/1', pos=4) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=5) self.assertIn('adminPass', output) def test_rebuild_preserve_ephemeral(self): self.run_command('rebuild sample-server %s --preserve-ephemeral' % FAKE_UUID_1) self.assert_called('GET', '/servers?name=sample-server', pos=0) - self.assert_called('GET', '/servers?marker=9014&name=sample-server', - pos=1) - self.assert_called('GET', '/servers/1234', pos=2) - self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=3) + self.assert_called('GET', '/servers/1234', pos=1) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=2) self.assert_called('POST', '/servers/1234/action', {'rebuild': {'imageRef': FAKE_UUID_1, - 'preserve_ephemeral': True}}, pos=4) - self.assert_called('GET', '/flavors/1', pos=5) - self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=6) + 'preserve_ephemeral': True}}, pos=3) + self.assert_called('GET', '/flavors/1', pos=4) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=5) def test_rebuild_name_meta(self): self.run_command('rebuild sample-server %s --name asdf --meta ' 'foo=bar' % FAKE_UUID_1) self.assert_called('GET', '/servers?name=sample-server', pos=0) - self.assert_called('GET', '/servers?marker=9014&name=sample-server', - pos=1) - self.assert_called('GET', '/servers/1234', pos=2) - self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=3) + self.assert_called('GET', '/servers/1234', pos=1) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=2) self.assert_called('POST', '/servers/1234/action', {'rebuild': {'imageRef': FAKE_UUID_1, 'name': 'asdf', - 'metadata': {'foo': 'bar'}}}, pos=4) - self.assert_called('GET', '/flavors/1', pos=5) - self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=6) + 'metadata': {'foo': 'bar'}}}, pos=3) + self.assert_called('GET', '/flavors/1', pos=4) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=5) def test_rebuild_reset_keypair(self): self.run_command('rebuild sample-server %s --key-name test_keypair' % FAKE_UUID_1, api_version='2.54') self.assert_called('GET', '/servers?name=sample-server', pos=0) - self.assert_called('GET', '/servers?marker=9014&name=sample-server', - pos=1) - self.assert_called('GET', '/servers/1234', pos=2) - self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=3) + self.assert_called('GET', '/servers/1234', pos=1) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=2) self.assert_called('POST', '/servers/1234/action', {'rebuild': {'imageRef': FAKE_UUID_1, 'key_name': 'test_keypair', - 'description': None}}, pos=4) - self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=5) + 'description': None}}, pos=3) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=4) def test_rebuild_unset_keypair(self): self.run_command('rebuild sample-server %s --key-unset' % FAKE_UUID_1, api_version='2.54') self.assert_called('GET', '/servers?name=sample-server', pos=0) - self.assert_called('GET', '/servers?marker=9014&name=sample-server', - pos=1) - self.assert_called('GET', '/servers/1234', pos=2) - self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=3) + self.assert_called('GET', '/servers/1234', pos=1) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=2) self.assert_called('POST', '/servers/1234/action', {'rebuild': {'imageRef': FAKE_UUID_1, 'key_name': None, - 'description': None}}, pos=4) - self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=5) + 'description': None}}, pos=3) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=4) def test_rebuild_unset_keypair_with_key_name(self): ex = self.assertRaises( @@ -1668,29 +1612,25 @@ def test_rebuild_change_user_data(self): FAKE_UUID_1, api_version='2.57') user_data = servers.ServerManager.transform_userdata('test') self.assert_called('GET', '/servers?name=sample-server', pos=0) - self.assert_called('GET', '/servers?marker=9014&name=sample-server', - pos=1) - self.assert_called('GET', '/servers/1234', pos=2) - self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=3) + self.assert_called('GET', '/servers/1234', pos=1) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=2) self.assert_called('POST', '/servers/1234/action', {'rebuild': {'imageRef': FAKE_UUID_1, 'user_data': user_data, - 'description': None}}, pos=4) - self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=5) + 'description': None}}, pos=3) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=4) def test_rebuild_unset_user_data(self): self.run_command('rebuild sample-server %s --user-data-unset' % FAKE_UUID_1, api_version='2.57') self.assert_called('GET', '/servers?name=sample-server', pos=0) - self.assert_called('GET', '/servers?marker=9014&name=sample-server', - pos=1) - self.assert_called('GET', '/servers/1234', pos=2) - self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=3) + self.assert_called('GET', '/servers/1234', pos=1) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=2) self.assert_called('POST', '/servers/1234/action', {'rebuild': {'imageRef': FAKE_UUID_1, 'user_data': None, - 'description': None}}, pos=4) - self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=5) + 'description': None}}, pos=3) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=4) def test_rebuild_user_data_and_unset_user_data(self): """Tests that trying to set --user-data and --unset-user-data in the @@ -1711,11 +1651,7 @@ def test_start_with_all_tenants(self): self.run_command('start sample-server --all-tenants') self.assert_called('GET', '/servers?all_tenants=1&name=sample-server', pos=0) - self.assert_called('GET', - ('/servers?all_tenants=1&marker=9014&' - 'name=sample-server'), - pos=1) - self.assert_called('GET', '/servers/1234', pos=2) + self.assert_called('GET', '/servers/1234', pos=1) self.assert_called('POST', '/servers/1234/action', {'os-start': None}) def test_stop(self): @@ -1726,11 +1662,7 @@ def test_stop_with_all_tenants(self): self.run_command('stop sample-server --all-tenants') self.assert_called('GET', '/servers?all_tenants=1&name=sample-server', pos=0) - self.assert_called('GET', - ('/servers?all_tenants=1&marker=9014&' - 'name=sample-server'), - pos=1) - self.assert_called('GET', '/servers/1234', pos=2) + self.assert_called('GET', '/servers/1234', pos=1) self.assert_called('POST', '/servers/1234/action', {'os-stop': None}) def test_pause(self): @@ -1833,12 +1765,10 @@ def test_set_password(self): def test_show(self): self.run_command('show 1234') self.assert_called('GET', '/servers?name=1234', pos=0) - self.assert_called('GET', '/servers?marker=9014&name=1234', pos=1) - self.assert_called('GET', '/servers?name=1234', pos=2) - self.assert_called('GET', '/servers?marker=9014&name=1234', pos=3) - self.assert_called('GET', '/servers/1234', pos=4) - self.assert_called('GET', '/flavors/1', pos=5) - self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=6) + self.assert_called('GET', '/servers?name=1234', pos=1) + self.assert_called('GET', '/servers/1234', pos=2) + self.assert_called('GET', '/flavors/1', pos=3) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=4) def test_show_no_image(self): self.run_command('show 9012') @@ -1897,31 +1827,21 @@ def test_restore_withname(self): self.run_command('restore sample-server') self.assert_called('GET', '/servers?deleted=True&name=sample-server', pos=0) - self.assert_called('GET', - ('/servers?deleted=True&marker=9014&' - 'name=sample-server'), - pos=1) - self.assert_called('GET', '/servers/1234', pos=2) + self.assert_called('GET', '/servers/1234', pos=1) self.assert_called('POST', '/servers/1234/action', {'restore': None}, - pos=3) + pos=2) def test_delete_two_with_two_existent(self): self.run_command('delete 1234 5678') - self.assert_called('DELETE', '/servers/1234', pos=-7) + self.assert_called('DELETE', '/servers/1234', pos=-5) self.assert_called('DELETE', '/servers/5678', pos=-1) self.run_command('delete sample-server sample-server2') self.assert_called('GET', - '/servers?name=sample-server', pos=-8) - self.assert_called('GET', - '/servers?marker=9014&name=sample-server', - pos=-7) - self.assert_called('GET', '/servers/1234', pos=-6) - self.assert_called('DELETE', '/servers/1234', pos=-5) + '/servers?name=sample-server', pos=-6) + self.assert_called('GET', '/servers/1234', pos=-5) + self.assert_called('DELETE', '/servers/1234', pos=-4) self.assert_called('GET', '/servers?name=sample-server2', - pos=-4) - self.assert_called('GET', - '/servers?marker=9014&name=sample-server2', pos=-3) self.assert_called('GET', '/servers/5678', pos=-2) self.assert_called('DELETE', '/servers/5678', pos=-1) @@ -1930,21 +1850,13 @@ def test_delete_two_with_two_existent_all_tenants(self): self.run_command('delete sample-server sample-server2 --all-tenants') self.assert_called('GET', '/servers?all_tenants=1&name=sample-server', pos=0) - self.assert_called('GET', - ('/servers?all_tenants=1&marker=9014&' - 'name=sample-server'), - pos=1) - self.assert_called('GET', '/servers/1234', pos=2) - self.assert_called('DELETE', '/servers/1234', pos=3) + self.assert_called('GET', '/servers/1234', pos=1) + self.assert_called('DELETE', '/servers/1234', pos=2) self.assert_called('GET', '/servers?all_tenants=1&name=sample-server2', - pos=4) - self.assert_called('GET', - ('/servers?all_tenants=1&marker=9014&' - 'name=sample-server2'), - pos=5) - self.assert_called('GET', '/servers/5678', pos=6) - self.assert_called('DELETE', '/servers/5678', pos=7) + pos=3) + self.assert_called('GET', '/servers/5678', pos=4) + self.assert_called('DELETE', '/servers/5678', pos=5) def test_delete_two_with_one_nonexistent(self): cmd = 'delete 1234 123456789' @@ -2591,25 +2503,21 @@ def test_reset_state_with_all_tenants(self): self.run_command('reset-state sample-server --all-tenants') self.assert_called('GET', '/servers?all_tenants=1&name=sample-server', pos=0) - self.assert_called('GET', - ('/servers?all_tenants=1&marker=9014&' - 'name=sample-server'), - pos=1) - self.assert_called('GET', '/servers/1234', pos=2) + self.assert_called('GET', '/servers/1234', pos=1) self.assert_called('POST', '/servers/1234/action', {'os-resetState': {'state': 'error'}}) def test_reset_state_multiple(self): self.run_command('reset-state sample-server sample-server2') self.assert_called('POST', '/servers/1234/action', - {'os-resetState': {'state': 'error'}}, pos=-5) + {'os-resetState': {'state': 'error'}}, pos=-4) self.assert_called('POST', '/servers/5678/action', {'os-resetState': {'state': 'error'}}, pos=-1) def test_reset_state_active_multiple(self): self.run_command('reset-state --active sample-server sample-server2') self.assert_called('POST', '/servers/1234/action', - {'os-resetState': {'state': 'active'}}, pos=-5) + {'os-resetState': {'state': 'active'}}, pos=-4) self.assert_called('POST', '/servers/5678/action', {'os-resetState': {'state': 'active'}}, pos=-1) @@ -3625,8 +3533,7 @@ def test_versions(self): def test_list_v2_10(self): self.run_command('list', api_version='2.10') - self.assert_called('GET', '/servers/detail', pos=0) - self.assert_called('GET', '/servers/detail?marker=9014') + self.assert_called('GET', '/servers/detail') def test_server_tag_add(self): self.run_command('server-tag-add sample-server tag', @@ -3669,43 +3576,19 @@ def test_server_tag_delete_all(self): def test_list_v2_26_tags(self): self.run_command('list --tags tag1,tag2', api_version='2.26') - self.assert_called( - 'GET', - '/servers/detail?tags=tag1%2Ctag2', - pos=0) - self.assert_called( - 'GET', - '/servers/detail?marker=9014&tags=tag1%2Ctag2') + self.assert_called('GET', '/servers/detail?tags=tag1%2Ctag2') def test_list_v2_26_tags_any(self): self.run_command('list --tags-any tag1,tag2', api_version='2.26') - self.assert_called( - 'GET', - '/servers/detail?tags-any=tag1%2Ctag2', - pos=0) - self.assert_called( - 'GET', - '/servers/detail?marker=9014&tags-any=tag1%2Ctag2') + self.assert_called('GET', '/servers/detail?tags-any=tag1%2Ctag2') def test_list_v2_26_not_tags(self): self.run_command('list --not-tags tag1,tag2', api_version='2.26') - self.assert_called( - 'GET', - '/servers/detail?not-tags=tag1%2Ctag2', - pos=0) - self.assert_called( - 'GET', - '/servers/detail?marker=9014¬-tags=tag1%2Ctag2') + self.assert_called('GET', '/servers/detail?not-tags=tag1%2Ctag2') def test_list_v2_26_not_tags_any(self): self.run_command('list --not-tags-any tag1,tag2', api_version='2.26') - self.assert_called( - 'GET', - '/servers/detail?not-tags-any=tag1%2Ctag2', - pos=0) - self.assert_called( - 'GET', - '/servers/detail?marker=9014¬-tags-any=tag1%2Ctag2') + self.assert_called('GET', '/servers/detail?not-tags-any=tag1%2Ctag2') class PollForStatusTestCase(utils.TestCase): diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index 805dd4541..79ab0cdce 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -855,10 +855,7 @@ def list(self, detailed=True, search_opts=None, marker=None, limit=None, result.extend(servers) result.append_request_ids(servers.request_ids) - if limit and limit != -1: - limit = max(limit - len(servers), 0) - - if not servers or limit == 0: + if not servers or limit != -1: break marker = result[-1].id return result diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 532306454..e73bd5040 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -1441,8 +1441,8 @@ def _print_flavor(flavor): default=None, help=_("Maximum number of servers to display. If limit == -1, all servers " "will be displayed. If limit is bigger than 'CONF.api.max_limit' " - "option of Nova API, multiple requests will be sent and results " - "will be merged.")) + "option of Nova API, limit 'CONF.api.max_limit' will be used " + "instead.")) @utils.arg( '--changes-since', dest='changes_since', From f4ed25f2f51c33f332e1d603ac6e484274a83f46 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Tue, 5 Jun 2018 17:48:31 -0400 Subject: [PATCH 1444/1705] Import nova CLI reference from openstack-manuals This replaces the old CLI reference main page for the various nova commands with the more complete CLI reference that was in the openstack-manuals report based on the "before-migration" tag in that repo, which was the tag before the centralized docs were dropped in Pike and should have been moved into per-project repos, in this case python-novaclient. The command reference in here is a bit old and should be cleaned up, but this is better than what we have today, which is nothing. Change-Id: If19cfcafc90fddb930c124a5b9845d4eae1f6093 Closes-Bug: #1775281 --- doc/source/cli/nova.rst | 3638 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 3596 insertions(+), 42 deletions(-) diff --git a/doc/source/cli/nova.rst b/doc/source/cli/nova.rst index 1a35bcf08..ef5fb37a0 100644 --- a/doc/source/cli/nova.rst +++ b/doc/source/cli/nova.rst @@ -2,80 +2,3634 @@ nova ====== +The nova client is the command-line interface (CLI) for +the Compute service (nova) API and its extensions. -SYNOPSIS -======== +This chapter documents :command:`nova` version ``9.0.1``. - `nova` [options] [command-options] +For help on a specific :command:`nova` command, enter: - `nova help` +.. code-block:: console - `nova help` + $ nova help COMMAND +.. _nova_command_usage: -DESCRIPTION -=========== +nova usage +~~~~~~~~~~ -`nova` is a command line client for controlling OpenStack Nova, the cloud -computing fabric controller. It implements 100% of the Nova API, allowing -management of instances, images, quotas and much more. +.. code-block:: console -Before you can issue commands with `nova`, you must ensure that your -environment contains the necessary variables so that you can prove to the CLI -who you are and what credentials you have to issue the commands. See -`Getting Credentials for a CLI` section of `OpenStack CLI Guide` for more -info. + usage: nova [--version] [--debug] [--os-cache] [--timings] + [--os-region-name ] [--service-type ] + [--service-name ] + [--os-endpoint-type ] + [--os-compute-api-version ] + [--endpoint-override ] [--profile HMAC_KEY] + [--insecure] [--os-cacert ] + [--os-cert ] [--os-key ] [--timeout ] + [--os-auth-type ] [--os-auth-url OS_AUTH_URL] + [--os-domain-id OS_DOMAIN_ID] [--os-domain-name OS_DOMAIN_NAME] + [--os-project-id OS_PROJECT_ID] + [--os-project-name OS_PROJECT_NAME] + [--os-project-domain-id OS_PROJECT_DOMAIN_ID] + [--os-project-domain-name OS_PROJECT_DOMAIN_NAME] + [--os-trust-id OS_TRUST_ID] + [--os-default-domain-id OS_DEFAULT_DOMAIN_ID] + [--os-default-domain-name OS_DEFAULT_DOMAIN_NAME] + [--os-user-id OS_USER_ID] [--os-username OS_USERNAME] + [--os-user-domain-id OS_USER_DOMAIN_ID] + [--os-user-domain-name OS_USER_DOMAIN_NAME] + [--os-password OS_PASSWORD] + ... -See `OpenStack Nova CLI Guide` for a full-fledged guide. +**Subcommands:** +``add-fixed-ip`` + **DEPRECATED** Add new IP address on a network to + server. -OPTIONS -======= +``add-secgroup`` + Add a Security Group to a server. -To get a list of available commands and options run:: +``agent-create`` + Create new agent build. - nova help +``agent-delete`` + Delete existing agent build. -To get usage and options of a command run:: +``agent-list`` + List all builds. - nova help +``agent-modify`` + Modify existing agent build. +``aggregate-add-host`` + Add the host to the specified aggregate. -EXAMPLES -======== +``aggregate-create`` + Create a new aggregate with the specified + details. -Get information about boot command:: +``aggregate-delete`` + Delete the aggregate. - nova help boot +``aggregate-list`` + Print a list of all aggregates. -List available flavors:: +``aggregate-remove-host`` + Remove the specified host from the specified + aggregate. - nova flavor-list +``aggregate-set-metadata`` + Update the metadata associated with the + aggregate. -Launch an instance:: +``aggregate-show`` + Show details of the specified aggregate. - nova boot myserver --image some-image --flavor 2 +``aggregate-update`` + Update the aggregate's name and optionally + availability zone. -View instance information:: +``availability-zone-list`` + List all the availability zones. - nova show myserver +``backup`` + Backup a server by creating a 'backup' type + snapshot. -List instances:: +``boot`` + Boot a new server. - nova list +``cell-capacities`` + Get cell capacities for all cells or a given + cell. -Terminate an instance:: +``cell-show`` + Show details of a given cell. - nova delete myserver +``clear-password`` + Clear the admin password for a server from the + metadata server. This action does not actually + change the instance server password. +``cloudpipe-configure`` + **DEPRECATED** Update the VPN IP/port of a + cloudpipe instance. -SEE ALSO -======== +``cloudpipe-create`` + **DEPRECATED** Create a cloudpipe instance for the + given project. -OpenStack Nova CLI Guide: https://docs.openstack.org/cli-reference/nova.html +``cloudpipe-list`` + **DEPRECATED** Print a list of all cloudpipe + instances. +``console-log`` + Get console log output of a server. -BUGS -==== +``delete`` + Immediately shut down and delete specified + server(s). + +``diagnostics`` + Retrieve server diagnostics. + +``evacuate`` + Evacuate server from failed host. + +``flavor-access-add`` + Add flavor access for the given tenant. + +``flavor-access-list`` + Print access information about the given + flavor. + +``flavor-access-remove`` + Remove flavor access for the given tenant. + +``flavor-create`` + Create a new flavor. + +``flavor-delete`` + Delete a specific flavor + +``flavor-key`` + Set or unset extra_spec for a flavor. + +``flavor-list`` + Print a list of available 'flavors' (sizes of + servers). + +``flavor-show`` + Show details about the given flavor. + +``floating-ip-associate`` + **DEPRECATED** Associate a floating IP address to + a server. + +``floating-ip-disassociate`` + **DEPRECATED** Disassociate a floating IP address + from a server. + +``force-delete`` + Force delete a server. + +``get-mks-console`` + Get an MKS console to a server. (Supported by + API versions '2.8' - '2.latest') [hint: use + '--os-compute-api-version' flag to show help + message for proper version] + +``get-password`` + Get the admin password for a server. This + operation calls the metadata service to query + metadata information and does not read + password information from the server itself. + +``get-rdp-console`` + Get a rdp console to a server. + +``get-serial-console`` + Get a serial console to a server. + +``get-spice-console`` + Get a spice console to a server. + +``get-vnc-console`` + Get a vnc console to a server. + +``host-action`` + **DEPRECATED** Perform a power action on a host. + +``host-describe`` + **DEPRECATED** Describe a specific host. + +``host-evacuate`` + Evacuate all instances from failed host. + +``host-evacuate-live`` + Live migrate all instances of the specified + host to other available hosts. + +``host-list`` + **DEPRECATED** List all hosts by service. + +``host-meta`` + Set or Delete metadata on all instances of a + host. + +``host-servers-migrate`` + Cold migrate all instances off the specified + host to other available hosts. + +``host-update`` + **DEPRECATED** Update host settings. + +``hypervisor-list`` + List hypervisors. (Supported by API versions + '2.0' + - + '2.latest') + [hint: + use + '--os-compute-api-version' + flag + to + show + help + message + for + proper version] + +``hypervisor-servers`` + List servers belonging to specific + hypervisors. + +``hypervisor-show`` + Display the details of the specified + hypervisor. + +``hypervisor-stats`` + Get hypervisor statistics over all compute + nodes. + +``hypervisor-uptime`` + Display the uptime of the specified + hypervisor. + +``image-create`` + Create a new image by taking a snapshot of a + running server. + +``instance-action`` + Show an action. + +``instance-action-list`` + List actions on a server. + +``interface-attach`` + Attach a network interface to a server. + +``interface-detach`` + Detach a network interface from a server. + +``interface-list`` + List interfaces attached to a server. + +``keypair-add`` + Create a new key pair for use with servers. + +``keypair-delete`` + Delete keypair given by its name. (Supported + by API versions '2.0' - '2.latest') [hint: use + '--os-compute-api-version' flag to show help + message for proper version] + +``keypair-list`` + Print a list of keypairs for a user (Supported + by API versions '2.0' - '2.latest') [hint: use + '--os-compute-api-version' flag to show help + message for proper version] + +``keypair-show`` + Show details about the given keypair. + (Supported by API versions '2.0' - '2.latest') + [hint: use '--os-compute-api-version' flag to + show help message for proper version] + +``limits`` + Print rate and absolute limits. + +``list`` + List servers. + +``list-extensions`` + List all the os-api extensions that are + available. + +``list-secgroup`` + List Security Group(s) of a server. + +``live-migration`` + Migrate running server to a new machine. + +``live-migration-abort`` + Abort an on-going live migration. (Supported + by API versions '2.24' - '2.latest') [hint: + use '--os-compute-api-version' flag to show + help message for proper version] + +``live-migration-force-complete`` + Force on-going live migration to complete. + (Supported + by + API + versions + '2.22' + -'2.latest') + [hint: + use + '--os-compute-api-version' + flag + to + show + help + message + for + proper + version] + +``lock`` + Lock a server. A normal (non-admin) user will + not be able to execute actions on a locked + server. + +``meta`` + Set or delete metadata on a server. + +``migrate`` + Migrate a server. The new host will be + selected by the scheduler. + +``migration-list`` + Print a list of migrations. + +``pause`` + Pause a server. + +``quota-class-show`` + List the quotas for a quota class. + +``quota-class-update`` + Update the quotas for a quota class. + (Supported by API versions '2.0' - '2.latest') + [hint: use '--os-compute-api-version' flag to + show help message for proper version] + +``quota-defaults`` + List the default quotas for a tenant. + +``quota-delete`` + Delete quota for a tenant/user so their quota + will Revert back to default. + +``quota-show`` + List the quotas for a tenant/user. + +``quota-update`` + Update the quotas for a tenant/user. + (Supported by API versions '2.0' - '2.latest') + [hint: use '--os-compute-api-version' flag to + show help message for proper version] + +``reboot`` + Reboot a server. + +``rebuild`` + Shutdown, re-image, and re-boot a server. + +``refresh-network`` + Refresh server network information. + +``remove-fixed-ip`` + **DEPRECATED** Remove an IP address from a server. + +``remove-secgroup`` + Remove a Security Group from a server. + +``rescue`` + Reboots a server into rescue mode, which + starts the machine from either the initial + image or a specified image, attaching the + current boot disk as secondary. + +``reset-network`` + Reset network of a server. + +``reset-state`` + Reset the state of a server. + +``resize`` + Resize a server. + +``resize-confirm`` + Confirm a previous resize. + +``resize-revert`` + Revert a previous resize (and return to the + previous VM). + +``restore`` + Restore a soft-deleted server. + +``resume`` + Resume a server. + +``server-group-create`` + Create a new server group with the specified + details. + +``server-group-delete`` + Delete specific server group(s). + +``server-group-get`` + Get a specific server group. + +``server-group-list`` + Print a list of all server groups. + +``server-migration-list`` + Get the migrations list of specified server. + (Supported + by + API + versions + '2.23' + -'2.latest') + [hint: + use + '--os-compute-api-version' + flag + to + show + help + message + for + proper + version] + +``server-migration-show`` + Get the migration of specified server. + (Supported + by + API + versions + '2.23' + -'2.latest') + [hint: + use + '--os-compute-api-version' + flag + to + show + help + message + for + proper + version] + +``server-tag-add`` + Add one or more tags to a server. (Supported + by API versions '2.26' - '2.latest') [hint: + use '--os-compute-api-version' flag to show + help message for proper version] + +``server-tag-delete`` + Delete one or more tags from a server. + (Supported + by + API + versions + '2.26' + -'2.latest') + [hint: + use + '--os-compute-api-version' + flag + to + show + help + message + for + proper + version] + +``server-tag-delete-all`` + Delete all tags from a server. (Supported by + API versions '2.26' - '2.latest') [hint: use + '--os-compute-api-version' flag to show help + message for proper version] + +``server-tag-list`` + Get list of tags from a server. (Supported by + API versions '2.26' - '2.latest') [hint: use + '--os-compute-api-version' flag to show help + message for proper version] + +``server-tag-set`` + Set list of tags to a server. (Supported by + API versions '2.26' - '2.latest') [hint: use + '--os-compute-api-version' flag to show help + message for proper version] + +``service-delete`` + Delete the service. + +``service-disable`` + Disable the service. + +``service-enable`` + Enable the service. + +``service-force-down`` + Force service to down. (Supported by API + versions '2.11' - '2.latest') [hint: use + '--os-compute-api-version' flag to show help + message for proper version] + +``service-list`` + Show a list of all running services. Filter by + host & binary. + +``set-password`` + Change the admin password for a server. + +``shelve`` + Shelve a server. + +``shelve-offload`` + Remove a shelved server from the compute node. + +``show`` + Show details about the given server. + +``ssh`` + SSH into a server. + +``start`` + Start the server(s). + +``stop`` + Stop the server(s). + +``suspend`` + Suspend a server. + +``trigger-crash-dump`` + Trigger crash dump in an instance. (Supported + by API versions '2.17' - '2.latest') [hint: + use '--os-compute-api-version' flag to show + help message for proper version] + +``unlock`` + Unlock a server. + +``unpause`` + Unpause a server. + +``unrescue`` + Restart the server from normal boot disk + again. + +``unshelve`` + Unshelve a server. + +``update`` + Update the name or the description for a + server. + +``usage`` + Show usage data for a single tenant. + +``usage-list`` + List usage data for all tenants. + +``version-list`` + List all API versions. + +``virtual-interface-list`` + **DEPRECATED** Show virtual interface info about + the given server. + +``volume-attach`` + Attach a volume to a server. + +``volume-attachments`` + List all the volumes attached to a server. + +``volume-detach`` + Detach a volume from a server. + +``volume-update`` + Update the attachment on the server. Migrates + the data from an attached volume to the + specified available volume and swaps out the + active attachment to the new volume. + +``x509-create-cert`` + **DEPRECATED** Create x509 cert for a user in + tenant. + +``x509-get-root-cert`` + **DEPRECATED** Fetch the x509 root cert. + +``bash-completion`` + Prints all of the commands and options to + stdout so that the nova.bash_completion script + doesn't have to hard code them. + +``help`` + Display help about this program or one of its + subcommands. + +.. _nova_command_options: + +nova optional arguments +~~~~~~~~~~~~~~~~~~~~~~~ + +``--version`` + show program's version number and exit + +``--debug`` + Print debugging output. + +``--os-cache`` + Use the auth token cache. Defaults to False if + ``env[OS_CACHE]`` is not set. + +``--timings`` + Print call timing info. + +``--os-region-name `` + Defaults to ``env[OS_REGION_NAME]``. + +``--service-type `` + Defaults to compute for most actions. + +``--service-name `` + Defaults to ``env[NOVA_SERVICE_NAME]``. + +``--os-endpoint-type `` + Defaults to ``env[NOVA_ENDPOINT_TYPE]``, + ``env[OS_ENDPOINT_TYPE]`` or publicURL. + +``--os-compute-api-version `` + Accepts X, X.Y (where X is major and Y is + minor part) or "X.latest", defaults to + ``env[OS_COMPUTE_API_VERSION]``. + +``--endpoint-override `` + Use this API endpoint instead of the Service + Catalog. Defaults to + ``env[NOVACLIENT_ENDPOINT_OVERRIDE]``. + +``--profile HMAC_KEY`` + HMAC key to use for encrypting context data + for performance profiling of operation. This + key should be the value of the HMAC key + configured for the OSprofiler middleware in + nova; it is specified in the Nova + configuration file at "/etc/nova/nova.conf". + Without the key, profiling will not be + triggered even if OSprofiler is enabled on the + server side. + +``--os-auth-type , --os-auth-plugin `` + Authentication type to use + +.. _nova_add-secgroup: + +nova add-secgroup +----------------- + +.. code-block:: console + + usage: nova add-secgroup + +Add a Security Group to a server. + +**Positional arguments:** + +```` + Name or ID of server. + +```` + Name or ID of Security Group. + +.. _nova_agent-create: + +nova agent-create +----------------- + +.. code-block:: console + + usage: nova agent-create + + +Create new agent build. + +**Positional arguments:** + +```` + Type of OS. + +```` + Type of architecture. + +```` + Version. + +```` + URL. + +```` + MD5 hash. + +```` + Type of hypervisor. + +.. _nova_agent-delete: + +nova agent-delete +----------------- + +.. code-block:: console + + usage: nova agent-delete + +Delete existing agent build. + +**Positional arguments:** + +```` + ID of the agent-build. + +.. _nova_agent-list: + +nova agent-list +--------------- + +.. code-block:: console + + usage: nova agent-list [--hypervisor ] + +List all builds. + +**Optional arguments:** + +``--hypervisor `` + Type of hypervisor. + +.. _nova_agent-modify: + +nova agent-modify +----------------- + +.. code-block:: console + + usage: nova agent-modify + +Modify existing agent build. + +**Positional arguments:** + +```` + ID of the agent-build. + +```` + Version. + +```` + URL + +```` + MD5 hash. + +.. _nova_aggregate-add-host: + +nova aggregate-add-host +----------------------- + +.. code-block:: console + + usage: nova aggregate-add-host + +Add the host to the specified aggregate. + +**Positional arguments:** + +```` + Name or ID of aggregate. + +```` + The host to add to the aggregate. + +.. _nova_aggregate-create: + +nova aggregate-create +--------------------- + +.. code-block:: console + + usage: nova aggregate-create [] + +Create a new aggregate with the specified details. + +**Positional arguments:** + +```` + Name of aggregate. + +```` + The availability zone of the aggregate (optional). + +.. _nova_aggregate-delete: + +nova aggregate-delete +--------------------- + +.. code-block:: console + + usage: nova aggregate-delete + +Delete the aggregate. + +**Positional arguments:** + +```` + Name or ID of aggregate to delete. + +.. _nova_aggregate-list: + +nova aggregate-list +------------------- + +.. code-block:: console + + usage: nova aggregate-list + +Print a list of all aggregates. + +.. _nova_aggregate-remove-host: + +nova aggregate-remove-host +-------------------------- + +.. code-block:: console + + usage: nova aggregate-remove-host + +Remove the specified host from the specified aggregate. + +**Positional arguments:** + +```` + Name or ID of aggregate. + +```` + The host to remove from the aggregate. + +.. _nova_aggregate-set-metadata: + +nova aggregate-set-metadata +--------------------------- + +.. code-block:: console + + usage: nova aggregate-set-metadata [ ...] + +Update the metadata associated with the aggregate. + +**Positional arguments:** + +```` + Name or ID of aggregate to update. + +```` + Metadata to add/update to aggregate. Specify only the key to + delete a metadata item. + +.. _nova_aggregate-show: + +nova aggregate-show +------------------- + +.. code-block:: console + + usage: nova aggregate-show + +Show details of the specified aggregate. + +**Positional arguments:** + +```` + Name or ID of aggregate. + +.. _nova_aggregate-update: + +nova aggregate-update +--------------------- + +.. code-block:: console + + usage: nova aggregate-update [--name NAME] + [--availability-zone ] + + +Update the aggregate's name and optionally availability zone. + +**Positional arguments:** + +```` + Name or ID of aggregate to update. + +**Optional arguments:** + +``--name NAME`` + New name for aggregate. + +``--availability-zone `` + New availability zone for aggregate. + +.. _nova_availability-zone-list: + +nova availability-zone-list +--------------------------- + +.. code-block:: console + + usage: nova availability-zone-list + +List all the availability zones. + +.. _nova_backup: + +nova backup +----------- + +.. code-block:: console + + usage: nova backup + +Backup a server by creating a 'backup' type snapshot. + +**Positional arguments:** + +```` + Name or ID of server. + +```` + Name of the backup image. + +```` + The backup type, like "daily" or "weekly". + +```` + Int parameter representing how many backups to keep around. + +.. _nova_boot: + +nova boot +--------- + +.. code-block:: console + + usage: nova boot [--flavor ] [--image ] + [--image-with ] [--boot-volume ] + [--snapshot ] [--min-count ] + [--max-count ] [--meta ] + [--file ] [--key-name ] + [--user-data ] + [--availability-zone ] + [--security-groups ] + [--block-device-mapping ] + [--block-device key1=value1[,key2=value2...]] + [--swap ] + [--ephemeral size=[,format=]] + [--hint ] + [--nic ] + [--config-drive ] [--poll] [--admin-pass ] + [--access-ip-v4 ] [--access-ip-v6 ] + [--description ] + + +Boot a new server. + +**Positional arguments:** + +```` + Name for the new server. + +**Optional arguments:** + +``--flavor `` + Name or ID of flavor (see 'nova flavor-list'). + +``--image `` + Name or ID of image (see 'glance image-list'). + +``--image-with `` + Image metadata property (see 'glance image-show'). + +``--boot-volume `` + Volume ID to boot from. + +``--snapshot `` + Snapshot ID to boot from (will create a + volume). + +``--min-count `` + Boot at least servers (limited by + quota). + +``--max-count `` + Boot up to servers (limited by + quota). + +``--meta `` + Record arbitrary key/value metadata to + /meta_data.json on the metadata server. Can be + specified multiple times. + +``--file `` + Store arbitrary files from locally + to on the new server. Limited by + the injected_files quota value. + +``--key-name `` + Key name of keypair that should be created + earlier with the command keypair-add. + +``--user-data `` + user data file to pass to be exposed by the + metadata server. + +``--availability-zone `` + The availability zone for server placement. + +``--security-groups `` + Comma separated list of security group names. + +``--block-device-mapping `` + Block + device + mapping + in + the + format + =:::. + +``--block-device`` + key1=value1[,key2=value2...] + Block device mapping with the keys: id=UUID + (image_id, snapshot_id or volume_id only if + using source image, snapshot or volume) + source=source type (image, snapshot, volume or + blank), dest=destination type of the block + device (volume or local), bus=device's bus + (e.g. uml, lxc, virtio, ...; if omitted, + hypervisor driver chooses a suitable default, + honoured only if device type is supplied) + type=device type (e.g. disk, cdrom, ...; + defaults to 'disk') device=name of the device + (e.g. vda, xda, ...; if omitted, hypervisor + driver chooses suitable device depending on + selected bus; note the libvirt driver always + uses default device names), size=size of the + block device in MB(for swap) and in GB(for + other formats) (if omitted, hypervisor driver + calculates size), format=device will be + formatted (e.g. swap, ntfs, ...; optional), + bootindex=integer used for ordering the boot + disks (for image backed instances it is equal + to 0, for others need to be specified), + shutdown=shutdown behaviour (either preserve + or remove, for local destination set to + remove) and tag=device metadata tag + (optional). (Supported by API versions '2.42' + - '2.latest') + +``--swap `` + Create and attach a local swap block device of + MB. + +``--ephemeral`` + size=[,format=] + Create and attach a local ephemeral block + device of GB and format it to . + +``--hint `` + Send arbitrary key/value pairs to the + scheduler for custom use. + +``--nic `` + Create a NIC on the server. Specify option + multiple times to create multiple nics unless + using the special 'auto' or 'none' values. + auto: automatically allocate network resources + if none are available. This cannot be + specified with any other nic value and cannot + be specified multiple times. none: do not + attach a NIC at all. This cannot be specified + with any other nic value and cannot be + specified multiple times. net-id: attach NIC + to network with a specific UUID. net-name: + attach NIC to network with this name (either + port-id or net-id or net-name must be + provided), v4-fixed-ip: IPv4 fixed address for + NIC (optional), v6-fixed-ip: IPv6 fixed + address for NIC (optional), port-id: attach + NIC to port with this UUID tag: interface + metadata tag (optional) (either port-id or + net-id must be provided). (Supported by API + versions '2.42' - '2.latest') + +``--config-drive `` + Enable config drive. + +``--poll`` + Report the new server boot progress until it + completes. + +``--admin-pass `` + Admin password for the instance. + +``--access-ip-v4 `` + Alternative access IPv4 of the instance. + +``--access-ip-v6 `` + Alternative access IPv6 of the instance. + +``--description `` + Description for the server. (Supported by API + versions '2.19' - '2.latest') + +.. _nova_cell-capacities: + +nova cell-capacities +-------------------- + +.. code-block:: console + + usage: nova cell-capacities [--cell ] + +Get cell capacities for all cells or a given cell. + +**Optional arguments:** + +``--cell `` + Name of the cell to get the capacities. + +.. _nova_cell-show: + +nova cell-show +-------------- + +.. code-block:: console + + usage: nova cell-show + +Show details of a given cell. + +**Positional arguments:** + +```` + Name of the cell. + +.. _nova_clear-password: + +nova clear-password +------------------- + +.. code-block:: console + + usage: nova clear-password + +Clear the admin password for a server from the metadata server. This action +does not actually change the instance server password. + +**Positional arguments:** + +```` + Name or ID of server. + +.. _nova_console-log: + +nova console-log +---------------- + +.. code-block:: console + + usage: nova console-log [--length ] + +Get console log output of a server. + +**Positional arguments:** + +```` + Name or ID of server. + +**Optional arguments:** + +``--length `` + Length in lines to tail. + +.. _nova_delete: + +nova delete +----------- + +.. code-block:: console + + usage: nova delete [--all-tenants] [ ...] + +Immediately shut down and delete specified server(s). + +**Positional arguments:** + +```` + Name or ID of server(s). + +**Optional arguments:** + +``--all-tenants`` + Delete server(s) in another tenant by name (Admin only). + +.. _nova_diagnostics: + +nova diagnostics +---------------- + +.. code-block:: console + + usage: nova diagnostics + +Retrieve server diagnostics. + +**Positional arguments:** + +```` + Name or ID of server. + +.. _nova_evacuate: + +nova evacuate +------------- + +.. code-block:: console + + usage: nova evacuate [--password ] [--force] [] + +Evacuate server from failed host. + +**Positional arguments:** + +```` + Name or ID of server. + +```` + Name or ID of the target host. If no host is + specified, the scheduler will choose one. + +**Optional arguments:** + +``--password `` + Set the provided admin password on the evacuated + server. Not applicable if the server is on shared + storage. + +``--force`` + Force to not verify the scheduler if a host is + provided. (Supported by API versions '2.29' -'2.latest') + +.. _nova_flavor-access-add: + +nova flavor-access-add +---------------------- + +.. code-block:: console + + usage: nova flavor-access-add + +Add flavor access for the given tenant. + +**Positional arguments:** + +```` + Flavor name or ID to add access for the given tenant. + +```` + Tenant ID to add flavor access for. + +.. _nova_flavor-access-list: + +nova flavor-access-list +----------------------- + +.. code-block:: console + + usage: nova flavor-access-list [--flavor ] + +Print access information about the given flavor. + +**Optional arguments:** + +``--flavor `` + Filter results by flavor name or ID. + +.. _nova_flavor-access-remove: + +nova flavor-access-remove +------------------------- + +.. code-block:: console + + usage: nova flavor-access-remove + +Remove flavor access for the given tenant. + +**Positional arguments:** + +```` + Flavor name or ID to remove access for the given tenant. + +```` + Tenant ID to remove flavor access for. + +.. _nova_flavor-create: + +nova flavor-create +------------------ + +.. code-block:: console + + usage: nova flavor-create [--ephemeral ] [--swap ] + [--rxtx-factor ] [--is-public ] + + +Create a new flavor. + +**Positional arguments:** + +```` + Unique name of the new flavor. + +```` + Unique ID of the new flavor. Specifying 'auto' will + generated a UUID for the ID. + +```` + Memory size in MB. + +```` + Disk size in GB. + +```` + Number of vcpus + +**Optional arguments:** + +``--ephemeral `` + Ephemeral space size in GB (default 0). + +``--swap `` + Swap space size in MB (default 0). + +``--rxtx-factor `` + RX/TX factor (default 1). + +``--is-public `` + Make flavor accessible to the public (default + true). + +.. _nova_flavor-delete: + +nova flavor-delete +------------------ + +.. code-block:: console + + usage: nova flavor-delete + +Delete a specific flavor + +**Positional arguments:** + +```` + Name or ID of the flavor to delete. + +.. _nova_flavor-key: + +nova flavor-key +--------------- + +.. code-block:: console + + usage: nova flavor-key [ ...] + +Set or unset extra_spec for a flavor. + +**Positional arguments:** + +```` + Name or ID of flavor. + +```` + Actions: 'set' or 'unset'. + +```` + Extra_specs to set/unset (only key is necessary on unset). + +.. _nova_flavor-list: + +nova flavor-list +---------------- + +.. code-block:: console + + usage: nova flavor-list [--extra-specs] [--all] [--marker ] + [--min-disk ] [--min-ram ] + [--limit ] [--sort-key ] + [--sort-dir ] + +Print a list of available 'flavors' (sizes of servers). + +**Optional arguments:** + +``--extra-specs`` + Get extra-specs of each flavor. + +``--all`` + Display all flavors (Admin only). + +``--marker `` + The last flavor ID of the previous page; displays + list of flavors after "marker". + +``--min-disk `` + Filters the flavors by a minimum disk space, in GiB. + +``--min-ram `` + Filters the flavors by a minimum RAM, in MB. + +``--limit `` + Maximum number of flavors to display. If limit is + bigger than 'CONF.api.max_limit' option of Nova API, + limit 'CONF.api.max_limit' will be used instead. + +``--sort-key `` + Flavors list sort key. + +``--sort-dir `` + Flavors list sort direction. + +.. _nova_flavor-show: + +nova flavor-show +---------------- + +.. code-block:: console + + usage: nova flavor-show + +Show details about the given flavor. + +**Positional arguments:** + +```` + Name or ID of flavor. + +.. _nova_force-delete: + +nova force-delete +----------------- + +.. code-block:: console + + usage: nova force-delete + +Force delete a server. + +**Positional arguments:** + +```` + Name or ID of server. + +.. _nova_get-mks-console: + +nova get-mks-console +-------------------- + +.. code-block:: console + + usage: nova get-mks-console + +Get an MKS console to a server. (Supported by API versions '2.8' - '2.latest') +[hint: use '--os-compute-api-version' flag to show help message for proper +version] + +**Positional arguments:** + +```` + Name or ID of server. + +.. _nova_get-password: + +nova get-password +----------------- + +.. code-block:: console + + usage: nova get-password [] + +Get the admin password for a server. This operation calls the metadata service +to query metadata information and does not read password information from the +server itself. + +**Positional arguments:** + +```` + Name or ID of server. + +```` + Private key (used locally to decrypt password) (Optional). + When specified, the command displays the clear (decrypted) VM + password. When not specified, the ciphered VM password is + displayed. + +.. _nova_get-rdp-console: + +nova get-rdp-console +-------------------- + +.. code-block:: console + + usage: nova get-rdp-console + +Get a rdp console to a server. + +**Positional arguments:** + +```` + Name or ID of server. + +```` + Type of rdp console ("rdp-html5"). + +.. _nova_get-serial-console: + +nova get-serial-console +----------------------- + +.. code-block:: console + + usage: nova get-serial-console [--console-type CONSOLE_TYPE] + +Get a serial console to a server. + +**Positional arguments:** + +```` + Name or ID of server. + +**Optional arguments:** + +``--console-type CONSOLE_TYPE`` + Type of serial console, default="serial". + +.. _nova_get-spice-console: + +nova get-spice-console +---------------------- + +.. code-block:: console + + usage: nova get-spice-console + +Get a spice console to a server. + +**Positional arguments:** + +```` + Name or ID of server. + +```` + Type of spice console ("spice-html5"). + +.. _nova_get-vnc-console: + +nova get-vnc-console +-------------------- + +.. code-block:: console + + usage: nova get-vnc-console + +Get a vnc console to a server. + +**Positional arguments:** + +```` + Name or ID of server. + +```` + Type of vnc console ("novnc" or "xvpvnc"). + +.. _nova_host-evacuate: + +nova host-evacuate +------------------ + +.. code-block:: console + + usage: nova host-evacuate [--target_host ] [--force] + +Evacuate all instances from failed host. + +**Positional arguments:** + +```` + Name of host. + +**Optional arguments:** + +``--target_host `` + Name of target host. If no host is specified + the scheduler will select a target. + +``--force`` + Force to not verify the scheduler if a host is + provided. (Supported by API versions '2.29' -'2.latest') + +.. _nova_host-evacuate-live: + +nova host-evacuate-live +----------------------- + +.. code-block:: console + + usage: nova host-evacuate-live [--target-host ] [--block-migrate] + [--max-servers ] [--force] + + +Live migrate all instances of the specified host to other available hosts. + +**Positional arguments:** + +```` + Name of host. + +**Optional arguments:** + +``--target-host `` + Name of target host. + +``--block-migrate`` + Enable block migration. (Default=auto) + (Supported by API versions '2.25' - '2.latest') + +``--max-servers `` + Maximum number of servers to live migrate + simultaneously + +``--force`` + Force to not verify the scheduler if a host is + provided. (Supported by API versions '2.30' -'2.latest') + +.. _nova_host-meta: + +nova host-meta +-------------- + +.. code-block:: console + + usage: nova host-meta [ ...] + +Set or Delete metadata on all instances of a host. + +**Positional arguments:** + +```` + Name of host. + +```` + Actions: 'set' or 'delete' + +```` + Metadata to set or delete (only key is necessary on delete) + +.. _nova_host-servers-migrate: + +nova host-servers-migrate +------------------------- + +.. code-block:: console + + usage: nova host-servers-migrate + +Cold migrate all instances off the specified host to other available hosts. + +**Positional arguments:** + +```` + Name of host. + +.. _nova_hypervisor-list: + +nova hypervisor-list +-------------------- + +.. code-block:: console + + usage: nova hypervisor-list [--matching ] [--marker ] + [--limit ] + +List hypervisors. (Supported by API versions '2.0' - '2.latest') [hint: use +'--os-compute-api-version' flag to show help message for proper version] + +**Optional arguments:** + +``--matching `` + List hypervisors matching the given . If + matching is used limit and marker options will be + ignored. + +``--marker `` + The last hypervisor of the previous page; displays + list of hypervisors after "marker". + +``--limit `` + Maximum number of hypervisors to display. If limit is + bigger than 'CONF.api.max_limit' option of Nova API, + limit 'CONF.api.max_limit' will be used instead. + +.. _nova_hypervisor-servers: + +nova hypervisor-servers +----------------------- + +.. code-block:: console + + usage: nova hypervisor-servers + +List servers belonging to specific hypervisors. + +**Positional arguments:** + +```` + The hypervisor hostname (or pattern) to search for. + +.. _nova_hypervisor-show: + +nova hypervisor-show +-------------------- + +.. code-block:: console + + usage: nova hypervisor-show [--wrap ] + +Display the details of the specified hypervisor. + +**Positional arguments:** + +```` + Name or ID of the hypervisor to show the details of. + +**Optional arguments:** + +``--wrap `` + Wrap the output to a specified length. Default is 40 or 0 + to disable + +.. _nova_hypervisor-stats: + +nova hypervisor-stats +--------------------- + +.. code-block:: console + + usage: nova hypervisor-stats + +Get hypervisor statistics over all compute nodes. + +.. _nova_hypervisor-uptime: + +nova hypervisor-uptime +---------------------- + +.. code-block:: console + + usage: nova hypervisor-uptime + +Display the uptime of the specified hypervisor. + +**Positional arguments:** + +```` + Name or ID of the hypervisor to show the uptime of. + +.. _nova_image-create: + +nova image-create +----------------- + +.. code-block:: console + + usage: nova image-create [--metadata ] [--show] [--poll] + + +Create a new image by taking a snapshot of a running server. + +**Positional arguments:** + +```` + Name or ID of server. + +```` + Name of snapshot. + +**Optional arguments:** + +``--metadata `` + Record arbitrary key/value metadata to + /meta_data.json on the metadata server. Can be + specified multiple times. + +``--show`` + Print image info. + +``--poll`` + Report the snapshot progress and poll until image + creation is complete. + +.. _nova_instance-action: + +nova instance-action +-------------------- + +.. code-block:: console + + usage: nova instance-action + +Show an action. + +**Positional arguments:** + +```` + Name or UUID of the server to show actions for. Only UUID can + be used to show actions for a deleted server. (Supported by + API versions '2.21' - '2.latest') + +```` + Request ID of the action to get. + +.. _nova_instance-action-list: + +nova instance-action-list +------------------------- + +.. code-block:: console + + usage: nova instance-action-list + +List actions on a server. + +**Positional arguments:** + +```` + Name or UUID of the server to list actions for. Only UUID can be + used to list actions on a deleted server. (Supported by API + versions '2.21' - '2.latest') + +.. _nova_interface-attach: + +nova interface-attach +--------------------- + +.. code-block:: console + + usage: nova interface-attach [--port-id ] [--net-id ] + [--fixed-ip ] + + +Attach a network interface to a server. + +**Positional arguments:** + +```` + Name or ID of server. + +**Optional arguments:** + +``--port-id `` + Port ID. + +``--net-id `` + Network ID + +``--fixed-ip `` + Requested fixed IP. + +.. _nova_interface-detach: + +nova interface-detach +--------------------- + +.. code-block:: console + + usage: nova interface-detach + +Detach a network interface from a server. + +**Positional arguments:** + +```` + Name or ID of server. + +```` + Port ID. + +.. _nova_interface-list: + +nova interface-list +------------------- + +.. code-block:: console + + usage: nova interface-list + +List interfaces attached to a server. + +**Positional arguments:** + +```` + Name or ID of server. + +.. _nova_keypair-add: + +nova keypair-add +---------------- + +.. code-block:: console + + usage: nova keypair-add [--pub-key ] [--key-type ] + [--user ] + + +Create a new key pair for use with servers. + +**Positional arguments:** + +```` + Name of key. + +**Optional arguments:** + +``--pub-key `` + Path to a public ssh key. + +``--key-type `` + Keypair type. Can be ssh or x509. (Supported by API + versions '2.2' - '2.latest') + +``--user `` + ID of user to whom to add key-pair (Admin only). + (Supported by API versions '2.10' - '2.latest') + +.. _nova_keypair-delete: + +nova keypair-delete +------------------- + +.. code-block:: console + + usage: nova keypair-delete [--user ] + +Delete keypair given by its name. (Supported by API versions '2.0' - +'2.latest') [hint: use '--os-compute-api-version' flag to show help message +for proper version] + +**Positional arguments:** + +```` + Keypair name to delete. + +**Optional arguments:** + +``--user `` + ID of key-pair owner (Admin only). + +.. _nova_keypair-list: + +nova keypair-list +----------------- + +.. code-block:: console + + usage: nova keypair-list [--user ] [--marker ] + [--limit ] + +Print a list of keypairs for a user (Supported by API versions '2.0' - +'2.latest') [hint: use '--os-compute-api-version' flag to show help message +for proper version] + +**Optional arguments:** + +``--user `` + List key-pairs of specified user ID (Admin only). + +``--marker `` + The last keypair of the previous page; displays list of + keypairs after "marker". + +``--limit `` + Maximum number of keypairs to display. If limit is bigger + than 'CONF.api.max_limit' option of Nova API, limit + 'CONF.api.max_limit' will be used instead. + +.. _nova_keypair-show: + +nova keypair-show +----------------- + +.. code-block:: console + + usage: nova keypair-show [--user ] + +Show details about the given keypair. (Supported by API versions '2.0' - +'2.latest') [hint: use '--os-compute-api-version' flag to show help message +for proper version] + +**Positional arguments:** + +```` + Name of keypair. + +**Optional arguments:** + +``--user `` + ID of key-pair owner (Admin only). + +.. _nova_limits: + +nova limits +----------- + +.. code-block:: console + + usage: nova limits [--tenant []] [--reserved] + +Print rate and absolute limits. + +**Optional arguments:** + +``--tenant []`` + Display information from single tenant (Admin only). + +``--reserved`` + Include reservations count. + +.. _nova_list: + +nova list +--------- + +.. code-block:: console + + usage: nova list [--reservation-id ] [--ip ] + [--ip6 ] [--name ] + [--instance-name ] [--status ] + [--flavor ] [--image ] [--host ] + [--all-tenants [<0|1>]] [--tenant []] + [--user []] [--deleted] [--fields ] [--minimal] + [--sort [:]] [--marker ] + [--limit ] [--changes-since ] + [--tags ] [--tags-any ] + [--not-tags ] [--not-tags-any ] + +List servers. + +**Optional arguments:** + +``--reservation-id `` + Only return servers that match reservation-id. + +``--ip `` + Search with regular expression match by IP + address. + +``--ip6 `` + Search with regular expression match by IPv6 + address. + +``--name `` + Search with regular expression match by name. + +``--instance-name `` + Search with regular expression match by server + name. + +``--status `` + Search by server status. + +``--flavor `` + Search by flavor name or ID. + +``--image `` + Search by image name or ID. + +``--host `` + Search servers by hostname to which they are + assigned (Admin only). + +``--all-tenants [<0|1>]`` + Display information from all tenants (Admin + only). + +``--tenant []`` + Display information from single tenant (Admin + only). + +``--user []`` + Display information from single user (Admin + only). + +``--deleted`` + Only display deleted servers (Admin only). + +``--fields `` + Comma-separated list of fields to display. Use + the show command to see which fields are + available. + +``--minimal`` + Get only UUID and name. + +``--sort [:]`` + Comma-separated list of sort keys and + directions in the form of [:]. + The direction defaults to descending if not + specified. + +``--marker `` + The last server UUID of the previous page; + displays list of servers after "marker". + +``--limit `` + Maximum number of servers to display. If limit + == -1, all servers will be displayed. If limit + is bigger than 'CONF.api.max_limit' option of + Nova API, limit 'CONF.api.max_limit' will be + used instead. + +``--changes-since `` + List only servers changed after a certain + point of time.The provided time should be an + ISO 8061 formatted time.ex + 2016-03-04T06:27:59Z . + +``--tags `` + The given tags must all be present for a + server to be included in the list result. + Boolean expression in this case is 't1 AND + t2'. Tags must be separated by commas: --tags + (Supported by API versions '2.26' + - '2.latest') + +``--tags-any `` + If one of the given tags is present the server + will be included in the list result. Boolean + expression in this case is 't1 OR t2'. Tags + must be separated by commas: --tags-any + (Supported by API versions '2.26' + - '2.latest') + +``--not-tags `` + Only the servers that do not have any of the + given tags will be included in the list + results. Boolean expression in this case is + 'NOT(t1 AND t2)'. Tags must be separated by + commas: --not-tags (Supported by + API versions '2.26' - '2.latest') + +``--not-tags-any `` + Only the servers that do not have at least one + of the given tags will be included in the list + result. Boolean expression in this case is + 'NOT(t1 OR t2)'. Tags must be separated by + commas: --not-tags-any (Supported + by API versions '2.26' - '2.latest') + +.. _nova_list-extensions: + +nova list-extensions +-------------------- + +.. code-block:: console + + usage: nova list-extensions + +List all the os-api extensions that are available. + +.. _nova_list-secgroup: + +nova list-secgroup +------------------ + +.. code-block:: console + + usage: nova list-secgroup + +List Security Group(s) of a server. + +**Positional arguments:** + +```` + Name or ID of server. + +.. _nova_live-migration: + +nova live-migration +------------------- + +.. code-block:: console + + usage: nova live-migration [--block-migrate] [--force] [] + +Migrate running server to a new machine. + +**Positional arguments:** + +```` + Name or ID of server. + +```` + Destination host name. + +**Optional arguments:** + +``--block-migrate`` + True in case of block_migration. + (Default=auto:live_migration) (Supported by API versions + '2.25' - '2.latest') + +``--force`` + Force to not verify the scheduler if a host is provided. + (Supported by API versions '2.30' - '2.latest') + +.. _nova_live-migration-abort: + +nova live-migration-abort +------------------------- + +.. code-block:: console + + usage: nova live-migration-abort + +Abort an on-going live migration. (Supported by API versions '2.24' - +'2.latest') [hint: use '--os-compute-api-version' flag to show help message +for proper version] + +**Positional arguments:** + +```` + Name or ID of server. + +```` + ID of migration. + +.. _nova_live-migration-force-complete: + +nova live-migration-force-complete +---------------------------------- + +.. code-block:: console + + usage: nova live-migration-force-complete + +Force on-going live migration to complete. (Supported by API versions '2.22' - +'2.latest') [hint: use '--os-compute-api-version' flag to show help message +for proper version] + +**Positional arguments:** + +```` + Name or ID of server. + +```` + ID of migration. + +.. _nova_lock: + +nova lock +--------- + +.. code-block:: console + + usage: nova lock + +Lock a server. A normal (non-admin) user will not be able to execute actions +on a locked server. + +**Positional arguments:** + +```` + Name or ID of server. + +.. _nova_meta: + +nova meta +--------- + +.. code-block:: console + + usage: nova meta [ ...] + +Set or delete metadata on a server. + +**Positional arguments:** + +```` + Name or ID of server. + +```` + Actions: 'set' or 'delete'. + +```` + Metadata to set or delete (only key is necessary on delete). + +.. _nova_migrate: + +nova migrate +------------ + +.. code-block:: console + + usage: nova migrate [--poll] + +Migrate a server. The new host will be selected by the scheduler. + +**Positional arguments:** + +```` + Name or ID of server. + +**Optional arguments:** + +``--poll`` + Report the server migration progress until it completes. + +.. _nova_migration-list: + +nova migration-list +------------------- + +.. code-block:: console + + usage: nova migration-list [--instance-uuid ] [--host ] + [--status ] + +Print a list of migrations. + +**Optional arguments:** + +``--instance-uuid `` + Fetch migrations for the given instance. + +``--host `` + Fetch migrations for the given host. + +``--status `` + Fetch migrations for the given status. + +.. _nova_pause: + +nova pause +---------- + +.. code-block:: console + + usage: nova pause + +Pause a server. + +**Positional arguments:** + +```` + Name or ID of server. + +.. _nova_quota-class-show: + +nova quota-class-show +--------------------- + +.. code-block:: console + + usage: nova quota-class-show + +List the quotas for a quota class. + +**Positional arguments:** + +```` + Name of quota class to list the quotas for. + +.. _nova_quota-class-update: + +nova quota-class-update +----------------------- + +.. code-block:: console + + usage: nova quota-class-update [--instances ] [--cores ] + [--ram ] + [--metadata-items ] + [--injected-files ] + [--injected-file-content-bytes ] + [--injected-file-path-bytes ] + [--key-pairs ] + [--server-groups ] + [--server-group-members ] + + +Update the quotas for a quota class. (Supported by API versions '2.0' - +'2.latest') [hint: use '--os-compute-api-version' flag to show help message +for proper version] + +**Positional arguments:** + +```` + Name of quota class to set the quotas for. + +**Optional arguments:** + +``--instances `` + New value for the "instances" quota. + +``--cores `` + New value for the "cores" quota. + +``--ram `` + New value for the "ram" quota. + +``--metadata-items `` + New value for the "metadata-items" quota. + +``--injected-files `` + New value for the "injected-files" quota. + +``--injected-file-content-bytes `` + New value for the "injected-file-content-bytes" quota. + +``--injected-file-path-bytes `` + New value for the "injected-file-path-bytes" + quota. + +``--key-pairs `` + New value for the "key-pairs" quota. + +``--server-groups `` + New value for the "server-groups" quota. + +``--server-group-members `` + New value for the "server-group-members" + quota. + +.. _nova_quota-defaults: + +nova quota-defaults +------------------- + +.. code-block:: console + + usage: nova quota-defaults [--tenant ] + +List the default quotas for a tenant. + +**Optional arguments:** + +``--tenant `` + ID of tenant to list the default quotas for. + +.. _nova_quota-delete: + +nova quota-delete +----------------- + +.. code-block:: console + + usage: nova quota-delete --tenant [--user ] + +Delete quota for a tenant/user so their quota will Revert back to default. + +**Optional arguments:** + +``--tenant `` + ID of tenant to delete quota for. + +``--user `` + ID of user to delete quota for. + +.. _nova_quota-show: + +nova quota-show +--------------- + +.. code-block:: console + + usage: nova quota-show [--tenant ] [--user ] [--detail] + +List the quotas for a tenant/user. + +**Optional arguments:** + +``--tenant `` + ID of tenant to list the quotas for. + +``--user `` + ID of user to list the quotas for. + +``--detail`` + Show detailed info (limit, reserved, in-use). + +.. _nova_quota-update: + +nova quota-update +----------------- + +.. code-block:: console + + usage: nova quota-update [--user ] [--instances ] + [--cores ] [--ram ] + [--metadata-items ] + [--injected-files ] + [--injected-file-content-bytes ] + [--injected-file-path-bytes ] + [--key-pairs ] + [--server-groups ] + [--server-group-members ] + [--force] + + +Update the quotas for a tenant/user. (Supported by API versions '2.0' - +'2.latest') [hint: use '--os-compute-api-version' flag to show help message +for proper version] + +**Positional arguments:** + +```` + ID of tenant to set the quotas for. + +**Optional arguments:** + +``--user `` + ID of user to set the quotas for. + +``--instances `` + New value for the "instances" quota. + +``--cores `` + New value for the "cores" quota. + +``--ram `` + New value for the "ram" quota. + +``--metadata-items `` + New value for the "metadata-items" quota. + +``--injected-files `` + New value for the "injected-files" quota. + +``--injected-file-content-bytes `` + New value for the "injected-file-content-bytes" quota. + +``--injected-file-path-bytes `` + New value for the "injected-file-path-bytes" + quota. + +``--key-pairs `` + New value for the "key-pairs" quota. + +``--server-groups `` + New value for the "server-groups" quota. + +``--server-group-members `` + New value for the "server-group-members" + quota. + +``--force`` + Whether force update the quota even if the + already used and reserved exceeds the new + quota. + +.. _nova_reboot: + +nova reboot +----------- + +.. code-block:: console + + usage: nova reboot [--hard] [--poll] [ ...] + +Reboot a server. + +**Positional arguments:** + +```` + Name or ID of server(s). + +**Optional arguments:** + +``--hard`` + Perform a hard reboot (instead of a soft one). Note: Ironic does + not currently support soft reboot; consequently, bare metal nodes + will always do a hard reboot, regardless of the use of this + option. + +``--poll`` + Poll until reboot is complete. + +.. _nova_rebuild: + +nova rebuild +------------ + +.. code-block:: console + + usage: nova rebuild [--rebuild-password ] [--poll] + [--minimal] [--preserve-ephemeral] [--name ] + [--description ] [--meta ] + [--file ] + + +Shutdown, re-image, and re-boot a server. + +**Positional arguments:** + +```` + Name or ID of server. + +```` + Name or ID of new image. + +**Optional arguments:** + +``--rebuild-password `` + Set the provided admin password on the rebuilt + server. + +``--poll`` + Report the server rebuild progress until it + completes. + +``--minimal`` + Skips flavor/image lookups when showing + servers. + +``--preserve-ephemeral`` + Preserve the default ephemeral storage + partition on rebuild. + +``--name `` + Name for the new server. + +``--description `` + New description for the server. (Supported by + API versions '2.19' - '2.latest') + +``--meta `` + Record arbitrary key/value metadata to + /meta_data.json on the metadata server. Can be + specified multiple times. + +``--file `` + Store arbitrary files from locally + to on the new server. You may store + up to 5 files. + +.. _nova_refresh-network: + +nova refresh-network +-------------------- + +.. code-block:: console + + usage: nova refresh-network + +Refresh server network information. + +**Positional arguments:** + +```` + Name or ID of a server for which the network cache should be + refreshed from neutron (Admin only). + +.. _nova_remove-secgroup: + +nova remove-secgroup +-------------------- + +.. code-block:: console + + usage: nova remove-secgroup + +Remove a Security Group from a server. + +**Positional arguments:** + +```` + Name or ID of server. + +```` + Name of Security Group. + +.. _nova_rescue: + +nova rescue +----------- + +.. code-block:: console + + usage: nova rescue [--password ] [--image ] + +Reboots a server into rescue mode, which starts the machine from either the +initial image or a specified image, attaching the current boot disk as +secondary. + +**Positional arguments:** + +```` + Name or ID of server. + +**Optional arguments:** + +``--password `` + The admin password to be set in the rescue + environment. + +``--image `` + The image to rescue with. + +.. _nova_reset-network: + +nova reset-network +------------------ + +.. code-block:: console + + usage: nova reset-network + +Reset network of a server. + +**Positional arguments:** + +```` + Name or ID of server. + +.. _nova_reset-state: + +nova reset-state +---------------- + +.. code-block:: console + + usage: nova reset-state [--all-tenants] [--active] [ ...] + +Reset the state of a server. + +**Positional arguments:** + +```` + Name or ID of server(s). + +**Optional arguments:** + +``--all-tenants`` + Reset state server(s) in another tenant by name (Admin only). + +``--active`` + Request the server be reset to "active" state instead of + "error" state (the default). + +.. _nova_resize: + +nova resize +----------- + +.. code-block:: console + + usage: nova resize [--poll] + +Resize a server. + +**Positional arguments:** + +```` + Name or ID of server. + +```` + Name or ID of new flavor. + +**Optional arguments:** + +``--poll`` + Report the server resize progress until it completes. + +.. _nova_resize-confirm: + +nova resize-confirm +------------------- + +.. code-block:: console + + usage: nova resize-confirm + +Confirm a previous resize. + +**Positional arguments:** + +```` + Name or ID of server. + +.. _nova_resize-revert: + +nova resize-revert +------------------ + +.. code-block:: console + + usage: nova resize-revert + +Revert a previous resize (and return to the previous VM). + +**Positional arguments:** + +```` + Name or ID of server. + +.. _nova_restore: + +nova restore +------------ + +.. code-block:: console + + usage: nova restore + +Restore a soft-deleted server. + +**Positional arguments:** + +```` + Name or ID of server. + +.. _nova_resume: + +nova resume +----------- + +.. code-block:: console + + usage: nova resume + +Resume a server. + +**Positional arguments:** + +```` + Name or ID of server. + +.. _nova_server-group-create: + +nova server-group-create +------------------------ + +.. code-block:: console + + usage: nova server-group-create [ ...] + +Create a new server group with the specified details. + +**Positional arguments:** + +```` + Server group name. + +```` + Policies for the server groups. + +.. _nova_server-group-delete: + +nova server-group-delete +------------------------ + +.. code-block:: console + + usage: nova server-group-delete [ ...] + +Delete specific server group(s). + +**Positional arguments:** + +```` + Unique ID(s) of the server group to delete. + +.. _nova_server-group-get: + +nova server-group-get +--------------------- + +.. code-block:: console + + usage: nova server-group-get + +Get a specific server group. + +**Positional arguments:** + +```` + Unique ID of the server group to get. + +.. _nova_server-group-list: + +nova server-group-list +---------------------- + +.. code-block:: console + + usage: nova server-group-list [--limit ] [--offset ] + [--all-projects] + +Print a list of all server groups. + +**Optional arguments:** + +``--limit `` + Maximum number of server groups to display. If limit is + bigger than 'CONF.api.max_limit' option of Nova API, + limit 'CONF.api.max_limit' will be used instead. + +``--offset `` + The offset of groups list to display; use with limit to + return a slice of server groups. + +``--all-projects`` + Display server groups from all projects (Admin only). + +.. _nova_server-migration-list: + +nova server-migration-list +-------------------------- + +.. code-block:: console + + usage: nova server-migration-list + +Get the migrations list of specified server. (Supported by API versions '2.23' +- '2.latest') [hint: use '--os-compute-api-version' flag to show help message +for proper version] + +**Positional arguments:** + +```` + Name or ID of server. + +.. _nova_server-migration-show: + +nova server-migration-show +-------------------------- + +.. code-block:: console + + usage: nova server-migration-show + +Get the migration of specified server. (Supported by API versions '2.23' - +'2.latest') [hint: use '--os-compute-api-version' flag to show help message +for proper version] + +**Positional arguments:** + +```` + Name or ID of server. + +```` + ID of migration. + +.. _nova_server-tag-add: + +nova server-tag-add +------------------- + +.. code-block:: console + + usage: nova server-tag-add [ ...] + +Add one or more tags to a server. (Supported by API versions '2.26' - +'2.latest') [hint: use '--os-compute-api-version' flag to show help message +for proper version] + +**Positional arguments:** + +```` + Name or ID of server. + +```` + Tag(s) to add. + +.. _nova_server-tag-delete: + +nova server-tag-delete +---------------------- + +.. code-block:: console + + usage: nova server-tag-delete [ ...] + +Delete one or more tags from a server. (Supported by API versions '2.26' - +'2.latest') [hint: use '--os-compute-api-version' flag to show help message +for proper version] + +**Positional arguments:** + +```` + Name or ID of server. + +```` + Tag(s) to delete. + +.. _nova_server-tag-delete-all: + +nova server-tag-delete-all +-------------------------- + +.. code-block:: console + + usage: nova server-tag-delete-all + +Delete all tags from a server. (Supported by API versions '2.26' - '2.latest') +[hint: use '--os-compute-api-version' flag to show help message for proper +version] + +**Positional arguments:** + +```` + Name or ID of server. + +.. _nova_server-tag-list: + +nova server-tag-list +-------------------- + +.. code-block:: console + + usage: nova server-tag-list + +Get list of tags from a server. (Supported by API versions '2.26' - +'2.latest') [hint: use '--os-compute-api-version' flag to show help message +for proper version] + +**Positional arguments:** + +```` + Name or ID of server. + +.. _nova_server-tag-set: + +nova server-tag-set +------------------- + +.. code-block:: console + + usage: nova server-tag-set [ ...] + +Set list of tags to a server. (Supported by API versions '2.26' - '2.latest') +[hint: use '--os-compute-api-version' flag to show help message for proper +version] + +**Positional arguments:** + +```` + Name or ID of server. + +```` + Tag(s) to set. + +.. _nova_service-delete: + +nova service-delete +------------------- + +.. code-block:: console + + usage: nova service-delete + +Delete the service. + +**Positional arguments:** + +```` + ID of service. + +.. _nova_service-disable: + +nova service-disable +-------------------- + +.. code-block:: console + + usage: nova service-disable [--reason ] + +Disable the service. + +**Positional arguments:** + +```` + Name of host. + +```` + Service binary. + +**Optional arguments:** + +``--reason `` + Reason for disabling service. + +.. _nova_service-enable: + +nova service-enable +------------------- + +.. code-block:: console + + usage: nova service-enable + +Enable the service. + +**Positional arguments:** + +```` + Name of host. + +```` + Service binary. + +.. _nova_service-force-down: + +nova service-force-down +----------------------- + +.. code-block:: console + + usage: nova service-force-down [--unset] + +Force service to down. (Supported by API versions '2.11' - '2.latest') [hint: +use '--os-compute-api-version' flag to show help message for proper version] + +**Positional arguments:** + +```` + Name of host. + +```` + Service binary. + +**Optional arguments:** + +``--unset`` + Unset the force state down of service. + +.. _nova_service-list: + +nova service-list +----------------- + +.. code-block:: console + + usage: nova service-list [--host ] [--binary ] + +Show a list of all running services. Filter by host & binary. + +**Optional arguments:** + +``--host `` + Name of host. + +``--binary `` + Service binary. + +.. _nova_set-password: + +nova set-password +----------------- + +.. code-block:: console + + usage: nova set-password + +Change the admin password for a server. + +**Positional arguments:** + +```` + Name or ID of server. + +.. _nova_shelve: + +nova shelve +----------- + +.. code-block:: console + + usage: nova shelve + +Shelve a server. + +**Positional arguments:** + +```` + Name or ID of server. + +.. _nova_shelve-offload: + +nova shelve-offload +------------------- + +.. code-block:: console + + usage: nova shelve-offload + +Remove a shelved server from the compute node. + +**Positional arguments:** + +```` + Name or ID of server. + +.. _nova_show: + +nova show +--------- + +.. code-block:: console + + usage: nova show [--minimal] [--wrap ] + +Show details about the given server. + +**Positional arguments:** + +```` + Name or ID of server. + +**Optional arguments:** + +``--minimal`` + Skips flavor/image lookups when showing servers. + +``--wrap `` + Wrap the output to a specified length, or 0 to disable. + +.. _nova_ssh: + +nova ssh +-------- + +.. code-block:: console + + usage: nova ssh [--port PORT] [--address-type ADDRESS_TYPE] + [--network ] [--ipv6] [--login ] [-i IDENTITY] + [--extra-opts EXTRA] + + +SSH into a server. + +**Positional arguments:** + +```` + Name or ID of server. + +**Optional arguments:** + +``--port PORT`` + Optional flag to indicate which port to use + for ssh. (Default=22) + +``--address-type ADDRESS_TYPE`` + Optional flag to indicate which IP type to + use. Possible values includes fixed and + floating (the Default). + +``--network `` + Network to use for the ssh. + +``--ipv6`` + Optional flag to indicate whether to use an + IPv6 address attached to a server. (Defaults + to IPv4 address) + +``--login `` + Login to use. + +``-i IDENTITY, --identity IDENTITY`` + Private key file, same as the -i option to the + ssh command. + +``--extra-opts EXTRA`` + Extra options to pass to ssh. see: man ssh. + +.. _nova_start: + +nova start +---------- + +.. code-block:: console + + usage: nova start [--all-tenants] [ ...] + +Start the server(s). + +**Positional arguments:** + +```` + Name or ID of server(s). + +**Optional arguments:** + +``--all-tenants`` + Start server(s) in another tenant by name (Admin only). + +.. _nova_stop: + +nova stop +--------- + +.. code-block:: console + + usage: nova stop [--all-tenants] [ ...] + +Stop the server(s). + +**Positional arguments:** + +```` + Name or ID of server(s). + +**Optional arguments:** + +``--all-tenants`` + Stop server(s) in another tenant by name (Admin only). + +.. _nova_suspend: + +nova suspend +------------ + +.. code-block:: console + + usage: nova suspend + +Suspend a server. + +**Positional arguments:** + +```` + Name or ID of server. + +.. _nova_trigger-crash-dump: + +nova trigger-crash-dump +----------------------- + +.. code-block:: console + + usage: nova trigger-crash-dump + +Trigger crash dump in an instance. (Supported by API versions '2.17' - +'2.latest') [hint: use '--os-compute-api-version' flag to show help message +for proper version] + +**Positional arguments:** + +```` + Name or ID of server. + +.. _nova_unlock: + +nova unlock +----------- + +.. code-block:: console + + usage: nova unlock + +Unlock a server. + +**Positional arguments:** + +```` + Name or ID of server. + +.. _nova_unpause: + +nova unpause +------------ + +.. code-block:: console + + usage: nova unpause + +Unpause a server. + +**Positional arguments:** + +```` + Name or ID of server. + +.. _nova_unrescue: + +nova unrescue +------------- + +.. code-block:: console + + usage: nova unrescue + +Restart the server from normal boot disk again. + +**Positional arguments:** + +```` + Name or ID of server. + +.. _nova_unshelve: + +nova unshelve +------------- + +.. code-block:: console + + usage: nova unshelve + +Unshelve a server. + +**Positional arguments:** + +```` + Name or ID of server. + +.. _nova_update: + +nova update +----------- + +.. code-block:: console + + usage: nova update [--name ] [--description ] + +Update the name or the description for a server. + +**Positional arguments:** + +```` + Name (old name) or ID of server. + +**Optional arguments:** + +``--name `` + New name for the server. + +``--description `` + New description for the server. If it equals to + empty string (i.g. ""), the server description + will be removed. (Supported by API versions + '2.19' - '2.latest') + +.. _nova_usage: + +nova usage +---------- + +.. code-block:: console + + usage: nova usage [--start ] [--end ] [--tenant ] + +Show usage data for a single tenant. + +**Optional arguments:** + +``--start `` + Usage range start date ex 2012-01-20. (default: 4 + weeks ago) + +``--end `` + Usage range end date, ex 2012-01-20. (default: + tomorrow) + +``--tenant `` + UUID of tenant to get usage for. + +.. _nova_usage-list: + +nova usage-list +--------------- + +.. code-block:: console + + usage: nova usage-list [--start ] [--end ] + +List usage data for all tenants. + +**Optional arguments:** + +``--start `` + Usage range start date ex 2012-01-20. (default: 4 weeks + ago) + +``--end `` + Usage range end date, ex 2012-01-20. (default: tomorrow) + +.. _nova_version-list: + +nova version-list +----------------- + +.. code-block:: console + + usage: nova version-list + +List all API versions. + +.. _nova_volume-attach: + +nova volume-attach +------------------ + +.. code-block:: console + + usage: nova volume-attach [] + +Attach a volume to a server. + +**Positional arguments:** + +```` + Name or ID of server. + +```` + ID of the volume to attach. + +```` + Name of the device e.g. /dev/vdb. Use "auto" for autoassign (if + supported). Libvirt driver will use default device name. + +.. _nova_volume-attachments: + +nova volume-attachments +----------------------- + +.. code-block:: console + + usage: nova volume-attachments + +List all the volumes attached to a server. + +**Positional arguments:** + +```` + Name or ID of server. + +.. _nova_volume-detach: + +nova volume-detach +------------------ + +.. code-block:: console + + usage: nova volume-detach + +Detach a volume from a server. + +**Positional arguments:** + +```` + Name or ID of server. + +```` + ID of the volume to detach. + +.. _nova_volume-update: + +nova volume-update +------------------ + +.. code-block:: console + + usage: nova volume-update + +Update the attachment on the server. Migrates the data from an attached volume +to the specified available volume and swaps out the active attachment to the +new volume. + +**Positional arguments:** + +```` + Name or ID of server. + +```` + ID of the source (original) volume. + +```` + ID of the destination volume. -Nova client is hosted in Launchpad so you can view current bugs at https://bugs.launchpad.net/python-novaclient/. From 681f1789ff265a6d9ac74d43d6b30c9ab0179891 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Tue, 5 Jun 2018 17:53:37 -0400 Subject: [PATCH 1445/1705] Add a note in the nova CLI reference about using OSC We want to eventually ween people off the nova CLI and get them to use the unified openstack CLI, so this adds a note about that. Change-Id: Ia854d2b83b489f67e514a6c066fe1bdc9549f9d3 --- doc/source/cli/nova.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/doc/source/cli/nova.rst b/doc/source/cli/nova.rst index ef5fb37a0..e53f4c1a6 100644 --- a/doc/source/cli/nova.rst +++ b/doc/source/cli/nova.rst @@ -13,6 +13,14 @@ For help on a specific :command:`nova` command, enter: $ nova help COMMAND +.. note:: Over time, command line functionality will be phased out + of the ``nova`` CLI and into the ``openstack`` CLI. Using + the ``openstack`` client where possible is preferred but + there is not full parity yet for all of the ``nova`` commands. + For information on using the ``openstack`` CLI, see: + + https://docs.openstack.org/python-openstackclient/latest/ + .. _nova_command_usage: nova usage From e23c448c323db0107e3a6d217b0fd15d37177e71 Mon Sep 17 00:00:00 2001 From: Doug Hellmann Date: Wed, 6 Jun 2018 17:58:18 -0400 Subject: [PATCH 1446/1705] fix tox python3 overrides We want to default to running all tox environments under python 3, so set the basepython value in each environment. We do not want to specify a minor version number, because we do not want to have to update the file every time we upgrade python. We do not want to set the override once in testenv, because that breaks the more specific versions used in default environments like py35 and py36. Change-Id: I2cb16250241781abe7c5a7d7de37612f07ff2b0b Signed-off-by: Doug Hellmann --- tox.ini | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tox.ini b/tox.ini index b4dfa8f44..2214780a0 100644 --- a/tox.ini +++ b/tox.ini @@ -23,12 +23,15 @@ commands = # mode. To do this define the TRACE_FAILONLY environmental variable. [testenv:pep8] +basepython = python3 commands = flake8 {posargs} [testenv:bandit] +basepython = python3 commands = bandit -r novaclient -n5 -x tests [testenv:venv] +basepython = python3 deps = -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} -r{toxinidir}/test-requirements.txt @@ -37,6 +40,7 @@ deps = commands = {posargs} [testenv:docs] +basepython = python3 deps = -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} -r{toxinidir}/requirements.txt @@ -45,6 +49,7 @@ commands = sphinx-build -b html doc/source doc/build/html [testenv:releasenotes] +basepython = python3 deps = -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} -r{toxinidir}/requirements.txt @@ -71,6 +76,7 @@ commands = python novaclient/tests/functional/hooks/check_resources.py [testenv:cover] +basepython = python3 commands = python setup.py testr --coverage --testr-args='{posargs}' coverage report @@ -92,6 +98,7 @@ exclude=.venv,.git,.tox,dist,*lib/python*,*egg,build,doc/source/conf.py,releasen import_exceptions = novaclient.i18n [testenv:bindep] +basepython = python3 # Do not install any requirements. We want this to be fast and work even if # system dependencies are missing, since it's used to tell you what system # dependencies are missing! This also means that bindep must be installed From b3cca2ba62e80d5ce9bd9e6a4484ca36fe4c7b0f Mon Sep 17 00:00:00 2001 From: Chen Date: Thu, 7 Jun 2018 22:38:41 +0800 Subject: [PATCH 1447/1705] Remove PyPI downloads According to official site, https://packaging.python.org/guides/analyzing-pypi-package-downloads/ PyPI package download statistics is no longer maintained and thus should be removed. Change-Id: I98fb84b6724a43afc91686e939ea26c94a4e8176 --- README.rst | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.rst b/README.rst index 156555121..2d1744104 100644 --- a/README.rst +++ b/README.rst @@ -15,10 +15,6 @@ Python bindings to the OpenStack Compute API :target: https://pypi.org/project/python-novaclient/ :alt: Latest Version -.. image:: https://img.shields.io/pypi/dm/python-novaclient.svg - :target: https://pypi.org/project/python-novaclient/ - :alt: Downloads - This is a client for the OpenStack Compute API. It provides a Python API (the ``novaclient`` module) and a command-line script (``nova``). Each implements 100% of the OpenStack Compute API. From ab1f6bd763873863e0b4cd02699f6b46fdbfd85d Mon Sep 17 00:00:00 2001 From: "shilpa.devharakar" Date: Wed, 23 May 2018 12:33:07 +0530 Subject: [PATCH 1448/1705] Modify novaclient to support basic attributes Added support for parsing OS_PROJECT_DOMAIN_ID, OS_PROJECT_DOMAIN_NAME, OS_USER_DOMAIN_ID, and OS_USER_DOMAIN_NAME options. Change-Id: I9e1a3426f174c2e6d9f4f6bf10e9aecb62bad8d0 Closes-Bug: #1744118 --- lower-constraints.txt | 3 +- novaclient/shell.py | 28 +++++++++++++++++++ novaclient/tests/unit/test_shell.py | 25 ++++++++++++++++- .../notes/bug-1744118-0b064d7062117317.yaml | 15 ++++++++++ requirements.txt | 2 +- test-requirements.txt | 1 + 6 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/bug-1744118-0b064d7062117317.yaml diff --git a/lower-constraints.txt b/lower-constraints.txt index 3f7fd32a3..c1cf2fc79 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -10,6 +10,7 @@ cmd2==0.8.0 contextlib2==0.4.0 coverage==4.0 cryptography==2.1 +ddt==1.0.1 debtcollector==1.2.0 decorator==3.4.0 deprecation==1.0 @@ -87,7 +88,7 @@ pytz==2013.6 PyYAML==3.12 repoze.lru==0.7 requests==2.14.2 -requests-mock==1.1.0 +requests-mock==1.2.0 requestsexceptions==1.2.0 rfc3986==0.3.1 Routes==2.3.1 diff --git a/novaclient/shell.py b/novaclient/shell.py index 2702fe23b..834712437 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -270,6 +270,14 @@ def _append_global_identity_args(self, parser, argv): 'OS_PROJECT_NAME', 'OS_TENANT_NAME', 'NOVA_PROJECT_ID')) parser.set_defaults(os_project_id=utils.env( 'OS_PROJECT_ID', 'OS_TENANT_ID')) + parser.set_defaults( + os_project_domain_id=utils.env('OS_PROJECT_DOMAIN_ID')) + parser.set_defaults( + os_project_domain_name=utils.env('OS_PROJECT_DOMAIN_NAME')) + parser.set_defaults( + os_user_domain_id=utils.env('OS_USER_DOMAIN_ID')) + parser.set_defaults( + os_user_domain_name=utils.env('OS_USER_DOMAIN_NAME')) def get_base_parser(self, argv): parser = NovaClientArgumentParser( @@ -596,6 +604,26 @@ def main(self, argv): _("You must provide an auth url " "via either --os-auth-url or env[OS_AUTH_URL].")) + # TODO(Shilpasd): need to provide support in python - novaclient + # for required options for below default auth type plugins: + # 1. v3oidcclientcredential + # 2. v3oidcpassword + # 3. v3oidcauthcode + # 4. v3oidcaccesstoken + # 5. v3oauth1 + # 6. v3fedkerb + # 7. v3adfspassword + # 8. v3samlpassword + # 9. v3applicationcredential + # TODO(Shilpasd): need to provide support in python - novaclient + # for below extra keystoneauth auth type plugins: + # We will need to add code to support discovering of versions + # supported by the keystone service based on the auth_url similar + # to the one supported by glanceclient. + # 1. v3password + # 2. v3token + # 3. v3kerberos + # 4. v3totp with utils.record_time(self.times, args.timings, 'auth_url', args.os_auth_url): keystone_session = ( diff --git a/novaclient/tests/unit/test_shell.py b/novaclient/tests/unit/test_shell.py index 86047923e..122f0727e 100644 --- a/novaclient/tests/unit/test_shell.py +++ b/novaclient/tests/unit/test_shell.py @@ -16,6 +16,7 @@ import re import sys +import ddt import fixtures from keystoneauth1 import fixture import mock @@ -35,7 +36,11 @@ 'OS_PASSWORD': 'password', 'OS_TENANT_NAME': 'tenant_name', 'OS_AUTH_URL': 'http://no.where/v2.0', - 'OS_COMPUTE_API_VERSION': '2'} + 'OS_COMPUTE_API_VERSION': '2', + 'OS_PROJECT_DOMAIN_ID': 'default', + 'OS_PROJECT_DOMAIN_NAME': 'default', + 'OS_USER_DOMAIN_ID': 'default', + 'OS_USER_DOMAIN_NAME': 'default'} FAKE_ENV2 = {'OS_USER_ID': 'user_id', 'OS_PASSWORD': 'password', @@ -349,6 +354,7 @@ def test_not_really_ambiguous_option(self): self.assertTrue(args.tic_tac) +@ddt.ddt class ShellTest(utils.TestCase): _msg_no_tenant_project = ("You must provide a project name or project" @@ -521,6 +527,23 @@ def test_no_auth_url(self): else: self.fail('CommandError not raised') + @ddt.data( + (None, 'project_domain_id', FAKE_ENV['OS_PROJECT_DOMAIN_ID']), + ('OS_PROJECT_DOMAIN_ID', 'project_domain_id', ''), + (None, 'project_domain_name', FAKE_ENV['OS_PROJECT_DOMAIN_NAME']), + ('OS_PROJECT_DOMAIN_NAME', 'project_domain_name', ''), + (None, 'user_domain_id', FAKE_ENV['OS_USER_DOMAIN_ID']), + ('OS_USER_DOMAIN_ID', 'user_domain_id', ''), + (None, 'user_domain_name', FAKE_ENV['OS_USER_DOMAIN_NAME']), + ('OS_USER_DOMAIN_NAME', 'user_domain_name', '') + ) + @ddt.unpack + def test_basic_attributes(self, exclude, client_arg, env_var): + self.make_env(exclude=exclude, fake_env=FAKE_ENV) + self.shell('list') + client_kwargs = self.mock_client.call_args_list[0][1] + self.assertEqual(env_var, client_kwargs[client_arg]) + @requests_mock.Mocker() def test_nova_endpoint_type(self, m_requests): self.make_env(fake_env=FAKE_ENV3) diff --git a/releasenotes/notes/bug-1744118-0b064d7062117317.yaml b/releasenotes/notes/bug-1744118-0b064d7062117317.yaml new file mode 100644 index 000000000..3a9688c22 --- /dev/null +++ b/releasenotes/notes/bug-1744118-0b064d7062117317.yaml @@ -0,0 +1,15 @@ +--- +fixes: + - | + A fix is made for `bug 1744118`_ which adds the below missing CLI + arguments. + + * OS_PROJECT_DOMAIN_ID + + * OS_PROJECT_DOMAIN_NAME + + * OS_USER_DOMAIN_ID + + * OS_USER_DOMAIN_NAME + + .. _bug 1744118: https://bugs.launchpad.net/python-novaclient/+bug/1744118 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 502c9df15..9ac07a62b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ iso8601>=0.1.11 # MIT oslo.i18n>=3.15.3 # Apache-2.0 oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0 oslo.utils>=3.33.0 # Apache-2.0 -PrettyTable<0.8,>=0.7.1 # BSD +PrettyTable<0.8,>=0.7.2 # BSD simplejson>=3.5.1 # MIT six>=1.10.0 # MIT Babel!=2.4.0,>=2.3.4 # BSD diff --git a/test-requirements.txt b/test-requirements.txt index e4062860d..271300b20 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -5,6 +5,7 @@ hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0 bandit>=1.1.0 # Apache-2.0 coverage!=4.4,>=4.0 # Apache-2.0 +ddt>=1.0.1 # MIT fixtures>=3.0.0 # Apache-2.0/BSD keyring>=5.5.1 # MIT/PSF mock>=2.0.0 # BSD From 7759b4b46d5834e2bbe7c40265c2d2635eb48243 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Fri, 15 Jun 2018 10:00:05 -0400 Subject: [PATCH 1449/1705] Remove doc/build when building docs Wipe out the existing doc/build when rebuilding the docs. Change-Id: Idd7f6dc171425402b8d7ff7962dc09e7c576986b --- tox.ini | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 2214780a0..852e234ff 100644 --- a/tox.ini +++ b/tox.ini @@ -7,8 +7,10 @@ skipsdist = True [testenv] usedevelop = True # tox is silly... these need to be separated by a newline.... -whitelist_externals = find - bash +whitelist_externals = + find + bash + rm passenv = ZUUL_CACHE_DIR REQUIREMENTS_PIP_LOCATION install_command = pip install {opts} {packages} @@ -46,6 +48,7 @@ deps = -r{toxinidir}/requirements.txt -r{toxinidir}/doc/requirements.txt commands = + rm -rf doc/build sphinx-build -b html doc/source doc/build/html [testenv:releasenotes] From 0ac7734e67f615aa2fd15d785b699b8832204e08 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Fri, 15 Jun 2018 10:28:23 -0400 Subject: [PATCH 1450/1705] Add CLI docs reference for flavor-update When the flavor-update command was added, we didn't have the CLI docs in-tree so this was missed. Change-Id: I3d5da9ac206d82b3fb3c51fa0872a3bae69d0a7e --- doc/source/cli/nova.rst | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/doc/source/cli/nova.rst b/doc/source/cli/nova.rst index e53f4c1a6..e066955f8 100644 --- a/doc/source/cli/nova.rst +++ b/doc/source/cli/nova.rst @@ -1499,6 +1499,29 @@ Show details about the given flavor. ```` Name or ID of flavor. +nova flavor-update +------------------ + +.. code-block:: console + + usage: nova flavor-update + +Update the description of an existing flavor. +(Supported by API versions '2.55' - '2.latest') +[hint: use '--os-compute-api-version' flag to show help message for proper +version] + +.. versionadded:: 10.0.0 + +**Positional arguments** + +```` + Name or ID of the flavor to update. + +```` + A free form description of the flavor. Limited to 65535 + characters in length. Only printable characters are allowed. + .. _nova_force-delete: nova force-delete From 7f10707e5d060874f1a562b5efdce5ddc2701389 Mon Sep 17 00:00:00 2001 From: Jackie Truong Date: Sun, 3 Sep 2017 17:24:40 -0400 Subject: [PATCH 1451/1705] Microversion 2.63 - Add trusted_image_certificates This change adds a `--trusted-image-certificate-id` option to the `nova boot` and `nova rebuild` commands. This option takes in a a single trusted certificate ID. The option may be used multiple times to specify multiple trusted certificate IDs, which will be used to validate certificates in the image signature verification process. If ID values are not specified using this option, the value of the newly added OS_TRUSTED_IMAGE_CERTIFICATE_IDS environment variable will be used instead. This value will be converted into a list before being passed on. The ``nova rebuild`` command also gets a new ``--trusted-image-certificates-unset`` option to unset/reset the trusted image certificates in a server during rebuild. This is similar to unsetting key_name and user_data during rebuild. Corresponding `trusted_image_certificates` kwarg has been added to the server create and rebuild Python API bindings. Co-Authored-By: Brianna Poulos Co-Authored-By: Matt Riedemann Change-Id: I235541a689732826950c7b2a510d5835211120c3 Implements: blueprint nova-validate-certificates --- doc/source/cli/nova.rst | 22 ++ doc/source/user/shell.rst | 10 + novaclient/__init__.py | 2 +- novaclient/tests/unit/v2/test_servers.py | 67 +++++ novaclient/tests/unit/v2/test_shell.py | 250 ++++++++++++++++++ novaclient/v2/servers.py | 35 ++- novaclient/v2/shell.py | 66 +++++ .../microversion-v2_63-cd058a9145550cae.yaml | 17 ++ 8 files changed, 465 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/microversion-v2_63-cd058a9145550cae.yaml diff --git a/doc/source/cli/nova.rst b/doc/source/cli/nova.rst index e53f4c1a6..35b950b0c 100644 --- a/doc/source/cli/nova.rst +++ b/doc/source/cli/nova.rst @@ -1011,6 +1011,7 @@ nova boot [--config-drive ] [--poll] [--admin-pass ] [--access-ip-v4 ] [--access-ip-v6 ] [--description ] + [--trusted-image-certificate-id] Boot a new server. @@ -1164,6 +1165,13 @@ Boot a new server. Description for the server. (Supported by API versions '2.19' - '2.latest') +``--trusted-image-certificate-id `` + Trusted image certificate IDs used to validate certificates + during the image signature verification process. + Defaults to env[OS_TRUSTED_IMAGE_CERTIFICATE_IDS]. + May be specified multiple times to pass multiple trusted image + certificate IDs. (Supported by API versions '2.63' - '2.latest') + .. _nova_cell-capacities: nova cell-capacities @@ -2660,6 +2668,8 @@ nova rebuild [--minimal] [--preserve-ephemeral] [--name ] [--description ] [--meta ] [--file ] + [--trusted-image-certificate-id ] + [--trusted-image-certificates-unset] Shutdown, re-image, and re-boot a server. @@ -2707,6 +2717,18 @@ Shutdown, re-image, and re-boot a server. to on the new server. You may store up to 5 files. +``--trusted-image-certificate-id `` + Trusted image certificate IDs used to validate certificates + during the image signature verification process. + Defaults to env[OS_TRUSTED_IMAGE_CERTIFICATE_IDS]. + May be specified multiple times to pass multiple trusted image + certificate IDs. (Supported by API versions '2.63' - '2.latest') + +``--trusted-image-certificates-unset`` + Unset trusted_image_certificates in the server. Cannot be + specified with the ``--trusted-image-certificate-id`` option. + (Supported by API versions '2.63' - '2.latest') + .. _nova_refresh-network: nova refresh-network diff --git a/doc/source/user/shell.rst b/doc/source/user/shell.rst index bd1fb7e93..882bb7560 100644 --- a/doc/source/user/shell.rst +++ b/doc/source/user/shell.rst @@ -60,6 +60,16 @@ some environment variables: The Keystone region name. Defaults to the first region if multiple regions are available. +.. envvar:: OS_TRUSTED_IMAGE_CERTIFICATE_IDS + + A comma-delimited list of trusted image certificate IDs. Only used + with the ``nova boot`` and ``nova rebuild`` commands starting with the + 2.63 microversion. + + For example:: + + export OS_TRUSTED_IMAGE_CERTIFICATE_IDS=trusted-cert-id1,trusted-cert-id2 + For example, in Bash you'd use:: export OS_USERNAME=yourname diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 1530b37a7..3bcad2062 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.62") +API_MAX_VERSION = api_versions.APIVersion("2.63") diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index 07ad1497e..540968cf5 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -1542,3 +1542,70 @@ def test_rebuild_server_name_meta_files(self): exceptions.UnsupportedAttribute, s.rebuild, image=1, name='new', meta={'foo': 'bar'}, files=files) self.assertIn('files', six.text_type(ex)) + + +class ServersV263Test(ServersV257Test): + + api_version = "2.63" + + def test_create_server_with_trusted_image_certificates(self): + self.cs.servers.create( + name="My server", + image=1, + flavor=1, + meta={'foo': 'bar'}, + userdata="hello moto", + key_name="fakekey", + nics=self._get_server_create_default_nics(), + trusted_image_certificates=['id1', 'id2'], + ) + self.assert_called('POST', '/servers', + {'server': { + 'flavorRef': '1', + 'imageRef': '1', + 'key_name': 'fakekey', + 'max_count': 1, + 'metadata': {'foo': 'bar'}, + 'min_count': 1, + 'name': 'My server', + 'networks': 'auto', + 'trusted_image_certificates': ['id1', 'id2'], + 'user_data': 'aGVsbG8gbW90bw==' + }} + ) + + def test_create_server_with_trusted_image_certificates_pre_263_fails(self): + self.cs.api_version = api_versions.APIVersion('2.62') + ex = self.assertRaises( + exceptions.UnsupportedAttribute, self.cs.servers.create, + name="My server", image=1, flavor=1, meta={'foo': 'bar'}, + userdata="hello moto", key_name="fakekey", + nics=self._get_server_create_default_nics(), + trusted_image_certificates=['id1', 'id2']) + self.assertIn('trusted_image_certificates', six.text_type(ex)) + + def test_rebuild_server_with_trusted_image_certificates(self): + s = self.cs.servers.get(1234) + ret = s.rebuild(image="1", trusted_image_certificates=['id1', 'id2']) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action', + {'rebuild': { + 'imageRef': '1', + 'trusted_image_certificates': ['id1', 'id2']}}) + + def test_rebuild_server_with_trusted_image_certificates_none(self): + s = self.cs.servers.get(1234) + ret = s.rebuild(image="1", trusted_image_certificates=None) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action', + {'rebuild': { + 'imageRef': '1', + 'trusted_image_certificates': None}}) + + def test_rebuild_with_trusted_image_certificates_pre_263_fails(self): + self.cs.api_version = api_versions.APIVersion('2.62') + ex = self.assertRaises(exceptions.UnsupportedAttribute, + self.cs.servers.rebuild, + '1234', fakes.FAKE_IMAGE_UUID_1, + trusted_image_certificates=['id1', 'id2']) + self.assertIn('trusted_image_certificates', six.text_type(ex)) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index b4ee79acd..a32d623b8 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -1155,6 +1155,113 @@ def test_boot_with_tags_pre_v2_52(self): self.assertRaises(SystemExit, self.run_command, cmd, api_version='2.51') + def test_boot_with_single_trusted_image_certificates(self): + self.run_command('boot --flavor 1 --image %s --nic auto some-server ' + '--trusted-image-certificate-id id1' + % FAKE_UUID_1, api_version='2.63') + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': FAKE_UUID_1, + 'min_count': 1, + 'max_count': 1, + 'networks': 'auto', + 'trusted_image_certificates': ['id1'] + }}, + ) + + def test_boot_with_multiple_trusted_image_certificates(self): + self.run_command('boot --flavor 1 --image %s --nic auto some-server ' + '--trusted-image-certificate-id id1 ' + '--trusted-image-certificate-id id2' + % FAKE_UUID_1, api_version='2.63') + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': FAKE_UUID_1, + 'min_count': 1, + 'max_count': 1, + 'networks': 'auto', + 'trusted_image_certificates': ['id1', 'id2'] + }}, + ) + + def test_boot_with_trusted_image_certificates_envar(self): + self.useFixture(fixtures.EnvironmentVariable( + 'OS_TRUSTED_IMAGE_CERTIFICATE_IDS', 'var_id1,var_id2')) + self.run_command('boot --flavor 1 --image %s --nic auto some-server' + % FAKE_UUID_1, api_version='2.63') + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': FAKE_UUID_1, + 'min_count': 1, + 'max_count': 1, + 'networks': 'auto', + 'trusted_image_certificates': ['var_id1', 'var_id2'] + }}, + ) + + def test_boot_without_trusted_image_certificates_v263(self): + self.run_command('boot --flavor 1 --image %s --nic auto some-server' + % FAKE_UUID_1, api_version='2.63') + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': FAKE_UUID_1, + 'min_count': 1, + 'max_count': 1, + 'networks': 'auto', + }}, + ) + + def test_boot_with_trusted_image_certificates_pre_v263(self): + cmd = ('boot --flavor 1 --image %s some-server ' + '--trusted-image-certificate-id id1 ' + '--trusted-image-certificate-id id2' % FAKE_UUID_1) + self.assertRaises(SystemExit, self.run_command, + cmd, api_version='2.62') + + # OS_TRUSTED_IMAGE_CERTIFICATE_IDS environment variable is not supported in + # microversions < 2.63 (should result in an UnsupportedAttribute exception) + def test_boot_with_trusted_image_certificates_envar_pre_v263(self): + self.useFixture(fixtures.EnvironmentVariable( + 'OS_TRUSTED_IMAGE_CERTIFICATE_IDS', 'var_id1,var_id2')) + cmd = ('boot --flavor 1 --image %s --nic auto some-server ' + % FAKE_UUID_1) + self.assertRaises(exceptions.UnsupportedAttribute, self.run_command, + cmd, api_version='2.62') + + def test_boot_with_trusted_image_certificates_arg_and_envvar(self): + """Tests that if both the environment variable and argument are + specified, the argument takes precedence. + """ + self.useFixture(fixtures.EnvironmentVariable( + 'OS_TRUSTED_IMAGE_CERTIFICATE_IDS', 'cert1')) + self.run_command('boot --flavor 1 --image %s --nic auto ' + '--trusted-image-certificate-id cert2 some-server' + % FAKE_UUID_1, api_version='2.63') + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': FAKE_UUID_1, + 'min_count': 1, + 'max_count': 1, + 'networks': 'auto', + 'trusted_image_certificates': ['cert2'] + }}, + ) + def test_flavor_list(self): out, _ = self.run_command('flavor-list') self.assert_called_anytime('GET', '/flavors/detail') @@ -1664,6 +1771,148 @@ def test_rebuild_user_data_and_unset_user_data(self): self.assertIn("Cannot specify '--user-data-unset' with " "'--user-data'.", six.text_type(ex)) + def test_rebuild_with_single_trusted_image_certificates(self): + self.run_command('rebuild sample-server %s ' + '--trusted-image-certificate-id id1' + % FAKE_UUID_1, api_version='2.63') + self.assert_called('GET', '/servers?name=sample-server', pos=0) + self.assert_called('GET', '/servers/1234', pos=1) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=2) + self.assert_called('POST', '/servers/1234/action', + {'rebuild': {'imageRef': FAKE_UUID_1, + 'description': None, + 'trusted_image_certificates': ['id1'] + } + }, pos=3) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=4) + + def test_rebuild_with_multiple_trusted_image_certificate_ids(self): + self.run_command('rebuild sample-server %s ' + '--trusted-image-certificate-id id1 ' + '--trusted-image-certificate-id id2' + % FAKE_UUID_1, api_version='2.63') + self.assert_called('GET', '/servers?name=sample-server', pos=0) + self.assert_called('GET', '/servers/1234', pos=1) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=2) + self.assert_called('POST', '/servers/1234/action', + {'rebuild': {'imageRef': FAKE_UUID_1, + 'description': None, + 'trusted_image_certificates': ['id1', + 'id2'] + } + }, pos=3) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=4) + + def test_rebuild_with_trusted_image_certificates_envar(self): + self.useFixture(fixtures.EnvironmentVariable( + 'OS_TRUSTED_IMAGE_CERTIFICATE_IDS', 'var_id1,var_id2')) + self.run_command('rebuild sample-server %s' + % FAKE_UUID_1, api_version='2.63') + self.assert_called('GET', '/servers?name=sample-server', pos=0) + self.assert_called('GET', '/servers/1234', pos=1) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=2) + self.assert_called('POST', '/servers/1234/action', + {'rebuild': {'imageRef': FAKE_UUID_1, + 'description': None, + 'trusted_image_certificates': + ['var_id1', 'var_id2']} + }, pos=3) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=4) + + def test_rebuild_without_trusted_image_certificates_v263(self): + self.run_command('rebuild sample-server %s' % FAKE_UUID_1, + api_version='2.63') + self.assert_called('GET', '/servers?name=sample-server', pos=0) + self.assert_called('GET', '/servers/1234', pos=1) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=2) + self.assert_called('POST', '/servers/1234/action', + {'rebuild': {'imageRef': FAKE_UUID_1, + 'description': None, + } + }, pos=3) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=4) + + def test_rebuild_with_trusted_image_certificates_pre_v263(self): + cmd = ('rebuild sample-server %s' + '--trusted-image-certificate-id id1 ' + '--trusted-image-certificate-id id2' % FAKE_UUID_1) + self.assertRaises(SystemExit, self.run_command, + cmd, api_version='2.62') + + # OS_TRUSTED_IMAGE_CERTIFICATE_IDS environment variable is not supported in + # microversions < 2.63 (should result in an UnsupportedAttribute exception) + def test_rebuild_with_trusted_image_certificates_envar_pre_v263(self): + self.useFixture(fixtures.EnvironmentVariable( + 'OS_TRUSTED_IMAGE_CERTIFICATE_IDS', 'var_id1,var_id2')) + cmd = ('rebuild sample-server %s' % FAKE_UUID_1) + self.assertRaises(exceptions.UnsupportedAttribute, self.run_command, + cmd, api_version='2.62') + + def test_rebuild_with_trusted_image_certificates_unset(self): + """Tests explicitly unsetting the existing server trusted image + certificate IDs. + """ + self.run_command('rebuild sample-server %s ' + '--trusted-image-certificates-unset' + % FAKE_UUID_1, api_version='2.63') + self.assert_called('GET', '/servers?name=sample-server', pos=0) + self.assert_called('GET', '/servers/1234', pos=1) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=2) + self.assert_called('POST', '/servers/1234/action', + {'rebuild': {'imageRef': FAKE_UUID_1, + 'description': None, + 'trusted_image_certificates': None + } + }, pos=3) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=4) + + def test_rebuild_with_trusted_image_certificates_unset_arg_conflict(self): + """Tests the error condition that trusted image certs are both unset + and set via argument during rebuild. + """ + ex = self.assertRaises( + exceptions.CommandError, self.run_command, + 'rebuild sample-server %s --trusted-image-certificate-id id1 ' + '--trusted-image-certificates-unset' % FAKE_UUID_1, + api_version='2.63') + self.assertIn("Cannot specify '--trusted-image-certificates-unset' " + "with '--trusted-image-certificate-id'", + six.text_type(ex)) + + def test_rebuild_with_trusted_image_certificates_unset_env_conflict(self): + """Tests the error condition that trusted image certs are both unset + and set via environment variable during rebuild. + """ + self.useFixture(fixtures.EnvironmentVariable( + 'OS_TRUSTED_IMAGE_CERTIFICATE_IDS', 'var_id1')) + ex = self.assertRaises( + exceptions.CommandError, self.run_command, + 'rebuild sample-server %s --trusted-image-certificates-unset' % + FAKE_UUID_1, api_version='2.63') + self.assertIn("Cannot specify '--trusted-image-certificates-unset' " + "with '--trusted-image-certificate-id'", + six.text_type(ex)) + + def test_rebuild_with_trusted_image_certificates_arg_and_envar(self): + """Tests that if both the environment variable and argument are + specified, the argument takes precedence. + """ + self.useFixture(fixtures.EnvironmentVariable( + 'OS_TRUSTED_IMAGE_CERTIFICATE_IDS', 'cert1')) + self.run_command('rebuild sample-server ' + '--trusted-image-certificate-id cert2 %s' + % FAKE_UUID_1, api_version='2.63') + self.assert_called('GET', '/servers?name=sample-server', pos=0) + self.assert_called('GET', '/servers/1234', pos=1) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=2) + self.assert_called('POST', '/servers/1234/action', + {'rebuild': {'imageRef': FAKE_UUID_1, + 'description': None, + 'trusted_image_certificates': + ['cert2']} + }, pos=3) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=4) + def test_start(self): self.run_command('start sample-server') self.assert_called('POST', '/servers/1234/action', {'os-start': None}) @@ -3547,6 +3796,7 @@ def test_versions(self): 60, # There are no client-side changes for volume multiattach. 61, # There are no version-wrapped shell method changes for this. 62, # There are no version-wrapped shell method changes for this. + 63, # There are no version-wrapped shell method changes for this. ]) versions_supported = set(range(0, novaclient.API_MAX_VERSION.ver_minor + 1)) diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index a7b3eda16..d872fa016 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -650,7 +650,7 @@ def _boot(self, response_key, name, image, flavor, block_device_mapping_v2=None, nics=None, scheduler_hints=None, config_drive=None, admin_pass=None, disk_config=None, access_ip_v4=None, access_ip_v6=None, description=None, - tags=None, **kwargs): + tags=None, trusted_image_certificates=None, **kwargs): """ Create (boot) a new server. """ @@ -768,6 +768,10 @@ def _boot(self, response_key, name, image, flavor, if tags: body['server']['tags'] = tags + if trusted_image_certificates: + body['server']['trusted_image_certificates'] = ( + trusted_image_certificates) + return self._create('/servers', body, response_key, return_raw=return_raw, **kwargs) @@ -1191,7 +1195,8 @@ def create(self, name, image, flavor, meta=None, files=None, block_device_mapping=None, block_device_mapping_v2=None, nics=None, scheduler_hints=None, config_drive=None, disk_config=None, admin_pass=None, - access_ip_v4=None, access_ip_v6=None, **kwargs): + access_ip_v4=None, access_ip_v6=None, + trusted_image_certificates=None, **kwargs): # TODO(anthony): indicate in doc string if param is an extension # and/or optional """ @@ -1252,6 +1257,8 @@ def create(self, name, image, flavor, meta=None, files=None, microversion 2.19) :param tags: A list of arbitrary strings to be added to the server as tags (allowed since microversion 2.52) + :param trusted_image_certificates: A list of trusted certificate IDs + (allowed since microversion 2.63) """ if not min_count: min_count = 1 @@ -1292,6 +1299,12 @@ def create(self, name, image, flavor, meta=None, files=None, if files and self.api_version >= personality_files_deprecation: raise exceptions.UnsupportedAttribute('files', '2.0', '2.56') + trusted_certs_microversion = api_versions.APIVersion("2.63") + if (trusted_image_certificates and + self.api_version < trusted_certs_microversion): + raise exceptions.UnsupportedAttribute("trusted_image_certificates", + "2.63") + boot_kwargs = dict( meta=meta, files=files, userdata=userdata, reservation_id=reservation_id, min_count=min_count, @@ -1299,7 +1312,8 @@ def create(self, name, image, flavor, meta=None, files=None, key_name=key_name, availability_zone=availability_zone, scheduler_hints=scheduler_hints, config_drive=config_drive, disk_config=disk_config, admin_pass=admin_pass, - access_ip_v4=access_ip_v4, access_ip_v6=access_ip_v6, **kwargs) + access_ip_v4=access_ip_v4, access_ip_v6=access_ip_v6, + trusted_image_certificates=trusted_image_certificates, **kwargs) if block_device_mapping: boot_kwargs['block_device_mapping'] = block_device_mapping @@ -1416,6 +1430,9 @@ def rebuild(self, server, image, password=None, disk_config=None, well or a string. If None is specified, the existing user_data is unset. (starting from microversion 2.57) + :param trusted_image_certificates: A list of trusted certificate IDs + or None to unset/reset the servers trusted image + certificates (allowed since microversion 2.63) :returns: :class:`Server` """ descr_microversion = api_versions.APIVersion("2.19") @@ -1436,6 +1453,15 @@ def rebuild(self, server, image, password=None, disk_config=None, if 'userdata' in kwargs and self.api_version < files_and_userdata: raise exceptions.UnsupportedAttribute('userdata', '2.57') + trusted_certs_microversion = api_versions.APIVersion("2.63") + # trusted_image_certificates is intentionally *not* a named kwarg + # so that trusted_image_certificates=None is not confused with an + # intentional unset/reset request. + if ("trusted_image_certificates" in kwargs and + self.api_version < trusted_certs_microversion): + raise exceptions.UnsupportedAttribute("trusted_image_certificates", + "2.63") + body = {'imageRef': base.getid(image)} if password is not None: body['adminPass'] = password @@ -1449,6 +1475,9 @@ def rebuild(self, server, image, password=None, disk_config=None, body["description"] = kwargs["description"] if 'key_name' in kwargs: body['key_name'] = kwargs['key_name'] + if "trusted_image_certificates" in kwargs: + body["trusted_image_certificates"] = kwargs[ + "trusted_image_certificates"] if meta: body['metadata'] = meta if files: diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 160418cc5..c1c044356 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -510,6 +510,14 @@ def _boot(cs, args): if include_files: boot_kwargs['files'] = files + if ('trusted_image_certificates' in args and + args.trusted_image_certificates): + boot_kwargs['trusted_image_certificates'] = ( + args.trusted_image_certificates) + elif utils.env('OS_TRUSTED_IMAGE_CERTIFICATE_IDS'): + boot_kwargs["trusted_image_certificates"] = utils.env( + 'OS_TRUSTED_IMAGE_CERTIFICATE_IDS').split(',') + return boot_args, boot_kwargs @@ -874,6 +882,18 @@ def _boot(cs, args): action="store_true", default=False, help=_("Return a reservation id bound to created servers.")) +@utils.arg( + '--trusted-image-certificate-id', + metavar='', + action='append', + dest='trusted_image_certificates', + default=[], + help=_('Trusted image certificate IDs used to validate certificates ' + 'during the image signature verification process. ' + 'Defaults to env[OS_TRUSTED_IMAGE_CERTIFICATE_IDS]. ' + 'May be specified multiple times to pass multiple trusted image ' + 'certificate IDs.'), + start_version="2.63") def do_boot(cs, args): """Boot a new server.""" boot_args, boot_kwargs = _boot(cs, args) @@ -1807,6 +1827,25 @@ def do_reboot(cs, args): help=_("Unset user_data in the server. Cannot be specified with the " "'--user-data' option."), start_version='2.57') +@utils.arg( + '--trusted-image-certificate-id', + metavar='', + action='append', + dest='trusted_image_certificates', + default=[], + help=_('Trusted image certificate IDs used to validate certificates ' + 'during the image signature verification process. ' + 'Defaults to env[OS_TRUSTED_IMAGE_CERTIFICATE_IDS]. ' + 'May be specified multiple times to pass multiple trusted image ' + 'certificate IDs.'), + start_version="2.63") +@utils.arg( + '--trusted-image-certificates-unset', + action='store_true', + default=False, + help=_("Unset trusted_image_certificates in the server. Cannot be " + "specified with the '--trusted-image-certificate-id' option."), + start_version="2.63") def do_rebuild(cs, args): """Shutdown, re-image, and re-boot a server.""" server = _find_server(cs, args.server) @@ -1861,6 +1900,33 @@ def do_rebuild(cs, args): elif args.key_name: kwargs['key_name'] = args.key_name + if cs.api_version >= api_versions.APIVersion('2.63'): + # First determine if the user specified anything via the command line + # or the environment variable. + trusted_image_certificates = None + if ('trusted_image_certificates' in args and + args.trusted_image_certificates): + trusted_image_certificates = args.trusted_image_certificates + elif utils.env('OS_TRUSTED_IMAGE_CERTIFICATE_IDS'): + trusted_image_certificates = utils.env( + 'OS_TRUSTED_IMAGE_CERTIFICATE_IDS').split(',') + + if args.trusted_image_certificates_unset: + kwargs['trusted_image_certificates'] = None + # Check for conflicts in option usage. + if trusted_image_certificates: + raise exceptions.CommandError( + _("Cannot specify '--trusted-image-certificates-unset' " + "with '--trusted-image-certificate-id' or with " + "OS_TRUSTED_IMAGE_CERTIFICATE_IDS env variable set.")) + elif trusted_image_certificates: + # Only specify the kwarg if there is a value specified to avoid + # confusion with unsetting the value. + kwargs['trusted_image_certificates'] = trusted_image_certificates + elif utils.env('OS_TRUSTED_IMAGE_CERTIFICATE_IDS'): + raise exceptions.UnsupportedAttribute("trusted_image_certificates", + "2.63") + server = server.rebuild(image, _password, **kwargs) _print_server(cs, args, server) diff --git a/releasenotes/notes/microversion-v2_63-cd058a9145550cae.yaml b/releasenotes/notes/microversion-v2_63-cd058a9145550cae.yaml new file mode 100644 index 000000000..f8299656f --- /dev/null +++ b/releasenotes/notes/microversion-v2_63-cd058a9145550cae.yaml @@ -0,0 +1,17 @@ +--- +features: + - | + Added support for `microversion 2.63`_, which includes the following + changes: + + - New environment variable called ``OS_TRUSTED_IMAGE_CERTIFICATE_IDS`` + - New ``nova boot`` option called ``--trusted-image-certificate-id`` + - New ``nova rebuild`` options called ``--trusted-image-certificate-id`` + and ``--trusted-image-certificates-unset`` + - New kwarg called ``trusted_image_certificates`` added to python API + bindings: + + - ``novaclient.v2.servers.ServerManager.create()`` + - ``novaclient.v2.servers.ServerManager.rebuild()`` + + .. _microversion 2.63: https://docs.openstack.org/nova/latest/api_microversion_history.html#id57 From d422fb061f9077b35188fde3a6d1ac4df1b9d83b Mon Sep 17 00:00:00 2001 From: Brianna Poulos Date: Tue, 19 Jun 2018 11:16:57 -0400 Subject: [PATCH 1452/1705] Fix trusted-image-certificate-id help text Fix the help text for nova boot to include that a parameter is expected for trusted-image-certificate-id. Also update the UnsupportedAttribute error to mention the env variable to reduce confusion. Change-Id: Ic5980b610e5fd97d3a858a2a513e2863657f36c2 --- doc/source/cli/nova.rst | 2 +- novaclient/v2/shell.py | 14 ++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/doc/source/cli/nova.rst b/doc/source/cli/nova.rst index ea11e8b6a..7c3b6cb66 100644 --- a/doc/source/cli/nova.rst +++ b/doc/source/cli/nova.rst @@ -1011,7 +1011,7 @@ nova boot [--config-drive ] [--poll] [--admin-pass ] [--access-ip-v4 ] [--access-ip-v6 ] [--description ] - [--trusted-image-certificate-id] + [--trusted-image-certificate-id ] Boot a new server. diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index c1c044356..99cc2f5a3 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -515,8 +515,13 @@ def _boot(cs, args): boot_kwargs['trusted_image_certificates'] = ( args.trusted_image_certificates) elif utils.env('OS_TRUSTED_IMAGE_CERTIFICATE_IDS'): - boot_kwargs["trusted_image_certificates"] = utils.env( - 'OS_TRUSTED_IMAGE_CERTIFICATE_IDS').split(',') + if cs.api_version >= api_versions.APIVersion('2.63'): + boot_kwargs["trusted_image_certificates"] = utils.env( + 'OS_TRUSTED_IMAGE_CERTIFICATE_IDS').split(',') + else: + raise exceptions.UnsupportedAttribute( + "OS_TRUSTED_IMAGE_CERTIFICATE_IDS", + "2.63") return boot_args, boot_kwargs @@ -1924,8 +1929,9 @@ def do_rebuild(cs, args): # confusion with unsetting the value. kwargs['trusted_image_certificates'] = trusted_image_certificates elif utils.env('OS_TRUSTED_IMAGE_CERTIFICATE_IDS'): - raise exceptions.UnsupportedAttribute("trusted_image_certificates", - "2.63") + raise exceptions.UnsupportedAttribute( + "OS_TRUSTED_IMAGE_CERTIFICATE_IDS", + "2.63") server = server.rebuild(image, _password, **kwargs) _print_server(cs, args, server) From db4fd9a5f07e560ddc6814661067949294bd3608 Mon Sep 17 00:00:00 2001 From: Chen Date: Fri, 22 Jun 2018 11:07:20 +0800 Subject: [PATCH 1453/1705] Fix help text in server-group-create Only one policy is allowed in server-group-create. Change-Id: Id9cb6f96a5f8bcece1bcda88a97c6fb5f8a58bba Related-Bug: #1767287 --- doc/source/cli/nova.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/cli/nova.rst b/doc/source/cli/nova.rst index ea11e8b6a..70977b334 100644 --- a/doc/source/cli/nova.rst +++ b/doc/source/cli/nova.rst @@ -2951,7 +2951,7 @@ nova server-group-create .. code-block:: console - usage: nova server-group-create [ ...] + usage: nova server-group-create Create a new server group with the specified details. From 4cca340520064978ac9fb73d742d7412bbc7af09 Mon Sep 17 00:00:00 2001 From: "wu.chunyang" Date: Thu, 28 Jun 2018 13:20:34 +0800 Subject: [PATCH 1454/1705] Add release note link in README Change-Id: Ife1471e7a3139f3997fff10b7a6f472509b8ee52 --- README.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.rst b/README.rst index 2d1744104..06914fe4c 100644 --- a/README.rst +++ b/README.rst @@ -28,6 +28,7 @@ This is a client for the OpenStack Compute API. It provides a Python API (the * `Source`_ * `Specs`_ * `How to Contribute`_ +* `Release Notes`_ .. _PyPi: https://pypi.org/project/python-novaclient .. _Online Documentation: https://docs.openstack.org/python-novaclient/latest @@ -37,3 +38,4 @@ This is a client for the OpenStack Compute API. It provides a Python API (the .. _Source: https://git.openstack.org/cgit/openstack/python-novaclient .. _How to Contribute: https://docs.openstack.org/infra/manual/developers.html .. _Specs: http://specs.openstack.org/openstack/nova-specs/ +.. _Release Notes: https://docs.openstack.org/releasenotes/python-novaclient From 2e6ef0c45a6ada10440eb0ce87df74bf7070c1e4 Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Mon, 9 Jul 2018 18:40:41 +0900 Subject: [PATCH 1455/1705] Rename --endpoint-override to --os-endpoint-override The ``--endpoint-override`` command line argument has been deprecated. It is renamed to ``--os-endpoint-override`` to avoid misinterpreting command line arguments. It defaults to the ``OS_ENDPOINT_OVERRIDE`` environment variable. The deprecated ``--bypass-url`` command line argument has been removed. Change-Id: Ic8a6559cd62d46b837fa9c04b482a46ceba829db Closes-Bug: #1778536 --- doc/source/cli/nova.rst | 6 ++--- novaclient/shell.py | 25 ++++++++++++++----- .../v2/legacy/test_readonly_nova.py | 2 +- .../notes/bug-1778536-a1b5d65a0d4ad622.yaml | 12 +++++++++ 4 files changed, 35 insertions(+), 10 deletions(-) create mode 100644 releasenotes/notes/bug-1778536-a1b5d65a0d4ad622.yaml diff --git a/doc/source/cli/nova.rst b/doc/source/cli/nova.rst index cd6d60eeb..14f63c4cb 100644 --- a/doc/source/cli/nova.rst +++ b/doc/source/cli/nova.rst @@ -33,7 +33,7 @@ nova usage [--service-name ] [--os-endpoint-type ] [--os-compute-api-version ] - [--endpoint-override ] [--profile HMAC_KEY] + [--os-endpoint-override ] [--profile HMAC_KEY] [--insecure] [--os-cacert ] [--os-cert ] [--os-key ] [--timeout ] [--os-auth-type ] [--os-auth-url OS_AUTH_URL] @@ -678,10 +678,10 @@ nova optional arguments minor part) or "X.latest", defaults to ``env[OS_COMPUTE_API_VERSION]``. -``--endpoint-override `` +``--os-endpoint-override `` Use this API endpoint instead of the Service Catalog. Defaults to - ``env[NOVACLIENT_ENDPOINT_OVERRIDE]``. + ``env[OS_ENDPOINT_OVERRIDE]``. ``--profile HMAC_KEY`` HMAC key to use for encrypting context data diff --git a/novaclient/shell.py b/novaclient/shell.py index 834712437..8eb4311ce 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -358,19 +358,32 @@ def get_base_parser(self, argv): '"X.latest", defaults to env[OS_COMPUTE_API_VERSION].')) parser.add_argument( - '--endpoint-override', + '--os-endpoint-override', metavar='', dest='endpoint_override', - default=utils.env('NOVACLIENT_ENDPOINT_OVERRIDE', + default=utils.env('OS_ENDPOINT_OVERRIDE', + 'NOVACLIENT_ENDPOINT_OVERRIDE', 'NOVACLIENT_BYPASS_URL'), help=_("Use this API endpoint instead of the Service Catalog. " - "Defaults to env[NOVACLIENT_ENDPOINT_OVERRIDE].")) + "Defaults to env[OS_ENDPOINT_OVERRIDE].")) + + # NOTE(takashin): This dummy '--end' argument was added + # to avoid misinterpreting command line arguments. + # If there is not this dummy argument, the '--end' is interpreted to + # the '--endpoint-override'. + # TODO(takashin): Remove this dummy '--end' argument + # when the deprecated '--endpoint-override' argument is removed. + parser.add_argument( + '--end', + metavar='', + nargs='?', + help=argparse.SUPPRESS) parser.add_argument( - '--bypass-url', + '--endpoint-override', action=DeprecatedAction, - use=_('use "%s"; this option will be removed after Pike OpenStack ' - 'release.') % '--os-endpoint-override', + use=_('use "%s"; this option will be removed after Rocky ' + 'OpenStack release.') % '--os-endpoint-override', dest='endpoint_override', help=argparse.SUPPRESS) diff --git a/novaclient/tests/functional/v2/legacy/test_readonly_nova.py b/novaclient/tests/functional/v2/legacy/test_readonly_nova.py index df88b8c6d..6b9598e96 100644 --- a/novaclient/tests/functional/v2/legacy/test_readonly_nova.py +++ b/novaclient/tests/functional/v2/legacy/test_readonly_nova.py @@ -124,4 +124,4 @@ def test_admin_invalid_bypass_url(self): self.assertRaises(exceptions.CommandFailed, self.nova, 'list', - flags='--endpoint-override badurl') + flags='--os-endpoint-override badurl') diff --git a/releasenotes/notes/bug-1778536-a1b5d65a0d4ad622.yaml b/releasenotes/notes/bug-1778536-a1b5d65a0d4ad622.yaml new file mode 100644 index 000000000..3e5410aad --- /dev/null +++ b/releasenotes/notes/bug-1778536-a1b5d65a0d4ad622.yaml @@ -0,0 +1,12 @@ +--- +upgrade: + - The deprecated ``--bypass-url`` command line argument has been removed. +deprecations: + - | + The ``--endpoint-override`` command line argument has been deprecated. + It is renamed to ``--os-endpoint-override`` to avoid misinterpreting + command line arguments. + It defaults to the ``OS_ENDPOINT_OVERRIDE`` environment variable. + See `bug 1778536`_ for more details. + + .. _bug 1778536: https://bugs.launchpad.net/python-novaclient/+bug/1778536 From 32a3244bb4879f5c0ea430de6fac284b2a987441 Mon Sep 17 00:00:00 2001 From: Vu Cong Tuan Date: Thu, 12 Jul 2018 15:23:08 +0700 Subject: [PATCH 1456/1705] Switch to stestr According to Openstack summit session [1], stestr is maintained project to which all Openstack projects should migrate. Let's switch to stestr as other projects have already moved to it. [1] https://etherpad.openstack.org/p/YVR-python-pti Change-Id: I55060b1d99b9a00b20c98fbb429d072568265695 --- .gitignore | 2 +- .stestr.conf | 3 +++ .testr.conf | 7 ------- lower-constraints.txt | 4 +--- test-requirements.txt | 3 +-- tools/pretty_tox.sh | 16 ---------------- tox.ini | 22 ++++++++++------------ 7 files changed, 16 insertions(+), 41 deletions(-) create mode 100644 .stestr.conf delete mode 100644 .testr.conf delete mode 100755 tools/pretty_tox.sh diff --git a/.gitignore b/.gitignore index 82ede6ad7..611c10e37 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ .coverage .venv -.testrepository +.stestr/ subunit.log .tox *,cover diff --git a/.stestr.conf b/.stestr.conf new file mode 100644 index 000000000..ac945e834 --- /dev/null +++ b/.stestr.conf @@ -0,0 +1,3 @@ +[DEFAULT] +test_path=${OS_TEST_PATH:-./novaclient/tests/unit} +top_dir=./ diff --git a/.testr.conf b/.testr.conf deleted file mode 100644 index c8fae426b..000000000 --- a/.testr.conf +++ /dev/null @@ -1,7 +0,0 @@ -[DEFAULT] -test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \ - OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} \ - OS_TEST_TIMEOUT=${OS_TEST_TIMEOUT:-300} \ - ${PYTHON:-python} -m subunit.run discover -t ./ ${OS_TEST_PATH:-./novaclient/tests/unit} $LISTOPT $IDOPTION -test_id_option=--load-list $IDFILE -test_list_option=--list diff --git a/lower-constraints.txt b/lower-constraints.txt index c1cf2fc79..4f156569f 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -48,7 +48,6 @@ netifaces==0.10.4 openstacksdk==0.11.2 os-client-config==1.28.0 os-service-types==1.2.0 -os-testr==1.0.0 osc-lib==1.8.0 oslo.concurrency==3.25.0 oslo.config==5.2.0 @@ -96,11 +95,10 @@ simplejson==3.5.1 six==1.10.0 smmap==0.9.0 statsd==3.2.1 -stestr==1.0.0 stevedore==1.20.0 tempest==17.1.0 tenacity==3.2.1 -testrepository==0.0.18 +stestr==2.0.0 testscenarios==0.4 testtools==2.2.0 traceback2==1.4.0 diff --git a/test-requirements.txt b/test-requirements.txt index 271300b20..3c35fec9e 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -15,9 +15,8 @@ python-glanceclient>=2.8.0 # Apache-2.0 python-neutronclient>=6.7.0 # Apache-2.0 requests-mock>=1.2.0 # Apache-2.0 os-client-config>=1.28.0 # Apache-2.0 -os-testr>=1.0.0 # Apache-2.0 osprofiler>=1.4.0 # Apache-2.0 -testrepository>=0.0.18 # Apache-2.0/BSD +stestr>=2.0.0 # Apache-2.0 testscenarios>=0.4 # Apache-2.0/BSD testtools>=2.2.0 # MIT tempest>=17.1.0 # Apache-2.0 diff --git a/tools/pretty_tox.sh b/tools/pretty_tox.sh deleted file mode 100755 index 799ac1848..000000000 --- a/tools/pretty_tox.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash - -set -o pipefail - -TESTRARGS=$1 - -# --until-failure is not compatible with --subunit see: -# -# https://bugs.launchpad.net/testrepository/+bug/1411804 -# -# this work around exists until that is addressed -if [[ "$TESTARGS" =~ "until-failure" ]]; then - python setup.py testr --slowest --testr-args="$TESTRARGS" -else - python setup.py testr --slowest --testr-args="--subunit $TESTRARGS" | subunit-trace -f -fi diff --git a/tox.ini b/tox.ini index 852e234ff..b3666b182 100644 --- a/tox.ini +++ b/tox.ini @@ -9,7 +9,6 @@ usedevelop = True # tox is silly... these need to be separated by a newline.... whitelist_externals = find - bash rm passenv = ZUUL_CACHE_DIR REQUIREMENTS_PIP_LOCATION @@ -20,9 +19,7 @@ deps = -r{toxinidir}/requirements.txt commands = find . -type f -name "*.pyc" -delete - bash tools/pretty_tox.sh '{posargs}' - # there is also secret magic in pretty_tox.sh which lets you run in a fail only - # mode. To do this define the TRACE_FAILONLY environmental variable. + stestr run {posargs} [testenv:pep8] basepython = python3 @@ -63,26 +60,27 @@ commands = [testenv:functional] basepython = python2.7 passenv = OS_NOVACLIENT_TEST_NETWORK -setenv = - OS_TEST_PATH = ./novaclient/tests/functional commands = - bash tools/pretty_tox.sh '--concurrency=1 {posargs}' + stestr --test-path=./novaclient/tests/functional run --concurrency=1 {posargs} python novaclient/tests/functional/hooks/check_resources.py [testenv:functional-py35] basepython = python3.5 passenv = OS_NOVACLIENT_TEST_NETWORK -setenv = - OS_TEST_PATH = ./novaclient/tests/functional commands = - bash tools/pretty_tox.sh '--concurrency=1 {posargs}' + stestr --test-path=./novaclient/tests/functional run --concurrency=1 {posargs} python novaclient/tests/functional/hooks/check_resources.py [testenv:cover] basepython = python3 +setenv = + PYTHON=coverage run --source novaclient --parallel-mode commands = - python setup.py testr --coverage --testr-args='{posargs}' - coverage report + stestr run {posargs} + coverage combine + coverage html -d cover + coverage xml -o cover/coverage.xml + coverage report [flake8] # Following checks should be enabled in the future. From 6c398058a6234e3a59822bbcd9ca5d3d05107e96 Mon Sep 17 00:00:00 2001 From: Yikun Jiang Date: Wed, 27 Jun 2018 11:52:04 +0800 Subject: [PATCH 1457/1705] Microversion 2.64 - Use new format policy in server group Added support for microversion 2.64, which includes the following changes: - The ``--rule`` option is added to the ``nova server-group-create`` CLI that enables user to create server group with specific policy rules. - Remove ``metadata`` column in the output of ``nova server-group-create``, ``nova server-group-get``, ``nova server-group-list``. - Remove ``policies`` column, , add ``policy`` and ``rules`` columns in the output of ``nova server-group-create``, ``nova server-group-get``, ``nova server-group-list``. Depends-On: 3cd26f1e68b09ba7925e794ac8912566c239b6df blueprint: complex-anti-affinity-policies Change-Id: I903f4b5544806b9d3c8bac529448abbc9dd3cee9 --- doc/source/cli/nova.rst | 12 +++- novaclient/__init__.py | 2 +- .../tests/functional/v2/test_server_groups.py | 69 ++++++++++++++++++- novaclient/tests/unit/v2/fakes.py | 9 ++- .../tests/unit/v2/test_server_groups.py | 34 +++++++++ novaclient/tests/unit/v2/test_shell.py | 45 ++++++++++++ novaclient/v2/server_groups.py | 30 ++++++++ novaclient/v2/shell.py | 42 ++++++++--- ...roversion-v2_64-66366829ec65bea4.yaml.yaml | 15 ++++ 9 files changed, 243 insertions(+), 15 deletions(-) create mode 100644 releasenotes/notes/microversion-v2_64-66366829ec65bea4.yaml.yaml diff --git a/doc/source/cli/nova.rst b/doc/source/cli/nova.rst index cd6d60eeb..34b9e674c 100644 --- a/doc/source/cli/nova.rst +++ b/doc/source/cli/nova.rst @@ -2951,7 +2951,7 @@ nova server-group-create .. code-block:: console - usage: nova server-group-create + usage: nova server-group-create [--rules ] Create a new server group with the specified details. @@ -2961,7 +2961,15 @@ Create a new server group with the specified details. Server group name. ```` - Policies for the server groups. + Policy for the server groups. + +``--rule`` + Policy rules for the server groups. (Supported by API versions + '2.64' - '2.latest'). Currently, only the ``max_server_per_host`` rule + is supported for the ``anti-affinity`` policy. The ``max_server_per_host`` + rule allows specifying how many members of the anti-affinity group can + reside on the same compute host. If not specified, only one member from + the same anti-affinity group can reside on a given host. .. _nova_server-group-delete: diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 3bcad2062..1923e6a12 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.63") +API_MAX_VERSION = api_versions.APIVersion("2.64") diff --git a/novaclient/tests/functional/v2/test_server_groups.py b/novaclient/tests/functional/v2/test_server_groups.py index b2ad1262e..37ebaa66d 100644 --- a/novaclient/tests/functional/v2/test_server_groups.py +++ b/novaclient/tests/functional/v2/test_server_groups.py @@ -17,7 +17,9 @@ class TestServerGroupClientV213(test_server_groups.TestServerGroupClient): """Server groups v2.13 functional tests.""" - COMPUTE_API_VERSION = "2.latest" + COMPUTE_API_VERSION = "2.13" + expected_metadata = True + expected_policy_rules = False def test_create_server_group(self): sg_id = self._create_sg("affinity") @@ -29,6 +31,11 @@ def test_create_server_group(self): self._get_column_value_from_single_row_table( sg, "Project Id") self.assertEqual(sg_id, result) + self._get_column_value_from_single_row_table(sg, "Metadata") + self.assertIn( + 'affinity', + self._get_column_value_from_single_row_table(sg, 'Policies')) + self.assertNotIn('Rules', sg) def test_list_server_groups(self): sg_id = self._create_sg("affinity") @@ -40,6 +47,22 @@ def test_list_server_groups(self): self._get_column_value_from_single_row_table( sg, "Project Id") self.assertEqual(sg_id, result) + if self.expected_metadata: + self._get_column_value_from_single_row_table(sg, "Metadata") + else: + self.assertNotIn(sg, 'Metadata') + if self.expected_policy_rules: + self.assertEqual( + 'affinity', + self._get_column_value_from_single_row_table(sg, "Policy")) + self.assertEqual( + '{}', + self._get_column_value_from_single_row_table(sg, "Rules")) + else: + self.assertIn( + 'affinity', + self._get_column_value_from_single_row_table(sg, 'Policies')) + self.assertNotIn('Rules', sg) def test_get_server_group(self): sg_id = self._create_sg("affinity") @@ -51,3 +74,47 @@ def test_get_server_group(self): self._get_column_value_from_single_row_table( sg, "Project Id") self.assertEqual(sg_id, result) + if self.expected_metadata: + self._get_column_value_from_single_row_table(sg, "Metadata") + else: + self.assertNotIn(sg, 'Metadata') + if self.expected_policy_rules: + self.assertEqual( + 'affinity', + self._get_column_value_from_single_row_table(sg, "Policy")) + self.assertEqual( + '{}', + self._get_column_value_from_single_row_table(sg, "Rules")) + else: + self.assertIn( + 'affinity', + self._get_column_value_from_single_row_table(sg, 'Policies')) + self.assertNotIn('Rules', sg) + + +class TestServerGroupClientV264(TestServerGroupClientV213): + """Server groups v2.64 functional tests.""" + + COMPUTE_API_VERSION = "2.64" + expected_metadata = False + expected_policy_rules = True + + def test_create_server_group(self): + output = self.nova('server-group-create complex-anti-affinity-group ' + 'anti-affinity --rule max_server_per_host=3') + sg_id = self._get_column_value_from_single_row_table(output, "Id") + self.addCleanup(self.nova, 'server-group-delete %s' % sg_id) + sg = self.nova('server-group-get %s' % sg_id) + result = self._get_column_value_from_single_row_table(sg, "Id") + self.assertEqual(sg_id, result) + self._get_column_value_from_single_row_table( + sg, "User Id") + self._get_column_value_from_single_row_table( + sg, "Project Id") + self.assertNotIn('Metadata', sg) + self.assertEqual( + 'anti-affinity', + self._get_column_value_from_single_row_table(sg, "Policy")) + self.assertIn( + 'max_server_per_host', + self._get_column_value_from_single_row_table(sg, "Rules")) diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 1d6f3f326..374648be3 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -2236,8 +2236,13 @@ def get_os_server_groups(self, **kw): return (200, {}, {"server_groups": server_groups}) def _return_server_group(self): - r = {'server_group': - self.get_os_server_groups()[2]['server_groups'][0]} + if self.api_version < api_versions.APIVersion("2.64"): + r = {'server_group': + self.get_os_server_groups()[2]['server_groups'][0]} + else: + r = {"members": [], "id": "2cbd51f4-fafe-4cdb-801b-cf913a6f288b", + 'server_group': {'name': 'ig1', 'policy': 'anti-affinity', + 'rules': {'max_server_per_host': 3}}} return (200, {}, r) def post_os_server_groups(self, body, **kw): diff --git a/novaclient/tests/unit/v2/test_server_groups.py b/novaclient/tests/unit/v2/test_server_groups.py index 9881b20aa..40af1a13e 100644 --- a/novaclient/tests/unit/v2/test_server_groups.py +++ b/novaclient/tests/unit/v2/test_server_groups.py @@ -13,6 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient import api_versions from novaclient import exceptions from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit.fixture_data import server_groups as data @@ -106,3 +107,36 @@ def test_find_no_existing_server_groups_by_name(self): self.cs.server_groups.find, **kwargs) self.assert_called('GET', '/os-server-groups') + + +class ServerGroupsTestV264(ServerGroupsTest): + def setUp(self): + super(ServerGroupsTestV264, self).setUp() + self.cs.api_version = api_versions.APIVersion("2.64") + + def test_create_server_group(self): + name = 'ig1' + policy = 'anti-affinity' + server_group = self.cs.server_groups.create(name, policy) + self.assert_request_id(server_group, fakes.FAKE_REQUEST_ID_LIST) + body = {'server_group': {'name': name, 'policy': policy}} + self.assert_called('POST', '/os-server-groups', body) + self.assertIsInstance(server_group, + server_groups.ServerGroup) + + def test_create_server_group_with_rules(self): + kwargs = {'name': 'ig1', + 'policy': 'anti-affinity', + 'rules': {'max_server_per_host': 3}} + server_group = self.cs.server_groups.create(**kwargs) + self.assert_request_id(server_group, fakes.FAKE_REQUEST_ID_LIST) + body = { + 'server_group': { + 'name': 'ig1', + 'policy': 'anti-affinity', + 'rules': {'max_server_per_host': 3} + } + } + self.assert_called('POST', '/os-server-groups', body) + self.assertIsInstance(server_group, + server_groups.ServerGroup) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index a32d623b8..b748dc6eb 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -3728,6 +3728,48 @@ def test_create_server_group(self): {'server_group': {'name': 'wjsg', 'policies': ['affinity']}}) + def test_create_server_group_v2_64(self): + self.run_command('server-group-create sg1 affinity', + api_version='2.64') + self.assert_called('POST', '/os-server-groups', + {'server_group': { + 'name': 'sg1', + 'policy': 'affinity' + }}) + + def test_create_server_group_with_rules(self): + self.run_command('server-group-create sg1 anti-affinity ' + '--rule max_server_per_host=3', api_version='2.64') + self.assert_called('POST', '/os-server-groups', + {'server_group': { + 'name': 'sg1', + 'policy': 'anti-affinity', + 'rules': {'max_server_per_host': 3} + }}) + + def test_create_server_group_with_multi_rules(self): + self.run_command('server-group-create sg1 anti-affinity ' + '--rule a=b --rule c=d', api_version='2.64') + self.assert_called('POST', '/os-server-groups', + {'server_group': { + 'name': 'sg1', + 'policy': 'anti-affinity', + 'rules': {'a': 'b', 'c': 'd'} + }}) + + def test_create_server_group_with_invalid_value(self): + result = self.assertRaises( + exceptions.CommandError, self.run_command, + 'server-group-create sg1 anti-affinity ' + '--rule max_server_per_host=foo', api_version='2.64') + self.assertIn("Invalid 'max_server_per_host' value: foo", + six.text_type(result)) + + def test_create_server_group_with_rules_pre_264(self): + self.assertRaises(SystemExit, self.run_command, + 'server-group-create sg1 anti-affinity ' + '--rule max_server_per_host=3', api_version='2.63') + def test_create_server_group_with_multiple_policies(self): self.assertRaises(SystemExit, self.run_command, 'server-group-create wjsg affinity anti-affinity') @@ -3758,6 +3800,9 @@ def test_versions(self): 7, # doesn't require any changes in novaclient 9, # doesn't require any changes in novaclient 12, # no longer supported + 13, # 13 adds information ``project_id`` and ``user_id`` to + # ``os-server-groups``, but is not explicitly tested + # via wraps and _SUBSTITUTIONS. 15, # doesn't require any changes in novaclient 16, # doesn't require any changes in novaclient 18, # NOTE(andreykurilin): this microversion requires changes in diff --git a/novaclient/v2/server_groups.py b/novaclient/v2/server_groups.py index 62a5c9a09..e8839ae59 100644 --- a/novaclient/v2/server_groups.py +++ b/novaclient/v2/server_groups.py @@ -17,7 +17,10 @@ Server group interface. """ +from novaclient import api_versions from novaclient import base +from novaclient import exceptions +from novaclient.i18n import _ class ServerGroup(base.Resource): @@ -80,6 +83,7 @@ def delete(self, id): """ return self._delete('/os-server-groups/%s' % id) + @api_versions.wraps("2.0", "2.63") def create(self, name, policies): """Create (allocate) a server group. @@ -92,3 +96,29 @@ def create(self, name, policies): body = {'server_group': {'name': name, 'policies': policies}} return self._create('/os-server-groups', body, 'server_group') + + @api_versions.wraps("2.64") + def create(self, name, policy, rules=None): + """Create (allocate) a server group. + + :param name: The name of the server group. + :param policy: Policy name to associate with the server group. + :param rules: The rules of policy which is a dict, can be applied to + the policy, now only ``max_server_per_host`` for ``anti-affinity`` + policy would be supported (optional). + :rtype: list of :class:`ServerGroup` + """ + body = {'server_group': { + 'name': name, 'policy': policy + }} + if rules: + key = 'max_server_per_host' + try: + if key in rules: + rules[key] = int(rules[key]) + except ValueError: + msg = _("Invalid '%(key)s' value: %(value)s") + raise exceptions.CommandError(msg % { + 'key': key, 'value': rules[key]}) + body['server_group']['rules'] = rules + return self._create('/os-server-groups', body, 'server_group') diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 99cc2f5a3..0b4450bd0 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -4525,16 +4525,15 @@ def do_availability_zone_list(cs, _args): sortby_index=None) -@api_versions.wraps("2.0", "2.12") def _print_server_group_details(cs, server_group): - columns = ['Id', 'Name', 'Policies', 'Members', 'Metadata'] - utils.print_list(server_group, columns) - - -@api_versions.wraps("2.13") -def _print_server_group_details(cs, server_group): # noqa - columns = ['Id', 'Name', 'Project Id', 'User Id', - 'Policies', 'Members', 'Metadata'] + if cs.api_version < api_versions.APIVersion('2.13'): + columns = ['Id', 'Name', 'Policies', 'Members', 'Metadata'] + elif cs.api_version < api_versions.APIVersion('2.64'): + columns = ['Id', 'Name', 'Project Id', 'User Id', + 'Policies', 'Members', 'Metadata'] + else: + columns = ['Id', 'Name', 'Project Id', 'User Id', + 'Policy', 'Rules', 'Members'] utils.print_list(server_group, columns) @@ -4569,6 +4568,7 @@ def do_server_group_list(cs, args): _print_server_group_details(cs, server_groups) +@api_versions.wraps("2.0", "2.63") @utils.arg('name', metavar='', help=_('Server group name.')) @utils.arg( 'policy', @@ -4581,6 +4581,30 @@ def do_server_group_create(cs, args): _print_server_group_details(cs, [server_group]) +@api_versions.wraps("2.64") +@utils.arg('name', metavar='', help=_('Server group name.')) +@utils.arg( + 'policy', + metavar='', + help=_('Policy for the server group.')) +@utils.arg( + '--rule', + metavar="", + dest='rules', + action='append', + default=[], + help=_('A rule for the policy. Currently, only the ' + '``max_server_per_host`` rule is supported for the ' + '``anti-affinity`` policy.')) +def do_server_group_create(cs, args): + """Create a new server group with the specified details.""" + rules = _meta_parsing(args.rules) + server_group = cs.server_groups.create(name=args.name, + policy=args.policy, + rules=rules) + _print_server_group_details(cs, [server_group]) + + @utils.arg( 'id', metavar='', diff --git a/releasenotes/notes/microversion-v2_64-66366829ec65bea4.yaml.yaml b/releasenotes/notes/microversion-v2_64-66366829ec65bea4.yaml.yaml new file mode 100644 index 000000000..0bd5d4a56 --- /dev/null +++ b/releasenotes/notes/microversion-v2_64-66366829ec65bea4.yaml.yaml @@ -0,0 +1,15 @@ +--- +features: + - | + Added support for `microversion 2.64`_, which includes the following + changes: + + * The ``--rule`` options is added to the ``nova server-group-create`` + CLI that enables user to create server group with specific policy rules. + * Remove ``metadata`` column in the output of ``nova server-group-create``, + ``nova server-group-get``, ``nova server-group-list``. + * Remove ``policies`` column, add ``policy`` and ``rules`` columns in + the output of ``nova server-group-create``, ``nova server-group-get``, + ``nova server-group-list``. + + .. _microversion 2.64: https://docs.openstack.org/nova/latest/api_microversion_history.html#id58 From 0b1e1bb7ce10bf989f37deb23dee5ff42e8dec6f Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Wed, 18 Jul 2018 10:27:05 -0400 Subject: [PATCH 1458/1705] Add support for microversion 2.65 There are no functional changes for this, just bumping the supported client-side version and updating docs. Depends-On: https://review.openstack.org/573136/ Part of blueprint abort-live-migration-in-queued-status Change-Id: Ie0777dbe4d82892ec75b353f6b13ee2d2c5db72c --- doc/source/cli/nova.rst | 4 ++++ novaclient/__init__.py | 2 +- novaclient/tests/unit/v2/test_shell.py | 1 + .../notes/microversion-v2_65-3c89c5932f4391cb.yaml | 9 +++++++++ 4 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/microversion-v2_65-3c89c5932f4391cb.yaml diff --git a/doc/source/cli/nova.rst b/doc/source/cli/nova.rst index 34b9e674c..a5a162ffc 100644 --- a/doc/source/cli/nova.rst +++ b/doc/source/cli/nova.rst @@ -2321,6 +2321,10 @@ Abort an on-going live migration. (Supported by API versions '2.24' - '2.latest') [hint: use '--os-compute-api-version' flag to show help message for proper version] +For microversions from 2.24 to 2.64 the migration status must be ``running``; +for microversion 2.65 and greater, the migration status can also be ``queued`` +and ``preparing``. + **Positional arguments:** ```` diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 1923e6a12..e4f84779d 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.64") +API_MAX_VERSION = api_versions.APIVersion("2.65") diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index b748dc6eb..c5492514f 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -3842,6 +3842,7 @@ def test_versions(self): 61, # There are no version-wrapped shell method changes for this. 62, # There are no version-wrapped shell method changes for this. 63, # There are no version-wrapped shell method changes for this. + 65, # There are no version-wrapped shell method changes for this. ]) versions_supported = set(range(0, novaclient.API_MAX_VERSION.ver_minor + 1)) diff --git a/releasenotes/notes/microversion-v2_65-3c89c5932f4391cb.yaml b/releasenotes/notes/microversion-v2_65-3c89c5932f4391cb.yaml new file mode 100644 index 000000000..f581e3cf7 --- /dev/null +++ b/releasenotes/notes/microversion-v2_65-3c89c5932f4391cb.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + Support has been added for the compute API `2.65`_ microversion. This + allows calling ``nova live-migration-abort`` on live migrations that are + in ``queued`` or ``preparing`` status in addition to the already accepted + ``running`` status. + + .. _2.65: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id59 From ba8b87fc979d500521085eacb35946434fca7a8c Mon Sep 17 00:00:00 2001 From: Chen Date: Wed, 6 Jun 2018 21:25:06 +0800 Subject: [PATCH 1459/1705] Fix inconsistency Replace "of"s with "off"s to be consistent with the rest. Change-Id: I4a99e733f9ecdbc080f1158b8c90af888d619cdc --- doc/source/cli/nova.rst | 4 ++-- novaclient/v2/shell.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/source/cli/nova.rst b/doc/source/cli/nova.rst index e53f4c1a6..bb5738c63 100644 --- a/doc/source/cli/nova.rst +++ b/doc/source/cli/nova.rst @@ -218,7 +218,7 @@ nova usage Evacuate all instances from failed host. ``host-evacuate-live`` - Live migrate all instances of the specified + Live migrate all instances off the specified host to other available hosts. ``host-list`` @@ -1672,7 +1672,7 @@ nova host-evacuate-live [--max-servers ] [--force] -Live migrate all instances of the specified host to other available hosts. +Live migrate all instances off the specified host to other available hosts. **Positional arguments:** diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 160418cc5..15beadd83 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -4815,7 +4815,7 @@ def __init__(self, server_uuid, live_migration_accepted, default=False, help=_('live Evacuate host with exact hypervisor hostname match')) def do_host_evacuate_live(cs, args): - """Live migrate all instances of the specified host + """Live migrate all instances off the specified host to other available hosts. """ response = [] From f1005ce76b0b4927569efdb99e15e04e5d402dc3 Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Mon, 23 Jul 2018 13:04:07 +0900 Subject: [PATCH 1460/1705] Fix the help text for server-group-create Replace `` with " in the help text of the server-group-create command for consistency with other help text. TrivialFix Change-Id: Iaeb00b11a723e16a295c6692f6f38e91bc9490f7 --- novaclient/v2/shell.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 4f8b695cf..e6a09a61f 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -4594,8 +4594,8 @@ def do_server_group_create(cs, args): action='append', default=[], help=_('A rule for the policy. Currently, only the ' - '``max_server_per_host`` rule is supported for the ' - '``anti-affinity`` policy.')) + '"max_server_per_host" rule is supported for the ' + '"anti-affinity" policy.')) def do_server_group_create(cs, args): """Create a new server group with the specified details.""" rules = _meta_parsing(args.rules) From 5680fea3980e58a15e3dd24d7353837e57972e2f Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Thu, 26 Jul 2018 22:13:03 +0000 Subject: [PATCH 1461/1705] Update reno for stable/rocky Change-Id: I05c3e246c1ae10110c9a951da2d3b9b66380e38a --- releasenotes/source/index.rst | 1 + releasenotes/source/rocky.rst | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 releasenotes/source/rocky.rst diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index fff709d3d..89e64c43d 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -8,6 +8,7 @@ Contents :maxdepth: 2 unreleased + rocky queens pike ocata diff --git a/releasenotes/source/rocky.rst b/releasenotes/source/rocky.rst new file mode 100644 index 000000000..40dd517b7 --- /dev/null +++ b/releasenotes/source/rocky.rst @@ -0,0 +1,6 @@ +=================================== + Rocky Series Release Notes +=================================== + +.. release-notes:: + :branch: stable/rocky From 1f75c7662d6759354210a63ab7cdc06ba4237a2d Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Tue, 31 Jul 2018 11:59:54 +0900 Subject: [PATCH 1462/1705] Use jsonutils of oslo.serialization Both standard json library and jsonutils of oslo.serialization are used. Replace standard json library with jsonutils for consistency. Change-Id: Id6cbb4d78817ff4993b73538935cc4cc61b64a72 --- .../tests/functional/v2/legacy/test_extended_attributes.py | 4 ++-- novaclient/tests/functional/v2/test_extended_attributes.py | 4 ++-- novaclient/utils.py | 3 +-- novaclient/v2/assisted_volume_snapshots.py | 5 +++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/novaclient/tests/functional/v2/legacy/test_extended_attributes.py b/novaclient/tests/functional/v2/legacy/test_extended_attributes.py index 06f4008ee..6affb742f 100644 --- a/novaclient/tests/functional/v2/legacy/test_extended_attributes.py +++ b/novaclient/tests/functional/v2/legacy/test_extended_attributes.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import json +from oslo_serialization import jsonutils from novaclient.tests.functional import base @@ -50,4 +50,4 @@ def test_extended_server_attributes(self): volume_attr = self._get_value_from_the_table( table, 'os-extended-volumes:volumes_attached') # Check that 'id' exists as a key of volume_attr dict - self.assertIn('id', json.loads(volume_attr)[0]) + self.assertIn('id', jsonutils.loads(volume_attr)[0]) diff --git a/novaclient/tests/functional/v2/test_extended_attributes.py b/novaclient/tests/functional/v2/test_extended_attributes.py index 6fddcc957..b585a34b1 100644 --- a/novaclient/tests/functional/v2/test_extended_attributes.py +++ b/novaclient/tests/functional/v2/test_extended_attributes.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import json +from oslo_serialization import jsonutils from novaclient.tests.functional.v2.legacy import test_extended_attributes @@ -41,4 +41,4 @@ def test_extended_server_attributes(self): table, 'os-extended-volumes:volumes_attached') # Check that 'delete_on_termination' exists as a key # of volume_attr dict - self.assertIn('delete_on_termination', json.loads(volume_attr)[0]) + self.assertIn('delete_on_termination', jsonutils.loads(volume_attr)[0]) diff --git a/novaclient/utils.py b/novaclient/utils.py index 8869f696b..6f3cf6408 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -12,7 +12,6 @@ # under the License. import contextlib -import json import os import re import textwrap @@ -199,7 +198,7 @@ def flatten_dict(data): for key, value in data.items(): if isinstance(value, six.string_types): try: - data[key] = json.loads(value) + data[key] = jsonutils.loads(value) except ValueError: pass diff --git a/novaclient/v2/assisted_volume_snapshots.py b/novaclient/v2/assisted_volume_snapshots.py index 79807897d..9e0675a93 100644 --- a/novaclient/v2/assisted_volume_snapshots.py +++ b/novaclient/v2/assisted_volume_snapshots.py @@ -16,7 +16,7 @@ Assisted volume snapshots - to be used by Cinder and not end users. """ -import json +from oslo_serialization import jsonutils from novaclient import base @@ -51,4 +51,5 @@ def delete(self, snapshot, delete_info): :returns: An instance of novaclient.base.TupleWithMeta """ return self._delete("/os-assisted-volume-snapshots/%s?delete_info=%s" % - (base.getid(snapshot), json.dumps(delete_info))) + (base.getid(snapshot), + jsonutils.dumps(delete_info))) From 045f641cec19c6d2826ef20e3ed1409b1991836f Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Mon, 6 Aug 2018 01:20:38 +0900 Subject: [PATCH 1463/1705] Refactor the getid method in novaclient/base.py TrivialFix Change-Id: I30aa4cea658bd3b96f9e7e3d7037232fed8d3749 --- novaclient/base.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/novaclient/base.py b/novaclient/base.py index 3bf61856b..40e100a9d 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -41,10 +41,7 @@ def getid(obj): Abstracts the common pattern of allowing both an object or an object's ID as a parameter when dealing with relationships. """ - try: - return obj.id - except AttributeError: - return obj + return getattr(obj, 'id', obj) # TODO(aababilov): call run_hooks() in HookableMixin's child classes From 33e89f99a43ca0ef4273d1896e849b7980f4769b Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Mon, 6 Aug 2018 00:16:29 +0900 Subject: [PATCH 1464/1705] Fix server strings in reboot operation The following message is shown currently in the reboot operation. Request to reboot server has been accepted. The server name string is a bit odd. So fix it as follows. Request to reboot server server1 (ff79e91e-e2a7-4e0f-b4c3-7157676d43c9) has been accepted. Change-Id: I62df4589dc950f69fdc23eafcbb5792e897cb635 Closes-Bug: #1785495 --- novaclient/tests/unit/test_utils.py | 25 +++++++++++++++++++++++++ novaclient/utils.py | 14 +++++++++++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/novaclient/tests/unit/test_utils.py b/novaclient/tests/unit/test_utils.py index 46ffbc13a..5927e65a1 100644 --- a/novaclient/tests/unit/test_utils.py +++ b/novaclient/tests/unit/test_utils.py @@ -23,6 +23,7 @@ from novaclient.tests.unit import fakes from novaclient.tests.unit import utils as test_utils from novaclient import utils +from novaclient.v2 import servers UUID = '8e8ec658-c7b0-4243-bdf8-6f7f2952c0d0' @@ -402,6 +403,30 @@ def test_do_action_on_many_first_fails(self): def test_do_action_on_many_last_fails(self): self._test_do_action_on_many([None, Exception()], fail=True) + @mock.patch('sys.stdout', new_callable=six.StringIO) + def _test_do_action_on_many_resource_string( + self, resource, expected_string, mock_stdout): + utils.do_action_on_many(mock.Mock(), [resource], 'success with %s', + 'error') + self.assertIn('success with %s' % expected_string, + mock_stdout.getvalue()) + + def test_do_action_on_many_resource_string_with_str(self): + self._test_do_action_on_many_resource_string('resource1', 'resource1') + + def test_do_action_on_many_resource_string_with_human_id(self): + resource = servers.Server(None, {'name': 'resource1'}) + self._test_do_action_on_many_resource_string(resource, 'resource1') + + def test_do_action_on_many_resource_string_with_id(self): + resource = servers.Server(None, {'id': UUID}) + self._test_do_action_on_many_resource_string(resource, UUID) + + def test_do_action_on_many_resource_string_with_id_and_human_id(self): + resource = servers.Server(None, {'name': 'resource1', 'id': UUID}) + self._test_do_action_on_many_resource_string(resource, + 'resource1 (%s)' % UUID) + class RecordTimeTestCase(test_utils.TestCase): diff --git a/novaclient/utils.py b/novaclient/utils.py index 6f3cf6408..6de659977 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -361,6 +361,18 @@ def safe_issubclass(*args): return False +def _get_resource_string(resource): + if hasattr(resource, 'human_id') and resource.human_id: + if hasattr(resource, 'id') and resource.id: + return "%s (%s)" % (resource.human_id, resource.id) + else: + return resource.human_id + elif hasattr(resource, 'id') and resource.id: + return resource.id + else: + return resource + + def do_action_on_many(action, resources, success_msg, error_msg): """Helper to run an action on many resources.""" failure_flag = False @@ -368,7 +380,7 @@ def do_action_on_many(action, resources, success_msg, error_msg): for resource in resources: try: action(resource) - print(success_msg % resource) + print(success_msg % _get_resource_string(resource)) except Exception as e: failure_flag = True print(encodeutils.safe_encode(six.text_type(e))) From 46ad782fb09d2fd325d041bfff57a030142a13ef Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Wed, 8 Aug 2018 15:00:34 +0900 Subject: [PATCH 1465/1705] Use uuidutils of oslo.utils Replace standard uuid library with uuidutils of oslo.utils. TrivialFix Change-Id: Ibb6ec8b56a404685d9727f1b771dbff178fdecdf --- novaclient/tests/unit/test_client.py | 4 ++-- novaclient/utils.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/novaclient/tests/unit/test_client.py b/novaclient/tests/unit/test_client.py index f5f677608..97cfbf31f 100644 --- a/novaclient/tests/unit/test_client.py +++ b/novaclient/tests/unit/test_client.py @@ -14,10 +14,10 @@ # under the License. import copy -import uuid from keystoneauth1 import session import mock +from oslo_utils import uuidutils import novaclient.api_versions import novaclient.client @@ -72,7 +72,7 @@ def test_client_get_reset_timings_v2(self): self.assertEqual(0, len(cs.get_timings())) def test_global_id(self): - global_id = "req-%s" % uuid.uuid4() + global_id = "req-%s" % uuidutils.generate_uuid() self.requests_mock.get('http://no.where') client = novaclient.client.SessionClient(session=session.Session(), diff --git a/novaclient/utils.py b/novaclient/utils.py index 6f3cf6408..11476c96f 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -16,10 +16,10 @@ import re import textwrap import time -import uuid from oslo_serialization import jsonutils from oslo_utils import encodeutils +from oslo_utils import uuidutils import prettytable import six from six.moves.urllib import parse @@ -255,9 +255,9 @@ def find_resource(manager, name_or_id, wrap_exception=True, **find_args): if six.PY3: tmp_id = tmp_id.decode() - uuid.UUID(tmp_id) - return manager.get(tmp_id) - except (TypeError, ValueError, exceptions.NotFound): + if uuidutils.is_uuid_like(tmp_id): + return manager.get(tmp_id) + except (TypeError, exceptions.NotFound): pass # then try to get entity as name From 2d023bcb0eaf258e7d856e14729eca1255bf7e79 Mon Sep 17 00:00:00 2001 From: liuyamin Date: Thu, 9 Aug 2018 15:07:36 +0800 Subject: [PATCH 1466/1705] Replace os-client-config to openstacksdk Since now os-client-config has been superceded by openstacksdk[1]. So need to replace the os-client-config. [1]:https://docs.openstack.org/os-client-config/latest/ Change-Id: Ia0e0671720de4713098e9d0faa1d7dc0c4ae6147 --- novaclient/tests/functional/base.py | 11 ++++++----- test-requirements.txt | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/novaclient/tests/functional/base.py b/novaclient/tests/functional/base.py index 943c44f4e..ce50db94a 100644 --- a/novaclient/tests/functional/base.py +++ b/novaclient/tests/functional/base.py @@ -22,7 +22,8 @@ from keystoneclient import client as keystoneclient from keystoneclient import discover as keystone_discover from neutronclient.v2_0 import client as neutronclient -import os_client_config +import openstack.config +import openstack.config.exceptions from oslo_utils import uuidutils import tempest.lib.cli.base import testtools @@ -173,18 +174,18 @@ def setUp(self): # tempest-lib, we do it in a way that's not available for top # level tests. Long term this probably needs to be in the base # class. - openstack_config = os_client_config.config.OpenStackConfig() + openstack_config = openstack.config.OpenStackConfig() try: cloud_config = openstack_config.get_one_cloud('functional_admin') - except os_client_config.exceptions.OpenStackConfigException: + except openstack.config.exceptions.OpenStackConfigException: try: cloud_config = openstack_config.get_one_cloud( 'devstack', auth=dict( username='admin', project_name='admin')) - except os_client_config.exceptions.OpenStackConfigException: + except openstack.config.exceptions.OpenStackConfigException: try: cloud_config = openstack_config.get_one_cloud('envvars') - except os_client_config.exceptions.OpenStackConfigException: + except openstack.config.exceptions.OpenStackConfigException: cloud_config = None if cloud_config is None: diff --git a/test-requirements.txt b/test-requirements.txt index 3c35fec9e..b01fb80af 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -14,7 +14,7 @@ python-cinderclient>=3.3.0 # Apache-2.0 python-glanceclient>=2.8.0 # Apache-2.0 python-neutronclient>=6.7.0 # Apache-2.0 requests-mock>=1.2.0 # Apache-2.0 -os-client-config>=1.28.0 # Apache-2.0 +openstacksdk>=0.11.2 # Apache-2.0 osprofiler>=1.4.0 # Apache-2.0 stestr>=2.0.0 # Apache-2.0 testscenarios>=0.4 # Apache-2.0/BSD From 05e287fa9f131d51e19e09ac3f3bfde20a9dc0d4 Mon Sep 17 00:00:00 2001 From: openstack Date: Wed, 25 Jul 2018 17:38:57 +0530 Subject: [PATCH 1467/1705] Enable split logging for cinder-novaclient interaction This patch ensures that log messages[1][2][3] are logged only when keystoneauth=DEBUG is enabled in cinder.conf. This will also enable us to consume split_logger config option [4] when enabled in cinder without making any additional changes in client code. [1] REQ: https://review.openstack.org/#/c/505764/8/keystoneauth1/session.py@391 [2] RESP: https://review.openstack.org/#/c/505764/8/keystoneauth1/session.py@422 [3] RESP BODY: https://review.openstack.org/#/c/505764/8/keystoneauth1/session.py@454 [4] https://review.openstack.org/#/c/568878/ Closes-Bug: #1782134 Change-Id:I3c47b5249141b3f05b2f54984bb5b1d7801a02bc --- lower-constraints.txt | 2 +- novaclient/v2/client.py | 2 +- requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lower-constraints.txt b/lower-constraints.txt index 4f156569f..5d9d5197d 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -34,7 +34,7 @@ jsonpatch==1.16 jsonpointer==1.13 jsonschema==2.6.0 keyring==5.5.1 -keystoneauth1==3.4.0 +keystoneauth1==3.5.0 kombu==4.0.0 linecache2==1.0.0 MarkupSafe==1.0 diff --git a/novaclient/v2/client.py b/novaclient/v2/client.py index bc66575f4..ee4fc3453 100644 --- a/novaclient/v2/client.py +++ b/novaclient/v2/client.py @@ -206,7 +206,7 @@ def __init__(self, endpoint_type=endpoint_type, http_log_debug=http_log_debug, insecure=insecure, - logger=self.logger, + logger=logger, os_cache=self.os_cache, password=password, project_domain_id=project_domain_id, diff --git a/requirements.txt b/requirements.txt index 9ac07a62b..9371e3274 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. pbr!=2.1.0,>=2.0.0 # Apache-2.0 -keystoneauth1>=3.4.0 # Apache-2.0 +keystoneauth1>=3.5.0 # Apache-2.0 iso8601>=0.1.11 # MIT oslo.i18n>=3.15.3 # Apache-2.0 oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0 From 5b9c4af775531d1e1b31b592be7dedca31c35e7a Mon Sep 17 00:00:00 2001 From: Doug Hellmann Date: Sun, 9 Sep 2018 05:55:56 -0400 Subject: [PATCH 1468/1705] import zuul job settings from project-config This is a mechanically generated patch to complete step 1 of moving the zuul job settings out of project-config and into each project repository. Because there will be a separate patch on each branch, the branch specifiers for branch-specific jobs have been removed. Because this patch is generated by a script, there may be some cosmetic changes to the layout of the YAML file(s) as the contents are normalized. See the python3-first goal document for details: https://governance.openstack.org/tc/goals/stein/python3-first.html Change-Id: I84676a89b612b7ac4e925534ba81203048fd2866 Story: #2002586 Task: #24315 --- .zuul.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.zuul.yaml b/.zuul.yaml index f8ae1458d..ebb95abd7 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -10,6 +10,13 @@ - openstack/python-novaclient - project: + templates: + - openstack-python-jobs + - openstack-python35-jobs + - publish-openstack-sphinx-docs + - check-requirements + - lib-forward-testing + - release-notes-jobs check: jobs: - novaclient-dsvm-functional @@ -18,3 +25,6 @@ jobs: - novaclient-dsvm-functional - openstack-tox-lower-constraints + post: + jobs: + - openstack-tox-cover From 1b6ae4b11e3bb0525dcebeb4c3d63ada753b74bf Mon Sep 17 00:00:00 2001 From: Doug Hellmann Date: Sun, 9 Sep 2018 05:55:59 -0400 Subject: [PATCH 1469/1705] switch documentation job to new PTI This is a mechanically generated patch to switch the documentation jobs to use the new PTI versions of the jobs as part of the python3-first goal. See the python3-first goal document for details: https://governance.openstack.org/tc/goals/stein/python3-first.html Change-Id: I7fa9df3d37285ee2427a4275b738b7c7e69a9177 Story: #2002586 Task: #24315 --- .zuul.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index ebb95abd7..6c7143c5d 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -13,10 +13,10 @@ templates: - openstack-python-jobs - openstack-python35-jobs - - publish-openstack-sphinx-docs + - publish-openstack-docs-pti - check-requirements - lib-forward-testing - - release-notes-jobs + - release-notes-jobs-python3 check: jobs: - novaclient-dsvm-functional From d46a8e6501a1f1a9e9ec23334653a534ae998840 Mon Sep 17 00:00:00 2001 From: Doug Hellmann Date: Sun, 9 Sep 2018 05:56:04 -0400 Subject: [PATCH 1470/1705] add python 3.6 unit test job This is a mechanically generated patch to add a unit test job running under Python 3.6 as part of the python3-first goal. See the python3-first goal document for details: https://governance.openstack.org/tc/goals/stein/python3-first.html Change-Id: Ib497e0a8cec7656925441c252f0eda96ad32b9b9 Story: #2002586 Task: #24315 --- .zuul.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.zuul.yaml b/.zuul.yaml index 6c7143c5d..22889e782 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -13,6 +13,7 @@ templates: - openstack-python-jobs - openstack-python35-jobs + - openstack-python36-jobs - publish-openstack-docs-pti - check-requirements - lib-forward-testing From 82474d0213260b4f1190a833672d37ed9ae6e100 Mon Sep 17 00:00:00 2001 From: Doug Hellmann Date: Sun, 9 Sep 2018 05:56:07 -0400 Subject: [PATCH 1471/1705] add lib-forward-testing-python3 test job This is a mechanically generated patch to add a functional test job running under Python 3 as part of the python3-first goal. See the python3-first goal document for details: https://governance.openstack.org/tc/goals/stein/python3-first.html Change-Id: I58ae5f91517700745073cc06ead073ba13ac923d Story: #2002586 Task: #24315 --- .zuul.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.zuul.yaml b/.zuul.yaml index 22889e782..787e2ecda 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -17,6 +17,7 @@ - publish-openstack-docs-pti - check-requirements - lib-forward-testing + - lib-forward-testing-python3 - release-notes-jobs-python3 check: jobs: From b5a4a47977700cb9780c94629b48645612acadb1 Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Tue, 11 Sep 2018 03:07:23 +0900 Subject: [PATCH 1472/1705] Add missing options in CLI reference Add the following missing options in the CLI reference. * nova instance-action-list - marker - limit - changes-since * nova migration-list - marker - limit - changes-since Change-Id: Ib4ce831a5031459d350fa73601e878e7b319640f Closes-Bug: #1791125 --- doc/source/cli/nova.rst | 40 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/doc/source/cli/nova.rst b/doc/source/cli/nova.rst index 41a5b232c..af9660aab 100644 --- a/doc/source/cli/nova.rst +++ b/doc/source/cli/nova.rst @@ -1921,7 +1921,9 @@ nova instance-action-list .. code-block:: console - usage: nova instance-action-list + usage: nova instance-action-list [--marker ] [--limit ] + [--changes-since ] + List actions on a server. @@ -1932,6 +1934,23 @@ List actions on a server. used to list actions on a deleted server. (Supported by API versions '2.21' - '2.latest') +**Optional arguments:** + +``--marker `` + The last instance action of the previous page; displays list of actions + after "marker". (Supported by API versions '2.58' - '2.latest') + +``--limit `` + Maximum number of instance actions to display. Note that there is + a configurable max limit on the server, and the limit that is used will be + the minimum between what is requested here and what is configured + in the server. (Supported by API versions '2.58' - '2.latest') + +``--changes-since `` + List only instance actions changed after a certain point of time. + The provided time should be an ISO 8061 formatted time. + ex 2016-03-04T06:27:59Z. (Supported by API versions '2.58' - '2.latest') + .. _nova_interface-attach: nova interface-attach @@ -2422,7 +2441,8 @@ nova migration-list .. code-block:: console usage: nova migration-list [--instance-uuid ] [--host ] - [--status ] + [--status ] [--marker ] + [--limit ] [--changes-since ] Print a list of migrations. @@ -2437,6 +2457,22 @@ Print a list of migrations. ``--status `` Fetch migrations for the given status. +``--marker `` + The last migration of the previous page; displays list of migrations after + "marker". Note that the marker is the migration UUID. + (Supported by API versions '2.59' - '2.latest') + +``--limit `` + Maximum number of migrations to display. Note that there is a configurable + max limit on the server, and the limit that is used will be the minimum + between what is requested here and what is configured in the server. + (Supported by API versions '2.59' - '2.latest') + +``--changes-since `` + List only migrations changed after a certain point of time. + The provided time should be an ISO 8061 formatted time. + ex 2016-03-04T06:27:59Z . (Supported by API versions '2.59' - '2.latest') + .. _nova_pause: nova pause From 8e1849eff14331e102da92787c586cdcea910be8 Mon Sep 17 00:00:00 2001 From: Andreas Jaeger Date: Mon, 10 Sep 2018 22:24:43 +0200 Subject: [PATCH 1473/1705] Cleanup zuul.yaml Couple of cleanups: * Use openstack-lower-constraints-template, remove jobs that are part of templates. * Use openstack-tox-cover template, this runs the cover job in the check queue only. Remove post job. * Sort list of templates. Change-Id: I0f186ba675ecb5802ef6a84ac05727a8621d3768 --- .zuul.yaml | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index 787e2ecda..67f4db476 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -11,22 +11,19 @@ - project: templates: + - check-requirements + - lib-forward-testing + - lib-forward-testing-python3 + - openstack-cover-jobs + - openstack-lower-constraints-jobs - openstack-python-jobs - openstack-python35-jobs - openstack-python36-jobs - publish-openstack-docs-pti - - check-requirements - - lib-forward-testing - - lib-forward-testing-python3 - release-notes-jobs-python3 check: jobs: - novaclient-dsvm-functional - - openstack-tox-lower-constraints gate: jobs: - novaclient-dsvm-functional - - openstack-tox-lower-constraints - post: - jobs: - - openstack-tox-cover From 72b4441fe1413396db2e0e11113033d9e562e934 Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Wed, 12 Sep 2018 00:25:31 +0900 Subject: [PATCH 1474/1705] Improve the description of optional arguments This patch is a follow-up patch for Ib4ce831a5031459d350fa73601e878e7b319640f. Improve the description of optional arguments in the CLI reference and help text. Change-Id: I6313b8a584385daa7e9fbd558d431faa43015449 --- doc/source/cli/nova.rst | 10 +++++----- novaclient/v2/shell.py | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/doc/source/cli/nova.rst b/doc/source/cli/nova.rst index af9660aab..35799551d 100644 --- a/doc/source/cli/nova.rst +++ b/doc/source/cli/nova.rst @@ -1943,13 +1943,13 @@ List actions on a server. ``--limit `` Maximum number of instance actions to display. Note that there is a configurable max limit on the server, and the limit that is used will be - the minimum between what is requested here and what is configured + the minimum of what is requested here and what is configured in the server. (Supported by API versions '2.58' - '2.latest') ``--changes-since `` List only instance actions changed after a certain point of time. The provided time should be an ISO 8061 formatted time. - ex 2016-03-04T06:27:59Z. (Supported by API versions '2.58' - '2.latest') + e.g. 2016-03-04T06:27:59Z. (Supported by API versions '2.58' - '2.latest') .. _nova_interface-attach: @@ -2464,14 +2464,14 @@ Print a list of migrations. ``--limit `` Maximum number of migrations to display. Note that there is a configurable - max limit on the server, and the limit that is used will be the minimum - between what is requested here and what is configured in the server. + max limit on the server, and the limit that is used will be the minimum of + what is requested here and what is configured in the server. (Supported by API versions '2.59' - '2.latest') ``--changes-since `` List only migrations changed after a certain point of time. The provided time should be an ISO 8061 formatted time. - ex 2016-03-04T06:27:59Z . (Supported by API versions '2.59' - '2.latest') + e.g. 2016-03-04T06:27:59Z . (Supported by API versions '2.59' - '2.latest') .. _nova_pause: diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index e6a09a61f..0bf6da8ee 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -5044,7 +5044,7 @@ def do_instance_action_list(cs, args): default=None, help=_('Maximum number of instance actions to display. Note that there ' 'is a configurable max limit on the server, and the limit that is ' - 'used will be the minimum between what is requested here and what ' + 'used will be the minimum of what is requested here and what ' 'is configured in the server.')) @utils.arg( '--changes-since', @@ -5053,7 +5053,7 @@ def do_instance_action_list(cs, args): default=None, help=_('List only instance actions changed after a certain point of ' 'time. The provided time should be an ISO 8061 formatted time. ' - 'ex 2016-03-04T06:27:59Z.')) + 'e.g. 2016-03-04T06:27:59Z.')) def do_instance_action_list(cs, args): """List actions on a server.""" server = _find_server(cs, args.server, raise_if_notfound=False) @@ -5197,7 +5197,7 @@ def do_migration_list(cs, args): default=None, help=_('Maximum number of migrations to display. Note that there is a ' 'configurable max limit on the server, and the limit that is used ' - 'will be the minimum between what is requested here and what ' + 'will be the minimum of what is requested here and what ' 'is configured in the server.')) @utils.arg( '--changes-since', @@ -5206,7 +5206,7 @@ def do_migration_list(cs, args): default=None, help=_('List only migrations changed after a certain point of time. ' 'The provided time should be an ISO 8061 formatted time. ' - 'ex 2016-03-04T06:27:59Z .')) + 'e.g. 2016-03-04T06:27:59Z .')) def do_migration_list(cs, args): """Print a list of migrations.""" if args.changes_since: From 9d8eda8b824d2cd106e4bb75dae6dcd4399ca343 Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Thu, 13 Sep 2018 04:38:58 +0900 Subject: [PATCH 1475/1705] Follow the new PTI for document build - Follow new PTI for docs build - Add sphinxcontrib.apidoc to replace pbr autodoc REF: https://governance.openstack.org/tc/reference/project-testing-interface.html http://lists.openstack.org/pipermail/openstack-dev/2017-December/125710.html http://lists.openstack.org/pipermail/openstack-dev/2018-March/128594.html Change-Id: Ic66fe4d5488c7777439a416fbf86d37af807804d Closes-Bug: #1792115 --- doc/requirements.txt | 1 + doc/source/conf.py | 11 ++++++++++- doc/source/reference/api/index.rst | 4 ++-- setup.cfg | 12 ------------ tox.ini | 2 +- 5 files changed, 14 insertions(+), 16 deletions(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index 012efb226..bfe78dea5 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -4,3 +4,4 @@ sphinx!=1.6.6,!=1.6.7,>=1.6.2 # BSD openstackdocstheme>=1.18.1 # Apache-2.0 reno>=2.5.0 # Apache-2.0 +sphinxcontrib-apidoc>=0.2.0 # BSD diff --git a/doc/source/conf.py b/doc/source/conf.py index c252baab1..42d2176e5 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -17,10 +17,19 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ - 'sphinx.ext.autodoc', 'openstackdocstheme', + 'sphinx.ext.autodoc', + 'sphinxcontrib.apidoc', ] +# sphinxcontrib.apidoc options +apidoc_module_dir = '../../novaclient' +apidoc_output_dir = 'reference/api' +apidoc_excluded_paths = [ + 'tests/*', + 'v2/contrib/*'] +apidoc_separate_modules = True + # The content that will be inserted into the main body of an autoclass # directive. autoclass_content = 'both' diff --git a/doc/source/reference/api/index.rst b/doc/source/reference/api/index.rst index e332c3647..e195abf2e 100644 --- a/doc/source/reference/api/index.rst +++ b/doc/source/reference/api/index.rst @@ -104,6 +104,6 @@ Reference For more information, see the reference: .. toctree:: - :maxdepth: 2 + :maxdepth: 6 - autoindex + modules diff --git a/setup.cfg b/setup.cfg index 7b3f4a201..19e05c17b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -29,21 +29,9 @@ packages = console_scripts = nova = novaclient.shell:main -[build_sphinx] -builders = html,man -all-files = 1 -warning-is-error = 1 -source-dir = doc/source -build-dir = doc/build - [upload_sphinx] upload-dir = doc/build/html -[pbr] -autodoc_index_modules = True -autodoc_exclude_modules = novaclient.tests.* novaclient.v2.contrib.* -api_doc_dir = reference/api - [compile_catalog] domain = novaclient directory = novaclient/locale diff --git a/tox.ini b/tox.ini index b3666b182..a6eaefbde 100644 --- a/tox.ini +++ b/tox.ini @@ -46,7 +46,7 @@ deps = -r{toxinidir}/doc/requirements.txt commands = rm -rf doc/build - sphinx-build -b html doc/source doc/build/html + sphinx-build -W -b html doc/source doc/build/html [testenv:releasenotes] basepython = python3 From fd9670bd41c879ad0dbad5c6aebed429f76e6923 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 24 Sep 2018 15:10:28 +0100 Subject: [PATCH 1476/1705] docs: Add redirects These were missed for some reason. Let's add them now. While we're here, we add the '-d DOCTREE_DIR' argument to our 'sphinx-build' call to handle Sphinx 1.8's change in behavior for where doctrees are output. Change-Id: I5c862a74f92a44bf5248f858e26826eef76c11f0 Signed-off-by: Stephen Finucane --- doc/requirements.txt | 3 +++ doc/source/_extra/.htaccess | 10 ++++++++++ doc/source/conf.py | 4 ++++ doc/test/redirect-tests.txt | 3 +++ tox.ini | 4 +++- 5 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 doc/source/_extra/.htaccess create mode 100644 doc/test/redirect-tests.txt diff --git a/doc/requirements.txt b/doc/requirements.txt index bfe78dea5..27cc8b9f6 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -5,3 +5,6 @@ sphinx!=1.6.6,!=1.6.7,>=1.6.2 # BSD openstackdocstheme>=1.18.1 # Apache-2.0 reno>=2.5.0 # Apache-2.0 sphinxcontrib-apidoc>=0.2.0 # BSD + +# redirect tests in docs +whereto>=0.3.0 # Apache-2.0 diff --git a/doc/source/_extra/.htaccess b/doc/source/_extra/.htaccess new file mode 100644 index 000000000..bdff533e2 --- /dev/null +++ b/doc/source/_extra/.htaccess @@ -0,0 +1,10 @@ +# The following is generated with: +# +# git log --follow --name-status --format='%H' ac25ae6fee.. -- doc/source/ | \ +# grep ^R | grep .rst | cut -f2- | \ +# sed -e 's|doc/source/|redirectmatch 301 ^/python-novaclient/([^/]+)/|' -e 's|doc/source/|/python-novaclient/$1/|' -e 's/.rst/.html$/' -e 's/.rst/.html/' | \ +# sort + +redirectmatch 301 ^/python-novaclient/([^/]+)/api.html$ /python-novaclient/$1/reference/api/index.html +redirectmatch 301 ^/python-novaclient/([^/]+)/man/nova.html$ /python-novaclient/$1/cli/nova.html +redirectmatch 301 ^/python-novaclient/([^/]+)/shell.html$ /python-novaclient/$1/user/shell.html diff --git a/doc/source/conf.py b/doc/source/conf.py index 42d2176e5..3d4d898ea 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -70,6 +70,10 @@ # Sphinx are currently 'default' and 'sphinxdoc'. html_theme = 'openstackdocs' +# Add any paths that contain "extra" files, such as .htaccess or +# robots.txt. +html_extra_path = ['_extra'] + # -- Options for openstackdocstheme ------------------------------------------- repository_name = 'openstack/python-novaclient' diff --git a/doc/test/redirect-tests.txt b/doc/test/redirect-tests.txt new file mode 100644 index 000000000..0959ec54f --- /dev/null +++ b/doc/test/redirect-tests.txt @@ -0,0 +1,3 @@ +/python-novaclient/latest/api.html 301 /python-novaclient/latest/reference/api/index.html +/python-novaclient/latest/man/nova.html 301 /python-novaclient/latest/cli/nova.html +/python-novaclient/latest/shell.html 301 /python-novaclient/latest/user/shell.html diff --git a/tox.ini b/tox.ini index a6eaefbde..508966826 100644 --- a/tox.ini +++ b/tox.ini @@ -46,7 +46,9 @@ deps = -r{toxinidir}/doc/requirements.txt commands = rm -rf doc/build - sphinx-build -W -b html doc/source doc/build/html + sphinx-build -W -b html -d doc/build/doctrees doc/source doc/build/html + # Test the redirects. This must run after the main docs build + whereto doc/build/html/.htaccess doc/test/redirect-tests.txt [testenv:releasenotes] basepython = python3 From 4464a88737798d0e527a35371412db803e9220cc Mon Sep 17 00:00:00 2001 From: zhangbailin Date: Wed, 19 Sep 2018 11:08:37 +0800 Subject: [PATCH 1477/1705] Add support changes-before for microversion 2.66 This adds the changes-before filter to the servers, os-instance-actions and os-migrations list for filtering resources which were last updated before or equal to the given time. The changes-before filter, like the changes-since filter, will return deleted server resources. Depends-On: https://review.openstack.org/599276/ Part of bp support-to-query-nova-resources-filter-by-changes-before Change-Id: I7c6ea00303374d605bda8ef1b62c5de1b4567696 --- doc/source/cli/nova.rst | 34 +++- novaclient/__init__.py | 2 +- .../functional/v2/test_instance_action.py | 36 ++++ .../tests/unit/v2/test_instance_actions.py | 20 ++ novaclient/tests/unit/v2/test_migrations.py | 17 ++ novaclient/tests/unit/v2/test_shell.py | 56 ++++++ novaclient/v2/instance_action.py | 42 +++- novaclient/v2/migrations.py | 39 +++- novaclient/v2/shell.py | 187 ++++++++++++++++-- .../microversion-v2_66-cda5d6dc31b56b46.yaml | 14 ++ 10 files changed, 418 insertions(+), 29 deletions(-) create mode 100644 releasenotes/notes/microversion-v2_66-cda5d6dc31b56b46.yaml diff --git a/doc/source/cli/nova.rst b/doc/source/cli/nova.rst index 35799551d..899a0e0bf 100644 --- a/doc/source/cli/nova.rst +++ b/doc/source/cli/nova.rst @@ -1923,6 +1923,7 @@ nova instance-action-list usage: nova instance-action-list [--marker ] [--limit ] [--changes-since ] + [--changes-before ] List actions on a server. @@ -1947,10 +1948,15 @@ List actions on a server. in the server. (Supported by API versions '2.58' - '2.latest') ``--changes-since `` - List only instance actions changed after a certain point of time. - The provided time should be an ISO 8061 formatted time. + List only instance actions changed later or equal to a certain + point of time. The provided time should be an ISO 8061 formatted time. e.g. 2016-03-04T06:27:59Z. (Supported by API versions '2.58' - '2.latest') +``--changes-before `` + List only instance actions changed earlier or equal to a certain + point of time. The provided time should be an ISO 8061 formatted time. + e.g. 2016-03-04T06:27:59Z. (Supported by API versions '2.66' - '2.latest') + .. _nova_interface-attach: nova interface-attach @@ -2154,6 +2160,7 @@ nova list [--user []] [--deleted] [--fields ] [--minimal] [--sort [:]] [--marker ] [--limit ] [--changes-since ] + [--changes-before ] [--tags ] [--tags-any ] [--not-tags ] [--not-tags-any ] @@ -2233,11 +2240,18 @@ List servers. used instead. ``--changes-since `` - List only servers changed after a certain - point of time.The provided time should be an - ISO 8061 formatted time.ex + List only servers changed later or equal to a + certain point of time. The provided time should + be an ISO 8061 formatted time. e.g. 2016-03-04T06:27:59Z . +``--changes-before `` + List only servers changed earlier or equal to a + certain point of time. The provided time should + be an ISO 8061 formatted time. e.g. + 2016-03-05T06:27:59Z . (Supported by API versions + '2.66' - '2.latest') + ``--tags `` The given tags must all be present for a server to be included in the list result. @@ -2443,6 +2457,7 @@ nova migration-list usage: nova migration-list [--instance-uuid ] [--host ] [--status ] [--marker ] [--limit ] [--changes-since ] + [--changes-before ] Print a list of migrations. @@ -2469,10 +2484,15 @@ Print a list of migrations. (Supported by API versions '2.59' - '2.latest') ``--changes-since `` - List only migrations changed after a certain point of time. - The provided time should be an ISO 8061 formatted time. + List only migrations changed later or equal to a certain + point of time. The provided time should be an ISO 8061 formatted time. e.g. 2016-03-04T06:27:59Z . (Supported by API versions '2.59' - '2.latest') +``--changes-before `` + List only migrations changed earlier or equal to a certain + point of time. The provided time should be an ISO 8061 formatted time. + e.g. 2016-03-04T06:27:59Z . (Supported by API versions '2.66' - '2.latest') + .. _nova_pause: nova pause diff --git a/novaclient/__init__.py b/novaclient/__init__.py index e4f84779d..49bd667d7 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.65") +API_MAX_VERSION = api_versions.APIVersion("2.66") diff --git a/novaclient/tests/functional/v2/test_instance_action.py b/novaclient/tests/functional/v2/test_instance_action.py index 9ea15c284..968c12d6b 100644 --- a/novaclient/tests/functional/v2/test_instance_action.py +++ b/novaclient/tests/functional/v2/test_instance_action.py @@ -151,3 +151,39 @@ def test_show_actions_with_host(self): output = self.nova("instance-action %s %s" % (server_id, request_id)) self.assertIn("'host'", output) self.assertIn("'hostId'", output) + + +class TestInstanceActionCLIV266(TestInstanceActionCLIV258, + base.TenantTestBase): + """Instance action functional tests for v2.66 nova-api microversion.""" + + COMPUTE_API_VERSION = "2.66" + expect_event_hostId_field = True + + def test_list_instance_action_with_changes_before(self): + # Ignore microseconds to make this a deterministic test. + server = self._create_server() + end_create = timeutils.utcnow().replace(microsecond=0).isoformat() + time.sleep(2) + server.stop() + end_stop = timeutils.utcnow().replace(microsecond=0).isoformat() + + stop_output = self.nova( + "instance-action-list %s --changes-before %s" % + (server.id, end_stop)) + action = self._get_list_of_values_from_single_column_table( + stop_output, "Action") + # The actions are sorted by created_at in descending order. + self.assertEqual(action, ['create', 'stop']) + + create_output = self.nova( + "instance-action-list %s --changes-before %s" % + (server.id, end_create)) + action = self._get_list_of_values_from_single_column_table( + create_output, "Action") + # Provide detailed debug information if this fails. + self.assertEqual(action, ['create'], + 'Expected to find the create action with ' + '--changes-before=%s but got: %s\n\n' + 'First instance-action-list output: %s' % + (end_create, create_output, stop_output)) diff --git a/novaclient/tests/unit/v2/test_instance_actions.py b/novaclient/tests/unit/v2/test_instance_actions.py index e26bf7a50..e2da9d03a 100644 --- a/novaclient/tests/unit/v2/test_instance_actions.py +++ b/novaclient/tests/unit/v2/test_instance_actions.py @@ -61,3 +61,23 @@ def test_list_instance_actions_with_limit_marker_params(self): 'marker=%s' % (server_uuid, '2016-02-29T06%3A23%3A22', marker)) for ia in ias: self.assertIsInstance(ia, instance_action.InstanceAction) + + +class InstanceActionExtensionV266Tests(InstanceActionExtensionV258Tests): + def setUp(self): + super(InstanceActionExtensionV266Tests, self).setUp() + self.cs.api_version = api_versions.APIVersion("2.66") + + def test_list_instance_actions_with_changes_before(self): + server_uuid = '1234' + + ias = self.cs.instance_action.list( + server_uuid, marker=None, limit=None, changes_since=None, + changes_before='2016-02-29T06:23:22') + self.assert_request_id(ias, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called( + 'GET', + '/servers/%s/os-instance-actions?changes-before=%s' % + (server_uuid, '2016-02-29T06%3A23%3A22')) + for ia in ias: + self.assertIsInstance(ia, instance_action.InstanceAction) diff --git a/novaclient/tests/unit/v2/test_migrations.py b/novaclient/tests/unit/v2/test_migrations.py index 4efa3cb44..fcafa6d29 100644 --- a/novaclient/tests/unit/v2/test_migrations.py +++ b/novaclient/tests/unit/v2/test_migrations.py @@ -95,3 +95,20 @@ def test_list_migrations_with_limit_marker_params(self): % ('2012-02-29T06%3A23%3A22', marker)) for m in ms: self.assertIsInstance(m, migrations.Migration) + + +class MigrationsV266Test(MigrationsV259Test): + def setUp(self): + super(MigrationsV266Test, self).setUp() + self.cs.api_version = api_versions.APIVersion("2.66") + + def test_list_migrations_with_changes_before(self): + params = {'changes_before': '2012-02-29T06:23:22'} + ms = self.cs.migrations.list(**params) + self.assert_request_id(ms, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called('GET', + '/os-migrations?' + 'changes-before=%s' % + '2012-02-29T06%3A23%3A22') + for m in ms: + self.assertIsInstance(m, migrations.Migration) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index c5492514f..7bde8cd3d 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -1597,6 +1597,23 @@ def test_list_with_changes_since_invalid_value(self): self.assertRaises(exceptions.CommandError, self.run_command, 'list --changes-since 0123456789') + def test_list_with_changes_before(self): + self.run_command('list --changes-before 2016-02-29T06:23:22', + api_version='2.66') + self.assert_called( + 'GET', '/servers/detail?changes-before=2016-02-29T06%3A23%3A22') + + def test_list_with_changes_before_invalid_value(self): + ex = self.assertRaises(exceptions.CommandError, self.run_command, + 'list --changes-before 0123456789', + api_version='2.66') + self.assertIn('Invalid changes-before value', six.text_type(ex)) + + def test_list_with_changes_before_pre_v266_not_allowed(self): + self.assertRaises(SystemExit, self.run_command, + 'list --changes-before 2016-02-29T06:23:22', + api_version='2.65') + def test_list_fields_redundant(self): output, _err = self.run_command('list --fields id,status,status') header = output.splitlines()[1] @@ -3487,6 +3504,28 @@ def test_instance_action_list_with_changes_since_invalid_value_v258(self): api_version='2.58') self.assertIn('Invalid changes-since value', six.text_type(ex)) + def test_instance_action_list_changes_before_pre_v266_not_allowed(self): + cmd = 'instance-action-list sample-server --changes-before ' \ + '2016-02-29T06:23:22' + self.assertRaises(SystemExit, self.run_command, + cmd, api_version='2.65') + + def test_instance_action_list_with_changes_before_v266(self): + self.run_command('instance-action-list sample-server ' + '--changes-before 2016-02-29T06:23:22', + api_version='2.66') + self.assert_called( + 'GET', + '/servers/1234/os-instance-actions?' + 'changes-before=2016-02-29T06%3A23%3A22') + + def test_instance_action_list_with_changes_before_invalid_value_v266(self): + ex = self.assertRaises( + exceptions.CommandError, self.run_command, + 'instance-action-list sample-server --changes-before 0123456789', + api_version='2.66') + self.assertIn('Invalid changes-before value', six.text_type(ex)) + def test_instance_usage_audit_log(self): self.run_command('instance-usage-audit-log') self.assert_called('GET', '/os-instance_usage_audit_log') @@ -3564,6 +3603,23 @@ def test_migration_list_with_changes_since_invalid_value_v259(self): api_version='2.59') self.assertIn('Invalid changes-since value', six.text_type(ex)) + def test_migration_list_with_changes_before_v266(self): + self.run_command('migration-list --changes-before 2016-02-29T06:23:22', + api_version='2.66') + self.assert_called( + 'GET', '/os-migrations?changes-before=2016-02-29T06%3A23%3A22') + + def test_migration_list_with_changes_before_invalid_value_v266(self): + ex = self.assertRaises(exceptions.CommandError, self.run_command, + 'migration-list --changes-before 0123456789', + api_version='2.66') + self.assertIn('Invalid changes-before value', six.text_type(ex)) + + def test_migration_list_with_changes_before_pre_v266_not_allowed(self): + cmd = 'migration-list --changes-before 2016-02-29T06:23:22' + self.assertRaises(SystemExit, self.run_command, cmd, + api_version='2.65') + @mock.patch('novaclient.v2.shell._find_server') @mock.patch('os.system') def test_ssh(self, mock_system, mock_find_server): diff --git a/novaclient/v2/instance_action.py b/novaclient/v2/instance_action.py index b8316c2d7..79f1b6173 100644 --- a/novaclient/v2/instance_action.py +++ b/novaclient/v2/instance_action.py @@ -43,7 +43,7 @@ def list(self, server): return self._list('/servers/%s/os-instance-actions' % base.getid(server), 'instanceActions') - @api_versions.wraps("2.58") + @api_versions.wraps("2.58", "2.65") def list(self, server, marker=None, limit=None, changes_since=None): """ Get a list of actions performed on a server. @@ -53,10 +53,10 @@ def list(self, server, marker=None, limit=None, changes_since=None): list than that represented by this action request id (optional). :param limit: Maximum number of actions to return. (optional). - :param changes_since: List only instance actions changed after a - certain point of time. The provided time should - be an ISO 8061 formatted time. ex - 2016-03-04T06:27:59Z . (optional). + :param changes_since: List only instance actions changed later or + equal to a certain point of time. The provided + time should be an ISO 8061 formatted time. + e.g. 2016-03-04T06:27:59Z . (optional). """ opts = {} if marker: @@ -67,3 +67,35 @@ def list(self, server, marker=None, limit=None, changes_since=None): opts['changes-since'] = changes_since return self._list('/servers/%s/os-instance-actions' % base.getid(server), 'instanceActions', filters=opts) + + @api_versions.wraps("2.66") + def list(self, server, marker=None, limit=None, changes_since=None, + changes_before=None): + """ + Get a list of actions performed on a server. + + :param server: The :class:`Server` (or its ID) + :param marker: Begin returning actions that appear later in the action + list than that represented by this action request id + (optional). + :param limit: Maximum number of actions to return. (optional). + :param changes_since: List only instance actions changed later or + equal to a certain point of time. The provided + time should be an ISO 8061 formatted time. + e.g. 2016-03-04T06:27:59Z . (optional). + :param changes_before: List only instance actions changed earlier or + equal to a certain point of time. The provided + time should be an ISO 8061 formatted time. + e.g. 2016-03-05T06:27:59Z . (optional). + """ + opts = {} + if marker: + opts['marker'] = marker + if limit: + opts['limit'] = limit + if changes_since: + opts['changes-since'] = changes_since + if changes_before: + opts['changes-before'] = changes_before + return self._list('/servers/%s/os-instance-actions' % + base.getid(server), 'instanceActions', filters=opts) diff --git a/novaclient/v2/migrations.py b/novaclient/v2/migrations.py index 00eb6d110..f1954f1ab 100644 --- a/novaclient/v2/migrations.py +++ b/novaclient/v2/migrations.py @@ -27,7 +27,8 @@ class MigrationManager(base.ManagerWithFind): resource_class = Migration def _list_base(self, host=None, status=None, instance_uuid=None, - marker=None, limit=None, changes_since=None): + marker=None, limit=None, changes_since=None, + changes_before=None): opts = {} if host: opts['host'] = host @@ -41,6 +42,8 @@ def _list_base(self, host=None, status=None, instance_uuid=None, opts['limit'] = limit if changes_since: opts['changes-since'] = changes_since + if changes_before: + opts['changes-before'] = changes_before return self._list("/os-migrations", "migrations", filters=opts) @@ -55,7 +58,7 @@ def list(self, host=None, status=None, instance_uuid=None): return self._list_base(host=host, status=status, instance_uuid=instance_uuid) - @api_versions.wraps("2.59") + @api_versions.wraps("2.59", "2.65") def list(self, host=None, status=None, instance_uuid=None, marker=None, limit=None, changes_since=None): """ @@ -67,11 +70,37 @@ def list(self, host=None, status=None, instance_uuid=None, migrations list than that represented by this migration UUID (optional). :param limit: maximum number of migrations to return (optional). - :param changes_since: only return migrations updated after. The - provided time should be an ISO 8061 formatted time. ex - 2016-03-04T06:27:59Z . (optional). + :param changes_since: only return migrations changed later or equal + to a certain point of time. The provided time should be an ISO 8061 + formatted time. e.g. 2016-03-04T06:27:59Z . (optional). """ return self._list_base(host=host, status=status, instance_uuid=instance_uuid, marker=marker, limit=limit, changes_since=changes_since) + + @api_versions.wraps("2.66") + def list(self, host=None, status=None, instance_uuid=None, + marker=None, limit=None, changes_since=None, + changes_before=None): + """ + Get a list of migrations. + :param host: filter migrations by host name (optional). + :param status: filter migrations by status (optional). + :param instance_uuid: filter migrations by instance uuid (optional). + :param marker: Begin returning migrations that appear later in the + migrations list than that represented by this migration UUID + (optional). + :param limit: maximum number of migrations to return (optional). + :param changes_since: Only return migrations changed later or equal + to a certain point of time. The provided time should be an ISO 8061 + formatted time. e.g. 2016-03-04T06:27:59Z . (optional). + :param changes_before: Only return migrations changed earlier or + equal to a certain point of time. The provided time should be an ISO + 8061 formatted time. e.g. 2016-03-05T06:27:59Z . (optional). + """ + return self._list_base(host=host, status=status, + instance_uuid=instance_uuid, + marker=marker, limit=limit, + changes_since=changes_since, + changes_before=changes_before) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 0bf6da8ee..7ddc5323e 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -1475,9 +1475,18 @@ def _print_flavor(flavor): dest='changes_since', metavar='', default=None, - help=_("List only servers changed after a certain point of time. " - "The provided time should be an ISO 8061 formatted time. " - "ex 2016-03-04T06:27:59Z .")) + help=_("List only servers changed later or equal to a certain point of " + "time. The provided time should be an ISO 8061 formatted time. " + "e.g. 2016-03-04T06:27:59Z .")) +@utils.arg( + '--changes-before', + dest='changes_before', + metavar='', + default=None, + help=_("List only servers changed earlier or equal to a certain point of " + "time. The provided time should be an ISO 8061 formatted time. " + "e.g. 2016-03-04T06:27:59Z ."), + start_version="2.66") @utils.arg( '--tags', dest='tags', @@ -1582,6 +1591,18 @@ def do_list(cs, args): raise exceptions.CommandError(_('Invalid changes-since value: %s') % search_opts['changes-since']) + # In microversion 2.66 we added ``changes-before`` option + # in server details. + have_added_changes_before = ( + cs.api_version >= api_versions.APIVersion('2.66')) + if have_added_changes_before and args.changes_before: + search_opts['changes-before'] = args.changes_before + try: + timeutils.parse_isotime(search_opts['changes-before']) + except ValueError: + raise exceptions.CommandError(_('Invalid changes-before value: %s') + % search_opts['changes-before']) + servers = cs.servers.list(detailed=detailed, search_opts=search_opts, sort_keys=sort_keys, @@ -1629,7 +1650,7 @@ def do_list(cs, args): # Tenant ID as well if search_opts['all_tenants']: columns.insert(2, 'Tenant ID') - if search_opts['changes-since']: + if search_opts['changes-since'] or search_opts.get('changes-before'): columns.append('Updated') formatters['Networks'] = utils.format_servers_list_networks sortby_index = 1 @@ -5023,7 +5044,7 @@ def do_instance_action_list(cs, args): sortby_index=3) -@api_versions.wraps("2.58") +@api_versions.wraps("2.58", "2.65") @utils.arg( 'server', metavar='', @@ -5051,9 +5072,9 @@ def do_instance_action_list(cs, args): dest='changes_since', metavar='', default=None, - help=_('List only instance actions changed after a certain point of ' - 'time. The provided time should be an ISO 8061 formatted time. ' - 'e.g. 2016-03-04T06:27:59Z.')) + help=_('List only instance actions changed later or equal to a certain ' + 'point of time. The provided time should be an ISO 8061 formatted ' + 'time. e.g. 2016-03-04T06:27:59Z.')) def do_instance_action_list(cs, args): """List actions on a server.""" server = _find_server(cs, args.server, raise_if_notfound=False) @@ -5073,6 +5094,75 @@ def do_instance_action_list(cs, args): sortby_index=3) +@api_versions.wraps("2.66") +@utils.arg( + 'server', + metavar='', + help=_('Name or UUID of the server to list actions for. Only UUID can be ' + 'used to list actions on a deleted server.')) +@utils.arg( + '--marker', + dest='marker', + metavar='', + default=None, + help=_('The last instance action of the previous page; displays list of ' + 'actions after "marker".')) +@utils.arg( + '--limit', + dest='limit', + metavar='', + type=int, + default=None, + help=_('Maximum number of instance actions to display. Note that there ' + 'is a configurable max limit on the server, and the limit that is ' + 'used will be the minimum of what is requested here and what ' + 'is configured in the server.')) +@utils.arg( + '--changes-since', + dest='changes_since', + metavar='', + default=None, + help=_('List only instance actions changed later or equal to a certain ' + 'point of time. The provided time should be an ISO 8061 formatted ' + 'time. e.g. 2016-03-04T06:27:59Z.')) +@utils.arg( + '--changes-before', + dest='changes_before', + metavar='', + default=None, + help=_('List only instance actions changed earlier or equal to a certain ' + 'point of time. The provided time should be an ISO 8061 formatted ' + 'time. e.g. 2016-03-04T06:27:59Z.'), + start_version="2.66") +def do_instance_action_list(cs, args): + """List actions on a server.""" + server = _find_server(cs, args.server, raise_if_notfound=False) + if args.changes_since: + try: + timeutils.parse_isotime(args.changes_since) + except ValueError: + raise exceptions.CommandError(_('Invalid changes-since value: %s') + % args.changes_since) + + # In microversion 2.66 we added ``changes-before`` option + # in instance actions. + if args.changes_before: + try: + timeutils.parse_isotime(args.changes_before) + except ValueError: + raise exceptions.CommandError(_('Invalid changes-before value: %s') + % args.changes_before) + + actions = cs.instance_action.list(server, marker=args.marker, + limit=args.limit, + changes_since=args.changes_since, + changes_before=args.changes_before) + utils.print_list(actions, + ['Action', 'Request_ID', 'Message', 'Start_Time', + 'Updated_At'], + sortby_index=3) + + def do_list_extensions(cs, _args): """ List all the os-api extensions that are available. @@ -5165,7 +5255,7 @@ def do_migration_list(cs, args): _print_migrations(cs, migrations) -@api_versions.wraps("2.59") +@api_versions.wraps("2.59", "2.65") @utils.arg( '--instance-uuid', dest='instance_uuid', @@ -5204,8 +5294,8 @@ def do_migration_list(cs, args): dest='changes_since', metavar='', default=None, - help=_('List only migrations changed after a certain point of time. ' - 'The provided time should be an ISO 8061 formatted time. ' + help=_('List only migrations changed later or equal to a certain point ' + 'of time. The provided time should be an ISO 8061 formatted time. ' 'e.g. 2016-03-04T06:27:59Z .')) def do_migration_list(cs, args): """Print a list of migrations.""" @@ -5224,6 +5314,81 @@ def do_migration_list(cs, args): _print_migrations(cs, migrations) +@api_versions.wraps("2.66") +@utils.arg( + '--instance-uuid', + dest='instance_uuid', + metavar='', + help=_('Fetch migrations for the given instance.')) +@utils.arg( + '--host', + dest='host', + metavar='', + help=_('Fetch migrations for the given host.')) +@utils.arg( + '--status', + dest='status', + metavar='', + help=_('Fetch migrations for the given status.')) +@utils.arg( + '--marker', + dest='marker', + metavar='', + default=None, + help=_('The last migration of the previous page; displays list of ' + 'migrations after "marker". Note that the marker is the ' + 'migration UUID.')) +@utils.arg( + '--limit', + dest='limit', + metavar='', + type=int, + default=None, + help=_('Maximum number of migrations to display. Note that there is a ' + 'configurable max limit on the server, and the limit that is used ' + 'will be the minimum of what is requested here and what ' + 'is configured in the server.')) +@utils.arg( + '--changes-since', + dest='changes_since', + metavar='', + default=None, + help=_('List only migrations changed later or equal to a certain point ' + 'of time. The provided time should be an ISO 8061 formatted time. ' + 'e.g. 2016-03-04T06:27:59Z .')) +@utils.arg( + '--changes-before', + dest='changes_before', + metavar='', + default=None, + help=_('List only migrations changed earlier or equal to a certain point ' + 'of time. The provided time should be an ISO 8061 formatted time. ' + 'e.g. 2016-03-04T06:27:59Z .'), + start_version="2.66") +def do_migration_list(cs, args): + """Print a list of migrations.""" + if args.changes_since: + try: + timeutils.parse_isotime(args.changes_since) + except ValueError: + raise exceptions.CommandError(_('Invalid changes-since value: %s') + % args.changes_since) + + if args.changes_before: + try: + timeutils.parse_isotime(args.changes_before) + except ValueError: + raise exceptions.CommandError(_('Invalid changes-before value: %s') + % args.changes_before) + + migrations = cs.migrations.list(args.host, args.status, + instance_uuid=args.instance_uuid, + marker=args.marker, limit=args.limit, + changes_since=args.changes_since, + changes_before=args.changes_before) + _print_migrations(cs, migrations) + + @utils.arg( '--before', dest='before', diff --git a/releasenotes/notes/microversion-v2_66-cda5d6dc31b56b46.yaml b/releasenotes/notes/microversion-v2_66-cda5d6dc31b56b46.yaml new file mode 100644 index 000000000..69c649521 --- /dev/null +++ b/releasenotes/notes/microversion-v2_66-cda5d6dc31b56b46.yaml @@ -0,0 +1,14 @@ +--- +features: + - | + Added support for `microversion 2.66`_ which adds ``changes-before`` + parameter to the servers, os-instance-actions or os-migrations APIs. + + * This parameter (``changes-before``) does not change any read-deleted + behavior in the os-instance-actions or os-migrations APIs. + * Like the ``changes-since`` filter, the ``changes-before`` filter will + also return deleted servers. + * The ``--changes-before`` options is added to the ``nova list``, + ``nova instance-action-list`` and ``nova migration-list`` CLIs. + + .. _microversion 2.66: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id59 From 98b088286d519b59cb7c2399eb5ac564a47cee78 Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Tue, 2 Oct 2018 09:45:06 +0900 Subject: [PATCH 1478/1705] Fix test_instance_action functional test failure Fix an intermittent failure of test_list_instance_action_with_changes_before method of the TestInstanceActionCLIV266 class. Change-Id: Ie898d9590e1701476eadf4895bce7874d989175a Closes-Bug: #1795392 --- novaclient/tests/functional/v2/test_instance_action.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/novaclient/tests/functional/v2/test_instance_action.py b/novaclient/tests/functional/v2/test_instance_action.py index 968c12d6b..450102103 100644 --- a/novaclient/tests/functional/v2/test_instance_action.py +++ b/novaclient/tests/functional/v2/test_instance_action.py @@ -161,12 +161,11 @@ class TestInstanceActionCLIV266(TestInstanceActionCLIV258, expect_event_hostId_field = True def test_list_instance_action_with_changes_before(self): - # Ignore microseconds to make this a deterministic test. server = self._create_server() - end_create = timeutils.utcnow().replace(microsecond=0).isoformat() - time.sleep(2) + end_create = timeutils.utcnow().isoformat() server.stop() - end_stop = timeutils.utcnow().replace(microsecond=0).isoformat() + self._wait_for_state_change(server.id, 'shutoff') + end_stop = timeutils.utcnow().isoformat() stop_output = self.nova( "instance-action-list %s --changes-before %s" % From cac7351b0ee9716f1be01e8711d6ed3a237362ca Mon Sep 17 00:00:00 2001 From: Sam Morrison Date: Wed, 26 Sep 2018 16:47:53 +1000 Subject: [PATCH 1479/1705] Fix up userdata argument to rebuild. This was using the name of the file as user data as opposed to the content of the file. Change-Id: I9752d849aa0e6cf608db0def3ca89565cff4debc Closes-bug: #1794419 --- novaclient/tests/unit/v2/test_shell.py | 19 +++++++++++++++---- novaclient/v2/shell.py | 11 +++++++++-- ...fix-rebuild-userdata-9315e5784feb8ba9.yaml | 5 +++++ 3 files changed, 29 insertions(+), 6 deletions(-) create mode 100644 releasenotes/notes/fix-rebuild-userdata-9315e5784feb8ba9.yaml diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 7bde8cd3d..e1166450b 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -1753,18 +1753,29 @@ def test_rebuild_files_2_57(self): cmd % (FAKE_UUID_1, testfile), api_version='2.57') def test_rebuild_change_user_data(self): - self.run_command('rebuild sample-server %s --user-data test' % - FAKE_UUID_1, api_version='2.57') - user_data = servers.ServerManager.transform_userdata('test') + testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt') + with open(testfile) as testfile_fd: + data = testfile_fd.read().encode('utf-8') + expected_file_data = servers.ServerManager.transform_userdata(data) + self.run_command('rebuild sample-server %s --user-data %s' % + (FAKE_UUID_1, testfile), api_version='2.57') self.assert_called('GET', '/servers?name=sample-server', pos=0) self.assert_called('GET', '/servers/1234', pos=1) self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=2) self.assert_called('POST', '/servers/1234/action', {'rebuild': {'imageRef': FAKE_UUID_1, - 'user_data': user_data, + 'user_data': expected_file_data, 'description': None}}, pos=3) self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=4) + def test_rebuild_invalid_user_data(self): + invalid_file = os.path.join(os.path.dirname(__file__), + 'no_such_file') + cmd = ('rebuild sample-server %s --user-data %s' + % (FAKE_UUID_1, invalid_file)) + self.assertRaises(exceptions.CommandError, self.run_command, cmd, + api_version='2.57') + def test_rebuild_unset_user_data(self): self.run_command('rebuild sample-server %s --user-data-unset' % FAKE_UUID_1, api_version='2.57') diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 7ddc5323e..8a1b9a6d4 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -1915,8 +1915,15 @@ def do_rebuild(cs, args): _("Cannot specify '--user-data-unset' with " "'--user-data'.")) elif args.user_data: - kwargs['userdata'] = args.user_data - + try: + kwargs['userdata'] = open(args.user_data) + except IOError as e: + raise exceptions.CommandError( + _("Can't open '%(user_data)s': %(exc)s") % { + 'user_data': args.user_data, + 'exc': e, + } + ) if cs.api_version >= api_versions.APIVersion('2.54'): if args.key_unset: kwargs['key_name'] = None diff --git a/releasenotes/notes/fix-rebuild-userdata-9315e5784feb8ba9.yaml b/releasenotes/notes/fix-rebuild-userdata-9315e5784feb8ba9.yaml new file mode 100644 index 000000000..4d5407584 --- /dev/null +++ b/releasenotes/notes/fix-rebuild-userdata-9315e5784feb8ba9.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + The user data argument to rebuild was passing the filename as is as userdata. + Now this passes the contents of the filename as intended. From f22685f588fafc6846d6fb5ceabb7b0be5ce8270 Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Mon, 1 Oct 2018 17:33:56 +0900 Subject: [PATCH 1480/1705] Update the contributor guide Add missing tox targets in contributor/testing.rst. Replace the launchpad link to nova project with the launchpad link to python-novaclient project in contributor/index.rst. Change-Id: I1b3736f25e59669624331d8f86820a45181c6760 Closes-Bug: #1795353 --- doc/source/contributor/index.rst | 8 ++++---- doc/source/contributor/testing.rst | 12 +++++++++--- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/doc/source/contributor/index.rst b/doc/source/contributor/index.rst index 4df1ed3f3..8dad2d857 100644 --- a/doc/source/contributor/index.rst +++ b/doc/source/contributor/index.rst @@ -2,12 +2,12 @@ Contributor Guide =================== -Code is hosted at `git.openstack.org`__. Submit bugs to the Nova project on -`Launchpad`__. Submit code to the `openstack/python-novaclient` project using -`Gerrit`__. +Code is hosted at `git.openstack.org`__. Submit bugs to the python-novaclient +project on `Launchpad`__. Submit code to the `openstack/python-novaclient` +project using `Gerrit`__. __ https://git.openstack.org/cgit/openstack/python-novaclient -__ https://launchpad.net/nova +__ https://bugs.launchpad.net/python-novaclient __ https://docs.openstack.org/infra/manual/developers.html#development-workflow .. toctree:: diff --git a/doc/source/contributor/testing.rst b/doc/source/contributor/testing.rst index 9b01c40ef..eeb32634e 100644 --- a/doc/source/contributor/testing.rst +++ b/doc/source/contributor/testing.rst @@ -6,16 +6,22 @@ The preferred way to run the unit tests is using ``tox``. There are multiple test targets that can be run to validate the code. ``tox -e pep8`` - Style guidelines enforcement. ``tox -e py27`` + Traditional unit testing (Python 2.7). - Traditional unit testing. +``tox -e py35`` + Traditional unit testing (Python 3.5). ``tox -e functional`` + Live functional testing against an existing OpenStack instance. (Python 2.7) + +``tox -e functional-py35`` + Live functional testing against an existing OpenStack instance. (Python 3.5) - Live functional testing against an existing OpenStack instance. +``tox -e cover`` + Generate a coverage report on unit testing. Functional testing assumes the existence of a `clouds.yaml` file as supported by `os-client-config `__ From 3b269568191948fa74abc2a5d0fbdc575f8c6ba2 Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Mon, 1 Oct 2018 08:03:40 +0900 Subject: [PATCH 1481/1705] Update the CLI reference Update the CLI reference according to the latest CLIs. The CLI reference (doc/source/cli/nova.rst) has been imported from openstack-manuals since If19cfcafc90fddb930c124a5b9845d4eae1f6093. We can update the CLI reference synchronously with changing the CLIs currently. So remove the description of nova CLI version in the reference. Change-Id: Icb97def5e4d65a31add7c60c205d5f9f0dfc4520 Closes-Bug: #1795283 --- doc/source/cli/nova.rst | 364 ++++++++++++++++++++++------------------ 1 file changed, 199 insertions(+), 165 deletions(-) diff --git a/doc/source/cli/nova.rst b/doc/source/cli/nova.rst index 899a0e0bf..16d276c6e 100644 --- a/doc/source/cli/nova.rst +++ b/doc/source/cli/nova.rst @@ -5,8 +5,6 @@ The nova client is the command-line interface (CLI) for the Compute service (nova) API and its extensions. -This chapter documents :command:`nova` version ``9.0.1``. - For help on a specific :command:`nova` command, enter: .. code-block:: console @@ -36,7 +34,8 @@ nova usage [--os-endpoint-override ] [--profile HMAC_KEY] [--insecure] [--os-cacert ] [--os-cert ] [--os-key ] [--timeout ] - [--os-auth-type ] [--os-auth-url OS_AUTH_URL] + [--collect-timing] [--os-auth-type ] + [--os-auth-url OS_AUTH_URL] [--os-system-scope OS_SYSTEM_SCOPE] [--os-domain-id OS_DOMAIN_ID] [--os-domain-name OS_DOMAIN_NAME] [--os-project-id OS_PROJECT_ID] [--os-project-name OS_PROJECT_NAME] @@ -173,6 +172,12 @@ nova usage ``flavor-show`` Show details about the given flavor. +``flavor-update`` + Update the description of an existing flavor. + (Supported by API versions '2.55' - '2.latest') + [hint: use '--os-compute-api-version' flag to show help message + for proper version] + ``floating-ip-associate`` **DEPRECATED** Associate a floating IP address to a server. @@ -236,20 +241,9 @@ nova usage **DEPRECATED** Update host settings. ``hypervisor-list`` - List hypervisors. (Supported by API versions - '2.0' - - - '2.latest') - [hint: - use - '--os-compute-api-version' - flag - to - show - help - message - for - proper version] + List hypervisors. (Supported by API versions '2.0' - '2.latest') + [hint: use '--os-compute-api-version' flag to show help message + for proper version] ``hypervisor-servers`` List servers belonging to specific @@ -277,6 +271,9 @@ nova usage ``instance-action-list`` List actions on a server. +``instance-usage-audit-log`` + List/Get server usage audits. + ``interface-attach`` Attach a network interface to a server. @@ -331,23 +328,9 @@ nova usage ``live-migration-force-complete`` Force on-going live migration to complete. - (Supported - by - API - versions - '2.22' - -'2.latest') - [hint: - use - '--os-compute-api-version' - flag - to - show - help - message - for - proper - version] + (Supported by API versions '2.22' - '2.latest') + [hint: use '--os-compute-api-version' flag to show help message + for proper version] ``lock`` Lock a server. A normal (non-admin) user will @@ -450,43 +433,15 @@ nova usage ``server-migration-list`` Get the migrations list of specified server. - (Supported - by - API - versions - '2.23' - -'2.latest') - [hint: - use - '--os-compute-api-version' - flag - to - show - help - message - for - proper - version] + (Supported by API versions '2.23' - '2.latest') + [hint: use '--os-compute-api-version' flag to show help message + for proper version] ``server-migration-show`` Get the migration of specified server. - (Supported - by - API - versions - '2.23' - -'2.latest') - [hint: - use - '--os-compute-api-version' - flag - to - show - help - message - for - proper - version] + (Supported by API versions '2.23' - '2.latest') + [hint: use '--os-compute-api-version' flag to show help message + for proper version] ``server-tag-add`` Add one or more tags to a server. (Supported @@ -496,23 +451,9 @@ nova usage ``server-tag-delete`` Delete one or more tags from a server. - (Supported - by - API - versions - '2.26' - -'2.latest') - [hint: - use - '--os-compute-api-version' - flag - to - show - help - message - for - proper - version] + (Supported by API versions '2.26' - '2.latest') + [hint: use '--os-compute-api-version' flag to show help message + for proper version] ``server-tag-delete-all`` Delete all tags from a server. (Supported by @@ -998,8 +939,7 @@ nova boot [--image-with ] [--boot-volume ] [--snapshot ] [--min-count ] [--max-count ] [--meta ] - [--file ] [--key-name ] - [--user-data ] + [--key-name ] [--user-data ] [--availability-zone ] [--security-groups ] [--block-device-mapping ] @@ -1010,7 +950,8 @@ nova boot [--nic ] [--config-drive ] [--poll] [--admin-pass ] [--access-ip-v4 ] [--access-ip-v6 ] - [--description ] + [--description ] [--tags ] + [--return-reservation-id] [--trusted-image-certificate-id ] @@ -1052,11 +993,6 @@ Boot a new server. /meta_data.json on the metadata server. Can be specified multiple times. -``--file `` - Store arbitrary files from locally - to on the new server. Limited by - the injected_files quota value. - ``--key-name `` Key name of keypair that should be created earlier with the command keypair-add. @@ -1072,13 +1008,8 @@ Boot a new server. Comma separated list of security group names. ``--block-device-mapping `` - Block - device - mapping - in - the - format - =:::. + Block device mapping in the format + =:::. ``--block-device`` key1=value1[,key2=value2...] @@ -1097,7 +1028,7 @@ Boot a new server. driver chooses suitable device depending on selected bus; note the libvirt driver always uses default device names), size=size of the - block device in MB(for swap) and in GB(for + block device in MB(for swap) and in GiB(for other formats) (if omitted, hypervisor driver calculates size), format=device will be formatted (e.g. swap, ntfs, ...; optional), @@ -1117,7 +1048,7 @@ Boot a new server. ``--ephemeral`` size=[,format=] Create and attach a local ephemeral block - device of GB and format it to . + device of GiB and format it to . ``--hint `` Send arbitrary key/value pairs to the @@ -1165,6 +1096,13 @@ Boot a new server. Description for the server. (Supported by API versions '2.19' - '2.latest') +``--tags `` + Tags for the server.Tags must be separated by commas: --tags + (Supported by API versions '2.52' - '2.latest') + +``--return-reservation-id`` + Return a reservation id bound to created servers. + ``--trusted-image-certificate-id `` Trusted image certificate IDs used to validate certificates during the image signature verification process. @@ -1373,6 +1311,7 @@ nova flavor-create usage: nova flavor-create [--ephemeral ] [--swap ] [--rxtx-factor ] [--is-public ] + [--description ] Create a new flavor. @@ -1390,7 +1329,7 @@ Create a new flavor. Memory size in MB. ```` - Disk size in GB. + Disk size in GiB. ```` Number of vcpus @@ -1398,7 +1337,7 @@ Create a new flavor. **Optional arguments:** ``--ephemeral `` - Ephemeral space size in GB (default 0). + Ephemeral space size in GiB (default 0). ``--swap `` Swap space size in MB (default 0). @@ -1410,6 +1349,11 @@ Create a new flavor. Make flavor accessible to the public (default true). +``--description `` + A free form description of the flavor. Limited to 65535 characters + in length. Only printable characters are allowed. + (Supported by API versions '2.55' - '2.latest') + .. _nova_flavor-delete: nova flavor-delete @@ -1559,6 +1503,8 @@ Get an MKS console to a server. (Supported by API versions '2.8' - '2.latest') [hint: use '--os-compute-api-version' flag to show help message for proper version] +.. versionadded:: 3.0.0 + **Positional arguments:** ```` @@ -1673,14 +1619,20 @@ nova host-evacuate .. code-block:: console - usage: nova host-evacuate [--target_host ] [--force] + usage: nova host-evacuate [--target_host ] [--force] [--strict] + Evacuate all instances from failed host. **Positional arguments:** ```` - Name of host. + The hypervisor hostname (or pattern) to search for. + + .. warning:: + + Use a fully qualified domain name if you only want to evacuate from + a specific host. **Optional arguments:** @@ -1692,6 +1644,9 @@ Evacuate all instances from failed host. Force to not verify the scheduler if a host is provided. (Supported by API versions '2.29' -'2.latest') +``--strict`` + Evacuate host with exact hypervisor hostname match + .. _nova_host-evacuate-live: nova host-evacuate-live @@ -1701,6 +1656,7 @@ nova host-evacuate-live usage: nova host-evacuate-live [--target-host ] [--block-migrate] [--max-servers ] [--force] + [--strict] Live migrate all instances off the specified host to other available hosts. @@ -1709,6 +1665,12 @@ Live migrate all instances off the specified host to other available hosts. ```` Name of host. + The hypervisor hostname (or pattern) to search for. + + .. warning:: + + Use a fully qualified domain name if you only want to live migrate + from a specific host. **Optional arguments:** @@ -1727,6 +1689,9 @@ Live migrate all instances off the specified host to other available hosts. Force to not verify the scheduler if a host is provided. (Supported by API versions '2.30' -'2.latest') +``--strict`` + live Evacuate host with exact hypervisor hostname match + .. _nova_host-meta: nova host-meta @@ -1734,14 +1699,19 @@ nova host-meta .. code-block:: console - usage: nova host-meta [ ...] + usage: nova host-meta [--strict] [ ...] Set or Delete metadata on all instances of a host. **Positional arguments:** ```` - Name of host. + The hypervisor hostname (or pattern) to search for. + + .. warning:: + + Use a fully qualified domain name if you only want to update + metadata for servers on a specific host. ```` Actions: 'set' or 'delete' @@ -1749,6 +1719,11 @@ Set or Delete metadata on all instances of a host. ```` Metadata to set or delete (only key is necessary on delete) +**Optional arguments:** + +``--strict`` + Set host-meta to the hypervisor with exact hostname match + .. _nova_host-servers-migrate: nova host-servers-migrate @@ -1756,7 +1731,7 @@ nova host-servers-migrate .. code-block:: console - usage: nova host-servers-migrate + usage: nova host-servers-migrate [--strict] Cold migrate all instances off the specified host to other available hosts. @@ -1764,6 +1739,17 @@ Cold migrate all instances off the specified host to other available hosts. ```` Name of host. + The hypervisor hostname (or pattern) to search for. + + .. warning:: + + Use a fully qualified domain name if you only want to cold migrate + from a specific host. + +**Optional arguments:** + +``--strict`` + Migrate host with exact hypervisor hostname match .. _nova_hypervisor-list: @@ -1824,7 +1810,8 @@ Display the details of the specified hypervisor. **Positional arguments:** ```` - Name or ID of the hypervisor to show the details of. + Name or ID of the hypervisor. + Starting with microversion 2.53 the ID must be a UUID. **Optional arguments:** @@ -1857,7 +1844,8 @@ Display the uptime of the specified hypervisor. **Positional arguments:** ```` - Name or ID of the hypervisor to show the uptime of. + Name or ID of the hypervisor. + Starting with microversion 2.53 the ID must be a UUID. .. _nova_image-create: @@ -1957,6 +1945,24 @@ List actions on a server. point of time. The provided time should be an ISO 8061 formatted time. e.g. 2016-03-04T06:27:59Z. (Supported by API versions '2.66' - '2.latest') +.. _nova_instance-usage-audit-log: + +nova instance-usage-audit-log +----------------------------- + +.. code-block:: console + + usage: nova instance-usage-audit-log [--before ] + +List/Get server usage audits. + +**Optional arguments:** + +``--before `` + Filters the response by the date and time before which to list usage audits. + The date and time stamp format is as follows: CCYY-MM-DD hh:mm:ss.NNNNNN + ex 2015-08-27 09:49:58 or 2015-08-27 09:49:58.123456. + .. _nova_interface-attach: nova interface-attach @@ -1965,7 +1971,7 @@ nova interface-attach .. code-block:: console usage: nova interface-attach [--port-id ] [--net-id ] - [--fixed-ip ] + [--fixed-ip ] [--tag ] Attach a network interface to a server. @@ -1986,6 +1992,10 @@ Attach a network interface to a server. ``--fixed-ip `` Requested fixed IP. +``--tag `` + Tag for the attached interface. + (Supported by API versions '2.49' - '2.latest') + .. _nova_interface-detach: nova interface-detach @@ -2358,6 +2368,8 @@ For microversions from 2.24 to 2.64 the migration status must be ``running``; for microversion 2.65 and greater, the migration status can also be ``queued`` and ``preparing``. +.. versionadded:: 3.3.0 + **Positional arguments:** ```` @@ -2379,6 +2391,8 @@ Force on-going live migration to complete. (Supported by API versions '2.22' - '2.latest') [hint: use '--os-compute-api-version' flag to show help message for proper version] +.. versionadded:: 3.3.0 + **Positional arguments:** ```` @@ -2433,7 +2447,7 @@ nova migrate .. code-block:: console - usage: nova migrate [--poll] + usage: nova migrate [--host ] [--poll] Migrate a server. The new host will be selected by the scheduler. @@ -2444,6 +2458,9 @@ Migrate a server. The new host will be selected by the scheduler. **Optional arguments:** +``--host `` + Destination host name. (Supported by API versions '2.56' - '2.latest') + ``--poll`` Report the server migration progress until it completes. @@ -2535,9 +2552,6 @@ nova quota-class-update usage: nova quota-class-update [--instances ] [--cores ] [--ram ] [--metadata-items ] - [--injected-files ] - [--injected-file-content-bytes ] - [--injected-file-path-bytes ] [--key-pairs ] [--server-groups ] [--server-group-members ] @@ -2566,16 +2580,6 @@ for proper version] ``--metadata-items `` New value for the "metadata-items" quota. -``--injected-files `` - New value for the "injected-files" quota. - -``--injected-file-content-bytes `` - New value for the "injected-file-content-bytes" quota. - -``--injected-file-path-bytes `` - New value for the "injected-file-path-bytes" - quota. - ``--key-pairs `` New value for the "key-pairs" quota. @@ -2653,9 +2657,6 @@ nova quota-update usage: nova quota-update [--user ] [--instances ] [--cores ] [--ram ] [--metadata-items ] - [--injected-files ] - [--injected-file-content-bytes ] - [--injected-file-path-bytes ] [--key-pairs ] [--server-groups ] [--server-group-members ] @@ -2688,16 +2689,6 @@ for proper version] ``--metadata-items `` New value for the "metadata-items" quota. -``--injected-files `` - New value for the "injected-files" quota. - -``--injected-file-content-bytes `` - New value for the "injected-file-content-bytes" quota. - -``--injected-file-path-bytes `` - New value for the "injected-file-path-bytes" - quota. - ``--key-pairs `` New value for the "key-pairs" quota. @@ -2750,7 +2741,8 @@ nova rebuild usage: nova rebuild [--rebuild-password ] [--poll] [--minimal] [--preserve-ephemeral] [--name ] [--description ] [--meta ] - [--file ] + [--key-name ] [--key-unset] + [--user-data ] [--user-data-unset] [--trusted-image-certificate-id ] [--trusted-image-certificates-unset] @@ -2795,10 +2787,24 @@ Shutdown, re-image, and re-boot a server. /meta_data.json on the metadata server. Can be specified multiple times. -``--file `` - Store arbitrary files from locally - to on the new server. You may store - up to 5 files. +``--key-name `` + Keypair name to set in the server. Cannot be specified with + the '--key-unset' option. + (Supported by API versions '2.54' - '2.latest') + +``--key-unset`` + Unset keypair in the server. Cannot be specified with + the '--key-name' option. + (Supported by API versions '2.54' - '2.latest') + +``--user-data `` + User data file to pass to be exposed by the metadata server. + (Supported by API versions '2.57' - '2.latest') + +``--user-data-unset`` + Unset user_data in the server. Cannot be specified with + the '--user-data' option. + (Supported by API versions '2.57' - '2.latest') ``--trusted-image-certificate-id `` Trusted image certificate IDs used to validate certificates @@ -3023,6 +3029,8 @@ Create a new server group with the specified details. ```` Policy for the server groups. +**Optional arguments:** + ``--rule`` Policy rules for the server groups. (Supported by API versions '2.64' - '2.latest'). Currently, only the ``max_server_per_host`` rule @@ -3102,6 +3110,8 @@ Get the migrations list of specified server. (Supported by API versions '2.23' - '2.latest') [hint: use '--os-compute-api-version' flag to show help message for proper version] +.. versionadded:: 3.3.0 + **Positional arguments:** ```` @@ -3120,6 +3130,8 @@ Get the migration of specified server. (Supported by API versions '2.23' - '2.latest') [hint: use '--os-compute-api-version' flag to show help message for proper version] +.. versionadded:: 3.3.0 + **Positional arguments:** ```` @@ -3141,6 +3153,8 @@ Add one or more tags to a server. (Supported by API versions '2.26' - '2.latest') [hint: use '--os-compute-api-version' flag to show help message for proper version] +.. versionadded:: 4.1.0 + **Positional arguments:** ```` @@ -3162,6 +3176,8 @@ Delete one or more tags from a server. (Supported by API versions '2.26' - '2.latest') [hint: use '--os-compute-api-version' flag to show help message for proper version] +.. versionadded:: 4.1.0 + **Positional arguments:** ```` @@ -3183,6 +3199,8 @@ Delete all tags from a server. (Supported by API versions '2.26' - '2.latest') [hint: use '--os-compute-api-version' flag to show help message for proper version] +.. versionadded:: 4.1.0 + **Positional arguments:** ```` @@ -3201,6 +3219,8 @@ Get list of tags from a server. (Supported by API versions '2.26' - '2.latest') [hint: use '--os-compute-api-version' flag to show help message for proper version] +.. versionadded:: 4.1.0 + **Positional arguments:** ```` @@ -3219,6 +3239,8 @@ Set list of tags to a server. (Supported by API versions '2.26' - '2.latest') [hint: use '--os-compute-api-version' flag to show help message for proper version] +.. versionadded:: 4.1.0 + **Positional arguments:** ```` @@ -3241,7 +3263,7 @@ Delete the service. **Positional arguments:** ```` - ID of service. + ID of service as a UUID. (Supported by API versions '2.53' - '2.latest') .. _nova_service-disable: @@ -3250,22 +3272,19 @@ nova service-disable .. code-block:: console - usage: nova service-disable [--reason ] + usage: nova service-disable [--reason ] Disable the service. **Positional arguments:** -```` - Name of host. - -```` - Service binary. +```` + ID of the service as a UUID. (Supported by API versions '2.53' - '2.latest') **Optional arguments:** ``--reason `` - Reason for disabling service. + Reason for disabling the service. .. _nova_service-enable: @@ -3274,17 +3293,14 @@ nova service-enable .. code-block:: console - usage: nova service-enable + usage: nova service-enable Enable the service. **Positional arguments:** -```` - Name of host. - -```` - Service binary. +```` + ID of the service as a UUID. (Supported by API versions '2.53' - '2.latest') .. _nova_service-force-down: @@ -3293,23 +3309,23 @@ nova service-force-down .. code-block:: console - usage: nova service-force-down [--unset] + usage: nova service-force-down [--unset] Force service to down. (Supported by API versions '2.11' - '2.latest') [hint: use '--os-compute-api-version' flag to show help message for proper version] +.. versionadded:: 2.27.0 + **Positional arguments:** -```` - Name of host. +```` + ID of the service as a UUID. (Supported by API versions '2.53' - '2.latest') -```` - Service binary. **Optional arguments:** ``--unset`` - Unset the force state down of service. + Unset the forced_down state of the service. .. _nova_service-list: @@ -3521,6 +3537,8 @@ Trigger crash dump in an instance. (Supported by API versions '2.17' - '2.latest') [hint: use '--os-compute-api-version' flag to show help message for proper version] +.. versionadded:: 3.3.0 + **Positional arguments:** ```` @@ -3679,7 +3697,7 @@ nova volume-attach .. code-block:: console - usage: nova volume-attach [] + usage: nova volume-attach [--tag ] [] Attach a volume to a server. @@ -3695,6 +3713,11 @@ Attach a volume to a server. Name of the device e.g. /dev/vdb. Use "auto" for autoassign (if supported). Libvirt driver will use default device name. +**Optional arguments:** + +``--tag `` + Tag for the attached volume. (Supported by API versions '2.49' - '2.latest') + .. _nova_volume-attachments: nova volume-attachments @@ -3754,3 +3777,14 @@ new volume. ```` ID of the destination volume. +.. _nova_bash-completion: + +nova bash-completion +-------------------- + +.. code-block:: console + + usage: nova bash-completion + +Prints all of the commands and options to stdout so that the +nova.bash_completion script doesn't have to hard code them. From 47dc339ab9bb2985d750dae11d0b3b8b2c85d2b2 Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Thu, 4 Oct 2018 12:02:07 +0900 Subject: [PATCH 1482/1705] Follow up "Fix up userdata argument to rebuild" This patch is a follow-up patch for I9752d849aa0e6cf608db0def3ca89565cff4debc. * Add checking a message of an exception in the unit test * Add 'with' statement when opening a file * Fix descriptions in the release note Change-Id: I2c399490f320a202b41a8f8d36710a36621c4853 --- novaclient/tests/unit/v2/test_shell.py | 7 +++++-- novaclient/v2/shell.py | 9 ++++++--- .../notes/fix-rebuild-userdata-9315e5784feb8ba9.yaml | 5 +++-- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index e1166450b..830ce53d7 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -1773,8 +1773,11 @@ def test_rebuild_invalid_user_data(self): 'no_such_file') cmd = ('rebuild sample-server %s --user-data %s' % (FAKE_UUID_1, invalid_file)) - self.assertRaises(exceptions.CommandError, self.run_command, cmd, - api_version='2.57') + ex = self.assertRaises(exceptions.CommandError, self.run_command, cmd, + api_version='2.57') + self.assertIn("Can't open '%(user_data)s': " + "[Errno 2] No such file or directory: '%(user_data)s'" % + {'user_data': invalid_file}, six.text_type(ex)) def test_rebuild_unset_user_data(self): self.run_command('rebuild sample-server %s --user-data-unset' % diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 8a1b9a6d4..ae7344bf7 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -398,7 +398,8 @@ def _boot(cs, args): for f in args.files: try: dst, src = f.split('=', 1) - files[dst] = open(src) + with open(src) as fo: + files[dst] = fo.read() except IOError as e: raise exceptions.CommandError( _("Can't open '%(src)s': %(exc)s") % @@ -416,7 +417,8 @@ def _boot(cs, args): if args.user_data: try: - userdata = open(args.user_data) + with open(args.user_data) as f: + userdata = f.read() except IOError as e: raise exceptions.CommandError(_("Can't open '%(user_data)s': " "%(exc)s") % @@ -1916,7 +1918,8 @@ def do_rebuild(cs, args): "'--user-data'.")) elif args.user_data: try: - kwargs['userdata'] = open(args.user_data) + with open(args.user_data) as f: + kwargs['userdata'] = f.read() except IOError as e: raise exceptions.CommandError( _("Can't open '%(user_data)s': %(exc)s") % { diff --git a/releasenotes/notes/fix-rebuild-userdata-9315e5784feb8ba9.yaml b/releasenotes/notes/fix-rebuild-userdata-9315e5784feb8ba9.yaml index 4d5407584..32065a113 100644 --- a/releasenotes/notes/fix-rebuild-userdata-9315e5784feb8ba9.yaml +++ b/releasenotes/notes/fix-rebuild-userdata-9315e5784feb8ba9.yaml @@ -1,5 +1,6 @@ --- fixes: - | - The user data argument to rebuild was passing the filename as is as userdata. - Now this passes the contents of the filename as intended. + The user data argument in the ``nova rebuild`` command was passing + the filename as userdata. Now this passes the contents of the file + as intended. From 0fdb154d9c094a4316c7f216dd6ee957287cd54c Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Tue, 9 Oct 2018 12:06:55 +0900 Subject: [PATCH 1483/1705] doc: Start using openstackdoctheme's extlink extension This ensures we have version-specific references to other projects [1]. [1] https://docs.openstack.org/openstackdocstheme/latest/#external-link-helper Change-Id: I3b9db8b71c082dd5f8d0564a9cdfdb2fa6dc5ed6 --- doc/source/cli/nova.rst | 5 ++--- doc/source/conf.py | 5 +++++ doc/source/contributor/testing.rst | 2 +- doc/source/reference/api/index.rst | 5 ++--- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/doc/source/cli/nova.rst b/doc/source/cli/nova.rst index 16d276c6e..a8ff713cc 100644 --- a/doc/source/cli/nova.rst +++ b/doc/source/cli/nova.rst @@ -15,9 +15,8 @@ For help on a specific :command:`nova` command, enter: of the ``nova`` CLI and into the ``openstack`` CLI. Using the ``openstack`` client where possible is preferred but there is not full parity yet for all of the ``nova`` commands. - For information on using the ``openstack`` CLI, see: - - https://docs.openstack.org/python-openstackclient/latest/ + For information on using the ``openstack`` CLI, see + :python-openstackclient-doc:`OpenStackClient <>`. .. _nova_command_usage: diff --git a/doc/source/conf.py b/doc/source/conf.py index 3d4d898ea..b98da5d9b 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -79,6 +79,11 @@ repository_name = 'openstack/python-novaclient' bug_project = 'python-novaclient' bug_tag = '' +openstack_projects = [ + 'keystoneauth', + 'os-client-config', + 'python-openstackclient', +] # -- Options for manual page output ------------------------------------------ diff --git a/doc/source/contributor/testing.rst b/doc/source/contributor/testing.rst index eeb32634e..1b285745e 100644 --- a/doc/source/contributor/testing.rst +++ b/doc/source/contributor/testing.rst @@ -24,7 +24,7 @@ test targets that can be run to validate the code. Generate a coverage report on unit testing. Functional testing assumes the existence of a `clouds.yaml` file as supported -by `os-client-config `__ +by :os-client-config-doc:`os-client-config <>`. It assumes the existence of a cloud named `devstack` that behaves like a normal DevStack installation with a demo and an admin user/tenant - or clouds named `functional_admin` and `functional_nonadmin`. diff --git a/doc/source/reference/api/index.rst b/doc/source/reference/api/index.rst index e195abf2e..e4d9c4d6b 100644 --- a/doc/source/reference/api/index.rst +++ b/doc/source/reference/api/index.rst @@ -53,9 +53,8 @@ application, you can append a (name, version) tuple to the session's >>> sess = session.Session(auth=auth) >>> sess.additional_user_agent.append(('shade', '1.2.3')) -For more information on this keystoneauth API, see `Using Sessions`_. - -.. _Using Sessions: https://docs.openstack.org/keystoneauth/latest/using-sessions.html +For more information on this keystoneauth API, see +:keystoneauth-doc:`Using Sessions `. It is also possible to use an instance as a context manager in which case there will be a session kept alive for the duration of the with statement:: From 4fef02de71d86c1aa0cb1ace74abd0fe19e0a1e9 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Thu, 11 Oct 2018 11:31:06 -0400 Subject: [PATCH 1484/1705] Add support for microversion 2.67: BDMv2 volume_type This adds the nova boot command and python API binding support for creating a server with block device mappings defined using a specific volume type. Depends-On: https://review.openstack.org/606398/ Depends-On: https://review.openstack.org/#/c/610349/ Part of blueprint boot-instance-specific-storage-backend Change-Id: I484ee065119b5783db212ea64efa60e87c40338c --- doc/source/cli/nova.rst | 10 ++- novaclient/__init__.py | 2 +- novaclient/tests/unit/utils.py | 2 +- novaclient/tests/unit/v2/test_servers.py | 45 ++++++++++++ novaclient/tests/unit/v2/test_shell.py | 73 +++++++++++++++++++ novaclient/v2/servers.py | 8 ++ novaclient/v2/shell.py | 42 +++++++++++ .../microversion-v2_67-da6d9b12730b8562.yaml | 10 +++ 8 files changed, 187 insertions(+), 5 deletions(-) create mode 100644 releasenotes/notes/microversion-v2_67-da6d9b12730b8562.yaml diff --git a/doc/source/cli/nova.rst b/doc/source/cli/nova.rst index a8ff713cc..a444c297a 100644 --- a/doc/source/cli/nova.rst +++ b/doc/source/cli/nova.rst @@ -1036,9 +1036,13 @@ Boot a new server. to 0, for others need to be specified), shutdown=shutdown behaviour (either preserve or remove, for local destination set to - remove) and tag=device metadata tag - (optional). (Supported by API versions '2.42' - - '2.latest') + remove), tag=device metadata tag + (optional; supported by API versions '2.42' + - '2.latest'), and volume_type=type of volume + to create (either ID or name) when source is + `blank`, `image` or `snapshot` and dest is `volume` + (optional; supported by API versions '2.67' + - '2.latest'). ``--swap `` Create and attach a local swap block device of diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 49bd667d7..43a8d1a94 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.66") +API_MAX_VERSION = api_versions.APIVersion("2.67") diff --git a/novaclient/tests/unit/utils.py b/novaclient/tests/unit/utils.py index 49d3631f3..0050941bc 100644 --- a/novaclient/tests/unit/utils.py +++ b/novaclient/tests/unit/utils.py @@ -104,7 +104,7 @@ def assert_called(self, method, path, body=None): if not isinstance(body, six.string_types): # json load if the input body to match against is not a string req_data = jsonutils.loads(req_data) - self.assertEqual(req_data, body) + self.assertEqual(body, req_data) class TestResponse(requests.Response): diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index 540968cf5..427a4eff8 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -1609,3 +1609,48 @@ def test_rebuild_with_trusted_image_certificates_pre_263_fails(self): '1234', fakes.FAKE_IMAGE_UUID_1, trusted_image_certificates=['id1', 'id2']) self.assertIn('trusted_image_certificates', six.text_type(ex)) + + +class ServersV267Test(ServersV263Test): + """Tests for creating a server with a block_device_mapping_v2 entry + using volume_type for microversion 2.67. + """ + api_version = '2.67' + + def test_create_server_boot_from_volume_with_volume_type(self): + bdm = [{"volume_size": 1, + "uuid": "11111111-1111-1111-1111-111111111111", + "delete_on_termination": True, + "source_type": "snapshot", + "destination_type": "volume", + "boot_index": 0, + "volume_type": "rbd"}] + s = self.cs.servers.create( + name="bfv server", image='', flavor=1, + nics='auto', block_device_mapping_v2=bdm) + self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers', { + 'server': { + 'flavorRef': '1', + 'imageRef': '', + 'name': 'bfv server', + 'networks': 'auto', + 'block_device_mapping_v2': bdm, + 'min_count': 1, + 'max_count': 1, + }}) + + def test_create_server_boot_from_volume_with_volume_type_pre_267(self): + self.cs.api_version = api_versions.APIVersion('2.66') + bdm = [{"volume_size": 1, + "uuid": "11111111-1111-1111-1111-111111111111", + "delete_on_termination": True, + "source_type": "snapshot", + "destination_type": "volume", + "boot_index": 0, + "volume_type": "rbd"}] + ex = self.assertRaises(ValueError, self.cs.servers.create, + name="bfv server", image='', flavor=1, + nics='none', block_device_mapping_v2=bdm) + self.assertIn("Block device volume_type is not supported before " + "microversion 2.67", six.text_type(ex)) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 830ce53d7..216241ecd 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -624,6 +624,78 @@ def test_boot_bdms_v2_invalid_shutdown_value(self): 'size=1,format=ext4,type=disk,shutdown=foobar ' 'some-server' % FAKE_UUID_1)) + def test_boot_from_volume_with_volume_type_old_microversion(self): + ex = self.assertRaises( + exceptions.CommandError, self.run_command, + 'boot --flavor 1 --block-device id=%s,source=image,dest=volume,' + 'size=1,bootindex=0,shutdown=remove,tag=foo,volume_type=lvm ' + 'bfv-server' % FAKE_UUID_1, api_version='2.66') + self.assertIn("'volume_type' in block device mapping is not supported " + "in API version", six.text_type(ex)) + + def test_boot_from_volume_with_volume_type(self): + """Tests creating a volume-backed server from a source image and + specifying the type of volume to create with microversion 2.67. + """ + self.run_command( + 'boot --flavor 1 --block-device id=%s,source=image,dest=volume,' + 'size=1,bootindex=0,shutdown=remove,tag=foo,volume_type=lvm ' + 'bfv-server' % FAKE_UUID_1, api_version='2.67') + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'bfv-server', + 'block_device_mapping_v2': [ + { + 'uuid': FAKE_UUID_1, + 'source_type': 'image', + 'destination_type': 'volume', + 'volume_size': '1', + 'delete_on_termination': True, + 'tag': 'foo', + 'boot_index': '0', + 'volume_type': 'lvm' + }, + ], + 'networks': 'auto', + 'imageRef': '', + 'min_count': 1, + 'max_count': 1, + }}) + + def test_boot_from_volume_without_volume_type_2_67(self): + """Tests creating a volume-backed server from a source image but + without specifying the type of volume to create with microversion 2.67. + The volume_type parameter should be omitted in the request to the + API server. + """ + self.run_command( + 'boot --flavor 1 --block-device id=%s,source=image,dest=volume,' + 'size=1,bootindex=0,shutdown=remove,tag=foo bfv-server' % + FAKE_UUID_1, api_version='2.67') + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'bfv-server', + 'block_device_mapping_v2': [ + { + 'uuid': FAKE_UUID_1, + 'source_type': 'image', + 'destination_type': 'volume', + 'volume_size': '1', + 'delete_on_termination': True, + 'tag': 'foo', + 'boot_index': '0', + }, + ], + 'networks': 'auto', + 'imageRef': '', + 'min_count': 1, + 'max_count': 1, + }}) + def test_boot_metadata(self): self.run_command('boot --image %s --flavor 1 --meta foo=bar=pants' ' --meta spam=eggs some-server ' % FAKE_UUID_1) @@ -3913,6 +3985,7 @@ def test_versions(self): 62, # There are no version-wrapped shell method changes for this. 63, # There are no version-wrapped shell method changes for this. 65, # There are no version-wrapped shell method changes for this. + 67, # There are no version-wrapped shell method changes for this. ]) versions_supported = set(range(0, novaclient.API_MAX_VERSION.ver_minor + 1)) diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index d872fa016..e94f3567a 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -1305,6 +1305,14 @@ def create(self, name, image, flavor, meta=None, files=None, raise exceptions.UnsupportedAttribute("trusted_image_certificates", "2.63") + if (block_device_mapping_v2 and + self.api_version < api_versions.APIVersion('2.67')): + for bdm in block_device_mapping_v2: + if bdm.get('volume_type'): + raise ValueError( + "Block device volume_type is not supported before " + "microversion 2.67") + boot_kwargs = dict( meta=meta, files=files, userdata=userdata, reservation_id=reservation_id, min_count=min_count, diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index ae7344bf7..f6704559e 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -71,6 +71,7 @@ def emit_duplicated_image_with_warning(img, image_with): 'type': 'device_type', 'shutdown': 'delete_on_termination', 'tag': 'tag', + 'volume_type': 'volume_type' # added in 2.67 } @@ -145,6 +146,8 @@ def _parse_block_device_mapping_v2(cs, args, image): 'delete_on_termination': False} bdm.append(bdm_dict) + supports_volume_type = cs.api_version == api_versions.APIVersion('2.67') + for device_spec in args.block_device: spec_dict = _parse_device_spec(device_spec) bdm_dict = {} @@ -155,6 +158,12 @@ def _parse_block_device_mapping_v2(cs, args, image): "in API version %(version)s.") % {'version': cs.api_version.get_string()}) + if 'volume_type' in spec_dict and not supports_volume_type: + raise exceptions.CommandError( + _("'volume_type' in block device mapping is not supported " + "in API version %(version)s.") + % {'version': cs.api_version.get_string()}) + for key, value in spec_dict.items(): bdm_dict[CLIENT_BDM2_KEYS[key]] = value @@ -709,6 +718,7 @@ def _boot(cs, args): action='append', default=[], start_version='2.42', + end_version='2.66', help=_("Block device mapping with the keys: " "id=UUID (image_id, snapshot_id or volume_id only if using source " "image, snapshot or volume) " @@ -732,6 +742,38 @@ def _boot(cs, args): "shutdown=shutdown behaviour (either preserve or remove, " "for local destination set to remove) and " "tag=device metadata tag (optional).")) +@utils.arg( + '--block-device', + metavar="key1=value1[,key2=value2...]", + action='append', + default=[], + start_version='2.67', + help=_("Block device mapping with the keys: " + "id=UUID (image_id, snapshot_id or volume_id only if using source " + "image, snapshot or volume) " + "source=source type (image, snapshot, volume or blank), " + "dest=destination type of the block device (volume or local), " + "bus=device's bus (e.g. uml, lxc, virtio, ...; if omitted, " + "hypervisor driver chooses a suitable default, " + "honoured only if device type is supplied) " + "type=device type (e.g. disk, cdrom, ...; defaults to 'disk') " + "device=name of the device (e.g. vda, xda, ...; " + "if omitted, hypervisor driver chooses suitable device " + "depending on selected bus; note the libvirt driver always " + "uses default device names), " + "size=size of the block device in MB(for swap) and in " + "GiB(for other formats) " + "(if omitted, hypervisor driver calculates size), " + "format=device will be formatted (e.g. swap, ntfs, ...; optional), " + "bootindex=integer used for ordering the boot disks " + "(for image backed instances it is equal to 0, " + "for others need to be specified), " + "shutdown=shutdown behaviour (either preserve or remove, " + "for local destination set to remove) and " + "tag=device metadata tag (optional), " + "volume_type=type of volume to create (either ID or name) when " + "source is blank, image or snapshot and dest is volume (optional)." + )) @utils.arg( '--swap', metavar="", diff --git a/releasenotes/notes/microversion-v2_67-da6d9b12730b8562.yaml b/releasenotes/notes/microversion-v2_67-da6d9b12730b8562.yaml new file mode 100644 index 000000000..0c1e360a0 --- /dev/null +++ b/releasenotes/notes/microversion-v2_67-da6d9b12730b8562.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + Support is added for the `2.67 microversion`_ which allows specifying + a ``volume_type`` with the ``--block-device`` option on the ``nova boot`` + command. The ``novaclient.v2.servers.ServerManager.create()`` method now + also supports a ``volume_type`` entry in the ``block_device_mapping_v2`` + parameter. + + .. _2.67 microversion: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id60 From 578b37f99419293f7c9eb38d3f71ab164e774b79 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Wed, 17 Oct 2018 16:32:42 -0400 Subject: [PATCH 1485/1705] Recommend against using --force for evacuate/live migration This copies the same warnings from the API reference change I85e7c2677f4d5eccc1e7f349de06960b53ef148d to the CLI help for the various evacuate/live migrate commands. It also mentions that if a host is not specified, one is selected by the scheduler (which is what we really want people doing). Given blueprint remove-force-flag-from-live-migrate-and-evacuate we really want to discourage people from using the --force flag. Change-Id: I15ef933cc09947e2bb3fb7bf17b15735171a9bec --- doc/source/cli/nova.rst | 42 +++++++++++++++++++++++++++++++---------- novaclient/v2/shell.py | 30 +++++++++++++++++++++++------ 2 files changed, 56 insertions(+), 16 deletions(-) diff --git a/doc/source/cli/nova.rst b/doc/source/cli/nova.rst index a444c297a..e14acd5c5 100644 --- a/doc/source/cli/nova.rst +++ b/doc/source/cli/nova.rst @@ -1248,8 +1248,13 @@ Evacuate server from failed host. storage. ``--force`` - Force to not verify the scheduler if a host is - provided. (Supported by API versions '2.29' -'2.latest') + Force an evacuation by not verifying the provided destination host by the + scheduler. (Supported by API versions '2.29' -'2.latest') + + .. warning:: This could result in failures to actually evacuate the + server to the specified host. It is recommended to either not specify + a host so that the scheduler will pick one, or specify a host without + ``--force``. .. _nova_flavor-access-add: @@ -1644,8 +1649,13 @@ Evacuate all instances from failed host. the scheduler will select a target. ``--force`` - Force to not verify the scheduler if a host is - provided. (Supported by API versions '2.29' -'2.latest') + Force an evacuation by not verifying the provided destination host by the + scheduler. (Supported by API versions '2.29' -'2.latest') + + .. warning:: This could result in failures to actually evacuate the + server to the specified host. It is recommended to either not specify + a host so that the scheduler will pick one, or specify a host without + ``--force``. ``--strict`` Evacuate host with exact hypervisor hostname match @@ -1678,7 +1688,8 @@ Live migrate all instances off the specified host to other available hosts. **Optional arguments:** ``--target-host `` - Name of target host. + Name of target host. If no host is specified, the scheduler will choose + one. ``--block-migrate`` Enable block migration. (Default=auto) @@ -1689,8 +1700,13 @@ Live migrate all instances off the specified host to other available hosts. simultaneously ``--force`` - Force to not verify the scheduler if a host is - provided. (Supported by API versions '2.30' -'2.latest') + Force a live-migration by not verifying the provided destination host by + the scheduler. (Supported by API versions '2.30' -'2.latest') + + .. warning:: This could result in failures to actually live migrate the + servers to the specified host. It is recommended to either not specify + a host so that the scheduler will pick one, or specify a host without + ``--force``. ``--strict`` live Evacuate host with exact hypervisor hostname match @@ -2341,7 +2357,8 @@ Migrate running server to a new machine. Name or ID of server. ```` - Destination host name. + Destination host name. If no host is specified, the scheduler will choose + one. **Optional arguments:** @@ -2351,8 +2368,13 @@ Migrate running server to a new machine. '2.25' - '2.latest') ``--force`` - Force to not verify the scheduler if a host is provided. - (Supported by API versions '2.30' - '2.latest') + Force a live-migration by not verifying the provided destination host by + the scheduler. (Supported by API versions '2.30' -'2.latest') + + .. warning:: This could result in failures to actually live migrate the + server to the specified host. It is recommended to either not specify + a host so that the scheduler will pick one, or specify a host without + ``--force``. .. _nova_live-migration-abort: diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index f6704559e..b993fbc0d 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -3425,7 +3425,8 @@ def parser_hosts(fields): @utils.arg('server', metavar='', help=_('Name or ID of server.')) @utils.arg( 'host', metavar='', default=None, nargs='?', - help=_('Destination host name.')) + help=_('Destination host name. If no host is specified, the scheduler ' + 'will choose one.')) @utils.arg( '--block-migrate', action='store_true', @@ -3452,7 +3453,11 @@ def parser_hosts(fields): dest='force', action='store_true', default=False, - help=_('Force to not verify the scheduler if a host is provided.'), + help=_('Force a live-migration by not verifying the provided destination ' + 'host by the scheduler. WARNING: This could result in failures to ' + 'actually live migrate the server to the specified host. It is ' + 'recommended to either not specify a host so that the scheduler ' + 'will pick one, or specify a host without --force.'), start_version='2.30') def do_live_migration(cs, args): """Migrate running server to a new machine.""" @@ -4437,7 +4442,11 @@ def do_quota_class_update(cs, args): dest='force', action='store_true', default=False, - help=_('Force to not verify the scheduler if a host is provided.'), + help=_('Force an evacuation by not verifying the provided destination ' + 'host by the scheduler. WARNING: This could result in failures to ' + 'actually evacuate the server to the specified host. It is ' + 'recommended to either not specify a host so that the scheduler ' + 'will pick one, or specify a host without --force.'), start_version='2.29') def do_evacuate(cs, args): """Evacuate server from failed host.""" @@ -4890,7 +4899,11 @@ def _hyper_servers(cs, host, strict): dest='force', action='store_true', default=False, - help=_('Force to not verify the scheduler if a host is provided.'), + help=_('Force an evacuation by not verifying the provided destination ' + 'host by the scheduler. WARNING: This could result in failures to ' + 'actually evacuate the server to the specified host. It is ' + 'recommended to either not specify a host so that the scheduler ' + 'will pick one, or specify a host without --force.'), start_version='2.29') @utils.arg( '--strict', @@ -4945,7 +4958,8 @@ def __init__(self, server_uuid, live_migration_accepted, '--target-host', metavar='', default=None, - help=_('Name of target host.')) + help=_('Name of target host. If no host is specified, the scheduler will ' + 'choose one.')) @utils.arg( '--block-migrate', action='store_true', @@ -4975,7 +4989,11 @@ def __init__(self, server_uuid, live_migration_accepted, dest='force', action='store_true', default=False, - help=_('Force to not verify the scheduler if a host is provided.'), + help=_('Force a live-migration by not verifying the provided destination ' + 'host by the scheduler. WARNING: This could result in failures to ' + 'actually live migrate the servers to the specified host. It is ' + 'recommended to either not specify a host so that the scheduler ' + 'will pick one, or specify a host without --force.'), start_version='2.30') @utils.arg( '--strict', From 80b428698172a41fc256174e61d2a005550fb1a3 Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Tue, 9 Oct 2018 10:16:33 +0900 Subject: [PATCH 1486/1705] Replace MB with MiB Change-Id: Ie5bd325d8e0f7e9af76c1ddae1d679b0e797c84b --- doc/source/cli/nova.rst | 10 +++--- .../v2/legacy/test_readonly_nova.py | 2 +- novaclient/tests/unit/fakes.py | 4 +-- novaclient/tests/unit/fixture_data/servers.py | 6 ++-- novaclient/tests/unit/test_base.py | 2 +- novaclient/tests/unit/v2/fakes.py | 20 +++++------ novaclient/tests/unit/v2/test_flavors.py | 4 +-- novaclient/tests/unit/v2/test_servers.py | 5 +-- novaclient/tests/unit/v2/test_shell.py | 32 ++++++++--------- novaclient/v2/flavors.py | 6 ++-- novaclient/v2/shell.py | 34 +++++++++---------- 11 files changed, 63 insertions(+), 62 deletions(-) diff --git a/doc/source/cli/nova.rst b/doc/source/cli/nova.rst index e14acd5c5..b8a212d49 100644 --- a/doc/source/cli/nova.rst +++ b/doc/source/cli/nova.rst @@ -1027,7 +1027,7 @@ Boot a new server. driver chooses suitable device depending on selected bus; note the libvirt driver always uses default device names), size=size of the - block device in MB(for swap) and in GiB(for + block device in MiB(for swap) and in GiB(for other formats) (if omitted, hypervisor driver calculates size), format=device will be formatted (e.g. swap, ntfs, ...; optional), @@ -1046,7 +1046,7 @@ Boot a new server. ``--swap `` Create and attach a local swap block device of - MB. + MiB. ``--ephemeral`` size=[,format=] @@ -1334,7 +1334,7 @@ Create a new flavor. generated a UUID for the ID. ```` - Memory size in MB. + Memory size in MiB. ```` Disk size in GiB. @@ -1348,7 +1348,7 @@ Create a new flavor. Ephemeral space size in GiB (default 0). ``--swap `` - Swap space size in MB (default 0). + Swap space size in MiB (default 0). ``--rxtx-factor `` RX/TX factor (default 1). @@ -1430,7 +1430,7 @@ Print a list of available 'flavors' (sizes of servers). Filters the flavors by a minimum disk space, in GiB. ``--min-ram `` - Filters the flavors by a minimum RAM, in MB. + Filters the flavors by a minimum RAM, in MiB. ``--limit `` Maximum number of flavors to display. If limit is diff --git a/novaclient/tests/functional/v2/legacy/test_readonly_nova.py b/novaclient/tests/functional/v2/legacy/test_readonly_nova.py index 6b9598e96..a31982e5d 100644 --- a/novaclient/tests/functional/v2/legacy/test_readonly_nova.py +++ b/novaclient/tests/functional/v2/legacy/test_readonly_nova.py @@ -49,7 +49,7 @@ def test_admin_flavor_acces_list(self): params='--flavor m1.tiny') def test_admin_flavor_list(self): - self.assertIn("Memory_MB", self.nova('flavor-list')) + self.assertIn("Memory_MiB", self.nova('flavor-list')) def test_admin_hypervisor_list(self): self.nova('hypervisor-list') diff --git a/novaclient/tests/unit/fakes.py b/novaclient/tests/unit/fakes.py index a9447f71f..312cca4fb 100644 --- a/novaclient/tests/unit/fakes.py +++ b/novaclient/tests/unit/fakes.py @@ -59,10 +59,10 @@ def assert_called(self, method, url, body=None, pos=-1): self.assert_called('GET', '/flavors/aa1/os-extra_specs') 2. self.run_command(["boot", "--image", "1", - "--flavor", "512 MB Server", + "--flavor", "512 MiB Server", "--max-count", "3", "server"]) self.assert_called('GET', '/images/1', pos=0) - self.assert_called('GET', '/flavors/512 MB Server', pos=1) + self.assert_called('GET', '/flavors/512 MiB Server', pos=1) self.assert_called('GET', '/flavors?is_public=None', pos=2) self.assert_called('GET', '/flavors/2', pos=3) self.assert_called( diff --git a/novaclient/tests/unit/fixture_data/servers.py b/novaclient/tests/unit/fixture_data/servers.py index 2402cd106..6df4129bd 100644 --- a/novaclient/tests/unit/fixture_data/servers.py +++ b/novaclient/tests/unit/fixture_data/servers.py @@ -43,7 +43,7 @@ def setUp(self): }, "flavor": { "id": 1, - "name": "256 MB Server", + "name": "256 MiB Server", }, "hostId": "e4d909c290d0fb1ca068ffaddf22cbd0", "status": "BUILD", @@ -85,7 +85,7 @@ def setUp(self): }, "flavor": { "id": 1, - "name": "256 MB Server", + "name": "256 MiB Server", }, "hostId": "9e107d9d372bb6826bd81d3542a419d6", "status": "ACTIVE", @@ -127,7 +127,7 @@ def setUp(self): "image": "", "flavor": { "id": 1, - "name": "256 MB Server", + "name": "256 MiB Server", }, "hostId": "9e107d9d372bb6826bd81d3542a419d6", "status": "ACTIVE", diff --git a/novaclient/tests/unit/test_base.py b/novaclient/tests/unit/test_base.py index 0b1ee83aa..b1067cf2b 100644 --- a/novaclient/tests/unit/test_base.py +++ b/novaclient/tests/unit/test_base.py @@ -49,7 +49,7 @@ class TmpObject(object): def test_resource_lazy_getattr(self): cs = fakes.FakeClient(api_versions.APIVersion("2.0")) f = flavors.Flavor(cs.flavors, {'id': 1}) - self.assertEqual('256 MB Server', f.name) + self.assertEqual('256 MiB Server', f.name) cs.assert_called('GET', '/flavors/1') # Missing stuff still fails after a second get diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 374648be3..c6f34c344 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -409,7 +409,7 @@ def get_servers_detail(self, **kw): }, "flavor": { "id": 1, - "name": "256 MB Server", + "name": "256 MiB Server", }, "hostId": "e4d909c290d0fb1ca068ffaddf22cbd0", "status": "BUILD", @@ -450,7 +450,7 @@ def get_servers_detail(self, **kw): }, "flavor": { "id": 1, - "name": "256 MB Server", + "name": "256 MiB Server", }, "hostId": "9e107d9d372bb6826bd81d3542a419d6", "status": "ACTIVE", @@ -491,7 +491,7 @@ def get_servers_detail(self, **kw): "image": "", "flavor": { "id": 1, - "name": "256 MB Server", + "name": "256 MiB Server", }, "hostId": "9e107d9d372bb6826bd81d3542a419d6", "status": "ACTIVE", @@ -844,19 +844,19 @@ def get_flavors(self, **kw): def get_flavors_detail(self, **kw): flavors = {'flavors': [ - {'id': 1, 'name': '256 MB Server', 'ram': 256, 'disk': 10, + {'id': 1, 'name': '256 MiB Server', 'ram': 256, 'disk': 10, 'OS-FLV-EXT-DATA:ephemeral': 10, 'os-flavor-access:is_public': True, 'links': {}}, - {'id': 2, 'name': '512 MB Server', 'ram': 512, 'disk': 20, + {'id': 2, 'name': '512 MiB Server', 'ram': 512, 'disk': 20, 'OS-FLV-EXT-DATA:ephemeral': 20, 'os-flavor-access:is_public': False, 'links': {}}, - {'id': 4, 'name': '1024 MB Server', 'ram': 1024, 'disk': 10, + {'id': 4, 'name': '1024 MiB Server', 'ram': 1024, 'disk': 10, 'OS-FLV-EXT-DATA:ephemeral': 10, 'os-flavor-access:is_public': True, 'links': {}}, - {'id': 'aa1', 'name': '128 MB Server', 'ram': 128, 'disk': 0, + {'id': 'aa1', 'name': '128 MiB Server', 'ram': 128, 'disk': 0, 'OS-FLV-EXT-DATA:ephemeral': 0, 'os-flavor-access:is_public': True, 'links': {}} @@ -925,16 +925,16 @@ def get_flavors_3(self, **kw): FAKE_RESPONSE_HEADERS, {'flavor': { 'id': 3, - 'name': '256 MB Server', + 'name': '256 MiB Server', 'ram': 256, 'disk': 10, }}, ) - def get_flavors_512_MB_Server(self, **kw): + def get_flavors_512_MiB_Server(self, **kw): raise exceptions.NotFound('404') - def get_flavors_128_MB_Server(self, **kw): + def get_flavors_128_MiB_Server(self, **kw): raise exceptions.NotFound('404') def get_flavors_80645cf4_6ad3_410a_bbc8_6f3e1e291f51(self, **kw): diff --git a/novaclient/tests/unit/v2/test_flavors.py b/novaclient/tests/unit/v2/test_flavors.py index f05e7cddb..e536af11f 100644 --- a/novaclient/tests/unit/v2/test_flavors.py +++ b/novaclient/tests/unit/v2/test_flavors.py @@ -122,11 +122,11 @@ def test_find(self): f = self.cs.flavors.find(ram=256) self.assert_request_id(f, fakes.FAKE_REQUEST_ID_LIST) self.cs.assert_called('GET', '/flavors/detail') - self.assertEqual('256 MB Server', f.name) + self.assertEqual('256 MiB Server', f.name) f = self.cs.flavors.find(disk=0) self.assert_request_id(f, fakes.FAKE_REQUEST_ID_LIST) - self.assertEqual('128 MB Server', f.name) + self.assertEqual('128 MiB Server', f.name) self.assertRaises(exceptions.NotFound, self.cs.flavors.find, disk=12345) diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index 427a4eff8..402f50203 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -455,9 +455,10 @@ def test_find(self): self.assertEqual('sample-server', server.name) self.assertRaises(exceptions.NoUniqueMatch, self.cs.servers.find, - flavor={"id": 1, "name": "256 MB Server"}) + flavor={"id": 1, "name": "256 MiB Server"}) - sl = self.cs.servers.findall(flavor={"id": 1, "name": "256 MB Server"}) + sl = self.cs.servers.findall(flavor={"id": 1, + "name": "256 MiB Server"}) self.assert_request_id(sl, fakes.FAKE_REQUEST_ID_LIST) self.assertEqual([1234, 5678, 9012], [s.id for s in sl]) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 216241ecd..5e9b9951a 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -1165,10 +1165,10 @@ def test_boot_with_poll_to_check_VM_state_error(self): def test_boot_named_flavor(self): self.run_command(["boot", "--image", FAKE_UUID_1, - "--flavor", "512 MB Server", + "--flavor", "512 MiB Server", "--max-count", "3", "server"]) self.assert_called('GET', '/v2/images/' + FAKE_UUID_1, pos=0) - self.assert_called('GET', '/flavors/512 MB Server', pos=1) + self.assert_called('GET', '/flavors/512 MiB Server', pos=1) self.assert_called('GET', '/flavors?is_public=None', pos=2) self.assert_called('GET', '/flavors/2', pos=3) self.assert_called( @@ -1404,15 +1404,15 @@ def test_flavor_show_with_alphanum_id(self): self.assert_called_anytime('GET', '/flavors/aa1') def test_flavor_show_by_name(self): - self.run_command(['flavor-show', '128 MB Server']) - self.assert_called('GET', '/flavors/128 MB Server', pos=0) + self.run_command(['flavor-show', '128 MiB Server']) + self.assert_called('GET', '/flavors/128 MiB Server', pos=0) self.assert_called('GET', '/flavors?is_public=None', pos=1) self.assert_called('GET', '/flavors/aa1', pos=2) self.assert_called('GET', '/flavors/aa1/os-extra_specs', pos=3) def test_flavor_show_by_name_priv(self): - self.run_command(['flavor-show', '512 MB Server']) - self.assert_called('GET', '/flavors/512 MB Server', pos=0) + self.run_command(['flavor-show', '512 MiB Server']) + self.assert_called('GET', '/flavors/512 MiB Server', pos=0) self.assert_called('GET', '/flavors?is_public=None', pos=1) self.assert_called('GET', '/flavors/2', pos=2) self.assert_called('GET', '/flavors/2/os-extra_specs', pos=3) @@ -1444,7 +1444,7 @@ def test_flavor_access_add_by_id(self): {'addTenantAccess': {'tenant': 'proj2'}}) def test_flavor_access_add_by_name(self): - self.run_command(['flavor-access-add', '512 MB Server', 'proj2']) + self.run_command(['flavor-access-add', '512 MiB Server', 'proj2']) self.assert_called('POST', '/flavors/2/action', {'addTenantAccess': {'tenant': 'proj2'}}) @@ -1454,7 +1454,7 @@ def test_flavor_access_remove_by_id(self): {'removeTenantAccess': {'tenant': 'proj2'}}) def test_flavor_access_remove_by_name(self): - self.run_command(['flavor-access-remove', '512 MB Server', 'proj2']) + self.run_command(['flavor-access-remove', '512 MiB Server', 'proj2']) self.assert_called('POST', '/flavors/2/action', {'removeTenantAccess': {'tenant': 'proj2'}}) @@ -2336,8 +2336,8 @@ def test_usage_list(self): 'start=2000-01-20T00:00:00&' + 'end=2005-02-01T00:00:00&' + 'detailed=1') - # Servers, RAM MB-Hours, CPU Hours, Disk GiB-Hours - self.assertIn('1 | 25451.76 | 49.71 | 0.00', stdout) + # Servers, RAM MiB-Hours, CPU Hours, Disk GiB-Hours + self.assertIn('1 | 25451.76 | 49.71 | 0.00', stdout) def test_usage_list_stitch_together_next_results(self): cmd = 'usage-list --start 2000-01-20 --end 2005-02-01' @@ -2357,8 +2357,8 @@ def test_usage_list_stitch_together_next_results(self): 'start=2000-01-20T00:00:00&' 'end=2005-02-01T00:00:00&' 'marker=%s&detailed=1' % (marker), pos=pos + 1) - # Servers, RAM MB-Hours, CPU Hours, Disk GiB-Hours - self.assertIn('2 | 50903.53 | 99.42 | 0.00', stdout) + # Servers, RAM MiB-Hours, CPU Hours, Disk GiB-Hours + self.assertIn('2 | 50903.53 | 99.42 | 0.00', stdout) def test_usage_list_no_args(self): timeutils.set_time_override(datetime.datetime(2005, 2, 1, 0, 0)) @@ -2377,8 +2377,8 @@ def test_usage(self): '/os-simple-tenant-usage/test?' + 'start=2000-01-20T00:00:00&' + 'end=2005-02-01T00:00:00') - # Servers, RAM MB-Hours, CPU Hours, Disk GiB-Hours - self.assertIn('1 | 25451.76 | 49.71 | 0.00', stdout) + # Servers, RAM MiB-Hours, CPU Hours, Disk GiB-Hours + self.assertIn('1 | 25451.76 | 49.71 | 0.00', stdout) def test_usage_stitch_together_next_results(self): cmd = 'usage --start 2000-01-20 --end 2005-02-01' @@ -2397,8 +2397,8 @@ def test_usage_stitch_together_next_results(self): 'start=2000-01-20T00:00:00&' 'end=2005-02-01T00:00:00&' 'marker=%s' % (marker), pos=pos + 1) - # Servers, RAM MB-Hours, CPU Hours, Disk GiB-Hours - self.assertIn('2 | 50903.53 | 99.42 | 0.00', stdout) + # Servers, RAM MiB-Hours, CPU Hours, Disk GiB-Hours + self.assertIn('2 | 50903.53 | 99.42 | 0.00', stdout) def test_usage_no_tenant(self): self.run_command('usage --start 2000-01-20 --end 2005-02-01') diff --git a/novaclient/v2/flavors.py b/novaclient/v2/flavors.py index f015a06e8..9cc2b6433 100644 --- a/novaclient/v2/flavors.py +++ b/novaclient/v2/flavors.py @@ -114,7 +114,7 @@ def list(self, detailed=True, is_public=True, marker=None, min_disk=None, :param marker: Begin returning flavors that appear later in the flavor list than that represented by this flavor id (optional). :param min_disk: Filters the flavors by a minimum disk space, in GiB. - :param min_ram: Filters the flavors by a minimum RAM, in MB. + :param min_ram: Filters the flavors by a minimum RAM, in MiB. :param limit: maximum number of flavors to return (optional). :param sort_key: Flavors list sort key (optional). :param sort_dir: Flavors list sort direction (optional). @@ -183,14 +183,14 @@ def create(self, name, ram, vcpus, disk, flavorid="auto", """Create a flavor. :param name: Descriptive name of the flavor - :param ram: Memory in MB for the flavor + :param ram: Memory in MiB for the flavor :param vcpus: Number of VCPUs for the flavor :param disk: Size of local disk in GiB :param flavorid: ID for the flavor (optional). You can use the reserved value ``"auto"`` to have Nova generate a UUID for the flavor in cases where you cannot simply pass ``None``. :param ephemeral: Ephemeral disk space in GiB. - :param swap: Swap space in MB + :param swap: Swap space in MiB :param rxtx_factor: RX/TX factor :param is_public: Whether or not the flavor is public. :param description: A free form description of the flavor. diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index b993fbc0d..a67a3323d 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -644,7 +644,7 @@ def _boot(cs, args): "if omitted, hypervisor driver chooses suitable device " "depending on selected bus; note the libvirt driver always " "uses default device names), " - "size=size of the block device in MB(for swap) and in " + "size=size of the block device in MiB(for swap) and in " "GiB(for other formats) " "(if omitted, hypervisor driver calculates size), " "format=device will be formatted (e.g. swap, ntfs, ...; optional), " @@ -674,7 +674,7 @@ def _boot(cs, args): "if omitted, hypervisor driver chooses suitable device " "depending on selected bus; note the libvirt driver always " "uses default device names), " - "size=size of the block device in MB(for swap) and in " + "size=size of the block device in MiB(for swap) and in " "GiB(for other formats) " "(if omitted, hypervisor driver calculates size), " "format=device will be formatted (e.g. swap, ntfs, ...; optional), " @@ -703,7 +703,7 @@ def _boot(cs, args): "if omitted, hypervisor driver chooses suitable device " "depending on selected bus; note the libvirt driver always " "uses default device names), " - "size=size of the block device in MB(for swap) and in " + "size=size of the block device in MiB(for swap) and in " "GiB(for other formats) " "(if omitted, hypervisor driver calculates size), " "format=device will be formatted (e.g. swap, ntfs, ...; optional), " @@ -732,7 +732,7 @@ def _boot(cs, args): "if omitted, hypervisor driver chooses suitable device " "depending on selected bus; note the libvirt driver always " "uses default device names), " - "size=size of the block device in MB(for swap) and in " + "size=size of the block device in MiB(for swap) and in " "GiB(for other formats) " "(if omitted, hypervisor driver calculates size), " "format=device will be formatted (e.g. swap, ntfs, ...; optional), " @@ -761,7 +761,7 @@ def _boot(cs, args): "if omitted, hypervisor driver chooses suitable device " "depending on selected bus; note the libvirt driver always " "uses default device names), " - "size=size of the block device in MB(for swap) and in " + "size=size of the block device in MiB(for swap) and in " "GiB(for other formats) " "(if omitted, hypervisor driver calculates size), " "format=device will be formatted (e.g. swap, ntfs, ...; optional), " @@ -778,7 +778,7 @@ def _boot(cs, args): '--swap', metavar="", default=None, - help=_("Create and attach a local swap block device of MB.")) + help=_("Create and attach a local swap block device of MiB.")) @utils.arg( '--ephemeral', metavar="size=[,format=]", @@ -1060,7 +1060,7 @@ def _translate_extended_states(collection): def _translate_flavor_keys(collection): - _translate_keys(collection, [('ram', 'memory_mb')]) + _translate_keys(collection, [('ram', 'memory_mib')]) def _print_flavor_extra_specs(flavor): @@ -1076,7 +1076,7 @@ def _print_flavor_list(cs, flavors, show_extra_specs=False): headers = [ 'ID', 'Name', - 'Memory_MB', + 'Memory_MiB', 'Disk', 'Ephemeral', 'Swap', @@ -1129,7 +1129,7 @@ def _print_flavor_list(cs, flavors, show_extra_specs=False): dest='min_ram', metavar='', default=None, - help=_('Filters the flavors by a minimum RAM, in MB.')) + help=_('Filters the flavors by a minimum RAM, in MiB.')) @utils.arg( '--limit', dest='limit', @@ -1196,7 +1196,7 @@ def do_flavor_show(cs, args): @utils.arg( 'ram', metavar='', - help=_("Memory size in MB.")) + help=_("Memory size in MiB.")) @utils.arg( 'disk', metavar='', @@ -1213,7 +1213,7 @@ def do_flavor_show(cs, args): @utils.arg( '--swap', metavar='', - help=_("Additional swap space size in MB (default 0)."), + help=_("Additional swap space size in MiB (default 0)."), default=0) @utils.arg( '--rxtx-factor', @@ -3087,7 +3087,7 @@ def _merge_usage_list(usages, next_usage_list): def do_usage_list(cs, args): """List usage data for all tenants.""" dateformat = "%Y-%m-%d" - rows = ["Tenant ID", "Servers", "RAM MB-Hours", "CPU Hours", + rows = ["Tenant ID", "Servers", "RAM MiB-Hours", "CPU Hours", "Disk GiB-Hours"] now = timeutils.utcnow() @@ -3156,7 +3156,7 @@ def simplify_usage(u): def do_usage(cs, args): """Show usage data for a single tenant.""" dateformat = "%Y-%m-%d" - rows = ["Servers", "RAM MB-Hours", "CPU Hours", "Disk GiB-Hours"] + rows = ["Servers", "RAM MiB-Hours", "CPU Hours", "Disk GiB-Hours"] now = timeutils.utcnow() @@ -4808,13 +4808,13 @@ def do_cell_show(cs, args): def do_cell_capacities(cs, args): """Get cell capacities for all cells or a given cell.""" cell = cs.cells.capacities(args.cell) - print(_("Ram Available: %s MB") % cell.capacities['ram_free']['total_mb']) + print(_("Ram Available: %s MiB") % cell.capacities['ram_free']['total_mb']) utils.print_dict(cell.capacities['ram_free']['units_by_mb'], - dict_property='Ram(MB)', dict_value="Units") - print(_("\nDisk Available: %s MB") % + dict_property='Ram(MiB)', dict_value="Units") + print(_("\nDisk Available: %s MiB") % cell.capacities['disk_free']['total_mb']) utils.print_dict(cell.capacities['disk_free']['units_by_mb'], - dict_property='Disk(MB)', dict_value="Units") + dict_property='Disk(MiB)', dict_value="Units") @utils.arg('server', metavar='', help='Name or ID of server.') From 910201f20157c57e8b24d1f0e82763250f6df0d7 Mon Sep 17 00:00:00 2001 From: Tao Li Date: Fri, 14 Sep 2018 11:09:38 +0800 Subject: [PATCH 1487/1705] Deprecate the unused instance-name The '--instnace-name' option exists in nova CLI for a long time, but it is not used, so we deprecate it firstly and will remove it in T release. Change-Id: I0c3d611fc322ae2c9f28ce3845b1c08eaab69485 Closes-Bug: #1788182 --- novaclient/v2/shell.py | 4 +++- .../deprecate-instance-name-option-bc76629d28f1d456.yaml | 7 +++++++ 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/deprecate-instance-name-option-bc76629d28f1d456.yaml diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 0bf6da8ee..f221a31e2 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -1379,7 +1379,9 @@ def _print_flavor(flavor): dest='instance_name', metavar='', default=None, - help=_('Search with regular expression match by server name.')) + action=shell.DeprecatedAction, + help=_('Search with regular expression match by server name. The option ' + 'is not used and will be removed in T release.')) @utils.arg( '--status', dest='status', diff --git a/releasenotes/notes/deprecate-instance-name-option-bc76629d28f1d456.yaml b/releasenotes/notes/deprecate-instance-name-option-bc76629d28f1d456.yaml new file mode 100644 index 000000000..270d8dcfe --- /dev/null +++ b/releasenotes/notes/deprecate-instance-name-option-bc76629d28f1d456.yaml @@ -0,0 +1,7 @@ +--- +deprecations: + - | + The ``--instance-name`` option has been deprecated from the ``nova list`` + command because the instance name query parameter is ignored by the + compute REST API. + From 3a8d09ec770a56d6ff8589b32152a3e9ceca9b64 Mon Sep 17 00:00:00 2001 From: Alessandro Pilotti Date: Mon, 24 Sep 2018 20:28:08 +0200 Subject: [PATCH 1488/1705] Fixes Python3 issue in decoding password The 'nova get-password' command shows a passowrd as bytes instead of string in Python 3 currently. It should be shown as string. So fix it. Co-Authored-By: Takashi Natsume Change-Id: Ibcfb071fcc3c74535b800295ec95ca5ec8bc3c9b Closes-Bug: #1794167 --- novaclient/crypto.py | 5 ++ novaclient/tests/unit/test_crypto.py | 74 ++++++++++++++++++++++++ novaclient/tests/unit/v2/test_servers.py | 2 +- 3 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 novaclient/tests/unit/test_crypto.py diff --git a/novaclient/crypto.py b/novaclient/crypto.py index 21af60cdc..699dbac0e 100644 --- a/novaclient/crypto.py +++ b/novaclient/crypto.py @@ -16,6 +16,8 @@ import base64 import subprocess +import six + class DecryptionFailure(Exception): pass @@ -35,4 +37,7 @@ def decrypt_password(private_key, password): proc.stdin.close() if proc.returncode: raise DecryptionFailure(err) + + if not six.PY2 and isinstance(out, bytes): + return out.decode('utf-8') return out diff --git a/novaclient/tests/unit/test_crypto.py b/novaclient/tests/unit/test_crypto.py new file mode 100644 index 000000000..0e876a070 --- /dev/null +++ b/novaclient/tests/unit/test_crypto.py @@ -0,0 +1,74 @@ +# Copyright 2018 NTT Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import base64 +import subprocess + +import mock + +from novaclient import crypto +from novaclient.tests.unit import utils + + +class CryptoTest(utils.TestCase): + + def setUp(self): + super(CryptoTest, self).setUp() + # The password string that passed as the method argument + self.password_string = 'Test Password' + # The return value of Popen.communicate + self.decrypt_password = b'Decrypt Password' + self.private_key = 'Test Private Key' + + @mock.patch('subprocess.Popen') + def test_decrypt_password(self, mock_open): + mocked_proc = mock.Mock() + mock_open.return_value = mocked_proc + mocked_proc.returncode = 0 + mocked_proc.communicate.return_value = (self.decrypt_password, '') + + decrypt_password = crypto.decrypt_password(self.private_key, + self.password_string) + + # The return value is 'str' in both python 2 and python 3 + self.assertIsInstance(decrypt_password, str) + self.assertEqual('Decrypt Password', decrypt_password) + + mock_open.assert_called_once_with( + ['openssl', 'rsautl', '-decrypt', '-inkey', self.private_key], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + mocked_proc.communicate.assert_called_once_with( + base64.b64decode(self.password_string)) + mocked_proc.stdin.close.assert_called_once_with() + + @mock.patch('subprocess.Popen') + def test_decrypt_password_failure(self, mock_open): + mocked_proc = mock.Mock() + mock_open.return_value = mocked_proc + mocked_proc.returncode = 1 # Error case + mocked_proc.communicate.return_value = (self.decrypt_password, '') + + self.assertRaises(crypto.DecryptionFailure, crypto.decrypt_password, + self.private_key, self.password_string) + + mock_open.assert_called_once_with( + ['openssl', 'rsautl', '-decrypt', '-inkey', self.private_key], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + mocked_proc.communicate.assert_called_once_with( + base64.b64decode(self.password_string)) + mocked_proc.stdin.close.assert_called_once_with() diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index 427a4eff8..cf3aec562 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -715,7 +715,7 @@ def test_get_password(self): s = self.cs.servers.get(1234) password = s.get_password('novaclient/tests/unit/idfake.pem') self.assert_request_id(password, fakes.FAKE_REQUEST_ID_LIST) - self.assertEqual(b'FooBar123', password) + self.assertEqual('FooBar123', password) self.assert_called('GET', '/servers/1234/os-server-password') def test_get_password_without_key(self): From 1df18e2d99d397bd3f0a8e5d21ba2d68b106e9a6 Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Wed, 21 Nov 2018 13:38:01 +0900 Subject: [PATCH 1489/1705] Fix a type of block_device_mapping_v2 in a comment Change-Id: I9c74402fbc4b2390b64265c6f174bc7e343900cc Closes-Bug: #1804198 --- novaclient/v2/servers.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index e94f3567a..98f22ba2b 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -710,6 +710,7 @@ def _boot(self, response_key, name, image, flavor, body["server"]["availability_zone"] = availability_zone # Block device mappings are passed as a list of dictionaries + # in the create API if block_device_mapping: body['server']['block_device_mapping'] = \ self._parse_block_device_mapping(block_device_mapping) @@ -1229,8 +1230,8 @@ def create(self, name, image, flavor, meta=None, files=None, placement. :param block_device_mapping: (optional extension) A dict of block device mappings for this server. - :param block_device_mapping_v2: (optional extension) A dict of block - device mappings for this server. + :param block_device_mapping_v2: (optional extension) A list of block + device mappings (dicts) for this server. :param nics: An ordered list of nics (dicts) to be added to this server, with information about connected networks, fixed IPs, port etc. From 3f967d975804f5cac4e492bace55b0322dc8fb93 Mon Sep 17 00:00:00 2001 From: qingszhao Date: Fri, 30 Nov 2018 07:10:52 +0000 Subject: [PATCH 1490/1705] Add Python 3.6 classifier to setup.cfg Change-Id: I091c82f60d9ae984cf61be8c6fe95ec68817cb75 --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index 19e05c17b..f2fcef911 100644 --- a/setup.cfg +++ b/setup.cfg @@ -20,6 +20,7 @@ classifier = Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 [files] packages = From a789bd30ae07eff636e6ea3da4d5d8f9d46e8ff1 Mon Sep 17 00:00:00 2001 From: sunjia Date: Mon, 3 Dec 2018 21:41:11 -0500 Subject: [PATCH 1491/1705] Change openstack-dev to openstack-discuss Mailinglists have been updated. Openstack-discuss replaces openstack-dev. Change-Id: I157626d7aa539e583d20f2f8aa39e06188347573 --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index f2fcef911..8ef6c1695 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,7 +5,7 @@ description-file = README.rst license = Apache License, Version 2.0 author = OpenStack -author-email = openstack-dev@lists.openstack.org +author-email = openstack-discuss@lists.openstack.org home-page = https://docs.openstack.org/python-novaclient/latest classifier = Development Status :: 5 - Production/Stable From b13ba0138f6ee2e10ce3b4b95aee37c6cebd1f20 Mon Sep 17 00:00:00 2001 From: zhaolihui Date: Thu, 12 Jul 2018 11:12:54 +0000 Subject: [PATCH 1492/1705] Fix flavor keyerror when nova boot vm When creating a server (the 'nova boot' command), it calls 'POST /servers' API. The response does not have 'addresses' and 'flavor' attributes. When accessing 'networks' attribute in the '_print_server' function, it calls 'GET /servers/{server_id}' and get the 'addresses' and 'flavor' attributes. If 'GET /servers/{server_id}' fails, the server object does not have the 'flavor' attribute, then KeyError is raised when accessing it. Fix the issue by making it raise a CommandError when the 'GET /servers/{server_id}' fails. Co-Authored-By: Takashi Natsume Change-Id: I3ef096c61b0e05a637ab0c4a1027338fa87e4f09 Closes-Bug: #1781368 --- novaclient/tests/unit/v2/test_shell.py | 12 ++++++++++++ novaclient/v2/servers.py | 2 +- novaclient/v2/shell.py | 6 +++++- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 5e9b9951a..ae11d89a0 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -1334,6 +1334,18 @@ def test_boot_with_trusted_image_certificates_arg_and_envvar(self): }}, ) + @mock.patch.object(servers.Server, 'networks', + new_callable=mock.PropertyMock) + def test_boot_with_not_found_when_accessing_addresses_attribute( + self, mock_networks): + mock_networks.side_effect = exceptions.NotFound( + 404, 'Instance %s could not be found.' % FAKE_UUID_1) + ex = self.assertRaises( + exceptions.CommandError, self.run_command, + 'boot --flavor 1 --image %s some-server' % FAKE_UUID_2) + self.assertIn('Instance %s could not be found.' % FAKE_UUID_1, + six.text_type(ex)) + def test_flavor_list(self): out, _ = self.run_command('flavor-list') self.assert_called_anytime('GET', '/flavors/detail') diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index 98f22ba2b..0843e4e3e 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -405,7 +405,7 @@ def networks(self): for network_label, address_list in self.addresses.items(): networks[network_label] = [a['addr'] for a in address_list] return networks - except Exception: + except AttributeError: return {} @api_versions.wraps("2.0", "2.24") diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index ef025a279..c98724c92 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -2368,7 +2368,11 @@ def _print_server(cs, args, server=None, wrap=0): minimal = getattr(args, "minimal", False) - networks = server.networks + try: + networks = server.networks + except Exception as e: + raise exceptions.CommandError(six.text_type(e)) + info = server.to_dict() for network_label, address_list in networks.items(): info['%s network' % network_label] = ', '.join(address_list) From 85e9b58e9b638bc49679d2c7460dd1db7f39f48a Mon Sep 17 00:00:00 2001 From: ZhijunWei Date: Fri, 28 Dec 2018 23:04:07 +0800 Subject: [PATCH 1493/1705] Update hacking version 1. update hacking version to latest 2. fix the pep8 failed Change-Id: I484a40fe3cb868d223a807edcd3e20f5e0ebdf4e --- lower-constraints.txt | 2 +- novaclient/api_versions.py | 8 ++++---- novaclient/client.py | 6 +++--- novaclient/shell.py | 4 ++-- novaclient/tests/unit/fixture_data/server_groups.py | 4 ++-- test-requirements.txt | 2 +- tox.ini | 2 +- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/lower-constraints.txt b/lower-constraints.txt index 5d9d5197d..350574be3 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -25,7 +25,7 @@ futurist==1.2.0 gitdb==0.6.4 GitPython==1.0.1 greenlet==0.4.10 -hacking==0.12.0 +hacking==1.1.0 idna==2.6 iso8601==0.1.11 Jinja2==2.10 diff --git a/novaclient/api_versions.py b/novaclient/api_versions.py index 231765198..5ff52b8f5 100644 --- a/novaclient/api_versions.py +++ b/novaclient/api_versions.py @@ -324,11 +324,11 @@ def update_headers(headers, api_version): def check_headers(response, api_version): """Checks that microversion header is in response.""" if api_version.ver_minor > 0: - if (api_version.ver_minor < 27 - and LEGACY_HEADER_NAME not in response.headers): + if (api_version.ver_minor < 27 and + LEGACY_HEADER_NAME not in response.headers): _warn_missing_microversion_header(LEGACY_HEADER_NAME) - elif (api_version.ver_minor >= 27 - and HEADER_NAME not in response.headers): + elif (api_version.ver_minor >= 27 and + HEADER_NAME not in response.headers): _warn_missing_microversion_header(HEADER_NAME) diff --git a/novaclient/client.py b/novaclient/client.py index 2d2716310..76a2007be 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -30,9 +30,6 @@ from oslo_utils import importutils import pkg_resources -osprofiler_profiler = importutils.try_import("osprofiler.profiler") -osprofiler_web = importutils.try_import("osprofiler.web") - import novaclient from novaclient import api_versions from novaclient import exceptions @@ -40,6 +37,9 @@ from novaclient.i18n import _ from novaclient import utils +osprofiler_profiler = importutils.try_import("osprofiler.profiler") +osprofiler_web = importutils.try_import("osprofiler.web") + # TODO(jichenjc): when an extension in contrib is moved to core extension, # Add the name into the following list, then after last patch merged, # remove the whole function diff --git a/novaclient/shell.py b/novaclient/shell.py index 8eb4311ce..2f6a33e81 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -29,8 +29,6 @@ from oslo_utils import strutils import six -osprofiler_profiler = importutils.try_import("osprofiler.profiler") - import novaclient from novaclient import api_versions from novaclient import client @@ -39,6 +37,8 @@ from novaclient.i18n import _ from novaclient import utils +osprofiler_profiler = importutils.try_import("osprofiler.profiler") + DEFAULT_MAJOR_OS_COMPUTE_API_VERSION = "2.0" # The default behaviour of nova client CLI is that CLI negotiates with server # to find out the most recent version between client and server, and diff --git a/novaclient/tests/unit/fixture_data/server_groups.py b/novaclient/tests/unit/fixture_data/server_groups.py index 5c4c88c6f..d2fd43c7d 100644 --- a/novaclient/tests/unit/fixture_data/server_groups.py +++ b/novaclient/tests/unit/fixture_data/server_groups.py @@ -87,8 +87,8 @@ def setUp(self): headers=headers) self.requests_mock.get(self.url(all_projects=True), - json={'server_groups': server_groups - + other_project_server_groups}, + json={'server_groups': server_groups + + other_project_server_groups}, headers=headers) self.requests_mock.get(self.url(limit=2, offset=1), diff --git a/test-requirements.txt b/test-requirements.txt index b01fb80af..83326f074 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,7 +1,7 @@ # 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. -hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0 +hacking>=1.1.0,<1.2.0 # Apache-2.0 bandit>=1.1.0 # Apache-2.0 coverage!=4.4,>=4.0 # Apache-2.0 diff --git a/tox.ini b/tox.ini index 508966826..0e6b4bbeb 100644 --- a/tox.ini +++ b/tox.ini @@ -93,7 +93,7 @@ commands = # Following checks are ignored on purpose. # # Additional checks are also ignored on purpose: F811, F821 -ignore = F811,F821,H404,H405 +ignore = E731,F811,F821,H404,H405 show-source = True exclude=.venv,.git,.tox,dist,*lib/python*,*egg,build,doc/source/conf.py,releasenotes From 4ceba48697201dede5d17020ca7bc4f5af244ea5 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Fri, 28 Dec 2018 08:44:02 -0500 Subject: [PATCH 1494/1705] Add a note in "nova service-delete" help about deleting computes This mirrors the note in the API reference from change I68f2074814c3ae890888a5c75fd2870bb99f0e08 to the service-delete CLI help and docs. Change-Id: I191f6e6a4b7c6c456afbd33b0256842b043c772e Related-Bug: #1646255 --- doc/source/cli/nova.rst | 6 ++++++ novaclient/v2/shell.py | 16 ++++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/doc/source/cli/nova.rst b/doc/source/cli/nova.rst index b8a212d49..ef4342811 100644 --- a/doc/source/cli/nova.rst +++ b/doc/source/cli/nova.rst @@ -3285,6 +3285,12 @@ nova service-delete Delete the service. +.. important:: If deleting a nova-compute service, be sure to stop the actual + ``nova-compute`` process on the physical host *before* deleting the + service with this command. Failing to do so can lead to the running + service re-creating orphaned **compute_nodes** table records in the + database. + **Positional arguments:** ```` diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index c98724c92..033d94121 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -3688,7 +3688,13 @@ def do_service_force_down(cs, args): help=_('ID of service as an integer. Note that this may not ' 'uniquely identify a service in a multi-cell deployment.')) def do_service_delete(cs, args): - """Delete the service by integer ID.""" + """Delete the service by integer ID. + + If deleting a nova-compute service, be sure to stop the actual + nova-compute process on the physical host before deleting the + service with this command. Failing to do so can lead to the running + service re-creating orphaned compute_nodes table records in the database. + """ cs.services.delete(args.id) @@ -3696,7 +3702,13 @@ def do_service_delete(cs, args): @api_versions.wraps('2.53') @utils.arg('id', metavar='', help=_('ID of service as a UUID.')) def do_service_delete(cs, args): - """Delete the service by UUID ID.""" + """Delete the service by UUID ID. + + If deleting a nova-compute service, be sure to stop the actual + nova-compute process on the physical host before deleting the + service with this command. Failing to do so can lead to the running + service re-creating orphaned compute_nodes table records in the database. + """ cs.services.delete(args.id) From 8eb7d1c5cc0d30c9e68aeb62dd7c73e4377e9fb0 Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Thu, 7 Feb 2019 13:22:00 +0900 Subject: [PATCH 1495/1705] Remove deprecated novaclient.v2.contrib modules All modules of novaclient.v2.contrib have been removed. The 'only_contrib' parameter for the 'novaclient.client.discover_extensions' method is no longer valid. Change-Id: I6da83057dda1f27afe98a2412bc0815f100f34a4 --- doc/source/conf.py | 3 +- novaclient/client.py | 13 ----- novaclient/tests/unit/test_client.py | 17 ------- novaclient/v2/client.py | 10 ---- novaclient/v2/contrib/__init__.py | 51 ------------------- .../v2/contrib/assisted_volume_snapshots.py | 29 ----------- novaclient/v2/contrib/cells.py | 23 --------- novaclient/v2/contrib/deferred_delete.py | 17 ------- novaclient/v2/contrib/host_evacuate.py | 22 -------- novaclient/v2/contrib/host_evacuate_live.py | 18 ------- novaclient/v2/contrib/host_servers_migrate.py | 22 -------- novaclient/v2/contrib/instance_action.py | 22 -------- novaclient/v2/contrib/list_extensions.py | 23 --------- novaclient/v2/contrib/metadata_extensions.py | 19 ------- novaclient/v2/contrib/migrations.py | 24 --------- .../v2/contrib/server_external_events.py | 29 ----------- .../remove-contrib-8b5e35ac8dddbab3.yaml | 5 ++ 17 files changed, 6 insertions(+), 341 deletions(-) delete mode 100644 novaclient/v2/contrib/__init__.py delete mode 100644 novaclient/v2/contrib/assisted_volume_snapshots.py delete mode 100644 novaclient/v2/contrib/cells.py delete mode 100644 novaclient/v2/contrib/deferred_delete.py delete mode 100644 novaclient/v2/contrib/host_evacuate.py delete mode 100644 novaclient/v2/contrib/host_evacuate_live.py delete mode 100644 novaclient/v2/contrib/host_servers_migrate.py delete mode 100644 novaclient/v2/contrib/instance_action.py delete mode 100644 novaclient/v2/contrib/list_extensions.py delete mode 100644 novaclient/v2/contrib/metadata_extensions.py delete mode 100644 novaclient/v2/contrib/migrations.py delete mode 100644 novaclient/v2/contrib/server_external_events.py create mode 100644 releasenotes/notes/remove-contrib-8b5e35ac8dddbab3.yaml diff --git a/doc/source/conf.py b/doc/source/conf.py index b98da5d9b..ffd5782d6 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -26,8 +26,7 @@ apidoc_module_dir = '../../novaclient' apidoc_output_dir = 'reference/api' apidoc_excluded_paths = [ - 'tests/*', - 'v2/contrib/*'] + 'tests/*'] apidoc_separate_modules = True # The content that will be inserted into the main body of an autoclass diff --git a/novaclient/client.py b/novaclient/client.py index 76a2007be..2808428ed 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -40,11 +40,6 @@ osprofiler_profiler = importutils.try_import("osprofiler.profiler") osprofiler_web = importutils.try_import("osprofiler.web") -# TODO(jichenjc): when an extension in contrib is moved to core extension, -# Add the name into the following list, then after last patch merged, -# remove the whole function -extensions_ignored_name = ["__init__"] - class SessionClient(adapter.LegacyJsonAdapter): @@ -178,14 +173,6 @@ def discover_extensions(*args, **kwargs): """Returns the list of extensions, which can be discovered by python path and by entry-point 'novaclient.extension'. """ - # TODO(mriedem): Remove support for 'only_contrib' in Queens. - if 'only_contrib' in kwargs and kwargs['only_contrib']: - warnings.warn(_('Discovering extensions only by contrib path is no ' - 'longer supported since all contrib extensions ' - 'have either been made required or removed. The ' - 'only_contrib argument is deprecated and will be ' - 'removed in a future release.')) - return [] chain = itertools.chain(_discover_via_python_path(), _discover_via_entry_points()) return [ext.Extension(name, module) for name, module in chain] diff --git a/novaclient/tests/unit/test_client.py b/novaclient/tests/unit/test_client.py index 97cfbf31f..3411eb505 100644 --- a/novaclient/tests/unit/test_client.py +++ b/novaclient/tests/unit/test_client.py @@ -109,23 +109,6 @@ def f(*args, **kwargs): mock_discover_via_entry_points.assert_called_once_with() self.assertEqual([mock_extension()] * 4, result) - @mock.patch('novaclient.client.warnings') - @mock.patch("novaclient.client._discover_via_entry_points") - @mock.patch("novaclient.client._discover_via_python_path") - @mock.patch("novaclient.extension.Extension") - def test_discover_extensions_only_contrib( - self, mock_extension, mock_discover_via_python_path, - mock_discover_via_entry_points, mock_warnings): - - version = novaclient.api_versions.APIVersion("2.0") - - self.assertEqual([], novaclient.client.discover_extensions( - version, only_contrib=True)) - self.assertFalse(mock_discover_via_python_path.called) - self.assertFalse(mock_discover_via_entry_points.called) - self.assertFalse(mock_extension.called) - self.assertTrue(mock_warnings.warn.called) - @mock.patch("novaclient.client.warnings") def test__check_arguments(self, mock_warnings): release = "Coolest" diff --git a/novaclient/v2/client.py b/novaclient/v2/client.py index ee4fc3453..54abcf27d 100644 --- a/novaclient/v2/client.py +++ b/novaclient/v2/client.py @@ -23,7 +23,6 @@ from novaclient.v2 import assisted_volume_snapshots from novaclient.v2 import availability_zones from novaclient.v2 import cells -from novaclient.v2 import contrib from novaclient.v2 import flavor_access from novaclient.v2 import flavors from novaclient.v2 import hypervisors @@ -182,15 +181,6 @@ def __init__(self, # Add in any extensions... if extensions: for extension in extensions: - # do not import extensions from contrib directory twice. - if extension.name in contrib.V2_0_EXTENSIONS: - # NOTE(andreykurilin): this message looks more like - # warning or note, but it is not critical, so let's do - # not flood "warning" logging level and use just debug.. - self.logger.debug("Nova 2.0 extenstion '%s' is auto-loaded" - " by default. You do not need to specify" - " it manually.", extension.name) - continue if extension.manager_class: setattr(self, extension.name, extension.manager_class(self)) diff --git a/novaclient/v2/contrib/__init__.py b/novaclient/v2/contrib/__init__.py deleted file mode 100644 index 3cbadc17b..000000000 --- a/novaclient/v2/contrib/__init__.py +++ /dev/null @@ -1,51 +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. - -import inspect -import warnings - -from novaclient.i18n import _ - -# NOTE(andreykurilin): "tenant_networks" extension excluded -# here deliberately. It was deprecated separately from deprecation -# extension mechanism and I prefer to not auto-load it by default -# (V2_0_EXTENSIONS is designed for such behaviour). -V2_0_EXTENSIONS = { - 'assisted_volume_snapshots': - 'novaclient.v2.assisted_volume_snapshots', - 'cells': 'novaclient.v2.cells', - 'instance_action': 'novaclient.v2.instance_action', - 'list_extensions': 'novaclient.v2.list_extensions', - 'migrations': 'novaclient.v2.migrations', - 'server_external_events': 'novaclient.v2.server_external_events', -} - - -def warn(alternative=True): - """Prints warning msg for contrib modules.""" - frm = inspect.stack()[1] - module_name = inspect.getmodule(frm[0]).__name__ - if module_name.startswith("novaclient.v2.contrib."): - if alternative: - new_module_name = module_name.replace("contrib.", "") - msg = _("Module `%(module)s` is deprecated as of OpenStack " - "Ocata in favor of `%(new_module)s` and will be " - "removed after OpenStack Pike.") % { - "module": module_name, "new_module": new_module_name} - - if not alternative: - msg = _("Module `%s` is deprecated as of OpenStack Ocata " - "All shell commands were moved to " - "`novaclient.v2.shell` and will be automatically " - "loaded.") % module_name - - warnings.warn(msg) diff --git a/novaclient/v2/contrib/assisted_volume_snapshots.py b/novaclient/v2/contrib/assisted_volume_snapshots.py deleted file mode 100644 index 95b5073a2..000000000 --- a/novaclient/v2/contrib/assisted_volume_snapshots.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright (C) 2013, Red Hat, Inc. -# -# 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. - -""" -Assisted volume snapshots - to be used by Cinder and not end users. -""" - -from novaclient.v2 import assisted_volume_snapshots -from novaclient.v2 import contrib - - -AssistedSnapshotManager = assisted_volume_snapshots.AssistedSnapshotManager -Snapshot = assisted_volume_snapshots.Snapshot - -manager_class = AssistedSnapshotManager -name = 'assisted_volume_snapshots' - -contrib.warn() diff --git a/novaclient/v2/contrib/cells.py b/novaclient/v2/contrib/cells.py deleted file mode 100644 index a689d00a9..000000000 --- a/novaclient/v2/contrib/cells.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright 2013 Rackspace Hosting -# All Rights Reserved. -# -# 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 novaclient.v2 import cells -from novaclient.v2 import contrib - - -Cell = cells.Cell -CellsManager = cells.CellsManager - -contrib.warn() diff --git a/novaclient/v2/contrib/deferred_delete.py b/novaclient/v2/contrib/deferred_delete.py deleted file mode 100644 index bd5798ac7..000000000 --- a/novaclient/v2/contrib/deferred_delete.py +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright 2013 OpenStack Foundation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from novaclient.v2 import contrib - -contrib.warn(alternative=False) diff --git a/novaclient/v2/contrib/host_evacuate.py b/novaclient/v2/contrib/host_evacuate.py deleted file mode 100644 index b5305bcb7..000000000 --- a/novaclient/v2/contrib/host_evacuate.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright 2013 Rackspace Hosting -# All Rights Reserved. -# -# 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 novaclient.v2 import contrib -from novaclient.v2 import shell - - -EvacuateHostResponse = shell.EvacuateHostResponse - -contrib.warn(alternative=False) diff --git a/novaclient/v2/contrib/host_evacuate_live.py b/novaclient/v2/contrib/host_evacuate_live.py deleted file mode 100644 index 7e27d7359..000000000 --- a/novaclient/v2/contrib/host_evacuate_live.py +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright 2014 OpenStack Foundation -# All Rights Reserved. -# -# 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 novaclient.v2 import contrib - -contrib.warn(alternative=False) diff --git a/novaclient/v2/contrib/host_servers_migrate.py b/novaclient/v2/contrib/host_servers_migrate.py deleted file mode 100644 index e88da876b..000000000 --- a/novaclient/v2/contrib/host_servers_migrate.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright 2013 Rackspace Hosting -# All Rights Reserved. -# -# 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 novaclient.v2 import contrib -from novaclient.v2 import shell - - -HostServersMigrateResponse = shell.HostServersMigrateResponse - -contrib.warn(alternative=False) diff --git a/novaclient/v2/contrib/instance_action.py b/novaclient/v2/contrib/instance_action.py deleted file mode 100644 index a90b99bd5..000000000 --- a/novaclient/v2/contrib/instance_action.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright 2013 Rackspace Hosting -# All Rights Reserved. -# -# 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 novaclient.v2 import contrib -from novaclient.v2 import instance_action - - -InstanceActionManager = instance_action.InstanceActionManager - -contrib.warn() diff --git a/novaclient/v2/contrib/list_extensions.py b/novaclient/v2/contrib/list_extensions.py deleted file mode 100644 index 07f4e3d7f..000000000 --- a/novaclient/v2/contrib/list_extensions.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright 2011 OpenStack Foundation -# All Rights Reserved. -# -# 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 novaclient.v2 import contrib -from novaclient.v2 import list_extensions - - -ListExtResource = list_extensions.ListExtResource -ListExtManager = list_extensions.ListExtManager - -contrib.warn() diff --git a/novaclient/v2/contrib/metadata_extensions.py b/novaclient/v2/contrib/metadata_extensions.py deleted file mode 100644 index eb6fbd224..000000000 --- a/novaclient/v2/contrib/metadata_extensions.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright 2013 Rackspace Hosting -# All Rights Reserved. -# -# 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 novaclient.v2 import contrib - - -contrib.warn(alternative=False) diff --git a/novaclient/v2/contrib/migrations.py b/novaclient/v2/contrib/migrations.py deleted file mode 100644 index 909b569c6..000000000 --- a/novaclient/v2/contrib/migrations.py +++ /dev/null @@ -1,24 +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. - -""" -migration interface -""" - -from novaclient.v2 import contrib -from novaclient.v2 import migrations - - -Migration = migrations.Migration -MigrationManager = migrations.MigrationManager - -contrib.warn() diff --git a/novaclient/v2/contrib/server_external_events.py b/novaclient/v2/contrib/server_external_events.py deleted file mode 100644 index bbd1b032f..000000000 --- a/novaclient/v2/contrib/server_external_events.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright (C) 2014, Red Hat, Inc. -# -# 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. - -""" -External event triggering for servers, not to be used by users. -""" - -from novaclient.v2 import contrib -from novaclient.v2 import server_external_events - - -Event = server_external_events.Event -ServerExternalEventManager = server_external_events.ServerExternalEventManager - -manager_class = ServerExternalEventManager -name = 'server_external_events' - -contrib.warn() diff --git a/releasenotes/notes/remove-contrib-8b5e35ac8dddbab3.yaml b/releasenotes/notes/remove-contrib-8b5e35ac8dddbab3.yaml new file mode 100644 index 000000000..413a283d2 --- /dev/null +++ b/releasenotes/notes/remove-contrib-8b5e35ac8dddbab3.yaml @@ -0,0 +1,5 @@ +--- +upgrade: + - All modules of ``novaclient.v2.contrib`` have been removed. + - The ``only_contrib`` parameter for the + ``novaclient.client.discover_extensions`` method is no longer valid. From 162f4769b8da47dfe9bffefab7ed497869b34033 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 6 Feb 2019 11:05:09 +0000 Subject: [PATCH 1496/1705] Microversion 2.68: Remove 'forced' live migrations, evacuations Update the commands and Python API bindings to reflect the new microversion. The various evacuate microversion functions are DRY'd up along the way. Change-Id: Ibfc905292258ffde05800387e5d6bbad4823085c Signed-off-by: Stephen Finucane Depends-On: https://review.openstack.org/#/c/634600/ Implements: blueprint remove-force-flag-from-live-migrate-and-evacuate --- doc/source/cli/nova.rst | 8 +- novaclient/__init__.py | 2 +- novaclient/tests/unit/v2/test_servers.py | 40 ++++ novaclient/tests/unit/v2/test_shell.py | 23 +++ novaclient/v2/servers.py | 176 ++++++++++++------ novaclient/v2/shell.py | 20 +- ...precate-force-option-7116d792bba17f09.yaml | 8 + 7 files changed, 214 insertions(+), 63 deletions(-) create mode 100644 releasenotes/notes/deprecate-force-option-7116d792bba17f09.yaml diff --git a/doc/source/cli/nova.rst b/doc/source/cli/nova.rst index b8a212d49..f3bc8eab9 100644 --- a/doc/source/cli/nova.rst +++ b/doc/source/cli/nova.rst @@ -1249,7 +1249,7 @@ Evacuate server from failed host. ``--force`` Force an evacuation by not verifying the provided destination host by the - scheduler. (Supported by API versions '2.29' -'2.latest') + scheduler. (Supported by API versions '2.29' - '2.67') .. warning:: This could result in failures to actually evacuate the server to the specified host. It is recommended to either not specify @@ -1650,7 +1650,7 @@ Evacuate all instances from failed host. ``--force`` Force an evacuation by not verifying the provided destination host by the - scheduler. (Supported by API versions '2.29' -'2.latest') + scheduler. (Supported by API versions '2.29' - '2.67') .. warning:: This could result in failures to actually evacuate the server to the specified host. It is recommended to either not specify @@ -1701,7 +1701,7 @@ Live migrate all instances off the specified host to other available hosts. ``--force`` Force a live-migration by not verifying the provided destination host by - the scheduler. (Supported by API versions '2.30' -'2.latest') + the scheduler. (Supported by API versions '2.30' - '2.67') .. warning:: This could result in failures to actually live migrate the servers to the specified host. It is recommended to either not specify @@ -2369,7 +2369,7 @@ Migrate running server to a new machine. ``--force`` Force a live-migration by not verifying the provided destination host by - the scheduler. (Supported by API versions '2.30' -'2.latest') + the scheduler. (Supported by API versions '2.30' - '2.67') .. warning:: This could result in failures to actually live migrate the server to the specified host. It is recommended to either not specify diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 43a8d1a94..a4eef58c7 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.67") +API_MAX_VERSION = api_versions.APIVersion("2.68") diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index 34ce21f99..e153fd571 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -1655,3 +1655,43 @@ def test_create_server_boot_from_volume_with_volume_type_pre_267(self): nics='none', block_device_mapping_v2=bdm) self.assertIn("Block device volume_type is not supported before " "microversion 2.67", six.text_type(ex)) + + +class ServersV268Test(ServersV267Test): + + api_version = "2.68" + + def test_evacuate(self): + s = self.cs.servers.get(1234) + ret = s.evacuate('fake_target_host') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action', + {'evacuate': {'host': 'fake_target_host'}}) + + ret = self.cs.servers.evacuate(s, 'fake_target_host') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action', + {'evacuate': {'host': 'fake_target_host'}}) + + ex = self.assertRaises(TypeError, self.cs.servers.evacuate, + 'fake_target_host', force=True) + self.assertIn('force', six.text_type(ex)) + + def test_live_migrate_server(self): + s = self.cs.servers.get(1234) + ret = s.live_migrate(host='hostname', block_migration='auto') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action', + {'os-migrateLive': {'host': 'hostname', + 'block_migration': 'auto'}}) + + ret = self.cs.servers.live_migrate(s, host='hostname', + block_migration='auto') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action', + {'os-migrateLive': {'host': 'hostname', + 'block_migration': 'auto'}}) + + ex = self.assertRaises(TypeError, self.cs.servers.live_migrate, + host='hostname', force=True) + self.assertIn('force', six.text_type(ex)) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index ae11d89a0..fbf939104 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2690,6 +2690,18 @@ def test_live_migration_v2_30(self): 'block_migration': 'auto', 'force': True}}) + def test_live_migration_v2_68(self): + self.run_command('live-migration sample-server hostname', + api_version='2.68') + self.assert_called('POST', '/servers/1234/action', + {'os-migrateLive': {'host': 'hostname', + 'block_migration': 'auto'}}) + + self.assertRaises( + SystemExit, self.run_command, + 'live-migration --force sample-server hostname', + api_version='2.68') + def test_live_migration_force_complete(self): self.run_command('live-migration-force-complete sample-server 1', api_version='2.22') @@ -3437,6 +3449,17 @@ def test_evacuate_v2_29(self): {'evacuate': {'host': 'new_host', 'force': True}}) + def test_evacuate_v2_68(self): + self.run_command('evacuate sample-server new_host', + api_version='2.68') + self.assert_called('POST', '/servers/1234/action', + {'evacuate': {'host': 'new_host'}}) + + self.assertRaises( + SystemExit, self.run_command, + 'evacuate --force sample-server new_host', + api_version='2.68') + def test_evacuate_with_no_target_host(self): self.run_command('evacuate sample-server') self.assert_called('POST', '/servers/1234/action', diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index 0843e4e3e..fa296bf62 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -444,7 +444,7 @@ def live_migrate(self, host=None, block_migration=None): block_migration = "auto" return self.manager.live_migrate(self, host, block_migration) - @api_versions.wraps("2.30") + @api_versions.wraps("2.30", "2.67") def live_migrate(self, host=None, block_migration=None, force=None): """ Migrates a running instance to a new machine. @@ -459,6 +459,20 @@ def live_migrate(self, host=None, block_migration=None, force=None): block_migration = "auto" return self.manager.live_migrate(self, host, block_migration, force) + @api_versions.wraps("2.68") + def live_migrate(self, host=None, block_migration=None): + """ + Migrates a running instance to a new machine. + + :param host: destination host name. + :param block_migration: if True, do block_migration, the default + value is None which is mapped to 'auto'. + :returns: An instance of novaclient.base.TupleWithMeta + """ + if block_migration is None: + block_migration = "auto" + return self.manager.live_migrate(self, host, block_migration) + def reset_state(self, state='error'): """ Reset the state of an instance to active or error. @@ -524,7 +538,7 @@ def evacuate(self, host=None, password=None): """ return self.manager.evacuate(self, host, password) - @api_versions.wraps("2.29") + @api_versions.wraps("2.29", "2.67") def evacuate(self, host=None, password=None, force=None): """ Evacuate an instance from failed host to specified host. @@ -537,6 +551,18 @@ def evacuate(self, host=None, password=None, force=None): """ return self.manager.evacuate(self, host, password, force) + @api_versions.wraps("2.68") + def evacuate(self, host=None, password=None): + """ + Evacuate an instance from failed host to specified host. + + :param host: Name of the target host + :param password: string to set as admin password on the evacuated + server. + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self.manager.evacuate(self, host, password) + def interface_list(self): """ List interfaces attached to an instance. @@ -1670,6 +1696,24 @@ def delete_meta(self, server, keys): return result + def _live_migrate(self, server, host, block_migration, disk_over_commit, + force): + """Inner function to abstract changes in live migration API.""" + body = { + 'host': host, + 'block_migration': block_migration, + } + + if disk_over_commit is not None: + body['disk_over_commit'] = disk_over_commit + + # NOTE(stephenfin): For some silly reason, we don't set this if it's + # False, hence why we're not explicitly checking against None + if force: + body['force'] = force + + return self._action('os-migrateLive', server, body) + @api_versions.wraps('2.0', '2.24') def live_migrate(self, server, host, block_migration, disk_over_commit): """ @@ -1681,10 +1725,10 @@ def live_migrate(self, server, host, block_migration, disk_over_commit): :param disk_over_commit: if True, allow disk overcommit. :returns: An instance of novaclient.base.TupleWithMeta """ - return self._action('os-migrateLive', server, - {'host': host, - 'block_migration': block_migration, - 'disk_over_commit': disk_over_commit}) + return self._live_migrate(server, host, + block_migration=block_migration, + disk_over_commit=disk_over_commit, + force=None) @api_versions.wraps('2.25', '2.29') def live_migrate(self, server, host, block_migration): @@ -1697,11 +1741,12 @@ def live_migrate(self, server, host, block_migration): 'auto' :returns: An instance of novaclient.base.TupleWithMeta """ - return self._action('os-migrateLive', server, - {'host': host, - 'block_migration': block_migration}) + return self._live_migrate(server, host, + block_migration=block_migration, + disk_over_commit=None, + force=None) - @api_versions.wraps('2.30') + @api_versions.wraps('2.30', '2.67') def live_migrate(self, server, host, block_migration, force=None): """ Migrates a running instance to a new machine. @@ -1713,10 +1758,26 @@ def live_migrate(self, server, host, block_migration, force=None): :param force: forces to bypass the scheduler if host is provided. :returns: An instance of novaclient.base.TupleWithMeta """ - body = {'host': host, 'block_migration': block_migration} - if force: - body['force'] = force - return self._action('os-migrateLive', server, body) + return self._live_migrate(server, host, + block_migration=block_migration, + disk_over_commit=None, + force=force) + + @api_versions.wraps('2.68') + def live_migrate(self, server, host, block_migration): + """ + Migrates a running instance to a new machine. + + :param server: instance id which comes from nova list. + :param host: destination host name. + :param block_migration: if True, do block_migration, can be set as + 'auto' + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self._live_migrate(server, host, + block_migration=block_migration, + disk_over_commit=None, + force=None) def reset_state(self, server, state='error'): """ @@ -1771,78 +1832,89 @@ def list_security_group(self, server): base.getid(server), 'security_groups', SecurityGroup) + def _evacuate(self, server, host, on_shared_storage, password, force): + """Inner function to abstract changes in evacuate API.""" + body = {} + + if on_shared_storage is not None: + body['onSharedStorage'] = on_shared_storage + + if host is not None: + body['host'] = host + + if password is not None: + body['adminPass'] = password + + if force: + body['force'] = force + + resp, body = self._action_return_resp_and_body('evacuate', server, + body) + return base.TupleWithMeta((resp, body), resp) + @api_versions.wraps("2.0", "2.13") def evacuate(self, server, host=None, on_shared_storage=True, password=None): """ Evacuate a server instance. - :param server: The :class:`Server` (or its ID) to share onto. + :param server: The :class:`Server` (or its ID) to evacuate to. :param host: Name of the target host. :param on_shared_storage: Specifies whether instance files located on shared storage :param password: string to set as password on the evacuated server. :returns: An instance of novaclient.base.TupleWithMeta """ - - body = {'onSharedStorage': on_shared_storage} - if host is not None: - body['host'] = host - - if password is not None: - body['adminPass'] = password - - resp, body = self._action_return_resp_and_body('evacuate', server, - body) - return base.TupleWithMeta((resp, body), resp) + return self._evacuate(server, host, + on_shared_storage=on_shared_storage, + password=password, + force=None) @api_versions.wraps("2.14", "2.28") def evacuate(self, server, host=None, password=None): """ Evacuate a server instance. - :param server: The :class:`Server` (or its ID) to share onto. + :param server: The :class:`Server` (or its ID) to evacuate to. :param host: Name of the target host. :param password: string to set as password on the evacuated server. :returns: An instance of novaclient.base.TupleWithMeta """ + return self._evacuate(server, host, + on_shared_storage=None, + password=password, + force=None) - body = {} - if host is not None: - body['host'] = host - - if password is not None: - body['adminPass'] = password - - resp, body = self._action_return_resp_and_body('evacuate', server, - body) - return base.TupleWithMeta((resp, body), resp) - - @api_versions.wraps("2.29") + @api_versions.wraps("2.29", "2.67") def evacuate(self, server, host=None, password=None, force=None): """ Evacuate a server instance. - :param server: The :class:`Server` (or its ID) to share onto. + :param server: The :class:`Server` (or its ID) to evacuate to. :param host: Name of the target host. :param password: string to set as password on the evacuated server. :param force: forces to bypass the scheduler if host is provided. :returns: An instance of novaclient.base.TupleWithMeta """ + return self._evacuate(server, host, + on_shared_storage=None, + password=password, + force=force) - body = {} - if host is not None: - body['host'] = host - - if password is not None: - body['adminPass'] = password - - if force: - body['force'] = force + @api_versions.wraps("2.68") + def evacuate(self, server, host=None, password=None): + """ + Evacuate a server instance. - resp, body = self._action_return_resp_and_body('evacuate', server, - body) - return base.TupleWithMeta((resp, body), resp) + :param server: The :class:`Server` (or its ID) to evacuate to. + :param host: Name of the target host. + :param password: string to set as password on the evacuated server. + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self._evacuate(server, host, + on_shared_storage=None, + password=password, + force=None) def interface_list(self, server): """ diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index c98724c92..a86c0d8ed 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -3464,7 +3464,8 @@ def parser_hosts(fields): 'actually live migrate the server to the specified host. It is ' 'recommended to either not specify a host so that the scheduler ' 'will pick one, or specify a host without --force.'), - start_version='2.30') + start_version='2.30', + end_version='2.67') def do_live_migration(cs, args): """Migrate running server to a new machine.""" @@ -4453,10 +4454,12 @@ def do_quota_class_update(cs, args): 'actually evacuate the server to the specified host. It is ' 'recommended to either not specify a host so that the scheduler ' 'will pick one, or specify a host without --force.'), - start_version='2.29') + start_version='2.29', + end_version='2.67') def do_evacuate(cs, args): """Evacuate server from failed host.""" + # TODO(stephenfin): Simply call '_server_evacuate' instead? server = _find_server(cs, args.server) on_shared_storage = getattr(args, 'on_shared_storage', None) force = getattr(args, 'force', None) @@ -4843,8 +4846,11 @@ def _server_evacuate(cs, server, args): success = True error_message = "" try: - if api_versions.APIVersion("2.29") <= cs.api_version: - # if microversion >= 2.29 + if api_versions.APIVersion('2.68') <= cs.api_version: + # if microversion >= 2.68 + cs.servers.evacuate(server=server['uuid'], host=args.target_host) + elif api_versions.APIVersion('2.29') <= cs.api_version: + # if microversion 2.29 - 2.67 force = getattr(args, 'force', None) cs.servers.evacuate(server=server['uuid'], host=args.target_host, force=force) @@ -4910,7 +4916,8 @@ def _hyper_servers(cs, host, strict): 'actually evacuate the server to the specified host. It is ' 'recommended to either not specify a host so that the scheduler ' 'will pick one, or specify a host without --force.'), - start_version='2.29') + start_version='2.29', + end_version='2.67') @utils.arg( '--strict', dest='strict', @@ -5000,7 +5007,8 @@ def __init__(self, server_uuid, live_migration_accepted, 'actually live migrate the servers to the specified host. It is ' 'recommended to either not specify a host so that the scheduler ' 'will pick one, or specify a host without --force.'), - start_version='2.30') + start_version='2.30', + end_version='2.67') @utils.arg( '--strict', dest='strict', diff --git a/releasenotes/notes/deprecate-force-option-7116d792bba17f09.yaml b/releasenotes/notes/deprecate-force-option-7116d792bba17f09.yaml new file mode 100644 index 000000000..89e5ffed6 --- /dev/null +++ b/releasenotes/notes/deprecate-force-option-7116d792bba17f09.yaml @@ -0,0 +1,8 @@ +--- +upgrade: + - | + Added support for `microversion 2.68`_, which removes the ``--force`` option + from the ``nova evacuate``, ``nova live-migration``, ``nova host-evacuate`` + and ``nova host-evacuate-live`` commands. + + .. _microversion 2.68: https://docs.openstack.org/nova/latest/api_microversion_history.html#id61 From 81ea9887f041a58fae547d5e6efbd60891ab9cab Mon Sep 17 00:00:00 2001 From: Corey Bryant Date: Fri, 15 Feb 2019 03:00:50 -0500 Subject: [PATCH 1497/1705] add python 3.7 unit test job This is a mechanically generated patch to add a unit test job running under Python 3.7. See ML discussion here [1] for context. [1] http://lists.openstack.org/pipermail/openstack-dev/2018-October/135626.html Change-Id: I3c9416b6628d0217b1bd46b51d976806645287fe Story: #2004073 Task: #27433 --- .zuul.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.zuul.yaml b/.zuul.yaml index 67f4db476..f497f3a11 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -19,6 +19,7 @@ - openstack-python-jobs - openstack-python35-jobs - openstack-python36-jobs + - openstack-python37-jobs - publish-openstack-docs-pti - release-notes-jobs-python3 check: From 7e56102201f92212285f6860ee1952fe025d3e21 Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Tue, 19 Feb 2019 11:07:48 +0900 Subject: [PATCH 1498/1705] Fix output of interface-attach command Add output of a result in the 'nova interface-attach' command when it is successful. Make the following methods return a 'NetworkInterface' object instead of a 'Server' object. * The 'interface_attach' method in the 'novaclient.v2.Server' class * The 'interface_attach' method in the 'novaclient.v2.ServerManager' class Remove unnecessary code in the 'nova interface-detach' command because the response body is not returned. Change-Id: Id5316d8ad4a4b67e8399b51e602aafc83bc128c6 Closes-Bug: #1816511 --- novaclient/base.py | 12 ++++++---- .../tests/functional/v2/test_servers.py | 17 +++++++++++++ novaclient/tests/unit/v2/test_servers.py | 2 ++ novaclient/v2/servers.py | 6 +++-- novaclient/v2/shell.py | 24 ++++++++++++------- ...erface-attach-output-02d633d9b2a60da1.yaml | 11 +++++++++ 6 files changed, 58 insertions(+), 14 deletions(-) create mode 100644 releasenotes/notes/interface-attach-output-02d633d9b2a60da1.yaml diff --git a/novaclient/base.py b/novaclient/base.py index 40e100a9d..431ac7d6f 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -358,15 +358,19 @@ def _get(self, url, response_key, filters=None): return self.resource_class(self, content, loaded=True, resp=resp) - def _create(self, url, body, response_key, return_raw=False, **kwargs): + def _create(self, url, body, response_key, return_raw=False, + obj_class=None, **kwargs): self.run_hooks('modify_body_for_create', body, **kwargs) resp, body = self.api.client.post(url, body=body) if return_raw: return self.convert_into_with_meta(body[response_key], resp) - with self.completion_cache('human_id', self.resource_class, mode="a"): - with self.completion_cache('uuid', self.resource_class, mode="a"): - return self.resource_class(self, body[response_key], resp=resp) + if obj_class is None: + obj_class = self.resource_class + + with self.completion_cache('human_id', obj_class, mode="a"): + with self.completion_cache('uuid', obj_class, mode="a"): + return obj_class(self, body[response_key], resp=resp) def _delete(self, url): resp, body = self.api.client.delete(url) diff --git a/novaclient/tests/functional/v2/test_servers.py b/novaclient/tests/functional/v2/test_servers.py index 8d5fd02c9..47fb9e061 100644 --- a/novaclient/tests/functional/v2/test_servers.py +++ b/novaclient/tests/functional/v2/test_servers.py @@ -326,3 +326,20 @@ def test_list(self): flavor_output = self.nova("flavor-show %s" % self.flavor.id) flavor_val = self._get_value_from_the_table(flavor_output, 'disk') self.assertEqual(flavor_val, server_flavor_val) + + +class TestInterfaceAttach(base.ClientTestBase): + + COMPUTE_API_VERSION = '2.latest' + + def test_interface_attach(self): + server = self._create_server() + output = self.nova("interface-attach --net-id %s %s" % + (self.network.id, server.id)) + + for key in ('ip_address', 'mac_addr', 'port_id', 'port_state'): + self._get_value_from_the_table(output, key) + + self.assertEqual( + self.network.id, + self._get_value_from_the_table(output, 'net_id')) diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index e153fd571..c9a3f8d10 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -941,6 +941,7 @@ def test_interface_attach(self): ret = s.interface_attach(None, None, None) self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/os-interface') + self.assertIsInstance(ret, servers.NetworkInterface) def test_interface_detach(self): s = self.cs.servers.get(1234) @@ -1409,6 +1410,7 @@ def test_interface_attach_with_tag(self): {'interfaceAttachment': {'port_id': '7f42712e-63fe-484c-a6df-30ae4867ff66', 'tag': 'test_tag'}}) + self.assertIsInstance(ret, servers.NetworkInterface) def test_add_fixed_ip(self): # novaclient.v2.servers.Server.add_fixed_ip() diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index fa296bf62..eed80694d 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -1944,7 +1944,8 @@ def interface_attach(self, server, port_id, net_id, fixed_ip): {'ip_address': fixed_ip}] return self._create('/servers/%s/os-interface' % base.getid(server), - body, 'interfaceAttachment') + body, 'interfaceAttachment', + obj_class=NetworkInterface) @api_versions.wraps("2.49") def interface_attach(self, server, port_id, net_id, fixed_ip, tag=None): @@ -1973,7 +1974,8 @@ def interface_attach(self, server, port_id, net_id, fixed_ip, tag=None): body['interfaceAttachment']['tag'] = tag return self._create('/servers/%s/os-interface' % base.getid(server), - body, 'interfaceAttachment') + body, 'interfaceAttachment', + obj_class=NetworkInterface) def interface_detach(self, server, port_id): """ diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index a86c0d8ed..eadd7b97d 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -4530,10 +4530,21 @@ def do_interface_attach(cs, args): if 'tag' in args and args.tag: update_kwargs['tag'] = args.tag - res = server.interface_attach(args.port_id, args.net_id, args.fixed_ip, - **update_kwargs) - if isinstance(res, dict): - utils.print_dict(res) + network_interface = server.interface_attach( + args.port_id, args.net_id, args.fixed_ip, **update_kwargs) + + _print_interface(network_interface) + + +def _print_interface(interface): + ni_dict = interface.to_dict() + + fixed_ips = ni_dict.pop('fixed_ips', None) + ni_dict['ip_address'] = (",".join( + [fip['ip_address'] for fip in fixed_ips]) + if fixed_ips is not None else None) + + utils.print_dict(ni_dict) @utils.arg('server', metavar='', help=_('Name or ID of server.')) @@ -4541,10 +4552,7 @@ def do_interface_attach(cs, args): def do_interface_detach(cs, args): """Detach a network interface from a server.""" server = _find_server(cs, args.server) - - res = server.interface_detach(args.port_id) - if isinstance(res, dict): - utils.print_dict(res) + server.interface_detach(args.port_id) @api_versions.wraps("2.17") diff --git a/releasenotes/notes/interface-attach-output-02d633d9b2a60da1.yaml b/releasenotes/notes/interface-attach-output-02d633d9b2a60da1.yaml new file mode 100644 index 000000000..af855a1ea --- /dev/null +++ b/releasenotes/notes/interface-attach-output-02d633d9b2a60da1.yaml @@ -0,0 +1,11 @@ +--- +upgrade: + - The ``nova interface-attach`` command shows output of its result + when it is successful. + - | + The following methods return a ``NetworkInterface`` object + instead of a ``Server`` object. + + * The ``interface_attach`` method in the ``novaclient.v2.Server`` class + * The ``interface_attach`` method in the ``novaclient.v2.ServerManager`` + class From 874b03068f1682cb0fe94d59f64e858b1514308d Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Wed, 20 Feb 2019 11:37:16 -0500 Subject: [PATCH 1499/1705] Make Server.networks use a predictable sort order This changes the Server.networks property method to return an OrderedDict where the keys (network labels) are sorted which will allow for a predictable sort order on the resulting networks attached to a Server resource. This affects the output of "nova list" and "nova show" commands so a simple release note is added to mention the change. Change-Id: I2e9b3c6a256509c045966035da24d58628f1b33b --- novaclient/tests/unit/v2/test_servers.py | 6 ++++++ novaclient/v2/servers.py | 11 +++++++++-- .../server-networks-sorted-1d3a7f1c1f88e846.yaml | 7 +++++++ 3 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/server-networks-sorted-1d3a7f1c1f88e846.yaml diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index e153fd571..8a734f82d 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -453,6 +453,12 @@ def test_find(self): self.assert_request_id(server, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/servers/1234') self.assertEqual('sample-server', server.name) + # The networks should be sorted. + networks = server.networks + self.assertEqual(2, len(networks)) + labels = list(networks) # returns the dict keys + self.assertEqual('private', labels[0]) + self.assertEqual('public', labels[1]) self.assertRaises(exceptions.NoUniqueMatch, self.cs.servers.find, flavor={"id": 1, "name": "256 MiB Server"}) diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index fa296bf62..5e29b1121 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -20,6 +20,7 @@ """ import base64 +import collections from oslo_utils import encodeutils import six @@ -399,10 +400,16 @@ def revert_resize(self): def networks(self): """ Generate a simplified list of addresses + + :returns: An OrderedDict, keyed by network name, and sorted by network + name in ascending order. """ - networks = {} + networks = collections.OrderedDict() try: - for network_label, address_list in self.addresses.items(): + # Sort the keys by network name in natural (ascending) order. + network_labels = sorted(self.addresses.keys()) + for network_label in network_labels: + address_list = self.addresses[network_label] networks[network_label] = [a['addr'] for a in address_list] return networks except AttributeError: diff --git a/releasenotes/notes/server-networks-sorted-1d3a7f1c1f88e846.yaml b/releasenotes/notes/server-networks-sorted-1d3a7f1c1f88e846.yaml new file mode 100644 index 000000000..d6b94706f --- /dev/null +++ b/releasenotes/notes/server-networks-sorted-1d3a7f1c1f88e846.yaml @@ -0,0 +1,7 @@ +--- +other: + - | + The ``novaclient.v2.servers.Server.networks`` property method now returns + an OrderedDict where the keys are sorted in natural (ascending) order. + This means the ``nova show`` and ``nova list`` output will have predictable + sort order on the networks attached to a server. From 14a45183ee9558281fd38a60471adf5db55637c8 Mon Sep 17 00:00:00 2001 From: Surya Seetharaman Date: Mon, 2 Jul 2018 15:46:13 +0200 Subject: [PATCH 1500/1705] API microversion 2.69: Handles Down Cells This patch explicitly points out the change needed while forming the detailed lists for embedded flavor information. In those cases where the server response for nova list has the flavor key missing for the instances in the down cell, the servers will be skipped. Depends-On: https://review.openstack.org/591657/ Related to blueprint handling-down-cell Change-Id: I007d9a68309b0d3aa85a4edf5026043154d4f42a --- doc/source/cli/nova.rst | 12 ++ novaclient/__init__.py | 2 +- novaclient/tests/unit/v2/fakes.py | 116 ++++++++++++++---- novaclient/tests/unit/v2/test_shell.py | 88 +++++++++++++ novaclient/v2/shell.py | 13 +- ...p-handling-down-cell-728cdb1efd1ea75b.yaml | 9 ++ 6 files changed, 216 insertions(+), 24 deletions(-) create mode 100644 releasenotes/notes/bp-handling-down-cell-728cdb1efd1ea75b.yaml diff --git a/doc/source/cli/nova.rst b/doc/source/cli/nova.rst index f3bc8eab9..64bdf2da9 100644 --- a/doc/source/cli/nova.rst +++ b/doc/source/cli/nova.rst @@ -2195,6 +2195,10 @@ nova list List servers. +Note that from microversion 2.69, during partial infrastructure failures in the +deployment, the output of this command may return partial results for the servers +present in the failure domain. + **Optional arguments:** ``--reservation-id `` @@ -3363,6 +3367,10 @@ nova service-list Show a list of all running services. Filter by host & binary. +Note that from microversion 2.69, during partial infrastructure failures in the +deployment, the output of this command may return partial results for the +services present in the failure domain. + **Optional arguments:** ``--host `` @@ -3430,6 +3438,10 @@ nova show Show details about the given server. +Note that from microversion 2.69, during partial infrastructure failures in the +deployment, the output of this command may return partial results for the server +if it exists in the failure domain. + **Positional arguments:** ```` diff --git a/novaclient/__init__.py b/novaclient/__init__.py index a4eef58c7..a41a77af3 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.68") +API_MAX_VERSION = api_versions.APIVersion("2.69") diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index c6f34c344..0b72a6773 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -392,14 +392,32 @@ def get_limits(self, **kw): # def get_servers(self, **kw): - return (200, {}, {"servers": [ + servers = {"servers": [ {'id': '1234', 'name': 'sample-server'}, {'id': '5678', 'name': 'sample-server2'}, {'id': '9014', 'name': 'help'} - ]}) + ]} + if self.api_version >= api_versions.APIVersion('2.69'): + # include "partial results" from non-responsive part of + # infrastructure. + servers['servers'].append( + {'id': '9015', + 'status': "UNKNOWN", + "links": [ + { + "href": "http://fake/v2.1/", + "rel": "self" + }, + { + "href": "http://fake", + "rel": "bookmark" + } + ]} + ) + return (200, {}, servers) def get_servers_detail(self, **kw): - return (200, {}, {"servers": [ + servers = {"servers": [ { "id": '1234', "name": "sample-server", @@ -538,7 +556,29 @@ def get_servers_detail(self, **kw): "hostId": "9e107d9d372bb6826bd81d3542a419d6", "status": "ACTIVE", }, - ]}) + ]} + if self.api_version >= api_versions.APIVersion('2.69'): + # include "partial results" from non-responsive part of + # infrastructure. + servers['servers'].append( + { + "id": "9015", + "status": "UNKNOWN", + "tenant_id": "6f70656e737461636b20342065766572", + "created": "2018-12-03T21:06:18Z", + "links": [ + { + "href": "http://fake/v2.1/", + "rel": "self" + }, + { + "href": "http://fake", + "rel": "bookmark" + } + ] + } + ) + return (200, {}, servers) def post_servers(self, body, **kw): assert set(body.keys()) <= set(['server', 'os:scheduler_hints']) @@ -599,6 +639,27 @@ def get_servers_9014(self, **kw): r = {'server': self.get_servers_detail()[2]['servers'][4]} return (200, {}, r) + def get_servers_9015(self, **kw): + r = {'server': self.get_servers_detail()[2]['servers'][5]} + r['server']["OS-EXT-AZ:availability_zone"] = 'geneva' + r['server']["OS-EXT-STS:power_state"] = 0 + flavor = { + "disk": 1, + "ephemeral": 0, + "original_name": "m1.tiny", + "ram": 512, + "swap": 0, + "vcpus": 1, + "extra_specs": {} + } + image = { + "id": "c99d7632-bd66-4be9-aed5-3dd14b223a76", + } + r['server']['image'] = image + r['server']['flavor'] = flavor + r['server']['user_id'] = "fake" + return (200, {}, r) + def delete_os_server_groups_12345(self, **kw): return (202, {}, None) @@ -1644,24 +1705,35 @@ def get_os_services(self, **kw): else: service_id_1 = 1 service_id_2 = 2 - return (200, FAKE_RESPONSE_HEADERS, - {'services': [{'binary': binary, - 'host': host, - 'zone': 'nova', - 'status': 'enabled', - 'state': 'up', - 'updated_at': datetime.datetime( - 2012, 10, 29, 13, 42, 2), - 'id': service_id_1}, - {'binary': binary, - 'host': host, - 'zone': 'nova', - 'status': 'disabled', - 'state': 'down', - 'updated_at': datetime.datetime( - 2012, 9, 18, 8, 3, 38), - 'id': service_id_2}, - ]}) + services = { + 'services': [ + {'binary': binary, + 'host': host, + 'zone': 'nova', + 'status': 'enabled', + 'state': 'up', + 'updated_at': datetime.datetime( + 2012, 10, 29, 13, 42, 2), + 'id': service_id_1}, + {'binary': binary, + 'host': host, + 'zone': 'nova', + 'status': 'disabled', + 'state': 'down', + 'updated_at': datetime.datetime( + 2012, 9, 18, 8, 3, 38), + 'id': service_id_2}, + ] + } + if self.api_version >= api_versions.APIVersion('2.69'): + services['services'].append( + { + "binary": "nova-compute", + "host": "host-down", + "status": "UNKNOWN" + } + ) + return (200, FAKE_RESPONSE_HEADERS, services) def put_os_services_enable(self, body, **kw): return (200, FAKE_RESPONSE_HEADERS, diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index fbf939104..eecd9315e 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2939,6 +2939,21 @@ def test_services_list_v2_53(self): self.run_command('service-list', api_version='2.53') self.assert_called('GET', '/os-services') + def test_services_list_v269_with_down_cells(self): + """Tests nova service-list at the 2.69 microversion.""" + stdout, _stderr = self.run_command('service-list', api_version='2.69') + self.assertEqual('''\ ++--------------------------------------+--------------+-----------+------+----------+-------+---------------------+-----------------+-------------+ +| Id | Binary | Host | Zone | Status | State | Updated_at | Disabled Reason | Forced down | ++--------------------------------------+--------------+-----------+------+----------+-------+---------------------+-----------------+-------------+ +| 75e9eabc-ed3b-4f11-8bba-add1e7e7e2de | nova-compute | host1 | nova | enabled | up | 2012-10-29 13:42:02 | | | +| 1f140183-c914-4ddf-8757-6df73028aa86 | nova-compute | host1 | nova | disabled | down | 2012-09-18 08:03:38 | | | +| | nova-compute | host-down | | UNKNOWN | | | | | ++--------------------------------------+--------------+-----------+------+----------+-------+---------------------+-----------------+-------------+ +''', # noqa + stdout) + self.assert_called('GET', '/os-services') + def test_services_list_with_host(self): self.run_command('service-list --host host1') self.assert_called('GET', '/os-services?host=host1') @@ -4021,6 +4036,15 @@ def test_versions(self): 63, # There are no version-wrapped shell method changes for this. 65, # There are no version-wrapped shell method changes for this. 67, # There are no version-wrapped shell method changes for this. + 69, # NOTE(tssurya): 2.69 adds support for missing keys in the + # responses of `GET /servers``, ``GET /servers/detail``, + # ``GET /servers/{server_id}`` and ``GET /os-services`` when + # a cell is down to return minimal constructs. From 2.69 and + # upwards, if the response for ``GET /servers/detail`` does + # not have the 'flavor' key for those instances in the down + # cell, they will be handled on the client side by being + # skipped when forming the detailed lists for embedded + # flavor information. ]) versions_supported = set(range(0, novaclient.API_MAX_VERSION.ver_minor + 1)) @@ -4098,6 +4122,70 @@ def test_list_v2_26_not_tags_any(self): self.run_command('list --not-tags-any tag1,tag2', api_version='2.26') self.assert_called('GET', '/servers/detail?not-tags-any=tag1%2Ctag2') + def test_list_detail_v269_with_down_cells(self): + """Tests nova list at the 2.69 microversion.""" + stdout, _stderr = self.run_command('list', api_version='2.69') + self.assertIn('''\ ++------+----------------+---------+------------+-------------+----------------------------------------------+ +| ID | Name | Status | Task State | Power State | Networks | ++------+----------------+---------+------------+-------------+----------------------------------------------+ +| 9015 | | UNKNOWN | N/A | N/A | | +| 9014 | help | ACTIVE | N/A | N/A | | +| 1234 | sample-server | BUILD | N/A | N/A | private=10.11.12.13; public=1.2.3.4, 5.6.7.8 | +| 5678 | sample-server2 | ACTIVE | N/A | N/A | private=10.13.12.13; public=4.5.6.7, 5.6.9.8 | +| 9012 | sample-server3 | ACTIVE | N/A | N/A | private=10.13.12.13; public=4.5.6.7, 5.6.9.8 | +| 9013 | sample-server4 | ACTIVE | N/A | N/A | | ++------+----------------+---------+------------+-------------+----------------------------------------------+ +''', # noqa + stdout) + self.assert_called('GET', '/servers/detail') + + def test_list_v269_with_down_cells(self): + stdout, _stderr = self.run_command( + 'list --minimal', api_version='2.69') + expected = '''\ ++------+----------------+ +| ID | Name | ++------+----------------+ +| 9015 | | +| 9014 | help | +| 1234 | sample-server | +| 5678 | sample-server2 | ++------+----------------+ +''' + self.assertEqual(expected, stdout) + self.assert_called('GET', '/servers') + + def test_show_v269_with_down_cells(self): + stdout, _stderr = self.run_command('show 9015', api_version='2.69') + self.assertEqual('''\ ++-----------------------------+---------------------------------------------------+ +| Property | Value | ++-----------------------------+---------------------------------------------------+ +| OS-EXT-AZ:availability_zone | geneva | +| OS-EXT-STS:power_state | 0 | +| created | 2018-12-03T21:06:18Z | +| flavor:disk | 1 | +| flavor:ephemeral | 0 | +| flavor:extra_specs | {} | +| flavor:original_name | m1.tiny | +| flavor:ram | 512 | +| flavor:swap | 0 | +| flavor:vcpus | 1 | +| id | 9015 | +| image | CentOS 5.2 (c99d7632-bd66-4be9-aed5-3dd14b223a76) | +| status | UNKNOWN | +| tenant_id | 6f70656e737461636b20342065766572 | +| user_id | fake | ++-----------------------------+---------------------------------------------------+ +''', # noqa + stdout) + FAKE_UUID_2 = 'c99d7632-bd66-4be9-aed5-3dd14b223a76' + self.assert_called('GET', '/servers?name=9015', pos=0) + self.assert_called('GET', '/servers?name=9015', pos=1) + self.assert_called('GET', '/servers/9015', pos=2) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=3) + class PollForStatusTestCase(utils.TestCase): @mock.patch("novaclient.v2.shell.time") diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index a86c0d8ed..0ff601561 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -1670,7 +1670,18 @@ def do_list(cs, args): # For detailed lists, if we have embedded flavor information then replace # the "flavor" attribute with more detailed information. if detailed and have_embedded_flavor_info: - _expand_dict_attr(servers, 'flavor') + if cs.api_version >= api_versions.APIVersion('2.69'): + # NOTE(tssurya): From 2.69, we will have the key 'flavor' missing + # in the server response during infrastructure failure situations. + # For those servers with partial constructs we just skip the + # process of expanding the flavor information. + servers_final = [] + for server in servers: + if hasattr(server, 'flavor'): + servers_final.append(server) + _expand_dict_attr(servers_final, 'flavor') + else: + _expand_dict_attr(servers, 'flavor') if servers: cols, fmts = _get_list_table_columns_and_formatters( diff --git a/releasenotes/notes/bp-handling-down-cell-728cdb1efd1ea75b.yaml b/releasenotes/notes/bp-handling-down-cell-728cdb1efd1ea75b.yaml new file mode 100644 index 000000000..0403d80f3 --- /dev/null +++ b/releasenotes/notes/bp-handling-down-cell-728cdb1efd1ea75b.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + From microversion 2.69 the results of ``nova list``, ``nova show`` and + ``nova service-list`` may contain missing information in their outputs + when there are partial infrastructure failure periods in the deployment. + See `Handling Down Cells`_ for more information on the missing keys/info. + + .. _Handling Down Cells: https://developer.openstack.org/api-guide/compute/down_cells.html From 4450e7ba3176cd26cdfa758355f8ed4ad8bacab2 Mon Sep 17 00:00:00 2001 From: asmita singh Date: Wed, 2 Jan 2019 06:59:56 +0000 Subject: [PATCH 1501/1705] Handle unicode multi-byte characters If you pass unicode multi-byte character to 'hypervisor-list' and 'instance-usage-audit-log' commands using --matching and --before options respectively, it returns keyerror. This patch fixes these issues by encoding the host name and date passed to the --matching and --before options respectively. Closes-Bug: #1804156 Change-Id: I9587af7e0fdd921fcaebe3e5c6c5bb40a9393e01 --- .../tests/unit/fixture_data/hypervisors.py | 28 +++++++++++++++++++ novaclient/tests/unit/v2/fakes.py | 8 ++++++ novaclient/tests/unit/v2/test_hypervisors.py | 12 ++++++++ .../unit/v2/test_instance_usage_audit_log.py | 6 ++++ novaclient/v2/hypervisors.py | 4 +++ novaclient/v2/instance_usage_audit_log.py | 4 +++ 6 files changed, 62 insertions(+) diff --git a/novaclient/tests/unit/fixture_data/hypervisors.py b/novaclient/tests/unit/fixture_data/hypervisors.py index 1e706786b..48d7efc86 100644 --- a/novaclient/tests/unit/fixture_data/hypervisors.py +++ b/novaclient/tests/unit/fixture_data/hypervisors.py @@ -10,6 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from oslo_utils import encodeutils +from six.moves.urllib import parse + from novaclient import api_versions from novaclient.tests.unit.fixture_data import base @@ -130,6 +133,31 @@ def setUp(self): json=get_os_hypervisors_search, headers=self.headers) + if uuid_as_id: + get_os_hypervisors_search_u_v2_53 = { + 'error_name': 'BadRequest', + 'message': 'Invalid input for query parameters ' + 'hypervisor_hostname_pattern.', + 'code': 400} + # hypervisor_hostname_pattern is encoded in the url method + url = self.url(hypervisor_hostname_pattern='\\u5de5\\u4f5c') + self.requests_mock.get(url, + json=get_os_hypervisors_search_u_v2_53, + headers=self.headers, status_code=400) + else: + get_os_hypervisors_search_unicode = { + 'error_name': 'NotFound', + 'message': "No hypervisor matching " + "'\\u5de5\\u4f5c' could be found.", + 'code': 404 + } + hypervisor_hostname_pattern = parse.quote(encodeutils.safe_encode( + '\\u5de5\\u4f5c')) + url = self.url(hypervisor_hostname_pattern, 'search') + self.requests_mock.get(url, + json=get_os_hypervisors_search_unicode, + headers=self.headers, status_code=404) + get_hyper_server = { 'hypervisors': [ { diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index c6f34c344..a7ce9ba15 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -126,6 +126,7 @@ def _cs_request(self, url, method, **kwargs): munged_url = munged_url.replace('@', '_') munged_url = munged_url.replace('%20', '_') munged_url = munged_url.replace('%3A', '_') + munged_url = munged_url.replace('%', '_') callback = "%s_%s" % (method.lower(), munged_url) if url is None or callback == "get_http:__nova_api:8774": @@ -176,6 +177,10 @@ def _cs_request(self, url, method, **kwargs): "text": body, "headers": headers, }) + + if status >= 400: + raise exceptions.from_response(r, body, url, method) + return r, body def get_versions(self): @@ -2095,6 +2100,9 @@ def get_os_instance_usage_audit_log_2016_12_10_13_59_59_999999(self, **kw): "total_errors": 3, "total_instances": 6}}) + def get_os_instance_usage_audit_log__5Cu5de5_5Cu4f5c(self, **kw): + return (400, {}, {"Invalid timestamp for date \u6f22\u5b57"}) + def post_servers_uuid1_action(self, **kw): return 202, {}, {} diff --git a/novaclient/tests/unit/v2/test_hypervisors.py b/novaclient/tests/unit/v2/test_hypervisors.py index 0a3a2514f..0c24209bc 100644 --- a/novaclient/tests/unit/v2/test_hypervisors.py +++ b/novaclient/tests/unit/v2/test_hypervisors.py @@ -14,6 +14,7 @@ # under the License. from novaclient import api_versions +from novaclient import exceptions from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit.fixture_data import hypervisors as data from novaclient.tests.unit import utils @@ -107,6 +108,17 @@ def test_hypervisor_search(self): for idx, hyper in enumerate(result): self.compare_to_expected(expected[idx], hyper) + def test_hypervisor_search_unicode(self): + hypervisor_match = u'\\u5de5\\u4f5c' + if self.cs.api_version >= api_versions.APIVersion('2.53'): + self.assertRaises(exceptions.BadRequest, + self.cs.hypervisors.search, + hypervisor_match) + else: + self.assertRaises(exceptions.NotFound, + self.cs.hypervisors.search, + hypervisor_match) + def test_hypervisor_servers(self): expected = [ dict(id=self.data_fixture.hyper_id_1, diff --git a/novaclient/tests/unit/v2/test_instance_usage_audit_log.py b/novaclient/tests/unit/v2/test_instance_usage_audit_log.py index 148ebbda2..f3cb65cd2 100644 --- a/novaclient/tests/unit/v2/test_instance_usage_audit_log.py +++ b/novaclient/tests/unit/v2/test_instance_usage_audit_log.py @@ -14,6 +14,7 @@ # under the License. from novaclient import api_versions +from novaclient import exceptions from novaclient.tests.unit import utils from novaclient.tests.unit.v2 import fakes @@ -35,3 +36,8 @@ def test_instance_usage_audit_log_with_before(self): self.cs.assert_called( 'GET', '/os-instance_usage_audit_log/2016-12-10%2013%3A59%3A59.999999') + + def test_instance_usage_audit_log_with_before_unicode(self): + before = u'\\u5de5\\u4f5c' + self.assertRaises(exceptions.BadRequest, + self.cs.instance_usage_audit_log.get, before) diff --git a/novaclient/v2/hypervisors.py b/novaclient/v2/hypervisors.py index e43ef7a18..f457fa084 100644 --- a/novaclient/v2/hypervisors.py +++ b/novaclient/v2/hypervisors.py @@ -17,6 +17,8 @@ Hypervisors interface """ +from oslo_utils import encodeutils +import six from six.moves.urllib import parse from novaclient import api_versions @@ -83,6 +85,8 @@ def search(self, hypervisor_match, servers=False): # Starting with microversion 2.53, the /servers and /search routes are # deprecated and we get the same results using GET /os-hypervisors # using query parameters for the hostname pattern and servers. + if six.PY2: + hypervisor_match = encodeutils.safe_encode(hypervisor_match) if self.api_version >= api_versions.APIVersion('2.53'): url = ('/os-hypervisors?hypervisor_hostname_pattern=%s' % parse.quote(hypervisor_match, safe='')) diff --git a/novaclient/v2/instance_usage_audit_log.py b/novaclient/v2/instance_usage_audit_log.py index 19b588e9a..9ada06c1e 100644 --- a/novaclient/v2/instance_usage_audit_log.py +++ b/novaclient/v2/instance_usage_audit_log.py @@ -13,6 +13,8 @@ # License for the specific language governing permissions and limitations # under the License. +from oslo_utils import encodeutils +import six from six.moves.urllib import parse from novaclient import base @@ -32,6 +34,8 @@ def get(self, before=None): before which to list usage audits. """ if before: + if six.PY2: + before = encodeutils.safe_encode(before) return self._get('/os-instance_usage_audit_log/%s' % parse.quote(before, safe=''), 'instance_usage_audit_log') From 7e877c4fdb7843b674236204a9a3d31d07e6ad6f Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Sun, 24 Feb 2019 00:15:19 +0900 Subject: [PATCH 1502/1705] Fix changes-before values in an instance action test In some DBMSs (e.g. MySQL 5.7 (*1)), fractions (millisecond and microsecond) of DateTime column is not stored by default. *1: https://dev.mysql.com/doc/refman/5.7/en/date-and-time-type-overview.html Fix specifying the 'changes-before' values in the functional test for the 'instance-action-list' command. Change-Id: I79e80088bcbf559a58aa90a831b54017af310a26 Closes-Bug: #1817064 --- .../functional/v2/test_instance_action.py | 36 ++++++++++++++++--- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/novaclient/tests/functional/v2/test_instance_action.py b/novaclient/tests/functional/v2/test_instance_action.py index 450102103..b1e884c5a 100644 --- a/novaclient/tests/functional/v2/test_instance_action.py +++ b/novaclient/tests/functional/v2/test_instance_action.py @@ -160,12 +160,35 @@ class TestInstanceActionCLIV266(TestInstanceActionCLIV258, COMPUTE_API_VERSION = "2.66" expect_event_hostId_field = True + def _wait_for_instance_actions(self, server, expected_num_of_actions): + start_time = time.time() + # Time out after 60 seconds + while time.time() - start_time < 60: + actions = self.client.instance_action.list(server) + if len(actions) == expected_num_of_actions: + break + # Sleep 1 second + time.sleep(1) + else: + self.fail("The number of instance actions for server %s " + "was not %d after 60 s" % + (server.id, expected_num_of_actions)) + # NOTE(takashin): In some DBMSs (e.g. MySQL 5.7), fractions + # (millisecond and microsecond) of DateTime column is not stored + # by default. So sleep an extra second. + time.sleep(1) + # Return time + return timeutils.utcnow().isoformat() + def test_list_instance_action_with_changes_before(self): server = self._create_server() - end_create = timeutils.utcnow().isoformat() + end_create = self._wait_for_instance_actions(server, 1) + # NOTE(takashin): In some DBMSs (e.g. MySQL 5.7), fractions + # (millisecond and microsecond) of DateTime column is not stored + # by default. So sleep a second. + time.sleep(1) server.stop() - self._wait_for_state_change(server.id, 'shutoff') - end_stop = timeutils.utcnow().isoformat() + end_stop = self._wait_for_instance_actions(server, 2) stop_output = self.nova( "instance-action-list %s --changes-before %s" % @@ -173,7 +196,10 @@ def test_list_instance_action_with_changes_before(self): action = self._get_list_of_values_from_single_column_table( stop_output, "Action") # The actions are sorted by created_at in descending order. - self.assertEqual(action, ['create', 'stop']) + self.assertEqual(['create', 'stop'], action, + 'Expected to find the create and stop actions with ' + '--changes-before=%s but got: %s\n\n' % + (end_stop, stop_output)) create_output = self.nova( "instance-action-list %s --changes-before %s" % @@ -181,7 +207,7 @@ def test_list_instance_action_with_changes_before(self): action = self._get_list_of_values_from_single_column_table( create_output, "Action") # Provide detailed debug information if this fails. - self.assertEqual(action, ['create'], + self.assertEqual(['create'], action, 'Expected to find the create action with ' '--changes-before=%s but got: %s\n\n' 'First instance-action-list output: %s' % From de22cdd3c84e03ed76a261fa41c4ed2d829c8d65 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Wed, 13 Feb 2019 18:29:39 -0500 Subject: [PATCH 1503/1705] Add support for microversion 2.70 - expose device tags This adds support for microversion 2.70 which exposes the 'tag' field in the following APIs: * GET /servers/{server_id}/os-volume_attachments * GET /servers/{server_id}/os-volume_attachments/{volume_id} * POST /servers/{server_id}/os-volume_attachments * GET /servers/{server_id}/os-interface * GET /servers/{server_id}/os-interface/{port_id} * POST /servers/{server_id}/os-interface Which corresponds to showing the tag in the output of the following commands: * nova volume-attachments * nova volume-attach * nova interface-list * nova interface-attach Depends-On: https://review.openstack.org/631948/ Part of blueprint expose-virtual-device-tags-in-rest-api Change-Id: I5e9d7e0219605503a56d2cf745b95c6e05d01101 --- novaclient/__init__.py | 2 +- novaclient/tests/unit/v2/fakes.py | 38 ++++++++++---- novaclient/tests/unit/v2/test_shell.py | 52 ++++++++++++++++--- novaclient/v2/shell.py | 14 +++-- .../microversion_v2_70-09cbe0933b3a9335.yaml | 12 +++++ 5 files changed, 96 insertions(+), 22 deletions(-) create mode 100644 releasenotes/notes/microversion_v2_70-09cbe0933b3a9335.yaml diff --git a/novaclient/__init__.py b/novaclient/__init__.py index a41a77af3..bb0e53d1d 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.69") +API_MAX_VERSION = api_versions.APIVersion("2.70") diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index a1e298e7d..0f1857ff3 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -2004,7 +2004,7 @@ def get_os_availability_zone_detail(self, **kw): "hosts": None}]}) def get_servers_1234_os_interface(self, **kw): - return (200, {}, { + attachments = { "interfaceAttachments": [ {"port_state": "ACTIVE", "net_id": "net-id-1", @@ -2017,27 +2017,38 @@ def get_servers_1234_os_interface(self, **kw): "port_id": "port-id-1", "mac_address": "aa:bb:cc:dd:ee:ff", "fixed_ips": [{"ip_address": "1.2.3.4"}], - }] - }) + } + ] + } + if self.api_version >= api_versions.APIVersion('2.70'): + # Include the "tag" field in each attachment. + for attachment in attachments['interfaceAttachments']: + attachment['tag'] = 'test-tag' + return (200, {}, attachments) def post_servers_1234_os_interface(self, **kw): - return (200, {}, {'interfaceAttachment': {}}) + attachment = {} + if self.api_version >= api_versions.APIVersion('2.70'): + # Include the "tag" field in the response. + attachment['tag'] = 'test-tag' + return (200, {}, {'interfaceAttachment': attachment}) def delete_servers_1234_os_interface_port_id(self, **kw): return (200, {}, None) def post_servers_1234_os_volume_attachments(self, **kw): - return (200, FAKE_RESPONSE_HEADERS, { - "volumeAttachment": - {"device": "/dev/vdb", - "volumeId": 2}}) + attachment = {"device": "/dev/vdb", "volumeId": 2} + if self.api_version >= api_versions.APIVersion('2.70'): + # Include the "tag" field in the response. + attachment['tag'] = 'test-tag' + return (200, FAKE_RESPONSE_HEADERS, {"volumeAttachment": attachment}) def put_servers_1234_os_volume_attachments_Work(self, **kw): return (200, FAKE_RESPONSE_HEADERS, {"volumeAttachment": {"volumeId": 2}}) def get_servers_1234_os_volume_attachments(self, **kw): - return (200, FAKE_RESPONSE_HEADERS, { + attachments = { "volumeAttachments": [ {"display_name": "Work", "display_description": "volume for work", @@ -2047,7 +2058,14 @@ def get_servers_1234_os_volume_attachments(self, **kw): "attached": "2011-11-11T00:00:00Z", "size": 1024, "attachments": [{"id": "3333", "links": ''}], - "metadata": {}}]}) + "metadata": {}} + ] + } + if self.api_version >= api_versions.APIVersion('2.70'): + # Include the "tag" field in each attachment. + for attachment in attachments['volumeAttachments']: + attachment['tag'] = 'test-tag' + return (200, FAKE_RESPONSE_HEADERS, attachments) def get_servers_1234_os_volume_attachments_Work(self, **kw): return (200, FAKE_RESPONSE_HEADERS, { diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index eecd9315e..778c74ac8 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -3518,8 +3518,14 @@ def test_server_security_group_list(self): self.assert_called('GET', '/servers/1234/os-security-groups') def test_interface_list(self): - self.run_command('interface-list 1234') + out = self.run_command('interface-list 1234')[0] self.assert_called('GET', '/servers/1234/os-interface') + self.assertNotIn('Tag', out) + + def test_interface_list_v2_70(self): + out = self.run_command('interface-list 1234', api_version='2.70')[0] + self.assert_called('GET', '/servers/1234/os-interface') + self.assertIn('test-tag', out) def test_interface_attach(self): self.run_command('interface-attach --port-id port_id 1234') @@ -3533,20 +3539,37 @@ def test_interface_attach_with_tag_pre_v2_49(self): api_version='2.48') def test_interface_attach_with_tag(self): - self.run_command( - 'interface-attach --port-id port_id --tag test_tag 1234', - api_version='2.49') + out = self.run_command( + 'interface-attach --port-id port_id --tag test-tag 1234', + api_version='2.49')[0] self.assert_called('POST', '/servers/1234/os-interface', {'interfaceAttachment': {'port_id': 'port_id', - 'tag': 'test_tag'}}) + 'tag': 'test-tag'}}) + self.assertNotIn('test-tag', out) + + def test_interface_attach_v2_70(self): + out = self.run_command( + 'interface-attach --port-id port_id --tag test-tag 1234', + api_version='2.70')[0] + self.assert_called('POST', '/servers/1234/os-interface', + {'interfaceAttachment': {'port_id': 'port_id', + 'tag': 'test-tag'}}) + self.assertIn('test-tag', out) def test_interface_detach(self): self.run_command('interface-detach 1234 port_id') self.assert_called('DELETE', '/servers/1234/os-interface/port_id') def test_volume_attachments(self): - self.run_command('volume-attachments 1234') + out = self.run_command('volume-attachments 1234')[0] self.assert_called('GET', '/servers/1234/os-volume_attachments') + self.assertNotIn('test-tag', out) + + def test_volume_attachments_v2_70(self): + out = self.run_command( + 'volume-attachments 1234', api_version='2.70')[0] + self.assert_called('GET', '/servers/1234/os-volume_attachments') + self.assertIn('test-tag', out) def test_volume_attach(self): self.run_command('volume-attach sample-server Work /dev/vdb') @@ -3568,14 +3591,26 @@ def test_volume_attach_with_tag_pre_v2_49(self): api_version='2.48') def test_volume_attach_with_tag(self): - self.run_command( + out = self.run_command( 'volume-attach --tag test_tag sample-server Work /dev/vdb', - api_version='2.49') + api_version='2.49')[0] self.assert_called('POST', '/servers/1234/os-volume_attachments', {'volumeAttachment': {'device': '/dev/vdb', 'volumeId': 'Work', 'tag': 'test_tag'}}) + self.assertNotIn('test-tag', out) + + def test_volume_attach_with_tag_v2_70(self): + out = self.run_command( + 'volume-attach --tag test-tag sample-server Work /dev/vdb', + api_version='2.70')[0] + self.assert_called('POST', '/servers/1234/os-volume_attachments', + {'volumeAttachment': + {'device': '/dev/vdb', + 'volumeId': 'Work', + 'tag': 'test-tag'}}) + self.assertIn('test-tag', out) def test_volume_update(self): self.run_command('volume-update sample-server Work Work') @@ -4045,6 +4080,7 @@ def test_versions(self): # cell, they will be handled on the client side by being # skipped when forming the detailed lists for embedded # flavor information. + 70, # There are no version-wrapped shell method changes for this. ]) versions_supported = set(range(0, novaclient.API_MAX_VERSION.ver_minor + 1)) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index cb0b1c4a2..4f65237d2 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -2617,7 +2617,11 @@ def do_volume_attachments(cs, args): """List all the volumes attached to a server.""" volumes = cs.volumes.get_server_volumes(_find_server(cs, args.server).id) _translate_volume_attachments_keys(volumes) - utils.print_list(volumes, ['ID', 'DEVICE', 'SERVER ID', 'VOLUME ID']) + # Microversion >= 2.70 returns the tag value. + fields = ['ID', 'DEVICE', 'SERVER ID', 'VOLUME ID'] + if cs.api_version >= api_versions.APIVersion('2.70'): + fields.append('TAG') + utils.print_list(volumes, fields) @api_versions.wraps('2.0', '2.5') @@ -4497,9 +4501,11 @@ def do_evacuate(cs, args): utils.print_dict(res) -def _print_interfaces(interfaces): +def _print_interfaces(interfaces, show_tag=False): columns = ['Port State', 'Port ID', 'Net ID', 'IP addresses', 'MAC Addr'] + if show_tag: + columns.append('Tag') class FormattedInterface(object): def __init__(self, interface): @@ -4519,7 +4525,9 @@ def do_interface_list(cs, args): res = server.interface_list() if isinstance(res, list): - _print_interfaces(res) + # The "tag" field is in the response starting with microversion 2.70. + show_tag = cs.api_version >= api_versions.APIVersion('2.70') + _print_interfaces(res, show_tag=show_tag) @utils.arg('server', metavar='', help=_('Name or ID of server.')) diff --git a/releasenotes/notes/microversion_v2_70-09cbe0933b3a9335.yaml b/releasenotes/notes/microversion_v2_70-09cbe0933b3a9335.yaml new file mode 100644 index 000000000..93dee6e2d --- /dev/null +++ b/releasenotes/notes/microversion_v2_70-09cbe0933b3a9335.yaml @@ -0,0 +1,12 @@ +--- +features: + - | + Added support for `microversion 2.70`_ which outputs the `Tag` field in + the following commands: + + * ``nova interface-list`` + * ``nova interface-attach`` + * ``nova volume-attachments`` + * ``nova volume-attach`` + + .. _microversion 2.70: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id63 From a306395d745a60de679a4d95983786dd104bcefc Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Fri, 1 Mar 2019 15:44:30 +0900 Subject: [PATCH 1504/1705] Remove unnecessary if statement The interface_list method of the novaclient.v2.server.Server class always returns an object of the novaclient.base.ListWithMeta class which is a sub class of list. So 'if' statement that check whether it is an instance of list is not necessary when printing the return value of the method. Change-Id: I5dc5bfc6a783bb59c3aec2cce626bb00de633754 --- novaclient/v2/shell.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 4f65237d2..c848521a8 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -4524,10 +4524,10 @@ def do_interface_list(cs, args): server = _find_server(cs, args.server) res = server.interface_list() - if isinstance(res, list): - # The "tag" field is in the response starting with microversion 2.70. - show_tag = cs.api_version >= api_versions.APIVersion('2.70') - _print_interfaces(res, show_tag=show_tag) + + # The "tag" field is in the response starting with microversion 2.70. + show_tag = cs.api_version >= api_versions.APIVersion('2.70') + _print_interfaces(res, show_tag=show_tag) @utils.arg('server', metavar='', help=_('Name or ID of server.')) From b2cd7e12ccb65e67be0a0b01243d6f19f95e70e8 Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Mon, 4 Mar 2019 07:07:00 +0900 Subject: [PATCH 1505/1705] Microversion 2.71 - show server group Add support microversion 2.71 which adds server group information in the output of the following commands. * nova show * nova rebuild The 'nova update' command does not output its result when it is successful. So there is no change for the command. The patch for microversion 2.71 in the nova side is I4a2a584df56ece7beb8b12c0ce9b0e6b30237120. Change-Id: Id324486b5ef32615881085cd46772aa55c245ac6 Implements: blueprint show-server-group --- novaclient/__init__.py | 2 +- novaclient/tests/unit/v2/fakes.py | 7 ++- novaclient/tests/unit/v2/test_shell.py | 51 +++++++++++++++++++ .../microversion-v2_71-a87b4bb4205c46e2.yaml | 10 ++++ 4 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/microversion-v2_71-a87b4bb4205c46e2.yaml diff --git a/novaclient/__init__.py b/novaclient/__init__.py index bb0e53d1d..5dc881d07 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.70") +API_MAX_VERSION = api_versions.APIVersion("2.71") diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 0f1857ff3..a32e22582 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -618,8 +618,11 @@ def post_os_volumes_boot(self, body, **kw): return (202, {}, self.get_servers_9012()[2]) def get_servers_1234(self, **kw): - r = {'server': self.get_servers_detail()[2]['servers'][0]} - return (200, {}, r) + server = self.get_servers_detail()[2]['servers'][0] + if self.api_version >= api_versions.APIVersion('2.71'): + server.update( + {'server_groups': ['a67359fb-d397-4697-88f1-f55e3ee7c499']}) + return (200, {}, {'server': server}) def get_servers_1235(self, **kw): r = {'server': self.get_servers_detail()[2]['servers'][0]} diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 778c74ac8..3589ec5c0 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2028,6 +2028,36 @@ def test_rebuild_with_trusted_image_certificates_arg_and_envar(self): }, pos=3) self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=4) + def test_rebuild_with_server_groups_in_response(self): + out = self.run_command('rebuild sample-server %s' % FAKE_UUID_1, + api_version='2.71')[0] + self.assert_called('GET', '/servers?name=sample-server', pos=0) + self.assert_called('GET', '/servers/1234', pos=1) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=2) + self.assert_called('POST', '/servers/1234/action', + {'rebuild': {'imageRef': FAKE_UUID_1, + 'description': None, + } + }, pos=3) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=4) + self.assertIn('server_groups', out) + self.assertIn('a67359fb-d397-4697-88f1-f55e3ee7c499', out) + + def test_rebuild_without_server_groups_in_response(self): + out = self.run_command('rebuild sample-server %s' % FAKE_UUID_1, + api_version='2.70')[0] + self.assert_called('GET', '/servers?name=sample-server', pos=0) + self.assert_called('GET', '/servers/1234', pos=1) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=2) + self.assert_called('POST', '/servers/1234/action', + {'rebuild': {'imageRef': FAKE_UUID_1, + 'description': None, + } + }, pos=3) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=4) + self.assertNotIn('server_groups', out) + self.assertNotIn('a67359fb-d397-4697-88f1-f55e3ee7c499', out) + def test_start(self): self.run_command('start sample-server') self.assert_called('POST', '/servers/1234/action', {'os-start': None}) @@ -2180,6 +2210,26 @@ def test_show_with_name_help(self): output, _ = self.run_command('show help') self.assert_called('GET', '/servers/9014', pos=-6) + def test_show_with_server_groups_in_response(self): + # Starting microversion 2.71, the 'server_groups' is included + # in the output (the response). + out = self.run_command('show 1234', api_version='2.71')[0] + self.assert_called('GET', '/servers?name=1234', pos=0) + self.assert_called('GET', '/servers?name=1234', pos=1) + self.assert_called('GET', '/servers/1234', pos=2) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=3) + self.assertIn('server_groups', out) + self.assertIn('a67359fb-d397-4697-88f1-f55e3ee7c499', out) + + def test_show_without_server_groups_in_response(self): + out = self.run_command('show 1234', api_version='2.70')[0] + self.assert_called('GET', '/servers?name=1234', pos=0) + self.assert_called('GET', '/servers?name=1234', pos=1) + self.assert_called('GET', '/servers/1234', pos=2) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=3) + self.assertNotIn('server_groups', out) + self.assertNotIn('a67359fb-d397-4697-88f1-f55e3ee7c499', out) + @mock.patch('novaclient.v2.shell.utils.print_dict') def test_print_server(self, mock_print_dict): self.run_command('show 5678') @@ -4081,6 +4131,7 @@ def test_versions(self): # skipped when forming the detailed lists for embedded # flavor information. 70, # There are no version-wrapped shell method changes for this. + 71, # There are no version-wrapped shell method changes for this. ]) versions_supported = set(range(0, novaclient.API_MAX_VERSION.ver_minor + 1)) diff --git a/releasenotes/notes/microversion-v2_71-a87b4bb4205c46e2.yaml b/releasenotes/notes/microversion-v2_71-a87b4bb4205c46e2.yaml new file mode 100644 index 000000000..7a4bf9609 --- /dev/null +++ b/releasenotes/notes/microversion-v2_71-a87b4bb4205c46e2.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + Added support for `microversion 2.71`_ which outputs the `server_groups` + field in the following commands: + + * ``nova show`` + * ``nova rebuild`` + + .. _microversion 2.71: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id64 From 62bf8809c660ed0675f301c235b1d434caeaf580 Mon Sep 17 00:00:00 2001 From: Lajos Katona Date: Wed, 20 Feb 2019 10:49:43 +0100 Subject: [PATCH 1506/1705] Add support for microversion v2.72 This microversion in Nova is added to support Neutron ports having resource request during server create to guarantee minimum bandwidth QoS. This is a behavior-only change in the compute API, there are no changes to the server create request or response schema. Change-Id: I1a39390015acd8703e8bab55af13f5c75ae226db Depends-On: https://review.openstack.org/636360 Partial-Bug: #1578989 See-Also: https://review.openstack.org/502306 (nova spec) See-Also: https://review.openstack.org/508149 (neutron spec) --- doc/source/cli/nova.rst | 4 ++++ novaclient/__init__.py | 2 +- novaclient/tests/unit/v2/test_shell.py | 1 + novaclient/v2/servers.py | 4 ++++ .../notes/microversion-v2_72-d910ce07ec3948d6.yaml | 10 ++++++++++ 5 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/microversion-v2_72-d910ce07ec3948d6.yaml diff --git a/doc/source/cli/nova.rst b/doc/source/cli/nova.rst index e9de8a0af..21f760d76 100644 --- a/doc/source/cli/nova.rst +++ b/doc/source/cli/nova.rst @@ -956,6 +956,10 @@ nova boot Boot a new server. +In order to create a server with pre-existing ports that contain a +``resource_request`` value, such as for guaranteed minimum bandwidth +quality of service support, microversion ``2.72`` is required. + **Positional arguments:** ```` diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 5dc881d07..8b6111de1 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.71") +API_MAX_VERSION = api_versions.APIVersion("2.72") diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 3589ec5c0..e2297699a 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -4132,6 +4132,7 @@ def test_versions(self): # flavor information. 70, # There are no version-wrapped shell method changes for this. 71, # There are no version-wrapped shell method changes for this. + 72, # There are no version-wrapped shell method changes for this. ]) versions_supported = set(range(0, novaclient.API_MAX_VERSION.ver_minor + 1)) diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index 87a415565..a37c1b387 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -1236,6 +1236,10 @@ def create(self, name, image, flavor, meta=None, files=None, """ Create (boot) a new server. + In order to create a server with pre-existing ports that contain a + ``resource_request`` value, such as for guaranteed minimum bandwidth + quality of service support, microversion ``2.72`` is required. + :param name: Something to name the server. :param image: The :class:`Image` to boot with. :param flavor: The :class:`Flavor` to boot onto. diff --git a/releasenotes/notes/microversion-v2_72-d910ce07ec3948d6.yaml b/releasenotes/notes/microversion-v2_72-d910ce07ec3948d6.yaml new file mode 100644 index 000000000..69ea49821 --- /dev/null +++ b/releasenotes/notes/microversion-v2_72-d910ce07ec3948d6.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + Support has been added for `microversion 2.72`_. This microversion + allows creating a server using the ``nova boot`` command with + pre-existing ports having a ``resource_request`` value to enable + features such as guaranteed minimum bandwidth for `quality of service`_. + + .. _microversion 2.72: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id65 + .. _quality of service: https://docs.openstack.org/neutron/latest/admin/config-qos.html From a4ea27fa3be8b7b1ef50eca40603b548b33542f6 Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Mon, 18 Mar 2019 14:54:12 +0000 Subject: [PATCH 1507/1705] Update master for stable/stein Add file to the reno documentation build to show release notes for stable/stein. Use pbr instruction to increment the minor version number automatically so that master versions are higher than the versions on stable/stein. Change-Id: I9c0b81a3ef8c1604128c0dd55ba0c39a070e90e5 Sem-Ver: feature --- releasenotes/source/index.rst | 1 + releasenotes/source/stein.rst | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 releasenotes/source/stein.rst diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index 89e64c43d..1d78d69a5 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -8,6 +8,7 @@ Contents :maxdepth: 2 unreleased + stein rocky queens pike diff --git a/releasenotes/source/stein.rst b/releasenotes/source/stein.rst new file mode 100644 index 000000000..efaceb667 --- /dev/null +++ b/releasenotes/source/stein.rst @@ -0,0 +1,6 @@ +=================================== + Stein Series Release Notes +=================================== + +.. release-notes:: + :branch: stable/stein From a7ea3fb09bbace3de6463d745347be4d24cb5856 Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Mon, 11 Mar 2019 11:07:36 +0900 Subject: [PATCH 1508/1705] Remove deprecated options Remove the following deprecated options. * --endpoint-override * --instance-name ('nova list' command) Change-Id: Ic6a78f04a98c1616750e6ecd6225f2750c214dd7 --- doc/source/cli/nova.rst | 12 ++++------- novaclient/shell.py | 20 ------------------- novaclient/v2/shell.py | 9 --------- ...ecated-option-14.0.0-c6d7189938f5f063.yaml | 7 +++++++ 4 files changed, 11 insertions(+), 37 deletions(-) create mode 100644 releasenotes/notes/remove-deprecated-option-14.0.0-c6d7189938f5f063.yaml diff --git a/doc/source/cli/nova.rst b/doc/source/cli/nova.rst index 21f760d76..75f6ee90e 100644 --- a/doc/source/cli/nova.rst +++ b/doc/source/cli/nova.rst @@ -2187,10 +2187,10 @@ nova list usage: nova list [--reservation-id ] [--ip ] [--ip6 ] [--name ] - [--instance-name ] [--status ] - [--flavor ] [--image ] [--host ] - [--all-tenants [<0|1>]] [--tenant []] - [--user []] [--deleted] [--fields ] [--minimal] + [--status ] [--flavor ] [--image ] + [--host ] [--all-tenants [<0|1>]] + [--tenant []] [--user []] [--deleted] + [--fields ] [--minimal] [--sort [:]] [--marker ] [--limit ] [--changes-since ] [--changes-before ] @@ -2219,10 +2219,6 @@ present in the failure domain. ``--name `` Search with regular expression match by name. -``--instance-name `` - Search with regular expression match by server - name. - ``--status `` Search by server status. diff --git a/novaclient/shell.py b/novaclient/shell.py index 2f6a33e81..c82cd506c 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -367,26 +367,6 @@ def get_base_parser(self, argv): help=_("Use this API endpoint instead of the Service Catalog. " "Defaults to env[OS_ENDPOINT_OVERRIDE].")) - # NOTE(takashin): This dummy '--end' argument was added - # to avoid misinterpreting command line arguments. - # If there is not this dummy argument, the '--end' is interpreted to - # the '--endpoint-override'. - # TODO(takashin): Remove this dummy '--end' argument - # when the deprecated '--endpoint-override' argument is removed. - parser.add_argument( - '--end', - metavar='', - nargs='?', - help=argparse.SUPPRESS) - - parser.add_argument( - '--endpoint-override', - action=DeprecatedAction, - use=_('use "%s"; this option will be removed after Rocky ' - 'OpenStack release.') % '--os-endpoint-override', - dest='endpoint_override', - help=argparse.SUPPRESS) - if osprofiler_profiler: parser.add_argument('--profile', metavar='HMAC_KEY', diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index c848521a8..a5a5ead2c 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -1418,14 +1418,6 @@ def _print_flavor(flavor): metavar='', default=None, help=_('Search with regular expression match by name.')) -@utils.arg( - '--instance-name', - dest='instance_name', - metavar='', - default=None, - action=shell.DeprecatedAction, - help=_('Search with regular expression match by server name. The option ' - 'is not used and will be removed in T release.')) @utils.arg( '--status', dest='status', @@ -1596,7 +1588,6 @@ def do_list(cs, args): 'user_id': args.user, 'host': args.host, 'deleted': args.deleted, - 'instance_name': args.instance_name, 'changes-since': args.changes_since} for arg in ('tags', "tags-any", 'not-tags', 'not-tags-any'): diff --git a/releasenotes/notes/remove-deprecated-option-14.0.0-c6d7189938f5f063.yaml b/releasenotes/notes/remove-deprecated-option-14.0.0-c6d7189938f5f063.yaml new file mode 100644 index 000000000..b91ff58fd --- /dev/null +++ b/releasenotes/notes/remove-deprecated-option-14.0.0-c6d7189938f5f063.yaml @@ -0,0 +1,7 @@ +--- +upgrade: + - | + The following deprecated options have been removed. + + * ``--endpoint-override`` (Authentication option) + * ``--instance-name`` (``nova list`` command) From df8e10ccc89fc586f132a6975f349fc24a1feecc Mon Sep 17 00:00:00 2001 From: Ian Wienand Date: Sun, 24 Mar 2019 20:35:54 +0000 Subject: [PATCH 1509/1705] Replace openstack.org git:// URLs with https:// This is a mechanically generated change to replace openstack.org git:// URLs with https:// equivalents. This is in aid of a planned future move of the git hosting infrastructure to a self-hosted instance of gitea (https://gitea.io), which does not support the git wire protocol at this stage. This update should result in no functional change. For more information see the thread at http://lists.openstack.org/pipermail/openstack-discuss/2019-March/003825.html Change-Id: I6423995b71d918efe9af67d293effabdd531d72c --- playbooks/legacy/novaclient-dsvm-functional/run.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playbooks/legacy/novaclient-dsvm-functional/run.yaml b/playbooks/legacy/novaclient-dsvm-functional/run.yaml index e4bed30cd..935b3f134 100644 --- a/playbooks/legacy/novaclient-dsvm-functional/run.yaml +++ b/playbooks/legacy/novaclient-dsvm-functional/run.yaml @@ -17,7 +17,7 @@ dest: devstack-gate EOF /usr/zuul-env/bin/zuul-cloner -m clonemap.yaml --cache-dir /opt/git \ - git://git.openstack.org \ + https://git.openstack.org \ openstack-infra/devstack-gate executable: /bin/bash chdir: '{{ ansible_user_dir }}/workspace' From a9b1125f7ba209887cac36b3c0900cffe2516fa8 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 5 Apr 2019 07:49:38 +0100 Subject: [PATCH 1510/1705] Revert "Fix crashing console-log" This reverts commit d272d6f3df2610a62f81e2ca26798ea8a6674b06. This worked around a misconfigured environment by forcing stdout to use UTF-8. This is kind of a hack and it only works in Python 2, breaking Python 3 [1]. The correct solution to this issue and all Python 2 unicode print issues is to correctly configuring LC_ALL to use a UTF-8 locale. [1] https://stackoverflow.com/q/4374455 Change-Id: Iaeec1e74262a35f3de3c81f7013835a6aa6f9029 Closes-Bug: #1823287 --- novaclient/v2/shell.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index a5a5ead2c..f7d59dfaa 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -19,7 +19,6 @@ from __future__ import print_function import argparse -import codecs import collections import datetime import getpass @@ -2750,10 +2749,7 @@ def do_console_log(cs, args): """Get console log output of a server.""" server = _find_server(cs, args.server) data = server.get_console_output(length=args.length) - - if data and data[-1] != '\n': - data += '\n' - codecs.getwriter('utf-8')(sys.stdout).write(data) + print(data) @utils.arg('server', metavar='', help=_('Name or ID of server.')) From e8f7aaf9091330bbb046f2bd42dc9c92f1dcebaa Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Thu, 11 Apr 2019 12:03:30 -0400 Subject: [PATCH 1511/1705] Add test for console-log and docs for bug 1746534 We had no unit test coverage for the console-log command so this adds a simple test. It unfortunately does not recreate the original UnicodeEncodeError bug though, but it gives us some test coverage anyway. This change also adds docs on how to resolve bug 1746534 by configuring the environment for UTF-8 as noted in the revert change Iaeec1e74262a35f3de3c81f7013835a6aa6f9029. The documentation added here is shamelessly copied from Click [1] which was found via the PEP 538 docs [2]. [1] https://click.palletsprojects.com/en/5.x/python3/#python-3-surrogate-handling [2] https://www.python.org/dev/peps/pep-0538/ Change-Id: Ic7059260dfc031ea1b08d2b8a7cec684bbe7dfa5 Related-Bug: #1823287 Related-Bug: #1746534 --- doc/source/cli/nova.rst | 27 ++++++++++++++++++++++++++ novaclient/tests/unit/v2/test_shell.py | 6 ++++++ 2 files changed, 33 insertions(+) diff --git a/doc/source/cli/nova.rst b/doc/source/cli/nova.rst index 75f6ee90e..eada5c693 100644 --- a/doc/source/cli/nova.rst +++ b/doc/source/cli/nova.rst @@ -1177,6 +1177,33 @@ nova console-log Get console log output of a server. +**Locale encoding issues** + +If you encounter an error such as: + +.. code-block:: console + + UnicodeEncodeError: 'ascii' codec can't encode characters in position + +The solution to these problems is different depending on which locale your +computer is running in. + +For instance, if you have a German Linux machine, you can fix the problem by +exporting the locale to de_DE.utf-8: + +.. code-block:: console + + export LC_ALL=de_DE.utf-8 + export LANG=de_DE.utf-8 + +If you are on a US machine, en_US.utf-8 is the encoding of choice. On some +newer Linux systems, you could also try C.UTF-8 as the locale: + +.. code-block:: console + + export LC_ALL=C.UTF-8 + export LANG=C.UTF-8 + **Positional arguments:** ```` diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index e2297699a..bf7a86674 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -3553,6 +3553,12 @@ def test_availability_zone_list(self): self.run_command('availability-zone-list') self.assert_called('GET', '/os-availability-zone/detail') + def test_console_log(self): + out = self.run_command('console-log --length 20 1234')[0] + self.assert_called('POST', '/servers/1234/action', + body={'os-getConsoleOutput': {'length': '20'}}) + self.assertIn('foo', out) + def test_server_security_group_add(self): self.run_command('add-secgroup sample-server testgroup') self.assert_called('POST', '/servers/1234/action', From 7cfd3abfef05afe500a62cd32138fdb9b35936f7 Mon Sep 17 00:00:00 2001 From: OpenDev Sysadmins Date: Fri, 19 Apr 2019 19:43:24 +0000 Subject: [PATCH 1512/1705] OpenDev Migration Patch This commit was bulk generated and pushed by the OpenDev sysadmins as a part of the Git hosting and code review systems migration detailed in these mailing list posts: http://lists.openstack.org/pipermail/openstack-discuss/2019-March/003603.html http://lists.openstack.org/pipermail/openstack-discuss/2019-April/004920.html Attempts have been made to correct repository namespaces and hostnames based on simple pattern matching, but it's possible some were updated incorrectly or missed entirely. Please reach out to us via the contact information listed at https://opendev.org/ with any questions you may have. --- .gitreview | 2 +- .zuul.yaml | 2 +- playbooks/legacy/novaclient-dsvm-functional/run.yaml | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.gitreview b/.gitreview index 033138304..9221a4f9d 100644 --- a/.gitreview +++ b/.gitreview @@ -1,4 +1,4 @@ [gerrit] -host=review.openstack.org +host=review.opendev.org port=29418 project=openstack/python-novaclient.git diff --git a/.zuul.yaml b/.zuul.yaml index f497f3a11..7acd53209 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -5,7 +5,7 @@ post-run: playbooks/legacy/novaclient-dsvm-functional/post.yaml timeout: 7200 required-projects: - - openstack-infra/devstack-gate + - openstack/devstack-gate - openstack/nova - openstack/python-novaclient diff --git a/playbooks/legacy/novaclient-dsvm-functional/run.yaml b/playbooks/legacy/novaclient-dsvm-functional/run.yaml index 935b3f134..a3d1f4979 100644 --- a/playbooks/legacy/novaclient-dsvm-functional/run.yaml +++ b/playbooks/legacy/novaclient-dsvm-functional/run.yaml @@ -13,12 +13,12 @@ set -x cat > clonemap.yaml << EOF clonemap: - - name: openstack-infra/devstack-gate + - name: openstack/devstack-gate dest: devstack-gate EOF /usr/zuul-env/bin/zuul-cloner -m clonemap.yaml --cache-dir /opt/git \ - https://git.openstack.org \ - openstack-infra/devstack-gate + https://opendev.org \ + openstack/devstack-gate executable: /bin/bash chdir: '{{ ansible_user_dir }}/workspace' environment: '{{ zuul | zuul_legacy_vars }}' From f970589f9ba62b61efdc68a4843e54bc1ecc76c5 Mon Sep 17 00:00:00 2001 From: zhangyangyang Date: Mon, 18 Mar 2019 15:40:20 +0800 Subject: [PATCH 1513/1705] Drop py35 tests Drop py35 tests because all the integration testing has been moved to Bionic. See the following URL for more details. http://lists.openstack.org/pipermail/openstack-discuss/2019-April/005097.html Co-Authored-By: Takashi Natsume Change-Id: Ied64e92d5833ed11e1213c42994cfebeaa2ace6a Signed-off-by: zhangyangyang --- .zuul.yaml | 1 - doc/source/contributor/testing.rst | 8 ++++---- setup.cfg | 1 - tox.ini | 7 +++---- 4 files changed, 7 insertions(+), 10 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index 7acd53209..170111275 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -17,7 +17,6 @@ - openstack-cover-jobs - openstack-lower-constraints-jobs - openstack-python-jobs - - openstack-python35-jobs - openstack-python36-jobs - openstack-python37-jobs - publish-openstack-docs-pti diff --git a/doc/source/contributor/testing.rst b/doc/source/contributor/testing.rst index 1b285745e..b699f4ada 100644 --- a/doc/source/contributor/testing.rst +++ b/doc/source/contributor/testing.rst @@ -11,14 +11,14 @@ test targets that can be run to validate the code. ``tox -e py27`` Traditional unit testing (Python 2.7). -``tox -e py35`` - Traditional unit testing (Python 3.5). +``tox -e py36`` + Traditional unit testing (Python 3.6). ``tox -e functional`` Live functional testing against an existing OpenStack instance. (Python 2.7) -``tox -e functional-py35`` - Live functional testing against an existing OpenStack instance. (Python 3.5) +``tox -e functional-py36`` + Live functional testing against an existing OpenStack instance. (Python 3.6) ``tox -e cover`` Generate a coverage report on unit testing. diff --git a/setup.cfg b/setup.cfg index 8ef6c1695..6fc6ba6ce 100644 --- a/setup.cfg +++ b/setup.cfg @@ -19,7 +19,6 @@ classifier = Programming Language :: Python :: 2 Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 [files] diff --git a/tox.ini b/tox.ini index 0e6b4bbeb..ee01ddd57 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,5 @@ -# noted to use py35 you need virtualenv >= 1.11.4 [tox] -envlist = py35,py27,pep8,docs +envlist = py36,py27,pep8,docs minversion = 2.0 skipsdist = True @@ -66,8 +65,8 @@ commands = stestr --test-path=./novaclient/tests/functional run --concurrency=1 {posargs} python novaclient/tests/functional/hooks/check_resources.py -[testenv:functional-py35] -basepython = python3.5 +[testenv:functional-py36] +basepython = python3.6 passenv = OS_NOVACLIENT_TEST_NETWORK commands = stestr --test-path=./novaclient/tests/functional run --concurrency=1 {posargs} From fe4138aea45da82c60259b5e4fd9cc18a50ab07b Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Mon, 22 Apr 2019 20:07:49 +0900 Subject: [PATCH 1514/1705] Updates for OpenDev transition Replace 'git.openstack.org' with 'opendev.org' in contributor/index.rst. Update URLs in other places as well though there are redirects. See the following URLs for more details: http://lists.openstack.org/pipermail/openstack-discuss/2019-March/003603.html http://lists.openstack.org/pipermail/openstack-discuss/2019-April/004920.html Change-Id: Ifb9f4274d2fd0fa81e7501fe176eeead3acd0e3e --- README.rst | 2 +- doc/source/contributor/index.rst | 4 ++-- doc/source/contributor/testing.rst | 2 +- doc/source/reference/deprecation-policy.rst | 4 ++-- novaclient/tests/functional/v2/legacy/test_os_services.py | 2 +- novaclient/tests/functional/v2/test_os_services.py | 2 +- novaclient/tests/unit/v2/test_shell.py | 2 +- tox.ini | 8 ++++---- 8 files changed, 13 insertions(+), 13 deletions(-) diff --git a/README.rst b/README.rst index 06914fe4c..fe139a7b4 100644 --- a/README.rst +++ b/README.rst @@ -35,7 +35,7 @@ This is a client for the OpenStack Compute API. It provides a Python API (the .. _Launchpad project: https://launchpad.net/python-novaclient .. _Blueprints: https://blueprints.launchpad.net/python-novaclient .. _Bugs: https://bugs.launchpad.net/python-novaclient -.. _Source: https://git.openstack.org/cgit/openstack/python-novaclient +.. _Source: https://opendev.org/openstack/python-novaclient .. _How to Contribute: https://docs.openstack.org/infra/manual/developers.html .. _Specs: http://specs.openstack.org/openstack/nova-specs/ .. _Release Notes: https://docs.openstack.org/releasenotes/python-novaclient diff --git a/doc/source/contributor/index.rst b/doc/source/contributor/index.rst index 8dad2d857..91ead0be4 100644 --- a/doc/source/contributor/index.rst +++ b/doc/source/contributor/index.rst @@ -2,11 +2,11 @@ Contributor Guide =================== -Code is hosted at `git.openstack.org`__. Submit bugs to the python-novaclient +Code is hosted at `opendev.org`__. Submit bugs to the python-novaclient project on `Launchpad`__. Submit code to the `openstack/python-novaclient` project using `Gerrit`__. -__ https://git.openstack.org/cgit/openstack/python-novaclient +__ https://opendev.org/openstack/python-novaclient __ https://bugs.launchpad.net/python-novaclient __ https://docs.openstack.org/infra/manual/developers.html#development-workflow diff --git a/doc/source/contributor/testing.rst b/doc/source/contributor/testing.rst index 1b285745e..f4f930214 100644 --- a/doc/source/contributor/testing.rst +++ b/doc/source/contributor/testing.rst @@ -31,4 +31,4 @@ DevStack installation with a demo and an admin user/tenant - or clouds named Refer to `Consistent Testing Interface`__ for more details. -__ https://git.openstack.org/cgit/openstack/governance/tree/reference/project-testing-interface.rst +__ https://opendev.org/openstack/governance/src/branch/master/reference/project-testing-interface.rst diff --git a/doc/source/reference/deprecation-policy.rst b/doc/source/reference/deprecation-policy.rst index 0085bca90..c7e84fbbc 100644 --- a/doc/source/reference/deprecation-policy.rst +++ b/doc/source/reference/deprecation-policy.rst @@ -21,9 +21,9 @@ The process for command deprecation is: 2. Once the change is approved, have a member of the `nova-release`_ team release a new version of `python-novaclient`. - .. _nova-release: https://review.openstack.org/#/admin/groups/147,members + .. _nova-release: https://review.opendev.org/#/admin/groups/147,members -3. Example: ``_ +3. Example: ``_ This change was made while the nova 12.0.0 Liberty release was in development. The current version of `python-novaclient` at the time was diff --git a/novaclient/tests/functional/v2/legacy/test_os_services.py b/novaclient/tests/functional/v2/legacy/test_os_services.py index 6a12de2c2..92d426518 100644 --- a/novaclient/tests/functional/v2/legacy/test_os_services.py +++ b/novaclient/tests/functional/v2/legacy/test_os_services.py @@ -28,7 +28,7 @@ def test_os_service_disable_enable(self): # services returned by client # NOTE(sdague): service disable has the chance in racing # with other tests. Now functional tests for novaclient are launched - # in serial way (https://review.openstack.org/#/c/217768/), but + # in serial way (https://review.opendev.org/#/c/217768/), but # it's a potential issue for making these tests parallel in the future for serv in self.client.services.list(): # In Pike the os-services API was made multi-cell aware and it diff --git a/novaclient/tests/functional/v2/test_os_services.py b/novaclient/tests/functional/v2/test_os_services.py index 40da9f749..8550bba6c 100644 --- a/novaclient/tests/functional/v2/test_os_services.py +++ b/novaclient/tests/functional/v2/test_os_services.py @@ -72,7 +72,7 @@ def test_os_service_disable_enable(self): # services returned by client # NOTE(sdague): service disable has the chance in racing # with other tests. Now functional tests for novaclient are launched - # in serial way (https://review.openstack.org/#/c/217768/), but + # in serial way (https://review.opendev.org/#/c/217768/), but # it's a potential issue for making these tests parallel in the future for serv in self.client.services.list(): # In Pike the os-services API was made multi-cell aware and it diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index bf7a86674..447a7229f 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -4095,7 +4095,7 @@ def test_versions(self): # before feature-freeze # (we can do it, since nova-api change didn't actually add # new microversion, just an additional checks. See - # https://review.openstack.org/#/c/233076/ for more details) + # https://review.opendev.org/#/c/233076/ for more details) 20, # doesn't require any changes in novaclient 27, # NOTE(cdent): 27 adds support for updated microversion # headers, and is tested in test_api_versions, but is diff --git a/tox.ini b/tox.ini index 0e6b4bbeb..afd097f07 100644 --- a/tox.ini +++ b/tox.ini @@ -14,7 +14,7 @@ passenv = ZUUL_CACHE_DIR REQUIREMENTS_PIP_LOCATION install_command = pip install {opts} {packages} deps = - -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} + -c{env:UPPER_CONSTRAINTS_FILE:https://opendev.org/openstack/requirements/raw/branch/master/upper-constraints.txt} -r{toxinidir}/test-requirements.txt -r{toxinidir}/requirements.txt commands = @@ -32,7 +32,7 @@ commands = bandit -r novaclient -n5 -x tests [testenv:venv] basepython = python3 deps = - -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} + -c{env:UPPER_CONSTRAINTS_FILE:https://opendev.org/openstack/requirements/raw/branch/master/upper-constraints.txt} -r{toxinidir}/test-requirements.txt -r{toxinidir}/requirements.txt -r{toxinidir}/doc/requirements.txt @@ -41,7 +41,7 @@ commands = {posargs} [testenv:docs] basepython = python3 deps = - -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} + -c{env:UPPER_CONSTRAINTS_FILE:https://opendev.org/openstack/requirements/raw/branch/master/upper-constraints.txt} -r{toxinidir}/requirements.txt -r{toxinidir}/doc/requirements.txt commands = @@ -53,7 +53,7 @@ commands = [testenv:releasenotes] basepython = python3 deps = - -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} + -c{env:UPPER_CONSTRAINTS_FILE:https://opendev.org/openstack/requirements/raw/branch/master/upper-constraints.txt} -r{toxinidir}/requirements.txt -r{toxinidir}/doc/requirements.txt commands = From 6ea7b506ca7b9b07065e6778f6a8adf23805394f Mon Sep 17 00:00:00 2001 From: chenxing Date: Tue, 7 May 2019 11:54:52 +0800 Subject: [PATCH 1515/1705] Tiny fix of documentation Change-Id: Ida6e6ec6919e4c2303200ff66fdc0a094c7d90d1 --- doc/source/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/index.rst b/doc/source/index.rst index 4ee0122c1..aeb2f268b 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -2,7 +2,7 @@ Python bindings to the OpenStack Nova API =========================================== -This is a client for OpenStack Nova API. There's :doc:`a Python API +This is a client for OpenStack Nova API. There's a :doc:`Python API ` (the :mod:`novaclient` module), and a :doc:`command-line script ` (installed as :program:`nova`). Each implements the entire OpenStack Nova API. From 2595bac2294ce05e389eaab6636977963d5fc66c Mon Sep 17 00:00:00 2001 From: Artom Lifshitz Date: Thu, 9 May 2019 14:34:35 -0400 Subject: [PATCH 1516/1705] Use SHA256 instead of MD5 in completion cache FIPS 140 are U.S. government computer security standards that specify requirements for cryptography modules. MD5 is not FIPS compliant [1]. Previously, MD5 was used as the hash algorithm for the bash completion cache. Hosts running in FIPS mode [2] block execution of the MD5 hash. This makes python-novaclient unusable on FIPS-enabled machines. This patch replaces MD5 with SHA256, which is FIPS compliant. [1] https://csrc.nist.gov/projects/hash-functions [2] https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/security_guide/chap-federal_standards_and_regulations Change-Id: Ia8750bc27aa9a2cfafb6f4f49252f5bd81bc1a40 --- novaclient/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/novaclient/base.py b/novaclient/base.py index 431ac7d6f..821e19bd9 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -307,8 +307,8 @@ def completion_cache(self, cache_type, obj_class, mode): # endpoint pair username = utils.env('OS_USERNAME', 'NOVA_USERNAME') url = utils.env('OS_URL', 'NOVA_URL') - uniqifier = hashlib.md5(username.encode('utf-8') + - url.encode('utf-8')).hexdigest() + uniqifier = hashlib.sha256(username.encode('utf-8') + + url.encode('utf-8')).hexdigest() cache_dir = os.path.expanduser(os.path.join(base_dir, uniqifier)) From a1ac69c69a4fede8ac02b165d23f62b30a242a48 Mon Sep 17 00:00:00 2001 From: Surya Seetharaman Date: Fri, 29 Mar 2019 11:34:04 +0100 Subject: [PATCH 1517/1705] Microversion 2.73: Support adding the reason behind a server lock This patch adds a new parameter ``--reason`` to ``nova lock`` command and ``--locked`` filtering/sorting parameter to ``nova list`` command. This can help users to provide a reason when locking the server and to filter/sort instances based on their locked or value from 2.73 microversion. Implements blueprint add-locked-reason Depends-On: https://review.opendev.org/#/c/648662/ Change-Id: I438e6db2dd5000ba388d0a0f1c8ab74b96b47a71 --- novaclient/__init__.py | 2 +- novaclient/tests/unit/fixture_data/servers.py | 2 ++ novaclient/tests/unit/v2/fakes.py | 18 +++++++++- novaclient/tests/unit/v2/test_servers.py | 23 +++++++++++++ novaclient/tests/unit/v2/test_shell.py | 34 +++++++++++++++++++ novaclient/v2/servers.py | 28 +++++++++++++++ novaclient/v2/shell.py | 25 +++++++++++++- ...bp-add-locked-reason-3f136db97b820c73.yaml | 6 ++++ 8 files changed, 135 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/bp-add-locked-reason-3f136db97b820c73.yaml diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 8b6111de1..304660c93 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.72") +API_MAX_VERSION = api_versions.APIVersion("2.73") diff --git a/novaclient/tests/unit/fixture_data/servers.py b/novaclient/tests/unit/fixture_data/servers.py index 6df4129bd..d3354469a 100644 --- a/novaclient/tests/unit/fixture_data/servers.py +++ b/novaclient/tests/unit/fixture_data/servers.py @@ -454,6 +454,8 @@ def post_servers_1234_action(self, request, context): pass elif action == 'migrate': return None + elif action == 'lock': + return None elif action == 'rebuild': body = body[action] adminPass = body.get('adminPass', 'randompassword') diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index a32e22582..29ec88877 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -774,7 +774,7 @@ def delete_servers_1234_os_server_password(self, **kw): none_actions = ['revertResize', 'os-stop', 'os-start', 'forceDelete', 'restore', 'pause', 'unpause', 'unlock', - 'unrescue', 'resume', 'suspend', 'lock', 'shelve', + 'unrescue', 'resume', 'suspend', 'shelve', 'shelveOffload', 'unshelve', 'resetNetwork'] type_actions = ['os-getVNCConsole', 'os-getSPICEConsole', 'os-getRDPConsole'] @@ -836,6 +836,22 @@ def post_servers_1234_action(self, body, **kw): # host can be optional expected.add('host') assert set(body[action].keys()) == expected + elif action == 'lock': + if self.api_version < api_versions.APIVersion("2.73"): + assert body[action] is None + else: + # In 2.73 and above, we allow body to be one of these: + # a) {'lock': None} + # b) {'lock': {}} + # c) {'lock': {locked_reason': 'blah'}} + if body[action] is not None: + expected = set() + if 'locked_reason' in body[action].keys(): + # reason can be optional + expected.add('locked_reason') + assert set(body[action].keys()) == expected + else: + assert body[action] is None elif action == 'rebuild': body = body[action] adminPass = body.get('adminPass', 'randompassword') diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index 529fd389d..43dbf2c28 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -1703,3 +1703,26 @@ def test_live_migrate_server(self): ex = self.assertRaises(TypeError, self.cs.servers.live_migrate, host='hostname', force=True) self.assertIn('force', six.text_type(ex)) + + +class ServersV273Test(ServersV268Test): + + api_version = "2.73" + + def test_lock_server(self): + s = self.cs.servers.get(1234) + ret = s.lock() + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action', + {'lock': None}) + ret = s.lock(reason='zombie-apocalypse') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action', + {'lock': {'locked_reason': 'zombie-apocalypse'}}) + + def test_lock_server_pre_273_fails_with_reason(self): + self.cs.api_version = api_versions.APIVersion('2.72') + s = self.cs.servers.get(1234) + e = self.assertRaises(TypeError, + s.lock, reason='blah') + self.assertIn("unexpected keyword argument 'reason'", six.text_type(e)) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 447a7229f..4f2ee2582 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2092,6 +2092,24 @@ def test_lock(self): self.run_command('lock sample-server') self.assert_called('POST', '/servers/1234/action', {'lock': None}) + def test_lock_pre_v273(self): + exp = self.assertRaises(SystemExit, + self.run_command, + 'lock sample-server --reason zombies', + api_version='2.72') + self.assertIn('2', six.text_type(exp)) + + def test_lock_v273(self): + self.run_command('lock sample-server', + api_version='2.73') + self.assert_called('POST', '/servers/1234/action', + {'lock': None}) + + self.run_command('lock sample-server --reason zombies', + api_version='2.73') + self.assert_called('POST', '/servers/1234/action', + {'lock': {'locked_reason': 'zombies'}}) + def test_unlock(self): self.run_command('unlock sample-server') self.assert_called('POST', '/servers/1234/action', {'unlock': None}) @@ -4280,6 +4298,22 @@ def test_show_v269_with_down_cells(self): self.assert_called('GET', '/servers/9015', pos=2) self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=3) + def test_list_pre_v273(self): + exp = self.assertRaises(SystemExit, + self.run_command, + 'list --locked t', + api_version='2.72') + self.assertEqual(2, exp.code) + + def test_list_v273(self): + self.run_command('list --locked t', api_version='2.73') + self.assert_called('GET', '/servers/detail?locked=t') + + def test_list_v273_with_sort_key_dir(self): + self.run_command('list --sort locked:asc', api_version='2.73') + self.assert_called( + 'GET', '/servers/detail?sort_dir=asc&sort_key=locked') + class PollForStatusTestCase(utils.TestCase): @mock.patch("novaclient.v2.shell.time") diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index a37c1b387..afbd5536c 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -214,6 +214,7 @@ def unpause(self): """ return self.manager.unpause(self) + @api_versions.wraps("2.0", "2.72") def lock(self): """ Lock -- Lock the instance from certain operations. @@ -222,6 +223,16 @@ def lock(self): """ return self.manager.lock(self) + @api_versions.wraps("2.73") + def lock(self, reason=None): + """ + Lock -- Lock the instance from certain operations. + + :param reason: (Optional) The lock reason. + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self.manager.lock(self, reason=reason) + def unlock(self): """ Unlock -- Remove instance lock. @@ -1097,6 +1108,7 @@ def unpause(self, server): """ return self._action('unpause', server, None) + @api_versions.wraps("2.0", "2.72") def lock(self, server): """ Lock the server. @@ -1106,6 +1118,22 @@ def lock(self, server): """ return self._action('lock', server, None) + @api_versions.wraps("2.73") + def lock(self, server, reason=None): + """ + Lock the server. + + :param server: The :class:`Server` (or its ID) to lock + :param reason: (Optional) The lock reason. + :returns: An instance of novaclient.base.TupleWithMeta + """ + info = None + + if reason: + info = {'locked_reason': reason} + + return self._action('lock', server, info) + def unlock(self, server): """ Unlock the server. diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index f7d59dfaa..4e8a730d3 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -1563,6 +1563,13 @@ def _print_flavor(flavor): "case is 'NOT(t1 OR t2)'. Tags must be separated by commas: " "--not-tags-any "), start_version="2.26") +@utils.arg( + '--locked', + dest='locked', + metavar='', + default=None, + help=_('Display servers based on their locked value'), + start_version="2.73") def do_list(cs, args): """List servers.""" imageid = None @@ -1639,6 +1646,11 @@ def do_list(cs, args): raise exceptions.CommandError(_('Invalid changes-before value: %s') % search_opts['changes-before']) + # In microversion 2.73 we added ``locked`` option in server details. + have_added_locked = cs.api_version >= api_versions.APIVersion('2.73') + if have_added_locked and args.locked: + search_opts['locked'] = args.locked + servers = cs.servers.list(detailed=detailed, search_opts=search_opts, sort_keys=sort_keys, @@ -2155,12 +2167,23 @@ def do_start(cs, args): _("Unable to start the specified server(s).")) +# From microversion 2.73, we can specify a reason for locking the server. @utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg( + '--reason', + metavar='', + help=_('Reason for locking the server.'), + start_version='2.73') def do_lock(cs, args): """Lock a server. A normal (non-admin) user will not be able to execute actions on a locked server. """ - _find_server(cs, args.server).lock() + update_kwargs = {} + if 'reason' in args and args.reason is not None: + update_kwargs['reason'] = args.reason + + server = _find_server(cs, args.server) + server.lock(**update_kwargs) @utils.arg('server', metavar='', help=_('Name or ID of server.')) diff --git a/releasenotes/notes/bp-add-locked-reason-3f136db97b820c73.yaml b/releasenotes/notes/bp-add-locked-reason-3f136db97b820c73.yaml new file mode 100644 index 000000000..60e19bfcf --- /dev/null +++ b/releasenotes/notes/bp-add-locked-reason-3f136db97b820c73.yaml @@ -0,0 +1,6 @@ +--- +features: + - Added a ``--reason`` option to ``nova lock`` command that enables users + to specify a reason when locking a server and a ``locked`` + filtering/sorting option to ``nova list`` command which enables users to + filter/sort servers based on their ``locked`` value in microversion 2.73. From f0388977c1cc557f4c7cb5bc73964ffdebe35016 Mon Sep 17 00:00:00 2001 From: Surya Seetharaman Date: Tue, 14 May 2019 11:31:46 +0200 Subject: [PATCH 1518/1705] [Docs] Update client docs to add reason and locked options This patch adds information about the ``locked`` filter/sorting key and the ``reason`` options that were added in https://review.opendev.org/#/c/648659/. Related to blueprint add-locked-reason Change-Id: I11cc4bd7cee0f03d4398f91bb3790c4c681061f4 --- doc/source/cli/nova.rst | 17 ++++++++++++++++- novaclient/v2/shell.py | 4 +++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/doc/source/cli/nova.rst b/doc/source/cli/nova.rst index eada5c693..e80786795 100644 --- a/doc/source/cli/nova.rst +++ b/doc/source/cli/nova.rst @@ -2223,6 +2223,7 @@ nova list [--changes-before ] [--tags ] [--tags-any ] [--not-tags ] [--not-tags-any ] + [--locked] List servers. @@ -2344,6 +2345,14 @@ present in the failure domain. commas: --not-tags-any (Supported by API versions '2.26' - '2.latest') +``--locked `` + Display servers based on their locked value. A + value must be specified; eg. 'true' will list + only locked servers and 'false' will list only + unlocked servers. (Supported by API versions + '2.73' - '2.latest') + + .. _nova_list-extensions: nova list-extensions @@ -2464,7 +2473,7 @@ nova lock .. code-block:: console - usage: nova lock + usage: nova lock [--reason ] Lock a server. A normal (non-admin) user will not be able to execute actions on a locked server. @@ -2474,6 +2483,12 @@ on a locked server. ```` Name or ID of server. +**Optional arguments:** + +``--reason `` + Reason for locking the server. (Supported by API versions + '2.73' - '2.latest') + .. _nova_meta: nova meta diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 4e8a730d3..88ebf2d79 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -1568,7 +1568,9 @@ def _print_flavor(flavor): dest='locked', metavar='', default=None, - help=_('Display servers based on their locked value'), + help=_("Display servers based on their locked value. A value must be " + "specified; eg. 'true' will list only locked servers and 'false' " + "will list only unlocked servers."), start_version="2.73") def do_list(cs, args): """List servers.""" From 8c3311eefe0ff94a3184d3a7caca89ae1310e934 Mon Sep 17 00:00:00 2001 From: zhangbailin Date: Wed, 15 May 2019 10:28:03 +0800 Subject: [PATCH 1519/1705] Cap sphinx for py2 and drop keyring dependency Sphinx 2.0.0 dropped Python 2.7 support. This is aligned with [1] in requirements project. [1]Change-Id If558f184c959e4b63b56dec3ca1571d1034cfe5c keyring is also removed since it hasn't been used since change I62188e73a48f6878ce920a3b4724dba101564aef. Change-Id: Ib632c327637ba1161a8c07605fd3ef327f5606ee --- doc/requirements.txt | 3 ++- lower-constraints.txt | 1 - test-requirements.txt | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index 27cc8b9f6..26ec8743f 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,7 +1,8 @@ # 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. -sphinx!=1.6.6,!=1.6.7,>=1.6.2 # BSD +sphinx!=1.6.6,!=1.6.7,>=1.6.2,<2.0.0;python_version=='2.7' # BSD +sphinx!=1.6.6,!=1.6.7,>=1.6.2;python_version>='3.4' # BSD openstackdocstheme>=1.18.1 # Apache-2.0 reno>=2.5.0 # Apache-2.0 sphinxcontrib-apidoc>=0.2.0 # BSD diff --git a/lower-constraints.txt b/lower-constraints.txt index 350574be3..b87fcaf0e 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -33,7 +33,6 @@ jmespath==0.9.0 jsonpatch==1.16 jsonpointer==1.13 jsonschema==2.6.0 -keyring==5.5.1 keystoneauth1==3.5.0 kombu==4.0.0 linecache2==1.0.0 diff --git a/test-requirements.txt b/test-requirements.txt index 83326f074..998e89a8b 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -7,7 +7,6 @@ bandit>=1.1.0 # Apache-2.0 coverage!=4.4,>=4.0 # Apache-2.0 ddt>=1.0.1 # MIT fixtures>=3.0.0 # Apache-2.0/BSD -keyring>=5.5.1 # MIT/PSF mock>=2.0.0 # BSD python-keystoneclient>=3.8.0 # Apache-2.0 python-cinderclient>=3.3.0 # Apache-2.0 From 2ecb39710f1fd9f6e5765e52d29953c2af44e714 Mon Sep 17 00:00:00 2001 From: chenke Date: Wed, 13 Mar 2019 20:27:37 +0800 Subject: [PATCH 1520/1705] Optimize limit option docs string description for novaclient Change-Id: I85ee36b0447869771782d1059957a46447940b59 --- novaclient/v2/flavors.py | 3 +++ novaclient/v2/hypervisors.py | 3 +++ novaclient/v2/instance_action.py | 3 +++ novaclient/v2/keypairs.py | 3 +++ novaclient/v2/migrations.py | 6 ++++++ novaclient/v2/server_groups.py | 3 +++ novaclient/v2/servers.py | 4 ++++ novaclient/v2/usage.py | 8 ++++++-- 8 files changed, 31 insertions(+), 2 deletions(-) diff --git a/novaclient/v2/flavors.py b/novaclient/v2/flavors.py index 9cc2b6433..803e226a0 100644 --- a/novaclient/v2/flavors.py +++ b/novaclient/v2/flavors.py @@ -116,6 +116,9 @@ def list(self, detailed=True, is_public=True, marker=None, min_disk=None, :param min_disk: Filters the flavors by a minimum disk space, in GiB. :param min_ram: Filters the flavors by a minimum RAM, in MiB. :param limit: maximum number of flavors to return (optional). + Note the API server has a configurable default limit. + If no limit is specified here or limit is larger than + default, the default limit will be used. :param sort_key: Flavors list sort key (optional). :param sort_dir: Flavors list sort direction (optional). :returns: list of :class:`Flavor`. diff --git a/novaclient/v2/hypervisors.py b/novaclient/v2/hypervisors.py index f457fa084..4b26bfc59 100644 --- a/novaclient/v2/hypervisors.py +++ b/novaclient/v2/hypervisors.py @@ -70,6 +70,9 @@ def list(self, detailed=True, marker=None, limit=None): marker must be a UUID hypervisor ID. (optional). :param limit: maximum number of hypervisors to return (optional). + Note the API server has a configurable default limit. + If no limit is specified here or limit is larger than + default, the default limit will be used. """ return self._list_base(detailed=detailed, marker=marker, limit=limit) diff --git a/novaclient/v2/instance_action.py b/novaclient/v2/instance_action.py index 79f1b6173..d84c50f90 100644 --- a/novaclient/v2/instance_action.py +++ b/novaclient/v2/instance_action.py @@ -53,6 +53,9 @@ def list(self, server, marker=None, limit=None, changes_since=None): list than that represented by this action request id (optional). :param limit: Maximum number of actions to return. (optional). + Note the API server has a configurable default limit. + If no limit is specified here or limit is larger than + default, the default limit will be used. :param changes_since: List only instance actions changed later or equal to a certain point of time. The provided time should be an ISO 8061 formatted time. diff --git a/novaclient/v2/keypairs.py b/novaclient/v2/keypairs.py index 08c5ea39e..9b2e73b71 100644 --- a/novaclient/v2/keypairs.py +++ b/novaclient/v2/keypairs.py @@ -185,6 +185,9 @@ def list(self, user_id=None, marker=None, limit=None): keypair list than that represented by this keypair name (optional). :param limit: maximum number of keypairs to return (optional). + Note the API server has a configurable default limit. + If no limit is specified here or limit is larger than + default, the default limit will be used. """ params = {} if user_id: diff --git a/novaclient/v2/migrations.py b/novaclient/v2/migrations.py index f1954f1ab..ab62f2c5d 100644 --- a/novaclient/v2/migrations.py +++ b/novaclient/v2/migrations.py @@ -70,6 +70,9 @@ def list(self, host=None, status=None, instance_uuid=None, migrations list than that represented by this migration UUID (optional). :param limit: maximum number of migrations to return (optional). + Note the API server has a configurable default limit. If no limit is + specified here or limit is larger than default, the default limit will + be used. :param changes_since: only return migrations changed later or equal to a certain point of time. The provided time should be an ISO 8061 formatted time. e.g. 2016-03-04T06:27:59Z . (optional). @@ -92,6 +95,9 @@ def list(self, host=None, status=None, instance_uuid=None, migrations list than that represented by this migration UUID (optional). :param limit: maximum number of migrations to return (optional). + Note the API server has a configurable default limit. If no limit is + specified here or limit is larger than default, the default limit will + be used. :param changes_since: Only return migrations changed later or equal to a certain point of time. The provided time should be an ISO 8061 formatted time. e.g. 2016-03-04T06:27:59Z . (optional). diff --git a/novaclient/v2/server_groups.py b/novaclient/v2/server_groups.py index e8839ae59..bc66b01a2 100644 --- a/novaclient/v2/server_groups.py +++ b/novaclient/v2/server_groups.py @@ -51,6 +51,9 @@ def list(self, all_projects=False, limit=None, offset=None): :param all_projects: Lists server groups for all projects. (optional) :param limit: Maximum number of server groups to return. (optional) + Note the API server has a configurable default limit. + If no limit is specified here or limit is larger than + default, the default limit will be used. :param offset: Use with `limit` to return a slice of server groups. `offset` is where to start in the groups list. (optional) diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index a37c1b387..44c77ffa3 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -832,6 +832,10 @@ def list(self, detailed=True, search_opts=None, marker=None, limit=None, :param marker: Begin returning servers that appear later in the server list than that represented by this server id (optional). :param limit: Maximum number of servers to return (optional). + Note the API server has a configurable default limit. + If no limit is specified here or limit is larger than + default, the default limit will be used. + If limit == -1, all servers will be returned. :param sort_keys: List of sort keys :param sort_dirs: List of sort directions diff --git a/novaclient/v2/usage.py b/novaclient/v2/usage.py index abbeca692..32fc55085 100644 --- a/novaclient/v2/usage.py +++ b/novaclient/v2/usage.py @@ -87,7 +87,9 @@ def list(self, start, end, detailed=False, marker=None, limit=None): later in the instance list than that represented by this instance UUID (optional). :param limit: Maximum number of instances to include in the usage - (optional). + (optional). Note the API server has a configurable + default limit. If no limit is specified here or limit + is larger than default, the default limit will be used. :rtype: list of :class:`Usage`. """ query_string = self._usage_query(start, end, marker, limit, detailed) @@ -120,7 +122,9 @@ def get(self, tenant_id, start, end, marker=None, limit=None): later in the instance list than that represented by this instance UUID (optional). :param limit: Maximum number of instances to include in the usage - (optional). + (optional). Note the API server has a configurable + default limit. If no limit is specified here or limit + is larger than default, the default limit will be used. :rtype: :class:`Usage` """ query_string = self._usage_query(start, end, marker, limit) From 0dc6b96ec83703a0607c4df16e43856632aecb61 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Fri, 17 May 2019 18:07:38 -0400 Subject: [PATCH 1521/1705] Allow searching for hypervisors and getting back details The 2.53 microversion allows listing hypervisors with details and filtering on a hypervisor_hostname substring pattern match. This makes the python API binding HypervisorManager.search method allow that as well by adding a new 'detailed' boolean kwarg which defaults to False for backward compatibility for the /search and /servers routes before 2.53, but allows searching and getting detailed results back as well. Change-Id: I81fa4520af3cc7f1298c262f4fdc4e15fbc6d7ba --- .../tests/functional/v2/test_hypervisors.py | 13 +++++++++++ novaclient/tests/unit/v2/test_hypervisors.py | 23 +++++++++++++++++++ novaclient/v2/hypervisors.py | 15 +++++++++--- ...-hypervisor-detailed-352f3ac70d42fe6e.yaml | 8 +++++++ 4 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/search-hypervisor-detailed-352f3ac70d42fe6e.yaml diff --git a/novaclient/tests/functional/v2/test_hypervisors.py b/novaclient/tests/functional/v2/test_hypervisors.py index ca66a44a2..bb58648b9 100644 --- a/novaclient/tests/functional/v2/test_hypervisors.py +++ b/novaclient/tests/functional/v2/test_hypervisors.py @@ -26,3 +26,16 @@ class TestHypervisorsV2_53(TestHypervisorsV28): def test_list(self): self._test_list(cpu_info_type=dict, uuid_as_id=True) + + def test_search_with_details(self): + # First find a hypervisor from the list to search on. + hypervisors = self.client.hypervisors.list() + # Now search for that hypervisor with details. + hypervisor = hypervisors[0] + hypervisors = self.client.hypervisors.search( + hypervisor.hypervisor_hostname, detailed=True) + self.assertEqual(1, len(hypervisors)) + hypervisor = hypervisors[0] + # We know we got details if service is in the response. + self.assertIsNotNone(hypervisor.service, + 'Expected service in hypervisor: %s' % hypervisor) diff --git a/novaclient/tests/unit/v2/test_hypervisors.py b/novaclient/tests/unit/v2/test_hypervisors.py index 0c24209bc..3eaeb576f 100644 --- a/novaclient/tests/unit/v2/test_hypervisors.py +++ b/novaclient/tests/unit/v2/test_hypervisors.py @@ -13,6 +13,8 @@ # License for the specific language governing permissions and limitations # under the License. +import six + from novaclient import api_versions from novaclient import exceptions from novaclient.tests.unit.fixture_data import client @@ -119,6 +121,14 @@ def test_hypervisor_search_unicode(self): self.cs.hypervisors.search, hypervisor_match) + def test_hypervisor_search_detailed(self): + # detailed=True is not supported before 2.53 + ex = self.assertRaises(exceptions.UnsupportedVersion, + self.cs.hypervisors.search, 'hyper', + detailed=True) + self.assertIn('Parameter "detailed" requires API version 2.53 or ' + 'greater.', six.text_type(ex)) + def test_hypervisor_servers(self): expected = [ dict(id=self.data_fixture.hyper_id_1, @@ -236,3 +246,16 @@ class HypervisorsV2_53Test(HypervisorsV233Test): def setUp(self): super(HypervisorsV2_53Test, self).setUp() self.cs.api_version = api_versions.APIVersion("2.53") + + def test_hypervisor_search_detailed(self): + expected = [ + dict(id=self.data_fixture.hyper_id_1, + hypervisor_hostname='hyper1'), + dict(id=self.data_fixture.hyper_id_2, + hypervisor_hostname='hyper2')] + result = self.cs.hypervisors.search('hyper', detailed=True) + self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called( + 'GET', '/os-hypervisors/detail?hypervisor_hostname_pattern=hyper') + for idx, hyper in enumerate(result): + self.compare_to_expected(expected[idx], hyper) diff --git a/novaclient/v2/hypervisors.py b/novaclient/v2/hypervisors.py index 4b26bfc59..e6f5038c8 100644 --- a/novaclient/v2/hypervisors.py +++ b/novaclient/v2/hypervisors.py @@ -23,6 +23,8 @@ from novaclient import api_versions from novaclient import base +from novaclient import exceptions +from novaclient.i18n import _ from novaclient import utils @@ -76,7 +78,7 @@ def list(self, detailed=True, marker=None, limit=None): """ return self._list_base(detailed=detailed, marker=marker, limit=limit) - def search(self, hypervisor_match, servers=False): + def search(self, hypervisor_match, servers=False, detailed=False): """ Get a list of matching hypervisors. @@ -84,6 +86,8 @@ def search(self, hypervisor_match, servers=False): The hypervisor hosts are selected with the host name matching this pattern. :param servers: If True, server information is also retrieved. + :param detailed: If True, detailed hypervisor information is returned. + This requires API version 2.53 or greater. """ # Starting with microversion 2.53, the /servers and /search routes are # deprecated and we get the same results using GET /os-hypervisors @@ -91,11 +95,16 @@ def search(self, hypervisor_match, servers=False): if six.PY2: hypervisor_match = encodeutils.safe_encode(hypervisor_match) if self.api_version >= api_versions.APIVersion('2.53'): - url = ('/os-hypervisors?hypervisor_hostname_pattern=%s' % - parse.quote(hypervisor_match, safe='')) + url = ('/os-hypervisors%s?hypervisor_hostname_pattern=%s' % + ('/detail' if detailed else '', + parse.quote(hypervisor_match, safe=''))) if servers: url += '&with_servers=True' else: + if detailed: + raise exceptions.UnsupportedVersion( + _('Parameter "detailed" requires API version 2.53 or ' + 'greater.')) target = 'servers' if servers else 'search' url = ('/os-hypervisors/%s/%s' % (parse.quote(hypervisor_match, safe=''), target)) diff --git a/releasenotes/notes/search-hypervisor-detailed-352f3ac70d42fe6e.yaml b/releasenotes/notes/search-hypervisor-detailed-352f3ac70d42fe6e.yaml new file mode 100644 index 000000000..026e1a283 --- /dev/null +++ b/releasenotes/notes/search-hypervisor-detailed-352f3ac70d42fe6e.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + The ``novaclient.v2.hypervisors.HypervisorManager.search`` method now + accepts a ``detailed`` boolean kwarg which defaults to False but when + True will search for the given hypervisor hostname match and return + details about any matching hypervisors. Specifying ``detailed=True`` + requires compute API version 2.53 or greater. From f78a4706d3522e64fd45daaf5e68c017c55c5240 Mon Sep 17 00:00:00 2001 From: Surya Seetharaman Date: Fri, 17 May 2019 14:04:17 +0200 Subject: [PATCH 1522/1705] Allow passing negative values for the locked search_opt in cs.servers.list This patch adds support for novaclient to pass the "locked" search_opts with a "False" value upto the server side. This is required for the OSC change[1] to support the "unlocked" filter parameter. [1] https://review.opendev.org/#/c/659124/ Related to blueprint add-locked-reason Change-Id: I0907f4ad2decb308c9db8b0cf9e7962bf1b517b5 --- novaclient/tests/unit/v2/test_servers.py | 17 +++++++++++++++++ novaclient/v2/servers.py | 9 +++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index 43dbf2c28..5410f1b31 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -76,6 +76,15 @@ def test_list_all_servers(self): for s in sl: self.assertIsInstance(s, servers.Server) + def test_filter_servers_unlocked(self): + # calling the cs.servers.list python binding + # will fail before 2.73 microversion. + e = self.assertRaises(exceptions.UnsupportedAttribute, + self.cs.servers.list, + search_opts={'locked': False}) + self.assertIn("'locked' argument is only allowed since " + "microversion 2.73.", six.text_type(e)) + def test_list_servers_undetailed(self): sl = self.cs.servers.list(detailed=False) self.assert_request_id(sl, fakes.FAKE_REQUEST_ID_LIST) @@ -1726,3 +1735,11 @@ def test_lock_server_pre_273_fails_with_reason(self): e = self.assertRaises(TypeError, s.lock, reason='blah') self.assertIn("unexpected keyword argument 'reason'", six.text_type(e)) + + def test_filter_servers_unlocked(self): + # support locked=False + sl = self.cs.servers.list(search_opts={'locked': False}) + self.assert_request_id(sl, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('GET', '/servers/detail?locked=False') + for s in sl: + self.assertIsInstance(s, servers.Server) diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index afbd5536c..e5a33fb6b 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -862,9 +862,14 @@ def list(self, detailed=True, search_opts=None, marker=None, limit=None, search_opts = {} qparams = {} - + # In microversion 2.73 we added ``locked`` filtering option + # for listing server details. + if ('locked' in search_opts and + self.api_version < api_versions.APIVersion('2.73')): + raise exceptions.UnsupportedAttribute("locked", "2.73") for opt, val in search_opts.items(): - if val: + # support locked=False from 2.73 microversion + if val or (opt == 'locked' and val is False): if isinstance(val, six.text_type): val = val.encode('utf-8') qparams[opt] = val From 7b9d3000893aefa9c5efc77b50dc0fec5cab29be Mon Sep 17 00:00:00 2001 From: zhangbailin Date: Thu, 23 May 2019 08:18:00 +0800 Subject: [PATCH 1523/1705] Set the lower limit of api_version for volume_type In boot server API, the volume_type is supported by microversion 2.67. Therefore, the lower limit of the volume_type should be added to the CLI command, otherwise an exception will occur. Co-Authored-By: Eric Xie Closes-Bug: #1829854 Change-Id: Ie909c16568b017ca0acc802194140da7bece76c5 --- novaclient/tests/unit/v2/test_shell.py | 28 ++++++++++++++++++++++++++ novaclient/v2/shell.py | 2 +- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 4f2ee2582..354475dc2 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -624,6 +624,34 @@ def test_boot_bdms_v2_invalid_shutdown_value(self): 'size=1,format=ext4,type=disk,shutdown=foobar ' 'some-server' % FAKE_UUID_1)) + def test_boot_from_volume_with_volume_type_latest_microversion(self): + self.run_command( + 'boot --flavor 1 --block-device id=%s,source=image,dest=volume,' + 'size=1,bootindex=0,shutdown=remove,tag=foo,volume_type=lvm ' + 'bfv-server' % FAKE_UUID_1, api_version='2.latest') + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'bfv-server', + 'block_device_mapping_v2': [ + { + 'uuid': FAKE_UUID_1, + 'source_type': 'image', + 'destination_type': 'volume', + 'volume_size': '1', + 'delete_on_termination': True, + 'tag': 'foo', + 'boot_index': '0', + 'volume_type': 'lvm' + }, + ], + 'networks': 'auto', + 'imageRef': '', + 'min_count': 1, + 'max_count': 1, + }}) + def test_boot_from_volume_with_volume_type_old_microversion(self): ex = self.assertRaises( exceptions.CommandError, self.run_command, diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 88ebf2d79..6653aa2f6 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -145,7 +145,7 @@ def _parse_block_device_mapping_v2(cs, args, image): 'delete_on_termination': False} bdm.append(bdm_dict) - supports_volume_type = cs.api_version == api_versions.APIVersion('2.67') + supports_volume_type = cs.api_version >= api_versions.APIVersion('2.67') for device_spec in args.block_device: spec_dict = _parse_device_spec(device_spec) From 6ce10633849cae65e9388eb75d7f0455c9d9a121 Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Mon, 27 May 2019 13:02:08 +0900 Subject: [PATCH 1524/1705] Add a description of --on-shared-storage Add a description of --on-shared-storage option in the 'nova evacuate' command in the CLI reference. Change-Id: I4069b804b434b70ee6d0098831af748e47913ce3 Closes-Bug: #1829932 --- doc/source/cli/nova.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/doc/source/cli/nova.rst b/doc/source/cli/nova.rst index e80786795..1fb287983 100644 --- a/doc/source/cli/nova.rst +++ b/doc/source/cli/nova.rst @@ -1258,7 +1258,7 @@ nova evacuate .. code-block:: console - usage: nova evacuate [--password ] [--force] [] + usage: nova evacuate [--password ] [--on-shared-storage] [--force] [] Evacuate server from failed host. @@ -1278,6 +1278,10 @@ Evacuate server from failed host. server. Not applicable if the server is on shared storage. +``--on-shared-storage`` + Specifies whether server files are located on shared + storage. (Supported by API versions '2.0' - '2.13') + ``--force`` Force an evacuation by not verifying the provided destination host by the scheduler. (Supported by API versions '2.29' - '2.67') From 3df454525172bad47ae69626e92fd6c4020eaa91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BF=9F=E5=B0=8F=E5=90=9B?= Date: Tue, 4 Jun 2019 10:02:00 +0800 Subject: [PATCH 1525/1705] Bump openstackdocstheme to 1.30.0 ...to pick up many improvements, including the return of table borders. Change-Id: I59c62ad129462c94bc4cc815101fb31ff062d2e3 --- doc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index 26ec8743f..0b7cb44e7 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -3,7 +3,7 @@ # process, which may cause wedges in the gate later. sphinx!=1.6.6,!=1.6.7,>=1.6.2,<2.0.0;python_version=='2.7' # BSD sphinx!=1.6.6,!=1.6.7,>=1.6.2;python_version>='3.4' # BSD -openstackdocstheme>=1.18.1 # Apache-2.0 +openstackdocstheme>=1.30.0 # Apache-2.0 reno>=2.5.0 # Apache-2.0 sphinxcontrib-apidoc>=0.2.0 # BSD From edbe4192bf88ae4907a35ad0e96a98f0a0462836 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BF=9F=E5=B0=8F=E5=90=9B?= Date: Tue, 4 Jun 2019 10:51:30 +0800 Subject: [PATCH 1526/1705] Blacklist python-cinderclient 4.0.0 This release of the Cinder client broke support for the v3 volume-transfer APIs unless microversion 3.55 or higher was requested. Depends-On https://review.opendev.org/#/c/587877/ Change-Id: I917b0a47896fcac3edab4b8e55bde71a0aa55632 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 998e89a8b..88cad0e46 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -9,7 +9,7 @@ ddt>=1.0.1 # MIT fixtures>=3.0.0 # Apache-2.0/BSD mock>=2.0.0 # BSD python-keystoneclient>=3.8.0 # Apache-2.0 -python-cinderclient>=3.3.0 # Apache-2.0 +python-cinderclient!=4.0.0,>=3.3.0 # Apache-2.0 python-glanceclient>=2.8.0 # Apache-2.0 python-neutronclient>=6.7.0 # Apache-2.0 requests-mock>=1.2.0 # Apache-2.0 From 763898687d67c0818f80428f21ca5f321951a052 Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Wed, 19 Jun 2019 11:29:51 +0900 Subject: [PATCH 1527/1705] Fix duplicate object description error Add a directive option flag :noindex: in reference/api/index.rst. Change-Id: Ifa5cf9820be29f498c218105272837cac50cb643 Closes-Bug: #1833327 --- doc/source/reference/api/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/source/reference/api/index.rst b/doc/source/reference/api/index.rst index e4d9c4d6b..d4959400a 100644 --- a/doc/source/reference/api/index.rst +++ b/doc/source/reference/api/index.rst @@ -4,6 +4,7 @@ .. module:: novaclient :synopsis: A client for the OpenStack Nova API. + :noindex: .. currentmodule:: novaclient From 52ae954bd09bbb1536bc03b0be334d02e66fe93c Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Wed, 19 Jun 2019 13:42:11 +0900 Subject: [PATCH 1528/1705] Add irrelevant files in dsvm job Change-Id: I25235eae93d2be6c83fbe9676304a03cfe187865 Closes-Bug: #1833338 --- .zuul.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.zuul.yaml b/.zuul.yaml index 170111275..acf13ce7c 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -8,6 +8,15 @@ - openstack/devstack-gate - openstack/nova - openstack/python-novaclient + irrelevant-files: + - ^(test-|)requirements.txt$ + - ^.*\.rst$ + - ^doc/.*$ + - ^novaclient/tests/.*$ + - ^releasenotes/.*$ + - ^setup.cfg$ + - ^tools/.*$ + - ^tox.ini$ - project: templates: From 4bfcc1a9faae1c505266abd0e2a61b8b3cf621f4 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Mon, 24 Jun 2019 16:05:47 +0000 Subject: [PATCH 1529/1705] Revert "Add irrelevant files in dsvm job" This reverts commit 52ae954bd09bbb1536bc03b0be334d02e66fe93c. The change ignored way too many things that should be run through the functional dsvm job, such as changes to the functional tests themselves, requirements, setup.cfg and tox.ini changes. The bug was about not running the dsvm functional job on docs changes and then went much further and clearly not much thought was put into what was being ignored and if it should have been, so let's just revert and have a do-over. Change-Id: Ie78badfeed7e51f1786eb5041010c7d17b02e412 --- .zuul.yaml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index acf13ce7c..170111275 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -8,15 +8,6 @@ - openstack/devstack-gate - openstack/nova - openstack/python-novaclient - irrelevant-files: - - ^(test-|)requirements.txt$ - - ^.*\.rst$ - - ^doc/.*$ - - ^novaclient/tests/.*$ - - ^releasenotes/.*$ - - ^setup.cfg$ - - ^tools/.*$ - - ^tox.ini$ - project: templates: From 0a2d108ec33a2eb197745157606deaa61a582872 Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Sat, 29 Jun 2019 23:52:46 +0900 Subject: [PATCH 1530/1705] Add irrelevant files in dsvm job again Change-Id: I0338ea22cce6435cfebfabc117f4613d0b4de33d Closes-Bug: #1833338 --- .zuul.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.zuul.yaml b/.zuul.yaml index 170111275..3e043f74f 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -8,6 +8,10 @@ - openstack/devstack-gate - openstack/nova - openstack/python-novaclient + irrelevant-files: + - ^.*\.rst$ + - ^doc/.*$ + - ^releasenotes/.*$ - project: templates: From 12193fc56b7524d12b208941101d83b019c8b87b Mon Sep 17 00:00:00 2001 From: pengyuesheng Date: Mon, 1 Jul 2019 17:20:06 +0800 Subject: [PATCH 1531/1705] Blacklist sphinx 2.1.0 (autodoc bug) See https://github.com/sphinx-doc/sphinx/issues/6440 for upstream details Depend-On: https://review.opendev.org/#/c/663060/ Change-Id: I3594f05a2eb189395d4b691c47e1e0764fa0fe44 --- doc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index 0b7cb44e7..7cfd8e099 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -2,7 +2,7 @@ # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. sphinx!=1.6.6,!=1.6.7,>=1.6.2,<2.0.0;python_version=='2.7' # BSD -sphinx!=1.6.6,!=1.6.7,>=1.6.2;python_version>='3.4' # BSD +sphinx!=1.6.6,!=1.6.7,!=2.1.0,>=1.6.2;python_version>='3.4' # BSD openstackdocstheme>=1.30.0 # Apache-2.0 reno>=2.5.0 # Apache-2.0 sphinxcontrib-apidoc>=0.2.0 # BSD From 77ad81bde80f728fe0f13809481ae5117ab8fec3 Mon Sep 17 00:00:00 2001 From: pengyuesheng Date: Mon, 1 Jul 2019 17:17:20 +0800 Subject: [PATCH 1532/1705] Add Python 3 Train unit tests See the Train python3-updates goal document for details: https://governance.openstack.org/tc/goals/train/python3-updates.html Change-Id: I691d7994070012af62d20d15809b7c4f143909a8 --- .zuul.yaml | 3 +-- setup.cfg | 1 + tox.ini | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index 170111275..192845fb2 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -17,8 +17,7 @@ - openstack-cover-jobs - openstack-lower-constraints-jobs - openstack-python-jobs - - openstack-python36-jobs - - openstack-python37-jobs + - openstack-python3-train-jobs - publish-openstack-docs-pti - release-notes-jobs-python3 check: diff --git a/setup.cfg b/setup.cfg index 6fc6ba6ce..42b82ac21 100644 --- a/setup.cfg +++ b/setup.cfg @@ -20,6 +20,7 @@ classifier = Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 [files] packages = diff --git a/tox.ini b/tox.ini index 3e9eab152..31a236291 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py36,py27,pep8,docs +envlist = py37,py36,py27,pep8,docs minversion = 2.0 skipsdist = True From 54904711a29687fa9e5aa7981dd6da6443608a39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BF=9F=E5=B0=8F=E5=90=9B?= Date: Tue, 18 Jun 2019 18:03:58 +0800 Subject: [PATCH 1533/1705] Modify the url of upper_constraints_file As described at [1]. [1] http://lists.openstack.org/pipermail/openstack-discuss/2019-May/006478.html Change-Id: I76c9f982ff6cf46db25f626575257af483e3434e --- tox.ini | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tox.ini b/tox.ini index 3e9eab152..36c42b989 100644 --- a/tox.ini +++ b/tox.ini @@ -13,7 +13,7 @@ passenv = ZUUL_CACHE_DIR REQUIREMENTS_PIP_LOCATION install_command = pip install {opts} {packages} deps = - -c{env:UPPER_CONSTRAINTS_FILE:https://opendev.org/openstack/requirements/raw/branch/master/upper-constraints.txt} + -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/test-requirements.txt -r{toxinidir}/requirements.txt commands = @@ -31,7 +31,7 @@ commands = bandit -r novaclient -n5 -x tests [testenv:venv] basepython = python3 deps = - -c{env:UPPER_CONSTRAINTS_FILE:https://opendev.org/openstack/requirements/raw/branch/master/upper-constraints.txt} + -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/test-requirements.txt -r{toxinidir}/requirements.txt -r{toxinidir}/doc/requirements.txt @@ -40,7 +40,7 @@ commands = {posargs} [testenv:docs] basepython = python3 deps = - -c{env:UPPER_CONSTRAINTS_FILE:https://opendev.org/openstack/requirements/raw/branch/master/upper-constraints.txt} + -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/requirements.txt -r{toxinidir}/doc/requirements.txt commands = @@ -52,7 +52,7 @@ commands = [testenv:releasenotes] basepython = python3 deps = - -c{env:UPPER_CONSTRAINTS_FILE:https://opendev.org/openstack/requirements/raw/branch/master/upper-constraints.txt} + -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/requirements.txt -r{toxinidir}/doc/requirements.txt commands = From 81ec72ecf8c32b409afedc60e013f9b4c47d5bae Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Thu, 27 Jun 2019 08:58:38 +0900 Subject: [PATCH 1534/1705] Remove deprecated methods and properties Remove deprecated methods, properties and unused classes. Change-Id: I16e0b6e55a9c9da04c4582f9be672018d37bf368 --- novaclient/client.py | 23 ----------- novaclient/exceptions.py | 36 ----------------- novaclient/tests/unit/test_client.py | 20 ---------- novaclient/v2/client.py | 39 ++----------------- ...ecated-methods-train-c450fe317c90d7f0.yaml | 24 ++++++++++++ 5 files changed, 27 insertions(+), 115 deletions(-) create mode 100644 releasenotes/notes/remove-deprecated-methods-train-c450fe317c90d7f0.yaml diff --git a/novaclient/client.py b/novaclient/client.py index 2808428ed..4b3e22819 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -85,21 +85,6 @@ def get_timings(self): def reset_timings(self): self.times = [] - @property - def management_url(self): - self.logger.warning( - _("Property `management_url` is deprecated for SessionClient. " - "Use `endpoint_override` instead.")) - return self.endpoint_override - - @management_url.setter - def management_url(self, value): - self.logger.warning( - _("Property `management_url` is deprecated for SessionClient. " - "It should be set via `endpoint_override` variable while class" - " initialization.")) - self.endpoint_override = value - def _construct_http_client(api_version=None, auth=None, @@ -211,14 +196,6 @@ def _get_client_class_and_version(version): "novaclient.v%s.client.Client" % version.ver_major) -def get_client_class(version): - """Returns Client class based on given version.""" - warnings.warn(_("'get_client_class' is deprecated. " - "Please use `novaclient.client.Client` instead.")) - _api_version, client_class = _get_client_class_and_version(version) - return client_class - - def _check_arguments(kwargs, release, deprecated_name, right_name=None): """Process deprecation of arguments. diff --git a/novaclient/exceptions.py b/novaclient/exceptions.py index a7046d338..4c0a5d8df 100644 --- a/novaclient/exceptions.py +++ b/novaclient/exceptions.py @@ -55,46 +55,10 @@ class CommandError(Exception): pass -class AuthorizationFailure(Exception): - pass - - class NoUniqueMatch(Exception): pass -class NoTokenLookupException(Exception): - """This form of authentication does not support looking up - endpoints from an existing token. - """ - pass - - -class EndpointNotFound(Exception): - """Could not find Service or Region in Service Catalog.""" - pass - - -class AmbiguousEndpoints(Exception): - """Found more than one matching endpoint in Service Catalog.""" - def __init__(self, endpoints=None): - self.endpoints = endpoints - - def __str__(self): - return "AmbiguousEndpoints: %s" % repr(self.endpoints) - - -class ConnectionRefused(Exception): - """ - Connection refused: the server refused the connection. - """ - def __init__(self, response=None): - self.response = response - - def __str__(self): - return "ConnectionRefused: %s" % repr(self.response) - - class ResourceInErrorState(Exception): """Resource is in the error state.""" diff --git a/novaclient/tests/unit/test_client.py b/novaclient/tests/unit/test_client.py index 3411eb505..6b348d1bb 100644 --- a/novaclient/tests/unit/test_client.py +++ b/novaclient/tests/unit/test_client.py @@ -26,26 +26,6 @@ import novaclient.v2.client -class ClientTest(utils.TestCase): - def test_get_client_class_v2(self): - output = novaclient.client.get_client_class('2') - self.assertEqual(output, novaclient.v2.client.Client) - - def test_get_client_class_v2_int(self): - output = novaclient.client.get_client_class(2) - self.assertEqual(output, novaclient.v2.client.Client) - - def test_get_client_class_unknown(self): - self.assertRaises(novaclient.exceptions.UnsupportedVersion, - novaclient.client.get_client_class, '0') - - def test_get_client_class_latest(self): - self.assertRaises(novaclient.exceptions.UnsupportedVersion, - novaclient.client.get_client_class, 'latest') - self.assertRaises(novaclient.exceptions.UnsupportedVersion, - novaclient.client.get_client_class, '2.latest') - - class SessionClientTest(utils.TestCase): def test_timings(self): diff --git a/novaclient/v2/client.py b/novaclient/v2/client.py index 54abcf27d..cec9f2f65 100644 --- a/novaclient/v2/client.py +++ b/novaclient/v2/client.py @@ -223,50 +223,17 @@ def api_version(self): def api_version(self, value): self.client.api_version = value - @property - def projectid(self): - self.logger.warning(_("Property 'projectid' is deprecated since " - "Ocata. Use 'project_name' instead.")) - return self.project_name - - @property - def tenant_id(self): - self.logger.warning(_("Property 'tenant_id' is deprecated since " - "Ocata. Use 'project_id' instead.")) - return self.project_id - def __enter__(self): - self.logger.warning(_("NovaClient instance can't be used as a " - "context manager since Ocata (deprecated " - "behaviour) since it is redundant in case of " - "SessionClient.")) - return self + raise exceptions.InvalidUsage(_( + "NovaClient instance can't be used as a context manager " + "since it is redundant in case of SessionClient.")) def __exit__(self, t, v, tb): # do not do anything pass - def set_management_url(self, url): - self.logger.warning( - _("Method `set_management_url` is deprecated since Ocata. " - "Use `endpoint_override` argument instead while initializing " - "novaclient's instance.")) - self.client.set_management_url(url) - def get_timings(self): return self.client.get_timings() def reset_timings(self): self.client.reset_timings() - - def authenticate(self): - """Authenticate against the server. - - Normally this is called automatically when you first access the API, - but you can call this method to force authentication right now. - - Returns on success; raises :exc:`exceptions.Unauthorized` if the - credentials are wrong. - """ - self.logger.warning(_( - "Method 'authenticate' is deprecated since Ocata.")) diff --git a/releasenotes/notes/remove-deprecated-methods-train-c450fe317c90d7f0.yaml b/releasenotes/notes/remove-deprecated-methods-train-c450fe317c90d7f0.yaml new file mode 100644 index 000000000..b8a1d5311 --- /dev/null +++ b/releasenotes/notes/remove-deprecated-methods-train-c450fe317c90d7f0.yaml @@ -0,0 +1,24 @@ +--- +upgrade: + - | + The following properties have been removed. + + - ``novaclient.client.SessionClient`` + + - ``management_url`` + + - ``novaclient.v2.client.Client`` + + - ``projectid`` + - ``tenant_id`` + + The following methods have been removed. + + - ``novaclient.client.get_client_class`` + - ``novaclient.v2.client.Client`` + + - ``set_management_url`` + - ``authenticate`` + + The ``novaclient.v2.client.Client.__enter__`` method now raises + the ``InvalidUsage`` runtime error. From 3cacca202ae1b8a42ca020285022e89a8967629e Mon Sep 17 00:00:00 2001 From: Corey Bryant Date: Fri, 5 Jul 2019 11:47:24 -0400 Subject: [PATCH 1535/1705] Add Python 3 Train unit tests This is a mechanically generated patch to ensure unit testing is in place for all of the Tested Runtimes for Train. See the Train python3-updates goal document for details: https://governance.openstack.org/tc/goals/train/python3-updates.html Change-Id: I2ed3c89b4e571f3e4b69287d1844b26cf4d0268d Story: #2005924 Task: #34226 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 67eb52e4a..dec23521e 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py37,py36,py27,pep8,docs +envlist = py27,py37,pep8,docs minversion = 2.0 skipsdist = True From 41c25881e669b1ebe6211615dd8d2b24a1db23dc Mon Sep 17 00:00:00 2001 From: "zhu.boxiang" Date: Tue, 26 Mar 2019 15:48:46 +0800 Subject: [PATCH 1536/1705] Add host and hypervisor_hostname to create servers Adds the --host and --hypervisor-hostname options to the nova boot command and related python API bindings. Depends-On: https://review.opendev.org/#/c/645520/ Change-Id: If16d00b75f4d5f2b96aa6e3f32a973108049d928 Blueprint: add-host-and-hypervisor-hostname-flag-to-create-server --- doc/source/cli/nova.rst | 10 ++ novaclient/__init__.py | 2 +- novaclient/tests/unit/v2/test_servers.py | 96 +++++++++++++++++++ novaclient/tests/unit/v2/test_shell.py | 78 +++++++++++++++ novaclient/v2/servers.py | 30 +++++- novaclient/v2/shell.py | 21 ++++ .../microversion-v2_74-43b128fe6b84b630.yaml | 9 ++ 7 files changed, 242 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/microversion-v2_74-43b128fe6b84b630.yaml diff --git a/doc/source/cli/nova.rst b/doc/source/cli/nova.rst index 1fb287983..f93c31243 100644 --- a/doc/source/cli/nova.rst +++ b/doc/source/cli/nova.rst @@ -952,6 +952,8 @@ nova boot [--description ] [--tags ] [--return-reservation-id] [--trusted-image-certificate-id ] + [--host ] + [--hypervisor-hostname ] Boot a new server. @@ -1117,6 +1119,14 @@ quality of service support, microversion ``2.72`` is required. May be specified multiple times to pass multiple trusted image certificate IDs. (Supported by API versions '2.63' - '2.latest') +``--host `` + Requested host to create servers. Admin only by default. + (Supported by API versions '2.74' - '2.latest') + +``--hypervisor-hostname `` + Requested hypervisor hostname to create servers. Admin only by default. + (Supported by API versions '2.74' - '2.latest') + .. _nova_cell-capacities: nova cell-capacities diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 304660c93..fb9a485eb 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.73") +API_MAX_VERSION = api_versions.APIVersion("2.74") diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index 5410f1b31..62c8d8d43 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -1743,3 +1743,99 @@ def test_filter_servers_unlocked(self): self.assert_called('GET', '/servers/detail?locked=False') for s in sl: self.assertIsInstance(s, servers.Server) + + +class ServersV274Test(ServersV273Test): + + api_version = "2.74" + + def test_create_server_with_host(self): + self.cs.servers.create( + name="My server", + image=1, + flavor=1, + nics="auto", + host="new-host" + ) + self.assert_called('POST', '/servers', + {'server': { + 'flavorRef': '1', + 'imageRef': '1', + 'max_count': 1, + 'min_count': 1, + 'name': 'My server', + 'networks': 'auto', + 'host': 'new-host' + }} + ) + + def test_create_server_with_hypervisor_hostname(self): + self.cs.servers.create( + name="My server", + image=1, + flavor=1, + nics="auto", + hypervisor_hostname="new-host" + ) + self.assert_called('POST', '/servers', + {'server': { + 'flavorRef': '1', + 'imageRef': '1', + 'max_count': 1, + 'min_count': 1, + 'name': 'My server', + 'networks': 'auto', + 'hypervisor_hostname': 'new-host' + }} + ) + + def test_create_server_with_host_and_hypervisor_hostname(self): + self.cs.servers.create( + name="My server", + image=1, + flavor=1, + nics="auto", + host="new-host", + hypervisor_hostname="new-host" + ) + self.assert_called('POST', '/servers', + {'server': { + 'flavorRef': '1', + 'imageRef': '1', + 'max_count': 1, + 'min_count': 1, + 'name': 'My server', + 'networks': 'auto', + 'host': 'new-host', + 'hypervisor_hostname': 'new-host' + }} + ) + + def test_create_server_with_host_pre_274_fails(self): + self.cs.api_version = api_versions.APIVersion('2.73') + ex = self.assertRaises(exceptions.UnsupportedAttribute, + self.cs.servers.create, + name="My server", image=1, flavor=1, + nics='auto', host="new-host") + self.assertIn("'host' argument is only allowed since microversion " + "2.74", six.text_type(ex)) + + def test_create_server_with_hypervisor_hostname_pre_274_fails(self): + self.cs.api_version = api_versions.APIVersion('2.73') + ex = self.assertRaises(exceptions.UnsupportedAttribute, + self.cs.servers.create, + name="My server", image=1, flavor=1, + nics='auto', hypervisor_hostname="new-host") + self.assertIn("'hypervisor_hostname' argument is only allowed since " + "microversion 2.74", six.text_type(ex)) + + def test_create_server_with_host_and_hypervisor_hostname_pre_274_fails( + self): + self.cs.api_version = api_versions.APIVersion('2.73') + ex = self.assertRaises(exceptions.UnsupportedAttribute, + self.cs.servers.create, + name="My server", image=1, flavor=1, + nics='auto', host="new-host", + hypervisor_hostname="new-host") + self.assertIn("'host' argument is only allowed since microversion " + "2.74", six.text_type(ex)) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 354475dc2..30ee8bc7a 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -1374,6 +1374,83 @@ def test_boot_with_not_found_when_accessing_addresses_attribute( self.assertIn('Instance %s could not be found.' % FAKE_UUID_1, six.text_type(ex)) + def test_boot_with_host_v274(self): + self.run_command('boot --flavor 1 --image %s ' + '--host new-host --nic auto ' + 'some-server' % FAKE_UUID_1, + api_version='2.74') + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': FAKE_UUID_1, + 'min_count': 1, + 'max_count': 1, + 'networks': 'auto', + 'host': 'new-host', + }}, + ) + + def test_boot_with_hypervisor_hostname_v274(self): + self.run_command('boot --flavor 1 --image %s --nic auto ' + '--hypervisor-hostname new-host ' + 'some-server' % FAKE_UUID_1, + api_version='2.74') + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': FAKE_UUID_1, + 'min_count': 1, + 'max_count': 1, + 'networks': 'auto', + 'hypervisor_hostname': 'new-host', + }}, + ) + + def test_boot_with_host_and_hypervisor_hostname_v274(self): + self.run_command('boot --flavor 1 --image %s ' + '--host new-host --nic auto ' + '--hypervisor-hostname new-host ' + 'some-server' % FAKE_UUID_1, + api_version='2.74') + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': FAKE_UUID_1, + 'min_count': 1, + 'max_count': 1, + 'networks': 'auto', + 'host': 'new-host', + 'hypervisor_hostname': 'new-host', + }}, + ) + + def test_boot_with_host_pre_v274(self): + cmd = ('boot --flavor 1 --image %s --nic auto ' + '--host new-host some-server' + % FAKE_UUID_1) + self.assertRaises(SystemExit, self.run_command, + cmd, api_version='2.73') + + def test_boot_with_hypervisor_hostname_pre_v274(self): + cmd = ('boot --flavor 1 --image %s --nic auto ' + '--hypervisor-hostname new-host some-server' + % FAKE_UUID_1) + self.assertRaises(SystemExit, self.run_command, + cmd, api_version='2.73') + + def test_boot_with_host_and_hypervisor_hostname_pre_v274(self): + cmd = ('boot --flavor 1 --image %s --nic auto ' + '--host new-host --hypervisor-hostname new-host some-server' + % FAKE_UUID_1) + self.assertRaises(SystemExit, self.run_command, + cmd, api_version='2.73') + def test_flavor_list(self): out, _ = self.run_command('flavor-list') self.assert_called_anytime('GET', '/flavors/detail') @@ -4185,6 +4262,7 @@ def test_versions(self): 70, # There are no version-wrapped shell method changes for this. 71, # There are no version-wrapped shell method changes for this. 72, # There are no version-wrapped shell method changes for this. + 74, # There are no version-wrapped shell method changes for this. ]) versions_supported = set(range(0, novaclient.API_MAX_VERSION.ver_minor + 1)) diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index 3f003c934..f9f176d2c 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -694,7 +694,8 @@ def _boot(self, response_key, name, image, flavor, block_device_mapping_v2=None, nics=None, scheduler_hints=None, config_drive=None, admin_pass=None, disk_config=None, access_ip_v4=None, access_ip_v6=None, description=None, - tags=None, trusted_image_certificates=None, **kwargs): + tags=None, trusted_image_certificates=None, + host=None, hypervisor_hostname=None, **kwargs): """ Create (boot) a new server. """ @@ -817,6 +818,12 @@ def _boot(self, response_key, name, image, flavor, body['server']['trusted_image_certificates'] = ( trusted_image_certificates) + if host: + body['server']['host'] = host + + if hypervisor_hostname: + body['server']['hypervisor_hostname'] = hypervisor_hostname + return self._create('/servers', body, response_key, return_raw=return_raw, **kwargs) @@ -1267,7 +1274,9 @@ def create(self, name, image, flavor, meta=None, files=None, nics=None, scheduler_hints=None, config_drive=None, disk_config=None, admin_pass=None, access_ip_v4=None, access_ip_v6=None, - trusted_image_certificates=None, **kwargs): + trusted_image_certificates=None, + host=None, hypervisor_hostname=None, + **kwargs): # TODO(anthony): indicate in doc string if param is an extension # and/or optional """ @@ -1334,6 +1343,10 @@ def create(self, name, image, flavor, meta=None, files=None, server as tags (allowed since microversion 2.52) :param trusted_image_certificates: A list of trusted certificate IDs (allowed since microversion 2.63) + :param host: requested host to create servers + (allowed since microversion 2.74) + :param hypervisor_hostname: requested hypervisor hostname to create + servers (allowed since microversion 2.74) """ if not min_count: min_count = 1 @@ -1388,6 +1401,15 @@ def create(self, name, image, flavor, meta=None, files=None, "Block device volume_type is not supported before " "microversion 2.67") + host_microversion = api_versions.APIVersion("2.74") + if host and self.api_version < host_microversion: + raise exceptions.UnsupportedAttribute("host", "2.74") + hypervisor_hostname_microversion = api_versions.APIVersion("2.74") + if (hypervisor_hostname and + self.api_version < hypervisor_hostname_microversion): + raise exceptions.UnsupportedAttribute( + "hypervisor_hostname", "2.74") + boot_kwargs = dict( meta=meta, files=files, userdata=userdata, reservation_id=reservation_id, min_count=min_count, @@ -1396,7 +1418,9 @@ def create(self, name, image, flavor, meta=None, files=None, scheduler_hints=scheduler_hints, config_drive=config_drive, disk_config=disk_config, admin_pass=admin_pass, access_ip_v4=access_ip_v4, access_ip_v6=access_ip_v6, - trusted_image_certificates=trusted_image_certificates, **kwargs) + trusted_image_certificates=trusted_image_certificates, + host=host, hypervisor_hostname=hypervisor_hostname, + **kwargs) if block_device_mapping: boot_kwargs['block_device_mapping'] = block_device_mapping diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 6653aa2f6..3ab22032a 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -517,6 +517,12 @@ def _boot(cs, args): if 'tags' in args and args.tags: boot_kwargs["tags"] = args.tags.split(',') + if 'host' in args and args.host: + boot_kwargs["host"] = args.host + + if 'hypervisor_hostname' in args and args.hypervisor_hostname: + boot_kwargs["hypervisor_hostname"] = args.hypervisor_hostname + if include_files: boot_kwargs['files'] = files @@ -942,6 +948,21 @@ def _boot(cs, args): 'May be specified multiple times to pass multiple trusted image ' 'certificate IDs.'), start_version="2.63") +@utils.arg( + '--host', + metavar='', + dest='host', + default=None, + help=_('Requested host to create servers. Admin only by default.'), + start_version="2.74") +@utils.arg( + '--hypervisor-hostname', + metavar='', + dest='hypervisor_hostname', + default=None, + help=_('Requested hypervisor hostname to create servers. Admin only by ' + 'default.'), + start_version="2.74") def do_boot(cs, args): """Boot a new server.""" boot_args, boot_kwargs = _boot(cs, args) diff --git a/releasenotes/notes/microversion-v2_74-43b128fe6b84b630.yaml b/releasenotes/notes/microversion-v2_74-43b128fe6b84b630.yaml new file mode 100644 index 000000000..de98f8664 --- /dev/null +++ b/releasenotes/notes/microversion-v2_74-43b128fe6b84b630.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + Support is added for the `2.74 microversion`_ which allows specifying the + ``--host`` and ``--hypervisor-hostname`` options on the ``nova boot`` + command. The ``novaclient.v2.servers.ServerManager.create()`` method now + also supports ``host`` and ``hypervisor_hostname`` parameters. + + .. _2.74 microversion: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id66 From 9fe78e8a60f7ff6797a8a03e1da05ebc3e251fb8 Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Mon, 24 Jun 2019 11:35:36 +0900 Subject: [PATCH 1537/1705] Add a guide to add a new microversion support Add a contributor guide for adding a new microversion support. Change-Id: I5e7699b6afbecf1e22cdc059a36832c144fc8e2f --- doc/source/conf.py | 1 + doc/source/contributor/index.rst | 1 + doc/source/contributor/microversions.rst | 71 ++++++++++++++++++++++++ 3 files changed, 73 insertions(+) create mode 100644 doc/source/contributor/microversions.rst diff --git a/doc/source/conf.py b/doc/source/conf.py index ffd5782d6..b535272d5 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -80,6 +80,7 @@ bug_tag = '' openstack_projects = [ 'keystoneauth', + 'nova', 'os-client-config', 'python-openstackclient', ] diff --git a/doc/source/contributor/index.rst b/doc/source/contributor/index.rst index 91ead0be4..58b9bd7e8 100644 --- a/doc/source/contributor/index.rst +++ b/doc/source/contributor/index.rst @@ -13,4 +13,5 @@ __ https://docs.openstack.org/infra/manual/developers.html#development-workflow .. toctree:: :maxdepth: 2 + microversions testing diff --git a/doc/source/contributor/microversions.rst b/doc/source/contributor/microversions.rst new file mode 100644 index 000000000..7cd41c466 --- /dev/null +++ b/doc/source/contributor/microversions.rst @@ -0,0 +1,71 @@ +===================================== +Adding support for a new microversion +===================================== + +If a new microversion is added on the nova side, +then support must be added on the *python-novaclient* side also. +The following procedure describes how to add support for a new microversion +in *python-novaclient*. + +#. Update ``API_MAX_VERSION`` + + Set ``API_MAX_VERSION`` in ``novaclient/__init__.py`` to the version + you are going to support. + + .. note:: + + Microversion support should be added one by one in order. + For example, microversion 2.74 should be added right after + microversion 2.73. Microversion 2.74 should not be added right + after microversion 2.72 or earlier. + +#. Update CLI and Python API + + Update CLI (``novaclient/v2/shell.py``) and/or Python API + (``novaclient/v2/*.py``) to support the microversion. + +#. Add tests + + Add unit tests for the change. Add unit tests for the previous microversion + to check raising an error or an exception when new arguments or parameters + are specified. Add functional tests if necessary. + + Add the microversion in the ``exclusions`` in the ``test_versions`` + method of the ``novaclient.tests.unit.v2.test_shell.ShellTest`` class + if there are no versioned wrapped shell method changes + for the microversion. + + For example (microversion 2.72 example):: + + exclusions = set([ + (snipped...) + 72, # There are no version-wrapped shell method changes for this. + ]) + +#. Update the CLI reference + + Update the CLI reference (``doc/source/cli/nova.rst``) + if the CLI commands and/or arguments are modified. + +#. Add a release note + + Add a release note for the change. The release note should include a link + to the description for the microversion in the + :nova-doc:`Compute API Microversion History + `. + +#. Commit message + + The description of the blueprint and dependency on the patch in nova side + should be added in the commit message. For example:: + + Implements: blueprint remove-force-flag-from-live-migrate-and-evacuate + Depends-On: https://review.opendev.org/#/c/634600/ + +See the following examples: + +- `Microversion 2.71 - show server group `_ +- `API microversion 2.69: Handles Down Cells `_ +- `Microversion 2.68: Remove 'forced' live migrations, evacuations `_ +- `Add support changes-before for microversion 2.66 `_ +- `Microversion 2.64 - Use new format policy in server group `_ From 3ac90a52737039fa027502376575d5ce792a018d Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Mon, 8 Jul 2019 10:34:37 +0900 Subject: [PATCH 1538/1705] Deprecate cells v1 and extension commands and APIs The API extension has been deprecated since 12.0.0 Liberty release (*1) and the cells v1 has been deprecated since 16.0.0 Pike release (*2) in the nova side. The API extension has already been removed (merged into main controllers and schema) since 19.0.0 Stein release (*3) and the cells v1 APIs has already been removed since Iddb519008515f591cf1d884872a5887afbe766f2. In the python-novaclient side, deprecate commands and API bindings related to the API extension and the cells v1 at first. Then the CLIs and API bindings will be removed in the first major release after Nova 20.0.0 Train is released. *1: I084444b11dceda7cf8f88c157aa67d36490fce49 *2: I1a173f7ce0715e684850e030c358e8175fa8724c *3: https://review.opendev.org/#/q/topic:bp/api-extensions-merge-stein Change-Id: I8dc4df95ac7f6974c5280e4107e449d04cd1402e Closes-Bug: #1835699 --- .../v2/legacy/test_readonly_nova.py | 6 ++++- novaclient/tests/unit/v2/test_cells.py | 20 +++++++++++++--- .../tests/unit/v2/test_list_extensions.py | 13 +++++++++- novaclient/tests/unit/v2/test_shell.py | 18 +++++++++++--- novaclient/v2/cells.py | 16 +++++++++++-- novaclient/v2/list_extensions.py | 14 +++++++++++ novaclient/v2/shell.py | 24 ++++++++++++++++--- ...te-cellsv1-extension-16482759993d112f.yaml | 11 +++++++++ 8 files changed, 109 insertions(+), 13 deletions(-) create mode 100644 releasenotes/notes/deprecate-cellsv1-extension-16482759993d112f.yaml diff --git a/novaclient/tests/functional/v2/legacy/test_readonly_nova.py b/novaclient/tests/functional/v2/legacy/test_readonly_nova.py index a31982e5d..7f81e41ae 100644 --- a/novaclient/tests/functional/v2/legacy/test_readonly_nova.py +++ b/novaclient/tests/functional/v2/legacy/test_readonly_nova.py @@ -86,7 +86,11 @@ def test_admin_help(self): self.nova('help') def test_admin_list_extensions(self): - self.nova('list-extensions') + output = self.nova('list-extensions', merge_stderr=True) + self.assertIn( + 'The API extension interface has been deprecated. This command ' + 'will be removed in the first major release after ' + 'the Nova server 20.0.0 Train release.', output) def test_agent_list(self): self.nova('agent-list') diff --git a/novaclient/tests/unit/v2/test_cells.py b/novaclient/tests/unit/v2/test_cells.py index 5a47e1c6b..ff898f532 100644 --- a/novaclient/tests/unit/v2/test_cells.py +++ b/novaclient/tests/unit/v2/test_cells.py @@ -13,29 +13,43 @@ # License for the specific language governing permissions and limitations # under the License. +import mock + from novaclient import api_versions from novaclient.tests.unit import utils from novaclient.tests.unit.v2 import fakes +CELL_V1_DEPRECATION_WARNING = ( + 'The cells v1 interface has been deprecated in Nova since 16.0.0 Pike ' + 'Release. This API binding will be removed in the first major release ' + 'after the Nova server 20.0.0 Train release.') + +@mock.patch('warnings.warn') class CellsExtensionTests(utils.TestCase): def setUp(self): super(CellsExtensionTests, self).setUp() self.cs = fakes.FakeClient(api_versions.APIVersion("2.1")) - def test_get_cells(self): + def test_get_cells(self, mock_warn): cell_name = 'child_cell' cell = self.cs.cells.get(cell_name) self.assert_request_id(cell, fakes.FAKE_REQUEST_ID_LIST) self.cs.assert_called('GET', '/os-cells/%s' % cell_name) + mock_warn.assert_called_once_with(CELL_V1_DEPRECATION_WARNING, + DeprecationWarning) - def test_get_capacities_for_a_given_cell(self): + def test_get_capacities_for_a_given_cell(self, mock_warn): cell_name = 'child_cell' ca = self.cs.cells.capacities(cell_name) self.assert_request_id(ca, fakes.FAKE_REQUEST_ID_LIST) self.cs.assert_called('GET', '/os-cells/%s/capacities' % cell_name) + mock_warn.assert_called_once_with(CELL_V1_DEPRECATION_WARNING, + DeprecationWarning) - def test_get_capacities_for_all_cells(self): + def test_get_capacities_for_all_cells(self, mock_warn): ca = self.cs.cells.capacities() self.assert_request_id(ca, fakes.FAKE_REQUEST_ID_LIST) self.cs.assert_called('GET', '/os-cells/capacities') + mock_warn.assert_called_once_with(CELL_V1_DEPRECATION_WARNING, + DeprecationWarning) diff --git a/novaclient/tests/unit/v2/test_list_extensions.py b/novaclient/tests/unit/v2/test_list_extensions.py index 60783b31a..de299cd01 100644 --- a/novaclient/tests/unit/v2/test_list_extensions.py +++ b/novaclient/tests/unit/v2/test_list_extensions.py @@ -11,6 +11,8 @@ # License for the specific language governing permissions and limitations # under the License. +import mock + from novaclient import api_versions from novaclient.tests.unit import utils from novaclient.tests.unit.v2 import fakes @@ -21,10 +23,19 @@ def setUp(self): super(ListExtensionsTests, self).setUp() self.cs = fakes.FakeClient(api_versions.APIVersion("2.1")) - def test_list_extensions(self): + @mock.patch('warnings.warn') + def test_list_extensions(self, mock_warn): all_exts = self.cs.list_extensions.show_all() self.assert_request_id(all_exts, fakes.FAKE_REQUEST_ID_LIST) self.cs.assert_called('GET', '/extensions') self.assertGreater(len(all_exts), 0) + warning_message = ( + 'The API extension interface has been deprecated since 12.0.0 ' + 'Liberty Release. This API binding will be removed in the first ' + 'major release after the Nova server 20.0.0 Train release.') + mock_warn.assert_called_once_with(warning_message, DeprecationWarning) for r in all_exts: + mock_warn.reset_mock() self.assertGreater(len(r.summary), 0) + mock_warn.assert_called_once_with(warning_message, + DeprecationWarning) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 354475dc2..926e21610 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -3811,16 +3811,28 @@ def test_instance_usage_audit_log_with_before(self): '/2016-12-10%2013%3A59%3A59.999999') def test_cell_show(self): - self.run_command('cell-show child_cell') + _, err = self.run_command('cell-show child_cell') self.assert_called('GET', '/os-cells/child_cell') + self.assertIn( + 'The cells v1 interface has been deprecated. This command will be ' + 'removed in the first major release after the Nova server 20.0.0 ' + 'Train release.', err) def test_cell_capacities_with_cell_name(self): - self.run_command('cell-capacities --cell child_cell') + _, err = self.run_command('cell-capacities --cell child_cell') self.assert_called('GET', '/os-cells/child_cell/capacities') + self.assertIn( + 'The cells v1 interface has been deprecated. This command will be ' + 'removed in the first major release after the Nova server 20.0.0 ' + 'Train release.', err) def test_cell_capacities_without_cell_name(self): - self.run_command('cell-capacities') + _, err = self.run_command('cell-capacities') self.assert_called('GET', '/os-cells/capacities') + self.assertIn( + 'The cells v1 interface has been deprecated. This command will be ' + 'removed in the first major release after the Nova server 20.0.0 ' + 'Train release.', err) def test_migration_list(self): self.run_command('migration-list') diff --git a/novaclient/v2/cells.py b/novaclient/v2/cells.py index e6a166671..44cadddbf 100644 --- a/novaclient/v2/cells.py +++ b/novaclient/v2/cells.py @@ -13,32 +13,44 @@ # License for the specific language governing permissions and limitations # under the License. +import warnings + from novaclient import base +from novaclient.i18n import _ + +CELL_V1_DEPRECATION_WARNING = _( + 'The cells v1 interface has been deprecated in Nova since 16.0.0 Pike ' + 'Release. This API binding will be removed in the first major release ' + 'after the Nova server 20.0.0 Train release.') class Cell(base.Resource): + """DEPRECATED""" def __repr__(self): return "" % self.name class CellsManager(base.Manager): + """DEPRECATED""" resource_class = Cell def get(self, cell_name): """ - Get a cell. + DEPRECATED Get a cell. :param cell_name: Name of the :class:`Cell` to get. :rtype: :class:`Cell` """ + warnings.warn(CELL_V1_DEPRECATION_WARNING, DeprecationWarning) return self._get("/os-cells/%s" % cell_name, "cell") def capacities(self, cell_name=None): """ - Get capacities for a cell. + DEPRECATED Get capacities for a cell. :param cell_name: Name of the :class:`Cell` to get capacities for. :rtype: :class:`Cell` """ + warnings.warn(CELL_V1_DEPRECATION_WARNING, DeprecationWarning) path = ["%s/capacities" % cell_name, "capacities"][cell_name is None] return self._get("/os-cells/%s" % path, "cell") diff --git a/novaclient/v2/list_extensions.py b/novaclient/v2/list_extensions.py index faeead601..e7043e31b 100644 --- a/novaclient/v2/list_extensions.py +++ b/novaclient/v2/list_extensions.py @@ -13,12 +13,23 @@ # License for the specific language governing permissions and limitations # under the License. +import warnings + from novaclient import base +from novaclient.i18n import _ + +EXTENSION_DEPRECATION_WARNING = _( + 'The API extension interface has been deprecated since 12.0.0 Liberty ' + 'Release. This API binding will be removed in the first major release ' + 'after the Nova server 20.0.0 Train release.') class ListExtResource(base.Resource): + """DEPRECATED""" @property def summary(self): + """DEPRECATED""" + warnings.warn(EXTENSION_DEPRECATION_WARNING, DeprecationWarning) descr = self.description.strip() if not descr: return '??' @@ -30,7 +41,10 @@ def summary(self): class ListExtManager(base.Manager): + """DEPRECATED""" resource_class = ListExtResource def show_all(self): + """DEPRECATED""" + warnings.warn(EXTENSION_DEPRECATION_WARNING, DeprecationWarning) return self._list("/extensions", 'extensions') diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 6653aa2f6..dbee93e54 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -48,6 +48,21 @@ logger = logging.getLogger(__name__) +CELL_V1_DEPRECATION_WARNING = _( + 'The cells v1 interface has been deprecated. This command will be removed ' + 'in the first major release after the Nova server 20.0.0 Train release.') + +EXTENSION_DEPRECATION_WARNING = _( + 'The API extension interface has been deprecated. This command will be ' + 'removed in the first major release after the Nova server 20.0.0 Train ' + 'release.') + + +# NOTE(takashin): Remove this along with the deprecated commands in the first +# major python-novaclient release AFTER the nova server 20.0.0 Train release. +def _emit_deprecation_warning(message): + print(message, file=sys.stderr) + def emit_duplicated_image_with_warning(img, image_with): img_uuid_list = [str(image.id) for image in img] @@ -4855,7 +4870,8 @@ def do_server_tag_delete_all(cs, args): metavar='', help=_('Name of the cell.')) def do_cell_show(cs, args): - """Show details of a given cell.""" + """DEPRECATED Show details of a given cell.""" + _emit_deprecation_warning(CELL_V1_DEPRECATION_WARNING) cell = cs.cells.get(args.cell) utils.print_dict(cell.to_dict()) @@ -4866,7 +4882,8 @@ def do_cell_show(cs, args): help=_("Name of the cell to get the capacities."), default=None) def do_cell_capacities(cs, args): - """Get cell capacities for all cells or a given cell.""" + """DEPRECATED Get cell capacities for all cells or a given cell.""" + _emit_deprecation_warning(CELL_V1_DEPRECATION_WARNING) cell = cs.cells.capacities(args.cell) print(_("Ram Available: %s MiB") % cell.capacities['ram_free']['total_mb']) utils.print_dict(cell.capacities['ram_free']['units_by_mb'], @@ -5300,8 +5317,9 @@ def do_instance_action_list(cs, args): def do_list_extensions(cs, _args): """ - List all the os-api extensions that are available. + DEPRECATED List all the os-api extensions that are available. """ + _emit_deprecation_warning(EXTENSION_DEPRECATION_WARNING) extensions = cs.list_extensions.show_all() fields = ["Name", "Summary", "Alias", "Updated"] utils.print_list(extensions, fields) diff --git a/releasenotes/notes/deprecate-cellsv1-extension-16482759993d112f.yaml b/releasenotes/notes/deprecate-cellsv1-extension-16482759993d112f.yaml new file mode 100644 index 000000000..a26ad849e --- /dev/null +++ b/releasenotes/notes/deprecate-cellsv1-extension-16482759993d112f.yaml @@ -0,0 +1,11 @@ +--- +deprecations: + - | + The following CLIs and their backing API bindings are deprecated. + + - ``nova list-extensions`` + - ``nova cell-capacities`` + - ``nova cell-show`` + + The CLIs and API bindings will be removed in the first major release after + Nova 20.0.0 Train is released. From 517ff099e8c1daba0f9e3bea22ea5fc1fac73dda Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Tue, 16 Jul 2019 18:09:30 +0900 Subject: [PATCH 1539/1705] doc: Clarify versioned wrapped method The 'versioned wrapped shell method' is a bit unclear. So fix it and add more description for it. Change-Id: Ie50453b73adf1df5a77a582cc40612c5213c04d5 --- doc/source/contributor/microversions.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/contributor/microversions.rst b/doc/source/contributor/microversions.rst index 7cd41c466..a3194a900 100644 --- a/doc/source/contributor/microversions.rst +++ b/doc/source/contributor/microversions.rst @@ -32,8 +32,8 @@ in *python-novaclient*. Add the microversion in the ``exclusions`` in the ``test_versions`` method of the ``novaclient.tests.unit.v2.test_shell.ShellTest`` class - if there are no versioned wrapped shell method changes - for the microversion. + if there are no versioned wrapped method changes for the microversion. + The versioned wrapped methods have ``@api_versions.wraps`` decorators. For example (microversion 2.72 example):: From 8fc24c7d5f9d20eadb75b411ee8f8cfbd018f979 Mon Sep 17 00:00:00 2001 From: zhangyangyang Date: Sat, 20 Jul 2019 15:17:53 +0800 Subject: [PATCH 1540/1705] Bump the openstackdocstheme extension to 1.20 Some options are now automatically configured by the version 1.20: - project - html_last_updated_fmt - latex_engine - latex_elements - version - release. Change-Id: Icd8404026ff5a73129a22b6c89b5cfd6c57432fb --- doc/source/conf.py | 1 - releasenotes/source/conf.py | 18 ------------------ 2 files changed, 19 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index b535272d5..f90c50ad3 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -46,7 +46,6 @@ repository_name = 'openstack/python-novaclient' bug_project = 'python-novaclient' bug_tag = 'doc' -project = 'python-novaclient' copyright = 'OpenStack Contributors' # List of directories, relative to source directory, that shouldn't be searched diff --git a/releasenotes/source/conf.py b/releasenotes/source/conf.py index 07b6d77f6..799455bf5 100644 --- a/releasenotes/source/conf.py +++ b/releasenotes/source/conf.py @@ -43,15 +43,8 @@ master_doc = 'index' # General information about the project. -project = u'Nova Client Release Notes' copyright = u'2015, Nova developers' -# Release notes are version independent. -# The short X.Y version. -version = '' -# The full version, including alpha/beta/rc tags. -release = '' - # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None @@ -178,17 +171,6 @@ # -- Options for LaTeX output --------------------------------------------- -latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', -} - # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). From acef73c9bddbf958cb97a619172662ede3a33de1 Mon Sep 17 00:00:00 2001 From: Andreas Jaeger Date: Mon, 22 Jul 2019 20:52:18 +0200 Subject: [PATCH 1541/1705] Update api-ref location The api documentation is now published on docs.openstack.org instead of developer.openstack.org. Update all links that are changed to the new location. Note that redirects will be set up as well but let's point now to the new location. For details, see: http://lists.openstack.org/pipermail/openstack-discuss/2019-July/007828.html Change-Id: I53120e2ff3ae337a55c4cd0904bc9ad1dd54d082 --- doc/source/index.rst | 2 +- novaclient/v2/servers.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/index.rst b/doc/source/index.rst index aeb2f268b..243c94979 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -16,7 +16,7 @@ such as TryStack, HP, or Rackspace, in order to use the nova client. to get an idea of the concepts. By understanding the concepts this library should make more sense. - __ https://developer.openstack.org/api-guide/compute/index.html + __ https://docs.openstack.org/api-guide/compute/index.html .. toctree:: :maxdepth: 2 diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index f9f176d2c..43ed34f22 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -846,7 +846,7 @@ def list(self, detailed=True, search_opts=None, marker=None, limit=None, match the search_opts (optional). The search opts format is a dictionary of key / value pairs that will be appended to the query string. For a complete list of keys see: - https://developer.openstack.org/api-ref/compute/#list-servers + https://docs.openstack.org/api-ref/compute/#list-servers :param marker: Begin returning servers that appear later in the server list than that represented by this server id (optional). :param limit: Maximum number of servers to return (optional). From e281368c9679b385ecfb05737e614a05a9bab291 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Tue, 6 Aug 2019 18:44:53 -0400 Subject: [PATCH 1542/1705] docs: clarify nova migration-list --host option The GET /os-migrations API takes both a host and source_compute filter parameter. The former filters on either the source or destination compute, so this change clarifies that in the command help for the --host option. Change-Id: I078add63896c7455be7e3672b7172debb962a5e2 --- doc/source/cli/nova.rst | 2 +- novaclient/v2/shell.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/source/cli/nova.rst b/doc/source/cli/nova.rst index f93c31243..1a0081eb2 100644 --- a/doc/source/cli/nova.rst +++ b/doc/source/cli/nova.rst @@ -2569,7 +2569,7 @@ Print a list of migrations. Fetch migrations for the given instance. ``--host `` - Fetch migrations for the given host. + Fetch migrations for the given source or destination host. ``--status `` Fetch migrations for the given status. diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 649097bed..7b9c02061 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -5416,7 +5416,7 @@ def migration_type(migration): '--host', dest='host', metavar='', - help=_('Fetch migrations for the given host.')) + help=_('Fetch migrations for the given source or destination host.')) @utils.arg( '--status', dest='status', @@ -5439,7 +5439,7 @@ def do_migration_list(cs, args): '--host', dest='host', metavar='', - help=_('Fetch migrations for the given host.')) + help=_('Fetch migrations for the given source or destination host.')) @utils.arg( '--status', dest='status', @@ -5498,7 +5498,7 @@ def do_migration_list(cs, args): '--host', dest='host', metavar='', - help=_('Fetch migrations for the given host.')) + help=_('Fetch migrations for the given source or destination host.')) @utils.arg( '--status', dest='status', From 0e7c99c8ead42326d8660103b0c78df70b775fe4 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Wed, 7 Aug 2019 09:11:14 -0400 Subject: [PATCH 1543/1705] Add --migration-type and --source-compute to migration-list The GET /os-migrations API take a migration_type and source_compute filter parameter on the request since the v2.0 API. This adds support for specifying those parameters in the "nova migration-list" CLI and related MigrationManager.list() python API binding methods. A functional test is added which will cover the new options on all three of the decorated do_migration_list shell methods with lower bounds on 2.1, 2.59 and 2.66. Since the only type of migration we can safely generate in a single-node CI job is a resize the test does a resize. As such, _pick_alternate_flavor is moved into the base test class for re-use. Implements blueprint more-migration-list-filters Change-Id: I4be9a06df3e0d40d3990d067ce112247a81b45b4 --- doc/source/cli/nova.rst | 30 +++++- novaclient/tests/functional/base.py | 21 ++++ .../tests/functional/v2/test_migrations.py | 99 +++++++++++++++++++ novaclient/tests/functional/v2/test_resize.py | 21 ---- novaclient/v2/migrations.py | 36 +++++-- novaclient/v2/shell.py | 45 ++++++++- ...gration-list-filters-6c801896c7ee5cdc.yaml | 9 ++ 7 files changed, 227 insertions(+), 34 deletions(-) create mode 100644 novaclient/tests/functional/v2/test_migrations.py create mode 100644 releasenotes/notes/bp-more-migration-list-filters-6c801896c7ee5cdc.yaml diff --git a/doc/source/cli/nova.rst b/doc/source/cli/nova.rst index 1a0081eb2..c0d80eda8 100644 --- a/doc/source/cli/nova.rst +++ b/doc/source/cli/nova.rst @@ -2556,13 +2556,26 @@ nova migration-list .. code-block:: console - usage: nova migration-list [--instance-uuid ] [--host ] - [--status ] [--marker ] - [--limit ] [--changes-since ] + usage: nova migration-list [--instance-uuid ] + [--host ] + [--status ] + [--migration-type ] + [--source-compute ] + [--marker ] + [--limit ] + [--changes-since ] [--changes-before ] Print a list of migrations. +**Examples** + +To see the list of evacuation operations *from* a compute service host: + +.. code-block:: console + + nova migration-list --migration-type evacuation --source-compute host.foo.bar + **Optional arguments:** ``--instance-uuid `` @@ -2574,6 +2587,17 @@ Print a list of migrations. ``--status `` Fetch migrations for the given status. +``--migration-type `` + Filter migrations by type. Valid values are: + + * evacuation + * live-migration + * migration + * resize + +``--source-compute `` + Filter migrations by source compute host name. + ``--marker `` The last migration of the previous page; displays list of migrations after "marker". Note that the marker is the migration UUID. diff --git a/novaclient/tests/functional/base.py b/novaclient/tests/functional/base.py index ce50db94a..f5c758be4 100644 --- a/novaclient/tests/functional/base.py +++ b/novaclient/tests/functional/base.py @@ -522,6 +522,27 @@ def _get_absolute_limits(self): return {limit.name: limit.value for limit in self.client.limits.get(reserved=True).absolute} + def _pick_alternate_flavor(self): + """Given the flavor picked in the base class setup, this finds the + opposite flavor to use for a resize test. For example, if m1.nano is + the flavor, then use m1.micro, but those are only available if Tempest + is configured. If m1.tiny, then use m1.small. + """ + flavor_name = self.flavor.name + if flavor_name == 'm1.nano': + # This is an upsize test. + return 'm1.micro' + if flavor_name == 'm1.micro': + # This is a downsize test. + return 'm1.nano' + if flavor_name == 'm1.tiny': + # This is an upsize test. + return 'm1.small' + if flavor_name == 'm1.small': + # This is a downsize test. + return 'm1.tiny' + self.fail('Unable to find alternate for flavor: %s' % flavor_name) + class TenantTestBase(ClientTestBase): """Base test class for additional tenant and user creation which diff --git a/novaclient/tests/functional/v2/test_migrations.py b/novaclient/tests/functional/v2/test_migrations.py new file mode 100644 index 000000000..855d8ce5d --- /dev/null +++ b/novaclient/tests/functional/v2/test_migrations.py @@ -0,0 +1,99 @@ +# 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 oslo_utils import uuidutils + +from novaclient.tests.functional import base + + +class TestMigrationList(base.ClientTestBase): + """Tests the "nova migration-list" command.""" + + def _filter_migrations( + self, version, migration_type, source_compute): + """ + Filters migrations by --migration-type and --source-compute. + + :param version: The --os-compute-api-version to use. + :param migration_type: The type of migrations to filter. + :param source_compute: The source compute service hostname to filter. + :return: output of the nova migration-list command with filters applied + """ + return self.nova('migration-list', + flags='--os-compute-api-version %s' % version, + params='--migration-type %s --source-compute %s' % ( + migration_type, source_compute)) + + def test_migration_list(self): + """Tests creating a server, resizing it and then listing and filtering + migrations using various microversion milestones. + """ + server_id = self._create_server(flavor=self.flavor.id).id + # Find the source compute by getting OS-EXT-SRV-ATTR:host from the + # nova show output. + server = self.nova('show', params='%s' % server_id) + source_compute = self._get_value_from_the_table( + server, 'OS-EXT-SRV-ATTR:host') + # now resize up + alternate_flavor = self._pick_alternate_flavor() + self.nova('resize', + params='%s %s --poll' % (server_id, alternate_flavor)) + # now confirm the resize + self.nova('resize-confirm', params='%s' % server_id) + # wait for the server to be active and then check the migration list + self._wait_for_state_change(server_id, 'active') + # First, list migrations with v2.1 and our server id should be in the + # output. There should only be the one migration. + migrations = self.nova('migration-list', + flags='--os-compute-api-version 2.1') + instance_uuid = self._get_column_value_from_single_row_table( + migrations, 'Instance UUID') + self.assertEqual(server_id, instance_uuid) + # A successfully confirmed resize should have the migration status + # of "confirmed". + migration_status = self._get_column_value_from_single_row_table( + migrations, 'Status') + self.assertEqual('confirmed', migration_status) + # Now listing migrations with 2.23 should give us the Type column which + # should have a value of "resize". + migrations = self.nova('migration-list', + flags='--os-compute-api-version 2.23') + migration_type = self._get_column_value_from_single_row_table( + migrations, 'Type') + self.assertEqual('resize', migration_type) + # Filter migrations with v2.1. + migrations = self._filter_migrations('2.1', 'resize', source_compute) + # Make sure we got something back. + src_compute = self._get_column_value_from_single_row_table( + migrations, 'Source Compute') + self.assertEqual(source_compute, src_compute) + # Filter migrations with v2.59 and make sure there is a migration UUID + # value in the output. + migrations = self._filter_migrations('2.59', 'resize', source_compute) + # _get_column_value_from_single_row_table will raise ValueError if a + # value is not found for the given column. We don't actually care what + # the migration UUID value is just that the filter works and the UUID + # is shown. + self._get_column_value_from_single_row_table(migrations, 'UUID') + # Filter migrations with v2.66, same as 2.59. + migrations = self._filter_migrations('2.66', 'resize', source_compute) + self._get_column_value_from_single_row_table(migrations, 'UUID') + # Now do a negative test to show that filtering on a migration type + # that we don't have a migration for will not return anything. + migrations = self._filter_migrations( + '2.1', 'evacuation', source_compute) + self.assertNotIn(server_id, migrations) + # Similarly, make sure we don't get anything back when filtering on + # a --source-compute that doesn't exist. + migrations = self._filter_migrations( + '2.66', 'resize', uuidutils.generate_uuid()) + self.assertNotIn(server_id, migrations) diff --git a/novaclient/tests/functional/v2/test_resize.py b/novaclient/tests/functional/v2/test_resize.py index 0354610fd..075a2f9a7 100644 --- a/novaclient/tests/functional/v2/test_resize.py +++ b/novaclient/tests/functional/v2/test_resize.py @@ -20,27 +20,6 @@ class TestServersResize(base.ClientTestBase): COMPUTE_API_VERSION = '2.1' - def _pick_alternate_flavor(self): - """Given the flavor picked in the base class setup, this finds the - opposite flavor to use for a resize test. For example, if m1.nano is - the flavor, then use m1.micro, but those are only available if Tempest - is configured. If m1.tiny, then use m1.small. - """ - flavor_name = self.flavor.name - if flavor_name == 'm1.nano': - # This is an upsize test. - return 'm1.micro' - if flavor_name == 'm1.micro': - # This is a downsize test. - return 'm1.nano' - if flavor_name == 'm1.tiny': - # This is an upsize test. - return 'm1.small' - if flavor_name == 'm1.small': - # This is a downsize test. - return 'm1.tiny' - self.fail('Unable to find alternate for flavor: %s' % flavor_name) - def _compare_quota_usage(self, old_usage, new_usage, expect_diff=True): """Compares the quota usage in the provided AbsoluteLimits.""" # For a resize, instance usage shouldn't change. diff --git a/novaclient/v2/migrations.py b/novaclient/v2/migrations.py index ab62f2c5d..0e94b4562 100644 --- a/novaclient/v2/migrations.py +++ b/novaclient/v2/migrations.py @@ -28,7 +28,8 @@ class MigrationManager(base.ManagerWithFind): def _list_base(self, host=None, status=None, instance_uuid=None, marker=None, limit=None, changes_since=None, - changes_before=None): + changes_before=None, migration_type=None, + source_compute=None): opts = {} if host: opts['host'] = host @@ -44,23 +45,34 @@ def _list_base(self, host=None, status=None, instance_uuid=None, opts['changes-since'] = changes_since if changes_before: opts['changes-before'] = changes_before + if migration_type: + opts['migration_type'] = migration_type + if source_compute: + opts['source_compute'] = source_compute return self._list("/os-migrations", "migrations", filters=opts) @api_versions.wraps("2.0", "2.58") - def list(self, host=None, status=None, instance_uuid=None): + def list(self, host=None, status=None, instance_uuid=None, + migration_type=None, source_compute=None): """ Get a list of migrations. :param host: filter migrations by host name (optional). :param status: filter migrations by status (optional). :param instance_uuid: filter migrations by instance uuid (optional). + :param migration_type: Filter migrations by type. Valid values are: + evacuation, live-migration, migration, resize + :param source_compute: Filter migrations by source compute host name. """ return self._list_base(host=host, status=status, - instance_uuid=instance_uuid) + instance_uuid=instance_uuid, + migration_type=migration_type, + source_compute=source_compute) @api_versions.wraps("2.59", "2.65") def list(self, host=None, status=None, instance_uuid=None, - marker=None, limit=None, changes_since=None): + marker=None, limit=None, changes_since=None, + migration_type=None, source_compute=None): """ Get a list of migrations. :param host: filter migrations by host name (optional). @@ -76,16 +88,21 @@ def list(self, host=None, status=None, instance_uuid=None, :param changes_since: only return migrations changed later or equal to a certain point of time. The provided time should be an ISO 8061 formatted time. e.g. 2016-03-04T06:27:59Z . (optional). + :param migration_type: Filter migrations by type. Valid values are: + evacuation, live-migration, migration, resize + :param source_compute: Filter migrations by source compute host name. """ return self._list_base(host=host, status=status, instance_uuid=instance_uuid, marker=marker, limit=limit, - changes_since=changes_since) + changes_since=changes_since, + migration_type=migration_type, + source_compute=source_compute) @api_versions.wraps("2.66") def list(self, host=None, status=None, instance_uuid=None, marker=None, limit=None, changes_since=None, - changes_before=None): + changes_before=None, migration_type=None, source_compute=None): """ Get a list of migrations. :param host: filter migrations by host name (optional). @@ -104,9 +121,14 @@ def list(self, host=None, status=None, instance_uuid=None, :param changes_before: Only return migrations changed earlier or equal to a certain point of time. The provided time should be an ISO 8061 formatted time. e.g. 2016-03-05T06:27:59Z . (optional). + :param migration_type: Filter migrations by type. Valid values are: + evacuation, live-migration, migration, resize + :param source_compute: Filter migrations by source compute host name. """ return self._list_base(host=host, status=status, instance_uuid=instance_uuid, marker=marker, limit=limit, changes_since=changes_since, - changes_before=changes_before) + changes_before=changes_before, + migration_type=migration_type, + source_compute=source_compute) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 7b9c02061..34dea57f4 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -5422,10 +5422,23 @@ def migration_type(migration): dest='status', metavar='', help=_('Fetch migrations for the given status.')) +@utils.arg( + '--migration-type', + dest='migration_type', + metavar='', + help=_('Filter migrations by type. Valid values are: evacuation, ' + 'live-migration, migration, resize')) +@utils.arg( + '--source-compute', + dest='source_compute', + metavar='', + help=_('Filter migrations by source compute host name.')) def do_migration_list(cs, args): """Print a list of migrations.""" migrations = cs.migrations.list(args.host, args.status, - instance_uuid=args.instance_uuid) + instance_uuid=args.instance_uuid, + migration_type=args.migration_type, + source_compute=args.source_compute) _print_migrations(cs, migrations) @@ -5445,6 +5458,17 @@ def do_migration_list(cs, args): dest='status', metavar='', help=_('Fetch migrations for the given status.')) +@utils.arg( + '--migration-type', + dest='migration_type', + metavar='', + help=_('Filter migrations by type. Valid values are: evacuation, ' + 'live-migration, migration, resize')) +@utils.arg( + '--source-compute', + dest='source_compute', + metavar='', + help=_('Filter migrations by source compute host name.')) @utils.arg( '--marker', dest='marker', @@ -5483,7 +5507,9 @@ def do_migration_list(cs, args): migrations = cs.migrations.list(args.host, args.status, instance_uuid=args.instance_uuid, marker=args.marker, limit=args.limit, - changes_since=args.changes_since) + changes_since=args.changes_since, + migration_type=args.migration_type, + source_compute=args.source_compute) # TODO(yikun): Output a "Marker" column if there is a next link? _print_migrations(cs, migrations) @@ -5504,6 +5530,17 @@ def do_migration_list(cs, args): dest='status', metavar='', help=_('Fetch migrations for the given status.')) +@utils.arg( + '--migration-type', + dest='migration_type', + metavar='', + help=_('Filter migrations by type. Valid values are: evacuation, ' + 'live-migration, migration, resize')) +@utils.arg( + '--source-compute', + dest='source_compute', + metavar='', + help=_('Filter migrations by source compute host name.')) @utils.arg( '--marker', dest='marker', @@ -5559,7 +5596,9 @@ def do_migration_list(cs, args): instance_uuid=args.instance_uuid, marker=args.marker, limit=args.limit, changes_since=args.changes_since, - changes_before=args.changes_before) + changes_before=args.changes_before, + migration_type=args.migration_type, + source_compute=args.source_compute) _print_migrations(cs, migrations) diff --git a/releasenotes/notes/bp-more-migration-list-filters-6c801896c7ee5cdc.yaml b/releasenotes/notes/bp-more-migration-list-filters-6c801896c7ee5cdc.yaml new file mode 100644 index 000000000..14e028d38 --- /dev/null +++ b/releasenotes/notes/bp-more-migration-list-filters-6c801896c7ee5cdc.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + The ``--migration-type`` and ``--source-compute`` options are added to the + ``nova migration-list`` CLI and related kwargs are added to the + ``novaclient.v2.migrations.MigrationManager.list`` method. These can be + used to filter the list of migrations by type (evacuation, live-migration, + migration, resize) and the name of the source compute service host involved + in the migration. From 0e873a2d5a69d559299da4c403a5c17cf8a70462 Mon Sep 17 00:00:00 2001 From: Ghanshyam Mann Date: Tue, 13 Aug 2019 20:30:24 +0000 Subject: [PATCH 1544/1705] Microversion 2.75 - Multiple API cleanup changes Add support microversion 2.75 which implement multiple API cleanup changes. 1. Making server representation always consistent among all APIs returning the complete server representation. - Test cases added. 2. Change the default return value of ``swap`` field from the empty string to 0 (integer) in flavor APIs. - Test cases added. Nova side path: https://review.opendev.org/#/c/666889/ Change-Id: Iec2cfc629dffd53178ef88a31fcd16a3f32e2e27 Partial-Implements: blueprint api-consistency-cleanup --- novaclient/__init__.py | 2 +- novaclient/tests/functional/v2/test_flavor.py | 72 +++++++++++++++++++ .../tests/functional/v2/test_servers.py | 41 +++++++++++ novaclient/tests/unit/v2/test_shell.py | 1 + .../microversion-v2_75-ea7fa3ba1396edea.yaml | 18 +++++ 5 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 novaclient/tests/functional/v2/test_flavor.py create mode 100644 releasenotes/notes/microversion-v2_75-ea7fa3ba1396edea.yaml diff --git a/novaclient/__init__.py b/novaclient/__init__.py index fb9a485eb..fc031f18f 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.74") +API_MAX_VERSION = api_versions.APIVersion("2.75") diff --git a/novaclient/tests/functional/v2/test_flavor.py b/novaclient/tests/functional/v2/test_flavor.py new file mode 100644 index 000000000..1458f0b54 --- /dev/null +++ b/novaclient/tests/functional/v2/test_flavor.py @@ -0,0 +1,72 @@ +# 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 novaclient.tests.functional import base + + +class TestFlavorNovaClientV274(base.TenantTestBase): + """Functional tests for flavors""" + + COMPUTE_API_VERSION = "2.74" + # NOTE(gmann): Before microversion 2.75, default value of 'swap' field is + # returned as empty string. + SWAP_DEFAULT = "" + + def _create_flavor(self, swap=None): + flv_name = self.name_generate() + cmd = 'flavor-create %s auto 512 1 1' + if swap: + cmd = cmd + (' --swap %s' % swap) + out = self.nova(cmd % flv_name) + self.addCleanup(self.nova, 'flavor-delete %s' % flv_name) + return out, flv_name + + def test_create_flavor_with_no_swap(self): + out, _ = self._create_flavor() + self.assertEqual( + self.SWAP_DEFAULT, + self._get_column_value_from_single_row_table(out, "Swap")) + + def test_update_flavor_with_no_swap(self): + _, flv_name = self._create_flavor() + out = self.nova('flavor-update %s new-description' % flv_name) + self.assertEqual( + self.SWAP_DEFAULT, + self._get_column_value_from_single_row_table(out, "Swap")) + + def test_show_flavor_with_no_swap(self): + _, flv_name = self._create_flavor() + out = self.nova('flavor-show %s' % flv_name) + self.assertEqual(self.SWAP_DEFAULT, + self._get_value_from_the_table(out, "swap")) + + def test_list_flavor_with_no_swap(self): + self._create_flavor() + out = self.nova('flavor-list') + self.assertEqual( + self.SWAP_DEFAULT, + self._get_column_value_from_single_row_table(out, "Swap")) + + def test_create_flavor_with_swap(self): + out, _ = self._create_flavor(swap=10) + self.assertEqual( + '10', + self._get_column_value_from_single_row_table(out, "Swap")) + + +class TestFlavorNovaClientV275(TestFlavorNovaClientV274): + """Functional tests for flavors""" + + COMPUTE_API_VERSION = "2.75" + # NOTE(gmann): Since microversion 2.75, default value of 'swap' field is + # returned as 0. + SWAP_DEFAULT = '0' diff --git a/novaclient/tests/functional/v2/test_servers.py b/novaclient/tests/functional/v2/test_servers.py index 47fb9e061..030f0f27f 100644 --- a/novaclient/tests/functional/v2/test_servers.py +++ b/novaclient/tests/functional/v2/test_servers.py @@ -343,3 +343,44 @@ def test_interface_attach(self): self.assertEqual( self.network.id, self._get_value_from_the_table(output, 'net_id')) + + +class TestServeRebuildV274(base.ClientTestBase): + + COMPUTE_API_VERSION = '2.74' + REBUILD_FIELDS = ["OS-DCF:diskConfig", "accessIPv4", "accessIPv6", + "adminPass", "created", "description", + "flavor", "hostId", "id", "image", "key_name", + "locked", "locked_reason", "metadata", "name", + "progress", "server_groups", "status", "tags", + "tenant_id", "trusted_image_certificates", "updated", + "user_data", "user_id"] + + def test_rebuild(self): + server = self._create_server() + output = self.nova("rebuild %s %s" % (server.id, self.image.name)) + for field in self.REBUILD_FIELDS: + self.assertIn(field, output) + + +class TestServeRebuildV275(TestServeRebuildV274): + + COMPUTE_API_VERSION = '2.75' + REBUILD_FIELDS_V275 = ['OS-EXT-AZ:availability_zone', 'config_drive', + 'OS-EXT-SRV-ATTR:host', + 'OS-EXT-SRV-ATTR:hypervisor_hostname', + 'OS-EXT-SRV-ATTR:instance_name', + 'OS-EXT-SRV-ATTR:hostname', + 'OS-EXT-SRV-ATTR:kernel_id', + 'OS-EXT-SRV-ATTR:launch_index', + 'OS-EXT-SRV-ATTR:ramdisk_id', + 'OS-EXT-SRV-ATTR:reservation_id', + 'OS-EXT-SRV-ATTR:root_device_name', + 'host_status', + 'OS-SRV-USG:launched_at', + 'OS-SRV-USG:terminated_at', + 'OS-EXT-STS:task_state', 'OS-EXT-STS:vm_state', + 'OS-EXT-STS:power_state', 'security_groups', + 'os-extended-volumes:volumes_attached'] + + REBUILD_FIELDS = TestServeRebuildV274.REBUILD_FIELDS + REBUILD_FIELDS_V275 diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 801ac43c4..cbadfdd54 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -4275,6 +4275,7 @@ def test_versions(self): 71, # There are no version-wrapped shell method changes for this. 72, # There are no version-wrapped shell method changes for this. 74, # There are no version-wrapped shell method changes for this. + 75, # There are no version-wrapped shell method changes for this. ]) versions_supported = set(range(0, novaclient.API_MAX_VERSION.ver_minor + 1)) diff --git a/releasenotes/notes/microversion-v2_75-ea7fa3ba1396edea.yaml b/releasenotes/notes/microversion-v2_75-ea7fa3ba1396edea.yaml new file mode 100644 index 000000000..e1993f9dd --- /dev/null +++ b/releasenotes/notes/microversion-v2_75-ea7fa3ba1396edea.yaml @@ -0,0 +1,18 @@ +--- +features: + - | + Added support for `microversion 2.75`_. The following changes were made: + + - Return all fields of ``server`` in ``nova rebuild`` command which are + returned in ``nova show``. Both command will return the same set of + fields of ``server`` representation. + + - Default return value of ``swap`` field will be 0 (integer) in below + commands: + + - ``nova flavor-list`` + - ``nova flavor-show`` + - ``nova flavor-create`` + - ``nova flavor-update`` + + .. _microversion 2.75: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id67 From a3e44e8b39492e9604724cdfc5c9d2ede06e6c02 Mon Sep 17 00:00:00 2001 From: Surya Seetharaman Date: Tue, 13 Aug 2019 20:39:09 +0000 Subject: [PATCH 1545/1705] API microversion 2.76: Add 'power-update' external event The 2.76 microversion adds the 'power-update' server external event to the os-server-external-events API. This is an admin-only API by default and this event is currently only used by Ironic as part of updating the power-state of a physical instance, and therefore does not have any CLI or python API binding impacts in the client. Depends-On: https://review.opendev.org/#/c/645611/ Part of blueprint nova-support-instance-power-update Story: 2004969 Task: 34271 Change-Id: I1f5de90e19b7b13c7746fea8bbdf8e09bcb92cff --- novaclient/__init__.py | 2 +- novaclient/tests/unit/v2/test_shell.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/novaclient/__init__.py b/novaclient/__init__.py index fc031f18f..eaf452773 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.75") +API_MAX_VERSION = api_versions.APIVersion("2.76") diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index cbadfdd54..7eb86d7a9 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -4276,6 +4276,7 @@ def test_versions(self): 72, # There are no version-wrapped shell method changes for this. 74, # There are no version-wrapped shell method changes for this. 75, # There are no version-wrapped shell method changes for this. + 76, # doesn't require any changes in novaclient. ]) versions_supported = set(range(0, novaclient.API_MAX_VERSION.ver_minor + 1)) From 6aae5b23112f166209afc0465427c7506ef37f7c Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Mon, 26 Aug 2019 16:06:41 +0900 Subject: [PATCH 1546/1705] Follow up for microversion 2.75 This is a follow-up for microversion 2.75 - Multiple API cleanup changes. The base class of the TestFlavorNovaClientV274 class is the TenantTestBase class. And the base class of the TenantTestBase class is the ClientTestBase class. It is not necessary to use the TenantTestBase class as the base class. So specify the ClientTestBase class directly. TrivialFix Change-Id: I2cb971f46ba697d9386ca61b7f51169f02b605ab --- novaclient/tests/functional/v2/test_flavor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/tests/functional/v2/test_flavor.py b/novaclient/tests/functional/v2/test_flavor.py index 1458f0b54..cc25ad7fc 100644 --- a/novaclient/tests/functional/v2/test_flavor.py +++ b/novaclient/tests/functional/v2/test_flavor.py @@ -13,7 +13,7 @@ from novaclient.tests.functional import base -class TestFlavorNovaClientV274(base.TenantTestBase): +class TestFlavorNovaClientV274(base.ClientTestBase): """Functional tests for flavors""" COMPUTE_API_VERSION = "2.74" From 96dcf13f46ff66a68ac5b93b88f21f57ef709bbf Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Mon, 26 Aug 2019 11:26:18 -0400 Subject: [PATCH 1547/1705] Clarify --migration-type migration value as cold migration This is a follow up to a review discussion [1] where the "migration" value for the --migration-type option is for a cold migration. This change just updates docs and help strings. An alternative is changing "migration" as a valid value to "cold-migration" (or accept both) and under the covers treat "cold-migration" as "migration" for the actual API call. [1] https://review.opendev.org/#/c/675117/3/novaclient/v2/shell.py@5430 Related to blueprint more-migration-list-filters Change-Id: I6baa6af8731252e3c4976db06f0ca9cdfcb5e2f1 --- doc/source/cli/nova.rst | 3 +++ novaclient/v2/migrations.py | 6 +++--- novaclient/v2/shell.py | 6 +++--- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/doc/source/cli/nova.rst b/doc/source/cli/nova.rst index c0d80eda8..eeb804627 100644 --- a/doc/source/cli/nova.rst +++ b/doc/source/cli/nova.rst @@ -2593,6 +2593,9 @@ To see the list of evacuation operations *from* a compute service host: * evacuation * live-migration * migration + + .. note:: This is a cold migration. + * resize ``--source-compute `` diff --git a/novaclient/v2/migrations.py b/novaclient/v2/migrations.py index 0e94b4562..3aecd1e7e 100644 --- a/novaclient/v2/migrations.py +++ b/novaclient/v2/migrations.py @@ -61,7 +61,7 @@ def list(self, host=None, status=None, instance_uuid=None, :param status: filter migrations by status (optional). :param instance_uuid: filter migrations by instance uuid (optional). :param migration_type: Filter migrations by type. Valid values are: - evacuation, live-migration, migration, resize + evacuation, live-migration, migration (cold), resize :param source_compute: Filter migrations by source compute host name. """ return self._list_base(host=host, status=status, @@ -89,7 +89,7 @@ def list(self, host=None, status=None, instance_uuid=None, to a certain point of time. The provided time should be an ISO 8061 formatted time. e.g. 2016-03-04T06:27:59Z . (optional). :param migration_type: Filter migrations by type. Valid values are: - evacuation, live-migration, migration, resize + evacuation, live-migration, migration (cold), resize :param source_compute: Filter migrations by source compute host name. """ return self._list_base(host=host, status=status, @@ -122,7 +122,7 @@ def list(self, host=None, status=None, instance_uuid=None, equal to a certain point of time. The provided time should be an ISO 8061 formatted time. e.g. 2016-03-05T06:27:59Z . (optional). :param migration_type: Filter migrations by type. Valid values are: - evacuation, live-migration, migration, resize + evacuation, live-migration, migration (cold), resize :param source_compute: Filter migrations by source compute host name. """ return self._list_base(host=host, status=status, diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 34dea57f4..86628c53d 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -5427,7 +5427,7 @@ def migration_type(migration): dest='migration_type', metavar='', help=_('Filter migrations by type. Valid values are: evacuation, ' - 'live-migration, migration, resize')) + 'live-migration, migration (cold), resize')) @utils.arg( '--source-compute', dest='source_compute', @@ -5463,7 +5463,7 @@ def do_migration_list(cs, args): dest='migration_type', metavar='', help=_('Filter migrations by type. Valid values are: evacuation, ' - 'live-migration, migration, resize')) + 'live-migration, migration (cold), resize')) @utils.arg( '--source-compute', dest='source_compute', @@ -5535,7 +5535,7 @@ def do_migration_list(cs, args): dest='migration_type', metavar='', help=_('Filter migrations by type. Valid values are: evacuation, ' - 'live-migration, migration, resize')) + 'live-migration, migration (cold), resize')) @utils.arg( '--source-compute', dest='source_compute', From ecfa521b2126e2f0cee25969a7d2c4c8637abba2 Mon Sep 17 00:00:00 2001 From: zhangbailin Date: Thu, 13 Jun 2019 21:22:28 +0800 Subject: [PATCH 1548/1705] Microversion 2.77: Support Specifying AZ to unshelve This patch adds a new parameter ``--availability-zone`` to ``nova unshelve`` command. This can help users to specify an ``availability_zone`` to unshelve a shelve offloaded server from 2.77 microversion. Depends-On: https://review.opendev.org/#/c/663851/ Implements: blueprint support-specifying-az-when-restore-shelved-server Change-Id: I8bce8f430bc54f03bacc105e37fc8b3bbf2432c2 --- doc/source/cli/nova.rst | 8 ++++- novaclient/__init__.py | 2 +- novaclient/tests/unit/fixture_data/servers.py | 2 ++ novaclient/tests/unit/v2/fakes.py | 12 ++++++- novaclient/tests/unit/v2/test_servers.py | 36 +++++++++++++++++++ novaclient/tests/unit/v2/test_shell.py | 22 ++++++++++++ novaclient/v2/servers.py | 29 +++++++++++++++ novaclient/v2/shell.py | 18 +++++++++- .../microversion-v2_77-ffee30c180aa4dbe.yaml | 8 +++++ 9 files changed, 133 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/microversion-v2_77-ffee30c180aa4dbe.yaml diff --git a/doc/source/cli/nova.rst b/doc/source/cli/nova.rst index 1a0081eb2..b9e3fffc1 100644 --- a/doc/source/cli/nova.rst +++ b/doc/source/cli/nova.rst @@ -3698,7 +3698,7 @@ nova unshelve .. code-block:: console - usage: nova unshelve + usage: nova unshelve [--availability-zone ] Unshelve a server. @@ -3707,6 +3707,12 @@ Unshelve a server. ```` Name or ID of server. +**Optional arguments:** + +``--availability-zone `` + Name of the availability zone in which to unshelve a ``SHELVED_OFFLOADED`` + server. (Supported by API versions '2.77' - '2.latest') + .. _nova_update: nova update diff --git a/novaclient/__init__.py b/novaclient/__init__.py index eaf452773..00e0b3e4d 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.76") +API_MAX_VERSION = api_versions.APIVersion("2.77") diff --git a/novaclient/tests/unit/fixture_data/servers.py b/novaclient/tests/unit/fixture_data/servers.py index d3354469a..9962842a8 100644 --- a/novaclient/tests/unit/fixture_data/servers.py +++ b/novaclient/tests/unit/fixture_data/servers.py @@ -456,6 +456,8 @@ def post_servers_1234_action(self, request, context): return None elif action == 'lock': return None + elif action == 'unshelve': + return None elif action == 'rebuild': body = body[action] adminPass = body.get('adminPass', 'randompassword') diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 29ec88877..faf5f6f49 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -775,7 +775,7 @@ def delete_servers_1234_os_server_password(self, **kw): none_actions = ['revertResize', 'os-stop', 'os-start', 'forceDelete', 'restore', 'pause', 'unpause', 'unlock', 'unrescue', 'resume', 'suspend', 'shelve', - 'shelveOffload', 'unshelve', 'resetNetwork'] + 'shelveOffload', 'resetNetwork'] type_actions = ['os-getVNCConsole', 'os-getSPICEConsole', 'os-getRDPConsole'] @@ -852,6 +852,16 @@ def post_servers_1234_action(self, body, **kw): assert set(body[action].keys()) == expected else: assert body[action] is None + elif action == 'unshelve': + if self.api_version < api_versions.APIVersion("2.77"): + assert body[action] is None + else: + # In 2.77 and above, we allow body to be one of these: + # {'unshelve': None} + # {'unshelve': {'availability_zone': 'foo-az'}} + if body[action] is not None: + assert set(body[action].keys()) == set( + ['availability_zone']) elif action == 'rebuild': body = body[action] adminPass = body.get('adminPass', 'randompassword') diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index 62c8d8d43..5ef987faa 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -1839,3 +1839,39 @@ def test_create_server_with_host_and_hypervisor_hostname_pre_274_fails( hypervisor_hostname="new-host") self.assertIn("'host' argument is only allowed since microversion " "2.74", six.text_type(ex)) + + +class ServersV277Test(ServersV274Test): + + api_version = "2.77" + + def test_unshelve_with_az(self): + s = self.cs.servers.get(1234) + # Test going through the Server object. + ret = s.unshelve(availability_zone='foo-az') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action', + {'unshelve': { + 'availability_zone': 'foo-az'}}) + # Test going through the ServerManager directly. + ret = self.cs.servers.unshelve(s, availability_zone='foo-az') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action', + {'unshelve': { + 'availability_zone': 'foo-az'}}) + + def test_unshelve_server_pre_277_fails_with_specified_az(self): + self.cs.api_version = api_versions.APIVersion('2.76') + s = self.cs.servers.get(1234) + # Test going through the Server object. + ex = self.assertRaises(TypeError, + s.unshelve, + availability_zone='foo-az') + self.assertIn("unexpected keyword argument 'availability_zone'", + six.text_type(ex)) + # Test going through the ServerManager directly. + ex = self.assertRaises(TypeError, + self.cs.servers.unshelve, + s, availability_zone='foo-az') + self.assertIn("unexpected keyword argument 'availability_zone'", + six.text_type(ex)) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 7eb86d7a9..2dc8a38a5 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2258,6 +2258,27 @@ def test_unshelve(self): self.run_command('unshelve sample-server') self.assert_called('POST', '/servers/1234/action', {'unshelve': None}) + def test_unshelve_pre_v277_with_az_fails(self): + """Tests that trying to unshelve with an --availability-zone before + 2.77 is an error. + """ + self.assertRaises(SystemExit, + self.run_command, + 'unshelve --availability-zone foo-az sample-server', + api_version='2.76') + + def test_unshelve_v277(self): + # Test backward compat without an AZ specified. + self.run_command('unshelve sample-server', + api_version='2.77') + self.assert_called('POST', '/servers/1234/action', + {'unshelve': None}) + # Test with specifying an AZ. + self.run_command('unshelve --availability-zone foo-az sample-server', + api_version='2.77') + self.assert_called('POST', '/servers/1234/action', + {'unshelve': {'availability_zone': 'foo-az'}}) + def test_migrate(self): self.run_command('migrate sample-server') self.assert_called('POST', '/servers/1234/action', {'migrate': None}) @@ -4277,6 +4298,7 @@ def test_versions(self): 74, # There are no version-wrapped shell method changes for this. 75, # There are no version-wrapped shell method changes for this. 76, # doesn't require any changes in novaclient. + 77, # There are no version-wrapped shell method changes for this. ]) versions_supported = set(range(0, novaclient.API_MAX_VERSION.ver_minor + 1)) diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index 43ed34f22..acef78568 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -291,6 +291,7 @@ def shelve_offload(self): """ return self.manager.shelve_offload(self) + @api_versions.wraps("2.0", "2.76") def unshelve(self): """ Unshelve -- Unshelve the server. @@ -299,6 +300,18 @@ def unshelve(self): """ return self.manager.unshelve(self) + @api_versions.wraps("2.77") + def unshelve(self, availability_zone=None): + """ + Unshelve -- Unshelve the server. + + :param availability_zone: The specified availability zone name + (Optional) + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self.manager.unshelve(self, + availability_zone=availability_zone) + def diagnostics(self): """Diagnostics -- Retrieve server diagnostics.""" return self.manager.diagnostics(self) @@ -1222,6 +1235,7 @@ def shelve_offload(self, server): """ return self._action('shelveOffload', server, None) + @api_versions.wraps("2.0", "2.76") def unshelve(self, server): """ Unshelve the server. @@ -1231,6 +1245,21 @@ def unshelve(self, server): """ return self._action('unshelve', server, None) + @api_versions.wraps("2.77") + def unshelve(self, server, availability_zone=None): + """ + Unshelve the server. + + :param server: The :class:`Server` (or its ID) to unshelve + :param availability_zone: The specified availability zone name + (Optional) + :returns: An instance of novaclient.base.TupleWithMeta + """ + info = None + if availability_zone: + info = {'availability_zone': availability_zone} + return self._action('unshelve', server, info) + def ips(self, server): """ Return IP Addresses associated with the server. diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 7b9c02061..3eb68bc28 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -2285,9 +2285,25 @@ def do_shelve_offload(cs, args): @utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg( + '--availability-zone', + metavar='', + default=None, + dest='availability_zone', + help=_('Name of the availability zone in which to unshelve a ' + 'SHELVED_OFFLOADED server.'), + start_version='2.77') def do_unshelve(cs, args): """Unshelve a server.""" - _find_server(cs, args.server).unshelve() + update_kwargs = {} + # Microversion >= 2.77 will support user to specify an + # availability_zone to unshelve a shelve offloaded server. + if cs.api_version >= api_versions.APIVersion('2.77'): + if 'availability_zone' in args and args.availability_zone is not None: + update_kwargs['availability_zone'] = args.availability_zone + + server = _find_server(cs, args.server) + server.unshelve(**update_kwargs) @utils.arg('server', metavar='', help=_('Name or ID of server.')) diff --git a/releasenotes/notes/microversion-v2_77-ffee30c180aa4dbe.yaml b/releasenotes/notes/microversion-v2_77-ffee30c180aa4dbe.yaml new file mode 100644 index 000000000..6262f2207 --- /dev/null +++ b/releasenotes/notes/microversion-v2_77-ffee30c180aa4dbe.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Support has been added for `microversion 2.77`_. This microversion + allows specifying an availability zone to unshelve a shelve + offloaded server. + + .. _microversion 2.77: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id69 From aae95dcc7a79be019fc304ced76a351c16382ede Mon Sep 17 00:00:00 2001 From: Yongli He Date: Mon, 15 Jul 2019 16:36:09 +0800 Subject: [PATCH 1549/1705] Microversion 2.78 - show server topology Add support microversion 2.78 which adds server topology information in the output of the following new command: nova server-topology Depends-on: https://review.opendev.org/#/c/621476/ Change-Id: I6467d52d2528a37348458baf4842b571a97f3ed2 Implements: blueprint show-server-numa-topology --- doc/source/cli/nova.rst | 23 ++++++++++ novaclient/__init__.py | 2 +- novaclient/tests/unit/fixture_data/servers.py | 4 ++ novaclient/tests/unit/v2/fakes.py | 45 +++++++++++++++++++ novaclient/tests/unit/v2/test_servers.py | 24 ++++++++++ novaclient/tests/unit/v2/test_shell.py | 13 ++++++ novaclient/v2/servers.py | 18 ++++++++ novaclient/v2/shell.py | 11 +++++ .../microversion-v2_78-77a12630e668c2ae.yaml | 14 ++++++ 9 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/microversion-v2_78-77a12630e668c2ae.yaml diff --git a/doc/source/cli/nova.rst b/doc/source/cli/nova.rst index dcd24be9f..ab3107581 100644 --- a/doc/source/cli/nova.rst +++ b/doc/source/cli/nova.rst @@ -472,6 +472,9 @@ nova usage '--os-compute-api-version' flag to show help message for proper version] +``server-topology`` + Retrieve NUMA topology of the given server. + ``service-delete`` Delete the service. @@ -3358,6 +3361,26 @@ version] ```` Tag(s) to set. +.. _nova_server_topology: + +nova server-topology +-------------------- + +.. code-block:: console + + usage: nova server-topology + +Retrieve server NUMA topology information. Host specific fields are only +visible to users with the administrative role. +(Supported by API versions '2.78' - '2.latest') + +.. versionadded:: 16.0.0 + +**Positional arguments:** + +```` + Name or ID of server. + .. _nova_service-delete: nova service-delete diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 00e0b3e4d..3e5daf213 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.77") +API_MAX_VERSION = api_versions.APIVersion("2.78") diff --git a/novaclient/tests/unit/fixture_data/servers.py b/novaclient/tests/unit/fixture_data/servers.py index 9962842a8..7e4ee64ab 100644 --- a/novaclient/tests/unit/fixture_data/servers.py +++ b/novaclient/tests/unit/fixture_data/servers.py @@ -371,6 +371,10 @@ def setUp(self): self.requests_mock.delete(self.url('1234', 'os-interface', 'port-id'), headers=self.json_headers) + self.requests_mock.get(self.url('1234', 'topology'), + json=v2_fakes.SERVER_TOPOLOGY, + headers=self.json_headers) + # Testing with the following password and key # # Clear password: FooBar123 diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index faf5f6f49..d6fbf129d 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -58,6 +58,48 @@ FAKE_SERVICE_UUID_1 = '75e9eabc-ed3b-4f11-8bba-add1e7e7e2de' FAKE_SERVICE_UUID_2 = '1f140183-c914-4ddf-8757-6df73028aa86' +SERVER_TOPOLOGY = { + "nodes": [ + { + "cpu_pinning": { + "0": 0, + "1": 5 + }, + "host_node": 0, + "memory_mb": 1024, + "siblings": [ + [ + 0, + 1 + ] + ], + "vcpu_set": [ + 0, + 1 + ] + }, + { + "cpu_pinning": { + "2": 1, + "3": 8 + }, + "host_node": 1, + "memory_mb": 2048, + "siblings": [ + [ + 2, + 3 + ] + ], + "vcpu_set": [ + 2, + 3 + ] + } + ], + "pagesize_kb": 4 +} + class FakeClient(fakes.FakeClient, client.Client): @@ -738,6 +780,9 @@ def get_servers_1234_os_security_groups(self, **kw): 'rules': []}] }) + def get_servers_1234_topology(self, **kw): + return 200, {}, SERVER_TOPOLOGY + # # Server password # diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index 5ef987faa..c255cbd7b 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -1875,3 +1875,27 @@ def test_unshelve_server_pre_277_fails_with_specified_az(self): s, availability_zone='foo-az') self.assertIn("unexpected keyword argument 'availability_zone'", six.text_type(ex)) + + +class ServersV278Test(ServersV273Test): + + api_version = "2.78" + + def test_get_server_topology(self): + s = self.cs.servers.get(1234) + topology = s.topology() + self.assert_request_id(topology, fakes.FAKE_REQUEST_ID_LIST) + self.assertIsNotNone(topology) + self.assert_called('GET', '/servers/1234/topology') + + topology_from_manager = self.cs.servers.topology(1234) + self.assert_request_id(topology, fakes.FAKE_REQUEST_ID_LIST) + self.assertIsNotNone(topology_from_manager) + self.assert_called('GET', '/servers/1234/topology') + + self.assertEqual(topology, topology_from_manager) + + def test_get_server_topology_pre278(self): + self.cs.api_version = api_versions.APIVersion('2.77') + s = self.cs.servers.get(1234) + self.assertRaises(exceptions.VersionNotFoundForAPIMethod, s.topology) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 2dc8a38a5..928a9de42 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2463,6 +2463,19 @@ def test_diagnostics(self): self.run_command('diagnostics sample-server') self.assert_called('GET', '/servers/1234/diagnostics') + def test_server_topology(self): + self.run_command('server-topology 1234', api_version='2.78') + self.assert_called('GET', '/servers/1234/topology') + self.run_command('server-topology sample-server', api_version='2.78') + self.assert_called('GET', '/servers/1234/topology') + + def test_server_topology_pre278(self): + exp = self.assertRaises(SystemExit, + self.run_command, + 'server-topology 1234', + api_version='2.77') + self.assertIn('2', six.text_type(exp)) + def test_refresh_network(self): self.run_command('refresh-network 1234') self.assert_called('POST', '/os-server-external-events', diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index acef78568..9870be0bc 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -316,6 +316,11 @@ def diagnostics(self): """Diagnostics -- Retrieve server diagnostics.""" return self.manager.diagnostics(self) + @api_versions.wraps("2.78") + def topology(self): + """Retrieve server topology.""" + return self.manager.topology(self) + @api_versions.wraps("2.0", "2.55") def migrate(self): """ @@ -1286,6 +1291,19 @@ def diagnostics(self, server): base.getid(server)) return base.TupleWithMeta((resp, body), resp) + @api_versions.wraps("2.78") + def topology(self, server): + """ + Retrieve server topology. + + :param server: The :class:`Server` (or its ID) for which + topology to be returned + :returns: An instance of novaclient.base.DictWithMeta + """ + resp, body = self.api.client.get("/servers/%s/topology" % + base.getid(server)) + return base.DictWithMeta(body, resp) + def _validate_create_nics(self, nics): # nics are required with microversion 2.37+ and can be a string or list if self.api_version > api_versions.APIVersion('2.36'): diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index a08c86746..acaf17833 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -2313,6 +2313,17 @@ def do_diagnostics(cs, args): utils.print_dict(cs.servers.diagnostics(server)[1], wrap=80) +@api_versions.wraps("2.78") +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +def do_server_topology(cs, args): + """Retrieve server topology.""" + server = _find_server(cs, args.server) + # This prints a dict with only two properties: nodes and pagesize_kb + # nodes is a list of dicts so it does not print very well, it's just a + # json blob in the output. + utils.print_dict(cs.servers.topology(server), wrap=80) + + @utils.arg( 'server', metavar='', help=_('Name or ID of a server for which the network cache should ' diff --git a/releasenotes/notes/microversion-v2_78-77a12630e668c2ae.yaml b/releasenotes/notes/microversion-v2_78-77a12630e668c2ae.yaml new file mode 100644 index 000000000..17e6289ec --- /dev/null +++ b/releasenotes/notes/microversion-v2_78-77a12630e668c2ae.yaml @@ -0,0 +1,14 @@ +--- +features: + - | + Added support for `microversion 2.78`_ which outputs the server NUMA + topology information in the following command: + + * ``nova server-topology`` + + And associated python API bindings: + + * ``novaclient.v2.servers.Server.topology`` + * ``novaclient.v2.servers.ServerManager.topology`` + + .. _microversion 2.78: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id70 From cd396b8b61ed7496f4166a2237b27aa0a138f6e5 Mon Sep 17 00:00:00 2001 From: zhangbailin Date: Tue, 30 Jul 2019 19:52:00 +0800 Subject: [PATCH 1550/1705] Microversion 2.79: Add delete_on_termination to volume-attach API Support add 'delete_on_termination' field to the voume attach API to support configuring whether to delete the data volume when the server is destroyed. * Updating the ``nova volume-attachments`` command to show the ``delete_on_termination`` value if 2.79 or greater is used. * The '--delete-on-termination' option is added to the `nova volume-attach` CLI. Depends-On: https://review.opendev.org/#/c/673133/ Part of blueprint support-delete-on-termination-in-server-attach-volume Change-Id: I8dcf2fd21a2fd99ca4e05bd953fbbe026be3a619 --- doc/source/cli/nova.rst | 8 ++++- novaclient/__init__.py | 2 +- novaclient/tests/unit/v2/fakes.py | 11 ++++++ novaclient/tests/unit/v2/test_shell.py | 36 +++++++++++++++++++ novaclient/tests/unit/v2/test_volumes.py | 31 ++++++++++++++++ novaclient/v2/shell.py | 13 +++++++ novaclient/v2/volumes.py | 30 +++++++++++++++- .../microversion-v2_79-f13bc0414743dc16.yaml | 16 +++++++++ 8 files changed, 144 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/microversion-v2_79-f13bc0414743dc16.yaml diff --git a/doc/source/cli/nova.rst b/doc/source/cli/nova.rst index ab3107581..007e9686d 100644 --- a/doc/source/cli/nova.rst +++ b/doc/source/cli/nova.rst @@ -3849,7 +3849,8 @@ nova volume-attach .. code-block:: console - usage: nova volume-attach [--tag ] [] + usage: nova volume-attach [--delete-on-termination] [--tag ] + [] Attach a volume to a server. @@ -3870,6 +3871,11 @@ Attach a volume to a server. ``--tag `` Tag for the attached volume. (Supported by API versions '2.49' - '2.latest') +``--delete-on-termination`` + Specify if the attached volume sholud be deleted when the server is + destroyed. By default the attached volume is not deleted when the server is + destroyed. (Supported by API versions '2.79' - '2.latest') + .. _nova_volume-attachments: nova volume-attachments diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 3e5daf213..a4f9b4eec 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.78") +API_MAX_VERSION = api_versions.APIVersion("2.79") diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index d6fbf129d..dd60d7f4a 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -2115,6 +2115,11 @@ def post_servers_1234_os_volume_attachments(self, **kw): if self.api_version >= api_versions.APIVersion('2.70'): # Include the "tag" field in the response. attachment['tag'] = 'test-tag' + + if self.api_version >= api_versions.APIVersion('2.79'): + # Include the "delete_on_termination" field in the + # response. + attachment['delete_on_termination'] = True return (200, FAKE_RESPONSE_HEADERS, {"volumeAttachment": attachment}) def put_servers_1234_os_volume_attachments_Work(self, **kw): @@ -2139,6 +2144,12 @@ def get_servers_1234_os_volume_attachments(self, **kw): # Include the "tag" field in each attachment. for attachment in attachments['volumeAttachments']: attachment['tag'] = 'test-tag' + + if self.api_version >= api_versions.APIVersion('2.79'): + # Include the "delete_on_termination" field in each + # attachment. + for attachment in attachments['volumeAttachments']: + attachment['delete_on_termination'] = True return (200, FAKE_RESPONSE_HEADERS, attachments) def get_servers_1234_os_volume_attachments_Work(self, **kw): diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 928a9de42..a57c37948 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -3825,6 +3825,42 @@ def test_volume_attach_with_tag_v2_70(self): 'tag': 'test-tag'}}) self.assertIn('test-tag', out) + def test_volume_attachments_pre_v2_79(self): + out = self.run_command( + 'volume-attachments 1234', api_version='2.78')[0] + self.assert_called('GET', '/servers/1234/os-volume_attachments') + self.assertNotIn('DELETE ON TERMINATION', out) + + def test_volume_attachments_v2_79(self): + out = self.run_command( + 'volume-attachments 1234', api_version='2.79')[0] + self.assert_called('GET', '/servers/1234/os-volume_attachments') + self.assertIn('DELETE ON TERMINATION', out) + + def test_volume_attach_with_delete_on_termination_pre_v2_79(self): + self.assertRaises( + SystemExit, self.run_command, + 'volume-attach --delete-on-termination sample-server ' + 'Work /dev/vdb', api_version='2.78') + + def test_volume_attach_with_delete_on_termination_v2_79(self): + out = self.run_command( + 'volume-attach --delete-on-termination sample-server ' + '2 /dev/vdb', api_version='2.79')[0] + self.assert_called('POST', '/servers/1234/os-volume_attachments', + {'volumeAttachment': + {'device': '/dev/vdb', + 'volumeId': '2', + 'delete_on_termination': True}}) + self.assertIn('delete_on_termination', out) + + def test_volume_attach_without_delete_on_termination(self): + self.run_command('volume-attach sample-server Work', + api_version='2.79') + self.assert_called('POST', '/servers/1234/os-volume_attachments', + {'volumeAttachment': + {'volumeId': 'Work'}}) + def test_volume_update(self): self.run_command('volume-update sample-server Work Work') self.assert_called('PUT', '/servers/1234/os-volume_attachments/Work', diff --git a/novaclient/tests/unit/v2/test_volumes.py b/novaclient/tests/unit/v2/test_volumes.py index 932b71d79..fbc55ddf7 100644 --- a/novaclient/tests/unit/v2/test_volumes.py +++ b/novaclient/tests/unit/v2/test_volumes.py @@ -14,6 +14,7 @@ # under the License. import mock +import six from novaclient import api_versions from novaclient.tests.unit import utils @@ -126,3 +127,33 @@ def test_delete_server_volume_with_warn(self, mock_warn): volume_id=None, attachment_id="Work") mock_warn.assert_called_once() + + +class VolumesV279Test(VolumesV249Test): + api_version = "2.79" + + def test_create_server_volume_with_delete_on_termination(self): + v = self.cs.volumes.create_server_volume( + server_id=1234, + volume_id='15e59938-07d5-11e1-90e3-e3dffe0c5983', + device='/dev/vdb', + tag='tag1', + delete_on_termination=True + ) + self.assert_request_id(v, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called( + 'POST', '/servers/1234/os-volume_attachments', + {'volumeAttachment': { + 'volumeId': '15e59938-07d5-11e1-90e3-e3dffe0c5983', + 'device': '/dev/vdb', + 'tag': 'tag1', + 'delete_on_termination': True}}) + self.assertIsInstance(v, volumes.Volume) + + def test_create_server_volume_with_delete_on_termination_pre_v279(self): + self.cs.api_version = api_versions.APIVersion('2.78') + ex = self.assertRaises( + TypeError, self.cs.volumes.create_server_volume, "1234", + volume_id='15e59938-07d5-11e1-90e3-e3dffe0c5983', + delete_on_termination=True) + self.assertIn('delete_on_termination', six.text_type(ex)) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index acaf17833..2dfd5c92d 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -2633,6 +2633,13 @@ def _translate_volume_attachments_keys(collection): default=None, help=_('Tag for the attached volume.'), start_version="2.49") +@utils.arg( + '--delete-on-termination', + action='store_true', + default=False, + help=_('Specify if the attached volume should be deleted ' + 'when the server is destroyed.'), + start_version="2.79") def do_volume_attach(cs, args): """Attach a volume to a server.""" if args.device == 'auto': @@ -2642,6 +2649,9 @@ def do_volume_attach(cs, args): if 'tag' in args and args.tag: update_kwargs['tag'] = args.tag + if 'delete_on_termination' in args and args.delete_on_termination: + update_kwargs['delete_on_termination'] = args.delete_on_termination + volume = cs.volumes.create_server_volume(_find_server(cs, args.server).id, args.volume, args.device, @@ -2699,6 +2709,9 @@ def do_volume_attachments(cs, args): fields = ['ID', 'DEVICE', 'SERVER ID', 'VOLUME ID'] if cs.api_version >= api_versions.APIVersion('2.70'): fields.append('TAG') + # Microversion >= 2.79 returns the delete_on_termination value. + if cs.api_version >= api_versions.APIVersion('2.79'): + fields.append('DELETE ON TERMINATION') utils.print_list(volumes, fields) diff --git a/novaclient/v2/volumes.py b/novaclient/v2/volumes.py index d6208cbd1..8fc755658 100644 --- a/novaclient/v2/volumes.py +++ b/novaclient/v2/volumes.py @@ -55,7 +55,7 @@ def create_server_volume(self, server_id, volume_id, device=None): return self._create("/servers/%s/os-volume_attachments" % server_id, body, "volumeAttachment") - @api_versions.wraps("2.49") + @api_versions.wraps("2.49", "2.78") def create_server_volume(self, server_id, volume_id, device=None, tag=None): """ @@ -75,6 +75,34 @@ def create_server_volume(self, server_id, volume_id, device=None, return self._create("/servers/%s/os-volume_attachments" % server_id, body, "volumeAttachment") + @api_versions.wraps("2.79") + def create_server_volume(self, server_id, volume_id, device=None, + tag=None, delete_on_termination=False): + """ + Attach a volume identified by the volume ID to the given server ID + + :param server_id: The ID of the server. + :param volume_id: The ID of the volume to attach. + :param device: The device name (optional). + :param tag: The tag (optional). + :param delete_on_termination: Marked whether to delete the attached + volume when the server is deleted + (optional). + :rtype: :class:`Volume` + """ + # TODO(mriedem): Move this body construction into a private common + # helper method for all versions of create_server_volume to use. + body = {'volumeAttachment': {'volumeId': volume_id}} + if device is not None: + body['volumeAttachment']['device'] = device + if tag is not None: + body['volumeAttachment']['tag'] = tag + if delete_on_termination: + body['volumeAttachment']['delete_on_termination'] = ( + delete_on_termination) + return self._create("/servers/%s/os-volume_attachments" % server_id, + body, "volumeAttachment") + def update_server_volume(self, server_id, src_volid, dest_volid): """ Swaps the existing volume attachment to point to a new volume. diff --git a/releasenotes/notes/microversion-v2_79-f13bc0414743dc16.yaml b/releasenotes/notes/microversion-v2_79-f13bc0414743dc16.yaml new file mode 100644 index 000000000..ca81f58de --- /dev/null +++ b/releasenotes/notes/microversion-v2_79-f13bc0414743dc16.yaml @@ -0,0 +1,16 @@ +--- +features: + - | + Added support for `microversion 2.79`_ which includes the following + changes: + + - The ``--delete-on-termination`` option is added to the + ``nova volume-attach`` CLI. + - A ``DELETE ON TERMINATION`` column is added to the + ``nova volume-attachments`` table. + - New kwarg called ``delete_on_termination`` added to the python API + binding: + + - ``novaclient.v2.volumes.VolumeManager.create_server_volume()`` + + .. _microversion 2.79: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id71 From 730bc2c47ede9eca44b400f74540418765fcab66 Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Tue, 10 Sep 2019 16:27:38 +0900 Subject: [PATCH 1551/1705] doc: Add support microversions for options Add descriptions of support microversions for the '--marker' option and the '--limit' option in the 'nova hypervisor-list' command in the CLI reference. Change-Id: Ie25e848b51fd0220e318393db123c97b4a642091 Closes-Bug: #1616450 --- doc/source/cli/nova.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/source/cli/nova.rst b/doc/source/cli/nova.rst index 007e9686d..11573f7d0 100644 --- a/doc/source/cli/nova.rst +++ b/doc/source/cli/nova.rst @@ -1841,11 +1841,13 @@ List hypervisors. (Supported by API versions '2.0' - '2.latest') [hint: use ``--marker `` The last hypervisor of the previous page; displays list of hypervisors after "marker". + (Supported by API versions '2.33' - '2.latest') ``--limit `` Maximum number of hypervisors to display. If limit is bigger than 'CONF.api.max_limit' option of Nova API, limit 'CONF.api.max_limit' will be used instead. + (Supported by API versions '2.33' - '2.latest') .. _nova_hypervisor-servers: From d1c5dc61d60b29428484a38703a0064933cc7c0e Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Wed, 17 Jul 2019 11:40:24 +0900 Subject: [PATCH 1552/1705] Add a check for --config-drive option on nova boot A value of the '--config-drive' option must be a boolean value on the 'nova boot' command because nova accepts a boolean value only for the 'config_drive' parameter. So add a check for the '--config-drive' option on the 'nova boot' command. Fix a description for 'config_drive' parameter in the 'create' method of the novaclient.v2.ServerManager class. Change-Id: Ic6e65139302fbb662fb6ba60e73633dad8ffb72e Closes-Bug: #1825061 --- doc/source/cli/nova.rst | 3 +- novaclient/tests/unit/v2/test_shell.py | 35 +++++++++++-------- novaclient/v2/servers.py | 4 +-- novaclient/v2/shell.py | 6 ++-- .../notes/bug-1825061-2beb95db4d6df0cb.yaml | 5 +++ 5 files changed, 34 insertions(+), 19 deletions(-) create mode 100644 releasenotes/notes/bug-1825061-2beb95db4d6df0cb.yaml diff --git a/doc/source/cli/nova.rst b/doc/source/cli/nova.rst index 11573f7d0..712a5ec1f 100644 --- a/doc/source/cli/nova.rst +++ b/doc/source/cli/nova.rst @@ -1089,7 +1089,8 @@ quality of service support, microversion ``2.72`` is required. versions '2.42' - '2.latest') ``--config-drive `` - Enable config drive. + Enable config drive. The value must be a + boolean value. ``--poll`` Report the new server boot progress until it diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index a57c37948..ad233c5e5 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -240,42 +240,42 @@ def test_boot_secgroup(self): }}, ) - def test_boot_config_drive(self): + def test_boot_access_ip(self): self.run_command( - 'boot --flavor 1 --image %s --config-drive 1 some-server' % - FAKE_UUID_1) + 'boot --flavor 1 --image %s --access-ip-v4 10.10.10.10 ' + '--access-ip-v6 ::1 some-server' % FAKE_UUID_1) self.assert_called_anytime( 'POST', '/servers', {'server': { 'flavorRef': '1', 'name': 'some-server', 'imageRef': FAKE_UUID_1, - 'min_count': 1, + 'accessIPv4': '10.10.10.10', + 'accessIPv6': '::1', 'max_count': 1, - 'config_drive': True + 'min_count': 1 }}, ) - def test_boot_access_ip(self): + def test_boot_config_drive(self): self.run_command( - 'boot --flavor 1 --image %s --access-ip-v4 10.10.10.10 ' - '--access-ip-v6 ::1 some-server' % FAKE_UUID_1) + 'boot --flavor 1 --image %s --config-drive 1 some-server' % + FAKE_UUID_1) self.assert_called_anytime( 'POST', '/servers', {'server': { 'flavorRef': '1', 'name': 'some-server', 'imageRef': FAKE_UUID_1, - 'accessIPv4': '10.10.10.10', - 'accessIPv6': '::1', + 'min_count': 1, 'max_count': 1, - 'min_count': 1 + 'config_drive': True }}, ) - def test_boot_config_drive_custom(self): + def test_boot_config_drive_false(self): self.run_command( - 'boot --flavor 1 --image %s --config-drive /dev/hda some-server' % + 'boot --flavor 1 --image %s --config-drive false some-server' % FAKE_UUID_1) self.assert_called_anytime( 'POST', '/servers', @@ -285,10 +285,17 @@ def test_boot_config_drive_custom(self): 'imageRef': FAKE_UUID_1, 'min_count': 1, 'max_count': 1, - 'config_drive': '/dev/hda' }}, ) + def test_boot_config_drive_invalid_value(self): + ex = self.assertRaises( + exceptions.CommandError, self.run_command, + 'boot --flavor 1 --image %s --config-drive /dev/hda some-server' % + FAKE_UUID_1) + self.assertIn("The value of the '--config-drive' option must be " + "a boolean value.", six.text_type(ex)) + def test_boot_invalid_user_data(self): invalid_file = os.path.join(os.path.dirname(__file__), 'no_such_file') diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index 9870be0bc..1351189bf 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -1375,8 +1375,8 @@ def create(self, name, image, flavor, meta=None, files=None, any networking for the server. :param scheduler_hints: (optional extension) arbitrary key-value pairs specified by the client to help boot an instance - :param config_drive: (optional extension) value for config drive - either boolean, or volume-id + :param config_drive: (optional extension) a boolean value to enable + config drive :param disk_config: (optional extension) control how the disk is partitioned when the server is created. possible values are 'AUTO' or 'MANUAL'. diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 2dfd5c92d..5aab35af3 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -506,7 +506,9 @@ def _boot(cs, args): elif str(args.config_drive).lower() in ("false", "0", "", "none"): config_drive = None else: - config_drive = args.config_drive + raise exceptions.CommandError( + _("The value of the '--config-drive' option must be " + "a boolean value.")) boot_kwargs = dict( meta=meta, @@ -906,7 +908,7 @@ def _boot(cs, args): metavar="", dest='config_drive', default=False, - help=_("Enable config drive.")) + help=_("Enable config drive. The value must be a boolean value.")) @utils.arg( '--poll', dest='poll', diff --git a/releasenotes/notes/bug-1825061-2beb95db4d6df0cb.yaml b/releasenotes/notes/bug-1825061-2beb95db4d6df0cb.yaml new file mode 100644 index 000000000..573ad7fc7 --- /dev/null +++ b/releasenotes/notes/bug-1825061-2beb95db4d6df0cb.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + A check for a value of the '--config-drive' option has been added on the + ``nova boot`` command. A boolean value is only allowed in the option now. From 5d236ce6ba2cadc982862ce0372a61423f7f5288 Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Fri, 20 Sep 2019 17:42:06 +0000 Subject: [PATCH 1553/1705] Update master for stable/train Add file to the reno documentation build to show release notes for stable/train. Use pbr instruction to increment the minor version number automatically so that master versions are higher than the versions on stable/train. Change-Id: I8f59c8a48c571acad43d3639d8331a998330b8a2 Sem-Ver: feature --- releasenotes/source/index.rst | 1 + releasenotes/source/train.rst | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 releasenotes/source/train.rst diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index 1d78d69a5..aeee0e6e0 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -8,6 +8,7 @@ Contents :maxdepth: 2 unreleased + train stein rocky queens diff --git a/releasenotes/source/train.rst b/releasenotes/source/train.rst new file mode 100644 index 000000000..583900393 --- /dev/null +++ b/releasenotes/source/train.rst @@ -0,0 +1,6 @@ +========================== +Train Series Release Notes +========================== + +.. release-notes:: + :branch: stable/train From 6954aacd54e85859fecde22ac04db1ce7601dd35 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 25 Sep 2019 12:10:21 +0100 Subject: [PATCH 1554/1705] Stop silently ignoring invalid 'nova boot --hint' options The '--hint' option for 'nova boot' expects a key-value pair like so: nova boot --hint group=245e1dfe-2d0e-4139-80a9-fce124948896 ... However, the command doesn't complain if this isn't the case, meaning typos like the below aren't indicated to the user: nova boot --hint 245e1dfe-2d0e-4139-80a9-fce124948896 Due to how we'd implemented this here, this ultimately results in us POSTing the following as part of the body to 'os-servers': { ... "OS-SCH-HNT:scheduler_hints": { "245e1dfe-2d0e-4139-80a9-fce124948896": null } ... } Which is unfortunately allowed and ignored by nova due to the use of 'additionalProperties' in the schema [1] Do what we do for loads of other options and explicitly fail on invalid values. [1] https://github.com/openstack/nova/blob/19.0.0/nova/api/openstack/compute/schemas/servers.py#L142-L146 Change-Id: I0f9f75cba68e7582d32d4aab2f8f077b4360d386 Signed-off-by: Stephen Finucane Closes-Bug: #1845322 --- novaclient/tests/unit/v2/test_shell.py | 44 +++++++++++++++++++++----- novaclient/v2/shell.py | 4 +-- 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index ad233c5e5..48b77b224 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -85,17 +85,27 @@ def setUp(self): self.useFixture(fixtures.MonkeyPatch( 'novaclient.client.Client', fakes.FakeClient)) + # TODO(stephenfin): We should migrate most of the existing assertRaises + # calls to simply pass expected_error to this instead so we can easily + # capture and compare output @mock.patch('sys.stdout', new_callable=six.StringIO) @mock.patch('sys.stderr', new_callable=six.StringIO) - def run_command(self, cmd, mock_stderr, mock_stdout, api_version=None): + def run_command(self, cmd, mock_stderr, mock_stdout, api_version=None, + expected_error=None): version_options = [] if api_version: version_options.extend(["--os-compute-api-version", api_version, "--service-type", "computev21"]) - if isinstance(cmd, list): - self.shell.main(version_options + cmd) + if not isinstance(cmd, list): + cmd = cmd.split() + + if expected_error: + self.assertRaises(expected_error, + self.shell.main, + version_options + cmd) else: - self.shell.main(version_options + cmd.split()) + self.shell.main(version_options + cmd) + return mock_stdout.getvalue(), mock_stderr.getvalue() def assert_called(self, method, url, body=None, **kwargs): @@ -755,9 +765,12 @@ def test_boot_with_incorrect_metadata(self): self.assertEqual(expected, result.args[0]) def test_boot_hints(self): - self.run_command('boot --image %s --flavor 1 ' - '--hint a=b0=c0 --hint a=b1=c1 some-server ' % - FAKE_UUID_1) + cmd = ('boot --image %s --flavor 1 ' + '--hint same_host=a0cf03a5-d921-4877-bb5c-86d26cf818e1 ' + '--hint same_host=8c19174f-4220-44f0-824a-cd1eeef10287 ' + '--hint query=[>=,$free_ram_mb,1024] ' + 'some-server' % FAKE_UUID_1) + self.run_command(cmd) self.assert_called_anytime( 'POST', '/servers', { @@ -768,10 +781,25 @@ def test_boot_hints(self): 'min_count': 1, 'max_count': 1, }, - 'os:scheduler_hints': {'a': ['b0=c0', 'b1=c1']}, + 'os:scheduler_hints': { + 'same_host': [ + 'a0cf03a5-d921-4877-bb5c-86d26cf818e1', + '8c19174f-4220-44f0-824a-cd1eeef10287', + ], + 'query': '[>=,$free_ram_mb,1024]', + }, }, ) + def test_boot_hints_invalid(self): + cmd = ('boot --image %s --flavor 1 ' + '--hint a0cf03a5-d921-4877-bb5c-86d26cf818e1 ' + 'some-server' % FAKE_UUID_1) + _, err = self.run_command(cmd, expected_error=SystemExit) + self.assertIn("'a0cf03a5-d921-4877-bb5c-86d26cf818e1' is not in " + "the format of 'key=value'", + err) + def test_boot_nic_auto_not_alone_after(self): cmd = ('boot --image %s --flavor 1 ' '--nic auto,tag=foo some-server' % diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 5aab35af3..772942e96 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -489,8 +489,7 @@ def _boot(cs, args): hints = {} if args.scheduler_hints: - for hint in args.scheduler_hints: - key, _sep, value = hint.partition('=') + for key, value in args.scheduler_hints: # NOTE(vish): multiple copies of the same hint will # result in a list of values if key in hints: @@ -812,6 +811,7 @@ def _boot(cs, args): '--hint', action='append', dest='scheduler_hints', + type=_key_value_pairing, default=[], metavar='', help=_("Send arbitrary key/value pairs to the scheduler for custom " From 48634ed51e64f10424eef5bfb18dc63eda051c1d Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 1 Oct 2019 15:43:26 +0100 Subject: [PATCH 1555/1705] Add release note for bug 1845322 In change I0f9f75cba68e7582d32d4aab2f8f077b4360d386, we modified the behavior of the ``--hint`` option for the ``boot`` command. We want to backport this so add a release note to alert users to the change in behavior. Change-Id: I753e9a0cda1e118578373c519cf2fb2dd605a623 Signed-off-by: Stephen Finucane Related-Bug: #1845322 --- releasenotes/notes/bug-1845322-463ee407b60131c9.yaml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 releasenotes/notes/bug-1845322-463ee407b60131c9.yaml diff --git a/releasenotes/notes/bug-1845322-463ee407b60131c9.yaml b/releasenotes/notes/bug-1845322-463ee407b60131c9.yaml new file mode 100644 index 000000000..9c60e72fb --- /dev/null +++ b/releasenotes/notes/bug-1845322-463ee407b60131c9.yaml @@ -0,0 +1,6 @@ +--- +upgrade: + - | + The ``--hint`` option for the ``boot`` command expects a key-value + argument. Previously, if this was not the case, the argument would be + silently ignored. It will now raise an error. From 364cad41912e2c0f99a30f78b2835f3480a18d6e Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Fri, 4 Oct 2019 09:37:02 +0900 Subject: [PATCH 1556/1705] Remove cells v1 and extension commands and APIs The following CLIs and their backing API bindings which have been deprecated since 20.0.0 Train release have now been removed. - list-extensions - cell-capacities - cell-show Change-Id: I8e6edf1e4c1bf12d51ed993363129b4f4c3aa36c --- doc/source/cli/nova.rst | 55 -------------- .../v2/legacy/test_readonly_nova.py | 7 -- novaclient/tests/unit/v2/fakes.py | 75 ------------------- novaclient/tests/unit/v2/test_cells.py | 55 -------------- .../tests/unit/v2/test_list_extensions.py | 41 ---------- novaclient/tests/unit/v2/test_shell.py | 24 ------ novaclient/v2/cells.py | 56 -------------- novaclient/v2/client.py | 4 - novaclient/v2/list_extensions.py | 50 ------------- novaclient/v2/shell.py | 54 ------------- ...-extentions-commands-4b26c826ad5194ca.yaml | 8 ++ 11 files changed, 8 insertions(+), 421 deletions(-) delete mode 100644 novaclient/tests/unit/v2/test_cells.py delete mode 100644 novaclient/tests/unit/v2/test_list_extensions.py delete mode 100644 novaclient/v2/cells.py delete mode 100644 novaclient/v2/list_extensions.py create mode 100644 releasenotes/notes/remove-deprecated-cellsv1-extentions-commands-4b26c826ad5194ca.yaml diff --git a/doc/source/cli/nova.rst b/doc/source/cli/nova.rst index 712a5ec1f..bf919d4b8 100644 --- a/doc/source/cli/nova.rst +++ b/doc/source/cli/nova.rst @@ -108,13 +108,6 @@ nova usage ``boot`` Boot a new server. -``cell-capacities`` - Get cell capacities for all cells or a given - cell. - -``cell-show`` - Show details of a given cell. - ``clear-password`` Clear the admin password for a server from the metadata server. This action does not actually @@ -309,10 +302,6 @@ nova usage ``list`` List servers. -``list-extensions`` - List all the os-api extensions that are - available. - ``list-secgroup`` List Security Group(s) of a server. @@ -1131,38 +1120,6 @@ quality of service support, microversion ``2.72`` is required. Requested hypervisor hostname to create servers. Admin only by default. (Supported by API versions '2.74' - '2.latest') -.. _nova_cell-capacities: - -nova cell-capacities --------------------- - -.. code-block:: console - - usage: nova cell-capacities [--cell ] - -Get cell capacities for all cells or a given cell. - -**Optional arguments:** - -``--cell `` - Name of the cell to get the capacities. - -.. _nova_cell-show: - -nova cell-show --------------- - -.. code-block:: console - - usage: nova cell-show - -Show details of a given cell. - -**Positional arguments:** - -```` - Name of the cell. - .. _nova_clear-password: nova clear-password @@ -2372,18 +2329,6 @@ present in the failure domain. unlocked servers. (Supported by API versions '2.73' - '2.latest') - -.. _nova_list-extensions: - -nova list-extensions --------------------- - -.. code-block:: console - - usage: nova list-extensions - -List all the os-api extensions that are available. - .. _nova_list-secgroup: nova list-secgroup diff --git a/novaclient/tests/functional/v2/legacy/test_readonly_nova.py b/novaclient/tests/functional/v2/legacy/test_readonly_nova.py index 7f81e41ae..ed002c50b 100644 --- a/novaclient/tests/functional/v2/legacy/test_readonly_nova.py +++ b/novaclient/tests/functional/v2/legacy/test_readonly_nova.py @@ -85,13 +85,6 @@ def test_admin_usage_list(self): def test_admin_help(self): self.nova('help') - def test_admin_list_extensions(self): - output = self.nova('list-extensions', merge_stderr=True) - self.assertIn( - 'The API extension interface has been deprecated. This command ' - 'will be removed in the first major release after ' - 'the Nova server 20.0.0 Train release.', output) - def test_agent_list(self): self.nova('agent-list') self.nova('agent-list', flags='--debug') diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index dd60d7f4a..3430679be 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -328,53 +328,6 @@ def put_os_agents_1(self, body, **kw): "md5hash": "add6bb58e139be103324d04d82d8f546", 'id': 1}}) - # - # List all extensions - # - - def get_extensions(self, **kw): - exts = [ - { - "alias": "NMN", - "description": "Multiple network support", - "links": [], - "name": "Multinic", - "namespace": ("http://docs.openstack.org/" - "compute/ext/multinic/api/v1.1"), - "updated": "2011-06-09T00:00:00+00:00" - }, - { - "alias": "OS-DCF", - "description": "Disk Management Extension", - "links": [], - "name": "DiskConfig", - "namespace": ("http://docs.openstack.org/" - "compute/ext/disk_config/api/v1.1"), - "updated": "2011-09-27T00:00:00+00:00" - }, - { - "alias": "OS-EXT-SRV-ATTR", - "description": "Extended Server Attributes support.", - "links": [], - "name": "ExtendedServerAttributes", - "namespace": ("http://docs.openstack.org/" - "compute/ext/extended_status/api/v1.1"), - "updated": "2011-11-03T00:00:00+00:00" - }, - { - "alias": "OS-EXT-STS", - "description": "Extended Status support", - "links": [], - "name": "ExtendedStatus", - "namespace": ("http://docs.openstack.org/" - "compute/ext/extended_status/api/v1.1"), - "updated": "2011-11-03T00:00:00+00:00" - }, - ] - return (200, FAKE_RESPONSE_HEADERS, { - "extensions": exts, - }) - # # Limits # @@ -2296,34 +2249,6 @@ def post_servers_uuid5_action(self, **kw): def post_servers_uuid6_action(self, **kw): return 202, {}, {} - def get_os_cells_child_cell(self, **kw): - cell = {'cell': { - 'username': 'cell1_user', - 'name': 'cell1', - 'rpc_host': '10.0.1.10', - 'info': { - 'username': 'cell1_user', - 'rpc_host': '10.0.1.10', - 'type': 'child', - 'name': 'cell1', - 'rpc_port': 5673}, - 'type': 'child', - 'rpc_port': 5673, - 'loaded': True - }} - return (200, FAKE_RESPONSE_HEADERS, cell) - - def get_os_cells_capacities(self, **kw): - cell_capacities_response = {"cell": {"capacities": {"ram_free": { - "units_by_mb": {"8192": 0, "512": 13, "4096": 1, "2048": 3, - "16384": 0}, "total_mb": 7680}, "disk_free": { - "units_by_mb": {"81920": 11, "20480": 46, "40960": 23, "163840": 5, - "0": 0}, "total_mb": 1052672}}}} - return (200, FAKE_RESPONSE_HEADERS, cell_capacities_response) - - def get_os_cells_child_cell_capacities(self, **kw): - return self.get_os_cells_capacities() - def get_os_migrations(self, **kw): migration1 = { "created_at": "2012-10-29T13:42:02.000000", diff --git a/novaclient/tests/unit/v2/test_cells.py b/novaclient/tests/unit/v2/test_cells.py deleted file mode 100644 index ff898f532..000000000 --- a/novaclient/tests/unit/v2/test_cells.py +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright 2013 Rackspace Hosting -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import mock - -from novaclient import api_versions -from novaclient.tests.unit import utils -from novaclient.tests.unit.v2 import fakes - -CELL_V1_DEPRECATION_WARNING = ( - 'The cells v1 interface has been deprecated in Nova since 16.0.0 Pike ' - 'Release. This API binding will be removed in the first major release ' - 'after the Nova server 20.0.0 Train release.') - - -@mock.patch('warnings.warn') -class CellsExtensionTests(utils.TestCase): - def setUp(self): - super(CellsExtensionTests, self).setUp() - self.cs = fakes.FakeClient(api_versions.APIVersion("2.1")) - - def test_get_cells(self, mock_warn): - cell_name = 'child_cell' - cell = self.cs.cells.get(cell_name) - self.assert_request_id(cell, fakes.FAKE_REQUEST_ID_LIST) - self.cs.assert_called('GET', '/os-cells/%s' % cell_name) - mock_warn.assert_called_once_with(CELL_V1_DEPRECATION_WARNING, - DeprecationWarning) - - def test_get_capacities_for_a_given_cell(self, mock_warn): - cell_name = 'child_cell' - ca = self.cs.cells.capacities(cell_name) - self.assert_request_id(ca, fakes.FAKE_REQUEST_ID_LIST) - self.cs.assert_called('GET', '/os-cells/%s/capacities' % cell_name) - mock_warn.assert_called_once_with(CELL_V1_DEPRECATION_WARNING, - DeprecationWarning) - - def test_get_capacities_for_all_cells(self, mock_warn): - ca = self.cs.cells.capacities() - self.assert_request_id(ca, fakes.FAKE_REQUEST_ID_LIST) - self.cs.assert_called('GET', '/os-cells/capacities') - mock_warn.assert_called_once_with(CELL_V1_DEPRECATION_WARNING, - DeprecationWarning) diff --git a/novaclient/tests/unit/v2/test_list_extensions.py b/novaclient/tests/unit/v2/test_list_extensions.py deleted file mode 100644 index de299cd01..000000000 --- a/novaclient/tests/unit/v2/test_list_extensions.py +++ /dev/null @@ -1,41 +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. - -import mock - -from novaclient import api_versions -from novaclient.tests.unit import utils -from novaclient.tests.unit.v2 import fakes - - -class ListExtensionsTests(utils.TestCase): - def setUp(self): - super(ListExtensionsTests, self).setUp() - self.cs = fakes.FakeClient(api_versions.APIVersion("2.1")) - - @mock.patch('warnings.warn') - def test_list_extensions(self, mock_warn): - all_exts = self.cs.list_extensions.show_all() - self.assert_request_id(all_exts, fakes.FAKE_REQUEST_ID_LIST) - self.cs.assert_called('GET', '/extensions') - self.assertGreater(len(all_exts), 0) - warning_message = ( - 'The API extension interface has been deprecated since 12.0.0 ' - 'Liberty Release. This API binding will be removed in the first ' - 'major release after the Nova server 20.0.0 Train release.') - mock_warn.assert_called_once_with(warning_message, DeprecationWarning) - for r in all_exts: - mock_warn.reset_mock() - self.assertGreater(len(r.summary), 0) - mock_warn.assert_called_once_with(warning_message, - DeprecationWarning) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 48b77b224..06b27ad39 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -3992,30 +3992,6 @@ def test_instance_usage_audit_log_with_before(self): self.assert_called('GET', '/os-instance_usage_audit_log' '/2016-12-10%2013%3A59%3A59.999999') - def test_cell_show(self): - _, err = self.run_command('cell-show child_cell') - self.assert_called('GET', '/os-cells/child_cell') - self.assertIn( - 'The cells v1 interface has been deprecated. This command will be ' - 'removed in the first major release after the Nova server 20.0.0 ' - 'Train release.', err) - - def test_cell_capacities_with_cell_name(self): - _, err = self.run_command('cell-capacities --cell child_cell') - self.assert_called('GET', '/os-cells/child_cell/capacities') - self.assertIn( - 'The cells v1 interface has been deprecated. This command will be ' - 'removed in the first major release after the Nova server 20.0.0 ' - 'Train release.', err) - - def test_cell_capacities_without_cell_name(self): - _, err = self.run_command('cell-capacities') - self.assert_called('GET', '/os-cells/capacities') - self.assertIn( - 'The cells v1 interface has been deprecated. This command will be ' - 'removed in the first major release after the Nova server 20.0.0 ' - 'Train release.', err) - def test_migration_list(self): self.run_command('migration-list') self.assert_called('GET', '/os-migrations') diff --git a/novaclient/v2/cells.py b/novaclient/v2/cells.py deleted file mode 100644 index 44cadddbf..000000000 --- a/novaclient/v2/cells.py +++ /dev/null @@ -1,56 +0,0 @@ -# Copyright 2013 Rackspace Hosting -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import warnings - -from novaclient import base -from novaclient.i18n import _ - -CELL_V1_DEPRECATION_WARNING = _( - 'The cells v1 interface has been deprecated in Nova since 16.0.0 Pike ' - 'Release. This API binding will be removed in the first major release ' - 'after the Nova server 20.0.0 Train release.') - - -class Cell(base.Resource): - """DEPRECATED""" - def __repr__(self): - return "" % self.name - - -class CellsManager(base.Manager): - """DEPRECATED""" - resource_class = Cell - - def get(self, cell_name): - """ - DEPRECATED Get a cell. - - :param cell_name: Name of the :class:`Cell` to get. - :rtype: :class:`Cell` - """ - warnings.warn(CELL_V1_DEPRECATION_WARNING, DeprecationWarning) - return self._get("/os-cells/%s" % cell_name, "cell") - - def capacities(self, cell_name=None): - """ - DEPRECATED Get capacities for a cell. - - :param cell_name: Name of the :class:`Cell` to get capacities for. - :rtype: :class:`Cell` - """ - warnings.warn(CELL_V1_DEPRECATION_WARNING, DeprecationWarning) - path = ["%s/capacities" % cell_name, "capacities"][cell_name is None] - return self._get("/os-cells/%s" % path, "cell") diff --git a/novaclient/v2/client.py b/novaclient/v2/client.py index cec9f2f65..dc1701169 100644 --- a/novaclient/v2/client.py +++ b/novaclient/v2/client.py @@ -22,7 +22,6 @@ from novaclient.v2 import aggregates from novaclient.v2 import assisted_volume_snapshots from novaclient.v2 import availability_zones -from novaclient.v2 import cells from novaclient.v2 import flavor_access from novaclient.v2 import flavors from novaclient.v2 import hypervisors @@ -31,7 +30,6 @@ from novaclient.v2 import instance_usage_audit_log from novaclient.v2 import keypairs from novaclient.v2 import limits -from novaclient.v2 import list_extensions from novaclient.v2 import migrations from novaclient.v2 import networks from novaclient.v2 import quota_classes @@ -167,11 +165,9 @@ def __init__(self, # deprecated now, which is why it is not initialized by default. self.assisted_volume_snapshots = \ assisted_volume_snapshots.AssistedSnapshotManager(self) - self.cells = cells.CellsManager(self) self.instance_action = instance_action.InstanceActionManager(self) self.instance_usage_audit_log = \ instance_usage_audit_log.InstanceUsageAuditLogManager(self) - self.list_extensions = list_extensions.ListExtManager(self) self.migrations = migrations.MigrationManager(self) self.server_external_events = \ server_external_events.ServerExternalEventManager(self) diff --git a/novaclient/v2/list_extensions.py b/novaclient/v2/list_extensions.py deleted file mode 100644 index e7043e31b..000000000 --- a/novaclient/v2/list_extensions.py +++ /dev/null @@ -1,50 +0,0 @@ -# Copyright 2011 OpenStack Foundation -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import warnings - -from novaclient import base -from novaclient.i18n import _ - -EXTENSION_DEPRECATION_WARNING = _( - 'The API extension interface has been deprecated since 12.0.0 Liberty ' - 'Release. This API binding will be removed in the first major release ' - 'after the Nova server 20.0.0 Train release.') - - -class ListExtResource(base.Resource): - """DEPRECATED""" - @property - def summary(self): - """DEPRECATED""" - warnings.warn(EXTENSION_DEPRECATION_WARNING, DeprecationWarning) - descr = self.description.strip() - if not descr: - return '??' - lines = descr.split("\n") - if len(lines) == 1: - return lines[0] - else: - return lines[0] + "..." - - -class ListExtManager(base.Manager): - """DEPRECATED""" - resource_class = ListExtResource - - def show_all(self): - """DEPRECATED""" - warnings.warn(EXTENSION_DEPRECATION_WARNING, DeprecationWarning) - return self._list("/extensions", 'extensions') diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 772942e96..3d7ba896d 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -48,21 +48,6 @@ logger = logging.getLogger(__name__) -CELL_V1_DEPRECATION_WARNING = _( - 'The cells v1 interface has been deprecated. This command will be removed ' - 'in the first major release after the Nova server 20.0.0 Train release.') - -EXTENSION_DEPRECATION_WARNING = _( - 'The API extension interface has been deprecated. This command will be ' - 'removed in the first major release after the Nova server 20.0.0 Train ' - 'release.') - - -# NOTE(takashin): Remove this along with the deprecated commands in the first -# major python-novaclient release AFTER the nova server 20.0.0 Train release. -def _emit_deprecation_warning(message): - print(message, file=sys.stderr) - def emit_duplicated_image_with_warning(img, image_with): img_uuid_list = [str(image.id) for image in img] @@ -4928,35 +4913,6 @@ def do_server_tag_delete_all(cs, args): server.delete_all_tags() -@utils.arg( - 'cell', - metavar='', - help=_('Name of the cell.')) -def do_cell_show(cs, args): - """DEPRECATED Show details of a given cell.""" - _emit_deprecation_warning(CELL_V1_DEPRECATION_WARNING) - cell = cs.cells.get(args.cell) - utils.print_dict(cell.to_dict()) - - -@utils.arg( - '--cell', - metavar='', - help=_("Name of the cell to get the capacities."), - default=None) -def do_cell_capacities(cs, args): - """DEPRECATED Get cell capacities for all cells or a given cell.""" - _emit_deprecation_warning(CELL_V1_DEPRECATION_WARNING) - cell = cs.cells.capacities(args.cell) - print(_("Ram Available: %s MiB") % cell.capacities['ram_free']['total_mb']) - utils.print_dict(cell.capacities['ram_free']['units_by_mb'], - dict_property='Ram(MiB)', dict_value="Units") - print(_("\nDisk Available: %s MiB") % - cell.capacities['disk_free']['total_mb']) - utils.print_dict(cell.capacities['disk_free']['units_by_mb'], - dict_property='Disk(MiB)', dict_value="Units") - - @utils.arg('server', metavar='', help='Name or ID of server.') def do_force_delete(cs, args): """Force delete a server.""" @@ -5378,16 +5334,6 @@ def do_instance_action_list(cs, args): sortby_index=3) -def do_list_extensions(cs, _args): - """ - DEPRECATED List all the os-api extensions that are available. - """ - _emit_deprecation_warning(EXTENSION_DEPRECATION_WARNING) - extensions = cs.list_extensions.show_all() - fields = ["Name", "Summary", "Alias", "Updated"] - utils.print_list(extensions, fields) - - @utils.arg('host', metavar='', help='The hypervisor hostname (or pattern) to search for. ' 'WARNING: Use a fully qualified domain name if you only ' diff --git a/releasenotes/notes/remove-deprecated-cellsv1-extentions-commands-4b26c826ad5194ca.yaml b/releasenotes/notes/remove-deprecated-cellsv1-extentions-commands-4b26c826ad5194ca.yaml new file mode 100644 index 000000000..eb452452e --- /dev/null +++ b/releasenotes/notes/remove-deprecated-cellsv1-extentions-commands-4b26c826ad5194ca.yaml @@ -0,0 +1,8 @@ +--- +upgrade: + - | + The following CLIs and their backing API bindings have been removed. + + - ``nova list-extensions`` + - ``nova cell-capacities`` + - ``nova cell-show`` From 6c0e4d7a3940ba6d4d11d0c89933cc00b898ed9e Mon Sep 17 00:00:00 2001 From: Akihiro Motoki Date: Fri, 11 Oct 2019 03:48:51 +0900 Subject: [PATCH 1557/1705] PDF documentation build Also reorganizes the document structure to match both HTML and PDF docs. Story: 2006100 Task: 35143 Change-Id: Ie3f38e2ecf52e6a6cbd52bb36196e6f589f1ca0f --- doc/source/conf.py | 14 ++++++++++++++ .../deprecation-policy.rst | 0 doc/source/contributor/index.rst | 1 + doc/source/index.rst | 4 ++-- doc/source/reference/index.rst | 6 +++--- doc/source/user/index.rst | 1 + .../api/index.rst => user/python-api.rst} | 7 +------ doc/source/user/shell.rst | 10 +--------- tox.ini | 12 +++++++++++- 9 files changed, 34 insertions(+), 21 deletions(-) rename doc/source/{reference => contributor}/deprecation-policy.rst (100%) rename doc/source/{reference/api/index.rst => user/python-api.rst} (97%) diff --git a/doc/source/conf.py b/doc/source/conf.py index f90c50ad3..5e69e49c2 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -72,6 +72,20 @@ # robots.txt. html_extra_path = ['_extra'] +# -- Options for LaTeX output ------------------------------------------------- + +latex_documents = [ + ('index', 'doc-python-novaclient.tex', u'python-novaclient Documentation', + u'OpenStack Foundation', 'manual'), +] + +latex_elements = { + 'extraclassoptions': 'openany,oneside', + 'preamble': r'\setcounter{tocdepth}{4}', + 'makeindex': '', + 'printindex': '', +} + # -- Options for openstackdocstheme ------------------------------------------- repository_name = 'openstack/python-novaclient' diff --git a/doc/source/reference/deprecation-policy.rst b/doc/source/contributor/deprecation-policy.rst similarity index 100% rename from doc/source/reference/deprecation-policy.rst rename to doc/source/contributor/deprecation-policy.rst diff --git a/doc/source/contributor/index.rst b/doc/source/contributor/index.rst index 58b9bd7e8..50fd507f5 100644 --- a/doc/source/contributor/index.rst +++ b/doc/source/contributor/index.rst @@ -15,3 +15,4 @@ __ https://docs.openstack.org/infra/manual/developers.html#development-workflow microversions testing + deprecation-policy diff --git a/doc/source/index.rst b/doc/source/index.rst index 243c94979..5c0887956 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -3,7 +3,7 @@ =========================================== This is a client for OpenStack Nova API. There's a :doc:`Python API -` (the :mod:`novaclient` module), and a :doc:`command-line +` (the :mod:`novaclient` module), and a :doc:`command-line script ` (installed as :program:`nova`). Each implements the entire OpenStack Nova API. @@ -22,6 +22,6 @@ such as TryStack, HP, or Rackspace, in order to use the nova client. :maxdepth: 2 user/index - reference/index cli/index + reference/index contributor/index diff --git a/doc/source/reference/index.rst b/doc/source/reference/index.rst index 92e93855a..272b64ada 100644 --- a/doc/source/reference/index.rst +++ b/doc/source/reference/index.rst @@ -1,8 +1,8 @@ +========= Reference ========= .. toctree:: - :maxdepth: 1 + :maxdepth: 6 - api/index - deprecation-policy + api/modules diff --git a/doc/source/user/index.rst b/doc/source/user/index.rst index 3c54920b2..32510e676 100644 --- a/doc/source/user/index.rst +++ b/doc/source/user/index.rst @@ -6,3 +6,4 @@ :maxdepth: 2 shell + python-api diff --git a/doc/source/reference/api/index.rst b/doc/source/user/python-api.rst similarity index 97% rename from doc/source/reference/api/index.rst rename to doc/source/user/python-api.rst index d4959400a..b01e07f11 100644 --- a/doc/source/reference/api/index.rst +++ b/doc/source/user/python-api.rst @@ -101,9 +101,4 @@ Then call methods on its managers:: Reference --------- -For more information, see the reference: - -.. toctree:: - :maxdepth: 6 - - modules +See :doc:`the module reference `. diff --git a/doc/source/user/shell.rst b/doc/source/user/shell.rst index 882bb7560..465b0fb4d 100644 --- a/doc/source/user/shell.rst +++ b/doc/source/user/shell.rst @@ -87,12 +87,4 @@ From there, all shell commands take the form:: Run :program:`nova help` to get a full list of all possible commands, and run :program:`nova help ` to get detailed help for that command. -Reference ---------- - -For more information, see the reference: - -.. toctree:: - :maxdepth: 2 - - /cli/nova +For more information, see :doc:`the command reference `. diff --git a/tox.ini b/tox.ini index dec23521e..ce99fb0b7 100644 --- a/tox.ini +++ b/tox.ini @@ -9,6 +9,7 @@ usedevelop = True whitelist_externals = find rm + make passenv = ZUUL_CACHE_DIR REQUIREMENTS_PIP_LOCATION install_command = pip install {opts} {packages} @@ -44,11 +45,20 @@ deps = -r{toxinidir}/requirements.txt -r{toxinidir}/doc/requirements.txt commands = - rm -rf doc/build + rm -rf doc/build/html doc/build/doctrees sphinx-build -W -b html -d doc/build/doctrees doc/source doc/build/html # Test the redirects. This must run after the main docs build whereto doc/build/html/.htaccess doc/test/redirect-tests.txt +[testenv:pdf-docs] +basepython = python3 +envdir = {toxworkdir}/docs +deps = {[testenv:docs]deps} +commands = + rm -rf doc/build/pdf + sphinx-build -W -b latex doc/source doc/build/pdf + make -C doc/build/pdf + [testenv:releasenotes] basepython = python3 deps = From 8744bea0e3ebe5bc4d0d899189bfa0bcdcb0a08f Mon Sep 17 00:00:00 2001 From: zhangbailin Date: Wed, 7 Aug 2019 11:36:10 +0800 Subject: [PATCH 1558/1705] Microversion 2.80: Add user_id/project_id to migration-list API Add ``user_id`` and ``project_id`` to the ``GET /os-migrations`` API, and it can called ``--user-id `` and/or ``--project-id `` by ``nova migration-list`` CLI. Showing the ``user_id`` and ``project_id`` when using api_version>=2.80 with the server-migration-list or server-migration-show APIs. Depends-On: https://review.opendev.org/#/c/674243/ Part of blueprint add-user-id-field-to-the-migrations-table Change-Id: I11343ca265ab2b6b6f46877897d8223ef340c258 --- doc/source/cli/nova.rst | 10 +++ novaclient/__init__.py | 2 +- novaclient/tests/unit/v2/fakes.py | 20 +++++ novaclient/tests/unit/v2/test_migrations.py | 57 ++++++++++++++ novaclient/tests/unit/v2/test_shell.py | 74 +++++++++++++++++++ novaclient/v2/migrations.py | 47 +++++++++++- novaclient/v2/shell.py | 43 +++++++++-- .../microversion-v2_80-c2394316f9212865.yaml | 20 +++++ 8 files changed, 263 insertions(+), 10 deletions(-) create mode 100644 releasenotes/notes/microversion-v2_80-c2394316f9212865.yaml diff --git a/doc/source/cli/nova.rst b/doc/source/cli/nova.rst index cada3d62e..b94e52afb 100644 --- a/doc/source/cli/nova.rst +++ b/doc/source/cli/nova.rst @@ -2516,6 +2516,8 @@ nova migration-list [--limit ] [--changes-since ] [--changes-before ] + [--project-id ] + [--user-id ] Print a list of migrations. @@ -2573,6 +2575,14 @@ To see the list of evacuation operations *from* a compute service host: point of time. The provided time should be an ISO 8061 formatted time. e.g. 2016-03-04T06:27:59Z . (Supported by API versions '2.66' - '2.latest') +``--project-id `` + Filter the migrations by the given project ID. + (Supported by API versions '2.80' - '2.latest') + +``--user-id `` + Filter the migrations by the given user ID. + (Supported by API versions '2.80' - '2.latest') + .. _nova_pause: nova pause diff --git a/novaclient/__init__.py b/novaclient/__init__.py index a4f9b4eec..c4f56fba0 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.79") +API_MAX_VERSION = api_versions.APIVersion("2.80") diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 3430679be..6302c116a 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -2288,6 +2288,14 @@ def get_os_migrations(self, **kw): migration1.update({"uuid": "11111111-07d5-11e1-90e3-e3dffe0c5983"}) migration2.update({"uuid": "22222222-07d5-11e1-90e3-e3dffe0c5983"}) + if self.api_version >= api_versions.APIVersion("2.80"): + migration1.update({ + "project_id": "b59c18e5fa284fd384987c5cb25a1853", + "user_id": "13cc0930d27c4be0acc14d7c47a3e1f7"}) + migration2.update({ + "project_id": "b59c18e5fa284fd384987c5cb25a1853", + "user_id": "13cc0930d27c4be0acc14d7c47a3e1f7"}) + migration_list = [] instance_uuid = kw.get('instance_uuid', None) if instance_uuid == migration1['instance_uuid']: @@ -2379,6 +2387,12 @@ def get_servers_1234_migrations_1(self, **kw): "disk_remaining_bytes": 230000, "updated_at": "2016-01-29T13:42:02.000000" }} + + if self.api_version >= api_versions.APIVersion("2.80"): + migration['migration'].update({ + "project_id": "b59c18e5fa284fd384987c5cb25a1853", + "user_id": "13cc0930d27c4be0acc14d7c47a3e1f7"}) + return (200, FAKE_RESPONSE_HEADERS, migration) @api_versions.wraps(start_version="2.23") @@ -2402,6 +2416,12 @@ def get_servers_1234_migrations(self, **kw): "disk_remaining_bytes": 230000, "updated_at": "2016-01-29T13:42:02.000000" }]} + + if self.api_version >= api_versions.APIVersion("2.80"): + migrations['migrations'][0].update({ + "project_id": "b59c18e5fa284fd384987c5cb25a1853", + "user_id": "13cc0930d27c4be0acc14d7c47a3e1f7"}) + return (200, FAKE_RESPONSE_HEADERS, migrations) def delete_servers_1234_migrations_1(self): diff --git a/novaclient/tests/unit/v2/test_migrations.py b/novaclient/tests/unit/v2/test_migrations.py index fcafa6d29..09d09405c 100644 --- a/novaclient/tests/unit/v2/test_migrations.py +++ b/novaclient/tests/unit/v2/test_migrations.py @@ -10,6 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. +import six + from novaclient import api_versions from novaclient.tests.unit import utils from novaclient.tests.unit.v2 import fakes @@ -112,3 +114,58 @@ def test_list_migrations_with_changes_before(self): '2012-02-29T06%3A23%3A22') for m in ms: self.assertIsInstance(m, migrations.Migration) + + +class MigrationsV280Test(MigrationsV266Test): + def setUp(self): + super(MigrationsV280Test, self).setUp() + self.cs.api_version = api_versions.APIVersion("2.80") + + def test_list_migrations_with_user_id(self): + user_id = '13cc0930d27c4be0acc14d7c47a3e1f7' + params = {'user_id': user_id} + ms = self.cs.migrations.list(**params) + self.assert_request_id(ms, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called('GET', '/os-migrations?user_id=%s' % user_id) + for m in ms: + self.assertIsInstance(m, migrations.Migration) + + def test_list_migrations_with_project_id(self): + project_id = 'b59c18e5fa284fd384987c5cb25a1853' + params = {'project_id': project_id} + ms = self.cs.migrations.list(**params) + self.assert_request_id(ms, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called('GET', '/os-migrations?project_id=%s' + % project_id) + for m in ms: + self.assertIsInstance(m, migrations.Migration) + + def test_list_migrations_with_user_and_project_id(self): + user_id = '13cc0930d27c4be0acc14d7c47a3e1f7' + project_id = 'b59c18e5fa284fd384987c5cb25a1853' + params = {'user_id': user_id, 'project_id': project_id} + ms = self.cs.migrations.list(**params) + self.assert_request_id(ms, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called('GET', + '/os-migrations?project_id=%s&user_id=%s' + % (project_id, user_id)) + for m in ms: + self.assertIsInstance(m, migrations.Migration) + + def test_list_migrations_with_user_id_pre_v280(self): + self.cs.api_version = api_versions.APIVersion('2.79') + user_id = '13cc0930d27c4be0acc14d7c47a3e1f7' + ex = self.assertRaises(TypeError, + self.cs.migrations.list, + user_id=user_id) + self.assertIn("unexpected keyword argument 'user_id'", + six.text_type(ex)) + + def test_list_migrations_with_project_id_pre_v280(self): + self.cs.api_version = api_versions.APIVersion('2.79') + project_id = '23cc0930d27c4be0acc14d7c47a3e1f7' + ex = self.assertRaises(TypeError, + self.cs.migrations.list, + project_id=project_id) + self.assertIn("unexpected keyword argument 'project_id'", + six.text_type(ex)) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 06b27ad39..79118afcc 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2955,11 +2955,39 @@ def test_list_migrations(self): api_version='2.23') self.assert_called('GET', '/servers/1234/migrations') + def test_list_migrations_pre_v280(self): + out = self.run_command('server-migration-list sample-server', + api_version='2.79')[0] + self.assert_called('GET', '/servers/1234/migrations') + self.assertNotIn('User ID', out) + self.assertNotIn('Project ID', out) + + def test_list_migrations_v280(self): + out = self.run_command('server-migration-list sample-server', + api_version='2.80')[0] + self.assert_called('GET', '/servers/1234/migrations') + self.assertIn('User ID', out) + self.assertIn('Project ID', out) + def test_get_migration(self): self.run_command('server-migration-show sample-server 1', api_version='2.23') self.assert_called('GET', '/servers/1234/migrations/1') + def test_get_migration_pre_v280(self): + out = self.run_command('server-migration-show sample-server 1', + api_version='2.79')[0] + self.assert_called('GET', '/servers/1234/migrations/1') + self.assertNotIn('user_id', out) + self.assertNotIn('project_id', out) + + def test_get_migration_v280(self): + out = self.run_command('server-migration-show sample-server 1', + api_version='2.80')[0] + self.assert_called('GET', '/servers/1234/migrations/1') + self.assertIn('user_id', out) + self.assertIn('project_id', out) + def test_live_migration_abort(self): self.run_command('live-migration-abort sample-server 1', api_version='2.24') @@ -4063,6 +4091,52 @@ def test_migration_list_with_changes_before_pre_v266_not_allowed(self): self.assertRaises(SystemExit, self.run_command, cmd, api_version='2.65') + def test_migration_list_with_user_id_v280(self): + user_id = '13cc0930d27c4be0acc14d7c47a3e1f7' + out = self.run_command('migration-list --user-id %s' % user_id, + api_version='2.80')[0] + self.assert_called('GET', '/os-migrations?user_id=%s' % user_id) + self.assertIn('User ID', out) + self.assertIn('Project ID', out) + + def test_migration_list_with_project_id_v280(self): + project_id = 'b59c18e5fa284fd384987c5cb25a1853' + out = self.run_command('migration-list --project-id %s' % project_id, + api_version='2.80')[0] + self.assert_called('GET', '/os-migrations?project_id=%s' % project_id) + self.assertIn('User ID', out) + self.assertIn('Project ID', out) + + def test_migration_list_with_user_and_project_id_v280(self): + user_id = '13cc0930d27c4be0acc14d7c47a3e1f7' + project_id = 'b59c18e5fa284fd384987c5cb25a1853' + out = self.run_command('migration-list --project-id %(project_id)s ' + '--user-id %(user_id)s' % + {'user_id': user_id, 'project_id': project_id}, + api_version='2.80')[0] + self.assert_called('GET', '/os-migrations?project_id=%s&user_id=%s' + % (project_id, user_id)) + self.assertIn('User ID', out) + self.assertIn('Project ID', out) + + def test_migration_list_with_user_id_pre_v280_not_allowed(self): + user_id = '13cc0930d27c4be0acc14d7c47a3e1f7' + cmd = 'migration-list --user-id %s' % user_id + self.assertRaises(SystemExit, self.run_command, cmd, + api_version='2.79') + + def test_migration_list_with_project_id_pre_v280_not_allowed(self): + project_id = 'b59c18e5fa284fd384987c5cb25a1853' + cmd = 'migration-list --project-id %s' % project_id + self.assertRaises(SystemExit, self.run_command, cmd, + api_version='2.79') + + def test_migration_list_pre_v280(self): + out = self.run_command('migration-list', api_version='2.79')[0] + self.assert_called('GET', '/os-migrations') + self.assertNotIn('User ID', out) + self.assertNotIn('Project ID', out) + @mock.patch('novaclient.v2.shell._find_server') @mock.patch('os.system') def test_ssh(self, mock_system, mock_find_server): diff --git a/novaclient/v2/migrations.py b/novaclient/v2/migrations.py index 3aecd1e7e..1fe764b5e 100644 --- a/novaclient/v2/migrations.py +++ b/novaclient/v2/migrations.py @@ -29,7 +29,7 @@ class MigrationManager(base.ManagerWithFind): def _list_base(self, host=None, status=None, instance_uuid=None, marker=None, limit=None, changes_since=None, changes_before=None, migration_type=None, - source_compute=None): + source_compute=None, user_id=None, project_id=None): opts = {} if host: opts['host'] = host @@ -49,6 +49,10 @@ def _list_base(self, host=None, status=None, instance_uuid=None, opts['migration_type'] = migration_type if source_compute: opts['source_compute'] = source_compute + if user_id: + opts['user_id'] = user_id + if project_id: + opts['project_id'] = project_id return self._list("/os-migrations", "migrations", filters=opts) @@ -99,7 +103,7 @@ def list(self, host=None, status=None, instance_uuid=None, migration_type=migration_type, source_compute=source_compute) - @api_versions.wraps("2.66") + @api_versions.wraps("2.66", "2.79") def list(self, host=None, status=None, instance_uuid=None, marker=None, limit=None, changes_since=None, changes_before=None, migration_type=None, source_compute=None): @@ -132,3 +136,42 @@ def list(self, host=None, status=None, instance_uuid=None, changes_before=changes_before, migration_type=migration_type, source_compute=source_compute) + + @api_versions.wraps("2.80") + def list(self, host=None, status=None, instance_uuid=None, + marker=None, limit=None, changes_since=None, + changes_before=None, migration_type=None, + source_compute=None, user_id=None, project_id=None): + """ + Get a list of migrations. + :param host: filter migrations by host name (optional). + :param status: filter migrations by status (optional). + :param instance_uuid: filter migrations by instance uuid (optional). + :param marker: Begin returning migrations that appear later in the + migrations list than that represented by this migration UUID + (optional). + :param limit: maximum number of migrations to return (optional). + Note the API server has a configurable default limit. If no limit is + specified here or limit is larger than default, the default limit will + be used. + :param changes_since: Only return migrations changed later or equal + to a certain point of time. The provided time should be an ISO 8061 + formatted time. e.g. 2016-03-04T06:27:59Z . (optional). + :param changes_before: Only return migrations changed earlier or + equal to a certain point of time. The provided time should be an ISO + 8061 formatted time. e.g. 2016-03-05T06:27:59Z . (optional). + :param migration_type: Filter migrations by type. Valid values are: + evacuation, live-migration, migration, resize + :param source_compute: Filter migrations by source compute host name. + :param user_id: filter migrations by user (optional). + :param project_id: filter migrations by project (optional). + """ + return self._list_base(host=host, status=status, + instance_uuid=instance_uuid, + marker=marker, limit=limit, + changes_since=changes_since, + changes_before=changes_before, + migration_type=migration_type, + source_compute=source_compute, + user_id=user_id, + project_id=project_id) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index b656babaa..2125ea992 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -3597,6 +3597,10 @@ def do_server_migration_list(cs, args): "memory_remaining_bytes", "disk_total_bytes", "disk_processed_bytes", "disk_remaining_bytes"] + if cs.api_version >= api_versions.APIVersion("2.80"): + fields.append("Project ID") + fields.append("User ID") + formatters = map(lambda field: utils.make_field_formatter(field)[1], format_key) formatters = dict(zip(format_name, formatters)) @@ -5391,6 +5395,10 @@ def migration_type(migration): fields.append("Type") formatters.update({"Type": migration_type}) + if cs.api_version >= api_versions.APIVersion("2.80"): + fields.append("Project ID") + fields.append("User ID") + utils.print_list(migrations, fields, formatters) @@ -5564,6 +5572,20 @@ def do_migration_list(cs, args): 'of time. The provided time should be an ISO 8061 formatted time. ' 'e.g. 2016-03-04T06:27:59Z .'), start_version="2.66") +@utils.arg( + '--project-id', + dest='project_id', + metavar='', + default=None, + help=_('Filter the migrations by the given project ID.'), + start_version='2.80') +@utils.arg( + '--user-id', + dest='user_id', + metavar='', + default=None, + help=_('Filter the migrations by the given user ID.'), + start_version='2.80') def do_migration_list(cs, args): """Print a list of migrations.""" if args.changes_since: @@ -5580,13 +5602,20 @@ def do_migration_list(cs, args): raise exceptions.CommandError(_('Invalid changes-before value: %s') % args.changes_before) - migrations = cs.migrations.list(args.host, args.status, - instance_uuid=args.instance_uuid, - marker=args.marker, limit=args.limit, - changes_since=args.changes_since, - changes_before=args.changes_before, - migration_type=args.migration_type, - source_compute=args.source_compute) + kwargs = dict( + instance_uuid=args.instance_uuid, + marker=args.marker, + limit=args.limit, + changes_since=args.changes_since, + changes_before=args.changes_before, + migration_type=args.migration_type, + source_compute=args.source_compute) + + if cs.api_version >= api_versions.APIVersion('2.80'): + kwargs['project_id'] = args.project_id + kwargs['user_id'] = args.user_id + + migrations = cs.migrations.list(args.host, args.status, **kwargs) _print_migrations(cs, migrations) diff --git a/releasenotes/notes/microversion-v2_80-c2394316f9212865.yaml b/releasenotes/notes/microversion-v2_80-c2394316f9212865.yaml new file mode 100644 index 000000000..cace47d18 --- /dev/null +++ b/releasenotes/notes/microversion-v2_80-c2394316f9212865.yaml @@ -0,0 +1,20 @@ +--- +features: + - | + Added support for `microversion 2.80`_ which adds ``user_id`` + and ``project_id`` filter parameters to the ``GET /os-migrations`` API. + + New kwargs ``project_id`` and ``user_id`` have been added to + the following python API binding: + + - novaclient.v2.migrations.MigrationManager.list + + The following CLI changes have been made: + + - The ``--project-id`` and ``--user-id`` options are added to the + ``nova migration-list`` CLI. + - The ``nova server-migration-list`` and ``nova server-migration-show`` + commands will show the ``Project ID`` and ``User ID`` values when + using microversion 2.80 or greater. + + .. _microversion 2.80: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id72 From f420c058513018132f36a743dcea069e4925370e Mon Sep 17 00:00:00 2001 From: zhangbailin Date: Tue, 15 Oct 2019 14:49:20 +0800 Subject: [PATCH 1559/1705] Add functional test for migration-list in v2.80 In microversion 2.80 add the functional test for the `nova migration-list` CLI. part of bp add-user-id-field-to-the-migrations-table Change-Id: I3cfe37b412971dfff4b217fc7cc6cfdf1b118ce0 --- novaclient/tests/functional/v2/test_migrations.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/novaclient/tests/functional/v2/test_migrations.py b/novaclient/tests/functional/v2/test_migrations.py index 855d8ce5d..34a036955 100644 --- a/novaclient/tests/functional/v2/test_migrations.py +++ b/novaclient/tests/functional/v2/test_migrations.py @@ -41,6 +41,8 @@ def test_migration_list(self): # Find the source compute by getting OS-EXT-SRV-ATTR:host from the # nova show output. server = self.nova('show', params='%s' % server_id) + server_user_id = self._get_value_from_the_table(server, 'user_id') + tenant_id = self._get_value_from_the_table(server, 'tenant_id') source_compute = self._get_value_from_the_table( server, 'OS-EXT-SRV-ATTR:host') # now resize up @@ -97,3 +99,14 @@ def test_migration_list(self): migrations = self._filter_migrations( '2.66', 'resize', uuidutils.generate_uuid()) self.assertNotIn(server_id, migrations) + + # Listing migrations with v2.80 and make sure there are the User ID + # and Project ID values in the output. + migrations = self.nova('migration-list', + flags='--os-compute-api-version 2.80') + user_id = self._get_column_value_from_single_row_table( + migrations, 'User ID') + self.assertEqual(server_user_id, user_id) + project_id = self._get_column_value_from_single_row_table( + migrations, 'Project ID') + self.assertEqual(tenant_id, project_id) From e1bb4378db1e5be85a59861c2d473388fa336da8 Mon Sep 17 00:00:00 2001 From: Dan Smith Date: Fri, 18 Oct 2019 07:37:19 -0700 Subject: [PATCH 1560/1705] Add images.GlanceManager.find_images() bulk query This adds a bulk query method to GlanceManager that takes multiple image names or ids and attempts to query glance for all of them in fewer requests than one-per-image. Change-Id: I1c6ce27b65920356df6b7001c581a2922765ce0c --- novaclient/tests/unit/v2/fakes.py | 13 +++++++-- novaclient/tests/unit/v2/test_shell.py | 24 +++++++++++++++++ novaclient/v2/images.py | 37 ++++++++++++++++++++++++++ novaclient/v2/shell.py | 8 ++++++ 4 files changed, 80 insertions(+), 2 deletions(-) diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 6302c116a..4cb4025ea 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -1141,7 +1141,7 @@ def post_flavors_2_action(self, body, **kw): # Images # def get_images(self, **kw): - return (200, {}, {'images': [ + images = [ { "id": FAKE_IMAGE_UUID_SNAPSHOT, "name": "My Server Backup", @@ -1191,7 +1191,16 @@ def get_images(self, **kw): "progress": 80, "links": {}, }, - ]}) + ] + + if 'id' in kw: + requested = kw['id'].replace('in:', '').split(',') + images = [i for i in images if i['id'] in requested] + if 'names' in kw: + requested = kw['names'].replace('in:', '').split(',') + images = [i for i in images if i['name'] in requested] + + return (200, {}, {'images': images}) def get_images_555cae93_fb41_4145_9c52_f5b923538a26(self, **kw): return (200, {}, {'image': self.get_images()[2]['images'][0]}) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 79118afcc..56fa3544b 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -4676,3 +4676,27 @@ def test_error_state(self, mock_time): action=action, show_progress=True, silent=False) + + +class TestUtilMethods(utils.TestCase): + def setUp(self): + super(TestUtilMethods, self).setUp() + self.shell = self.useFixture(ShellFixture()).shell + # NOTE(danms): Get a client that we can use to call things outside of + # the shell main + self.shell.cs = fakes.FakeClient('2.1') + + def test_find_images(self): + """Test find_images() with a name and id.""" + images = novaclient.v2.shell._find_images(self.shell.cs, + [FAKE_UUID_1, + 'back1']) + self.assertEqual(2, len(images)) + self.assertEqual(FAKE_UUID_1, images[0].id) + self.assertEqual(fakes.FAKE_IMAGE_UUID_BACKUP, images[1].id) + + def test_find_images_missing(self): + """Test find_images() where one of the images is not found.""" + self.assertRaises(exceptions.CommandError, + novaclient.v2.shell._find_images, + self.shell.cs, [FAKE_UUID_1, 'foo']) diff --git a/novaclient/v2/images.py b/novaclient/v2/images.py index e83d9b2fe..0aced5c7a 100644 --- a/novaclient/v2/images.py +++ b/novaclient/v2/images.py @@ -66,6 +66,43 @@ def find_image(self, name_or_id): matches[0].append_request_ids(matches.request_ids) return matches[0] + def find_images(self, names_or_ids): + """Find multiple images by name or id (user provided input). + + :param names_or_ids: A list of strings to use to find images. + :returns: novaclient.v2.images.Image objects for each images found + :raises exceptions.NotFound: If one or more images is not found + :raises exceptions.ClientException: If the image service returns any + unexpected images. + + NOTE: This method always makes two calls to the image service, even if + only one image is provided by ID and is returned in the first query. + """ + with self.alternate_service_type( + 'image', allowed_types=('image',)): + matches = self._list('/v2/images?id=in:%s' % ','.join( + names_or_ids), 'images') + matches.extend(self._list('/v2/images?names=in:%s' % ','.join( + names_or_ids), 'images')) + missed = (set(names_or_ids) - + set(m.name for m in matches) - + set(m.id for m in matches)) + if missed: + msg = _("Unable to find image(s): %(images)s") % { + "images": ",".join(missed)} + raise exceptions.NotFound(404, msg) + for match in matches: + match.append_request_ids(matches.request_ids) + + additional = [] + for i in matches: + if i.name not in names_or_ids and i.id not in names_or_ids: + additional.append(i) + if additional: + msg = _('Additional images found in response') + raise exceptions.ClientException(500, msg) + return matches + def list(self): """ Get a detailed list of all images. diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 2125ea992..818006b98 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -2570,6 +2570,14 @@ def _find_image(cs, image): raise exceptions.CommandError(six.text_type(e)) +def _find_images(cs, images): + """Get images by name or ID.""" + try: + return cs.glance.find_images(images) + except (exceptions.NotFound, exceptions.NoUniqueMatch) as e: + raise exceptions.CommandError(six.text_type(e)) + + def _find_flavor(cs, flavor): """Get a flavor by name, ID, or RAM size.""" try: From 71c29a184b388b38d81dc609b9883c5e8f7166cf Mon Sep 17 00:00:00 2001 From: Dan Smith Date: Mon, 7 Oct 2019 11:23:28 -0700 Subject: [PATCH 1561/1705] Add aggregate-cache-images command and client routines This adds the ability to request image precache support for an aggregate in support of the matching server feature. Related to blueprint image-precache-support Depends-On: https://review.opendev.org/#/c/687140 Change-Id: Id354ccfa99e500a598685e6b794c12160ea2a990 --- doc/source/cli/nova.rst | 22 ++++++++++ novaclient/__init__.py | 2 +- .../tests/unit/fixture_data/aggregates.py | 7 ++++ novaclient/tests/unit/v2/fakes.py | 3 ++ novaclient/tests/unit/v2/test_aggregates.py | 40 +++++++++++++++++++ novaclient/tests/unit/v2/test_shell.py | 24 +++++++++++ novaclient/v2/aggregates.py | 22 ++++++++++ novaclient/v2/shell.py | 15 +++++++ .../microversion-v2_81-3ddd8e2fc7e45030.yaml | 10 +++++ 9 files changed, 144 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/microversion-v2_81-3ddd8e2fc7e45030.yaml diff --git a/doc/source/cli/nova.rst b/doc/source/cli/nova.rst index b94e52afb..fd8dbc4d4 100644 --- a/doc/source/cli/nova.rst +++ b/doc/source/cli/nova.rst @@ -73,6 +73,9 @@ nova usage ``aggregate-add-host`` Add the host to the specified aggregate. +``aggregate-cache-images`` + Request images be pre-cached on hosts within an aggregate. + ``aggregate-create`` Create a new aggregate with the specified details. @@ -756,6 +759,25 @@ Add the host to the specified aggregate. ```` The host to add to the aggregate. +.. _nova_aggregate-cache-images: + +nova aggregate-cache-images +--------------------------- + +.. code-block:: console + + usage: nova aggregate-cache-images [ ..] + +Request image(s) be pre-cached on hosts within the aggregate. + +**Positional arguments:** + +```` + Name or ID of aggregate. + +```` + Name or ID of image(s) to cache. + .. _nova_aggregate-create: nova aggregate-create diff --git a/novaclient/__init__.py b/novaclient/__init__.py index c4f56fba0..6855e2f21 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.80") +API_MAX_VERSION = api_versions.APIVersion("2.81") diff --git a/novaclient/tests/unit/fixture_data/aggregates.py b/novaclient/tests/unit/fixture_data/aggregates.py index 3ea64b8b7..b3ab88c5f 100644 --- a/novaclient/tests/unit/fixture_data/aggregates.py +++ b/novaclient/tests/unit/fixture_data/aggregates.py @@ -51,3 +51,10 @@ def setUp(self): self.requests_mock.delete(self.url(1), status_code=202, headers=self.json_headers) + + self.requests_mock.register_uri('POST', self.url(1), + json={}, + headers=self.json_headers) + self.requests_mock.post(self.url(1, 'images'), + json={}, + headers=self.json_headers) diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 4cb4025ea..62d5e727e 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -1734,6 +1734,9 @@ def post_os_aggregates_3_action(self, body, **kw): def delete_os_aggregates_1(self, **kw): return (202, {}, None) + def post_os_aggregates_1_images(self, body, **kw): + return (202, {}, None) + # # Services # diff --git a/novaclient/tests/unit/v2/test_aggregates.py b/novaclient/tests/unit/v2/test_aggregates.py index 1de128238..4f3eecdf5 100644 --- a/novaclient/tests/unit/v2/test_aggregates.py +++ b/novaclient/tests/unit/v2/test_aggregates.py @@ -13,11 +13,14 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient import api_versions +from novaclient import exceptions from novaclient.tests.unit.fixture_data import aggregates as data from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit import utils from novaclient.tests.unit.v2 import fakes from novaclient.v2 import aggregates +from novaclient.v2 import images class AggregatesTest(utils.FixturedTestCase): @@ -161,3 +164,40 @@ def test_delete_aggregate(self): result3 = self.cs.aggregates.delete(aggregate) self.assert_request_id(result3, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('DELETE', '/os-aggregates/1') + + +class AggregatesV281Test(utils.FixturedTestCase): + api_version = "2.81" + data_fixture_class = data.Fixture + + scenarios = [('original', {'client_fixture_class': client.V1}), + ('session', {'client_fixture_class': client.SessionV1})] + + def setUp(self): + super(AggregatesV281Test, self).setUp() + self.cs.api_version = api_versions.APIVersion(self.api_version) + + def test_cache_images(self): + aggregate = self.cs.aggregates.list()[0] + _images = [images.Image(self.cs.aggregates, {'id': '1'}), + images.Image(self.cs.aggregates, {'id': '2'})] + aggregate.cache_images(_images) + expected_body = {'cache': [{'id': image.id} + for image in _images]} + self.assert_called('POST', '/os-aggregates/1/images', + expected_body) + + def test_cache_images_just_ids(self): + aggregate = self.cs.aggregates.list()[0] + _images = ['1'] + aggregate.cache_images(_images) + expected_body = {'cache': [{'id': '1'}]} + self.assert_called('POST', '/os-aggregates/1/images', + expected_body) + + def test_cache_images_pre281(self): + self.cs.api_version = api_versions.APIVersion('2.80') + aggregate = self.cs.aggregates.list()[0] + _images = [images.Image(self.cs.aggregates, {'id': '1'})] + self.assertRaises(exceptions.VersionNotFoundForAPIMethod, + aggregate.cache_images, _images) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 56fa3544b..b68254a21 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2884,6 +2884,30 @@ def test_aggregate_show_by_name(self): self.run_command('aggregate-show test') self.assert_called('GET', '/os-aggregates') + def test_aggregate_cache_images(self): + self.run_command( + 'aggregate-cache-images 1 %s %s' % ( + FAKE_UUID_1, FAKE_UUID_2), + api_version='2.81') + body = { + 'cache': [{'id': FAKE_UUID_1}, + {'id': FAKE_UUID_2}], + } + self.assert_called('POST', '/os-aggregates/1/images', body) + + def test_aggregate_cache_images_no_images(self): + self.assertRaises(SystemExit, + self.run_command, + 'aggregate-cache-images 1', + api_version='2.81') + + def test_aggregate_cache_images_pre281(self): + self.assertRaises(SystemExit, + self.run_command, + 'aggregate-cache-images 1 %s %s' % ( + FAKE_UUID_1, FAKE_UUID_2), + api_version='2.80') + def test_live_migration(self): self.run_command('live-migration sample-server hostname') self.assert_called('POST', '/servers/1234/action', diff --git a/novaclient/v2/aggregates.py b/novaclient/v2/aggregates.py index 9d4dff822..d2cbaa858 100644 --- a/novaclient/v2/aggregates.py +++ b/novaclient/v2/aggregates.py @@ -15,6 +15,7 @@ """Aggregate interface.""" +from novaclient import api_versions from novaclient import base @@ -45,6 +46,10 @@ def delete(self): """ return self.manager.delete(self) + @api_versions.wraps("2.81") + def cache_images(self, images): + return self.manager.cache_images(self, images) + class AggregateManager(base.ManagerWithFind): resource_class = Aggregate @@ -103,3 +108,20 @@ def delete(self, aggregate): :returns: An instance of novaclient.base.TupleWithMeta """ return self._delete('/os-aggregates/%s' % (base.getid(aggregate))) + + @api_versions.wraps("2.81") + def cache_images(self, aggregate, images): + """ + Request images be cached on a given aggregate. + + :param aggregate: The aggregate to target + :param images: A list of image IDs to request caching + :returns: An instance of novaclient.base.TupleWithMeta + """ + body = { + 'cache': [{'id': base.getid(image)} for image in images], + } + resp, body = self.api.client.post( + "/os-aggregates/%s/images" % base.getid(aggregate), + body=body) + return self.convert_into_with_meta(body, resp) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 818006b98..0f8b5ce6f 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -3526,6 +3526,21 @@ def parser_hosts(fields): utils.print_list([aggregate], columns, formatters=formatters) +@api_versions.wraps("2.81") +@utils.arg( + 'aggregate', metavar='', + help=_('Name or ID of the aggregate.')) +@utils.arg( + 'images', metavar='', nargs='+', + help=_('Name or ID of image(s) to cache on the hosts within ' + 'the aggregate.')) +def do_aggregate_cache_images(cs, args): + """Request images be cached.""" + aggregate = _find_aggregate(cs, args.aggregate) + images = _find_images(cs, args.images) + cs.aggregates.cache_images(aggregate, images) + + @utils.arg('server', metavar='', help=_('Name or ID of server.')) @utils.arg( 'host', metavar='', default=None, nargs='?', diff --git a/releasenotes/notes/microversion-v2_81-3ddd8e2fc7e45030.yaml b/releasenotes/notes/microversion-v2_81-3ddd8e2fc7e45030.yaml new file mode 100644 index 000000000..a51336d60 --- /dev/null +++ b/releasenotes/notes/microversion-v2_81-3ddd8e2fc7e45030.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + Added support for `microversion 2.81`_ which adds image pre-caching support by + aggregate. + + - The ``aggregate-cache-images`` command is added to the CLI + - The ``cache_images()`` method is added to the python API binding + + .. _microversion 2.81: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id73 From c15a5a055561fbbe622cf3990f192f8e22ae54de Mon Sep 17 00:00:00 2001 From: zhangbailin Date: Tue, 15 Oct 2019 09:52:26 +0800 Subject: [PATCH 1562/1705] Add minor version [21] to the test_versions In v2.21, the os-instance-actions API returns information from deleted instances. This change does not involve changes to novaclient, but since novaclient does not add support for v2.21, it does not match API_MAX_VERSION when executing unit tests (novaclient.tests.unit.v2.test_shell.ShellTest.test_versions). Closes-Bug: #1848110 Change-Id: Ib1ad244bb059bb86a9f3025d4bc16c20946433d0 --- novaclient/tests/unit/v2/test_shell.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 06b27ad39..65240a405 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -4313,6 +4313,7 @@ def test_versions(self): # new microversion, just an additional checks. See # https://review.opendev.org/#/c/233076/ for more details) 20, # doesn't require any changes in novaclient + 21, # doesn't require any changes in novaclient 27, # NOTE(cdent): 27 adds support for updated microversion # headers, and is tested in test_api_versions, but is # not explicitly tested via wraps and _SUBSTITUTIONS. @@ -4365,9 +4366,11 @@ def test_versions(self): versions_covered = set() for key, values in api_versions._SUBSTITUTIONS.items(): - for value in values: - if value.start_version.ver_major == 2: - versions_covered.add(value.start_version.ver_minor) + # Exclude version-wrapped + if 'novaclient.tests' not in key: + for value in values: + if value.start_version.ver_major == 2: + versions_covered.add(value.start_version.ver_minor) versions_not_covered = versions_supported - versions_covered unaccounted_for = versions_not_covered - exclusions From cd9958bdb0748aab493fefbf2818ee0d71d8c172 Mon Sep 17 00:00:00 2001 From: kangyufei Date: Tue, 22 Oct 2019 17:04:24 +0800 Subject: [PATCH 1563/1705] Switch to Ussuri jobs Change-Id: I4e04d090e8b0f7f99170c09a450cb1bd12d4acb7 --- .zuul.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.zuul.yaml b/.zuul.yaml index 0880f0569..c44db2efa 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -21,7 +21,7 @@ - openstack-cover-jobs - openstack-lower-constraints-jobs - openstack-python-jobs - - openstack-python3-train-jobs + - openstack-python3-ussuri-jobs - publish-openstack-docs-pti - release-notes-jobs-python3 check: From baccd5bcfa316cec04908e21d5fd95ef52edccfe Mon Sep 17 00:00:00 2001 From: Eric Fried Date: Fri, 25 Oct 2019 09:28:16 -0500 Subject: [PATCH 1564/1705] Stop supporting and testing python2 Change-Id: If678d77b8da69121b0075bfbc4216531be25da6a --- .zuul.yaml | 3 +-- .../novaclient-dsvm-functional/run.yaml | 1 + ...drop-python2-support-d3a1bedc75445edc.yaml | 4 ++++ setup.cfg | 2 -- tox.ini | 20 ++----------------- 5 files changed, 8 insertions(+), 22 deletions(-) create mode 100644 releasenotes/notes/drop-python2-support-d3a1bedc75445edc.yaml diff --git a/.zuul.yaml b/.zuul.yaml index c44db2efa..95eabd355 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -1,4 +1,5 @@ - job: + # TODO(efried): Cut over to zuulv3 name: novaclient-dsvm-functional parent: legacy-dsvm-base run: playbooks/legacy/novaclient-dsvm-functional/run.yaml @@ -16,11 +17,9 @@ - project: templates: - check-requirements - - lib-forward-testing - lib-forward-testing-python3 - openstack-cover-jobs - openstack-lower-constraints-jobs - - openstack-python-jobs - openstack-python3-ussuri-jobs - publish-openstack-docs-pti - release-notes-jobs-python3 diff --git a/playbooks/legacy/novaclient-dsvm-functional/run.yaml b/playbooks/legacy/novaclient-dsvm-functional/run.yaml index a3d1f4979..183d3989c 100644 --- a/playbooks/legacy/novaclient-dsvm-functional/run.yaml +++ b/playbooks/legacy/novaclient-dsvm-functional/run.yaml @@ -27,6 +27,7 @@ cmd: | set -e set -x + export DEVSTACK_GATE_USE_PYTHON3=true export PYTHONUNBUFFERED=true export BRANCH_OVERRIDE=default export DEVSTACK_PROJECT_FROM_GIT=python-novaclient diff --git a/releasenotes/notes/drop-python2-support-d3a1bedc75445edc.yaml b/releasenotes/notes/drop-python2-support-d3a1bedc75445edc.yaml new file mode 100644 index 000000000..9fd7dee19 --- /dev/null +++ b/releasenotes/notes/drop-python2-support-d3a1bedc75445edc.yaml @@ -0,0 +1,4 @@ +--- +upgrade: + - | + Python 2 is no longer supported. Python 3 is required. diff --git a/setup.cfg b/setup.cfg index 42b82ac21..d2048632d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,8 +16,6 @@ classifier = License :: OSI Approved :: Apache Software License Operating System :: OS Independent Programming Language :: Python - Programming Language :: Python :: 2 - Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 diff --git a/tox.ini b/tox.ini index ce99fb0b7..2b9cb62db 100644 --- a/tox.ini +++ b/tox.ini @@ -1,9 +1,10 @@ [tox] -envlist = py27,py37,pep8,docs +envlist = py37,pep8,docs minversion = 2.0 skipsdist = True [testenv] +basepython = python3 usedevelop = True # tox is silly... these need to be separated by a newline.... whitelist_externals = @@ -22,15 +23,12 @@ commands = stestr run {posargs} [testenv:pep8] -basepython = python3 commands = flake8 {posargs} [testenv:bandit] -basepython = python3 commands = bandit -r novaclient -n5 -x tests [testenv:venv] -basepython = python3 deps = -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/test-requirements.txt @@ -39,7 +37,6 @@ deps = commands = {posargs} [testenv:docs] -basepython = python3 deps = -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/requirements.txt @@ -51,7 +48,6 @@ commands = whereto doc/build/html/.htaccess doc/test/redirect-tests.txt [testenv:pdf-docs] -basepython = python3 envdir = {toxworkdir}/docs deps = {[testenv:docs]deps} commands = @@ -60,7 +56,6 @@ commands = make -C doc/build/pdf [testenv:releasenotes] -basepython = python3 deps = -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/requirements.txt @@ -69,21 +64,12 @@ commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html [testenv:functional] -basepython = python2.7 -passenv = OS_NOVACLIENT_TEST_NETWORK -commands = - stestr --test-path=./novaclient/tests/functional run --concurrency=1 {posargs} - python novaclient/tests/functional/hooks/check_resources.py - -[testenv:functional-py36] -basepython = python3.6 passenv = OS_NOVACLIENT_TEST_NETWORK commands = stestr --test-path=./novaclient/tests/functional run --concurrency=1 {posargs} python novaclient/tests/functional/hooks/check_resources.py [testenv:cover] -basepython = python3 setenv = PYTHON=coverage run --source novaclient --parallel-mode commands = @@ -110,7 +96,6 @@ exclude=.venv,.git,.tox,dist,*lib/python*,*egg,build,doc/source/conf.py,releasen import_exceptions = novaclient.i18n [testenv:bindep] -basepython = python3 # Do not install any requirements. We want this to be fast and work even if # system dependencies are missing, since it's used to tell you what system # dependencies are missing! This also means that bindep must be installed @@ -119,7 +104,6 @@ deps = bindep commands = bindep test [testenv:lower-constraints] -basepython = python3 deps = -c{toxinidir}/lower-constraints.txt -r{toxinidir}/test-requirements.txt From ed98fdba412593ff3374e97327f40260d79f76bd Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Wed, 6 Nov 2019 07:49:32 +0900 Subject: [PATCH 1565/1705] doc: Fix supported version descriptions Fix the version that the 'nova server-topology' command was added. Add some missing descriptions about supported versions. Change-Id: I32f9eca87c90211b0172b040649aad105298846b Closes-Bug: #1851443 --- doc/source/cli/nova.rst | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/doc/source/cli/nova.rst b/doc/source/cli/nova.rst index fd8dbc4d4..84c441bd7 100644 --- a/doc/source/cli/nova.rst +++ b/doc/source/cli/nova.rst @@ -75,6 +75,7 @@ nova usage ``aggregate-cache-images`` Request images be pre-cached on hosts within an aggregate. + (Supported by API versions '2.81' - '2.latest') ``aggregate-create`` Create a new aggregate with the specified @@ -466,6 +467,7 @@ nova usage ``server-topology`` Retrieve NUMA topology of the given server. + (Supported by API versions '2.78' - '2.latest') ``service-delete`` Delete the service. @@ -769,6 +771,9 @@ nova aggregate-cache-images usage: nova aggregate-cache-images [ ..] Request image(s) be pre-cached on hosts within the aggregate. +(Supported by API versions '2.81' - '2.latest') + +.. versionadded:: 16.0.0 **Positional arguments:** @@ -3357,7 +3362,7 @@ Retrieve server NUMA topology information. Host specific fields are only visible to users with the administrative role. (Supported by API versions '2.78' - '2.latest') -.. versionadded:: 16.0.0 +.. versionadded:: 15.1.0 **Positional arguments:** From eb98178ea88c77881f7c763b068408f769956eb2 Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Tue, 26 Nov 2019 09:12:26 +0900 Subject: [PATCH 1566/1705] doc: Update Testing document Python 2 support has been dropped since If678d77b8da69121b0075bfbc4216531be25da6a. Update the 'Testing' document. Change-Id: Ic263943b1d6110925b9c9849c01b0d52d41a8351 --- doc/source/contributor/testing.rst | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/doc/source/contributor/testing.rst b/doc/source/contributor/testing.rst index ef1a31c93..021d0ae76 100644 --- a/doc/source/contributor/testing.rst +++ b/doc/source/contributor/testing.rst @@ -8,17 +8,11 @@ test targets that can be run to validate the code. ``tox -e pep8`` Style guidelines enforcement. -``tox -e py27`` - Traditional unit testing (Python 2.7). - -``tox -e py36`` - Traditional unit testing (Python 3.6). +``tox -e py37`` + Traditional unit testing (Python 3.7). ``tox -e functional`` - Live functional testing against an existing OpenStack instance. (Python 2.7) - -``tox -e functional-py36`` - Live functional testing against an existing OpenStack instance. (Python 3.6) + Live functional testing against an existing OpenStack instance. (Python 3.7) ``tox -e cover`` Generate a coverage report on unit testing. From 0f7d723c462ab0c083120f811fba682057f2b081 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 21 Feb 2020 09:43:13 +0000 Subject: [PATCH 1567/1705] trivial: Remove 'u' prefix from string It looks like nova is using 'repr()' in some validation error logging. On Python 2.7, this would result in unicode strings (which most web apps use) being output as "u'foo'", while in Python 3 these would just be "'foo'". Since nova only supports Python 3 now, we only have to support the Python 3 string formatting variant in our tests too. Change-Id: I546c06a3251e86f39e2e7db48e04ec382c8da854 Signed-off-by: Stephen Finucane --- novaclient/tests/functional/v2/test_servers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/tests/functional/v2/test_servers.py b/novaclient/tests/functional/v2/test_servers.py index 030f0f27f..a25dd2aef 100644 --- a/novaclient/tests/functional/v2/test_servers.py +++ b/novaclient/tests/functional/v2/test_servers.py @@ -130,7 +130,7 @@ def test_update_with_description_longer_than_255_symbols(self): output = self.nova("update %s --description '%s'" % (server.id, descr), fail_ok=True, merge_stderr=True) self.assertIn("ERROR (BadRequest): Invalid input for field/attribute" - " description. Value: %s. u\'%s\' is too long (HTTP 400)" + " description. Value: %s. '%s' is too long (HTTP 400)" % (descr, descr), output) From 1e05dec589268fe50e15b07b788a007a5c89fc62 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 20 Feb 2020 09:16:26 +0000 Subject: [PATCH 1568/1705] setup.cfg: Various Python 3 fixes Now that we've dropped Python 2 support, we shouldn't be stating that we support universal (Python 2 and 3) wheels. We should also use the proper setuptools machinery to prevent people accidentally installing novaclient in a Python 2.7 environment. Resolve both issues, removing an unused 'upload_sphinx' section in the process. Change-Id: Icee145f44a42c233008b3328f52a3eec933101e0 Signed-off-by: Stephen Finucane --- setup.cfg | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/setup.cfg b/setup.cfg index d2048632d..aac59e112 100644 --- a/setup.cfg +++ b/setup.cfg @@ -7,6 +7,7 @@ license = Apache License, Version 2.0 author = OpenStack author-email = openstack-discuss@lists.openstack.org home-page = https://docs.openstack.org/python-novaclient/latest +python-requires = >=3.6 classifier = Development Status :: 5 - Production/Stable Environment :: Console @@ -19,6 +20,8 @@ classifier = Programming Language :: Python :: 3 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3 :: Only + Programming Language :: Python :: Implementation :: CPython [files] packages = @@ -28,9 +31,6 @@ packages = console_scripts = nova = novaclient.shell:main -[upload_sphinx] -upload-dir = doc/build/html - [compile_catalog] domain = novaclient directory = novaclient/locale @@ -44,6 +44,3 @@ input_file = novaclient/locale/novaclient.pot keywords = _ gettext ngettext l_ lazy_gettext mapping_file = babel.cfg output_file = novaclient/locale/novaclient.pot - -[wheel] -universal = 1 From 9dee28ae6c32814494031ef503b835d4728d91dc Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 20 Feb 2020 09:56:40 +0000 Subject: [PATCH 1569/1705] tox: Configure 'ignore_basepython_conflict' Resolves the following warning: UserWarning: conflicting basepython version (set 37, should be 36) for env 'py36';resolve conflict or set ignore_basepython_conflict and makes sure we're testing with what we should be. Some random indentation is fixed while we're here. Change-Id: I6f3e98cc33731f528a3a755300f1fcc65c6b53ff Signed-off-by: Stephen Finucane --- tox.ini | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/tox.ini b/tox.ini index 2b9cb62db..a5122f9c0 100644 --- a/tox.ini +++ b/tox.ini @@ -1,19 +1,20 @@ [tox] envlist = py37,pep8,docs -minversion = 2.0 -skipsdist = True +minversion = 3.1 +skipsdist = true +ignore_basepython_conflict = true [testenv] basepython = python3 -usedevelop = True +usedevelop = true # tox is silly... these need to be separated by a newline.... whitelist_externals = find rm make -passenv = ZUUL_CACHE_DIR - REQUIREMENTS_PIP_LOCATION -install_command = pip install {opts} {packages} +passenv = + ZUUL_CACHE_DIR + REQUIREMENTS_PIP_LOCATION deps = -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/test-requirements.txt @@ -71,13 +72,13 @@ commands = [testenv:cover] setenv = - PYTHON=coverage run --source novaclient --parallel-mode + PYTHON=coverage run --source novaclient --parallel-mode commands = - stestr run {posargs} - coverage combine - coverage html -d cover - coverage xml -o cover/coverage.xml - coverage report + stestr run {posargs} + coverage combine + coverage html -d cover + coverage xml -o cover/coverage.xml + coverage report [flake8] # Following checks should be enabled in the future. @@ -89,7 +90,7 @@ commands = # # Additional checks are also ignored on purpose: F811, F821 ignore = E731,F811,F821,H404,H405 -show-source = True +show-source = true exclude=.venv,.git,.tox,dist,*lib/python*,*egg,build,doc/source/conf.py,releasenotes [hacking] From c4c44bcb2df01b77089139b267b1219008f9421e Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 20 Feb 2020 09:42:03 +0000 Subject: [PATCH 1570/1705] Remove six Mostly a find-replace job. Let's do this now so we don't have to carry it for the next decade. Change-Id: I7bef9fb7c6895f746cee1aca6522786f38b9857c Signed-off-by: Stephen Finucane --- lower-constraints.txt | 1 - novaclient/base.py | 25 +- novaclient/crypto.py | 4 +- novaclient/shell.py | 8 +- novaclient/tests/functional/test_auth.py | 3 +- .../functional/v2/legacy/test_hypervisors.py | 4 +- .../functional/v2/test_device_tagging.py | 5 +- .../functional/v2/test_instance_action.py | 3 +- novaclient/tests/unit/fixture_data/base.py | 3 +- .../tests/unit/fixture_data/hypervisors.py | 3 +- novaclient/tests/unit/test_base.py | 12 - novaclient/tests/unit/test_shell.py | 26 +- novaclient/tests/unit/test_utils.py | 44 ++- novaclient/tests/unit/utils.py | 5 +- novaclient/tests/unit/v2/fakes.py | 267 +++++++++--------- .../tests/unit/v2/test_availability_zone.py | 27 +- novaclient/tests/unit/v2/test_hypervisors.py | 4 +- novaclient/tests/unit/v2/test_migrations.py | 8 +- novaclient/tests/unit/v2/test_servers.py | 50 ++-- novaclient/tests/unit/v2/test_shell.py | 52 ++-- novaclient/tests/unit/v2/test_usage.py | 6 +- novaclient/tests/unit/v2/test_volumes.py | 3 +- novaclient/utils.py | 22 +- novaclient/v2/hypervisors.py | 6 +- novaclient/v2/instance_usage_audit_log.py | 6 +- novaclient/v2/servers.py | 27 +- novaclient/v2/services.py | 7 +- novaclient/v2/shell.py | 13 +- novaclient/v2/versions.py | 4 +- requirements.txt | 1 - 30 files changed, 278 insertions(+), 371 deletions(-) diff --git a/lower-constraints.txt b/lower-constraints.txt index b87fcaf0e..72fdac0e6 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -91,7 +91,6 @@ requestsexceptions==1.2.0 rfc3986==0.3.1 Routes==2.3.1 simplejson==3.5.1 -six==1.10.0 smmap==0.9.0 statsd==3.2.1 stevedore==1.20.0 diff --git a/novaclient/base.py b/novaclient/base.py index 821e19bd9..48c06f372 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -29,7 +29,6 @@ from oslo_utils import reflection from oslo_utils import strutils import requests -import six from novaclient import exceptions from novaclient import utils @@ -388,12 +387,9 @@ def _update(self, url, body, response_key=None, **kwargs): return StrWithMeta(body, resp) def convert_into_with_meta(self, item, resp): - if isinstance(item, six.string_types): - if six.PY2 and isinstance(item, six.text_type): - return UnicodeWithMeta(item, resp) - else: - return StrWithMeta(item, resp) - elif isinstance(item, six.binary_type): + if isinstance(item, str): + return StrWithMeta(item, resp) + elif isinstance(item, bytes): return BytesWithMeta(item, resp) elif isinstance(item, list): return ListWithMeta(item, resp) @@ -405,8 +401,7 @@ def convert_into_with_meta(self, item, resp): return DictWithMeta(item, resp) -@six.add_metaclass(abc.ABCMeta) -class ManagerWithFind(Manager): +class ManagerWithFind(Manager, metaclass=abc.ABCMeta): """Like a `Manager`, but with additional `find()`/`findall()` methods.""" @abc.abstractmethod @@ -560,20 +555,10 @@ def __init__(self, values, resp): self.append_request_ids(resp) -class BytesWithMeta(six.binary_type, RequestIdMixin): +class BytesWithMeta(bytes, RequestIdMixin): def __new__(cls, value, resp): return super(BytesWithMeta, cls).__new__(cls, value) def __init__(self, values, resp): self.request_ids_setup() self.append_request_ids(resp) - - -if six.PY2: - class UnicodeWithMeta(six.text_type, RequestIdMixin): - def __new__(cls, value, resp): - return super(UnicodeWithMeta, cls).__new__(cls, value) - - def __init__(self, values, resp): - self.request_ids_setup() - self.append_request_ids(resp) diff --git a/novaclient/crypto.py b/novaclient/crypto.py index 699dbac0e..527bc82a6 100644 --- a/novaclient/crypto.py +++ b/novaclient/crypto.py @@ -16,8 +16,6 @@ import base64 import subprocess -import six - class DecryptionFailure(Exception): pass @@ -38,6 +36,6 @@ def decrypt_password(private_key, password): if proc.returncode: raise DecryptionFailure(err) - if not six.PY2 and isinstance(out, bytes): + if isinstance(out, bytes): return out.decode('utf-8') return out diff --git a/novaclient/shell.py b/novaclient/shell.py index c82cd506c..a5d1e9377 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -27,7 +27,6 @@ from oslo_utils import encodeutils from oslo_utils import importutils from oslo_utils import strutils -import six import novaclient from novaclient import api_versions @@ -123,7 +122,7 @@ def __init__(self, option_strings, dest, help=None, # option self.real_action_args = False self.real_action = None - elif real_action is None or isinstance(real_action, six.string_types): + elif real_action is None or isinstance(real_action, str): # Specified by string (or None); we have to have a parser # to look up the actual action, so defer to later self.real_action_args = (option_strings, dest, help, kwargs) @@ -810,10 +809,7 @@ def main(): OpenStackComputeShell().main(argv) except Exception as exc: logger.debug(exc, exc_info=1) - if six.PY2: - message = encodeutils.safe_encode(six.text_type(exc)) - else: - message = encodeutils.exception_to_unicode(exc) + message = encodeutils.exception_to_unicode(exc) print("ERROR (%(type)s): %(msg)s" % { 'type': exc.__class__.__name__, 'msg': message}, diff --git a/novaclient/tests/functional/test_auth.py b/novaclient/tests/functional/test_auth.py index 74fa7fb19..8987c7289 100644 --- a/novaclient/tests/functional/test_auth.py +++ b/novaclient/tests/functional/test_auth.py @@ -10,7 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. -from six.moves.urllib import parse +from urllib import parse + import tempest.lib.cli.base from novaclient import client diff --git a/novaclient/tests/functional/v2/legacy/test_hypervisors.py b/novaclient/tests/functional/v2/legacy/test_hypervisors.py index ecc102dc0..621401f9b 100644 --- a/novaclient/tests/functional/v2/legacy/test_hypervisors.py +++ b/novaclient/tests/functional/v2/legacy/test_hypervisors.py @@ -10,8 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import six - from novaclient.tests.functional import base from novaclient import utils @@ -41,4 +39,4 @@ def _test_list(self, cpu_info_type, uuid_as_id=False): 'Expected hypervisor.service.id to be an integer.') def test_list(self): - self._test_list(six.text_type) + self._test_list(str) diff --git a/novaclient/tests/functional/v2/test_device_tagging.py b/novaclient/tests/functional/v2/test_device_tagging.py index 5bb900e74..5909137ea 100644 --- a/novaclient/tests/functional/v2/test_device_tagging.py +++ b/novaclient/tests/functional/v2/test_device_tagging.py @@ -12,7 +12,6 @@ # License for the specific language governing permissions and limitations # under the License. -import six from tempest.lib import exceptions from novaclient.tests.functional import base @@ -40,7 +39,7 @@ def test_boot_server_with_tagged_block_devices_with_error(self): self.assertIn("ERROR (CommandError): " "'tag' in block device mapping is not supported " "in API version %s." % self.COMPUTE_API_VERSION, - six.text_type(e)) + str(e)) else: server_id = self._get_value_from_the_table(output, 'id') self.client.servers.delete(server_id) @@ -67,7 +66,7 @@ def test_boot_server_with_tagged_nic_devices_with_error(self): 'net-uuid': self.network.id, 'image': self.image.id})) except exceptions.CommandFailed as e: - self.assertIn("Invalid nic argument", six.text_type(e)) + self.assertIn('Invalid nic argument', str(e)) else: server_id = self._get_value_from_the_table(output, 'id') self.client.servers.delete(server_id) diff --git a/novaclient/tests/functional/v2/test_instance_action.py b/novaclient/tests/functional/v2/test_instance_action.py index b1e884c5a..45849b95b 100644 --- a/novaclient/tests/functional/v2/test_instance_action.py +++ b/novaclient/tests/functional/v2/test_instance_action.py @@ -14,7 +14,6 @@ from oslo_utils import timeutils from oslo_utils import uuidutils -import six from tempest.lib import exceptions from novaclient.tests.functional import base @@ -28,7 +27,7 @@ def _test_cmd_with_not_existing_instance(self, cmd, args): try: self.nova("%s %s" % (cmd, args)) except exceptions.CommandFailed as e: - self.assertIn("ERROR (NotFound):", six.text_type(e)) + self.assertIn("ERROR (NotFound):", str(e)) else: self.fail("%s is not failed on non existing instance." % cmd) diff --git a/novaclient/tests/unit/fixture_data/base.py b/novaclient/tests/unit/fixture_data/base.py index 6580787a9..e08d82db6 100644 --- a/novaclient/tests/unit/fixture_data/base.py +++ b/novaclient/tests/unit/fixture_data/base.py @@ -10,8 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from urllib import parse + import fixtures -from six.moves.urllib import parse from novaclient.tests.unit.v2 import fakes diff --git a/novaclient/tests/unit/fixture_data/hypervisors.py b/novaclient/tests/unit/fixture_data/hypervisors.py index 48d7efc86..eeb5d51e4 100644 --- a/novaclient/tests/unit/fixture_data/hypervisors.py +++ b/novaclient/tests/unit/fixture_data/hypervisors.py @@ -10,8 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from urllib import parse + from oslo_utils import encodeutils -from six.moves.urllib import parse from novaclient import api_versions from novaclient.tests.unit.fixture_data import base diff --git a/novaclient/tests/unit/test_base.py b/novaclient/tests/unit/test_base.py index b1067cf2b..634cc93b4 100644 --- a/novaclient/tests/unit/test_base.py +++ b/novaclient/tests/unit/test_base.py @@ -12,7 +12,6 @@ # under the License. import requests -import six from novaclient import api_versions from novaclient import base @@ -148,14 +147,3 @@ def test_bytes_with_meta(self): # Check request_ids attribute is added to obj self.assertTrue(hasattr(obj, 'request_ids')) self.assertEqual(fakes.FAKE_REQUEST_ID_LIST, obj.request_ids) - - -if six.PY2: - class UnicodeWithMetaTest(utils.TestCase): - def test_unicode_with_meta(self): - resp = create_response_obj_with_header() - obj = base.UnicodeWithMeta(u'test-unicode', resp) - self.assertEqual(u'test-unicode', obj) - # Check request_ids attribute is added to obj - self.assertTrue(hasattr(obj, 'request_ids')) - self.assertEqual(fakes.FAKE_REQUEST_ID_LIST, obj.request_ids) diff --git a/novaclient/tests/unit/test_shell.py b/novaclient/tests/unit/test_shell.py index 122f0727e..9ef6141f7 100644 --- a/novaclient/tests/unit/test_shell.py +++ b/novaclient/tests/unit/test_shell.py @@ -13,6 +13,7 @@ import argparse import distutils.version as dist_version +import io import re import sys @@ -22,7 +23,6 @@ import mock import prettytable import requests_mock -import six from testtools import matchers from novaclient import api_versions @@ -212,7 +212,7 @@ def test_init_action_other(self, mock_init): action.assert_called_once_with( 'option_strings', 'dest', help='Deprecated', a=1, b=2, c=3) - @mock.patch.object(sys, 'stderr', six.StringIO()) + @mock.patch.object(sys, 'stderr', io.StringIO()) def test_get_action_nolookup(self): action_class = mock.Mock() parser = mock.Mock(**{ @@ -230,7 +230,7 @@ def test_get_action_nolookup(self): self.assertFalse(action_class.called) self.assertEqual(sys.stderr.getvalue(), '') - @mock.patch.object(sys, 'stderr', six.StringIO()) + @mock.patch.object(sys, 'stderr', io.StringIO()) def test_get_action_lookup_noresult(self): parser = mock.Mock(**{ '_registry_get.return_value': None, @@ -248,7 +248,7 @@ def test_get_action_lookup_noresult(self): 'WARNING: Programming error: Unknown real action ' '"store"\n') - @mock.patch.object(sys, 'stderr', six.StringIO()) + @mock.patch.object(sys, 'stderr', io.StringIO()) def test_get_action_lookup_withresult(self): action_class = mock.Mock() parser = mock.Mock(**{ @@ -267,7 +267,7 @@ def test_get_action_lookup_withresult(self): 'option_strings', 'dest', help='Deprecated', const=1) self.assertEqual(sys.stderr.getvalue(), '') - @mock.patch.object(sys, 'stderr', six.StringIO()) + @mock.patch.object(sys, 'stderr', io.StringIO()) @mock.patch.object(novaclient.shell.DeprecatedAction, '_get_action') def test_call_unemitted_nouse(self, mock_get_action): obj = novaclient.shell.DeprecatedAction( @@ -282,7 +282,7 @@ def test_call_unemitted_nouse(self, mock_get_action): self.assertEqual(sys.stderr.getvalue(), 'WARNING: Option "option_string" is deprecated\n') - @mock.patch.object(sys, 'stderr', six.StringIO()) + @mock.patch.object(sys, 'stderr', io.StringIO()) @mock.patch.object(novaclient.shell.DeprecatedAction, '_get_action') def test_call_unemitted_withuse(self, mock_get_action): obj = novaclient.shell.DeprecatedAction( @@ -298,7 +298,7 @@ def test_call_unemitted_withuse(self, mock_get_action): 'WARNING: Option "option_string" is deprecated; ' 'use this instead\n') - @mock.patch.object(sys, 'stderr', six.StringIO()) + @mock.patch.object(sys, 'stderr', io.StringIO()) @mock.patch.object(novaclient.shell.DeprecatedAction, '_get_action') def test_call_emitted_nouse(self, mock_get_action): obj = novaclient.shell.DeprecatedAction( @@ -313,7 +313,7 @@ def test_call_emitted_nouse(self, mock_get_action): 'parser', 'namespace', 'values', 'option_string') self.assertEqual(sys.stderr.getvalue(), '') - @mock.patch.object(sys, 'stderr', six.StringIO()) + @mock.patch.object(sys, 'stderr', io.StringIO()) @mock.patch.object(novaclient.shell.DeprecatedAction, '_get_action') def test_call_emitted_withuse(self, mock_get_action): obj = novaclient.shell.DeprecatedAction( @@ -393,8 +393,8 @@ def shell(self, argstr, exitcodes=(0,)): orig = sys.stdout orig_stderr = sys.stderr try: - sys.stdout = six.StringIO() - sys.stderr = six.StringIO() + sys.stdout = io.StringIO() + sys.stderr = io.StringIO() _shell = novaclient.shell.OpenStackComputeShell() _shell.main(argstr.split()) except SystemExit: @@ -622,8 +622,8 @@ def test_v_unknown_service_type(self): 'unknown', 'compute', self.mock_client) @mock.patch('sys.argv', ['nova']) - @mock.patch('sys.stdout', six.StringIO()) - @mock.patch('sys.stderr', six.StringIO()) + @mock.patch('sys.stdout', io.StringIO()) + @mock.patch('sys.stderr', io.StringIO()) def test_main_noargs(self): # Ensure that main works with no command-line arguments try: @@ -761,7 +761,7 @@ def test_microversion_with_specific_version_without_microversions(self): def test_main_error_handling(self, mock_compute_shell): class MyException(Exception): pass - with mock.patch('sys.stderr', six.StringIO()): + with mock.patch('sys.stderr', io.StringIO()): mock_compute_shell.side_effect = MyException('message') self.assertRaises(SystemExit, novaclient.shell.main) err = sys.stderr.getvalue() diff --git a/novaclient/tests/unit/test_utils.py b/novaclient/tests/unit/test_utils.py index 5927e65a1..0fb65c75c 100644 --- a/novaclient/tests/unit/test_utils.py +++ b/novaclient/tests/unit/test_utils.py @@ -11,12 +11,11 @@ # License for the specific language governing permissions and limitations # under the License. +import io import sys +from urllib import parse import mock -from oslo_utils import encodeutils -import six -from six.moves.urllib import parse from novaclient import base from novaclient import exceptions @@ -177,7 +176,7 @@ def __init__(self, name, value): class PrintResultTestCase(test_utils.TestCase): - @mock.patch('sys.stdout', six.StringIO()) + @mock.patch('sys.stdout', io.StringIO()) def test_print_dict(self): dict = {'key': 'value'} utils.print_dict(dict) @@ -188,7 +187,7 @@ def test_print_dict(self): '+----------+-------+\n', sys.stdout.getvalue()) - @mock.patch('sys.stdout', six.StringIO()) + @mock.patch('sys.stdout', io.StringIO()) def test_print_dict_wrap(self): dict = {'key1': 'not wrapped', 'key2': 'this will be wrapped'} @@ -202,7 +201,7 @@ def test_print_dict_wrap(self): '+----------+--------------+\n', sys.stdout.getvalue()) - @mock.patch('sys.stdout', six.StringIO()) + @mock.patch('sys.stdout', io.StringIO()) def test_print_list_sort_by_str(self): objs = [_FakeResult("k1", 1), _FakeResult("k3", 2), @@ -219,7 +218,7 @@ def test_print_list_sort_by_str(self): '+------+-------+\n', sys.stdout.getvalue()) - @mock.patch('sys.stdout', six.StringIO()) + @mock.patch('sys.stdout', io.StringIO()) def test_print_list_sort_by_integer(self): objs = [_FakeResult("k1", 1), _FakeResult("k3", 2), @@ -236,14 +235,11 @@ def test_print_list_sort_by_integer(self): '+------+-------+\n', sys.stdout.getvalue()) - @mock.patch('sys.stdout', six.StringIO()) + @mock.patch('sys.stdout', io.StringIO()) def test_print_unicode_list(self): objs = [_FakeResult("k", u'\u2026')] utils.print_list(objs, ["Name", "Value"]) - if six.PY3: - s = u'\u2026' - else: - s = encodeutils.safe_encode(u'\u2026') + s = u'\u2026' self.assertEqual('+------+-------+\n' '| Name | Value |\n' '+------+-------+\n' @@ -252,7 +248,7 @@ def test_print_unicode_list(self): sys.stdout.getvalue()) # without sorting - @mock.patch('sys.stdout', six.StringIO()) + @mock.patch('sys.stdout', io.StringIO()) def test_print_list_sort_by_none(self): objs = [_FakeResult("k1", 1), _FakeResult("k3", 3), @@ -269,7 +265,7 @@ def test_print_list_sort_by_none(self): '+------+-------+\n', sys.stdout.getvalue()) - @mock.patch('sys.stdout', six.StringIO()) + @mock.patch('sys.stdout', io.StringIO()) def test_print_dict_dictionary(self): dict = {'k': {'foo': 'bar'}} utils.print_dict(dict) @@ -280,7 +276,7 @@ def test_print_dict_dictionary(self): '+----------+----------------+\n', sys.stdout.getvalue()) - @mock.patch('sys.stdout', six.StringIO()) + @mock.patch('sys.stdout', io.StringIO()) def test_print_dict_list_dictionary(self): dict = {'k': [{'foo': 'bar'}]} utils.print_dict(dict) @@ -291,7 +287,7 @@ def test_print_dict_list_dictionary(self): '+----------+------------------+\n', sys.stdout.getvalue()) - @mock.patch('sys.stdout', six.StringIO()) + @mock.patch('sys.stdout', io.StringIO()) def test_print_dict_list(self): dict = {'k': ['foo', 'bar']} utils.print_dict(dict) @@ -302,7 +298,7 @@ def test_print_dict_list(self): '+----------+----------------+\n', sys.stdout.getvalue()) - @mock.patch('sys.stdout', six.StringIO()) + @mock.patch('sys.stdout', io.StringIO()) def test_print_large_dict_list(self): dict = {'k': ['foo1', 'bar1', 'foo2', 'bar2', 'foo3', 'bar3', 'foo4', 'bar4']} @@ -316,14 +312,11 @@ def test_print_large_dict_list(self): '+----------+------------------------------------------+\n', sys.stdout.getvalue()) - @mock.patch('sys.stdout', six.StringIO()) + @mock.patch('sys.stdout', io.StringIO()) def test_print_unicode_dict(self): dict = {'k': u'\u2026'} utils.print_dict(dict) - if six.PY3: - s = u'\u2026' - else: - s = encodeutils.safe_encode(u'\u2026') + s = u'\u2026' self.assertEqual('+----------+-------+\n' '| Property | Value |\n' '+----------+-------+\n' @@ -403,7 +396,7 @@ def test_do_action_on_many_first_fails(self): def test_do_action_on_many_last_fails(self): self._test_do_action_on_many([None, Exception()], fail=True) - @mock.patch('sys.stdout', new_callable=six.StringIO) + @mock.patch('sys.stdout', new_callable=io.StringIO) def _test_do_action_on_many_resource_string( self, resource, expected_string, mock_stdout): utils.do_action_on_many(mock.Mock(), [resource], 'success with %s', @@ -452,9 +445,8 @@ class PrepareQueryStringTestCase(test_utils.TestCase): def setUp(self): super(PrepareQueryStringTestCase, self).setUp() self.ustr = b'?\xd0\xbf=1&\xd1\x80=2' - if six.PY3: - # in py3 real unicode symbols will be urlencoded - self.ustr = self.ustr.decode('utf8') + # in py3 real unicode symbols will be urlencoded + self.ustr = self.ustr.decode('utf8') self.cases = ( ({}, ''), (None, ''), diff --git a/novaclient/tests/unit/utils.py b/novaclient/tests/unit/utils.py index 0050941bc..229d999ec 100644 --- a/novaclient/tests/unit/utils.py +++ b/novaclient/tests/unit/utils.py @@ -18,7 +18,6 @@ from oslo_serialization import jsonutils import requests from requests_mock.contrib import fixture as requests_mock_fixture -import six import testscenarios import testtools @@ -99,9 +98,9 @@ def assert_called(self, method, path, body=None): if body: req_data = self.requests_mock.last_request.body - if isinstance(req_data, six.binary_type): + if isinstance(req_data, bytes): req_data = req_data.decode('utf-8') - if not isinstance(body, six.string_types): + if not isinstance(body, str): # json load if the input body to match against is not a string req_data = jsonutils.loads(req_data) self.assertEqual(body, req_data) diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 62d5e727e..69ecc8936 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -17,11 +17,10 @@ import copy import datetime import re +from urllib import parse import mock from oslo_utils import strutils -import six -from six.moves.urllib import parse import novaclient from novaclient import api_versions @@ -1528,158 +1527,146 @@ def put_os_quota_class_sets_97f4c221bff44578b0300df4ef119353(self, # Tenant Usage # def get_os_simple_tenant_usage(self, **kw): - return (200, FAKE_RESPONSE_HEADERS, - {six.u('tenant_usages'): [{ - six.u('total_memory_mb_usage'): 25451.762807466665, - six.u('total_vcpus_usage'): 49.71047423333333, - six.u('total_hours'): 49.71047423333333, - six.u('tenant_id'): - six.u('7b0a1d73f8fb41718f3343c207597869'), - six.u('stop'): six.u('2012-01-22 19:48:41.750722'), - six.u('server_usages'): [{ - six.u('hours'): 49.71047423333333, - six.u('uptime'): 27035, - six.u('local_gb'): 0, - six.u('ended_at'): None, - six.u('name'): six.u('f15image1'), - six.u('tenant_id'): - six.u('7b0a1d73f8fb41718f3343c207597869'), - six.u('instance_id'): - six.u('f079e394-1111-457b-b350-bb5ecc685cdd'), - six.u('vcpus'): 1, - six.u('memory_mb'): 512, - six.u('state'): six.u('active'), - six.u('flavor'): six.u('m1.tiny'), - six.u('started_at'): - six.u('2012-01-20 18:06:06.479998')}], - six.u('start'): six.u('2011-12-25 19:48:41.750687'), - six.u('total_local_gb_usage'): 0.0}]}) + return (200, FAKE_RESPONSE_HEADERS, {'tenant_usages': [{ + 'total_memory_mb_usage': 25451.762807466665, + 'total_vcpus_usage': 49.71047423333333, + 'total_hours': 49.71047423333333, + 'tenant_id': '7b0a1d73f8fb41718f3343c207597869', + 'stop': '2012-01-22 19:48:41.750722', + 'server_usages': [{ + 'hours': 49.71047423333333, + 'uptime': 27035, + 'local_gb': 0, + 'ended_at': None, + 'name': 'f15image1', + 'tenant_id': '7b0a1d73f8fb41718f3343c207597869', + 'instance_id': 'f079e394-1111-457b-b350-bb5ecc685cdd', + 'vcpus': 1, + 'memory_mb': 512, + 'state': 'active', + 'flavor': 'm1.tiny', + 'started_at': '2012-01-20 18:06:06.479998', + }], + 'start': '2011-12-25 19:48:41.750687', + 'total_local_gb_usage': 0.0}]}) def get_os_simple_tenant_usage_next(self, **kw): - return (200, FAKE_RESPONSE_HEADERS, - {six.u('tenant_usages'): [{ - six.u('total_memory_mb_usage'): 25451.762807466665, - six.u('total_vcpus_usage'): 49.71047423333333, - six.u('total_hours'): 49.71047423333333, - six.u('tenant_id'): - six.u('7b0a1d73f8fb41718f3343c207597869'), - six.u('stop'): six.u('2012-01-22 19:48:41.750722'), - six.u('server_usages'): [{ - six.u('hours'): 49.71047423333333, - six.u('uptime'): 27035, - six.u('local_gb'): 0, - six.u('ended_at'): None, - six.u('name'): six.u('f15image1'), - six.u('tenant_id'): - six.u('7b0a1d73f8fb41718f3343c207597869'), - six.u('instance_id'): - six.u('f079e394-2222-457b-b350-bb5ecc685cdd'), - six.u('vcpus'): 1, - six.u('memory_mb'): 512, - six.u('state'): six.u('active'), - six.u('flavor'): six.u('m1.tiny'), - six.u('started_at'): - six.u('2012-01-20 18:06:06.479998')}], - six.u('start'): six.u('2011-12-25 19:48:41.750687'), - six.u('total_local_gb_usage'): 0.0}]}) + return (200, FAKE_RESPONSE_HEADERS, {'tenant_usages': [{ + 'total_memory_mb_usage': 25451.762807466665, + 'total_vcpus_usage': 49.71047423333333, + 'total_hours': 49.71047423333333, + 'tenant_id': '7b0a1d73f8fb41718f3343c207597869', + 'stop': '2012-01-22 19:48:41.750722', + 'server_usages': [{ + 'hours': 49.71047423333333, + 'uptime': 27035, + 'local_gb': 0, + 'ended_at': None, + 'name': 'f15image1', + 'tenant_id': '7b0a1d73f8fb41718f3343c207597869', + 'instance_id': 'f079e394-2222-457b-b350-bb5ecc685cdd', + 'vcpus': 1, + 'memory_mb': 512, + 'state': 'active', + 'flavor': 'm1.tiny', + 'started_at': '2012-01-20 18:06:06.479998', + }], + 'start': '2011-12-25 19:48:41.750687', + 'total_local_gb_usage': 0.0}]}) def get_os_simple_tenant_usage_next_next(self, **kw): - return (200, FAKE_RESPONSE_HEADERS, {six.u('tenant_usages'): []}) + return (200, FAKE_RESPONSE_HEADERS, {'tenant_usages': []}) def get_os_simple_tenant_usage_tenantfoo(self, **kw): - return (200, FAKE_RESPONSE_HEADERS, - {six.u('tenant_usage'): { - six.u('total_memory_mb_usage'): 25451.762807466665, - six.u('total_vcpus_usage'): 49.71047423333333, - six.u('total_hours'): 49.71047423333333, - six.u('tenant_id'): - six.u('7b0a1d73f8fb41718f3343c207597869'), - six.u('stop'): six.u('2012-01-22 19:48:41.750722'), - six.u('server_usages'): [{ - six.u('hours'): 49.71047423333333, - six.u('uptime'): 27035, six.u('local_gb'): 0, - six.u('ended_at'): None, - six.u('name'): six.u('f15image1'), - six.u('tenant_id'): - six.u('7b0a1d73f8fb41718f3343c207597869'), - six.u('instance_id'): - six.u('f079e394-1111-457b-b350-bb5ecc685cdd'), - six.u('vcpus'): 1, six.u('memory_mb'): 512, - six.u('state'): six.u('active'), - six.u('flavor'): six.u('m1.tiny'), - six.u('started_at'): - six.u('2012-01-20 18:06:06.479998')}], - six.u('start'): six.u('2011-12-25 19:48:41.750687'), - six.u('total_local_gb_usage'): 0.0}}) + return (200, FAKE_RESPONSE_HEADERS, {'tenant_usage': { + 'total_memory_mb_usage': 25451.762807466665, + 'total_vcpus_usage': 49.71047423333333, + 'total_hours': 49.71047423333333, + 'tenant_id': '7b0a1d73f8fb41718f3343c207597869', + 'stop': '2012-01-22 19:48:41.750722', + 'server_usages': [{ + 'hours': 49.71047423333333, + 'uptime': 27035, 'local_gb': 0, + 'ended_at': None, + 'name': 'f15image1', + 'tenant_id': '7b0a1d73f8fb41718f3343c207597869', + 'instance_id': 'f079e394-1111-457b-b350-bb5ecc685cdd', + 'vcpus': 1, 'memory_mb': 512, + 'state': 'active', + 'flavor': 'm1.tiny', + 'started_at': '2012-01-20 18:06:06.479998', + }], + 'start': '2011-12-25 19:48:41.750687', + 'total_local_gb_usage': 0.0}}) def get_os_simple_tenant_usage_test(self, **kw): - return (200, {}, {six.u('tenant_usage'): { - six.u('total_memory_mb_usage'): 25451.762807466665, - six.u('total_vcpus_usage'): 49.71047423333333, - six.u('total_hours'): 49.71047423333333, - six.u('tenant_id'): six.u('7b0a1d73f8fb41718f3343c207597869'), - six.u('stop'): six.u('2012-01-22 19:48:41.750722'), - six.u('server_usages'): [{ - six.u('hours'): 49.71047423333333, - six.u('uptime'): 27035, six.u('local_gb'): 0, - six.u('ended_at'): None, - six.u('name'): six.u('f15image1'), - six.u('tenant_id'): six.u('7b0a1d73f8fb41718f3343c207597869'), - six.u('instance_id'): - six.u('f079e394-1111-457b-b350-bb5ecc685cdd'), - six.u('vcpus'): 1, six.u('memory_mb'): 512, - six.u('state'): six.u('active'), - six.u('flavor'): six.u('m1.tiny'), - six.u('started_at'): six.u('2012-01-20 18:06:06.479998')}], - six.u('start'): six.u('2011-12-25 19:48:41.750687'), - six.u('total_local_gb_usage'): 0.0}}) + return (200, {}, {'tenant_usage': { + 'total_memory_mb_usage': 25451.762807466665, + 'total_vcpus_usage': 49.71047423333333, + 'total_hours': 49.71047423333333, + 'tenant_id': '7b0a1d73f8fb41718f3343c207597869', + 'stop': '2012-01-22 19:48:41.750722', + 'server_usages': [{ + 'hours': 49.71047423333333, + 'uptime': 27035, 'local_gb': 0, + 'ended_at': None, + 'name': 'f15image1', + 'tenant_id': '7b0a1d73f8fb41718f3343c207597869', + 'instance_id': 'f079e394-1111-457b-b350-bb5ecc685cdd', + 'vcpus': 1, 'memory_mb': 512, + 'state': 'active', + 'flavor': 'm1.tiny', + 'started_at': '2012-01-20 18:06:06.479998', + }], + 'start': '2011-12-25 19:48:41.750687', + 'total_local_gb_usage': 0.0}}) def get_os_simple_tenant_usage_tenant_id(self, **kw): - return (200, {}, {six.u('tenant_usage'): { - six.u('total_memory_mb_usage'): 25451.762807466665, - six.u('total_vcpus_usage'): 49.71047423333333, - six.u('total_hours'): 49.71047423333333, - six.u('tenant_id'): six.u('7b0a1d73f8fb41718f3343c207597869'), - six.u('stop'): six.u('2012-01-22 19:48:41.750722'), - six.u('server_usages'): [{ - six.u('hours'): 49.71047423333333, - six.u('uptime'): 27035, six.u('local_gb'): 0, - six.u('ended_at'): None, - six.u('name'): six.u('f15image1'), - six.u('tenant_id'): six.u('7b0a1d73f8fb41718f3343c207597869'), - six.u('instance_id'): - six.u('f079e394-1111-457b-b350-bb5ecc685cdd'), - six.u('vcpus'): 1, six.u('memory_mb'): 512, - six.u('state'): six.u('active'), - six.u('flavor'): six.u('m1.tiny'), - six.u('started_at'): six.u('2012-01-20 18:06:06.479998')}], - six.u('start'): six.u('2011-12-25 19:48:41.750687'), - six.u('total_local_gb_usage'): 0.0}}) + return (200, {}, {'tenant_usage': { + 'total_memory_mb_usage': 25451.762807466665, + 'total_vcpus_usage': 49.71047423333333, + 'total_hours': 49.71047423333333, + 'tenant_id': '7b0a1d73f8fb41718f3343c207597869', + 'stop': '2012-01-22 19:48:41.750722', + 'server_usages': [{ + 'hours': 49.71047423333333, + 'uptime': 27035, 'local_gb': 0, + 'ended_at': None, + 'name': 'f15image1', + 'tenant_id': '7b0a1d73f8fb41718f3343c207597869', + 'instance_id': 'f079e394-1111-457b-b350-bb5ecc685cdd', + 'vcpus': 1, 'memory_mb': 512, + 'state': 'active', + 'flavor': 'm1.tiny', + 'started_at': '2012-01-20 18:06:06.479998', + }], + 'start': '2011-12-25 19:48:41.750687', + 'total_local_gb_usage': 0.0}}) def get_os_simple_tenant_usage_tenant_id_next(self, **kw): - return (200, {}, {six.u('tenant_usage'): { - six.u('total_memory_mb_usage'): 25451.762807466665, - six.u('total_vcpus_usage'): 49.71047423333333, - six.u('total_hours'): 49.71047423333333, - six.u('tenant_id'): six.u('7b0a1d73f8fb41718f3343c207597869'), - six.u('stop'): six.u('2012-01-22 19:48:41.750722'), - six.u('server_usages'): [{ - six.u('hours'): 49.71047423333333, - six.u('uptime'): 27035, six.u('local_gb'): 0, - six.u('ended_at'): None, - six.u('name'): six.u('f15image1'), - six.u('tenant_id'): six.u('7b0a1d73f8fb41718f3343c207597869'), - six.u('instance_id'): - six.u('f079e394-2222-457b-b350-bb5ecc685cdd'), - six.u('vcpus'): 1, six.u('memory_mb'): 512, - six.u('state'): six.u('active'), - six.u('flavor'): six.u('m1.tiny'), - six.u('started_at'): six.u('2012-01-20 18:06:06.479998')}], - six.u('start'): six.u('2011-12-25 19:48:41.750687'), - six.u('total_local_gb_usage'): 0.0}}) + return (200, {}, {'tenant_usage': { + 'total_memory_mb_usage': 25451.762807466665, + 'total_vcpus_usage': 49.71047423333333, + 'total_hours': 49.71047423333333, + 'tenant_id': '7b0a1d73f8fb41718f3343c207597869', + 'stop': '2012-01-22 19:48:41.750722', + 'server_usages': [{ + 'hours': 49.71047423333333, + 'uptime': 27035, 'local_gb': 0, + 'ended_at': None, + 'name': 'f15image1', + 'tenant_id': '7b0a1d73f8fb41718f3343c207597869', + 'instance_id': 'f079e394-2222-457b-b350-bb5ecc685cdd', + 'vcpus': 1, 'memory_mb': 512, + 'state': 'active', + 'flavor': 'm1.tiny', + 'started_at': '2012-01-20 18:06:06.479998', + }], + 'start': '2011-12-25 19:48:41.750687', + 'total_local_gb_usage': 0.0}}) def get_os_simple_tenant_usage_tenant_id_next_next(self, **kw): - return (200, {}, {six.u('tenant_usage'): {}}) + return (200, {}, {'tenant_usage': {}}) # # Aggregates diff --git a/novaclient/tests/unit/v2/test_availability_zone.py b/novaclient/tests/unit/v2/test_availability_zone.py index ac2bf0f2b..c3ac6f07e 100644 --- a/novaclient/tests/unit/v2/test_availability_zone.py +++ b/novaclient/tests/unit/v2/test_availability_zone.py @@ -14,8 +14,6 @@ # License for the specific language governing permissions and limitations # under the License. -import six - from novaclient.tests.unit.fixture_data import availability_zones as data from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit import utils @@ -54,8 +52,8 @@ def test_list_availability_zone(self): self.assertEqual(2, len(zones)) - l0 = [six.u('zone-1'), six.u('available')] - l1 = [six.u('zone-2'), six.u('not available')] + l0 = ['zone-1', 'available'] + l1 = ['zone-2', 'not available'] z0 = self.shell._treeizeAvailabilityZone(zones[0]) z1 = self.shell._treeizeAvailabilityZone(zones[1]) @@ -75,18 +73,15 @@ def test_detail_availability_zone(self): self.assertEqual(3, len(zones)) - l0 = [six.u('zone-1'), six.u('available')] - l1 = [six.u('|- fake_host-1'), six.u('')] - l2 = [six.u('| |- nova-compute'), - six.u('enabled :-) 2012-12-26 14:45:25')] - l3 = [six.u('internal'), six.u('available')] - l4 = [six.u('|- fake_host-1'), six.u('')] - l5 = [six.u('| |- nova-sched'), - six.u('enabled :-) 2012-12-26 14:45:25')] - l6 = [six.u('|- fake_host-2'), six.u('')] - l7 = [six.u('| |- nova-network'), - six.u('enabled XXX 2012-12-26 14:45:24')] - l8 = [six.u('zone-2'), six.u('not available')] + l0 = ['zone-1', 'available'] + l1 = ['|- fake_host-1', ''] + l2 = ['| |- nova-compute', 'enabled :-) 2012-12-26 14:45:25'] + l3 = ['internal', 'available'] + l4 = ['|- fake_host-1', ''] + l5 = ['| |- nova-sched', 'enabled :-) 2012-12-26 14:45:25'] + l6 = ['|- fake_host-2', ''] + l7 = ['| |- nova-network', 'enabled XXX 2012-12-26 14:45:24'] + l8 = ['zone-2', 'not available'] z0 = self.shell._treeizeAvailabilityZone(zones[0]) z1 = self.shell._treeizeAvailabilityZone(zones[1]) diff --git a/novaclient/tests/unit/v2/test_hypervisors.py b/novaclient/tests/unit/v2/test_hypervisors.py index 3eaeb576f..3907687fa 100644 --- a/novaclient/tests/unit/v2/test_hypervisors.py +++ b/novaclient/tests/unit/v2/test_hypervisors.py @@ -13,8 +13,6 @@ # License for the specific language governing permissions and limitations # under the License. -import six - from novaclient import api_versions from novaclient import exceptions from novaclient.tests.unit.fixture_data import client @@ -127,7 +125,7 @@ def test_hypervisor_search_detailed(self): self.cs.hypervisors.search, 'hyper', detailed=True) self.assertIn('Parameter "detailed" requires API version 2.53 or ' - 'greater.', six.text_type(ex)) + 'greater.', str(ex)) def test_hypervisor_servers(self): expected = [ diff --git a/novaclient/tests/unit/v2/test_migrations.py b/novaclient/tests/unit/v2/test_migrations.py index 09d09405c..0b5ebb84e 100644 --- a/novaclient/tests/unit/v2/test_migrations.py +++ b/novaclient/tests/unit/v2/test_migrations.py @@ -10,8 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import six - from novaclient import api_versions from novaclient.tests.unit import utils from novaclient.tests.unit.v2 import fakes @@ -158,8 +156,7 @@ def test_list_migrations_with_user_id_pre_v280(self): ex = self.assertRaises(TypeError, self.cs.migrations.list, user_id=user_id) - self.assertIn("unexpected keyword argument 'user_id'", - six.text_type(ex)) + self.assertIn("unexpected keyword argument 'user_id'", str(ex)) def test_list_migrations_with_project_id_pre_v280(self): self.cs.api_version = api_versions.APIVersion('2.79') @@ -167,5 +164,4 @@ def test_list_migrations_with_project_id_pre_v280(self): ex = self.assertRaises(TypeError, self.cs.migrations.list, project_id=project_id) - self.assertIn("unexpected keyword argument 'project_id'", - six.text_type(ex)) + self.assertIn("unexpected keyword argument 'project_id'", str(ex)) diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index c255cbd7b..6ad9df9ad 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -13,11 +13,11 @@ # under the License. import base64 +import io import os import tempfile import mock -import six from novaclient import api_versions from novaclient import exceptions @@ -83,7 +83,7 @@ def test_filter_servers_unlocked(self): self.cs.servers.list, search_opts={'locked': False}) self.assertIn("'locked' argument is only allowed since " - "microversion 2.73.", six.text_type(e)) + "microversion 2.73.", str(e)) def test_list_servers_undetailed(self): sl = self.cs.servers.list(detailed=False) @@ -140,7 +140,7 @@ def test_create_server(self): if self.supports_files: kwargs['files'] = { '/etc/passwd': 'some data', # a file - '/tmp/foo.txt': six.StringIO('data'), # a stream + '/tmp/foo.txt': io.StringIO('data'), # a stream } s = self.cs.servers.create( name="My server", @@ -270,14 +270,14 @@ def test_create_server_userdata_file_object(self): if self.supports_files: kwargs['files'] = { '/etc/passwd': 'some data', # a file - '/tmp/foo.txt': six.StringIO('data'), # a stream + '/tmp/foo.txt': io.StringIO('data'), # a stream } s = self.cs.servers.create( name="My server", image=1, flavor=1, meta={'foo': 'bar'}, - userdata=six.StringIO('hello moto'), + userdata=io.StringIO('hello moto'), nics=self._get_server_create_default_nics(), **kwargs ) @@ -290,14 +290,14 @@ def test_create_server_userdata_unicode(self): if self.supports_files: kwargs['files'] = { '/etc/passwd': 'some data', # a file - '/tmp/foo.txt': six.StringIO('data'), # a stream + '/tmp/foo.txt': io.StringIO('data'), # a stream } s = self.cs.servers.create( name="My server", image=1, flavor=1, meta={'foo': 'bar'}, - userdata=six.u('こんにちは'), + userdata='こんにちは', key_name="fakekey", nics=self._get_server_create_default_nics(), **kwargs @@ -311,7 +311,7 @@ def test_create_server_userdata_utf8(self): if self.supports_files: kwargs['files'] = { '/etc/passwd': 'some data', # a file - '/tmp/foo.txt': six.StringIO('data'), # a stream + '/tmp/foo.txt': io.StringIO('data'), # a stream } s = self.cs.servers.create( name="My server", @@ -349,7 +349,7 @@ def test_create_server_userdata_bin(self): if self.supports_files: kwargs['files'] = { '/etc/passwd': 'some data', # a file - '/tmp/foo.txt': six.StringIO('data'), # a stream + '/tmp/foo.txt': io.StringIO('data'), # a stream } with tempfile.TemporaryFile(mode='wb+') as bin_file: original_data = os.urandom(1024) @@ -1510,7 +1510,7 @@ def test_rebuild_with_key_name_pre_254_fails(self): self.cs.servers.rebuild, '1234', fakes.FAKE_IMAGE_UUID_1, key_name='test_keypair') - self.assertIn('key_name', six.text_type(ex.message)) + self.assertIn('key_name', str(ex.message)) class ServersV256Test(ServersV254Test): @@ -1533,7 +1533,7 @@ def test_migrate_server_pre_256_fails(self): s = self.cs.servers.get(1234) ex = self.assertRaises(TypeError, s.migrate, host='target-host') - self.assertIn('host', six.text_type(ex)) + self.assertIn('host', str(ex)) class ServersV257Test(ServersV256Test): @@ -1549,9 +1549,9 @@ def test_create_server_with_files_fails(self): name="My server", image=1, flavor=1, files={ '/etc/passwd': 'some data', # a file - '/tmp/foo.txt': six.StringIO('data'), # a stream + '/tmp/foo.txt': io.StringIO('data'), # a stream }, nics='auto') - self.assertIn('files', six.text_type(ex)) + self.assertIn('files', str(ex)) def test_rebuild_server_name_meta_files(self): files = {'/etc/passwd': 'some data'} @@ -1559,7 +1559,7 @@ def test_rebuild_server_name_meta_files(self): ex = self.assertRaises( exceptions.UnsupportedAttribute, s.rebuild, image=1, name='new', meta={'foo': 'bar'}, files=files) - self.assertIn('files', six.text_type(ex)) + self.assertIn('files', str(ex)) class ServersV263Test(ServersV257Test): @@ -1600,7 +1600,7 @@ def test_create_server_with_trusted_image_certificates_pre_263_fails(self): userdata="hello moto", key_name="fakekey", nics=self._get_server_create_default_nics(), trusted_image_certificates=['id1', 'id2']) - self.assertIn('trusted_image_certificates', six.text_type(ex)) + self.assertIn('trusted_image_certificates', str(ex)) def test_rebuild_server_with_trusted_image_certificates(self): s = self.cs.servers.get(1234) @@ -1626,7 +1626,7 @@ def test_rebuild_with_trusted_image_certificates_pre_263_fails(self): self.cs.servers.rebuild, '1234', fakes.FAKE_IMAGE_UUID_1, trusted_image_certificates=['id1', 'id2']) - self.assertIn('trusted_image_certificates', six.text_type(ex)) + self.assertIn('trusted_image_certificates', str(ex)) class ServersV267Test(ServersV263Test): @@ -1671,7 +1671,7 @@ def test_create_server_boot_from_volume_with_volume_type_pre_267(self): name="bfv server", image='', flavor=1, nics='none', block_device_mapping_v2=bdm) self.assertIn("Block device volume_type is not supported before " - "microversion 2.67", six.text_type(ex)) + "microversion 2.67", str(ex)) class ServersV268Test(ServersV267Test): @@ -1692,7 +1692,7 @@ def test_evacuate(self): ex = self.assertRaises(TypeError, self.cs.servers.evacuate, 'fake_target_host', force=True) - self.assertIn('force', six.text_type(ex)) + self.assertIn('force', str(ex)) def test_live_migrate_server(self): s = self.cs.servers.get(1234) @@ -1711,7 +1711,7 @@ def test_live_migrate_server(self): ex = self.assertRaises(TypeError, self.cs.servers.live_migrate, host='hostname', force=True) - self.assertIn('force', six.text_type(ex)) + self.assertIn('force', str(ex)) class ServersV273Test(ServersV268Test): @@ -1734,7 +1734,7 @@ def test_lock_server_pre_273_fails_with_reason(self): s = self.cs.servers.get(1234) e = self.assertRaises(TypeError, s.lock, reason='blah') - self.assertIn("unexpected keyword argument 'reason'", six.text_type(e)) + self.assertIn("unexpected keyword argument 'reason'", str(e)) def test_filter_servers_unlocked(self): # support locked=False @@ -1818,7 +1818,7 @@ def test_create_server_with_host_pre_274_fails(self): name="My server", image=1, flavor=1, nics='auto', host="new-host") self.assertIn("'host' argument is only allowed since microversion " - "2.74", six.text_type(ex)) + "2.74", str(ex)) def test_create_server_with_hypervisor_hostname_pre_274_fails(self): self.cs.api_version = api_versions.APIVersion('2.73') @@ -1827,7 +1827,7 @@ def test_create_server_with_hypervisor_hostname_pre_274_fails(self): name="My server", image=1, flavor=1, nics='auto', hypervisor_hostname="new-host") self.assertIn("'hypervisor_hostname' argument is only allowed since " - "microversion 2.74", six.text_type(ex)) + "microversion 2.74", str(ex)) def test_create_server_with_host_and_hypervisor_hostname_pre_274_fails( self): @@ -1838,7 +1838,7 @@ def test_create_server_with_host_and_hypervisor_hostname_pre_274_fails( nics='auto', host="new-host", hypervisor_hostname="new-host") self.assertIn("'host' argument is only allowed since microversion " - "2.74", six.text_type(ex)) + "2.74", str(ex)) class ServersV277Test(ServersV274Test): @@ -1868,13 +1868,13 @@ def test_unshelve_server_pre_277_fails_with_specified_az(self): s.unshelve, availability_zone='foo-az') self.assertIn("unexpected keyword argument 'availability_zone'", - six.text_type(ex)) + str(ex)) # Test going through the ServerManager directly. ex = self.assertRaises(TypeError, self.cs.servers.unshelve, s, availability_zone='foo-az') self.assertIn("unexpected keyword argument 'availability_zone'", - six.text_type(ex)) + str(ex)) class ServersV278Test(ServersV273Test): diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index dbd2b3bea..6c3b3cfa0 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -18,15 +18,15 @@ import argparse import base64 +import builtins import collections import datetime +import io import os import fixtures import mock from oslo_utils import timeutils -import six -from six.moves import builtins import testtools import novaclient @@ -88,8 +88,8 @@ def setUp(self): # TODO(stephenfin): We should migrate most of the existing assertRaises # calls to simply pass expected_error to this instead so we can easily # capture and compare output - @mock.patch('sys.stdout', new_callable=six.StringIO) - @mock.patch('sys.stderr', new_callable=six.StringIO) + @mock.patch('sys.stdout', new_callable=io.StringIO) + @mock.patch('sys.stderr', new_callable=io.StringIO) def run_command(self, cmd, mock_stderr, mock_stdout, api_version=None, expected_error=None): version_options = [] @@ -304,7 +304,7 @@ def test_boot_config_drive_invalid_value(self): 'boot --flavor 1 --image %s --config-drive /dev/hda some-server' % FAKE_UUID_1) self.assertIn("The value of the '--config-drive' option must be " - "a boolean value.", six.text_type(ex)) + "a boolean value.", str(ex)) def test_boot_invalid_user_data(self): invalid_file = os.path.join(os.path.dirname(__file__), @@ -676,7 +676,7 @@ def test_boot_from_volume_with_volume_type_old_microversion(self): 'size=1,bootindex=0,shutdown=remove,tag=foo,volume_type=lvm ' 'bfv-server' % FAKE_UUID_1, api_version='2.66') self.assertIn("'volume_type' in block device mapping is not supported " - "in API version", six.text_type(ex)) + "in API version", str(ex)) def test_boot_from_volume_with_volume_type(self): """Tests creating a volume-backed server from a source image and @@ -895,7 +895,7 @@ def test_boot_invalid_nics_pre_v2_32(self): '--nic net-id=1,port-id=2 some-server' % FAKE_UUID_1) ex = self.assertRaises(exceptions.CommandError, self.run_command, cmd, api_version='2.1') - self.assertNotIn('tag=tag', six.text_type(ex)) + self.assertNotIn('tag=tag', str(ex)) def test_boot_invalid_nics_v2_32(self): # This is a negative test to make sure we fail with the correct message @@ -903,7 +903,7 @@ def test_boot_invalid_nics_v2_32(self): '--nic net-id=1,port-id=2 some-server' % FAKE_UUID_1) ex = self.assertRaises(exceptions.CommandError, self.run_command, cmd, api_version='2.32') - self.assertIn('tag=tag', six.text_type(ex)) + self.assertIn('tag=tag', str(ex)) def test_boot_invalid_nics_v2_36_auto(self): """This is a negative test to make sure we fail with the correct @@ -912,7 +912,7 @@ def test_boot_invalid_nics_v2_36_auto(self): cmd = ('boot --image %s --flavor 1 --nic auto test' % FAKE_UUID_1) ex = self.assertRaises(exceptions.CommandError, self.run_command, cmd, api_version='2.36') - self.assertNotIn('auto,none', six.text_type(ex)) + self.assertNotIn('auto,none', str(ex)) def test_boot_invalid_nics_v2_37(self): """This is a negative test to make sure we fail with the correct @@ -922,7 +922,7 @@ def test_boot_invalid_nics_v2_37(self): '--nic net-id=1 --nic auto some-server' % FAKE_UUID_1) ex = self.assertRaises(exceptions.CommandError, self.run_command, cmd, api_version='2.37') - self.assertIn('auto,none', six.text_type(ex)) + self.assertIn('auto,none', str(ex)) def test_boot_nics_auto_allocate_default(self): """Tests that if microversion>=2.37 is specified and no --nics are @@ -1407,7 +1407,7 @@ def test_boot_with_not_found_when_accessing_addresses_attribute( exceptions.CommandError, self.run_command, 'boot --flavor 1 --image %s some-server' % FAKE_UUID_2) self.assertIn('Instance %s could not be found.' % FAKE_UUID_1, - six.text_type(ex)) + str(ex)) def test_boot_with_host_v274(self): self.run_command('boot --flavor 1 --image %s ' @@ -1831,7 +1831,7 @@ def test_list_with_changes_before_invalid_value(self): ex = self.assertRaises(exceptions.CommandError, self.run_command, 'list --changes-before 0123456789', api_version='2.66') - self.assertIn('Invalid changes-before value', six.text_type(ex)) + self.assertIn('Invalid changes-before value', str(ex)) def test_list_with_changes_before_pre_v266_not_allowed(self): self.assertRaises(SystemExit, self.run_command, @@ -1947,7 +1947,7 @@ def test_rebuild_unset_keypair_with_key_name(self): 'rebuild sample-server %s --key-unset --key-name test_keypair' % FAKE_UUID_1, api_version='2.54') self.assertIn("Cannot specify '--key-unset' with '--key-name'.", - six.text_type(ex)) + str(ex)) def test_rebuild_with_incorrect_metadata(self): cmd = 'rebuild sample-server %s --name asdf --meta foo' % FAKE_UUID_1 @@ -2001,7 +2001,7 @@ def test_rebuild_invalid_user_data(self): api_version='2.57') self.assertIn("Can't open '%(user_data)s': " "[Errno 2] No such file or directory: '%(user_data)s'" % - {'user_data': invalid_file}, six.text_type(ex)) + {'user_data': invalid_file}, str(ex)) def test_rebuild_unset_user_data(self): self.run_command('rebuild sample-server %s --user-data-unset' % @@ -2024,7 +2024,7 @@ def test_rebuild_user_data_and_unset_user_data(self): ex = self.assertRaises(exceptions.CommandError, self.run_command, cmd, api_version='2.57') self.assertIn("Cannot specify '--user-data-unset' with " - "'--user-data'.", six.text_type(ex)) + "'--user-data'.", str(ex)) def test_rebuild_with_single_trusted_image_certificates(self): self.run_command('rebuild sample-server %s ' @@ -2132,7 +2132,7 @@ def test_rebuild_with_trusted_image_certificates_unset_arg_conflict(self): api_version='2.63') self.assertIn("Cannot specify '--trusted-image-certificates-unset' " "with '--trusted-image-certificate-id'", - six.text_type(ex)) + str(ex)) def test_rebuild_with_trusted_image_certificates_unset_env_conflict(self): """Tests the error condition that trusted image certs are both unset @@ -2146,7 +2146,7 @@ def test_rebuild_with_trusted_image_certificates_unset_env_conflict(self): FAKE_UUID_1, api_version='2.63') self.assertIn("Cannot specify '--trusted-image-certificates-unset' " "with '--trusted-image-certificate-id'", - six.text_type(ex)) + str(ex)) def test_rebuild_with_trusted_image_certificates_arg_and_envar(self): """Tests that if both the environment variable and argument are @@ -2237,7 +2237,7 @@ def test_lock_pre_v273(self): self.run_command, 'lock sample-server --reason zombies', api_version='2.72') - self.assertIn('2', six.text_type(exp)) + self.assertIn('2', str(exp)) def test_lock_v273(self): self.run_command('lock sample-server', @@ -2509,7 +2509,7 @@ def test_server_topology_pre278(self): self.run_command, 'server-topology 1234', api_version='2.77') - self.assertIn('2', six.text_type(exp)) + self.assertIn('2', str(exp)) def test_refresh_network(self): self.run_command('refresh-network 1234') @@ -2787,7 +2787,7 @@ def test_aggregate_update_without_availability_zone_and_name(self): 'aggregate-update test') self.assertIn("Either '--name ' or '--availability-zone " "' must be specified.", - six.text_type(ex)) + str(ex)) def test_aggregate_set_metadata_add_by_id(self): out, err = self.run_command('aggregate-set-metadata 3 foo=bar') @@ -4009,7 +4009,7 @@ def test_instance_action_list_with_changes_since_invalid_value_v258(self): exceptions.CommandError, self.run_command, 'instance-action-list sample-server --changes-since 0123456789', api_version='2.58') - self.assertIn('Invalid changes-since value', six.text_type(ex)) + self.assertIn('Invalid changes-since value', str(ex)) def test_instance_action_list_changes_before_pre_v266_not_allowed(self): cmd = 'instance-action-list sample-server --changes-before ' \ @@ -4031,7 +4031,7 @@ def test_instance_action_list_with_changes_before_invalid_value_v266(self): exceptions.CommandError, self.run_command, 'instance-action-list sample-server --changes-before 0123456789', api_version='2.66') - self.assertIn('Invalid changes-before value', six.text_type(ex)) + self.assertIn('Invalid changes-before value', str(ex)) def test_instance_usage_audit_log(self): self.run_command('instance-usage-audit-log') @@ -4096,7 +4096,7 @@ def test_migration_list_with_changes_since_invalid_value_v259(self): ex = self.assertRaises(exceptions.CommandError, self.run_command, 'migration-list --changes-since 0123456789', api_version='2.59') - self.assertIn('Invalid changes-since value', six.text_type(ex)) + self.assertIn('Invalid changes-since value', str(ex)) def test_migration_list_with_changes_before_v266(self): self.run_command('migration-list --changes-before 2016-02-29T06:23:22', @@ -4108,7 +4108,7 @@ def test_migration_list_with_changes_before_invalid_value_v266(self): ex = self.assertRaises(exceptions.CommandError, self.run_command, 'migration-list --changes-before 0123456789', api_version='2.66') - self.assertIn('Invalid changes-before value', six.text_type(ex)) + self.assertIn('Invalid changes-before value', str(ex)) def test_migration_list_with_changes_before_pre_v266_not_allowed(self): cmd = 'migration-list --changes-before 2016-02-29T06:23:22' @@ -4284,7 +4284,7 @@ def test_keypair_import_x509(self): api_version="2.2") def test_keypair_stdin(self): - with mock.patch('sys.stdin', six.StringIO('FAKE_PUBLIC_KEY')): + with mock.patch('sys.stdin', io.StringIO('FAKE_PUBLIC_KEY')): self.run_command('keypair-add --pub-key - test', api_version="2.2") self.assert_called( 'POST', '/os-keypairs', { @@ -4360,7 +4360,7 @@ def test_create_server_group_with_invalid_value(self): 'server-group-create sg1 anti-affinity ' '--rule max_server_per_host=foo', api_version='2.64') self.assertIn("Invalid 'max_server_per_host' value: foo", - six.text_type(result)) + str(result)) def test_create_server_group_with_rules_pre_264(self): self.assertRaises(SystemExit, self.run_command, diff --git a/novaclient/tests/unit/v2/test_usage.py b/novaclient/tests/unit/v2/test_usage.py index 900842334..c7473aa9d 100644 --- a/novaclient/tests/unit/v2/test_usage.py +++ b/novaclient/tests/unit/v2/test_usage.py @@ -13,8 +13,6 @@ import datetime -import six - from novaclient import api_versions from novaclient.tests.unit import utils from novaclient.tests.unit.v2 import fakes @@ -60,8 +58,8 @@ def test_usage_get(self): self.assertIsInstance(u, usage.Usage) def test_usage_class_get(self): - start = six.u('2012-01-22T19:48:41.750722') - stop = six.u('2012-01-22T19:48:41.750722') + start = '2012-01-22T19:48:41.750722' + stop = '2012-01-22T19:48:41.750722' info = {'tenant_id': 'tenantfoo', 'start': start, 'stop': stop} diff --git a/novaclient/tests/unit/v2/test_volumes.py b/novaclient/tests/unit/v2/test_volumes.py index fbc55ddf7..d18f84664 100644 --- a/novaclient/tests/unit/v2/test_volumes.py +++ b/novaclient/tests/unit/v2/test_volumes.py @@ -14,7 +14,6 @@ # under the License. import mock -import six from novaclient import api_versions from novaclient.tests.unit import utils @@ -156,4 +155,4 @@ def test_create_server_volume_with_delete_on_termination_pre_v279(self): TypeError, self.cs.volumes.create_server_volume, "1234", volume_id='15e59938-07d5-11e1-90e3-e3dffe0c5983', delete_on_termination=True) - self.assertIn('delete_on_termination', six.text_type(ex)) + self.assertIn('delete_on_termination', str(ex)) diff --git a/novaclient/utils.py b/novaclient/utils.py index 05a86d92a..fba708b55 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -16,13 +16,12 @@ import re import textwrap import time +from urllib import parse from oslo_serialization import jsonutils from oslo_utils import encodeutils from oslo_utils import uuidutils import prettytable -import six -from six.moves.urllib import parse from novaclient import exceptions from novaclient.i18n import _ @@ -149,7 +148,7 @@ def print_list(objs, fields, formatters={}, sortby_index=None): if data is None: data = '-' # '\r' would break the table, so remove it. - data = six.text_type(data).replace("\r", "") + data = str(data).replace("\r", "") row.append(data) pt.add_row(row) @@ -158,8 +157,7 @@ def print_list(objs, fields, formatters={}, sortby_index=None): else: result = encodeutils.safe_encode(pt.get_string()) - if six.PY3: - result = result.decode() + result = result.decode() print(result) @@ -196,7 +194,7 @@ def flatten_dict(data): data = data.copy() # Try and decode any nested JSON structures. for key, value in data.items(): - if isinstance(value, six.string_types): + if isinstance(value, str): try: data[key] = jsonutils.loads(value) except ValueError: @@ -213,10 +211,10 @@ def print_dict(d, dict_property="Property", dict_value="Value", wrap=0): if isinstance(v, (dict, list)): v = jsonutils.dumps(v, ensure_ascii=False) if wrap > 0: - v = textwrap.fill(six.text_type(v), wrap) + v = textwrap.fill(str(v), wrap) # if value has a newline, add in multiple rows # e.g. fault with stacktrace - if v and isinstance(v, six.string_types) and (r'\n' in v or '\r' in v): + if v and isinstance(v, str) and (r'\n' in v or '\r' in v): # '\r' would break the table, so remove it. if '\r' in v: v = v.replace('\r', '') @@ -232,8 +230,7 @@ def print_dict(d, dict_property="Property", dict_value="Value", wrap=0): result = encodeutils.safe_encode(pt.get_string()) - if six.PY3: - result = result.decode() + result = result.decode() print(result) @@ -252,8 +249,7 @@ def find_resource(manager, name_or_id, wrap_exception=True, **find_args): try: tmp_id = encodeutils.safe_encode(name_or_id) - if six.PY3: - tmp_id = tmp_id.decode() + tmp_id = tmp_id.decode() if uuidutils.is_uuid_like(tmp_id): return manager.get(tmp_id) @@ -383,7 +379,7 @@ def do_action_on_many(action, resources, success_msg, error_msg): print(success_msg % _get_resource_string(resource)) except Exception as e: failure_flag = True - print(encodeutils.safe_encode(six.text_type(e))) + print(encodeutils.safe_encode(str(e))) if failure_flag: raise exceptions.CommandError(error_msg) diff --git a/novaclient/v2/hypervisors.py b/novaclient/v2/hypervisors.py index e6f5038c8..c705dc659 100644 --- a/novaclient/v2/hypervisors.py +++ b/novaclient/v2/hypervisors.py @@ -17,9 +17,7 @@ Hypervisors interface """ -from oslo_utils import encodeutils -import six -from six.moves.urllib import parse +from urllib import parse from novaclient import api_versions from novaclient import base @@ -92,8 +90,6 @@ def search(self, hypervisor_match, servers=False, detailed=False): # Starting with microversion 2.53, the /servers and /search routes are # deprecated and we get the same results using GET /os-hypervisors # using query parameters for the hostname pattern and servers. - if six.PY2: - hypervisor_match = encodeutils.safe_encode(hypervisor_match) if self.api_version >= api_versions.APIVersion('2.53'): url = ('/os-hypervisors%s?hypervisor_hostname_pattern=%s' % ('/detail' if detailed else '', diff --git a/novaclient/v2/instance_usage_audit_log.py b/novaclient/v2/instance_usage_audit_log.py index 9ada06c1e..83db8c8e6 100644 --- a/novaclient/v2/instance_usage_audit_log.py +++ b/novaclient/v2/instance_usage_audit_log.py @@ -13,9 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. -from oslo_utils import encodeutils -import six -from six.moves.urllib import parse +from urllib import parse from novaclient import base @@ -34,8 +32,6 @@ def get(self, before=None): before which to list usage audits. """ if before: - if six.PY2: - before = encodeutils.safe_encode(before) return self._get('/os-instance_usage_audit_log/%s' % parse.quote(before, safe=''), 'instance_usage_audit_log') diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index 1351189bf..ff8c166ec 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -21,10 +21,7 @@ import base64 import collections - -from oslo_utils import encodeutils -import six -from six.moves.urllib import parse +from urllib import parse from novaclient import api_versions from novaclient import base @@ -690,17 +687,11 @@ def transform_userdata(userdata): # NOTE(melwitt): Text file data is converted to bytes prior to # base64 encoding. The utf-8 encoding will fail for binary files. - if six.PY3: - try: - userdata = userdata.encode("utf-8") - except AttributeError: - # In python 3, 'bytes' object has no attribute 'encode' - pass - else: - try: - userdata = encodeutils.safe_encode(userdata) - except UnicodeDecodeError: - pass + try: + userdata = userdata.encode("utf-8") + except AttributeError: + # In python 3, 'bytes' object has no attribute 'encode' + pass return base64.b64encode(userdata).decode('utf-8') @@ -761,7 +752,7 @@ def _boot(self, response_key, name, image, flavor, else: data = file_or_string - if six.PY3 and isinstance(data, str): + if isinstance(data, str): data = data.encode('utf-8') cont = base64.b64encode(data).decode('utf-8') personality.append({ @@ -791,7 +782,7 @@ def _boot(self, response_key, name, image, flavor, if nics is not None: # With microversion 2.37+ nics can be an enum of 'auto' or 'none' # or a list of dicts. - if isinstance(nics, six.string_types): + if isinstance(nics, str): all_net_data = nics else: # NOTE(tr3buchet): nics can be an empty list @@ -899,7 +890,7 @@ def list(self, detailed=True, search_opts=None, marker=None, limit=None, for opt, val in search_opts.items(): # support locked=False from 2.73 microversion if val or (opt == 'locked' and val is False): - if isinstance(val, six.text_type): + if isinstance(val, str): val = val.encode('utf-8') qparams[opt] = val diff --git a/novaclient/v2/services.py b/novaclient/v2/services.py index f3d1255dc..7adbf1376 100644 --- a/novaclient/v2/services.py +++ b/novaclient/v2/services.py @@ -14,9 +14,10 @@ # under the License. """ -service interface +Service interface. """ -from six.moves import urllib + +from urllib import parse from novaclient import api_versions from novaclient import base @@ -48,7 +49,7 @@ def list(self, host=None, binary=None): if binary: filters.append(("binary", binary)) if filters: - url = "%s?%s" % (url, urllib.parse.urlencode(filters)) + url = "%s?%s" % (url, parse.urlencode(filters)) return self._list(url, "services") @api_versions.wraps("2.0", "2.10") diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 0f8b5ce6f..d8de74e8f 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -31,7 +31,6 @@ from oslo_utils import netutils from oslo_utils import strutils from oslo_utils import timeutils -import six import novaclient from novaclient import api_versions @@ -478,7 +477,7 @@ def _boot(cs, args): # NOTE(vish): multiple copies of the same hint will # result in a list of values if key in hints: - if isinstance(hints[key], six.string_types): + if isinstance(hints[key], str): hints[key] = [hints[key]] hints[key] += [value] else: @@ -2447,7 +2446,7 @@ def _print_server(cs, args, server=None, wrap=0): try: networks = server.networks except Exception as e: - raise exceptions.CommandError(six.text_type(e)) + raise exceptions.CommandError(str(e)) info = server.to_dict() for network_label, address_list in networks.items(): @@ -2556,7 +2555,7 @@ def _find_server(cs, server, raise_if_notfound=True, **find_args): return utils.find_resource(cs.servers, server, wrap_exception=False) except exceptions.NoUniqueMatch as e: - raise exceptions.CommandError(six.text_type(e)) + raise exceptions.CommandError(str(e)) except exceptions.NotFound: # The server can be deleted return server @@ -2567,7 +2566,7 @@ def _find_image(cs, image): try: return cs.glance.find_image(image) except (exceptions.NotFound, exceptions.NoUniqueMatch) as e: - raise exceptions.CommandError(six.text_type(e)) + raise exceptions.CommandError(str(e)) def _find_images(cs, images): @@ -2575,7 +2574,7 @@ def _find_images(cs, images): try: return cs.glance.find_images(images) except (exceptions.NotFound, exceptions.NoUniqueMatch) as e: - raise exceptions.CommandError(six.text_type(e)) + raise exceptions.CommandError(str(e)) def _find_flavor(cs, flavor): @@ -2591,7 +2590,7 @@ def _find_network_id(cs, net_name): try: return cs.neutron.find_network(net_name).id except (exceptions.NotFound, exceptions.NoUniqueMatch) as e: - raise exceptions.CommandError(six.text_type(e)) + raise exceptions.CommandError(str(e)) def _print_volume(volume): diff --git a/novaclient/v2/versions.py b/novaclient/v2/versions.py index dd157d9f2..b9895f911 100644 --- a/novaclient/v2/versions.py +++ b/novaclient/v2/versions.py @@ -16,7 +16,7 @@ version interface """ -from six.moves import urllib +from urllib import parse from novaclient import base from novaclient import exceptions as exc @@ -79,7 +79,7 @@ def list(self): """List all versions.""" endpoint = self.api.client.get_endpoint() - url = urllib.parse.urlparse(endpoint) + url = parse.urlparse(endpoint) # NOTE(andreykurilin): endpoint URL has at least 3 formats: # 1. the classic (legacy) endpoint: # http://{host}:{optional_port}/v{2 or 2.1}/{project-id} diff --git a/requirements.txt b/requirements.txt index 9371e3274..609ea42b0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,5 +9,4 @@ oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0 oslo.utils>=3.33.0 # Apache-2.0 PrettyTable<0.8,>=0.7.2 # BSD simplejson>=3.5.1 # MIT -six>=1.10.0 # MIT Babel!=2.4.0,>=2.3.4 # BSD From 8f50f84981f354e0dbac0af843d7d936c319b77f Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 20 Feb 2020 09:46:55 +0000 Subject: [PATCH 1571/1705] Bump to hacking 2.x This is compatible with Python 3-only syntax. Change-Id: I462f4242b9a5f8d8cd6b0cb3d328dfd9d93ba200 Signed-off-by: Stephen Finucane --- novaclient/tests/functional/v2/test_servers.py | 2 +- novaclient/tests/unit/test_shell.py | 2 +- novaclient/tests/unit/utils.py | 1 + novaclient/tests/unit/v2/test_shell.py | 2 +- novaclient/v2/shell.py | 9 ++++----- test-requirements.txt | 2 +- tox.ini | 4 ++-- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/novaclient/tests/functional/v2/test_servers.py b/novaclient/tests/functional/v2/test_servers.py index a25dd2aef..3de668dd4 100644 --- a/novaclient/tests/functional/v2/test_servers.py +++ b/novaclient/tests/functional/v2/test_servers.py @@ -288,7 +288,7 @@ def _validate_flavor_details(self, flavor_details, server_details): flavor_details, key) server_flavor_val = self._get_value_from_the_table( server_details, flavor_key_mapping[key]) - if key is "swap" and flavor_val is "": + if key == "swap" and flavor_val == "": # "flavor-show" displays zero swap as empty string. flavor_val = '0' self.assertEqual(flavor_val, server_flavor_val) diff --git a/novaclient/tests/unit/test_shell.py b/novaclient/tests/unit/test_shell.py index 9ef6141f7..2c6af3b41 100644 --- a/novaclient/tests/unit/test_shell.py +++ b/novaclient/tests/unit/test_shell.py @@ -433,7 +433,7 @@ def _test_help(self, command, required=None): if required is None: required = [ '.*?^usage: ', - '.*?^\s+set-password\s+Change the admin password', + '.*?^\\s+set-password\\s+Change the admin password', '.*?^See "nova help COMMAND" for help on a specific command', ] stdout, stderr = self.shell(command) diff --git a/novaclient/tests/unit/utils.py b/novaclient/tests/unit/utils.py index 229d999ec..f987fc3e5 100644 --- a/novaclient/tests/unit/utils.py +++ b/novaclient/tests/unit/utils.py @@ -44,6 +44,7 @@ def wrapper(_self, name): mock.Mock.__getattr__ = raise_for_invalid_assert_calls( mock.Mock.__getattr__) + # NOTE(gibi): needs to be called only once at import time # to patch the mock lib _patch_mock_to_raise_for_invalid_assert_calls() diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 6c3b3cfa0..2473061d5 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -1062,7 +1062,7 @@ def test_boot_nics_net_name_neutron_blank(self): cmd = ('boot --image %s --flavor 1 ' '--nic net-name=blank some-server' % FAKE_UUID_1) # this should raise a multiple matches error - msg = 'No Network matching blank\..*' + msg = 'No Network matching blank\\..*' with testtools.ExpectedException(exceptions.CommandError, msg): self.run_command(cmd) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index d8de74e8f..773e6ce08 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -3112,11 +3112,10 @@ def __init__(self, name, used, max, other): limit_list = [] for name in limit_names: - l = Limit(name, - used.get(name, "-"), - max.get(name, "-"), - other.get(name, "-")) - limit_list.append(l) + limit_list.append(Limit( + name, used.get(name, '-'), max.get(name, '-'), + other.get(name, '-'), + )) utils.print_list(limit_list, columns) diff --git a/test-requirements.txt b/test-requirements.txt index 88cad0e46..a11038e0f 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,7 +1,7 @@ # 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. -hacking>=1.1.0,<1.2.0 # Apache-2.0 +hacking>=2.0.0,<2.1.0 # Apache-2.0 bandit>=1.1.0 # Apache-2.0 coverage!=4.4,>=4.0 # Apache-2.0 diff --git a/tox.ini b/tox.ini index a5122f9c0..fc1c9c999 100644 --- a/tox.ini +++ b/tox.ini @@ -88,8 +88,8 @@ commands = # # Following checks are ignored on purpose. # -# Additional checks are also ignored on purpose: F811, F821 -ignore = E731,F811,F821,H404,H405 +# Additional checks are also ignored on purpose: F811, F821, W504 +ignore = E731,F811,F821,H404,H405,W504 show-source = true exclude=.venv,.git,.tox,dist,*lib/python*,*egg,build,doc/source/conf.py,releasenotes From b9a7e03074cbaacc3f270b2b8228a5b85350a2de Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 21 Feb 2020 09:56:37 +0000 Subject: [PATCH 1572/1705] Random cleanups Remove some cruft from Sphinx config files, drop the use of 'u' prefixed strings, which are unnecessary in Python 3, and generally tidy stuff up. Change-Id: Ib0f33576e160ec842d7fc82b4fcfee99829623d7 Signed-off-by: Stephen Finucane --- HACKING.rst | 30 +-- doc/source/conf.py | 24 +- novaclient/tests/unit/test_utils.py | 8 +- novaclient/tests/unit/v2/test_hypervisors.py | 2 +- .../unit/v2/test_instance_usage_audit_log.py | 2 +- novaclient/tests/unit/v2/test_servers.py | 2 +- releasenotes/source/conf.py | 216 +----------------- 7 files changed, 18 insertions(+), 266 deletions(-) diff --git a/HACKING.rst b/HACKING.rst index 5a5d01d7f..b5653449b 100644 --- a/HACKING.rst +++ b/HACKING.rst @@ -5,30 +5,12 @@ Nova Client Style Commandments https://docs.openstack.org/hacking/latest - Step 2: Read on - Nova Client Specific Commandments --------------------------------- None so far Text encoding ------------- -- All text within python code should be of type 'unicode'. - - WRONG: - - >>> s = 'foo' - >>> s - 'foo' - >>> type(s) - - - RIGHT: - - >>> u = u'foo' - >>> u - u'foo' - >>> type(u) - - Transitions between internal unicode and external strings should always be immediately and explicitly encoded or decoded. @@ -36,13 +18,13 @@ Text encoding - All external text that is not explicitly encoded (database storage, commandline arguments, etc.) should be presumed to be encoded as utf-8. - WRONG: + Wrong:: mystring = infile.readline() myreturnstring = do_some_magic_with(mystring) outfile.write(myreturnstring) - RIGHT: + Right:: mystring = infile.readline() mytext = s.decode('utf-8') @@ -52,8 +34,8 @@ Text encoding Running Tests ------------- -The testing system is based on a combination of tox and testr. If you just -want to run the whole suite, run `tox` and all will be fine. However, if + +The testing system is based on a combination of tox and stestr. If you just +want to run the whole suite, run ``tox`` and all will be fine. However, if you'd like to dig in a bit more, you might want to learn some things about -testr itself. A basic walkthrough for OpenStack can be found at -http://wiki.openstack.org/testr +stestr itself. diff --git a/doc/source/conf.py b/doc/source/conf.py index 5e69e49c2..515e754df 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -33,12 +33,6 @@ # directive. autoclass_content = 'both' -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix of source filenames. -source_suffix = '.rst' - # The master toctree document. master_doc = 'index' @@ -48,19 +42,6 @@ bug_tag = 'doc' copyright = 'OpenStack Contributors' -# List of directories, relative to source directory, that shouldn't be searched -# for source files. -exclude_trees = [] - -# If true, '()' will be appended to :func: etc. cross-reference text. -add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -add_module_names = True - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' # -- Options for HTML output -------------------------------------------------- @@ -72,11 +53,12 @@ # robots.txt. html_extra_path = ['_extra'] + # -- Options for LaTeX output ------------------------------------------------- latex_documents = [ - ('index', 'doc-python-novaclient.tex', u'python-novaclient Documentation', - u'OpenStack Foundation', 'manual'), + ('index', 'doc-python-novaclient.tex', 'python-novaclient Documentation', + 'OpenStack Foundation', 'manual'), ] latex_elements = { diff --git a/novaclient/tests/unit/test_utils.py b/novaclient/tests/unit/test_utils.py index 0fb65c75c..4a755a99b 100644 --- a/novaclient/tests/unit/test_utils.py +++ b/novaclient/tests/unit/test_utils.py @@ -237,9 +237,9 @@ def test_print_list_sort_by_integer(self): @mock.patch('sys.stdout', io.StringIO()) def test_print_unicode_list(self): - objs = [_FakeResult("k", u'\u2026')] + objs = [_FakeResult("k", '\u2026')] utils.print_list(objs, ["Name", "Value"]) - s = u'\u2026' + s = '\u2026' self.assertEqual('+------+-------+\n' '| Name | Value |\n' '+------+-------+\n' @@ -314,9 +314,9 @@ def test_print_large_dict_list(self): @mock.patch('sys.stdout', io.StringIO()) def test_print_unicode_dict(self): - dict = {'k': u'\u2026'} + dict = {'k': '\u2026'} utils.print_dict(dict) - s = u'\u2026' + s = '\u2026' self.assertEqual('+----------+-------+\n' '| Property | Value |\n' '+----------+-------+\n' diff --git a/novaclient/tests/unit/v2/test_hypervisors.py b/novaclient/tests/unit/v2/test_hypervisors.py index 3907687fa..1c216b648 100644 --- a/novaclient/tests/unit/v2/test_hypervisors.py +++ b/novaclient/tests/unit/v2/test_hypervisors.py @@ -109,7 +109,7 @@ def test_hypervisor_search(self): self.compare_to_expected(expected[idx], hyper) def test_hypervisor_search_unicode(self): - hypervisor_match = u'\\u5de5\\u4f5c' + hypervisor_match = '\\u5de5\\u4f5c' if self.cs.api_version >= api_versions.APIVersion('2.53'): self.assertRaises(exceptions.BadRequest, self.cs.hypervisors.search, diff --git a/novaclient/tests/unit/v2/test_instance_usage_audit_log.py b/novaclient/tests/unit/v2/test_instance_usage_audit_log.py index f3cb65cd2..61b4ce734 100644 --- a/novaclient/tests/unit/v2/test_instance_usage_audit_log.py +++ b/novaclient/tests/unit/v2/test_instance_usage_audit_log.py @@ -38,6 +38,6 @@ def test_instance_usage_audit_log_with_before(self): '/os-instance_usage_audit_log/2016-12-10%2013%3A59%3A59.999999') def test_instance_usage_audit_log_with_before_unicode(self): - before = u'\\u5de5\\u4f5c' + before = '\\u5de5\\u4f5c' self.assertRaises(exceptions.BadRequest, self.cs.instance_usage_audit_log.get, before) diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index 6ad9df9ad..d65ec145c 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -55,7 +55,7 @@ def test_list_servers(self): self.assertIsInstance(s, servers.Server) def test_filter_servers_unicode(self): - sl = self.cs.servers.list(search_opts={'name': u't€sting'}) + sl = self.cs.servers.list(search_opts={'name': 't€sting'}) self.assert_request_id(sl, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/servers/detail?name=t%E2%82%ACsting') for s in sl: diff --git a/releasenotes/source/conf.py b/releasenotes/source/conf.py index 799455bf5..a328a1af1 100644 --- a/releasenotes/source/conf.py +++ b/releasenotes/source/conf.py @@ -1,27 +1,9 @@ # -*- coding: utf-8 -*- # -# Nova Client Release Notes documentation build configuration file, created by -# sphinx-quickstart on Mon Nov 23 20:38:38 2015. -# -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) +# python-novaclient Release Notes documentation build configuration file # -- General configuration ------------------------------------------------ -# If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' - # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. @@ -30,59 +12,9 @@ 'openstackdocstheme', ] -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix of source filenames. -source_suffix = '.rst' - -# The encoding of source files. -#source_encoding = 'utf-8-sig' - # The master toctree document. master_doc = 'index' -# General information about the project. -copyright = u'2015, Nova developers' - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -#language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -#today = '' -# Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -exclude_patterns = [] - -# The reST default role (used for this markup: `text`) to use for all -# documents. -#default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -#add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -#show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] - -# If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False - # -- Options for HTML output ---------------------------------------------- @@ -90,151 +22,7 @@ # a list of builtin themes. html_theme = 'openstackdocs' -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -#html_theme_options = {} - -# Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] - -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -#html_title = None - -# A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -#html_logo = None - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -#html_favicon = None - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] - -# Add any extra paths that contain custom files (such as robots.txt or -# .htaccess) here, relative to this directory. These files are copied -# directly to the root of the documentation. -#html_extra_path = [] - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -#html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -#html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -#html_additional_pages = {} - -# If false, no module index is generated. -#html_domain_indices = True - -# If false, no index is generated. -#html_use_index = True - -# If true, the index is split into individual pages for each letter. -#html_split_index = False - -# If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -#html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None - -# Output file base name for HTML help builder. -htmlhelp_basename = 'NovaClientReleaseNotestdoc' - - -# -- Options for LaTeX output --------------------------------------------- - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - ('index', 'PythonNovaClient.tex', u'Nova Client Release Notes Documentation', - u'Nova developers', 'manual'), -] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -#latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -#latex_use_parts = False - -# If true, show page references after internal links. -#latex_show_pagerefs = False - -# If true, show URL addresses after external links. -#latex_show_urls = False - -# Documents to append as an appendix to all manuals. -#latex_appendices = [] - -# If false, no module index is generated. -#latex_domain_indices = True - - -# -- Options for manual page output --------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - ('index', 'pythonnovaclient', u'Nova Client Release Notes Documentation', - [u'Nova developers'], 1) -] - -# If true, show URL addresses after external links. -#man_show_urls = False - - -# -- Options for Texinfo output ------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - ('index', 'PythonNovaClient', u'Nova Client Release Notes Documentation', - u'Nova developers', 'PythonNovaClient', 'One line description of project.', - 'Miscellaneous'), -] - -# Documents to append as an appendix to all manuals. -#texinfo_appendices = [] - -# If false, no module index is generated. -#texinfo_domain_indices = True - -# How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' - -# If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False # -- Options for Internationalization output ------------------------------ + locale_dirs = ['locale/'] From 03dca4bc823c82054869dfaf6925d5e1e068ac51 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 20 Feb 2020 11:01:40 +0000 Subject: [PATCH 1573/1705] Don't print user_data for 'nova show' User data is a blob of data that the user can specify when they launch an instance. It's generally binary data, which means it's not something we should show by default on the CLI. Stop doing that. Change-Id: If8f6cc040d0077a7902a5fd425e67d74d7925a46 Signed-off-by: Stephen Finucane Closes-Bug: #1669140 --- novaclient/tests/functional/v2/test_extended_attributes.py | 3 +-- novaclient/v2/shell.py | 1 + releasenotes/notes/bug-1669140-c21d045491201352.yaml | 5 +++++ 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/bug-1669140-c21d045491201352.yaml diff --git a/novaclient/tests/functional/v2/test_extended_attributes.py b/novaclient/tests/functional/v2/test_extended_attributes.py index b585a34b1..bf06875c6 100644 --- a/novaclient/tests/functional/v2/test_extended_attributes.py +++ b/novaclient/tests/functional/v2/test_extended_attributes.py @@ -32,8 +32,7 @@ def test_extended_server_attributes(self): 'OS-EXT-SRV-ATTR:ramdisk_id', 'OS-EXT-SRV-ATTR:kernel_id', 'OS-EXT-SRV-ATTR:hostname', - 'OS-EXT-SRV-ATTR:root_device_name', - 'OS-EXT-SRV-ATTR:user_data']: + 'OS-EXT-SRV-ATTR:root_device_name']: self._get_value_from_the_table(table, attr) # Check that attribute given below also exists in 'nova show' table # as a key (first column) of table dict diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 773e6ce08..e86add103 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -2502,6 +2502,7 @@ def _print_server(cs, args, server=None, wrap=0): info.pop('links', None) info.pop('addresses', None) + info.pop('OS-EXT-SRV-ATTR:user_data', None) utils.print_dict(info, wrap=wrap) diff --git a/releasenotes/notes/bug-1669140-c21d045491201352.yaml b/releasenotes/notes/bug-1669140-c21d045491201352.yaml new file mode 100644 index 000000000..1c950c1f5 --- /dev/null +++ b/releasenotes/notes/bug-1669140-c21d045491201352.yaml @@ -0,0 +1,5 @@ +--- +issues: + - | + The ``nova show`` command will no longer output the ``user_data`` column. + This is traditionally binary data of limited value from a CLI perspective. From 0ab746b4d12f469484c30cd11095a79135bac63b Mon Sep 17 00:00:00 2001 From: Takashi Natsume Date: Thu, 12 Mar 2020 23:34:44 +0900 Subject: [PATCH 1574/1705] Microversion 2.82 - nova cyborg interaction This patch adds support for microversion 2.82 "Define Cyborg ARQ binding notification event" (*). *: I7a626544d8221dc0eeb5672986ca897ce4718be8 No changes are required in the novaclient side, so just increment the microversion. Change-Id: I3a8a2f8b5ced5519082f0b7609702d4f62be88ac Implements: blueprint nova-cyborg-interaction --- novaclient/__init__.py | 2 +- novaclient/tests/unit/v2/test_shell.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 6855e2f21..773a4b709 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.81") +API_MAX_VERSION = api_versions.APIVersion("2.82") diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 2473061d5..dcd487076 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -4458,6 +4458,7 @@ def test_versions(self): 75, # There are no version-wrapped shell method changes for this. 76, # doesn't require any changes in novaclient. 77, # There are no version-wrapped shell method changes for this. + 82, # doesn't require any changes in novaclient. ]) versions_supported = set(range(0, novaclient.API_MAX_VERSION.ver_minor + 1)) From 0722e80886285f7e5630d5d4012c42806ebc53c2 Mon Sep 17 00:00:00 2001 From: Andreas Jaeger Date: Tue, 31 Mar 2020 12:20:38 +0200 Subject: [PATCH 1575/1705] Update to hacking 3.0 Hacking 3.0 was just released with minor changes, update to the new version. Change-Id: Id3c08f718899343b3ec12a8b53e1e08a5eba42f8 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index a11038e0f..69b11b873 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,7 +1,7 @@ # 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. -hacking>=2.0.0,<2.1.0 # Apache-2.0 +hacking>=3.0,<3.1.0 # Apache-2.0 bandit>=1.1.0 # Apache-2.0 coverage!=4.4,>=4.0 # Apache-2.0 From 9ee74d3ac6d26018d0161e24fe8c0f3f29b21c06 Mon Sep 17 00:00:00 2001 From: Victor Coutellier Date: Wed, 11 Mar 2020 12:50:13 +0100 Subject: [PATCH 1576/1705] Microversion 2.83 - Add more filters for the nova list command Add these new filters, admin-only until microversion 2.82: - availability-zone - config-drive - key-name - power-state - task-state - vm-state - progress Existing user filter will be available to non admin since microversion 2.83. Part of blueprint non-admin-filter-instance-by-az Change-Id: Id2b5e600c0a41790830823031b20983808cb5ace --- doc/source/cli/nova.rst | 37 +++++++++- novaclient/__init__.py | 2 +- novaclient/tests/unit/v2/test_servers.py | 7 ++ novaclient/tests/unit/v2/test_shell.py | 50 ++++++++++++- novaclient/v2/servers.py | 6 ++ novaclient/v2/shell.py | 71 ++++++++++++++++++- ...-filter-to-nova-list-831dcbb34420fb29.yaml | 18 +++++ 7 files changed, 183 insertions(+), 8 deletions(-) create mode 100644 releasenotes/notes/add-filter-to-nova-list-831dcbb34420fb29.yaml diff --git a/doc/source/cli/nova.rst b/doc/source/cli/nova.rst index 84c441bd7..48b1dbda2 100644 --- a/doc/source/cli/nova.rst +++ b/doc/source/cli/nova.rst @@ -2223,7 +2223,11 @@ nova list [--tenant []] [--user []] [--deleted] [--fields ] [--minimal] [--sort [:]] [--marker ] - [--limit ] [--changes-since ] + [--limit ] [--availability-zone ] + [--key-name ] [--config-drive ] + [--progress ] [--vm-state ] + [--task-state ] [--power-state ] + [--changes-since ] [--changes-before ] [--tags ] [--tags-any ] [--not-tags ] [--not-tags-any ] @@ -2274,7 +2278,7 @@ present in the failure domain. ``--user []`` Display information from single user (Admin - only). + only until microversion 2.82). ``--deleted`` Only display deleted servers (Admin only). @@ -2304,6 +2308,35 @@ present in the failure domain. Nova API, limit 'CONF.api.max_limit' will be used instead. +``--availability-zone `` + Display servers based on their availability zone + (Admin only until microversion 2.82). + +``--key-name `` + Display servers based on their keypair name + (Admin only until microversion 2.82). + +``--config-drive `` + Display servers based on their config_drive value + The value must be a boolean. (Admin only until + microversion 2.82). + +``--progress `` + Display servers based on their progress value + (Admin only until microversion 2.82). + +``--vm-state `` + Display servers based on their vm_state value + (Admin only until microversion 2.82). + +``--task-state `` + Display servers based on their task_state value + (Admin only until microversion 2.82). + +``--power-state `` + Display servers based on their power_state value + (Admin only until microversion 2.82). + ``--changes-since `` List only servers changed later or equal to a certain point of time. The provided time should diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 773a4b709..f86d680fb 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.82") +API_MAX_VERSION = api_versions.APIVersion("2.83") diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index d65ec145c..0d3ca4b2c 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -85,6 +85,13 @@ def test_filter_servers_unlocked(self): self.assertIn("'locked' argument is only allowed since " "microversion 2.73.", str(e)) + def test_filter_without_config_drive(self): + sl = self.cs.servers.list(search_opts={'config_drive': None}) + self.assert_request_id(sl, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('GET', '/servers/detail') + for s in sl: + self.assertIsInstance(s, servers.Server) + def test_list_servers_undetailed(self): sl = self.cs.servers.list(detailed=False) self.assert_request_id(sl, fakes.FAKE_REQUEST_ID_LIST) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index dcd487076..c8d3dc9be 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -1704,7 +1704,7 @@ def test_list_by_user(self): self.run_command('list --user fake_user') self.assert_called( 'GET', - '/servers/detail?all_tenants=1&user_id=fake_user') + '/servers/detail?user_id=fake_user') def test_list_with_single_sort_key_no_dir(self): self.run_command('list --sort 1') @@ -1838,6 +1838,51 @@ def test_list_with_changes_before_pre_v266_not_allowed(self): 'list --changes-before 2016-02-29T06:23:22', api_version='2.65') + def test_list_with_availability_zone(self): + self.run_command('list --availability-zone nova') + self.assert_called('GET', '/servers/detail?availability_zone=nova') + + def test_list_with_key_name(self): + self.run_command('list --key-name my_key') + self.assert_called('GET', '/servers/detail?key_name=my_key') + + def test_list_with_config_drive_passing_through_any_value(self): + self.run_command('list --config-drive True') + self.assert_called('GET', '/servers/detail?config_drive=True') + self.run_command('list --config-drive some-random-string') + self.assert_called( + 'GET', '/servers/detail?config_drive=some-random-string') + # This form is special for the test env to pass through an empty string + # as a parameter. The real CLI call would look like + # list --config drive '' + self.run_command(['list', '--config-drive', '']) + self.assert_called( + 'GET', '/servers/detail?config_drive=') + + def test_list_with_progress(self): + self.run_command('list --progress 100') + self.assert_called('GET', '/servers/detail?progress=100') + + def test_list_with_0_progress(self): + self.run_command('list --progress 0') + self.assert_called('GET', '/servers/detail?progress=0') + + def test_list_with_vm_state(self): + self.run_command('list --vm-state active') + self.assert_called('GET', '/servers/detail?vm_state=active') + + def test_list_with_task_state(self): + self.run_command('list --task-state reboot_started') + self.assert_called('GET', '/servers/detail?task_state=reboot_started') + + def test_list_with_power_state(self): + self.run_command('list --power-state 1') + self.assert_called('GET', '/servers/detail?power_state=1') + + def test_list_with_power_state_filter_for_0_state(self): + self.run_command('list --power-state 0') + self.assert_called('GET', '/servers/detail?power_state=0') + def test_list_fields_redundant(self): output, _err = self.run_command('list --fields id,status,status') header = output.splitlines()[1] @@ -4458,7 +4503,8 @@ def test_versions(self): 75, # There are no version-wrapped shell method changes for this. 76, # doesn't require any changes in novaclient. 77, # There are no version-wrapped shell method changes for this. - 82, # doesn't require any changes in novaclient. + 82, # There are no version-wrapped shell method changes for this. + 83, # There are no version-wrapped shell method changes for this. ]) versions_supported = set(range(0, novaclient.API_MAX_VERSION.ver_minor + 1)) diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index ff8c166ec..42184345d 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -893,6 +893,12 @@ def list(self, detailed=True, search_opts=None, marker=None, limit=None, if isinstance(val, str): val = val.encode('utf-8') qparams[opt] = val + # NOTE(gibi): After we fixing bug 1871409 and cleaning up the API + # inconsistency around config_drive we can make the + # config_drive filter a boolean filter. But until that we + # simply pass through any value to the API even empty string. + if opt == 'config_drive' and val is not None: + qparams[opt] = val detail = "" if detailed: diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index e86add103..8dafc164d 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -1486,7 +1486,8 @@ def _print_flavor(flavor): dest='user', metavar='', nargs='?', - help=_('Display information from single user (Admin only).')) + help=_('Display information from single user (Admin only until ' + 'microversion 2.82).')) @utils.arg( '--deleted', dest='deleted', @@ -1529,6 +1530,61 @@ def _print_flavor(flavor): "will be displayed. If limit is bigger than 'CONF.api.max_limit' " "option of Nova API, limit 'CONF.api.max_limit' will be used " "instead.")) +@utils.arg( + '--availability-zone', + dest='availability_zone', + metavar='', + default=None, + help=_('Display servers based on their availability zone (Admin only ' + 'until microversion 2.82).')) +@utils.arg( + '--key-name', + dest='key_name', + metavar='', + default=None, + help=_('Display servers based on their keypair name (Admin only until ' + 'microversion 2.82).')) +# NOTE(gibi): we can make this a real boolean filter after bug 1871409 is fixed +# and the REST API is cleaned up regarding the values of config_drive. Unit +# that we simply pass through any string from the user to the REST API. +@utils.arg( + '--config-drive', + dest='config_drive', + metavar='', + default=None, + help=_('Display servers based on their config_drive value (Admin only ' + 'until microversion 2.82). The value must be a boolean value.')) +@utils.arg( + '--progress', + dest='progress', + metavar='', + default=None, + help=_('Display servers based on their progress value (Admin only until ' + 'microversion 2.82).')) +@utils.arg( + '--vm-state', + dest='vm_state', + metavar='', + default=None, + help=_('Display servers based on their vm_state value (Admin only until ' + 'microversion 2.82).')) +@utils.arg( + '--task-state', + dest='task_state', + metavar='', + default=None, + help=_('Display servers based on their task_state value (Admin only until ' + 'microversion 2.82).')) +# TODO(gibi): this is now only work with the integer power state values. +# Later on we can extend this to accept the string values of the power state +# and translate it to integers towards the REST API. +@utils.arg( + '--power-state', + dest='power_state', + metavar='', + default=None, + help=_('Display servers based on their power_state value (Admin only ' + 'until microversion 2.82).')) @utils.arg( '--changes-since', dest='changes_since', @@ -1603,8 +1659,9 @@ def do_list(cs, args): if args.flavor: flavorid = _find_flavor(cs, args.flavor).id # search by tenant or user only works with all_tenants - if args.tenant or args.user: + if args.tenant: args.all_tenants = 1 + search_opts = { 'all_tenants': args.all_tenants, 'reservation_id': args.reservation_id, @@ -1618,7 +1675,15 @@ def do_list(cs, args): 'user_id': args.user, 'host': args.host, 'deleted': args.deleted, - 'changes-since': args.changes_since} + 'changes-since': args.changes_since, + 'availability_zone': args.availability_zone, + 'config_drive': args.config_drive, + 'key_name': args.key_name, + 'progress': args.progress, + 'vm_state': args.vm_state, + 'task_state': args.task_state, + 'power_state': args.power_state, + } for arg in ('tags', "tags-any", 'not-tags', 'not-tags-any'): if arg in args: diff --git a/releasenotes/notes/add-filter-to-nova-list-831dcbb34420fb29.yaml b/releasenotes/notes/add-filter-to-nova-list-831dcbb34420fb29.yaml new file mode 100644 index 000000000..e91a6080e --- /dev/null +++ b/releasenotes/notes/add-filter-to-nova-list-831dcbb34420fb29.yaml @@ -0,0 +1,18 @@ +--- + features: + - | + Added the following filters support for the ``nova list`` command, + these filters are admin-only restricted until microversion 2.82: + + * --availability-zone + * --config-drive + * --key-name + * --power-state + * --task-state + * --vm-state + * --progress + + Existing user filter will be available to non admin since + `microversion 2.83`_. + + .. _microversion 2.83: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id76 From d712c0fbc7a320f6eea4f8741f2f8d6e517a6507 Mon Sep 17 00:00:00 2001 From: zhangbailin Date: Tue, 24 Mar 2020 11:10:29 +0800 Subject: [PATCH 1577/1705] Microversion 2.84 - action event fault details This patch adds support for microversion 2.84 "Expose instance action event details out of the API" [1]. [1] https://review.opendev.org/#/c/694430/ No changes are required in the novaclient side, so just increment the microversion. Depends-On: https://review.opendev.org/#/c/694430/ Implements blueprint action-event-fault-details Change-Id: I1d71284de9a5f25f4554dd2ec7291d5706381de7 --- novaclient/__init__.py | 2 +- novaclient/tests/unit/v2/test_shell.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/novaclient/__init__.py b/novaclient/__init__.py index f86d680fb..adcb85dd5 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.83") +API_MAX_VERSION = api_versions.APIVersion("2.84") diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index c8d3dc9be..f00f583e2 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -4505,6 +4505,7 @@ def test_versions(self): 77, # There are no version-wrapped shell method changes for this. 82, # There are no version-wrapped shell method changes for this. 83, # There are no version-wrapped shell method changes for this. + 84, # There are no version-wrapped shell method changes for this. ]) versions_supported = set(range(0, novaclient.API_MAX_VERSION.ver_minor + 1)) From ea092b29880e71f0ba2d8e1eb93a9cf73edee2a2 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 8 Apr 2020 09:57:41 +0100 Subject: [PATCH 1578/1705] Make 'server list --config-drive' a boolean option Instead of passing through whatever the user provides and exposing this bug in the REST API, simply make the opt a boolean one in expectation of a day where the API issues have been resolved. This also introduces machinery necessary to use more of these types of opts in the future. Change-Id: I9033540ac65ac0ee7337f16bdd002060652092ea Signed-off-by: Stephen Finucane --- doc/source/cli/nova.rst | 13 +++++++---- novaclient/shell.py | 23 ++++++++++++++----- novaclient/tests/unit/v2/test_shell.py | 21 ++++++++--------- novaclient/v2/servers.py | 9 ++++---- novaclient/v2/shell.py | 19 +++++++++------ ...-filter-to-nova-list-831dcbb34420fb29.yaml | 1 + 6 files changed, 52 insertions(+), 34 deletions(-) diff --git a/doc/source/cli/nova.rst b/doc/source/cli/nova.rst index 48b1dbda2..26c2ce2a8 100644 --- a/doc/source/cli/nova.rst +++ b/doc/source/cli/nova.rst @@ -2224,7 +2224,7 @@ nova list [--fields ] [--minimal] [--sort [:]] [--marker ] [--limit ] [--availability-zone ] - [--key-name ] [--config-drive ] + [--key-name ] [--[no-]config-drive] [--progress ] [--vm-state ] [--task-state ] [--power-state ] [--changes-since ] @@ -2316,10 +2316,13 @@ present in the failure domain. Display servers based on their keypair name (Admin only until microversion 2.82). -``--config-drive `` - Display servers based on their config_drive value - The value must be a boolean. (Admin only until - microversion 2.82). +``--config-drive`` + Display servers that have a config drive attached. + (Admin only until microversion 2.82). + +``--no-config-drive`` + Display servers that do not have a config drive attached. + (Admin only until microversion 2.82). ``--progress `` Display servers based on their progress value diff --git a/novaclient/shell.py b/novaclient/shell.py index a5d1e9377..22952b7ca 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -442,6 +442,7 @@ def _find_actions(self, subparsers, actions_module, version, do_help): action_help = desc.strip() arguments = getattr(callback, 'arguments', []) + groups = {} subparser = subparsers.add_parser( command, @@ -456,10 +457,14 @@ def _find_actions(self, subparsers, actions_module, version, do_help): ) self.subcommands[command] = subparser for (args, kwargs) in arguments: - start_version = kwargs.get("start_version", None) + kwargs = kwargs.copy() + + start_version = kwargs.pop("start_version", None) + end_version = kwargs.pop("end_version", None) + group = kwargs.pop("group", None) + if start_version: start_version = api_versions.APIVersion(start_version) - end_version = kwargs.get("end_version", None) if end_version: end_version = api_versions.APIVersion(end_version) else: @@ -471,10 +476,16 @@ def _find_actions(self, subparsers, actions_module, version, do_help): "end": end_version.get_string()}) if not version.matches(start_version, end_version): continue - kw = kwargs.copy() - kw.pop("start_version", None) - kw.pop("end_version", None) - subparser.add_argument(*args, **kw) + + if group: + if group not in groups: + groups[group] = ( + subparser.add_mutually_exclusive_group() + ) + kwargs['dest'] = kwargs.get('dest', group) + groups[group].add_argument(*args, **kwargs) + else: + subparser.add_argument(*args, **kwargs) subparser.set_defaults(func=callback) def setup_debugging(self, debug): diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index f00f583e2..18badc0b7 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -1846,18 +1846,17 @@ def test_list_with_key_name(self): self.run_command('list --key-name my_key') self.assert_called('GET', '/servers/detail?key_name=my_key') - def test_list_with_config_drive_passing_through_any_value(self): - self.run_command('list --config-drive True') + def test_list_with_config_drive(self): + self.run_command('list --config-drive') self.assert_called('GET', '/servers/detail?config_drive=True') - self.run_command('list --config-drive some-random-string') - self.assert_called( - 'GET', '/servers/detail?config_drive=some-random-string') - # This form is special for the test env to pass through an empty string - # as a parameter. The real CLI call would look like - # list --config drive '' - self.run_command(['list', '--config-drive', '']) - self.assert_called( - 'GET', '/servers/detail?config_drive=') + + def test_list_with_no_config_drive(self): + self.run_command('list --no-config-drive') + self.assert_called('GET', '/servers/detail?config_drive=False') + + def test_list_with_conflicting_config_drive(self): + self.assertRaises(SystemExit, self.run_command, + 'list --config-drive --no-config-drive') def test_list_with_progress(self): self.run_command('list --progress 100') diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index 42184345d..7768ef7af 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -893,12 +893,11 @@ def list(self, detailed=True, search_opts=None, marker=None, limit=None, if isinstance(val, str): val = val.encode('utf-8') qparams[opt] = val - # NOTE(gibi): After we fixing bug 1871409 and cleaning up the API - # inconsistency around config_drive we can make the - # config_drive filter a boolean filter. But until that we - # simply pass through any value to the API even empty string. + # NOTE(gibi): The False value won't actually do anything until we + # fix bug 1871409 and clean up the API inconsistency, but we do it + # in preparation for that (hopefully backportable) fix if opt == 'config_drive' and val is not None: - qparams[opt] = val + qparams[opt] = str(val) detail = "" if detailed: diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 8dafc164d..ac6c5ea1f 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -1544,16 +1544,21 @@ def _print_flavor(flavor): default=None, help=_('Display servers based on their keypair name (Admin only until ' 'microversion 2.82).')) -# NOTE(gibi): we can make this a real boolean filter after bug 1871409 is fixed -# and the REST API is cleaned up regarding the values of config_drive. Unit -# that we simply pass through any string from the user to the REST API. @utils.arg( '--config-drive', - dest='config_drive', - metavar='', + action='store_true', + group='config_drive', default=None, - help=_('Display servers based on their config_drive value (Admin only ' - 'until microversion 2.82). The value must be a boolean value.')) + help=_('Display servers that have a config drive attached. (Admin only ' + 'until microversion 2.82).')) +# NOTE(gibi): this won't actually do anything until bug 1871409 is fixed +# and the REST API is cleaned up regarding the values of config_drive +@utils.arg( + '--no-config-drive', + action='store_false', + group='config_drive', + help=_('Display servers that do not have a config drive attached (Admin ' + 'only until microversion 2.82)')) @utils.arg( '--progress', dest='progress', diff --git a/releasenotes/notes/add-filter-to-nova-list-831dcbb34420fb29.yaml b/releasenotes/notes/add-filter-to-nova-list-831dcbb34420fb29.yaml index e91a6080e..77acb7573 100644 --- a/releasenotes/notes/add-filter-to-nova-list-831dcbb34420fb29.yaml +++ b/releasenotes/notes/add-filter-to-nova-list-831dcbb34420fb29.yaml @@ -6,6 +6,7 @@ * --availability-zone * --config-drive + * --no-config-drive * --key-name * --power-state * --task-state From 4d6c70d25df99a4f28f263cd3160c74ccf1343e3 Mon Sep 17 00:00:00 2001 From: zhangbailin Date: Thu, 12 Mar 2020 18:44:36 +0800 Subject: [PATCH 1579/1705] Microversion 2.85: Change volume-update CLI This commit add a new CLI ``nova volume-update [--[no-]delete-on-termination] `` to update 'delete_on_termination' for an attached volume, that the user can decide whether to delete attached volumes when destroying the server. Depends-On: https://review.opendev.org/#/c/711194/ Change-Id: I1fc64fb6e6611c92c6b72265e1bf4b32e9c45f0a Blueprint: destroy-instance-with-datavolume --- doc/source/cli/nova.rst | 29 ++++++++++----- novaclient/__init__.py | 2 +- novaclient/tests/unit/v2/test_shell.py | 36 +++++++++++++++++-- novaclient/tests/unit/v2/test_volumes.py | 23 ++++++++++++ novaclient/v2/shell.py | 34 ++++++++++++++---- novaclient/v2/volumes.py | 30 ++++++++++++++++ .../microversion-v2_85-230931f88c4f1d52.yaml | 16 +++++++++ 7 files changed, 153 insertions(+), 17 deletions(-) create mode 100644 releasenotes/notes/microversion-v2_85-230931f88c4f1d52.yaml diff --git a/doc/source/cli/nova.rst b/doc/source/cli/nova.rst index 26c2ce2a8..6db1cd0cb 100644 --- a/doc/source/cli/nova.rst +++ b/doc/source/cli/nova.rst @@ -558,10 +558,12 @@ nova usage Detach a volume from a server. ``volume-update`` - Update the attachment on the server. Migrates - the data from an attached volume to the - specified available volume and swaps out the - active attachment to the new volume. + Update the attachment on the server. Migrates the data from an + attached volume to the specified available volume and swaps out + the active attachment to the new volume. + Since microversion 2.85, support for updating the + ``delete_on_termination`` delete flag, which allows changing the + behavior of volume deletion on instance deletion. ``x509-create-cert`` **DEPRECATED** Create x509 cert for a user in @@ -3896,7 +3898,7 @@ Attach a volume to a server. Tag for the attached volume. (Supported by API versions '2.49' - '2.latest') ``--delete-on-termination`` - Specify if the attached volume sholud be deleted when the server is + Specify if the attached volume should be deleted when the server is destroyed. By default the attached volume is not deleted when the server is destroyed. (Supported by API versions '2.79' - '2.latest') @@ -3942,7 +3944,8 @@ nova volume-update .. code-block:: console - usage: nova volume-update + usage: nova volume-update [--[no-]delete-on-termination] + Update the attachment on the server. Migrates the data from an attached volume to the specified available volume and swaps out the active attachment to the @@ -3953,12 +3956,22 @@ new volume. ```` Name or ID of server. -```` +```` ID of the source (original) volume. -```` +```` ID of the destination volume. +**Optional arguments:** + +``--delete-on-termination`` + Specify that the volume should be deleted when the server is destroyed. + (Supported by API versions '2.85' - '2.latest') + +``--no-delete-on-termination`` + Specify that the attached volume should not be deleted when + the server is destroyed. (Supported by API versions '2.85' - '2.latest') + .. _nova_bash-completion: nova bash-completion diff --git a/novaclient/__init__.py b/novaclient/__init__.py index adcb85dd5..5e3e77016 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.84") +API_MAX_VERSION = api_versions.APIVersion("2.85") diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 18badc0b7..d21252bd3 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -3992,11 +3992,43 @@ def test_volume_attach_without_delete_on_termination(self): {'volumeAttachment': {'volumeId': 'Work'}}) - def test_volume_update(self): - self.run_command('volume-update sample-server Work Work') + def test_volume_update_pre_v285(self): + """Before microversion 2.85, we should keep the original behavior""" + self.run_command('volume-update sample-server Work Work', + api_version='2.84') self.assert_called('PUT', '/servers/1234/os-volume_attachments/Work', {'volumeAttachment': {'volumeId': 'Work'}}) + def test_volume_update_swap_v285(self): + """Microversion 2.85, we should also keep the original behavior.""" + self.run_command('volume-update sample-server Work Work', + api_version='2.85') + self.assert_called('PUT', '/servers/1234/os-volume_attachments/Work', + {'volumeAttachment': {'volumeId': 'Work'}}) + + def test_volume_update_v285(self): + self.run_command('volume-update sample-server --delete-on-termination ' + 'Work Work', api_version='2.85') + body = {'volumeAttachment': + {'volumeId': 'Work', 'delete_on_termination': True}} + self.assert_called('PUT', '/servers/1234/os-volume_attachments/Work', + body) + + self.run_command('volume-update sample-server ' + '--no-delete-on-termination ' + 'Work Work', api_version='2.85') + body = {'volumeAttachment': + {'volumeId': 'Work', 'delete_on_termination': False}} + self.assert_called('PUT', '/servers/1234/os-volume_attachments/Work', + body) + + def test_volume_update_v285_conflicting(self): + self.assertRaises( + SystemExit, self.run_command, + 'volume-update sample-server --delete-on-termination ' + '--no-delete-on-termination Work Work', + api_version='2.85') + def test_volume_detach(self): self.run_command('volume-detach sample-server Work') self.assert_called('DELETE', diff --git a/novaclient/tests/unit/v2/test_volumes.py b/novaclient/tests/unit/v2/test_volumes.py index d18f84664..93ea1c961 100644 --- a/novaclient/tests/unit/v2/test_volumes.py +++ b/novaclient/tests/unit/v2/test_volumes.py @@ -156,3 +156,26 @@ def test_create_server_volume_with_delete_on_termination_pre_v279(self): volume_id='15e59938-07d5-11e1-90e3-e3dffe0c5983', delete_on_termination=True) self.assertIn('delete_on_termination', str(ex)) + + +class VolumesV285Test(VolumesV279Test): + api_version = "2.85" + + def test_volume_update_server_volume(self): + v = self.cs.volumes.update_server_volume( + server_id=1234, + src_volid='Work', + dest_volid='Work', + delete_on_termination=True + ) + self.assert_request_id(v, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called('PUT', + '/servers/1234/os-volume_attachments/Work') + self.assertIsInstance(v, volumes.Volume) + + def test_volume_update_server_volume_pre_v285(self): + self.cs.api_version = api_versions.APIVersion('2.84') + ex = self.assertRaises( + TypeError, self.cs.volumes.update_server_volume, "1234", + 'Work', 'Work', delete_on_termination=True) + self.assertIn('delete_on_termination', str(ex)) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index ac6c5ea1f..e0498df81 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -2730,22 +2730,44 @@ def do_volume_attach(cs, args): help=_('Name or ID of server.')) @utils.arg( 'src_volume', - metavar='', + metavar='', help=_('ID of the source (original) volume.')) @utils.arg( 'dest_volume', - metavar='', + metavar='', help=_('ID of the destination volume.')) +@utils.arg( + '--delete-on-termination', + default=None, + group='delete_on_termination', + action='store_true', + help=_('Specify that the volume should be deleted ' + 'when the server is destroyed.'), + start_version='2.85') +@utils.arg( + '--no-delete-on-termination', + group='delete_on_termination', + action='store_false', + help=_('Specify that the volume should not be deleted ' + 'when the server is destroyed.'), + start_version='2.85') def do_volume_update(cs, args): """Update the attachment on the server. - Migrates the data from an attached volume to the - specified available volume and swaps out the active - attachment to the new volume. + If dest_volume is the same as the src_volume then the command migrates + the data from the attached volume to the specified available volume + and swaps out the active attachment to the new volume. Otherwise it + only updates the parameters of the existing attachment. """ + kwargs = dict() + if (cs.api_version >= api_versions.APIVersion('2.85') and + args.delete_on_termination is not None): + kwargs['delete_on_termination'] = args.delete_on_termination + cs.volumes.update_server_volume(_find_server(cs, args.server).id, args.src_volume, - args.dest_volume) + args.dest_volume, + **kwargs) @utils.arg( diff --git a/novaclient/v2/volumes.py b/novaclient/v2/volumes.py index 8fc755658..7153c835d 100644 --- a/novaclient/v2/volumes.py +++ b/novaclient/v2/volumes.py @@ -103,6 +103,7 @@ def create_server_volume(self, server_id, volume_id, device=None, return self._create("/servers/%s/os-volume_attachments" % server_id, body, "volumeAttachment") + @api_versions.wraps("2.0", "2.84") def update_server_volume(self, server_id, src_volid, dest_volid): """ Swaps the existing volume attachment to point to a new volume. @@ -124,6 +125,35 @@ def update_server_volume(self, server_id, src_volid, dest_volid): (server_id, src_volid,), body, "volumeAttachment") + @api_versions.wraps("2.85") + def update_server_volume(self, server_id, src_volid, dest_volid, + delete_on_termination=None): + """ + Swaps the existing volume attachment to point to a new volume. + + Takes a server, a source (attached) volume and a destination volume and + performs a hypervisor assisted data migration from src to dest volume, + detaches the original (source) volume and attaches the new destination + volume. Note that not all backing hypervisor drivers support this + operation and it may be disabled via policy. + + + :param server_id: The ID of the server + :param source_volume: The ID of the src volume + :param dest_volume: The ID of the destination volume + :param delete_on_termination: Marked whether to delete the attached + volume when the server is deleted + (optional). + :rtype: :class:`Volume` + """ + body = {'volumeAttachment': {'volumeId': dest_volid}} + if delete_on_termination is not None: + body['volumeAttachment']['delete_on_termination'] = ( + delete_on_termination) + return self._update("/servers/%s/os-volume_attachments/%s" % + (server_id, src_volid), + body, "volumeAttachment") + def get_server_volume(self, server_id, volume_id=None, attachment_id=None): """ Get the volume identified by the volume ID, that is attached to diff --git a/releasenotes/notes/microversion-v2_85-230931f88c4f1d52.yaml b/releasenotes/notes/microversion-v2_85-230931f88c4f1d52.yaml new file mode 100644 index 000000000..859534c4b --- /dev/null +++ b/releasenotes/notes/microversion-v2_85-230931f88c4f1d52.yaml @@ -0,0 +1,16 @@ +--- +features: + - | + Support is added for compute API `microversion 2.85`_. This adds the + ability to update an attached volume with a ``delete_on_termination``, + which specify if the attached volume should be deleted when the server + is destroyed. + + - The ``--delete-on-termination`` and ``--no-delete-on-termination`` + options are added to the ``nova volume-update`` CLI. + - New kwarg called ``delete_on_termination`` added to the python API + binding: + + - ``novaclient.v2.volumes.VolumeManager.update_server_volume()`` + + .. _microversion 2.85: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id78 From ca1262daeb793b5de1f5a267cf03d5b76651dbed Mon Sep 17 00:00:00 2001 From: Lee Yarwood Date: Tue, 7 Apr 2020 11:55:24 +0100 Subject: [PATCH 1580/1705] Microversion 2.86 - Extra spec validation This microversion introduces extra spec validation within the API. No changes are required within novaclient so this change only bumps API_MAX_VERSION. Depends-On: https://review.opendev.org/#/c/708436/ Change-Id: Ic8fef7ee363435e9ac728b87d494593fcc6defc0 --- novaclient/__init__.py | 2 +- novaclient/tests/unit/v2/test_shell.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 5e3e77016..878cb9a29 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.85") +API_MAX_VERSION = api_versions.APIVersion("2.86") diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index d21252bd3..0994b5bda 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -4537,6 +4537,7 @@ def test_versions(self): 82, # There are no version-wrapped shell method changes for this. 83, # There are no version-wrapped shell method changes for this. 84, # There are no version-wrapped shell method changes for this. + 86, # doesn't require any changes in novaclient. ]) versions_supported = set(range(0, novaclient.API_MAX_VERSION.ver_minor + 1)) From c5f29d683315e1afbe944aca8b3ae819de74d286 Mon Sep 17 00:00:00 2001 From: zhangbailin Date: Fri, 10 Apr 2020 08:39:35 +0800 Subject: [PATCH 1581/1705] FUP: Add volume-update CLI pre V285 tests This commit mainly to fix some comments from [1]. [1]https://review.opendev.org/#/c/712651/19/novaclient/tests/unit/v2/test_shell.py@4001 Blueprint: destroy-instance-with-datavolume Change-Id: Id809f22d0da2cdedf33a2c0df202f3953fd01673 --- doc/source/cli/nova.rst | 4 +++- novaclient/tests/unit/v2/test_shell.py | 12 ++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/doc/source/cli/nova.rst b/doc/source/cli/nova.rst index 6db1cd0cb..d661599ed 100644 --- a/doc/source/cli/nova.rst +++ b/doc/source/cli/nova.rst @@ -3966,11 +3966,13 @@ new volume. ``--delete-on-termination`` Specify that the volume should be deleted when the server is destroyed. + It is mutually exclusive with '--no-delete-on-termination'. (Supported by API versions '2.85' - '2.latest') ``--no-delete-on-termination`` Specify that the attached volume should not be deleted when - the server is destroyed. (Supported by API versions '2.85' - '2.latest') + the server is destroyed. It is mutually exclusive with '--delete-on-termination'. + (Supported by API versions '2.85' - '2.latest') .. _nova_bash-completion: diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index d21252bd3..6aa37f7d4 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -4006,6 +4006,18 @@ def test_volume_update_swap_v285(self): self.assert_called('PUT', '/servers/1234/os-volume_attachments/Work', {'volumeAttachment': {'volumeId': 'Work'}}) + def test_volume_update_delete_on_termination_pre_v285(self): + self.assertRaises( + SystemExit, self.run_command, + 'volume-update sample-server --delete-on-termination Work Work', + api_version='2.84') + + def test_volume_update_no_delete_on_termination_pre_v285(self): + self.assertRaises( + SystemExit, self.run_command, + 'volume-update sample-server --no-delete-on-termination Work Work', + api_version='2.84') + def test_volume_update_v285(self): self.run_command('volume-update sample-server --delete-on-termination ' 'Work Work', api_version='2.85') From 3ae6ecc9c051d6541a39e1b6de74039a951a4dca Mon Sep 17 00:00:00 2001 From: zhangbailin Date: Fri, 10 Apr 2020 11:52:24 +0800 Subject: [PATCH 1582/1705] [Trivial] FUP: Enhanced description for 'server list --config-drive' help This commit mainly address comments in [1]. [1]https://review.opendev.org/#/c/718349/4/doc/source/cli/nova.rst@2321 Change-Id: I6010adbc895b4e2438f9f0729e3c89f37a69c8f8 --- doc/source/cli/nova.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/source/cli/nova.rst b/doc/source/cli/nova.rst index 6db1cd0cb..d418b786d 100644 --- a/doc/source/cli/nova.rst +++ b/doc/source/cli/nova.rst @@ -2320,10 +2320,12 @@ present in the failure domain. ``--config-drive`` Display servers that have a config drive attached. + It is mutually exclusive with '--no-config-drive'. (Admin only until microversion 2.82). ``--no-config-drive`` Display servers that do not have a config drive attached. + It is mutually exclusive with '--config-drive'. (Admin only until microversion 2.82). ``--progress `` From 7ed265bbf580cb35876738f16b1d430efd75aa2a Mon Sep 17 00:00:00 2001 From: Lee Yarwood Date: Wed, 25 Mar 2020 14:26:53 +0000 Subject: [PATCH 1583/1705] Microversion 2.87 - Stable device boot from volume rescue This microversion is used to request a stable device rescue for boot from volume instances and will only succeed when the compute hosting the instance reports the COMPUTE_RESCUE_BFV trait. No changes are required within novaclient so this change only bumps API_MAX_VERSION. Depends-On: https://review.opendev.org/#/c/701430/ Change-Id: I7885b8558db7657abbfe1f45877f52e947f5f655 --- novaclient/__init__.py | 2 +- novaclient/tests/unit/v2/test_shell.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 878cb9a29..0b29e4f1b 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.86") +API_MAX_VERSION = api_versions.APIVersion("2.87") diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 0994b5bda..040f8cbaf 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -4538,6 +4538,7 @@ def test_versions(self): 83, # There are no version-wrapped shell method changes for this. 84, # There are no version-wrapped shell method changes for this. 86, # doesn't require any changes in novaclient. + 87, # doesn't require any changes in novaclient. ]) versions_supported = set(range(0, novaclient.API_MAX_VERSION.ver_minor + 1)) From 81f57aabc863e7d5e9880d79d13de14f0da50365 Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Sat, 11 Apr 2020 10:48:50 +0000 Subject: [PATCH 1584/1705] Update master for stable/ussuri Add file to the reno documentation build to show release notes for stable/ussuri. Use pbr instruction to increment the minor version number automatically so that master versions are higher than the versions on stable/ussuri. Change-Id: I0ef054d7c3d47d4a86dc2a67f2ef0f584cb96ddf Sem-Ver: feature --- releasenotes/source/index.rst | 1 + releasenotes/source/ussuri.rst | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 releasenotes/source/ussuri.rst diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index aeee0e6e0..fb30e94a6 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -8,6 +8,7 @@ Contents :maxdepth: 2 unreleased + ussuri train stein rocky diff --git a/releasenotes/source/ussuri.rst b/releasenotes/source/ussuri.rst new file mode 100644 index 000000000..e21e50e0c --- /dev/null +++ b/releasenotes/source/ussuri.rst @@ -0,0 +1,6 @@ +=========================== +Ussuri Series Release Notes +=========================== + +.. release-notes:: + :branch: stable/ussuri From 7d6cf15f8cad55da593a09a5aa1b5c2f25d5916e Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Sat, 11 Apr 2020 10:48:53 +0000 Subject: [PATCH 1585/1705] Add Python3 victoria unit tests This is an automatically generated patch to ensure unit testing is in place for all the of the tested runtimes for victoria. See also the PTI in governance [1]. [1]: https://governance.openstack.org/tc/reference/project-testing-interface.html Change-Id: I99e3fc9c5a2d8c627e6e083bee157733065546bc --- .zuul.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.zuul.yaml b/.zuul.yaml index 95eabd355..a821c26dc 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -20,7 +20,7 @@ - lib-forward-testing-python3 - openstack-cover-jobs - openstack-lower-constraints-jobs - - openstack-python3-ussuri-jobs + - openstack-python3-victoria-jobs - publish-openstack-docs-pti - release-notes-jobs-python3 check: From 812f10661283c092d50fbf4417901ea23a5b3f26 Mon Sep 17 00:00:00 2001 From: Ghanshyam Mann Date: Thu, 23 Apr 2020 10:51:31 -0500 Subject: [PATCH 1586/1705] [Community goal] Update contributor documentation As the Ussuri goal we have an OpenStack project wide contributing documentation template [1]. pyhton-novaclient use its own bug or feature tracking LP and core team is also not exactly same as nova so adding its own contributor guide with keeping common links from nova with python-novaclient specific information. Also the top level CONTRIBUTING.rst template has been changed in the cookiecutter repo[2]. So this patch updates the CONTRIBUTING.rst according to the new template. [1] https://opendev.org/openstack/cookiecutter/src/branch/master/%7b%7bcookiecutter.repo_name%7d%7d/doc/source/contributor/contributing.rst [2] https://review.opendev.org/#/c/696001 Change-Id: Idd35fdf54a92a4a000d0d7776884682c8722854b Story: #2007236 Task: #38541 --- CONTRIBUTING.rst | 21 +++++----- doc/source/contributor/contributing.rst | 52 +++++++++++++++++++++++++ doc/source/contributor/index.rst | 14 ++++--- 3 files changed, 72 insertions(+), 15 deletions(-) create mode 100644 doc/source/contributor/contributing.rst diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 20115405c..d4205aed3 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -1,16 +1,19 @@ -If you would like to contribute to the development of OpenStack, -you must follow the steps documented at: +The source repository for this project can be found at: - https://docs.openstack.org/infra/manual/developers.html#development-workflow + https://opendev.org/openstack/python-novaclient -Once those steps have been completed, changes to OpenStack -should be submitted for review via the Gerrit tool, following -the workflow documented at: +Pull requests submitted through GitHub are not monitored. - https://docs.openstack.org/infra/manual/developers.html#development-workflow +To start contributing to OpenStack, follow the steps in the contribution guide +to set up and use Gerrit: -Pull requests submitted through GitHub will be ignored. + https://docs.openstack.org/contributors/code-and-documentation/quick-start.html -Bugs should be filed on Launchpad, not GitHub: +Bugs should be filed on Launchpad: https://bugs.launchpad.net/python-novaclient + +For more specific information about contributing to this repository, see the +python-novaclient contributor guide: + + https://docs.openstack.org/python-novaclient/latest/contributor/contributing.html diff --git a/doc/source/contributor/contributing.rst b/doc/source/contributor/contributing.rst new file mode 100644 index 000000000..38eac4ec9 --- /dev/null +++ b/doc/source/contributor/contributing.rst @@ -0,0 +1,52 @@ +============================ +So You Want to Contribute... +============================ + +For general information on contributing to OpenStack, please check out the +`contributor guide `_ to get started. +It covers all the basics that are common to all OpenStack projects: the accounts +you need, the basics of interacting with our Gerrit review system, how we +communicate as a community, etc. + +Below will cover the more project specific information you need to get started +with python-novaclient. + +Communication +~~~~~~~~~~~~~ + +Please refer `how-to-get-involved `_. + +Contacting the Core Team +~~~~~~~~~~~~~~~~~~~~~~~~ + +The overall structure of the Nova team including python-novaclient is +documented on `the wiki `_. + +New Feature Planning +~~~~~~~~~~~~~~~~~~~~ + +If you want to propose a new feature please read the +`blueprints `_ page. + +Task Tracking +~~~~~~~~~~~~~ + +We track our tasks in `Launchpad `__. + +If you're looking for some smaller, easier work item to pick up and get started +on, search for the 'low-hanging-fruit' tag. + +Reporting a Bug +~~~~~~~~~~~~~~~ + +You found an issue and want to make sure we are aware of it? You can do so on +`Launchpad `__. +More info about Launchpad usage can be found on `OpenStack docs page +`_. + +Getting Your Patch Merged +~~~~~~~~~~~~~~~~~~~~~~~~~ + +All changes proposed to the python-novaclient requires two ``Code-Review +2`` +votes from python-novaclient core reviewers before one of the core reviewers +can approve patch by giving ``Workflow +1`` vote.. diff --git a/doc/source/contributor/index.rst b/doc/source/contributor/index.rst index 50fd507f5..b52cfce63 100644 --- a/doc/source/contributor/index.rst +++ b/doc/source/contributor/index.rst @@ -2,14 +2,16 @@ Contributor Guide =================== -Code is hosted at `opendev.org`__. Submit bugs to the python-novaclient -project on `Launchpad`__. Submit code to the `openstack/python-novaclient` -project using `Gerrit`__. +Basic Information +================= -__ https://opendev.org/openstack/python-novaclient -__ https://bugs.launchpad.net/python-novaclient -__ https://docs.openstack.org/infra/manual/developers.html#development-workflow +.. toctree:: + :maxdepth: 2 + + contributing +Developer Guide +=============== .. toctree:: :maxdepth: 2 From cfcc95d6e8064b9c78cabd2ea126dcd9fb233d93 Mon Sep 17 00:00:00 2001 From: Sean McGinnis Date: Fri, 24 Apr 2020 08:23:18 -0500 Subject: [PATCH 1587/1705] Add py38 package metadata Now that we are running the Victoria tests that include a voting py38, we can now add the Python 3.8 metadata to the package information to reflect that support. Change-Id: I3645bf029ee92e9dc93412c522d7694f6a61fc4f Signed-off-by: Sean McGinnis --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index aac59e112..0a5cd0867 100644 --- a/setup.cfg +++ b/setup.cfg @@ -20,6 +20,7 @@ classifier = Programming Language :: Python :: 3 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 Programming Language :: Python :: 3 :: Only Programming Language :: Python :: Implementation :: CPython From 439912743f1fa5d429c5ca97f36e674a13d74f57 Mon Sep 17 00:00:00 2001 From: Takashi Natsume Date: Fri, 24 Apr 2020 23:02:12 +0900 Subject: [PATCH 1588/1705] Switch to using TOX_CONSTRAINTS_FILE UPPER_CONSTRAINTS_FILE is deprecated. So switch to using TOX_CONSTRAINTS_FILE. See I3f957187ed4f29fcf88db99cb79fb33b21a8dd8d. Change-Id: I1812eb64b1afa8e90f4de8604f3f26075da6332f --- tox.ini | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tox.ini b/tox.ini index fc1c9c999..1368d07e1 100644 --- a/tox.ini +++ b/tox.ini @@ -16,7 +16,7 @@ passenv = ZUUL_CACHE_DIR REQUIREMENTS_PIP_LOCATION deps = - -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} + -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/test-requirements.txt -r{toxinidir}/requirements.txt commands = @@ -31,7 +31,7 @@ commands = bandit -r novaclient -n5 -x tests [testenv:venv] deps = - -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} + -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/test-requirements.txt -r{toxinidir}/requirements.txt -r{toxinidir}/doc/requirements.txt @@ -39,7 +39,7 @@ commands = {posargs} [testenv:docs] deps = - -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} + -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/requirements.txt -r{toxinidir}/doc/requirements.txt commands = @@ -58,7 +58,7 @@ commands = [testenv:releasenotes] deps = - -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} + -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/requirements.txt -r{toxinidir}/doc/requirements.txt commands = From ddc4c674224f82d573635ae534b1e3ca9e6b0dd6 Mon Sep 17 00:00:00 2001 From: Sean McGinnis Date: Fri, 24 Apr 2020 10:25:57 -0500 Subject: [PATCH 1589/1705] Bump default tox env from py37 to py38 Python 3.8 is now our highest level supported python runtime. This updates the default tox target environments to swap out py37 for py38 to make sure local development testing is covering this version. This does not impact zuul jobs in any way, nor prevent local tests against py37. It just changes the default if none is explicitly provided. Change-Id: I093085cf6e85a66f80559eba62f9edf684f5dbea Signed-off-by: Sean McGinnis --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index fc1c9c999..abcec687d 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py37,pep8,docs +envlist = py38,pep8,docs minversion = 3.1 skipsdist = true ignore_basepython_conflict = true From 2c5c30ba01352164639feaf04c867bc7ffc62404 Mon Sep 17 00:00:00 2001 From: Takashi Natsume Date: Sat, 25 Apr 2020 10:41:03 +0000 Subject: [PATCH 1590/1705] doc: Update Testing document The default tox target environments has been changed from Python 3.7 to 3.8 since I093085cf6e85a66f80559eba62f9edf684f5dbea. Update the 'Testing' document. Change-Id: I33b32eb1d88225ae7ad44cbae28edc5c5f9c4925 --- doc/source/contributor/testing.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/source/contributor/testing.rst b/doc/source/contributor/testing.rst index 021d0ae76..4a1e1938b 100644 --- a/doc/source/contributor/testing.rst +++ b/doc/source/contributor/testing.rst @@ -8,11 +8,11 @@ test targets that can be run to validate the code. ``tox -e pep8`` Style guidelines enforcement. -``tox -e py37`` - Traditional unit testing (Python 3.7). +``tox -e py38`` + Traditional unit testing (Python 3.8). ``tox -e functional`` - Live functional testing against an existing OpenStack instance. (Python 3.7) + Live functional testing against an existing OpenStack instance. (Python 3.8) ``tox -e cover`` Generate a coverage report on unit testing. From e93a00b24e05b9b0fa6eeb0eb7e4974a711b404d Mon Sep 17 00:00:00 2001 From: Takashi Natsume Date: Sun, 26 Apr 2020 06:56:23 +0000 Subject: [PATCH 1591/1705] Remove future imports These particular imports are no longer needed in a Python 3-only world. Change-Id: I405b32c8cdd60fb19270a8f6fe2e2728022f7c6e Signed-off-by: Takashi Natsume --- novaclient/shell.py | 1 - novaclient/v2/shell.py | 2 -- 2 files changed, 3 deletions(-) diff --git a/novaclient/shell.py b/novaclient/shell.py index 22952b7ca..a81db89a8 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -18,7 +18,6 @@ Command-line interface to the OpenStack Nova API. """ -from __future__ import print_function import argparse import logging import sys diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index e0498df81..753dd127f 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -16,8 +16,6 @@ # License for the specific language governing permissions and limitations # under the License. -from __future__ import print_function - import argparse import collections import datetime From 7ef2c28bf3b38917e4465205031e8476c5938195 Mon Sep 17 00:00:00 2001 From: Takashi Natsume Date: Sun, 26 Apr 2020 07:08:53 +0000 Subject: [PATCH 1592/1705] Use unittest.mock instead of third party mock Now that we no longer support py27, we can use the standard library unittest.mock module instead of the third party mock lib. Change-Id: I4d45ae17f6f84f945f5dd049a929216ce6b6b58e Signed-off-by: Takashi Natsume --- novaclient/tests/unit/test_api_versions.py | 2 +- novaclient/tests/unit/test_client.py | 2 +- novaclient/tests/unit/test_crypto.py | 3 +-- novaclient/tests/unit/test_discover.py | 2 +- novaclient/tests/unit/test_shell.py | 2 +- novaclient/tests/unit/test_utils.py | 3 +-- novaclient/tests/unit/utils.py | 2 +- novaclient/tests/unit/v2/fakes.py | 2 +- novaclient/tests/unit/v2/test_flavors.py | 2 +- novaclient/tests/unit/v2/test_images.py | 2 +- novaclient/tests/unit/v2/test_servers.py | 3 +-- novaclient/tests/unit/v2/test_shell.py | 2 +- novaclient/tests/unit/v2/test_versions.py | 2 +- novaclient/tests/unit/v2/test_volumes.py | 2 +- test-requirements.txt | 1 - 15 files changed, 14 insertions(+), 18 deletions(-) diff --git a/novaclient/tests/unit/test_api_versions.py b/novaclient/tests/unit/test_api_versions.py index 89d806072..6969718f8 100644 --- a/novaclient/tests/unit/test_api_versions.py +++ b/novaclient/tests/unit/test_api_versions.py @@ -13,7 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. -import mock +from unittest import mock import novaclient from novaclient import api_versions diff --git a/novaclient/tests/unit/test_client.py b/novaclient/tests/unit/test_client.py index 6b348d1bb..d157b73b2 100644 --- a/novaclient/tests/unit/test_client.py +++ b/novaclient/tests/unit/test_client.py @@ -14,9 +14,9 @@ # under the License. import copy +from unittest import mock from keystoneauth1 import session -import mock from oslo_utils import uuidutils import novaclient.api_versions diff --git a/novaclient/tests/unit/test_crypto.py b/novaclient/tests/unit/test_crypto.py index 0e876a070..3b4a80ed8 100644 --- a/novaclient/tests/unit/test_crypto.py +++ b/novaclient/tests/unit/test_crypto.py @@ -14,8 +14,7 @@ import base64 import subprocess - -import mock +from unittest import mock from novaclient import crypto from novaclient.tests.unit import utils diff --git a/novaclient/tests/unit/test_discover.py b/novaclient/tests/unit/test_discover.py index c03c1d0fd..9bbbe3917 100644 --- a/novaclient/tests/unit/test_discover.py +++ b/novaclient/tests/unit/test_discover.py @@ -15,8 +15,8 @@ import imp import inspect +from unittest import mock -import mock import pkg_resources from novaclient import client diff --git a/novaclient/tests/unit/test_shell.py b/novaclient/tests/unit/test_shell.py index 2c6af3b41..889916ea7 100644 --- a/novaclient/tests/unit/test_shell.py +++ b/novaclient/tests/unit/test_shell.py @@ -16,11 +16,11 @@ import io import re import sys +from unittest import mock import ddt import fixtures from keystoneauth1 import fixture -import mock import prettytable import requests_mock from testtools import matchers diff --git a/novaclient/tests/unit/test_utils.py b/novaclient/tests/unit/test_utils.py index 4a755a99b..8411f3a40 100644 --- a/novaclient/tests/unit/test_utils.py +++ b/novaclient/tests/unit/test_utils.py @@ -13,10 +13,9 @@ import io import sys +from unittest import mock from urllib import parse -import mock - from novaclient import base from novaclient import exceptions from novaclient.tests.unit import fakes diff --git a/novaclient/tests/unit/utils.py b/novaclient/tests/unit/utils.py index f987fc3e5..3a660eccd 100644 --- a/novaclient/tests/unit/utils.py +++ b/novaclient/tests/unit/utils.py @@ -12,9 +12,9 @@ # under the License. import os +from unittest import mock import fixtures -import mock from oslo_serialization import jsonutils import requests from requests_mock.contrib import fixture as requests_mock_fixture diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 69ecc8936..f3f8193a4 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -17,9 +17,9 @@ import copy import datetime import re +from unittest import mock from urllib import parse -import mock from oslo_utils import strutils import novaclient diff --git a/novaclient/tests/unit/v2/test_flavors.py b/novaclient/tests/unit/v2/test_flavors.py index e536af11f..fccfc8fdb 100644 --- a/novaclient/tests/unit/v2/test_flavors.py +++ b/novaclient/tests/unit/v2/test_flavors.py @@ -13,7 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. -import mock +from unittest import mock from novaclient import api_versions from novaclient import base diff --git a/novaclient/tests/unit/v2/test_images.py b/novaclient/tests/unit/v2/test_images.py index a3177a9cb..5fa448e01 100644 --- a/novaclient/tests/unit/v2/test_images.py +++ b/novaclient/tests/unit/v2/test_images.py @@ -11,7 +11,7 @@ # License for the specific language governing permissions and limitations # under the License. -import mock +from unittest import mock from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit.fixture_data import images as data diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index 0d3ca4b2c..40d88a19a 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -16,8 +16,7 @@ import io import os import tempfile - -import mock +from unittest import mock from novaclient import api_versions from novaclient import exceptions diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index adec6a4d2..aeb8a5601 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -23,9 +23,9 @@ import datetime import io import os +from unittest import mock import fixtures -import mock from oslo_utils import timeutils import testtools diff --git a/novaclient/tests/unit/v2/test_versions.py b/novaclient/tests/unit/v2/test_versions.py index 63d63c1a3..23b72840e 100644 --- a/novaclient/tests/unit/v2/test_versions.py +++ b/novaclient/tests/unit/v2/test_versions.py @@ -12,7 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. -import mock +from unittest import mock from novaclient import api_versions from novaclient import exceptions as exc diff --git a/novaclient/tests/unit/v2/test_volumes.py b/novaclient/tests/unit/v2/test_volumes.py index 93ea1c961..c06661186 100644 --- a/novaclient/tests/unit/v2/test_volumes.py +++ b/novaclient/tests/unit/v2/test_volumes.py @@ -13,7 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. -import mock +from unittest import mock from novaclient import api_versions from novaclient.tests.unit import utils diff --git a/test-requirements.txt b/test-requirements.txt index 69b11b873..6882de9f8 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -7,7 +7,6 @@ bandit>=1.1.0 # Apache-2.0 coverage!=4.4,>=4.0 # Apache-2.0 ddt>=1.0.1 # MIT fixtures>=3.0.0 # Apache-2.0/BSD -mock>=2.0.0 # BSD python-keystoneclient>=3.8.0 # Apache-2.0 python-cinderclient!=4.0.0,>=3.3.0 # Apache-2.0 python-glanceclient>=2.8.0 # Apache-2.0 From 6e78eb3539026b9266e4a5b8030ac4ecddbeae58 Mon Sep 17 00:00:00 2001 From: Takashi Natsume Date: Sat, 2 May 2020 14:16:22 +0900 Subject: [PATCH 1593/1705] Remove mock in lower-constraints.txt This is a follow up patch for I4d45ae17f6f84f945f5dd049a929216ce6b6b58e. The 'mock' line has already been removed in test-requirements.txt. So the 'mock' line is removed in lower-constraints.txt. Change-Id: Ic215d48477cfb7441e3bddba969258157468713c Signed-off-by: Takashi Natsume --- lower-constraints.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/lower-constraints.txt b/lower-constraints.txt index 72fdac0e6..cd722665d 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -38,7 +38,6 @@ kombu==4.0.0 linecache2==1.0.0 MarkupSafe==1.0 mccabe==0.2.1 -mock==2.0.0 monotonic==0.6 msgpack-python==0.4.0 munch==2.1.0 From 5b2e14e109a5177245f6b59bc3ff2749f9fae5ae Mon Sep 17 00:00:00 2001 From: Ghanshyam Mann Date: Tue, 12 May 2020 09:48:10 -0500 Subject: [PATCH 1594/1705] Bump hacking min version to 3.0.1 hacking 3.0.1 fix the pinning of flake8 to avoid bringing in a new version with new checks. bumping the min version for hacking so that any older hacking versions which auto adopt the new checks are not used. Change-Id: I43ddb7bac8b7734f6e4fa4632d4524d454931709 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 6882de9f8..f2df90178 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,7 +1,7 @@ # 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. -hacking>=3.0,<3.1.0 # Apache-2.0 +hacking>=3.0.1,<3.1.0 # Apache-2.0 bandit>=1.1.0 # Apache-2.0 coverage!=4.4,>=4.0 # Apache-2.0 From 553040ed4b27a36f0557595dcf33abe5739f9517 Mon Sep 17 00:00:00 2001 From: Andreas Jaeger Date: Thu, 14 May 2020 21:45:12 +0200 Subject: [PATCH 1595/1705] Switch to newer openstackdocstheme and reno versions Switch to openstackdocstheme 2.1.2 and reno 3.1.0 versions. Using these versions will allow parallelizing building of documents. Update Sphinx version as well. openstackdocstheme renames some variables, so follow the renames. A couple of variables are also not needed anymore, remove them. Remove duplicated variables. Depends-On: https://review.opendev.org/728432 Change-Id: Icb604b31150a1ad6c29a9a4a934ed13f8a0b4976 --- doc/requirements.txt | 7 +++---- doc/source/conf.py | 12 ++++-------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index 7cfd8e099..d1818034a 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,10 +1,9 @@ # 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. -sphinx!=1.6.6,!=1.6.7,>=1.6.2,<2.0.0;python_version=='2.7' # BSD -sphinx!=1.6.6,!=1.6.7,!=2.1.0,>=1.6.2;python_version>='3.4' # BSD -openstackdocstheme>=1.30.0 # Apache-2.0 -reno>=2.5.0 # Apache-2.0 +sphinx>=2.0.0,!=2.1.0 # BSD +openstackdocstheme>=2.1.2 # Apache-2.0 +reno>=3.1.0 # Apache-2.0 sphinxcontrib-apidoc>=0.2.0 # BSD # redirect tests in docs diff --git a/doc/source/conf.py b/doc/source/conf.py index 515e754df..1be9ec51d 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -36,10 +36,6 @@ # The master toctree document. master_doc = 'index' -# openstackdocstheme options -repository_name = 'openstack/python-novaclient' -bug_project = 'python-novaclient' -bug_tag = 'doc' copyright = 'OpenStack Contributors' @@ -70,10 +66,10 @@ # -- Options for openstackdocstheme ------------------------------------------- -repository_name = 'openstack/python-novaclient' -bug_project = 'python-novaclient' -bug_tag = '' -openstack_projects = [ +openstackdocs_repo_name = 'openstack/python-novaclient' +openstackdocs_bug_project = 'python-novaclient' +openstackdocs_bug_tag = '' +openstackdocs_projects = [ 'keystoneauth', 'nova', 'os-client-config', From 45bad61b809c046f8616d1c64b10a173732c84d5 Mon Sep 17 00:00:00 2001 From: Andreas Jaeger Date: Mon, 18 May 2020 18:19:47 +0200 Subject: [PATCH 1596/1705] Add link to PDF document Switch to openstackdocstheme 2.2.0 that can link to PDF document, enable this with setting openstackdocs_pdf_link. Depends-On: https://review.opendev.org/728938 Change-Id: I103a90d4fa101fd261b2b1a86d0a42b9591accc9 --- doc/requirements.txt | 2 +- doc/source/conf.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index d1818034a..b5cc23e6f 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -2,7 +2,7 @@ # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. sphinx>=2.0.0,!=2.1.0 # BSD -openstackdocstheme>=2.1.2 # Apache-2.0 +openstackdocstheme>=2.2.0 # Apache-2.0 reno>=3.1.0 # Apache-2.0 sphinxcontrib-apidoc>=0.2.0 # BSD diff --git a/doc/source/conf.py b/doc/source/conf.py index 1be9ec51d..0552cccab 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -69,6 +69,7 @@ openstackdocs_repo_name = 'openstack/python-novaclient' openstackdocs_bug_project = 'python-novaclient' openstackdocs_bug_tag = '' +openstackdocs_pdf_link = True openstackdocs_projects = [ 'keystoneauth', 'nova', From 600384fa3b046f3c01e7f375f384610df99fae90 Mon Sep 17 00:00:00 2001 From: qiufossen Date: Sun, 26 Apr 2020 14:30:01 +0800 Subject: [PATCH 1597/1705] Remove Babel requirement Babel is not needed as requirement, remove it. Remove translation sections from setup.cfg. See also http://lists.openstack.org/pipermail/openstack-discuss/2020-April/014227.html Depends-On: https://review.opendev.org/#/c/730427/ Change-Id: I4d82cfc09d255ad574e44ac303fd187c5348ca47 --- babel.cfg | 1 - requirements.txt | 1 - setup.cfg | 14 -------------- 3 files changed, 16 deletions(-) delete mode 100644 babel.cfg diff --git a/babel.cfg b/babel.cfg deleted file mode 100644 index efceab818..000000000 --- a/babel.cfg +++ /dev/null @@ -1 +0,0 @@ -[python: **.py] diff --git a/requirements.txt b/requirements.txt index 609ea42b0..682f6b94a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,4 +9,3 @@ oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0 oslo.utils>=3.33.0 # Apache-2.0 PrettyTable<0.8,>=0.7.2 # BSD simplejson>=3.5.1 # MIT -Babel!=2.4.0,>=2.3.4 # BSD diff --git a/setup.cfg b/setup.cfg index 0a5cd0867..dcdc643f3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -31,17 +31,3 @@ packages = [entry_points] console_scripts = nova = novaclient.shell:main - -[compile_catalog] -domain = novaclient -directory = novaclient/locale - -[update_catalog] -domain = novaclient -output_dir = novaclient/locale -input_file = novaclient/locale/novaclient.pot - -[extract_messages] -keywords = _ gettext ngettext l_ lazy_gettext -mapping_file = babel.cfg -output_file = novaclient/locale/novaclient.pot From 109d41e722aec8df1a2325ea31630dc54e88e957 Mon Sep 17 00:00:00 2001 From: Doug Hellmann Date: Sat, 4 Jul 2020 15:10:29 -0400 Subject: [PATCH 1598/1705] use stevedore to load extensions Importing pkg_resources causes the app to scan the entire import path for all distributions, not just those providing entry points. The scanner in stevedore will have a cache of the entry point data, making it significantly faster. This will be especially useful in command line programs like python-openstackclient. Change-Id: Ic5eb9401c8ea3bd9624b818e0ffb8dcc13f61559 Signed-off-by: Doug Hellmann --- lower-constraints.txt | 2 +- novaclient/client.py | 15 +++++++++------ novaclient/tests/unit/test_discover.py | 25 +++++++++++++++---------- requirements.txt | 1 + 4 files changed, 26 insertions(+), 17 deletions(-) diff --git a/lower-constraints.txt b/lower-constraints.txt index cd722665d..d40bb1752 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -92,7 +92,7 @@ Routes==2.3.1 simplejson==3.5.1 smmap==0.9.0 statsd==3.2.1 -stevedore==1.20.0 +stevedore==2.0.1 tempest==17.1.0 tenacity==3.2.1 stestr==2.0.0 diff --git a/novaclient/client.py b/novaclient/client.py index 4b3e22819..6c15f04d1 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -28,7 +28,7 @@ from keystoneauth1 import identity from keystoneauth1 import session as ksession from oslo_utils import importutils -import pkg_resources +import stevedore import novaclient from novaclient import api_versions @@ -176,12 +176,15 @@ def _discover_via_python_path(): yield name, module -def _discover_via_entry_points(): - for ep in pkg_resources.iter_entry_points('novaclient.extension'): - name = ep.name - module = ep.load() +def _make_discovery_manager(): + # This function provides a place to mock out the entry point scan + return stevedore.ExtensionManager('novaclient.extension') + - yield name, module +def _discover_via_entry_points(): + mgr = _make_discovery_manager() + for extension in mgr: + yield extension.name, extension.plugin def _get_client_class_and_version(version): diff --git a/novaclient/tests/unit/test_discover.py b/novaclient/tests/unit/test_discover.py index 9bbbe3917..d5fa54000 100644 --- a/novaclient/tests/unit/test_discover.py +++ b/novaclient/tests/unit/test_discover.py @@ -17,7 +17,8 @@ import inspect from unittest import mock -import pkg_resources +import stevedore +from stevedore import extension from novaclient import client from novaclient.tests.unit import utils @@ -27,16 +28,20 @@ class DiscoverTest(utils.TestCase): def test_discover_via_entry_points(self): - def mock_iter_entry_points(group): - if group == 'novaclient.extension': - fake_ep = mock.Mock() - fake_ep.name = 'foo' - fake_ep.module = imp.new_module('foo') - fake_ep.load.return_value = fake_ep.module - return [fake_ep] + def mock_mgr(): + fake_ep = mock.Mock() + fake_ep.name = 'foo' + fake_ep.module = imp.new_module('foo') + fake_ep.load.return_value = fake_ep.module + fake_ext = extension.Extension( + name='foo', + entry_point=fake_ep, + plugin=fake_ep.module, + obj=None, + ) + return stevedore.ExtensionManager.make_test_instance([fake_ext]) - @mock.patch.object(pkg_resources, 'iter_entry_points', - mock_iter_entry_points) + @mock.patch.object(client, '_make_discovery_manager', mock_mgr) def test(): for name, module in client._discover_via_entry_points(): self.assertEqual('foo', name) diff --git a/requirements.txt b/requirements.txt index 682f6b94a..3f6c92808 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,3 +9,4 @@ oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0 oslo.utils>=3.33.0 # Apache-2.0 PrettyTable<0.8,>=0.7.2 # BSD simplejson>=3.5.1 # MIT +stevedore>=2.0.1 # Apache-2.0 From b7ae8c75cb8c34e2c6ab81a4f6b60fd937ec7f98 Mon Sep 17 00:00:00 2001 From: Takashi Natsume Date: Mon, 4 May 2020 13:50:07 +0900 Subject: [PATCH 1599/1705] Switch legacy Zuul jobs to native Zuul v3 jobs Replace the 'novaclient-dsvm-functional' legacy Zuul job with the 'python-novaclient-functional' native Zuul v3 job. See https://governance.openstack.org/tc/goals/selected/victoria/native-zuulv3-jobs.html for more details. Change-Id: I35b5699b8b0814f2d17a37e024286cd440047ec8 Signed-off-by: Takashi Natsume Co-Authored-By: Andrey Kurilin --- .zuul.yaml | 17 ++-- novaclient/tests/functional/base.py | 8 +- .../tests/functional/hooks/post_test_hook.sh | 46 ----------- novaclient/tests/functional/test_auth.py | 54 ++++++++++--- .../novaclient-dsvm-functional/post.yaml | 80 ------------------- .../novaclient-dsvm-functional/run.yaml | 54 ------------- playbooks/post.yaml | 6 ++ playbooks/python-novaclient-functional.yaml | 14 ++++ roles/get-os-environment/defaults/main.yaml | 2 + roles/get-os-environment/tasks/main.yaml | 12 +++ tox.ini | 2 +- 11 files changed, 92 insertions(+), 203 deletions(-) delete mode 100755 novaclient/tests/functional/hooks/post_test_hook.sh delete mode 100644 playbooks/legacy/novaclient-dsvm-functional/post.yaml delete mode 100644 playbooks/legacy/novaclient-dsvm-functional/run.yaml create mode 100644 playbooks/post.yaml create mode 100644 playbooks/python-novaclient-functional.yaml create mode 100644 roles/get-os-environment/defaults/main.yaml create mode 100644 roles/get-os-environment/tasks/main.yaml diff --git a/.zuul.yaml b/.zuul.yaml index a821c26dc..c07ac2b00 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -1,14 +1,15 @@ - job: - # TODO(efried): Cut over to zuulv3 - name: novaclient-dsvm-functional - parent: legacy-dsvm-base - run: playbooks/legacy/novaclient-dsvm-functional/run.yaml - post-run: playbooks/legacy/novaclient-dsvm-functional/post.yaml + name: python-novaclient-functional + parent: devstack + run: playbooks/python-novaclient-functional.yaml + post-run: playbooks/post.yaml timeout: 7200 required-projects: - - openstack/devstack-gate - openstack/nova - openstack/python-novaclient + vars: + devstack_localrc: + USE_PYTHON3: true irrelevant-files: - ^.*\.rst$ - ^doc/.*$ @@ -25,7 +26,7 @@ - release-notes-jobs-python3 check: jobs: - - novaclient-dsvm-functional + - python-novaclient-functional gate: jobs: - - novaclient-dsvm-functional + - python-novaclient-functional diff --git a/novaclient/tests/functional/base.py b/novaclient/tests/functional/base.py index f5c758be4..9497389f6 100644 --- a/novaclient/tests/functional/base.py +++ b/novaclient/tests/functional/base.py @@ -206,6 +206,8 @@ def setUp(self): self.insecure = cloud_config.config['insecure'] else: self.insecure = False + self.cacert = cloud_config.config['cacert'] + self.cert = cloud_config.config['cert'] auth = identity.Password(username=user, password=passwd, @@ -213,7 +215,11 @@ def setUp(self): auth_url=auth_url, project_domain_id=self.project_domain_id, user_domain_id=user_domain_id) - session = ksession.Session(auth=auth, verify=(not self.insecure)) + session = ksession.Session( + cert=self.cert, + auth=auth, + verify=(self.cacert or not self.insecure) + ) self.client = self._get_novaclient(session) diff --git a/novaclient/tests/functional/hooks/post_test_hook.sh b/novaclient/tests/functional/hooks/post_test_hook.sh deleted file mode 100755 index 473393eb6..000000000 --- a/novaclient/tests/functional/hooks/post_test_hook.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/bin/bash -xe - -# 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. - -# This script is executed inside post_test_hook function in devstack gate. - -function generate_testr_results { - if [ -f .testrepository/0 ]; then - sudo .tox/functional/bin/testr last --subunit > $WORKSPACE/testrepository.subunit - sudo mv $WORKSPACE/testrepository.subunit $BASE/logs/testrepository.subunit - sudo /usr/os-testr-env/bin/subunit2html $BASE/logs/testrepository.subunit $BASE/logs/testr_results.html - sudo gzip -9 $BASE/logs/testrepository.subunit - sudo gzip -9 $BASE/logs/testr_results.html - sudo chown $USER:$USER $BASE/logs/testrepository.subunit.gz $BASE/logs/testr_results.html.gz - sudo chmod a+r $BASE/logs/testrepository.subunit.gz $BASE/logs/testr_results.html.gz - fi -} - -export NOVACLIENT_DIR="$BASE/new/python-novaclient" - -sudo chown -R $USER:stack $NOVACLIENT_DIR - -# Go to the novaclient dir -cd $NOVACLIENT_DIR - -# Run tests -echo "Running novaclient functional test suite" -set +e -# Preserve env for OS_ credentials -sudo -E -H -u $USER tox -e ${TOX_ENV:-functional} -EXIT_CODE=$? -set -e - -# Collect and parse result -generate_testr_results -exit $EXIT_CODE diff --git a/novaclient/tests/functional/test_auth.py b/novaclient/tests/functional/test_auth.py index 8987c7289..c8d2396a1 100644 --- a/novaclient/tests/functional/test_auth.py +++ b/novaclient/tests/functional/test_auth.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import os from urllib import parse import tempest.lib.cli.base @@ -28,14 +29,19 @@ def _get_url(self, identity_api_version): url.fragment)) def nova_auth_with_password(self, action, identity_api_version): - flags = ('--os-username %s --os-tenant-name %s --os-password %s ' - '--os-auth-url %s --os-endpoint-type publicURL' % ( - self.cli_clients.username, - self.cli_clients.tenant_name, - self.cli_clients.password, - self._get_url(identity_api_version))) + flags = ( + f'--os-username {self.cli_clients.username} ' + f'--os-tenant-name {self.cli_clients.tenant_name} ' + f'--os-password {self.cli_clients.password} ' + f'--os-auth-url {self._get_url(identity_api_version)} ' + f'--os-endpoint-type publicURL' + ) + if self.cacert: + flags = f'{flags} --os-cacert {self.cacert}' + if self.cert: + flags = f'{flags} --os-cert {self.cert}' if self.cli_clients.insecure: - flags += ' --insecure ' + flags = f'{flags} --insecure' return tempest.lib.cli.base.execute( "nova", action, flags, cli_dir=self.cli_clients.cli_dir) @@ -49,15 +55,37 @@ def nova_auth_with_token(self, identity_api_version): if identity_api_version == "3": kw["project_domain_id"] = self.project_domain_id nova = client.Client("2", auth_token=token, auth_url=auth_url, - project_name=self.project_name, **kw) + project_name=self.project_name, + cacert=self.cacert, cert=self.cert, + **kw) nova.servers.list() - flags = ('--os-tenant-name %(project_name)s --os-token %(token)s ' - '--os-auth-url %(auth_url)s --os-endpoint-type publicURL' - % {"project_name": self.project_name, - "token": token, "auth_url": auth_url}) + # NOTE(andreykurilin): tempest.lib.cli.base.execute doesn't allow to + # pass 'env' argument to subprocess.Popen for overriding the current + # process' environment. + # When one of OS_AUTH_TYPE or OS_AUTH_PLUGIN environment variables + # presents, keystoneauth1 can load the wrong auth plugin with wrong + # expected cli arguments. To avoid this case, we need to modify + # current environment. + # TODO(andreykurilin): tempest.lib.cli.base.execute is quite simple + # method that can be replaced by subprocess.check_output direct call + # with passing env argument to avoid modifying the current process + # environment. or we probably can propose a change to tempest. + os.environ.pop("OS_AUTH_TYPE", None) + os.environ.pop("OS_AUTH_PLUGIN", None) + + flags = ( + f'--os-tenant-name {self.project_name} ' + f'--os-token {token} ' + f'--os-auth-url {auth_url} ' + f'--os-endpoint-type publicURL' + ) + if self.cacert: + flags = f'{flags} --os-cacert {self.cacert}' + if self.cert: + flags = f'{flags} --os-cert {self.cert}' if self.cli_clients.insecure: - flags += ' --insecure ' + flags = f'{flags} --insecure' tempest.lib.cli.base.execute( "nova", "list", flags, cli_dir=self.cli_clients.cli_dir) diff --git a/playbooks/legacy/novaclient-dsvm-functional/post.yaml b/playbooks/legacy/novaclient-dsvm-functional/post.yaml deleted file mode 100644 index dac875340..000000000 --- a/playbooks/legacy/novaclient-dsvm-functional/post.yaml +++ /dev/null @@ -1,80 +0,0 @@ -- hosts: primary - tasks: - - - name: Copy files from {{ ansible_user_dir }}/workspace/ on node - synchronize: - src: '{{ ansible_user_dir }}/workspace/' - dest: '{{ zuul.executor.log_root }}' - mode: pull - copy_links: true - verify_host: true - rsync_opts: - - --include=**/*nose_results.html - - --include=*/ - - --exclude=* - - --prune-empty-dirs - - - name: Copy files from {{ ansible_user_dir }}/workspace/ on node - synchronize: - src: '{{ ansible_user_dir }}/workspace/' - dest: '{{ zuul.executor.log_root }}' - mode: pull - copy_links: true - verify_host: true - rsync_opts: - - --include=**/*testr_results.html.gz - - --include=*/ - - --exclude=* - - --prune-empty-dirs - - - name: Copy files from {{ ansible_user_dir }}/workspace/ on node - synchronize: - src: '{{ ansible_user_dir }}/workspace/' - dest: '{{ zuul.executor.log_root }}' - mode: pull - copy_links: true - verify_host: true - rsync_opts: - - --include=/.testrepository/tmp* - - --include=*/ - - --exclude=* - - --prune-empty-dirs - - - name: Copy files from {{ ansible_user_dir }}/workspace/ on node - synchronize: - src: '{{ ansible_user_dir }}/workspace/' - dest: '{{ zuul.executor.log_root }}' - mode: pull - copy_links: true - verify_host: true - rsync_opts: - - --include=**/*testrepository.subunit.gz - - --include=*/ - - --exclude=* - - --prune-empty-dirs - - - name: Copy files from {{ ansible_user_dir }}/workspace/ on node - synchronize: - src: '{{ ansible_user_dir }}/workspace/' - dest: '{{ zuul.executor.log_root }}/tox' - mode: pull - copy_links: true - verify_host: true - rsync_opts: - - --include=/.tox/*/log/* - - --include=*/ - - --exclude=* - - --prune-empty-dirs - - - name: Copy files from {{ ansible_user_dir }}/workspace/ on node - synchronize: - src: '{{ ansible_user_dir }}/workspace/' - dest: '{{ zuul.executor.log_root }}' - mode: pull - copy_links: true - verify_host: true - rsync_opts: - - --include=/logs/** - - --include=*/ - - --exclude=* - - --prune-empty-dirs diff --git a/playbooks/legacy/novaclient-dsvm-functional/run.yaml b/playbooks/legacy/novaclient-dsvm-functional/run.yaml deleted file mode 100644 index 183d3989c..000000000 --- a/playbooks/legacy/novaclient-dsvm-functional/run.yaml +++ /dev/null @@ -1,54 +0,0 @@ -- hosts: all - name: novaclient-dsvm-functional job with identity v3 and neutron - tasks: - - - name: Ensure legacy workspace directory - file: - path: '{{ ansible_user_dir }}/workspace' - state: directory - - - shell: - cmd: | - set -e - set -x - cat > clonemap.yaml << EOF - clonemap: - - name: openstack/devstack-gate - dest: devstack-gate - EOF - /usr/zuul-env/bin/zuul-cloner -m clonemap.yaml --cache-dir /opt/git \ - https://opendev.org \ - openstack/devstack-gate - executable: /bin/bash - chdir: '{{ ansible_user_dir }}/workspace' - environment: '{{ zuul | zuul_legacy_vars }}' - - - shell: - cmd: | - set -e - set -x - export DEVSTACK_GATE_USE_PYTHON3=true - export PYTHONUNBUFFERED=true - export BRANCH_OVERRIDE=default - export DEVSTACK_PROJECT_FROM_GIT=python-novaclient - if [ "$BRANCH_OVERRIDE" != "default" ] ; then - export OVERRIDE_ZUUL_BRANCH=$BRANCH_OVERRIDE - fi - # This ensures that if we set override branch to something - # else, we still take python-novaclient from the zuul branch - # name. So override branch can be 'stable/mitaka' but we can - # test master changes. - uc_project=`echo $DEVSTACK_PROJECT_FROM_GIT | tr [:lower:] [:upper:] | tr '-' '_' | sed 's/[^A-Z_]//'` - export "OVERRIDE_"$uc_project"_PROJECT_BRANCH"=$ZUUL_BRANCH - - function post_test_hook { - # Configure and run functional tests - $BASE/new/python-novaclient/novaclient/tests/functional/hooks/post_test_hook.sh - } - export -f post_test_hook - - cp devstack-gate/devstack-vm-gate-wrap.sh ./safe-devstack-vm-gate-wrap.sh - ./safe-devstack-vm-gate-wrap.sh - executable: /bin/bash - chdir: '{{ ansible_user_dir }}/workspace' - environment: '{{ zuul | zuul_legacy_vars }}' diff --git a/playbooks/post.yaml b/playbooks/post.yaml new file mode 100644 index 000000000..9860e2a96 --- /dev/null +++ b/playbooks/post.yaml @@ -0,0 +1,6 @@ +- hosts: all + vars: + tox_envlist: functional + roles: + - fetch-tox-output + - fetch-subunit-output diff --git a/playbooks/python-novaclient-functional.yaml b/playbooks/python-novaclient-functional.yaml new file mode 100644 index 000000000..ea7d2db27 --- /dev/null +++ b/playbooks/python-novaclient-functional.yaml @@ -0,0 +1,14 @@ +- hosts: all + roles: + - run-devstack + # Run bindep and test-setup after devstack so that they won't interfere + - role: bindep + bindep_profile: test + bindep_dir: "{{ zuul_work_dir }}" + - test-setup + - get-os-environment + - ensure-tox + - role: tox + tox_envlist: functional + tox_install_siblings: false + environment: "{{ os_env_vars }}" diff --git a/roles/get-os-environment/defaults/main.yaml b/roles/get-os-environment/defaults/main.yaml new file mode 100644 index 000000000..91190b325 --- /dev/null +++ b/roles/get-os-environment/defaults/main.yaml @@ -0,0 +1,2 @@ +--- +openrc_file: "{{ devstack_base_dir|default('/opt/stack') }}/devstack/openrc" diff --git a/roles/get-os-environment/tasks/main.yaml b/roles/get-os-environment/tasks/main.yaml new file mode 100644 index 000000000..b3f457bbb --- /dev/null +++ b/roles/get-os-environment/tasks/main.yaml @@ -0,0 +1,12 @@ +- name: Extract the OS_ environment variables + shell: + cmd: | + source {{ openrc_file }} admin admin &>/dev/null + env | awk -F= 'BEGIN {print "---" } /^OS_/ { print " "$1": \""$2"\""} ' + args: + executable: "/bin/bash" + register: env_os + +- name: Save the OS_ environment variables as a fact + set_fact: + os_env_vars: "{{ env_os.stdout|from_yaml }}" diff --git a/tox.ini b/tox.ini index 89a99017e..5f4f4ac10 100644 --- a/tox.ini +++ b/tox.ini @@ -65,7 +65,7 @@ commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html [testenv:functional] -passenv = OS_NOVACLIENT_TEST_NETWORK +passenv = OS_* commands = stestr --test-path=./novaclient/tests/functional run --concurrency=1 {posargs} python novaclient/tests/functional/hooks/check_resources.py From 1322199845a095bafd0a16900f4899283fd0fabe Mon Sep 17 00:00:00 2001 From: Ghanshyam Mann Date: Fri, 10 Jul 2020 10:17:50 -0500 Subject: [PATCH 1600/1705] migrate testing to ubuntu focal As per victoria cycle testing runtime and community goal[1] we need to migrate upstream CI/CD to Ubuntu Focal(20.04). Fixing: - bug#1886298 Bump the lower constraints for below deps which added python3.8 support Markupsafe==1.1.1 cffi==1.14.0 greenlet==0.4.15 PyYAML==3.13 Story: #2007865 Task: #40200 Closes-Bug: #1886298 [1] https://governance.openstack.org/tc/goals/selected/victoria/migrate-ci-cd-jobs-to-ubuntu-focal.html Change-Id: I9857dc20057b97a2d0bae5b0b3410fa958bdc74f --- bindep.txt | 2 +- lower-constraints.txt | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/bindep.txt b/bindep.txt index 7aea44026..db7c560cf 100644 --- a/bindep.txt +++ b/bindep.txt @@ -10,7 +10,7 @@ libdbus-1-dev [platform:dpkg] libdbus-glib-1-dev [platform:dpkg] libffi-dev [platform:dpkg] libffi-devel [platform:rpm] -libssl-dev [platform:ubuntu-xenial] +libssl-dev [platform:ubuntu] libuuid-devel [platform:rpm] locales [platform:debian] python-dev [platform:dpkg] diff --git a/lower-constraints.txt b/lower-constraints.txt index d40bb1752..d5bda209b 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -4,7 +4,7 @@ asn1crypto==0.23.0 Babel==2.3.4 bandit==1.4.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 @@ -24,7 +24,7 @@ future==0.16.0 futurist==1.2.0 gitdb==0.6.4 GitPython==1.0.1 -greenlet==0.4.10 +greenlet==0.4.15 hacking==1.1.0 idna==2.6 iso8601==0.1.11 @@ -36,7 +36,7 @@ jsonschema==2.6.0 keystoneauth1==3.5.0 kombu==4.0.0 linecache2==1.0.0 -MarkupSafe==1.0 +MarkupSafe==1.1.1 mccabe==0.2.1 monotonic==0.6 msgpack-python==0.4.0 @@ -82,7 +82,7 @@ python-mimeparse==1.6.0 python-neutronclient==6.7.0 python-subunit==1.0.0 pytz==2013.6 -PyYAML==3.12 +PyYAML==3.13 repoze.lru==0.7 requests==2.14.2 requests-mock==1.2.0 From 1ce9edcd73c6da9dd4dcded12747957fc297cb21 Mon Sep 17 00:00:00 2001 From: Takashi Natsume Date: Tue, 28 Jul 2020 16:24:13 +0000 Subject: [PATCH 1601/1705] Add a cleanup for a server in a functional test A VM instance is created in the following functional test. * novaclient.tests.functional.v2.test_instance_action. TestInstanceActionCLIV262.test_show_actions_with_host However the VM instance is not deleted after the functional test. Add a cleanup for the server in the functional test. Change-Id: I8c2a6f91739d50baa283b37b16de67c542ea691b Closes-Bug: #1889283 Signed-off-by: Takashi Natsume --- novaclient/tests/functional/v2/test_instance_action.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/novaclient/tests/functional/v2/test_instance_action.py b/novaclient/tests/functional/v2/test_instance_action.py index 45849b95b..faeafe0f7 100644 --- a/novaclient/tests/functional/v2/test_instance_action.py +++ b/novaclient/tests/functional/v2/test_instance_action.py @@ -136,6 +136,8 @@ def test_show_actions_with_host(self): server = self.another_nova('boot --flavor %s --image %s --poll %s' % (self.flavor.name, self.image.name, name)) server_id = self._get_value_from_the_table(server, 'id') + self.addCleanup(self.client.servers.delete, server_id) + output = self.nova("instance-action-list %s" % server_id) request_id = self._get_column_value_from_single_row_table( output, "Request_ID") From 2ed34e657c251ac0c0a88e9f96eb4733f1937616 Mon Sep 17 00:00:00 2001 From: Takashi Natsume Date: Fri, 31 Jul 2020 10:54:53 +0000 Subject: [PATCH 1602/1705] Remove unused code The code has not been used since If0161a89877f19f24e91d780cf227fdc48e7e860 . Change-Id: I106404f9d12814fec2ec2bf16cdd100834085912 Signed-off-by: Takashi Natsume --- novaclient/tests/unit/fixture_data/servers.py | 22 ------------------- novaclient/tests/unit/v2/fakes.py | 18 --------------- 2 files changed, 40 deletions(-) diff --git a/novaclient/tests/unit/fixture_data/servers.py b/novaclient/tests/unit/fixture_data/servers.py index 7e4ee64ab..a8301548e 100644 --- a/novaclient/tests/unit/fixture_data/servers.py +++ b/novaclient/tests/unit/fixture_data/servers.py @@ -271,28 +271,6 @@ def put_servers_1234(request, context): status_code=204, headers=self.json_headers) - def post_os_volumes_boot(request, context): - body = request.json() - assert (set(body.keys()) <= - set(['server', 'os:scheduler_hints'])) - - fakes.assert_has_keys(body['server'], - required=['name', 'flavorRef'], - optional=['imageRef']) - - data = body['server'] - - # Require one, and only one, of the keys for bdm - if 'block_device_mapping' not in data: - if 'block_device_mapping_v2' not in data: - msg = "missing required keys: 'block_device_mapping'" - raise AssertionError(msg) - elif 'block_device_mapping_v2' in data: - msg = "found extra keys: 'block_device_mapping'" - raise AssertionError(msg) - - return {'server': self.server_9012} - # # Server password # diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index f3f8193a4..29492cbfb 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -593,24 +593,6 @@ def post_servers(self, body, **kw): else: return (202, {}, self.get_servers_1234()[2]) - def post_os_volumes_boot(self, body, **kw): - assert set(body.keys()) <= set(['server', 'os:scheduler_hints']) - fakes.assert_has_keys( - body['server'], - required=['name', 'flavorRef'], - optional=['imageRef']) - - # Require one, and only one, of the keys for bdm - if 'block_device_mapping' not in body['server']: - if 'block_device_mapping_v2' not in body['server']: - raise AssertionError( - "missing required keys: 'block_device_mapping'" - ) - elif 'block_device_mapping_v2' in body['server']: - raise AssertionError("found extra keys: 'block_device_mapping'") - - return (202, {}, self.get_servers_9012()[2]) - def get_servers_1234(self, **kw): server = self.get_servers_detail()[2]['servers'][0] if self.api_version >= api_versions.APIVersion('2.71'): From 553257d1b74493b7a9b30ffa2a960485d3588adf Mon Sep 17 00:00:00 2001 From: Luigi Toscano Date: Tue, 18 Aug 2020 17:03:36 +0200 Subject: [PATCH 1603/1705] zuul functional job: drop the custom playbooks The base devstack-tox-functional* jobs now set the required environment (OS_* vars set by devstack) before calling tox. Depends-On: https://review.opendev.org/746235 Change-Id: I7b6d49ea8320e014e2ef444e456f6eea02eca912 --- .zuul.yaml | 5 ++--- playbooks/post.yaml | 6 ------ playbooks/python-novaclient-functional.yaml | 14 -------------- roles/get-os-environment/defaults/main.yaml | 2 -- roles/get-os-environment/tasks/main.yaml | 12 ------------ 5 files changed, 2 insertions(+), 37 deletions(-) delete mode 100644 playbooks/post.yaml delete mode 100644 playbooks/python-novaclient-functional.yaml delete mode 100644 roles/get-os-environment/defaults/main.yaml delete mode 100644 roles/get-os-environment/tasks/main.yaml diff --git a/.zuul.yaml b/.zuul.yaml index c07ac2b00..ce16d8d85 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -1,13 +1,12 @@ - job: name: python-novaclient-functional - parent: devstack - run: playbooks/python-novaclient-functional.yaml - post-run: playbooks/post.yaml + parent: devstack-tox-functional timeout: 7200 required-projects: - openstack/nova - openstack/python-novaclient vars: + openrc_enable_export: true devstack_localrc: USE_PYTHON3: true irrelevant-files: diff --git a/playbooks/post.yaml b/playbooks/post.yaml deleted file mode 100644 index 9860e2a96..000000000 --- a/playbooks/post.yaml +++ /dev/null @@ -1,6 +0,0 @@ -- hosts: all - vars: - tox_envlist: functional - roles: - - fetch-tox-output - - fetch-subunit-output diff --git a/playbooks/python-novaclient-functional.yaml b/playbooks/python-novaclient-functional.yaml deleted file mode 100644 index ea7d2db27..000000000 --- a/playbooks/python-novaclient-functional.yaml +++ /dev/null @@ -1,14 +0,0 @@ -- hosts: all - roles: - - run-devstack - # Run bindep and test-setup after devstack so that they won't interfere - - role: bindep - bindep_profile: test - bindep_dir: "{{ zuul_work_dir }}" - - test-setup - - get-os-environment - - ensure-tox - - role: tox - tox_envlist: functional - tox_install_siblings: false - environment: "{{ os_env_vars }}" diff --git a/roles/get-os-environment/defaults/main.yaml b/roles/get-os-environment/defaults/main.yaml deleted file mode 100644 index 91190b325..000000000 --- a/roles/get-os-environment/defaults/main.yaml +++ /dev/null @@ -1,2 +0,0 @@ ---- -openrc_file: "{{ devstack_base_dir|default('/opt/stack') }}/devstack/openrc" diff --git a/roles/get-os-environment/tasks/main.yaml b/roles/get-os-environment/tasks/main.yaml deleted file mode 100644 index b3f457bbb..000000000 --- a/roles/get-os-environment/tasks/main.yaml +++ /dev/null @@ -1,12 +0,0 @@ -- name: Extract the OS_ environment variables - shell: - cmd: | - source {{ openrc_file }} admin admin &>/dev/null - env | awk -F= 'BEGIN {print "---" } /^OS_/ { print " "$1": \""$2"\""} ' - args: - executable: "/bin/bash" - register: env_os - -- name: Save the OS_ environment variables as a fact - set_fact: - os_env_vars: "{{ env_os.stdout|from_yaml }}" From fd24dc3487a78182bc65b40e86e9eaf3ea49b8c7 Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Thu, 10 Sep 2020 09:46:19 +0000 Subject: [PATCH 1604/1705] Update master for stable/victoria Add file to the reno documentation build to show release notes for stable/victoria. Use pbr instruction to increment the minor version number automatically so that master versions are higher than the versions on stable/victoria. Change-Id: I0fdaa5f2095d55221336212332d644d256c0c2c7 Sem-Ver: feature --- releasenotes/source/index.rst | 1 + releasenotes/source/victoria.rst | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 releasenotes/source/victoria.rst diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index fb30e94a6..e3a355281 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -8,6 +8,7 @@ Contents :maxdepth: 2 unreleased + victoria ussuri train stein diff --git a/releasenotes/source/victoria.rst b/releasenotes/source/victoria.rst new file mode 100644 index 000000000..4efc7b6f3 --- /dev/null +++ b/releasenotes/source/victoria.rst @@ -0,0 +1,6 @@ +============================= +Victoria Series Release Notes +============================= + +.. release-notes:: + :branch: stable/victoria From d47a2d846fcc1448b2448d54c9c6025356ab4cc0 Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Thu, 10 Sep 2020 09:46:21 +0000 Subject: [PATCH 1605/1705] Add Python3 wallaby unit tests This is an automatically generated patch to ensure unit testing is in place for all the of the tested runtimes for wallaby. See also the PTI in governance [1]. [1]: https://governance.openstack.org/tc/reference/project-testing-interface.html Change-Id: Ib43ce1fe7ae66f26b3c37bfaa7b58a0043ee26d6 --- .zuul.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.zuul.yaml b/.zuul.yaml index ce16d8d85..e1d4081f5 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -20,7 +20,7 @@ - lib-forward-testing-python3 - openstack-cover-jobs - openstack-lower-constraints-jobs - - openstack-python3-victoria-jobs + - openstack-python3-wallaby-jobs - publish-openstack-docs-pti - release-notes-jobs-python3 check: From 015f45b4ab84075ae1a0f7c1f316d53d43f84ffc Mon Sep 17 00:00:00 2001 From: "wu.shiming" Date: Mon, 14 Sep 2020 11:18:38 +0800 Subject: [PATCH 1606/1705] Remove install unnecessary packages The docs and releasenotes requirements migrated to doc/requirements.txt we need not install things from requirements.txt. Change-Id: I9d73c83b0520f724479c0e09b3686d3aab96df71 --- tox.ini | 2 -- 1 file changed, 2 deletions(-) diff --git a/tox.ini b/tox.ini index 5f4f4ac10..9036fcdbe 100644 --- a/tox.ini +++ b/tox.ini @@ -40,7 +40,6 @@ commands = {posargs} [testenv:docs] deps = -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} - -r{toxinidir}/requirements.txt -r{toxinidir}/doc/requirements.txt commands = rm -rf doc/build/html doc/build/doctrees @@ -59,7 +58,6 @@ commands = [testenv:releasenotes] deps = -c{env:TOX_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 releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html From 4012a88483e2ea43d40e6d48b13df7f6775e14a6 Mon Sep 17 00:00:00 2001 From: "wu.shiming" Date: Tue, 29 Sep 2020 18:08:21 +0800 Subject: [PATCH 1607/1705] Remove the unused coding style modules Python modules related to coding style checks (listed in blacklist.txt in openstack/requirements repo) are dropped from lower-constraints.txt as they are not actually used in tests (other than pep8). more info: https://github.com/openstack/requirements/blob/master/blacklist.txt Change-Id: I333bbddf70a4ead1778813a8311969ec372604d3 --- lower-constraints.txt | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lower-constraints.txt b/lower-constraints.txt index d5bda209b..66bf49ee5 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -2,7 +2,6 @@ amqp==2.1.1 appdirs==1.3.0 asn1crypto==0.23.0 Babel==2.3.4 -bandit==1.4.0 cachetools==2.0.0 cffi==1.14.0 cliff==2.8.0 @@ -19,13 +18,11 @@ eventlet==0.18.2 extras==1.0.0 fasteners==0.7.0 fixtures==3.0.0 -flake8==2.5.5 future==0.16.0 futurist==1.2.0 gitdb==0.6.4 GitPython==1.0.1 greenlet==0.4.15 -hacking==1.1.0 idna==2.6 iso8601==0.1.11 Jinja2==2.10 @@ -37,7 +34,6 @@ keystoneauth1==3.5.0 kombu==4.0.0 linecache2==1.0.0 MarkupSafe==1.1.1 -mccabe==0.2.1 monotonic==0.6 msgpack-python==0.4.0 munch==2.1.0 @@ -62,14 +58,12 @@ paramiko==2.0.0 Paste==2.0.2 PasteDeploy==1.5.0 pbr==2.0.0 -pep8==1.5.7 pika==0.10.0 pika-pool==0.1.3 positional==1.2.1 prettytable==0.7.2 pyasn1==0.1.8 pycparser==2.18 -pyflakes==0.8.1 pyinotify==0.9.6 pyOpenSSL==17.1.0 pyparsing==2.1.0 From 80c32f286640c708d4bb84b97a2d9f49aa8bbb6a Mon Sep 17 00:00:00 2001 From: songwenping Date: Mon, 12 Oct 2020 17:27:05 +0800 Subject: [PATCH 1608/1705] Cleanup py27 support Remove py2 stanza from setup.py Change-Id: I7aa67986248fc627c3f36203e0fb653bde2d483f --- setup.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/setup.py b/setup.py index 566d84432..cd35c3c35 100644 --- a/setup.py +++ b/setup.py @@ -13,17 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT import setuptools -# In python < 2.7.4, a lazy loading of package `pbr` will break -# setuptools if some other modules registered functions in `atexit`. -# solution from: http://bugs.python.org/issue15881#msg170215 -try: - import multiprocessing # noqa -except ImportError: - pass - setuptools.setup( setup_requires=['pbr>=2.0.0'], pbr=True) From 81eccaadf7eb6a483f0c0dc8a1ebb66bb7b53d0f Mon Sep 17 00:00:00 2001 From: songwenping Date: Mon, 12 Oct 2020 19:41:40 +0800 Subject: [PATCH 1609/1705] Remove Babel from lower-constraints.txt This can be removed because it's no longer a transitive dependency of oslo.i18n. Change-Id: I316363450caff890d67984f1466d3663785baf8f --- lower-constraints.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/lower-constraints.txt b/lower-constraints.txt index 66bf49ee5..a228819f3 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -1,7 +1,6 @@ amqp==2.1.1 appdirs==1.3.0 asn1crypto==0.23.0 -Babel==2.3.4 cachetools==2.0.0 cffi==1.14.0 cliff==2.8.0 From 1b5f29a3a4cead30c6455d9ecc47850f5e4994e1 Mon Sep 17 00:00:00 2001 From: Takashi Natsume Date: Sun, 3 Jan 2021 21:42:11 +0900 Subject: [PATCH 1610/1705] Fix a functional test for 'nova agent-list' The os-agents APIs have been removed by the following change. I9512f605dd2b3b0e88c951ed086250d57056303d This patch fixes a gate failure. A subsequent patch will make things related to the os-agents APIs deprecated. Change-Id: I9dab95fda5902bf9619393eb2c4a22d9f395d65a Closes-Bug: #1909899 Signed-off-by: Takashi Natsume --- .../tests/functional/v2/legacy/test_readonly_nova.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/novaclient/tests/functional/v2/legacy/test_readonly_nova.py b/novaclient/tests/functional/v2/legacy/test_readonly_nova.py index ed002c50b..a91188b87 100644 --- a/novaclient/tests/functional/v2/legacy/test_readonly_nova.py +++ b/novaclient/tests/functional/v2/legacy/test_readonly_nova.py @@ -86,8 +86,16 @@ def test_admin_help(self): self.nova('help') def test_agent_list(self): - self.nova('agent-list') - self.nova('agent-list', flags='--debug') + ex = self.assertRaises(exceptions.CommandFailed, + self.nova, 'agent-list') + self.assertIn( + "This resource is no longer available. " + "No forwarding address is given. (HTTP 410)", str(ex)) + ex = self.assertRaises(exceptions.CommandFailed, + self.nova, 'agent-list', flags='--debug') + self.assertIn( + "This resource is no longer available. " + "No forwarding address is given. (HTTP 410)", str(ex)) def test_migration_list(self): self.nova('migration-list') From e85d845b1aa5a18537907ba561a2718db2ccbed8 Mon Sep 17 00:00:00 2001 From: Takashi Natsume Date: Mon, 4 Jan 2021 13:39:38 +0900 Subject: [PATCH 1611/1705] Fix undesirable raw Python error Using the novaclient without a subcommand while passing an optional argument triggers the raw Python error `ERROR: 'Namespace' object has no attribute 'func'`. This bug can be reproduced by issuing the command `nova --os-compute-api-version 2.87`. Added a default value to `func` and an empty value to `command` as placeholders so that a help message is shown instead of the Python error. Change-Id: Ic3e87b67f6d27d40b03d7d8e520fa0f79a4d09e5 Closes-Bug: #1903727 Signed-off-by: Takashi Natsume --- novaclient/shell.py | 3 +++ novaclient/tests/unit/test_shell.py | 3 +++ .../notes/fix-raw-python-error-debd3edb17c2f675.yaml | 7 +++++++ 3 files changed, 13 insertions(+) create mode 100644 releasenotes/notes/fix-raw-python-error-debd3edb17c2f675.yaml diff --git a/novaclient/shell.py b/novaclient/shell.py index a81db89a8..7762be9b3 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -365,6 +365,9 @@ def get_base_parser(self, argv): help=_("Use this API endpoint instead of the Service Catalog. " "Defaults to env[OS_ENDPOINT_OVERRIDE].")) + parser.set_defaults(func=self.do_help) + parser.set_defaults(command='') + if osprofiler_profiler: parser.add_argument('--profile', metavar='HMAC_KEY', diff --git a/novaclient/tests/unit/test_shell.py b/novaclient/tests/unit/test_shell.py index 889916ea7..641952910 100644 --- a/novaclient/tests/unit/test_shell.py +++ b/novaclient/tests/unit/test_shell.py @@ -451,6 +451,9 @@ def test_help_option(self): def test_help_no_options(self): self._test_help('') + def test_help_no_subcommand(self): + self._test_help('--os-compute-api-version 2.87') + def test_help_on_subcommand(self): required = [ '.*?^usage: nova set-password', diff --git a/releasenotes/notes/fix-raw-python-error-debd3edb17c2f675.yaml b/releasenotes/notes/fix-raw-python-error-debd3edb17c2f675.yaml new file mode 100644 index 000000000..7686ecbeb --- /dev/null +++ b/releasenotes/notes/fix-raw-python-error-debd3edb17c2f675.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + `Bug #1903727 `_: + Fixed raw Python error message when using ``nova`` without + a subcommand while passing an optional argument, such as + ``--os-compute-api-version 2.87``. From 9b474afdb2e15d058babe82e50efe6c93c2e94a2 Mon Sep 17 00:00:00 2001 From: Takashi Natsume Date: Sun, 3 Jan 2021 23:06:46 +0900 Subject: [PATCH 1612/1705] Deprecate agent commands and APIs The os-agents APIs have been removed by the following change. I9512f605dd2b3b0e88c951ed086250d57056303d This patch makes commands related to the APIs deprecated in accordance with the following policy. * https://docs.openstack.org/python-novaclient/latest/contributor/deprecation-policy.html The API bindings related to the APIs remains as they are because python-openstackclient depends on the API bindings. Change-Id: I89d7877e23e8802fe77987a7b24ea247e08d5218 Signed-off-by: Takashi Natsume --- .../v2/legacy/test_readonly_nova.py | 8 +++++ novaclient/tests/unit/v2/test_shell.py | 32 ++++++++++++++----- novaclient/v2/agents.py | 5 +++ novaclient/v2/shell.py | 20 +++++++++--- .../deprecate-agent-d0f58718ad1782f6.yaml | 12 +++++++ 5 files changed, 65 insertions(+), 12 deletions(-) create mode 100644 releasenotes/notes/deprecate-agent-d0f58718ad1782f6.yaml diff --git a/novaclient/tests/functional/v2/legacy/test_readonly_nova.py b/novaclient/tests/functional/v2/legacy/test_readonly_nova.py index a91188b87..57d4107a1 100644 --- a/novaclient/tests/functional/v2/legacy/test_readonly_nova.py +++ b/novaclient/tests/functional/v2/legacy/test_readonly_nova.py @@ -91,11 +91,19 @@ def test_agent_list(self): self.assertIn( "This resource is no longer available. " "No forwarding address is given. (HTTP 410)", str(ex)) + self.assertIn( + "This command has been deprecated since 23.0.0 Wallaby Release " + "and will be removed in the first major release " + "after the Nova server 24.0.0 X release.", str(ex.stderr)) ex = self.assertRaises(exceptions.CommandFailed, self.nova, 'agent-list', flags='--debug') self.assertIn( "This resource is no longer available. " "No forwarding address is given. (HTTP 410)", str(ex)) + self.assertIn( + "This command has been deprecated since 23.0.0 Wallaby Release " + "and will be removed in the first major release " + "after the Nova server 24.0.0 X release.", str(ex.stderr)) def test_migration_list(self): self.nova('migration-list') diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index aeb8a5601..fd9cbfac7 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -118,14 +118,18 @@ def assert_not_called(self, method, url, body=None): return self.shell.cs.assert_not_called(method, url, body) def test_agents_list_with_hypervisor(self): - self.run_command('agent-list --hypervisor xen') + _, err = self.run_command('agent-list --hypervisor xen') self.assert_called('GET', '/os-agents?hypervisor=xen') + self.assertIn( + 'This command has been deprecated since 23.0.0 Wallaby Release ' + 'and will be removed in the first major release ' + 'after the Nova server 24.0.0 X release.', err) def test_agents_create(self): - self.run_command('agent-create win x86 7.0 ' - '/xxx/xxx/xxx ' - 'add6bb58e139be103324d04d82d8f546 ' - 'kvm') + _, err = self.run_command('agent-create win x86 7.0 ' + '/xxx/xxx/xxx ' + 'add6bb58e139be103324d04d82d8f546 ' + 'kvm') self.assert_called( 'POST', '/os-agents', {'agent': { @@ -135,19 +139,31 @@ def test_agents_create(self): 'version': '7.0', 'url': '/xxx/xxx/xxx', 'md5hash': 'add6bb58e139be103324d04d82d8f546'}}) + self.assertIn( + 'This command has been deprecated since 23.0.0 Wallaby Release ' + 'and will be removed in the first major release ' + 'after the Nova server 24.0.0 X release.', err) def test_agents_delete(self): - self.run_command('agent-delete 1') + _, err = self.run_command('agent-delete 1') self.assert_called('DELETE', '/os-agents/1') + self.assertIn( + 'This command has been deprecated since 23.0.0 Wallaby Release ' + 'and will be removed in the first major release ' + 'after the Nova server 24.0.0 X release.', err) def test_agents_modify(self): - self.run_command('agent-modify 1 8.0 /yyy/yyyy/yyyy ' - 'add6bb58e139be103324d04d82d8f546') + _, err = self.run_command('agent-modify 1 8.0 /yyy/yyyy/yyyy ' + 'add6bb58e139be103324d04d82d8f546') self.assert_called('PUT', '/os-agents/1', {"para": { "url": "/yyy/yyyy/yyyy", "version": "8.0", "md5hash": "add6bb58e139be103324d04d82d8f546"}}) + self.assertIn( + 'This command has been deprecated since 23.0.0 Wallaby Release ' + 'and will be removed in the first major release ' + 'after the Nova server 24.0.0 X release.', err) def test_boot(self): self.run_command('boot --flavor 1 --image %s ' diff --git a/novaclient/v2/agents.py b/novaclient/v2/agents.py index 0a6c223e5..d71bf1943 100644 --- a/novaclient/v2/agents.py +++ b/novaclient/v2/agents.py @@ -19,6 +19,11 @@ from novaclient import base +# NOTE(takashin): The os-agents APIs have been removed +# in https://review.opendev.org/c/openstack/nova/+/749309 . +# But the following API bindings remains as ther are +# because the python-openstackclient depends on them. + class Agent(base.Resource): def __repr__(self): diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 753dd127f..23f5486f6 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -55,6 +55,14 @@ def emit_duplicated_image_with_warning(img, image_with): file=sys.stderr) +# TODO(takashin): Remove this along with the deprecated commands in the first +# major python-novaclient release AFTER the nova server 24.0.0 X release. +def _emit_agent_deprecation_warning(): + print('This command has been deprecated since 23.0.0 Wallaby Release ' + 'and will be removed in the first major release ' + 'after the Nova server 24.0.0 X release.', file=sys.stderr) + + CLIENT_BDM2_KEYS = { 'id': 'uuid', 'source': 'source_type', @@ -3411,7 +3419,8 @@ def simplify_usage(u): default=None, help=_('Type of hypervisor.')) def do_agent_list(cs, args): - """List all builds.""" + """DEPRECATED List all builds.""" + _emit_agent_deprecation_warning() result = cs.agents.list(args.hypervisor) columns = ["Agent_id", "Hypervisor", "OS", "Architecture", "Version", 'Md5hash', 'Url'] @@ -3432,7 +3441,8 @@ def do_agent_list(cs, args): default='xen', help=_('Type of hypervisor.')) def do_agent_create(cs, args): - """Create new agent build.""" + """DEPRECATED Create new agent build.""" + _emit_agent_deprecation_warning() result = cs.agents.create(args.os, args.architecture, args.version, args.url, args.md5hash, args.hypervisor) @@ -3441,7 +3451,8 @@ def do_agent_create(cs, args): @utils.arg('id', metavar='', help=_('ID of the agent-build.')) def do_agent_delete(cs, args): - """Delete existing agent build.""" + """DEPRECATED Delete existing agent build.""" + _emit_agent_deprecation_warning() cs.agents.delete(args.id) @@ -3450,7 +3461,8 @@ def do_agent_delete(cs, args): @utils.arg('url', metavar='', help=_('URL')) @utils.arg('md5hash', metavar='', help=_('MD5 hash.')) def do_agent_modify(cs, args): - """Modify existing agent build.""" + """DEPRECATED Modify existing agent build.""" + _emit_agent_deprecation_warning() result = cs.agents.update(args.id, args.version, args.url, args.md5hash) utils.print_dict(result.to_dict()) diff --git a/releasenotes/notes/deprecate-agent-d0f58718ad1782f6.yaml b/releasenotes/notes/deprecate-agent-d0f58718ad1782f6.yaml new file mode 100644 index 000000000..c43d92cec --- /dev/null +++ b/releasenotes/notes/deprecate-agent-d0f58718ad1782f6.yaml @@ -0,0 +1,12 @@ +--- +deprecations: + - | + The following CLIs are deprecated. + + - ``nova agent-create`` + - ``nova agent-delete`` + - ``nova agent-list`` + - ``nova agent-modify`` + + The CLIs will be removed in the first major release after Nova 24.0.0 X + is released. From f50782f4028da030f4fdeafdc284b6e45d7c2f71 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 11 Feb 2021 12:24:11 +0000 Subject: [PATCH 1613/1705] Uncap PrettyTable This is now maintained as a Jazzband project [1]. [1] https://github.com/jazzband/prettytable Change-Id: I9477b1c5bc3bd4979d2cb38df3fc3b541a4b9bf5 Signed-off-by: Stephen Finucane --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 3f6c92808..d231ed542 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,6 +7,6 @@ iso8601>=0.1.11 # MIT oslo.i18n>=3.15.3 # Apache-2.0 oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0 oslo.utils>=3.33.0 # Apache-2.0 -PrettyTable<0.8,>=0.7.2 # BSD +PrettyTable>=0.7.2 # BSD simplejson>=3.5.1 # MIT stevedore>=2.0.1 # Apache-2.0 From e45953927898b639de9dbcba8edb6d07bcb4cba3 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 11 Feb 2021 12:24:45 +0000 Subject: [PATCH 1614/1705] requirements: Remove simplejson We don't need to support any version of Python that is missing the json stdlib library. Change-Id: I6d13719ef9d4ae40945cfc66c1d0b40018950c58 Signed-off-by: Stephen Finucane --- lower-constraints.txt | 1 - requirements.txt | 1 - 2 files changed, 2 deletions(-) diff --git a/lower-constraints.txt b/lower-constraints.txt index a228819f3..117b697ce 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -82,7 +82,6 @@ requests-mock==1.2.0 requestsexceptions==1.2.0 rfc3986==0.3.1 Routes==2.3.1 -simplejson==3.5.1 smmap==0.9.0 statsd==3.2.1 stevedore==2.0.1 diff --git a/requirements.txt b/requirements.txt index d231ed542..b93814eba 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,5 +8,4 @@ oslo.i18n>=3.15.3 # Apache-2.0 oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0 oslo.utils>=3.33.0 # Apache-2.0 PrettyTable>=0.7.2 # BSD -simplejson>=3.5.1 # MIT stevedore>=2.0.1 # Apache-2.0 From 54d4da112a6e84db5bda497364a49b9debfc2904 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 12 Jan 2021 17:18:56 +0000 Subject: [PATCH 1615/1705] Add support for microversion v2.88 The key change here is that the 'GET /os-hypervisors/{id}/uptime' API will now returns a HTTP 404 starting in 2.88. The 'GET /os-hypervisors/{id}' will instead now include an 'uptime' value. The 'novaclient.v2.hypervisors.HypervisorManager.uptime' method is updated to handle this. Change-Id: Ib99fbd820a586c14527ff64b319df0b7a44e1b8b Signed-off-by: Stephen Finucane --- novaclient/__init__.py | 2 +- .../tests/unit/fixture_data/hypervisors.py | 89 ++++++++++++--- novaclient/tests/unit/v2/test_hypervisors.py | 104 +++++++++++++++--- novaclient/tests/unit/v2/test_shell.py | 10 ++ novaclient/v2/hypervisors.py | 28 ++++- novaclient/v2/shell.py | 10 ++ .../microversion-v2_88-d91136020e3a3621.yaml | 16 +++ 7 files changed, 228 insertions(+), 31 deletions(-) create mode 100644 releasenotes/notes/microversion-v2_88-d91136020e3a3621.yaml diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 0b29e4f1b..50ad89d84 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.87") +API_MAX_VERSION = api_versions.APIVersion("2.88") diff --git a/novaclient/tests/unit/fixture_data/hypervisors.py b/novaclient/tests/unit/fixture_data/hypervisors.py index eeb5d51e4..3c04daa74 100644 --- a/novaclient/tests/unit/fixture_data/hypervisors.py +++ b/novaclient/tests/unit/fixture_data/hypervisors.py @@ -27,15 +27,41 @@ class V1(base.Fixture): service_id_1 = 1 service_id_2 = 2 + @staticmethod + def _transform_hypervisor_details(hypervisor): + """Transform a detailed hypervisor view from 2.53 to 2.88.""" + del hypervisor['current_workload'] + del hypervisor['disk_available_least'] + del hypervisor['free_ram_mb'] + del hypervisor['free_disk_gb'] + del hypervisor['local_gb'] + del hypervisor['local_gb_used'] + del hypervisor['memory_mb'] + del hypervisor['memory_mb_used'] + del hypervisor['running_vms'] + del hypervisor['vcpus'] + del hypervisor['vcpus_used'] + hypervisor['uptime'] = 'fake uptime' + def setUp(self): super(V1, self).setUp() - uuid_as_id = (api_versions.APIVersion(self.api_version) >= - api_versions.APIVersion('2.53')) + + api_version = api_versions.APIVersion(self.api_version) get_os_hypervisors = { 'hypervisors': [ - {'id': self.hyper_id_1, 'hypervisor_hostname': 'hyper1'}, - {'id': self.hyper_id_2, 'hypervisor_hostname': 'hyper2'}, + { + 'id': self.hyper_id_1, + 'hypervisor_hostname': 'hyper1', + 'state': 'up', + 'status': 'enabled', + }, + { + 'id': self.hyper_id_2, + 'hypervisor_hostname': 'hyper2', + 'state': 'up', + 'status': 'enabled', + }, ] } @@ -67,7 +93,9 @@ def setUp(self): 'current_workload': 2, 'running_vms': 2, 'cpu_info': 'cpu_info', - 'disk_available_least': 100 + 'disk_available_least': 100, + 'state': 'up', + 'status': 'enabled', }, { 'id': self.hyper_id_2, @@ -89,11 +117,17 @@ def setUp(self): 'current_workload': 2, 'running_vms': 2, 'cpu_info': 'cpu_info', - 'disk_available_least': 100 + 'disk_available_least': 100, + 'state': 'up', + 'status': 'enabled', } ] } + if api_version >= api_versions.APIVersion('2.88'): + for hypervisor in get_os_hypervisors_detail['hypervisors']: + self._transform_hypervisor_details(hypervisor) + self.requests_mock.get(self.url('detail'), json=get_os_hypervisors_detail, headers=self.headers) @@ -121,12 +155,22 @@ def setUp(self): get_os_hypervisors_search = { 'hypervisors': [ - {'id': self.hyper_id_1, 'hypervisor_hostname': 'hyper1'}, - {'id': self.hyper_id_2, 'hypervisor_hostname': 'hyper2'} + { + 'id': self.hyper_id_1, + 'hypervisor_hostname': 'hyper1', + 'state': 'up', + 'status': 'enabled', + }, + { + 'id': self.hyper_id_2, + 'hypervisor_hostname': 'hyper2', + 'state': 'up', + 'status': 'enabled', + }, ] } - if uuid_as_id: + if api_version >= api_versions.APIVersion('2.53'): url = self.url(hypervisor_hostname_pattern='hyper') else: url = self.url('hyper', 'search') @@ -134,7 +178,7 @@ def setUp(self): json=get_os_hypervisors_search, headers=self.headers) - if uuid_as_id: + if api_version >= api_versions.APIVersion('2.53'): get_os_hypervisors_search_u_v2_53 = { 'error_name': 'BadRequest', 'message': 'Invalid input for query parameters ' @@ -164,6 +208,8 @@ def setUp(self): { 'id': self.hyper_id_1, 'hypervisor_hostname': 'hyper1', + 'state': 'up', + 'status': 'enabled', 'servers': [ {'name': 'inst1', 'uuid': 'uuid1'}, {'name': 'inst2', 'uuid': 'uuid2'} @@ -172,6 +218,8 @@ def setUp(self): { 'id': self.hyper_id_2, 'hypervisor_hostname': 'hyper2', + 'state': 'up', + 'status': 'enabled', 'servers': [ {'name': 'inst3', 'uuid': 'uuid3'}, {'name': 'inst4', 'uuid': 'uuid4'} @@ -180,7 +228,7 @@ def setUp(self): ] } - if uuid_as_id: + if api_version >= api_versions.APIVersion('2.53'): url = self.url(hypervisor_hostname_pattern='hyper', with_servers=True) else: @@ -207,10 +255,16 @@ def setUp(self): 'current_workload': 2, 'running_vms': 2, 'cpu_info': 'cpu_info', - 'disk_available_least': 100 + 'disk_available_least': 100, + 'state': 'up', + 'status': 'enabled', } } + if api_version >= api_versions.APIVersion('2.88'): + self._transform_hypervisor_details( + get_os_hypervisors_hyper1['hypervisor']) + self.requests_mock.get(self.url(self.hyper_id_1), json=get_os_hypervisors_hyper1, headers=self.headers) @@ -219,7 +273,9 @@ def setUp(self): 'hypervisor': { 'id': self.hyper_id_1, 'hypervisor_hostname': 'hyper1', - 'uptime': 'fake uptime' + 'uptime': 'fake uptime', + 'state': 'up', + 'status': 'enabled', } } @@ -228,10 +284,15 @@ def setUp(self): headers=self.headers) -class V2_53(V1): +class V253(V1): """Fixture data for the os-hypervisors 2.53 API.""" api_version = '2.53' hyper_id_1 = 'd480b1b6-2255-43c2-b2c2-d60d42c2c074' hyper_id_2 = '43a8214d-f36a-4fc0-a25c-3cf35c17522d' service_id_1 = 'a87743ff-9c29-42ff-805d-2444659b5fc0' service_id_2 = '0486ab8b-1cfc-4ccb-9d94-9f22ec8bbd6b' + + +class V288(V253): + """Fixture data for the os-hypervisors 2.88 API.""" + api_version = '2.88' diff --git a/novaclient/tests/unit/v2/test_hypervisors.py b/novaclient/tests/unit/v2/test_hypervisors.py index 1c216b648..be48914fc 100644 --- a/novaclient/tests/unit/v2/test_hypervisors.py +++ b/novaclient/tests/unit/v2/test_hypervisors.py @@ -63,7 +63,9 @@ def test_hypervisor_detail(self): current_workload=2, running_vms=2, cpu_info='cpu_info', - disk_available_least=100), + disk_available_least=100, + state='up', + status='enabled'), dict(id=self.data_fixture.hyper_id_2, service=dict(id=self.data_fixture.service_id_2, host="compute2"), @@ -81,7 +83,24 @@ def test_hypervisor_detail(self): current_workload=2, running_vms=2, cpu_info='cpu_info', - disk_available_least=100)] + disk_available_least=100, + state='up', + status='enabled')] + + if self.cs.api_version >= api_versions.APIVersion('2.88'): + for hypervisor in expected: + del hypervisor['current_workload'] + del hypervisor['disk_available_least'] + del hypervisor['free_ram_mb'] + del hypervisor['free_disk_gb'] + del hypervisor['local_gb'] + del hypervisor['local_gb_used'] + del hypervisor['memory_mb'] + del hypervisor['memory_mb_used'] + del hypervisor['running_vms'] + del hypervisor['vcpus'] + del hypervisor['vcpus_used'] + hypervisor['uptime'] = 'fake uptime' result = self.cs.hypervisors.list() self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) @@ -93,9 +112,13 @@ def test_hypervisor_detail(self): def test_hypervisor_search(self): expected = [ dict(id=self.data_fixture.hyper_id_1, - hypervisor_hostname='hyper1'), + hypervisor_hostname='hyper1', + state='up', + status='enabled'), dict(id=self.data_fixture.hyper_id_2, - hypervisor_hostname='hyper2')] + hypervisor_hostname='hyper2', + state='up', + status='enabled')] result = self.cs.hypervisors.search('hyper') self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) @@ -131,11 +154,15 @@ def test_hypervisor_servers(self): expected = [ dict(id=self.data_fixture.hyper_id_1, hypervisor_hostname='hyper1', + state='up', + status='enabled', servers=[ dict(name='inst1', uuid='uuid1'), dict(name='inst2', uuid='uuid2')]), dict(id=self.data_fixture.hyper_id_2, hypervisor_hostname='hyper2', + state='up', + status='enabled', servers=[ dict(name='inst3', uuid='uuid3'), dict(name='inst4', uuid='uuid4')]), @@ -171,7 +198,23 @@ def test_hypervisor_get(self): current_workload=2, running_vms=2, cpu_info='cpu_info', - disk_available_least=100) + disk_available_least=100, + state='up', + status='enabled') + + if self.cs.api_version >= api_versions.APIVersion('2.88'): + del expected['current_workload'] + del expected['disk_available_least'] + del expected['free_ram_mb'] + del expected['free_disk_gb'] + del expected['local_gb'] + del expected['local_gb_used'] + del expected['memory_mb'] + del expected['memory_mb_used'] + del expected['running_vms'] + del expected['vcpus'] + del expected['vcpus_used'] + expected['uptime'] = 'fake uptime' result = self.cs.hypervisors.get(self.data_fixture.hyper_id_1) self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) @@ -184,7 +227,9 @@ def test_hypervisor_uptime(self): expected = dict( id=self.data_fixture.hyper_id_1, hypervisor_hostname="hyper1", - uptime="fake uptime") + uptime="fake uptime", + state='up', + status='enabled') result = self.cs.hypervisors.uptime(self.data_fixture.hyper_id_1) self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) @@ -215,11 +260,6 @@ def test_hypervisor_statistics(self): self.compare_to_expected(expected, result) - def test_hypervisor_statistics_data_model(self): - result = self.cs.hypervisor_stats.statistics() - self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('GET', '/os-hypervisors/statistics') - # Test for Bug #1370415, the line below used to raise AttributeError self.assertEqual("", result.__repr__()) @@ -237,19 +277,23 @@ def test_use_limit_marker_params(self): self.assertEqual([v], self.requests_mock.last_request.qs[k]) -class HypervisorsV2_53Test(HypervisorsV233Test): +class HypervisorsV253Test(HypervisorsV233Test): """Tests the os-hypervisors 2.53 API bindings.""" - data_fixture_class = data.V2_53 + data_fixture_class = data.V253 def setUp(self): - super(HypervisorsV2_53Test, self).setUp() + super(HypervisorsV253Test, self).setUp() self.cs.api_version = api_versions.APIVersion("2.53") def test_hypervisor_search_detailed(self): expected = [ dict(id=self.data_fixture.hyper_id_1, + state='up', + status='enabled', hypervisor_hostname='hyper1'), dict(id=self.data_fixture.hyper_id_2, + state='up', + status='enabled', hypervisor_hostname='hyper2')] result = self.cs.hypervisors.search('hyper', detailed=True) self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) @@ -257,3 +301,35 @@ def test_hypervisor_search_detailed(self): 'GET', '/os-hypervisors/detail?hypervisor_hostname_pattern=hyper') for idx, hyper in enumerate(result): self.compare_to_expected(expected[idx], hyper) + + +class HypervisorsV288Test(HypervisorsV253Test): + data_fixture_class = data.V288 + + def setUp(self): + super().setUp() + self.cs.api_version = api_versions.APIVersion('2.88') + + def test_hypervisor_uptime(self): + expected = { + 'id': self.data_fixture.hyper_id_1, + 'hypervisor_hostname': 'hyper1', + 'uptime': 'fake uptime', + 'state': 'up', + 'status': 'enabled', + } + + result = self.cs.hypervisors.uptime(self.data_fixture.hyper_id_1) + self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called( + 'GET', '/os-hypervisors/%s' % self.data_fixture.hyper_id_1) + + self.compare_to_expected(expected, result) + + def test_hypervisor_statistics(self): + exc = self.assertRaises( + exceptions.UnsupportedVersion, + self.cs.hypervisor_stats.statistics) + self.assertIn( + "The 'statistics' API is removed in API version 2.88 or later.", + str(exc)) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index fd9cbfac7..17c8974d3 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -3575,6 +3575,16 @@ def test_hypervisor_stats(self): self.run_command('hypervisor-stats') self.assert_called('GET', '/os-hypervisors/statistics') + def test_hypervisor_stats_v2_88(self): + """Tests nova hypervisor-stats at the 2.88 microversion.""" + ex = self.assertRaises( + exceptions.CommandError, self.run_command, + 'hypervisor-stats', api_version='2.88') + self.assertIn( + 'The hypervisor-stats command is not supported in API version ' + '2.88 or later.', + str(ex)) + def test_quota_show(self): self.run_command( 'quota-show --tenant ' diff --git a/novaclient/v2/hypervisors.py b/novaclient/v2/hypervisors.py index c705dc659..f0cc5863c 100644 --- a/novaclient/v2/hypervisors.py +++ b/novaclient/v2/hypervisors.py @@ -123,8 +123,25 @@ def uptime(self, hypervisor): :param hypervisor: Either a Hypervisor object or an ID. Starting with microversion 2.53 the ID must be a UUID value. """ - return self._get("/os-hypervisors/%s/uptime" % base.getid(hypervisor), - "hypervisor") + # Starting with microversion 2.88, the '/os-hypervisors/{id}/uptime' + # route is removed in favour of returning 'uptime' in the response of + # the '/os-hypervisors/{id}' route. This behaves slightly differently, + # in that it won't error out if a virt driver doesn't support reporting + # uptime or if the hypervisor is down, but it's a good enough + # approximation + if self.api_version < api_versions.APIVersion("2.88"): + return self._get( + "/os-hypervisors/%s/uptime" % base.getid(hypervisor), + "hypervisor") + + resp, body = self.api.client.get( + "/os-hypervisors/%s" % base.getid(hypervisor) + ) + content = { + k: v for k, v in body['hypervisor'].items() + if k in ('id', 'hypervisor_hostname', 'state', 'status', 'uptime') + } + return self.resource_class(self, content, loaded=True, resp=resp) def statistics(self): """ @@ -145,8 +162,15 @@ def __repr__(self): class HypervisorStatsManager(base.Manager): resource_class = HypervisorStats + @api_versions.wraps("2.0", "2.87") def statistics(self): """ Get hypervisor statistics over all compute nodes. """ return self._get("/os-hypervisors/statistics", "hypervisor_statistics") + + @api_versions.wraps("2.88") + def statistics(self): + raise exceptions.UnsupportedVersion( + _("The 'statistics' API is removed in API version 2.88 or later.") + ) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 23f5486f6..eb0315fd0 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -4049,12 +4049,22 @@ def do_hypervisor_uptime(cs, args): utils.print_dict(hyper.to_dict()) +@api_versions.wraps('2.0', '2.87') def do_hypervisor_stats(cs, args): """Get hypervisor statistics over all compute nodes.""" stats = cs.hypervisor_stats.statistics() utils.print_dict(stats.to_dict()) +@api_versions.wraps('2.88') +def do_hypervisor_stats(cs, args): + msg = _( + "The hypervisor-stats command is not supported in API version 2.88 " + "or later." + ) + raise exceptions.CommandError(msg) + + @utils.arg('server', metavar='', help=_('Name or ID of server.')) @utils.arg( '--port', diff --git a/releasenotes/notes/microversion-v2_88-d91136020e3a3621.yaml b/releasenotes/notes/microversion-v2_88-d91136020e3a3621.yaml new file mode 100644 index 000000000..994c6802f --- /dev/null +++ b/releasenotes/notes/microversion-v2_88-d91136020e3a3621.yaml @@ -0,0 +1,16 @@ +--- +features: + - | + Added support for `microversion 2.88`_. The + ``novaclient.v2.hypervisors.HypervisorManager.uptime`` method will now + transparently switch between the ``/os-hypervisors/{id}/uptime`` API, + which is deprecated in 2.88, and the ``/os-hypervisors/{id}`` API, which + now includes uptime information, based on the microversion. + + .. _microversion 2.88: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id80 +deprecations: + - | + The ``nova hypervisor-stats`` command and underlying + ``novaclient.v2.hypervisors.HypervisorStatsManager.statistics`` API are + deprecated starting in microversion 2.88 and will return an error starting + on this version. From 8cdbb0f999bd6ff712da7d0c3fa04f296a63e74f Mon Sep 17 00:00:00 2001 From: Balazs Gibizer Date: Thu, 18 Mar 2021 14:53:33 +0100 Subject: [PATCH 1616/1705] Use well named anchor into the microversion history Depends-On: https://review.opendev.org/c/openstack/nova/+/780401 Change-Id: Ic6667b1b5efe75273035c65aca2c111c90fe8e00 --- releasenotes/notes/microversion-v2_88-d91136020e3a3621.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/releasenotes/notes/microversion-v2_88-d91136020e3a3621.yaml b/releasenotes/notes/microversion-v2_88-d91136020e3a3621.yaml index 994c6802f..6d8666373 100644 --- a/releasenotes/notes/microversion-v2_88-d91136020e3a3621.yaml +++ b/releasenotes/notes/microversion-v2_88-d91136020e3a3621.yaml @@ -7,7 +7,7 @@ features: which is deprecated in 2.88, and the ``/os-hypervisors/{id}`` API, which now includes uptime information, based on the microversion. - .. _microversion 2.88: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id80 + .. _microversion 2.88: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#microversion-2-88 deprecations: - | The ``nova hypervisor-stats`` command and underlying From 91df194909825d3f4a3a142629602c2a576a4c2f Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Fri, 19 Mar 2021 19:49:28 +0000 Subject: [PATCH 1617/1705] Update master for stable/wallaby Add file to the reno documentation build to show release notes for stable/wallaby. Use pbr instruction to increment the minor version number automatically so that master versions are higher than the versions on stable/wallaby. Sem-Ver: feature Change-Id: I8db7244df81c9474b8e8bcb7ce46b7500d9dca6b --- releasenotes/source/index.rst | 1 + releasenotes/source/wallaby.rst | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 releasenotes/source/wallaby.rst diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index e3a355281..eb104532d 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -8,6 +8,7 @@ Contents :maxdepth: 2 unreleased + wallaby victoria ussuri train diff --git a/releasenotes/source/wallaby.rst b/releasenotes/source/wallaby.rst new file mode 100644 index 000000000..d77b56599 --- /dev/null +++ b/releasenotes/source/wallaby.rst @@ -0,0 +1,6 @@ +============================ +Wallaby Series Release Notes +============================ + +.. release-notes:: + :branch: stable/wallaby From ef6b853f3f72382d5c68de5203d464a322c1ee36 Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Fri, 19 Mar 2021 19:49:39 +0000 Subject: [PATCH 1618/1705] Add Python3 xena unit tests This is an automatically generated patch to ensure unit testing is in place for all the of the tested runtimes for xena. See also the PTI in governance [1]. [1]: https://governance.openstack.org/tc/reference/project-testing-interface.html Change-Id: I35d282c9b0342a4520c6faf07f65a121c6b9ecb2 --- .zuul.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.zuul.yaml b/.zuul.yaml index e1d4081f5..8072db0b1 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -20,7 +20,7 @@ - lib-forward-testing-python3 - openstack-cover-jobs - openstack-lower-constraints-jobs - - openstack-python3-wallaby-jobs + - openstack-python3-xena-jobs - publish-openstack-docs-pti - release-notes-jobs-python3 check: From d4399d3d36c456c1001b96178ce9b3e7ec082460 Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Thu, 25 Mar 2021 15:08:20 +0100 Subject: [PATCH 1619/1705] When creating a client, pass the default logger When a client is created without a logger, novaclient creates a default logger, but it's then not used, because it's not passed to the factory function. Because of that, all novaclient calls are getting logged as 'keystoneauth.session' instead of 'novaclient.v2.client' as they should. Closes-bug: 1921388 Change-Id: I53caceb08667eb12e27016731868e8015dd10e34 --- novaclient/v2/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/v2/client.py b/novaclient/v2/client.py index dc1701169..e7b677b72 100644 --- a/novaclient/v2/client.py +++ b/novaclient/v2/client.py @@ -192,7 +192,7 @@ def __init__(self, endpoint_type=endpoint_type, http_log_debug=http_log_debug, insecure=insecure, - logger=logger, + logger=self.logger, os_cache=self.os_cache, password=password, project_domain_id=project_domain_id, From 21edd11ba95df0d71403a0b995d26df67b2f629f Mon Sep 17 00:00:00 2001 From: melanie witt Date: Sat, 10 Apr 2021 02:49:37 +0000 Subject: [PATCH 1620/1705] Add unit tests for client logger Change I53caceb08667eb12e27016731868e8015dd10e34 fixed a bug where we weren't using our novaclient logger default when a logger was not passed to the Client constructor. This adds unit tests to assert the fixed behavior and protect against regression. Related-Bug: #1921388 Change-Id: I9f622c01a6b1abe328a60de6d3e438e68872dd80 --- novaclient/tests/unit/test_client.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/novaclient/tests/unit/test_client.py b/novaclient/tests/unit/test_client.py index d157b73b2..f0154ab78 100644 --- a/novaclient/tests/unit/test_client.py +++ b/novaclient/tests/unit/test_client.py @@ -124,3 +124,15 @@ def test__check_arguments(self, mock_warnings): self.assertNotEqual(original_kwargs, actual_kwargs) self.assertEqual({}, actual_kwargs) self.assertTrue(mock_warnings.warn.called) + + +class ClientTest(utils.TestCase): + + def test_logger(self): + client = novaclient.client.Client('2.1', logger=mock.sentinel.logger) + self.assertEqual(mock.sentinel.logger, client.logger) + self.assertEqual(mock.sentinel.logger, client.client.logger) + client = novaclient.client.Client('2.1') + self.assertEqual('novaclient.v2.client', client.logger.name) + self.assertIsNotNone(client.client.logger) + self.assertEqual('novaclient.v2.client', client.client.logger.name) From b5faf37f1b9f8cd6c065327309c8290c211d13d2 Mon Sep 17 00:00:00 2001 From: zhangboye Date: Tue, 20 Apr 2021 09:34:55 +0800 Subject: [PATCH 1621/1705] Use py3 as the default runtime for tox Moving on py3 as the default runtime for tox to avoid to update this at each new cycle. Change-Id: Icad6d0d0d375d1995baa603b091be50797210215 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 9036fcdbe..b8072ee48 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py38,pep8,docs +envlist = py3,pep8,docs minversion = 3.1 skipsdist = true ignore_basepython_conflict = true From 3dc9ad974e86bdf93177bc27e85978f943cc944c Mon Sep 17 00:00:00 2001 From: YuehuiLei Date: Tue, 4 May 2021 10:52:16 +0800 Subject: [PATCH 1622/1705] setup.cfg: Replace dashes with underscores Setuptools v54.1.0 introduces a warning that the use of dash-separated options in 'setup.cfg' will not be supported in a future version [1]. Get ahead of the issue by replacing the dashes with underscores. Without this, we see 'UserWarning' messages like the following on new enough versions of setuptools: UserWarning: Usage of dash-separated 'description-file' will not be supported in future versions. Please use the underscore name 'description_file' instead [1] https://github.com/pypa/setuptools/commit/a2e9ae4cb Change-Id: Ia98a366c65321f76394940be0f9ce2a55dd321d4 --- setup.cfg | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/setup.cfg b/setup.cfg index dcdc643f3..62c23d46d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,13 +1,13 @@ [metadata] name = python-novaclient summary = Client library for OpenStack Compute API -description-file = +description_file = README.rst license = Apache License, Version 2.0 author = OpenStack -author-email = openstack-discuss@lists.openstack.org -home-page = https://docs.openstack.org/python-novaclient/latest -python-requires = >=3.6 +author_email = openstack-discuss@lists.openstack.org +home_page = https://docs.openstack.org/python-novaclient/latest +python_requires = >=3.6 classifier = Development Status :: 5 - Production/Stable Environment :: Console From 935fe75a660ac366f4780ef37658a599cdcc6a35 Mon Sep 17 00:00:00 2001 From: Takashi Natsume Date: Thu, 6 May 2021 17:31:18 +0900 Subject: [PATCH 1623/1705] Refactor constructing request body Add a private static method to construct a request body for create requests in the novaclient.v2.volumes.VolumeManager class. Change-Id: I884ad4b471e3d196255901499c75a1a2f0535f65 Signed-off-by: Takashi Natsume --- novaclient/v2/volumes.py | 51 +++++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/novaclient/v2/volumes.py b/novaclient/v2/volumes.py index 7153c835d..93fdd8682 100644 --- a/novaclient/v2/volumes.py +++ b/novaclient/v2/volumes.py @@ -39,6 +39,20 @@ class VolumeManager(base.Manager): """ resource_class = Volume + @staticmethod + def _get_request_body_for_create(volume_id, device=None, tag=None, + delete_on_termination=False): + body = {'volumeAttachment': {'volumeId': volume_id}} + if device is not None: + body['volumeAttachment']['device'] = device + if tag is not None: + body['volumeAttachment']['tag'] = tag + if delete_on_termination: + body['volumeAttachment']['delete_on_termination'] = ( + delete_on_termination) + + return body + @api_versions.wraps("2.0", "2.48") def create_server_volume(self, server_id, volume_id, device=None): """ @@ -49,11 +63,10 @@ def create_server_volume(self, server_id, volume_id, device=None): :param device: The device name (optional) :rtype: :class:`Volume` """ - body = {'volumeAttachment': {'volumeId': volume_id}} - if device is not None: - body['volumeAttachment']['device'] = device - return self._create("/servers/%s/os-volume_attachments" % server_id, - body, "volumeAttachment") + return self._create( + "/servers/%s/os-volume_attachments" % server_id, + VolumeManager._get_request_body_for_create(volume_id, device), + "volumeAttachment") @api_versions.wraps("2.49", "2.78") def create_server_volume(self, server_id, volume_id, device=None, @@ -67,13 +80,10 @@ def create_server_volume(self, server_id, volume_id, device=None, :param tag: The tag (optional) :rtype: :class:`Volume` """ - body = {'volumeAttachment': {'volumeId': volume_id}} - if device is not None: - body['volumeAttachment']['device'] = device - if tag is not None: - body['volumeAttachment']['tag'] = tag - return self._create("/servers/%s/os-volume_attachments" % server_id, - body, "volumeAttachment") + return self._create( + "/servers/%s/os-volume_attachments" % server_id, + VolumeManager._get_request_body_for_create(volume_id, device, tag), + "volumeAttachment") @api_versions.wraps("2.79") def create_server_volume(self, server_id, volume_id, device=None, @@ -90,18 +100,11 @@ def create_server_volume(self, server_id, volume_id, device=None, (optional). :rtype: :class:`Volume` """ - # TODO(mriedem): Move this body construction into a private common - # helper method for all versions of create_server_volume to use. - body = {'volumeAttachment': {'volumeId': volume_id}} - if device is not None: - body['volumeAttachment']['device'] = device - if tag is not None: - body['volumeAttachment']['tag'] = tag - if delete_on_termination: - body['volumeAttachment']['delete_on_termination'] = ( - delete_on_termination) - return self._create("/servers/%s/os-volume_attachments" % server_id, - body, "volumeAttachment") + return self._create( + "/servers/%s/os-volume_attachments" % server_id, + VolumeManager._get_request_body_for_create(volume_id, device, tag, + delete_on_termination), + "volumeAttachment") @api_versions.wraps("2.0", "2.84") def update_server_volume(self, server_id, src_volid, dest_volid): From 665f1c8d4c5102bc06298a4c1a46208949639610 Mon Sep 17 00:00:00 2001 From: Takashi Natsume Date: Tue, 18 May 2021 22:27:57 +0900 Subject: [PATCH 1624/1705] Change minversion of tox to 3.18.0 The patch bumps min version of tox to 3.18.0 in order to replace whitelist_externals by allowlist_externals option: https://github.com/tox-dev/tox/blob/master/docs/changelog.rst#v3180-2020-07-23 Change-Id: I75844ded50c0032445dd27b1ced7f214c754e901 Signed-off-by: Takashi Natsume --- tox.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index b8072ee48..108052813 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] envlist = py3,pep8,docs -minversion = 3.1 +minversion = 3.18.0 skipsdist = true ignore_basepython_conflict = true @@ -8,7 +8,7 @@ ignore_basepython_conflict = true basepython = python3 usedevelop = true # tox is silly... these need to be separated by a newline.... -whitelist_externals = +allowlist_externals = find rm make From 273c41574a3d22a5b204d9c3775fe8ed55976a4b Mon Sep 17 00:00:00 2001 From: Takashi Kajinami Date: Tue, 27 Jul 2021 11:00:06 +0900 Subject: [PATCH 1625/1705] Use Block Storage API v3 instead of API v2 Block Storage API v2 was deprecated during Pike cycle and is being removed during Xena cycle, and current v3 API should be used instead. Change-Id: I4b4c08d65e642866d81d7fd12a7c82162a0b979e --- novaclient/tests/functional/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/tests/functional/base.py b/novaclient/tests/functional/base.py index 9497389f6..be4c5d74f 100644 --- a/novaclient/tests/functional/base.py +++ b/novaclient/tests/functional/base.py @@ -13,7 +13,7 @@ import os import time -from cinderclient.v2 import client as cinderclient +from cinderclient.v3 import client as cinderclient import fixtures from glanceclient import client as glanceclient from keystoneauth1.exceptions import discovery as discovery_exc From bff8d4137057c9bc37436b8df29d86a3c2584938 Mon Sep 17 00:00:00 2001 From: Takashi Kajinami Date: Mon, 16 Aug 2021 09:54:06 +0900 Subject: [PATCH 1626/1705] Use importlib instead of imp ... because the imp module is deprecated since Python 3.4 . Closes-Bug: #1937904 Change-Id: Ia3f83df336fd243c25f7471d56a44370c11bb5e1 --- novaclient/tests/unit/test_discover.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/novaclient/tests/unit/test_discover.py b/novaclient/tests/unit/test_discover.py index d5fa54000..f294e2e81 100644 --- a/novaclient/tests/unit/test_discover.py +++ b/novaclient/tests/unit/test_discover.py @@ -13,7 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. -import imp +import importlib import inspect from unittest import mock @@ -31,7 +31,8 @@ def test_discover_via_entry_points(self): def mock_mgr(): fake_ep = mock.Mock() fake_ep.name = 'foo' - fake_ep.module = imp.new_module('foo') + module_spec = importlib.machinery.ModuleSpec('foo', None) + fake_ep.module = importlib.util.module_from_spec(module_spec) fake_ep.load.return_value = fake_ep.module fake_ext = extension.Extension( name='foo', @@ -52,10 +53,14 @@ def test(): def test_discover_extensions(self): def mock_discover_via_python_path(): - yield 'foo', imp.new_module('foo') + module_spec = importlib.machinery.ModuleSpec('foo', None) + module = importlib.util.module_from_spec(module_spec) + yield 'foo', module def mock_discover_via_entry_points(): - yield 'baz', imp.new_module('baz') + module_spec = importlib.machinery.ModuleSpec('baz', None) + module = importlib.util.module_from_spec(module_spec) + yield 'baz', module @mock.patch.object(client, '_discover_via_python_path', From cfa172c4fd75cae1c6cbed31def48f28eafbb89b Mon Sep 17 00:00:00 2001 From: Lee Yarwood Date: Tue, 17 Aug 2021 12:06:25 +0100 Subject: [PATCH 1627/1705] Microversion 2.89 - os-volume_attachments Depends-On: https://review.opendev.org/c/openstack/nova/+/804275 Change-Id: If6275dbd3795047c111ac507a12b034e60029df8 --- novaclient/__init__.py | 2 +- novaclient/tests/unit/v2/test_shell.py | 19 +++++++++++++++++++ novaclient/v2/shell.py | 4 ++++ .../microversion_v2_89-af6223273b2bdfb0.yaml | 11 +++++++++++ 4 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/microversion_v2_89-af6223273b2bdfb0.yaml diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 50ad89d84..5f3b69530 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.88") +API_MAX_VERSION = api_versions.APIVersion("2.89") diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 17c8974d3..10e8c732e 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -3994,6 +3994,24 @@ def test_volume_attachments_v2_79(self): self.assert_called('GET', '/servers/1234/os-volume_attachments') self.assertIn('DELETE ON TERMINATION', out) + def test_volume_attachments_pre_v2_89(self): + out = self.run_command( + 'volume-attachments 1234', api_version='2.88')[0] + self.assert_called('GET', '/servers/1234/os-volume_attachments') + # We can't assert just ID here as it's part of various other fields + self.assertIn('| ID', out) + self.assertNotIn('ATTACHMENT ID', out) + self.assertNotIn('BDM UUID', out) + + def test_volume_attachments_v2_89(self): + out = self.run_command( + 'volume-attachments 1234', api_version='2.89')[0] + self.assert_called('GET', '/servers/1234/os-volume_attachments') + # We can't assert just ID here as it's part of various other fields + self.assertNotIn('| ID', out) + self.assertIn('ATTACHMENT ID', out) + self.assertIn('BDM UUID', out) + def test_volume_attach_with_delete_on_termination_pre_v2_79(self): self.assertRaises( SystemExit, self.run_command, @@ -4577,6 +4595,7 @@ def test_versions(self): 84, # There are no version-wrapped shell method changes for this. 86, # doesn't require any changes in novaclient. 87, # doesn't require any changes in novaclient. + 89, # There are no version-wrapped shell method changes for this. ]) versions_supported = set(range(0, novaclient.API_MAX_VERSION.ver_minor + 1)) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index eb0315fd0..e47fcbc01 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -2800,6 +2800,10 @@ def do_volume_attachments(cs, args): _translate_volume_attachments_keys(volumes) # Microversion >= 2.70 returns the tag value. fields = ['ID', 'DEVICE', 'SERVER ID', 'VOLUME ID'] + if cs.api_version >= api_versions.APIVersion('2.89'): + fields.remove('ID') + fields.append('ATTACHMENT ID') + fields.append('BDM UUID') if cs.api_version >= api_versions.APIVersion('2.70'): fields.append('TAG') # Microversion >= 2.79 returns the delete_on_termination value. diff --git a/releasenotes/notes/microversion_v2_89-af6223273b2bdfb0.yaml b/releasenotes/notes/microversion_v2_89-af6223273b2bdfb0.yaml new file mode 100644 index 000000000..39682f59b --- /dev/null +++ b/releasenotes/notes/microversion_v2_89-af6223273b2bdfb0.yaml @@ -0,0 +1,11 @@ +--- +features: + - | + Added support for `microversion 2.89`_. This microversion removes the + ``id`` field while adding the ``attachment_id`` and ``bdm_uuid`` fields to + the responses of ``GET /servers/{server_id}/os-volume_attachments`` and + ``GET /servers/{server_id}/os-volume_attachments/{volume_id}`` with these + changes reflected in novaclient under the ``nova volume-attachments`` + command. + + .. _microversion 2.89: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#microversion-2-89 From 4a5bdde3b0c330a06c9aa56f28541cd22bc438f3 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 1 Sep 2021 11:09:31 +0100 Subject: [PATCH 1628/1705] Add pre-commit Change-Id: I66ed7fb2f0b4ef5227bdf40d51e3c15c3a54816a Signed-off-by: Stephen Finucane --- .pre-commit-config.yaml | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..76fa799a2 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,32 @@ +--- +default_language_version: + # force all unspecified python hooks to run python3 + python: python3 +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.0.1 + hooks: + - id: trailing-whitespace + - id: mixed-line-ending + args: ['--fix', 'lf'] + - id: check-byte-order-marker + - id: check-executables-have-shebangs + - id: check-merge-conflict + - id: debug-statements + - id: check-yaml + files: .*\.(yaml|yml)$ + - repo: https://github.com/Lucas-C/pre-commit-hooks + rev: v1.1.10 + hooks: + - id: remove-tabs + exclude: '.*\.(svg)$' + - repo: local + hooks: + - id: flake8 + name: flake8 + additional_dependencies: + - hacking>=3.0.1,<3.1.0 + language: python + entry: flake8 + files: '^.*\.py$' + exclude: '^(doc|releasenotes|tools)/.*$' From 01c7a3aa10f13eb889221f609dc4fb1463904fe3 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 1 Sep 2021 11:27:31 +0100 Subject: [PATCH 1629/1705] tests: Add missing 'nova update' unit tests We have functional tests for the 'nova update' commands, but no unit tests to verify e.g. that we can't set a description for the server before microversion 2.19. Add such tests. Change-Id: I9af89655a7e7276446a881fd28d21ddd6581048c Signed-off-by: Stephen Finucane --- novaclient/tests/unit/v2/fakes.py | 3 +++ novaclient/tests/unit/v2/test_shell.py | 29 ++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 29492cbfb..07216adc2 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -2408,6 +2408,9 @@ def get_servers_1234_migrations(self, **kw): def delete_servers_1234_migrations_1(self): return (202, {}, None) + def put_servers_1234(self, **kw): + return (201, {}, None) + def put_servers_1234_tags_tag(self, **kw): return (201, {}, None) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 10e8c732e..ed36685f2 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2395,6 +2395,35 @@ def test_migrate_v256(self): self.assert_called('POST', '/servers/1234/action', {'migrate': {'host': 'target-host'}}) + def test_update(self): + self.run_command('update --name new-name sample-server') + expected_put_body = { + "server": { + "name": "new-name" + } + } + self.assert_called('GET', '/servers/1234', pos=-2) + self.assert_called('PUT', '/servers/1234', expected_put_body, pos=-1) + + def test_update_with_description(self): + self.run_command( + 'update --description new-description sample-server', + api_version='2.19') + expected_put_body = { + "server": { + "description": "new-description" + } + } + self.assert_called('GET', '/servers/1234', pos=-2) + self.assert_called('PUT', '/servers/1234', expected_put_body, pos=-1) + + def test_update_with_description_pre_v219(self): + self.assertRaises( + SystemExit, + self.run_command, + 'update --description new-description sample-server', + api_version='2.18') + def test_resize(self): self.run_command('resize sample-server 1') self.assert_called('POST', '/servers/1234/action', From 8066f8c745054caeb7bd66d4c2be15ab5fac1d1f Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 31 Aug 2021 17:44:20 +0100 Subject: [PATCH 1630/1705] Microversion 2.90 - Configurable hostnames Change-Id: Icd4362a07196e59bafcdfaff44323ce1386d4f55 Signed-off-by: Stephen Finucane Depends-On: https://review.opendev.org/c/openstack/nova/+/778550/ --- doc/source/cli/nova.rst | 26 +++- novaclient/__init__.py | 2 +- novaclient/tests/unit/v2/test_servers.py | 87 ++++++++++++ novaclient/tests/unit/v2/test_shell.py | 72 ++++++++++ novaclient/v2/servers.py | 133 +++++++++++++----- novaclient/v2/shell.py | 38 ++++- .../microversion-v2_90-259779668e67dfb5.yaml | 13 ++ 7 files changed, 333 insertions(+), 38 deletions(-) create mode 100644 releasenotes/notes/microversion-v2_90-259779668e67dfb5.yaml diff --git a/doc/source/cli/nova.rst b/doc/source/cli/nova.rst index 4b5019491..54ec99910 100644 --- a/doc/source/cli/nova.rst +++ b/doc/source/cli/nova.rst @@ -975,6 +975,7 @@ nova boot [--trusted-image-certificate-id ] [--host ] [--hypervisor-hostname ] + [--hostname ] Boot a new server. @@ -1149,6 +1150,12 @@ quality of service support, microversion ``2.72`` is required. Requested hypervisor hostname to create servers. Admin only by default. (Supported by API versions '2.74' - '2.latest') +``--hostname `` + Hostname for the instance. This sets the hostname stored in the + metadata server: a utility such as cloud-init running on the guest + is required to propagate these changes to the guest. + (Supported by API versions '2.90' - '2.latest') + .. _nova_clear-password: nova clear-password @@ -2885,6 +2892,7 @@ nova rebuild [--user-data ] [--user-data-unset] [--trusted-image-certificate-id ] [--trusted-image-certificates-unset] + [--hostname ] Shutdown, re-image, and re-boot a server. @@ -2958,6 +2966,12 @@ Shutdown, re-image, and re-boot a server. specified with the ``--trusted-image-certificate-id`` option. (Supported by API versions '2.63' - '2.latest') +``--hostname `` + New hostname for the instance. This only updates the hostname + stored in the metadata server: a utility running on the guest + is required to propagate these changes to the guest. + (Supported by API versions '2.90' - '2.latest') + .. _nova_refresh-network: nova refresh-network @@ -3795,9 +3809,11 @@ nova update .. code-block:: console - usage: nova update [--name ] [--description ] + usage: nova update [--name ] [--description ] + [--hostname ] + -Update the name or the description for a server. +Update attributes of a server. **Positional arguments:** @@ -3815,6 +3831,12 @@ Update the name or the description for a server. will be removed. (Supported by API versions '2.19' - '2.latest') +``--hostname `` + New hostname for the instance. This only updates the hostname + stored in the metadata server: a utility running on the guest + is required to propagate these changes to the guest. + (Supported by API versions '2.90' - '2.latest') + .. _nova_usage: nova usage diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 5f3b69530..d49e8841a 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.89") +API_MAX_VERSION = api_versions.APIVersion("2.90") diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index 40d88a19a..071ae5c18 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -1905,3 +1905,90 @@ def test_get_server_topology_pre278(self): self.cs.api_version = api_versions.APIVersion('2.77') s = self.cs.servers.get(1234) self.assertRaises(exceptions.VersionNotFoundForAPIMethod, s.topology) + + +class ServersV290Test(ServersV278Test): + + api_version = '2.90' + + def test_create_server_with_hostname(self): + self.cs.servers.create( + name='My server', + image=1, + flavor=1, + nics='auto', + hostname='new-hostname', + ) + self.assert_called( + 'POST', '/servers', + { + 'server': { + 'flavorRef': '1', + 'imageRef': '1', + 'max_count': 1, + 'min_count': 1, + 'name': 'My server', + 'networks': 'auto', + 'hostname': 'new-hostname' + }, + } + ) + + def test_create_server_with_hostname_pre_290_fails(self): + self.cs.api_version = api_versions.APIVersion('2.89') + ex = self.assertRaises( + exceptions.UnsupportedAttribute, + self.cs.servers.create, + name='My server', + image=1, + flavor=1, + nics='auto', + hostname='new-hostname') + self.assertIn( + "'hostname' argument is only allowed since microversion 2.90", + str(ex)) + + def test_rebuild_server_with_hostname(self): + s = self.cs.servers.get(1234) + ret = s.rebuild(image="1", hostname='new-hostname') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called( + 'POST', '/servers/1234/action', + { + 'rebuild': { + 'imageRef': '1', + 'hostname': 'new-hostname', + }, + }, + ) + + def test_rebuild_server_with_hostname_pre_290_fails(self): + self.cs.api_version = api_versions.APIVersion('2.89') + ex = self.assertRaises( + exceptions.UnsupportedAttribute, + self.cs.servers.rebuild, + '1234', fakes.FAKE_IMAGE_UUID_1, + hostname='new-hostname') + self.assertIn('hostname', str(ex)) + + def test_update_server_with_hostname(self): + s = self.cs.servers.get(1234) + + s.update(hostname='new-hostname') + self.assert_called( + 'PUT', '/servers/1234', + { + 'server': { + 'hostname': 'new-hostname', + }, + }, + ) + + def test_update_with_hostname_pre_290_fails(self): + self.cs.api_version = api_versions.APIVersion('2.89') + s = self.cs.servers.get(1234) + ex = self.assertRaises( + TypeError, + s.update, + hostname='new-hostname') + self.assertIn('hostname', str(ex)) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index ed36685f2..e6c0fda7b 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -1502,6 +1502,34 @@ def test_boot_with_host_and_hypervisor_hostname_pre_v274(self): self.assertRaises(SystemExit, self.run_command, cmd, api_version='2.73') + def test_boot_with_hostname(self): + self.run_command( + 'boot --flavor 1 --image %s ' + '--hostname my-hostname --nic auto ' + 'some-server' % FAKE_UUID_1, + api_version='2.90') + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': FAKE_UUID_1, + 'min_count': 1, + 'max_count': 1, + 'networks': 'auto', + 'hostname': 'my-hostname', + }}, + ) + + def test_boot_with_hostname_pre_v290(self): + cmd = ( + 'boot --flavor 1 --image %s --nic auto ' + '--hostname my-hostname some-server' % FAKE_UUID_1 + ) + self.assertRaises( + SystemExit, self.run_command, + cmd, api_version='2.89') + def test_flavor_list(self): out, _ = self.run_command('flavor-list') self.assert_called_anytime('GET', '/flavors/detail') @@ -2258,6 +2286,31 @@ def test_rebuild_without_server_groups_in_response(self): self.assertNotIn('server_groups', out) self.assertNotIn('a67359fb-d397-4697-88f1-f55e3ee7c499', out) + def test_rebuild_with_hostname(self): + self.run_command( + 'rebuild sample-server %s --hostname new-hostname' % FAKE_UUID_1, + api_version='2.90') + self.assert_called('GET', '/servers?name=sample-server', pos=0) + self.assert_called('GET', '/servers/1234', pos=1) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=2) + self.assert_called( + 'POST', '/servers/1234/action', + { + 'rebuild': { + 'imageRef': FAKE_UUID_1, + 'description': None, + 'hostname': 'new-hostname', + }, + }, + pos=3) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=4) + + def test_rebuild_with_hostname_pre_v290(self): + self.assertRaises( + SystemExit, self.run_command, + 'rebuild sample-server %s --hostname hostname' % FAKE_UUID_1, + api_version='2.89') + def test_start(self): self.run_command('start sample-server') self.assert_called('POST', '/servers/1234/action', {'os-start': None}) @@ -2424,6 +2477,25 @@ def test_update_with_description_pre_v219(self): 'update --description new-description sample-server', api_version='2.18') + def test_update_with_hostname(self): + self.run_command( + 'update --hostname new-hostname sample-server', + api_version='2.90') + expected_put_body = { + "server": { + "hostname": "new-hostname" + } + } + self.assert_called('GET', '/servers/1234', pos=-2) + self.assert_called('PUT', '/servers/1234', expected_put_body, pos=-1) + + def test_update_with_hostname_pre_v290(self): + self.assertRaises( + SystemExit, + self.run_command, + 'update --hostname new-hostname sample-server', + api_version='2.89') + def test_resize(self): self.run_command('resize sample-server 1') self.assert_called('POST', '/servers/1234/action', diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index 7768ef7af..76709ec5e 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -67,17 +67,17 @@ def delete(self): @api_versions.wraps("2.0", "2.18") def update(self, name=None): """ - Update the name for this server. + Update attributes of this server. :param name: Update the server's name. :returns: :class:`Server` """ return self.manager.update(self, name=name) - @api_versions.wraps("2.19") + @api_versions.wraps("2.19", "2.89") def update(self, name=None, description=None): """ - Update the name and the description for this server. + Update attributes of this server. :param name: Update the server's name. :param description: Update the server's description. @@ -88,6 +88,23 @@ def update(self, name=None, description=None): update_kwargs["description"] = description return self.manager.update(self, **update_kwargs) + @api_versions.wraps("2.90") + def update(self, name=None, description=None, hostname=None): + """ + Update attributes of this server. + + :param name: Update the server's name. + :param description: Update the server's description. + :param hostname: Update the server's hostname. + :returns: :class:`Server` + """ + update_kwargs = {"name": name} + if description is not None: + update_kwargs["description"] = description + if hostname is not None: + update_kwargs["hostname"] = hostname + return self.manager.update(self, **update_kwargs) + def get_console_output(self, length=None): """ Get text console log output from Server. @@ -704,7 +721,7 @@ def _boot(self, response_key, name, image, flavor, config_drive=None, admin_pass=None, disk_config=None, access_ip_v4=None, access_ip_v6=None, description=None, tags=None, trusted_image_certificates=None, - host=None, hypervisor_hostname=None, **kwargs): + host=None, hypervisor_hostname=None, hostname=None, **kwargs): """ Create (boot) a new server. """ @@ -833,6 +850,9 @@ def _boot(self, response_key, name, image, flavor, if hypervisor_hostname: body['server']['hypervisor_hostname'] = hypervisor_hostname + if hostname: + body['server']['hostname'] = hostname + return self._create('/servers', body, response_key, return_raw=return_raw, **kwargs) @@ -1318,10 +1338,8 @@ def create(self, name, image, flavor, meta=None, files=None, config_drive=None, disk_config=None, admin_pass=None, access_ip_v4=None, access_ip_v6=None, trusted_image_certificates=None, - host=None, hypervisor_hostname=None, + host=None, hypervisor_hostname=None, hostname=None, **kwargs): - # TODO(anthony): indicate in doc string if param is an extension - # and/or optional """ Create (boot) a new server. @@ -1390,6 +1408,8 @@ def create(self, name, image, flavor, meta=None, files=None, (allowed since microversion 2.74) :param hypervisor_hostname: requested hypervisor hostname to create servers (allowed since microversion 2.74) + :param hostname: requested hostname of server (allowed since + microversion 2.90) """ if not min_count: min_count = 1 @@ -1453,6 +1473,10 @@ def create(self, name, image, flavor, meta=None, files=None, raise exceptions.UnsupportedAttribute( "hypervisor_hostname", "2.74") + hostname_microversion = api_versions.APIVersion("2.90") + if hostname and self.api_version < hostname_microversion: + raise exceptions.UnsupportedAttribute("hostname", "2.90") + boot_kwargs = dict( meta=meta, files=files, userdata=userdata, reservation_id=reservation_id, min_count=min_count, @@ -1463,7 +1487,7 @@ def create(self, name, image, flavor, meta=None, files=None, access_ip_v4=access_ip_v4, access_ip_v6=access_ip_v6, trusted_image_certificates=trusted_image_certificates, host=host, hypervisor_hostname=hypervisor_hostname, - **kwargs) + hostname=hostname, **kwargs) if block_device_mapping: boot_kwargs['block_device_mapping'] = block_device_mapping @@ -1479,10 +1503,11 @@ def create(self, name, image, flavor, meta=None, files=None, @api_versions.wraps("2.0", "2.18") def update(self, server, name=None): """ - Update the name for a server. + Update attributes of a server. :param server: The :class:`Server` (or its ID) to update. :param name: Update the server's name. + :returns: :class:`Server` """ if name is None: return @@ -1495,15 +1520,16 @@ def update(self, server, name=None): return self._update("/servers/%s" % base.getid(server), body, "server") - @api_versions.wraps("2.19") + @api_versions.wraps("2.19", "2.89") def update(self, server, name=None, description=None): """ - Update the name or the description for a server. + Update attributes of a server. :param server: The :class:`Server` (or its ID) to update. :param name: Update the server's name. :param description: Update the server's description. If it equals to empty string(i.g. ""), the server description will be removed. + :returns: :class:`Server` """ if name is None and description is None: return @@ -1518,6 +1544,36 @@ def update(self, server, name=None, description=None): return self._update("/servers/%s" % base.getid(server), body, "server") + @api_versions.wraps("2.90") + def update(self, server, name=None, description=None, hostname=None): + """ + Update attributes of a server. + + :param server: The :class:`Server` (or its ID) to update. + :param name: Update the server's name. + :param description: Update the server's description. If it equals to + empty string(i.g. ""), the server description will be removed. + :param hostname: Update the server's hostname as recorded by the + metadata service. Note that a separate utility running on the + guest will be necessary to reflect these changes in the guest + itself. + :returns: :class:`Server` + """ + if name is None and description is None and hostname is None: + return + + body = {"server": {}} + if name: + body["server"]["name"] = name + if description == "": + body["server"]["description"] = None + elif description: + body["server"]["description"] = description + if hostname: + body["server"]["hostname"] = hostname + + return self._update("/servers/%s" % base.getid(server), body, "server") + def change_password(self, server, password): """ Update the password for a server. @@ -1548,6 +1604,7 @@ def reboot(self, server, reboot_type=REBOOT_SOFT): """ return self._action('reboot', server, {'type': reboot_type}) + # TODO(stephenfin): Expand out kwargs def rebuild(self, server, image, password=None, disk_config=None, preserve_ephemeral=False, name=None, meta=None, files=None, **kwargs): @@ -1555,34 +1612,36 @@ def rebuild(self, server, image, password=None, disk_config=None, Rebuild -- shut down and then re-image -- a server. :param server: The :class:`Server` (or its ID) to share onto. - :param image: the :class:`Image` (or its ID) to re-image with. - :param password: string to set as password on the rebuilt server. - :param disk_config: partitioning mode to use on the rebuilt server. - Valid values are 'AUTO' or 'MANUAL' + :param image: The :class:`Image` (or its ID) to re-image with. + :param password: String to set as password on the rebuilt server. + :param disk_config: Partitioning mode to use on the rebuilt server. + Valid values are 'AUTO' or 'MANUAL' :param preserve_ephemeral: If True, request that any ephemeral device be preserved when rebuilding the instance. Defaults to False. :param name: Something to name the server. :param meta: A dict of arbitrary key/value metadata to store for this - server. Both keys and values must be <=255 characters. + server. Both keys and values must be <=255 characters. :param files: A dict of files to overwrite on the server upon boot. - Keys are file names (i.e. ``/etc/passwd``) and values - are the file contents (either as a string or as a - file-like object). A maximum of five entries is allowed, - and each file must be 10k or less. - (deprecated starting with microversion 2.57) - :param description: optional description of the server (allowed since - microversion 2.19) - :param key_name: optional key pair name for rebuild operation; passing - None will unset the key for the server instance - (starting from microversion 2.54) - :param userdata: optional user data to pass to be exposed by the - metadata server; this can be a file type object as - well or a string. If None is specified, the existing - user_data is unset. - (starting from microversion 2.57) + Keys are file names (i.e. ``/etc/passwd``) and values are the file + contents (either as a string or as a file-like object). A maximum + of five entries is allowed, and each file must be 10k or less. + (deprecated starting with microversion 2.57) + :param description: Optional description of the server. If None is + specified, the existing description will be unset. + (starting from microversion 2.19) + :param key_name: Optional key pair name for rebuild operation. If None + is specified, the existing key will be unset. + (starting from microversion 2.54) + :param userdata: Optional user data to pass to be exposed by the + metadata server; this can be a file type object as well or a + string. If None is specified, the existing user_data is unset. + (starting from microversion 2.57) :param trusted_image_certificates: A list of trusted certificate IDs - or None to unset/reset the servers trusted image - certificates (allowed since microversion 2.63) + or None to unset/reset the servers trusted image certificates + (starting from microversion 2.63) + :param hostname: Optional hostname to configure for the instance. If + None is specified, the existing hostname will be unset. + (starting from microversion 2.90) :returns: :class:`Server` """ descr_microversion = api_versions.APIVersion("2.19") @@ -1612,6 +1671,12 @@ def rebuild(self, server, image, password=None, disk_config=None, raise exceptions.UnsupportedAttribute("trusted_image_certificates", "2.63") + if ( + 'hostname' in kwargs and + self.api_version < api_versions.APIVersion("2.90") + ): + raise exceptions.UnsupportedAttribute('hostname', '2.90') + body = {'imageRef': base.getid(image)} if password is not None: body['adminPass'] = password @@ -1628,6 +1693,8 @@ def rebuild(self, server, image, password=None, disk_config=None, if "trusted_image_certificates" in kwargs: body["trusted_image_certificates"] = kwargs[ "trusted_image_certificates"] + if "hostname" in kwargs: + body["hostname"] = kwargs["hostname"] if meta: body['metadata'] = meta if files: diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index e47fcbc01..dfe93c01a 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -532,8 +532,10 @@ def _boot(cs, args): if include_files: boot_kwargs['files'] = files - if ('trusted_image_certificates' in args and - args.trusted_image_certificates): + if ( + 'trusted_image_certificates' in args and + args.trusted_image_certificates + ): boot_kwargs['trusted_image_certificates'] = ( args.trusted_image_certificates) elif utils.env('OS_TRUSTED_IMAGE_CERTIFICATE_IDS'): @@ -545,6 +547,9 @@ def _boot(cs, args): "OS_TRUSTED_IMAGE_CERTIFICATE_IDS", "2.63") + if 'hostname' in args and args.hostname: + boot_kwargs['hostname'] = args.hostname + return boot_args, boot_kwargs @@ -970,6 +975,14 @@ def _boot(cs, args): help=_('Requested hypervisor hostname to create servers. Admin only by ' 'default.'), start_version="2.74") +@utils.arg( + '--hostname', + help=_( + 'Hostname for the instance. This sets the hostname stored in the ' + 'metadata server: a utility such as cloud-init running on the guest ' + 'is required to propagate these changes to the guest.' + ), + start_version='2.90') def do_boot(cs, args): """Boot a new server.""" boot_args, boot_kwargs = _boot(cs, args) @@ -2031,6 +2044,14 @@ def do_reboot(cs, args): help=_("Unset trusted_image_certificates in the server. Cannot be " "specified with the '--trusted-image-certificate-id' option."), start_version="2.63") +@utils.arg( + '--hostname', + help=_( + 'New hostname for the instance. This only updates the hostname ' + 'stored in the metadata server: a utility running on the guest ' + 'is required to propagate these changes to the guest.' + ), + start_version='2.90') def do_rebuild(cs, args): """Shutdown, re-image, and re-boot a server.""" server = _find_server(cs, args.server) @@ -2121,6 +2142,9 @@ def do_rebuild(cs, args): "OS_TRUSTED_IMAGE_CERTIFICATE_IDS", "2.63") + if 'hostname' in args and args.hostname is not None: + kwargs['hostname'] = args.hostname + server = server.rebuild(image, _password, **kwargs) _print_server(cs, args, server) @@ -2145,6 +2169,14 @@ def do_rebuild(cs, args): help=_('New description for the server. If it equals to empty string ' '(i.g. ""), the server description will be removed.'), start_version="2.19") +@utils.arg( + '--hostname', + help=_( + 'New hostname for the instance. This only updates the hostname ' + 'stored in the metadata server: a utility running on the guest ' + 'is required to propagate these changes to the guest.' + ), + start_version='2.90') def do_update(cs, args): """Update the name or the description for a server.""" update_kwargs = {} @@ -2152,6 +2184,8 @@ def do_update(cs, args): update_kwargs["name"] = args.name if "description" in args and args.description is not None: update_kwargs["description"] = args.description + if "hostname" in args and args.hostname is not None: + update_kwargs["hostname"] = args.hostname _find_server(cs, args.server).update(**update_kwargs) diff --git a/releasenotes/notes/microversion-v2_90-259779668e67dfb5.yaml b/releasenotes/notes/microversion-v2_90-259779668e67dfb5.yaml new file mode 100644 index 000000000..8ab21c91d --- /dev/null +++ b/releasenotes/notes/microversion-v2_90-259779668e67dfb5.yaml @@ -0,0 +1,13 @@ +--- +features: + - | + Added support for `microversion 2.90`_. This microversion provides the + ability to manually configure the instance ``hostname`` attribute when + creating a new instance (``nova boot --hostname HOSTNAME ...``), updating + an existing instance (``nova update --hostname HOSTNAME ...``), or + rebuilding an existing instance (``nova rebuild --hostname HOSTNAME``). + This attribute is published via the metadata service and config drive and + can be used by init scripts such as ``cloud-init`` to configure the guest's + hostname. + + .. _microversion 2.90: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#microversion-2-90 From 5eb6dd2787bb8aba61a8b3b55023a6e574b45249 Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Fri, 10 Sep 2021 15:22:31 +0000 Subject: [PATCH 1631/1705] Update master for stable/xena Add file to the reno documentation build to show release notes for stable/xena. Use pbr instruction to increment the minor version number automatically so that master versions are higher than the versions on stable/xena. Sem-Ver: feature Change-Id: I62070f8d3fded9f47c03a8c43d3481b2e2aef367 --- releasenotes/source/index.rst | 1 + releasenotes/source/xena.rst | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 releasenotes/source/xena.rst diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index eb104532d..c33e50d1d 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -8,6 +8,7 @@ Contents :maxdepth: 2 unreleased + xena wallaby victoria ussuri diff --git a/releasenotes/source/xena.rst b/releasenotes/source/xena.rst new file mode 100644 index 000000000..1be85be3e --- /dev/null +++ b/releasenotes/source/xena.rst @@ -0,0 +1,6 @@ +========================= +Xena Series Release Notes +========================= + +.. release-notes:: + :branch: stable/xena From d3b4c01ea4e6e5c4bca9e961a36806b533faa9ac Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Fri, 10 Sep 2021 15:22:39 +0000 Subject: [PATCH 1632/1705] Add Python3 yoga unit tests This is an automatically generated patch to ensure unit testing is in place for all the of the tested runtimes for yoga. See also the PTI in governance [1]. [1]: https://governance.openstack.org/tc/reference/project-testing-interface.html Change-Id: Iff5e28efd84750cf7e0ae166b4828339bad5c7c6 --- .zuul.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.zuul.yaml b/.zuul.yaml index 8072db0b1..791aa8fd2 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -20,7 +20,7 @@ - lib-forward-testing-python3 - openstack-cover-jobs - openstack-lower-constraints-jobs - - openstack-python3-xena-jobs + - openstack-python3-yoga-jobs - publish-openstack-docs-pti - release-notes-jobs-python3 check: From 871c50c5b91cc0b338c78d73606179f08b7c9bb8 Mon Sep 17 00:00:00 2001 From: Takashi Natsume Date: Fri, 17 Dec 2021 00:23:26 +0900 Subject: [PATCH 1633/1705] Fix check job failures This patch fixes the following failures. * package version conflict in the lower-constraints job * An error about admin endpoint for identity service in the python-novaclient-functional job Closes-bug: 1954916 Closes-bug: 1954917 Change-Id: Ie48dda004aaf5d01d286bdc0d9fe355d58e62d75 Signed-off-by: Takashi Natsume --- .zuul.yaml | 1 + lower-constraints.txt | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.zuul.yaml b/.zuul.yaml index 791aa8fd2..12ce938d1 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -8,6 +8,7 @@ vars: openrc_enable_export: true devstack_localrc: + KEYSTONE_ADMIN_ENDPOINT: true USE_PYTHON3: true irrelevant-files: - ^.*\.rst$ diff --git a/lower-constraints.txt b/lower-constraints.txt index 117b697ce..dd3199a20 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -10,7 +10,7 @@ coverage==4.0 cryptography==2.1 ddt==1.0.1 debtcollector==1.2.0 -decorator==3.4.0 +decorator==4.1.0 deprecation==1.0 dogpile.cache==0.6.2 eventlet==0.18.2 From 63d368168c87bc0b9a9b7928b42553c609e46089 Mon Sep 17 00:00:00 2001 From: Ghanshyam Mann Date: Wed, 24 Nov 2021 18:09:48 -0600 Subject: [PATCH 1634/1705] Updating python testing classifier as per Yoga testing runtime Yoga testing runtime[1] has been updated to add py39 testing as voting. Unit tests update are handled by the job template change in openstack-zuul-job - https://review.opendev.org/c/openstack/openstack-zuul-jobs/+/820286 this commit updates the classifier in setup.cfg file. [1] https://governance.openstack.org/tc/reference/runtimes/yoga.html Change-Id: Ic6dbe3ae45089f7947d45dc81eb5d1e29b3d0597 --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index 62c23d46d..66b11b738 100644 --- a/setup.cfg +++ b/setup.cfg @@ -21,6 +21,7 @@ classifier = Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 Programming Language :: Python :: 3 :: Only Programming Language :: Python :: Implementation :: CPython From a36214f3939dac6e727523a542b30e71fefdeed3 Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Thu, 3 Mar 2022 10:51:26 +0000 Subject: [PATCH 1635/1705] Update master for stable/yoga Add file to the reno documentation build to show release notes for stable/yoga. Use pbr instruction to increment the minor version number automatically so that master versions are higher than the versions on stable/yoga. Sem-Ver: feature Change-Id: I8057ebf8bd5714acb7fd11223ffbfe8504a5f5dd --- releasenotes/source/index.rst | 1 + releasenotes/source/yoga.rst | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 releasenotes/source/yoga.rst diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index c33e50d1d..68524accd 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -8,6 +8,7 @@ Contents :maxdepth: 2 unreleased + yoga xena wallaby victoria diff --git a/releasenotes/source/yoga.rst b/releasenotes/source/yoga.rst new file mode 100644 index 000000000..7cd5e908a --- /dev/null +++ b/releasenotes/source/yoga.rst @@ -0,0 +1,6 @@ +========================= +Yoga Series Release Notes +========================= + +.. release-notes:: + :branch: stable/yoga From cd08a847179d144fea03a7ffd8f92b06c387b966 Mon Sep 17 00:00:00 2001 From: Takashi Natsume Date: Mon, 28 Mar 2022 12:00:07 +0900 Subject: [PATCH 1636/1705] Remove USE_PYTHON3 in .zuul.yaml USE_PYTHON3 is unnecessary now, so remove it. Change-Id: I2be7877d3b90d8cf94ab63e735d176a548a13a43 Signed-off-by: Takashi Natsume --- .zuul.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.zuul.yaml b/.zuul.yaml index 12ce938d1..93f755366 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -9,7 +9,6 @@ openrc_enable_export: true devstack_localrc: KEYSTONE_ADMIN_ENDPOINT: true - USE_PYTHON3: true irrelevant-files: - ^.*\.rst$ - ^doc/.*$ From 0fb7190c062a08a3969a71955c014e3a572cdc9e Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 5 Apr 2022 17:48:01 +0100 Subject: [PATCH 1637/1705] Deprecate nova CLI It is time to signal that we're fully committed to delivering a pure OSC experience. Based on the neutron change from 6 (!!) years ago [1] [1] https://github.com/openstack/python-neutronclient/commit/3a64a7a166be25d40436fd40c8351a79267bd3c4 Change-Id: Ib80548e104a751179f36f2a6ebff9916d38fdf1e Signed-off-by: Stephen Finucane --- novaclient/shell.py | 15 +++++++++-- novaclient/tests/unit/test_shell.py | 26 ++++++++++++++----- .../notes/deprecate-cli-75074850847a8452.yaml | 9 +++++++ 3 files changed, 41 insertions(+), 9 deletions(-) create mode 100644 releasenotes/notes/deprecate-cli-75074850847a8452.yaml diff --git a/novaclient/shell.py b/novaclient/shell.py index 7762be9b3..468e889b1 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -20,6 +20,7 @@ import argparse import logging +import os import sys from keystoneauth1 import loading @@ -816,9 +817,19 @@ def start_section(self, heading): super(OpenStackHelpFormatter, self).start_section(heading) -def main(): +def main(argv=sys.argv[1:]): try: - argv = [encodeutils.safe_decode(a) for a in sys.argv[1:]] + # Special dansmith envvar to hide the warning. Don't rely on this + # because we will eventually remove all this stuff. + if os.environ.get("NOVACLIENT_ISHOULDNTBEDOINGTHIS") != "1": + print( + _( + "nova CLI is deprecated and will be a removed in a future " + "release" + ), + file=sys.stderr, + ) + argv = [encodeutils.safe_decode(a) for a in argv] OpenStackComputeShell().main(argv) except Exception as exc: logger.debug(exc, exc_info=1) diff --git a/novaclient/tests/unit/test_shell.py b/novaclient/tests/unit/test_shell.py index 641952910..b6b708a82 100644 --- a/novaclient/tests/unit/test_shell.py +++ b/novaclient/tests/unit/test_shell.py @@ -624,26 +624,32 @@ def test_v_unknown_service_type(self): self._test_service_type, 'unknown', 'compute', self.mock_client) - @mock.patch('sys.argv', ['nova']) @mock.patch('sys.stdout', io.StringIO()) @mock.patch('sys.stderr', io.StringIO()) def test_main_noargs(self): # Ensure that main works with no command-line arguments try: - novaclient.shell.main() + novaclient.shell.main([]) except SystemExit: self.fail('Unexpected SystemExit') # We expect the normal usage as a result - self.assertIn('Command-line interface to the OpenStack Nova API', - sys.stdout.getvalue()) + self.assertIn( + 'Command-line interface to the OpenStack Nova API', + sys.stdout.getvalue(), + ) + # We also expect to see the deprecation warning + self.assertIn( + 'nova CLI is deprecated and will be a removed in a future release', + sys.stderr.getvalue(), + ) @mock.patch.object(novaclient.shell.OpenStackComputeShell, 'main') def test_main_keyboard_interrupt(self, mock_compute_shell): # Ensure that exit code is 130 for KeyboardInterrupt mock_compute_shell.side_effect = KeyboardInterrupt() try: - novaclient.shell.main() + novaclient.shell.main([]) except SystemExit as ex: self.assertEqual(ex.code, 130) @@ -766,9 +772,15 @@ class MyException(Exception): pass with mock.patch('sys.stderr', io.StringIO()): mock_compute_shell.side_effect = MyException('message') - self.assertRaises(SystemExit, novaclient.shell.main) + self.assertRaises(SystemExit, novaclient.shell.main, []) err = sys.stderr.getvalue() - self.assertEqual(err, 'ERROR (MyException): message\n') + # We expect to see the error propagated + self.assertIn('ERROR (MyException): message\n', err) + # We also expect to see the deprecation warning + self.assertIn( + 'nova CLI is deprecated and will be a removed in a future release', + err, + ) class TestLoadVersionedActions(utils.TestCase): diff --git a/releasenotes/notes/deprecate-cli-75074850847a8452.yaml b/releasenotes/notes/deprecate-cli-75074850847a8452.yaml new file mode 100644 index 000000000..6d51ce03c --- /dev/null +++ b/releasenotes/notes/deprecate-cli-75074850847a8452.yaml @@ -0,0 +1,9 @@ +--- +deprecations: + - | + The ``nova`` CLI is now deprecated. This is the signal that it is + time to start using the openstack CLI. No new features will be + added to the ``nova`` CLI, though fixes to the CLI will be assessed + on a case by case basis. Fixes to the API bindings, development of + new API bindings, and changes to the compute commands in the openstack + CLI are exempt from this deprecation. From 52cdbd271ed9fbaa0f08a4f74f0a1c44552698fa Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 5 Apr 2022 18:02:39 +0100 Subject: [PATCH 1638/1705] docs: Update docs to reflect deprecation status Change-Id: I56b862305b31916cef143494050e5e08b1ac70b1 Signed-off-by: Stephen Finucane --- README.rst | 4 ++-- doc/source/cli/nova.rst | 11 +++++------ doc/source/contributor/contributing.rst | 12 +++++++++--- doc/source/index.rst | 10 +++++----- doc/source/user/shell.rst | 6 ++++++ 5 files changed, 27 insertions(+), 16 deletions(-) diff --git a/README.rst b/README.rst index fe139a7b4..1bc3bdcc2 100644 --- a/README.rst +++ b/README.rst @@ -16,8 +16,8 @@ Python bindings to the OpenStack Compute API :alt: Latest Version This is a client for the OpenStack Compute API. It provides a Python API (the -``novaclient`` module) and a command-line script (``nova``). Each implements -100% of the OpenStack Compute API. +``novaclient`` module) and a deprecated command-line script (``nova``). The +Python API implements 100% of the OpenStack Compute API. * License: Apache License, Version 2.0 * `PyPi`_ - package installation diff --git a/doc/source/cli/nova.rst b/doc/source/cli/nova.rst index 54ec99910..03145381a 100644 --- a/doc/source/cli/nova.rst +++ b/doc/source/cli/nova.rst @@ -11,12 +11,11 @@ For help on a specific :command:`nova` command, enter: $ nova help COMMAND -.. note:: Over time, command line functionality will be phased out - of the ``nova`` CLI and into the ``openstack`` CLI. Using - the ``openstack`` client where possible is preferred but - there is not full parity yet for all of the ``nova`` commands. - For information on using the ``openstack`` CLI, see - :python-openstackclient-doc:`OpenStackClient <>`. +.. deprecated:: 17.8.0 + + The ``nova`` CLI has been deprecated in favour of the unified + ``openstack`` CLI. For information on using the ``openstack`` CLI, see + :python-openstackclient-doc:`OpenStackClient <>`. .. _nova_command_usage: diff --git a/doc/source/contributor/contributing.rst b/doc/source/contributor/contributing.rst index 38eac4ec9..10365558c 100644 --- a/doc/source/contributor/contributing.rst +++ b/doc/source/contributor/contributing.rst @@ -11,6 +11,12 @@ communicate as a community, etc. Below will cover the more project specific information you need to get started with python-novaclient. +.. important:: + + The ``nova`` CLI has been deprecated in favour of the unified ``openstack`` + CLI. Changes to the Python bindings are still welcome, however, no further + changes should be made to the shell. + Communication ~~~~~~~~~~~~~ @@ -19,8 +25,8 @@ Please refer `how-to-get-involved `_. +The easiest way to reach the core team is via IRC, using the ``openstack-nova`` +OFTC IRC channel. New Feature Planning ~~~~~~~~~~~~~~~~~~~~ @@ -48,5 +54,5 @@ Getting Your Patch Merged ~~~~~~~~~~~~~~~~~~~~~~~~~ All changes proposed to the python-novaclient requires two ``Code-Review +2`` -votes from python-novaclient core reviewers before one of the core reviewers +votes from ``python-novaclient`` core reviewers before one of the core reviewers can approve patch by giving ``Workflow +1`` vote.. diff --git a/doc/source/index.rst b/doc/source/index.rst index 5c0887956..60791b8e7 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -3,12 +3,12 @@ =========================================== This is a client for OpenStack Nova API. There's a :doc:`Python API -` (the :mod:`novaclient` module), and a :doc:`command-line -script ` (installed as :program:`nova`). Each implements the -entire OpenStack Nova API. +` (the :mod:`novaclient` module), and a deprecated +:doc:`command-line script ` (installed as :program:`nova`). +Each implements the entire OpenStack Nova API. -You'll need credentials for an OpenStack cloud that implements the Compute API, -such as TryStack, HP, or Rackspace, in order to use the nova client. +You'll need credentials for an OpenStack cloud that implements the Compute API +in order to use the nova client. .. seealso:: diff --git a/doc/source/user/shell.rst b/doc/source/user/shell.rst index 465b0fb4d..de96637a7 100644 --- a/doc/source/user/shell.rst +++ b/doc/source/user/shell.rst @@ -15,6 +15,12 @@ information. You can do this with the `--os-username`, `--os-password`, options, but it's easier to just set them as environment variables by setting some environment variables: +.. deprecated:: 17.8.0 + + The ``nova`` CLI has been deprecated in favour of the unified + ``openstack`` CLI. For information on using the ``openstack`` CLI, see + :python-openstackclient-doc:`OpenStackClient <>`. + .. envvar:: OS_USERNAME Your OpenStack Keystone user name. From c408db2dd987c251e0d6f30280072d78d46515b0 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 6 Apr 2022 11:02:19 +0100 Subject: [PATCH 1639/1705] Update pre-commit hook, hacking versions This was done with 'pre-commit autoupdate'. An invalid message is removed from the requirements.txt files as it no longer applies with pip's new dependency resolver. Change-Id: I01c3ece51f81d67c740e6faca6b77df7c9932435 Signed-off-by: Stephen Finucane --- .pre-commit-config.yaml | 6 +++--- novaclient/tests/unit/v2/test_shell.py | 18 ++++++++++++------ novaclient/utils.py | 9 +++++---- novaclient/v2/shell.py | 10 +++++----- requirements.txt | 3 --- test-requirements.txt | 6 +----- 6 files changed, 26 insertions(+), 26 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 76fa799a2..e782b53e0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,7 @@ default_language_version: python: python3 repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.0.1 + rev: v4.1.0 hooks: - id: trailing-whitespace - id: mixed-line-ending @@ -16,7 +16,7 @@ repos: - id: check-yaml files: .*\.(yaml|yml)$ - repo: https://github.com/Lucas-C/pre-commit-hooks - rev: v1.1.10 + rev: v1.1.13 hooks: - id: remove-tabs exclude: '.*\.(svg)$' @@ -25,7 +25,7 @@ repos: - id: flake8 name: flake8 additional_dependencies: - - hacking>=3.0.1,<3.1.0 + - hacking~=4.1.0 language: python entry: flake8 files: '^.*\.py$' diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index e6c0fda7b..da70e3ccb 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -3397,7 +3397,8 @@ def test_services_list_v2_53(self): def test_services_list_v269_with_down_cells(self): """Tests nova service-list at the 2.69 microversion.""" stdout, _stderr = self.run_command('service-list', api_version='2.69') - self.assertEqual('''\ + self.assertEqual( + '''\ +--------------------------------------+--------------+-----------+------+----------+-------+---------------------+-----------------+-------------+ | Id | Binary | Host | Zone | Status | State | Updated_at | Disabled Reason | Forced down | +--------------------------------------+--------------+-----------+------+----------+-------+---------------------+-----------------+-------------+ @@ -3406,7 +3407,8 @@ def test_services_list_v269_with_down_cells(self): | | nova-compute | host-down | | UNKNOWN | | | | | +--------------------------------------+--------------+-----------+------+----------+-------+---------------------+-----------------+-------------+ ''', # noqa - stdout) + stdout, + ) self.assert_called('GET', '/os-services') def test_services_list_with_host(self): @@ -4779,7 +4781,8 @@ def test_list_v2_26_not_tags_any(self): def test_list_detail_v269_with_down_cells(self): """Tests nova list at the 2.69 microversion.""" stdout, _stderr = self.run_command('list', api_version='2.69') - self.assertIn('''\ + self.assertIn( + '''\ +------+----------------+---------+------------+-------------+----------------------------------------------+ | ID | Name | Status | Task State | Power State | Networks | +------+----------------+---------+------------+-------------+----------------------------------------------+ @@ -4791,7 +4794,8 @@ def test_list_detail_v269_with_down_cells(self): | 9013 | sample-server4 | ACTIVE | N/A | N/A | | +------+----------------+---------+------------+-------------+----------------------------------------------+ ''', # noqa - stdout) + stdout, + ) self.assert_called('GET', '/servers/detail') def test_list_v269_with_down_cells(self): @@ -4812,7 +4816,8 @@ def test_list_v269_with_down_cells(self): def test_show_v269_with_down_cells(self): stdout, _stderr = self.run_command('show 9015', api_version='2.69') - self.assertEqual('''\ + self.assertEqual( + '''\ +-----------------------------+---------------------------------------------------+ | Property | Value | +-----------------------------+---------------------------------------------------+ @@ -4833,7 +4838,8 @@ def test_show_v269_with_down_cells(self): | user_id | fake | +-----------------------------+---------------------------------------------------+ ''', # noqa - stdout) + stdout, + ) FAKE_UUID_2 = 'c99d7632-bd66-4be9-aed5-3dd14b223a76' self.assert_called('GET', '/servers?name=9015', pos=0) self.assert_called('GET', '/servers?name=9015', pos=1) diff --git a/novaclient/utils.py b/novaclient/utils.py index fba708b55..d0219795c 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -116,13 +116,14 @@ def inner(f): return inner -def pretty_choice_list(l): - return ', '.join("'%s'" % i for i in l) +def pretty_choice_list(values): + return ', '.join("'%s'" % x for x in values) -def pretty_choice_dict(d): +def pretty_choice_dict(values): """Returns a formatted dict as 'key=value'.""" - return pretty_choice_list(['%s=%s' % (k, d[k]) for k in sorted(d.keys())]) + return pretty_choice_list( + ['%s=%s' % (k, values[k]) for k in sorted(values)]) def print_list(objs, fields, formatters={}, sortby_index=None): diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index dfe93c01a..58823fc70 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -3231,15 +3231,15 @@ def __init__(self, name, used, max, other): other = {} limit_names = [] columns = ['Name', 'Used', 'Max'] - for l in limits: - map = limit_map.get(l.name, {'name': l.name, 'type': 'other'}) + for limit in limits: + map = limit_map.get(limit.name, {'name': limit.name, 'type': 'other'}) name = map['name'] if map['type'] == 'max': - max[name] = l.value + max[name] = limit.value elif map['type'] == 'used': - used[name] = l.value + used[name] = limit.value else: - other[name] = l.value + other[name] = limit.value if 'Other' not in columns: columns.append('Other') if name not in limit_names: diff --git a/requirements.txt b/requirements.txt index b93814eba..462e1c9c0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,3 @@ -# 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. pbr!=2.1.0,>=2.0.0 # Apache-2.0 keystoneauth1>=3.5.0 # Apache-2.0 iso8601>=0.1.11 # MIT diff --git a/test-requirements.txt b/test-requirements.txt index f2df90178..5845d2d50 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,8 +1,4 @@ -# 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. -hacking>=3.0.1,<3.1.0 # Apache-2.0 - +hacking~=4.1.0 # Apache-2.0 bandit>=1.1.0 # Apache-2.0 coverage!=4.4,>=4.0 # Apache-2.0 ddt>=1.0.1 # MIT From 2f45f7cef218dbb144834648ea11efde7224b912 Mon Sep 17 00:00:00 2001 From: Takashi Natsume Date: Sat, 26 Mar 2022 23:29:57 +0900 Subject: [PATCH 1640/1705] Add Python3 zed unit tests This is an automatically generated patch to ensure unit testing is in place for all the of the tested runtimes for zed. See also the PTI in governance [1]. [1]: https://governance.openstack.org/tc/reference/project-testing-interface.html Change-Id: I12a04dc2f21bfa6af676f93c099f815e6b2cd370 --- .zuul.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.zuul.yaml b/.zuul.yaml index 93f755366..938cdb675 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -20,7 +20,7 @@ - lib-forward-testing-python3 - openstack-cover-jobs - openstack-lower-constraints-jobs - - openstack-python3-yoga-jobs + - openstack-python3-zed-jobs - publish-openstack-docs-pti - release-notes-jobs-python3 check: From a66f0702155a26b90da556a041080f6b2f766de9 Mon Sep 17 00:00:00 2001 From: Takashi Natsume Date: Sun, 27 Mar 2022 00:05:00 +0900 Subject: [PATCH 1641/1705] Add openssl in bindep.txt Change-Id: If812bf19a569667b96e4cada106da04346449778 Closes-Bug: 1966551 Signed-off-by: Takashi Natsume --- bindep.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/bindep.txt b/bindep.txt index db7c560cf..f5e71bd56 100644 --- a/bindep.txt +++ b/bindep.txt @@ -13,6 +13,7 @@ libffi-devel [platform:rpm] libssl-dev [platform:ubuntu] libuuid-devel [platform:rpm] locales [platform:debian] +openssl python-dev [platform:dpkg] python-devel [platform:rpm] python3-all-dev [platform:ubuntu !platform:ubuntu-precise] From 13ab47e7869b94ed1cfc867d59ff019a6d883e11 Mon Sep 17 00:00:00 2001 From: Takashi Natsume Date: Sat, 16 Apr 2022 02:55:48 +0900 Subject: [PATCH 1642/1705] Remove unnecessary packages in bindep.txt Change-Id: I3975e5a67e0a8614ac2d6568e294c32a14f789e9 Signed-off-by: Takashi Natsume --- bindep.txt | 5 ----- 1 file changed, 5 deletions(-) diff --git a/bindep.txt b/bindep.txt index f5e71bd56..eaa5e1f2f 100644 --- a/bindep.txt +++ b/bindep.txt @@ -14,12 +14,7 @@ libssl-dev [platform:ubuntu] libuuid-devel [platform:rpm] locales [platform:debian] openssl -python-dev [platform:dpkg] -python-devel [platform:rpm] python3-all-dev [platform:ubuntu !platform:ubuntu-precise] python3-dev [platform:dpkg] python3-devel [platform:fedora] -python3.4 [platform:ubuntu-trusty] -python3.5 [platform:ubuntu-xenial] -python34-devel [platform:centos] uuid-dev [platform:dpkg] From 81a67ac08a7343c87494ddd1d29f34ac3efa3432 Mon Sep 17 00:00:00 2001 From: Ghanshyam Mann Date: Thu, 21 Apr 2022 14:23:05 -0500 Subject: [PATCH 1643/1705] Update python classifier as per testing runtime In Zed cycle, we ave dropped the python 3.6/3.7[1] testing and its support. Updating the python classifier also to reflect the same. [1] https://governance.openstack.org/tc/reference/runtimes/zed.html Change-Id: I06ef1e3f73ddf6c7a62e0d1aa0ab6eeb220436f6 --- setup.cfg | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/setup.cfg b/setup.cfg index 66b11b738..7982b6647 100644 --- a/setup.cfg +++ b/setup.cfg @@ -7,7 +7,7 @@ license = Apache License, Version 2.0 author = OpenStack author_email = openstack-discuss@lists.openstack.org home_page = https://docs.openstack.org/python-novaclient/latest -python_requires = >=3.6 +python_requires = >=3.8 classifier = Development Status :: 5 - Production/Stable Environment :: Console @@ -18,8 +18,6 @@ classifier = Operating System :: OS Independent Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3 :: Only From 782a46753303150bb9e00a85bde52819665a36d7 Mon Sep 17 00:00:00 2001 From: Ghanshyam Mann Date: Fri, 29 Apr 2022 20:56:13 -0500 Subject: [PATCH 1644/1705] Drop lower-constraints.txt and its testing As discussed in TC PTG[1] and TC resolution[2], we are dropping the lower-constraints.txt file and its testing. We will keep lower bounds in the requirements.txt file but with a note that these are not tested lower bounds and we try our best to keep them updated. [1] https://etherpad.opendev.org/p/tc-zed-ptg#L326 [2] https://governance.openstack.org/tc/resolutions/20220414-drop-lower-constraints.html#proposal Change-Id: Ibb492285ef7d5357c921035409d6654c80df3725 --- .zuul.yaml | 1 - lower-constraints.txt | 99 ------------------------------------------- requirements.txt | 4 ++ tox.ini | 6 --- 4 files changed, 4 insertions(+), 106 deletions(-) delete mode 100644 lower-constraints.txt diff --git a/.zuul.yaml b/.zuul.yaml index 938cdb675..791a19f11 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -19,7 +19,6 @@ - check-requirements - lib-forward-testing-python3 - openstack-cover-jobs - - openstack-lower-constraints-jobs - openstack-python3-zed-jobs - publish-openstack-docs-pti - release-notes-jobs-python3 diff --git a/lower-constraints.txt b/lower-constraints.txt deleted file mode 100644 index dd3199a20..000000000 --- a/lower-constraints.txt +++ /dev/null @@ -1,99 +0,0 @@ -amqp==2.1.1 -appdirs==1.3.0 -asn1crypto==0.23.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.1 -ddt==1.0.1 -debtcollector==1.2.0 -decorator==4.1.0 -deprecation==1.0 -dogpile.cache==0.6.2 -eventlet==0.18.2 -extras==1.0.0 -fasteners==0.7.0 -fixtures==3.0.0 -future==0.16.0 -futurist==1.2.0 -gitdb==0.6.4 -GitPython==1.0.1 -greenlet==0.4.15 -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.5.0 -kombu==4.0.0 -linecache2==1.0.0 -MarkupSafe==1.1.1 -monotonic==0.6 -msgpack-python==0.4.0 -munch==2.1.0 -netaddr==0.7.18 -netifaces==0.10.4 -openstacksdk==0.11.2 -os-client-config==1.28.0 -os-service-types==1.2.0 -osc-lib==1.8.0 -oslo.concurrency==3.25.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 -osprofiler==1.4.0 -paramiko==2.0.0 -Paste==2.0.2 -PasteDeploy==1.5.0 -pbr==2.0.0 -pika==0.10.0 -pika-pool==0.1.3 -positional==1.2.1 -prettytable==0.7.2 -pyasn1==0.1.8 -pycparser==2.18 -pyinotify==0.9.6 -pyOpenSSL==17.1.0 -pyparsing==2.1.0 -pyperclip==1.5.27 -python-cinderclient==3.3.0 -python-dateutil==2.5.3 -python-glanceclient==2.8.0 -python-keystoneclient==3.8.0 -python-mimeparse==1.6.0 -python-neutronclient==6.7.0 -python-subunit==1.0.0 -pytz==2013.6 -PyYAML==3.13 -repoze.lru==0.7 -requests==2.14.2 -requests-mock==1.2.0 -requestsexceptions==1.2.0 -rfc3986==0.3.1 -Routes==2.3.1 -smmap==0.9.0 -statsd==3.2.1 -stevedore==2.0.1 -tempest==17.1.0 -tenacity==3.2.1 -stestr==2.0.0 -testscenarios==0.4 -testtools==2.2.0 -traceback2==1.4.0 -unittest2==1.1.0 -urllib3==1.21.1 -vine==1.1.4 -warlock==1.2.0 -WebOb==1.7.1 -wrapt==1.7.0 diff --git a/requirements.txt b/requirements.txt index 462e1c9c0..2eec1a70d 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. + pbr!=2.1.0,>=2.0.0 # Apache-2.0 keystoneauth1>=3.5.0 # Apache-2.0 iso8601>=0.1.11 # MIT diff --git a/tox.ini b/tox.ini index 108052813..0fbe142e7 100644 --- a/tox.ini +++ b/tox.ini @@ -101,9 +101,3 @@ import_exceptions = novaclient.i18n # separately, outside of the requirements files. deps = bindep commands = bindep test - -[testenv:lower-constraints] -deps = - -c{toxinidir}/lower-constraints.txt - -r{toxinidir}/test-requirements.txt - -r{toxinidir}/requirements.txt From 81dece81683a096d331d4ed5eecec0fda2d653dd Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Tue, 21 Jun 2022 03:56:47 +0000 Subject: [PATCH 1645/1705] Imported Translations from Zanata For more information about this automatic import see: https://docs.openstack.org/i18n/latest/reviewing-translation-import.html Change-Id: I4bd5c1ed4b6bb7c99e7724d77076bfb759bac376 --- .../locale/en_GB/LC_MESSAGES/releasenotes.po | 237 ++++++++++++++++++ 1 file changed, 237 insertions(+) create mode 100644 releasenotes/source/locale/en_GB/LC_MESSAGES/releasenotes.po diff --git a/releasenotes/source/locale/en_GB/LC_MESSAGES/releasenotes.po b/releasenotes/source/locale/en_GB/LC_MESSAGES/releasenotes.po new file mode 100644 index 000000000..7cde652c5 --- /dev/null +++ b/releasenotes/source/locale/en_GB/LC_MESSAGES/releasenotes.po @@ -0,0 +1,237 @@ +# Andi Chandler , 2017. #zanata +# Andi Chandler , 2018. #zanata +# Andi Chandler , 2022. #zanata +msgid "" +msgstr "" +"Project-Id-Version: python-novaclient\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2022-05-08 05:06+0000\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"PO-Revision-Date: 2022-06-13 07:34+0000\n" +"Last-Translator: Andi Chandler \n" +"Language-Team: English (United Kingdom)\n" +"Language: en_GB\n" +"X-Generator: Zanata 4.3.3\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" + +msgid "" +"**auto**: This tells the Compute service to automatically allocate a network " +"for the project if one is not available and then associate an IP from that " +"network with the server. This is the same behavior as passing nics=None " +"before the 2.37 microversion." +msgstr "" +"**auto**: This tells the Compute service to automatically allocate a network " +"for the project if one is not available and then associate an IP from that " +"network with the server. This is the same behaviour as passing nics=None " +"before the 2.37 microversion." + +msgid "" +"**none**: This tells the Compute service to not allocate any networking for " +"the server." +msgstr "" +"**none**: This tells the Compute service to not allocate any networking for " +"the server." + +msgid "--all_tenants replaced by --all-tenants" +msgstr "--all_tenants replaced by --all-tenants" + +msgid "--availability-zone" +msgstr "--availability-zone" + +msgid "--availability_zone replaced by -- availability-zone" +msgstr "--availability_zone replaced by -- availability-zone" + +msgid "--availability_zone replaced by --availability-zone" +msgstr "--availability_zone replaced by --availability-zone" + +msgid "--block_device_mapping replaced by --block-device-mapping" +msgstr "--block_device_mapping replaced by --block-device-mapping" + +msgid "--block_migrate replaced by --block-migrate" +msgstr "--block_migrate replaced by --block-migrate" + +msgid "--bypass_url replaced by --bypass-url" +msgstr "--bypass_url replaced by --bypass-url" + +msgid "--config-drive" +msgstr "--config-drive" + +msgid "--console_type replaced by --console-type" +msgstr "--console_type replaced by --console-type" + +msgid "--disk_over_commit replaced by --disk-over-commit" +msgstr "--disk_over_commit replaced by --disk-over-commit" + +msgid "--endpoint-type replaced by --os-endpoint-type" +msgstr "--endpoint-type replaced by --os-endpoint-type" + +msgid "--floating_ips replaced by --floating-ips" +msgstr "--floating_ips replaced by --floating-ips" + +msgid "--injected_file_content_bytes replaced by --injected-file-content-bytes" +msgstr "" +"--injected_file_content_bytes replaced by --injected-file-content-bytes" + +msgid "--injected_files replaced by --injected-files" +msgstr "--injected_files replaced by --injected-files" + +msgid "--instance_name replaced by --instance-name" +msgstr "--instance_name replaced by --instance-name" + +msgid "--key-name" +msgstr "--key-name" + +msgid "--key_name replaced by --key-name" +msgstr "--key_name replaced by --key-name" + +msgid "--metadata_items replaced by --metadata-items" +msgstr "--metadata_items replaced by --metadata-items" + +msgid "--no-config-drive" +msgstr "--no-config-drive" + +msgid "--num-instance replaced by --min-count and --max-count" +msgstr "--num-instance replaced by --min-count and --max-count" + +msgid "--os_auth_system replaced by --os-auth-system" +msgstr "--os_auth_system replaced by --os-auth-system" + +msgid "--os_auth_url replaced by --os-auth-url" +msgstr "--os_auth_url replaced by --os-auth-url" + +msgid "--os_compute_api_version replaced by --os-compute-api-version" +msgstr "--os_compute_api_version replaced by --os-compute-api-version" + +msgid "--os_password replaced by --os-password" +msgstr "--os_password replaced by --os-password" + +msgid "--os_region_name replaced by --os-region-name" +msgstr "--os_region_name replaced by --os-region-name" + +msgid "--os_tenant_name replaced by --os-tenant-name" +msgstr "--os_tenant_name replaced by --os-tenant-name" + +msgid "--os_username replaced by --os-username" +msgstr "--os_username replaced by --os-username" + +msgid "--policy" +msgstr "--policy" + +msgid "--power-state" +msgstr "--power-state" + +msgid "--progress" +msgstr "--progress" + +msgid "--pub_key replaced by --pub-key" +msgstr "--pub_key replaced by --pub-key" + +msgid "--rebuild_password replaced by --rebuild-password" +msgstr "--rebuild_password replaced by --rebuild-password" + +msgid "--reservation_id replaced by --reservation-id" +msgstr "--reservation_id replaced by --reservation-id" + +msgid "--security_groups replaced by --sercurity-groups" +msgstr "--security_groups replaced by --security-groups" + +msgid "--service_name replaced by --service-name" +msgstr "--service_name replaced by --service-name" + +msgid "--service_type replaced by --service-type" +msgstr "--service_type replaced by --service-type" + +msgid "--task-state" +msgstr "--task-state" + +msgid "--user_data replaced by --user-data" +msgstr "--user_data replaced by --user-data" + +msgid "--volume_service_name replaced by --volume-service-name" +msgstr "--volume_service_name replaced by --volume-service-name" + +msgid "10.0.0" +msgstr "10.0.0" + +msgid "10.1.0" +msgstr "10.1.0" + +msgid "10.2.0" +msgstr "10.2.0" + +msgid "10.3.0" +msgstr "10.3.0" + +msgid "11.0.0" +msgstr "11.0.0" + +msgid "3.0.0" +msgstr "3.0.0" + +msgid "3.3.0" +msgstr "3.3.0" + +msgid "4.0.0" +msgstr "4.0.0" + +msgid "4.1.0" +msgstr "4.1.0" + +msgid "5.0.0" +msgstr "5.0.0" + +msgid "5.1.0" +msgstr "5.1.0" + +msgid "6.0.0" +msgstr "6.0.0" + +msgid ":ref:`search`" +msgstr ":ref:`search`" + +msgid "Current Series Release Notes" +msgstr "Current Series Release Notes" + +msgid "Liberty Series Release Notes" +msgstr "Liberty Series Release Notes" + +msgid "Mitaka Series Release Notes" +msgstr "Mitaka Series Release Notes" + +msgid "Newton Series Release Notes" +msgstr "Newton Series Release Notes" + +msgid "Ocata Series Release Notes" +msgstr "Ocata Series Release Notes" + +msgid "Pike Series Release Notes" +msgstr "Pike Series Release Notes" + +msgid "Queens Series Release Notes" +msgstr "Queens Series Release Notes" + +msgid "Rocky Series Release Notes" +msgstr "Rocky Series Release Notes" + +msgid "Stein Series Release Notes" +msgstr "Stein Series Release Notes" + +msgid "Train Series Release Notes" +msgstr "Train Series Release Notes" + +msgid "Ussuri Series Release Notes" +msgstr "Ussuri Series Release Notes" + +msgid "Victoria Series Release Notes" +msgstr "Victoria Series Release Notes" + +msgid "Wallaby Series Release Notes" +msgstr "Wallaby Series Release Notes" + +msgid "Xena Series Release Notes" +msgstr "Xena Series Release Notes" + +msgid "Yoga Series Release Notes" +msgstr "Yoga Series Release Notes" From d867ef65511cf6c9bda625a5ace22f90c3445c14 Mon Sep 17 00:00:00 2001 From: Takashi Natsume Date: Fri, 6 May 2022 00:55:03 +0900 Subject: [PATCH 1646/1705] Replace old URLs with new ones Change-Id: I66d76bed4ec19ef7517d88de20fcc32fea635261 Signed-off-by: Takashi Natsume --- README.rst | 4 ++-- bindep.txt | 2 +- doc/source/contributor/testing.rst | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 1bc3bdcc2..cdaf86536 100644 --- a/README.rst +++ b/README.rst @@ -36,6 +36,6 @@ Python API implements 100% of the OpenStack Compute API. .. _Blueprints: https://blueprints.launchpad.net/python-novaclient .. _Bugs: https://bugs.launchpad.net/python-novaclient .. _Source: https://opendev.org/openstack/python-novaclient -.. _How to Contribute: https://docs.openstack.org/infra/manual/developers.html -.. _Specs: http://specs.openstack.org/openstack/nova-specs/ +.. _How to Contribute: https://docs.opendev.org/opendev/infra-manual/latest/developers.html +.. _Specs: https://specs.openstack.org/openstack/nova-specs/ .. _Release Notes: https://docs.openstack.org/releasenotes/python-novaclient diff --git a/bindep.txt b/bindep.txt index eaa5e1f2f..c3ae8263a 100644 --- a/bindep.txt +++ b/bindep.txt @@ -1,5 +1,5 @@ # This is a cross-platform list tracking distribution packages needed by tests; -# see https://docs.openstack.org/infra/bindep/ for additional information. +# see https://docs.opendev.org/opendev/bindep/latest/ for additional information. build-essential [platform:dpkg] dbus-devel [platform:rpm] diff --git a/doc/source/contributor/testing.rst b/doc/source/contributor/testing.rst index 4a1e1938b..ed0fa506d 100644 --- a/doc/source/contributor/testing.rst +++ b/doc/source/contributor/testing.rst @@ -25,4 +25,4 @@ DevStack installation with a demo and an admin user/tenant - or clouds named Refer to `Consistent Testing Interface`__ for more details. -__ https://opendev.org/openstack/governance/src/branch/master/reference/project-testing-interface.rst +__ https://governance.openstack.org/tc/reference/project-testing-interface.html From 4d4cdb49922b5e27ab1f892a5a5f7ac5994eedf1 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Wed, 6 Jul 2022 03:56:50 +0000 Subject: [PATCH 1647/1705] Imported Translations from Zanata For more information about this automatic import see: https://docs.openstack.org/i18n/latest/reviewing-translation-import.html Change-Id: I9eeb37d4b4771798e09802915c42bf79df9422c0 --- .../locale/en_GB/LC_MESSAGES/releasenotes.po | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/releasenotes/source/locale/en_GB/LC_MESSAGES/releasenotes.po b/releasenotes/source/locale/en_GB/LC_MESSAGES/releasenotes.po index 7cde652c5..2b450f7c2 100644 --- a/releasenotes/source/locale/en_GB/LC_MESSAGES/releasenotes.po +++ b/releasenotes/source/locale/en_GB/LC_MESSAGES/releasenotes.po @@ -5,11 +5,11 @@ msgid "" msgstr "" "Project-Id-Version: python-novaclient\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-05-08 05:06+0000\n" +"POT-Creation-Date: 2022-06-24 11:46+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"PO-Revision-Date: 2022-06-13 07:34+0000\n" +"PO-Revision-Date: 2022-07-05 09:42+0000\n" "Last-Translator: Andi Chandler \n" "Language-Team: English (United Kingdom)\n" "Language: en_GB\n" @@ -149,6 +149,9 @@ msgstr "--task-state" msgid "--user_data replaced by --user-data" msgstr "--user_data replaced by --user-data" +msgid "--vm-state" +msgstr "--vm-state" + msgid "--volume_service_name replaced by --volume-service-name" msgstr "--volume_service_name replaced by --volume-service-name" @@ -158,6 +161,9 @@ msgstr "10.0.0" msgid "10.1.0" msgstr "10.1.0" +msgid "10.1.1" +msgstr "10.1.1" + msgid "10.2.0" msgstr "10.2.0" @@ -167,6 +173,9 @@ msgstr "10.3.0" msgid "11.0.0" msgstr "11.0.0" +msgid "11.0.1" +msgstr "11.0.1" + msgid "3.0.0" msgstr "3.0.0" @@ -235,3 +244,24 @@ msgstr "Xena Series Release Notes" msgid "Yoga Series Release Notes" msgstr "Yoga Series Release Notes" + +msgid "secgroup-create" +msgstr "secgroup-create" + +msgid "secgroup-delete" +msgstr "secgroup-delete" + +msgid "secgroup-delete-default-rule" +msgstr "secgroup-delete-default-rule" + +msgid "secgroup-delete-group-rule" +msgstr "secgroup-delete-group-rule" + +msgid "secgroup-delete-rule" +msgstr "secgroup-delete-rule" + +msgid "secgroup-list" +msgstr "secgroup-list" + +msgid "secgroup-list-default-rules" +msgstr "secgroup-list-default-rules" From ee9b277c5f442f299b853118c900c3ee2996c67a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Ribaud?= Date: Thu, 3 Mar 2022 11:18:59 +0100 Subject: [PATCH 1648/1705] Microversion 2.91: Support specifying destination host to unshelve This patch adds ``host`` to novaclient api. This can help administrators to specify a ``host`` to unshelve a shelve offloaded server from 2.91 microversion. Depends-On: https://review.opendev.org/c/openstack/nova/+/831507 Implements: blueprint unshelve-to-host Change-Id: I7efc8f0b0ef159e16cefee761bff5d7e90d0c427 --- novaclient/__init__.py | 2 +- novaclient/tests/unit/fixture_data/servers.py | 11 +++ novaclient/tests/unit/v2/fakes.py | 2 +- novaclient/tests/unit/v2/test_servers.py | 98 ++++++++++++++++++- novaclient/v2/servers.py | 65 +++++++++++- .../bp-unshelve-to-host-b220131a00dff8a2.yaml | 9 ++ 6 files changed, 182 insertions(+), 5 deletions(-) create mode 100644 releasenotes/notes/bp-unshelve-to-host-b220131a00dff8a2.yaml diff --git a/novaclient/__init__.py b/novaclient/__init__.py index d49e8841a..bbfd95224 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.90") +API_MAX_VERSION = api_versions.APIVersion("2.91") diff --git a/novaclient/tests/unit/fixture_data/servers.py b/novaclient/tests/unit/fixture_data/servers.py index a8301548e..8e53ecdb2 100644 --- a/novaclient/tests/unit/fixture_data/servers.py +++ b/novaclient/tests/unit/fixture_data/servers.py @@ -439,6 +439,17 @@ def post_servers_1234_action(self, request, context): elif action == 'lock': return None elif action == 'unshelve': + if api_version >= api_versions.APIVersion("2.91"): + # In 2.91 and above, we allow body to be one of these: + # {'unshelve': None} + # {'unshelve': {'availability_zone': }} + # {'unshelve': {'availability_zone': None}} (Unpin az) + # {'unshelve': {'host': }} + # {'unshelve': {'availability_zone': , 'host': }} + # {'unshelve': {'availability_zone': None, 'host': }} + if body[action] is not None: + for key in body[action].keys(): + key in ['availability_zone', 'host'] return None elif action == 'rebuild': body = body[action] diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 07216adc2..059a7147f 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -835,7 +835,7 @@ def post_servers_1234_action(self, body, **kw): if self.api_version < api_versions.APIVersion("2.77"): assert body[action] is None else: - # In 2.77 and above, we allow body to be one of these: + # In 2.77 to 2.91, we allow body to be one of these: # {'unshelve': None} # {'unshelve': {'availability_zone': 'foo-az'}} if body[action] is not None: diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index 071ae5c18..93fefabfe 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -1851,6 +1851,23 @@ class ServersV277Test(ServersV274Test): api_version = "2.77" + def test_unshelve(self): + s = self.cs.servers.get(1234) + # Test going through the Server object. + ret = s.unshelve() + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called( + 'POST', + '/servers/1234/action', + {'unshelve': None}) + # Test going through the ServerManager directly. + ret = self.cs.servers.unshelve(s) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called( + 'POST', + '/servers/1234/action', + {'unshelve': None}) + def test_unshelve_with_az(self): s = self.cs.servers.get(1234) # Test going through the Server object. @@ -1883,7 +1900,7 @@ def test_unshelve_server_pre_277_fails_with_specified_az(self): str(ex)) -class ServersV278Test(ServersV273Test): +class ServersV278Test(ServersV277Test): api_version = "2.78" @@ -1992,3 +2009,82 @@ def test_update_with_hostname_pre_290_fails(self): s.update, hostname='new-hostname') self.assertIn('hostname', str(ex)) + + +class ServersV291Test(ServersV290Test): + + api_version = "2.91" + + def test_unshelve_with_host(self): + s = self.cs.servers.get(1234) + # Test going through the Server object. + ret = s.unshelve(host='server1') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called( + 'POST', + '/servers/1234/action', + {'unshelve': {'host': 'server1'}}) + # Test going through the ServerManager directly. + ret = self.cs.servers.unshelve(s, host='server1') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called( + 'POST', + '/servers/1234/action', + {'unshelve': {'host': 'server1'}}) + + def test_unshelve_server_with_az_and_host(self): + s = self.cs.servers.get(1234) + # Test going through the Server object. + ret = s.unshelve(host='server1', availability_zone='foo-az') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called( + 'POST', + '/servers/1234/action', + {'unshelve': {'host': 'server1', + 'availability_zone': 'foo-az'}}) + # Test going through the ServerManager directly. + ret = self.cs.servers.unshelve( + s, host='server1', availability_zone='foo-az') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called( + 'POST', + '/servers/1234/action', + {'unshelve': {'host': 'server1', + 'availability_zone': 'foo-az'}}) + + def test_unshelve_unpin_az(self): + s = self.cs.servers.get(1234) + # Test going through the Server object. + ret = s.unshelve(availability_zone=None) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called( + 'POST', + '/servers/1234/action', + {'unshelve': {'availability_zone': None}}) + # Test going through the ServerManager directly. + ret = self.cs.servers.unshelve(s, availability_zone=None) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called( + 'POST', + '/servers/1234/action', + {'unshelve': {'availability_zone': None}}) + + def test_unshelve_server_with_host_and_unpin(self): + s = self.cs.servers.get(1234) + # Test going through the Server object. + ret = s.unshelve(availability_zone=None, host='server1') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called( + 'POST', + '/servers/1234/action', + {'unshelve': {'host': 'server1', + 'availability_zone': None}}) + # Test going through the ServerManager directly. + ret = self.cs.servers.unshelve( + s, availability_zone=None, host='server1') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called( + 'POST', + '/servers/1234/action', + {'unshelve': {'host': 'server1', + 'availability_zone': None}}) diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index 76709ec5e..8a9301f6d 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -314,7 +314,7 @@ def unshelve(self): """ return self.manager.unshelve(self) - @api_versions.wraps("2.77") + @api_versions.wraps("2.77", "2.90") def unshelve(self, availability_zone=None): """ Unshelve -- Unshelve the server. @@ -326,6 +326,37 @@ def unshelve(self, availability_zone=None): return self.manager.unshelve(self, availability_zone=availability_zone) + @api_versions.wraps("2.91") + def unshelve(self, availability_zone=object(), host=None): + """ + Unshelve -- Unshelve the server. + + :param availability_zone: If specified the instance will be unshelved + to the availability_zone. + If None is passed the instance defined + availability_zone is unpin and the instance + will be scheduled to any availability_zone + (free scheduling). + If not specified the instance will be + unshelved to either its defined + availability_zone or any + availability_zone (free scheduling). + :param host: The specified host + (Optional) + :returns: An instance of novaclient.base.TupleWithMeta + """ + if ( + availability_zone is None or isinstance(availability_zone, str) + ) and host: + return self.manager.unshelve( + self, availability_zone=availability_zone, host=host) + if availability_zone is None or isinstance(availability_zone, str): + return self.manager.unshelve( + self, availability_zone=availability_zone) + if host: + return self.manager.unshelve(self, host=host) + return self.manager.unshelve(self) + def diagnostics(self): """Diagnostics -- Retrieve server diagnostics.""" return self.manager.diagnostics(self) @@ -1266,7 +1297,7 @@ def unshelve(self, server): """ return self._action('unshelve', server, None) - @api_versions.wraps("2.77") + @api_versions.wraps("2.77", "2.90") def unshelve(self, server, availability_zone=None): """ Unshelve the server. @@ -1281,6 +1312,36 @@ def unshelve(self, server, availability_zone=None): info = {'availability_zone': availability_zone} return self._action('unshelve', server, info) + @api_versions.wraps("2.91") + def unshelve(self, server, availability_zone=object(), host=None): + """ + Unshelve the server. + + :param availability_zone: If specified the instance will be unshelved + to the availability_zone. + If None is passed the instance defined + availability_zone is unpin and the instance + will be scheduled to any availability_zone + (free scheduling). + If not specified the instance will be + unshelved to either its defined + availability_zone or any + availability_zone (free scheduling). + :param host: The specified host + (Optional) + :returns: An instance of novaclient.base.TupleWithMeta + """ + info = None + + if availability_zone is None or isinstance(availability_zone, str): + info = {'availability_zone': availability_zone} + if host: + if info: + info['host'] = host + else: + info = {'host': host} + return self._action('unshelve', server, info) + def ips(self, server): """ Return IP Addresses associated with the server. diff --git a/releasenotes/notes/bp-unshelve-to-host-b220131a00dff8a2.yaml b/releasenotes/notes/bp-unshelve-to-host-b220131a00dff8a2.yaml new file mode 100644 index 000000000..98fe2c9f9 --- /dev/null +++ b/releasenotes/notes/bp-unshelve-to-host-b220131a00dff8a2.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + Support has been added for `microversion 2.91`_. This microversion + allows specifying a destination host to unshelve a shelve + offloaded server. And availability zone can be set to None to unpin + the availability zone of a server. + + .. _microversion 2.91: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#microversion-2-91 From 2b5d989990b14ef81e3ca2a48df8e43b6b205d0e Mon Sep 17 00:00:00 2001 From: Sylvain Bauza Date: Wed, 27 Jul 2022 16:10:30 +0200 Subject: [PATCH 1649/1705] Add support for 2.92 : keypair import mandatory Now, when creating a keypair, the 'public_key' parameter is now mandatory. Depends-On: https://review.opendev.org/c/openstack/nova/+/849133 Implements: blueprint keypair-generation-removal Change-Id: I03570d0a49b73021de91dc50b65b1bbf5d4b878b --- novaclient/__init__.py | 2 +- novaclient/tests/unit/v2/fakes.py | 7 +++++- novaclient/tests/unit/v2/test_keypairs.py | 25 +++++++++++++++++++ novaclient/v2/keypairs.py | 19 +++++++++++++- ...r-generation-removal-1b5d84a8906d3918.yaml | 8 ++++++ 5 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/bp-keypair-generation-removal-1b5d84a8906d3918.yaml diff --git a/novaclient/__init__.py b/novaclient/__init__.py index bbfd95224..afffde0f0 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.91") +API_MAX_VERSION = api_versions.APIVersion("2.92") diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 059a7147f..c7687c1e9 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -1228,8 +1228,13 @@ def delete_os_keypairs_test(self, **kw): def post_os_keypairs(self, body, **kw): assert list(body) == ['keypair'] + if self.api_version >= api_versions.APIVersion("2.92"): + # In 2.92, public_key becomes mandatory + required = ['name', 'public_key'] + else: + required = ['name'] fakes.assert_has_keys(body['keypair'], - required=['name']) + required=required) r = {'keypair': self.get_os_keypairs()[2]['keypairs'][0]['keypair']} return (202, {}, r) diff --git a/novaclient/tests/unit/v2/test_keypairs.py b/novaclient/tests/unit/v2/test_keypairs.py index cf310b0ef..7e16438d8 100644 --- a/novaclient/tests/unit/v2/test_keypairs.py +++ b/novaclient/tests/unit/v2/test_keypairs.py @@ -127,3 +127,28 @@ def test_list_keypairs(self): % self.keypair_prefix) for kp in kps: self.assertIsInstance(kp, keypairs.Keypair) + + +class KeypairsV92TestCase(KeypairsTest): + def setUp(self): + super(KeypairsV92TestCase, self).setUp() + self.cs.api_version = api_versions.APIVersion("2.92") + + def test_create_keypair(self): + name = "foo" + key_type = "some_type" + public_key = "fake-public-key" + kp = self.cs.keypairs.create(name, public_key=public_key, + key_type=key_type) + self.assert_request_id(kp, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/%s' % self.keypair_prefix, + body={'keypair': {'name': name, + 'public_key': public_key, + 'type': key_type}}) + self.assertIsInstance(kp, keypairs.Keypair) + + def test_create_keypair_without_pubkey(self): + name = "foo" + key_type = "some_type" + self.assertRaises(TypeError, + self.cs.keypairs.create, name, key_type=key_type) diff --git a/novaclient/v2/keypairs.py b/novaclient/v2/keypairs.py index 9b2e73b71..5d12f8cd4 100644 --- a/novaclient/v2/keypairs.py +++ b/novaclient/v2/keypairs.py @@ -114,7 +114,7 @@ def create(self, name, public_key=None, key_type="ssh"): body['keypair']['public_key'] = public_key return self._create('/%s' % self.keypair_prefix, body, 'keypair') - @api_versions.wraps("2.10") + @api_versions.wraps("2.10", "2.91") def create(self, name, public_key=None, key_type="ssh", user_id=None): """ Create a keypair @@ -132,6 +132,23 @@ def create(self, name, public_key=None, key_type="ssh", user_id=None): body['keypair']['user_id'] = user_id return self._create('/%s' % self.keypair_prefix, body, 'keypair') + @api_versions.wraps("2.92") + def create(self, name, public_key, key_type="ssh", user_id=None): + """ + Create a keypair + + :param name: name for the keypair to create + :param public_key: existing public key to import + :param key_type: keypair type to create + :param user_id: user to add. + """ + body = {'keypair': {'name': name, + 'type': key_type, + 'public_key': public_key}} + if user_id: + body['keypair']['user_id'] = user_id + return self._create('/%s' % self.keypair_prefix, body, 'keypair') + @api_versions.wraps("2.0", "2.9") def delete(self, key): """ diff --git a/releasenotes/notes/bp-keypair-generation-removal-1b5d84a8906d3918.yaml b/releasenotes/notes/bp-keypair-generation-removal-1b5d84a8906d3918.yaml new file mode 100644 index 000000000..c7cdb6424 --- /dev/null +++ b/releasenotes/notes/bp-keypair-generation-removal-1b5d84a8906d3918.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Support has been added for `microversion 2.92`_. This microversion only + accepts to import a public key and no longer to generate one, hence now the + public_key parameter be mandatory. + + .. _microversion 2.92: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#microversion-2-92 From be9517cb027d960caf4c9c8171b3ad2568bedae9 Mon Sep 17 00:00:00 2001 From: Takashi Natsume Date: Fri, 5 Aug 2022 23:35:50 +0900 Subject: [PATCH 1650/1705] Fix a fixture for keypairs tests This patch is a follow-up for I03570d0a49b73021de91dc50b65b1bbf5d4b878b. The following file is for shell (CLI) tests, so it does not need to be changed anymore. * novaclient/tests/unit/v2/fakes.py Change-Id: I3b1cf5d402b04854177265f2ba429956edb73203 Signed-off-by: Takashi Natsume --- novaclient/tests/unit/fixture_data/keypairs.py | 12 +++++++++++- novaclient/tests/unit/v2/fakes.py | 7 +------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/novaclient/tests/unit/fixture_data/keypairs.py b/novaclient/tests/unit/fixture_data/keypairs.py index 1d73d2528..5ac1b4ad7 100644 --- a/novaclient/tests/unit/fixture_data/keypairs.py +++ b/novaclient/tests/unit/fixture_data/keypairs.py @@ -10,16 +10,20 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient import api_versions from novaclient.tests.unit import fakes from novaclient.tests.unit.fixture_data import base class V1(base.Fixture): + api_version = '2.1' base_url = 'os-keypairs' def setUp(self): super(V1, self).setUp() + api_version = api_versions.APIVersion(self.api_version) + keypair = {'fingerprint': 'FAKE_KEYPAIR', 'name': 'test'} headers = self.json_headers @@ -39,7 +43,13 @@ def setUp(self): def post_os_keypairs(request, context): body = request.json() assert list(body) == ['keypair'] - fakes.assert_has_keys(body['keypair'], required=['name']) + if api_version >= api_versions.APIVersion("2.92"): + # In 2.92, public_key becomes mandatory + required = ['name', 'public_key'] + else: + required = ['name'] + fakes.assert_has_keys(body['keypair'], + required=required) return {'keypair': keypair} self.requests_mock.post(self.url(), diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index c7687c1e9..059a7147f 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -1228,13 +1228,8 @@ def delete_os_keypairs_test(self, **kw): def post_os_keypairs(self, body, **kw): assert list(body) == ['keypair'] - if self.api_version >= api_versions.APIVersion("2.92"): - # In 2.92, public_key becomes mandatory - required = ['name', 'public_key'] - else: - required = ['name'] fakes.assert_has_keys(body['keypair'], - required=required) + required=['name']) r = {'keypair': self.get_os_keypairs()[2]['keypairs'][0]['keypair']} return (202, {}, r) From 94d3445e10711ff965e0ec141aaa0d0828dc43f0 Mon Sep 17 00:00:00 2001 From: whoami-rajat Date: Mon, 31 Jan 2022 23:54:27 +0530 Subject: [PATCH 1651/1705] MV 2.93 - Add support to rebuild boot volume This patch bumps the API microversion to 2.93 to allow rebuilding a volume backed instance. Implements: blueprint volume-backed-server-rebuild Depends-On: https://review.opendev.org/c/openstack/nova/+/830883 Change-Id: Ie46df7ad76082e7631bb26243abed4dc3b1f40ac --- novaclient/__init__.py | 2 +- novaclient/tests/unit/v2/test_shell.py | 1 + ...upport-for-volume-backed-rebuild-6a32d9d88fed6b4a.yaml | 8 ++++++++ 3 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/add-support-for-volume-backed-rebuild-6a32d9d88fed6b4a.yaml diff --git a/novaclient/__init__.py b/novaclient/__init__.py index afffde0f0..2a89e3385 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.92") +API_MAX_VERSION = api_versions.APIVersion("2.93") diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index da70e3ccb..84f4fcdc3 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -4699,6 +4699,7 @@ def test_versions(self): 86, # doesn't require any changes in novaclient. 87, # doesn't require any changes in novaclient. 89, # There are no version-wrapped shell method changes for this. + 93, # There are no version-wrapped shell method changes for this. ]) versions_supported = set(range(0, novaclient.API_MAX_VERSION.ver_minor + 1)) diff --git a/releasenotes/notes/add-support-for-volume-backed-rebuild-6a32d9d88fed6b4a.yaml b/releasenotes/notes/add-support-for-volume-backed-rebuild-6a32d9d88fed6b4a.yaml new file mode 100644 index 000000000..b9e2054e8 --- /dev/null +++ b/releasenotes/notes/add-support-for-volume-backed-rebuild-6a32d9d88fed6b4a.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Added support for `microversion 2.93`_. + This microversion provides the ability to rebuild a volume + backed instance. + + .. _microversion 2.93: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#microversion-2-93 From f50bcbc47f31461424a66cd0f099ef3df12fc0ea Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Fri, 9 Sep 2022 15:04:19 +0000 Subject: [PATCH 1652/1705] Update master for stable/zed Add file to the reno documentation build to show release notes for stable/zed. Use pbr instruction to increment the minor version number automatically so that master versions are higher than the versions on stable/zed. Sem-Ver: feature Change-Id: I722c5645b9e039fd5d4e8fc75fd6c614eed593d5 --- releasenotes/source/index.rst | 1 + releasenotes/source/zed.rst | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 releasenotes/source/zed.rst diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index 68524accd..839eff57c 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -8,6 +8,7 @@ Contents :maxdepth: 2 unreleased + zed yoga xena wallaby diff --git a/releasenotes/source/zed.rst b/releasenotes/source/zed.rst new file mode 100644 index 000000000..9608c05e4 --- /dev/null +++ b/releasenotes/source/zed.rst @@ -0,0 +1,6 @@ +======================== +Zed Series Release Notes +======================== + +.. release-notes:: + :branch: stable/zed From d4ea897bc95ab5cb8a715fcb6321b0c2ed7a8d23 Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Fri, 9 Sep 2022 15:04:21 +0000 Subject: [PATCH 1653/1705] Switch to 2023.1 Python3 unit tests and generic template name This is an automatically generated patch to ensure unit testing is in place for all the of the tested runtimes for antelope. Also, updating the template name to generic one. See also the PTI in governance [1]. [1]: https://governance.openstack.org/tc/reference/project-testing-interface.html Change-Id: Icf54ae2ae9db6996da6b881898fea2de4b67190c --- .zuul.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.zuul.yaml b/.zuul.yaml index 791a19f11..5dfc1c5a1 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -19,7 +19,7 @@ - check-requirements - lib-forward-testing-python3 - openstack-cover-jobs - - openstack-python3-zed-jobs + - openstack-python3-jobs - publish-openstack-docs-pti - release-notes-jobs-python3 check: From 2af1d0c51431b3a773e1e52158b9cf43e4645fdd Mon Sep 17 00:00:00 2001 From: Takashi Natsume Date: Fri, 12 Aug 2022 20:46:34 +0900 Subject: [PATCH 1654/1705] Remove unnecessary testing code Now PrettyTable >= 0.7.2 in requirements.txt, remove code for PrettyTable < 0.7.2. Change-Id: Ie6edcc24fbb67394ff8abe0c5f18ad2bed6903ab Signed-off-by: Takashi Natsume --- novaclient/tests/unit/test_shell.py | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/novaclient/tests/unit/test_shell.py b/novaclient/tests/unit/test_shell.py index b6b708a82..a997b23ff 100644 --- a/novaclient/tests/unit/test_shell.py +++ b/novaclient/tests/unit/test_shell.py @@ -12,7 +12,6 @@ # under the License. import argparse -import distutils.version as dist_version import io import re import sys @@ -21,7 +20,6 @@ import ddt import fixtures from keystoneauth1 import fixture -import prettytable import requests_mock from testtools import matchers @@ -583,20 +581,13 @@ def test_default_endpoint_type(self): def test_password(self, mock_getpass, mock_stdin, m_requests): mock_stdin.encoding = "utf-8" - # default output of empty tables differs depending between prettytable - # versions - if (hasattr(prettytable, '__version__') and - dist_version.StrictVersion(prettytable.__version__) < - dist_version.StrictVersion('0.7.2')): - ex = '\n' - else: - ex = '\n'.join([ - '+----+------+--------+------------+-------------+----------+', - '| ID | Name | Status | Task State | Power State | Networks |', - '+----+------+--------+------------+-------------+----------+', - '+----+------+--------+------------+-------------+----------+', - '' - ]) + ex = '\n'.join([ + '+----+------+--------+------------+-------------+----------+', + '| ID | Name | Status | Task State | Power State | Networks |', + '+----+------+--------+------------+-------------+----------+', + '+----+------+--------+------------+-------------+----------+', + '' + ]) self.make_env(exclude='OS_PASSWORD') self.register_keystone_discovery_fixture(m_requests) stdout, stderr = self.shell('list') From 059398398fb13675e1544b965680974131d8677d Mon Sep 17 00:00:00 2001 From: Ghanshyam Mann Date: Fri, 14 Oct 2022 15:08:58 -0500 Subject: [PATCH 1655/1705] Update python classifier for python 3.10 In 2023.1 cycle, we are testing the python 3.10 as voting job so updating the python classifier. Currently we have py3.8 unit test job running on focal and python 3.10 job on Jammy. Change-Id: I4ec8e9663ddf41aa5d3858446e4e382db041ce48 --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index 7982b6647..dafd50f78 100644 --- a/setup.cfg +++ b/setup.cfg @@ -20,6 +20,7 @@ classifier = Programming Language :: Python :: 3 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 Programming Language :: Python :: 3 :: Only Programming Language :: Python :: Implementation :: CPython From 80ee69aa30d36e941547a9b67668adfaf57355fd Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 1 Sep 2021 12:18:43 +0100 Subject: [PATCH 1656/1705] trivial: Remove use of kwargs Make use of a "sentinel" object to allow us to remove the use of kwargs and provide a more helpful docstring. With any luck, Python will support these objects natively in a future release [1]. [1] https://www.python.org/dev/peps/pep-0661/ Change-Id: I411c0393754c8fe8a6698f0d278b73f12209ace8 Signed-off-by: Stephen Finucane --- novaclient/v2/servers.py | 411 ++++++++++++++++++++++++++------------- novaclient/v2/shell.py | 13 +- 2 files changed, 278 insertions(+), 146 deletions(-) diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index 8a9301f6d..e564e818c 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -29,6 +29,7 @@ from novaclient import exceptions from novaclient.i18n import _ +_SENTINEL = object() REBOOT_SOFT, REBOOT_HARD = 'SOFT', 'HARD' @@ -404,34 +405,90 @@ def reboot(self, reboot_type=REBOOT_SOFT): """ return self.manager.reboot(self, reboot_type) - def rebuild(self, image, password=None, preserve_ephemeral=False, - **kwargs): + # NOTE(stephenfin): It would be nice to make everything bar image a + # kwarg-only argument but there are backwards-compatbility concerns + def rebuild( + self, + image, + password=None, + preserve_ephemeral=False, + *, + disk_config=None, + name=None, + meta=None, + files=None, + description=_SENTINEL, + key_name=_SENTINEL, + userdata=_SENTINEL, + trusted_image_certificates=_SENTINEL, + hostname=_SENTINEL, + ): """ Rebuild -- shut down and then re-image -- this server. - :param image: the :class:`Image` (or its ID) to re-image with. - :param password: string to set as the admin password on the rebuilt - server. + :param image: The :class:`Image` (or its ID) to re-image with. + :param password: String to set as password on the rebuilt server. :param preserve_ephemeral: If True, request that any ephemeral device be preserved when rebuilding the instance. Defaults to False. + :param disk_config: Partitioning mode to use on the rebuilt server. + Valid values are 'AUTO' or 'MANUAL' + :param name: Something to name the server. + :param meta: A dict of arbitrary key/value metadata to store for this + server. Both keys and values must be <=255 characters. + :param files: A dict of files to overwrite on the server upon boot. + Keys are file names (i.e. ``/etc/passwd``) and values are the file + contents (either as a string or as a file-like object). A maximum + of five entries is allowed, and each file must be 10k or less. + (deprecated starting with microversion 2.57) + :param description: Optional description of the server. If None is + specified, the existing description will be unset. + (starting from microversion 2.19) + :param key_name: Optional key pair name for rebuild operation. If None + is specified, the existing key will be unset. + (starting from microversion 2.54) + :param userdata: Optional user data to pass to be exposed by the + metadata server; this can be a file type object as well or a + string. If None is specified, the existing user_data is unset. + (starting from microversion 2.57) + :param trusted_image_certificates: A list of trusted certificate IDs + or None to unset/reset the servers trusted image certificates + (starting from microversion 2.63) + :param hostname: Optional hostname to configure for the instance. If + None is specified, the existing hostname will be unset. + (starting from microversion 2.90) + :returns: :class:`Server` """ - return self.manager.rebuild(self, image, password=password, - preserve_ephemeral=preserve_ephemeral, - **kwargs) + return self.manager.rebuild( + self, + image, + password=password, + disk_config=disk_config, + preserve_ephemeral=preserve_ephemeral, + name=name, + meta=meta, + files=files, + description=description, + key_name=key_name, + userdata=userdata, + trusted_image_certificates=trusted_image_certificates, + hostname=hostname, + ) - def resize(self, flavor, **kwargs): + def resize(self, flavor, *, disk_config=None): """ Resize the server's resources. - :param flavor: the :class:`Flavor` (or its ID) to resize to. + :param flavor: The :class:`Flavor` (or its ID) to resize to. + :param disk_config: Partitioning mode to use on the rebuilt server. + Valid values are 'AUTO' or 'MANUAL'. :returns: An instance of novaclient.base.TupleWithMeta Until a resize event is confirmed with :meth:`confirm_resize`, the old server will be kept around and you'll be able to roll back to the old flavor quickly with :meth:`revert_resize`. All resizes are - automatically confirmed after 24 hours. + automatically confirmed after 24 hours by default. """ - return self.manager.resize(self, flavor, **kwargs) + return self.manager.resize(self, flavor, disk_config=disk_config) def create_image(self, image_name, metadata=None): """ @@ -743,16 +800,38 @@ def transform_userdata(userdata): return base64.b64encode(userdata).decode('utf-8') - def _boot(self, response_key, name, image, flavor, - meta=None, files=None, userdata=None, - reservation_id=False, return_raw=False, min_count=None, - max_count=None, security_groups=None, key_name=None, - availability_zone=None, block_device_mapping=None, - block_device_mapping_v2=None, nics=None, scheduler_hints=None, - config_drive=None, admin_pass=None, disk_config=None, - access_ip_v4=None, access_ip_v6=None, description=None, - tags=None, trusted_image_certificates=None, - host=None, hypervisor_hostname=None, hostname=None, **kwargs): + def _boot( + self, + response_key, + name, + image, + flavor, + meta=None, + files=None, + userdata=None, + reservation_id=False, + return_raw=False, + min_count=None, + max_count=None, + security_groups=None, + key_name=None, + availability_zone=None, + block_device_mapping=None, + block_device_mapping_v2=None, + nics=None, + scheduler_hints=None, + config_drive=None, + admin_pass=None, + disk_config=None, + access_ip_v4=None, + access_ip_v6=None, + description=None, + tags=None, + trusted_image_certificates=None, + host=None, + hypervisor_hostname=None, + hostname=None, + ): """ Create (boot) a new server. """ @@ -884,8 +963,9 @@ def _boot(self, response_key, name, image, flavor, if hostname: body['server']['hostname'] = hostname - return self._create('/servers', body, response_key, - return_raw=return_raw, **kwargs) + return self._create( + '/servers', body, response_key, return_raw=return_raw, + ) def get(self, server): """ @@ -1390,17 +1470,39 @@ def _validate_create_nics(self, nics): raise ValueError('nics must be a list or a tuple, not %s' % type(nics)) - def create(self, name, image, flavor, meta=None, files=None, - reservation_id=False, min_count=None, - max_count=None, security_groups=None, userdata=None, - key_name=None, availability_zone=None, - block_device_mapping=None, block_device_mapping_v2=None, - nics=None, scheduler_hints=None, - config_drive=None, disk_config=None, admin_pass=None, - access_ip_v4=None, access_ip_v6=None, - trusted_image_certificates=None, - host=None, hypervisor_hostname=None, hostname=None, - **kwargs): + # NOTE(stephenfin): It would be nice to make everything bar name, image and + # flavor a kwarg-only argument but there are backwards-compatbility + # concerns + def create( + self, + name, + image, + flavor, + meta=None, + files=None, + reservation_id=False, + min_count=None, + max_count=None, + security_groups=None, + userdata=None, + key_name=None, + availability_zone=None, + block_device_mapping=None, + block_device_mapping_v2=None, + nics=None, + scheduler_hints=None, + config_drive=None, + disk_config=None, + admin_pass=None, + access_ip_v4=None, + access_ip_v6=None, + description=None, + tags=None, + trusted_image_certificates=None, + host=None, + hypervisor_hostname=None, + hostname=None, + ): """ Create (boot) a new server. @@ -1412,65 +1514,59 @@ def create(self, name, image, flavor, meta=None, files=None, :param image: The :class:`Image` to boot with. :param flavor: The :class:`Flavor` to boot onto. :param meta: A dict of arbitrary key/value metadata to store for this - server. Both keys and values must be <=255 characters. + server. Both keys and values must be <=255 characters. :param files: A dict of files to overwrite on the server upon boot. - Keys are file names (i.e. ``/etc/passwd``) and values - are the file contents (either as a string or as a - file-like object). A maximum of five entries is allowed, - and each file must be 10k or less. - (deprecated starting with microversion 2.57) - :param reservation_id: return a reservation_id for the set of - servers being requested, boolean. - :param min_count: (optional extension) The minimum number of - servers to launch. - :param max_count: (optional extension) The maximum number of - servers to launch. + Keys are file names (i.e. ``/etc/passwd``) and values + are the file contents (either as a string or as a + file-like object). A maximum of five entries is allowed, + and each file must be 10k or less. + (deprecated starting with microversion 2.57) + :param reservation_id: Return a reservation_id for the set of + servers being requested, boolean. + :param min_count: The minimum number of servers to launch. + :param max_count: The maximum number of servers to launch. :param security_groups: A list of security group names - :param userdata: user data to pass to be exposed by the metadata - server this can be a file type object as well or a - string. - :param key_name: (optional extension) name of previously created - keypair to inject into the instance. + :param userdata: User data to pass to be exposed by the metadata + server this can be a file type object as well or a string. + :param key_name: Name of previously created keypair to inject into the + instance. :param availability_zone: Name of the availability zone for instance - placement. - :param block_device_mapping: (optional extension) A dict of block - device mappings for this server. - :param block_device_mapping_v2: (optional extension) A list of block - device mappings (dicts) for this server. - :param nics: An ordered list of nics (dicts) to be added to this - server, with information about connected networks, - fixed IPs, port etc. - Beginning in microversion 2.37 this field is required and - also supports a single string value of 'auto' or 'none'. - The 'auto' value means the Compute service will - automatically allocate a network for the project if one - is not available. This is the same behavior as not - passing anything for nics before microversion 2.37. The - 'none' value tells the Compute service to not allocate - any networking for the server. - :param scheduler_hints: (optional extension) arbitrary key-value pairs - specified by the client to help boot an instance - :param config_drive: (optional extension) a boolean value to enable - config drive - :param disk_config: (optional extension) control how the disk is - partitioned when the server is created. possible - values are 'AUTO' or 'MANUAL'. - :param admin_pass: (optional extension) add a user supplied admin - password. - :param access_ip_v4: (optional extension) add alternative access ip v4 - :param access_ip_v6: (optional extension) add alternative access ip v6 - :param description: optional description of the server (allowed since - microversion 2.19) - :param tags: A list of arbitrary strings to be added to the - server as tags (allowed since microversion 2.52) + placement. + :param block_device_mapping: A dict of block device mappings for this + server. + :param block_device_mapping_v2: A list of block device mappings (dicts) + for this server. + :param nics: An ordered list of nics (dicts) to be added to this + server, with information about connected networks, fixed IPs, port + etc. Beginning in microversion 2.37 this field is required and also + supports a single string value of 'auto' or 'none'. The 'auto' + value means the Compute service will automatically allocate a + network for the project if one is not available. This is the same + behavior as not passing anything for nics before microversion 2.37. + The 'none' value tells the Compute service to not allocate any + networking for the server. + :param scheduler_hints: Arbitrary key-value pairs specified by the + client to help boot an instance. + :param config_drive: A boolean value to enable config drive. + :param disk_config: Control how the disk is partitioned when the server + is created. Possible values are 'AUTO' or 'MANUAL'. + :param admin_pass: Add a user supplied admin password. + :param access_ip_v4: Add alternative access IP (v4) + :param access_ip_v6: Add alternative access IP (v6) + :param description: Optional description of the server + (allowed since microversion 2.19) + :param tags: A list of arbitrary strings to be added to the server as + tags + (allowed since microversion 2.52) :param trusted_image_certificates: A list of trusted certificate IDs - (allowed since microversion 2.63) - :param host: requested host to create servers - (allowed since microversion 2.74) - :param hypervisor_hostname: requested hypervisor hostname to create - servers (allowed since microversion 2.74) - :param hostname: requested hostname of server (allowed since - microversion 2.90) + (allowed since microversion 2.63) + :param host: Requested host to create servers + (allowed since microversion 2.74) + :param hypervisor_hostname: Requested hypervisor hostname to create + servers + (allowed since microversion 2.74) + :param hostname: Requested hostname of server + (allowed since microversion 2.90) """ if not min_count: min_count = 1 @@ -1481,8 +1577,7 @@ def create(self, name, image, flavor, meta=None, files=None, boot_args = [name, image, flavor] - descr_microversion = api_versions.APIVersion("2.19") - if "description" in kwargs and self.api_version < descr_microversion: + if description and self.api_version < api_versions.APIVersion("2.19"): raise exceptions.UnsupportedAttribute("description", "2.19") self._validate_create_nics(nics) @@ -1503,8 +1598,7 @@ def create(self, name, image, flavor, meta=None, files=None, "unsupported before microversion " "2.32") - boot_tags_microversion = api_versions.APIVersion("2.52") - if "tags" in kwargs and self.api_version < boot_tags_microversion: + if tags and self.api_version < api_versions.APIVersion("2.52"): raise exceptions.UnsupportedAttribute("tags", "2.52") personality_files_deprecation = api_versions.APIVersion('2.57') @@ -1546,9 +1640,10 @@ def create(self, name, image, flavor, meta=None, files=None, scheduler_hints=scheduler_hints, config_drive=config_drive, disk_config=disk_config, admin_pass=admin_pass, access_ip_v4=access_ip_v4, access_ip_v6=access_ip_v6, + description=description, tags=tags, trusted_image_certificates=trusted_image_certificates, host=host, hypervisor_hostname=hypervisor_hostname, - hostname=hostname, **kwargs) + hostname=hostname) if block_device_mapping: boot_kwargs['block_device_mapping'] = block_device_mapping @@ -1665,10 +1760,25 @@ def reboot(self, server, reboot_type=REBOOT_SOFT): """ return self._action('reboot', server, {'type': reboot_type}) - # TODO(stephenfin): Expand out kwargs - def rebuild(self, server, image, password=None, disk_config=None, - preserve_ephemeral=False, name=None, meta=None, files=None, - **kwargs): + # NOTE(stephenfin): It would be nice to make everything bar server and + # image a kwarg-only argument but there are backwards-compatbility concerns + def rebuild( + self, + server, + image, + password=None, + disk_config=None, + preserve_ephemeral=False, + name=None, + meta=None, + files=None, + *, + description=_SENTINEL, + key_name=_SENTINEL, + userdata=_SENTINEL, + trusted_image_certificates=_SENTINEL, + hostname=_SENTINEL, + ): """ Rebuild -- shut down and then re-image -- a server. @@ -1705,63 +1815,80 @@ def rebuild(self, server, image, password=None, disk_config=None, (starting from microversion 2.90) :returns: :class:`Server` """ - descr_microversion = api_versions.APIVersion("2.19") - if "description" in kwargs and self.api_version < descr_microversion: - raise exceptions.UnsupportedAttribute("description", "2.19") + # Microversion 2.19 adds the optional 'description' parameter + if ( + description is not _SENTINEL and + self.api_version < api_versions.APIVersion('2.19') + ): + raise exceptions.UnsupportedAttribute('description', '2.19') - # Starting from microversion 2.54, - # the optional 'key_name' parameter has been added. - if ('key_name' in kwargs and - self.api_version < api_versions.APIVersion('2.54')): + # Microversion 2.54 adds the optional 'key_name' parameter + if ( + key_name is not _SENTINEL and + self.api_version < api_versions.APIVersion('2.54') + ): raise exceptions.UnsupportedAttribute('key_name', '2.54') # Microversion 2.57 deprecates personality files and adds support # for user_data. - files_and_userdata = api_versions.APIVersion('2.57') - if files and self.api_version >= files_and_userdata: + if files and self.api_version >= api_versions.APIVersion('2.57'): raise exceptions.UnsupportedAttribute('files', '2.0', '2.56') - if 'userdata' in kwargs and self.api_version < files_and_userdata: + + if ( + userdata is not _SENTINEL and + self.api_version < api_versions.APIVersion('2.57') + ): raise exceptions.UnsupportedAttribute('userdata', '2.57') - trusted_certs_microversion = api_versions.APIVersion("2.63") - # trusted_image_certificates is intentionally *not* a named kwarg - # so that trusted_image_certificates=None is not confused with an - # intentional unset/reset request. - if ("trusted_image_certificates" in kwargs and - self.api_version < trusted_certs_microversion): - raise exceptions.UnsupportedAttribute("trusted_image_certificates", - "2.63") + # Microversion 2.63 adds trusted image certificate support + if ( + trusted_image_certificates is not _SENTINEL and + self.api_version < api_versions.APIVersion('2.63') + ): + raise exceptions.UnsupportedAttribute( + 'trusted_image_certificates', '2.63') + # Microversion 2.90 adds the optional 'hostname' parameter if ( - 'hostname' in kwargs and - self.api_version < api_versions.APIVersion("2.90") + hostname is not _SENTINEL and + self.api_version < api_versions.APIVersion('2.90') ): raise exceptions.UnsupportedAttribute('hostname', '2.90') body = {'imageRef': base.getid(image)} + if password is not None: body['adminPass'] = password + if disk_config is not None: body['OS-DCF:diskConfig'] = disk_config + if preserve_ephemeral is not False: body['preserve_ephemeral'] = True + if name is not None: body['name'] = name - if "description" in kwargs: - body["description"] = kwargs["description"] - if 'key_name' in kwargs: - body['key_name'] = kwargs['key_name'] - if "trusted_image_certificates" in kwargs: - body["trusted_image_certificates"] = kwargs[ - "trusted_image_certificates"] - if "hostname" in kwargs: - body["hostname"] = kwargs["hostname"] + + if description is not _SENTINEL: + body["description"] = description + + if key_name is not _SENTINEL: + body['key_name'] = key_name + + if trusted_image_certificates is not _SENTINEL: + body["trusted_image_certificates"] = trusted_image_certificates + + if hostname is not _SENTINEL: + body["hostname"] = hostname + if meta: body['metadata'] = meta + if files: personality = body['personality'] = [] - for filepath, file_or_string in sorted(files.items(), - key=lambda x: x[0]): + for filepath, file_or_string in sorted( + files.items(), key=lambda x: x[0], + ): if hasattr(file_or_string, 'read'): data = file_or_string.read() else: @@ -1772,15 +1899,17 @@ def rebuild(self, server, image, password=None, disk_config=None, 'path': filepath, 'contents': cont, }) - if 'userdata' in kwargs: + + if userdata is not _SENTINEL: # If userdata is specified but None, it means unset the existing # user_data on the instance. - userdata = kwargs['userdata'] + userdata = userdata body['user_data'] = (userdata if userdata is None else self.transform_userdata(userdata)) - resp, body = self._action_return_resp_and_body('rebuild', server, - body, **kwargs) + resp, body = self._action_return_resp_and_body( + 'rebuild', server, body, + ) return Server(self, body['server'], resp=resp) @api_versions.wraps("2.0", "2.55") @@ -1809,26 +1938,28 @@ def migrate(self, server, host=None): return self._action('migrate', server, info) - def resize(self, server, flavor, disk_config=None, **kwargs): + # NOTE(stephenfin): It would be nice to make disk_config a kwarg-only + # argument but there are backwards-compatbility concerns + def resize(self, server, flavor, disk_config=None): """ Resize a server's resources. :param server: The :class:`Server` (or its ID) to share onto. - :param flavor: the :class:`Flavor` (or its ID) to resize to. - :param disk_config: partitioning mode to use on the rebuilt server. - Valid values are 'AUTO' or 'MANUAL' + :param flavor: The :class:`Flavor` (or its ID) to resize to. + :param disk_config: Partitioning mode to use on the rebuilt server. + Valid values are 'AUTO' or 'MANUAL'. :returns: An instance of novaclient.base.TupleWithMeta Until a resize event is confirmed with :meth:`confirm_resize`, the old server will be kept around and you'll be able to roll back to the old flavor quickly with :meth:`revert_resize`. All resizes are - automatically confirmed after 24 hours. + automatically confirmed after 24 hours by default. """ info = {'flavorRef': base.getid(flavor)} if disk_config is not None: info['OS-DCF:diskConfig'] = disk_config - return self._action('resize', server, info=info, **kwargs) + return self._action('resize', server, info=info) def confirm_resize(self, server): """ diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 58823fc70..0ca9e7942 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -2057,14 +2057,15 @@ def do_rebuild(cs, args): server = _find_server(cs, args.server) image = _find_image(cs, args.image) - if args.rebuild_password is not False: - _password = args.rebuild_password - else: - _password = None - kwargs = {'preserve_ephemeral': args.preserve_ephemeral, 'name': args.name, 'meta': _meta_parsing(args.meta)} + + if args.rebuild_password is not False: + kwargs['password'] = args.rebuild_password + else: + kwargs['password'] = None + if 'description' in args: kwargs['description'] = args.description @@ -2145,7 +2146,7 @@ def do_rebuild(cs, args): if 'hostname' in args and args.hostname is not None: kwargs['hostname'] = args.hostname - server = server.rebuild(image, _password, **kwargs) + server = server.rebuild(image, **kwargs) _print_server(cs, args, server) if args.poll: From 1d8a06da78ed16ed29cdc3b153d67ec1961b57bf Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 12 Dec 2022 17:28:12 +0000 Subject: [PATCH 1657/1705] tests: Fix Python 3.11 compatibility The argparse lib in Python 3.11 will not allow you to register a subparser more than once with the same name. We were inadvertently doing this in two of our unit tests as part of our check for version handling. There's no need for this. Stop doing it and simply create a new parser each time. An unnecessary check is removed from one of the tests since it confuses matters. Change-Id: I93827f84c456c9f6960e30e2424b67947254752c Signed-off-by: Stephen Finucane --- novaclient/tests/unit/test_shell.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/novaclient/tests/unit/test_shell.py b/novaclient/tests/unit/test_shell.py index a997b23ff..f46d0ad62 100644 --- a/novaclient/tests/unit/test_shell.py +++ b/novaclient/tests/unit/test_shell.py @@ -777,6 +777,8 @@ class MyException(Exception): class TestLoadVersionedActions(utils.TestCase): def test_load_versioned_actions(self): + # first load with API version 2.15, ensuring we use the 2.15 version of + # the underlying function (which returns 1) parser = novaclient.shell.NovaClientArgumentParser() subparsers = parser.add_subparsers(metavar='') shell = novaclient.shell.OpenStackComputeShell() @@ -787,6 +789,12 @@ def test_load_versioned_actions(self): self.assertEqual( 1, shell.subcommands['fake-action'].get_default('func')()) + # now load with API version 2.25, ensuring we now use the + # correspponding version of the underlying function (which now returns + # 2) + parser = novaclient.shell.NovaClientArgumentParser() + subparsers = parser.add_subparsers(metavar='') + shell = novaclient.shell.OpenStackComputeShell() shell.subcommands = {} shell._find_actions(subparsers, fake_actions_module, api_versions.APIVersion("2.25"), False) @@ -794,10 +802,6 @@ def test_load_versioned_actions(self): self.assertEqual( 2, shell.subcommands['fake-action'].get_default('func')()) - self.assertIn('fake-action2', shell.subcommands.keys()) - self.assertEqual( - 3, shell.subcommands['fake-action2'].get_default('func')()) - def test_load_versioned_actions_not_in_version_range(self): parser = novaclient.shell.NovaClientArgumentParser() subparsers = parser.add_subparsers(metavar='') @@ -908,6 +912,10 @@ def test_load_actions_with_versioned_args(self, mock_add_arg): mock_add_arg.reset_mock() + parser = novaclient.shell.NovaClientArgumentParser(add_help=False) + subparsers = parser.add_subparsers(metavar='') + shell = novaclient.shell.OpenStackComputeShell() + shell.subcommands = {} shell._find_actions(subparsers, fake_actions_module, api_versions.APIVersion("2.21"), False) self.assertNotIn(mock.call('--foo', help="first foo"), From c7cb02f1f7d508863c5e33f9d4b8e26b1a7ab6c0 Mon Sep 17 00:00:00 2001 From: Balazs Gibizer Date: Thu, 22 Dec 2022 16:11:29 +0100 Subject: [PATCH 1658/1705] Make tox.ini tox 4.0.0 compatible * removed skipsdist=True to make sure novaclient is available in the virtual env. The usedevelop and skipsdist does not work together any more https://github.com/tox-dev/tox/issues/2730. For bindep we still don't need the current repo to be installed in the env so skipsdist added there. Depends-On: https://review.opendev.org/c/zuul/zuul-jobs/+/866943 Change-Id: I979b91570c7b60273f35fbdf8464f6a9ee2007d6 --- tox.ini | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 0fbe142e7..f444646a0 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,6 @@ [tox] envlist = py3,pep8,docs minversion = 3.18.0 -skipsdist = true ignore_basepython_conflict = true [testenv] @@ -100,4 +99,6 @@ import_exceptions = novaclient.i18n # dependencies are missing! This also means that bindep must be installed # separately, outside of the requirements files. deps = bindep +skipsdist=True +usedevelop=False commands = bindep test From 85e4f08309490fa2ab6f0b581b3d645d2dbb5c4b Mon Sep 17 00:00:00 2001 From: Artom Lifshitz Date: Wed, 1 Feb 2023 10:08:52 -0500 Subject: [PATCH 1659/1705] Bump microversion to 2.95 There are no client-side changes for either 2.94 or 2.95, so just do the bump and add release notes. Change-Id: I8c2bfd48526840fc618820b9ae6a12dc98cdef45 --- novaclient/__init__.py | 2 +- novaclient/tests/unit/v2/test_shell.py | 2 ++ .../notes/microversion-v2_94-5368d5dd7c5f6484.yaml | 11 +++++++++++ .../notes/microversion-v2_95-3c6ad46be2656684.yaml | 10 ++++++++++ 4 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/microversion-v2_94-5368d5dd7c5f6484.yaml create mode 100644 releasenotes/notes/microversion-v2_95-3c6ad46be2656684.yaml diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 2a89e3385..de4417d63 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.93") +API_MAX_VERSION = api_versions.APIVersion("2.95") diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 84f4fcdc3..52738632e 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -4700,6 +4700,8 @@ def test_versions(self): 87, # doesn't require any changes in novaclient. 89, # There are no version-wrapped shell method changes for this. 93, # There are no version-wrapped shell method changes for this. + 94, # There are no version-wrapped shell method changes for this. + 95, # There are no version-wrapped shell method changes for this. ]) versions_supported = set(range(0, novaclient.API_MAX_VERSION.ver_minor + 1)) diff --git a/releasenotes/notes/microversion-v2_94-5368d5dd7c5f6484.yaml b/releasenotes/notes/microversion-v2_94-5368d5dd7c5f6484.yaml new file mode 100644 index 000000000..587969dd7 --- /dev/null +++ b/releasenotes/notes/microversion-v2_94-5368d5dd7c5f6484.yaml @@ -0,0 +1,11 @@ +--- +features: + - | + Added support for `microversion 2.94`_. There are no client-side changes + for this microversion, but sending this microversion allows the + ``hostname`` parameter in the server create, server update, and server + rebuild APIs to be a fully qualified domain name (FQDN). Prior to this + microversion, server-side validation only allows short names as the + ``hostname``. + + .. _microversion 2.94: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id83 diff --git a/releasenotes/notes/microversion-v2_95-3c6ad46be2656684.yaml b/releasenotes/notes/microversion-v2_95-3c6ad46be2656684.yaml new file mode 100644 index 000000000..3452f926f --- /dev/null +++ b/releasenotes/notes/microversion-v2_95-3c6ad46be2656684.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + Added support for `microversion 2.95`_. There are no client-side changes + for this microversion, but sending this microversion triggers evacuated + instances to be stopped on the destination host. Prior to this + microversion, instances were keeping their state from source to + destination host. + + .. _microversion 2.95: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id84 From 67caa10401cd5112f9e3876a6f8dd7d16d69ef6b Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Fri, 24 Feb 2023 15:09:23 +0000 Subject: [PATCH 1660/1705] Update master for stable/2023.1 Add file to the reno documentation build to show release notes for stable/2023.1. Use pbr instruction to increment the minor version number automatically so that master versions are higher than the versions on stable/2023.1. Sem-Ver: feature Change-Id: Iac2e628333518455eef637f626ce6a3b54057afd --- releasenotes/source/2023.1.rst | 6 ++++++ releasenotes/source/index.rst | 1 + 2 files changed, 7 insertions(+) create mode 100644 releasenotes/source/2023.1.rst diff --git a/releasenotes/source/2023.1.rst b/releasenotes/source/2023.1.rst new file mode 100644 index 000000000..d1238479b --- /dev/null +++ b/releasenotes/source/2023.1.rst @@ -0,0 +1,6 @@ +=========================== +2023.1 Series Release Notes +=========================== + +.. release-notes:: + :branch: stable/2023.1 diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index 839eff57c..bac7bc881 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -8,6 +8,7 @@ Contents :maxdepth: 2 unreleased + 2023.1 zed yoga xena From e464940f39d1643128c9cef0b92e02c3d0e754c7 Mon Sep 17 00:00:00 2001 From: David Wlazlo Date: Wed, 17 May 2023 11:41:41 +1000 Subject: [PATCH 1661/1705] Typo - nova CLI deprecation warning Change-Id: I1e604867bd00d51caebcb049777bbc675a398969 --- novaclient/shell.py | 2 +- novaclient/tests/unit/test_shell.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/novaclient/shell.py b/novaclient/shell.py index 468e889b1..adb8b4dcb 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -824,7 +824,7 @@ def main(argv=sys.argv[1:]): if os.environ.get("NOVACLIENT_ISHOULDNTBEDOINGTHIS") != "1": print( _( - "nova CLI is deprecated and will be a removed in a future " + "nova CLI is deprecated and will be removed in a future " "release" ), file=sys.stderr, diff --git a/novaclient/tests/unit/test_shell.py b/novaclient/tests/unit/test_shell.py index f46d0ad62..415440751 100644 --- a/novaclient/tests/unit/test_shell.py +++ b/novaclient/tests/unit/test_shell.py @@ -631,7 +631,7 @@ def test_main_noargs(self): ) # We also expect to see the deprecation warning self.assertIn( - 'nova CLI is deprecated and will be a removed in a future release', + 'nova CLI is deprecated and will be removed in a future release', sys.stderr.getvalue(), ) @@ -769,7 +769,7 @@ class MyException(Exception): self.assertIn('ERROR (MyException): message\n', err) # We also expect to see the deprecation warning self.assertIn( - 'nova CLI is deprecated and will be a removed in a future release', + 'nova CLI is deprecated and will be removed in a future release', err, ) From dc2cb6cdd5062d398e187351e835fb51177e3cdf Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Thu, 7 Sep 2023 09:37:25 +0000 Subject: [PATCH 1662/1705] Update master for stable/2023.2 Add file to the reno documentation build to show release notes for stable/2023.2. Use pbr instruction to increment the minor version number automatically so that master versions are higher than the versions on stable/2023.2. Sem-Ver: feature Change-Id: I7b70843227db7e74151f8483485cf9995724438f --- releasenotes/source/2023.2.rst | 6 ++++++ releasenotes/source/index.rst | 1 + 2 files changed, 7 insertions(+) create mode 100644 releasenotes/source/2023.2.rst diff --git a/releasenotes/source/2023.2.rst b/releasenotes/source/2023.2.rst new file mode 100644 index 000000000..a4838d7d0 --- /dev/null +++ b/releasenotes/source/2023.2.rst @@ -0,0 +1,6 @@ +=========================== +2023.2 Series Release Notes +=========================== + +.. release-notes:: + :branch: stable/2023.2 diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index bac7bc881..b81f22137 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -8,6 +8,7 @@ Contents :maxdepth: 2 unreleased + 2023.2 2023.1 zed yoga From 1024736eeb60380170dbf495684794d8e1936f7e Mon Sep 17 00:00:00 2001 From: Sean Mooney Date: Thu, 2 Nov 2023 11:16:49 +0000 Subject: [PATCH 1663/1705] add pyproject.toml to support pip 23.1 pip 23.1 removed the "setup.py install" fallback for projects that do not have pyproject.toml and now uses a pyproject.toml which is vendored in pip. To address that, this change adds the minimal pyproject.toml to enable pbr to be properly used to build editable wheels. This is required to support installing devstack on centos stream 9 and related distros with GLOBAL_VENV=True Without this change the wsgi scripts are not generated in editable mode. i.e. pip install -e /opt/stack/keystone See https://pip.pypa.io/en/stable/news/#v23-1 and https://github.com/pypa/pip/issues/8368 for more details on the removal of the fallback support. setuptools v64.0.0 is used to support editable installs via its PEP-660 implmentation https://github.com/pypa/setuptools/pull/3488 Change-Id: Ieea0ac142e79a9de4d2fbf45fdad70d0ff079304 --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 pyproject.toml diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..4f536f6aa --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["pbr>=5.7.0", "setuptools>=64.0.0", "wheel"] +build-backend = "pbr.build" From 1f029ce57805daa5c9b9ba98f9790dc776932b08 Mon Sep 17 00:00:00 2001 From: Rajesh Tailor Date: Tue, 20 Sep 2022 21:29:12 +0530 Subject: [PATCH 1664/1705] Fix typos Change-Id: Iaef3767dd9e7f43503dd8287b9b2345585bd87fb --- .../tests/functional/v2/legacy/test_readonly_nova.py | 4 ++-- .../tests/functional/v2/test_instance_usage_audit_log.py | 2 +- novaclient/tests/functional/v2/test_quota_classes.py | 8 ++++---- novaclient/tests/unit/v2/fakes.py | 2 +- novaclient/v2/agents.py | 2 +- novaclient/v2/servers.py | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/novaclient/tests/functional/v2/legacy/test_readonly_nova.py b/novaclient/tests/functional/v2/legacy/test_readonly_nova.py index 57d4107a1..7dffba414 100644 --- a/novaclient/tests/functional/v2/legacy/test_readonly_nova.py +++ b/novaclient/tests/functional/v2/legacy/test_readonly_nova.py @@ -38,7 +38,7 @@ def test_admin_aggregate_list(self): def test_admin_availability_zone_list(self): self.assertIn("internal", self.nova('availability-zone-list')) - def test_admin_flavor_acces_list(self): + def test_admin_flavor_access_list(self): self.assertRaises(exceptions.CommandFailed, self.nova, 'flavor-access-list') @@ -73,7 +73,7 @@ def test_admin_list(self): def test_admin_server_group_list(self): self.nova('server-group-list') - def test_admin_servce_list(self): + def test_admin_service_list(self): self.nova('service-list') def test_admin_usage(self): diff --git a/novaclient/tests/functional/v2/test_instance_usage_audit_log.py b/novaclient/tests/functional/v2/test_instance_usage_audit_log.py index 5e7ce8892..9a57710cf 100644 --- a/novaclient/tests/functional/v2/test_instance_usage_audit_log.py +++ b/novaclient/tests/functional/v2/test_instance_usage_audit_log.py @@ -21,7 +21,7 @@ class TestInstanceUsageAuditLogCLI(base.ClientTestBase): COMPUTE_API_VERSION = '2.1' # NOTE(takashin): By default, 'instance_usage_audit' is False in nova. - # So the instance usage audit log is not recoreded. + # So the instance usage audit log is not recorded. # Therefore an empty result can be got. # But it is tested here to call APIs and get responses normally. diff --git a/novaclient/tests/functional/v2/test_quota_classes.py b/novaclient/tests/functional/v2/test_quota_classes.py index 399f53968..837759f7a 100644 --- a/novaclient/tests/functional/v2/test_quota_classes.py +++ b/novaclient/tests/functional/v2/test_quota_classes.py @@ -44,7 +44,7 @@ def _get_quota_class_name(self): """Returns a fake quota class name specific to this test class.""" return 'fake-class-%s' % self.COMPUTE_API_VERSION.replace('.', '-') - def _verify_qouta_class_show_output(self, output, expected_values): + def _verify_quota_class_show_output(self, output, expected_values): # Assert that the expected key/value pairs are in the output table for quota_name in self._included_resources: # First make sure the resource is actually in expected quota. @@ -74,7 +74,7 @@ def test_quota_class_show(self): } output = self.nova('quota-class-show %s' % self._get_quota_class_name()) - self._verify_qouta_class_show_output(output, default_values) + self._verify_quota_class_show_output(output, default_values) def test_quota_class_update(self): """Tests updating a fake quota class. The way this works in the API @@ -96,7 +96,7 @@ def test_quota_class_update(self): self.nova("quota-class-update", params=" ".join(params)) # Assert the results using the quota-class-show output. output = self.nova('quota-class-show %s' % class_name) - self._verify_qouta_class_show_output(output, expected_values) + self._verify_quota_class_show_output(output, expected_values) # Assert that attempting to update resources that are blocked will # result in a failure. @@ -115,7 +115,7 @@ class TestQuotasNovaClient2_50(TestQuotaClassesNovaClient): # The 2.50 microversion added the server_groups and server_group_members # to the response, and filtered out floating_ips, fixed_ips, # security_groups and security_group_members, similar to the 2.36 - # microversion in the os-qouta-sets API. + # microversion in the os-quota-sets API. _included_resources = ['instances', 'cores', 'ram', 'metadata_items', 'injected_files', 'injected_file_content_bytes', 'injected_file_path_bytes', 'key_pairs', diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 059a7147f..f3e6d9908 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -1218,7 +1218,7 @@ def get_os_keypairs(self, user_id=None, limit=None, marker=None, *kw): "deleted": False, "created_at": "2014-04-19T02:16:44.000000", "updated_at": "2014-04-19T10:12:3.000000", - "figerprint": "FAKE_KEYPAIR", + "fingerprint": "FAKE_KEYPAIR", "deleted_at": None, "id": 4}} ]}) diff --git a/novaclient/v2/agents.py b/novaclient/v2/agents.py index d71bf1943..dac3ff214 100644 --- a/novaclient/v2/agents.py +++ b/novaclient/v2/agents.py @@ -21,7 +21,7 @@ # NOTE(takashin): The os-agents APIs have been removed # in https://review.opendev.org/c/openstack/nova/+/749309 . -# But the following API bindings remains as ther are +# But the following API bindings remains as there are # because the python-openstackclient depends on them. diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index e564e818c..37f55316a 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -1429,7 +1429,7 @@ def ips(self, server): Often a cheaper call then getting all the details for a server. :param server: The :class:`Server` (or its ID) for which - the IP adresses are to be returned + the IP addresses are to be returned :returns: An instance of novaclient.base.DictWithMeta """ resp, body = self.api.client.get("/servers/%s/ips" % From ed2a507b60bff511d26ec504db8a9992f94d80ab Mon Sep 17 00:00:00 2001 From: Andrey Kurilin Date: Tue, 21 Nov 2023 20:37:46 +0100 Subject: [PATCH 1665/1705] Disable NEUTRON_ENFORCE_SCOPE at function job The following devstack change I3361d33885b2e3af7cad0141f9b799b2723ee8a1 may be a root cause for failing functional job. This commit should verify this. Change-Id: Ica272a5ce5d20dcb52e8a636849af2d71e15afb2 --- .zuul.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.zuul.yaml b/.zuul.yaml index 5dfc1c5a1..8abe5d285 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -9,6 +9,7 @@ openrc_enable_export: true devstack_localrc: KEYSTONE_ADMIN_ENDPOINT: true + NEUTRON_ENFORCE_SCOPE: false irrelevant-files: - ^.*\.rst$ - ^doc/.*$ From 338ce361491dfa5c4589205983a84ec81fb58669 Mon Sep 17 00:00:00 2001 From: Takashi Kajinami Date: Tue, 19 Dec 2023 20:56:59 +0900 Subject: [PATCH 1666/1705] coveragerc: Fix wrong omitted directory We should exclude the test code from coverage. Also the openstack directory does not exist. Change-Id: I89a25629f0426843dd72dd0d663942323473ac47 --- .coveragerc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.coveragerc b/.coveragerc index 933b5e5ac..b474b73c7 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,7 +1,7 @@ [run] branch = True source = novaclient -omit = novaclient/openstack/* +omit = novaclient/tests/* [report] ignore_errors = True From cd7a79e965c5f8f699e0870e9f6f2a5423fc488b Mon Sep 17 00:00:00 2001 From: Ghanshyam Mann Date: Wed, 3 Jan 2024 19:21:05 -0800 Subject: [PATCH 1667/1705] Update python classifier in setup.cfg As per the current release tested runtime, we test till python 3.11 so updating the same in python classifier in setup.cfg Change-Id: Ib4658c25e7f20a0e4bc8fd01e8ebacfb4a27b3e2 --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index dafd50f78..d017074b2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -21,6 +21,7 @@ classifier = Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 + Programming Language :: Python :: 3.11 Programming Language :: Python :: 3 :: Only Programming Language :: Python :: Implementation :: CPython From e059ab8a89f77fe873c7f254a897e3b94b60dccc Mon Sep 17 00:00:00 2001 From: Takashi Kajinami Date: Wed, 17 Jan 2024 01:29:39 +0900 Subject: [PATCH 1668/1705] Bump hacking hacking 4.0.x is very old (it was released 3 years ago). Change-Id: Ia82da2421db271a97603930c6eef50e93cea77c5 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 5845d2d50..7d4278a80 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,4 +1,4 @@ -hacking~=4.1.0 # Apache-2.0 +hacking>=6.1.0,<6.2.0 # Apache-2.0 bandit>=1.1.0 # Apache-2.0 coverage!=4.4,>=4.0 # Apache-2.0 ddt>=1.0.1 # MIT From 693edd2a01b39cc381028c33098a24b0b710222f Mon Sep 17 00:00:00 2001 From: Takashi Kajinami Date: Sat, 27 Jan 2024 23:03:00 +0900 Subject: [PATCH 1669/1705] Update hacking version in pre commit config This was overlooked when hacking was bumped in test-requirements. Change-Id: I96bf83f36d6b623ee509e9fa7b2770bc2d026965 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e782b53e0..c539fabb0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: - id: flake8 name: flake8 additional_dependencies: - - hacking~=4.1.0 + - hacking>=6.1.0,<6.2.0 language: python entry: flake8 files: '^.*\.py$' From a23deccf3186d2c579bdabfc441d21d34cc1666d Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Tue, 6 Feb 2024 14:27:58 +0000 Subject: [PATCH 1670/1705] reno: Update master for unmaintained/yoga Update the yoga release notes configuration to build from unmaintained/yoga. Change-Id: Idfcb4d2bd9a365019adcd1c227c86d8f69dc47f6 --- releasenotes/source/yoga.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/releasenotes/source/yoga.rst b/releasenotes/source/yoga.rst index 7cd5e908a..43cafdea8 100644 --- a/releasenotes/source/yoga.rst +++ b/releasenotes/source/yoga.rst @@ -3,4 +3,4 @@ Yoga Series Release Notes ========================= .. release-notes:: - :branch: stable/yoga + :branch: unmaintained/yoga From 59a147ab23973ee0dc167880a3246356c5404c9b Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Tue, 5 Mar 2024 18:48:46 +0000 Subject: [PATCH 1671/1705] reno: Update master for unmaintained/victoria Update the victoria release notes configuration to build from unmaintained/victoria. Change-Id: I656c372feb90f642ec5e7ad44a7e4134ee723e71 --- releasenotes/source/victoria.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/releasenotes/source/victoria.rst b/releasenotes/source/victoria.rst index 4efc7b6f3..8ce933419 100644 --- a/releasenotes/source/victoria.rst +++ b/releasenotes/source/victoria.rst @@ -3,4 +3,4 @@ Victoria Series Release Notes ============================= .. release-notes:: - :branch: stable/victoria + :branch: unmaintained/victoria From f2d51a0f1269e0af6f942bcfd4d443b3d8f4a5df Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Tue, 5 Mar 2024 18:50:43 +0000 Subject: [PATCH 1672/1705] reno: Update master for unmaintained/wallaby Update the wallaby release notes configuration to build from unmaintained/wallaby. Change-Id: I9f980872108c61d7080860fa2c48b190316933e2 --- releasenotes/source/wallaby.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/releasenotes/source/wallaby.rst b/releasenotes/source/wallaby.rst index d77b56599..bcf35c5f8 100644 --- a/releasenotes/source/wallaby.rst +++ b/releasenotes/source/wallaby.rst @@ -3,4 +3,4 @@ Wallaby Series Release Notes ============================ .. release-notes:: - :branch: stable/wallaby + :branch: unmaintained/wallaby From 99bbecfc8fcb17e7a924aacd3cf82060721cb631 Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Tue, 5 Mar 2024 18:52:19 +0000 Subject: [PATCH 1673/1705] reno: Update master for unmaintained/xena Update the xena release notes configuration to build from unmaintained/xena. Change-Id: I3a92127b6389c6c37625cbf696b33c0127cb0685 --- releasenotes/source/xena.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/releasenotes/source/xena.rst b/releasenotes/source/xena.rst index 1be85be3e..d19eda488 100644 --- a/releasenotes/source/xena.rst +++ b/releasenotes/source/xena.rst @@ -3,4 +3,4 @@ Xena Series Release Notes ========================= .. release-notes:: - :branch: stable/xena + :branch: unmaintained/xena From 533d5df5168123578da5eb5380efb64096aa3c43 Mon Sep 17 00:00:00 2001 From: Rajesh Tailor Date: Wed, 6 Mar 2024 18:25:47 +0530 Subject: [PATCH 1674/1705] Bump microversion to 2.96 This change bumps to microversion 2.96 and add release notes. Change-Id: I3b9775f1adecdca121a68a4caa6cd25e312dec2f --- novaclient/__init__.py | 2 +- novaclient/tests/unit/v2/test_shell.py | 1 + .../notes/microversion-v2_96-a50af976133de0ab.yaml | 8 ++++++++ 3 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/microversion-v2_96-a50af976133de0ab.yaml diff --git a/novaclient/__init__.py b/novaclient/__init__.py index de4417d63..bf021c6ac 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.95") +API_MAX_VERSION = api_versions.APIVersion("2.96") diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 52738632e..3f7bc3960 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -4702,6 +4702,7 @@ def test_versions(self): 93, # There are no version-wrapped shell method changes for this. 94, # There are no version-wrapped shell method changes for this. 95, # There are no version-wrapped shell method changes for this. + 96, # There are no version-wrapped shell method changes for this. ]) versions_supported = set(range(0, novaclient.API_MAX_VERSION.ver_minor + 1)) diff --git a/releasenotes/notes/microversion-v2_96-a50af976133de0ab.yaml b/releasenotes/notes/microversion-v2_96-a50af976133de0ab.yaml new file mode 100644 index 000000000..510548720 --- /dev/null +++ b/releasenotes/notes/microversion-v2_96-a50af976133de0ab.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + The `microversion 2.96`_ has been added. This microversion adds + pinned_availability_zone in ``server show`` and + ``server list --long`` responses. + + .. _microversion 2.96: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#microversion-2-96 From 3ffc5e5bf44c7beeb2835e700bc787d7a2d59b65 Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Tue, 12 Mar 2024 18:15:33 +0000 Subject: [PATCH 1675/1705] Update master for stable/2024.1 Add file to the reno documentation build to show release notes for stable/2024.1. Use pbr instruction to increment the minor version number automatically so that master versions are higher than the versions on stable/2024.1. Sem-Ver: feature Change-Id: I5804821f5095072fea32bf2787cfff35133e8fd9 --- releasenotes/source/2024.1.rst | 6 ++++++ releasenotes/source/index.rst | 1 + 2 files changed, 7 insertions(+) create mode 100644 releasenotes/source/2024.1.rst diff --git a/releasenotes/source/2024.1.rst b/releasenotes/source/2024.1.rst new file mode 100644 index 000000000..4977a4f1a --- /dev/null +++ b/releasenotes/source/2024.1.rst @@ -0,0 +1,6 @@ +=========================== +2024.1 Series Release Notes +=========================== + +.. release-notes:: + :branch: stable/2024.1 diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index b81f22137..cfe6d23a8 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -8,6 +8,7 @@ Contents :maxdepth: 2 unreleased + 2024.1 2023.2 2023.1 zed From ec292ac6d139359de01eb0fdf96ea4d4c6154f2d Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Tue, 30 Apr 2024 15:02:45 +0000 Subject: [PATCH 1676/1705] reno: Update master for unmaintained/zed Update the zed release notes configuration to build from unmaintained/zed. Change-Id: I84a6101f132182948052c6d904d73df55a656cc8 --- releasenotes/source/zed.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/releasenotes/source/zed.rst b/releasenotes/source/zed.rst index 9608c05e4..6cc2b1554 100644 --- a/releasenotes/source/zed.rst +++ b/releasenotes/source/zed.rst @@ -3,4 +3,4 @@ Zed Series Release Notes ======================== .. release-notes:: - :branch: stable/zed + :branch: unmaintained/zed From 2bd135c13793da48af5fbad5208b00aaba7e39dc Mon Sep 17 00:00:00 2001 From: Takashi Kajinami Date: Tue, 30 Apr 2024 21:44:18 +0900 Subject: [PATCH 1677/1705] Remove old excludes These are detected as errors since the clean up was done[1] in the requirements repository. [1] 314734e938f107cbd5ebcc7af4d9167c11347406 Bump the minimum versions to avoid installing these known bad versions. Change-Id: I3536dbbbec41d144c9f622c9ea3433917ac66c63 --- doc/requirements.txt | 5 +---- requirements.txt | 4 ++-- test-requirements.txt | 4 ++-- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index b5cc23e6f..87e359a55 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,7 +1,4 @@ -# 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. -sphinx>=2.0.0,!=2.1.0 # BSD +sphinx>=2.1.1 # BSD openstackdocstheme>=2.2.0 # Apache-2.0 reno>=3.1.0 # Apache-2.0 sphinxcontrib-apidoc>=0.2.0 # BSD diff --git a/requirements.txt b/requirements.txt index 2eec1a70d..88c0e2b6c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,11 +2,11 @@ # 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. -pbr!=2.1.0,>=2.0.0 # Apache-2.0 +pbr>=3.0.0 # Apache-2.0 keystoneauth1>=3.5.0 # Apache-2.0 iso8601>=0.1.11 # MIT oslo.i18n>=3.15.3 # Apache-2.0 -oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0 +oslo.serialization>=2.20.0 # Apache-2.0 oslo.utils>=3.33.0 # Apache-2.0 PrettyTable>=0.7.2 # BSD stevedore>=2.0.1 # Apache-2.0 diff --git a/test-requirements.txt b/test-requirements.txt index 7d4278a80..0c56a31a6 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,10 +1,10 @@ hacking>=6.1.0,<6.2.0 # Apache-2.0 bandit>=1.1.0 # Apache-2.0 -coverage!=4.4,>=4.0 # Apache-2.0 +coverage>=4.4.1 # Apache-2.0 ddt>=1.0.1 # MIT fixtures>=3.0.0 # Apache-2.0/BSD python-keystoneclient>=3.8.0 # Apache-2.0 -python-cinderclient!=4.0.0,>=3.3.0 # Apache-2.0 +python-cinderclient>=4.0.1 # Apache-2.0 python-glanceclient>=2.8.0 # Apache-2.0 python-neutronclient>=6.7.0 # Apache-2.0 requests-mock>=1.2.0 # Apache-2.0 From 8f8f5ab49d440254d17d5d9de50ea2db8465cd20 Mon Sep 17 00:00:00 2001 From: Kien Nguyen Date: Tue, 16 Apr 2024 16:20:04 +0700 Subject: [PATCH 1678/1705] support get and list server's metadata Adds support for Nova Server get and list metadata API. Change-Id: I4e53c6af932537004564371374bb54396f0cc05c --- novaclient/tests/unit/fixture_data/servers.py | 13 ++++++++++++ novaclient/tests/unit/v2/test_servers.py | 17 ++++++++++++++++ novaclient/v2/servers.py | 20 +++++++++++++++++++ .../get-list-metadata-8afcc8f32ad82dda.yaml | 4 ++++ 4 files changed, 54 insertions(+) create mode 100644 releasenotes/notes/get-list-metadata-8afcc8f32ad82dda.yaml diff --git a/novaclient/tests/unit/fixture_data/servers.py b/novaclient/tests/unit/fixture_data/servers.py index 8e53ecdb2..1635e33dc 100644 --- a/novaclient/tests/unit/fixture_data/servers.py +++ b/novaclient/tests/unit/fixture_data/servers.py @@ -205,6 +205,19 @@ def setUp(self): self.requests_mock.delete(self.url(u, 'metadata', 'key1'), json=self.diagnostic, headers=self.json_headers) + metadata3 = {'meta': { + 'Server Label': 'Web Head 1' + }} + self.requests_mock.get(self.url(1234, 'metadata', 'Server Label'), + json=metadata3, + headers=self.json_headers) + metadata4 = {'metadata': { + 'Server Label': 'Web Head 1', + 'Image Version': '2.1' + }} + self.requests_mock.get(self.url(1234, 'metadata'), + json=metadata4, + headers=self.json_headers) get_security_groups = { "security_groups": [{ diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index 93fefabfe..36eded7a8 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -463,6 +463,23 @@ def test_set_server_meta_item(self): self.assert_called('PUT', '/servers/1234/metadata/test_key', {'meta': {'test_key': 'test_value'}}) + def test_get_server_meta(self): + m = self.cs.servers.get_meta(1234, 'Server Label') + self.assert_request_id(m, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('GET', '/servers/1234/metadata/Server%20Label') + self.assertEqual(m, {'meta': { + 'Server Label': 'Web Head 1' + }}) + + def test_list_server_meta(self): + m = self.cs.servers.list_meta(1234) + self.assert_request_id(m, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('GET', '/servers/1234/metadata') + self.assertEqual(m, {'metadata': { + 'Server Label': 'Web Head 1', + 'Image Version': '2.1' + }}) + def test_find(self): server = self.cs.servers.find(name='sample-server') self.assert_request_id(server, fakes.FAKE_REQUEST_ID_LIST) diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index 37f55316a..81c702dfb 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -2039,6 +2039,26 @@ def set_meta_item(self, server, key, value): return self._update("/servers/%s/metadata/%s" % (base.getid(server), key), body) + def list_meta(self, server): + """ + Lists all metadata for a server. + :param server: The :class:`Server` (or its ID). + :returns: A dict of metadata. + """ + resp, body = self.api.client.get("/servers/%s/metadata" % + base.getid(server)) + return base.DictWithMeta(body, resp) + + def get_meta(self, server, key): + """ + Shows details for a metadata item, by key, for a server. + :param server: The :class:`Server` (or its ID). + :param key: metadata key to get + """ + resp, body = self.api.client.get("/servers/%s/metadata/%s" % + (base.getid(server), key)) + return base.DictWithMeta(body, resp) + def get_console_output(self, server, length=None): """ Get text console log output from Server. diff --git a/releasenotes/notes/get-list-metadata-8afcc8f32ad82dda.yaml b/releasenotes/notes/get-list-metadata-8afcc8f32ad82dda.yaml new file mode 100644 index 000000000..c67cd18a6 --- /dev/null +++ b/releasenotes/notes/get-list-metadata-8afcc8f32ad82dda.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Adds supports for Nova Server get and list metadata API. From 3e63fb9dd9aaa360d1a30090c3429b590de09ec8 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 27 Aug 2024 11:06:47 +0100 Subject: [PATCH 1679/1705] functional: Handle multiple networks The 'TestServersBootNovaClient.test_boot_server_using_image_with' tests in 'novaclient.tests.functional.v2.legacy.test_servers' was failing due to multiple networks errors. Something has changed here, but I don't know what and I haven't bisected to figure it out. However, this test was the only one that wasn't respecting the 'multiple_networks' attribute we set to detect exactly this issue. Change that, fixing the test in the process. Closes-bug: #2077168 Change-Id: If8384aaf138559fe83ba4485049f9a7b45a44e12 Signed-off-by: Stephen Finucane --- .../functional/v2/legacy/test_servers.py | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/novaclient/tests/functional/v2/legacy/test_servers.py b/novaclient/tests/functional/v2/legacy/test_servers.py index aa812bb20..181f59f33 100644 --- a/novaclient/tests/functional/v2/legacy/test_servers.py +++ b/novaclient/tests/functional/v2/legacy/test_servers.py @@ -89,11 +89,15 @@ def test_boot_server_using_image_with(self): 3. Create a second server using the --image-with option using the meta key stored in the snapshot image created in step 2. """ - # create the first server and wait for it to be active - server_info = self.nova('boot', params=( + params = ( '--flavor %(flavor)s --image %(image)s --poll ' 'image-with-server-1' % {'image': self.image.id, - 'flavor': self.flavor.id})) + 'flavor': self.flavor.id}) + # check to see if we have to pass in a network id + if self.multiple_networks: + params += ' --nic net-id=%s' % self.network.id + # create the first server and wait for it to be active + server_info = self.nova('boot', params=params) server_id = self._get_value_from_the_table(server_info, 'id') self.addCleanup(self._cleanup_server, server_id) @@ -114,11 +118,15 @@ def test_boot_server_using_image_with(self): snapshot_info, 'image_with_meta') self.assertEqual(server_id, meta_value) - # create the second server using --image-with - server_info = self.nova('boot', params=( + params = ( '--flavor %(flavor)s --image-with image_with_meta=%(meta_value)s ' '--poll image-with-server-2' % {'meta_value': server_id, - 'flavor': self.flavor.id})) + 'flavor': self.flavor.id}) + # check to see if we have to pass in a network id + if self.multiple_networks: + params += ' --nic net-id=%s' % self.network.id + # create the second server using --image-with + server_info = self.nova('boot', params=params) server_id = self._get_value_from_the_table(server_info, 'id') self.addCleanup(self._cleanup_server, server_id) From 43e6dbaf6753f9e27df5abf241a2053679876195 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 27 Aug 2024 10:53:06 +0100 Subject: [PATCH 1680/1705] tox: Add Python-specific functional envs Change-Id: I87815faa6139eb3b09f21b917b8250ac0619a979 Signed-off-by: Stephen Finucane --- novaclient/tests/functional/base.py | 2 +- tox.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/novaclient/tests/functional/base.py b/novaclient/tests/functional/base.py index be4c5d74f..58403a078 100644 --- a/novaclient/tests/functional/base.py +++ b/novaclient/tests/functional/base.py @@ -258,7 +258,7 @@ def setUp(self): # something more sensible. cli_dir = os.environ.get( 'OS_NOVACLIENT_EXEC_DIR', - os.path.join(os.path.abspath('.'), '.tox/functional/bin')) + os.path.join(os.environ['TOX_ENV_DIR'], 'bin')) self.cli_clients = tempest.lib.cli.base.CLIClient( username=user, diff --git a/tox.ini b/tox.ini index f444646a0..319a2a81f 100644 --- a/tox.ini +++ b/tox.ini @@ -61,7 +61,7 @@ deps = commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html -[testenv:functional] +[testenv:functional{,-py38,-py39,-py310,-py311,-py312}] passenv = OS_* commands = stestr --test-path=./novaclient/tests/functional run --concurrency=1 {posargs} From d27282b5c76c6d4f99d82de439d6c52dfd219faf Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 27 Aug 2024 10:29:51 +0100 Subject: [PATCH 1681/1705] Fix Python 3.12 compatibility Handle a change in Python 3.12 [1]. [1] https://github.com/python/cpython/commit/cffb4c78d3b2a8a724301e59fb39b73b20e544de Change-Id: I772579d297ef51c57019218a4ca8a566987b9b5c Signed-off-by: Stephen Finucane --- novaclient/shell.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/novaclient/shell.py b/novaclient/shell.py index adb8b4dcb..49cba94f0 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -232,8 +232,11 @@ def _get_option_tuples(self, option_string): option_tuples = (super(NovaClientArgumentParser, self) ._get_option_tuples(option_string)) if len(option_tuples) > 1: - normalizeds = [option.replace('_', '-') - for action, option, value in option_tuples] + # In Python < 3.12, this is a 3-part tuple: + # action, option_string, explicit_arg + # In Python >= 3.12, this is a 4 part tuple: + # action, option_string, sep, explicit_arg + normalizeds = [opt[1].replace('_', '-') for opt in option_tuples] if len(set(normalizeds)) == 1: return option_tuples[:1] return option_tuples From bc7db06629e87ce9da627671e02d368d175e12b1 Mon Sep 17 00:00:00 2001 From: Elod Illes Date: Thu, 29 Aug 2024 17:20:52 +0200 Subject: [PATCH 1682/1705] [doc] change directive noindex to no-index Due to a bug in Sphinx [1], the ':noindex:' flag is currently broken for the 'module' directive. Workaround it by replacing it with the ':no-index' flag, which is what we'll need come Sphinx 9.x anyway. [1] https://github.com/sphinx-doc/sphinx/issues/12843 Change-Id: If3fa5c51b566ea0144acf84d186ce12c8127c9a8 --- doc/source/user/python-api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/user/python-api.rst b/doc/source/user/python-api.rst index b01e07f11..80adda3a1 100644 --- a/doc/source/user/python-api.rst +++ b/doc/source/user/python-api.rst @@ -4,7 +4,7 @@ .. module:: novaclient :synopsis: A client for the OpenStack Nova API. - :noindex: + :no-index: .. currentmodule:: novaclient From edebd28d54c7790738cd54e295c2a756b0f95420 Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Fri, 6 Sep 2024 13:08:42 +0000 Subject: [PATCH 1683/1705] Update master for stable/2024.2 Add file to the reno documentation build to show release notes for stable/2024.2. Use pbr instruction to increment the minor version number automatically so that master versions are higher than the versions on stable/2024.2. Sem-Ver: feature Change-Id: I4cdc92e17fb170b053191c0d99e5e873acbec00d --- releasenotes/source/2024.2.rst | 6 ++++++ releasenotes/source/index.rst | 1 + 2 files changed, 7 insertions(+) create mode 100644 releasenotes/source/2024.2.rst diff --git a/releasenotes/source/2024.2.rst b/releasenotes/source/2024.2.rst new file mode 100644 index 000000000..aaebcbc8c --- /dev/null +++ b/releasenotes/source/2024.2.rst @@ -0,0 +1,6 @@ +=========================== +2024.2 Series Release Notes +=========================== + +.. release-notes:: + :branch: stable/2024.2 diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index cfe6d23a8..9986ccdfb 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -8,6 +8,7 @@ Contents :maxdepth: 2 unreleased + 2024.2 2024.1 2023.2 2023.1 From b72236df71daa11236b07c63bd78cb8a130a851e Mon Sep 17 00:00:00 2001 From: Takashi Natsume Date: Fri, 4 Oct 2024 20:33:46 +0900 Subject: [PATCH 1684/1705] Remove Python 3.8 support Python 3.8 is no longer be part of tested runtimes since 2024.2 and is reaching its EOL soon. Bump the minimum supported python version. Also declare Python 3.12 support because now it's part of the tested runtimes. Change-Id: Ie2b1cb2eb0f2eb6da3266c3fb55b5f3aef7e6af7 Signed-off-by: Takashi Natsume --- releasenotes/notes/remove-py38-ae196c568a1577db.yaml | 5 +++++ setup.cfg | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/remove-py38-ae196c568a1577db.yaml diff --git a/releasenotes/notes/remove-py38-ae196c568a1577db.yaml b/releasenotes/notes/remove-py38-ae196c568a1577db.yaml new file mode 100644 index 000000000..040316360 --- /dev/null +++ b/releasenotes/notes/remove-py38-ae196c568a1577db.yaml @@ -0,0 +1,5 @@ +--- +upgrade: + - | + Support for Python 3.8 has been removed. Now the minimum python version + supported is 3.9 . diff --git a/setup.cfg b/setup.cfg index d017074b2..d78f29505 100644 --- a/setup.cfg +++ b/setup.cfg @@ -7,7 +7,7 @@ license = Apache License, Version 2.0 author = OpenStack author_email = openstack-discuss@lists.openstack.org home_page = https://docs.openstack.org/python-novaclient/latest -python_requires = >=3.8 +python_requires = >=3.9 classifier = Development Status :: 5 - Production/Stable Environment :: Console @@ -18,10 +18,10 @@ classifier = Operating System :: OS Independent Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 + Programming Language :: Python :: 3.12 Programming Language :: Python :: 3 :: Only Programming Language :: Python :: Implementation :: CPython From 3d179e7a1a0f95152e0a2fc0c1acb007044a7632 Mon Sep 17 00:00:00 2001 From: Takashi Kajinami Date: Sun, 13 Oct 2024 01:31:00 +0900 Subject: [PATCH 1685/1705] tox: Drop envdir tox now always recreates an env although the env is shared using envdir options. ~~~ $ tox -e genpolicy genpolicy: recreate env because env type changed from {'name': 'genconfig', 'type': 'VirtualEnvRunner'} to {'name': 'genpolicy', 'type': 'VirtualEnvRunner'} ~~~ According to the maintainer of tox, this functionality is not intended to be supported. https://github.com/tox-dev/tox/issues/425#issuecomment-1011944293 Change-Id: I0c77304938aaeb47036e672eff8288252fde7b2d --- tox.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/tox.ini b/tox.ini index 319a2a81f..56361f038 100644 --- a/tox.ini +++ b/tox.ini @@ -47,7 +47,6 @@ commands = whereto doc/build/html/.htaccess doc/test/redirect-tests.txt [testenv:pdf-docs] -envdir = {toxworkdir}/docs deps = {[testenv:docs]deps} commands = rm -rf doc/build/pdf From bc489166584f6b8b3ae54c16540791a5ec64e4fb Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Thu, 14 Nov 2024 10:45:29 +0000 Subject: [PATCH 1686/1705] reno: Update master for unmaintained/2023.1 Update the 2023.1 release notes configuration to build from unmaintained/2023.1. Change-Id: Ic28f7df7b90e0ad6e85e35f5c1aa6da467345126 --- releasenotes/source/2023.1.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/releasenotes/source/2023.1.rst b/releasenotes/source/2023.1.rst index d1238479b..2c9a36fae 100644 --- a/releasenotes/source/2023.1.rst +++ b/releasenotes/source/2023.1.rst @@ -3,4 +3,4 @@ =========================== .. release-notes:: - :branch: stable/2023.1 + :branch: unmaintained/2023.1 From f1631ecb9734ac248f273b89fe8b262bc5095598 Mon Sep 17 00:00:00 2001 From: Cyril Roelandt Date: Thu, 3 Oct 2024 15:52:18 +0200 Subject: [PATCH 1687/1705] Replace glanceclient with openstacksdk Nowadays, the SDK and the unified OpenStack client should be used instead of the service-specific clients. Change-Id: I9f67db82e8cd4ef8e6ff6186de17e8f84dabbbbd --- novaclient/tests/functional/base.py | 6 +++--- novaclient/tests/functional/v2/legacy/test_servers.py | 2 +- test-requirements.txt | 1 - 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/novaclient/tests/functional/base.py b/novaclient/tests/functional/base.py index 58403a078..138d5f513 100644 --- a/novaclient/tests/functional/base.py +++ b/novaclient/tests/functional/base.py @@ -15,7 +15,6 @@ from cinderclient.v3 import client as cinderclient import fixtures -from glanceclient import client as glanceclient from keystoneauth1.exceptions import discovery as discovery_exc from keystoneauth1 import identity from keystoneauth1 import session as ksession @@ -24,6 +23,7 @@ from neutronclient.v2_0 import client as neutronclient import openstack.config import openstack.config.exceptions +import openstack.connection from oslo_utils import uuidutils import tempest.lib.cli.base import testtools @@ -223,13 +223,13 @@ def setUp(self): self.client = self._get_novaclient(session) - self.glance = glanceclient.Client('2', session=session) + self.openstack = openstack.connection.Connection(session=session) # pick some reasonable flavor / image combo if "flavor" not in CACHE: CACHE["flavor"] = pick_flavor(self.client.flavors.list()) if "image" not in CACHE: - CACHE["image"] = pick_image(self.glance.images.list()) + CACHE["image"] = pick_image(self.openstack.image.images()) self.flavor = CACHE["flavor"] self.image = CACHE["image"] diff --git a/novaclient/tests/functional/v2/legacy/test_servers.py b/novaclient/tests/functional/v2/legacy/test_servers.py index 181f59f33..246b8d60c 100644 --- a/novaclient/tests/functional/v2/legacy/test_servers.py +++ b/novaclient/tests/functional/v2/legacy/test_servers.py @@ -111,7 +111,7 @@ def test_boot_server_using_image_with(self): # get the snapshot image id out of the output table for the second # server create request snapshot_id = self._get_value_from_the_table(snapshot_info, 'id') - self.addCleanup(self.glance.images.delete, snapshot_id) + self.addCleanup(self.openstack.image.delete_image, snapshot_id) # verify the metadata was set on the snapshot image meta_value = self._get_value_from_the_table( diff --git a/test-requirements.txt b/test-requirements.txt index 0c56a31a6..1004a2837 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -5,7 +5,6 @@ ddt>=1.0.1 # MIT fixtures>=3.0.0 # Apache-2.0/BSD python-keystoneclient>=3.8.0 # Apache-2.0 python-cinderclient>=4.0.1 # Apache-2.0 -python-glanceclient>=2.8.0 # Apache-2.0 python-neutronclient>=6.7.0 # Apache-2.0 requests-mock>=1.2.0 # Apache-2.0 openstacksdk>=0.11.2 # Apache-2.0 From 4200e9c12ddf318b61abc6095e6e862ffc4430aa Mon Sep 17 00:00:00 2001 From: Takashi Kajinami Date: Mon, 13 Jan 2025 14:03:54 +0900 Subject: [PATCH 1688/1705] Remove environment for Python 3.8 ... because Python 3.8 is no longer supported. Change-Id: I6270877762f8f1f82ac4e48705bccc1d85a668e6 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 56361f038..87ff7cba7 100644 --- a/tox.ini +++ b/tox.ini @@ -60,7 +60,7 @@ deps = commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html -[testenv:functional{,-py38,-py39,-py310,-py311,-py312}] +[testenv:functional{,-py39,-py310,-py311,-py312}] passenv = OS_* commands = stestr --test-path=./novaclient/tests/functional run --concurrency=1 {posargs} From 4d2dacbff9c3e4c5fd87b2ee331a2b86787b0a00 Mon Sep 17 00:00:00 2001 From: Takashi Kajinami Date: Mon, 3 Feb 2025 20:59:48 +0900 Subject: [PATCH 1689/1705] Replace oslo_utils.encodeutils.exception_to_unicode The function is deprecated because it is equivalent to str(ex) in Python 3. Depends-on: https://review.opendev.org/c/openstack/oslo.utils/+/938929 Change-Id: Ifc18c0824fc5ca953d48651e4d4f8e5cf776f412 --- novaclient/shell.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/novaclient/shell.py b/novaclient/shell.py index 49cba94f0..1b95c0b40 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -836,10 +836,9 @@ def main(argv=sys.argv[1:]): OpenStackComputeShell().main(argv) except Exception as exc: logger.debug(exc, exc_info=1) - message = encodeutils.exception_to_unicode(exc) print("ERROR (%(type)s): %(msg)s" % { 'type': exc.__class__.__name__, - 'msg': message}, + 'msg': exc}, file=sys.stderr) sys.exit(1) except KeyboardInterrupt: From b2481a6f9739afe0e68327fc58c5bc208f69c052 Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Fri, 7 Mar 2025 14:36:33 +0000 Subject: [PATCH 1690/1705] Update master for stable/2025.1 Add file to the reno documentation build to show release notes for stable/2025.1. Use pbr instruction to increment the minor version number automatically so that master versions are higher than the versions on stable/2025.1. Sem-Ver: feature Change-Id: I9cc2fb72f625119a4517c9da09f581b2ec2025a0 --- releasenotes/source/2025.1.rst | 6 ++++++ releasenotes/source/index.rst | 1 + 2 files changed, 7 insertions(+) create mode 100644 releasenotes/source/2025.1.rst diff --git a/releasenotes/source/2025.1.rst b/releasenotes/source/2025.1.rst new file mode 100644 index 000000000..3add0e53a --- /dev/null +++ b/releasenotes/source/2025.1.rst @@ -0,0 +1,6 @@ +=========================== +2025.1 Series Release Notes +=========================== + +.. release-notes:: + :branch: stable/2025.1 diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index 9986ccdfb..7eedd366a 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -8,6 +8,7 @@ Contents :maxdepth: 2 unreleased + 2025.1 2024.2 2024.1 2023.2 From 42d499dc25abf7034e80b7305218b3722c2c697f Mon Sep 17 00:00:00 2001 From: Takashi Natsume Date: Sat, 19 Apr 2025 23:28:26 +0900 Subject: [PATCH 1691/1705] Add libpcre3-dev in bindep.txt for pcre.h Doc job is going to run on Ubuntu Noble[1] and we need libpcre3-dev package for pcre.h [1] https://review.opendev.org/c/openstack/openstack-zuul-jobs/+/935459 Change-Id: I126f7a33fb8d3e13c2611541d0a4068f21866e55 Signed-off-by: Takashi Natsume --- bindep.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/bindep.txt b/bindep.txt index c3ae8263a..22b5d2a32 100644 --- a/bindep.txt +++ b/bindep.txt @@ -18,3 +18,4 @@ python3-all-dev [platform:ubuntu !platform:ubuntu-precise] python3-dev [platform:dpkg] python3-devel [platform:fedora] uuid-dev [platform:dpkg] +libpcre3-dev [test platform:dpkg] From 06da36e5654846e0b5d6058daa745b8db34b22fd Mon Sep 17 00:00:00 2001 From: Takashi Natsume Date: Wed, 9 Oct 2024 13:46:10 +0000 Subject: [PATCH 1692/1705] Replace cinderclient with openstacksdk Change-Id: I0f8dad338953bb0c325f228cefa00c27f51cbb9f Signed-off-by: Takashi Natsume --- novaclient/tests/functional/base.py | 4 +--- .../functional/v2/legacy/test_extended_attributes.py | 4 ++-- novaclient/tests/functional/v2/legacy/test_instances.py | 4 ++-- novaclient/tests/functional/v2/legacy/test_servers.py | 9 ++++----- test-requirements.txt | 1 - 5 files changed, 9 insertions(+), 13 deletions(-) diff --git a/novaclient/tests/functional/base.py b/novaclient/tests/functional/base.py index 138d5f513..eb306a529 100644 --- a/novaclient/tests/functional/base.py +++ b/novaclient/tests/functional/base.py @@ -13,7 +13,6 @@ import os import time -from cinderclient.v3 import client as cinderclient import fixtures from keystoneauth1.exceptions import discovery as discovery_exc from keystoneauth1 import identity @@ -271,7 +270,6 @@ def setUp(self): self.keystone = keystoneclient.Client(session=session, username=user, password=passwd) - self.cinder = cinderclient.Client(auth=auth, session=session) def _get_novaclient(self, session): nc = novaclient.client.Client("2", session=session) @@ -332,7 +330,7 @@ def wait_for_volume_status(self, volume, status, timeout=60, """ start_time = time.time() while time.time() - start_time < timeout: - volume = self.cinder.volumes.get(volume.id) + volume = self.openstack.block_storage.get_volume(volume) if volume.status == status: break time.sleep(poll_interval) diff --git a/novaclient/tests/functional/v2/legacy/test_extended_attributes.py b/novaclient/tests/functional/v2/legacy/test_extended_attributes.py index 6affb742f..2b2378485 100644 --- a/novaclient/tests/functional/v2/legacy/test_extended_attributes.py +++ b/novaclient/tests/functional/v2/legacy/test_extended_attributes.py @@ -22,8 +22,8 @@ class TestExtAttrNovaClient(base.ClientTestBase): def _create_server_and_attach_volume(self): server = self._create_server() - volume = self.cinder.volumes.create(1) - self.addCleanup(volume.delete) + volume = self.openstack.block_storage.create_volume(size=1) + self.addCleanup(self.openstack.block_storage.delete_volume, volume) self.wait_for_volume_status(volume, 'available') self.nova('volume-attach', params="%s %s" % (server.name, volume.id)) self.addCleanup(self._release_volume, server, volume) diff --git a/novaclient/tests/functional/v2/legacy/test_instances.py b/novaclient/tests/functional/v2/legacy/test_instances.py index 131407a77..f933ec1e7 100644 --- a/novaclient/tests/functional/v2/legacy/test_instances.py +++ b/novaclient/tests/functional/v2/legacy/test_instances.py @@ -53,8 +53,8 @@ def test_attach_volume(self): self.addCleanup(server.delete) # create a volume for attachment - volume = self.cinder.volumes.create(1) - self.addCleanup(volume.delete) + volume = self.openstack.block_storage.create_volume(size=1) + self.addCleanup(self.openstack.block_storage.delete_volume, volume) # allow volume to become available self.wait_for_volume_status(volume, 'available') diff --git a/novaclient/tests/functional/v2/legacy/test_servers.py b/novaclient/tests/functional/v2/legacy/test_servers.py index 246b8d60c..ded7eb3a5 100644 --- a/novaclient/tests/functional/v2/legacy/test_servers.py +++ b/novaclient/tests/functional/v2/legacy/test_servers.py @@ -25,9 +25,8 @@ class TestServersBootNovaClient(base.ClientTestBase): def _boot_server_with_legacy_bdm(self, bdm_params=()): volume_size = 1 volume_name = self.name_generate() - volume = self.cinder.volumes.create(size=volume_size, - name=volume_name, - imageRef=self.image.id) + volume = self.openstack.block_storage.create_volume( + size=volume_size, name=volume_name, image_id=self.image.id) self.wait_for_volume_status(volume, "available") if (len(bdm_params) >= 3 and bdm_params[2] == '1'): @@ -56,8 +55,8 @@ def _boot_server_with_legacy_bdm(self, bdm_params=()): self.wait_for_resource_delete(server_id, self.client.servers) if delete_volume: - self.cinder.volumes.delete(volume.id) - self.wait_for_resource_delete(volume.id, self.cinder.volumes) + self.openstack.block_storage.delete_volume(volume) + self.openstack.block_storage.wait_for_delete(volume) def test_boot_server_with_legacy_bdm(self): # bdm v1 format diff --git a/test-requirements.txt b/test-requirements.txt index 1004a2837..428e3b629 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -4,7 +4,6 @@ coverage>=4.4.1 # Apache-2.0 ddt>=1.0.1 # MIT fixtures>=3.0.0 # Apache-2.0/BSD python-keystoneclient>=3.8.0 # Apache-2.0 -python-cinderclient>=4.0.1 # Apache-2.0 python-neutronclient>=6.7.0 # Apache-2.0 requests-mock>=1.2.0 # Apache-2.0 openstacksdk>=0.11.2 # Apache-2.0 From ee73d6beec94256e8b80b0a4828fb206e9629dcd Mon Sep 17 00:00:00 2001 From: Takashi Natsume Date: Thu, 10 Oct 2024 11:38:16 +0000 Subject: [PATCH 1693/1705] Replace neutronclient with openstacksdk Change-Id: I4c9dd506ada20744367afbe0cbb93482d66ee8ec Signed-off-by: Takashi Natsume --- novaclient/tests/functional/base.py | 4 +--- test-requirements.txt | 1 - 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/novaclient/tests/functional/base.py b/novaclient/tests/functional/base.py index eb306a529..5e0a2e0c2 100644 --- a/novaclient/tests/functional/base.py +++ b/novaclient/tests/functional/base.py @@ -19,7 +19,6 @@ from keystoneauth1 import session as ksession from keystoneclient import client as keystoneclient from keystoneclient import discover as keystone_discover -from neutronclient.v2_0 import client as neutronclient import openstack.config import openstack.config.exceptions import openstack.connection @@ -234,8 +233,7 @@ def setUp(self): if "network" not in CACHE: # Get the networks from neutron. - neutron = neutronclient.Client(session=session) - neutron_networks = neutron.list_networks()['networks'] + neutron_networks = self.openstack.network.networks() # Convert the neutron dicts to Network objects. nets = [] for network in neutron_networks: diff --git a/test-requirements.txt b/test-requirements.txt index 428e3b629..80e9a1d9a 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -4,7 +4,6 @@ coverage>=4.4.1 # Apache-2.0 ddt>=1.0.1 # MIT fixtures>=3.0.0 # Apache-2.0/BSD python-keystoneclient>=3.8.0 # Apache-2.0 -python-neutronclient>=6.7.0 # Apache-2.0 requests-mock>=1.2.0 # Apache-2.0 openstacksdk>=0.11.2 # Apache-2.0 osprofiler>=1.4.0 # Apache-2.0 From 103c251b4ef38be945d97615af97606642aefb0d Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 20 Jun 2025 13:34:18 +0100 Subject: [PATCH 1694/1705] tests: Stop "testing" against Identity v2 API The v2 API was removed in Queens [1], over 7 years ago. Stop pretending we can support it in our tests. [1] https://docs.openstack.org/keystone/latest/contributor/http-api.html Change-Id: Ie2c8de5d6e73a60b30fdf566a41c7ec39aba349c Signed-off-by: Stephen Finucane --- novaclient/tests/functional/base.py | 57 ++++++------------- novaclient/tests/functional/test_auth.py | 14 ----- .../v2/legacy/test_flavor_access.py | 2 +- .../functional/v2/test_instance_action.py | 4 +- .../tests/functional/v2/test_keypairs.py | 4 +- .../functional/v2/test_trigger_crash_dump.py | 2 +- novaclient/tests/unit/fixture_data/client.py | 2 +- novaclient/tests/unit/test_shell.py | 16 +++--- novaclient/tests/unit/utils.py | 4 -- novaclient/tests/unit/v2/test_shell.py | 2 +- 10 files changed, 32 insertions(+), 75 deletions(-) diff --git a/novaclient/tests/functional/base.py b/novaclient/tests/functional/base.py index 5e0a2e0c2..bb7f370fb 100644 --- a/novaclient/tests/functional/base.py +++ b/novaclient/tests/functional/base.py @@ -14,11 +14,9 @@ import time import fixtures -from keystoneauth1.exceptions import discovery as discovery_exc from keystoneauth1 import identity from keystoneauth1 import session as ksession from keystoneclient import client as keystoneclient -from keystoneclient import discover as keystone_discover import openstack.config import openstack.config.exceptions import openstack.connection @@ -37,18 +35,6 @@ "'gocubsgo'. use 'sudo' for root.") -def is_keystone_version_available(session, version): - """Given a (major, minor) pair, check if the API version is enabled.""" - - d = keystone_discover.Discover(session) - try: - d.create_client(version) - except (discovery_exc.DiscoveryFailure, discovery_exc.VersionNotAvailable): - return False - else: - return True - - # The following are simple filter functions that filter our available # image / flavor list so that they can be used in standard testing. def pick_flavor(flavors): @@ -503,10 +489,7 @@ def _wait_for_state_change(self, server_id, status): def _get_project_id(self, name): """Obtain project id by project name.""" - if self.keystone.version == "v3": - project = self.keystone.projects.find(name=name) - else: - project = self.keystone.tenants.find(name=name) + project = self.keystone.projects.find(name=name) return project.id def _cleanup_server(self, server_id): @@ -546,39 +529,31 @@ def _pick_alternate_flavor(self): self.fail('Unable to find alternate for flavor: %s' % flavor_name) -class TenantTestBase(ClientTestBase): - """Base test class for additional tenant and user creation which +class ProjectTestBase(ClientTestBase): + """Base test class for additional project and user creation which could be required in various test scenarios """ def setUp(self): - super(TenantTestBase, self).setUp() + super(ProjectTestBase, self).setUp() user_name = uuidutils.generate_uuid() project_name = uuidutils.generate_uuid() password = 'password' - if self.keystone.version == "v3": - project = self.keystone.projects.create(project_name, - self.project_domain_id) - self.project_id = project.id - self.addCleanup(self.keystone.projects.delete, self.project_id) + project = self.keystone.projects.create(project_name, + self.project_domain_id) + self.project_id = project.id + self.addCleanup(self.keystone.projects.delete, self.project_id) - self.user_id = self.keystone.users.create( - name=user_name, password=password, - default_project=self.project_id).id + self.user_id = self.keystone.users.create( + name=user_name, password=password, + default_project=self.project_id).id - for role in self.keystone.roles.list(): - if "member" in role.name.lower(): - self.keystone.roles.grant(role.id, user=self.user_id, - project=self.project_id) - break - else: - project = self.keystone.tenants.create(project_name) - self.project_id = project.id - self.addCleanup(self.keystone.tenants.delete, self.project_id) - - self.user_id = self.keystone.users.create( - user_name, password, tenant_id=self.project_id).id + for role in self.keystone.roles.list(): + if "member" in role.name.lower(): + self.keystone.roles.grant(role.id, user=self.user_id, + project=self.project_id) + break self.addCleanup(self.keystone.users.delete, self.user_id) self.cli_clients_2 = tempest.lib.cli.base.CLIClient( diff --git a/novaclient/tests/functional/test_auth.py b/novaclient/tests/functional/test_auth.py index c8d2396a1..77c3503ee 100644 --- a/novaclient/tests/functional/test_auth.py +++ b/novaclient/tests/functional/test_auth.py @@ -90,20 +90,6 @@ def nova_auth_with_token(self, identity_api_version): tempest.lib.cli.base.execute( "nova", "list", flags, cli_dir=self.cli_clients.cli_dir) - def test_auth_via_keystone_v2(self): - session = self.keystone.session - version = (2, 0) - if not base.is_keystone_version_available(session, version): - self.skipTest("Identity API version 2.0 is not available.") - - self.nova_auth_with_password("list", identity_api_version="2.0") - self.nova_auth_with_token(identity_api_version="2.0") - def test_auth_via_keystone_v3(self): - session = self.keystone.session - version = (3, 0) - if not base.is_keystone_version_available(session, version): - self.skipTest("Identity API version 3.0 is not available.") - self.nova_auth_with_password("list", identity_api_version="3") self.nova_auth_with_token(identity_api_version="3") diff --git a/novaclient/tests/functional/v2/legacy/test_flavor_access.py b/novaclient/tests/functional/v2/legacy/test_flavor_access.py index 964e3d610..aa8bb415c 100644 --- a/novaclient/tests/functional/v2/legacy/test_flavor_access.py +++ b/novaclient/tests/functional/v2/legacy/test_flavor_access.py @@ -13,7 +13,7 @@ from novaclient.tests.functional import base -class TestFlvAccessNovaClient(base.TenantTestBase): +class TestFlvAccessNovaClient(base.ProjectTestBase): """Functional tests for flavors with public and non-public access""" COMPUTE_API_VERSION = "2.1" diff --git a/novaclient/tests/functional/v2/test_instance_action.py b/novaclient/tests/functional/v2/test_instance_action.py index faeafe0f7..1578d1012 100644 --- a/novaclient/tests/functional/v2/test_instance_action.py +++ b/novaclient/tests/functional/v2/test_instance_action.py @@ -124,7 +124,7 @@ def test_list_instance_action_with_changes_since(self): class TestInstanceActionCLIV262(TestInstanceActionCLIV258, - base.TenantTestBase): + base.ProjectTestBase): """Instance action functional tests for v2.62 nova-api microversion.""" COMPUTE_API_VERSION = "2.62" @@ -155,7 +155,7 @@ def test_show_actions_with_host(self): class TestInstanceActionCLIV266(TestInstanceActionCLIV258, - base.TenantTestBase): + base.ProjectTestBase): """Instance action functional tests for v2.66 nova-api microversion.""" COMPUTE_API_VERSION = "2.66" diff --git a/novaclient/tests/functional/v2/test_keypairs.py b/novaclient/tests/functional/v2/test_keypairs.py index 4f2df58a7..14133be12 100644 --- a/novaclient/tests/functional/v2/test_keypairs.py +++ b/novaclient/tests/functional/v2/test_keypairs.py @@ -44,7 +44,7 @@ def test_import_keypair_x509(self): self.assertIn('x509', keypair) -class TestKeypairsNovaClientV210(base.TenantTestBase): +class TestKeypairsNovaClientV210(base.ProjectTestBase): """Keypairs functional tests for v2.10 nova-api microversion.""" COMPUTE_API_VERSION = "2.10" @@ -93,7 +93,7 @@ def cleanup(): self._get_column_value_from_single_row_table, output, "Name") -class TestKeypairsNovaClientV235(base.TenantTestBase): +class TestKeypairsNovaClientV235(base.ProjectTestBase): """Keypairs functional tests for v2.35 nova-api microversion.""" COMPUTE_API_VERSION = "2.35" diff --git a/novaclient/tests/functional/v2/test_trigger_crash_dump.py b/novaclient/tests/functional/v2/test_trigger_crash_dump.py index b7df75140..19dcbd96e 100644 --- a/novaclient/tests/functional/v2/test_trigger_crash_dump.py +++ b/novaclient/tests/functional/v2/test_trigger_crash_dump.py @@ -19,7 +19,7 @@ @decorators.skip_because(bug="1675526") -class TestTriggerCrashDumpNovaClientV217(base.TenantTestBase): +class TestTriggerCrashDumpNovaClientV217(base.ProjectTestBase): """Functional tests for trigger crash dump""" COMPUTE_API_VERSION = "2.17" diff --git a/novaclient/tests/unit/fixture_data/client.py b/novaclient/tests/unit/fixture_data/client.py index d0ce3cc52..d72ed6a35 100644 --- a/novaclient/tests/unit/fixture_data/client.py +++ b/novaclient/tests/unit/fixture_data/client.py @@ -17,7 +17,7 @@ from novaclient import client -IDENTITY_URL = 'http://identityserver:5000/v2.0' +IDENTITY_URL = 'http://identity.host/v3' COMPUTE_URL = 'http://compute.host' diff --git a/novaclient/tests/unit/test_shell.py b/novaclient/tests/unit/test_shell.py index 415440751..c22aca6da 100644 --- a/novaclient/tests/unit/test_shell.py +++ b/novaclient/tests/unit/test_shell.py @@ -33,7 +33,7 @@ FAKE_ENV = {'OS_USERNAME': 'username', 'OS_PASSWORD': 'password', 'OS_TENANT_NAME': 'tenant_name', - 'OS_AUTH_URL': 'http://no.where/v2.0', + 'OS_AUTH_URL': 'http://no.where/v3', 'OS_COMPUTE_API_VERSION': '2', 'OS_PROJECT_DOMAIN_ID': 'default', 'OS_PROJECT_DOMAIN_NAME': 'default', @@ -43,13 +43,13 @@ FAKE_ENV2 = {'OS_USER_ID': 'user_id', 'OS_PASSWORD': 'password', 'OS_TENANT_ID': 'tenant_id', - 'OS_AUTH_URL': 'http://no.where/v2.0', + 'OS_AUTH_URL': 'http://no.where/v3', 'OS_COMPUTE_API_VERSION': '2'} FAKE_ENV3 = {'OS_USER_ID': 'user_id', 'OS_PASSWORD': 'password', 'OS_TENANT_ID': 'tenant_id', - 'OS_AUTH_URL': 'http://no.where/v2.0', + 'OS_AUTH_URL': 'http://no.where/v3', 'NOVA_ENDPOINT_TYPE': 'novaURL', 'OS_ENDPOINT_TYPE': 'osURL', 'OS_COMPUTE_API_VERSION': '2'} @@ -57,7 +57,7 @@ FAKE_ENV4 = {'OS_USER_ID': 'user_id', 'OS_PASSWORD': 'password', 'OS_TENANT_ID': 'tenant_id', - 'OS_AUTH_URL': 'http://no.where/v2.0', + 'OS_AUTH_URL': 'http://no.where/v3', 'NOVA_ENDPOINT_TYPE': 'internal', 'OS_ENDPOINT_TYPE': 'osURL', 'OS_COMPUTE_API_VERSION': '2'} @@ -65,7 +65,7 @@ FAKE_ENV5 = {'OS_USERNAME': 'username', 'OS_PASSWORD': 'password', 'OS_TENANT_NAME': 'tenant_name', - 'OS_AUTH_URL': 'http://no.where/v2.0'} + 'OS_AUTH_URL': 'http://no.where/v3'} def _create_ver_list(versions): @@ -408,10 +408,10 @@ def shell(self, argstr, exitcodes=(0,)): return (stdout, stderr) def register_keystone_discovery_fixture(self, mreq): - v2_url = "http://no.where/v2.0" - v2_version = fixture.V2Discovery(v2_url) + v3_url = "http://no.where/v3" + v3_version = fixture.V3Discovery(v3_url) mreq.register_uri( - 'GET', v2_url, json=_create_ver_list([v2_version]), + 'GET', v3_url, json=_create_ver_list([v3_version]), status_code=200) def test_help_unknown_command(self): diff --git a/novaclient/tests/unit/utils.py b/novaclient/tests/unit/utils.py index 3a660eccd..c3c77b39f 100644 --- a/novaclient/tests/unit/utils.py +++ b/novaclient/tests/unit/utils.py @@ -21,10 +21,6 @@ import testscenarios import testtools -AUTH_URL = "http://localhost:5002/auth_url" -AUTH_URL_V1 = "http://localhost:5002/auth_url/v1.0" -AUTH_URL_V2 = "http://localhost:5002/auth_url/v2.0" - def _patch_mock_to_raise_for_invalid_assert_calls(): def raise_for_invalid_assert_calls(wrapped): diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 3f7bc3960..846faf9e1 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -71,7 +71,7 @@ class ShellTest(utils.TestCase): 'NOVA_PROJECT_ID': 'project_id', 'OS_COMPUTE_API_VERSION': '2', 'NOVA_URL': 'http://no.where', - 'OS_AUTH_URL': 'http://no.where/v2.0', + 'OS_AUTH_URL': 'http://no.where/v3', } def setUp(self): From 45a4ab5eb50662c456ac1f42fc248d6fe253f5d2 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 20 Jun 2025 13:43:29 +0100 Subject: [PATCH 1695/1705] Replace keystoneclient with openstacksdk Change-Id: If48e1b26f712b6336d77a4c56176cde79626935d Signed-off-by: Stephen Finucane --- novaclient/tests/functional/base.py | 32 +++++++++++++++-------------- test-requirements.txt | 1 - 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/novaclient/tests/functional/base.py b/novaclient/tests/functional/base.py index bb7f370fb..9a391d1a0 100644 --- a/novaclient/tests/functional/base.py +++ b/novaclient/tests/functional/base.py @@ -16,7 +16,6 @@ import fixtures from keystoneauth1 import identity from keystoneauth1 import session as ksession -from keystoneclient import client as keystoneclient import openstack.config import openstack.config.exceptions import openstack.connection @@ -251,10 +250,6 @@ def setUp(self): cli_dir=cli_dir, insecure=self.insecure) - self.keystone = keystoneclient.Client(session=session, - username=user, - password=passwd) - def _get_novaclient(self, session): nc = novaclient.client.Client("2", session=session) @@ -489,8 +484,9 @@ def _wait_for_state_change(self, server_id, status): def _get_project_id(self, name): """Obtain project id by project name.""" - project = self.keystone.projects.find(name=name) - return project.id + return self.openstack.identity.find_project( + name, ignore_missing=False + ).id def _cleanup_server(self, server_id): """Deletes a server and waits for it to be gone.""" @@ -540,22 +536,28 @@ def setUp(self): project_name = uuidutils.generate_uuid() password = 'password' - project = self.keystone.projects.create(project_name, - self.project_domain_id) + project = self.openstack.identity.create_project( + name=project_name, + domain_id=self.project_domain_id) self.project_id = project.id - self.addCleanup(self.keystone.projects.delete, self.project_id) + self.addCleanup( + self.openstack.identity.delete_project, self.project_id) - self.user_id = self.keystone.users.create( + self.user_id = self.openstack.identity.create_user( name=user_name, password=password, default_project=self.project_id).id - for role in self.keystone.roles.list(): + for role in self.openstack.identity.roles(): if "member" in role.name.lower(): - self.keystone.roles.grant(role.id, user=self.user_id, - project=self.project_id) + self.openstack.identity.assign_project_role_to_user( + project=self.project_id, + user=self.user_id, + role=role.id) break - self.addCleanup(self.keystone.users.delete, self.user_id) + self.addCleanup( + self.openstack.identity.delete_user, self.user_id) + self.cli_clients_2 = tempest.lib.cli.base.CLIClient( username=user_name, password=password, diff --git a/test-requirements.txt b/test-requirements.txt index 80e9a1d9a..210af0bfb 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -3,7 +3,6 @@ bandit>=1.1.0 # Apache-2.0 coverage>=4.4.1 # Apache-2.0 ddt>=1.0.1 # MIT fixtures>=3.0.0 # Apache-2.0/BSD -python-keystoneclient>=3.8.0 # Apache-2.0 requests-mock>=1.2.0 # Apache-2.0 openstacksdk>=0.11.2 # Apache-2.0 osprofiler>=1.4.0 # Apache-2.0 From d54efa9f581864bcd31ec354d0636f2484515499 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 20 Jun 2025 14:28:13 +0100 Subject: [PATCH 1696/1705] Use pre-commit for lint jobs Also bump the versions, switch to the native hacking pre-commit hook, fix some bandit issues that have crept in, and add descriptions for all tox jobs. Change-Id: I4717023ad81bd2899114f30bd5320a29654f8f12 Signed-off-by: Stephen Finucane --- .pre-commit-config.yaml | 24 ++++++------ novaclient/crypto.py | 4 +- novaclient/v2/shell.py | 2 +- .../remove_api_v_1_1-88b3f18ce1423b46.yaml | 1 - test-requirements.txt | 2 - tools/nova.bash_completion | 36 ++++++++--------- tox.ini | 39 ++++++++++++++----- 7 files changed, 62 insertions(+), 46 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c539fabb0..894b69869 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,10 +1,7 @@ --- -default_language_version: - # force all unspecified python hooks to run python3 - python: python3 repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.1.0 + rev: v5.0.0 hooks: - id: trailing-whitespace - id: mixed-line-ending @@ -16,17 +13,18 @@ repos: - id: check-yaml files: .*\.(yaml|yml)$ - repo: https://github.com/Lucas-C/pre-commit-hooks - rev: v1.1.13 + rev: v1.5.5 hooks: - id: remove-tabs exclude: '.*\.(svg)$' - - repo: local + - repo: https://github.com/PyCQA/bandit + rev: 1.8.5 hooks: - - id: flake8 - name: flake8 - additional_dependencies: - - hacking>=6.1.0,<6.2.0 - language: python - entry: flake8 - files: '^.*\.py$' + - id: bandit + exclude: '^novaclient/tests/.*$' + - repo: https://opendev.org/openstack/hacking + rev: 7.0.0 + hooks: + - id: hacking + additional_dependencies: [] exclude: '^(doc|releasenotes|tools)/.*$' diff --git a/novaclient/crypto.py b/novaclient/crypto.py index 527bc82a6..f6d77fa75 100644 --- a/novaclient/crypto.py +++ b/novaclient/crypto.py @@ -14,7 +14,7 @@ # under the License. import base64 -import subprocess +import subprocess # nosec: B404 class DecryptionFailure(Exception): @@ -30,7 +30,7 @@ def decrypt_password(private_key, password): cmd = ['openssl', 'rsautl', '-decrypt', '-inkey', private_key] proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, - stderr=subprocess.PIPE) + stderr=subprocess.PIPE) # nosec: B603 out, err = proc.communicate(unencoded) proc.stdin.close() if proc.returncode: diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 0ca9e7942..7242dbc13 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -4208,7 +4208,7 @@ def do_ssh(cs, args): cmd = "ssh -%d -p%d %s %s@%s %s" % (version, args.port, identity, args.login, ip_address, args.extra) logger.debug("Executing cmd '%s'", cmd) - os.system(cmd) + os.system(cmd) # nosec: B605 # NOTE(mriedem): In the 2.50 microversion, the os-quota-class-sets API diff --git a/releasenotes/notes/remove_api_v_1_1-88b3f18ce1423b46.yaml b/releasenotes/notes/remove_api_v_1_1-88b3f18ce1423b46.yaml index 5748c5e67..4b32d376b 100644 --- a/releasenotes/notes/remove_api_v_1_1-88b3f18ce1423b46.yaml +++ b/releasenotes/notes/remove_api_v_1_1-88b3f18ce1423b46.yaml @@ -2,4 +2,3 @@ upgrade: - remove version 1.1 API support as we only support v2 and v2.1 API in nova side now. - diff --git a/test-requirements.txt b/test-requirements.txt index 210af0bfb..45c86fed8 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,5 +1,3 @@ -hacking>=6.1.0,<6.2.0 # Apache-2.0 -bandit>=1.1.0 # Apache-2.0 coverage>=4.4.1 # Apache-2.0 ddt>=1.0.1 # MIT fixtures>=3.0.0 # Apache-2.0/BSD diff --git a/tools/nova.bash_completion b/tools/nova.bash_completion index 3c58d3493..bbaee901c 100644 --- a/tools/nova.bash_completion +++ b/tools/nova.bash_completion @@ -3,25 +3,25 @@ _nova_flags="" # lazy init _nova_opts_exp="" # lazy init _nova() { - local cur prev nbc cflags - COMPREPLY=() - cur="${COMP_WORDS[COMP_CWORD]}" - prev="${COMP_WORDS[COMP_CWORD-1]}" + local cur prev nbc cflags + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" - if [ "x$_nova_opts" == "x" ] ; then - nbc="`nova bash-completion | sed -e "s/ *-h */ /" -e "s/ *-i */ /"`" - _nova_opts="`echo "$nbc" | sed -e "s/--[a-z0-9_-]*//g" -e "s/ */ /g"`" - _nova_flags="`echo " $nbc" | sed -e "s/ [^-][^-][a-z0-9_-]*//g" -e "s/ */ /g"`" - _nova_opts_exp="`echo "$_nova_opts" | tr ' ' '|'`" - fi + if [ "x$_nova_opts" == "x" ] ; then + nbc="`nova bash-completion | sed -e "s/ *-h */ /" -e "s/ *-i */ /"`" + _nova_opts="`echo "$nbc" | sed -e "s/--[a-z0-9_-]*//g" -e "s/ */ /g"`" + _nova_flags="`echo " $nbc" | sed -e "s/ [^-][^-][a-z0-9_-]*//g" -e "s/ */ /g"`" + _nova_opts_exp="`echo "$_nova_opts" | tr ' ' '|'`" + fi - if [[ " ${COMP_WORDS[@]} " =~ " "($_nova_opts_exp)" " && "$prev" != "help" ]] ; then - COMPLETION_CACHE=~/.novaclient/*/*-cache - cflags="$_nova_flags "$(cat $COMPLETION_CACHE 2> /dev/null | tr '\n' ' ') - COMPREPLY=($(compgen -W "${cflags}" -- ${cur})) - else - COMPREPLY=($(compgen -W "${_nova_opts}" -- ${cur})) - fi - return 0 + if [[ " ${COMP_WORDS[@]} " =~ " "($_nova_opts_exp)" " && "$prev" != "help" ]] ; then + COMPLETION_CACHE=~/.novaclient/*/*-cache + cflags="$_nova_flags "$(cat $COMPLETION_CACHE 2> /dev/null | tr '\n' ' ') + COMPREPLY=($(compgen -W "${cflags}" -- ${cur})) + else + COMPREPLY=($(compgen -W "${_nova_opts}" -- ${cur})) + fi + return 0 } complete -F _nova nova diff --git a/tox.ini b/tox.ini index 87ff7cba7..0401d37ff 100644 --- a/tox.ini +++ b/tox.ini @@ -1,12 +1,11 @@ [tox] envlist = py3,pep8,docs -minversion = 3.18.0 -ignore_basepython_conflict = true +minversion = 4.6.0 [testenv] -basepython = python3 +description = + Run unit tests. usedevelop = true -# tox is silly... these need to be separated by a newline.... allowlist_externals = find rm @@ -23,10 +22,20 @@ commands = stestr run {posargs} [testenv:pep8] -commands = flake8 {posargs} +description = + Run style checks. +deps = + pre-commit +commands = + pre-commit run --all-files --show-diff-on-failure [testenv:bandit] -commands = bandit -r novaclient -n5 -x tests +description = + Run security checks. +deps = + pre-commit +commands = + pre-commit run --all-files --show-diff-on-failure bandit [testenv:venv] deps = @@ -37,6 +46,8 @@ deps = commands = {posargs} [testenv:docs] +description = + Build documentation in HTML format. deps = -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/doc/requirements.txt @@ -47,6 +58,8 @@ commands = whereto doc/build/html/.htaccess doc/test/redirect-tests.txt [testenv:pdf-docs] +description = + Build documentation in PDF format. deps = {[testenv:docs]deps} commands = rm -rf doc/build/pdf @@ -54,6 +67,8 @@ commands = make -C doc/build/pdf [testenv:releasenotes] +description = + Build release notes. deps = -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/doc/requirements.txt @@ -61,12 +76,17 @@ commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html [testenv:functional{,-py39,-py310,-py311,-py312}] -passenv = OS_* +description = + Run functional tests. +passenv = + OS_* commands = stestr --test-path=./novaclient/tests/functional run --concurrency=1 {posargs} python novaclient/tests/functional/hooks/check_resources.py [testenv:cover] +description = + Run unit tests and print coverage information. setenv = PYTHON=coverage run --source novaclient --parallel-mode commands = @@ -93,11 +113,12 @@ exclude=.venv,.git,.tox,dist,*lib/python*,*egg,build,doc/source/conf.py,releasen import_exceptions = novaclient.i18n [testenv:bindep] +description = + Check for installed binary dependencies. # Do not install any requirements. We want this to be fast and work even if # system dependencies are missing, since it's used to tell you what system # dependencies are missing! This also means that bindep must be installed # separately, outside of the requirements files. deps = bindep -skipsdist=True -usedevelop=False +skip_install = true commands = bindep test From 51e9d269daf889cd90197baf3dad3ad99dbdde74 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 24 Jun 2025 08:01:31 +0100 Subject: [PATCH 1697/1705] tests: Remove use of ddt Change-Id: I8a2616728fe904e971dbedb650247b89a9dcd525 Signed-off-by: Stephen Finucane --- novaclient/tests/unit/test_shell.py | 35 ++++++++++++++--------------- test-requirements.txt | 1 - 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/novaclient/tests/unit/test_shell.py b/novaclient/tests/unit/test_shell.py index c22aca6da..efad2ac9f 100644 --- a/novaclient/tests/unit/test_shell.py +++ b/novaclient/tests/unit/test_shell.py @@ -17,7 +17,6 @@ import sys from unittest import mock -import ddt import fixtures from keystoneauth1 import fixture import requests_mock @@ -352,7 +351,6 @@ def test_not_really_ambiguous_option(self): self.assertTrue(args.tic_tac) -@ddt.ddt class ShellTest(utils.TestCase): _msg_no_tenant_project = ("You must provide a project name or project" @@ -528,22 +526,23 @@ def test_no_auth_url(self): else: self.fail('CommandError not raised') - @ddt.data( - (None, 'project_domain_id', FAKE_ENV['OS_PROJECT_DOMAIN_ID']), - ('OS_PROJECT_DOMAIN_ID', 'project_domain_id', ''), - (None, 'project_domain_name', FAKE_ENV['OS_PROJECT_DOMAIN_NAME']), - ('OS_PROJECT_DOMAIN_NAME', 'project_domain_name', ''), - (None, 'user_domain_id', FAKE_ENV['OS_USER_DOMAIN_ID']), - ('OS_USER_DOMAIN_ID', 'user_domain_id', ''), - (None, 'user_domain_name', FAKE_ENV['OS_USER_DOMAIN_NAME']), - ('OS_USER_DOMAIN_NAME', 'user_domain_name', '') - ) - @ddt.unpack - def test_basic_attributes(self, exclude, client_arg, env_var): - self.make_env(exclude=exclude, fake_env=FAKE_ENV) - self.shell('list') - client_kwargs = self.mock_client.call_args_list[0][1] - self.assertEqual(env_var, client_kwargs[client_arg]) + def test_basic_attributes(self): + for exclude, client_arg, env_var in ( + (None, 'project_domain_id', FAKE_ENV['OS_PROJECT_DOMAIN_ID']), + ('OS_PROJECT_DOMAIN_ID', 'project_domain_id', ''), + (None, 'project_domain_name', FAKE_ENV['OS_PROJECT_DOMAIN_NAME']), + ('OS_PROJECT_DOMAIN_NAME', 'project_domain_name', ''), + (None, 'user_domain_id', FAKE_ENV['OS_USER_DOMAIN_ID']), + ('OS_USER_DOMAIN_ID', 'user_domain_id', ''), + (None, 'user_domain_name', FAKE_ENV['OS_USER_DOMAIN_NAME']), + ('OS_USER_DOMAIN_NAME', 'user_domain_name', '') + ): + with self.subTest(f'{exclude},{client_arg},{env_var}'): + self.mock_client.reset_mock() + self.make_env(exclude=exclude, fake_env=FAKE_ENV) + self.shell('list') + client_kwargs = self.mock_client.call_args_list[0][1] + self.assertEqual(env_var, client_kwargs[client_arg]) @requests_mock.Mocker() def test_nova_endpoint_type(self, m_requests): diff --git a/test-requirements.txt b/test-requirements.txt index 45c86fed8..9ade238ad 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,5 +1,4 @@ coverage>=4.4.1 # Apache-2.0 -ddt>=1.0.1 # MIT fixtures>=3.0.0 # Apache-2.0/BSD requests-mock>=1.2.0 # Apache-2.0 openstacksdk>=0.11.2 # Apache-2.0 From 3b9e9591e91149d3a94d56818cdaead7b131a242 Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Fri, 5 Sep 2025 12:20:47 +0000 Subject: [PATCH 1698/1705] Update master for stable/2025.2 Add file to the reno documentation build to show release notes for stable/2025.2. Use pbr instruction to increment the minor version number automatically so that master versions are higher than the versions on stable/2025.2. Sem-Ver: feature Change-Id: I0fe2b73c2d02ad71fb4717b7765fc8f007fdd303 Signed-off-by: OpenStack Release Bot Generated-By: openstack/project-config:roles/copy-release-tools-scripts/files/release-tools/add_release_note_page.sh --- releasenotes/source/2025.2.rst | 6 ++++++ releasenotes/source/index.rst | 1 + 2 files changed, 7 insertions(+) create mode 100644 releasenotes/source/2025.2.rst diff --git a/releasenotes/source/2025.2.rst b/releasenotes/source/2025.2.rst new file mode 100644 index 000000000..4dae18d86 --- /dev/null +++ b/releasenotes/source/2025.2.rst @@ -0,0 +1,6 @@ +=========================== +2025.2 Series Release Notes +=========================== + +.. release-notes:: + :branch: stable/2025.2 diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index 7eedd366a..85ab6674b 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -8,6 +8,7 @@ Contents :maxdepth: 2 unreleased + 2025.2 2025.1 2024.2 2024.1 From 5e9be946ecb4ffe7a8627ce56db7e6bc74851e89 Mon Sep 17 00:00:00 2001 From: Takashi Kajinami Date: Mon, 13 Oct 2025 02:54:33 +0900 Subject: [PATCH 1699/1705] Replace remaining py39 target ... because Python 3.9 is no longer supported. Change-Id: Ibe68739a1bbcc6a745e3c6fd5ecf9f832c0bf002 Signed-off-by: Takashi Kajinami --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 0401d37ff..e52b28949 100644 --- a/tox.ini +++ b/tox.ini @@ -75,7 +75,7 @@ deps = commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html -[testenv:functional{,-py39,-py310,-py311,-py312}] +[testenv:functional{,-py310,-py311,-py312,-py313}] description = Run functional tests. passenv = From 3e2dd5bd638352ac453d975ec2194315d4daea8f Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Fri, 31 Oct 2025 11:39:23 +0000 Subject: [PATCH 1700/1705] reno: Update master for unmaintained/2024.1 Update the 2024.1 release notes configuration to build from unmaintained/2024.1. Change-Id: I016efe7d9def1f2b5a5a7f4229c64296323a0c66 Signed-off-by: OpenStack Release Bot Generated-By: openstack/project-config:roles/copy-release-tools-scripts/files/release-tools/change_reno_branch_to_unmaintained.sh --- releasenotes/source/2024.1.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/releasenotes/source/2024.1.rst b/releasenotes/source/2024.1.rst index 4977a4f1a..6896656be 100644 --- a/releasenotes/source/2024.1.rst +++ b/releasenotes/source/2024.1.rst @@ -3,4 +3,4 @@ =========================== .. release-notes:: - :branch: stable/2024.1 + :branch: unmaintained/2024.1 From 4881d04dedc4dc5d1ffd742531db4030cf188fcf Mon Sep 17 00:00:00 2001 From: Takashi Natsume Date: Sun, 9 Nov 2025 14:29:46 +0900 Subject: [PATCH 1701/1705] Update the Testing document Python 3.8 is no longer supported in 2026.1. Python 3.10 is the minimum supported/required version for 2026.1. Change-Id: Ic7fea2195f2ec6f2f48f2e9b359d021d0f01d0b7 Signed-off-by: Takashi Natsume --- doc/source/contributor/testing.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/source/contributor/testing.rst b/doc/source/contributor/testing.rst index ed0fa506d..d6a2cfd71 100644 --- a/doc/source/contributor/testing.rst +++ b/doc/source/contributor/testing.rst @@ -8,11 +8,11 @@ test targets that can be run to validate the code. ``tox -e pep8`` Style guidelines enforcement. -``tox -e py38`` - Traditional unit testing (Python 3.8). +``tox -e py310`` + Traditional unit testing (Python 3.10). ``tox -e functional`` - Live functional testing against an existing OpenStack instance. (Python 3.8) + Live functional testing against an existing OpenStack instance. (Python 3.10) ``tox -e cover`` Generate a coverage report on unit testing. From 6f6013510acc929774b1d7a2f74f2cca705896dd Mon Sep 17 00:00:00 2001 From: Takashi Kajinami Date: Tue, 20 Jan 2026 00:14:24 +0900 Subject: [PATCH 1702/1705] Install pcre packages only in doc jobs ... because the packages are required by whereto, which is used only in the docs target. Change-Id: I354fda635090ebda25eb232a2d026a54b8494e0f Signed-off-by: Takashi Kajinami --- bindep.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bindep.txt b/bindep.txt index 22b5d2a32..398db8ead 100644 --- a/bindep.txt +++ b/bindep.txt @@ -18,4 +18,5 @@ python3-all-dev [platform:ubuntu !platform:ubuntu-precise] python3-dev [platform:dpkg] python3-devel [platform:fedora] uuid-dev [platform:dpkg] -libpcre3-dev [test platform:dpkg] +libpcre3-dev [platform:dpkg doc] +pcre-devel [platform:rpm doc] From e00ed698af04e2c5b78db3516d26c79ffed9851f Mon Sep 17 00:00:00 2001 From: Takashi Kajinami Date: Mon, 8 Dec 2025 01:21:07 +0900 Subject: [PATCH 1703/1705] Replace obsolete PCRE packages pcre3 was removed from recent debian-based releases (eg. Trixie[1]), while RHEL10/CentOS Stream 10 no longer ships pcre in favor of pcre2. Use the latest whereto library release (0.5.0) which uses pcre2 instead. [1] https://lists.debian.org/debian-devel/2021/11/msg00176.html Depends-on: https://review.opendev.org/c/openstack/requirements/+/971428 Change-Id: Ibf96fca5c25e4b26ce35d9aef0d7f06b28cb391b Signed-off-by: Takashi Kajinami --- bindep.txt | 4 ++-- doc/requirements.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bindep.txt b/bindep.txt index 398db8ead..bf36b2d84 100644 --- a/bindep.txt +++ b/bindep.txt @@ -18,5 +18,5 @@ python3-all-dev [platform:ubuntu !platform:ubuntu-precise] python3-dev [platform:dpkg] python3-devel [platform:fedora] uuid-dev [platform:dpkg] -libpcre3-dev [platform:dpkg doc] -pcre-devel [platform:rpm doc] +libpcre2-dev [platform:dpkg doc] +pcre2-devel [platform:rpm doc] diff --git a/doc/requirements.txt b/doc/requirements.txt index 87e359a55..e7a8ac75b 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -4,4 +4,4 @@ reno>=3.1.0 # Apache-2.0 sphinxcontrib-apidoc>=0.2.0 # BSD # redirect tests in docs -whereto>=0.3.0 # Apache-2.0 +whereto>=0.5.0 # Apache-2.0 From 7d6ce9668e36d13a4b1f84cc3d53097a327d9ef5 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 27 Feb 2026 18:47:55 +0000 Subject: [PATCH 1704/1705] Add version foot protector python-novaclient is frozen and won't accept support for new microversions. Attempting to use a microversion newer than what we support is likely to break things is unexpected ways. Save people's feet from this shotgun by introducing a new helper function, 'check_version', that we use to ensure we actually support the version in question. We rework the existing check_major_version to make it much faster since we only care about v2 and have done so for a very long time. Change-Id: I4d3fba6fcbf785ef3309b8f9eee45e31c7919777 Signed-off-by: Stephen Finucane --- novaclient/api_versions.py | 69 ++++++++++++++-------- novaclient/client.py | 6 ++ novaclient/tests/unit/test_api_versions.py | 21 +++++++ 3 files changed, 73 insertions(+), 23 deletions(-) diff --git a/novaclient/api_versions.py b/novaclient/api_versions.py index 5ff52b8f5..59b8c1cbc 100644 --- a/novaclient/api_versions.py +++ b/novaclient/api_versions.py @@ -14,7 +14,6 @@ import functools import logging import os -import pkgutil import re import traceback import warnings @@ -195,35 +194,59 @@ def __repr__(self): def get_available_major_versions(): - # NOTE(andreykurilin): available clients version should not be - # hardcoded, so let's discover them. - matcher = re.compile(r"v[0-9]*$") - submodules = pkgutil.iter_modules([os.path.dirname(__file__)]) - available_versions = [name[1:] for loader, name, ispkg in submodules - if matcher.search(name)] - - return available_versions + return ['2'] def check_major_version(api_version): """Checks major part of ``APIVersion`` obj is supported. :raises novaclient.exceptions.UnsupportedVersion: if major part is not - supported + supported """ - available_versions = get_available_major_versions() - if (not api_version.is_null() and - str(api_version.ver_major) not in available_versions): - if len(available_versions) == 1: - msg = _("Invalid client version '%(version)s'. " - "Major part should be '%(major)s'") % { - "version": api_version.get_string(), - "major": available_versions[0]} - else: - msg = _("Invalid client version '%(version)s'. " - "Major part must be one of: '%(major)s'") % { - "version": api_version.get_string(), - "major": ", ".join(available_versions)} + if api_version.is_null(): + return + + if api_version.ver_major == 2: + return + + msg = _( + "Invalid client version '%(version)s'. Major part should be '2'" + ) % {"version": api_version.get_string()} + raise exceptions.UnsupportedVersion(msg) + + +def check_version(api_version): + """Checks if version of ``APIVersion`` is supported. + + Provided as an alternative to :func:`check_major_version` to avoid changing + the behavior of that function. + + :raises novaclient.exceptions.UnsupportedVersion: if major part is not + supported + """ + if api_version.is_null(): + return + + # we can't use API_MIN_VERSION since we do support 2.0 (which is 2.1 but + # less strict) + if api_version < APIVersion('2.0'): + msg = _( + "Invalid client version '%(version)s'. " + "Min version supported is '%(min_version)s'" + ) % { + "version": api_version.get_string(), + "min_version": novaclient.API_MIN_VERSION, + } + raise exceptions.UnsupportedVersion(msg) + + if api_version > novaclient.API_MAX_VERSION: + msg = _( + "Invalid client version '%(version)s'. " + "Max version supported is '%(max_version)s'" + ) % { + "version": api_version.get_string(), + "max_version": novaclient.API_MAX_VERSION, + } raise exceptions.UnsupportedVersion(msg) diff --git a/novaclient/client.py b/novaclient/client.py index 6c15f04d1..369d8d08e 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -51,6 +51,12 @@ def __init__(self, *args, **kwargs): self.timings = kwargs.pop('timings', False) self.api_version = kwargs.pop('api_version', None) self.api_version = self.api_version or api_versions.APIVersion() + + if isinstance(self.api_version, str): + self.api_version = api_versions.APIVersion(self.api_version) + + api_versions.check_version(self.api_version) + super(SessionClient, self).__init__(*args, **kwargs) def request(self, url, method, **kwargs): diff --git a/novaclient/tests/unit/test_api_versions.py b/novaclient/tests/unit/test_api_versions.py index 6969718f8..a5c8cb1c2 100644 --- a/novaclient/tests/unit/test_api_versions.py +++ b/novaclient/tests/unit/test_api_versions.py @@ -343,6 +343,27 @@ def fake_func(): self.assertEqual(expected_name, fake_func.__id__) +class CheckVersionTestCase(utils.TestCase): + def test_version_unsupported(self): + for version in ('1.0', '1.5', '1.100'): + with self.subTest('version too old', version=version): + self.assertRaises( + exceptions.UnsupportedVersion, + api_versions.check_version, + api_versions.APIVersion(version)) + + for version in ('2.97', '2.101', '3.0'): + with self.subTest('version too new', version=version): + self.assertRaises( + exceptions.UnsupportedVersion, + api_versions.check_version, + api_versions.APIVersion(version)) + + for version in ('2.1', '2.57', '2.96'): + with self.subTest('version just right', version=version): + api_versions.check_version(api_versions.APIVersion(version)) + + class DiscoverVersionTestCase(utils.TestCase): def setUp(self): super(DiscoverVersionTestCase, self).setUp() From 7f257abb568a65a8a91ad91cf60e36ecaf69e847 Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Tue, 3 Mar 2026 08:47:35 +0000 Subject: [PATCH 1705/1705] Update master for stable/2026.1 Add file to the reno documentation build to show release notes for stable/2026.1. Use pbr instruction to increment the minor version number automatically so that master versions are higher than the versions on stable/2026.1. Sem-Ver: feature Change-Id: I72725af3d2c60a722836f50ce8c5ad481b0451e5 Signed-off-by: OpenStack Release Bot Generated-By: openstack/project-config:roles/copy-release-tools-scripts/files/release-tools/add_release_note_page.sh --- releasenotes/source/2026.1.rst | 6 ++++++ releasenotes/source/index.rst | 1 + 2 files changed, 7 insertions(+) create mode 100644 releasenotes/source/2026.1.rst diff --git a/releasenotes/source/2026.1.rst b/releasenotes/source/2026.1.rst new file mode 100644 index 000000000..3d2861580 --- /dev/null +++ b/releasenotes/source/2026.1.rst @@ -0,0 +1,6 @@ +=========================== +2026.1 Series Release Notes +=========================== + +.. release-notes:: + :branch: stable/2026.1 diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index 85ab6674b..5588c8d02 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -8,6 +8,7 @@ Contents :maxdepth: 2 unreleased + 2026.1 2025.2 2025.1 2024.2